From 97aff0743eb216631387f2d35e4b3a20610f01b8 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 23 Nov 2025 19:16:16 -0500 Subject: [PATCH 001/349] unicode: switch to `uucode` grapheme break to match unicode 16 spec --- src/benchmark/GraphemeBreak.zig | 3 +- src/build/uucode_config.zig | 10 +- src/font/shaper/web_canvas.zig | 3 +- src/terminal/Terminal.zig | 57 +++++++++- src/unicode/grapheme.zig | 189 +++++++++++++------------------- src/unicode/main.zig | 1 - src/unicode/props.zig | 44 +------- src/unicode/props_uucode.zig | 43 +------- 8 files changed, 152 insertions(+), 198 deletions(-) diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig index a1b3380f0..e09ce85dd 100644 --- a/src/benchmark/GraphemeBreak.zig +++ b/src/benchmark/GraphemeBreak.zig @@ -10,6 +10,7 @@ const Benchmark = @import("Benchmark.zig"); const options = @import("options.zig"); const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); const unicode = @import("../unicode/main.zig"); +const uucode = @import("uucode"); const log = std.log.scoped(.@"terminal-stream-bench"); @@ -118,7 +119,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void { var r = &f_reader.interface; var d: UTF8Decoder = .{}; - var state: unicode.GraphemeBreakState = .{}; + var state: uucode.grapheme.BreakState = .default; var cp1: u21 = 0; var buf: [4096]u8 align(std.atomic.cache_line) = undefined; while (true) { diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 0843732b1..b6d527d0e 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -4,6 +4,7 @@ const config = @import("config.zig"); const config_x = @import("config.x.zig"); const d = config.default; const wcwidth = config_x.wcwidth; +const grapheme_break_no_control = config_x.grapheme_break_no_control; const Allocator = std.mem.Allocator; @@ -85,10 +86,15 @@ pub const tables = [_]config.Table{ }, .{ .name = "buildtime", - .extensions = &.{ wcwidth, width, is_symbol }, + .extensions = &.{ + wcwidth, + grapheme_break_no_control, + width, + is_symbol, + }, .fields = &.{ width.field("width"), - d.field("grapheme_break"), + grapheme_break_no_control.field("grapheme_break_no_control"), is_symbol.field("is_symbol"), d.field("is_emoji_vs_text"), d.field("is_emoji_vs_emoji"), diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index c8334ec9d..590c1d2a3 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); const unicode = @import("../../unicode/main.zig"); +const uucode = @import("uucode"); const log = std.log.scoped(.font_shaper); @@ -111,7 +112,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.grapheme.BreakState = .default; var cp1: u21 = @intCast(codepoints[0]); var start: usize = 0; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 1ec5b5d47..bd605ef2d 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -10,6 +10,7 @@ const assert = @import("../quirks.zig").inlineAssert; const testing = std.testing; const Allocator = std.mem.Allocator; const unicode = @import("../unicode/main.zig"); +const uucode = @import("uucode"); const ansi = @import("ansi.zig"); const modespkg = @import("modes.zig"); @@ -352,7 +353,7 @@ pub fn print(self: *Terminal, c: u21) !void { if (prev.cell.codepoint() == 0) break :grapheme; const grapheme_break = brk: { - var state: unicode.GraphemeBreakState = .{}; + var state: uucode.grapheme.BreakState = .default; var cp1: u21 = prev.cell.content.codepoint; if (prev.cell.hasGrapheme()) { const cps = self.screens.active.cursor.page_pin.node.data.lookupGrapheme(prev.cell).?; @@ -378,9 +379,10 @@ pub fn print(self: *Terminal, c: u21) !void { // But it is correct enough (the emoji check alone served us // well through Ghostty 1.2.3!) and we can fix it up later. - // Emoji always allow VS15/16 const prev_props = unicode.table.get(prev.cell.content.codepoint); - const emoji = prev_props.grapheme_boundary_class.isExtendedPictographic(); + // This is incorrect, but will be fixed in a separate PR. + const emoji = prev_props.grapheme_break == .extended_pictographic or + prev_props.grapheme_break == .emoji_modifier_base; if (!emoji) valid_check: { // If not an emoji, check if it is a defined variation // sequence in emoji-variation-sequences.txt @@ -513,7 +515,7 @@ pub fn print(self: *Terminal, c: u21) !void { // If this is a emoji variation selector, prev must be an emoji if (c == 0xFE0F or c == 0xFE0E) { const prev_props = unicode.table.get(prev.content.codepoint); - const emoji = prev_props.grapheme_boundary_class == .extended_pictographic; + const emoji = prev_props.grapheme_break == .extended_pictographic; if (!emoji) return; } @@ -3890,6 +3892,53 @@ test "Terminal: overwrite multicodepoint grapheme tail clears grapheme data" { try testing.expectEqual(@as(usize, 0), page.graphemeCount()); } +test "Terminal: print breaks valid grapheme cluster with Prepend + ASCII for speed" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + t.modes.set(.grapheme_cluster, true); + + // Make sure we're not at cursor.x == 0 for the next char. + try t.print('_'); + + // U+0600 ARABIC NUMBER SIGN (Prepend) + try t.print(0x0600); + try t.print('1'); + + // We should have 3 cells taken up, each narrow. Note that this is + // **incorrect** grapheme break behavior, since a Prepend code point should + // not break with the one following it per UAX #29 GB9b. However, as an + // optimization we assume a grapheme break when c <= 255, and note that + // this deviation only affects these very uncommon scenarios (e.g. the + // Arabic number sign should precede Arabic-script digits). + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x); + // This is what we'd expect if we did break correctly: + //try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); + + // Assert various properties about our screen to verify + // we have all expected cells. + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0x0600), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + // This is what we'd expect if we did break correctly: + //try testing.expect(cell.hasGrapheme()); + //try testing.expectEqualSlices(u21, &.{'1'}, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, '1'), cell.content.codepoint); + // This is what we'd expect if we did break correctly: + //try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + } +} + test "Terminal: print writes to bottom if scrolled" { var t = try init(testing.allocator, .{ .cols = 5, .rows = 2 }); defer t.deinit(testing.allocator); diff --git a/src/unicode/grapheme.zig b/src/unicode/grapheme.zig index 47be43bb0..d233dec72 100644 --- a/src/unicode/grapheme.zig +++ b/src/unicode/grapheme.zig @@ -1,6 +1,6 @@ const std = @import("std"); const table = @import("props_table.zig").table; -const GraphemeBoundaryClass = @import("props.zig").GraphemeBoundaryClass; +const uucode = @import("uucode"); /// Determines if there is a grapheme break between two codepoints. This /// must be called sequentially maintaining the state between calls. @@ -9,11 +9,11 @@ const GraphemeBoundaryClass = @import("props.zig").GraphemeBoundaryClass; /// line feeds, and carriage returns are expected to be filtered out before /// calling this function. This is because this function is tuned for /// Ghostty. -pub fn graphemeBreak(cp1: u21, cp2: u21, state: *BreakState) bool { +pub fn graphemeBreak(cp1: u21, cp2: u21, state: *uucode.grapheme.BreakState) bool { const value = Precompute.data[ (Precompute.Key{ - .gbc1 = table.get(cp1).grapheme_boundary_class, - .gbc2 = table.get(cp2).grapheme_boundary_class, + .gb1 = table.get(cp1).grapheme_break, + .gb2 = table.get(cp2).grapheme_break, .state = state.*, }).index() ]; @@ -21,133 +21,64 @@ pub fn graphemeBreak(cp1: u21, cp2: u21, state: *BreakState) bool { return value.result; } -/// The state that must be maintained between calls to `graphemeBreak`. -pub const BreakState = packed struct(u2) { - extended_pictographic: bool = false, - regional_indicator: bool = false, -}; - /// This is all the structures and data for the precomputed lookup table -/// for all possible permutations of state and grapheme boundary classes. -/// Precomputation only requires 2^10 keys of 3 bit values so the whole -/// table is less than 1KB. +/// for all possible permutations of state and grapheme break properties. +/// Precomputation requires 2^13 keys of 4 bit values so the whole table is +/// 8KB. const Precompute = struct { - const Key = packed struct(u10) { - state: BreakState, - gbc1: GraphemeBoundaryClass, - gbc2: GraphemeBoundaryClass, + const Key = packed struct(u13) { + state: uucode.grapheme.BreakState, + gb1: uucode.x.types.GraphemeBreakNoControl, + gb2: uucode.x.types.GraphemeBreakNoControl, fn index(self: Key) usize { - return @intCast(@as(u10, @bitCast(self))); + return @intCast(@as(u13, @bitCast(self))); } }; - const Value = packed struct(u3) { + const Value = packed struct(u4) { result: bool, - state: BreakState, + state: uucode.grapheme.BreakState, }; const data = precompute: { - var result: [std.math.maxInt(u10)]Value = undefined; + var result: [std.math.maxInt(u13) + 1]Value = undefined; + + const max_state_int = blk: { + var max: usize = 0; + for (@typeInfo(uucode.grapheme.BreakState).@"enum".fields) |field| { + if (field.value > max) max = field.value; + } + break :blk max; + }; - @setEvalBranchQuota(3_000); - const info = @typeInfo(GraphemeBoundaryClass).@"enum"; - for (0..std.math.maxInt(u2) + 1) |state_init| { + @setEvalBranchQuota(10_000); + const info = @typeInfo(uucode.x.types.GraphemeBreakNoControl).@"enum"; + for (0..max_state_int + 1) |state_int| { for (info.fields) |field1| { for (info.fields) |field2| { - var state: BreakState = @bitCast(@as(u2, @intCast(state_init))); + var state: uucode.grapheme.BreakState = @enumFromInt(state_int); + const key: Key = .{ - .gbc1 = @field(GraphemeBoundaryClass, field1.name), - .gbc2 = @field(GraphemeBoundaryClass, field2.name), + .gb1 = @field(uucode.x.types.GraphemeBreakNoControl, field1.name), + .gb2 = @field(uucode.x.types.GraphemeBreakNoControl, field2.name), .state = state, }; - const v = graphemeBreakClass(key.gbc1, key.gbc2, &state); + const v = uucode.x.grapheme.computeGraphemeBreakNoControl( + key.gb1, + key.gb2, + &state, + ); result[key.index()] = .{ .result = v, .state = state }; } } } + std.debug.assert(@sizeOf(@TypeOf(result)) == 8192); break :precompute result; }; }; -/// This is the algorithm from utf8proc. We only use this offline for -/// precomputing the lookup table. -fn graphemeBreakClass( - gbc1: GraphemeBoundaryClass, - gbc2: GraphemeBoundaryClass, - state: *BreakState, -) bool { - // GB11: Emoji Extend* ZWJ x Emoji - if (!state.extended_pictographic and gbc1.isExtendedPictographic()) { - state.extended_pictographic = true; - } - - // These two properties are ignored because they're not relevant to - // Ghostty -- they're filtered out before checking grapheme boundaries. - // GB3: CR x LF - // GB4: Control - - // GB6: Hangul L x (L|V|LV|VT) - if (gbc1 == .L) { - if (gbc2 == .L or - gbc2 == .V or - gbc2 == .LV or - gbc2 == .LVT) return false; - } - - // GB7: Hangul (LV | V) x (V | T) - if (gbc1 == .LV or gbc1 == .V) { - if (gbc2 == .V or - gbc2 == .T) return false; - } - - // GB8: Hangul (LVT | T) x T - if (gbc1 == .LVT or gbc1 == .T) { - if (gbc2 == .T) return false; - } - - // GB9b: x (Extend | ZWJ) - if (gbc2 == .extend or gbc2 == .zwj) return false; - - // GB9a: x Spacing - if (gbc2 == .spacing_mark) return false; - - // GB9b: Prepend x - if (gbc1 == .prepend) return false; - - // GB12, GB13: RI x RI - if (gbc1 == .regional_indicator and gbc2 == .regional_indicator) { - if (state.regional_indicator) { - state.regional_indicator = false; - return true; - } else { - state.regional_indicator = true; - return false; - } - } - - // GB11: Emoji Extend* ZWJ x Emoji - if (state.extended_pictographic and - gbc1 == .zwj and - gbc2.isExtendedPictographic()) - { - state.extended_pictographic = false; - return false; - } - - // UTS #51. This isn't covered by UAX #29 as far as I can tell (but - // I'm probably wrong). This is a special case for emoji modifiers - // which only do not break if they're next to a base. - // - // emoji_modifier_sequence := emoji_modifier_base emoji_modifier - if (gbc2 == .emoji_modifier and gbc1 == .extended_pictographic_base) { - return false; - } - - return true; -} - /// 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. @@ -156,13 +87,11 @@ fn graphemeBreakClass( /// adding a `-Demit-unicode-test` option for `zig build`, but that /// hasn't been done here. pub fn main() !void { - const uucode = @import("uucode"); - // Set the min and max to control the test range. const min = 0; const max = uucode.config.max_code_point + 1; - var state: BreakState = .{}; + var state: uucode.grapheme.BreakState = .default; var uu_state: uucode.grapheme.BreakState = .default; for (min..max) |cp1| { if (cp1 % 1000 == 0) std.log.warn("progress cp1={}", .{cp1}); @@ -199,13 +128,53 @@ test "grapheme break: emoji modifier" { // Emoji and modifier { - var state: BreakState = .{}; + var state: uucode.grapheme.BreakState = .default; try testing.expect(!graphemeBreak(0x261D, 0x1F3FF, &state)); } // Non-emoji and emoji modifier { - var state: BreakState = .{}; + var state: uucode.grapheme.BreakState = .default; try testing.expect(graphemeBreak(0x22, 0x1F3FF, &state)); } } + +test "long emoji zwj sequences" { + var state: uucode.grapheme.BreakState = .default; + // ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ (family: woman, woman, girl, boy) + var it = uucode.utf8.Iterator.init("\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}_"); + var cp1 = it.next() orelse unreachable; + var cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x1F469); // ๐Ÿ‘ฉ + try std.testing.expect(!graphemeBreak(cp1, cp2, &state)); + + cp1 = cp2; + cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x200D); + try std.testing.expect(!graphemeBreak(cp1, cp2, &state)); + + cp1 = cp2; + cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x1F469); // ๐Ÿ‘ฉ + try std.testing.expect(!graphemeBreak(cp1, cp2, &state)); + + cp1 = cp2; + cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x200D); + try std.testing.expect(!graphemeBreak(cp1, cp2, &state)); + + cp1 = cp2; + cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x1F467); // ๐Ÿ‘ง + try std.testing.expect(!graphemeBreak(cp1, cp2, &state)); + + cp1 = cp2; + cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x200D); + try std.testing.expect(!graphemeBreak(cp1, cp2, &state)); + + cp1 = cp2; + cp2 = it.next() orelse unreachable; + try std.testing.expect(cp1 == 0x1F466); // ๐Ÿ‘ฆ + try std.testing.expect(graphemeBreak(cp1, cp2, &state)); // break +} diff --git a/src/unicode/main.zig b/src/unicode/main.zig index 427c65614..11ecbd903 100644 --- a/src/unicode/main.zig +++ b/src/unicode/main.zig @@ -4,7 +4,6 @@ const grapheme = @import("grapheme.zig"); pub const table = @import("props_table.zig").table; pub const Properties = @import("props.zig").Properties; pub const graphemeBreak = grapheme.graphemeBreak; -pub const GraphemeBreakState = grapheme.BreakState; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 7099e79cd..5f6b88936 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -5,6 +5,7 @@ //! benchmarks in src/bench to verify that we haven't regressed. const std = @import("std"); +const uucode = @import("uucode"); pub const Properties = packed struct { /// Codepoint width. We clamp to [0, 2] since Ghostty handles control @@ -12,8 +13,8 @@ pub const Properties = packed struct { /// becomes a 2-em dash). width: u2 = 0, - /// Grapheme boundary class. - grapheme_boundary_class: GraphemeBoundaryClass = .invalid, + /// Grapheme break property. + grapheme_break: uucode.x.types.GraphemeBreakNoControl = .other, /// Emoji VS compatibility emoji_vs_text: bool = false, @@ -22,7 +23,7 @@ pub const Properties = packed struct { // Needed for lut.Generator pub fn eql(a: Properties, b: Properties) bool { return a.width == b.width and - a.grapheme_boundary_class == b.grapheme_boundary_class and + a.grapheme_break == b.grapheme_break and a.emoji_vs_text == b.emoji_vs_text and a.emoji_vs_emoji == b.emoji_vs_emoji; } @@ -35,48 +36,15 @@ pub const Properties = packed struct { try writer.print( \\.{{ \\ .width= {}, - \\ .grapheme_boundary_class= .{s}, + \\ .grapheme_break= .{s}, \\ .emoji_vs_text= {}, \\ .emoji_vs_emoji= {}, \\}} , .{ self.width, - @tagName(self.grapheme_boundary_class), + @tagName(self.grapheme_break), self.emoji_vs_text, self.emoji_vs_emoji, }); } }; - -/// 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} - - /// 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, - }; - } -}; diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index b30c4be3a..d0052b739 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -4,57 +4,18 @@ const assert = std.debug.assert; const uucode = @import("uucode"); const lut = @import("lut.zig"); const Properties = @import("props.zig").Properties; -const GraphemeBoundaryClass = @import("props.zig").GraphemeBoundaryClass; - -/// Gets the grapheme boundary class for a codepoint. -/// The use case for this is only in generating lookup tables. -fn graphemeBoundaryClass(cp: u21) GraphemeBoundaryClass { - if (cp > uucode.config.max_code_point) return .invalid; - - 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, - .emoji_modifier => .emoji_modifier, - .emoji_modifier_base => .extended_pictographic_base, - - .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. Also note - // that `indic_conjunct_break_consonant` is `other` in - // 'GraphemeBreakProperty.txt' (it's missing). - .other, - .indic_conjunct_break_consonant, - .cr, - .lf, - .control, - => .invalid, - }; -} pub fn get(cp: u21) Properties { if (cp > uucode.config.max_code_point) return .{ .width = 1, - .grapheme_boundary_class = .invalid, + .grapheme_break = .other, .emoji_vs_text = false, .emoji_vs_emoji = false, }; return .{ .width = uucode.get(.width, cp), - .grapheme_boundary_class = graphemeBoundaryClass(cp), + .grapheme_break = uucode.get(.grapheme_break_no_control, cp), .emoji_vs_text = uucode.get(.is_emoji_vs_text, cp), .emoji_vs_emoji = uucode.get(.is_emoji_vs_emoji, cp), }; -- 2.51.2 From bc42c821bb099b08dae4bb295e01bacaa719099f Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 24 Nov 2025 11:48:40 -0500 Subject: [PATCH 002/349] Add back accidentally removed line --- src/terminal/Terminal.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index f2d66bf2c..341a54850 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -375,6 +375,7 @@ pub fn print(self: *Terminal, c: u21) !void { // the cell width accordingly. VS16 makes the character wide and // VS15 makes it narrow. if (c == 0xFE0F or c == 0xFE0E) { + const prev_props = unicode.table.get(prev.cell.content.codepoint); // Check if it is a valid variation sequence in // emoji-variation-sequences.txt, and if not, ignore the char. if (!prev_props.emoji_vs_base) return; -- 2.51.2 From 07b47b87fa26390c697e4e78292d4cf991031ae2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2025 07:27:57 -0800 Subject: [PATCH 003/349] renderer/metal: clamp texture sizes to the maximum allowed by the device This prevents a crash in our renderer when it is larger. I will pair this with apprt changes so that our mac app won't ever allow a default window larger than the screen but we should be resilient at the renderer level as well. --- src/renderer/Metal.zig | 44 ++++++++++++++++++++++++++++++++++++-- src/renderer/metal/api.zig | 21 ++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 168f54c2b..f1d912152 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -55,6 +55,9 @@ blending: configpkg.Config.AlphaBlending, /// the "shared" storage mode, instead we have to use the "managed" mode. default_storage_mode: mtl.MTLResourceOptions.StorageMode, +/// The maximum 2D texture width and height supported by the device. +max_texture_size: u32, + /// We start an AutoreleasePool before `drawFrame` and end it afterwards. autorelease_pool: ?*objc.AutoreleasePool = null, @@ -72,8 +75,14 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); errdefer queue.release(); + // Grab metadata about the device. const default_storage_mode: mtl.MTLResourceOptions.StorageMode = if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed; + const max_texture_size = queryMaxTextureSize(device); + log.debug( + "device properties default_storage_mode={} max_texture_size={}", + .{ default_storage_mode, max_texture_size }, + ); const ViewInfo = struct { view: objc.Object, @@ -138,6 +147,7 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { .queue = queue, .blending = opts.config.blending, .default_storage_mode = default_storage_mode, + .max_texture_size = max_texture_size, }; } @@ -202,9 +212,19 @@ pub fn initShaders( pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } { const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds"); const scale = self.layer.layer.getProperty(f64, "contentsScale"); + + // We need to clamp our runtime surface size to the maximum + // possible texture size since we can't create a screen buffer (texture) + // larger than that. return .{ - .width = @intFromFloat(bounds.size.width * scale), - .height = @intFromFloat(bounds.size.height * scale), + .width = @min( + @as(u32, @intFromFloat(bounds.size.width * scale)), + self.max_texture_size, + ), + .height = @min( + @as(u32, @intFromFloat(bounds.size.height * scale)), + self.max_texture_size, + ), }; } @@ -412,3 +432,23 @@ fn chooseDevice() error{NoMetalDevice}!objc.Object { const device = chosen_device orelse return error.NoMetalDevice; return device.retain(); } + +/// Determines the maximum 2D texture size supported by the device. +/// We need to clamp our frame size to this if its larger. +fn queryMaxTextureSize(device: objc.Object) u32 { + // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf + + if (device.msgSend( + bool, + objc.sel("supportsFamily:"), + .{mtl.MTLGPUFamily.apple10}, + )) return 32768; + + if (device.msgSend( + bool, + objc.sel("supportsFamily:"), + .{mtl.MTLGPUFamily.apple3}, + )) return 16384; + + return 8192; +} diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index e1daa6848..a2d8a1356 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -391,6 +391,27 @@ pub const MTLRenderStage = enum(c_ulong) { mesh = 16, }; +/// https://developer.apple.com/documentation/metal/mtlgpufamily?language=objc +pub const MTLGPUFamily = enum(c_long) { + apple1 = 1001, + apple2 = 1002, + apple3 = 1003, + apple4 = 1004, + apple5 = 1005, + apple6 = 1006, + apple7 = 1007, + apple8 = 1008, + apple9 = 1009, + apple10 = 1010, + + common1 = 3001, + common2 = 3002, + common3 = 3003, + + metal3 = 5001, + metal4 = 5002, +}; + pub const MTLClearColor = extern struct { red: f64, green: f64, -- 2.51.2 From 594195963d650d09ed6cb3245671f282aad4b8a7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2025 09:31:10 -0800 Subject: [PATCH 004/349] Update src/renderer/Metal.zig Co-authored-by: Jon Parise --- src/renderer/Metal.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f1d912152..2aac285c6 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -434,7 +434,7 @@ fn chooseDevice() error{NoMetalDevice}!objc.Object { } /// Determines the maximum 2D texture size supported by the device. -/// We need to clamp our frame size to this if its larger. +/// We need to clamp our frame size to this if it's larger. fn queryMaxTextureSize(device: objc.Object) u32 { // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf -- 2.51.2 From d1bea9d737eef15cfb0a2a6a3f8e33f132158a48 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2025 10:28:51 -0800 Subject: [PATCH 005/349] macos: window width/height should be clamped, work with position Fixes #9952 Fixes #9969 This fixes our `constrainToScreen` implementation to properly clamp the window size to the visible screen its coming on as documented. Further, this addresses the positioning problem, too. --- .../Helpers/Extensions/NSWindow+Extension.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index d834f5e63..f8df803db 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -16,19 +16,23 @@ extension NSWindow { return firstWindow === self } - /// Adjusts the window origin if necessary to ensure the window remains visible on screen. + /// Adjusts the window frame if necessary to ensure the window remains visible on screen. + /// This constrains both the size (to not exceed the screen) and the origin (to keep the window on screen). func constrainToScreen() { guard let screen = screen ?? NSScreen.main else { return } let visibleFrame = screen.visibleFrame var windowFrame = frame + windowFrame.size.width = min(windowFrame.size.width, visibleFrame.size.width) + windowFrame.size.height = min(windowFrame.size.height, visibleFrame.size.height) + windowFrame.origin.x = max(visibleFrame.minX, min(windowFrame.origin.x, visibleFrame.maxX - windowFrame.width)) windowFrame.origin.y = max(visibleFrame.minY, min(windowFrame.origin.y, visibleFrame.maxY - windowFrame.height)) - if windowFrame.origin != frame.origin { - setFrameOrigin(windowFrame.origin) + if windowFrame != frame { + setFrame(windowFrame, display: true) } } } -- 2.51.2 From 63422f4d4e10a9c78fe7162bf784689cf226bada Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Dec 2025 11:56:04 -0800 Subject: [PATCH 006/349] add the catch_all binding key Part of #9963 This adds a new special key `catch_all` that can be used in keybinding definitions to match any key that is not explicitly bound. For example: `keybind = catch_all=new_window` (chaos!). `catch_all` can be used in combination with modifiers, so if you want to catch any non-bound key with Ctrl held down, you can do: `keybind = ctrl+catch_all=new_window`. `catch_all` can also be used with trigger sequences, so you can do: `keybind = ctrl+a>catch_all=new_window` to catch any key pressed after `ctrl+a` that is not explicitly bound and make a new window! And if you want to remove the catch all binding, it is like any other: `keybind = catch_all=unbind`. --- include/ghostty.h | 2 + macos/Sources/Ghostty/Ghostty.Input.swift | 4 + src/apprt/gtk/key.zig | 2 + src/cli/list_keybinds.zig | 5 + src/config/Config.zig | 7 ++ src/input/Binding.zig | 129 +++++++++++++++++++++- 6 files changed, 148 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index 47db34e71..736c7546b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -317,12 +317,14 @@ typedef struct { typedef enum { GHOSTTY_TRIGGER_PHYSICAL, GHOSTTY_TRIGGER_UNICODE, + GHOSTTY_TRIGGER_CATCH_ALL, } ghostty_input_trigger_tag_e; typedef union { ghostty_input_key_e translated; ghostty_input_key_e physical; uint32_t unicode; + // catch_all has no payload } ghostty_input_trigger_key_u; typedef struct { diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index e05911c06..6b4eb0ae4 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -32,6 +32,10 @@ extension Ghostty { guard let scalar = UnicodeScalar(trigger.key.unicode) else { return nil } key = KeyEquivalent(Character(scalar)) + case GHOSTTY_TRIGGER_CATCH_ALL: + // catch_all matches any key, so it can't be represented as a KeyboardShortcut + return nil + default: return nil } diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 19bdc8315..35c9390b2 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -74,6 +74,8 @@ fn writeTriggerKey( try writer.print("{u}", .{cp}); } }, + + .catch_all => return false, } return true; diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index a8899a4f5..e463f55b9 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -166,16 +166,19 @@ const ChordBinding = struct { var r_trigger = rhs.triggers.first; while (l_trigger != null and r_trigger != null) { + // We want catch_all to sort last. const lhs_key: c_int = blk: { switch (TriggerNode.get(l_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), + .catch_all => break :blk std.math.maxInt(c_int), } }; const rhs_key: c_int = blk: { switch (TriggerNode.get(r_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), + .catch_all => break :blk std.math.maxInt(c_int), } }; @@ -268,6 +271,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { const key = switch (trigger.data.key) { .physical => |k| try std.fmt.allocPrint(alloc, "{t}", .{k}), .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}), + .catch_all => "catch_all", }; result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col }); @@ -314,6 +318,7 @@ fn iterateBindings( switch (t.key) { .physical => |k| try buf.writer.print("{t}", .{k}), .unicode => |c| try buf.writer.print("{u}", .{c}), + .catch_all => try buf.writer.print("catch_all", .{}), } break :blk win.gwidth(buf.written()); diff --git a/src/config/Config.zig b/src/config/Config.zig index 1aad62d7d..f2f6f2322 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1477,6 +1477,13 @@ class: ?[:0]const u8 = null, /// so if you specify both `a` and `KeyA`, the physical key will always be used /// regardless of what order they are configured. /// +/// The special key `catch_all` can be used to match any key that is not +/// otherwise bound. This can be combined with modifiers, for example +/// `ctrl+catch_all` will match any key pressed with `ctrl` that is not +/// otherwise bound. When looking up a binding, Ghostty first tries to match +/// `catch_all` with modifiers. If no match is found and the event has +/// modifiers, it falls back to `catch_all` without modifiers. +/// /// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`, /// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier /// or the alias. When debugging keybinds, the non-aliased modifier will always diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 9f3ad8a2a..666852094 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1505,6 +1505,10 @@ pub const Trigger = struct { /// codepoint. This is useful for binding to keys that don't have a /// registered keycode with Ghostty. unicode: u21, + + /// A catch-all key that matches any key press that is otherwise + /// unbound. + catch_all, }; /// The extern struct used for triggers in the C API. @@ -1516,6 +1520,7 @@ pub const Trigger = struct { pub const Tag = enum(c_int) { physical, unicode, + catch_all, }; pub const Key = extern union { @@ -1611,6 +1616,13 @@ pub const Trigger = struct { continue :loop; } + // Check for catch_all. We do this near the end since its unlikely + // in most cases that we're setting a catch-all key. + if (std.mem.eql(u8, part, "catch_all")) { + result.key = .catch_all; + continue :loop; + } + // If we're still unset then we look for backwards compatible // keys with Ghostty 1.1.x. We do this last so its least likely // to impact performance for modern users. @@ -1751,7 +1763,7 @@ pub const Trigger = struct { pub fn isKeyUnset(self: Trigger) bool { return switch (self.key) { .physical => |v| v == .unidentified, - else => false, + .unicode, .catch_all => false, }; } @@ -1771,6 +1783,7 @@ pub const Trigger = struct { hasher, foldedCodepoint(cp), ), + .catch_all => {}, } std.hash.autoHash(hasher, self.mods.binding()); } @@ -1801,6 +1814,9 @@ pub const Trigger = struct { .key = switch (self.key) { .physical => |v| .{ .physical = v }, .unicode => |v| .{ .unicode = @intCast(v) }, + // catch_all has no associated value so its an error + // for a C consumer to look at it. + .catch_all => undefined, }, .mods = self.mods, }; @@ -1821,6 +1837,7 @@ pub const Trigger = struct { switch (self.key) { .physical => |k| try writer.print("{t}", .{k}), .unicode => |c| try writer.print("{u}", .{c}), + .catch_all => try writer.writeAll("catch_all"), } } }; @@ -2213,6 +2230,14 @@ pub const Set = struct { if (self.get(trigger)) |v| return v; } + // Fallback to catch_all with modifiers first, then without modifiers. + trigger.key = .catch_all; + if (self.get(trigger)) |v| return v; + if (!trigger.mods.empty()) { + trigger.mods = .{}; + if (self.get(trigger)) |v| return v; + } + return null; } @@ -2433,6 +2458,31 @@ test "parse: w3c key names" { try testing.expectError(Error.InvalidFormat, parseSingle("Keya=ignore")); } +test "parse: catch_all" { + const testing = std.testing; + + // Basic catch_all + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .catch_all }, + .action = .{ .ignore = {} }, + }, + try parseSingle("catch_all=ignore"), + ); + + // catch_all with modifiers + try testing.expectEqual( + Binding{ + .trigger = .{ + .mods = .{ .ctrl = true }, + .key = .catch_all, + }, + .action = .{ .ignore = {} }, + }, + try parseSingle("ctrl+catch_all=ignore"), + ); +} + test "parse: plus sign" { const testing = std.testing; @@ -3329,6 +3379,83 @@ test "set: getEvent codepoint case folding" { } } +test "set: getEvent catch_all fallback" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "catch_all=ignore"); + + // Matches unbound key without modifiers + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{}, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } + + // Matches unbound key with modifiers (falls back to catch_all without mods) + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } + + // Specific binding takes precedence over catch_all + try s.parseAndPut(alloc, "ctrl+b=new_window"); + { + const action = s.getEvent(.{ + .key = .key_b, + .mods = .{ .ctrl = true }, + .unshifted_codepoint = 'b', + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } +} + +test "set: getEvent catch_all with modifiers" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "ctrl+catch_all=close_surface"); + try s.parseAndPut(alloc, "catch_all=ignore"); + + // Key with ctrl matches catch_all with ctrl modifier + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .close_surface); + } + + // Key without mods matches catch_all without mods + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{}, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } + + // Key with different mods falls back to catch_all without mods + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .alt = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .ignore); + } +} + test "Action: clone" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); -- 2.51.2 From c53b3fffd5af411be05467ebc88ee9222e67b031 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 13:26:20 -0800 Subject: [PATCH 007/349] config: keybind table parsing --- src/config/Config.zig | 313 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index f2f6f2322..8d941c733 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5812,6 +5812,10 @@ pub const RepeatableFontVariation = struct { pub const Keybinds = struct { set: inputpkg.Binding.Set = .{}, + /// Defined key tables. The default key table is always the root "set", + /// which allows all table names to be available without reservation. + tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty, + pub fn init(self: *Keybinds, alloc: Allocator) !void { // We don't clear the memory because it's in the arena and unlikely // to be free-able anyways (since arenas can only clear the last @@ -6590,18 +6594,69 @@ pub const Keybinds = struct { return; } - // Let our much better tested binding package handle parsing and storage. + // Check for table syntax: "name/" or "name/binding" + // We look for '/' only before the first '=' to avoid matching + // action arguments like "foo=text:/hello". + const eq_idx = std.mem.indexOfScalar(u8, value, '=') orelse value.len; + if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| { + const table_name = value[0..slash_idx]; + const binding = value[slash_idx + 1 ..]; + + // Table name cannot be empty + if (table_name.len == 0) return error.InvalidFormat; + + // Get or create the table + const gop = try self.tables.getOrPut(alloc, table_name); + if (!gop.found_existing) { + gop.value_ptr.* = .{}; + } + + // If there's no binding after the slash, this is a table + // definition/clear command + if (binding.len == 0) { + log.debug("config has 'keybind = {s}/', table cleared", .{table_name}); + gop.value_ptr.* = .{}; + return; + } + + // Parse and add the binding to the table + try gop.value_ptr.parseAndPut(alloc, binding); + return; + } + + // Parse into default set try self.set.parseAndPut(alloc, value); } /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Keybinds, alloc: Allocator) Allocator.Error!Keybinds { - return .{ .set = try self.set.clone(alloc) }; + var tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty; + try tables.ensureTotalCapacity(alloc, @intCast(self.tables.count())); + var it = self.tables.iterator(); + while (it.next()) |entry| { + tables.putAssumeCapacity(entry.key_ptr.*, try entry.value_ptr.clone(alloc)); + } + + return .{ + .set = try self.set.clone(alloc), + .tables = tables, + }; } /// Compare if two of our value are requal. Required by Config. pub fn equal(self: Keybinds, other: Keybinds) bool { - return equalSet(&self.set, &other.set); + if (!equalSet(&self.set, &other.set)) return false; + + // Compare tables + if (self.tables.count() != other.tables.count()) return false; + + var it = self.tables.iterator(); + while (it.next()) |entry| { + const other_set = other.tables.get(entry.key_ptr.*) orelse return false; + if (!equalSet(entry.value_ptr, &other_set)) return false; + } + + return true; } fn equalSet( @@ -6652,12 +6707,14 @@ pub const Keybinds = struct { /// Like formatEntry but has an option to include docs. pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void { - if (self.set.bindings.size == 0) { + if (self.set.bindings.size == 0 and self.tables.count() == 0) { try formatter.formatEntry(void, {}); return; } var buf: [1024]u8 = undefined; + + // Format root set bindings var iter = self.set.bindings.iterator(); while (iter.next()) |next| { const k = next.key_ptr.*; @@ -6684,6 +6741,23 @@ pub const Keybinds = struct { writer.print("{f}", .{k}) catch return error.OutOfMemory; try v.formatEntries(&writer, formatter); } + + // Format table bindings + var table_iter = self.tables.iterator(); + while (table_iter.next()) |table_entry| { + const table_name = table_entry.key_ptr.*; + const table_set = table_entry.value_ptr.*; + + var binding_iter = table_set.bindings.iterator(); + while (binding_iter.next()) |next| { + const k = next.key_ptr.*; + const v = next.value_ptr.*; + + var writer: std.Io.Writer = .fixed(&buf); + writer.print("{s}/{f}", .{ table_name, k }) catch return error.OutOfMemory; + try v.formatEntries(&writer, formatter); + } + } } /// Used by Formatter @@ -6768,6 +6842,237 @@ pub const Keybinds = struct { ; try std.testing.expectEqualStrings(want, buf.written()); } + + test "parseCLI table definition" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Define a table by adding a binding to it + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("foo")); + + const table = keybinds.tables.get("foo").?; + try testing.expectEqual(1, table.bindings.count()); + } + + test "parseCLI table clear" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Add a binding to a table + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count()); + + // Clear the table with "foo/" + try keybinds.parseCLI(alloc, "foo/"); + try testing.expectEqual(0, keybinds.tables.get("foo").?.bindings.count()); + } + + test "parseCLI table multiple bindings" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window"); + + try testing.expectEqual(2, keybinds.tables.count()); + try testing.expectEqual(2, keybinds.tables.get("foo").?.bindings.count()); + try testing.expectEqual(1, keybinds.tables.get("bar").?.bindings.count()); + } + + test "parseCLI table does not affect root set" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + + // Root set should have the first binding + try testing.expectEqual(1, keybinds.set.bindings.count()); + // Table should have the second binding + try testing.expectEqual(1, keybinds.tables.get("foo").?.bindings.count()); + } + + test "parseCLI table empty name is invalid" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try testing.expectError(error.InvalidFormat, keybinds.parseCLI(alloc, "/shift+a=copy_to_clipboard")); + } + + test "parseCLI table with key sequence" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Key sequences should work within tables + try keybinds.parseCLI(alloc, "foo/ctrl+a>ctrl+b=new_window"); + + const table = keybinds.tables.get("foo").?; + try testing.expectEqual(1, table.bindings.count()); + } + + test "parseCLI slash in action argument is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // A slash after the = should not be interpreted as a table delimiter + try keybinds.parseCLI(alloc, "ctrl+a=text:/hello"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "clone with tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window"); + + const cloned = try keybinds.clone(alloc); + + // Verify the clone has the same structure + try testing.expectEqual(keybinds.set.bindings.count(), cloned.set.bindings.count()); + try testing.expectEqual(keybinds.tables.count(), cloned.tables.count()); + try testing.expectEqual( + keybinds.tables.get("foo").?.bindings.count(), + cloned.tables.get("foo").?.bindings.count(), + ); + try testing.expectEqual( + keybinds.tables.get("bar").?.bindings.count(), + cloned.tables.get("bar").?.bindings.count(), + ); + } + + test "equal with tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + try testing.expect(keybinds1.equal(keybinds2)); + } + + test "equal with tables different table count" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try keybinds2.parseCLI(alloc, "bar/shift+b=paste_from_clipboard"); + + try testing.expect(!keybinds1.equal(keybinds2)); + } + + test "equal with tables different table names" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "bar/shift+a=copy_to_clipboard"); + + try testing.expect(!keybinds1.equal(keybinds2)); + } + + test "equal with tables different bindings" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds1: Keybinds = .{}; + try keybinds1.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + + var keybinds2: Keybinds = .{}; + try keybinds2.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + + try testing.expect(!keybinds1.equal(keybinds2)); + } + + test "formatEntry with tables" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello"); + try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer)); + + try testing.expectEqualStrings("keybind = foo/shift+a=csi:hello\n", buf.written()); + } + + test "formatEntry with tables and root set" { + const testing = std.testing; + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + try keybinds.parseCLI(alloc, "shift+b=csi:world"); + try keybinds.parseCLI(alloc, "foo/shift+a=csi:hello"); + try keybinds.formatEntry(formatterpkg.entryFormatter("keybind", &buf.writer)); + + const output = buf.written(); + try testing.expect(std.mem.indexOf(u8, output, "keybind = shift+b=csi:world\n") != null); + try testing.expect(std.mem.indexOf(u8, output, "keybind = foo/shift+a=csi:hello\n") != null); + } }; /// See "font-codepoint-map" for documentation. -- 2.51.2 From 8c59143c1a9d5418ef9f97c7dfd57d8caa8d697f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 13:46:00 -0800 Subject: [PATCH 008/349] rename some key sequence state so it is clearer what it is --- src/Surface.zig | 56 ++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index fc5a239ab..711c79833 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -253,18 +253,9 @@ const Mouse = struct { /// Keyboard state for the surface. pub const Keyboard = struct { - /// The currently active keybindings for the surface. This is used to - /// implement sequences: as leader keys are pressed, the active bindings - /// set is updated to reflect the current leader key sequence. If this is - /// null then the root bindings are used. - bindings: ?*const input.Binding.Set = null, - - /// The last handled binding. This is used to prevent encoding release - /// events for handled bindings. We only need to keep track of one because - /// at least at the time of writing this, its impossible for two keys of - /// a combination to be handled by different bindings before the release - /// of the prior (namely since you can't bind modifier-only). - last_trigger: ?u64 = null, + /// The currently active key sequence for the surface. If this is null + /// then we're not currently in a key sequence. + sequence_set: ?*const input.Binding.Set = null, /// The queued keys when we're in the middle of a sequenced binding. /// These are flushed when the sequence is completed and unconsumed or @@ -272,7 +263,14 @@ pub const Keyboard = struct { /// /// This is naturally bounded due to the configuration maximum /// length of a sequence. - queued: std.ArrayListUnmanaged(termio.Message.WriteReq) = .{}, + sequence_queued: std.ArrayListUnmanaged(termio.Message.WriteReq) = .empty, + + /// The last handled binding. This is used to prevent encoding release + /// events for handled bindings. We only need to keep track of one because + /// at least at the time of writing this, its impossible for two keys of + /// a combination to be handled by different bindings before the release + /// of the prior (namely since you can't bind modifier-only). + last_trigger: ?u64 = null, }; /// The configuration that a surface has, this is copied from the main @@ -793,8 +791,8 @@ pub fn deinit(self: *Surface) void { } // Clean up our keyboard state - for (self.keyboard.queued.items) |req| req.deinit(); - self.keyboard.queued.deinit(self.alloc); + for (self.keyboard.sequence_queued.items) |req| req.deinit(); + self.keyboard.sequence_queued.deinit(self.alloc); // Clean up our font grid self.app.font_grid_set.deref(self.font_grid_key); @@ -2565,7 +2563,7 @@ pub fn keyEventIsBinding( // Our keybinding set is either our current nested set (for // sequences) or the root set. - const set = self.keyboard.bindings orelse &self.config.keybind.set; + const set = self.keyboard.sequence_set orelse &self.config.keybind.set; // log.warn("text keyEventIsBinding event={} match={}", .{ event, set.getEvent(event) != null }); @@ -2791,7 +2789,7 @@ fn maybeHandleBinding( // Find an entry in the keybind set that matches our event. const entry: input.Binding.Set.Entry = entry: { - const set = self.keyboard.bindings orelse &self.config.keybind.set; + const set = self.keyboard.sequence_set orelse &self.config.keybind.set; // Get our entry from the set for the given event. if (set.getEvent(event)) |v| break :entry v; @@ -2802,7 +2800,7 @@ fn maybeHandleBinding( // // We also ignore modifiers so that nested sequences such as // ctrl+a>ctrl+b>c work. - if (self.keyboard.bindings != null and + if (self.keyboard.sequence_set != null and !event.key.modifier()) { // Encode everything up to this point @@ -2816,13 +2814,13 @@ fn maybeHandleBinding( const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) { .leader => |set| { // Setup the next set we'll look at. - self.keyboard.bindings = set; + self.keyboard.sequence_set = set; // Store this event so that we can drain and encode on invalid. // We don't need to cap this because it is naturally capped by // the config validation. if (try self.encodeKey(event, insp_ev)) |req| { - try self.keyboard.queued.append(self.alloc, req); + try self.keyboard.sequence_queued.append(self.alloc, req); } // Start or continue our key sequence @@ -2861,8 +2859,8 @@ fn maybeHandleBinding( // perform an action (below) self.keyboard.last_trigger = null; - // An action also always resets the binding set. - self.keyboard.bindings = null; + // An action also always resets the sequence set. + self.keyboard.sequence_set = null; // Attempt to perform the action log.debug("key event binding flags={} action={f}", .{ @@ -2952,13 +2950,13 @@ fn endKeySequence( ); }; - // No matter what we clear our current binding set. This restores + // No matter what we clear our current sequence set. This restores // the set we look at to the root set. - self.keyboard.bindings = null; + self.keyboard.sequence_set = null; - if (self.keyboard.queued.items.len > 0) { + if (self.keyboard.sequence_queued.items.len > 0) { switch (action) { - .flush => for (self.keyboard.queued.items) |write_req| { + .flush => for (self.keyboard.sequence_queued.items) |write_req| { self.queueIo(switch (write_req) { .small => |v| .{ .write_small = v }, .stable => |v| .{ .write_stable = v }, @@ -2966,12 +2964,12 @@ fn endKeySequence( }, .unlocked); }, - .drop => for (self.keyboard.queued.items) |req| req.deinit(), + .drop => for (self.keyboard.sequence_queued.items) |req| req.deinit(), } switch (mem) { - .free => self.keyboard.queued.clearAndFree(self.alloc), - .retain => self.keyboard.queued.clearRetainingCapacity(), + .free => self.keyboard.sequence_queued.clearAndFree(self.alloc), + .retain => self.keyboard.sequence_queued.clearRetainingCapacity(), } } } -- 2.51.2 From 34ae3848b6ec356ef56299d26a74d2d4a566147b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 13:40:45 -0800 Subject: [PATCH 009/349] core: key tables --- src/Surface.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 711c79833..77daaf7e9 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -265,6 +265,10 @@ pub const Keyboard = struct { /// length of a sequence. sequence_queued: std.ArrayListUnmanaged(termio.Message.WriteReq) = .empty, + /// The stack of tables that is currently active. The first value + /// in this is the first activated table (NOT the default keybinding set). + table_stack: std.ArrayListUnmanaged(*const input.Binding.Set) = .empty, + /// The last handled binding. This is used to prevent encoding release /// events for handled bindings. We only need to keep track of one because /// at least at the time of writing this, its impossible for two keys of @@ -793,6 +797,7 @@ pub fn deinit(self: *Surface) void { // Clean up our keyboard state for (self.keyboard.sequence_queued.items) |req| req.deinit(); self.keyboard.sequence_queued.deinit(self.alloc); + self.keyboard.table_stack.deinit(self.alloc); // Clean up our font grid self.app.font_grid_set.deref(self.font_grid_key); -- 2.51.2 From 18ce219d78986d5dedadee88e47f6da7b7a59304 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 14:19:23 -0800 Subject: [PATCH 010/349] input: activate/deactivate key table binding actions --- src/Surface.zig | 43 +++++++++++++++++++++++++++++++++++++++++++ src/input/Binding.zig | 30 ++++++++++++++++++++++++++++++ src/input/command.zig | 4 ++++ 3 files changed, 77 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 77daaf7e9..b4a1048e5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5569,6 +5569,49 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + inline .activate_key_table, + .activate_key_table_once, + => |name, tag| { + // Look up the table in our config + const set = self.config.keybind.tables.getPtr(name) orelse + return false; + + // If this is the same table as is currently active, then + // do nothing. + if (self.keyboard.table_stack.items.len > 0) { + const items = self.keyboard.table_stack.items; + const active = items[items.len - 1]; + if (active == set) return false; + } + + // Add the table to the stack. + try self.keyboard.table_stack.append(self.alloc, set); + + // TODO: once + _ = tag; + }, + + .deactivate_key_table => switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, + + // Final key table active, clear our state. + 1 => self.keyboard.table_stack.clearAndFree(self.alloc), + + // Restore the prior key table. We don't free any memory in + // this case because we assume it will be freed later when + // we finish our key table. + else => _ = self.keyboard.table_stack.pop(), + }, + + .deactivate_all_key_tables => switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, + + // Clear the entire table stack. + else => self.keyboard.table_stack.clearAndFree(self.alloc), + }, + .crash => |location| switch (location) { .main => @panic("crash binding action, crashing intentionally"), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 666852094..983ed98b3 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -799,6 +799,32 @@ pub const Action = union(enum) { /// be undone or redone. redo, + /// Activate a named key table (see `keybind` configuration documentation). + /// The named key table will remain active until `deactivate_key_table` + /// is called. If you want a one-shot key table activation, use the + /// `activate_key_table_once` action instead. + /// + /// If the named key table does not exist, this action has no effect + /// and performable will report false. + /// + /// If the named key table is already the currently active key table, + /// this action has no effect and performable will report false. + activate_key_table: []const u8, + + /// Same as activate_key_table, but the key table will only be active + /// until the first valid keybinding from that table is used (including + /// any defined `catch_all` bindings). + activate_key_table_once: []const u8, + + /// Deactivate the currently active key table, if any. The next most + /// recently activated key table (if any) will become active again. + /// If no key table is active, this action has no effect. + deactivate_key_table, + + /// Deactivate all active key tables. If no active key table exists, + /// this will report performable as false. + deactivate_all_key_tables, + /// Quit Ghostty. quit, @@ -1253,6 +1279,10 @@ pub const Action = union(enum) { .toggle_background_opacity, .show_on_screen_keyboard, .reset_window_size, + .activate_key_table, + .activate_key_table_once, + .deactivate_key_table, + .deactivate_all_key_tables, .crash, => .surface, diff --git a/src/input/command.zig b/src/input/command.zig index 6ac4312a9..67086f7ec 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -671,6 +671,10 @@ fn actionCommands(action: Action.Key) []const Command { .write_scrollback_file, .goto_tab, .resize_split, + .activate_key_table, + .activate_key_table_once, + .deactivate_key_table, + .deactivate_all_key_tables, .crash, => comptime &.{}, -- 2.51.2 From 36f891afd8a63b64b6448277bf6cb59e44d005c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 14:30:33 -0800 Subject: [PATCH 011/349] implement key table lookup in maybeHandleBinding --- src/Surface.zig | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index b4a1048e5..47cafaf6f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2794,25 +2794,39 @@ fn maybeHandleBinding( // Find an entry in the keybind set that matches our event. const entry: input.Binding.Set.Entry = entry: { - const set = self.keyboard.sequence_set orelse &self.config.keybind.set; + // Handle key sequences first. + if (self.keyboard.sequence_set) |set| { + // Get our entry from the set for the given event. + if (set.getEvent(event)) |v| break :entry v; + + // No entry found. We need to encode everything up to this + // point and send to the pty since we're in a sequence. + // + // We also ignore modifiers so that nested sequences such as + // ctrl+a>ctrl+b>c work. + if (!event.key.modifier()) { + // Encode everything up to this point + self.endKeySequence(.flush, .retain); + } - // Get our entry from the set for the given event. - if (set.getEvent(event)) |v| break :entry v; + return null; + } - // No entry found. If we're not looking at the root set of the - // bindings we need to encode everything up to this point and - // send to the pty. - // - // We also ignore modifiers so that nested sequences such as - // ctrl+a>ctrl+b>c work. - if (self.keyboard.sequence_set != null and - !event.key.modifier()) - { - // Encode everything up to this point - self.endKeySequence(.flush, .retain); + // No currently active sequence, move on to tables. For tables, + // we search inner-most table to outer-most. The table stack does + // NOT include the root set. + const table_items = self.keyboard.table_stack.items; + if (table_items.len > 0) { + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + const set = table_items[rev_i]; + if (set.getEvent(event)) |v| break :entry v; + } } - return null; + // No table, use our default set + break :entry self.config.keybind.set.getEvent(event) orelse + return null; }; // Determine if this entry has an action or if its a leader key. -- 2.51.2 From 14bbc4893f84bd02d0f2026d3cd091d1ba29dd07 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 14:36:35 -0800 Subject: [PATCH 012/349] implement one-shot key tables --- src/Surface.zig | 30 ++++++++++++++++++++++-------- src/input/Binding.zig | 5 +++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 47cafaf6f..8c1c31a84 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -267,7 +267,10 @@ pub const Keyboard = struct { /// The stack of tables that is currently active. The first value /// in this is the first activated table (NOT the default keybinding set). - table_stack: std.ArrayListUnmanaged(*const input.Binding.Set) = .empty, + table_stack: std.ArrayListUnmanaged(struct { + set: *const input.Binding.Set, + once: bool, + }) = .empty, /// The last handled binding. This is used to prevent encoding release /// events for handled bindings. We only need to keep track of one because @@ -2819,8 +2822,19 @@ fn maybeHandleBinding( if (table_items.len > 0) { for (0..table_items.len) |i| { const rev_i: usize = table_items.len - 1 - i; - const set = table_items[rev_i]; - if (set.getEvent(event)) |v| break :entry v; + const table = table_items[rev_i]; + if (table.set.getEvent(event)) |v| { + // If this is a one-shot activation AND its the currently + // active table, then we deactivate it after this. + // Note: we may want to change the semantics here to + // remove this table no matter where it is in the stack, + // maybe. + if (table.once and i == 0) _ = try self.performBindingAction( + .deactivate_key_table, + ); + + break :entry v; + } } } @@ -5594,15 +5608,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool // do nothing. if (self.keyboard.table_stack.items.len > 0) { const items = self.keyboard.table_stack.items; - const active = items[items.len - 1]; + const active = items[items.len - 1].set; if (active == set) return false; } // Add the table to the stack. - try self.keyboard.table_stack.append(self.alloc, set); - - // TODO: once - _ = tag; + try self.keyboard.table_stack.append(self.alloc, .{ + .set = set, + .once = tag == .activate_key_table_once, + }); }, .deactivate_key_table => switch (self.keyboard.table_stack.items.len) { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 983ed98b3..22a5e8386 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -814,6 +814,11 @@ pub const Action = union(enum) { /// Same as activate_key_table, but the key table will only be active /// until the first valid keybinding from that table is used (including /// any defined `catch_all` bindings). + /// + /// The "once" check is only done if this is the currently active + /// key table. If another key table is activated later, then this + /// table will remain active until it pops back out to being the + /// active key table. activate_key_table_once: []const u8, /// Deactivate the currently active key table, if any. The next most -- 2.51.2 From daa613482e6a7ceacb0e97ee24f96c0ecc3a871e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 14:57:28 -0800 Subject: [PATCH 013/349] keybind = clear and reset should reset tables, too --- src/Surface.zig | 20 ++++++++++++------ src/config/Config.zig | 48 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 8c1c31a84..2254b287c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2569,14 +2569,22 @@ pub fn keyEventIsBinding( .press, .repeat => {}, } - // Our keybinding set is either our current nested set (for - // sequences) or the root set. - const set = self.keyboard.sequence_set orelse &self.config.keybind.set; + // If we're in a sequence, check the sequence set + if (self.keyboard.sequence_set) |set| { + return set.getEvent(event) != null; + } - // log.warn("text keyEventIsBinding event={} match={}", .{ event, set.getEvent(event) != null }); + // Check active key tables (inner-most to outer-most) + const table_items = self.keyboard.table_stack.items; + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + if (table_items[rev_i].set.getEvent(event) != null) { + return true; + } + } - // If we have a keybinding for this event then we return true. - return set.getEvent(event) != null; + // Check the root set + return self.config.keybind.set.getEvent(event) != null; } /// Called for any key events. This handles keybindings, encoding and diff --git a/src/config/Config.zig b/src/config/Config.zig index 8d941c733..17b4275b0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5822,6 +5822,7 @@ pub const Keybinds = struct { // allocated value). This isn't a memory leak because the arena // will be freed when the config is freed. self.set = .{}; + self.tables = .empty; // keybinds for opening and reloading config try self.set.put( @@ -6591,6 +6592,7 @@ pub const Keybinds = struct { // will be freed when the config is freed. log.info("config has 'keybind = clear', all keybinds cleared", .{}); self.set = .{}; + self.tables = .empty; return; } @@ -7073,6 +7075,52 @@ pub const Keybinds = struct { try testing.expect(std.mem.indexOf(u8, output, "keybind = shift+b=csi:world\n") != null); try testing.expect(std.mem.indexOf(u8, output, "keybind = foo/shift+a=csi:hello\n") != null); } + + test "parseCLI clear clears tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Add bindings to root set and tables + try keybinds.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "foo/shift+b=paste_from_clipboard"); + try keybinds.parseCLI(alloc, "bar/ctrl+c=close_window"); + + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(2, keybinds.tables.count()); + + // Clear all keybinds + try keybinds.parseCLI(alloc, "clear"); + + // Both root set and tables should be cleared + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI reset clears tables" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Add bindings to tables + try keybinds.parseCLI(alloc, "foo/shift+a=copy_to_clipboard"); + try keybinds.parseCLI(alloc, "bar/shift+b=paste_from_clipboard"); + + try testing.expectEqual(2, keybinds.tables.count()); + + // Reset to defaults (empty value) + try keybinds.parseCLI(alloc, ""); + + // Tables should be cleared, root set has defaults + try testing.expectEqual(0, keybinds.tables.count()); + try testing.expect(keybinds.set.bindings.count() > 0); + } }; /// See "font-codepoint-map" for documentation. -- 2.51.2 From 845bcdb498da4d2e3552ffb9385ed8c9b7fc5218 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 15:11:08 -0800 Subject: [PATCH 014/349] config: copy key table name into arena --- src/Surface.zig | 11 +++++++++-- src/config/Config.zig | 6 +++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 2254b287c..af7cdf136 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5609,15 +5609,20 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .activate_key_table_once, => |name, tag| { // Look up the table in our config - const set = self.config.keybind.tables.getPtr(name) orelse + const set = self.config.keybind.tables.getPtr(name) orelse { + log.debug("key table not found: {s}", .{name}); return false; + }; // If this is the same table as is currently active, then // do nothing. if (self.keyboard.table_stack.items.len > 0) { const items = self.keyboard.table_stack.items; const active = items[items.len - 1].set; - if (active == set) return false; + if (active == set) { + log.debug("ignoring duplicate activate table: {s}", .{name}); + return false; + } } // Add the table to the stack. @@ -5625,6 +5630,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .set = set, .once = tag == .activate_key_table_once, }); + + log.debug("key table activated: {s}", .{name}); }, .deactivate_key_table => switch (self.keyboard.table_stack.items.len) { diff --git a/src/config/Config.zig b/src/config/Config.zig index 17b4275b0..c0d8e813e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6610,6 +6610,9 @@ pub const Keybinds = struct { // Get or create the table const gop = try self.tables.getOrPut(alloc, table_name); if (!gop.found_existing) { + // We need to copy our table name into the arena + // for valid lookups later. + gop.key_ptr.* = try alloc.dupe(u8, table_name); gop.value_ptr.* = .{}; } @@ -6636,7 +6639,8 @@ pub const Keybinds = struct { try tables.ensureTotalCapacity(alloc, @intCast(self.tables.count())); var it = self.tables.iterator(); while (it.next()) |entry| { - tables.putAssumeCapacity(entry.key_ptr.*, try entry.value_ptr.clone(alloc)); + const key = try alloc.dupe(u8, entry.key_ptr.*); + tables.putAssumeCapacity(key, try entry.value_ptr.clone(alloc)); } return .{ -- 2.51.2 From 1fbdcf1ee76406abec58b19ce8598a4000b06cb2 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 21 Dec 2025 00:15:47 +0000 Subject: [PATCH 015/349] deps: Update iTerm2 color schemes --- build.zig.zon | 2 +- build.zig.zon.json | 2 +- build.zig.zon.nix | 2 +- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 271428778..373c97aba 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -116,7 +116,7 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", + .url = "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz", .hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", .lazy = true, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index c9a64ca5f..9ad3c39da 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -51,7 +51,7 @@ }, "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz", "hash": "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 43a8efe46..9627286dd 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -166,7 +166,7 @@ in name = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz"; + url = "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz"; hash = "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk="; }; } diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 24a2978d6..484e1949b 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -5,6 +5,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918 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/ghostty-themes-release-20251201-150531-bfb3ee1.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz @@ -32,4 +33,3 @@ 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-20251201-150531-bfb3ee1/ghostty-themes.tgz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 21f79ec04..3b17b64a6 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,7 +61,7 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz", + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz", "dest": "vendor/p/N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", "sha256": "e5078f050952b33f56dabe334d2dd00fe70301ec8e6e1479d44d80909dd92149" }, -- 2.51.2 From 44972198aeeaa21e7184de4e4425114c0fdbb870 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 19:47:51 -0800 Subject: [PATCH 016/349] apprt: add action for key table activation/deactivation --- include/ghostty.h | 23 ++++++++++ src/Surface.zig | 68 +++++++++++++++++++++++------ src/apprt/action.zig | 48 ++++++++++++++++++++ src/apprt/gtk/class/application.zig | 1 + 4 files changed, 126 insertions(+), 14 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 736c7546b..48915b179 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -691,6 +691,27 @@ typedef struct { ghostty_input_trigger_s trigger; } ghostty_action_key_sequence_s; +// apprt.action.KeyTable.Tag +typedef enum { + GHOSTTY_KEY_TABLE_ACTIVATE, + GHOSTTY_KEY_TABLE_DEACTIVATE, + GHOSTTY_KEY_TABLE_DEACTIVATE_ALL, +} ghostty_action_key_table_tag_e; + +// apprt.action.KeyTable.CValue +typedef union { + struct { + const char *name; + size_t len; + } activate; +} ghostty_action_key_table_u; + +// apprt.action.KeyTable.C +typedef struct { + ghostty_action_key_table_tag_e tag; + ghostty_action_key_table_u value; +} ghostty_action_key_table_s; + // apprt.action.ColorKind typedef enum { GHOSTTY_ACTION_COLOR_KIND_FOREGROUND = -1, @@ -836,6 +857,7 @@ typedef enum { GHOSTTY_ACTION_FLOAT_WINDOW, GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, + GHOSTTY_ACTION_KEY_TABLE, GHOSTTY_ACTION_COLOR_CHANGE, GHOSTTY_ACTION_RELOAD_CONFIG, GHOSTTY_ACTION_CONFIG_CHANGE, @@ -881,6 +903,7 @@ typedef union { ghostty_action_float_window_e float_window; ghostty_action_secure_input_e secure_input; ghostty_action_key_sequence_s key_sequence; + ghostty_action_key_table_s key_table; ghostty_action_color_change_s color_change; ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; diff --git a/src/Surface.zig b/src/Surface.zig index af7cdf136..3a83c704a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5631,28 +5631,68 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .once = tag == .activate_key_table_once, }); + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .{ .activate = name }, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; + log.debug("key table activated: {s}", .{name}); }, - .deactivate_key_table => switch (self.keyboard.table_stack.items.len) { - // No key table active. This does nothing. - 0 => return false, + .deactivate_key_table => { + switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, + + // Final key table active, clear our state. + 1 => self.keyboard.table_stack.clearAndFree(self.alloc), - // Final key table active, clear our state. - 1 => self.keyboard.table_stack.clearAndFree(self.alloc), + // Restore the prior key table. We don't free any memory in + // this case because we assume it will be freed later when + // we finish our key table. + else => _ = self.keyboard.table_stack.pop(), + } - // Restore the prior key table. We don't free any memory in - // this case because we assume it will be freed later when - // we finish our key table. - else => _ = self.keyboard.table_stack.pop(), + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .deactivate, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; }, - .deactivate_all_key_tables => switch (self.keyboard.table_stack.items.len) { - // No key table active. This does nothing. - 0 => return false, + .deactivate_all_key_tables => { + switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, - // Clear the entire table stack. - else => self.keyboard.table_stack.clearAndFree(self.alloc), + // Clear the entire table stack. + else => self.keyboard.table_stack.clearAndFree(self.alloc), + } + + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .deactivate_all, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; }, .crash => |location| switch (location) { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 8e0a9d018..25fc6f08a 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -250,6 +250,9 @@ pub const Action = union(Key) { /// key mode because other input may be ignored. key_sequence: KeySequence, + /// A key table has been activated or deactivated. + key_table: KeyTable, + /// A terminal color was changed programmatically through things /// such as OSC 10/11. color_change: ColorChange, @@ -371,6 +374,7 @@ pub const Action = union(Key) { float_window, secure_input, key_sequence, + key_table, color_change, reload_config, config_change, @@ -711,6 +715,50 @@ pub const KeySequence = union(enum) { } }; +pub const KeyTable = union(enum) { + activate: []const u8, + deactivate, + deactivate_all, + + // Sync with: ghostty_action_key_table_tag_e + pub const Tag = enum(c_int) { + activate, + deactivate, + deactivate_all, + }; + + // Sync with: ghostty_action_key_table_u + pub const CValue = extern union { + activate: extern struct { + name: [*]const u8, + len: usize, + }, + }; + + // Sync with: ghostty_action_key_table_s + pub const C = extern struct { + tag: Tag, + value: CValue, + }; + + pub fn cval(self: KeyTable) C { + return switch (self) { + .activate => |name| .{ + .tag = .activate, + .value = .{ .activate = .{ .name = name.ptr, .len = name.len } }, + }, + .deactivate => .{ + .tag = .deactivate, + .value = undefined, + }, + .deactivate_all => .{ + .tag = .deactivate_all, + .value = undefined, + }, + }; + } +}; + pub const ColorChange = extern struct { kind: ColorKind, r: u8, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index be0f3f2c8..1c0863f3c 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -744,6 +744,7 @@ pub const Application = extern struct { .toggle_background_opacity, .cell_size, .key_sequence, + .key_table, .render_inspector, .renderer_health, .color_change, -- 2.51.2 From 901618cd8f944be82b47fad7efb577507d9802a7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 19:58:24 -0800 Subject: [PATCH 017/349] macOS: hook up key table apprt action to state --- macos/Sources/Ghostty/Ghostty.Action.swift | 25 +++++++++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 32 +++++++++++++++++-- macos/Sources/Ghostty/Package.swift | 4 +++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 24 ++++++++++++++ macos/Sources/Ghostty/SurfaceView_UIKit.swift | 5 ++- 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 9eb7a8e46..bde3b3d69 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -141,6 +141,31 @@ extension Ghostty.Action { } } } + + enum KeyTable { + case activate(name: String) + case deactivate + case deactivateAll + + init?(c: ghostty_action_key_table_s) { + switch c.tag { + case GHOSTTY_KEY_TABLE_ACTIVATE: + let name = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: c.value.activate.name), + length: c.value.activate.len, + encoding: .utf8, + freeWhenDone: false + ) ?? "" + self = .activate(name: name) + case GHOSTTY_KEY_TABLE_DEACTIVATE: + self = .deactivate + case GHOSTTY_KEY_TABLE_DEACTIVATE_ALL: + self = .deactivateAll + default: + return nil + } + } + } } // 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 3348ab714..4e9166168 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -578,7 +578,10 @@ extension Ghostty { case GHOSTTY_ACTION_KEY_SEQUENCE: keySequence(app, target: target, v: action.action.key_sequence) - + + case GHOSTTY_ACTION_KEY_TABLE: + keyTable(app, target: target, v: action.action.key_table) + case GHOSTTY_ACTION_PROGRESS_REPORT: progressReport(app, target: target, v: action.action.progress_report) @@ -1771,7 +1774,32 @@ extension Ghostty { assertionFailure() } } - + + private static func keyTable( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_key_table_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("key table 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 } + guard let action = Ghostty.Action.KeyTable(c: v) else { return } + + NotificationCenter.default.post( + name: Notification.didChangeKeyTable, + object: surfaceView, + userInfo: [Notification.KeyTableKey: action] + ) + + default: + assertionFailure() + } + } + private static func progressReport( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 375e5c37b..aa62c16f7 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -475,6 +475,10 @@ extension Ghostty.Notification { static let didContinueKeySequence = Notification.Name("com.mitchellh.ghostty.didContinueKeySequence") static let didEndKeySequence = Notification.Name("com.mitchellh.ghostty.didEndKeySequence") static let KeySequenceKey = didContinueKeySequence.rawValue + ".key" + + /// Notifications related to key tables + static let didChangeKeyTable = Notification.Name("com.mitchellh.ghostty.didChangeKeyTable") + static let KeyTableKey = didChangeKeyTable.rawValue + ".action" } // Make the input enum hashable. diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 88a0bb6e8..455249ff4 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -65,6 +65,9 @@ extension Ghostty { // The currently active key sequence. The sequence is not active if this is empty. @Published var keySequence: [KeyboardShortcut] = [] + // The currently active key tables. Empty if no tables are active. + @Published var keyTables: [String] = [] + // The current search state. When non-nil, the search overlay should be shown. @Published var searchState: SearchState? = nil { didSet { @@ -324,6 +327,11 @@ extension Ghostty { selector: #selector(ghosttyDidEndKeySequence), name: Ghostty.Notification.didEndKeySequence, object: self) + center.addObserver( + self, + selector: #selector(ghosttyDidChangeKeyTable), + name: Ghostty.Notification.didChangeKeyTable, + object: self) center.addObserver( self, selector: #selector(ghosttyConfigDidChange(_:)), @@ -680,6 +688,22 @@ extension Ghostty { } } + @objc private func ghosttyDidChangeKeyTable(notification: SwiftUI.Notification) { + guard let action = notification.userInfo?[Ghostty.Notification.KeyTableKey] as? Ghostty.Action.KeyTable else { return } + + DispatchQueue.main.async { [weak self] in + guard let self else { return } + switch action { + case .activate(let name): + self.keyTables.append(name) + case .deactivate: + _ = self.keyTables.popLast() + case .deactivateAll: + self.keyTables.removeAll() + } + } + } + @objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) { // Get our managed configuration object out guard let config = notification.userInfo?[ diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift index b2e429455..eb8a60fd9 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift @@ -43,7 +43,10 @@ extension Ghostty { // The current search state. When non-nil, the search overlay should be shown. @Published var searchState: SearchState? = nil - + + // The currently active key tables. Empty if no tables are active. + @Published var keyTables: [String] = [] + /// True when the surface is in readonly mode. @Published private(set) var readonly: Bool = false -- 2.51.2 From eac0ec14fdc89e7bb63831a427670a5ade094380 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 20:11:39 -0800 Subject: [PATCH 018/349] macOS: revamped key table/sequence UI --- macos/Sources/Ghostty/SurfaceView.swift | 257 +++++++++++++++++++++--- 1 file changed, 233 insertions(+), 24 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 49c6a4982..fce50073c 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -123,30 +123,12 @@ extension Ghostty { } } - // If we are in the middle of a key sequence, then we show a visual element. We only - // support this on macOS currently although in theory we can support mobile with keyboards! - if !surfaceView.keySequence.isEmpty { - let padding: CGFloat = 5 - VStack { - Spacer() - - HStack { - Text(verbatim: "Pending Key Sequence:") - ForEach(0.. dragThreshold { + position = .bottom + } + dragOffset = .zero + } + } + ) + } + .transition(.move(edge: .bottom).combined(with: .opacity)) + .animation(.spring(response: 0.3, dampingFraction: 0.8), value: keyTables) + .animation(.spring(response: 0.3, dampingFraction: 0.8), value: keySequence.count) + } + + private struct CapsuleSizeKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() + } + } + + private var indicatorContent: some View { + HStack(alignment: .center, spacing: 8) { + // Key table indicator + if !keyTables.isEmpty { + HStack(alignment: .firstTextBaseline, spacing: 5) { + Image(systemName: "keyboard.badge.ellipsis") + .font(.system(size: 13)) + .foregroundStyle(.secondary) + + // Show table stack with arrows between them + ForEach(Array(keyTables.enumerated()), id: \.offset) { index, table in + if index > 0 { + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.tertiary) + } + Text(verbatim: table) + .font(.system(size: 13, weight: .medium, design: .rounded)) + } + } + } + + // Separator when both are active + if !keyTables.isEmpty && !keySequence.isEmpty { + Divider() + .frame(height: 14) + } + + // Key sequence indicator + if !keySequence.isEmpty { + HStack(alignment: .center, spacing: 4) { + ForEach(Array(keySequence.enumerated()), id: \.offset) { index, key in + KeyCap(key.description) + } + + // Animated ellipsis to indicate waiting for next key + PendingIndicator() + } + } + } + .frame(height: 18) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background { + Capsule() + .fill(.regularMaterial) + .overlay { + Capsule() + .strokeBorder(Color.primary.opacity(0.15), lineWidth: 1) + } + .shadow(color: .black.opacity(0.2), radius: 8, y: 2) + } + .contentShape(Capsule()) + .backport.pointerStyle(.link) + .popover(isPresented: $isShowingPopover, arrowEdge: position.popoverEdge) { + VStack(alignment: .leading, spacing: 8) { + if !keyTables.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Label("Key Table", systemImage: "keyboard.badge.ellipsis") + .font(.headline) + Text("A key table is a named set of keybindings, activated by some other key. Keys are interpreted using this table until it is deactivated.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + + if !keyTables.isEmpty && !keySequence.isEmpty { + Divider() + } + + if !keySequence.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Label("Key Sequence", systemImage: "character.cursor.ibeam") + .font(.headline) + Text("A key sequence is a series of key presses that trigger an action. A pending key sequence is currently active.") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + } + .padding() + .frame(maxWidth: 400) + .fixedSize(horizontal: false, vertical: true) + } + .onTapGesture { + isShowingPopover.toggle() + } + } + + /// A small keycap-style view for displaying keyboard shortcuts + struct KeyCap: View { + let text: String + + init(_ text: String) { + self.text = text + } + + var body: some View { + Text(verbatim: text) + .font(.system(size: 12, weight: .medium, design: .rounded)) + .padding(.horizontal, 5) + .padding(.vertical, 2) + .background( + RoundedRectangle(cornerRadius: 4) + .fill(Color(NSColor.controlBackgroundColor)) + .shadow(color: .black.opacity(0.12), radius: 0.5, y: 0.5) + ) + .overlay( + RoundedRectangle(cornerRadius: 4) + .strokeBorder(Color.primary.opacity(0.15), lineWidth: 0.5) + ) + } + } + + /// Animated dots to indicate waiting for the next key + struct PendingIndicator: View { + @State private var animationPhase: Int = 0 + + var body: some View { + HStack(spacing: 2) { + ForEach(0..<3, id: \.self) { index in + Circle() + .fill(Color.secondary) + .frame(width: 4, height: 4) + .opacity(dotOpacity(for: index)) + } + } + .onAppear { + withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: false)) { + animationPhase = 3 + } + } + } + + private func dotOpacity(for index: Int) -> Double { + let phase = Double(animationPhase) + let offset = Double(index) / 3.0 + let wave = sin((phase + offset) * .pi * 2) + return 0.3 + 0.7 * ((wave + 1) / 2) + } + } + } +#endif + /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool -- 2.51.2 From dc8f08239235d85be875e03fc67148e8eaab758e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Dec 2025 20:36:35 -0800 Subject: [PATCH 019/349] macos: copy the key table action bytes --- macos/Sources/Ghostty/Ghostty.Action.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index bde3b3d69..91f1491dd 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -150,12 +150,8 @@ extension Ghostty.Action { init?(c: ghostty_action_key_table_s) { switch c.tag { case GHOSTTY_KEY_TABLE_ACTIVATE: - let name = String( - bytesNoCopy: UnsafeMutableRawPointer(mutating: c.value.activate.name), - length: c.value.activate.len, - encoding: .utf8, - freeWhenDone: false - ) ?? "" + let data = Data(bytes: c.value.activate.name, count: c.value.activate.len) + let name = String(data: data, encoding: .utf8) ?? "" self = .activate(name: name) case GHOSTTY_KEY_TABLE_DEACTIVATE: self = .deactivate -- 2.51.2 From 7d3db17396eeda31ec025cec314a42889fa115d4 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Sun, 21 Dec 2025 09:17:01 +0100 Subject: [PATCH 020/349] macOS: key table animations and cleanup --- macos/Sources/Ghostty/SurfaceView.swift | 158 +++++++++++------------- 1 file changed, 74 insertions(+), 84 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index fce50073c..cf4bd37f6 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -124,12 +124,10 @@ extension Ghostty { } // Show key state indicator for active key tables and/or pending key sequences - if !surfaceView.keyTables.isEmpty || !surfaceView.keySequence.isEmpty { - KeyStateIndicator( - keyTables: surfaceView.keyTables, - keySequence: surfaceView.keySequence - ) - } + KeyStateIndicator( + keyTables: surfaceView.keyTables, + keySequence: surfaceView.keySequence + ) #endif // If we have a URL from hovering a link, we show that. @@ -745,7 +743,6 @@ extension Ghostty { @State private var position: Position = .bottom @State private var dragOffset: CGSize = .zero @State private var isDragging = false - @State private var capsuleSize: CGSize = .zero private let padding: CGFloat = 8 @@ -765,82 +762,75 @@ extension Ghostty { case .bottom: return .bottom } } + + var transitionEdge: Edge { + popoverEdge + } } - + var body: some View { - GeometryReader { geo in - indicatorContent - .background( - GeometryReader { capsuleGeo in - Color.clear.preference( - key: CapsuleSizeKey.self, - value: capsuleGeo.size - ) - } - ) - .offset(dragOffset) - .padding(padding) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: position.alignment) - .onPreferenceChange(CapsuleSizeKey.self) { size in - capsuleSize = size - } - .highPriorityGesture( - DragGesture(coordinateSpace: .local) - .onChanged { value in - isDragging = true - dragOffset = CGSize(width: 0, height: value.translation.height) - } - .onEnded { value in - isDragging = false - let dragThreshold: CGFloat = 50 - - withAnimation(.easeOut(duration: 0.2)) { - if position == .bottom && value.translation.height < -dragThreshold { - position = .top - } else if position == .top && value.translation.height > dragThreshold { - position = .bottom - } - dragOffset = .zero - } - } - ) + Group { + if !keyTables.isEmpty { + content + // Reset pointer style incase the mouse didn't move away + .backport.pointerStyle(keyTables.isEmpty ? nil : .link) + } } - .transition(.move(edge: .bottom).combined(with: .opacity)) + .transition(.move(edge: position.transitionEdge).combined(with: .opacity)) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: keyTables) .animation(.spring(response: 0.3, dampingFraction: 0.8), value: keySequence.count) } - - private struct CapsuleSizeKey: PreferenceKey { - static var defaultValue: CGSize = .zero - static func reduce(value: inout CGSize, nextValue: () -> CGSize) { - value = nextValue() - } + + var content: some View { + indicatorContent + .offset(dragOffset) + .padding(padding) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: position.alignment) + .highPriorityGesture( + DragGesture(coordinateSpace: .local) + .onChanged { value in + isDragging = true + dragOffset = CGSize(width: 0, height: value.translation.height) + } + .onEnded { value in + isDragging = false + let dragThreshold: CGFloat = 50 + + withAnimation(.easeOut(duration: 0.2)) { + if position == .bottom && value.translation.height < -dragThreshold { + position = .top + } else if position == .top && value.translation.height > dragThreshold { + position = .bottom + } + dragOffset = .zero + } + } + ) } - + + @ViewBuilder private var indicatorContent: some View { HStack(alignment: .center, spacing: 8) { // Key table indicator - if !keyTables.isEmpty { - HStack(alignment: .firstTextBaseline, spacing: 5) { - Image(systemName: "keyboard.badge.ellipsis") - .font(.system(size: 13)) - .foregroundStyle(.secondary) - - // Show table stack with arrows between them - ForEach(Array(keyTables.enumerated()), id: \.offset) { index, table in - if index > 0 { - Image(systemName: "chevron.right") - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(.tertiary) - } - Text(verbatim: table) - .font(.system(size: 13, weight: .medium, design: .rounded)) + HStack(spacing: 5) { + Image(systemName: "keyboard.badge.ellipsis") + .font(.system(size: 13)) + .foregroundStyle(.secondary) + + // Show table stack with arrows between them + ForEach(Array(keyTables.enumerated()), id: \.offset) { index, table in + if index > 0 { + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.tertiary) } + Text(verbatim: table) + .font(.system(size: 13, weight: .medium, design: .rounded)) } } - + // Separator when both are active - if !keyTables.isEmpty && !keySequence.isEmpty { + if !keySequence.isEmpty { Divider() .frame(height: 14) } @@ -853,11 +843,10 @@ extension Ghostty { } // Animated ellipsis to indicate waiting for next key - PendingIndicator() + PendingIndicator(paused: isDragging) } } } - .frame(height: 18) .padding(.horizontal, 12) .padding(.vertical, 6) .background { @@ -933,26 +922,27 @@ extension Ghostty { /// Animated dots to indicate waiting for the next key struct PendingIndicator: View { - @State private var animationPhase: Int = 0 - + @State private var animationPhase: Double = 0 + let paused: Bool + var body: some View { - HStack(spacing: 2) { - ForEach(0..<3, id: \.self) { index in - Circle() - .fill(Color.secondary) - .frame(width: 4, height: 4) - .opacity(dotOpacity(for: index)) + TimelineView(.animation(paused: paused)) { context in + HStack(spacing: 2) { + ForEach(0..<3, id: \.self) { index in + Circle() + .fill(Color.secondary) + .frame(width: 4, height: 4) + .opacity(dotOpacity(for: index)) + } } - } - .onAppear { - withAnimation(.easeInOut(duration: 0.4).repeatForever(autoreverses: false)) { - animationPhase = 3 + .onChange(of: context.date.timeIntervalSinceReferenceDate) { newValue in + animationPhase = newValue } } } private func dotOpacity(for index: Int) -> Double { - let phase = Double(animationPhase) + let phase = animationPhase let offset = Double(index) / 3.0 let wave = sin((phase + offset) * .pi * 2) return 0.3 + 0.7 * ((wave + 1) / 2) -- 2.51.2 From 18c8c338e0a19b17f505ae37736eac481bb1922a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Dec 2025 07:30:58 -0800 Subject: [PATCH 021/349] Reset key tables on config reload, bound max active key tables Two unrelated changes to polish key tables: 1. Key tables should be reset (deactivated) when teh config is reloaded. This matches the behavior of key sequences as well, which are reset on config reload. 2. A maximum number of active key tables is now enforced (8). This prevents a misbehaving config from consuming too much memory by activating too many key tables. This is an arbitrary limit we can adjust later if needed. --- src/Surface.zig | 67 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3a83c704a..2784f93db 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -49,6 +49,10 @@ const Renderer = rendererpkg.Renderer; const min_window_width_cells: u32 = 10; const min_window_height_cells: u32 = 4; +/// The maximum number of key tables that can be active at any +/// given time. `activate_key_table` calls after this are ignored. +const max_active_key_tables = 8; + /// Allocator alloc: Allocator, @@ -267,6 +271,8 @@ pub const Keyboard = struct { /// The stack of tables that is currently active. The first value /// in this is the first activated table (NOT the default keybinding set). + /// + /// This is bounded by `max_active_key_tables`. table_stack: std.ArrayListUnmanaged(struct { set: *const input.Binding.Set, once: bool, @@ -1737,6 +1743,14 @@ pub fn updateConfig( // If we are in the middle of a key sequence, clear it. self.endKeySequence(.drop, .free); + // Deactivate all key tables since they may have changed. Importantly, + // we store pointers into the config as part of our table stack so + // we can't keep them active across config changes. But this behavior + // also matches key sequences. + _ = self.deactivateAllKeyTables() catch |err| { + log.warn("failed to deactivate key tables err={}", .{err}); + }; + // Before sending any other config changes, we give the renderer a new font // grid. We could check to see if there was an actual change to the font, // but this is easier and pretty rare so it's not a performance concern. @@ -2967,6 +2981,30 @@ fn maybeHandleBinding( return null; } +fn deactivateAllKeyTables(self: *Surface) !bool { + switch (self.keyboard.table_stack.items.len) { + // No key table active. This does nothing. + 0 => return false, + + // Clear the entire table stack. + else => self.keyboard.table_stack.clearAndFree(self.alloc), + } + + // Notify the UI. + _ = self.rt_app.performAction( + .{ .surface = self }, + .key_table, + .deactivate_all, + ) catch |err| { + log.warn( + "failed to notify app of key table err={}", + .{err}, + ); + }; + + return true; +} + const KeySequenceQueued = enum { flush, drop }; const KeySequenceMemory = enum { retain, free }; @@ -5625,6 +5663,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool } } + // If we're already at the max, ignore it. + if (self.keyboard.table_stack.items.len >= max_active_key_tables) { + log.info( + "ignoring activate table, max depth reached: {s}", + .{name}, + ); + return false; + } + // Add the table to the stack. try self.keyboard.table_stack.append(self.alloc, .{ .set = set, @@ -5674,25 +5721,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, .deactivate_all_key_tables => { - switch (self.keyboard.table_stack.items.len) { - // No key table active. This does nothing. - 0 => return false, - - // Clear the entire table stack. - else => self.keyboard.table_stack.clearAndFree(self.alloc), - } - - // Notify the UI. - _ = self.rt_app.performAction( - .{ .surface = self }, - .key_table, - .deactivate_all, - ) catch |err| { - log.warn( - "failed to notify app of key table err={}", - .{err}, - ); - }; + return try self.deactivateAllKeyTables(); }, .crash => |location| switch (location) { -- 2.51.2 From 9ce04b81b74d069e719a1d982277e3b6ed78a929 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 21 Dec 2025 12:17:20 -0500 Subject: [PATCH 022/349] shell-integration: ensure shell resources exist Our automatic shell integrations require certain resource paths to exist. If they're missing, the launched shell could end up in an inconsistent and unexpected state. For example, we temporarily set ZDOTDIR to our zsh shell integration directory and then restore it from our .zshenv file, but if that script isn't available, the user's shell environment will be broken. The actual runtime logic change was simple: each shell integration routine attempts to open its expected resource path and skips automatic shell integration upon failure. The more complex change was reworking our unit tests to run in a temporary resources directory structure. --- src/termio/shell_integration.zig | 205 ++++++++++++++++++++++++------- 1 file changed, 164 insertions(+), 41 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 71492230e..fc2d4827a 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -114,7 +114,7 @@ fn setupShell( } if (std.mem.eql(u8, "elvish", exe)) { - try setupXdgDataDirs(alloc_arena, resource_dir, env); + if (!try setupXdgDataDirs(alloc_arena, resource_dir, env)) return null; return .{ .shell = .elvish, .command = try command.clone(alloc_arena), @@ -122,7 +122,7 @@ fn setupShell( } if (std.mem.eql(u8, "fish", exe)) { - try setupXdgDataDirs(alloc_arena, resource_dir, env); + if (!try setupXdgDataDirs(alloc_arena, resource_dir, env)) return null; return .{ .shell = .fish, .command = try command.clone(alloc_arena), @@ -130,7 +130,7 @@ fn setupShell( } if (std.mem.eql(u8, "zsh", exe)) { - try setupZsh(resource_dir, env); + if (!try setupZsh(resource_dir, env)) return null; return .{ .shell = .zsh, .command = try command.clone(alloc_arena), @@ -152,9 +152,13 @@ test "force shell" { inline for (@typeInfo(Shell).@"enum".fields) |field| { const shell = @field(Shell, field.name); + + var res: TmpResourcesDir = try .init(alloc, shell); + defer res.deinit(); + const result = try setup( alloc, - ".", + res.path, .{ .shell = "sh" }, &env, shell, @@ -345,13 +349,18 @@ fn setupBash( } // Set our new ENV to point to our integration script. - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const integ_dir = try std.fmt.bufPrint( - &path_buf, + var script_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const script_path = try std.fmt.bufPrint( + &script_path_buf, "{s}/shell-integration/bash/ghostty.bash", .{resource_dir}, ); - try env.put("ENV", integ_dir); + if (std.fs.openFileAbsolute(script_path, .{})) |file| { + file.close(); + try env.put("ENV", script_path); + } else |_| { + return null; + } // Get the command string from the builder, then copy it to the arena // allocator. The stackFallback allocator's memory becomes invalid after @@ -366,14 +375,21 @@ test "bash" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - + const command = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expectEqualStrings("bash --posix", command.?.shell); - try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/ghostty.bash", .{res.shell_path}), + env.get("ENV").?, + ); } test "bash: unsupported options" { @@ -382,6 +398,9 @@ test "bash: unsupported options" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + const cmdlines = [_][:0]const u8{ "bash --posix", "bash --rcfile script.sh --posix", @@ -394,7 +413,7 @@ test "bash: unsupported options" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, ".", &env) == null); + try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, res.path, &env) == null); try testing.expect(env.get("GHOSTTY_BASH_INJECT") == null); try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); @@ -407,13 +426,15 @@ test "bash: inject flags" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + // bash --norc { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env); - + const command = try setupBash(alloc, .{ .shell = "bash --norc" }, res.path, &env); try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?); } @@ -423,8 +444,7 @@ test "bash: inject flags" { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env); - + const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, res.path, &env); try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?); } @@ -436,19 +456,22 @@ test "bash: rcfile" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); // bash --rcfile { - const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env); + const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, res.path, &env); try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } // bash --init-file { - const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env); + const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, res.path, &env); try testing.expectEqualStrings("bash --posix", command.?.shell); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } @@ -460,12 +483,15 @@ test "bash: HISTFILE" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + // HISTFILE unset { var env = EnvMap.init(alloc); defer env.deinit(); - _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); + _ = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expect(std.mem.endsWith(u8, env.get("HISTFILE").?, ".bash_history")); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE").?); } @@ -477,7 +503,7 @@ test "bash: HISTFILE" { try env.put("HISTFILE", "my_history"); - _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); + _ = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expectEqualStrings("my_history", env.get("HISTFILE").?); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); } @@ -489,14 +515,22 @@ test "bash: ENV" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); try env.put("ENV", "env.sh"); - _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); + _ = try setupBash(alloc, .{ .shell = "bash" }, res.path, &env); try testing.expectEqualStrings("env.sh", env.get("GHOSTTY_BASH_ENV").?); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/ghostty.bash", .{res.shell_path}), + env.get("ENV").?, + ); } test "bash: additional arguments" { @@ -505,18 +539,21 @@ test "bash: additional arguments" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .bash); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); // "-" argument separator { - const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env); + const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, res.path, &env); try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?.shell); } // "--" argument separator { - const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env); + const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, res.path, &env); try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?.shell); } } @@ -532,20 +569,22 @@ fn setupXdgDataDirs( alloc_arena: Allocator, resource_dir: []const u8, env: *EnvMap, -) !void { +) !bool { var path_buf: [std.fs.max_path_bytes]u8 = undefined; // Get our path to the shell integration directory. - const integ_dir = try std.fmt.bufPrint( + const integ_path = try std.fmt.bufPrint( &path_buf, "{s}/shell-integration", .{resource_dir}, ); + var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch return false; + integ_dir.close(); // Set an env var so we can remove this from XDG_DATA_DIRS later. // This happens in the shell integration config itself. We do this // so that our modifications don't interfere with other commands. - try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir); + try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_path); // We attempt to avoid allocating by using the stack up to 4K. // Max stack size is considerably larger on mac @@ -565,9 +604,11 @@ fn setupXdgDataDirs( try internal_os.prependEnv( stack_alloc, env.get(xdg_data_dirs_key) orelse "/usr/local/share:/usr/share", - integ_dir, + integ_path, ), ); + + return true; } test "xdg: empty XDG_DATA_DIRS" { @@ -577,13 +618,23 @@ test "xdg: empty XDG_DATA_DIRS" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .fish); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); - try setupXdgDataDirs(alloc, ".", &env); + try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); - try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?); - try testing.expectEqualStrings("./shell-integration:/usr/local/share:/usr/share", env.get("XDG_DATA_DIRS").?); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?, + ); + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration:/usr/local/share:/usr/share", .{res.path}), + env.get("XDG_DATA_DIRS").?, + ); } test "xdg: existing XDG_DATA_DIRS" { @@ -593,14 +644,24 @@ test "xdg: existing XDG_DATA_DIRS" { defer arena.deinit(); const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(alloc, .fish); + defer res.deinit(); + var env = EnvMap.init(alloc); defer env.deinit(); try env.put("XDG_DATA_DIRS", "/opt/share"); - try setupXdgDataDirs(alloc, ".", &env); + try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); - try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?); - try testing.expectEqualStrings("./shell-integration:/opt/share", env.get("XDG_DATA_DIRS").?); + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?, + ); + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration:/opt/share", .{res.path}), + env.get("XDG_DATA_DIRS").?, + ); } /// Setup the zsh automatic shell integration. This works by setting @@ -609,7 +670,7 @@ test "xdg: existing XDG_DATA_DIRS" { fn setupZsh( resource_dir: []const u8, env: *EnvMap, -) !void { +) !bool { // Preserve an existing ZDOTDIR value. We're about to overwrite it. if (env.get("ZDOTDIR")) |old| { try env.put("GHOSTTY_ZSH_ZDOTDIR", old); @@ -617,34 +678,96 @@ fn setupZsh( // Set our new ZDOTDIR to point to our shell resource directory. var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const integ_dir = try std.fmt.bufPrint( + const integ_path = try std.fmt.bufPrint( &path_buf, "{s}/shell-integration/zsh", .{resource_dir}, ); - try env.put("ZDOTDIR", integ_dir); + var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch return false; + integ_dir.close(); + try env.put("ZDOTDIR", integ_path); + + return true; } test "zsh" { const testing = std.testing; + var res: TmpResourcesDir = try .init(testing.allocator, .zsh); + defer res.deinit(); + var env = EnvMap.init(testing.allocator); defer env.deinit(); - try setupZsh(".", &env); - try testing.expectEqualStrings("./shell-integration/zsh", env.get("ZDOTDIR").?); + try testing.expect(try setupZsh(res.path, &env)); + try testing.expectEqualStrings(res.shell_path, env.get("ZDOTDIR").?); try testing.expect(env.get("GHOSTTY_ZSH_ZDOTDIR") == null); } test "zsh: ZDOTDIR" { const testing = std.testing; + var res: TmpResourcesDir = try .init(testing.allocator, .zsh); + defer res.deinit(); + var env = EnvMap.init(testing.allocator); defer env.deinit(); try env.put("ZDOTDIR", "$HOME/.config/zsh"); - try setupZsh(".", &env); - try testing.expectEqualStrings("./shell-integration/zsh", env.get("ZDOTDIR").?); + try testing.expect(try setupZsh(res.path, &env)); + try testing.expectEqualStrings(res.shell_path, env.get("ZDOTDIR").?); try testing.expectEqualStrings("$HOME/.config/zsh", env.get("GHOSTTY_ZSH_ZDOTDIR").?); } + +/// Test helper that creates a temporary resources directory with shell integration paths. +const TmpResourcesDir = struct { + allocator: Allocator, + tmp_dir: std.testing.TmpDir, + path: []const u8, + shell_path: []const u8, + + fn init(allocator: std.mem.Allocator, shell: Shell) !TmpResourcesDir { + var tmp_dir = std.testing.tmpDir(.{}); + errdefer tmp_dir.cleanup(); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const relative_shell_path = try std.fmt.bufPrint( + &path_buf, + "shell-integration/{s}", + .{@tagName(shell)}, + ); + try tmp_dir.dir.makePath(relative_shell_path); + + const path = try tmp_dir.dir.realpathAlloc(allocator, "."); + errdefer allocator.free(path); + + const shell_path = try std.fmt.allocPrint( + allocator, + "{s}/{s}", + .{ path, relative_shell_path }, + ); + errdefer allocator.free(shell_path); + + switch (shell) { + .bash => try tmp_dir.dir.writeFile(.{ + .sub_path = "shell-integration/bash/ghostty.bash", + .data = "", + }), + else => {}, + } + + return .{ + .allocator = allocator, + .tmp_dir = tmp_dir, + .path = path, + .shell_path = shell_path, + }; + } + + fn deinit(self: *TmpResourcesDir) void { + self.allocator.free(self.shell_path); + self.allocator.free(self.path); + self.tmp_dir.cleanup(); + } +}; -- 2.51.2 From 97cd4c71d50d3adc5690ee2645af0e08aa5f4b3c Mon Sep 17 00:00:00 2001 From: Henrique Albuquerque <18596542+henrialb@users.noreply.github.com> Date: Sun, 21 Dec 2025 17:57:23 +0000 Subject: [PATCH 023/349] Fix typo --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index c0d8e813e..15a1877ff 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3271,7 +3271,7 @@ else /// more subtle border. @"gtk-toolbar-style": GtkToolbarStyle = .raised, -/// The style of the GTK titlbar. Available values are `native` and `tabs`. +/// The style of the GTK titlebar. Available values are `native` and `tabs`. /// /// The `native` titlebar style is a traditional titlebar with a title, a few /// buttons and window controls. A separate tab bar will show up below the -- 2.51.2 From 8a8b06e74dc52cb440df83931004b03b250e1e62 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Dec 2025 13:28:12 -0800 Subject: [PATCH 024/349] config: document key tables for `keybind` --- src/config/Config.zig | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 15a1877ff..8f1cece45 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1666,6 +1666,50 @@ class: ?[:0]const u8 = null, /// /// - Notably, global shortcuts have not been implemented on wlroots-based /// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). +/// +/// You may also create a named set of keybindings known as a "key table." +/// A key table must be explicitly activated for the bindings to become +/// available. This can be used to implement features such as a +/// "copy mode", "vim mode", etc. Generically, this can implement modal +/// keyboard input. +/// +/// Key tables are defined using the syntax `/`. The +/// `` value is everything documented above for keybinds. The +/// `
` value is the name of the key table. Table names can contain +/// anything except `/` and `=`. For example `foo/ctrl+a=new_window` +/// defines a binding within a table named `foo`. +/// +/// Tables are activated and deactivated using the binding actions +/// `activate_key_table:` and `deactivate_key_table`. Other table +/// related binding actions also exist; see the documentation for a full list. +/// These are the primary way to interact with key tables. +/// +/// Binding lookup proceeds from the innermost table outward, so keybinds in +/// the default table remain available unless explicitly unbound in an inner +/// table. +/// +/// A key table has some special syntax and handling: +/// +/// * `/` (with no binding) defines and clears a table, resetting all +/// of its keybinds and settings. +/// +/// * You cannot activate a table that is already the innermost table; such +/// attempts are ignored. However, the same table can appear multiple times +/// in the stack as long as it is not innermost (e.g., `A -> B -> A -> B` +/// is valid, but `A -> B -> B` is not). +/// +/// * A table can be activated in one-shot mode using +/// `activate_key_table_once:`. A one-shot table is automatically +/// deactivated when any non-catch-all binding is invoked. +/// +/// * Key sequences work within tables: `foo/ctrl+a>ctrl+b=new_window`. +/// If an invalid key is pressed, the sequence ends but the table remains +/// active. +/// +/// * Prefixes like `global:` work within tables: +/// `foo/global:ctrl+a=new_window`. +/// +/// Key tables are available since Ghostty 1.3.0. keybind: Keybinds = .{}, /// Horizontal window padding. This applies padding between the terminal cells -- 2.51.2 From 39481453fe8b958a94ec4aeae1a4f885954c6386 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Dec 2025 13:32:23 -0800 Subject: [PATCH 025/349] macos: show the key sequence overlay if no tables are active --- macos/Sources/Ghostty/SurfaceView.swift | 35 +++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index cf4bd37f6..2d5039d29 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -770,10 +770,9 @@ extension Ghostty { var body: some View { Group { - if !keyTables.isEmpty { + if !keyTables.isEmpty || !keySequence.isEmpty { content - // Reset pointer style incase the mouse didn't move away - .backport.pointerStyle(keyTables.isEmpty ? nil : .link) + .backport.pointerStyle(!keyTables.isEmpty ? .link : nil) } } .transition(.move(edge: position.transitionEdge).combined(with: .opacity)) @@ -812,25 +811,27 @@ extension Ghostty { private var indicatorContent: some View { HStack(alignment: .center, spacing: 8) { // Key table indicator - HStack(spacing: 5) { - Image(systemName: "keyboard.badge.ellipsis") - .font(.system(size: 13)) - .foregroundStyle(.secondary) - - // Show table stack with arrows between them - ForEach(Array(keyTables.enumerated()), id: \.offset) { index, table in - if index > 0 { - Image(systemName: "chevron.right") - .font(.system(size: 10, weight: .semibold)) - .foregroundStyle(.tertiary) + if !keyTables.isEmpty { + HStack(spacing: 5) { + Image(systemName: "keyboard.badge.ellipsis") + .font(.system(size: 13)) + .foregroundStyle(.secondary) + + // Show table stack with arrows between them + ForEach(Array(keyTables.enumerated()), id: \.offset) { index, table in + if index > 0 { + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.tertiary) + } + Text(verbatim: table) + .font(.system(size: 13, weight: .medium, design: .rounded)) } - Text(verbatim: table) - .font(.system(size: 13, weight: .medium, design: .rounded)) } } // Separator when both are active - if !keySequence.isEmpty { + if !keyTables.isEmpty && !keySequence.isEmpty { Divider() .frame(height: 14) } -- 2.51.2 From 73fd007a836f1f7423606c94d2c9c935aa3c81f5 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 21 Dec 2025 16:44:43 -0500 Subject: [PATCH 026/349] shell-integration: log warnings for missing paths --- src/termio/shell_integration.zig | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index fc2d4827a..3a541dcae 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -358,7 +358,8 @@ fn setupBash( if (std.fs.openFileAbsolute(script_path, .{})) |file| { file.close(); try env.put("ENV", script_path); - } else |_| { + } else |err| { + log.warn("unable to open {s}: {}", .{ script_path, err }); return null; } @@ -578,7 +579,10 @@ fn setupXdgDataDirs( "{s}/shell-integration", .{resource_dir}, ); - var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch return false; + var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { + log.warn("unable to open {s}: {}", .{ integ_path, err }); + return false; + }; integ_dir.close(); // Set an env var so we can remove this from XDG_DATA_DIRS later. @@ -683,7 +687,10 @@ fn setupZsh( "{s}/shell-integration/zsh", .{resource_dir}, ); - var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch return false; + var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { + log.warn("unable to open {s}: {}", .{ integ_path, err }); + return false; + }; integ_dir.close(); try env.put("ZDOTDIR", integ_path); -- 2.51.2 From d0767a089aa7d81eec523fa3ab1233f401e9d0e4 Mon Sep 17 00:00:00 2001 From: -k Date: Sun, 21 Dec 2025 17:11:34 -0500 Subject: [PATCH 027/349] build: fix `simdutf`/`highway` flags --- pkg/highway/build.zig | 5 +++++ pkg/simdutf/build.zig | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index fd93675e6..04fe70853 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -72,6 +72,11 @@ pub fn build(b: *std.Build) !void { "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined", }); + + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + if (target.result.os.tag != .windows) { try flags.appendSlice(b.allocator, &.{ "-fmath-errno", diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index 0d827c1cc..2b157d1a9 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -32,6 +32,10 @@ pub fn build(b: *std.Build) !void { "-fno-sanitize-trap=undefined", }); + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + lib.addCSourceFiles(.{ .flags = flags.items, .files = &.{ -- 2.51.2 From ab352b5af9694a7cba8e237d0b1b5a507a6e4226 Mon Sep 17 00:00:00 2001 From: Yasu Flores Date: Sun, 21 Dec 2025 20:26:57 -0600 Subject: [PATCH 028/349] macos: Support native actions to move to beginning of document and move to end of document --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 455249ff4..817fca191 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -9,7 +9,7 @@ extension Ghostty { /// The NSView implementation for a terminal surface. class SurfaceView: OSView, ObservableObject, Codable, Identifiable { typealias ID = UUID - + /// Unique ID per surface let id: UUID @@ -44,14 +44,14 @@ extension Ghostty { // The hovered URL string @Published var hoverUrl: String? = nil - + // The progress report (if any) @Published var progressReport: Action.ProgressReport? = nil { didSet { // Cancel any existing timer progressReportTimer?.invalidate() progressReportTimer = nil - + // If we have a new progress report, start a timer to remove it after 15 seconds if progressReport != nil { progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in @@ -101,7 +101,7 @@ extension Ghostty { } } } - + // Cancellable for search state needle changes private var searchNeedleCancellable: AnyCancellable? @@ -219,7 +219,7 @@ extension Ghostty { // A timer to fallback to ghost emoji if no title is set within the grace period private var titleFallbackTimer: Timer? - + // Timer to remove progress report after 15 seconds private var progressReportTimer: Timer? @@ -418,7 +418,7 @@ extension Ghostty { // Remove any notifications associated with this surface let identifiers = Array(self.notificationIdentifiers) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) - + // Cancel progress report timer progressReportTimer?.invalidate() } @@ -555,16 +555,16 @@ extension Ghostty { // Add buttons alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Cancel") - + // Make the text field the first responder so it gets focus alert.window.initialFirstResponder = textField - + let completionHandler: (NSApplication.ModalResponse) -> Void = { [weak self] response in guard let self else { return } - + // Check if the user clicked "OK" guard response == .alertFirstButtonReturn else { return } - + // Get the input text let newTitle = textField.stringValue if newTitle.isEmpty { @@ -988,7 +988,7 @@ extension Ghostty { var x = event.scrollingDeltaX var y = event.scrollingDeltaY let precision = event.hasPreciseScrollingDeltas - + if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. x *= 2; @@ -1350,7 +1350,7 @@ extension Ghostty { var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing - + // For text, we only encode UTF8 if we don't have a single control // character. Control characters are encoded by Ghostty itself. // Without this, `ctrl+enter` does the wrong thing. @@ -1509,7 +1509,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func find(_ sender: Any?) { guard let surface = self.surface else { return } let action = "start_search" @@ -1517,7 +1517,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" @@ -1533,7 +1533,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func findHide(_ sender: Any?) { guard let surface = self.surface else { return } let action = "end_search" @@ -1593,7 +1593,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func changeTitle(_ sender: Any) { promptTitle() } @@ -1703,7 +1703,7 @@ extension Ghostty { let isUserSetTitle = try container.decodeIfPresent(Bool.self, forKey: .isUserSetTitle) ?? false self.init(app, baseConfig: config, uuid: uuid) - + // Restore the saved title after initialization if let title = savedTitle { self.title = title @@ -1920,6 +1920,16 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } + // Process MacOS native scroll events + switch selector { + case #selector(moveToBeginningOfDocument(_:)): + surfaceModel!.perform(action: "scroll_to_top") + case #selector(moveToEndOfDocument(_:)): + surfaceModel!.perform(action: "scroll_to_bottom") + default: + break + } + print("SEL: \(selector)") } @@ -1960,14 +1970,14 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { // The "COMBINATION" bit is key: we might get sent a string (we can handle that) // but get requested an image (we can't handle that at the time of writing this), // so we must bubble up. - + // Types we can receive let receivable: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")] - + // Types that we can send. Currently the same as receivable but I'm separating // this out so we can modify this in the future. let sendable: [NSPasteboard.PasteboardType] = receivable - + // The sendable types that require a selection (currently all) let sendableRequiresSelection = sendable @@ -1984,7 +1994,7 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { return super.validRequestor(forSendType: sendType, returnType: returnType) } } - + return self } @@ -2030,7 +2040,7 @@ extension Ghostty.SurfaceView: NSMenuItemValidation { let pb = NSPasteboard.ghosttySelection guard let str = pb.getOpinionatedStringContents() else { return false } return !str.isEmpty - + case #selector(findHide): return searchState != nil @@ -2135,7 +2145,7 @@ extension Ghostty.SurfaceView { override func accessibilitySelectedTextRange() -> NSRange { return selectedRange() } - + /// Returns the currently selected text as a string. /// This allows assistive technologies to read the selected content. override func accessibilitySelectedText() -> String? { @@ -2149,21 +2159,21 @@ extension Ghostty.SurfaceView { let str = String(cString: text.text) return str.isEmpty ? nil : str } - + /// Returns the number of characters in the terminal content. /// This helps assistive technologies understand the size of the content. override func accessibilityNumberOfCharacters() -> Int { let content = cachedScreenContents.get() return content.count } - + /// Returns the visible character range for the terminal. /// For terminals, we typically show all content as visible. override func accessibilityVisibleCharacterRange() -> NSRange { let content = cachedScreenContents.get() return NSRange(location: 0, length: content.count) } - + /// Returns the line number for a given character index. /// This helps assistive technologies navigate by line. override func accessibilityLine(for index: Int) -> Int { @@ -2171,7 +2181,7 @@ extension Ghostty.SurfaceView { let substring = String(content.prefix(index)) return substring.components(separatedBy: .newlines).count - 1 } - + /// Returns a substring for the given range. /// This allows assistive technologies to read specific portions of the content. override func accessibilityString(for range: NSRange) -> String? { @@ -2179,7 +2189,7 @@ extension Ghostty.SurfaceView { guard let swiftRange = Range(range, in: content) else { return nil } return String(content[swiftRange]) } - + /// Returns an attributed string for the given range. /// /// Note: right now this only applies font information. One day it'd be nice to extend @@ -2190,9 +2200,9 @@ extension Ghostty.SurfaceView { override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? { guard let surface = self.surface else { return nil } guard let plainString = accessibilityString(for: range) else { return nil } - + var attributes: [NSAttributedString.Key: Any] = [:] - + // Try to get the font from the surface if let fontRaw = ghostty_surface_quicklook_font(surface) { let font = Unmanaged.fromOpaque(fontRaw) -- 2.51.2 From 2215b731da23013324b242fbb28f00b230441770 Mon Sep 17 00:00:00 2001 From: Yasu Flores Date: Sun, 21 Dec 2025 20:47:56 -0600 Subject: [PATCH 029/349] Address warning and add guard clause --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 817fca191..c54f674a5 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1910,6 +1910,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { /// 1. Prevents an audible NSBeep for unimplemented actions. /// 2. Allows us to properly encode super+key input events that we don't handle override func doCommand(by selector: Selector) { + guard let surfaceModel else { return } // If we are being processed by performKeyEquivalent with a command binding, // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, @@ -1923,9 +1924,9 @@ extension Ghostty.SurfaceView: NSTextInputClient { // Process MacOS native scroll events switch selector { case #selector(moveToBeginningOfDocument(_:)): - surfaceModel!.perform(action: "scroll_to_top") + _ = surfaceModel.perform(action: "scroll_to_top") case #selector(moveToEndOfDocument(_:)): - surfaceModel!.perform(action: "scroll_to_bottom") + _ = surfaceModel.perform(action: "scroll_to_bottom") default: break } -- 2.51.2 From b4a5ddfef966f8def62864f901b0d9cec608fd82 Mon Sep 17 00:00:00 2001 From: Suyeol Jeon Date: Thu, 18 Dec 2025 17:13:02 +0900 Subject: [PATCH 030/349] macos: apply window position after setting content size When window-width/height is configured, the window size is set via setContentSize in windowDidLoad. However, window-position-x/y was not being applied after this resize, causing the window to appear at an incorrect position. This was a regression introduced in c75bade89 which refactored the default size logic from a computed NSRect property to a DefaultSize enum. The original code called adjustForWindowPosition after calculating the frame, but this was lost during the refactoring. Fixes the issue by calling adjustForWindowPosition after applying contentIntrinsicSize to ensure window position is correctly set. --- macos/Sources/Features/Terminal/TerminalController.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 8a0c5f46d..bccdd9c69 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -952,9 +952,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr case .contentIntrinsicSize: // Content intrinsic size requires a short delay so that AppKit // can layout our SwiftUI views. - DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak window] in - guard let window else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak self, weak window] in + guard let self, let window else { return } defaultSize.apply(to: window) + if let screen = window.screen ?? NSScreen.main { + let frame = self.adjustForWindowPosition(frame: window.frame, on: screen) + window.setFrameOrigin(frame.origin) + } } } } -- 2.51.2 From 5bd814adf8b2fad4f7d8ca7c05776c8dcb6cd35a Mon Sep 17 00:00:00 2001 From: Yasu Flores Date: Mon, 22 Dec 2025 08:53:43 -0600 Subject: [PATCH 031/349] move guard down to keep surfaceModel logic together --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index c54f674a5..37cc9282e 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1910,7 +1910,6 @@ extension Ghostty.SurfaceView: NSTextInputClient { /// 1. Prevents an audible NSBeep for unimplemented actions. /// 2. Allows us to properly encode super+key input events that we don't handle override func doCommand(by selector: Selector) { - guard let surfaceModel else { return } // If we are being processed by performKeyEquivalent with a command binding, // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, @@ -1921,6 +1920,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } + guard let surfaceModel else { return } // Process MacOS native scroll events switch selector { case #selector(moveToBeginningOfDocument(_:)): -- 2.51.2 From 3877ead07133070792b02b79a10aa9e51d560879 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 08:46:57 -0800 Subject: [PATCH 032/349] input: parse chains (don't do anything with them yet) --- src/input/Binding.zig | 59 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 22a5e8386..0bacea87d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -52,6 +52,7 @@ pub const Parser = struct { trigger_it: SequenceIterator, action: Action, flags: Flags = .{}, + chain: bool, pub const Elem = union(enum) { /// A leader trigger in a sequence. @@ -59,6 +60,12 @@ pub const Parser = struct { /// The final trigger and action in a sequence. binding: Binding, + + /// A chained action `chain=` that should be appended + /// to the previous binding. Note that any action is parsed, including + /// invalid actions for chains such as `unbind`. We expect downstream + /// consumers to validate that the action is valid for chaining. + chain: Action, }; pub fn init(raw_input: []const u8) Error!Parser { @@ -95,12 +102,23 @@ pub const Parser = struct { return Error.InvalidFormat; }; + // Detect chains. Chains must not have flags. + const chain = std.mem.eql(u8, input[0..eql_idx], "chain"); + if (chain and start_idx > 0) return Error.InvalidFormat; + // Sequence iterator goes up to the equal, action is after. We can // parse the action now. return .{ - .trigger_it = .{ .input = input[0..eql_idx] }, + .trigger_it = .{ + // This is kind of hacky but we put a dummy trigger + // for chained inputs. The `next` will never yield this + // because we have chain set. When we find a nicer way to + // do this we can remove it, the e2e is tested. + .input = if (chain) "a" else input[0..eql_idx], + }, .action = try .parse(input[eql_idx + 1 ..]), .flags = flags, + .chain = chain, }; } @@ -156,6 +174,9 @@ pub const Parser = struct { return .{ .leader = trigger }; } + // If we're a chain then return it as-is. + if (self.chain) return .{ .chain = self.action }; + // Out of triggers, yield the final action. return .{ .binding = .{ .trigger = trigger, @@ -191,19 +212,26 @@ const SequenceIterator = struct { /// Returns true if there are no more triggers to parse. pub fn done(self: *const SequenceIterator) bool { - return self.i > self.input.len; + return self.i >= self.input.len; } }; /// Parse a single, non-sequenced binding. To support sequences you must /// use parse. This is a convenience function for single bindings aimed /// primarily at tests. -fn parseSingle(raw_input: []const u8) (Error || error{UnexpectedSequence})!Binding { +/// +/// This doesn't support `chain` either, since chaining requires some +/// stateful concept of a prior binding. +fn parseSingle(raw_input: []const u8) (Error || error{ + UnexpectedChain, + UnexpectedSequence, +})!Binding { var p = try Parser.init(raw_input); const elem = (try p.next()) orelse return Error.InvalidFormat; return switch (elem) { .leader => error.UnexpectedSequence, .binding => elem.binding, + .chain => error.UnexpectedChain, }; } @@ -2155,6 +2183,8 @@ pub const Set = struct { b.flags, ), }, + + .chain => @panic("TODO"), } } @@ -2887,6 +2917,29 @@ test "parse: action with a tuple" { try testing.expectError(Error.InvalidFormat, parseSingle("a=resize_split:up,four")); } +test "parse: chain" { + const testing = std.testing; + + // Valid + { + var p = try Parser.init("chain=new_tab"); + try testing.expectEqual(Parser.Elem{ + .chain = .new_tab, + }, try p.next()); + try testing.expect(try p.next() == null); + } + + // Chain can't have flags + try testing.expectError(error.InvalidFormat, Parser.init("global:chain=ignore")); + + // Chain can't be part of a sequence + { + var p = try Parser.init("a>chain=ignore"); + _ = try p.next(); + try testing.expectError(error.InvalidFormat, p.next()); + } +} + test "sequence iterator" { const testing = std.testing; -- 2.51.2 From 42c21eb16b0afc6d44820968bf4d585bbf9ccb21 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 09:13:05 -0800 Subject: [PATCH 033/349] input: leaf_chained tagged union value --- src/App.zig | 53 +++++++++++++------- src/Surface.zig | 44 ++++++++++++----- src/apprt/embedded.zig | 2 +- src/cli/list_keybinds.zig | 4 +- src/config/Config.zig | 15 ++++++ src/input/Binding.zig | 101 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 184 insertions(+), 35 deletions(-) diff --git a/src/App.zig b/src/App.zig index 99d03399c..00be56f49 100644 --- a/src/App.zig +++ b/src/App.zig @@ -357,15 +357,17 @@ pub fn keyEvent( // Get the keybind entry for this event. We don't support key sequences // so we can look directly in the top-level set. const entry = rt_app.config.keybind.set.getEvent(event) orelse return false; - const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) { + const leaf: input.Binding.Set.GenericLeaf = switch (entry.value_ptr.*) { // Sequences aren't supported. Our configuration parser verifies // this for global keybinds but we may still get an entry for // a non-global keybind. .leader => return false, // Leaf entries are good - .leaf => |leaf| leaf, + inline .leaf, .leaf_chained => |leaf| leaf.generic(), }; + const actions: []const input.Binding.Action = leaf.actionsSlice(); + assert(actions.len > 0); // If we aren't focused, then we only process global keybinds. if (!self.focused and !leaf.flags.global) return false; @@ -373,13 +375,7 @@ pub fn keyEvent( // Global keybinds are done using performAll so that they // can target all surfaces too. if (leaf.flags.global) { - self.performAllAction(rt_app, leaf.action) catch |err| { - log.warn("error performing global keybind action action={s} err={}", .{ - @tagName(leaf.action), - err, - }); - }; - + self.performAllChainedAction(rt_app, actions); return true; } @@ -389,14 +385,20 @@ pub fn keyEvent( // If we are focused, then we process keybinds only if they are // app-scoped. Otherwise, we do nothing. Surface-scoped should - // be processed by Surface.keyEvent. - const app_action = leaf.action.scoped(.app) orelse return false; - self.performAction(rt_app, app_action) catch |err| { - log.warn("error performing app keybind action action={s} err={}", .{ - @tagName(app_action), - err, - }); - }; + // be processed by Surface.keyEvent. For chained actions, all + // actions must be app-scoped. + for (actions) |action| if (action.scoped(.app) == null) return false; + for (actions) |action| { + self.performAction( + rt_app, + action.scoped(.app).?, + ) catch |err| { + log.warn("error performing app keybind action action={s} err={}", .{ + @tagName(action), + err, + }); + }; + } return true; } @@ -454,6 +456,23 @@ pub fn performAction( } } +/// Performs a chained action. We will continue executing each action +/// even if there is a failure in a prior action. +pub fn performAllChainedAction( + self: *App, + rt_app: *apprt.App, + actions: []const input.Binding.Action, +) void { + for (actions) |action| { + self.performAllAction(rt_app, action) catch |err| { + log.warn("error performing chained action action={s} err={}", .{ + @tagName(action), + err, + }); + }; + } +} + /// Perform an app-wide binding action. If the action is surface-specific /// then it will be performed on all surfaces. To perform only app-scoped /// actions, use performAction. diff --git a/src/Surface.zig b/src/Surface.zig index 2784f93db..0ce758636 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2866,7 +2866,7 @@ fn maybeHandleBinding( }; // Determine if this entry has an action or if its a leader key. - const leaf: input.Binding.Set.Leaf = switch (entry.value_ptr.*) { + const leaf: input.Binding.Set.GenericLeaf = switch (entry.value_ptr.*) { .leader => |set| { // Setup the next set we'll look at. self.keyboard.sequence_set = set; @@ -2893,9 +2893,8 @@ fn maybeHandleBinding( return .consumed; }, - .leaf => |leaf| leaf, + inline .leaf, .leaf_chained => |leaf| leaf.generic(), }; - const action = leaf.action; // consumed determines if the input is consumed or if we continue // encoding the key (if we have a key to encode). @@ -2917,36 +2916,58 @@ fn maybeHandleBinding( // An action also always resets the sequence set. self.keyboard.sequence_set = null; + // Setup our actions + const actions = leaf.actionsSlice(); + // Attempt to perform the action - log.debug("key event binding flags={} action={f}", .{ + log.debug("key event binding flags={} action={any}", .{ leaf.flags, - action, + actions, }); const performed = performed: { // If this is a global or all action, then we perform it on // the app and it applies to every surface. if (leaf.flags.global or leaf.flags.all) { - try self.app.performAllAction(self.rt_app, action); + self.app.performAllChainedAction( + self.rt_app, + actions, + ); // "All" actions are always performed since they are global. break :performed true; } - break :performed try self.performBindingAction(action); + // Perform each action. We are performed if ANY of the chained + // actions perform. + var performed: bool = false; + for (actions) |action| { + if (self.performBindingAction(action)) |_| { + performed = true; + } else |err| { + log.info( + "key binding action failed action={t} err={}", + .{ action, err }, + ); + } + } + + break :performed performed; }; if (performed) { // If we performed an action and it was a closing action, // our "self" pointer is not safe to use anymore so we need to // just exit immediately. - if (closingAction(action)) { + for (actions) |action| if (closingAction(action)) { log.debug("key binding is a closing binding, halting key event processing", .{}); return .closed; - } + }; // If our action was "ignore" then we return the special input // effect of "ignored". - if (action == .ignore) return .ignored; + for (actions) |action| if (action == .ignore) { + return .ignored; + }; } // If we have the performable flag and the action was not performed, @@ -2970,7 +2991,8 @@ fn maybeHandleBinding( // Store our last trigger so we don't encode the release event self.keyboard.last_trigger = event.bindingHash(); - if (insp_ev) |ev| ev.binding = action; + // TODO: Inspector must support chained events + if (insp_ev) |ev| ev.binding = actions[0]; return .consumed; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index da7a585a5..1cb9231bc 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -155,7 +155,7 @@ pub const App = struct { while (it.next()) |entry| { switch (entry.value_ptr.*) { .leader => {}, - .leaf => |leaf| if (leaf.flags.global) return true, + inline .leaf, .leaf_chained => |leaf| if (leaf.flags.global) return true, } } diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index e463f55b9..fb7ad19ec 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -326,7 +326,6 @@ fn iterateBindings( switch (bind.value_ptr.*) { .leader => |leader| { - // Recursively iterate on the set of bindings for this leader key var n_iter = leader.bindings.iterator(); const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win); @@ -353,6 +352,9 @@ fn iterateBindings( .action = leaf.action, }); }, + .leaf_chained => { + // TODO: Show these. + }, } } diff --git a/src/config/Config.zig b/src/config/Config.zig index 8f1cece45..f75944aeb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6749,6 +6749,21 @@ pub const Keybinds = struct { other_leaf, )) return false; }, + + .leaf_chained => { + const self_chain = self_entry.value_ptr.*.leaf_chained; + const other_chain = other_entry.value_ptr.*.leaf_chained; + + if (self_chain.flags != other_chain.flags) return false; + if (self_chain.actions.items.len != other_chain.actions.items.len) return false; + for (self_chain.actions.items, other_chain.actions.items) |a1, a2| { + if (!equalField( + inputpkg.Binding.Action, + a1, + a2, + )) return false; + } + }, } } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 0bacea87d..39b6ffcba 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -212,7 +212,7 @@ const SequenceIterator = struct { /// Returns true if there are no more triggers to parse. pub fn done(self: *const SequenceIterator) bool { - return self.i >= self.input.len; + return self.i > self.input.len; } }; @@ -1953,6 +1953,9 @@ pub const Set = struct { /// to take along with the flags that may define binding behavior. leaf: Leaf, + /// A set of actions to take in response to a trigger. + leaf_chained: LeafChained, + /// Implements the formatter for the fmt package. This encodes the /// action back into the format used by parse. pub fn format( @@ -2018,6 +2021,8 @@ pub const Set = struct { buffer.print("={f}", .{leaf.action}) catch return error.OutOfMemory; try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); }, + + .leaf_chained => @panic("TODO"), } } }; @@ -2044,6 +2049,47 @@ pub const Set = struct { std.hash.autoHash(&hasher, self.flags); return hasher.final(); } + + pub fn generic(self: *const Leaf) GenericLeaf { + return .{ + .flags = self.flags, + .actions = .{ .single = .{self.action} }, + }; + } + }; + + /// Leaf node of a set that triggers multiple actions in sequence. + pub const LeafChained = struct { + actions: std.ArrayList(Action), + flags: Flags, + + pub fn deinit(self: *LeafChained, alloc: Allocator) void { + self.actions.deinit(alloc); + } + + pub fn generic(self: *const LeafChained) GenericLeaf { + return .{ + .flags = self.flags, + .actions = .{ .many = self.actions.items }, + }; + } + }; + + /// A generic leaf node that can be used to unify the handling of + /// leaf and leaf_chained. + pub const GenericLeaf = struct { + flags: Flags, + actions: union(enum) { + single: [1]Action, + many: []const Action, + }, + + pub fn actionsSlice(self: *const GenericLeaf) []const Action { + return switch (self.actions) { + .single => |*arr| arr, + .many => |slice| slice, + }; + } }; /// A full key-value entry for the set. @@ -2057,6 +2103,9 @@ pub const Set = struct { s.deinit(alloc); alloc.destroy(s); }, + + .leaf_chained => |*l| l.deinit(alloc), + .leaf => {}, }; @@ -2133,7 +2182,7 @@ pub const Set = struct { error.OutOfMemory => return error.OutOfMemory, }, - .leaf => { + .leaf, .leaf_chained => { // Remove the existing action. Fallthrough as if // we don't have a leader. set.remove(alloc, t); @@ -2163,6 +2212,7 @@ pub const Set = struct { leaf.action, leaf.flags, ) catch {}, + .leaf_chained => @panic("TODO"), }; }, @@ -2184,7 +2234,9 @@ pub const Set = struct { ), }, - .chain => @panic("TODO"), + .chain => { + // TODO: Do this, ignore for now. + }, } } @@ -2236,6 +2288,12 @@ pub const Set = struct { } } }, + + // Chained leaves aren't in the reverse mapping so we just + // clear it out. + .leaf_chained => |*l| { + l.deinit(alloc); + }, }; gop.value_ptr.* = .{ .leaf = .{ @@ -2312,7 +2370,7 @@ pub const Set = struct { } fn removeExact(self: *Set, alloc: Allocator, t: Trigger) void { - const entry = self.bindings.get(t) orelse return; + var entry = self.bindings.get(t) orelse return; _ = self.bindings.remove(t); switch (entry) { @@ -2334,7 +2392,7 @@ pub const Set = struct { var it = self.bindings.iterator(); while (it.next()) |it_entry| { switch (it_entry.value_ptr.*) { - .leader => {}, + .leader, .leaf_chained => {}, .leaf => |leaf_search| { if (leaf_search.action.hash() == action_hash) { self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*); @@ -2348,6 +2406,12 @@ pub const Set = struct { _ = self.reverse.remove(leaf.action); } }, + + // Chained leaves are never in our reverse mapping so no + // cleanup is required. + .leaf_chained => |*l| { + l.deinit(alloc); + }, } } @@ -2366,6 +2430,8 @@ pub const Set = struct { // contain allocated strings). .leaf => |*s| s.* = try s.clone(alloc), + .leaf_chained => @panic("TODO"), + // Must be deep cloned. .leader => |*s| { const ptr = try alloc.create(Set); @@ -3356,6 +3422,31 @@ test "set: consumed state" { try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); } +// test "set: parseAndPut chain" { +// const testing = std.testing; +// const alloc = testing.allocator; +// +// var s: Set = .{}; +// defer s.deinit(alloc); +// +// try s.parseAndPut(alloc, "a=new_window"); +// try s.parseAndPut(alloc, "chain=new_tab"); +// +// // Creates forward mapping +// { +// const action = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf; +// try testing.expect(action.action == .new_window); +// try testing.expectEqual(Flags{}, action.flags); +// } +// +// // Does not create reverse mapping, because reverse mappings are only for +// // non-chain actions. +// { +// const trigger = s.getTrigger(.new_window); +// try testing.expect(trigger == null); +// } +// } + test "set: getEvent physical" { const testing = std.testing; const alloc = testing.allocator; -- 2.51.2 From 457fededeb6f30b272b81bfe4583987091a0a846 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 10:51:43 -0800 Subject: [PATCH 034/349] input: keep track of chain parent --- src/input/Binding.zig | 323 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 305 insertions(+), 18 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 39b6ffcba..6c91415e4 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1943,6 +1943,10 @@ pub const Set = struct { /// integration with GUI toolkits. reverse: ReverseMap = .{}, + /// The chain parent is the information necessary to attach a chained + /// action to the proper location in our mapping. + chain_parent: ?HashMap.Entry = null, + /// The entry type for the forward mapping of trigger to action. pub const Value = union(enum) { /// This key is a leader key in a sequence. You must follow the given @@ -2135,26 +2139,55 @@ pub const Set = struct { // We use recursion so that we can utilize the stack as our state // for cleanup. - self.parseAndPutRecurse(alloc, &it) catch |err| switch (err) { - // If this gets sent up to the root then we've unbound - // all the way up and this put was a success. - error.SequenceUnbind => {}, + const updated_set_ = self.parseAndPutRecurse( + alloc, + &it, + ) catch |err| err: { + switch (err) { + // If this gets sent up to the root then we've unbound + // all the way up and this put was a success. + error.SequenceUnbind => break :err null, + + // If our parser input was too short then the format + // is invalid because we handle all valid cases. + error.UnexpectedEndOfInput => return error.InvalidFormat, + + // Unrecoverable + error.OutOfMemory => return error.OutOfMemory, + } - // Unrecoverable - error.OutOfMemory => return error.OutOfMemory, + // Errors must never fall through. + unreachable; }; + + // If we have an updated set (a binding was added) then we store + // it for our chain parent. If we didn't update a set then we clear + // our chain parent since chaining is no longer valid until a + // valid binding is saved. + if (updated_set_) |updated_set| { + // A successful addition must have recorded a chain parent. + assert(updated_set.chain_parent != null); + if (updated_set != self) self.chain_parent = updated_set.chain_parent; + assert(self.chain_parent != null); + } else { + self.chain_parent = null; + } } const ParseAndPutRecurseError = Allocator.Error || error{ SequenceUnbind, + UnexpectedEndOfInput, }; + /// Returns the set that was ultimately updated if a binding was + /// added. Unbind does not return a set since nothing was added. fn parseAndPutRecurse( set: *Set, alloc: Allocator, it: *Parser, - ) ParseAndPutRecurseError!void { - const elem = (it.next() catch unreachable) orelse return; + ) ParseAndPutRecurseError!?*Set { + const elem = (it.next() catch unreachable) orelse + return error.UnexpectedEndOfInput; switch (elem) { .leader => |t| { // If we have a leader, we need to upsert a set for it. @@ -2177,9 +2210,11 @@ pub const Set = struct { error.SequenceUnbind => if (s.bindings.count() == 0) { set.remove(alloc, t); return error.SequenceUnbind; - }, + } else null, - error.OutOfMemory => return error.OutOfMemory, + error.UnexpectedEndOfInput, + error.OutOfMemory, + => err, }, .leaf, .leaf_chained => { @@ -2199,7 +2234,7 @@ pub const Set = struct { try set.bindings.put(alloc, t, .{ .leader = next }); // Recurse - parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { + return parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { // If our action was to unbind, we restore the old // action if we have it. error.SequenceUnbind => { @@ -2214,9 +2249,13 @@ pub const Set = struct { ) catch {}, .leaf_chained => @panic("TODO"), }; + + return null; }, - error.OutOfMemory => return error.OutOfMemory, + error.UnexpectedEndOfInput, + error.OutOfMemory, + => return err, }; }, @@ -2226,16 +2265,20 @@ pub const Set = struct { return error.SequenceUnbind; }, - else => try set.putFlags( - alloc, - b.trigger, - b.action, - b.flags, - ), + else => { + try set.putFlags( + alloc, + b.trigger, + b.action, + b.flags, + ); + return set; + }, }, .chain => { // TODO: Do this, ignore for now. + return set; }, } } @@ -2267,7 +2310,21 @@ pub const Set = struct { // See the reverse map docs for more information. const track_reverse: bool = !flags.performable; + // No matter what our chained parent becomes invalid because + // getOrPut invalidates pointers. + self.chain_parent = null; + const gop = try self.bindings.getOrPut(alloc, t); + self.chain_parent = .{ + .key_ptr = gop.key_ptr, + .value_ptr = gop.value_ptr, + }; + errdefer { + // If we have any errors we can't trust our values here. And + // we can't restore the old values because they're also invalidated + // by getOrPut so we just disable chaining. + self.chain_parent = null; + } if (gop.found_existing) switch (gop.value_ptr.*) { // If we have a leader we need to clean up the memory @@ -2304,6 +2361,13 @@ pub const Set = struct { if (track_reverse) try self.reverse.put(alloc, action, t); errdefer if (track_reverse) self.reverse.remove(action); + + // Invariant: after successful put, chain_parent must be valid and point + // to the entry we just added/updated. + assert(self.chain_parent != null); + assert(self.chain_parent.?.key_ptr == gop.key_ptr); + assert(self.chain_parent.?.value_ptr == gop.value_ptr); + assert(self.chain_parent.?.value_ptr.* == .leaf); } /// Get a binding for a given trigger. @@ -2370,6 +2434,11 @@ pub const Set = struct { } fn removeExact(self: *Set, alloc: Allocator, t: Trigger) void { + // Removal always resets our chain parent. We could make this + // finer grained but the way it is documented is that chaining + // must happen directly after sets so this works. + self.chain_parent = null; + var entry = self.bindings.get(t) orelse return; _ = self.bindings.remove(t); @@ -3097,6 +3166,15 @@ test "set: parseAndPut typical binding" { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); } + + // Sets up the chain parent properly + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } } test "set: parseAndPut unconsumed binding" { @@ -3121,6 +3199,15 @@ test "set: parseAndPut unconsumed binding" { const trigger = s.getTrigger(.{ .new_window = {} }).?; try testing.expect(trigger.key.unicode == 'a'); } + + // Sets up the chain parent properly + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } } test "set: parseAndPut removed binding" { @@ -3139,6 +3226,206 @@ test "set: parseAndPut removed binding" { try testing.expect(s.get(trigger) == null); } try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); + + // Sets up the chain parent properly + try testing.expect(s.chain_parent == null); +} + +test "set: put sets chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + + // chain_parent should be set + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } + + // chain_parent value should be a leaf + try testing.expect(s.chain_parent.?.value_ptr.* == .leaf); +} + +test "set: putFlags sets chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.putFlags( + alloc, + .{ .key = .{ .unicode = 'a' } }, + .{ .new_window = {} }, + .{ .consumed = false }, + ); + + // chain_parent should be set + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("a", buf.written()); + } + + // chain_parent value should be a leaf with correct flags + try testing.expect(s.chain_parent.?.value_ptr.* == .leaf); + try testing.expect(!s.chain_parent.?.value_ptr.*.leaf.flags.consumed); +} + +test "set: sequence sets chain_parent to final leaf" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + + // chain_parent should be set and point to 'b' (the final leaf) + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("b", buf.written()); + } + + // chain_parent value should be a leaf + try testing.expect(s.chain_parent.?.value_ptr.* == .leaf); + try testing.expect(s.chain_parent.?.value_ptr.*.leaf.action == .new_window); +} + +test "set: multiple leaves under leader updates chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + + // After first binding, chain_parent should be 'b' + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("b", buf.written()); + } + + try s.parseAndPut(alloc, "a>c=new_tab"); + + // After second binding, chain_parent should be updated to 'c' + try testing.expect(s.chain_parent != null); + { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try s.chain_parent.?.key_ptr.format(&buf.writer); + try testing.expectEqualStrings("c", buf.written()); + } + try testing.expect(s.chain_parent.?.value_ptr.*.leaf.action == .new_tab); +} + +test "set: sequence unbind clears chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + try testing.expect(s.chain_parent != null); + + try s.parseAndPut(alloc, "a>b=unbind"); + + // After unbind, chain_parent should be cleared + try testing.expect(s.chain_parent == null); +} + +test "set: sequence unbind with remaining leaves clears chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + try s.parseAndPut(alloc, "a>c=new_tab"); + try s.parseAndPut(alloc, "a>b=unbind"); + + // After unbind, chain_parent should be cleared even though 'c' remains + try testing.expect(s.chain_parent == null); + + // But 'c' should still exist + const a_entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(a_entry.value_ptr.* == .leader); + const inner_set = a_entry.value_ptr.*.leader; + try testing.expect(inner_set.get(.{ .key = .{ .unicode = 'c' } }) != null); +} + +test "set: direct remove clears chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try testing.expect(s.chain_parent != null); + + s.remove(alloc, .{ .key = .{ .unicode = 'a' } }); + + // After removal, chain_parent should be cleared + try testing.expect(s.chain_parent == null); +} + +test "set: invalid format preserves chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + const before_key = s.chain_parent.?.key_ptr; + const before_value = s.chain_parent.?.value_ptr; + + // Try an invalid parse - should fail + try testing.expectError(error.InvalidAction, s.parseAndPut(alloc, "a=invalid_action_xyz")); + + // chain_parent should be unchanged + try testing.expect(s.chain_parent != null); + try testing.expect(s.chain_parent.?.key_ptr == before_key); + try testing.expect(s.chain_parent.?.value_ptr == before_value); +} + +test "set: clone produces null chain_parent" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try testing.expect(s.chain_parent != null); + + var cloned = try s.clone(alloc); + defer cloned.deinit(alloc); + + // Clone should have null chain_parent + try testing.expect(cloned.chain_parent == null); + + // But should have the binding + try testing.expect(cloned.get(.{ .key = .{ .unicode = 'a' } }) != null); } test "set: parseAndPut sequence" { -- 2.51.2 From 4fdc52b920c03035ad3ce9aef042b602d2bd0a0c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 12:32:52 -0800 Subject: [PATCH 035/349] input: appendChain --- src/input/Binding.zig | 123 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6c91415e4..a5bb44b4d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2370,6 +2370,52 @@ pub const Set = struct { assert(self.chain_parent.?.value_ptr.* == .leaf); } + /// Append a chained action to the prior set action. + /// + /// It is an error if there is no valid prior chain parent. + pub fn appendChain( + self: *Set, + alloc: Allocator, + action: Action, + ) (Allocator.Error || error{NoChainParent})!void { + const parent = self.chain_parent orelse return error.NoChainParent; + switch (parent.value_ptr.*) { + // Leader can never be a chain parent. Verified through various + // assertions and unit tests. + .leader => unreachable, + + // If it is already a chained action, we just append the + // action. Easy! + .leaf_chained => |*leaf| try leaf.actions.append( + alloc, + action, + ), + + // If it is a leaf, we need to convert it to a leaf_chained. + // We also need to be careful to remove any prior reverse + // mappings for this action since chained actions are not + // part of the reverse mapping. + .leaf => |leaf| { + // Setup our failable actions list first. + var actions: std.ArrayList(Action) = .empty; + try actions.ensureTotalCapacity(alloc, 2); + errdefer actions.deinit(alloc); + actions.appendAssumeCapacity(leaf.action); + actions.appendAssumeCapacity(action); + + // Clean up our reverse mapping. We only do this if + // we're the chain parent because only the root set + // maintains reverse mappings. + // TODO + + parent.value_ptr.* = .{ .leaf_chained = .{ + .actions = actions, + .flags = leaf.flags, + } }; + }, + } + } + /// Get a binding for a given trigger. pub fn get(self: Set, t: Trigger) ?Entry { return self.bindings.getEntry(t); @@ -4069,3 +4115,80 @@ test "action: format" { try a.format(&buf.writer); try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written()); } + +test "set: appendChain with no parent returns error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try testing.expectError(error.NoChainParent, s.appendChain(alloc, .{ .new_tab = {} })); +} + +test "set: appendChain after put converts to leaf_chained" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + + // First appendChain converts leaf to leaf_chained and appends the new action + try s.appendChain(alloc, .{ .new_tab = {} }); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + const chained = entry.value_ptr.*.leaf_chained; + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); +} + +test "set: appendChain after putFlags preserves flags" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.putFlags( + alloc, + .{ .key = .{ .unicode = 'a' } }, + .{ .new_window = {} }, + .{ .consumed = false }, + ); + try s.appendChain(alloc, .{ .new_tab = {} }); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + const chained = entry.value_ptr.*.leaf_chained; + try testing.expect(!chained.flags.consumed); + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); +} + +test "set: appendChain multiple times" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try s.appendChain(alloc, .{ .new_tab = {} }); + try s.appendChain(alloc, .{ .close_surface = {} }); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + const chained = entry.value_ptr.*.leaf_chained; + try testing.expectEqual(@as(usize, 3), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); + try testing.expect(chained.actions.items[2] == .close_surface); +} -- 2.51.2 From a3373f3c6a3653df70c0bb447b14cf72a181e906 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 12:43:16 -0800 Subject: [PATCH 036/349] input: appendChain reverse mapping --- src/input/Binding.zig | 166 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index a5bb44b4d..b4f519379 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1944,8 +1944,21 @@ pub const Set = struct { reverse: ReverseMap = .{}, /// The chain parent is the information necessary to attach a chained - /// action to the proper location in our mapping. - chain_parent: ?HashMap.Entry = null, + /// action to the proper location in our mapping. It tracks both the + /// entry in the hashmap and the set it belongs to, which is needed + /// to properly update reverse mappings when converting a leaf to + /// a chained action. + chain_parent: ?ChainParent = null, + + /// Information about a chain parent entry, including which set it + /// belongs to. This is needed because reverse mappings are only + /// maintained in the root set, but the chain parent entry may be + /// in a nested set (for leader key sequences). + const ChainParent = struct { + key_ptr: *Trigger, + value_ptr: *Value, + set: *Set, + }; /// The entry type for the forward mapping of trigger to action. pub const Value = union(enum) { @@ -2318,6 +2331,7 @@ pub const Set = struct { self.chain_parent = .{ .key_ptr = gop.key_ptr, .value_ptr = gop.value_ptr, + .set = self, }; errdefer { // If we have any errors we can't trust our values here. And @@ -2403,15 +2417,22 @@ pub const Set = struct { actions.appendAssumeCapacity(leaf.action); actions.appendAssumeCapacity(action); - // Clean up our reverse mapping. We only do this if - // we're the chain parent because only the root set - // maintains reverse mappings. - // TODO - + // Convert to leaf_chained first, before fixing up reverse + // mapping. This is important because fixupReverseForAction + // searches for other bindings with the same action, and we + // don't want to find this entry (which is now chained). parent.value_ptr.* = .{ .leaf_chained = .{ .actions = actions, .flags = leaf.flags, } }; + + // Clean up our reverse mapping. Chained actions are not + // part of the reverse mapping, so we need to fix up the + // reverse map (possibly restoring another trigger for the + // same action). + if (!leaf.flags.performable) { + parent.set.fixupReverseForAction(leaf.action); + } }, } } @@ -2498,29 +2519,7 @@ pub const Set = struct { }, // For an action we need to fix up the reverse mapping. - // Note: we'd LIKE to replace this with the most recent binding but - // our hash map obviously has no concept of ordering so we have to - // choose whatever. Maybe a switch to an array hash map here. - .leaf => |leaf| { - const action_hash = leaf.action.hash(); - - var it = self.bindings.iterator(); - while (it.next()) |it_entry| { - switch (it_entry.value_ptr.*) { - .leader, .leaf_chained => {}, - .leaf => |leaf_search| { - if (leaf_search.action.hash() == action_hash) { - self.reverse.putAssumeCapacity(leaf.action, it_entry.key_ptr.*); - break; - } - }, - } - } else { - // No other trigger points to this action so we remove - // the reverse mapping completely. - _ = self.reverse.remove(leaf.action); - } - }, + .leaf => |leaf| self.fixupReverseForAction(leaf.action), // Chained leaves are never in our reverse mapping so no // cleanup is required. @@ -2530,6 +2529,38 @@ pub const Set = struct { } } + /// Fix up the reverse mapping after removing an action. + /// + /// When an action is removed from a binding (either by removal or by + /// converting to a chained action), we need to update the reverse mapping. + /// If another binding has the same action, we update the reverse mapping + /// to point to that binding. Otherwise, we remove the action from the + /// reverse mapping entirely. + /// + /// Note: we'd LIKE to replace this with the most recent binding but + /// our hash map obviously has no concept of ordering so we have to + /// choose whatever. Maybe a switch to an array hash map here. + fn fixupReverseForAction(self: *Set, action: Action) void { + const action_hash = action.hash(); + + var it = self.bindings.iterator(); + while (it.next()) |it_entry| { + switch (it_entry.value_ptr.*) { + .leader, .leaf_chained => {}, + .leaf => |leaf_search| { + if (leaf_search.action.hash() == action_hash) { + self.reverse.putAssumeCapacity(action, it_entry.key_ptr.*); + return; + } + }, + } + } + + // No other trigger points to this action so we remove + // the reverse mapping completely. + _ = self.reverse.remove(action); + } + /// Deep clone the set. pub fn clone(self: *const Set, alloc: Allocator) !Set { var result: Set = .{ @@ -4192,3 +4223,78 @@ test "set: appendChain multiple times" { try testing.expect(chained.actions.items[1] == .new_tab); try testing.expect(chained.actions.items[2] == .close_surface); } + +test "set: appendChain removes reverse mapping" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + + // Verify reverse mapping exists before chaining + try testing.expect(s.getTrigger(.{ .new_window = {} }) != null); + + // Chaining should remove the reverse mapping + try s.appendChain(alloc, .{ .new_tab = {} }); + + // Reverse mapping should be gone since chained actions are not in reverse map + try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); +} + +test "set: appendChain with performable does not affect reverse mapping" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Add a non-performable binding first + try s.put(alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }); + try testing.expect(s.getTrigger(.{ .new_window = {} }) != null); + + // Add a performable binding (not in reverse map) and chain it + try s.putFlags( + alloc, + .{ .key = .{ .unicode = 'a' } }, + .{ .close_surface = {} }, + .{ .performable = true }, + ); + + // close_surface was performable, so not in reverse map + try testing.expect(s.getTrigger(.{ .close_surface = {} }) == null); + + // Chaining the performable binding should not crash or affect anything + try s.appendChain(alloc, .{ .new_tab = {} }); + + // The non-performable new_window binding should still be in reverse map + try testing.expect(s.getTrigger(.{ .new_window = {} }) != null); +} + +test "set: appendChain restores next valid reverse mapping" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Add two bindings for the same action + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }); + + // Reverse mapping should point to 'b' (most recent) + { + const trigger = s.getTrigger(.{ .new_window = {} }).?; + try testing.expect(trigger.key.unicode == 'b'); + } + + // Chain an action to 'b', which should restore 'a' in the reverse map + try s.appendChain(alloc, .{ .new_tab = {} }); + + // Now reverse mapping should point to 'a' + { + const trigger = s.getTrigger(.{ .new_window = {} }).?; + try testing.expect(trigger.key.unicode == 'a'); + } +} -- 2.51.2 From 67be309e3f43ab91c759368d3d7d4fd396182b94 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 12:50:39 -0800 Subject: [PATCH 037/349] input: Trigger.eql --- src/input/Binding.zig | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b4f519379..0d52f23cd 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1870,6 +1870,19 @@ pub const Trigger = struct { return array; } + /// Returns true if two triggers are equal. + pub fn eql(self: Trigger, other: Trigger) bool { + if (self.mods != other.mods) return false; + const self_tag = std.meta.activeTag(self.key); + const other_tag = std.meta.activeTag(other.key); + if (self_tag != other_tag) return false; + return switch (self.key) { + .physical => |v| v == other.key.physical, + .unicode => |v| v == other.key.unicode, + .catch_all => true, + }; + } + /// Convert the trigger to a C API compatible trigger. pub fn cval(self: Trigger) C { return .{ @@ -2974,6 +2987,66 @@ test "parse: all triggers" { } } +test "Trigger: eql" { + const testing = std.testing; + + // Equal physical keys + { + const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + try testing.expect(t1.eql(t2)); + } + + // Different physical keys + { + const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + const t2: Trigger = .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true } }; + try testing.expect(!t1.eql(t2)); + } + + // Different mods + { + const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; + const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true } }; + try testing.expect(!t1.eql(t2)); + } + + // Equal unicode keys + { + const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + const t2: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + try testing.expect(t1.eql(t2)); + } + + // Different unicode keys + { + const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + const t2: Trigger = .{ .key = .{ .unicode = 'b' }, .mods = .{} }; + try testing.expect(!t1.eql(t2)); + } + + // Different key types + { + const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; + const t2: Trigger = .{ .key = .{ .physical = .key_a }, .mods = .{} }; + try testing.expect(!t1.eql(t2)); + } + + // catch_all + { + const t1: Trigger = .{ .key = .catch_all, .mods = .{} }; + const t2: Trigger = .{ .key = .catch_all, .mods = .{} }; + try testing.expect(t1.eql(t2)); + } + + // catch_all with different mods + { + const t1: Trigger = .{ .key = .catch_all, .mods = .{} }; + const t2: Trigger = .{ .key = .catch_all, .mods = .{ .alt = true } }; + try testing.expect(!t1.eql(t2)); + } +} + test "parse: modifier aliases" { const testing = std.testing; -- 2.51.2 From 9bf1b9ac711252782aa73d166173570c8fedd44d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 12:54:05 -0800 Subject: [PATCH 038/349] input: cleaner reverse mapping cleanup --- src/input/Binding.zig | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 0d52f23cd..2594cdbc5 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2443,9 +2443,10 @@ pub const Set = struct { // part of the reverse mapping, so we need to fix up the // reverse map (possibly restoring another trigger for the // same action). - if (!leaf.flags.performable) { - parent.set.fixupReverseForAction(leaf.action); - } + parent.set.fixupReverseForAction( + leaf.action, + parent.key_ptr.*, + ); }, } } @@ -2532,7 +2533,10 @@ pub const Set = struct { }, // For an action we need to fix up the reverse mapping. - .leaf => |leaf| self.fixupReverseForAction(leaf.action), + .leaf => |leaf| self.fixupReverseForAction( + leaf.action, + t, + ), // Chained leaves are never in our reverse mapping so no // cleanup is required. @@ -2550,19 +2554,35 @@ pub const Set = struct { /// to point to that binding. Otherwise, we remove the action from the /// reverse mapping entirely. /// + /// The `old` parameter is the trigger that was previously bound to this + /// action. It is used to check if the reverse mapping still points to + /// this trigger; if not, no fixup is needed since the reverse map already + /// points to a different trigger for this action. + /// /// Note: we'd LIKE to replace this with the most recent binding but /// our hash map obviously has no concept of ordering so we have to /// choose whatever. Maybe a switch to an array hash map here. - fn fixupReverseForAction(self: *Set, action: Action) void { - const action_hash = action.hash(); + fn fixupReverseForAction( + self: *Set, + action: Action, + old: Trigger, + ) void { + const entry = self.reverse.getEntry(action) orelse return; + // If our value is not the same as the old trigger, we can + // ignore it because our reverse mapping points somewhere else. + if (!entry.value_ptr.eql(old)) return; + + // It is the same trigger, so let's now go through our bindings + // and try to find another trigger that maps to the same action. + const action_hash = action.hash(); var it = self.bindings.iterator(); while (it.next()) |it_entry| { switch (it_entry.value_ptr.*) { .leader, .leaf_chained => {}, .leaf => |leaf_search| { if (leaf_search.action.hash() == action_hash) { - self.reverse.putAssumeCapacity(action, it_entry.key_ptr.*); + entry.value_ptr.* = it_entry.key_ptr.*; return; } }, -- 2.51.2 From b8fe66a70162712d845b7ff9bd5c989d629fde2c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 12:58:04 -0800 Subject: [PATCH 039/349] input: parseAndPut handles chains --- src/input/Binding.zig | 189 +++++++++++++++++++++++++++++++++++------- 1 file changed, 161 insertions(+), 28 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2594cdbc5..66ebb49a2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2166,6 +2166,7 @@ pub const Set = struct { // We use recursion so that we can utilize the stack as our state // for cleanup. const updated_set_ = self.parseAndPutRecurse( + self, alloc, &it, ) catch |err| err: { @@ -2178,6 +2179,12 @@ pub const Set = struct { // is invalid because we handle all valid cases. error.UnexpectedEndOfInput => return error.InvalidFormat, + // If we had a chain without a parent then the format is wrong. + error.NoChainParent => return error.InvalidFormat, + + // If we had an invalid action for a chain (e.g. unbind). + error.InvalidChainAction => return error.InvalidFormat, + // Unrecoverable error.OutOfMemory => return error.OutOfMemory, } @@ -2202,12 +2209,15 @@ pub const Set = struct { const ParseAndPutRecurseError = Allocator.Error || error{ SequenceUnbind, + NoChainParent, UnexpectedEndOfInput, + InvalidChainAction, }; /// Returns the set that was ultimately updated if a binding was /// added. Unbind does not return a set since nothing was added. fn parseAndPutRecurse( + root: *Set, set: *Set, alloc: Allocator, it: *Parser, @@ -2225,7 +2235,7 @@ pub const Set = struct { if (old) |entry| switch (entry) { // We have an existing leader for this key already // so recurse into this set. - .leader => |s| return parseAndPutRecurse( + .leader => |s| return root.parseAndPutRecurse( s, alloc, it, @@ -2238,7 +2248,9 @@ pub const Set = struct { return error.SequenceUnbind; } else null, + error.NoChainParent, error.UnexpectedEndOfInput, + error.InvalidChainAction, error.OutOfMemory, => err, }, @@ -2260,7 +2272,7 @@ pub const Set = struct { try set.bindings.put(alloc, t, .{ .leader = next }); // Recurse - return parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { + return root.parseAndPutRecurse(next, alloc, it) catch |err| switch (err) { // If our action was to unbind, we restore the old // action if we have it. error.SequenceUnbind => { @@ -2279,7 +2291,9 @@ pub const Set = struct { return null; }, + error.NoChainParent, error.UnexpectedEndOfInput, + error.InvalidChainAction, error.OutOfMemory, => return err, }; @@ -2302,8 +2316,12 @@ pub const Set = struct { }, }, - .chain => { - // TODO: Do this, ignore for now. + .chain => |action| { + // Chains can only happen on the root. + assert(set == root); + // Unbind is not valid for chains. + if (action == .unbind) return error.InvalidChainAction; + try set.appendChain(alloc, action); return set; }, } @@ -2405,6 +2423,9 @@ pub const Set = struct { alloc: Allocator, action: Action, ) (Allocator.Error || error{NoChainParent})!void { + // Unbind is not a valid chain action; callers must check this. + assert(action != .unbind); + const parent = self.chain_parent orelse return error.NoChainParent; switch (parent.value_ptr.*) { // Leader can never be a chain parent. Verified through various @@ -3879,30 +3900,142 @@ test "set: consumed state" { try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); } -// test "set: parseAndPut chain" { -// const testing = std.testing; -// const alloc = testing.allocator; -// -// var s: Set = .{}; -// defer s.deinit(alloc); -// -// try s.parseAndPut(alloc, "a=new_window"); -// try s.parseAndPut(alloc, "chain=new_tab"); -// -// // Creates forward mapping -// { -// const action = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf; -// try testing.expect(action.action == .new_window); -// try testing.expectEqual(Flags{}, action.flags); -// } -// -// // Does not create reverse mapping, because reverse mappings are only for -// // non-chain actions. -// { -// const trigger = s.getTrigger(.new_window); -// try testing.expect(trigger == null); -// } -// } +test "set: parseAndPut chain" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Creates forward mapping as leaf_chained + { + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf_chained); + const chained = entry.leaf_chained; + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); + } + + // Does not create reverse mapping, because reverse mappings are only for + // non-chained actions. + { + try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); + } +} + +test "set: parseAndPut chain without parent is error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Chain without a prior binding should fail + try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "chain=new_tab")); +} + +test "set: parseAndPut chain multiple times" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + try s.parseAndPut(alloc, "chain=close_surface"); + + // Should have 3 actions chained + { + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf_chained); + const chained = entry.leaf_chained; + try testing.expectEqual(@as(usize, 3), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); + try testing.expect(chained.actions.items[2] == .close_surface); + } +} + +test "set: parseAndPut chain preserves flags" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "unconsumed:a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Should preserve unconsumed flag + { + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf_chained); + const chained = entry.leaf_chained; + try testing.expect(!chained.flags.consumed); + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + } +} + +test "set: parseAndPut chain after unbind is error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "a=unbind"); + + // Chain after unbind should fail because chain_parent is cleared + try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "chain=new_tab")); +} + +test "set: parseAndPut chain on sequence" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a>b=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Navigate to the inner set + const a_entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(a_entry == .leader); + const inner_set = a_entry.leader; + + // Check the chained binding + const b_entry = inner_set.get(.{ .key = .{ .unicode = 'b' } }).?.value_ptr.*; + try testing.expect(b_entry == .leaf_chained); + const chained = b_entry.leaf_chained; + try testing.expectEqual(@as(usize, 2), chained.actions.items.len); + try testing.expect(chained.actions.items[0] == .new_window); + try testing.expect(chained.actions.items[1] == .new_tab); +} + +test "set: parseAndPut chain with unbind is error" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "a=new_window"); + + // chain=unbind is not valid + try testing.expectError(error.InvalidFormat, s.parseAndPut(alloc, "chain=unbind")); + + // Original binding should still exist + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*; + try testing.expect(entry == .leaf); + try testing.expect(entry.leaf.action == .new_window); +} test "set: getEvent physical" { const testing = std.testing; -- 2.51.2 From 442146cf9f8a60f4b4d127199b162df662588971 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 13:13:34 -0800 Subject: [PATCH 040/349] input: implement leaf_chained clone --- src/input/Binding.zig | 75 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 66ebb49a2..85c3c2942 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2093,6 +2093,21 @@ pub const Set = struct { actions: std.ArrayList(Action), flags: Flags, + pub fn clone( + self: LeafChained, + alloc: Allocator, + ) Allocator.Error!LeafChained { + var cloned_actions = try self.actions.clone(alloc); + errdefer cloned_actions.deinit(alloc); + for (cloned_actions.items) |*action| { + action.* = try action.clone(alloc); + } + return .{ + .actions = cloned_actions, + .flags = self.flags, + }; + } + pub fn deinit(self: *LeafChained, alloc: Allocator) void { self.actions.deinit(alloc); } @@ -2630,7 +2645,7 @@ pub const Set = struct { // contain allocated strings). .leaf => |*s| s.* = try s.clone(alloc), - .leaf_chained => @panic("TODO"), + .leaf_chained => |*s| s.* = try s.clone(alloc), // Must be deep cloned. .leader => |*s| { @@ -3619,6 +3634,64 @@ test "set: clone produces null chain_parent" { try testing.expect(cloned.get(.{ .key = .{ .unicode = 'a' } }) != null); } +test "set: clone with leaf_chained" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding using parseAndPut with chain= + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Verify we have a leaf_chained + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + try testing.expectEqual(@as(usize, 2), entry.value_ptr.leaf_chained.actions.items.len); + + // Clone the set + var cloned = try s.clone(alloc); + defer cloned.deinit(alloc); + + // Verify the cloned set has the leaf_chained with same actions + const cloned_entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(cloned_entry.value_ptr.* == .leaf_chained); + try testing.expectEqual(@as(usize, 2), cloned_entry.value_ptr.leaf_chained.actions.items.len); + try testing.expect(cloned_entry.value_ptr.leaf_chained.actions.items[0] == .new_window); + try testing.expect(cloned_entry.value_ptr.leaf_chained.actions.items[1] == .new_tab); +} + +test "set: clone with leaf_chained containing allocated data" { + const testing = std.testing; + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var s: Set = .{}; + + // Create a chained binding with text actions (which have allocated strings) + try s.parseAndPut(alloc, "a=text:hello"); + try s.parseAndPut(alloc, "chain=text:world"); + + // Verify we have a leaf_chained + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + // Clone the set + const cloned = try s.clone(alloc); + + // Verify the cloned set has independent copies of the text + const cloned_entry = cloned.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(cloned_entry.value_ptr.* == .leaf_chained); + try testing.expectEqualStrings("hello", cloned_entry.value_ptr.leaf_chained.actions.items[0].text); + try testing.expectEqualStrings("world", cloned_entry.value_ptr.leaf_chained.actions.items[1].text); + + // Verify the pointers are different (truly cloned, not shared) + try testing.expect(entry.value_ptr.leaf_chained.actions.items[0].text.ptr != + cloned_entry.value_ptr.leaf_chained.actions.items[0].text.ptr); +} + test "set: parseAndPut sequence" { const testing = std.testing; const alloc = testing.allocator; -- 2.51.2 From 578b4c284b80fed9e07e121ada4e2515d17c8f58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 13:19:09 -0800 Subject: [PATCH 041/349] apprt/gtk: handle global actions with chains --- src/apprt/gtk/class/global_shortcuts.zig | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/class/global_shortcuts.zig b/src/apprt/gtk/class/global_shortcuts.zig index 57652916a..718b371fd 100644 --- a/src/apprt/gtk/class/global_shortcuts.zig +++ b/src/apprt/gtk/class/global_shortcuts.zig @@ -169,13 +169,16 @@ pub const GlobalShortcuts = extern struct { var trigger_buf: [1024]u8 = undefined; var it = config.keybind.set.bindings.iterator(); while (it.next()) |entry| { - const leaf = switch (entry.value_ptr.*) { - // Global shortcuts can't have leaders + const leaf: Binding.Set.GenericLeaf = switch (entry.value_ptr.*) { .leader => continue, - .leaf => |leaf| leaf, + inline .leaf, .leaf_chained => |leaf| leaf.generic(), }; if (!leaf.flags.global) continue; + // We only allow global keybinds that map to exactly a single + // action for now. TODO: remove this restriction + if (leaf.actions.len != 1) continue; + const trigger = if (key.xdgShortcutFromTrigger( &trigger_buf, entry.key_ptr.*, @@ -197,7 +200,7 @@ pub const GlobalShortcuts = extern struct { try priv.map.put( alloc, try alloc.dupeZ(u8, trigger), - leaf.action, + leaf.actions[0], ); } -- 2.51.2 From e4c7d4e059eb46fc98e63f2c7c9291e6354a6463 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 13:22:38 -0800 Subject: [PATCH 042/349] input: handle unbind cleanup for leaf chains --- src/input/Binding.zig | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 85c3c2942..b02d67019 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2300,7 +2300,25 @@ pub const Set = struct { leaf.action, leaf.flags, ) catch {}, - .leaf_chained => @panic("TODO"), + + .leaf_chained => |leaf| chain: { + // Rebuild our chain + set.putFlags( + alloc, + t, + leaf.actions.items[0], + leaf.flags, + ) catch break :chain; + for (leaf.actions.items[1..]) |action| { + set.appendChain( + alloc, + action, + ) catch { + set.remove(alloc, t); + break :chain; + }; + } + }, }; return null; -- 2.51.2 From 7dd903588b5ddcab6c67256b6590b869b71af9a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 13:27:40 -0800 Subject: [PATCH 043/349] input: formatter for chained entries --- src/input/Binding.zig | 117 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b02d67019..83c6ef38f 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2052,7 +2052,19 @@ pub const Set = struct { try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); }, - .leaf_chained => @panic("TODO"), + .leaf_chained => |leaf| { + const pos = buffer.end; + for (leaf.actions.items, 0..) |action, i| { + if (i == 0) { + buffer.print("={f}", .{action}) catch return error.OutOfMemory; + } else { + buffer.end = 0; + buffer.print("chain={f}", .{action}) catch return error.OutOfMemory; + } + try formatter.formatEntry([]const u8, buffer.buffer[0..buffer.end]); + buffer.end = pos; + } + }, } } }; @@ -4615,3 +4627,106 @@ test "set: appendChain restores next valid reverse mapping" { try testing.expect(trigger.key.unicode == 'a'); } } + +test "set: formatEntries leaf_chained" { + const testing = std.testing; + const alloc = testing.allocator; + const formatterpkg = @import("../config/formatter.zig"); + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding + try s.parseAndPut(alloc, "a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + + // Verify it's a leaf_chained + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + + // Format the entries + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + + // Write the trigger first (as formatEntry in Config.zig does) + try entry.key_ptr.format(&writer); + try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer)); + + const expected = + \\keybind = a=new_window + \\keybind = chain=new_tab + \\ + ; + try testing.expectEqualStrings(expected, output.written()); +} + +test "set: formatEntries leaf_chained multiple chains" { + const testing = std.testing; + const alloc = testing.allocator; + const formatterpkg = @import("../config/formatter.zig"); + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding with 3 actions + try s.parseAndPut(alloc, "ctrl+a=new_window"); + try s.parseAndPut(alloc, "chain=new_tab"); + try s.parseAndPut(alloc, "chain=close_surface"); + + // Verify it's a leaf_chained with 3 actions + const entry = s.get(.{ .key = .{ .unicode = 'a' }, .mods = .{ .ctrl = true } }).?; + try testing.expect(entry.value_ptr.* == .leaf_chained); + try testing.expectEqual(@as(usize, 3), entry.value_ptr.leaf_chained.actions.items.len); + + // Format the entries + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + + try entry.key_ptr.format(&writer); + try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer)); + + const expected = + \\keybind = ctrl+a=new_window + \\keybind = chain=new_tab + \\keybind = chain=close_surface + \\ + ; + try testing.expectEqualStrings(expected, output.written()); +} + +test "set: formatEntries leaf_chained with text action" { + const testing = std.testing; + const alloc = testing.allocator; + const formatterpkg = @import("../config/formatter.zig"); + + var s: Set = .{}; + defer s.deinit(alloc); + + // Create a chained binding with text actions + try s.parseAndPut(alloc, "a=text:hello"); + try s.parseAndPut(alloc, "chain=text:world"); + + // Format the entries + var output: std.Io.Writer.Allocating = .init(alloc); + defer output.deinit(); + + var buf: [1024]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + + const entry = s.get(.{ .key = .{ .unicode = 'a' } }).?; + try entry.key_ptr.format(&writer); + try entry.value_ptr.formatEntries(&writer, formatterpkg.entryFormatter("keybind", &output.writer)); + + const expected = + \\keybind = a=text:hello + \\keybind = chain=text:world + \\ + ; + try testing.expectEqualStrings(expected, output.written()); +} -- 2.51.2 From 99325a3d451bdf5b3bb64b2569633f6f16f2e3c8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 13:32:12 -0800 Subject: [PATCH 044/349] config: docs for chains --- src/config/Config.zig | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index f75944aeb..a2ce88320 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1667,6 +1667,44 @@ class: ?[:0]const u8 = null, /// - Notably, global shortcuts have not been implemented on wlroots-based /// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). /// +/// ## Chained Actions +/// +/// A keybind can have multiple actions by using the `chain` keyword for +/// subsequent actions. When a keybind is activated, all chained actions are +/// executed in order. The syntax is: +/// +/// ```ini +/// keybind = ctrl+a=new_window +/// keybind = chain=goto_split:left +/// ``` +/// +/// This binds `ctrl+a` to first open a new window, then move focus to the +/// left split. Each `chain` entry appends an action to the most recently +/// defined keybind. You can chain as many actions as you want: +/// +/// ```ini +/// keybind = ctrl+a=new_window +/// keybind = chain=goto_split:left +/// keybind = chain=toggle_fullscreen +/// ``` +/// +/// Chained actions cannot have prefixes like `global:` or `unconsumed:`. +/// The flags from the original keybind apply to the entire chain. +/// +/// Chained actions work with key sequences as well. For example: +/// +/// ```ini +/// keybind = ctrl+a>n=new_window +/// keybind = chain=goto_split:left +/// ```` +/// +/// Chains with key sequences apply to the most recent binding in the +/// sequence. +/// +/// Chained keybinds are available since Ghostty 1.3.0. +/// +/// ## Key Tables +/// /// You may also create a named set of keybindings known as a "key table." /// A key table must be explicitly activated for the bindings to become /// available. This can be used to implement features such as a -- 2.51.2 From 931c6c71f2f522f1ac19563035879a75cdbc12cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 13:38:46 -0800 Subject: [PATCH 045/349] fix up gtk --- src/apprt/gtk/class/global_shortcuts.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/class/global_shortcuts.zig b/src/apprt/gtk/class/global_shortcuts.zig index 718b371fd..cf0f31a6e 100644 --- a/src/apprt/gtk/class/global_shortcuts.zig +++ b/src/apprt/gtk/class/global_shortcuts.zig @@ -177,7 +177,8 @@ pub const GlobalShortcuts = extern struct { // We only allow global keybinds that map to exactly a single // action for now. TODO: remove this restriction - if (leaf.actions.len != 1) continue; + const actions = leaf.actionsSlice(); + if (actions.len != 1) continue; const trigger = if (key.xdgShortcutFromTrigger( &trigger_buf, @@ -200,7 +201,7 @@ pub const GlobalShortcuts = extern struct { try priv.map.put( alloc, try alloc.dupeZ(u8, trigger), - leaf.actions[0], + actions[0], ); } -- 2.51.2 From 76c0bdf559f466c56803e9b2f72a0790cbad07ad Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 22 Dec 2025 17:48:04 -0600 Subject: [PATCH 046/349] input: fix performable bindings --- src/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0ce758636..0fb0c034b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2941,8 +2941,8 @@ fn maybeHandleBinding( // actions perform. var performed: bool = false; for (actions) |action| { - if (self.performBindingAction(action)) |_| { - performed = true; + if (self.performBindingAction(action)) |performed_| { + performed = performed or performed_; } else |err| { log.info( "key binding action failed action={t} err={}", -- 2.51.2 From dcbb3fe56fbc67ed5257fd4233bedfd69e5e1a3c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 20:34:23 -0800 Subject: [PATCH 047/349] inspector: show chained bindings --- src/Surface.zig | 18 ++++++++++++++---- src/inspector/key.zig | 24 +++++++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0fb0c034b..ea17c6104 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2941,8 +2941,8 @@ fn maybeHandleBinding( // actions perform. var performed: bool = false; for (actions) |action| { - if (self.performBindingAction(action)) |performed_| { - performed = performed or performed_; + if (self.performBindingAction(action)) |v| { + performed = performed or v; } else |err| { log.info( "key binding action failed action={t} err={}", @@ -2991,8 +2991,18 @@ fn maybeHandleBinding( // Store our last trigger so we don't encode the release event self.keyboard.last_trigger = event.bindingHash(); - // TODO: Inspector must support chained events - if (insp_ev) |ev| ev.binding = actions[0]; + if (insp_ev) |ev| { + ev.binding = self.alloc.dupe( + input.Binding.Action, + actions, + ) catch |err| binding: { + log.warn( + "error allocating binding action for inspector err={}", + .{err}, + ); + break :binding &.{}; + }; + } return .consumed; } diff --git a/src/inspector/key.zig b/src/inspector/key.zig index dbccb47a8..e42e4f23c 100644 --- a/src/inspector/key.zig +++ b/src/inspector/key.zig @@ -13,7 +13,8 @@ pub const Event = struct { event: input.KeyEvent, /// The binding that was triggered as a result of this event. - binding: ?input.Binding.Action = null, + /// Multiple bindings are possible if they are chained. + binding: []const input.Binding.Action = &.{}, /// The data sent to the pty as a result of this keyboard event. /// This is allocated using the inspector allocator. @@ -32,6 +33,7 @@ pub const Event = struct { } pub fn deinit(self: *const Event, alloc: Allocator) void { + alloc.free(self.binding); if (self.event.utf8.len > 0) alloc.free(self.event.utf8); if (self.pty.len > 0) alloc.free(self.pty); } @@ -79,12 +81,28 @@ pub const Event = struct { ); defer cimgui.c.igEndTable(); - if (self.binding) |binding| { + if (self.binding.len > 0) { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); cimgui.c.igText("Triggered Binding"); _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(binding).ptr); + + const height: f32 = height: { + const item_count: f32 = @floatFromInt(@min(self.binding.len, 5)); + const padding = cimgui.c.igGetStyle().*.FramePadding.y * 2; + break :height cimgui.c.igGetTextLineHeightWithSpacing() * item_count + padding; + }; + if (cimgui.c.igBeginListBox("##bindings", .{ .x = 0, .y = height })) { + defer cimgui.c.igEndListBox(); + for (self.binding) |action| { + _ = cimgui.c.igSelectable_Bool( + @tagName(action).ptr, + false, + cimgui.c.ImGuiSelectableFlags_None, + .{ .x = 0, .y = 0 }, + ); + } + } } pty: { -- 2.51.2 From c11febd0dd8d31e20c70227e0e1af6e212181b1b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 20:42:50 -0800 Subject: [PATCH 048/349] cli/list-keybinds: support chained keybindings --- src/cli/list_keybinds.zig | 54 +++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index fb7ad19ec..2fb900e48 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -96,7 +96,7 @@ const TriggerNode = struct { const ChordBinding = struct { triggers: std.SinglyLinkedList, - action: Binding.Action, + actions: []const Binding.Action, // Order keybinds based on various properties // 1. Longest chord sequence @@ -281,16 +281,32 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { } } - 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(&.{ - .{ .text = action[0..idx] }, - .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } }, - .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } }, - }, .{ .col_offset = widest_chord + 3 }); - } else { - _ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 }); + var action_col: u16 = widest_chord + 3; + for (bind.actions, 0..) |act, i| { + if (i > 0) { + const chain_result = win.printSegment( + .{ .text = ", ", .style = .{ .dim = true } }, + .{ .col_offset = action_col }, + ); + action_col = chain_result.col; + } + + const action = try std.fmt.allocPrint(alloc, "{f}", .{act}); + // If our action has an argument, we print the argument in a different color + if (std.mem.indexOfScalar(u8, action, ':')) |idx| { + const print_result = win.print(&.{ + .{ .text = action[0..idx] }, + .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } }, + .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } }, + }, .{ .col_offset = action_col }); + action_col = print_result.col; + } else { + const print_result = win.printSegment( + .{ .text = action }, + .{ .col_offset = action_col }, + ); + action_col = print_result.col; + } } try vx.prettyPrint(writer); } @@ -346,14 +362,24 @@ fn iterateBindings( const node = try alloc.create(TriggerNode); node.* = .{ .data = bind.key_ptr.* }; + const actions = try alloc.alloc(Binding.Action, 1); + actions[0] = leaf.action; + widest_chord = @max(widest_chord, width); try bindings.append(alloc, .{ .triggers = .{ .first = &node.node }, - .action = leaf.action, + .actions = actions, }); }, - .leaf_chained => { - // TODO: Show these. + .leaf_chained => |leaf| { + const node = try alloc.create(TriggerNode); + node.* = .{ .data = bind.key_ptr.* }; + + widest_chord = @max(widest_chord, width); + try bindings.append(alloc, .{ + .triggers = .{ .first = &node.node }, + .actions = leaf.actions.items, + }); }, } } -- 2.51.2 From 56f5a14dde230907e069b15955aacefd5a85a354 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 20:57:06 -0800 Subject: [PATCH 049/349] config: improve key table parsing robustness Fixes #10020 This improves parsing key tables so that the following edge cases are now handled correctly, which were regressions from prior tip behavior: - `/=action` - `ctrl+/=action` - `table//=action` (valid to bind `/` in a table) - `table/a>//=action` (valid to bind a table with a sequence) --- src/config/Config.zig | 122 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 8f1cece45..52141293a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1676,8 +1676,10 @@ class: ?[:0]const u8 = null, /// Key tables are defined using the syntax `
/`. The /// `` value is everything documented above for keybinds. The /// `
` value is the name of the key table. Table names can contain -/// anything except `/` and `=`. For example `foo/ctrl+a=new_window` -/// defines a binding within a table named `foo`. +/// anything except `/`, `=`, `+`, and `>`. The characters `+` and `>` are +/// reserved for keybind syntax (modifier combinations and key sequences). +/// For example `foo/ctrl+a=new_window` defines a binding within a table +/// named `foo`. /// /// Tables are activated and deactivated using the binding actions /// `activate_key_table:` and `deactivate_key_table`. Other table @@ -6644,12 +6646,21 @@ pub const Keybinds = struct { // We look for '/' only before the first '=' to avoid matching // action arguments like "foo=text:/hello". const eq_idx = std.mem.indexOfScalar(u8, value, '=') orelse value.len; - if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| { + if (std.mem.indexOfScalar(u8, value[0..eq_idx], '/')) |slash_idx| table: { const table_name = value[0..slash_idx]; - const binding = value[slash_idx + 1 ..]; - // Table name cannot be empty - if (table_name.len == 0) return error.InvalidFormat; + // Length zero is valid, so you can set `/=action` for the slash key + if (table_name.len == 0) break :table; + + // Ignore '+', '>' because they can be part of sequences and + // triggers. This lets things like `ctrl+/=action` work. + if (std.mem.indexOfAny( + u8, + table_name, + "+>", + ) != null) break :table; + + const binding = value[slash_idx + 1 ..]; // Get or create the table const gop = try self.tables.getOrPut(alloc, table_name); @@ -7002,6 +7013,105 @@ pub const Keybinds = struct { try testing.expectEqual(0, keybinds.tables.count()); } + test "parseCLI slash as key with modifier is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // ctrl+/ should be parsed as a keybind with '/' as the key, not a table + try keybinds.parseCLI(alloc, "ctrl+/=text:foo"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI shift+slash as key is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // shift+/ should be parsed as a keybind, not a table + try keybinds.parseCLI(alloc, "shift+/=ignore"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI bare slash as key is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Bare / as a key should work (empty table name is rejected) + try keybinds.parseCLI(alloc, "/=text:foo"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI slash in key sequence is not a table" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Key sequence ending with / should work + try keybinds.parseCLI(alloc, "ctrl+a>ctrl+/=new_window"); + + // Should be in root set, not a table + try testing.expectEqual(1, keybinds.set.bindings.count()); + try testing.expectEqual(0, keybinds.tables.count()); + } + + test "parseCLI table with slash in binding" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Table with a binding that uses / as the key + try keybinds.parseCLI(alloc, "mytable//=text:foo"); + + // Should be in the table + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("mytable")); + try testing.expectEqual(1, keybinds.tables.get("mytable").?.bindings.count()); + } + + test "parseCLI table with sequence containing slash" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var keybinds: Keybinds = .{}; + + // Table with a key sequence that ends with / + try keybinds.parseCLI(alloc, "mytable/a>/=new_window"); + + // Should be in the table + try testing.expectEqual(0, keybinds.set.bindings.count()); + try testing.expectEqual(1, keybinds.tables.count()); + try testing.expect(keybinds.tables.contains("mytable")); + } + test "clone with tables" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); -- 2.51.2 From 6720076c952123e96578f1eec93111badd2a1b8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Dec 2025 21:18:59 -0800 Subject: [PATCH 050/349] ci: update macOS builds to use Xcode 26.2 We were fixed on 26.0 previously. --- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 6 +++--- .github/workflows/test.yml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 748965513..960ff4efe 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -143,7 +143,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index fb6aef87d..3af59e7a5 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -232,7 +232,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -466,7 +466,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -650,7 +650,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30f34120a..b91555f2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -456,7 +456,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -499,7 +499,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version @@ -764,7 +764,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_26.0.app + run: sudo xcode-select -s /Applications/Xcode_26.2.app - name: Xcode Version run: xcodebuild -version -- 2.51.2 From 12815f7fa32460c6168d65209f8da7a252a855b2 Mon Sep 17 00:00:00 2001 From: kadekillary Date: Tue, 23 Dec 2025 08:19:45 -0600 Subject: [PATCH 051/349] feat(cli): list keybindings from key tables - Display keybindings grouped by their source table, with table name as prefix - Sort default bindings before table bindings, maintaining visual hierarchy - Support keybindings defined in key tables alongside default bindings - Enable users to discover all available keybindings across the entire config --- src/cli/list_keybinds.zig | 61 ++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index 2fb900e48..61050d0cb 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -95,18 +95,35 @@ const TriggerNode = struct { }; const ChordBinding = struct { + table_name: ?[]const u8 = null, triggers: std.SinglyLinkedList, actions: []const Binding.Action, // Order keybinds based on various properties - // 1. Longest chord sequence - // 2. Most active modifiers - // 3. Alphabetically by active modifiers - // 4. Trigger key order + // 1. Default bindings before table bindings (tables grouped at end) + // 2. Longest chord sequence + // 3. Most active modifiers + // 4. Alphabetically by active modifiers + // 5. Trigger key order + // 6. Within tables, sort by table name // These properties propagate through chorded keypresses // // Adapted from Binding.lessThan pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool { + const lhs_has_table = lhs.table_name != null; + const rhs_has_table = rhs.table_name != null; + + if (lhs_has_table != rhs_has_table) { + return !lhs_has_table; + } + + if (lhs_has_table) { + const table_cmp = std.mem.order(u8, lhs.table_name.?, rhs.table_name.?); + if (table_cmp != .eq) { + return table_cmp == .lt; + } + } + const lhs_len = lhs.triggers.len(); const rhs_len = rhs.triggers.len(); @@ -231,10 +248,30 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { const win = vx.window(); - // Generate a list of bindings, recursively traversing chorded keybindings + // Collect default bindings, recursively flattening chords var iter = keybinds.set.bindings.iterator(); - const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win); + const default_bindings, var widest_chord = try iterateBindings(alloc, &iter, &win); + + var bindings_list: std.ArrayList(ChordBinding) = .empty; + try bindings_list.appendSlice(alloc, default_bindings); + + // Collect key table bindings + var widest_table_prefix: u16 = 0; + var table_iter = keybinds.tables.iterator(); + while (table_iter.next()) |table_entry| { + const table_name = table_entry.key_ptr.*; + var binding_iter = table_entry.value_ptr.bindings.iterator(); + const table_bindings, const table_width = try iterateBindings(alloc, &binding_iter, &win); + for (table_bindings) |*b| { + b.table_name = table_name; + } + + try bindings_list.appendSlice(alloc, table_bindings); + widest_chord = @max(widest_chord, table_width); + widest_table_prefix = @max(widest_table_prefix, @as(u16, @intCast(win.gwidth(table_name) + win.gwidth("/")))); + } + const bindings = bindings_list.items; std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan); // Set up styles for each modifier @@ -242,12 +279,22 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { const ctrl_style: vaxis.Style = .{ .fg = .{ .index = 2 } }; const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } }; const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } }; + const table_style: vaxis.Style = .{ .fg = .{ .index = 8 } }; // Print the list for (bindings) |bind| { win.clear(); var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; + + if (bind.table_name) |name| { + result = win.printSegment( + .{ .text = name, .style = table_style }, + .{ .col_offset = result.col }, + ); + result = win.printSegment(.{ .text = "/", .style = table_style }, .{ .col_offset = result.col }); + } + var maybe_trigger = bind.triggers.first; while (maybe_trigger) |node| : (maybe_trigger = node.next) { const trigger: *TriggerNode = .get(node); @@ -281,7 +328,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { } } - var action_col: u16 = widest_chord + 3; + var action_col: u16 = widest_table_prefix + widest_chord + 3; for (bind.actions, 0..) |act, i| { if (i > 0) { const chain_result = win.printSegment( -- 2.51.2 From a1ee2f07648aea90a9df3d50c0a5341f1624d741 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 09:06:46 -0800 Subject: [PATCH 052/349] apprt/gtk: store key sequences/tables in surface state --- src/apprt/gtk.zig | 1 + src/apprt/gtk/class/application.zig | 35 +++++++- src/apprt/gtk/class/surface.zig | 72 ++++++++++++++++ src/apprt/gtk/key.zig | 122 ++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 415d3773d..07b4eb0e7 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -10,4 +10,5 @@ pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef; test { @import("std").testing.refAllDecls(@This()); _ = @import("gtk/ext.zig"); + _ = @import("gtk/key.zig"); } diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 1c0863f3c..b16bce049 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -669,6 +669,9 @@ pub const Application = extern struct { .inspector => return Action.controlInspector(target, value), + .key_sequence => return Action.keySequence(target, value), + .key_table => return Action.keyTable(target, value), + .mouse_over_link => Action.mouseOverLink(target, value), .mouse_shape => Action.mouseShape(target, value), .mouse_visibility => Action.mouseVisibility(target, value), @@ -743,8 +746,6 @@ pub const Application = extern struct { .toggle_visibility, .toggle_background_opacity, .cell_size, - .key_sequence, - .key_table, .render_inspector, .renderer_health, .color_change, @@ -2660,6 +2661,36 @@ const Action = struct { }, } } + + pub fn keySequence(target: apprt.Target, value: apprt.Action.Value(.key_sequence)) bool { + switch (target) { + .app => { + log.warn("key_sequence action to app is unexpected", .{}); + return false; + }, + .surface => |core| { + core.rt_surface.gobj().keySequenceAction(value) catch |err| { + log.warn("error handling key_sequence action: {}", .{err}); + }; + return true; + }, + } + } + + pub fn keyTable(target: apprt.Target, value: apprt.Action.Value(.key_table)) bool { + switch (target) { + .app => { + log.warn("key_table action to app is unexpected", .{}); + return false; + }, + .surface => |core| { + core.rt_surface.gobj().keyTableAction(value) catch |err| { + log.warn("error handling key_table action: {}", .{err}); + }; + return true; + }, + } + } }; /// This sets various GTK-related environment variables as necessary diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 93d1beeb2..c35c78302 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -617,6 +617,10 @@ pub const Surface = extern struct { vscroll_policy: gtk.ScrollablePolicy = .natural, vadj_signal_group: ?*gobject.SignalGroup = null, + // Key state tracking for key sequences and tables + key_sequence: std.ArrayListUnmanaged([:0]const u8) = .empty, + key_tables: std.ArrayListUnmanaged([:0]const u8) = .empty, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -778,6 +782,66 @@ pub const Surface = extern struct { if (priv.inspector) |v| v.queueRender(); } + /// Handle a key sequence action from the apprt. + pub fn keySequenceAction( + self: *Self, + value: apprt.action.KeySequence, + ) Allocator.Error!void { + const priv = self.private(); + const alloc = Application.default().allocator(); + + switch (value) { + .trigger => |trigger| { + // Convert the trigger to a human-readable label + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + if (gtk_key.labelFromTrigger(&buf.writer, trigger)) |success| { + if (!success) return; + } else |_| return error.OutOfMemory; + + // Make space + try priv.key_sequence.ensureUnusedCapacity(alloc, 1); + + // Copy and append + const duped = try buf.toOwnedSliceSentinel(0); + errdefer alloc.free(duped); + priv.key_sequence.appendAssumeCapacity(duped); + }, + .end => { + // Free all the stored strings and clear + for (priv.key_sequence.items) |s| alloc.free(s); + priv.key_sequence.clearAndFree(alloc); + }, + } + } + + /// Handle a key table action from the apprt. + pub fn keyTableAction( + self: *Self, + value: apprt.action.KeyTable, + ) Allocator.Error!void { + const priv = self.private(); + const alloc = Application.default().allocator(); + + switch (value) { + .activate => |name| { + // Duplicate the name string and push onto stack + const duped = try alloc.dupeZ(u8, name); + errdefer alloc.free(duped); + try priv.key_tables.append(alloc, duped); + }, + .deactivate => { + // Pop and free the top table + if (priv.key_tables.pop()) |s| alloc.free(s); + }, + .deactivate_all => { + // Free all tables and clear + for (priv.key_tables.items) |s| alloc.free(s); + priv.key_tables.clearAndFree(alloc); + }, + } + } + pub fn showOnScreenKeyboard(self: *Self, event: ?*gdk.Event) bool { const priv = self.private(); return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0; @@ -1787,6 +1851,14 @@ pub const Surface = extern struct { glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } + + // Clean up key sequence and key table state + const alloc = Application.default().allocator(); + for (priv.key_sequence.items) |s| alloc.free(s); + priv.key_sequence.deinit(alloc); + for (priv.key_tables.items) |s| alloc.free(s); + priv.key_tables.deinit(alloc); + self.clearCgroup(); gobject.Object.virtual_methods.finalize.call( diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 35c9390b2..5f717e14a 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -233,6 +233,70 @@ pub fn keyvalFromKey(key: input.Key) ?c_uint { } } +/// Converts a trigger to a human-readable label for display in UI. +/// +/// Uses GTK accelerator-style formatting (e.g., "Ctrl+Shift+A"). +/// Returns false if the trigger cannot be formatted (e.g., catch_all). +pub fn labelFromTrigger( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) std.Io.Writer.Error!bool { + // Modifiers first, using human-readable format + if (trigger.mods.super) try writer.writeAll("Super+"); + if (trigger.mods.ctrl) try writer.writeAll("Ctrl+"); + if (trigger.mods.alt) try writer.writeAll("Alt+"); + if (trigger.mods.shift) try writer.writeAll("Shift+"); + + // Write the key + return writeTriggerKeyLabel(writer, trigger); +} + +/// Writes the key portion of a trigger in human-readable format. +fn writeTriggerKeyLabel( + writer: *std.Io.Writer, + trigger: input.Binding.Trigger, +) error{WriteFailed}!bool { + switch (trigger.key) { + .physical => |k| { + const keyval = keyvalFromKey(k) orelse return false; + const name = gdk.keyvalName(keyval) orelse return false; + // Capitalize the first letter for nicer display + const span = std.mem.span(name); + if (span.len > 0) { + if (span[0] >= 'a' and span[0] <= 'z') { + try writer.writeByte(span[0] - 'a' + 'A'); + if (span.len > 1) try writer.writeAll(span[1..]); + } else { + try writer.writeAll(span); + } + } + }, + + .unicode => |cp| { + // Try to get a nice name from GDK first + if (gdk.keyvalName(cp)) |name| { + const span = std.mem.span(name); + if (span.len > 0) { + // Capitalize the first letter for nicer display + if (span[0] >= 'a' and span[0] <= 'z') { + try writer.writeByte(span[0] - 'a' + 'A'); + if (span.len > 1) try writer.writeAll(span[1..]); + } else { + try writer.writeAll(span); + } + } + } else { + // Fall back to printing the character + try writer.print("{u}", .{cp}); + } + }, + + .catch_all => return false, + } + + return true; +} + test "accelFromTrigger" { const testing = std.testing; var buf: [256]u8 = undefined; @@ -263,6 +327,64 @@ test "xdgShortcutFromTrigger" { })).?); } +test "labelFromTrigger" { + const testing = std.testing; + + // Simple unicode key with modifier + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .super = true }, + .key = .{ .unicode = 'q' }, + })); + try testing.expectEqualStrings("Super+Q", buf.written()); + } + + // Multiple modifiers + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true }, + .key = .{ .unicode = 92 }, + })); + try testing.expectEqualStrings("Super+Ctrl+Alt+Shift+Backslash", buf.written()); + } + + // Physical key + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{ .ctrl = true }, + .key = .{ .physical = .key_a }, + })); + try testing.expectEqualStrings("Ctrl+A", buf.written()); + } + + // No modifiers + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(try labelFromTrigger(&buf.writer, .{ + .mods = .{}, + .key = .{ .physical = .escape }, + })); + try testing.expectEqualStrings("Escape", buf.written()); + } + + // catch_all returns false + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + try testing.expect(!try labelFromTrigger(&buf.writer, .{ + .mods = .{}, + .key = .catch_all, + })); + } +} + /// A raw entry in the keymap. Our keymap contains mappings between /// GDK keys and our own key enum. const RawEntry = struct { c_uint, input.Key }; -- 2.51.2 From 8f44b74b331d4977c89cc928791905840f29d7f6 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 23 Dec 2025 12:22:29 -0500 Subject: [PATCH 053/349] shell-integration: add failure regression test Add a unit test to prevent regressions in our failure state. For example, we always want to set GHOSTTY_SHELL_FEATURES, even if automatic shell integration fails, because it's also used for manual shell integration (e.g. #5048). --- src/termio/shell_integration.zig | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 3a541dcae..dba4a8f32 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -70,7 +70,6 @@ pub fn setup( exe, ); - // Setup our feature env vars try setupFeatures(env, features); return result; @@ -168,6 +167,29 @@ test "force shell" { } } +test "shell integration failure" { + const testing = std.testing; + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + const result = try setup( + alloc, + "/nonexistent", + .{ .shell = "sh" }, + &env, + null, + .{ .cursor = true, .title = false, .path = false }, + ); + + try testing.expect(result == null); + try testing.expectEqualStrings("cursor", env.get("GHOSTTY_SHELL_FEATURES").?); +} + /// Set up the shell integration features environment variable. pub fn setupFeatures( env: *EnvMap, @@ -234,7 +256,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, .path = false }); + try setupFeatures(&env, std.mem.zeroes(config.ShellIntegrationFeatures)); try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } -- 2.51.2 From 1562967d5103355916b4065123caf89608b70ae4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 09:35:51 -0800 Subject: [PATCH 054/349] apprt/gtk: key state overlay --- src/apprt/gtk/build/gresource.zig | 1 + src/apprt/gtk/class/key_state_overlay.zig | 290 +++++++++++++++++++++ src/apprt/gtk/class/surface.zig | 6 + src/apprt/gtk/css/style.css | 12 + src/apprt/gtk/ui/1.2/key-state-overlay.blp | 58 +++++ src/apprt/gtk/ui/1.2/surface.blp | 3 + 6 files changed, 370 insertions(+) create mode 100644 src/apprt/gtk/class/key_state_overlay.zig create mode 100644 src/apprt/gtk/ui/1.2/key-state-overlay.blp diff --git a/src/apprt/gtk/build/gresource.zig b/src/apprt/gtk/build/gresource.zig index c77579aab..d3684c171 100644 --- a/src/apprt/gtk/build/gresource.zig +++ b/src/apprt/gtk/build/gresource.zig @@ -44,6 +44,7 @@ pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 5, .name = "inspector-window" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 2, .name = "search-overlay" }, + .{ .major = 1, .minor = 2, .name = "key-state-overlay" }, .{ .major = 1, .minor = 5, .name = "split-tree" }, .{ .major = 1, .minor = 5, .name = "split-tree-split" }, .{ .major = 1, .minor = 2, .name = "surface" }, diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig new file mode 100644 index 000000000..15dc0d502 --- /dev/null +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -0,0 +1,290 @@ +const std = @import("std"); +const adw = @import("adw"); +const glib = @import("glib"); +const gobject = @import("gobject"); +const gdk = @import("gdk"); +const gtk = @import("gtk"); + +const gresource = @import("../build/gresource.zig"); +const Common = @import("../class.zig").Common; + +const log = std.log.scoped(.gtk_ghostty_key_state_overlay); + +/// An overlay that displays the current key table stack and pending key sequence. +/// This helps users understand what key bindings are active and what keys they've +/// pressed in a multi-key sequence. +pub const KeyStateOverlay = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = adw.Bin; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyKeyStateOverlay", + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct { + pub const active = struct { + pub const name = "active"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = C.privateShallowFieldAccessor("active"), + }, + ); + }; + + pub const @"tables-text" = struct { + pub const name = "tables-text"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .default = null, + .accessor = C.privateStringFieldAccessor("tables_text"), + }, + ); + }; + + pub const @"has-tables" = struct { + pub const name = "has-tables"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ .getter = getHasTables }, + ), + }, + ); + }; + + pub const @"sequence-text" = struct { + pub const name = "sequence-text"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?[:0]const u8, + .{ + .default = null, + .accessor = C.privateStringFieldAccessor("sequence_text"), + }, + ); + }; + + pub const @"has-sequence" = struct { + pub const name = "has-sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.typedAccessor( + Self, + bool, + .{ .getter = getHasSequence }, + ), + }, + ); + }; + + pub const pending = struct { + pub const name = "pending"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = C.privateShallowFieldAccessor("pending"), + }, + ); + }; + + pub const @"valign-target" = struct { + pub const name = "valign-target"; + const impl = gobject.ext.defineProperty( + name, + Self, + gtk.Align, + .{ + .default = .end, + .accessor = C.privateShallowFieldAccessor("valign_target"), + }, + ); + }; + }; + + const Private = struct { + /// Whether the overlay is active/visible. + active: bool = false, + + /// The formatted key table stack text (e.g., "default โ€บ vim"). + tables_text: ?[:0]const u8 = null, + + /// The formatted key sequence text (e.g., "Ctrl+A B"). + sequence_text: ?[:0]const u8 = null, + + /// Whether we're waiting for more keys in a sequence. + pending: bool = false, + + /// Target vertical alignment for the overlay. + valign_target: gtk.Align = .end, + + pub var offset: c_int = 0; + }; + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Set dummy data for UI iteration + const priv = self.private(); + priv.active = true; + priv.tables_text = glib.ext.dupeZ(u8, "default โ€บ vim"); + priv.sequence_text = glib.ext.dupeZ(u8, "Ctrl+A"); + priv.pending = true; + + // Notify property changes so bindings update + const obj = self.as(gobject.Object); + obj.notifyByPspec(properties.active.impl.param_spec); + obj.notifyByPspec(properties.@"tables-text".impl.param_spec); + obj.notifyByPspec(properties.@"has-tables".impl.param_spec); + obj.notifyByPspec(properties.@"sequence-text".impl.param_spec); + obj.notifyByPspec(properties.@"has-sequence".impl.param_spec); + obj.notifyByPspec(properties.pending.impl.param_spec); + } + + fn getHasTables(self: *Self) bool { + return self.private().tables_text != null; + } + + fn getHasSequence(self: *Self) bool { + return self.private().sequence_text != null; + } + + fn closureShowChevron( + _: *Self, + has_tables: bool, + has_sequence: bool, + ) callconv(.c) c_int { + return if (has_tables and has_sequence) 1 else 0; + } + + //--------------------------------------------------------------- + // Template callbacks + + fn onDragEnd( + _: *gtk.GestureDrag, + _: f64, + offset_y: f64, + self: *Self, + ) callconv(.c) void { + // Key state overlay only moves between top-center and bottom-center. + // Horizontal alignment is always center. + const priv = self.private(); + const widget = self.as(gtk.Widget); + const parent = widget.getParent() orelse return; + + const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight()); + const self_height: f64 = @floatFromInt(widget.getAllocatedHeight()); + + const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height; + const new_y = self_y + offset_y + (self_height / 2); + + const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start; + + if (new_valign != priv.valign_target) { + priv.valign_target = new_valign; + self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec); + self.as(gtk.Widget).queueResize(); + } + } + + //--------------------------------------------------------------- + // Virtual methods + + fn dispose(self: *Self) callconv(.c) void { + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn finalize(self: *Self) callconv(.c) void { + const priv = self.private(); + + if (priv.tables_text) |v| { + glib.free(@ptrCast(@constCast(v))); + } + if (priv.sequence_text) |v| { + glib.free(@ptrCast(@constCast(v))); + } + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + comptime gresource.blueprint(.{ + .major = 1, + .minor = 2, + .name = "key-state-overlay", + }), + ); + + // Template Callbacks + class.bindTemplateCallback("on_drag_end", &onDragEnd); + class.bindTemplateCallback("show_chevron", &closureShowChevron); + + // Properties + gobject.ext.registerProperties(class, &.{ + properties.active.impl, + properties.@"tables-text".impl, + properties.@"has-tables".impl, + properties.@"sequence-text".impl, + properties.@"has-sequence".impl, + properties.pending.impl, + properties.@"valign-target".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/class/surface.zig b/src/apprt/gtk/class/surface.zig index c35c78302..50d7f3dc2 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -26,6 +26,7 @@ const Application = @import("application.zig").Application; const Config = @import("config.zig").Config; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; const SearchOverlay = @import("search_overlay.zig").SearchOverlay; +const KeyStateOverlay = @import("key_state_overlay.zig").KeyStateOverlay; const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited; const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog; const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog; @@ -553,6 +554,9 @@ pub const Surface = extern struct { /// The search overlay search_overlay: *SearchOverlay, + /// The key state overlay + key_state_overlay: *KeyStateOverlay, + /// The apprt Surface. rt_surface: ApprtSurface = undefined, @@ -3308,6 +3312,7 @@ pub const Surface = extern struct { fn init(class: *Class) callconv(.c) void { gobject.ext.ensureType(ResizeOverlay); gobject.ext.ensureType(SearchOverlay); + gobject.ext.ensureType(KeyStateOverlay); gobject.ext.ensureType(ChildExited); gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), @@ -3328,6 +3333,7 @@ pub const Surface = extern struct { class.bindTemplateChildPrivate("progress_bar_overlay", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); class.bindTemplateChildPrivate("search_overlay", .{}); + class.bindTemplateChildPrivate("key_state_overlay", .{}); class.bindTemplateChildPrivate("terminal_page", .{}); class.bindTemplateChildPrivate("drop_target", .{}); class.bindTemplateChildPrivate("im_context", .{}); diff --git a/src/apprt/gtk/css/style.css b/src/apprt/gtk/css/style.css index 938d23ad8..f5491b7de 100644 --- a/src/apprt/gtk/css/style.css +++ b/src/apprt/gtk/css/style.css @@ -46,6 +46,18 @@ label.url-overlay.right { outline-width: 1px; } +/* + * GhosttySurface key state overlay + */ +.key-state-overlay { + padding: 6px 10px; + margin: 8px; + border-radius: 8px; + outline-style: solid; + outline-color: #555555; + outline-width: 1px; +} + /* * GhosttySurface resize overlay */ diff --git a/src/apprt/gtk/ui/1.2/key-state-overlay.blp b/src/apprt/gtk/ui/1.2/key-state-overlay.blp new file mode 100644 index 000000000..504d2e26e --- /dev/null +++ b/src/apprt/gtk/ui/1.2/key-state-overlay.blp @@ -0,0 +1,58 @@ +using Gtk 4.0; +using Adw 1; + +template $GhosttyKeyStateOverlay: Adw.Bin { + visible: bind template.active; + valign-target: end; + halign: center; + valign: bind template.valign-target; + + GestureDrag { + button: 1; + propagation-phase: capture; + drag-end => $on_drag_end(); + } + + Adw.Bin { + Box container { + styles [ + "background", + "key-state-overlay", + ] + + orientation: horizontal; + spacing: 6; + + Image { + icon-name: "input-keyboard-symbolic"; + pixel-size: 16; + } + + Label tables_label { + visible: bind template.has-tables; + label: bind template.tables-text; + xalign: 0.0; + } + + Label chevron_label { + visible: bind $show_chevron(template.has-tables, template.has-sequence) as ; + label: "โ€บ"; + + styles [ + "dim-label", + ] + } + + Label sequence_label { + visible: bind template.has-sequence; + label: bind template.sequence-text; + xalign: 0.0; + } + + Spinner pending_spinner { + visible: bind template.pending; + spinning: bind template.pending; + } + } + } +} diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 4ebfeabfb..e9db4208e 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -155,6 +155,9 @@ Overlay terminal_page { previous-match => $search_previous_match(); } + [overlay] + $GhosttyKeyStateOverlay key_state_overlay {} + [overlay] // Apply unfocused-split-fill and unfocused-split-opacity to current surface // this is only applied when a tab has more than one surface -- 2.51.2 From 3d2aa9bd829665fcb96500317b4bb67767cfc5cc Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 23 Dec 2025 12:59:58 -0500 Subject: [PATCH 055/349] shell-integration: always call setupFeatures Our existing logic already ensured that setupFeatures() was always called, but that was happening from two code paths: explicitly when shell integration is .none and implicitly via setup(). We can simplify this by always calling setupFeatures() once, outside of the (automatic) shell integration path. There's one small behavioral change: we previously didn't set up shell features in the automatic shell integration path if we didn't have a resources directory (as a side effect). Resources are required for shell integrations, but we don't need them to export GHOSTTY_SHELL_FEATURES, which could potentially still be useful on its on. --- src/termio/Exec.zig | 15 +++++++-------- src/termio/shell_integration.zig | 9 ++------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 7c7b711fd..93ad835c5 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -750,15 +750,15 @@ const Subprocess = struct { else => "sh", } }; + // Always set up shell features (GHOSTTY_SHELL_FEATURES). These are + // used by both automatic and manual shell integrations. + try shell_integration.setupFeatures( + &env, + cfg.shell_integration_features, + ); + const force: ?shell_integration.Shell = switch (cfg.shell_integration) { .none => { - // Even if shell integration is none, we still want to - // set up the feature env vars - try shell_integration.setupFeatures( - &env, - cfg.shell_integration_features, - ); - // This is a source of confusion for users despite being // opt-in since it results in some Ghostty features not // working. We always want to log it. @@ -784,7 +784,6 @@ const Subprocess = struct { default_shell_command, &env, force, - cfg.shell_integration_features, ) orelse { log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); break :shell default_shell_command; diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index dba4a8f32..e9f85e44c 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -44,7 +44,6 @@ pub fn setup( command: config.Command, env: *EnvMap, force_shell: ?Shell, - features: config.ShellIntegrationFeatures, ) !?ShellIntegration { const exe = if (force_shell) |shell| switch (shell) { .bash => "bash", @@ -70,8 +69,6 @@ pub fn setup( exe, ); - try setupFeatures(env, features); - return result; } @@ -161,7 +158,6 @@ test "force shell" { .{ .shell = "sh" }, &env, shell, - .{}, ); try testing.expectEqual(shell, result.?.shell); } @@ -183,11 +179,10 @@ test "shell integration failure" { .{ .shell = "sh" }, &env, null, - .{ .cursor = true, .title = false, .path = false }, ); try testing.expect(result == null); - try testing.expectEqualStrings("cursor", env.get("GHOSTTY_SHELL_FEATURES").?); + try testing.expectEqual(0, env.count()); } /// Set up the shell integration features environment variable. @@ -756,7 +751,7 @@ const TmpResourcesDir = struct { path: []const u8, shell_path: []const u8, - fn init(allocator: std.mem.Allocator, shell: Shell) !TmpResourcesDir { + fn init(allocator: Allocator, shell: Shell) !TmpResourcesDir { var tmp_dir = std.testing.tmpDir(.{}); errdefer tmp_dir.cleanup(); -- 2.51.2 From 85ce7d0b04f258cb53068aa753e80d26dad15865 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:10:44 -0800 Subject: [PATCH 056/349] apprt/gtk: write StringList for boxed type of strings --- src/apprt/gtk/ext.zig | 3 + src/apprt/gtk/ext/slice.zig | 106 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/apprt/gtk/ext/slice.zig diff --git a/src/apprt/gtk/ext.zig b/src/apprt/gtk/ext.zig index 9b1eeecc6..df9ab4ea2 100644 --- a/src/apprt/gtk/ext.zig +++ b/src/apprt/gtk/ext.zig @@ -12,6 +12,8 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); pub const actions = @import("ext/actions.zig"); +const slice = @import("ext/slice.zig"); +pub const StringList = slice.StringList; /// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`. pub fn boxedCopy(comptime T: type, ptr: *const T) *T { @@ -64,4 +66,5 @@ pub fn gValueHolds(value_: ?*const gobject.Value, g_type: gobject.Type) bool { test { _ = actions; + _ = slice; } diff --git a/src/apprt/gtk/ext/slice.zig b/src/apprt/gtk/ext/slice.zig new file mode 100644 index 000000000..a746d8045 --- /dev/null +++ b/src/apprt/gtk/ext/slice.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const glib = @import("glib"); +const gobject = @import("gobject"); + +/// A boxed type that holds a list of string slices. +pub const StringList = struct { + arena: ArenaAllocator, + strings: []const [:0]const u8, + + pub fn create( + alloc: Allocator, + strings: []const [:0]const u8, + ) Allocator.Error!*StringList { + var arena: ArenaAllocator = .init(alloc); + errdefer arena.deinit(); + const arena_alloc = arena.allocator(); + var stored = try arena_alloc.alloc([:0]const u8, strings.len); + for (strings, 0..) |s, i| stored[i] = try arena_alloc.dupeZ(u8, s); + + const ptr = try alloc.create(StringList); + errdefer alloc.destroy(ptr); + ptr.* = .{ .arena = arena, .strings = stored }; + + return ptr; + } + + pub fn deinit(self: *StringList) void { + self.arena.deinit(); + } + + pub fn destroy(self: *StringList) void { + const alloc = self.arena.child_allocator; + self.deinit(); + alloc.destroy(self); + } + + pub const getGObjectType = gobject.ext.defineBoxed( + StringList, + .{ + .name = "GhosttyStringList", + .funcs = .{ + .copy = &struct { + fn copy(self: *StringList) callconv(.c) *StringList { + return StringList.create( + self.arena.child_allocator, + self.strings, + ) catch @panic("OOM"); + } + }.copy, + .free = &struct { + fn free(self: *StringList) callconv(.c) void { + self.destroy(); + } + }.free, + }, + }, + ); +}; + +test "StringList create and destroy" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "hello", "world" }; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 2), list.strings.len); + try testing.expectEqualStrings("hello", list.strings[0]); + try testing.expectEqualStrings("world", list.strings[1]); +} + +test "StringList create empty list" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{}; + const list = try StringList.create(alloc, input); + defer list.destroy(); + + try testing.expectEqual(@as(usize, 0), list.strings.len); +} + +test "StringList boxedCopy and boxedFree" { + const testing = std.testing; + const alloc = testing.allocator; + + const input: []const [:0]const u8 = &.{ "foo", "bar", "baz" }; + const original = try StringList.create(alloc, input); + defer original.destroy(); + + const copied: *StringList = @ptrCast(@alignCast(gobject.boxedCopy( + StringList.getGObjectType(), + original, + ))); + defer gobject.boxedFree(StringList.getGObjectType(), copied); + + try testing.expectEqual(@as(usize, 3), copied.strings.len); + try testing.expectEqualStrings("foo", copied.strings[0]); + try testing.expectEqualStrings("bar", copied.strings[1]); + try testing.expectEqualStrings("baz", copied.strings[2]); + + try testing.expect(original.strings.ptr != copied.strings.ptr); +} -- 2.51.2 From 481490bd1176760d154b4fa6584604d5de231757 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:25:04 -0800 Subject: [PATCH 057/349] apprt/gtk: add getters for key-sequence and key-table --- src/apprt/gtk/class/surface.zig | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 50d7f3dc2..556e5e2ec 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -361,6 +361,44 @@ pub const Surface = extern struct { }, ); }; + + pub const @"key-sequence" = struct { + pub const name = "key-sequence"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getKeySequence, + .getter_transfer = .full, + }, + ), + }, + ); + }; + + pub const @"key-table" = struct { + pub const name = "key-table"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ext.StringList, + .{ + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getKeyTable, + .getter_transfer = .full, + }, + ), + }, + ); + }; }; pub const signals = struct { @@ -1949,6 +1987,20 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec); } + /// Get the key sequence list. Full transfer. + fn getKeySequence(self: *Self) ?*ext.StringList { + const priv = self.private(); + const alloc = Application.default().allocator(); + return ext.StringList.create(alloc, priv.key_sequence.items) catch null; + } + + /// Get the key table list. Full transfer. + fn getKeyTable(self: *Self) ?*ext.StringList { + const priv = self.private(); + const alloc = Application.default().allocator(); + return ext.StringList.create(alloc, priv.key_tables.items) catch null; + } + /// Return the min size, if set. pub fn getMinSize(self: *Self) ?*Size { const priv = self.private(); @@ -3385,6 +3437,8 @@ pub const Surface = extern struct { properties.@"error".impl, properties.@"font-size-request".impl, properties.focused.impl, + properties.@"key-sequence".impl, + properties.@"key-table".impl, properties.@"min-size".impl, properties.@"mouse-shape".impl, properties.@"mouse-hidden".impl, -- 2.51.2 From 7ca3f41f6f25e13e5da065700eaad0c5798a9ddb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:31:53 -0800 Subject: [PATCH 058/349] apprt/gtk: key state overlay take bindings from surface --- src/apprt/gtk/class/key_state_overlay.zig | 113 ++++++++++++++-------- src/apprt/gtk/ext/slice.zig | 5 + src/apprt/gtk/ui/1.2/surface.blp | 5 +- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig index 15dc0d502..20c0a8ab8 100644 --- a/src/apprt/gtk/class/key_state_overlay.zig +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -1,10 +1,9 @@ const std = @import("std"); const adw = @import("adw"); -const glib = @import("glib"); const gobject = @import("gobject"); -const gdk = @import("gdk"); const gtk = @import("gtk"); +const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; @@ -39,15 +38,23 @@ pub const KeyStateOverlay = extern struct { ); }; - pub const @"tables-text" = struct { - pub const name = "tables-text"; + pub const tables = struct { + pub const name = "tables"; const impl = gobject.ext.defineProperty( name, Self, - ?[:0]const u8, + ?*ext.StringList, .{ - .default = null, - .accessor = C.privateStringFieldAccessor("tables_text"), + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getTables, + .getter_transfer = .full, + .setter = setTables, + .setter_transfer = .full, + }, + ), }, ); }; @@ -69,15 +76,23 @@ pub const KeyStateOverlay = extern struct { ); }; - pub const @"sequence-text" = struct { - pub const name = "sequence-text"; + pub const sequence = struct { + pub const name = "sequence"; const impl = gobject.ext.defineProperty( name, Self, - ?[:0]const u8, + ?*ext.StringList, .{ - .default = null, - .accessor = C.privateStringFieldAccessor("sequence_text"), + .accessor = gobject.ext.typedAccessor( + Self, + ?*ext.StringList, + .{ + .getter = getSequence, + .getter_transfer = .full, + .setter = setSequence, + .setter_transfer = .full, + }, + ), }, ); }; @@ -130,11 +145,11 @@ pub const KeyStateOverlay = extern struct { /// Whether the overlay is active/visible. active: bool = false, - /// The formatted key table stack text (e.g., "default โ€บ vim"). - tables_text: ?[:0]const u8 = null, + /// The key table stack. + tables: ?*ext.StringList = null, - /// The formatted key sequence text (e.g., "Ctrl+A B"). - sequence_text: ?[:0]const u8 = null, + /// The key sequence. + sequence: ?*ext.StringList = null, /// Whether we're waiting for more keys in a sequence. pending: bool = false, @@ -147,30 +162,52 @@ pub const KeyStateOverlay = extern struct { fn init(self: *Self, _: *Class) callconv(.c) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + + fn getTables(self: *Self) ?*ext.StringList { + const priv = self.private(); + if (priv.tables) |tables| { + return ext.StringList.create(tables.allocator(), tables.strings) catch null; + } + return null; + } - // Set dummy data for UI iteration + fn getSequence(self: *Self) ?*ext.StringList { const priv = self.private(); - priv.active = true; - priv.tables_text = glib.ext.dupeZ(u8, "default โ€บ vim"); - priv.sequence_text = glib.ext.dupeZ(u8, "Ctrl+A"); - priv.pending = true; - - // Notify property changes so bindings update - const obj = self.as(gobject.Object); - obj.notifyByPspec(properties.active.impl.param_spec); - obj.notifyByPspec(properties.@"tables-text".impl.param_spec); - obj.notifyByPspec(properties.@"has-tables".impl.param_spec); - obj.notifyByPspec(properties.@"sequence-text".impl.param_spec); - obj.notifyByPspec(properties.@"has-sequence".impl.param_spec); - obj.notifyByPspec(properties.pending.impl.param_spec); + if (priv.sequence) |sequence| { + return ext.StringList.create(sequence.allocator(), sequence.strings) catch null; + } + return null; + } + + fn setTables(self: *Self, value: ?*ext.StringList) void { + const priv = self.private(); + if (priv.tables) |old| { + old.destroy(); + priv.tables = null; + } + + priv.tables = value; + self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec); + } + + fn setSequence(self: *Self, value: ?*ext.StringList) void { + const priv = self.private(); + if (priv.sequence) |old| { + old.destroy(); + priv.sequence = null; + } + + priv.sequence = value; + self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec); } fn getHasTables(self: *Self) bool { - return self.private().tables_text != null; + return self.private().tables != null; } fn getHasSequence(self: *Self) bool { - return self.private().sequence_text != null; + return self.private().sequence != null; } fn closureShowChevron( @@ -229,11 +266,11 @@ pub const KeyStateOverlay = extern struct { fn finalize(self: *Self) callconv(.c) void { const priv = self.private(); - if (priv.tables_text) |v| { - glib.free(@ptrCast(@constCast(v))); + if (priv.tables) |v| { + v.destroy(); } - if (priv.sequence_text) |v| { - glib.free(@ptrCast(@constCast(v))); + if (priv.sequence) |v| { + v.destroy(); } gobject.Object.virtual_methods.finalize.call( @@ -270,9 +307,9 @@ pub const KeyStateOverlay = extern struct { // Properties gobject.ext.registerProperties(class, &.{ properties.active.impl, - properties.@"tables-text".impl, + properties.tables.impl, properties.@"has-tables".impl, - properties.@"sequence-text".impl, + properties.sequence.impl, properties.@"has-sequence".impl, properties.pending.impl, properties.@"valign-target".impl, diff --git a/src/apprt/gtk/ext/slice.zig b/src/apprt/gtk/ext/slice.zig index a746d8045..49ad63d85 100644 --- a/src/apprt/gtk/ext/slice.zig +++ b/src/apprt/gtk/ext/slice.zig @@ -36,6 +36,11 @@ pub const StringList = struct { alloc.destroy(self); } + /// Returns the general-purpose allocator used by this StringList. + pub fn allocator(self: *const StringList) Allocator { + return self.arena.child_allocator; + } + pub const getGObjectType = gobject.ext.defineBoxed( StringList, .{ diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index e9db4208e..a594ba98f 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -156,7 +156,10 @@ Overlay terminal_page { } [overlay] - $GhosttyKeyStateOverlay key_state_overlay {} + $GhosttyKeyStateOverlay key_state_overlay { + tables: bind template.key-table; + sequence: bind template.key-sequence; + } [overlay] // Apply unfocused-split-fill and unfocused-split-opacity to current surface -- 2.51.2 From 71d5ae5a51b96b4fc841e6cc07b2ccfc79489987 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 10:43:40 -0800 Subject: [PATCH 059/349] apprt/gtk: key state overlay text is dynamic --- src/apprt/gtk/class/key_state_overlay.zig | 115 ++++++++++++--------- src/apprt/gtk/class/surface.zig | 8 ++ src/apprt/gtk/ui/1.2/key-state-overlay.blp | 10 +- 3 files changed, 78 insertions(+), 55 deletions(-) diff --git a/src/apprt/gtk/class/key_state_overlay.zig b/src/apprt/gtk/class/key_state_overlay.zig index 20c0a8ab8..7aca8f01d 100644 --- a/src/apprt/gtk/class/key_state_overlay.zig +++ b/src/apprt/gtk/class/key_state_overlay.zig @@ -1,10 +1,12 @@ const std = @import("std"); const adw = @import("adw"); +const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); +const Application = @import("application.zig").Application; const Common = @import("../class.zig").Common; const log = std.log.scoped(.gtk_ghostty_key_state_overlay); @@ -25,19 +27,6 @@ pub const KeyStateOverlay = extern struct { }); pub const properties = struct { - pub const active = struct { - pub const name = "active"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .default = false, - .accessor = C.privateShallowFieldAccessor("active"), - }, - ); - }; - pub const tables = struct { pub const name = "tables"; const impl = gobject.ext.defineProperty( @@ -50,7 +39,7 @@ pub const KeyStateOverlay = extern struct { ?*ext.StringList, .{ .getter = getTables, - .getter_transfer = .full, + .getter_transfer = .none, .setter = setTables, .setter_transfer = .full, }, @@ -88,7 +77,7 @@ pub const KeyStateOverlay = extern struct { ?*ext.StringList, .{ .getter = getSequence, - .getter_transfer = .full, + .getter_transfer = .none, .setter = setSequence, .setter_transfer = .full, }, @@ -114,19 +103,6 @@ pub const KeyStateOverlay = extern struct { ); }; - pub const pending = struct { - pub const name = "pending"; - const impl = gobject.ext.defineProperty( - name, - Self, - bool, - .{ - .default = false, - .accessor = C.privateShallowFieldAccessor("pending"), - }, - ); - }; - pub const @"valign-target" = struct { pub const name = "valign-target"; const impl = gobject.ext.defineProperty( @@ -142,18 +118,12 @@ pub const KeyStateOverlay = extern struct { }; const Private = struct { - /// Whether the overlay is active/visible. - active: bool = false, - /// The key table stack. tables: ?*ext.StringList = null, /// The key sequence. sequence: ?*ext.StringList = null, - /// Whether we're waiting for more keys in a sequence. - pending: bool = false, - /// Target vertical alignment for the overlay. valign_target: gtk.Align = .end, @@ -165,19 +135,11 @@ pub const KeyStateOverlay = extern struct { } fn getTables(self: *Self) ?*ext.StringList { - const priv = self.private(); - if (priv.tables) |tables| { - return ext.StringList.create(tables.allocator(), tables.strings) catch null; - } - return null; + return self.private().tables; } fn getSequence(self: *Self) ?*ext.StringList { - const priv = self.private(); - if (priv.sequence) |sequence| { - return ext.StringList.create(sequence.allocator(), sequence.strings) catch null; - } - return null; + return self.private().sequence; } fn setTables(self: *Self, value: ?*ext.StringList) void { @@ -186,8 +148,11 @@ pub const KeyStateOverlay = extern struct { old.destroy(); priv.tables = null; } + if (value) |v| { + priv.tables = v; + } - priv.tables = value; + self.as(gobject.Object).notifyByPspec(properties.tables.impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.@"has-tables".impl.param_spec); } @@ -197,17 +162,22 @@ pub const KeyStateOverlay = extern struct { old.destroy(); priv.sequence = null; } + if (value) |v| { + priv.sequence = v; + } - priv.sequence = value; + self.as(gobject.Object).notifyByPspec(properties.sequence.impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.@"has-sequence".impl.param_spec); } fn getHasTables(self: *Self) bool { - return self.private().tables != null; + const v = self.private().tables orelse return false; + return v.strings.len > 0; } fn getHasSequence(self: *Self) bool { - return self.private().sequence != null; + const v = self.private().sequence orelse return false; + return v.strings.len > 0; } fn closureShowChevron( @@ -218,6 +188,50 @@ pub const KeyStateOverlay = extern struct { return if (has_tables and has_sequence) 1 else 0; } + fn closureHasState( + _: *Self, + has_tables: bool, + has_sequence: bool, + ) callconv(.c) c_int { + return if (has_tables or has_sequence) 1 else 0; + } + + fn closureTablesText( + _: *Self, + tables: ?*ext.StringList, + ) callconv(.c) ?[*:0]const u8 { + const list = tables orelse return null; + if (list.strings.len == 0) return null; + + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + for (list.strings, 0..) |s, i| { + if (i > 0) buf.writer.writeAll(" > ") catch return null; + buf.writer.writeAll(s) catch return null; + } + + return glib.ext.dupeZ(u8, buf.written()); + } + + fn closureSequenceText( + _: *Self, + sequence: ?*ext.StringList, + ) callconv(.c) ?[*:0]const u8 { + const list = sequence orelse return null; + if (list.strings.len == 0) return null; + + var buf: std.Io.Writer.Allocating = .init(Application.default().allocator()); + defer buf.deinit(); + + for (list.strings, 0..) |s, i| { + if (i > 0) buf.writer.writeAll(" ") catch return null; + buf.writer.writeAll(s) catch return null; + } + + return glib.ext.dupeZ(u8, buf.written()); + } + //--------------------------------------------------------------- // Template callbacks @@ -303,15 +317,16 @@ pub const KeyStateOverlay = extern struct { // Template Callbacks class.bindTemplateCallback("on_drag_end", &onDragEnd); class.bindTemplateCallback("show_chevron", &closureShowChevron); + class.bindTemplateCallback("has_state", &closureHasState); + class.bindTemplateCallback("tables_text", &closureTablesText); + class.bindTemplateCallback("sequence_text", &closureSequenceText); // Properties gobject.ext.registerProperties(class, &.{ - properties.active.impl, properties.tables.impl, properties.@"has-tables".impl, properties.sequence.impl, properties.@"has-sequence".impl, - properties.pending.impl, properties.@"valign-target".impl, }); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 556e5e2ec..a14d53c32 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -832,6 +832,10 @@ pub const Surface = extern struct { const priv = self.private(); const alloc = Application.default().allocator(); + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"key-sequence".impl.param_spec); + switch (value) { .trigger => |trigger| { // Convert the trigger to a human-readable label @@ -865,6 +869,10 @@ pub const Surface = extern struct { const priv = self.private(); const alloc = Application.default().allocator(); + self.as(gobject.Object).freezeNotify(); + defer self.as(gobject.Object).thawNotify(); + self.as(gobject.Object).notifyByPspec(properties.@"key-table".impl.param_spec); + switch (value) { .activate => |name| { // Duplicate the name string and push onto stack diff --git a/src/apprt/gtk/ui/1.2/key-state-overlay.blp b/src/apprt/gtk/ui/1.2/key-state-overlay.blp index 504d2e26e..c8654bfbb 100644 --- a/src/apprt/gtk/ui/1.2/key-state-overlay.blp +++ b/src/apprt/gtk/ui/1.2/key-state-overlay.blp @@ -2,7 +2,7 @@ using Gtk 4.0; using Adw 1; template $GhosttyKeyStateOverlay: Adw.Bin { - visible: bind template.active; + visible: bind $has_state(template.has-tables, template.has-sequence) as ; valign-target: end; halign: center; valign: bind template.valign-target; @@ -30,7 +30,7 @@ template $GhosttyKeyStateOverlay: Adw.Bin { Label tables_label { visible: bind template.has-tables; - label: bind template.tables-text; + label: bind $tables_text(template.tables) as ; xalign: 0.0; } @@ -45,13 +45,13 @@ template $GhosttyKeyStateOverlay: Adw.Bin { Label sequence_label { visible: bind template.has-sequence; - label: bind template.sequence-text; + label: bind $sequence_text(template.sequence) as ; xalign: 0.0; } Spinner pending_spinner { - visible: bind template.pending; - spinning: bind template.pending; + visible: bind template.has-sequence; + spinning: bind template.has-sequence; } } } -- 2.51.2 From f2fe979bab56f434d9b3711b373a9b172ec1fc91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 11:23:02 -0800 Subject: [PATCH 060/349] update valgrind suppressions --- valgrind.supp | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/valgrind.supp b/valgrind.supp index eeb395d03..27479fd5c 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -72,7 +72,16 @@ fun:gdk_surface_handle_event ... } - +{ + GTK CSS Node Validation + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + ... + fun:gtk_css_node_validate_internal + fun:gtk_css_node_validate + ... +} { GTK CSS Provider Leak Memcheck:Leak @@ -196,8 +205,44 @@ fun:svga_context_flush ... } - { + SVGA Stuff + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + fun:svga_create_surface_view + fun:svga_set_framebuffer_state + fun:st_update_framebuffer_state + fun:st_Clear + fun:gsk_gpu_render_pass_op_gl_command + ... +} +{ + GTK Icon + Memcheck:Leak + match-leak-kinds: possible + fun:*alloc + ... + fun:gtk_icon_theme_set_display + fun:gtk_icon_theme_get_for_display + ... +} +{ + GDK Wayland Connection + Memcheck:Leak + match-leak-kinds: possible + fun:calloc + fun:wl_closure_init + fun:wl_connection_demarshal + fun:wl_display_read_events + fun:gdk_wayland_poll_source_check + fun:g_main_context_check_unlocked + fun:g_main_context_iterate_unlocked.isra.0 + fun:g_main_context_iteration + ... +} +{ + GSK Renderer GPU Stuff Memcheck:Leak match-leak-kinds: possible @@ -297,6 +342,21 @@ fun:g_main_context_iteration ... } +{ + GSK More Forms + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gsk_gl_device_use_program + fun:gsk_gl_frame_use_program + fun:gsk_gpu_shader_op_gl_command_n + fun:gsk_gpu_render_pass_op_gl_command + fun:gsk_gl_frame_submit + fun:gsk_gpu_renderer_render_texture + fun:gsk_renderer_render_texture + fun:render_contents + ... +} { GTK Shader Selector Memcheck:Leak -- 2.51.2 From 256c3b9ffba19d6d18963dd34f719f8878d388f6 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 23 Dec 2025 14:51:09 -0500 Subject: [PATCH 061/349] shell-integration: ensure clean env on failure Our shell integration routines can now fail when resources are missing. This change introduces tests to ensure that they leave behind a clean environment upon failure. The bash integration needed a little reordering to support this. --- src/termio/shell_integration.zig | 103 ++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index dba4a8f32..519c226c7 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -344,26 +344,6 @@ fn setupBash( try cmd.appendArg(arg); } } - try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]); - if (rcfile) |v| { - try env.put("GHOSTTY_BASH_RCFILE", v); - } - - // In POSIX mode, HISTFILE defaults to ~/.sh_history, so unless we're - // staying in POSIX mode (--posix), change it back to ~/.bash_history. - if (env.get("HISTFILE") == null) { - var home_buf: [1024]u8 = undefined; - if (try homedir.home(&home_buf)) |home| { - var histfile_buf: [std.fs.max_path_bytes]u8 = undefined; - const histfile = try std.fmt.bufPrint( - &histfile_buf, - "{s}/.bash_history", - .{home}, - ); - try env.put("HISTFILE", histfile); - try env.put("GHOSTTY_BASH_UNEXPORT_HISTFILE", "1"); - } - } // Preserve an existing ENV value. We're about to overwrite it. if (env.get("ENV")) |v| { @@ -382,9 +362,31 @@ fn setupBash( try env.put("ENV", script_path); } else |err| { log.warn("unable to open {s}: {}", .{ script_path, err }); + env.remove("GHOSTTY_BASH_ENV"); return null; } + try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]); + if (rcfile) |v| { + try env.put("GHOSTTY_BASH_RCFILE", v); + } + + // In POSIX mode, HISTFILE defaults to ~/.sh_history, so unless we're + // staying in POSIX mode (--posix), change it back to ~/.bash_history. + if (env.get("HISTFILE") == null) { + var home_buf: [1024]u8 = undefined; + if (try homedir.home(&home_buf)) |home| { + var histfile_buf: [std.fs.max_path_bytes]u8 = undefined; + const histfile = try std.fmt.bufPrint( + &histfile_buf, + "{s}/.bash_history", + .{home}, + ); + try env.put("HISTFILE", histfile); + try env.put("GHOSTTY_BASH_UNEXPORT_HISTFILE", "1"); + } + } + // Get the command string from the builder, then copy it to the arena // allocator. The stackFallback allocator's memory becomes invalid after // this function returns, so we must copy to the arena. @@ -437,9 +439,7 @@ test "bash: unsupported options" { defer env.deinit(); try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, res.path, &env) == null); - try testing.expect(env.get("GHOSTTY_BASH_INJECT") == null); - try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null); - try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); + try testing.expectEqual(0, env.count()); } } @@ -581,6 +581,25 @@ test "bash: additional arguments" { } } +test "bash: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupBash(alloc, .{ .shell = "bash" }, resources_dir, &env) == null); + try testing.expectEqual(0, env.count()); +} + /// Setup automatic shell integration for shells that include /// their modules from paths in `XDG_DATA_DIRS` env variable. /// @@ -690,6 +709,25 @@ test "xdg: existing XDG_DATA_DIRS" { ); } +test "xdg: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(!try setupXdgDataDirs(alloc, resources_dir, &env)); + try testing.expectEqual(0, env.count()); +} + /// Setup the zsh automatic shell integration. This works by setting /// ZDOTDIR to our resources dir so that zsh will load our config. This /// config then loads the true user config. @@ -749,6 +787,25 @@ test "zsh: ZDOTDIR" { try testing.expectEqualStrings("$HOME/.config/zsh", env.get("GHOSTTY_ZSH_ZDOTDIR").?); } +test "zsh: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(!try setupZsh(resources_dir, &env)); + try testing.expectEqual(0, env.count()); +} + /// Test helper that creates a temporary resources directory with shell integration paths. const TmpResourcesDir = struct { allocator: Allocator, -- 2.51.2 From 0db0655ea56ee919043cf0ff841ce81dc00a41e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Dec 2025 20:28:49 -0800 Subject: [PATCH 062/349] Invalid key sequence does not encode if a `catch_all` has `ignore` This adds some new special case handling for key sequences when an unbound keyboard input is received. If the current keybinding set scope (i.e. active tables) has a `catch_all` binding that would `ignore` input, then the entire key sequence is dropped. Normally, when an unbound key sequence is received, Ghostty encodes it and sends it to the running program. This special behavior is useful for things like Vim mode which have `g>g` to scroll to top, and a `catch_all=ignore` to drop all other input. If the user presses `g>h` (unbound), you don't want `gh` to show up in your terminal input, because the `catch_all=ignore` indicates that the user wants that mode to drop all unbound input. --- src/Surface.zig | 78 ++++++++++++++++++++++++++++++++----------- src/config/Config.zig | 5 +++ 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index ea17c6104..fd658a43b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2826,14 +2826,21 @@ fn maybeHandleBinding( // No entry found. We need to encode everything up to this // point and send to the pty since we're in a sequence. - // - // We also ignore modifiers so that nested sequences such as + + // We ignore modifiers so that nested sequences such as // ctrl+a>ctrl+b>c work. - if (!event.key.modifier()) { - // Encode everything up to this point - self.endKeySequence(.flush, .retain); + if (event.key.modifier()) return null; + + // If we have a catch-all of ignore, then we special case our + // invalid sequence handling to ignore it. + if (self.catchAllIsIgnore()) { + self.endKeySequence(.drop, .retain); + return .ignored; } + // Encode everything up to this point + self.endKeySequence(.flush, .retain); + return null; } @@ -3037,6 +3044,34 @@ fn deactivateAllKeyTables(self: *Surface) !bool { return true; } +/// This checks if the current keybinding sets have a catch_all binding +/// with `ignore`. This is used to determine some special input cases. +fn catchAllIsIgnore(self: *Surface) bool { + // Get our catch all + const entry: input.Binding.Set.Entry = entry: { + const trigger: input.Binding.Trigger = .{ .key = .catch_all }; + + const table_items = self.keyboard.table_stack.items; + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + const entry = table_items[rev_i].set.get(trigger) orelse continue; + break :entry entry; + } + + break :entry self.config.keybind.set.get(trigger) orelse + return false; + }; + + // We have a catch-all entry, see if its an ignore + return switch (entry.value_ptr.*) { + .leader => false, + .leaf => |leaf| leaf.action == .ignore, + .leaf_chained => |leaf| chained: for (leaf.actions.items) |action| { + if (action == .ignore) break :chained true; + } else false, + }; +} + const KeySequenceQueued = enum { flush, drop }; const KeySequenceMemory = enum { retain, free }; @@ -3065,23 +3100,26 @@ fn endKeySequence( // the set we look at to the root set. self.keyboard.sequence_set = null; - if (self.keyboard.sequence_queued.items.len > 0) { - switch (action) { - .flush => for (self.keyboard.sequence_queued.items) |write_req| { - self.queueIo(switch (write_req) { - .small => |v| .{ .write_small = v }, - .stable => |v| .{ .write_stable = v }, - .alloc => |v| .{ .write_alloc = v }, - }, .unlocked); - }, + // If we have no queued data, there is nothing else to do. + if (self.keyboard.sequence_queued.items.len == 0) return; - .drop => for (self.keyboard.sequence_queued.items) |req| req.deinit(), - } + // Run the proper action first + switch (action) { + .flush => for (self.keyboard.sequence_queued.items) |write_req| { + self.queueIo(switch (write_req) { + .small => |v| .{ .write_small = v }, + .stable => |v| .{ .write_stable = v }, + .alloc => |v| .{ .write_alloc = v }, + }, .unlocked); + }, - switch (mem) { - .free => self.keyboard.sequence_queued.clearAndFree(self.alloc), - .retain => self.keyboard.sequence_queued.clearRetainingCapacity(), - } + .drop => for (self.keyboard.sequence_queued.items) |req| req.deinit(), + } + + // Memory handling of the sequence after the action + switch (mem) { + .free => self.keyboard.sequence_queued.clearAndFree(self.alloc), + .retain => self.keyboard.sequence_queued.clearRetainingCapacity(), } } diff --git a/src/config/Config.zig b/src/config/Config.zig index 6911cd9f7..0df5c91b0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1521,6 +1521,11 @@ class: ?[:0]const u8 = null, /// specifically output that key (e.g. `ctrl+a>ctrl+a=text:foo`) or /// press an unbound key which will send both keys to the program. /// +/// * If an unbound key is pressed during a sequence and a `catch_all` +/// binding exists that would `ignore` the input, the entire sequence +/// is dropped and nothing happens. Otherwise, the entire sequence is +/// encoded and sent to the running program as if no keybind existed. +/// /// * If a prefix in a sequence is previously bound, the sequence will /// override the previous binding. For example, if `ctrl+a` is bound to /// `new_window` and `ctrl+a>n` is bound to `new_tab`, pressing `ctrl+a` -- 2.51.2 From 7ce88b6811065683d74405a66ab41d034c7c2bd1 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 24 Dec 2025 19:41:05 +0100 Subject: [PATCH 063/349] macOS: fix initial surface color scheme in quickTerminal --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index d79c89d2d..1750e949d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1298,7 +1298,7 @@ extension BaseTerminalController: NSMenuItemValidation { } else { scheme = GHOSTTY_COLOR_SCHEME_LIGHT } - guard scheme != appliedColorScheme else { + guard scheme != appliedColorScheme, !surfaceTree.isEmpty else { return } for surfaceView in surfaceTree { -- 2.51.2 From 574ee470bd870314d6d13daef6668b84b0746873 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 24 Dec 2025 21:29:14 +0100 Subject: [PATCH 064/349] macOS: move `NSGlassEffectView` into `TerminalViewContainer` --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../QuickTerminalController.swift | 13 +- .../Terminal/TerminalController.swift | 4 +- .../Terminal/TerminalViewContainer.swift | 127 ++++++++++++++++++ .../Window Styles/TerminalWindow.swift | 62 +-------- macos/Sources/Ghostty/Ghostty.Config.swift | 12 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 3 + 7 files changed, 155 insertions(+), 67 deletions(-) create mode 100644 macos/Sources/Features/Terminal/TerminalViewContainer.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 1a810e621..779d95e5b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ Features/Terminal/TerminalRestorable.swift, Features/Terminal/TerminalTabColor.swift, Features/Terminal/TerminalView.swift, + Features/Terminal/TerminalViewContainer.swift, "Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift", "Features/Terminal/Window Styles/Terminal.xib", "Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib", diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 8a642034f..07c0c4c19 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -137,11 +137,11 @@ class QuickTerminalController: BaseTerminalController { } // Setup our content - window.contentView = NSHostingView(rootView: TerminalView( + window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self - )) + ) // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { @@ -609,7 +609,7 @@ class QuickTerminalController: BaseTerminalController { // If we have window transparency then set it transparent. Otherwise set it opaque. // Also check if the user has overridden transparency to be fully opaque. - if !isBackgroundOpaque && self.derivedConfig.backgroundOpacity < 1 { + if !isBackgroundOpaque && (self.derivedConfig.backgroundOpacity < 1 || derivedConfig.backgroundBlur.isGlassStyle) { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -617,7 +617,9 @@ class QuickTerminalController: BaseTerminalController { // Terminal.app more easily. window.backgroundColor = .white.withAlphaComponent(0.001) - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + if !derivedConfig.backgroundBlur.isGlassStyle { + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + } } else { window.isOpaque = true window.backgroundColor = .windowBackgroundColor @@ -722,6 +724,7 @@ class QuickTerminalController: BaseTerminalController { let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior let quickTerminalSize: QuickTerminalSize let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur init() { self.quickTerminalScreen = .main @@ -730,6 +733,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = .move self.quickTerminalSize = QuickTerminalSize() self.backgroundOpacity = 1.0 + self.backgroundBlur = .disabled } init(_ config: Ghostty.Config) { @@ -739,6 +743,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior self.quickTerminalSize = config.quickTerminalSize self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bccdd9c69..c5481851b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -936,11 +936,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // Initialize our content view to the SwiftUI root - window.contentView = NSHostingView(rootView: TerminalView( + window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self, - )) + ) // If we have a default size, we want to apply it. if let defaultSize { diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift new file mode 100644 index 000000000..f4e2fc080 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -0,0 +1,127 @@ +import AppKit +import SwiftUI + +/// Use this container to achieve a glass effect at the window level. +/// Modifying `NSThemeFrame` can sometimes be unpredictable. +class TerminalViewContainer: NSView { + private let terminalView: NSView + + /// Glass effect view for liquid glass background when transparency is enabled + private var glassEffectView: NSView? + private var derivedConfig: DerivedConfig + + init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) { + self.derivedConfig = DerivedConfig(config: ghostty.config) + self.terminalView = NSHostingView(rootView: TerminalView( + ghostty: ghostty, + viewModel: viewModel, + delegate: delegate + )) + super.init(frame: .zero) + setup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + addSubview(terminalView) + terminalView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + terminalView.topAnchor.constraint(equalTo: topAnchor), + terminalView.leadingAnchor.constraint(equalTo: leadingAnchor), + terminalView.bottomAnchor.constraint(equalTo: bottomAnchor), + terminalView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + + NotificationCenter.default.addObserver( + self, + selector: #selector(ghosttyConfigDidChange(_:)), + name: .ghosttyConfigDidChange, + object: nil + ) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateGlassEffectIfNeeded() + } + + @objc private func ghosttyConfigDidChange(_ notification: Notification) { + guard let config = notification.userInfo?[ + Notification.Name.GhosttyConfigChangeKey + ] as? Ghostty.Config else { return } + let newValue = DerivedConfig(config: config) + guard newValue != derivedConfig else { return } + derivedConfig = newValue + DispatchQueue.main.async(execute: updateGlassEffectIfNeeded) + } +} + +// MARK: Glass + +private extension TerminalViewContainer { +#if compiler(>=6.2) + @available(macOS 26.0, *) + func addGlassEffectViewIfNeeded() -> NSGlassEffectView? { + if let existed = glassEffectView as? NSGlassEffectView { + return existed + } + guard let themeFrameView = window?.contentView?.superview else { + return nil + } + let effectView = NSGlassEffectView() + addSubview(effectView, positioned: .below, relativeTo: terminalView) + effectView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + effectView.topAnchor.constraint(equalTo: topAnchor, constant: -themeFrameView.safeAreaInsets.top), + effectView.leadingAnchor.constraint(equalTo: leadingAnchor), + effectView.bottomAnchor.constraint(equalTo: bottomAnchor), + effectView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + glassEffectView = effectView + return effectView + } +#endif // compiler(>=6.2) + + func updateGlassEffectIfNeeded() { +#if compiler(>=6.2) + guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + glassEffectView?.removeFromSuperview() + glassEffectView = nil + return + } + guard let effectView = addGlassEffectViewIfNeeded() else { + return + } + switch derivedConfig.backgroundBlur { + case .macosGlassRegular: + effectView.style = NSGlassEffectView.Style.regular + case .macosGlassClear: + effectView.style = NSGlassEffectView.Style.clear + default: + break + } + let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor) + effectView.tintColor = backgroundColor + .withAlphaComponent(derivedConfig.backgroundOpacity) + if let window, window.responds(to: Selector(("_cornerRadius"))), let cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat { + effectView.cornerRadius = cornerRadius + } +#endif // compiler(>=6.2) + } + + struct DerivedConfig: Equatable { + var backgroundOpacity: Double = 0 + var backgroundBlur: Ghostty.Config.BackgroundBlur + var backgroundColor: Color = .clear + + init(config: Ghostty.Config) { + self.backgroundBlur = config.backgroundBlur + self.backgroundOpacity = config.backgroundOpacity + self.backgroundColor = config.backgroundColor + } + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 4196df97f..9debd2cb3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -474,7 +474,7 @@ class TerminalWindow: NSWindow { let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && !forceOpaque && - surfaceConfig.backgroundOpacity < 1 + (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) { isOpaque = false @@ -483,15 +483,8 @@ class TerminalWindow: NSWindow { // Terminal.app more easily. backgroundColor = .white.withAlphaComponent(0.001) - // Add liquid glass behind terminal content - if #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle { - setupGlassLayer() - } else if let appDelegate = NSApp.delegate as? AppDelegate { - // If we had a prior glass layer we should remove it - if #available(macOS 26.0, *) { - removeGlassLayer() - } - + // We don't need to set blur when using glass + if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) @@ -499,11 +492,6 @@ class TerminalWindow: NSWindow { } else { isOpaque = true - // Remove liquid glass when not transparent - if #available(macOS 26.0, *) { - removeGlassLayer() - } - let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor) self.backgroundColor = backgroundColor.withAlphaComponent(1) } @@ -581,50 +569,6 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } - -#if compiler(>=6.2) - // MARK: Glass - - @available(macOS 26.0, *) - private func setupGlassLayer() { - // Remove existing glass effect view - removeGlassLayer() - - // Get the window content view (parent of the NSHostingView) - guard let contentView else { return } - guard let windowContentView = contentView.superview else { return } - - // Create NSGlassEffectView for native glass effect - let effectView = NSGlassEffectView() - - // Map Ghostty config to NSGlassEffectView style - switch derivedConfig.backgroundBlur { - case .macosGlassRegular: - effectView.style = NSGlassEffectView.Style.regular - case .macosGlassClear: - effectView.style = NSGlassEffectView.Style.clear - default: - // Should not reach here since we check for glass style before calling - // setupGlassLayer() - assertionFailure() - } - - effectView.cornerRadius = derivedConfig.windowCornerRadius - effectView.tintColor = preferredBackgroundColor - effectView.frame = windowContentView.bounds - effectView.autoresizingMask = [.width, .height] - - // Position BELOW the terminal content to act as background - windowContentView.addSubview(effectView, positioned: .below, relativeTo: contentView) - glassEffectView = effectView - } - - @available(macOS 26.0, *) - private func removeGlassLayer() { - glassEffectView?.removeFromSuperview() - glassEffectView = nil - } -#endif // compiler(>=6.2) // MARK: Config diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 7ea545f7a..71b9eb17d 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -648,9 +648,17 @@ extension Ghostty.Config { case 0: self = .disabled case -1: - self = .macosGlassRegular + if #available(macOS 26.0, *) { + self = .macosGlassRegular + } else { + self = .disabled + } case -2: - self = .macosGlassClear + if #available(macOS 26.0, *) { + self = .macosGlassClear + } else { + self = .disabled + } default: self = .radius(Int(value)) } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 37cc9282e..77e1c43d4 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1654,6 +1654,7 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1662,6 +1663,7 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 + self.backgroundBlur = .disabled self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1671,6 +1673,7 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) -- 2.51.2 From 017021787c32f364b547d48d1911c40273d119d8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Dec 2025 14:22:21 -0800 Subject: [PATCH 065/349] config: RepeatableCommand cval --- include/ghostty.h | 6 +++ src/config/Config.zig | 86 ++++++++++++++++++++++++++++++++++++++++--- src/input/command.zig | 23 +++++++++++- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 48915b179..d6e6fba70 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -454,6 +454,12 @@ typedef struct { size_t len; } ghostty_config_color_list_s; +// config.RepeatableCommand +typedef struct { + const ghostty_command_s* commands; + size_t len; +} ghostty_config_command_list_s; + // config.Palette typedef struct { ghostty_config_color_s colors[256]; diff --git a/src/config/Config.zig b/src/config/Config.zig index 0df5c91b0..92caa5744 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -8041,15 +8041,37 @@ pub const SplitPreserveZoom = packed struct { }; pub const RepeatableCommand = struct { + const Self = @This(); + value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, + value_c: std.ArrayListUnmanaged(inputpkg.Command.C) = .empty, + + /// ghostty_config_command_list_s + pub const C = extern struct { + commands: [*]inputpkg.Command.C, + len: usize, + }; - pub fn init(self: *RepeatableCommand, alloc: Allocator) !void { + pub fn cval(self: *const Self) C { + return .{ + .commands = self.value_c.items.ptr, + .len = self.value_c.items.len, + }; + } + + pub fn init(self: *Self, alloc: Allocator) !void { self.value = .empty; + self.value_c = .empty; + errdefer { + self.value.deinit(alloc); + self.value_c.deinit(alloc); + } try self.value.appendSlice(alloc, inputpkg.command.defaults); + try self.value_c.appendSlice(alloc, inputpkg.command.defaultsC); } pub fn parseCLI( - self: *RepeatableCommand, + self: *Self, alloc: Allocator, input_: ?[]const u8, ) !void { @@ -8057,26 +8079,36 @@ pub const RepeatableCommand = struct { const input = input_ orelse ""; if (input.len == 0) { self.value.clearRetainingCapacity(); + self.value_c.clearRetainingCapacity(); return; } + // Reserve space in our lists + try self.value.ensureUnusedCapacity(alloc, 1); + try self.value_c.ensureUnusedCapacity(alloc, 1); + const cmd = try cli.args.parseAutoStruct( inputpkg.Command, alloc, input, null, ); - try self.value.append(alloc, cmd); + const cmd_c = try cmd.cval(alloc); + self.value.appendAssumeCapacity(cmd); + self.value_c.appendAssumeCapacity(cmd_c); } /// Deep copy of the struct. Required by Config. - pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand { + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { const value = try self.value.clone(alloc); for (value.items) |*item| { item.* = try item.clone(alloc); } - return .{ .value = value }; + return .{ + .value = value, + .value_c = try self.value_c.clone(alloc), + }; } /// Compare if two of our value are equal. Required by Config. @@ -8232,6 +8264,50 @@ pub const RepeatableCommand = struct { try testing.expectEqualStrings("kurwa", item.action.text); } } + + test "RepeatableCommand cval" { + const testing = std.testing; + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,action:ignore"); + try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle"); + + try testing.expectEqual(@as(usize, 2), list.value.items.len); + try testing.expectEqual(@as(usize, 2), list.value_c.items.len); + + const cv = list.cval(); + try testing.expectEqual(@as(usize, 2), cv.len); + + // First entry + try testing.expectEqualStrings("Foo", std.mem.sliceTo(cv.commands[0].title, 0)); + try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action_key, 0)); + try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action, 0)); + + // Second entry + try testing.expectEqualStrings("Bar", std.mem.sliceTo(cv.commands[1].title, 0)); + try testing.expectEqualStrings("bobr", std.mem.sliceTo(cv.commands[1].description, 0)); + try testing.expectEqualStrings("text", std.mem.sliceTo(cv.commands[1].action_key, 0)); + try testing.expectEqualStrings("text:ale bydle", std.mem.sliceTo(cv.commands[1].action, 0)); + } + + test "RepeatableCommand cval cleared" { + const testing = std.testing; + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,action:ignore"); + try testing.expectEqual(@as(usize, 1), list.cval().len); + + try list.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), list.cval().len); + } }; /// OSC 4, 10, 11, and 12 default color reporting format. diff --git a/src/input/command.zig b/src/input/command.zig index 67086f7ec..936f2211c 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -43,7 +43,7 @@ pub const Command = struct { return true; } - /// Convert this command to a C struct. + /// Convert this command to a C struct at comptime. pub fn comptimeCval(self: Command) C { assert(@inComptime()); @@ -55,6 +55,27 @@ pub const Command = struct { }; } + /// Convert this command to a C struct at runtime. + /// + /// This shares memory with the original command. + /// + /// The action string is allocated using the provided allocator. You can + /// free the slice directly if you need to but we recommend an arena + /// for this. + pub fn cval(self: Command, alloc: Allocator) Allocator.Error!C { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + self.action.format(&buf.writer) catch return error.OutOfMemory; + const action = try buf.toOwnedSliceSentinel(0); + + return .{ + .action_key = @tagName(self.action), + .action = action.ptr, + .title = self.title, + .description = self.description, + }; + } + /// Implements a comparison function for std.mem.sortUnstable /// and similar functions. The sorting is defined by Ghostty /// to be what we prefer. If a caller wants some other sorting, -- 2.51.2 From 12523ca61c2d3d3dfbe0a0f36183e11b61e88d2e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Dec 2025 14:28:12 -0800 Subject: [PATCH 066/349] macOS: command-palette-entry is now visible in macOS --- .../Command Palette/TerminalCommandPalette.swift | 15 ++++++++++++++- macos/Sources/Ghostty/Ghostty.Config.swift | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 902186ad3..6efb588cd 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -64,7 +64,7 @@ struct TerminalCommandPaletteView: View { // Sort the rest. We replace ":" with a character that sorts before space // so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker // for stable ordering when titles are equal. - options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in + options.append(contentsOf: (jumpOptions + terminalOptions + customEntries).sorted { a, b in let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t") let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t") let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized) @@ -135,6 +135,19 @@ struct TerminalCommandPaletteView: View { } } + /// Custom commands from the command-palette-entry configuration. + private var customEntries: [CommandOption] { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] } + return appDelegate.ghostty.config.commandPaletteEntries.map { c in + CommandOption( + title: c.title, + description: c.description + ) { + onAction(c.action) + } + } + } + /// Commands for jumping to other terminal surfaces. private var jumpOptions: [CommandOption] { TerminalController.all.flatMap { controller -> [CommandOption] in diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 7ea545f7a..5aa79a149 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -622,6 +622,16 @@ extension Ghostty { let str = String(cString: ptr) return Scrollbar(rawValue: str) ?? defaultValue } + + var commandPaletteEntries: [Ghostty.Command] { + guard let config = self.config else { return [] } + var v: ghostty_config_command_list_s = .init() + let key = "command-palette-entry" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return [] } + guard v.len > 0 else { return [] } + let buffer = UnsafeBufferPointer(start: v.commands, count: v.len) + return buffer.map { Ghostty.Command(cValue: $0) } + } } } -- 2.51.2 From f7f29934f30cb469d15f909fdd68466352e0e2c5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 24 Dec 2025 14:41:40 -0800 Subject: [PATCH 067/349] macos: ghostty.command should be part of iOS build --- macos/Ghostty.xcodeproj/project.pbxproj | 1 - 1 file changed, 1 deletion(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 1a810e621..91c2300cc 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -137,7 +137,6 @@ Features/Update/UpdateSimulator.swift, Features/Update/UpdateViewModel.swift, "Ghostty/FullscreenMode+Extension.swift", - Ghostty/Ghostty.Command.swift, Ghostty/Ghostty.Error.swift, Ghostty/Ghostty.Event.swift, Ghostty/Ghostty.Input.swift, -- 2.51.2 From bf73f753048a4b4262b71a7aea5f6bfc75c2720e Mon Sep 17 00:00:00 2001 From: rezky_nightky Date: Fri, 26 Dec 2025 00:27:08 +0700 Subject: [PATCH 068/349] chore: fixed some typo Author: rezky_nightky Repository: ghostty Branch: main Signing: GPG (4B65AAC2) HashAlgo: BLAKE3 [ Block Metadata ] BlockHash: c37f4ee817412728a8058ba6087f5ca6aaff5a845560447d595d8055972d0eac PrevHash: 3510917a780936278debe21786b7bae3a2162cb3857957314c3b8702e921b3d4 PatchHash: 5e5bb4ab35df304ea13c3d297c6d9a965156052c82bccf852b1f00b7bcaa7dd4 FilesChanged: 18 Lines: +92 / -92 Timestamp: 2025-12-25T17:27:08Z Signature1: c1970dbb94600d1e24dfe8efcc00f001664db7b777902df9632a689b1d9d1498 Signature2: 30babb1e3ca07264931e067bfe36c676fb7988c2e06f8c54e0c9538fe7c7fc9a --- .../TitlebarTabsVenturaTerminalWindow.swift | 4 +- .../TransparentTitlebarTerminalWindow.swift | 32 +++--- macos/Sources/Ghostty/Ghostty.Input.swift | 100 +++++++++--------- macos/Sources/Helpers/Fullscreen.swift | 10 +- src/Surface.zig | 4 +- src/config/theme.zig | 2 +- src/font/Atlas.zig | 2 +- src/font/shaper/coretext.zig | 2 +- src/font/sprite/Face.zig | 2 +- src/input/Binding.zig | 2 +- src/input/key_encode.zig | 2 +- src/input/mouse.zig | 2 +- src/inspector/Inspector.zig | 2 +- src/renderer/generic.zig | 2 +- src/terminal/Terminal.zig | 2 +- src/terminal/kitty/graphics_unicode.zig | 4 +- src/terminal/ref_counted_set.zig | 8 +- src/terminal/tmux/viewer.zig | 2 +- 18 files changed, 92 insertions(+), 92 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index c0aad46b3..39db13c6d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -5,7 +5,7 @@ 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 @@ -395,7 +395,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // Hide the window drag handle. windowDragHandle?.isHidden = true - // Reenable the main toolbar title + // Re-enable the main toolbar title if let toolbar = toolbar as? TerminalToolbar { toolbar.titleIsHidden = false } diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 57b889b82..a72436d7f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -7,16 +7,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { /// This is necessary because various macOS operations (tab switching, tab bar /// visibility changes) can reset the titlebar appearance. private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig? - + /// KVO observation for tab group window changes. private var tabGroupWindowsObservation: NSKeyValueObservation? private var tabBarVisibleObservation: NSKeyValueObservation? - + deinit { tabGroupWindowsObservation?.invalidate() tabBarVisibleObservation?.invalidate() } - + // MARK: NSWindow override func awakeFromNib() { @@ -29,7 +29,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { override func becomeMain() { super.becomeMain() - + guard let lastSurfaceConfig else { return } syncAppearance(lastSurfaceConfig) @@ -42,7 +42,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } } } - + override func update() { super.update() @@ -67,7 +67,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // Save our config in case we need to reapply lastSurfaceConfig = surfaceConfig - // Everytime we change appearance, set KVO up again in case any of our + // Every time we change appearance, set KVO up again in case any of our // references changed (e.g. tabGroup is new). setupKVO() @@ -99,7 +99,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { ? NSColor.clear.cgColor : preferredBackgroundColor?.cgColor } - + // In all cases, we have to hide the background view since this has multiple subviews // that force a background color. titlebarBackgroundView?.isHidden = true @@ -108,14 +108,14 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 13.0, *) private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { guard let titlebarContainer else { return } - + // Setup the titlebar background color to match ours titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor - + // See the docs for the function that sets this to true on why effectViewIsHidden = false - + // Necessary to not draw the border around the title titlebarAppearsTransparent = true } @@ -141,7 +141,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // Remove existing observation if any tabGroupWindowsObservation?.invalidate() tabGroupWindowsObservation = nil - + // Check if tabGroup is available guard let tabGroup else { return } @@ -170,7 +170,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // Remove existing observation if any tabBarVisibleObservation?.invalidate() tabBarVisibleObservation = nil - + // Set up KVO observation for isTabBarVisible tabBarVisibleObservation = tabGroup?.observe( \.isTabBarVisible, @@ -181,18 +181,18 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { self.syncAppearance(lastSurfaceConfig) } } - + // MARK: macOS 13 to 15 - + // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to // remove the effect view if the default tab bar is being used since the effect created in // `updateTabsForVeryDarkBackgrounds` creates a confusing visual design. private var effectViewIsHidden = false - + private func hideEffectView() { guard !effectViewIsHidden else { return } - + // By hiding the visual effect view, we allow the window's (or titlebar's in this case) // background color to show through. If we were to set `titlebarAppearsTransparent` to true // the selected tab would look fine, but the unselected ones and new tab button backgrounds diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 6b4eb0ae4..44011d5b9 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -68,7 +68,7 @@ extension Ghostty { if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } // Handle sided input. We can't tell that both are pressed in the - // Ghostty structure but thats okay -- we don't use that information. + // Ghostty structure but that's okay -- we don't use that information. let rawFlags = flags.rawValue if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } @@ -139,7 +139,7 @@ extension Ghostty.Input { case GHOSTTY_ACTION_REPEAT: self.action = .repeat default: self.action = .press } - + // Convert key from keycode guard let key = Key(keyCode: UInt16(cValue.keycode)) else { return nil } self.key = key @@ -150,18 +150,18 @@ extension Ghostty.Input { } else { self.text = nil } - + // Set composing state self.composing = cValue.composing - + // Convert modifiers self.mods = Mods(cMods: cValue.mods) self.consumedMods = Mods(cMods: cValue.consumed_mods) - + // Set unshifted codepoint self.unshiftedCodepoint = cValue.unshifted_codepoint } - + /// Executes a closure with a temporary C representation of this KeyEvent. /// /// This method safely converts the Swift KeyEntity to a C `ghostty_input_key_s` struct @@ -180,7 +180,7 @@ extension Ghostty.Input { keyEvent.mods = mods.cMods keyEvent.consumed_mods = consumedMods.cMods keyEvent.unshifted_codepoint = unshiftedCodepoint - + // Handle text with proper memory management if let text = text { return text.withCString { textPtr in @@ -203,7 +203,7 @@ extension Ghostty.Input { case release case press case `repeat` - + var cAction: ghostty_input_action_e { switch self { case .release: GHOSTTY_ACTION_RELEASE @@ -232,7 +232,7 @@ extension Ghostty.Input { let action: MouseState let button: MouseButton let mods: Mods - + init( action: MouseState, button: MouseButton, @@ -242,7 +242,7 @@ extension Ghostty.Input { self.button = button self.mods = mods } - + /// Creates a MouseEvent from C enum values. /// /// This initializer converts C-style mouse input enums to Swift types. @@ -259,7 +259,7 @@ extension Ghostty.Input { case GHOSTTY_MOUSE_PRESS: self.action = .press default: return nil } - + // Convert button switch button { case GHOSTTY_MOUSE_UNKNOWN: self.button = .unknown @@ -268,7 +268,7 @@ extension Ghostty.Input { case GHOSTTY_MOUSE_MIDDLE: self.button = .middle default: return nil } - + // Convert modifiers self.mods = Mods(cMods: mods) } @@ -279,7 +279,7 @@ extension Ghostty.Input { let x: Double let y: Double let mods: Mods - + init( x: Double, y: Double, @@ -316,7 +316,7 @@ extension Ghostty.Input { enum MouseState: String, CaseIterable { case release case press - + var cMouseState: ghostty_input_mouse_state_e { switch self { case .release: GHOSTTY_MOUSE_RELEASE @@ -344,7 +344,7 @@ extension Ghostty.Input { case left case right case middle - + var cMouseButton: ghostty_input_mouse_button_e { switch self { case .unknown: GHOSTTY_MOUSE_UNKNOWN @@ -382,18 +382,18 @@ extension Ghostty.Input { /// for scroll events, matching the Zig `ScrollMods` packed struct. struct ScrollMods { let rawValue: Int32 - + /// True if this is a high-precision scroll event (e.g., trackpad, Magic Mouse) var precision: Bool { rawValue & 0b0000_0001 != 0 } - + /// The momentum phase of the scroll event for inertial scrolling var momentum: Momentum { let momentumBits = (rawValue >> 1) & 0b0000_0111 return Momentum(rawValue: UInt8(momentumBits)) ?? .none } - + init(precision: Bool = false, momentum: Momentum = .none) { var value: Int32 = 0 if precision { @@ -402,11 +402,11 @@ extension Ghostty.Input { value |= Int32(momentum.rawValue) << 1 self.rawValue = value } - + init(rawValue: Int32) { self.rawValue = rawValue } - + var cScrollMods: ghostty_input_scroll_mods_t { rawValue } @@ -425,7 +425,7 @@ extension Ghostty.Input { case ended = 4 case cancelled = 5 case mayBegin = 6 - + var cMomentum: ghostty_input_mouse_momentum_e { switch self { case .none: GHOSTTY_MOUSE_MOMENTUM_NONE @@ -442,7 +442,7 @@ extension Ghostty.Input { extension Ghostty.Input.Momentum: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum") - + static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [ .none: "None", .began: "Began", @@ -479,7 +479,7 @@ extension Ghostty.Input { /// `ghostty_input_mods_e` struct Mods: OptionSet { let rawValue: UInt32 - + static let none = Mods(rawValue: GHOSTTY_MODS_NONE.rawValue) static let shift = Mods(rawValue: GHOSTTY_MODS_SHIFT.rawValue) static let ctrl = Mods(rawValue: GHOSTTY_MODS_CTRL.rawValue) @@ -490,23 +490,23 @@ extension Ghostty.Input { static let ctrlRight = Mods(rawValue: GHOSTTY_MODS_CTRL_RIGHT.rawValue) static let altRight = Mods(rawValue: GHOSTTY_MODS_ALT_RIGHT.rawValue) static let superRight = Mods(rawValue: GHOSTTY_MODS_SUPER_RIGHT.rawValue) - + var cMods: ghostty_input_mods_e { ghostty_input_mods_e(rawValue) } - + init(rawValue: UInt32) { self.rawValue = rawValue } - + init(cMods: ghostty_input_mods_e) { self.rawValue = cMods.rawValue } - + init(nsFlags: NSEvent.ModifierFlags) { self.init(cMods: Ghostty.ghosttyMods(nsFlags)) } - + var nsFlags: NSEvent.ModifierFlags { Ghostty.eventModifierFlags(mods: cMods) } @@ -1120,43 +1120,43 @@ extension Ghostty.Input.Key: AppEnum { return [ // Letters (A-Z) .a, .b, .c, .d, .e, .f, .g, .h, .i, .j, .k, .l, .m, .n, .o, .p, .q, .r, .s, .t, .u, .v, .w, .x, .y, .z, - + // Numbers (0-9) .digit0, .digit1, .digit2, .digit3, .digit4, .digit5, .digit6, .digit7, .digit8, .digit9, - + // Common Control Keys .space, .enter, .tab, .backspace, .escape, .delete, - + // Arrow Keys .arrowUp, .arrowDown, .arrowLeft, .arrowRight, - + // Navigation Keys .home, .end, .pageUp, .pageDown, .insert, - + // Function Keys (F1-F20) .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10, .f11, .f12, .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20, - + // Modifier Keys .shiftLeft, .shiftRight, .controlLeft, .controlRight, .altLeft, .altRight, .metaLeft, .metaRight, .capsLock, - + // Punctuation & Symbols .minus, .equal, .backquote, .bracketLeft, .bracketRight, .backslash, .semicolon, .quote, .comma, .period, .slash, - + // Numpad .numLock, .numpad0, .numpad1, .numpad2, .numpad3, .numpad4, .numpad5, .numpad6, .numpad7, .numpad8, .numpad9, .numpadAdd, .numpadSubtract, .numpadMultiply, .numpadDivide, .numpadDecimal, .numpadEqual, .numpadEnter, .numpadComma, - + // Media Keys .audioVolumeUp, .audioVolumeDown, .audioVolumeMute, - + // International Keys .intlBackslash, .intlRo, .intlYen, - + // Other .contextMenu ] @@ -1167,11 +1167,11 @@ extension Ghostty.Input.Key: AppEnum { .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J", .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T", .u: "U", .v: "V", .w: "W", .x: "X", .y: "Y", .z: "Z", - + // Numbers (0-9) .digit0: "0", .digit1: "1", .digit2: "2", .digit3: "3", .digit4: "4", .digit5: "5", .digit6: "6", .digit7: "7", .digit8: "8", .digit9: "9", - + // Common Control Keys .space: "Space", .enter: "Enter", @@ -1179,26 +1179,26 @@ extension Ghostty.Input.Key: AppEnum { .backspace: "Backspace", .escape: "Escape", .delete: "Delete", - + // Arrow Keys .arrowUp: "Up Arrow", .arrowDown: "Down Arrow", .arrowLeft: "Left Arrow", .arrowRight: "Right Arrow", - + // Navigation Keys .home: "Home", .end: "End", .pageUp: "Page Up", .pageDown: "Page Down", .insert: "Insert", - + // Function Keys (F1-F20) .f1: "F1", .f2: "F2", .f3: "F3", .f4: "F4", .f5: "F5", .f6: "F6", .f7: "F7", .f8: "F8", .f9: "F9", .f10: "F10", .f11: "F11", .f12: "F12", .f13: "F13", .f14: "F14", .f15: "F15", .f16: "F16", .f17: "F17", .f18: "F18", .f19: "F19", .f20: "F20", - + // Modifier Keys .shiftLeft: "Left Shift", .shiftRight: "Right Shift", @@ -1209,7 +1209,7 @@ extension Ghostty.Input.Key: AppEnum { .metaLeft: "Left Command", .metaRight: "Right Command", .capsLock: "Caps Lock", - + // Punctuation & Symbols .minus: "Minus (-)", .equal: "Equal (=)", @@ -1222,7 +1222,7 @@ extension Ghostty.Input.Key: AppEnum { .comma: "Comma (,)", .period: "Period (.)", .slash: "Slash (/)", - + // Numpad .numLock: "Num Lock", .numpad0: "Numpad 0", .numpad1: "Numpad 1", .numpad2: "Numpad 2", @@ -1236,17 +1236,17 @@ extension Ghostty.Input.Key: AppEnum { .numpadEqual: "Numpad Equal", .numpadEnter: "Numpad Enter", .numpadComma: "Numpad Comma", - + // Media Keys .audioVolumeUp: "Volume Up", .audioVolumeDown: "Volume Down", .audioVolumeMute: "Volume Mute", - + // International Keys .intlBackslash: "International Backslash", .intlRo: "International Ro", .intlYen: "International Yen", - + // Other .contextMenu: "Context Menu" ] diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 78c967661..8ab476267 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -130,7 +130,7 @@ class NativeFullscreen: FullscreenBase, FullscreenStyle { class NonNativeFullscreen: FullscreenBase, FullscreenStyle { var fullscreenMode: FullscreenMode { .nonNative } - + // Non-native fullscreen never supports tabs because tabs require // the "titled" style and we don't have it for non-native fullscreen. var supportsTabs: Bool { false } @@ -223,7 +223,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Being untitled let's our content take up the full frame. window.styleMask.remove(.titled) - // We dont' want the non-native fullscreen window to be resizable + // We don't want the non-native fullscreen window to be resizable // from the edges. window.styleMask.remove(.resizable) @@ -277,7 +277,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if let window = window as? TerminalWindow, window.isTabBar(c) { continue } - + if window.titlebarAccessoryViewControllers.firstIndex(of: c) == nil { window.addTitlebarAccessoryViewController(c) } @@ -286,7 +286,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Removing "titled" also clears our toolbar window.toolbar = savedState.toolbar window.toolbarStyle = savedState.toolbarStyle - + // If the window was previously in a tab group that isn't empty now, // we re-add it. We have to do this because our process of doing non-native // fullscreen removes the window from the tab group. @@ -412,7 +412,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.toolbar = window.toolbar self.toolbarStyle = window.toolbarStyle self.dock = window.screen?.hasDock ?? false - + self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. window.titlebarAccessoryViewControllers diff --git a/src/Surface.zig b/src/Surface.zig index fd658a43b..c9223d0ad 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1222,7 +1222,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { break :gui false; }) return; - // If a native GUI notification was not showm. update our terminal to + // If a native GUI notification was not shown, update our terminal to // note the abnormal exit. self.childExitedAbnormally(info) catch |err| { log.err("error handling abnormal child exit err={}", .{err}); @@ -1232,7 +1232,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { return; } - // We output a message so that the user knows whats going on and + // We output a message so that the user knows what's going on and // doesn't think their terminal just froze. We show this unconditionally // on close even if `wait_after_command` is false and the surface closes // immediately because if a user does an `undo` to restore a closed diff --git a/src/config/theme.zig b/src/config/theme.zig index 7ba6e5885..8776fb1bf 100644 --- a/src/config/theme.zig +++ b/src/config/theme.zig @@ -221,7 +221,7 @@ pub fn open( // Unlikely scenario: the theme doesn't exist. In this case, we reset // our iterator, reiterate over in order to build a better error message. - // This does double allocate some memory but for errors I think thats + // This does double allocate some memory but for errors I think that's // fine. it.reset(); while (try it.next()) |loc| { diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 0648c0edf..7dcff8416 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -562,7 +562,7 @@ test "exact fit" { try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1)); } -test "doesnt fit" { +test "doesn't fit" { const alloc = testing.allocator; var atlas = try init(alloc, 32, .grayscale); defer atlas.deinit(alloc); diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 6b01d79aa..17d7801ff 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -52,7 +52,7 @@ pub const Shaper = struct { /// 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. + /// by just creating it once and saving it for reuse. typesetter_attr_dict: *macos.foundation.Dictionary, /// List where we cache fonts, so we don't have to remake them for diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 94bfa2f0b..596a92044 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -405,7 +405,7 @@ fn testDrawRanges( const padding_x = width / 4; const padding_y = height / 4; - // Canvas to draw glyphs on, we'll re-use this for all glyphs. + // Canvas to draw glyphs on, we'll reuse this for all glyphs. var canvas = try font.sprite.Canvas.init( alloc, width, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 83c6ef38f..e7507b112 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -2020,7 +2020,7 @@ pub const Set = struct { /// /// `buffer_stream` is a FixedBufferStream used for temporary storage /// 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 + /// For example, 'a>b>c=x' and 'a>b>d=y' will reuse the 'a>b' written /// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'. pub fn formatEntries( self: Value, diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index 736df58a0..3716c226e 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -153,7 +153,7 @@ fn kitty( // 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 + // what's happening. See legacy()'s similar logic for more details // on how to verify this. if (event.utf8.len > 0) utf8: { switch (event.key) { diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 2be2b9a26..bdf967ed2 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -10,7 +10,7 @@ pub const ButtonState = enum(c_int) { press, }; -/// Possible mouse buttons. We only track up to 11 because thats the maximum +/// Possible mouse buttons. We only track up to 11 because that's the maximum /// button input that terminal mouse tracking handles without becoming /// ambiguous. /// diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 86a7b473c..dc498b58d 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -1213,7 +1213,7 @@ fn renderTermioWindow(self: *Inspector) void { cimgui.c.igText("%s", ev.str.ptr); // If the event is selected, we render info about it. For now - // we put this in the last column because thats the widest and + // we put this in the last column because that's the widest and // imgui has no way to make a column span. if (ev.imgui_selected) { { diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 39eec7b43..4ebe501f7 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2099,7 +2099,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // We also need to reset the shaper cache so shaper info - // from the previous font isn't re-used for the new font. + // from the previous font isn't reused for the new font. const font_shaper_cache = font.ShaperCache.init(); self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6c9d8b585..8bb167cd1 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -675,7 +675,7 @@ fn printCell( // TODO: this case was not handled in the old terminal implementation // but it feels like we should do something. investigate other - // terminals (xterm mainly) and see whats up. + // terminals (xterm mainly) and see what's up. .spacer_head => {}, } } diff --git a/src/terminal/kitty/graphics_unicode.zig b/src/terminal/kitty/graphics_unicode.zig index ceadf63ee..a223797ba 100644 --- a/src/terminal/kitty/graphics_unicode.zig +++ b/src/terminal/kitty/graphics_unicode.zig @@ -256,7 +256,7 @@ pub const Placement = struct { if (img_scale_source.y < img_scaled.y_offset) { // If our source rect y is within the offset area, we need to // adjust our source rect and destination since the source texture - // doesnt actually have the offset area blank. + // doesn't actually have the offset area blank. const offset: f64 = img_scaled.y_offset - img_scale_source.y; img_scale_source.height -= offset; y_offset = offset; @@ -286,7 +286,7 @@ pub const Placement = struct { if (img_scale_source.x < img_scaled.x_offset) { // If our source rect x is within the offset area, we need to // adjust our source rect and destination since the source texture - // doesnt actually have the offset area blank. + // doesn't actually have the offset area blank. const offset: f64 = img_scaled.x_offset - img_scale_source.x; img_scale_source.width -= offset; x_offset = offset; diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index e67682ff5..883dd2f0d 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -215,7 +215,7 @@ pub fn RefCountedSet( OutOfMemory, /// The set needs to be rehashed, as there are many dead - /// items with lower IDs which are inaccessible for re-use. + /// items with lower IDs which are inaccessible for reuse. NeedsRehash, }; @@ -437,7 +437,7 @@ pub fn RefCountedSet( } /// Delete an item, removing any references from - /// the table, and freeing its ID to be re-used. + /// the table, and freeing its ID to be reused. fn deleteItem(self: *Self, base: anytype, id: Id, ctx: Context) void { const table = self.table.ptr(base); const items = self.items.ptr(base); @@ -585,7 +585,7 @@ pub fn RefCountedSet( const item = &items[id]; // If there's a dead item then we resurrect it - // for our value so that we can re-use its ID, + // for our value so that we can reuse its ID, // unless its ID is greater than the one we're // given (i.e. prefer smaller IDs). if (item.meta.ref == 0) { @@ -645,7 +645,7 @@ pub fn RefCountedSet( } // Our chosen ID may have changed if we decided - // to re-use a dead item's ID, so we make sure + // to reuse a dead item's ID, so we make sure // the chosen bucket contains the correct ID. table[new_item.meta.bucket] = chosen_id; diff --git a/src/terminal/tmux/viewer.zig b/src/terminal/tmux/viewer.zig index 0fcaaf207..62a0f1d00 100644 --- a/src/terminal/tmux/viewer.zig +++ b/src/terminal/tmux/viewer.zig @@ -208,7 +208,7 @@ pub const Viewer = struct { /// caller is responsible for diffing the new window list against /// the prior one. Remember that for a given Viewer, window IDs /// are guaranteed to be stable. Additionally, tmux (as of Dec 2025) - /// never re-uses window IDs within a server process lifetime. + /// never reuses window IDs within a server process lifetime. windows: []const Window, pub fn format(self: Action, writer: *std.Io.Writer) !void { -- 2.51.2 From f54ac110802a71ac26b7e6c62008085425a7da09 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Dec 2025 13:36:43 -0800 Subject: [PATCH 069/349] terminal: search will re-scroll to navigate to a single match Fixes #9958 Replaces #9989 This changes the search navigation logic to always scroll if there is a selected search result so long as the search result isn't already within the viewport. --- src/terminal/PageList.zig | 9 +++++++ src/terminal/search/Thread.zig | 48 +++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 9e14e2a75..07b264ef5 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3821,6 +3821,15 @@ pub const PageIterator = struct { pub fn fullPage(self: Chunk) bool { return self.start == 0 and self.end == self.node.data.size.rows; } + + /// Returns true if this chunk overlaps with the given other chunk + /// in any way. + pub fn overlaps(self: Chunk, other: Chunk) bool { + if (self.node != other.node) return false; + if (self.end <= other.start) return false; + if (self.start >= other.end) return false; + return true; + } }; }; diff --git a/src/terminal/search/Thread.zig b/src/terminal/search/Thread.zig index 8f2d73f16..3f5377417 100644 --- a/src/terminal/search/Thread.zig +++ b/src/terminal/search/Thread.zig @@ -257,18 +257,46 @@ fn select(self: *Thread, sel: ScreenSearch.Select) !void { self.opts.mutex.lock(); defer self.opts.mutex.unlock(); - // The selection will trigger a selection change notification - // if it did change. - if (try screen_search.select(sel)) scroll: { - if (screen_search.selected) |m| { - // Selection changed, let's scroll the viewport to see it - // since we have the lock anyways. - const screen = self.opts.terminal.screens.get( - s.last_screen.key, - ) orelse break :scroll; - screen.scroll(.{ .pin = m.highlight.start.* }); + // Make the selection. Ignore the result because we don't + // care if the selection didn't change. + _ = try screen_search.select(sel); + + // Grab our match if we have one. If we don't have a selection + // then we do nothing. + const flattened = screen_search.selectedMatch() orelse return; + + // No matter what we reset our selected match cache. This will + // trigger a callback which will trigger the renderer to wake up + // so it can be notified the screen scrolled. + s.last_screen.selected = null; + + // Grab the current screen and see if this match is visible within + // the viewport already. If it is, we do nothing. + const screen = self.opts.terminal.screens.get( + s.last_screen.key, + ) orelse return; + + // Grab the viewport. Viewports and selections are usually small + // so this check isn't very expensive, despite appearing O(N^2), + // both Ns are usually equal to 1. + var it = screen.pages.pageIterator( + .right_down, + .{ .viewport = .{} }, + null, + ); + const hl_chunks = flattened.chunks.slice(); + while (it.next()) |chunk| { + for (0..hl_chunks.len) |i| { + const hl_chunk = hl_chunks.get(i); + if (chunk.overlaps(.{ + .node = hl_chunk.node, + .start = hl_chunk.start, + .end = hl_chunk.end, + })) return; } } + + screen.scroll(.{ .pin = flattened.startPin() }); } /// Change the search term to the given value. -- 2.51.2 From 2415116ad0680c9ae2d18f8445e067b24830ca49 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Dec 2025 13:52:59 -0800 Subject: [PATCH 070/349] Revert "macOS: move `NSGlassEffectView` into `TerminalViewContainer` (#10046)" This reverts commit b8490f40c5a88d3ef879043d05a5493f24e123d4, reversing changes made to 050278feaeb79fe9d849fca48e445acadcd74397. --- macos/Ghostty.xcodeproj/project.pbxproj | 1 - .../QuickTerminalController.swift | 13 +- .../Terminal/BaseTerminalController.swift | 2 +- .../Terminal/TerminalController.swift | 4 +- .../Terminal/TerminalViewContainer.swift | 127 ------------------ .../Window Styles/TerminalWindow.swift | 62 ++++++++- macos/Sources/Ghostty/Ghostty.Config.swift | 12 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 3 - 8 files changed, 68 insertions(+), 156 deletions(-) delete mode 100644 macos/Sources/Features/Terminal/TerminalViewContainer.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index feda1bed0..91c2300cc 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -118,7 +118,6 @@ Features/Terminal/TerminalRestorable.swift, Features/Terminal/TerminalTabColor.swift, Features/Terminal/TerminalView.swift, - Features/Terminal/TerminalViewContainer.swift, "Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift", "Features/Terminal/Window Styles/Terminal.xib", "Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib", diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 07c0c4c19..8a642034f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -137,11 +137,11 @@ class QuickTerminalController: BaseTerminalController { } // Setup our content - window.contentView = TerminalViewContainer( + window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self - ) + )) // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { @@ -609,7 +609,7 @@ class QuickTerminalController: BaseTerminalController { // If we have window transparency then set it transparent. Otherwise set it opaque. // Also check if the user has overridden transparency to be fully opaque. - if !isBackgroundOpaque && (self.derivedConfig.backgroundOpacity < 1 || derivedConfig.backgroundBlur.isGlassStyle) { + if !isBackgroundOpaque && self.derivedConfig.backgroundOpacity < 1 { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -617,9 +617,7 @@ class QuickTerminalController: BaseTerminalController { // Terminal.app more easily. window.backgroundColor = .white.withAlphaComponent(0.001) - if !derivedConfig.backgroundBlur.isGlassStyle { - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) - } + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) } else { window.isOpaque = true window.backgroundColor = .windowBackgroundColor @@ -724,7 +722,6 @@ class QuickTerminalController: BaseTerminalController { let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior let quickTerminalSize: QuickTerminalSize let backgroundOpacity: Double - let backgroundBlur: Ghostty.Config.BackgroundBlur init() { self.quickTerminalScreen = .main @@ -733,7 +730,6 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = .move self.quickTerminalSize = QuickTerminalSize() self.backgroundOpacity = 1.0 - self.backgroundBlur = .disabled } init(_ config: Ghostty.Config) { @@ -743,7 +739,6 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior self.quickTerminalSize = config.quickTerminalSize self.backgroundOpacity = config.backgroundOpacity - self.backgroundBlur = config.backgroundBlur } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 1750e949d..d79c89d2d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1298,7 +1298,7 @@ extension BaseTerminalController: NSMenuItemValidation { } else { scheme = GHOSTTY_COLOR_SCHEME_LIGHT } - guard scheme != appliedColorScheme, !surfaceTree.isEmpty else { + guard scheme != appliedColorScheme else { return } for surfaceView in surfaceTree { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c5481851b..bccdd9c69 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -936,11 +936,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // Initialize our content view to the SwiftUI root - window.contentView = TerminalViewContainer( + window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self, - ) + )) // If we have a default size, we want to apply it. if let defaultSize { diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift deleted file mode 100644 index f4e2fc080..000000000 --- a/macos/Sources/Features/Terminal/TerminalViewContainer.swift +++ /dev/null @@ -1,127 +0,0 @@ -import AppKit -import SwiftUI - -/// Use this container to achieve a glass effect at the window level. -/// Modifying `NSThemeFrame` can sometimes be unpredictable. -class TerminalViewContainer: NSView { - private let terminalView: NSView - - /// Glass effect view for liquid glass background when transparency is enabled - private var glassEffectView: NSView? - private var derivedConfig: DerivedConfig - - init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) { - self.derivedConfig = DerivedConfig(config: ghostty.config) - self.terminalView = NSHostingView(rootView: TerminalView( - ghostty: ghostty, - viewModel: viewModel, - delegate: delegate - )) - super.init(frame: .zero) - setup() - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setup() { - addSubview(terminalView) - terminalView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - terminalView.topAnchor.constraint(equalTo: topAnchor), - terminalView.leadingAnchor.constraint(equalTo: leadingAnchor), - terminalView.bottomAnchor.constraint(equalTo: bottomAnchor), - terminalView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - - NotificationCenter.default.addObserver( - self, - selector: #selector(ghosttyConfigDidChange(_:)), - name: .ghosttyConfigDidChange, - object: nil - ) - } - - override func viewDidMoveToWindow() { - super.viewDidMoveToWindow() - updateGlassEffectIfNeeded() - } - - @objc private func ghosttyConfigDidChange(_ notification: Notification) { - guard let config = notification.userInfo?[ - Notification.Name.GhosttyConfigChangeKey - ] as? Ghostty.Config else { return } - let newValue = DerivedConfig(config: config) - guard newValue != derivedConfig else { return } - derivedConfig = newValue - DispatchQueue.main.async(execute: updateGlassEffectIfNeeded) - } -} - -// MARK: Glass - -private extension TerminalViewContainer { -#if compiler(>=6.2) - @available(macOS 26.0, *) - func addGlassEffectViewIfNeeded() -> NSGlassEffectView? { - if let existed = glassEffectView as? NSGlassEffectView { - return existed - } - guard let themeFrameView = window?.contentView?.superview else { - return nil - } - let effectView = NSGlassEffectView() - addSubview(effectView, positioned: .below, relativeTo: terminalView) - effectView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - effectView.topAnchor.constraint(equalTo: topAnchor, constant: -themeFrameView.safeAreaInsets.top), - effectView.leadingAnchor.constraint(equalTo: leadingAnchor), - effectView.bottomAnchor.constraint(equalTo: bottomAnchor), - effectView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) - glassEffectView = effectView - return effectView - } -#endif // compiler(>=6.2) - - func updateGlassEffectIfNeeded() { -#if compiler(>=6.2) - guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { - glassEffectView?.removeFromSuperview() - glassEffectView = nil - return - } - guard let effectView = addGlassEffectViewIfNeeded() else { - return - } - switch derivedConfig.backgroundBlur { - case .macosGlassRegular: - effectView.style = NSGlassEffectView.Style.regular - case .macosGlassClear: - effectView.style = NSGlassEffectView.Style.clear - default: - break - } - let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor) - effectView.tintColor = backgroundColor - .withAlphaComponent(derivedConfig.backgroundOpacity) - if let window, window.responds(to: Selector(("_cornerRadius"))), let cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat { - effectView.cornerRadius = cornerRadius - } -#endif // compiler(>=6.2) - } - - struct DerivedConfig: Equatable { - var backgroundOpacity: Double = 0 - var backgroundBlur: Ghostty.Config.BackgroundBlur - var backgroundColor: Color = .clear - - init(config: Ghostty.Config) { - self.backgroundBlur = config.backgroundBlur - self.backgroundOpacity = config.backgroundOpacity - self.backgroundColor = config.backgroundColor - } - } -} diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 9debd2cb3..4196df97f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -474,7 +474,7 @@ class TerminalWindow: NSWindow { let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && !forceOpaque && - (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) + surfaceConfig.backgroundOpacity < 1 { isOpaque = false @@ -483,8 +483,15 @@ class TerminalWindow: NSWindow { // Terminal.app more easily. backgroundColor = .white.withAlphaComponent(0.001) - // We don't need to set blur when using glass - if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { + // Add liquid glass behind terminal content + if #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle { + setupGlassLayer() + } else if let appDelegate = NSApp.delegate as? AppDelegate { + // If we had a prior glass layer we should remove it + if #available(macOS 26.0, *) { + removeGlassLayer() + } + ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) @@ -492,6 +499,11 @@ class TerminalWindow: NSWindow { } else { isOpaque = true + // Remove liquid glass when not transparent + if #available(macOS 26.0, *) { + removeGlassLayer() + } + let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor) self.backgroundColor = backgroundColor.withAlphaComponent(1) } @@ -569,6 +581,50 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } + +#if compiler(>=6.2) + // MARK: Glass + + @available(macOS 26.0, *) + private func setupGlassLayer() { + // Remove existing glass effect view + removeGlassLayer() + + // Get the window content view (parent of the NSHostingView) + guard let contentView else { return } + guard let windowContentView = contentView.superview else { return } + + // Create NSGlassEffectView for native glass effect + let effectView = NSGlassEffectView() + + // Map Ghostty config to NSGlassEffectView style + switch derivedConfig.backgroundBlur { + case .macosGlassRegular: + effectView.style = NSGlassEffectView.Style.regular + case .macosGlassClear: + effectView.style = NSGlassEffectView.Style.clear + default: + // Should not reach here since we check for glass style before calling + // setupGlassLayer() + assertionFailure() + } + + effectView.cornerRadius = derivedConfig.windowCornerRadius + effectView.tintColor = preferredBackgroundColor + effectView.frame = windowContentView.bounds + effectView.autoresizingMask = [.width, .height] + + // Position BELOW the terminal content to act as background + windowContentView.addSubview(effectView, positioned: .below, relativeTo: contentView) + glassEffectView = effectView + } + + @available(macOS 26.0, *) + private func removeGlassLayer() { + glassEffectView?.removeFromSuperview() + glassEffectView = nil + } +#endif // compiler(>=6.2) // MARK: Config diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index b3a8700e9..5aa79a149 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -658,17 +658,9 @@ extension Ghostty.Config { case 0: self = .disabled case -1: - if #available(macOS 26.0, *) { - self = .macosGlassRegular - } else { - self = .disabled - } + self = .macosGlassRegular case -2: - if #available(macOS 26.0, *) { - self = .macosGlassClear - } else { - self = .disabled - } + self = .macosGlassClear default: self = .radius(Int(value)) } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 77e1c43d4..37cc9282e 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1654,7 +1654,6 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double - let backgroundBlur: Ghostty.Config.BackgroundBlur let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1663,7 +1662,6 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 - self.backgroundBlur = .disabled self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1673,7 +1671,6 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity - self.backgroundBlur = config.backgroundBlur self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) -- 2.51.2 From 1c90af3569d3591f735678987639885783e97e20 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 24 Dec 2025 21:29:14 +0100 Subject: [PATCH 071/349] macOS: move `NSGlassEffectView` into `TerminalViewContainer` --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../QuickTerminalController.swift | 13 +- .../Terminal/TerminalController.swift | 4 +- .../Terminal/TerminalViewContainer.swift | 127 ++++++++++++++++++ .../Window Styles/TerminalWindow.swift | 62 +-------- macos/Sources/Ghostty/Ghostty.Config.swift | 12 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 3 + 7 files changed, 155 insertions(+), 67 deletions(-) create mode 100644 macos/Sources/Features/Terminal/TerminalViewContainer.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 91c2300cc..feda1bed0 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ Features/Terminal/TerminalRestorable.swift, Features/Terminal/TerminalTabColor.swift, Features/Terminal/TerminalView.swift, + Features/Terminal/TerminalViewContainer.swift, "Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift", "Features/Terminal/Window Styles/Terminal.xib", "Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib", diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 8a642034f..07c0c4c19 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -137,11 +137,11 @@ class QuickTerminalController: BaseTerminalController { } // Setup our content - window.contentView = NSHostingView(rootView: TerminalView( + window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self - )) + ) // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { @@ -609,7 +609,7 @@ class QuickTerminalController: BaseTerminalController { // If we have window transparency then set it transparent. Otherwise set it opaque. // Also check if the user has overridden transparency to be fully opaque. - if !isBackgroundOpaque && self.derivedConfig.backgroundOpacity < 1 { + if !isBackgroundOpaque && (self.derivedConfig.backgroundOpacity < 1 || derivedConfig.backgroundBlur.isGlassStyle) { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -617,7 +617,9 @@ class QuickTerminalController: BaseTerminalController { // Terminal.app more easily. window.backgroundColor = .white.withAlphaComponent(0.001) - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + if !derivedConfig.backgroundBlur.isGlassStyle { + ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) + } } else { window.isOpaque = true window.backgroundColor = .windowBackgroundColor @@ -722,6 +724,7 @@ class QuickTerminalController: BaseTerminalController { let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior let quickTerminalSize: QuickTerminalSize let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur init() { self.quickTerminalScreen = .main @@ -730,6 +733,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = .move self.quickTerminalSize = QuickTerminalSize() self.backgroundOpacity = 1.0 + self.backgroundBlur = .disabled } init(_ config: Ghostty.Config) { @@ -739,6 +743,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior self.quickTerminalSize = config.quickTerminalSize self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bccdd9c69..c5481851b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -936,11 +936,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // Initialize our content view to the SwiftUI root - window.contentView = NSHostingView(rootView: TerminalView( + window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self, - )) + ) // If we have a default size, we want to apply it. if let defaultSize { diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift new file mode 100644 index 000000000..f4e2fc080 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -0,0 +1,127 @@ +import AppKit +import SwiftUI + +/// Use this container to achieve a glass effect at the window level. +/// Modifying `NSThemeFrame` can sometimes be unpredictable. +class TerminalViewContainer: NSView { + private let terminalView: NSView + + /// Glass effect view for liquid glass background when transparency is enabled + private var glassEffectView: NSView? + private var derivedConfig: DerivedConfig + + init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) { + self.derivedConfig = DerivedConfig(config: ghostty.config) + self.terminalView = NSHostingView(rootView: TerminalView( + ghostty: ghostty, + viewModel: viewModel, + delegate: delegate + )) + super.init(frame: .zero) + setup() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + addSubview(terminalView) + terminalView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + terminalView.topAnchor.constraint(equalTo: topAnchor), + terminalView.leadingAnchor.constraint(equalTo: leadingAnchor), + terminalView.bottomAnchor.constraint(equalTo: bottomAnchor), + terminalView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + + NotificationCenter.default.addObserver( + self, + selector: #selector(ghosttyConfigDidChange(_:)), + name: .ghosttyConfigDidChange, + object: nil + ) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateGlassEffectIfNeeded() + } + + @objc private func ghosttyConfigDidChange(_ notification: Notification) { + guard let config = notification.userInfo?[ + Notification.Name.GhosttyConfigChangeKey + ] as? Ghostty.Config else { return } + let newValue = DerivedConfig(config: config) + guard newValue != derivedConfig else { return } + derivedConfig = newValue + DispatchQueue.main.async(execute: updateGlassEffectIfNeeded) + } +} + +// MARK: Glass + +private extension TerminalViewContainer { +#if compiler(>=6.2) + @available(macOS 26.0, *) + func addGlassEffectViewIfNeeded() -> NSGlassEffectView? { + if let existed = glassEffectView as? NSGlassEffectView { + return existed + } + guard let themeFrameView = window?.contentView?.superview else { + return nil + } + let effectView = NSGlassEffectView() + addSubview(effectView, positioned: .below, relativeTo: terminalView) + effectView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + effectView.topAnchor.constraint(equalTo: topAnchor, constant: -themeFrameView.safeAreaInsets.top), + effectView.leadingAnchor.constraint(equalTo: leadingAnchor), + effectView.bottomAnchor.constraint(equalTo: bottomAnchor), + effectView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + glassEffectView = effectView + return effectView + } +#endif // compiler(>=6.2) + + func updateGlassEffectIfNeeded() { +#if compiler(>=6.2) + guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + glassEffectView?.removeFromSuperview() + glassEffectView = nil + return + } + guard let effectView = addGlassEffectViewIfNeeded() else { + return + } + switch derivedConfig.backgroundBlur { + case .macosGlassRegular: + effectView.style = NSGlassEffectView.Style.regular + case .macosGlassClear: + effectView.style = NSGlassEffectView.Style.clear + default: + break + } + let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor) + effectView.tintColor = backgroundColor + .withAlphaComponent(derivedConfig.backgroundOpacity) + if let window, window.responds(to: Selector(("_cornerRadius"))), let cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat { + effectView.cornerRadius = cornerRadius + } +#endif // compiler(>=6.2) + } + + struct DerivedConfig: Equatable { + var backgroundOpacity: Double = 0 + var backgroundBlur: Ghostty.Config.BackgroundBlur + var backgroundColor: Color = .clear + + init(config: Ghostty.Config) { + self.backgroundBlur = config.backgroundBlur + self.backgroundOpacity = config.backgroundOpacity + self.backgroundColor = config.backgroundColor + } + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 4196df97f..9debd2cb3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -474,7 +474,7 @@ class TerminalWindow: NSWindow { let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && !forceOpaque && - surfaceConfig.backgroundOpacity < 1 + (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) { isOpaque = false @@ -483,15 +483,8 @@ class TerminalWindow: NSWindow { // Terminal.app more easily. backgroundColor = .white.withAlphaComponent(0.001) - // Add liquid glass behind terminal content - if #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle { - setupGlassLayer() - } else if let appDelegate = NSApp.delegate as? AppDelegate { - // If we had a prior glass layer we should remove it - if #available(macOS 26.0, *) { - removeGlassLayer() - } - + // We don't need to set blur when using glass + if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) @@ -499,11 +492,6 @@ class TerminalWindow: NSWindow { } else { isOpaque = true - // Remove liquid glass when not transparent - if #available(macOS 26.0, *) { - removeGlassLayer() - } - let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor) self.backgroundColor = backgroundColor.withAlphaComponent(1) } @@ -581,50 +569,6 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } - -#if compiler(>=6.2) - // MARK: Glass - - @available(macOS 26.0, *) - private func setupGlassLayer() { - // Remove existing glass effect view - removeGlassLayer() - - // Get the window content view (parent of the NSHostingView) - guard let contentView else { return } - guard let windowContentView = contentView.superview else { return } - - // Create NSGlassEffectView for native glass effect - let effectView = NSGlassEffectView() - - // Map Ghostty config to NSGlassEffectView style - switch derivedConfig.backgroundBlur { - case .macosGlassRegular: - effectView.style = NSGlassEffectView.Style.regular - case .macosGlassClear: - effectView.style = NSGlassEffectView.Style.clear - default: - // Should not reach here since we check for glass style before calling - // setupGlassLayer() - assertionFailure() - } - - effectView.cornerRadius = derivedConfig.windowCornerRadius - effectView.tintColor = preferredBackgroundColor - effectView.frame = windowContentView.bounds - effectView.autoresizingMask = [.width, .height] - - // Position BELOW the terminal content to act as background - windowContentView.addSubview(effectView, positioned: .below, relativeTo: contentView) - glassEffectView = effectView - } - - @available(macOS 26.0, *) - private func removeGlassLayer() { - glassEffectView?.removeFromSuperview() - glassEffectView = nil - } -#endif // compiler(>=6.2) // MARK: Config diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 5aa79a149..b3a8700e9 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -658,9 +658,17 @@ extension Ghostty.Config { case 0: self = .disabled case -1: - self = .macosGlassRegular + if #available(macOS 26.0, *) { + self = .macosGlassRegular + } else { + self = .disabled + } case -2: - self = .macosGlassClear + if #available(macOS 26.0, *) { + self = .macosGlassClear + } else { + self = .disabled + } default: self = .radius(Int(value)) } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 37cc9282e..77e1c43d4 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1654,6 +1654,7 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1662,6 +1663,7 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 + self.backgroundBlur = .disabled self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1671,6 +1673,7 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) -- 2.51.2 From 88e471e015083e6970b81e66e3e6b4719004d5ed Mon Sep 17 00:00:00 2001 From: Zongyuan Li Date: Fri, 26 Dec 2025 18:33:00 +0800 Subject: [PATCH 072/349] fix(iOS): fix iOS app startup failure Fixes #7643 This commit address the issue with 3 minor fixes: 1. Initialize ghostty lib before app start, or global allocator will be null. 2. `addSublayer` should be called on CALayer object, which is the property 'layer' of UIView 3. According to apple's [document](https://developer.apple.com/documentation/metal/mtlstoragemode/managed?language=objc), managed storage mode is not supported by iOS. So always use shared mode. FYI, another [fix](https://github.com/mitchellh/libxev/pull/204) in libxev is also required to make iOS app work. --- macos/Sources/App/iOS/iOSApp.swift | 10 +++++++++- src/renderer/Metal.zig | 10 +++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift index 4af94491c..a1aafcc7d 100644 --- a/macos/Sources/App/iOS/iOSApp.swift +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -1,8 +1,16 @@ import SwiftUI +import GhosttyKit @main struct Ghostty_iOSApp: App { - @StateObject private var ghostty_app = Ghostty.App() + @StateObject private var ghostty_app: Ghostty.App + + init() { + if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS { + preconditionFailure("Initialize ghostty backend failed") + } + _ghostty_app = StateObject(wrappedValue: Ghostty.App()) + } var body: some Scene { WindowGroup { diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 2aac285c6..6c7432d21 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -76,8 +76,11 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { errdefer queue.release(); // Grab metadata about the device. - const default_storage_mode: mtl.MTLResourceOptions.StorageMode = - if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed; + const default_storage_mode: mtl.MTLResourceOptions.StorageMode = switch (comptime builtin.os.tag) { + // manage mode is not supported by iOS + .ios => .shared, + else => if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed, + }; const max_texture_size = queryMaxTextureSize(device); log.debug( "device properties default_storage_mode={} max_texture_size={}", @@ -123,7 +126,8 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { }, .ios => { - info.view.msgSend(void, objc.sel("addSublayer"), .{layer.layer.value}); + const view_layer = objc.Object.fromId(info.view.getProperty(?*anyopaque, "layer")); + view_layer.msgSend(void, objc.sel("addSublayer:"), .{layer.layer.value}); }, else => @compileError("unsupported target for Metal"), -- 2.51.2 From 6ab884d69f8854351dd140ed9a9fabf9c526488c Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Fri, 26 Dec 2025 13:35:25 +0100 Subject: [PATCH 073/349] macOS: fix intrinsicContentSize of `TerminalViewContainer` --- .../Sources/Features/Terminal/TerminalViewContainer.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift index f4e2fc080..1765edec3 100644 --- a/macos/Sources/Features/Terminal/TerminalViewContainer.swift +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -26,6 +26,13 @@ class TerminalViewContainer: NSView { fatalError("init(coder:) has not been implemented") } + /// To make ``TerminalController/DefaultSize/contentIntrinsicSize`` + /// work in ``TerminalController/windowDidLoad()``, + /// we override this to provide the correct size. + override var intrinsicContentSize: NSSize { + terminalView.intrinsicContentSize + } + private func setup() { addSubview(terminalView) terminalView.translatesAutoresizingMaskIntoConstraints = false -- 2.51.2 From 79cc22e18685709aa2d8e1657a7492e5f9030d6d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 07:28:41 -0800 Subject: [PATCH 074/349] terminal: fix crash when sliding window sees empty node Related to #10063 This fixes a crash that can happen if the SlidingWindow search portion sees a zero-byte page. We have more fixes to implement in the circular buffer handling but putting the fix at this layer also prevents some unnecessary allocations for zero-byte data. --- src/terminal/search/sliding_window.zig | 39 +++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/terminal/search/sliding_window.zig b/src/terminal/search/sliding_window.zig index 3d64042ce..c3c29e085 100644 --- a/src/terminal/search/sliding_window.zig +++ b/src/terminal/search/sliding_window.zig @@ -575,9 +575,16 @@ pub const SlidingWindow = struct { ); } + // If our written data is empty, then there is nothing to + // add to our data set. + const written = encoded.written(); + if (written.len == 0) { + self.assertIntegrity(); + return 0; + } + // Get our written data. If we're doing a reverse search then we // need to reverse all our encodings. - const written = encoded.written(); switch (self.direction) { .forward => {}, .reverse => { @@ -1637,3 +1644,33 @@ test "SlidingWindow single append reversed soft wrapped" { try testing.expect(w.next() == null); try testing.expect(w.next() == null); } + +// This tests a real bug that occurred where a whitespace-only page +// that encodes to zero bytes would crash. +test "SlidingWindow append whitespace only node" { + const testing = std.testing; + const alloc = testing.allocator; + + var w: SlidingWindow = try .init(alloc, .forward, "x"); + defer w.deinit(); + + var s = try Screen.init(alloc, .{ + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }); + defer s.deinit(); + + // By setting the empty page to wrap we get a zero-byte page. + // This is invasive but its otherwise hard to reproduce naturally + // without creating a slow test. + const node: *PageList.List.Node = s.pages.pages.first.?; + const last_row = node.data.getRow(node.data.size.rows - 1); + last_row.wrap = true; + + try testing.expect(s.pages.pages.first == s.pages.pages.last); + _ = try w.append(node); + + // No matches expected + try testing.expect(w.next() == null); +} -- 2.51.2 From eb5d2e034bed4875deb84278aa3db9d34b29c243 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 10:33:50 -0800 Subject: [PATCH 075/349] datastruct/circ_buf: fix n=0 edge cases From #10063 This fixes and tests various edge cases around noop operations. --- src/datastruct/circ_buf.zig | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index 0caa9e85d..3e373cb94 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -217,6 +217,13 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { pub fn deleteOldest(self: *Self, n: usize) void { assert(n <= self.storage.len); + // Special case n == 0 otherwise we will accidentally break + // our circular buffer. + if (n == 0) { + @branchHint(.cold); + return; + } + // Clear the values back to default const slices = self.getPtrSlice(0, n); inline for (slices) |slice| @memset(slice, default); @@ -233,6 +240,12 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { /// the end of our buffer. This never "rotates" the buffer because /// the offset can only be within the size of the buffer. pub fn getPtrSlice(self: *Self, offset: usize, slice_len: usize) [2][]T { + // Special case the empty slice fast-path. + if (slice_len == 0) { + @branchHint(.cold); + return .{ &.{}, &.{} }; + } + // Note: this assertion is very important, it hints the compiler // which generates ~10% faster code than without it. assert(offset + slice_len <= self.capacity()); @@ -779,3 +792,75 @@ test "CircBuf resize shrink" { try testing.expectEqual(@as(u8, 3), slices[0][2]); } } + +test "CircBuf append empty slice" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + // Appending an empty slice to empty buffer should be a no-op + buf.appendSliceAssumeCapacity(""); + try testing.expectEqual(@as(usize, 0), buf.len()); + try testing.expect(!buf.full); + + // Buffer should still work normally after appending empty slice + buf.appendSliceAssumeCapacity("hi"); + try testing.expectEqual(@as(usize, 2), buf.len()); + + // Appending an empty slice to non-empty buffer should also be a no-op + buf.appendSliceAssumeCapacity(""); + try testing.expectEqual(@as(usize, 2), buf.len()); +} + +test "CircBuf getPtrSlice zero length" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + // getPtrSlice with zero length on empty buffer should return empty slices + const slices = buf.getPtrSlice(0, 0); + try testing.expectEqual(@as(usize, 0), slices[0].len); + try testing.expectEqual(@as(usize, 0), slices[1].len); + try testing.expectEqual(@as(usize, 0), buf.len()); + + // Fill buffer partially + buf.appendSliceAssumeCapacity("abc"); + try testing.expectEqual(@as(usize, 3), buf.len()); + + // getPtrSlice with zero length on non-empty buffer should also work + const slices2 = buf.getPtrSlice(0, 0); + try testing.expectEqual(@as(usize, 0), slices2[0].len); + try testing.expectEqual(@as(usize, 0), slices2[1].len); + try testing.expectEqual(@as(usize, 3), buf.len()); +} + +test "CircBuf deleteOldest zero" { + const testing = std.testing; + const alloc = testing.allocator; + + const Buf = CircBuf(u8, 0); + var buf = try Buf.init(alloc, 5); + defer buf.deinit(alloc); + + // deleteOldest(0) on empty buffer should be a no-op + buf.deleteOldest(0); + try testing.expectEqual(@as(usize, 0), buf.len()); + + // Fill buffer + buf.appendSliceAssumeCapacity("hello"); + try testing.expectEqual(@as(usize, 5), buf.len()); + + // deleteOldest(0) on non-empty buffer should be a no-op + buf.deleteOldest(0); + try testing.expectEqual(@as(usize, 5), buf.len()); + + // Verify data is unchanged + var it = buf.iterator(.forward); + try testing.expectEqual(@as(u8, 'h'), it.next().?.*); +} -- 2.51.2 From 26b104c9e0cc2c013480cc378cf62836cc24d64f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 10:43:35 -0800 Subject: [PATCH 076/349] terminal: Fix possible crash on RenderState with invalid mouse point Fixes #10032 --- src/terminal/render.zig | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/terminal/render.zig b/src/terminal/render.zig index 093476f2c..9d75fe4b7 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -816,6 +816,12 @@ pub const RenderState = struct { const row_pins = row_slice.items(.pin); const row_cells = row_slice.items(.cells); + // Our viewport point is sent in by the caller and can't be trusted. + // If it is outside the valid area then just return empty because + // we can't possibly have a link there. + if (viewport_point.x >= self.cols or + viewport_point.y >= row_pins.len) return result; + // Grab our link ID const link_pin: PageList.Pin = row_pins[viewport_point.y]; const link_page: *page.Page = &link_pin.node.data; @@ -1360,6 +1366,44 @@ test "linkCells with scrollback spanning pages" { try testing.expectEqual(@as(usize, 4), cells.count()); } +test "linkCells with invalid viewport point" { + const testing = std.testing; + const alloc = testing.allocator; + + var t = try Terminal.init(alloc, .{ + .cols = 10, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + var state: RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Row out of bound + { + var cells = try state.linkCells( + alloc, + .{ .x = 0, .y = t.rows + 10 }, + ); + defer cells.deinit(alloc); + try testing.expectEqual(0, cells.count()); + } + + // Col out of bound + { + var cells = try state.linkCells( + alloc, + .{ .x = t.cols + 10, .y = 0 }, + ); + defer cells.deinit(alloc); + try testing.expectEqual(0, cells.count()); + } +} + test "dirty row resets highlights" { const testing = std.testing; const alloc = testing.allocator; -- 2.51.2 From 14f592b8d4651c1257763fbcb7ab04f96988bb8d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 11:01:44 -0800 Subject: [PATCH 077/349] macOS: Don't duplicate command palette entries for terminal commands This is a regression introduced when we added macOS support for custom entries. I mistakingly thought that only custom entries were in the config, but we do initialize it with all! --- include/ghostty.h | 1 - .../App Intents/Entities/CommandEntity.swift | 23 ++++++------- .../TerminalCommandPalette.swift | 32 ++++--------------- macos/Sources/Ghostty/Ghostty.Surface.swift | 11 ------- src/apprt/embedded.zig | 17 ---------- 5 files changed, 19 insertions(+), 65 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index d6e6fba70..0ad15cf69 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1050,7 +1050,6 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_color_scheme_e); ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); -void ghostty_surface_commands(ghostty_surface_t, ghostty_command_s**, size_t*); bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift index f7abcc6de..3c7745e7c 100644 --- a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -1,4 +1,5 @@ import AppIntents +import Cocoa // MARK: AppEntity @@ -94,23 +95,23 @@ struct CommandQuery: EntityQuery { @MainActor func entities(for identifiers: [CommandEntity.ID]) async throws -> [CommandEntity] { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] } + let commands = appDelegate.ghostty.config.commandPaletteEntries + // Extract unique terminal IDs to avoid fetching duplicates let terminalIds = Set(identifiers.map(\.terminalId)) let terminals = try await TerminalEntity.defaultQuery.entities(for: Array(terminalIds)) - // Build a cache of terminals and their available commands - // This avoids repeated command fetching for the same terminal - typealias Tuple = (terminal: TerminalEntity, commands: [Ghostty.Command]) - let commandMap: [TerminalEntity.ID: Tuple] = + // Build a lookup from terminal ID to terminal entity + let terminalMap: [TerminalEntity.ID: TerminalEntity] = terminals.reduce(into: [:]) { result, terminal in - guard let commands = try? terminal.surfaceModel?.commands() else { return } - result[terminal.id] = (terminal: terminal, commands: commands) + result[terminal.id] = terminal } - + // Map each identifier to its corresponding CommandEntity. If a command doesn't // exist it maps to nil and is removed via compactMap. return identifiers.compactMap { id in - guard let (terminal, commands) = commandMap[id.terminalId], + guard let terminal = terminalMap[id.terminalId], let command = commands.first(where: { $0.actionKey == id.actionKey }) else { return nil } @@ -121,8 +122,8 @@ struct CommandQuery: EntityQuery { @MainActor func suggestedEntities() async throws -> [CommandEntity] { - guard let terminal = commandPaletteIntent?.terminal, - let surface = terminal.surfaceModel else { return [] } - return try surface.commands().map { CommandEntity($0, for: terminal) } + guard let appDelegate = NSApp.delegate as? AppDelegate, + let terminal = commandPaletteIntent?.terminal else { return [] } + return appDelegate.ghostty.config.commandPaletteEntries.map { CommandEntity($0, for: terminal) } } } diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 6efb588cd..e0237f257 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -64,7 +64,7 @@ struct TerminalCommandPaletteView: View { // Sort the rest. We replace ":" with a character that sorts before space // so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker // for stable ordering when titles are equal. - options.append(contentsOf: (jumpOptions + terminalOptions + customEntries).sorted { a, b in + options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t") let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t") let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized) @@ -83,11 +83,11 @@ struct TerminalCommandPaletteView: View { /// Commands for installing or canceling available updates. private var updateOptions: [CommandOption] { var options: [CommandOption] = [] - + guard let updateViewModel, updateViewModel.state.isInstallable else { return options } - + // We override the update available one only because we want to properly // convey it'll go all the way through. let title: String @@ -96,7 +96,7 @@ struct TerminalCommandPaletteView: View { } else { title = updateViewModel.text } - + options.append(CommandOption( title: title, description: updateViewModel.description, @@ -106,37 +106,19 @@ struct TerminalCommandPaletteView: View { ) { (NSApp.delegate as? AppDelegate)?.updateController.installUpdate() }) - + options.append(CommandOption( title: "Cancel or Skip Update", description: "Dismiss the current update process" ) { updateViewModel.state.cancel() }) - + return options } - /// Commands exposed by the terminal surface. - private var terminalOptions: [CommandOption] { - guard let surface = surfaceView.surfaceModel else { return [] } - do { - return try surface.commands().map { c in - CommandOption( - title: c.title, - description: c.description, - symbols: ghosttyConfig.keyboardShortcut(for: c.action)?.keyList, - ) { - onAction(c.action) - } - } - } catch { - return [] - } - } - /// Custom commands from the command-palette-entry configuration. - private var customEntries: [CommandOption] { + private var terminalOptions: [CommandOption] { guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] } return appDelegate.ghostty.config.commandPaletteEntries.map { c in CommandOption( diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index c7198e147..e86952e50 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -134,16 +134,5 @@ extension Ghostty { ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } } - - /// Command options for this surface. - @MainActor - func commands() throws -> [Command] { - var ptr: UnsafeMutablePointer? = nil - var count: Int = 0 - ghostty_surface_commands(surface, &ptr, &count) - guard let ptr else { throw Error.apiFailed } - let buffer = UnsafeBufferPointer(start: ptr, count: count) - return Array(buffer).map { Command(cValue: $0) }.filter { $0.isSupported } - } } } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 1cb9231bc..64900cef1 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1700,23 +1700,6 @@ pub const CAPI = struct { return @intCast(@as(input.Mods.Backing, @bitCast(result))); } - /// Returns the current possible commands for a surface - /// in the output parameter. The memory is owned by libghostty - /// and doesn't need to be freed. - export fn ghostty_surface_commands( - surface: *Surface, - out: *[*]const input.Command.C, - len: *usize, - ) void { - // In the future we may use this information to filter - // some commands. - _ = surface; - - const commands = input.command.defaultsC; - out.* = commands.ptr; - len.* = commands.len; - } - /// Send this for raw keypresses (i.e. the keyDown event on macOS). /// This will handle the keymap translation and send the appropriate /// key and char events. -- 2.51.2 From 03ecc9fdbfb7e36f4d6beea2b8db6e5baab96c83 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 28 Dec 2025 00:16:19 +0000 Subject: [PATCH 078/349] 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 373c97aba..fc94c081c 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/ghostty-themes-release-20251201-150531-bfb3ee1.tgz", - .hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", + .url = "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz", + .hash = "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 9ad3c39da..a8d21dd96 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-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-": { + "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU": { "name": "iterm2_themes", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz", - "hash": "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk=" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz", + "hash": "sha256-cMIEDZFYdilCavhL5pWQ6jRerGUFSZIjLwbxNossSeg=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 9627286dd..eb9b90bea 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-"; + name = "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz"; - hash = "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk="; + url = "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz"; + hash = "sha256-cMIEDZFYdilCavhL5pWQ6jRerGUFSZIjLwbxNossSeg="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 484e1949b..5b1946049 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -5,7 +5,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918 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/ghostty-themes-release-20251201-150531-bfb3ee1.tgz +https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 3b17b64a6..0edc6ab43 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251201-150531-bfb3ee1.tgz", - "dest": "vendor/p/N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-", - "sha256": "e5078f050952b33f56dabe334d2dd00fe70301ec8e6e1479d44d80909dd92149" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz", + "dest": "vendor/p/N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU", + "sha256": "70c2040d91587629426af84be69590ea345eac65054992232f06f1368b2c49e8" }, { "type": "archive", -- 2.51.2 From e63a4ab77464068ba1e6ad920664a7b39c41f5c0 Mon Sep 17 00:00:00 2001 From: -k Date: Sun, 28 Dec 2025 07:21:58 -0500 Subject: [PATCH 079/349] build: fix pkgs for FBSD port runs --- pkg/cimgui/build.zig | 4 ++++ pkg/freetype/build.zig | 4 ++++ pkg/glslang/build.zig | 4 ++++ pkg/spirv-cross/build.zig | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index b94f11943..890873ef9 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -72,6 +72,10 @@ pub fn build(b: *std.Build) !void { }); } + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + if (imgui_) |imgui| { lib.addCSourceFile(.{ .file = b.path("vendor/cimgui.cpp"), .flags = flags.items }); lib.addCSourceFile(.{ .file = imgui.path("imgui.cpp"), .flags = flags.items }); diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index a25dc18da..e0a041be7 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -90,6 +90,10 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu "-fno-sanitize=undefined", }); + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + const dynamic_link_opts = options.dynamic_link_opts; // Zlib diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 746a41497..da9a82e31 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -66,6 +66,10 @@ fn buildGlslang( "-fno-sanitize-trap=undefined", }); + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + if (upstream_) |upstream| { lib.addCSourceFiles(.{ .root = upstream.path(""), diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index 003ec43cf..31af1974e 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -74,6 +74,10 @@ fn buildSpirvCross( "-fno-sanitize-trap=undefined", }); + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + if (b.lazyDependency("spirv_cross", .{})) |upstream| { lib.addIncludePath(upstream.path("")); module.addIncludePath(upstream.path("")); -- 2.51.2 From b7a12effce2b99186650518018785e3fb07fe66f Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 20 Dec 2025 10:59:17 -0800 Subject: [PATCH 080/349] Only use macOS 26.0 workarounds on macOS 26.0 --- .../TitlebarTabsTahoeTerminalWindow.swift | 3 +- macos/Sources/Ghostty/SurfaceScrollView.swift | 31 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 5d910d2e0..b18fff291 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -322,7 +322,8 @@ extension TitlebarTabsTahoeTerminalWindow { } 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. This appears fixed in 26.1 Beta but keep it safe for 26.0. + // know. On macOS 26.1+ the view no longer disappears, but the + // toolbar still logs an ambiguous content size warning. Color.clear.frame(width: 1, height: 1) } } diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/SurfaceScrollView.swift index 157136136..b55f2e231 100644 --- a/macos/Sources/Ghostty/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/SurfaceScrollView.swift @@ -120,18 +120,20 @@ class SurfaceScrollView: NSView { self?.handleScrollerStyleChange() }) - // Listen for frame change events. See the docstring for - // handleFrameChange for why this is necessary. - observers.append(NotificationCenter.default.addObserver( - forName: NSView.frameDidChangeNotification, - object: nil, - // Since this observer is used to immediately override the event - // that produced the notification, we let it run synchronously on - // the posting thread. - queue: nil - ) { [weak self] notification in - self?.handleFrameChange(notification) - }) + // Listen for frame change events on macOS 26.0. See the docstring for + // handleFrameChangeForNSScrollPocket for why this is necessary. + if #unavailable(macOS 26.1) { if #available(macOS 26.0, *) { + observers.append(NotificationCenter.default.addObserver( + forName: NSView.frameDidChangeNotification, + object: nil, + // Since this observer is used to immediately override the event + // that produced the notification, we let it run synchronously on + // the posting thread. + queue: nil + ) { [weak self] notification in + self?.handleFrameChangeForNSScrollPocket(notification) + }) + }} // Listen for derived config changes to update scrollbar settings live surfaceView.$derivedConfig @@ -328,7 +330,10 @@ class SurfaceScrollView: NSView { /// and reset their frame to zero. /// /// See also https://developer.apple.com/forums/thread/798392. - private func handleFrameChange(_ notification: Notification) { + /// + /// This bug is only present in macOS 26.0. + @available(macOS, introduced: 26.0, obsoleted: 26.1) + private func handleFrameChangeForNSScrollPocket(_ notification: Notification) { guard let window = window as? HiddenTitlebarTerminalWindow else { return } guard !window.styleMask.contains(.fullScreen) else { return } guard let view = notification.object as? NSView else { return } -- 2.51.2 From 6d361933780e5f1462bad3c9b56865caceaeb0f9 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Sun, 28 Dec 2025 12:30:36 -0800 Subject: [PATCH 081/349] deps: update z2d to v0.10.0 Release notes at: https://github.com/vancluever/z2d/blob/v0.10.0/CHANGELOG.md Mainly a maintenance update with regards to Ghostty use, with a couple of minor changes required to some type references to match new API semantics. --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- src/font/sprite/canvas.zig | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index fc94c081c..191c1bec9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -21,8 +21,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz", - .hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", + .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + .hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index a8d21dd96..6ab567279 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.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": { + "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ": { "name": "z2d", - "url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz", - "hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + "hash": "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c=" }, "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index eb9b90bea..905ee1ec3 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -307,11 +307,11 @@ in }; } { - name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T"; + name = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ"; path = fetchZigArtifact { name = "z2d"; - url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz"; - hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="; + url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz"; + hash = "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 5b1946049..09cf8007c 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -26,10 +26,10 @@ https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.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-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.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/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 0edc6ab43..03907fba1 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -169,9 +169,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz", - "dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T", - "sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz", + "dest": "vendor/p/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ", + "sha256": "69f21da2efd5ee0937fe55c4d09e48afc4fb2f91a01ef167c8c275ae046797f7" }, { "type": "archive", diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index 19d27eb45..7904f20a5 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -360,7 +360,7 @@ pub const Canvas = struct { pub fn strokePath( self: *Canvas, path: z2d.Path, - opts: z2d.painter.StrokeOpts, + opts: z2d.painter.StrokeOptions, color: Color, ) z2d.painter.StrokeError!void { try z2d.painter.stroke( @@ -380,7 +380,7 @@ pub const Canvas = struct { pub fn innerStrokePath( self: *Canvas, path: z2d.Path, - opts: z2d.painter.StrokeOpts, + opts: z2d.painter.StrokeOptions, color: Color, ) (z2d.painter.StrokeError || z2d.painter.FillError)!void { // On one surface we fill the shape, this will be a mask we @@ -459,7 +459,7 @@ pub const Canvas = struct { pub fn fillPath( self: *Canvas, path: z2d.Path, - opts: z2d.painter.FillOpts, + opts: z2d.painter.FillOptions, color: Color, ) z2d.painter.FillError!void { try z2d.painter.fill( -- 2.51.2 From 8a419e5526b1fc69eef06cfae1457b217ae900fe Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 09:58:09 +0100 Subject: [PATCH 082/349] gtk: pass through keypress when clipboard has no text When paste_from_clipboard is triggered but the clipboard contains no text (e.g., an image), send the raw Ctrl+V keypress to the terminal instead of silently returning. This allows applications to handle their own clipboard reading (e.g., via wl-paste for images on Wayland). --- src/apprt/gtk/class/surface.zig | 34 +++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index a14d53c32..965f518c9 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3814,21 +3814,34 @@ const Clipboard = struct { const self = req.self; defer self.unref(); + const surface = self.private().core_surface orelse return; + var gerr: ?*glib.Error = null; const cstr_ = clipboard.readTextFinish(res, &gerr); + + // If clipboard has no text (error, null, or empty), pass through the + // original keypress so applications can handle their own clipboard + // (e.g., reading images via wl-paste). We send raw Ctrl+V (0x16) + // directly using the text action to bypass bracketed paste encoding. if (gerr) |err| { defer err.free(); - log.warn( - "failed to read clipboard err={s}", - .{err.f_message orelse "(no message)"}, - ); + log.debug("clipboard has no text format: {s}", .{err.f_message orelse "(no message)"}); + passthroughKeypress(surface); return; } - const cstr = cstr_ orelse return; + + const cstr = cstr_ orelse { + passthroughKeypress(surface); + return; + }; defer glib.free(cstr); const str = std.mem.sliceTo(cstr, 0); - const surface = self.private().core_surface orelse return; + if (str.len == 0) { + passthroughKeypress(surface); + return; + } + surface.completeClipboardRequest( req.state, str, @@ -3862,6 +3875,15 @@ const Clipboard = struct { ); } + /// Send raw Ctrl+V (ASCII 22) to the terminal, bypassing paste encoding. + /// This allows applications to handle their own clipboard reading + /// (e.g., for image paste via wl-paste on Wayland). + fn passthroughKeypress(surface: *CoreSurface) void { + _ = surface.performBindingAction(.{ .text = "\\x16" }) catch |err| { + log.warn("error sending passthrough keypress: {}", .{err}); + }; + } + /// The request we send as userdata to the clipboard read. const Request = struct { /// "Self" is reffed so we can't dispose it until the clipboard -- 2.51.2 From 0da650e7dd6d08a376b5d212036d0b9d3231b9e3 Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 15:44:46 +0100 Subject: [PATCH 083/349] gtk: support performable keybinds for clipboard paste Make clipboardRequest return bool to indicate whether the action could be performed. For paste requests, synchronously check if the clipboard contains text formats before starting the async read. This allows 'performable:paste_from_clipboard' keybinds to pass through when the clipboard contains non-text content (e.g., images), enabling terminal applications to handle their own clipboard reading. Changes: - Surface.startClipboardRequest now returns bool - paste_from_clipboard/paste_from_selection actions return the result - GTK apprt checks clipboard formats synchronously before async read - Embedded apprt always returns true (can't check synchronously) - All other call sites discard the return value with _ --- src/Surface.zig | 22 +++++++----- src/apprt/embedded.zig | 6 +++- src/apprt/gtk/Surface.zig | 4 +-- src/apprt/gtk/class/surface.zig | 62 ++++++++++++++++----------------- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c9223d0ad..614f40475 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1026,7 +1026,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { return; } - try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard }); + _ = try self.startClipboardRequest(.standard, .{ .osc_52_read = clipboard }); }, .clipboard_write => |w| switch (w.req) { @@ -4173,7 +4173,7 @@ pub fn mouseButtonCallback( .selection else .standard; - try self.startClipboardRequest(clipboard, .{ .paste = {} }); + _ = try self.startClipboardRequest(clipboard, .{ .paste = {} }); } // Right-click down selects word for context menus. If the apprt @@ -4251,7 +4251,7 @@ pub fn mouseButtonCallback( // request so we need to unlock. self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.lock(); - try self.startClipboardRequest(.standard, .paste); + _ = try self.startClipboardRequest(.standard, .paste); // We don't need to clear selection because we didn't have // one to begin with. @@ -4266,7 +4266,7 @@ pub fn mouseButtonCallback( // request so we need to unlock. self.renderer_state.mutex.unlock(); defer self.renderer_state.mutex.lock(); - try self.startClipboardRequest(.standard, .paste); + _ = try self.startClipboardRequest(.standard, .paste); }, } @@ -5330,12 +5330,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return true; }, - .paste_from_clipboard => try self.startClipboardRequest( + .paste_from_clipboard => return try self.startClipboardRequest( .standard, .{ .paste = {} }, ), - .paste_from_selection => try self.startClipboardRequest( + .paste_from_selection => return try self.startClipboardRequest( .selection, .{ .paste = {} }, ), @@ -6049,11 +6049,15 @@ pub fn completeClipboardRequest( /// This starts a clipboard request, with some basic validation. For example, /// an OSC 52 request is not actually requested if OSC 52 is disabled. +/// +/// Returns true if the request was started, false if it was not (e.g., clipboard +/// doesn't contain text for paste requests). This allows performable keybinds +/// to pass through when the action cannot be performed. fn startClipboardRequest( self: *Surface, loc: apprt.Clipboard, req: apprt.ClipboardRequest, -) !void { +) !bool { switch (req) { .paste => {}, // always allowed .osc_52_read => if (self.config.clipboard_read == .deny) { @@ -6061,14 +6065,14 @@ fn startClipboardRequest( "application attempted to read clipboard, but 'clipboard-read' is set to deny", .{}, ); - return; + return false; }, // No clipboard write code paths travel through this function .osc_52_write => unreachable, } - try self.rt_surface.clipboardRequest(loc, req); + return try self.rt_surface.clipboardRequest(loc, req); } fn completeClipboardPaste( diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 64900cef1..a1b6a6e9b 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -652,7 +652,7 @@ pub const Surface = struct { self: *Surface, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, - ) !void { + ) !bool { // We need to allocate to get a pointer to store our clipboard request // so that it is stable until the read_clipboard callback and call // complete_clipboard_request. This sucks but clipboard requests aren't @@ -667,6 +667,10 @@ pub const Surface = struct { @intCast(@intFromEnum(clipboard_type)), state_ptr, ); + + // Embedded apprt can't synchronously check clipboard content types, + // so we always return true to indicate the request was started. + return true; } fn completeClipboardRequest( diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 009ce018d..918e77146 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -73,8 +73,8 @@ pub fn clipboardRequest( self: *Self, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, -) !void { - try self.surface.clipboardRequest( +) !bool { + return try self.surface.clipboardRequest( clipboard_type, state, ); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 965f518c9..cbd444936 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1666,8 +1666,8 @@ pub const Surface = extern struct { self: *Self, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, - ) !void { - try Clipboard.request( + ) !bool { + return try Clipboard.request( self, clipboard_type, state, @@ -3623,16 +3623,34 @@ const Clipboard = struct { /// Request data from the clipboard (read the clipboard). This /// completes asynchronously and will call the `completeClipboardRequest` /// core surface API when done. + /// + /// Returns true if the request was started, false if the clipboard + /// doesn't contain text (allowing performable keybinds to pass through). pub fn request( self: *Surface, clipboard_type: apprt.Clipboard, state: apprt.ClipboardRequest, - ) Allocator.Error!void { + ) Allocator.Error!bool { // Get our requested clipboard const clipboard = get( self.private().gl_area.as(gtk.Widget), clipboard_type, - ) orelse return; + ) orelse return false; + + // For paste requests, check if clipboard has text format available. + // This is a synchronous check that allows performable keybinds to + // pass through when the clipboard contains non-text content (e.g., images). + if (state == .paste) { + const formats = clipboard.getFormats(); + if (formats.containMimeType("text/plain") == 0 and + formats.containMimeType("UTF8_STRING") == 0 and + formats.containMimeType("TEXT") == 0 and + formats.containMimeType("STRING") == 0) + { + log.debug("clipboard has no text format, not starting paste request", .{}); + return false; + } + } // Allocate our userdata const alloc = Application.default().allocator(); @@ -3652,6 +3670,8 @@ const Clipboard = struct { clipboardReadText, ud, ); + + return true; } /// Paste explicit text directly into the surface, regardless of the @@ -3814,34 +3834,21 @@ const Clipboard = struct { const self = req.self; defer self.unref(); - const surface = self.private().core_surface orelse return; - var gerr: ?*glib.Error = null; const cstr_ = clipboard.readTextFinish(res, &gerr); - - // If clipboard has no text (error, null, or empty), pass through the - // original keypress so applications can handle their own clipboard - // (e.g., reading images via wl-paste). We send raw Ctrl+V (0x16) - // directly using the text action to bypass bracketed paste encoding. if (gerr) |err| { defer err.free(); - log.debug("clipboard has no text format: {s}", .{err.f_message orelse "(no message)"}); - passthroughKeypress(surface); + log.warn( + "failed to read clipboard err={s}", + .{err.f_message orelse "(no message)"}, + ); return; } - - const cstr = cstr_ orelse { - passthroughKeypress(surface); - return; - }; + const cstr = cstr_ orelse return; defer glib.free(cstr); const str = std.mem.sliceTo(cstr, 0); - if (str.len == 0) { - passthroughKeypress(surface); - return; - } - + const surface = self.private().core_surface orelse return; surface.completeClipboardRequest( req.state, str, @@ -3875,15 +3882,6 @@ const Clipboard = struct { ); } - /// Send raw Ctrl+V (ASCII 22) to the terminal, bypassing paste encoding. - /// This allows applications to handle their own clipboard reading - /// (e.g., for image paste via wl-paste on Wayland). - fn passthroughKeypress(surface: *CoreSurface) void { - _ = surface.performBindingAction(.{ .text = "\\x16" }) catch |err| { - log.warn("error sending passthrough keypress: {}", .{err}); - }; - } - /// The request we send as userdata to the clipboard read. const Request = struct { /// "Self" is reffed so we can't dispose it until the clipboard -- 2.51.2 From ab232b30604a49e6ad586294392434295333050f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 13:49:34 -0800 Subject: [PATCH 084/349] macos: move Ghostty surface view into dedicated folder --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++++---- .../Ghostty/{ => Surface View}/InspectorView.swift | 0 .../Ghostty/{ => Surface View}/SurfaceProgressBar.swift | 0 .../Ghostty/{ => Surface View}/SurfaceScrollView.swift | 0 .../Sources/Ghostty/{ => Surface View}/SurfaceView.swift | 0 .../Ghostty/{ => Surface View}/SurfaceView_AppKit.swift | 3 --- .../Ghostty/{ => Surface View}/SurfaceView_UIKit.swift | 0 7 files changed, 4 insertions(+), 7 deletions(-) rename macos/Sources/Ghostty/{ => Surface View}/InspectorView.swift (100%) rename macos/Sources/Ghostty/{ => Surface View}/SurfaceProgressBar.swift (100%) rename macos/Sources/Ghostty/{ => Surface View}/SurfaceScrollView.swift (100%) rename macos/Sources/Ghostty/{ => Surface View}/SurfaceView.swift (100%) rename macos/Sources/Ghostty/{ => Surface View}/SurfaceView_AppKit.swift (99%) rename macos/Sources/Ghostty/{ => Surface View}/SurfaceView_UIKit.swift (100%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index feda1bed0..49f668e12 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -142,10 +142,10 @@ Ghostty/Ghostty.Event.swift, Ghostty/Ghostty.Input.swift, Ghostty/Ghostty.Surface.swift, - Ghostty/InspectorView.swift, "Ghostty/NSEvent+Extension.swift", - Ghostty/SurfaceScrollView.swift, - Ghostty/SurfaceView_AppKit.swift, + "Ghostty/Surface View/InspectorView.swift", + "Ghostty/Surface View/SurfaceScrollView.swift", + "Ghostty/Surface View/SurfaceView_AppKit.swift", Helpers/AppInfo.swift, Helpers/CodableBridge.swift, Helpers/Cursor.swift, @@ -187,7 +187,7 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( App/iOS/iOSApp.swift, - Ghostty/SurfaceView_UIKit.swift, + "Ghostty/Surface View/SurfaceView_UIKit.swift", ); target = A5B30530299BEAAA0047F10C /* Ghostty */; }; diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift similarity index 100% rename from macos/Sources/Ghostty/InspectorView.swift rename to macos/Sources/Ghostty/Surface View/InspectorView.swift diff --git a/macos/Sources/Ghostty/SurfaceProgressBar.swift b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift similarity index 100% rename from macos/Sources/Ghostty/SurfaceProgressBar.swift rename to macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift diff --git a/macos/Sources/Ghostty/SurfaceScrollView.swift b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift similarity index 100% rename from macos/Sources/Ghostty/SurfaceScrollView.swift rename to macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift similarity index 100% rename from macos/Sources/Ghostty/SurfaceView.swift rename to macos/Sources/Ghostty/Surface View/SurfaceView.swift diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift similarity index 99% rename from macos/Sources/Ghostty/SurfaceView_AppKit.swift rename to macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 77e1c43d4..37cc9282e 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1654,7 +1654,6 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double - let backgroundBlur: Ghostty.Config.BackgroundBlur let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1663,7 +1662,6 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 - self.backgroundBlur = .disabled self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1673,7 +1671,6 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity - self.backgroundBlur = config.backgroundBlur self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift similarity index 100% rename from macos/Sources/Ghostty/SurfaceView_UIKit.swift rename to macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift -- 2.51.2 From 88adffd734431a2f610d14bc5344420186981418 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 13:59:49 -0800 Subject: [PATCH 085/349] macOS: add handle to the top of surfaces that can be used to drag UI only --- .../Ghostty/Surface View/SurfaceView.swift | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 2d5039d29..a5e677c54 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -224,6 +224,10 @@ extension Ghostty { .opacity(overlayOpacity) } } + + // Grab handle for dragging the window. We want this to appear at the very + // top Z-index os it isn't faded by the unfocused overlay. + SurfaceGrabHandle() } } @@ -952,6 +956,49 @@ extension Ghostty { } #endif + /// A grab handle overlay at the top of the surface for dragging the window. + /// Only appears when hovering in the top region of the surface. + struct SurfaceGrabHandle: View { + private let handleHeight: CGFloat = 10 + + @State private var isHovering: Bool = false + @State private var isDragging: Bool = false + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(Color.white.opacity(isHovering || isDragging ? 0.15 : 0)) + .frame(height: handleHeight) + .overlay(alignment: .center) { + if isHovering || isDragging { + Capsule() + .fill(Color.white.opacity(0.4)) + .frame(width: 40, height: 4) + } + } + .contentShape(Rectangle()) + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } + } + .gesture( + DragGesture() + .onChanged { _ in + isDragging = true + } + .onEnded { _ in + isDragging = false + } + ) + .backport.pointerStyle(isHovering ? .grabIdle : nil) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool -- 2.51.2 From 57bb636655aa129d7a01d028c565d15dd82ecdc3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Dec 2025 14:23:55 -0800 Subject: [PATCH 086/349] surfaceview as transferable --- .../SurfaceView+Transferable.swift | 20 +++++++++++++++++++ .../Ghostty/Surface View/SurfaceView.swift | 14 ++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift new file mode 100644 index 000000000..af20c9f37 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -0,0 +1,20 @@ +import CoreTransferable +import UniformTypeIdentifiers + +extension Ghostty.SurfaceView: Transferable { + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(contentType: .ghosttySurfaceId) { surface in + withUnsafeBytes(of: surface.id.uuid) { Data($0) } + } importing: { data in + throw TransferError.importNotSupported + } + } + + enum TransferError: Error { + case importNotSupported + } +} + +extension UTType { + static let ghosttySurfaceId = UTType(exportedAs: "com.mitchellh.ghosttySurfaceId") +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index a5e677c54..140266ec5 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -227,7 +227,7 @@ extension Ghostty { // Grab handle for dragging the window. We want this to appear at the very // top Z-index os it isn't faded by the unfocused overlay. - SurfaceGrabHandle() + SurfaceGrabHandle(surfaceView: surfaceView) } } @@ -961,6 +961,8 @@ extension Ghostty { struct SurfaceGrabHandle: View { private let handleHeight: CGFloat = 10 + let surfaceView: SurfaceView + @State private var isHovering: Bool = false @State private var isDragging: Bool = false @@ -982,20 +984,12 @@ extension Ghostty { isHovering = hovering } } - .gesture( - DragGesture() - .onChanged { _ in - isDragging = true - } - .onEnded { _ in - isDragging = false - } - ) .backport.pointerStyle(isHovering ? .grabIdle : nil) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) + .draggable(surfaceView) } } -- 2.51.2 From 50456886237db7be7da3b9ae3f2c9dc122d418e8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 12:51:26 -0800 Subject: [PATCH 087/349] macos: move grab handle to separate file --- .../Surface View/SurfaceGrabHandle.swift | 40 +++++++++++++++++++ .../Ghostty/Surface View/SurfaceView.swift | 37 ----------------- 2 files changed, 40 insertions(+), 37 deletions(-) create mode 100644 macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift new file mode 100644 index 000000000..6e99bc281 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -0,0 +1,40 @@ +import SwiftUI + +extension Ghostty { + /// A grab handle overlay at the top of the surface for dragging the window. + /// Only appears when hovering in the top region of the surface. + struct SurfaceGrabHandle: View { + private let handleHeight: CGFloat = 10 + + let surfaceView: SurfaceView + + @State private var isHovering: Bool = false + @State private var isDragging: Bool = false + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .fill(Color.white.opacity(isHovering || isDragging ? 0.15 : 0)) + .frame(height: handleHeight) + .overlay(alignment: .center) { + if isHovering || isDragging { + Capsule() + .fill(Color.white.opacity(0.4)) + .frame(width: 40, height: 4) + } + } + .contentShape(Rectangle()) + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } + } + .backport.pointerStyle(isHovering ? .grabIdle : nil) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .draggable(surfaceView) + } + } +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 140266ec5..872b89d30 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -956,43 +956,6 @@ extension Ghostty { } #endif - /// A grab handle overlay at the top of the surface for dragging the window. - /// Only appears when hovering in the top region of the surface. - struct SurfaceGrabHandle: View { - private let handleHeight: CGFloat = 10 - - let surfaceView: SurfaceView - - @State private var isHovering: Bool = false - @State private var isDragging: Bool = false - - var body: some View { - VStack(spacing: 0) { - Rectangle() - .fill(Color.white.opacity(isHovering || isDragging ? 0.15 : 0)) - .frame(height: handleHeight) - .overlay(alignment: .center) { - if isHovering || isDragging { - Capsule() - .fill(Color.white.opacity(0.4)) - .frame(width: 40, height: 4) - } - } - .contentShape(Rectangle()) - .onHover { hovering in - withAnimation(.easeInOut(duration: 0.15)) { - isHovering = hovering - } - } - .backport.pointerStyle(isHovering ? .grabIdle : nil) - - Spacer() - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .draggable(surfaceView) - } - } - /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool -- 2.51.2 From 304e2612abf9779ce486313580c10b334b2e81be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 13:01:49 -0800 Subject: [PATCH 088/349] macOS: work on drop destination --- .../Splits/TerminalSplitTreeView.swift | 10 ++++-- .../SurfaceView+Transferable.swift | 33 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 103413c70..44123de6d 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -32,8 +32,14 @@ struct TerminalSplitSubtreeView: View { Ghostty.InspectableSurface( surfaceView: leafView, isSplit: !isRoot) - .accessibilityElement(children: .contain) - .accessibilityLabel("Terminal pane") + .dropDestination(for: Ghostty.SurfaceView.self) { views, point in + Ghostty.logger.warning("BABY WHAT!") + return false + } isTargeted: { targeted in + Ghostty.logger.warning("BABY TARGETED=\(targeted)") + } + .accessibilityElement(children: .contain) + .accessibilityLabel("Terminal pane") case .split(let split): let splitViewDirection: SplitViewDirection = switch (split.direction) { diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index af20c9f37..da4b420d5 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -1,3 +1,4 @@ +import AppKit import CoreTransferable import UniformTypeIdentifiers @@ -6,12 +7,40 @@ extension Ghostty.SurfaceView: Transferable { DataRepresentation(contentType: .ghosttySurfaceId) { surface in withUnsafeBytes(of: surface.id.uuid) { Data($0) } } importing: { data in - throw TransferError.importNotSupported + guard data.count == 16 else { + throw TransferError.invalidData + } + + let uuid = data.withUnsafeBytes { + $0.load(as: UUID.self) + } + + guard let imported = await Self.find(uuid: uuid) else { + throw TransferError.invalidData + } + + return imported } } enum TransferError: Error { - case importNotSupported + case invalidData + } + + @MainActor + static func find(uuid: UUID) -> Self? { + for window in NSApp.windows { + guard let controller = window.windowController as? BaseTerminalController else { + continue + } + for surface in controller.surfaceTree { + if surface.id == uuid { + return surface as? Self + } + } + } + + return nil } } -- 2.51.2 From ddfd4fe7c241acec9ad858c918429321d0135bba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 13:14:01 -0800 Subject: [PATCH 089/349] macos: export our ghostty surface ID type --- macos/Ghostty-Info.plist | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 2bf3b0bae..5960dc0e7 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -100,5 +100,20 @@ SUPublicEDKey wsNcGf5hirwtdXMVnYoxRIX/SqZQLMOsYlD3q3imeok= + UTExportedTypeDeclarations + + + UTTypeIdentifier + com.mitchellh.ghosttySurfaceId + UTTypeDescription + Ghostty Surface Identifier + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + + -- 2.51.2 From 43d87cf9f8f1f6ced10ea78a17dc11db313cdeee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 13:41:09 -0800 Subject: [PATCH 090/349] macos: setup drop UI on our split views --- .../Splits/TerminalSplitTreeView.swift | 145 ++++++++++++++++-- 1 file changed, 134 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 44123de6d..5c291dcba 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -29,17 +29,7 @@ struct TerminalSplitSubtreeView: View { var body: some View { switch (node) { case .leaf(let leafView): - Ghostty.InspectableSurface( - surfaceView: leafView, - isSplit: !isRoot) - .dropDestination(for: Ghostty.SurfaceView.self) { views, point in - Ghostty.logger.warning("BABY WHAT!") - return false - } isTargeted: { targeted in - Ghostty.logger.warning("BABY TARGETED=\(targeted)") - } - .accessibilityElement(children: .contain) - .accessibilityLabel("Terminal pane") + TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot) case .split(let split): let splitViewDirection: SplitViewDirection = switch (split.direction) { @@ -70,3 +60,136 @@ struct TerminalSplitSubtreeView: View { } } } + +struct TerminalSplitLeaf: View { + let surfaceView: Ghostty.SurfaceView + let isSplit: Bool + + @State private var dropZone: DropZone = .none + + var body: some View { + Ghostty.InspectableSurface( + surfaceView: surfaceView, + isSplit: isSplit) + .background { + // We use background for the drop delegate and overlay for the visual indicator + // so that we don't block mouse events from reaching the surface view. The + // background receives drop events while the overlay (with allowsHitTesting + // disabled) only provides visual feedback. + GeometryReader { geometry in + Color.clear + .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( + dropZone: $dropZone, + viewSize: geometry.size + )) + } + } + .overlay { + if dropZone != .none { + GeometryReader { geometry in + dropZoneOverlay(for: dropZone, in: geometry) + } + .allowsHitTesting(false) + } + } + .accessibilityElement(children: .contain) + .accessibilityLabel("Terminal pane") + } + + @ViewBuilder + private func dropZoneOverlay(for zone: DropZone, in geometry: GeometryProxy) -> some View { + let overlayColor = Color.accentColor.opacity(0.3) + + switch zone { + case .none: + EmptyView() + case .top: + VStack(spacing: 0) { + Rectangle() + .fill(overlayColor) + .frame(height: geometry.size.height / 2) + Spacer() + } + case .bottom: + VStack(spacing: 0) { + Spacer() + Rectangle() + .fill(overlayColor) + .frame(height: geometry.size.height / 2) + } + case .left: + HStack(spacing: 0) { + Rectangle() + .fill(overlayColor) + .frame(width: geometry.size.width / 2) + Spacer() + } + case .right: + HStack(spacing: 0) { + Spacer() + Rectangle() + .fill(overlayColor) + .frame(width: geometry.size.width / 2) + } + } + } + + enum DropZone: Equatable { + case none + case top + case bottom + case left + case right + } + + struct SplitDropDelegate: DropDelegate { + @Binding var dropZone: DropZone + let viewSize: CGSize + + func validateDrop(info: DropInfo) -> Bool { + info.hasItemsConforming(to: [.ghosttySurfaceId]) + } + + func dropEntered(info: DropInfo) { + _ = dropUpdated(info: info) + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + dropZone = calculateDropZone(at: info.location) + return DropProposal(operation: .move) + } + + func dropExited(info: DropInfo) { + dropZone = .none + } + + func performDrop(info: DropInfo) -> Bool { + dropZone = .none + return false + } + + /// Determines which drop zone the cursor is in based on proximity to edges. + /// + /// Divides the view into four triangular regions by drawing diagonals from + /// corner to corner. The drop zone is determined by which edge the cursor + /// is closest to, creating natural triangular hit regions for each side. + private func calculateDropZone(at point: CGPoint) -> DropZone { + guard viewSize.width > 0, viewSize.height > 0 else { return .none } + + let relX = point.x / viewSize.width + let relY = point.y / viewSize.height + + let distToLeft = relX + let distToRight = 1 - relX + let distToTop = relY + let distToBottom = 1 - relY + + let minDist = min(distToLeft, distToRight, distToTop, distToBottom) + + if minDist == distToLeft { return .left } + if minDist == distToRight { return .right } + if minDist == distToTop { return .top } + return .bottom + } + } +} -- 2.51.2 From 0a80f77766a2f234233e870eac1d0aa4e317ba1c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 14:22:46 -0800 Subject: [PATCH 091/349] macos: wire up onDrop --- .../Splits/TerminalSplitTreeView.swift | 55 ++++++++++++------- .../Features/Terminal/TerminalView.swift | 6 +- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 5c291dcba..9a09ed743 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -1,15 +1,18 @@ import SwiftUI +import os struct TerminalSplitTreeView: View { let tree: SplitTree let onResize: (SplitTree.Node, Double) -> Void + let onDrop: (Ghostty.SurfaceView, TerminalSplitLeaf.DropZone) -> Void var body: some View { if let node = tree.zoomed ?? tree.root { TerminalSplitSubtreeView( node: node, isRoot: node == tree.root, - onResize: onResize) + onResize: onResize, + onDrop: onDrop) // 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 behaviors. @@ -25,11 +28,12 @@ struct TerminalSplitSubtreeView: View { let node: SplitTree.Node var isRoot: Bool = false let onResize: (SplitTree.Node, Double) -> Void + let onDrop: (Ghostty.SurfaceView, TerminalSplitLeaf.DropZone) -> Void var body: some View { switch (node) { case .leaf(let leafView): - TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot) + TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, onDrop: onDrop) case .split(let split): let splitViewDirection: SplitViewDirection = switch (split.direction) { @@ -47,10 +51,10 @@ struct TerminalSplitSubtreeView: View { dividerColor: ghostty.config.splitDividerColor, resizeIncrements: .init(width: 1, height: 1), left: { - TerminalSplitSubtreeView(node: split.left, onResize: onResize) + TerminalSplitSubtreeView(node: split.left, onResize: onResize, onDrop: onDrop) }, right: { - TerminalSplitSubtreeView(node: split.right, onResize: onResize) + TerminalSplitSubtreeView(node: split.right, onResize: onResize, onDrop: onDrop) }, onEqualize: { guard let surface = node.leftmostLeaf().surface else { return } @@ -64,8 +68,9 @@ struct TerminalSplitSubtreeView: View { struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool + let onDrop: (Ghostty.SurfaceView, DropZone) -> Void - @State private var dropZone: DropZone = .none + @State private var dropState: DropState = .idle var body: some View { Ghostty.InspectableSurface( @@ -79,15 +84,16 @@ struct TerminalSplitLeaf: View { GeometryReader { geometry in Color.clear .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( - dropZone: $dropZone, - viewSize: geometry.size + dropState: $dropState, + viewSize: geometry.size, + onDrop: { zone in onDrop(surfaceView, zone) } )) } } .overlay { - if dropZone != .none { + if case .dropping(let zone) = dropState { GeometryReader { geometry in - dropZoneOverlay(for: dropZone, in: geometry) + dropZoneOverlay(for: zone, in: geometry) } .allowsHitTesting(false) } @@ -101,8 +107,6 @@ struct TerminalSplitLeaf: View { let overlayColor = Color.accentColor.opacity(0.3) switch zone { - case .none: - EmptyView() case .top: VStack(spacing: 0) { Rectangle() @@ -134,38 +138,49 @@ struct TerminalSplitLeaf: View { } } - enum DropZone: Equatable { - case none + enum DropZone: String, Equatable { case top case bottom case left case right } + enum DropState: Equatable { + case idle + case dropping(DropZone) + } + struct SplitDropDelegate: DropDelegate { - @Binding var dropZone: DropZone + @Binding var dropState: DropState let viewSize: CGSize + let onDrop: (DropZone) -> Void func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) } func dropEntered(info: DropInfo) { - _ = dropUpdated(info: info) + dropState = .dropping(calculateDropZone(at: info.location)) } func dropUpdated(info: DropInfo) -> DropProposal? { - dropZone = calculateDropZone(at: info.location) + // For some reason dropUpdated is sent after performDrop is called + // and we don't want to reset our drop zone to show it so we have + // to guard on the state here. + guard case .dropping = dropState else { return DropProposal(operation: .forbidden) } + dropState = .dropping(calculateDropZone(at: info.location)) return DropProposal(operation: .move) } func dropExited(info: DropInfo) { - dropZone = .none + dropState = .idle } func performDrop(info: DropInfo) -> Bool { - dropZone = .none - return false + let zone = calculateDropZone(at: info.location) + dropState = .idle + onDrop(zone) + return true } /// Determines which drop zone the cursor is in based on proximity to edges. @@ -174,8 +189,6 @@ struct TerminalSplitLeaf: View { /// corner to corner. The drop zone is determined by which edge the cursor /// is closest to, creating natural triangular hit regions for each side. private func calculateDropZone(at point: CGPoint) -> DropZone { - guard viewSize.width > 0, viewSize.height > 0 else { return .none } - let relX = point.x / viewSize.width let relY = point.y / viewSize.height diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index fd53a617b..8e70c2a55 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -1,5 +1,6 @@ import SwiftUI import GhosttyKit +import os /// This delegate is notified of actions and property changes regarding the terminal view. This /// delegate is optional and can be used by a TerminalView caller to react to changes such as @@ -81,7 +82,10 @@ struct TerminalView: View { TerminalSplitTreeView( tree: viewModel.surfaceTree, - onResize: { delegate?.splitDidResize(node: $0, to: $1) }) + onResize: { delegate?.splitDidResize(node: $0, to: $1) }, + onDrop: { surface, zone in + Ghostty.logger.info("Drop on surface \(surface) in zone \(zone.rawValue)") + }) .environmentObject(ghostty) .focused($focused) .onAppear { self.focused = true } -- 2.51.2 From 5d8c9357c096b7e907437acdb8dd1efe1eab2b54 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 14:26:55 -0800 Subject: [PATCH 092/349] macos: move around some functions --- .../Splits/TerminalSplitTreeView.swift | 145 +++++++++--------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 9a09ed743..bcb425908 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -4,7 +4,7 @@ import os struct TerminalSplitTreeView: View { let tree: SplitTree let onResize: (SplitTree.Node, Double) -> Void - let onDrop: (Ghostty.SurfaceView, TerminalSplitLeaf.DropZone) -> Void + let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void var body: some View { if let node = tree.zoomed ?? tree.root { @@ -28,7 +28,7 @@ struct TerminalSplitSubtreeView: View { let node: SplitTree.Node var isRoot: Bool = false let onResize: (SplitTree.Node, Double) -> Void - let onDrop: (Ghostty.SurfaceView, TerminalSplitLeaf.DropZone) -> Void + let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void var body: some View { switch (node) { @@ -68,7 +68,7 @@ struct TerminalSplitSubtreeView: View { struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool - let onDrop: (Ghostty.SurfaceView, DropZone) -> Void + let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void @State private var dropState: DropState = .idle @@ -93,7 +93,7 @@ struct TerminalSplitLeaf: View { .overlay { if case .dropping(let zone) = dropState { GeometryReader { geometry in - dropZoneOverlay(for: zone, in: geometry) + zone.overlay(in: geometry) } .allowsHitTesting(false) } @@ -102,65 +102,22 @@ struct TerminalSplitLeaf: View { .accessibilityLabel("Terminal pane") } - @ViewBuilder - private func dropZoneOverlay(for zone: DropZone, in geometry: GeometryProxy) -> some View { - let overlayColor = Color.accentColor.opacity(0.3) - - switch zone { - case .top: - VStack(spacing: 0) { - Rectangle() - .fill(overlayColor) - .frame(height: geometry.size.height / 2) - Spacer() - } - case .bottom: - VStack(spacing: 0) { - Spacer() - Rectangle() - .fill(overlayColor) - .frame(height: geometry.size.height / 2) - } - case .left: - HStack(spacing: 0) { - Rectangle() - .fill(overlayColor) - .frame(width: geometry.size.width / 2) - Spacer() - } - case .right: - HStack(spacing: 0) { - Spacer() - Rectangle() - .fill(overlayColor) - .frame(width: geometry.size.width / 2) - } - } - } - - enum DropZone: String, Equatable { - case top - case bottom - case left - case right - } - - enum DropState: Equatable { + private enum DropState: Equatable { case idle - case dropping(DropZone) + case dropping(TerminalSplitDropZone) } - struct SplitDropDelegate: DropDelegate { + private struct SplitDropDelegate: DropDelegate { @Binding var dropState: DropState let viewSize: CGSize - let onDrop: (DropZone) -> Void + let onDrop: (TerminalSplitDropZone) -> Void func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) } func dropEntered(info: DropInfo) { - dropState = .dropping(calculateDropZone(at: info.location)) + dropState = .dropping(.calculate(at: info.location, in: viewSize)) } func dropUpdated(info: DropInfo) -> DropProposal? { @@ -168,7 +125,7 @@ struct TerminalSplitLeaf: View { // and we don't want to reset our drop zone to show it so we have // to guard on the state here. guard case .dropping = dropState else { return DropProposal(operation: .forbidden) } - dropState = .dropping(calculateDropZone(at: info.location)) + dropState = .dropping(.calculate(at: info.location, in: viewSize)) return DropProposal(operation: .move) } @@ -177,32 +134,74 @@ struct TerminalSplitLeaf: View { } func performDrop(info: DropInfo) -> Bool { - let zone = calculateDropZone(at: info.location) dropState = .idle - onDrop(zone) + onDrop(.calculate(at: info.location, in: viewSize)) return true } - - /// Determines which drop zone the cursor is in based on proximity to edges. - /// - /// Divides the view into four triangular regions by drawing diagonals from - /// corner to corner. The drop zone is determined by which edge the cursor - /// is closest to, creating natural triangular hit regions for each side. - private func calculateDropZone(at point: CGPoint) -> DropZone { - let relX = point.x / viewSize.width - let relY = point.y / viewSize.height + } +} + +enum TerminalSplitDropZone: String, Equatable { + case top + case bottom + case left + case right + + /// Determines which drop zone the cursor is in based on proximity to edges. + /// + /// Divides the view into four triangular regions by drawing diagonals from + /// corner to corner. The drop zone is determined by which edge the cursor + /// is closest to, creating natural triangular hit regions for each side. + static func calculate(at point: CGPoint, in size: CGSize) -> TerminalSplitDropZone { + let relX = point.x / size.width + let relY = point.y / size.height + + let distToLeft = relX + let distToRight = 1 - relX + let distToTop = relY + let distToBottom = 1 - relY - let distToLeft = relX - let distToRight = 1 - relX - let distToTop = relY - let distToBottom = 1 - relY + let minDist = min(distToLeft, distToRight, distToTop, distToBottom) - let minDist = min(distToLeft, distToRight, distToTop, distToBottom) + if minDist == distToLeft { return .left } + if minDist == distToRight { return .right } + if minDist == distToTop { return .top } + return .bottom + } - if minDist == distToLeft { return .left } - if minDist == distToRight { return .right } - if minDist == distToTop { return .top } - return .bottom + @ViewBuilder + func overlay(in geometry: GeometryProxy) -> some View { + let overlayColor = Color.accentColor.opacity(0.3) + + switch self { + case .top: + VStack(spacing: 0) { + Rectangle() + .fill(overlayColor) + .frame(height: geometry.size.height / 2) + Spacer() + } + case .bottom: + VStack(spacing: 0) { + Spacer() + Rectangle() + .fill(overlayColor) + .frame(height: geometry.size.height / 2) + } + case .left: + HStack(spacing: 0) { + Rectangle() + .fill(overlayColor) + .frame(width: geometry.size.width / 2) + Spacer() + } + case .right: + HStack(spacing: 0) { + Spacer() + Rectangle() + .fill(overlayColor) + .frame(width: geometry.size.width / 2) + } } } } -- 2.51.2 From 485b8613429fb88ba6181ab326e5b99f1afc7a43 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 14:37:25 -0800 Subject: [PATCH 093/349] macos: drag preview is an image of the surface --- .../Surface View/SurfaceGrabHandle.swift | 31 ++++++++++++++++++- .../Surface View/SurfaceView+Image.swift | 28 +++++++++++++++++ .../Surface View/SurfaceView_AppKit.swift | 1 + 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index 6e99bc281..d35045315 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -5,6 +5,7 @@ extension Ghostty { /// Only appears when hovering in the top region of the surface. struct SurfaceGrabHandle: View { private let handleHeight: CGFloat = 10 + private let previewScale: CGFloat = 0.2 let surfaceView: SurfaceView @@ -34,7 +35,35 @@ extension Ghostty { Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) - .draggable(surfaceView) + .draggable(surfaceView) { + SurfaceDragPreview(surfaceView: surfaceView, scale: previewScale) + } + } + } + + /// A miniature preview of the surface view for drag operations that updates periodically. + private struct SurfaceDragPreview: View { + let surfaceView: SurfaceView + let scale: CGFloat + + var body: some View { + // We need to use a TimelineView to ensure that this doesn't + // cache forever. This will NOT let the view live update while + // being dragged; macOS doesn't seem to allow that. But it will + // make sure on new drags the screenshot is updated. + TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { _ in + if let snapshot = surfaceView.asImage { + Image(nsImage: snapshot) + .resizable() + .aspectRatio(contentMode: .fit) + .frame( + width: snapshot.size.width * scale, + height: snapshot.size.height * scale + ) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .shadow(radius: 10) + } + } } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift new file mode 100644 index 000000000..11b7b4694 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Image.swift @@ -0,0 +1,28 @@ +#if canImport(AppKit) +import AppKit +#elseif canImport(UIKit) +import UIKit +#endif + +extension Ghostty.SurfaceView { + #if canImport(AppKit) + /// A snapshot image of the current surface view. + var asImage: NSImage? { + guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { + return nil + } + cacheDisplay(in: bounds, to: bitmapRep) + let image = NSImage(size: bounds.size) + image.addRepresentation(bitmapRep) + return image + } + #elseif canImport(UIKit) + /// A snapshot image of the current surface view. + var asImage: UIImage? { + let renderer = UIGraphicsImageRenderer(bounds: bounds) + return renderer.image { _ in + drawHierarchy(in: bounds, afterScreenUpdates: true) + } + } + #endif +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 37cc9282e..37d868a9f 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -2213,6 +2213,7 @@ extension Ghostty.SurfaceView { return NSAttributedString(string: plainString, attributes: attributes) } + } /// Caches a value for some period of time, evicting it automatically when that time expires. -- 2.51.2 From d92fe44d0d32d2bb33283d9e1118652f835440b8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 14:42:27 -0800 Subject: [PATCH 094/349] macos: fix various iOS build errors --- .../Ghostty/Surface View/SurfaceGrabHandle.swift | 12 ++++++++++++ .../Surface View/SurfaceView+Transferable.swift | 4 ++++ .../Ghostty/Surface View/SurfaceView_UIKit.swift | 6 ++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index d35045315..8f4347644 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -53,6 +53,7 @@ extension Ghostty { // make sure on new drags the screenshot is updated. TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { _ in if let snapshot = surfaceView.asImage { + #if canImport(AppKit) Image(nsImage: snapshot) .resizable() .aspectRatio(contentMode: .fit) @@ -62,6 +63,17 @@ extension Ghostty { ) .clipShape(RoundedRectangle(cornerRadius: 8)) .shadow(radius: 10) + #elseif canImport(UIKit) + Image(uiImage: snapshot) + .resizable() + .aspectRatio(contentMode: .fit) + .frame( + width: snapshot.size.width * scale, + height: snapshot.size.height * scale + ) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .shadow(radius: 10) + #endif } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index da4b420d5..da3050eae 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -1,4 +1,6 @@ +#if canImport(AppKit) import AppKit +#endif import CoreTransferable import UniformTypeIdentifiers @@ -29,6 +31,7 @@ extension Ghostty.SurfaceView: Transferable { @MainActor static func find(uuid: UUID) -> Self? { + #if canImport(AppKit) for window in NSApp.windows { guard let controller = window.windowController as? BaseTerminalController else { continue @@ -39,6 +42,7 @@ extension Ghostty.SurfaceView: Transferable { } } } + #endif return nil } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index eb8a60fd9..f9baf56c9 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -4,8 +4,10 @@ import GhosttyKit extension Ghostty { /// The UIView implementation for a terminal surface. class SurfaceView: UIView, ObservableObject { + typealias ID = UUID + /// Unique ID per surface - let uuid: UUID + let id: UUID // The current title of the surface as defined by the pty. This can be // changed with escape codes. This is public because the callbacks go @@ -63,7 +65,7 @@ extension Ghostty { private(set) var surface: ghostty_surface_t? init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { - self.uuid = uuid ?? .init() + self.id = uuid ?? .init() // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer -- 2.51.2 From 8f8b5846c66fb889877dfc21e4c89dea6afafbfc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 14:47:09 -0800 Subject: [PATCH 095/349] macos: hook up onDrop to move splits --- .../Splits/TerminalSplitTreeView.swift | 33 +++++++++++++++---- .../Terminal/BaseTerminalController.swift | 32 ++++++++++++++++++ .../Features/Terminal/TerminalView.swift | 7 ++-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index bcb425908..1f7969c03 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -4,7 +4,7 @@ import os struct TerminalSplitTreeView: View { let tree: SplitTree let onResize: (SplitTree.Node, Double) -> Void - let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void var body: some View { if let node = tree.zoomed ?? tree.root { @@ -28,7 +28,7 @@ struct TerminalSplitSubtreeView: View { let node: SplitTree.Node var isRoot: Bool = false let onResize: (SplitTree.Node, Double) -> Void - let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void var body: some View { switch (node) { @@ -68,7 +68,7 @@ struct TerminalSplitSubtreeView: View { struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool - let onDrop: (Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void @State private var dropState: DropState = .idle @@ -86,7 +86,8 @@ struct TerminalSplitLeaf: View { .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( dropState: $dropState, viewSize: geometry.size, - onDrop: { zone in onDrop(surfaceView, zone) } + destinationSurface: surfaceView, + onDrop: onDrop )) } } @@ -110,7 +111,8 @@ struct TerminalSplitLeaf: View { private struct SplitDropDelegate: DropDelegate { @Binding var dropState: DropState let viewSize: CGSize - let onDrop: (TerminalSplitDropZone) -> Void + let destinationSurface: Ghostty.SurfaceView + let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) @@ -134,8 +136,27 @@ struct TerminalSplitLeaf: View { } func performDrop(info: DropInfo) -> Bool { + let zone = TerminalSplitDropZone.calculate(at: info.location, in: viewSize) dropState = .idle - onDrop(.calculate(at: info.location, in: viewSize)) + + // Load the dropped surface asynchronously using Transferable + let providers = info.itemProviders(for: [.ghosttySurfaceId]) + guard let provider = providers.first else { return false } + _ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in + switch result { + case .success(let sourceSurface): + DispatchQueue.main.async { + // Don't allow dropping on self + guard let destinationSurface else { return } + guard sourceSurface !== destinationSurface else { return } + onDrop(sourceSurface, destinationSurface, zone) + } + + case .failure: + break + } + } + return true } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index d79c89d2d..7f023a8e1 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -827,6 +827,38 @@ class BaseTerminalController: NSWindowController, } } + func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) { + // Find the source node in the tree + guard let sourceNode = surfaceTree.root?.node(view: source) else { + Ghostty.logger.warning("source surface not found in tree during drop") + return + } + + // Map drop zone to split direction + let direction: SplitTree.NewDirection = switch zone { + case .top: .up + case .bottom: .down + case .left: .left + case .right: .right + } + + // Remove source from its current position first + let treeWithoutSource = surfaceTree.remove(sourceNode) + + // Insert source at destination in the appropriate direction + do { + let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface, + undoAction: "Move Split") + } catch { + Ghostty.logger.warning("failed to insert surface during drop: \(error)") + return + } + } + func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { guard let surface = surfaceView.surface else { return } let len = action.utf8CString.count diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 8e70c2a55..6fc0c1d4b 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -20,6 +20,9 @@ protocol TerminalViewDelegate: AnyObject { /// A split is resizing to a given value. func splitDidResize(node: SplitTree.Node, to newRatio: Double) + + /// A surface was dropped onto another surface to create a split. + func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) } /// The view model is a required implementation for TerminalView callers. This contains @@ -83,8 +86,8 @@ struct TerminalView: View { TerminalSplitTreeView( tree: viewModel.surfaceTree, onResize: { delegate?.splitDidResize(node: $0, to: $1) }, - onDrop: { surface, zone in - Ghostty.logger.info("Drop on surface \(surface) in zone \(zone.rawValue)") + onDrop: { source, destination, zone in + delegate?.splitDidDrop(source: source, destination: destination, zone: zone) }) .environmentObject(ghostty) .focused($focused) -- 2.51.2 From 5916755388cac95a56406ef69027cc8d639de6a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Dec 2025 14:59:18 -0800 Subject: [PATCH 096/349] macos: drop splits across windows --- .../Terminal/BaseTerminalController.swift | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 7f023a8e1..e5a80fdea 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -828,12 +828,6 @@ class BaseTerminalController: NSWindowController, } func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) { - // Find the source node in the tree - guard let sourceNode = surfaceTree.root?.node(view: source) else { - Ghostty.logger.warning("source surface not found in tree during drop") - return - } - // Map drop zone to split direction let direction: SplitTree.NewDirection = switch zone { case .top: .up @@ -842,20 +836,63 @@ class BaseTerminalController: NSWindowController, case .right: .right } - // Remove source from its current position first - let treeWithoutSource = surfaceTree.remove(sourceNode) + // Check if source is in our tree + if let sourceNode = surfaceTree.root?.node(view: source) { + // Source is in our tree - same window move + let treeWithoutSource = surfaceTree.remove(sourceNode) + + do { + let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface, + undoAction: "Move Split") + } catch { + Ghostty.logger.warning("failed to insert surface during drop: \(error)") + } + + return + } - // Insert source at destination in the appropriate direction + // Source is not in our tree - search other windows + var sourceController: BaseTerminalController? + var sourceNode: SplitTree.Node? + for window in NSApp.windows { + guard let controller = window.windowController as? BaseTerminalController else { continue } + guard controller !== self else { continue } + if let node = controller.surfaceTree.root?.node(view: source) { + sourceController = controller + sourceNode = node + break + } + } + + guard let sourceController, let sourceNode else { + Ghostty.logger.warning("source surface not found in any window during drop") + return + } + + // TODO: Undo for cross window move. + + // Remove from source controller's tree + let sourceTreeWithoutNode = sourceController.surfaceTree.remove(sourceNode) + sourceController.replaceSurfaceTree( + sourceTreeWithoutNode, + moveFocusTo: nil, + moveFocusFrom: nil, + undoAction: nil) + + // Insert into our tree do { - let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) + let newTree = try surfaceTree.insert(view: source, at: destination, direction: direction) replaceSurfaceTree( newTree, moveFocusTo: source, moveFocusFrom: focusedSurface, undoAction: "Move Split") } catch { - Ghostty.logger.warning("failed to insert surface during drop: \(error)") - return + Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") } } -- 2.51.2 From 60bc5a9ae772053177f5b825dbc6d69bbcf5c1a3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 06:12:19 -0800 Subject: [PATCH 097/349] macos: clean up some SwiftUI nesting --- .../Splits/TerminalSplitTreeView.swift | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 1f7969c03..60acf6ab6 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -73,34 +73,25 @@ struct TerminalSplitLeaf: View { @State private var dropState: DropState = .idle var body: some View { - Ghostty.InspectableSurface( - surfaceView: surfaceView, - isSplit: isSplit) - .background { - // We use background for the drop delegate and overlay for the visual indicator - // so that we don't block mouse events from reaching the surface view. The - // background receives drop events while the overlay (with allowsHitTesting - // disabled) only provides visual feedback. - GeometryReader { geometry in - Color.clear - .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( - dropState: $dropState, - viewSize: geometry.size, - destinationSurface: surfaceView, - onDrop: onDrop - )) - } - } - .overlay { - if case .dropping(let zone) = dropState { - GeometryReader { geometry in + GeometryReader { geometry in + Ghostty.InspectableSurface( + surfaceView: surfaceView, + isSplit: isSplit) + .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( + dropState: $dropState, + viewSize: geometry.size, + destinationSurface: surfaceView, + onDrop: onDrop + )) + .overlay { + if case .dropping(let zone) = dropState { zone.overlay(in: geometry) + .allowsHitTesting(false) } - .allowsHitTesting(false) } + .accessibilityElement(children: .contain) + .accessibilityLabel("Terminal pane") } - .accessibilityElement(children: .contain) - .accessibilityLabel("Terminal pane") } private enum DropState: Equatable { -- 2.51.2 From 9724541a335d0f9cdbf223cd1c538afc1991271d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 07:07:44 -0800 Subject: [PATCH 098/349] macos: unify split callbacks into a single tagged enum --- .../Splits/TerminalSplitTreeView.swift | 48 +++++++++++++------ .../Terminal/BaseTerminalController.swift | 18 +++++-- .../Features/Terminal/TerminalView.swift | 14 ++---- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 60acf6ab6..12a9b990b 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -1,18 +1,37 @@ import SwiftUI import os +enum TerminalSplitOperation { + case resize(Resize) + case drop(Drop) + + struct Resize { + let node: SplitTree.Node + let ratio: Double + } + + struct Drop { + /// The surface being dragged. + let payload: Ghostty.SurfaceView + + /// The surface it was dragged onto + let destination: Ghostty.SurfaceView + + /// The zone it was dropped to determine how to split the destination. + let zone: TerminalSplitDropZone + } +} + struct TerminalSplitTreeView: View { let tree: SplitTree - let onResize: (SplitTree.Node, Double) -> Void - let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let action: (TerminalSplitOperation) -> Void var body: some View { if let node = tree.zoomed ?? tree.root { TerminalSplitSubtreeView( node: node, isRoot: node == tree.root, - onResize: onResize, - onDrop: onDrop) + action: action) // 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 behaviors. @@ -27,13 +46,12 @@ struct TerminalSplitSubtreeView: View { let node: SplitTree.Node var isRoot: Bool = false - let onResize: (SplitTree.Node, Double) -> Void - let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let action: (TerminalSplitOperation) -> Void var body: some View { switch (node) { case .leaf(let leafView): - TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, onDrop: onDrop) + TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, action: action) case .split(let split): let splitViewDirection: SplitViewDirection = switch (split.direction) { @@ -46,15 +64,15 @@ struct TerminalSplitSubtreeView: View { .init(get: { CGFloat(split.ratio) }, set: { - onResize(node, $0) + action(.resize(.init(node: node, ratio: $0))) }), dividerColor: ghostty.config.splitDividerColor, resizeIncrements: .init(width: 1, height: 1), left: { - TerminalSplitSubtreeView(node: split.left, onResize: onResize, onDrop: onDrop) + TerminalSplitSubtreeView(node: split.left, action: action) }, right: { - TerminalSplitSubtreeView(node: split.right, onResize: onResize, onDrop: onDrop) + TerminalSplitSubtreeView(node: split.right, action: action) }, onEqualize: { guard let surface = node.leftmostLeaf().surface else { return } @@ -68,7 +86,7 @@ struct TerminalSplitSubtreeView: View { struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool - let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let action: (TerminalSplitOperation) -> Void @State private var dropState: DropState = .idle @@ -81,7 +99,7 @@ struct TerminalSplitLeaf: View { dropState: $dropState, viewSize: geometry.size, destinationSurface: surfaceView, - onDrop: onDrop + action: action )) .overlay { if case .dropping(let zone) = dropState { @@ -103,7 +121,7 @@ struct TerminalSplitLeaf: View { @Binding var dropState: DropState let viewSize: CGSize let destinationSurface: Ghostty.SurfaceView - let onDrop: (_ source: Ghostty.SurfaceView, _ destination: Ghostty.SurfaceView, TerminalSplitDropZone) -> Void + let action: (TerminalSplitOperation) -> Void func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) @@ -133,6 +151,8 @@ struct TerminalSplitLeaf: View { // Load the dropped surface asynchronously using Transferable let providers = info.itemProviders(for: [.ghosttySurfaceId]) guard let provider = providers.first else { return false } + + // Capture action before the async closure _ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in switch result { case .success(let sourceSurface): @@ -140,7 +160,7 @@ struct TerminalSplitLeaf: View { // Don't allow dropping on self guard let destinationSurface else { return } guard sourceSurface !== destinationSurface else { return } - onDrop(sourceSurface, destinationSurface, zone) + action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone))) } case .failure: diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e5a80fdea..20e0d6b4f 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -817,17 +817,25 @@ class BaseTerminalController: NSWindowController, self.window?.contentResizeIncrements = to } - func splitDidResize(node: SplitTree.Node, to newRatio: Double) { + func performSplitAction(_ action: TerminalSplitOperation) { + switch action { + case .resize(let resize): + splitDidResize(node: resize.node, to: resize.ratio) + case .drop(let drop): + splitDidDrop(source: drop.payload, destination: drop.destination, zone: drop.zone) + } + } + + private func splitDidResize(node: SplitTree.Node, to newRatio: Double) { let resizedNode = node.resize(to: newRatio) do { surfaceTree = try surfaceTree.replace(node: node, with: resizedNode) } catch { Ghostty.logger.warning("failed to replace node during split resize: \(error)") - return } } - func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) { + private func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) { // Map drop zone to split direction let direction: SplitTree.NewDirection = switch zone { case .top: .up @@ -851,7 +859,7 @@ class BaseTerminalController: NSWindowController, } catch { Ghostty.logger.warning("failed to insert surface during drop: \(error)") } - + return } @@ -872,7 +880,7 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("source surface not found in any window during drop") return } - + // TODO: Undo for cross window move. // Remove from source controller's tree diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 6fc0c1d4b..e117e0647 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -17,12 +17,9 @@ protocol TerminalViewDelegate: AnyObject { /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) - - /// A split is resizing to a given value. - func splitDidResize(node: SplitTree.Node, to newRatio: Double) - - /// A surface was dropped onto another surface to create a split. - func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) + + /// A split tree operation + func performSplitAction(_ action: TerminalSplitOperation) } /// The view model is a required implementation for TerminalView callers. This contains @@ -85,10 +82,7 @@ struct TerminalView: View { TerminalSplitTreeView( tree: viewModel.surfaceTree, - onResize: { delegate?.splitDidResize(node: $0, to: $1) }, - onDrop: { source, destination, zone in - delegate?.splitDidDrop(source: source, destination: destination, zone: zone) - }) + action: { delegate?.performSplitAction($0) }) .environmentObject(ghostty) .focused($focused) .onAppear { self.focused = true } -- 2.51.2 From 1dd8e3ef4a15a78b5158c07f9f70233ef26bcf32 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 07:23:14 -0800 Subject: [PATCH 099/349] macos: add GhosttyDelegate for global operations --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../App/macOS/AppDelegate+Ghostty.swift | 18 +++++++++++++++ macos/Sources/Ghostty/GhosttyDelegate.swift | 8 +++++++ .../SurfaceView+Transferable.swift | 22 +++++++++---------- 4 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 macos/Sources/App/macOS/AppDelegate+Ghostty.swift create mode 100644 macos/Sources/Ghostty/GhosttyDelegate.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 49f668e12..a2516fe10 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( App/macOS/AppDelegate.swift, + "App/macOS/AppDelegate+Ghostty.swift", App/macOS/main.swift, App/macOS/MainMenu.xib, Features/About/About.xib, diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift new file mode 100644 index 000000000..7cc74ba7d --- /dev/null +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -0,0 +1,18 @@ +import AppKit + +extension AppDelegate: Ghostty.Delegate { + func ghosttySurface(id: UUID) -> Ghostty.SurfaceView? { + for window in NSApp.windows { + guard let controller = window.windowController as? BaseTerminalController else { + continue + } + for surface in controller.surfaceTree { + if surface.id == id { + return surface + } + } + } + + return nil + } +} diff --git a/macos/Sources/Ghostty/GhosttyDelegate.swift b/macos/Sources/Ghostty/GhosttyDelegate.swift new file mode 100644 index 000000000..0ce3dced4 --- /dev/null +++ b/macos/Sources/Ghostty/GhosttyDelegate.swift @@ -0,0 +1,8 @@ +extension Ghostty { + /// This is a delegate that should be applied to your global app delegate for GhosttyKit + /// to perform app-global operations. + protocol Delegate { + /// Look up a surface within the application by ID. + func ghosttySurface(id: UUID) -> SurfaceView? + } +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index da3050eae..d5d47f601 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -4,6 +4,7 @@ import AppKit import CoreTransferable import UniformTypeIdentifiers +/// Conformance to `Transferable` enables drag-and-drop. extension Ghostty.SurfaceView: Transferable { static var transferRepresentation: some TransferRepresentation { DataRepresentation(contentType: .ghosttySurfaceId) { surface in @@ -32,22 +33,19 @@ extension Ghostty.SurfaceView: Transferable { @MainActor static func find(uuid: UUID) -> Self? { #if canImport(AppKit) - for window in NSApp.windows { - guard let controller = window.windowController as? BaseTerminalController else { - continue - } - for surface in controller.surfaceTree { - if surface.id == uuid { - return surface as? Self - } - } - } - #endif - + guard let del = NSApp.delegate as? Ghostty.Delegate else { return nil } + return del.ghosttySurface(id: uuid) as? Self + #elseif canImport(UIKit) + // We should be able to use UIApplication here. + return nil + #else return nil + #endif } } extension UTType { + /// A format that encodes the bare UUID only for the surface. This can be used if you have + /// a way to look up a surface by ID. static let ghosttySurfaceId = UTType(exportedAs: "com.mitchellh.ghosttySurfaceId") } -- 2.51.2 From 524575787515d2720893c16a7de0514cf6bb24eb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 13:02:45 -0800 Subject: [PATCH 100/349] macos: all sorts of cleanups --- .../App/macOS/AppDelegate+Ghostty.swift | 5 + .../Splits/TerminalSplitTreeView.swift | 9 +- .../Terminal/BaseTerminalController.swift | 111 ++++++++++-------- .../Terminal/TerminalController.swift | 2 +- 4 files changed, 77 insertions(+), 50 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift index 7cc74ba7d..4d798a1a5 100644 --- a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -1,11 +1,16 @@ import AppKit +// MARK: Ghostty Delegate + +/// This implements the Ghostty app delegate protocol which is used by the Ghostty +/// APIs for app-global information. extension AppDelegate: Ghostty.Delegate { func ghosttySurface(id: UUID) -> Ghostty.SurfaceView? { for window in NSApp.windows { guard let controller = window.windowController as? BaseTerminalController else { continue } + for surface in controller.surfaceTree { if surface.id == id { return surface diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 12a9b990b..cffab8c6c 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -1,6 +1,9 @@ import SwiftUI -import os +/// A single operation within the split tree. +/// +/// Rather than binding the split tree (which is immutable), any mutable operations are +/// exposed via this enum to the embedder to handle. enum TerminalSplitOperation { case resize(Resize) case drop(Drop) @@ -41,7 +44,7 @@ struct TerminalSplitTreeView: View { } } -struct TerminalSplitSubtreeView: View { +fileprivate struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App let node: SplitTree.Node @@ -83,7 +86,7 @@ struct TerminalSplitSubtreeView: View { } } -struct TerminalSplitLeaf: View { +fileprivate struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool let action: (TerminalSplitOperation) -> Void diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 20e0d6b4f..e278f653b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -466,33 +466,33 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: newView, from: oldView) } } - + // Setup our undo - if let undoManager { - if let undoAction { - undoManager.setActionName(undoAction) + guard let undoManager else { return } + if let undoAction { + undoManager.setActionName(undoAction) + } + + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration + ) { target in + target.surfaceTree = oldTree + if let oldView { + DispatchQueue.main.async { + Ghostty.moveFocus(to: oldView, from: target.focusedSurface) + } } + undoManager.registerUndo( - withTarget: self, - expiresAfter: undoExpiration + withTarget: target, + expiresAfter: target.undoExpiration ) { target in - target.surfaceTree = oldTree - if let oldView { - DispatchQueue.main.async { - Ghostty.moveFocus(to: oldView, from: target.focusedSurface) - } - } - - undoManager.registerUndo( - withTarget: target, - expiresAfter: target.undoExpiration - ) { target in - target.replaceSurfaceTree( - newTree, - moveFocusTo: newView, - moveFocusFrom: target.focusedSurface, - undoAction: undoAction) - } + target.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: target.focusedSurface, + undoAction: undoAction) } } } @@ -835,7 +835,11 @@ class BaseTerminalController: NSWindowController, } } - private func splitDidDrop(source: Ghostty.SurfaceView, destination: Ghostty.SurfaceView, zone: TerminalSplitDropZone) { + private func splitDidDrop( + source: Ghostty.SurfaceView, + destination: Ghostty.SurfaceView, + zone: TerminalSplitDropZone + ) { // Map drop zone to split direction let direction: SplitTree.NewDirection = switch zone { case .top: .up @@ -843,12 +847,12 @@ class BaseTerminalController: NSWindowController, case .left: .left case .right: .right } - + // Check if source is in our tree if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move let treeWithoutSource = surfaceTree.remove(sourceNode) - + do { let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) replaceSurfaceTree( @@ -859,10 +863,10 @@ class BaseTerminalController: NSWindowController, } catch { Ghostty.logger.warning("failed to insert surface during drop: \(error)") } - + return } - + // Source is not in our tree - search other windows var sourceController: BaseTerminalController? var sourceNode: SplitTree.Node? @@ -875,33 +879,48 @@ class BaseTerminalController: NSWindowController, break } } - + guard let sourceController, let sourceNode else { Ghostty.logger.warning("source surface not found in any window during drop") return } - - // TODO: Undo for cross window move. - - // Remove from source controller's tree + + // Remove from source controller's tree and add it to our tree. + // We do this first because if there is an error then we can + // abort. let sourceTreeWithoutNode = sourceController.surfaceTree.remove(sourceNode) - sourceController.replaceSurfaceTree( - sourceTreeWithoutNode, - moveFocusTo: nil, - moveFocusFrom: nil, - undoAction: nil) - - // Insert into our tree + let newTree: SplitTree do { - let newTree = try surfaceTree.insert(view: source, at: destination, direction: direction) - replaceSurfaceTree( - newTree, - moveFocusTo: source, - moveFocusFrom: focusedSurface, - undoAction: "Move Split") + newTree = try surfaceTree.insert(view: source, at: destination, direction: direction) } catch { Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") + return + } + + // If our old sourceTree became empty, disable undo, because this will + // close the window and we don't have a way to restore that currently. + if sourceTreeWithoutNode.isEmpty { + undoManager?.disableUndoRegistration() } + defer { + if sourceTreeWithoutNode.isEmpty { + undoManager?.enableUndoRegistration() + } + } + + // Treat our undo below as a full group. + undoManager?.beginUndoGrouping() + undoManager?.setActionName("Move Split") + defer { + undoManager?.endUndoGrouping() + } + + sourceController.replaceSurfaceTree( + sourceTreeWithoutNode) + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface) } func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c5481851b..ae0b44e4a 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -671,7 +671,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr /// Closes the current window (including any other tabs) immediately and without /// confirmation. This will setup proper undo state so the action can be undone. - private func closeWindowImmediately() { + func closeWindowImmediately() { guard let window = window else { return } registerUndoForCloseWindow() -- 2.51.2 From 9b7124cf62140356817a0bc2c1f10b318fd99cab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 13:31:10 -0800 Subject: [PATCH 101/349] macos: use preference key to detect self dragging --- .../Features/Splits/TerminalSplitTreeView.swift | 7 ++++++- .../Ghostty/Surface View/SurfaceGrabHandle.swift | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index cffab8c6c..eb946673b 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -92,6 +92,7 @@ fileprivate struct TerminalSplitLeaf: View { let action: (TerminalSplitOperation) -> Void @State private var dropState: DropState = .idle + @State private var isSelfDragging: Bool = false var body: some View { GeometryReader { geometry in @@ -105,11 +106,15 @@ fileprivate struct TerminalSplitLeaf: View { action: action )) .overlay { - if case .dropping(let zone) = dropState { + if !isSelfDragging, case .dropping(let zone) = dropState { zone.overlay(in: geometry) .allowsHitTesting(false) } } + .onPreferenceChange(Ghostty.DraggingSurfaceKey.self) { value in + Ghostty.logger.warning("BABY WE DRAGGING \(String(describing: value))") + isSelfDragging = value == surfaceView.id + } .accessibilityElement(children: .contain) .accessibilityLabel("Terminal pane") } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index 8f4347644..2d2ce59e3 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -1,6 +1,16 @@ import SwiftUI extension Ghostty { + /// A preference key that propagates the ID of the SurfaceView currently being dragged, + /// or nil if no surface is being dragged. + struct DraggingSurfaceKey: PreferenceKey { + static var defaultValue: SurfaceView.ID? = nil + + static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { + value = nextValue() ?? value + } + } + /// A grab handle overlay at the top of the surface for dragging the window. /// Only appears when hovering in the top region of the surface. struct SurfaceGrabHandle: View { @@ -38,6 +48,7 @@ extension Ghostty { .draggable(surfaceView) { SurfaceDragPreview(surfaceView: surfaceView, scale: previewScale) } + .preference(key: DraggingSurfaceKey.self, value: isDragging ? surfaceView.id : nil) } } -- 2.51.2 From be97b5bede3cbdf73b1d46e56ddd2124ed9023b5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 13:59:41 -0800 Subject: [PATCH 102/349] macOS: convert Surface dragging to use NSDraggingSource --- macos/Ghostty.xcodeproj/project.pbxproj | 2 + .../Splits/TerminalSplitTreeView.swift | 1 - .../Surface View/SurfaceDragSource.swift | 196 ++++++++++++++++++ .../Surface View/SurfaceGrabHandle.swift | 67 +----- .../SurfaceView+Transferable.swift | 5 + .../Ghostty/Surface View/SurfaceView.swift | 4 + 6 files changed, 215 insertions(+), 60 deletions(-) create mode 100644 macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a2516fe10..100ddeaf5 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -145,6 +145,8 @@ Ghostty/Ghostty.Surface.swift, "Ghostty/NSEvent+Extension.swift", "Ghostty/Surface View/InspectorView.swift", + "Ghostty/Surface View/SurfaceDragSource.swift", + "Ghostty/Surface View/SurfaceGrabHandle.swift", "Ghostty/Surface View/SurfaceScrollView.swift", "Ghostty/Surface View/SurfaceView_AppKit.swift", Helpers/AppInfo.swift, diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index eb946673b..73d61439c 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -112,7 +112,6 @@ fileprivate struct TerminalSplitLeaf: View { } } .onPreferenceChange(Ghostty.DraggingSurfaceKey.self) { value in - Ghostty.logger.warning("BABY WE DRAGGING \(String(describing: value))") isSelfDragging = value == surfaceView.id } .accessibilityElement(children: .contain) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift new file mode 100644 index 000000000..534834af3 --- /dev/null +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -0,0 +1,196 @@ +import AppKit +import SwiftUI + +extension Ghostty { + /// A preference key that propagates the ID of the SurfaceView currently being dragged, + /// or nil if no surface is being dragged. + struct DraggingSurfaceKey: PreferenceKey { + static var defaultValue: SurfaceView.ID? = nil + + static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { + value = nextValue() ?? value + } + } + + /// A SwiftUI view that provides drag source functionality for terminal surfaces. + /// + /// This view wraps an AppKit-based drag source to enable drag-and-drop reordering + /// of terminal surfaces within split views. When the user drags this view, it initiates + /// an `NSDraggingSession` with the surface's UUID encoded in the pasteboard, allowing + /// drop targets to identify which surface is being moved. + /// + /// The view also publishes the dragging state via `DraggingSurfaceKey` preference, + /// enabling parent views to react to ongoing drag operations. + struct SurfaceDragSource: View { + /// The surface view that will be dragged. + let surfaceView: SurfaceView + + /// Binding that reflects whether a drag session is currently active. + @Binding var isDragging: Bool + + /// Binding that reflects whether the mouse is hovering over this view. + @Binding var isHovering: Bool + + var body: some View { + SurfaceDragSourceViewRepresentable( + surfaceView: surfaceView, + isDragging: $isDragging, + isHovering: $isHovering) + .preference(key: DraggingSurfaceKey.self, value: isDragging ? surfaceView.id : nil) + } + } + + /// An NSViewRepresentable that provides AppKit-based drag source functionality. + /// This gives us control over the drag lifecycle, particularly detecting drag start. + fileprivate struct SurfaceDragSourceViewRepresentable: NSViewRepresentable { + let surfaceView: SurfaceView + @Binding var isDragging: Bool + @Binding var isHovering: Bool + + func makeNSView(context: Context) -> SurfaceDragSourceView { + let view = SurfaceDragSourceView() + view.surfaceView = surfaceView + view.onDragStateChanged = { dragging in + isDragging = dragging + } + view.onHoverChanged = { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } + } + return view + } + + func updateNSView(_ nsView: SurfaceDragSourceView, context: Context) { + nsView.surfaceView = surfaceView + nsView.onDragStateChanged = { dragging in + isDragging = dragging + } + nsView.onHoverChanged = { hovering in + withAnimation(.easeInOut(duration: 0.15)) { + isHovering = hovering + } + } + } + } + + /// The underlying NSView that handles drag operations. + /// + /// This view manages mouse tracking and drag initiation for surface reordering. + /// It uses a local event loop to detect drag gestures and initiates an + /// `NSDraggingSession` when the user drags beyond the threshold distance. + fileprivate class SurfaceDragSourceView: NSView, NSDraggingSource { + /// Scale factor applied to the surface snapshot for the drag preview image. + private static let previewScale: CGFloat = 0.2 + + /// The surface view that will be dragged. Its UUID is encoded into the + /// pasteboard for drop targets to identify which surface is being moved. + var surfaceView: SurfaceView? + + /// Callback invoked when the drag state changes. Called with `true` when + /// a drag session begins, and `false` when it ends (completed or cancelled). + var onDragStateChanged: ((Bool) -> Void)? + + /// Callback invoked when the mouse enters or exits this view's bounds. + /// Used to update the hover state for visual feedback in the parent view. + var onHoverChanged: ((Bool) -> Void)? + + /// Whether we are currently in a mouse tracking loop (between mouseDown + /// and either mouseUp or drag initiation). Used to determine cursor state. + private var isTracking: Bool = false + + override func updateTrackingAreas() { + super.updateTrackingAreas() + + // To update our tracking area we just recreate it all. + trackingAreas.forEach { removeTrackingArea($0) } + + // Add our tracking area for mouse events + addTrackingArea(NSTrackingArea( + rect: bounds, + options: [.mouseEnteredAndExited, .activeInActiveApp], + owner: self, + userInfo: nil + )) + } + + override func resetCursorRects() { + addCursorRect(bounds, cursor: isTracking ? .closedHand : .openHand) + } + + override func mouseEntered(with event: NSEvent) { + onHoverChanged?(true) + } + + override func mouseExited(with event: NSEvent) { + onHoverChanged?(false) + } + + override func mouseDragged(with event: NSEvent) { + guard !isTracking, let surfaceView = surfaceView else { return } + + // Create the pasteboard item with the surface ID + let data = withUnsafeBytes(of: surfaceView.id.uuid) { Data($0) } + let pasteboardItem = NSPasteboardItem() + pasteboardItem.setData(data, forType: .ghosttySurfaceId) + let item = NSDraggingItem(pasteboardWriter: pasteboardItem) + + // Create a scaled preview image from the surface snapshot + if let snapshot = surfaceView.asImage { + let imageSize = NSSize( + width: snapshot.size.width * Self.previewScale, + height: snapshot.size.height * Self.previewScale + ) + let scaledImage = NSImage(size: imageSize) + scaledImage.lockFocus() + snapshot.draw( + in: NSRect(origin: .zero, size: imageSize), + from: NSRect(origin: .zero, size: snapshot.size), + operation: .copy, + fraction: 1.0 + ) + scaledImage.unlockFocus() + + item.setDraggingFrame( + NSRect(origin: .zero, size: imageSize), + contents: scaledImage + ) + } + + onDragStateChanged?(true) + beginDraggingSession(with: [item], event: event, source: self) + } + + // MARK: NSDraggingSource + + func draggingSession( + _ session: NSDraggingSession, + sourceOperationMaskFor context: NSDraggingContext + ) -> NSDragOperation { + return context == .withinApplication ? .move : [] + } + + func draggingSession( + _ session: NSDraggingSession, + willBeginAt screenPoint: NSPoint + ) { + isTracking = true + } + + func draggingSession( + _ session: NSDraggingSession, + movedTo screenPoint: NSPoint + ) { + NSCursor.closedHand.set() + } + + func draggingSession( + _ session: NSDraggingSession, + endedAt screenPoint: NSPoint, + operation: NSDragOperation + ) { + isTracking = false + onDragStateChanged?(false) + } + } +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index 2d2ce59e3..0b0b394ce 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -1,21 +1,11 @@ +import AppKit import SwiftUI -extension Ghostty { - /// A preference key that propagates the ID of the SurfaceView currently being dragged, - /// or nil if no surface is being dragged. - struct DraggingSurfaceKey: PreferenceKey { - static var defaultValue: SurfaceView.ID? = nil - - static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { - value = nextValue() ?? value - } - } - +extension Ghostty { /// A grab handle overlay at the top of the surface for dragging the window. /// Only appears when hovering in the top region of the surface. struct SurfaceGrabHandle: View { private let handleHeight: CGFloat = 10 - private let previewScale: CGFloat = 0.2 let surfaceView: SurfaceView @@ -35,58 +25,17 @@ extension Ghostty { } } .contentShape(Rectangle()) - .onHover { hovering in - withAnimation(.easeInOut(duration: 0.15)) { - isHovering = hovering - } + .overlay { + SurfaceDragSource( + surfaceView: surfaceView, + isDragging: $isDragging, + isHovering: $isHovering + ) } - .backport.pointerStyle(isHovering ? .grabIdle : nil) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) - .draggable(surfaceView) { - SurfaceDragPreview(surfaceView: surfaceView, scale: previewScale) - } - .preference(key: DraggingSurfaceKey.self, value: isDragging ? surfaceView.id : nil) - } - } - - /// A miniature preview of the surface view for drag operations that updates periodically. - private struct SurfaceDragPreview: View { - let surfaceView: SurfaceView - let scale: CGFloat - - var body: some View { - // We need to use a TimelineView to ensure that this doesn't - // cache forever. This will NOT let the view live update while - // being dragged; macOS doesn't seem to allow that. But it will - // make sure on new drags the screenshot is updated. - TimelineView(.periodic(from: .now, by: 1.0 / 30.0)) { _ in - if let snapshot = surfaceView.asImage { - #if canImport(AppKit) - Image(nsImage: snapshot) - .resizable() - .aspectRatio(contentMode: .fit) - .frame( - width: snapshot.size.width * scale, - height: snapshot.size.height * scale - ) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .shadow(radius: 10) - #elseif canImport(UIKit) - Image(uiImage: snapshot) - .resizable() - .aspectRatio(contentMode: .fit) - .frame( - width: snapshot.size.width * scale, - height: snapshot.size.height * scale - ) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .shadow(radius: 10) - #endif - } - } } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index d5d47f601..7eef69a71 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -49,3 +49,8 @@ extension UTType { /// a way to look up a surface by ID. static let ghosttySurfaceId = UTType(exportedAs: "com.mitchellh.ghosttySurfaceId") } + +extension NSPasteboard.PasteboardType { + /// Pasteboard type for dragging surface IDs. + static let ghosttySurfaceId = NSPasteboard.PasteboardType(UTType.ghosttySurfaceId.identifier) +} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 872b89d30..c224d373e 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -225,9 +225,13 @@ extension Ghostty { } } + #if canImport(AppKit) // Grab handle for dragging the window. We want this to appear at the very // top Z-index os it isn't faded by the unfocused overlay. + // + // This is disabled except on macOS because it uses AppKit drag/drop APIs. SurfaceGrabHandle(surfaceView: surfaceView) + #endif } } -- 2.51.2 From 7b743164ef1d5f7569fb451d23054937aae2faee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 14:39:41 -0800 Subject: [PATCH 103/349] macos: fix iOS builds --- macos/Sources/Ghostty/GhosttyDelegate.swift | 2 ++ .../Sources/Ghostty/Surface View/SurfaceView+Transferable.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/macos/Sources/Ghostty/GhosttyDelegate.swift b/macos/Sources/Ghostty/GhosttyDelegate.swift index 0ce3dced4..a9d255737 100644 --- a/macos/Sources/Ghostty/GhosttyDelegate.swift +++ b/macos/Sources/Ghostty/GhosttyDelegate.swift @@ -1,3 +1,5 @@ +import Foundation + extension Ghostty { /// This is a delegate that should be applied to your global app delegate for GhosttyKit /// to perform app-global operations. diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index 7eef69a71..509713309 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -50,7 +50,9 @@ extension UTType { static let ghosttySurfaceId = UTType(exportedAs: "com.mitchellh.ghosttySurfaceId") } +#if canImport(AppKit) extension NSPasteboard.PasteboardType { /// Pasteboard type for dragging surface IDs. static let ghosttySurfaceId = NSPasteboard.PasteboardType(UTType.ghosttySurfaceId.identifier) } +#endif -- 2.51.2 From e1f22472f623c821771a049c62cd12538eb0db96 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Dec 2025 15:04:27 -0800 Subject: [PATCH 104/349] macos: convert the transferable to a nsdraggingitem --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + .../Surface View/SurfaceDragSource.swift | 6 +- .../Extensions/Transferable+Extension.swift | 58 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/Transferable+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 100ddeaf5..ad2c54ca9 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -169,6 +169,7 @@ "Helpers/Extensions/NSView+Extension.swift", "Helpers/Extensions/NSWindow+Extension.swift", "Helpers/Extensions/NSWorkspace+Extension.swift", + "Helpers/Extensions/Transferable+Extension.swift", "Helpers/Extensions/UndoManager+Extension.swift", "Helpers/Extensions/View+Extension.swift", Helpers/Fullscreen.swift, diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 534834af3..e40a1fc9b 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -129,10 +129,8 @@ extension Ghostty { override func mouseDragged(with event: NSEvent) { guard !isTracking, let surfaceView = surfaceView else { return } - // Create the pasteboard item with the surface ID - let data = withUnsafeBytes(of: surfaceView.id.uuid) { Data($0) } - let pasteboardItem = NSPasteboardItem() - pasteboardItem.setData(data, forType: .ghosttySurfaceId) + // Create our dragging item from our transferable + guard let pasteboardItem = surfaceView.pasteboardItem() else { return } let item = NSDraggingItem(pasteboardWriter: pasteboardItem) // Create a scaled preview image from the surface snapshot diff --git a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift new file mode 100644 index 000000000..3bcc9057f --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift @@ -0,0 +1,58 @@ +import AppKit +import CoreTransferable +import UniformTypeIdentifiers + +extension Transferable { + /// Converts this Transferable to an NSPasteboardItem with lazy data loading. + /// Data is only fetched when the pasteboard consumer requests it. This allows + /// bridging a Transferable to NSDraggingSource. + func pasteboardItem() -> NSPasteboardItem? { + let itemProvider = NSItemProvider() + itemProvider.register(self) + + let types = itemProvider.registeredTypeIdentifiers.compactMap { UTType($0) } + guard !types.isEmpty else { return nil } + + let item = NSPasteboardItem() + let dataProvider = TransferableDataProvider(itemProvider: itemProvider) + let pasteboardTypes = types.map { NSPasteboard.PasteboardType($0.identifier) } + item.setDataProvider(dataProvider, forTypes: pasteboardTypes) + + return item + } +} + +private final class TransferableDataProvider: NSObject, NSPasteboardItemDataProvider { + private let itemProvider: NSItemProvider + + init(itemProvider: NSItemProvider) { + self.itemProvider = itemProvider + super.init() + } + + func pasteboard( + _ pasteboard: NSPasteboard?, + item: NSPasteboardItem, + provideDataForType type: NSPasteboard.PasteboardType + ) { + // NSPasteboardItemDataProvider requires synchronous data return, but + // NSItemProvider.loadDataRepresentation is async. We use a semaphore + // to block until the async load completes. This is safe because AppKit + // calls this method on a background thread during drag operations. + let semaphore = DispatchSemaphore(value: 0) + + var result: Data? + itemProvider.loadDataRepresentation(forTypeIdentifier: type.rawValue) { data, _ in + result = data + semaphore.signal() + } + + // Wait for the data to load + semaphore.wait() + + // Set it. I honestly don't know what happens here if this fails. + if let data = result { + item.setData(data, forType: type) + } + } +} -- 2.51.2 From e56dce3d84e62ee40e40426ab91c9c871bbb303b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 06:12:41 -0800 Subject: [PATCH 105/349] macos: don't create drop zone at all if self dragging --- .../Splits/TerminalSplitTreeView.swift | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 73d61439c..2a42dc599 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -99,12 +99,20 @@ fileprivate struct TerminalSplitLeaf: View { Ghostty.InspectableSurface( surfaceView: surfaceView, isSplit: isSplit) - .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( - dropState: $dropState, - viewSize: geometry.size, - destinationSurface: surfaceView, - action: action - )) + .background { + // If we're dragging ourself, we hide the entire drop zone. This makes + // it so that a released drop animates back to its source properly + // so it is a proper invalid drop zone. + if !isSelfDragging { + Color.clear + .onDrop(of: [.ghosttySurfaceId], delegate: SplitDropDelegate( + dropState: $dropState, + viewSize: geometry.size, + destinationSurface: surfaceView, + action: action + )) + } + } .overlay { if !isSelfDragging, case .dropping(let zone) = dropState { zone.overlay(in: geometry) @@ -113,6 +121,9 @@ fileprivate struct TerminalSplitLeaf: View { } .onPreferenceChange(Ghostty.DraggingSurfaceKey.self) { value in isSelfDragging = value == surfaceView.id + if isSelfDragging { + dropState = .idle + } } .accessibilityElement(children: .contain) .accessibilityLabel("Terminal pane") -- 2.51.2 From dbeeb952cc68072f13cef1004eda332114ef524d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 06:15:51 -0800 Subject: [PATCH 106/349] macos: fix dragging point --- .../Ghostty/Surface View/SurfaceDragSource.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index e40a1fc9b..ce243b7b3 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -149,8 +149,17 @@ extension Ghostty { ) scaledImage.unlockFocus() + // Position the drag image so the mouse is at the center of the image. + // I personally like the top middle or top left corner best but + // this matches macOS native tab dragging behavior (at least, as of + // macOS 26.2 on Dec 29, 2025). + let mouseLocation = convert(event.locationInWindow, from: nil) + let origin = NSPoint( + x: mouseLocation.x - imageSize.width / 2, + y: mouseLocation.y - imageSize.height / 2 + ) item.setDraggingFrame( - NSRect(origin: .zero, size: imageSize), + NSRect(origin: origin, size: imageSize), contents: scaledImage ) } -- 2.51.2 From 1b1ff3d76c17af6571a03c0fb027efef73153959 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 06:25:47 -0800 Subject: [PATCH 107/349] macos: add some unit tests --- .../Helpers/TransferablePasteboardTests.swift | 124 +++++++++++++++++ .../Splits/TerminalSplitDropZoneTests.swift | 128 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 macos/Tests/Helpers/TransferablePasteboardTests.swift create mode 100644 macos/Tests/Splits/TerminalSplitDropZoneTests.swift diff --git a/macos/Tests/Helpers/TransferablePasteboardTests.swift b/macos/Tests/Helpers/TransferablePasteboardTests.swift new file mode 100644 index 000000000..055dd5785 --- /dev/null +++ b/macos/Tests/Helpers/TransferablePasteboardTests.swift @@ -0,0 +1,124 @@ +import Testing +import AppKit +import CoreTransferable +import UniformTypeIdentifiers +@testable import Ghostty + +struct TransferablePasteboardTests { + // MARK: - Test Helpers + + /// A simple Transferable type for testing pasteboard conversion. + private struct DummyTransferable: Transferable, Equatable { + let payload: String + + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(contentType: .utf8PlainText) { value in + value.payload.data(using: .utf8)! + } importing: { data in + let string = String(data: data, encoding: .utf8)! + return DummyTransferable(payload: string) + } + } + } + + /// A Transferable type that registers multiple content types. + private struct MultiTypeTransferable: Transferable { + let text: String + + static var transferRepresentation: some TransferRepresentation { + DataRepresentation(contentType: .utf8PlainText) { value in + value.text.data(using: .utf8)! + } importing: { data in + MultiTypeTransferable(text: String(data: data, encoding: .utf8)!) + } + DataRepresentation(contentType: .plainText) { value in + value.text.data(using: .utf8)! + } importing: { data in + MultiTypeTransferable(text: String(data: data, encoding: .utf8)!) + } + } + } + + // MARK: - Basic Functionality + + @Test func pasteboardItemIsCreated() { + let transferable = DummyTransferable(payload: "hello") + let item = transferable.pasteboardItem() + #expect(item != nil) + } + + @Test func pasteboardItemContainsExpectedType() { + let transferable = DummyTransferable(payload: "hello") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let expectedType = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + #expect(item.types.contains(expectedType)) + } + + @Test func pasteboardItemProvidesCorrectData() { + let transferable = DummyTransferable(payload: "test data") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let pasteboardType = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + + // Write to a pasteboard to trigger data provider + let pasteboard = NSPasteboard(name: .init("test-\(UUID().uuidString)")) + pasteboard.clearContents() + pasteboard.writeObjects([item]) + + // Read back the data + guard let data = pasteboard.data(forType: pasteboardType) else { + Issue.record("Expected data to be available on pasteboard") + return + } + + let string = String(data: data, encoding: .utf8) + #expect(string == "test data") + } + + // MARK: - Multiple Content Types + + @Test func multipleTypesAreRegistered() { + let transferable = MultiTypeTransferable(text: "multi") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let utf8Type = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + let plainType = NSPasteboard.PasteboardType(UTType.plainText.identifier) + + #expect(item.types.contains(utf8Type)) + #expect(item.types.contains(plainType)) + } + + @Test func multipleTypesProvideCorrectData() { + let transferable = MultiTypeTransferable(text: "shared content") + guard let item = transferable.pasteboardItem() else { + Issue.record("Expected pasteboard item to be created") + return + } + + let pasteboard = NSPasteboard(name: .init("test-\(UUID().uuidString)")) + pasteboard.clearContents() + pasteboard.writeObjects([item]) + + // Both types should provide the same content + let utf8Type = NSPasteboard.PasteboardType(UTType.utf8PlainText.identifier) + let plainType = NSPasteboard.PasteboardType(UTType.plainText.identifier) + + if let utf8Data = pasteboard.data(forType: utf8Type) { + #expect(String(data: utf8Data, encoding: .utf8) == "shared content") + } + + if let plainData = pasteboard.data(forType: plainType) { + #expect(String(data: plainData, encoding: .utf8) == "shared content") + } + } +} diff --git a/macos/Tests/Splits/TerminalSplitDropZoneTests.swift b/macos/Tests/Splits/TerminalSplitDropZoneTests.swift new file mode 100644 index 000000000..5c956fcc8 --- /dev/null +++ b/macos/Tests/Splits/TerminalSplitDropZoneTests.swift @@ -0,0 +1,128 @@ +import Testing +import Foundation +@testable import Ghostty + +struct TerminalSplitDropZoneTests { + private let standardSize = CGSize(width: 100, height: 100) + + // MARK: - Basic Edge Detection + + @Test func topEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 5), in: standardSize) + #expect(zone == .top) + } + + @Test func bottomEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 95), in: standardSize) + #expect(zone == .bottom) + } + + @Test func leftEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 5, y: 50), in: standardSize) + #expect(zone == .left) + } + + @Test func rightEdge() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 95, y: 50), in: standardSize) + #expect(zone == .right) + } + + // MARK: - Corner Tie-Breaking + // When distances are equal, the check order determines the result: + // left -> right -> top -> bottom + + @Test func topLeftCornerSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 0, y: 0), in: standardSize) + #expect(zone == .left) + } + + @Test func topRightCornerSelectsRight() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 100, y: 0), in: standardSize) + #expect(zone == .right) + } + + @Test func bottomLeftCornerSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 0, y: 100), in: standardSize) + #expect(zone == .left) + } + + @Test func bottomRightCornerSelectsRight() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 100, y: 100), in: standardSize) + #expect(zone == .right) + } + + // MARK: - Center Point (All Distances Equal) + + @Test func centerSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 50), in: standardSize) + #expect(zone == .left) + } + + // MARK: - Non-Square Aspect Ratio + + @Test func rectangularViewTopEdge() { + let size = CGSize(width: 200, height: 100) + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 100, y: 10), in: size) + #expect(zone == .top) + } + + @Test func rectangularViewLeftEdge() { + let size = CGSize(width: 200, height: 100) + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 10, y: 50), in: size) + #expect(zone == .left) + } + + @Test func tallRectangleTopEdge() { + let size = CGSize(width: 100, height: 200) + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 10), in: size) + #expect(zone == .top) + } + + // MARK: - Out-of-Bounds Points + + @Test func pointLeftOfViewSelectsLeft() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: -10, y: 50), in: standardSize) + #expect(zone == .left) + } + + @Test func pointAboveViewSelectsTop() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: -10), in: standardSize) + #expect(zone == .top) + } + + @Test func pointRightOfViewSelectsRight() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 110, y: 50), in: standardSize) + #expect(zone == .right) + } + + @Test func pointBelowViewSelectsBottom() { + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 50, y: 110), in: standardSize) + #expect(zone == .bottom) + } + + // MARK: - Diagonal Regions (Triangular Zones) + + @Test func upperLeftTriangleSelectsLeft() { + // Point in the upper-left triangle, closer to left than top + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 20, y: 30), in: standardSize) + #expect(zone == .left) + } + + @Test func upperRightTriangleSelectsRight() { + // Point in the upper-right triangle, closer to right than top + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 80, y: 30), in: standardSize) + #expect(zone == .right) + } + + @Test func lowerLeftTriangleSelectsLeft() { + // Point in the lower-left triangle, closer to left than bottom + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 20, y: 70), in: standardSize) + #expect(zone == .left) + } + + @Test func lowerRightTriangleSelectsRight() { + // Point in the lower-right triangle, closer to right than bottom + let zone = TerminalSplitDropZone.calculate(at: CGPoint(x: 80, y: 70), in: standardSize) + #expect(zone == .right) + } +} -- 2.51.2 From cfa3de5d9b34e9ee9edb9adb91b6cf7616ce3c7a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 06:33:29 -0800 Subject: [PATCH 108/349] macos: change style --- macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index 0b0b394ce..d8ff49163 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -19,9 +19,9 @@ extension Ghostty { .frame(height: handleHeight) .overlay(alignment: .center) { if isHovering || isDragging { - Capsule() - .fill(Color.white.opacity(0.4)) - .frame(width: 40, height: 4) + Image(systemName: "ellipsis") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.white.opacity(0.5)) } } .contentShape(Rectangle()) -- 2.51.2 From c164e3bc02ed459d0b29c5b81406cfd97b0f7399 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 07:13:23 -0800 Subject: [PATCH 109/349] macos: fix messy rebase --- macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 37d868a9f..f9a5480ec 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1654,6 +1654,7 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double + let backgroundBlur: Ghostty.Config.BackgroundBlur let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1662,6 +1663,7 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 + self.backgroundBlur = .disabled self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1671,6 +1673,7 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity + self.backgroundBlur = config.backgroundBlur self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config) -- 2.51.2 From 83314473983d41b99547144dde83624041b8e58f Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 16:15:02 +0100 Subject: [PATCH 110/349] Improve type detection --- src/apprt/gtk/class/surface.zig | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cbd444936..22e08c598 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3642,11 +3642,9 @@ const Clipboard = struct { // pass through when the clipboard contains non-text content (e.g., images). if (state == .paste) { const formats = clipboard.getFormats(); - if (formats.containMimeType("text/plain") == 0 and - formats.containMimeType("UTF8_STRING") == 0 and - formats.containMimeType("TEXT") == 0 and - formats.containMimeType("STRING") == 0) - { + // G_TYPE_STRING = G_TYPE_MAKE_FUNDAMENTAL(16) = (16 << 2) = 64 + const G_TYPE_STRING: usize = 64; + if (formats.containGtype(G_TYPE_STRING) == 0) { log.debug("clipboard has no text format, not starting paste request", .{}); return false; } -- 2.51.2 From 86d5048dad55f6931b8e35b736b335911f1d1f55 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 08:50:23 -0800 Subject: [PATCH 111/349] terminal: PageList needs to fix up viewport pin after row change From #10074 The test comments explain in detail. --- src/terminal/PageList.zig | 94 +++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 07b264ef5..910083b7b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -394,6 +394,7 @@ pub inline fn pauseIntegrityChecks(self: *PageList, pause: bool) void { const IntegrityError = error{ TotalRowsMismatch, ViewportPinOffsetMismatch, + ViewportPinInsufficientRows, PageSerialInvalid, }; @@ -435,9 +436,8 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void { 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; + if (self.viewport == .pin) { + // Verify that our viewport pin row offset is correct. const actual_offset: usize = offset: { var offset: usize = 0; var node = self.pages.last; @@ -456,12 +456,24 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void { return error.ViewportPinOffsetMismatch; }; - if (cached_offset != actual_offset) { + if (self.viewport_pin_row_offset) |cached_offset| { + if (cached_offset != actual_offset) { + log.warn( + "PageList integrity violation: viewport pin offset mismatch cached={} actual={}", + .{ cached_offset, actual_offset }, + ); + return error.ViewportPinOffsetMismatch; + } + } + + // Ensure our viewport has enough rows. + const rows = self.total_rows - actual_offset; + if (rows < self.rows) { log.warn( - "PageList integrity violation: viewport pin offset mismatch cached={} actual={}", - .{ cached_offset, actual_offset }, + "PageList integrity violation: viewport pin rows too small rows={} needed={}", + .{ rows, self.rows }, ); - return error.ViewportPinOffsetMismatch; + return error.ViewportPinInsufficientRows; } } } @@ -856,6 +868,16 @@ pub fn resize(self: *PageList, opts: Resize) !void { try self.resizeCols(cols, opts.cursor); }, } + + // Various resize operations can change our total row count such + // that our viewport pin is now in the active area and has insufficient + // space. We need to check for this case and fix it up. + switch (self.viewport) { + .pin => if (self.pinIsActive(self.viewport_pin.*)) { + self.viewport = .active; + }, + .active, .top => {}, + } } /// Resize the pagelist with reflow by adding or removing columns. @@ -1717,7 +1739,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // area, since that will lead to all sorts of problems. switch (self.viewport) { .pin => if (self.pinIsActive(self.viewport_pin.*)) { - self.viewport = .{ .active = {} }; + self.viewport = .active; }, .active, .top => {}, } @@ -8284,7 +8306,7 @@ test "PageList resize reflow invalidates viewport offset cache" { // Verify scrollbar cache was invalidated during reflow try testing.expectEqual(Scrollbar{ .total = s.total_rows, - .offset = 8, + .offset = 5, .len = s.rows, }, s.scrollbar()); } @@ -10850,3 +10872,57 @@ test "PageList resize reflow grapheme map capacity exceeded" { // Verify the resize succeeded try testing.expectEqual(@as(usize, 2), s.cols); } + +test "PageList resize grow cols with unwrap fixes viewport pin" { + // Regression test: after resize/reflow, the viewport pin can end up at a + // position where pin.y + rows > total_rows, causing getBottomRight to panic. + + // The plan is to pin viewport in history, then grow columns to unwrap rows. + // The unwrap reduces total_rows, but the tracked pin moves to a position + // that no longer has enough rows below it for the viewport height. + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 10, null); + defer s.deinit(); + + // Make sure we have some history, in this case we have 30 rows of history + try s.growRows(30); + try testing.expectEqual(@as(usize, 40), s.totalRows()); + + // Fill all rows with wrapped content (pairs that unwrap when cols increase) + var it = s.pageIterator(.right_down, .{ .screen = .{} }, null); + while (it.next()) |chunk| { + const page = &chunk.node.data; + for (chunk.start..chunk.end) |y| { + const rac = page.getRowAndCell(0, y); + if (y % 2 == 0) { + rac.row.wrap = true; + } else { + rac.row.wrap_continuation = true; + } + for (0..s.cols) |x| { + page.getRowAndCell(x, y).cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'A' }, + }; + } + } + } + + // Pin viewport at row 28 (in history, 2 rows before active area at row 30). + // After unwrap: row 28 -> row 14, total_rows 40 -> 20, active starts at 10. + // Pin at 14 needs rows 14-23, but only 0-19 exist -> overflow. + s.scroll(.{ .pin = s.pin(.{ .screen = .{ .y = 28 } }).? }); + try testing.expect(s.viewport == .pin); + try testing.expect(s.getBottomRight(.viewport) != null); + + // Resize with reflow: unwraps rows, reducing total_rows + try s.resize(.{ .cols = 4, .reflow = true }); + try testing.expectEqual(@as(usize, 4), s.cols); + try testing.expect(s.totalRows() < 40); + + // Used to panic here, so test that we can get the bottom right. + const br_after = s.getBottomRight(.viewport); + try testing.expect(br_after != null); +} -- 2.51.2 From 972b65eb1b12d7872790639066c20c0bf46f783b Mon Sep 17 00:00:00 2001 From: cyppe Date: Mon, 29 Dec 2025 18:32:25 +0100 Subject: [PATCH 112/349] review --- src/apprt/gtk/class/surface.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 22e08c598..cb5122314 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3642,9 +3642,7 @@ const Clipboard = struct { // pass through when the clipboard contains non-text content (e.g., images). if (state == .paste) { const formats = clipboard.getFormats(); - // G_TYPE_STRING = G_TYPE_MAKE_FUNDAMENTAL(16) = (16 << 2) = 64 - const G_TYPE_STRING: usize = 64; - if (formats.containGtype(G_TYPE_STRING) == 0) { + if (formats.containGtype(gobject.ext.types.string) == 0) { log.debug("clipboard has no text format, not starting paste request", .{}); return false; } -- 2.51.2 From 25c413005b2ffa3a414fbd1a77ff4e416559c92a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 09:49:57 -0800 Subject: [PATCH 113/349] macos: emit a notification when the surface drag ends outside area --- .../Surface View/SurfaceDragSource.swift | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index ce243b7b3..34184e46e 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -99,6 +99,18 @@ extension Ghostty { /// and either mouseUp or drag initiation). Used to determine cursor state. private var isTracking: Bool = false + /// Local event monitor to detect escape key presses during drag. + private var escapeMonitor: Any? + + /// Whether the current drag was cancelled by pressing escape. + private var dragCancelledByEscape: Bool = false + + deinit { + if let escapeMonitor { + NSEvent.removeMonitor(escapeMonitor) + } + } + override func updateTrackingAreas() { super.updateTrackingAreas() @@ -182,6 +194,15 @@ extension Ghostty { willBeginAt screenPoint: NSPoint ) { isTracking = true + + // Reset our escape tracking + dragCancelledByEscape = false + escapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + if event.keyCode == 53 { // Escape key + self?.dragCancelledByEscape = true + } + return event + } } func draggingSession( @@ -196,8 +217,32 @@ extension Ghostty { endedAt screenPoint: NSPoint, operation: NSDragOperation ) { + if let escapeMonitor { + NSEvent.removeMonitor(escapeMonitor) + self.escapeMonitor = nil + } + + if operation == [] && !dragCancelledByEscape { + let endsInWindow = NSApplication.shared.windows.contains { window in + window.isVisible && window.frame.contains(screenPoint) + } + if !endsInWindow { + NotificationCenter.default.post( + name: .ghosttySurfaceDragEndedNoTarget, + object: surfaceView + ) + } + } + isTracking = false onDragStateChanged?(false) } } } + +extension Notification.Name { + /// Posted when a surface drag session ends with no operation (the drag was + /// released outside a valid drop target) and was not cancelled by the user + /// pressing escape. The notification's object is the SurfaceView that was dragged. + static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") +} -- 2.51.2 From 89c515cab50b2d3d78b33065f8567bffb92ca706 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 10:01:29 -0800 Subject: [PATCH 114/349] macos: new window from tree in TerminalController --- .../Terminal/TerminalController.swift | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ae0b44e4a..bd2c99a22 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -275,6 +275,56 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return c } + /// Create a new window with an existing split tree. + /// The window will be sized to match the tree's current view bounds if available. + static func newWindow( + _ ghostty: Ghostty.App, + tree: SplitTree + ) -> TerminalController { + let c = TerminalController.init(ghostty, withSurfaceTree: tree) + + // Calculate the target frame based on the tree's view bounds + let treeSize: CGSize? = tree.root?.viewBounds() + + DispatchQueue.main.async { + if let window = c.window { + // If we have a tree size, resize the window's content to match + if let treeSize, treeSize.width > 0, treeSize.height > 0 { + window.setContentSize(treeSize) + window.constrainToScreen() + } + + if !window.styleMask.contains(.fullScreen) { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } + } + + c.showWindow(self) + } + + // Setup our undo + if let undoManager = c.undoManager { + undoManager.setActionName("New Window") + undoManager.registerUndo( + withTarget: c, + expiresAfter: c.undoExpiration + ) { target in + undoManager.disableUndoRegistration { + target.closeWindow(nil) + } + + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: target.undoExpiration + ) { ghostty in + _ = TerminalController.newWindow(ghostty, tree: tree) + } + } + } + + return c + } + static func newTab( _ ghostty: Ghostty.App, from parent: NSWindow? = nil, @@ -397,7 +447,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return controller } - + //MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { -- 2.51.2 From 5ecd26727e738ca9a26a02f92fc9711dd593b542 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 10:11:58 -0800 Subject: [PATCH 115/349] macos: allow pulling split out into its own window --- .../Terminal/BaseTerminalController.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e278f653b..ce2d5a5f2 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -200,6 +200,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyDidPresentTerminal(_:)), name: Ghostty.Notification.ghosttyPresentTerminal, object: nil) + center.addObserver( + self, + selector: #selector(ghosttySurfaceDragEndedNoTarget(_:)), + name: .ghosttySurfaceDragEndedNoTarget, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -721,6 +726,31 @@ class BaseTerminalController: NSWindowController, target.highlight() } + @objc private func ghosttySurfaceDragEndedNoTarget(_ notification: Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard let targetNode = surfaceTree.root?.node(view: target) else { return } + + // If our tree isn't split, then we never create a new window, because + // it is already a single split. + guard surfaceTree.isSplit else { return } + + // Remove the surface from our tree + let removedTree = surfaceTree.remove(targetNode) + + // Create a new tree with the dragged surface and open a new window + let newTree = SplitTree(view: target) + + // Treat our undo below as a full group. + undoManager?.beginUndoGrouping() + undoManager?.setActionName("Move Split") + defer { + undoManager?.endUndoGrouping() + } + + replaceSurfaceTree(removedTree, moveFocusFrom: focusedSurface) + _ = TerminalController.newWindow(ghostty, tree: newTree) + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { -- 2.51.2 From 29edbbbc8620e66c50fa3163522220da2a48b3af Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 10:19:51 -0800 Subject: [PATCH 116/349] macos: open dragged windows where they are dropped --- .../Terminal/BaseTerminalController.swift | 5 ++++- .../Features/Terminal/TerminalController.swift | 15 +++++++++++++-- .../Ghostty/Surface View/SurfaceDragSource.swift | 12 ++++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ce2d5a5f2..529b1d18a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -734,6 +734,9 @@ class BaseTerminalController: NSWindowController, // it is already a single split. guard surfaceTree.isSplit else { return } + // Extract the drop position from the notification + let dropPoint = notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint + // Remove the surface from our tree let removedTree = surfaceTree.remove(targetNode) @@ -748,7 +751,7 @@ class BaseTerminalController: NSWindowController, } replaceSurfaceTree(removedTree, moveFocusFrom: focusedSurface) - _ = TerminalController.newWindow(ghostty, tree: newTree) + _ = TerminalController.newWindow(ghostty, tree: newTree, position: dropPoint) } // MARK: Local Events diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bd2c99a22..021b9f394 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -277,9 +277,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr /// Create a new window with an existing split tree. /// The window will be sized to match the tree's current view bounds if available. + /// - Parameters: + /// - ghostty: The Ghostty app instance. + /// - tree: The split tree to use for the new window. + /// - position: Optional screen position (top-left corner) for the new window. + /// If nil, the window will cascade from the last cascade point. static func newWindow( _ ghostty: Ghostty.App, - tree: SplitTree + tree: SplitTree, + position: NSPoint? = nil ) -> TerminalController { let c = TerminalController.init(ghostty, withSurfaceTree: tree) @@ -295,7 +301,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } if !window.styleMask.contains(.fullScreen) { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + if let position { + window.setFrameTopLeftPoint(position) + window.constrainToScreen() + } else { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 34184e46e..21416ac75 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -177,7 +177,11 @@ extension Ghostty { } onDragStateChanged?(true) - beginDraggingSession(with: [item], event: event, source: self) + let session = beginDraggingSession(with: [item], event: event, source: self) + + // We need to disable this so that endedAt happens immediately for our + // drags outside of any targets. + session.animatesToStartingPositionsOnCancelOrFail = false } // MARK: NSDraggingSource @@ -229,7 +233,8 @@ extension Ghostty { if !endsInWindow { NotificationCenter.default.post( name: .ghosttySurfaceDragEndedNoTarget, - object: surfaceView + object: surfaceView, + userInfo: [Foundation.Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey: screenPoint] ) } } @@ -245,4 +250,7 @@ extension Notification.Name { /// released outside a valid drop target) and was not cancelled by the user /// pressing escape. The notification's object is the SurfaceView that was dragged. static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") + + /// Key for the screen point where the drag ended in the userInfo dictionary. + static let ghosttySurfaceDragEndedNoTargetPointKey = "endedAtPoint" } -- 2.51.2 From 19f7b57cd1eddff5b02ca2eb227ffb13d2dee4dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 10:36:17 -0800 Subject: [PATCH 117/349] macos: fixup focus issues when closing the new window --- .../Terminal/BaseTerminalController.swift | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 529b1d18a..c34119561 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -733,9 +733,11 @@ class BaseTerminalController: NSWindowController, // If our tree isn't split, then we never create a new window, because // it is already a single split. guard surfaceTree.isSplit else { return } - - // Extract the drop position from the notification - let dropPoint = notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint + + // If we are removing our focused surface then we move it. + if focusedSurface == target { + focusedSurface = findNextFocusTargetAfterClosing(node: targetNode) + } // Remove the surface from our tree let removedTree = surfaceTree.remove(targetNode) @@ -751,7 +753,10 @@ class BaseTerminalController: NSWindowController, } replaceSurfaceTree(removedTree, moveFocusFrom: focusedSurface) - _ = TerminalController.newWindow(ghostty, tree: newTree, position: dropPoint) + _ = TerminalController.newWindow( + ghostty, + tree: newTree, + position: notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint) } // MARK: Local Events @@ -1205,6 +1210,15 @@ class BaseTerminalController: NSWindowController, } func windowDidBecomeKey(_ notification: Notification) { + // If when we become key our first responder is the window itself, then we + // want to move focus to our focused terminal surface. This works around + // various weirdness with moving surfaces around. + if let window, window.firstResponder == window, let focusedSurface { + DispatchQueue.main.async { + Ghostty.moveFocus(to: focusedSurface) + } + } + // Becoming/losing key means we have to notify our surface(s) that we have focus // so things like cursors blink, pty events are sent, etc. self.syncFocusToSurfaceTree() -- 2.51.2 From 7512f6158bbfddd88411a17735deba77f08ecf0a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 10:46:58 -0800 Subject: [PATCH 118/349] macos: fix bugs --- .../Features/Terminal/BaseTerminalController.swift | 9 ++++++--- macos/Sources/Features/Terminal/TerminalController.swift | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index c34119561..e7caf70a9 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -734,7 +734,9 @@ class BaseTerminalController: NSWindowController, // it is already a single split. guard surfaceTree.isSplit else { return } - // If we are removing our focused surface then we move it. + // If we are removing our focused surface then we move it. We need to + // keep track of our old one so undo sends focus back to the right place. + let oldFocusedSurface = focusedSurface if focusedSurface == target { focusedSurface = findNextFocusTargetAfterClosing(node: targetNode) } @@ -752,11 +754,12 @@ class BaseTerminalController: NSWindowController, undoManager?.endUndoGrouping() } - replaceSurfaceTree(removedTree, moveFocusFrom: focusedSurface) + replaceSurfaceTree(removedTree, moveFocusFrom: oldFocusedSurface) _ = TerminalController.newWindow( ghostty, tree: newTree, - position: notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint) + position: notification.userInfo?[Notification.Name.ghosttySurfaceDragEndedNoTargetPointKey] as? NSPoint, + confirmUndo: false) } // MARK: Local Events diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 021b9f394..fbd215e2b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -285,7 +285,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr static func newWindow( _ ghostty: Ghostty.App, tree: SplitTree, - position: NSPoint? = nil + position: NSPoint? = nil, + confirmUndo: Bool = true, ) -> TerminalController { let c = TerminalController.init(ghostty, withSurfaceTree: tree) @@ -321,7 +322,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr expiresAfter: c.undoExpiration ) { target in undoManager.disableUndoRegistration { - target.closeWindow(nil) + if confirmUndo { + target.closeWindow(nil) + } else { + target.closeWindowImmediately() + } } undoManager.registerUndo( -- 2.51.2 From a826892ef7502333dc81b74625dd82253f4e4323 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 11:07:52 -0800 Subject: [PATCH 119/349] macos: make undo/redo work for final split dragged out Fixes #10093 --- .../Terminal/BaseTerminalController.swift | 50 +++++++++++-------- .../Terminal/TerminalController.swift | 9 +++- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e7caf70a9..e4f700170 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -893,18 +893,19 @@ class BaseTerminalController: NSWindowController, if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move let treeWithoutSource = surfaceTree.remove(sourceNode) - + let newTree: SplitTree do { - let newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) - replaceSurfaceTree( - newTree, - moveFocusTo: source, - moveFocusFrom: focusedSurface, - undoAction: "Move Split") + newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) } catch { Ghostty.logger.warning("failed to insert surface during drop: \(error)") + return } + replaceSurfaceTree( + newTree, + moveFocusTo: source, + moveFocusFrom: focusedSurface, + undoAction: "Move Split") return } @@ -938,17 +939,6 @@ class BaseTerminalController: NSWindowController, return } - // If our old sourceTree became empty, disable undo, because this will - // close the window and we don't have a way to restore that currently. - if sourceTreeWithoutNode.isEmpty { - undoManager?.disableUndoRegistration() - } - defer { - if sourceTreeWithoutNode.isEmpty { - undoManager?.enableUndoRegistration() - } - } - // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") @@ -956,8 +946,28 @@ class BaseTerminalController: NSWindowController, undoManager?.endUndoGrouping() } - sourceController.replaceSurfaceTree( - sourceTreeWithoutNode) + if sourceTreeWithoutNode.isEmpty { + // If our source tree is becoming empty, then we're closing this terminal. + // We need to handle this carefully to get undo to work properly. If the + // controller is a TerminalController this is easy because it has a way + // to do this. + if let c = sourceController as? TerminalController { + c.closeWindowImmediately() + } else { + // Not a TerminalController so we always undo into a new window. + _ = TerminalController.newWindow( + sourceController.ghostty, + tree: sourceController.surfaceTree, + confirmUndo: false) + } + } else { + // The source isn't empty so we can do a simple replace which will handle + // the undo properly. + sourceController.replaceSurfaceTree( + sourceTreeWithoutNode) + } + + // Add in the surface to our tree replaceSurfaceTree( newTree, moveFocusTo: source, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index fbd215e2b..26ca8f70e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -945,13 +945,20 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Make it the key window window.makeKeyAndOrderFront(nil) } - + // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) { DispatchQueue.main.async { Ghostty.moveFocus(to: focusTarget, from: nil) } + } else if let focusedSurface = surfaceTree.first { + // No prior focused surface or we can't find it, let's focus + // the first. + self.focusedSurface = focusedSurface + DispatchQueue.main.async { + Ghostty.moveFocus(to: focusedSurface, from: nil) + } } } } -- 2.51.2 From 61df50d70b7d80096e1a3ca758309308c35303c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Dec 2025 12:06:36 -0800 Subject: [PATCH 120/349] input: add `end_key_sequence` binding action End the currently active key sequence, if any, and flush the keys up to this point to the terminal, excluding the key that triggered this action. For example: `ctrl+w>escape=end_key_sequence` would encode `ctrl+w` to the terminal and exit the key sequence. Normally, an invalid sequence will reset the key sequence and flush all data including the invalid key. This action allows you to flush only the prior keys, which is useful when you want to bind something like a control key (`ctrl+w`) but not send additional inputs. --- src/Surface.zig | 8 ++++++++ src/input/Binding.zig | 15 +++++++++++++++ src/input/command.zig | 1 + 3 files changed, 24 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 614f40475..43ee440c2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5794,6 +5794,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return try self.deactivateAllKeyTables(); }, + .end_key_sequence => { + // End the key sequence and flush queued keys to the terminal, + // but don't encode the key that triggered this action. This + // will do that because leaf keys (keys with bindings) aren't + // in the queued encoding list. + self.endKeySequence(.flush, .retain); + }, + .crash => |location| switch (location) { .main => @panic("crash binding action, crashing intentionally"), diff --git a/src/input/Binding.zig b/src/input/Binding.zig index e7507b112..d5b24c61b 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -827,6 +827,20 @@ pub const Action = union(enum) { /// be undone or redone. redo, + /// End the currently active key sequence, if any, and flush the + /// keys up to this point to the terminal, excluding the key that + /// triggered this action. + /// + /// For example: `ctrl+w>escape=end_key_sequence` would encode + /// `ctrl+w` to the terminal and exit the key sequence. + /// + /// Normally, an invalid sequence will reset the key sequence and + /// flush all data including the invalid key. This action allows + /// you to flush only the prior keys, which is useful when you want + /// to bind something like a control key (`ctrl+w`) but not send + /// additional inputs. + end_key_sequence, + /// Activate a named key table (see `keybind` configuration documentation). /// The named key table will remain active until `deactivate_key_table` /// is called. If you want a one-shot key table activation, use the @@ -1316,6 +1330,7 @@ pub const Action = union(enum) { .activate_key_table_once, .deactivate_key_table, .deactivate_all_key_tables, + .end_key_sequence, .crash, => .surface, diff --git a/src/input/command.zig b/src/input/command.zig index 936f2211c..f089112db 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -696,6 +696,7 @@ fn actionCommands(action: Action.Key) []const Command { .activate_key_table_once, .deactivate_key_table, .deactivate_all_key_tables, + .end_key_sequence, .crash, => comptime &.{}, -- 2.51.2 From d09bac64aec6e8ef0ce84d3383ae19b94cbf1848 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Sun, 28 Dec 2025 15:39:29 +0100 Subject: [PATCH 121/349] Remove systemd integration from nautilus extension As far as I understand ghostty integrates with systemd already nowadays, and manages its own scopes/cgroups for tabs. As such, explicitly moving launching ghostty into a separate systemd scope from nautilus no longer seems necessary. --- dist/linux/ghostty_nautilus.py | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index 42c397642..4f13dd617 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -19,39 +19,13 @@ from os.path import isdir from gi import require_version -from gi.repository import Nautilus, GObject, Gio, GLib +from gi.repository import Nautilus, GObject, Gio class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): - def __init__(self): - super().__init__() - session = Gio.bus_get_sync(Gio.BusType.SESSION, None) - self._systemd = None - # Check if the this system runs under systemd, per sd_booted(3) - if isdir('/run/systemd/system/'): - self._systemd = Gio.DBusProxy.new_sync(session, - Gio.DBusProxyFlags.NONE, - None, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", None) - def _open_terminal(self, path): cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false'] - child = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) - if self._systemd: - # Move new terminal into a dedicated systemd scope to make systemd - # track the terminal separately; in particular this makes systemd - # keep a separate CPU and memory account for the terminal which in turn - # ensures that oomd doesn't take nautilus down if a process in - # ghostty consumes a lot of memory. - pid = int(child.get_identifier()) - props = [("PIDs", GLib.Variant('au', [pid])), - ('CollectMode', GLib.Variant('s', 'inactive-or-failed'))] - name = 'app-nautilus-com.mitchellh.ghostty-{}.scope'.format(pid) - args = GLib.Variant('(ssa(sv)a(sa(sv)))', (name, 'fail', props, [])) - self._systemd.call_sync('StartTransientUnit', args, - Gio.DBusCallFlags.NO_AUTO_START, 500, None) + Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) def _menu_item_activated(self, _menu, paths): for path in paths: -- 2.51.2 From 622d49206a27659381f8a994b792215525a80c2f Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 29 Dec 2025 17:42:19 +0100 Subject: [PATCH 122/349] Remove unused imports --- dist/linux/ghostty_nautilus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index 4f13dd617..7e6c6e302 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -17,8 +17,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from os.path import isdir -from gi import require_version from gi.repository import Nautilus, GObject, Gio -- 2.51.2 From 5c4af69765b9668ca1a6f7623e42498f26040973 Mon Sep 17 00:00:00 2001 From: qingyunha <845767657@qq.com> Date: Tue, 30 Dec 2025 08:47:18 +0800 Subject: [PATCH 123/349] Update Vim filetype detection patterns Fixes #10094 --- 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 9140b83f8..062ccd2b6 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/*,*.ghostty setf ghostty + \\au BufRead,BufNewFile */ghostty/config,*/*.ghostty/config,*/ghostty/themes/*,*.ghostty setf ghostty \\ ; pub const ftplugin = -- 2.51.2 From 53c510ac4055166038da29f4d05dbb7fa958e4e7 Mon Sep 17 00:00:00 2001 From: John Xu Date: Tue, 30 Dec 2025 16:32:12 +0800 Subject: [PATCH 124/349] macos: keep glass titlebar inset in sync on layout --- .../Terminal/TerminalViewContainer.swift | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift index 1765edec3..c65dca1d2 100644 --- a/macos/Sources/Features/Terminal/TerminalViewContainer.swift +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -8,6 +8,7 @@ class TerminalViewContainer: NSView { /// Glass effect view for liquid glass background when transparency is enabled private var glassEffectView: NSView? + private var glassTopConstraint: NSLayoutConstraint? private var derivedConfig: DerivedConfig init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) { @@ -54,6 +55,12 @@ class TerminalViewContainer: NSView { override func viewDidMoveToWindow() { super.viewDidMoveToWindow() updateGlassEffectIfNeeded() + updateGlassEffectTopInsetIfNeeded() + } + + override func layout() { + super.layout() + updateGlassEffectTopInsetIfNeeded() } @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -74,6 +81,7 @@ private extension TerminalViewContainer { @available(macOS 26.0, *) func addGlassEffectViewIfNeeded() -> NSGlassEffectView? { if let existed = glassEffectView as? NSGlassEffectView { + updateGlassEffectTopInsetIfNeeded() return existed } guard let themeFrameView = window?.contentView?.superview else { @@ -82,12 +90,18 @@ private extension TerminalViewContainer { let effectView = NSGlassEffectView() addSubview(effectView, positioned: .below, relativeTo: terminalView) effectView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - effectView.topAnchor.constraint(equalTo: topAnchor, constant: -themeFrameView.safeAreaInsets.top), - effectView.leadingAnchor.constraint(equalTo: leadingAnchor), - effectView.bottomAnchor.constraint(equalTo: bottomAnchor), - effectView.trailingAnchor.constraint(equalTo: trailingAnchor), - ]) + glassTopConstraint = effectView.topAnchor.constraint( + equalTo: topAnchor, + constant: -themeFrameView.safeAreaInsets.top + ) + if let glassTopConstraint { + NSLayoutConstraint.activate([ + glassTopConstraint, + effectView.leadingAnchor.constraint(equalTo: leadingAnchor), + effectView.bottomAnchor.constraint(equalTo: bottomAnchor), + effectView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + } glassEffectView = effectView return effectView } @@ -98,6 +112,7 @@ private extension TerminalViewContainer { guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { glassEffectView?.removeFromSuperview() glassEffectView = nil + glassTopConstraint = nil return } guard let effectView = addGlassEffectViewIfNeeded() else { @@ -120,6 +135,17 @@ private extension TerminalViewContainer { #endif // compiler(>=6.2) } + func updateGlassEffectTopInsetIfNeeded() { +#if compiler(>=6.2) + guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else { + return + } + guard glassEffectView != nil else { return } + guard let themeFrameView = window?.contentView?.superview else { return } + glassTopConstraint?.constant = -themeFrameView.safeAreaInsets.top +#endif // compiler(>=6.2) + } + struct DerivedConfig: Equatable { var backgroundOpacity: Double = 0 var backgroundBlur: Ghostty.Config.BackgroundBlur -- 2.51.2 From f1bed9dd6a4a44aedbf634a6e7627cdb6d1b2366 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 29 Dec 2025 22:24:37 +0100 Subject: [PATCH 125/349] Inline trivial method --- dist/linux/ghostty_nautilus.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index 7e6c6e302..965e8107e 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -21,13 +21,10 @@ from gi.repository import Nautilus, GObject, Gio class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): - def _open_terminal(self, path): - cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false'] - Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) - def _menu_item_activated(self, _menu, paths): for path in paths: - self._open_terminal(path) + cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false'] + Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) def _make_item(self, name, paths): item = Nautilus.MenuItem(name=name, label='Open in Ghostty', -- 2.51.2 From 09d6a1ee2ee8c79f0690d271935d201cf4694ac9 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 29 Dec 2025 22:25:33 +0100 Subject: [PATCH 126/349] Lift functions out of class Neither of these used self. --- dist/linux/ghostty_nautilus.py | 48 ++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index 965e8107e..e993d0aaa 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -20,37 +20,39 @@ from gi.repository import Nautilus, GObject, Gio -class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): - def _menu_item_activated(self, _menu, paths): - for path in paths: - cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false'] - Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) +def open_in_ghostty_activated(_menu, paths): + for path in paths: + cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false'] + Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE) + + +def get_paths_to_open(files): + paths = [] + for file in files: + location = file.get_location() if file.is_directory() else file.get_parent_location() + path = location.get_path() + if path and path not in paths: + paths.append(path) + if 10 < len(paths): + # Let's not open anything if the user selected a lot of directories, + # to avoid accidentally spamming their desktop with dozends of + # new windows or tabs. Ten is a totally arbitrary limit :) + return [] + else: + return paths + +class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): def _make_item(self, name, paths): item = Nautilus.MenuItem(name=name, label='Open in Ghostty', icon='com.mitchellh.ghostty') - item.connect('activate', self._menu_item_activated, paths) + item.connect('activate', open_in_ghostty_activated, paths) return item - def _paths_to_open(self, files): - paths = [] - for file in files: - location = file.get_location() if file.is_directory() else file.get_parent_location() - path = location.get_path() - if path and path not in paths: - paths.append(path) - if 10 < len(paths): - # Let's not open anything if the user selected a lot of directories, - # to avoid accidentally spamming their desktop with dozends of - # new windows or tabs. Ten is a totally arbitrary limit :) - return [] - else: - return paths - def get_file_items(self, *args): # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files files = args[0] if len(args) == 1 else args[1] - paths = self._paths_to_open(files) + paths = get_paths_to_open(files) if paths: return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)] else: @@ -59,7 +61,7 @@ class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): def get_background_items(self, *args): # Nautilus 3.0 API passes args (window, file), 4.0 API just passes file file = args[0] if len(args) == 1 else args[1] - paths = self._paths_to_open([file]) + paths = get_paths_to_open([file]) if paths: return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)] else: -- 2.51.2 From dee093db579c1e28c4695351c0e16eda968291a2 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 29 Dec 2025 22:33:57 +0100 Subject: [PATCH 127/349] Extract duplicated code into single helper Extract duplicated code from the two different menu item hooks into a single helper function, and then inline _make_item, as it's only used once now. --- dist/linux/ghostty_nautilus.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index e993d0aaa..80ec84ac3 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -42,27 +42,24 @@ def get_paths_to_open(files): return paths -class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): - def _make_item(self, name, paths): +def get_items_for_files(name, files): + paths = get_paths_to_open(files) + if paths: item = Nautilus.MenuItem(name=name, label='Open in Ghostty', icon='com.mitchellh.ghostty') item.connect('activate', open_in_ghostty_activated, paths) - return item + return [item] + else: + return [] + +class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): def get_file_items(self, *args): # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files files = args[0] if len(args) == 1 else args[1] - paths = get_paths_to_open(files) - if paths: - return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)] - else: - return [] + return get_items_for_files('GhosttyNautilus::open_in_ghostty', files) def get_background_items(self, *args): # Nautilus 3.0 API passes args (window, file), 4.0 API just passes file file = args[0] if len(args) == 1 else args[1] - paths = get_paths_to_open([file]) - if paths: - return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)] - else: - return [] + return get_items_for_files('GhosttyNautilus::open_folder_in_ghostty', [file]) -- 2.51.2 From 5c35a4710d91ec5d1eadb84c13f395dbd2ed9387 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 29 Dec 2025 22:51:07 +0100 Subject: [PATCH 128/349] Rename class It's a menu provider not a single action. --- dist/linux/ghostty_nautilus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index 80ec84ac3..cd5011b9f 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -53,7 +53,7 @@ def get_items_for_files(name, files): return [] -class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider): +class GhosttyMenuProvider(GObject.GObject, Nautilus.MenuProvider): def get_file_items(self, *args): # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files files = args[0] if len(args) == 1 else args[1] -- 2.51.2 From ab4b54e2a45e949a857468a2d218f6ec10194d4a Mon Sep 17 00:00:00 2001 From: Sebastian Wiesner Date: Mon, 29 Dec 2025 23:07:01 +0100 Subject: [PATCH 129/349] Drop GNOME 42 compatibility GNOME 43 is from 2022-09, i.e. almost as old as Ghostty, so not longer worth supporting here. --- dist/linux/ghostty_nautilus.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dist/linux/ghostty_nautilus.py b/dist/linux/ghostty_nautilus.py index cd5011b9f..01202031c 100644 --- a/dist/linux/ghostty_nautilus.py +++ b/dist/linux/ghostty_nautilus.py @@ -54,12 +54,8 @@ def get_items_for_files(name, files): class GhosttyMenuProvider(GObject.GObject, Nautilus.MenuProvider): - def get_file_items(self, *args): - # Nautilus 3.0 API passes args (window, files), 4.0 API just passes files - files = args[0] if len(args) == 1 else args[1] + def get_file_items(self, files): return get_items_for_files('GhosttyNautilus::open_in_ghostty', files) - def get_background_items(self, *args): - # Nautilus 3.0 API passes args (window, file), 4.0 API just passes file - file = args[0] if len(args) == 1 else args[1] + def get_background_items(self, file): return get_items_for_files('GhosttyNautilus::open_folder_in_ghostty', [file]) -- 2.51.2 From 43c7277a6067c19bef16617e0217446b213d4297 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Dec 2025 13:06:41 -0800 Subject: [PATCH 130/349] macos: make surface grab handle visible in light mode --- macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index d8ff49163..f3ee80874 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -15,13 +15,13 @@ extension Ghostty { var body: some View { VStack(spacing: 0) { Rectangle() - .fill(Color.white.opacity(isHovering || isDragging ? 0.15 : 0)) + .fill(Color.primary.opacity(isHovering || isDragging ? 0.15 : 0)) .frame(height: handleHeight) .overlay(alignment: .center) { if isHovering || isDragging { Image(systemName: "ellipsis") .font(.system(size: 14, weight: .semibold)) - .foregroundColor(.white.opacity(0.5)) + .foregroundColor(.primary.opacity(0.5)) } } .contentShape(Rectangle()) -- 2.51.2 From c34bb5976a9d471bbe0a635b67dfc844c08ae650 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Dec 2025 13:09:19 -0800 Subject: [PATCH 131/349] macos: Ghostty.Command must copy string values We were previously storing the C struct which contained pointers into ephemeral memory that could cause segfaults later on. --- macos/Sources/Ghostty/Ghostty.Command.swift | 23 +++++++-------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Command.swift b/macos/Sources/Ghostty/Ghostty.Command.swift index 1479ae92d..797d469c5 100644 --- a/macos/Sources/Ghostty/Ghostty.Command.swift +++ b/macos/Sources/Ghostty/Ghostty.Command.swift @@ -3,28 +3,18 @@ import GhosttyKit extension Ghostty { /// `ghostty_command_s` struct Command: Sendable { - private let cValue: ghostty_command_s - /// The title of the command. - var title: String { - String(cString: cValue.title) - } + let title: String /// Human-friendly description of what this command will do. - var description: String { - String(cString: cValue.description) - } + let description: String /// The full action that must be performed to invoke this command. - var action: String { - String(cString: cValue.action) - } + let action: String /// Only the key portion of the action so you can compare action types, e.g. `goto_split` /// instead of `goto_split:left`. - var actionKey: String { - String(cString: cValue.action_key) - } + let actionKey: String /// True if this can be performed on this target. var isSupported: Bool { @@ -40,7 +30,10 @@ extension Ghostty { ] init(cValue: ghostty_command_s) { - self.cValue = cValue + self.title = String(cString: cValue.title) + self.description = String(cString: cValue.description) + self.action = String(cString: cValue.action) + self.actionKey = String(cString: cValue.action_key) } } } -- 2.51.2 From 0a648cddf83b59352b409db7504a484f83a3d7c0 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 30 Dec 2025 17:36:45 -0500 Subject: [PATCH 132/349] shaping: Use position.y directly for CoreText --- src/font/shaper/coretext.zig | 71 ++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 17d7801ff..09f123b4b 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -103,15 +103,9 @@ pub const Shaper = struct { } }; - const RunOffset = struct { - x: f64 = 0, - y: f64 = 0, - }; - const CellOffset = struct { cluster: u32 = 0, x: f64 = 0, - y: f64 = 0, }; /// Create a CoreFoundation Dictionary suitable for @@ -388,15 +382,15 @@ pub const Shaper = struct { const line = typesetter.createLine(.{ .location = 0, .length = 0 }); self.cf_release_pool.appendAssumeCapacity(line); - // This keeps track of the current offsets within a run. - var run_offset: RunOffset = .{}; + // This keeps track of the current x offset within a run. + var run_offset_x: f64 = 0.0; - // This keeps track of the current offsets within a cell. + // This keeps track of the current x offset and cluster for a cell. var cell_offset: CellOffset = .{}; // For debugging positions, turn this on: - //var start_index: usize = 0; - //var end_index: usize = 0; + //var run_offset_y: f64 = 0.0; + //var cell_offset_y: f64 = 0.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 @@ -450,39 +444,31 @@ pub const Shaper = struct { cell_offset = .{ .cluster = cluster, - .x = run_offset.x, - .y = run_offset.y, + .x = run_offset_x, }; // For debugging positions, turn this on: - // start_index = index; - // end_index = index; - //} else { - // if (index < start_index) { - // start_index = index; - // } - // if (index > end_index) { - // end_index = index; - // } + //cell_offset_y = run_offset_y; } // For debugging positions, turn this on: - //try self.debugPositions(alloc, run_offset, cell_offset, position, start_index, end_index, index); + //try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); const x_offset = position.x - cell_offset.x; - const y_offset = position.y - cell_offset.y; self.cell_buf.appendAssumeCapacity(.{ .x = @intCast(cluster), .x_offset = @intFromFloat(@round(x_offset)), - .y_offset = @intFromFloat(@round(y_offset)), + .y_offset = @intFromFloat(@round(position.y)), .glyph_index = glyph, }); // Add our advances to keep track of our run offsets. // Advances apply to the NEXT cell. - run_offset.x += advance.width; - run_offset.y += advance.height; + run_offset_x += advance.width; + + // For debugging positions, turn this on: + //run_offset_y += advance.height; } } @@ -655,33 +641,38 @@ pub const Shaper = struct { fn debugPositions( self: *Shaper, alloc: Allocator, - run_offset: RunOffset, + run_offset_x: f64, + run_offset_y: f64, cell_offset: CellOffset, + cell_offset_y: f64, position: macos.graphics.Point, - start_index: usize, - end_index: usize, index: usize, ) !void { const state = &self.run_state; const x_offset = position.x - cell_offset.x; - const y_offset = position.y - cell_offset.y; - const advance_x_offset = run_offset.x - cell_offset.x; - const advance_y_offset = run_offset.y - cell_offset.y; + const advance_x_offset = run_offset_x - cell_offset.x; + const advance_y_offset = run_offset_y - cell_offset_y; const x_offset_diff = x_offset - advance_x_offset; - const y_offset_diff = y_offset - advance_y_offset; + const y_offset_diff = position.y - advance_y_offset; if (@abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001) { var allocating = std.Io.Writer.Allocating.init(alloc); const writer = &allocating.writer; - const codepoints = state.codepoints.items[start_index .. end_index + 1]; + const codepoints = state.codepoints.items; for (codepoints) |cp| { - if (cp.codepoint == 0) continue; // Skip surrogate pair padding - try writer.print("\\u{{{x}}}", .{cp.codepoint}); + if (cp.cluster == cell_offset.cluster and + cp.codepoint != 0 // Skip surrogate pair padding + ) { + try writer.print("\\u{{{x}}}", .{cp.codepoint}); + } } try writer.writeAll(" โ†’ "); for (codepoints) |cp| { - if (cp.codepoint == 0) continue; // Skip surrogate pair padding - try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); + if (cp.cluster == cell_offset.cluster and + cp.codepoint != 0 // Skip surrogate pair padding + ) { + try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); + } } const formatted_cps = try allocating.toOwnedSlice(); @@ -698,7 +689,7 @@ pub const Shaper = struct { log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) current cp={x}, cps={s}", .{ cell_offset.cluster, x_offset, - y_offset, + position.y, advance_x_offset, advance_y_offset, x_offset_diff, -- 2.51.2 From 3e399a3d3506392f3559f406c1241bc4bc7882fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Dec 2025 14:12:15 -0800 Subject: [PATCH 133/349] macos: detect surface tab bar hovers and focus them --- .../Window Styles/TerminalWindow.swift | 27 +------ .../TitlebarTabsTahoeTerminalWindow.swift | 14 ++-- .../Surface View/SurfaceDragSource.swift | 78 +++++++++++++++++++ .../Extensions/NSWindow+Extension.swift | 56 +++++++++++-- 4 files changed, 136 insertions(+), 39 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 9debd2cb3..501ac0e67 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -194,7 +194,7 @@ class TerminalWindow: NSWindow { // Its possible we miss the accessory titlebar call so we check again // whenever the window becomes main. Both of these are idempotent. - if hasTabBar { + if tabBarView != nil { tabBarDidAppear() } else { tabBarDidDisappear() @@ -243,31 +243,6 @@ class TerminalWindow: NSWindow { /// added. static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") - func findTitlebarView() -> NSView? { - // Find our tab bar. If it doesn't exist we don't do anything. - // - // 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 nil } - let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) { - themeFrameView.value(forKey: "titlebarView") as? NSView - } else { - NSView?.none - } - return titlebarView - } - - func findTabBar() -> NSView? { - findTitlebarView()?.firstDescendant(withClassName: "NSTabBar") - } - - /// Returns true if there is a tab bar visible on this window. - var hasTabBar: Bool { - findTabBar() != nil - } - var hasMoreThanOneTabs: Bool { /// accessing ``tabGroup?.windows`` here /// will cause other edge cases, be careful diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index b18fff291..918191522 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -85,7 +85,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool return } - guard let tabBarView = findTabBar() else { + guard let tabBarView else { super.sendEvent(event) return } @@ -176,8 +176,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard tabBarObserver == nil else { return } guard - let titlebarView = findTitlebarView(), - let tabBar = findTabBar() + let titlebarView, + let tabBarView = self.tabBarView else { return } // View model updates must happen on their own ticks. @@ -186,13 +186,13 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool } // Find our clip view - guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } + guard let clipView = tabBarView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] 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 + tabBarView.frame.size.height = newTabButton.frame.width // The container is the view that we'll constrain our tab bar within. let container = toolbarView @@ -228,10 +228,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // other events occur, the tab bar can resize and clear our constraints. When this // happens, we need to remove our custom constraints and re-apply them once the // tab bar has proper dimensions again to avoid constraint conflicts. - tabBar.postsFrameChangedNotifications = true + tabBarView.postsFrameChangedNotifications = true tabBarObserver = NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, - object: tabBar, + object: tabBarView, queue: .main ) { [weak self] _ in guard let self else { return } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 21416ac75..77127583c 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -214,6 +214,9 @@ extension Ghostty { movedTo screenPoint: NSPoint ) { NSCursor.closedHand.set() + + // Handle hovering over a tab bar. + detectTabBarHover(at: screenPoint) } func draggingSession( @@ -226,6 +229,8 @@ extension Ghostty { self.escapeMonitor = nil } + hoveredTabState = nil + if operation == [] && !dragCancelledByEscape { let endsInWindow = NSApplication.shared.windows.contains { window in window.isVisible && window.frame.contains(screenPoint) @@ -242,6 +247,79 @@ extension Ghostty { isTracking = false onDragStateChanged?(false) } + + // MARK: Hovered Native Tabs + + /// The currently hovered tab, tracked to detect when hover changes. + private var hoveredTabState: HoveredTabState? + + /// This detects if the drag hover is over a native tab bar in our app. If it is then + /// we start a timer to focus that tab if the drag remains over it. + private func detectTabBarHover(at screenPoint: NSPoint) { + for window in NSApplication.shared.windows { + if let index = window.tabIndex(atScreenPoint: screenPoint) { + let state: HoveredTabState = .init(window: window, index: index) + guard hoveredTabState != state else { + // We already are tracking this. + return + } + + // Stop our prior state since we've changed tabs. + hoveredTabState = nil + + // Grab our window and ensure that it isn't the key window. If + // it is already key then the tab is already focused so we don't + // need to do it again. + guard let targetWindow = window.tabbedWindows?[safe: index] else { return } + guard !targetWindow.isKeyWindow else { return } + + // Start our timer to focus it and store our state. + state.startTimer() + hoveredTabState = state + return + } + } + + hoveredTabState = nil + } + + fileprivate class HoveredTabState: Equatable { + let window: NSWindow + let index: Int + var focusTimer: Timer? + + /// Duration to hover over a tab before it becomes focused. + private static let hoverDelay: TimeInterval = 0.5 + + init(window: NSWindow, index: Int) { + self.window = window + self.index = index + } + + deinit { + focusTimer?.invalidate() + } + + func startTimer() { + focusTimer?.invalidate() + focusTimer = Timer.scheduledTimer( + withTimeInterval: Self.hoverDelay, + repeats: false) { [weak self] _ in + self?.focus() + } + } + + private func focus() { + guard let targetWindow = window.tabbedWindows?[safe: index] else { return } + targetWindow.makeKeyAndOrderFront(nil) + } + + static func == (lhs: HoveredTabState, rhs: HoveredTabState) -> Bool { + return + lhs.window == rhs.window && + lhs.index == rhs.index + } + } } } diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index f8df803db..5d1831f26 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -10,12 +10,6 @@ extension NSWindow { return CGWindowID(windowNumber) } - /// True if this is the first window in the tab group. - var isFirstWindowInTabGroup: Bool { - guard let firstWindow = tabGroup?.windows.first else { return true } - return firstWindow === self - } - /// Adjusts the window frame if necessary to ensure the window remains visible on screen. /// This constrains both the size (to not exceed the screen) and the origin (to keep the window on screen). func constrainToScreen() { @@ -36,3 +30,53 @@ extension NSWindow { } } } + +// MARK: Native Tabbing + +extension NSWindow { + /// True if this is the first window in the tab group. + var isFirstWindowInTabGroup: Bool { + guard let firstWindow = tabGroup?.windows.first else { return true } + return firstWindow === self + } +} + +/// Native tabbing private API usage. :( +extension NSWindow { + var titlebarView: NSView? { + // 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 nil } + guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil } + return themeFrameView.value(forKey: "titlebarView") as? NSView + } + + /// Returns the [private] NSTabBar view, if it exists. + var tabBarView: NSView? { + titlebarView?.firstDescendant(withClassName: "NSTabBar") + } + + /// Returns the index of the tab button at the given screen point, if any. + func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? { + guard let tabBarView else { return nil } + let locationInWindow = convertPoint(fromScreen: screenPoint) + let locationInTabBar = tabBarView.convert(locationInWindow, from: nil) + guard tabBarView.bounds.contains(locationInTabBar) else { return nil } + + // Find all tab buttons and sort by x position to get visual order. + // The view hierarchy order doesn't match the visual tab order. + let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton") + .sorted { $0.frame.origin.x < $1.frame.origin.x } + + for (index, tabItemView) in tabItemViews.enumerated() { + let locationInTab = tabItemView.convert(locationInWindow, from: nil) + if tabItemView.bounds.contains(locationInTab) { + return index + } + } + + return nil + } +} -- 2.51.2 From d4ba0fa27ee2aedb9081c3dafd1424ff339af6be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Dec 2025 15:00:32 -0800 Subject: [PATCH 134/349] macos: last surface should close tab immediately not window --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 2 +- macos/Sources/Features/Terminal/TerminalController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e4f700170..fb86ce8f7 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -952,7 +952,7 @@ class BaseTerminalController: NSWindowController, // controller is a TerminalController this is easy because it has a way // to do this. if let c = sourceController as? TerminalController { - c.closeWindowImmediately() + c.closeTabImmediately() } else { // Not a TerminalController so we always undo into a new window. _ = TerminalController.newWindow( diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 26ca8f70e..abaedbe41 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -614,7 +614,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeWindow(nil) } - private func closeTabImmediately(registerRedo: Bool = true) { + func closeTabImmediately(registerRedo: Bool = true) { guard let window = window else { return } guard let tabGroup = window.tabGroup, tabGroup.windows.count > 1 else { -- 2.51.2 From 18abcaa7974db77c108dfbbfcd5dda785831a129 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 06:15:01 -0800 Subject: [PATCH 135/349] macos: remove tab hover event, seems native handles it --- .../Surface View/SurfaceDragSource.swift | 78 ------------------- 1 file changed, 78 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 77127583c..21416ac75 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -214,9 +214,6 @@ extension Ghostty { movedTo screenPoint: NSPoint ) { NSCursor.closedHand.set() - - // Handle hovering over a tab bar. - detectTabBarHover(at: screenPoint) } func draggingSession( @@ -229,8 +226,6 @@ extension Ghostty { self.escapeMonitor = nil } - hoveredTabState = nil - if operation == [] && !dragCancelledByEscape { let endsInWindow = NSApplication.shared.windows.contains { window in window.isVisible && window.frame.contains(screenPoint) @@ -247,79 +242,6 @@ extension Ghostty { isTracking = false onDragStateChanged?(false) } - - // MARK: Hovered Native Tabs - - /// The currently hovered tab, tracked to detect when hover changes. - private var hoveredTabState: HoveredTabState? - - /// This detects if the drag hover is over a native tab bar in our app. If it is then - /// we start a timer to focus that tab if the drag remains over it. - private func detectTabBarHover(at screenPoint: NSPoint) { - for window in NSApplication.shared.windows { - if let index = window.tabIndex(atScreenPoint: screenPoint) { - let state: HoveredTabState = .init(window: window, index: index) - guard hoveredTabState != state else { - // We already are tracking this. - return - } - - // Stop our prior state since we've changed tabs. - hoveredTabState = nil - - // Grab our window and ensure that it isn't the key window. If - // it is already key then the tab is already focused so we don't - // need to do it again. - guard let targetWindow = window.tabbedWindows?[safe: index] else { return } - guard !targetWindow.isKeyWindow else { return } - - // Start our timer to focus it and store our state. - state.startTimer() - hoveredTabState = state - return - } - } - - hoveredTabState = nil - } - - fileprivate class HoveredTabState: Equatable { - let window: NSWindow - let index: Int - var focusTimer: Timer? - - /// Duration to hover over a tab before it becomes focused. - private static let hoverDelay: TimeInterval = 0.5 - - init(window: NSWindow, index: Int) { - self.window = window - self.index = index - } - - deinit { - focusTimer?.invalidate() - } - - func startTimer() { - focusTimer?.invalidate() - focusTimer = Timer.scheduledTimer( - withTimeInterval: Self.hoverDelay, - repeats: false) { [weak self] _ in - self?.focus() - } - } - - private func focus() { - guard let targetWindow = window.tabbedWindows?[safe: index] else { return } - targetWindow.makeKeyAndOrderFront(nil) - } - - static func == (lhs: HoveredTabState, rhs: HoveredTabState) -> Bool { - return - lhs.window == rhs.window && - lhs.index == rhs.index - } - } } } -- 2.51.2 From 3bd898603a4487c56d17a11c1835a3ef7151785f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 09:34:57 -0800 Subject: [PATCH 136/349] pkg/dcimgui: DearBindings-based Imgui support --- pkg/dcimgui/build.zig | 157 ++++++++++++++++++++++++++++++++++++++ pkg/dcimgui/build.zig.zon | 26 +++++++ pkg/dcimgui/main.zig | 26 +++++++ 3 files changed, 209 insertions(+) create mode 100644 pkg/dcimgui/build.zig create mode 100644 pkg/dcimgui/build.zig.zon create mode 100644 pkg/dcimgui/main.zig diff --git a/pkg/dcimgui/build.zig b/pkg/dcimgui/build.zig new file mode 100644 index 000000000..2d1594cba --- /dev/null +++ b/pkg/dcimgui/build.zig @@ -0,0 +1,157 @@ +const std = @import("std"); +const NativeTargetInfo = std.zig.system.NativeTargetInfo; + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const backend_opengl3 = b.option(bool, "backend-opengl3", "OpenGL3 backend") orelse false; + const backend_metal = b.option(bool, "backend-metal", "Metal backend") orelse false; + const backend_osx = b.option(bool, "backend-osx", "OSX backend") orelse false; + + // Build options + const options = b.addOptions(); + options.addOption(bool, "backend_opengl3", backend_opengl3); + options.addOption(bool, "backend_metal", backend_metal); + options.addOption(bool, "backend_osx", backend_osx); + + // Main static lib + const lib = b.addLibrary(.{ + .name = "dcimgui", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), + .linkage = .static, + }); + lib.linkLibC(); + lib.linkLibCpp(); + b.installArtifact(lib); + + // Zig module + const mod = b.addModule("dcimgui", .{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }); + mod.addOptions("build_options", options); + mod.linkLibrary(lib); + + // We need to add proper Apple SDKs to find stdlib headers + if (target.result.os.tag.isDarwin()) { + if (!target.query.isNative()) { + try @import("apple_sdk").addPaths(b, lib); + } + } + + // Flags for C compilation, common to all. + var flags: std.ArrayList([]const u8) = .empty; + defer flags.deinit(b.allocator); + try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_USE_WCHAR32=1", + "-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1", + }); + if (target.result.os.tag == .windows) { + try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", + }); + } else { + try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_IMPL_API=extern\t\"C\"", + }); + } + if (target.result.os.tag == .freebsd) { + try flags.append(b.allocator, "-fPIC"); + } + + // Add the core Dear Imgui source files + if (b.lazyDependency("imgui", .{})) |upstream| { + lib.addIncludePath(upstream.path("")); + lib.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "imgui_demo.cpp", + "imgui_draw.cpp", + "imgui_tables.cpp", + "imgui_widgets.cpp", + "imgui.cpp", + }, + .flags = flags.items, + }); + + lib.installHeadersDirectory( + upstream.path(""), + "", + .{ .include_extensions = &.{".h"} }, + ); + + if (backend_metal) { + lib.addCSourceFiles(.{ + .root = upstream.path("backends"), + .files = &.{"imgui_impl_metal.mm"}, + .flags = flags.items, + }); + lib.installHeadersDirectory( + upstream.path("backends"), + "", + .{ .include_extensions = &.{"imgui_impl_metal.h"} }, + ); + } + if (backend_osx) { + lib.addCSourceFiles(.{ + .root = upstream.path("backends"), + .files = &.{"imgui_impl_osx.mm"}, + .flags = flags.items, + }); + lib.installHeadersDirectory( + upstream.path("backends"), + "", + .{ .include_extensions = &.{"imgui_impl_osx.h"} }, + ); + } + if (backend_opengl3) { + lib.addCSourceFiles(.{ + .root = upstream.path("backends"), + .files = &.{"imgui_impl_opengl3.cpp"}, + .flags = flags.items, + }); + lib.installHeadersDirectory( + upstream.path("backends"), + "", + .{ .include_extensions = &.{"imgui_impl_opengl3.h"} }, + ); + } + } + + // Add the C bindings + if (b.lazyDependency("bindings", .{})) |upstream| { + lib.addIncludePath(upstream.path("")); + lib.addCSourceFiles(.{ + .root = upstream.path(""), + .files = &.{ + "dcimgui.cpp", + "dcimgui_internal.cpp", + }, + .flags = flags.items, + }); + + lib.installHeadersDirectory( + upstream.path(""), + "", + .{ .include_extensions = &.{".h"} }, + ); + } + + const test_exe = b.addTest(.{ + .name = "test", + .root_module = b.createModule(.{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + }), + }); + test_exe.root_module.addOptions("build_options", options); + test_exe.linkLibrary(lib); + const tests_run = b.addRunArtifact(test_exe); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&tests_run.step); +} diff --git a/pkg/dcimgui/build.zig.zon b/pkg/dcimgui/build.zig.zon new file mode 100644 index 000000000..95d0120e1 --- /dev/null +++ b/pkg/dcimgui/build.zig.zon @@ -0,0 +1,26 @@ +.{ + .name = .dcimgui, + .version = "1.92.5", // -docking branch + .fingerprint = 0x1a25797442c6324f, + .paths = .{""}, + .dependencies = .{ + // The bindings and imgui versions below must match exactly. + + .bindings = .{ + // https://github.com/dearimgui/dear_bindings + .url = "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz", + .hash = "N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr", + .lazy = true, + }, + + .imgui = .{ + // https://github.com/ocornut/imgui + .url = "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + .hash = "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI", + .lazy = true, + }, + + .apple_sdk = .{ .path = "../apple-sdk" }, + .freetype = .{ .path = "../freetype" }, + }, +} diff --git a/pkg/dcimgui/main.zig b/pkg/dcimgui/main.zig new file mode 100644 index 000000000..c73a74515 --- /dev/null +++ b/pkg/dcimgui/main.zig @@ -0,0 +1,26 @@ +pub const build_options = @import("build_options"); + +pub const c = @cImport({ + @cInclude("dcimgui.h"); +}); + +// OpenGL3 backend +pub extern fn ImGui_ImplOpenGL3_Init(glsl_version: ?[*:0]const u8) callconv(.c) bool; +pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void; +pub extern fn ImGui_ImplOpenGL3_RenderDrawData(draw_data: *c.ImDrawData) callconv(.c) void; + +// Metal backend +pub extern fn ImGui_ImplMetal_Init(device: *anyopaque) callconv(.c) bool; +pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplMetal_NewFrame(render_pass_descriptor: *anyopaque) callconv(.c) void; +pub extern fn ImGui_ImplMetal_RenderDrawData(draw_data: *c.ImDrawData, command_buffer: *anyopaque, command_encoder: *anyopaque) callconv(.c) void; + +// OSX +pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool; +pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void; + +test { + _ = c; +} -- 2.51.2 From 978400b0b0fa0d63b010f35c5cb76002e37d41e4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 10:10:03 -0800 Subject: [PATCH 137/349] replace cimgui with dcimgui --- build.zig.zon | 1 + pkg/dcimgui/main.zig | 7 + src/apprt/embedded.zig | 63 ++- src/apprt/gtk/class/imgui_widget.zig | 41 +- src/build/SharedDeps.zig | 12 +- src/input/key.zig | 4 +- src/inspector/Inspector.zig | 555 +++++++++++++-------------- src/inspector/cell.zig | 90 +++-- src/inspector/cursor.zig | 74 ++-- src/inspector/key.zig | 104 +++-- src/inspector/page.zig | 132 +++---- src/inspector/termio.zig | 11 +- 12 files changed, 524 insertions(+), 570 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 191c1bec9..eff6dc12e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -64,6 +64,7 @@ // C libs .cimgui = .{ .path = "./pkg/cimgui", .lazy = true }, + .dcimgui = .{ .path = "./pkg/dcimgui", .lazy = true }, .fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true }, .freetype = .{ .path = "./pkg/freetype", .lazy = true }, .gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true }, diff --git a/pkg/dcimgui/main.zig b/pkg/dcimgui/main.zig index c73a74515..7ea135cdb 100644 --- a/pkg/dcimgui/main.zig +++ b/pkg/dcimgui/main.zig @@ -21,6 +21,13 @@ pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool; pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void; pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void; +// Internal API functions from dcimgui_internal.h +// We declare these manually because the internal header contains bitfields +// that Zig's cImport cannot translate. +pub extern fn ImGui_DockBuilderDockWindow(window_name: [*:0]const u8, node_id: c.ImGuiID) callconv(.c) void; +pub extern fn ImGui_DockBuilderSplitNode(node_id: c.ImGuiID, split_dir: c.ImGuiDir, size_ratio_for_node_at_dir: f32, out_id_at_dir: *c.ImGuiID, out_id_at_opposite_dir: *c.ImGuiID) callconv(.c) c.ImGuiID; +pub extern fn ImGui_DockBuilderFinish(node_id: c.ImGuiID) callconv(.c) void; + test { _ = c; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a1b6a6e9b..c6c4d2ab8 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -947,7 +947,7 @@ pub const Surface = struct { /// Inspector is the state required for the terminal inspector. A terminal /// inspector is 1:1 with a Surface. pub const Inspector = struct { - const cimgui = @import("cimgui"); + const cimgui = @import("dcimgui"); surface: *Surface, ig_ctx: *cimgui.c.ImGuiContext, @@ -968,10 +968,10 @@ pub const Inspector = struct { }; pub fn init(surface: *Surface) !Inspector { - const ig_ctx = cimgui.c.igCreateContext(null) orelse return error.OutOfMemory; - errdefer cimgui.c.igDestroyContext(ig_ctx); - cimgui.c.igSetCurrentContext(ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const ig_ctx = cimgui.c.ImGui_CreateContext(null) orelse return error.OutOfMemory; + errdefer cimgui.c.ImGui_DestroyContext(ig_ctx); + cimgui.c.ImGui_SetCurrentContext(ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); io.BackendPlatformName = "ghostty_embedded"; // Setup our core inspector @@ -988,9 +988,9 @@ pub const Inspector = struct { pub fn deinit(self: *Inspector) void { self.surface.core_surface.deactivateInspector(); - cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); if (self.backend) |v| v.deinit(); - cimgui.c.igDestroyContext(self.ig_ctx); + cimgui.c.ImGui_DestroyContext(self.ig_ctx); } /// Queue a render for the next frame. @@ -1001,7 +1001,7 @@ pub const Inspector = struct { /// Initialize the inspector for a metal backend. pub fn initMetal(self: *Inspector, device: objc.Object) bool { defer device.msgSend(void, objc.sel("release"), .{}); - cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); if (self.backend) |v| { v.deinit(); @@ -1036,7 +1036,7 @@ pub const Inspector = struct { for (0..2) |_| { cimgui.ImGui_ImplMetal_NewFrame(desc.value); try self.newFrame(); - cimgui.c.igNewFrame(); + cimgui.c.ImGui_NewFrame(); // Build our UI render: { @@ -1046,7 +1046,7 @@ pub const Inspector = struct { } // Render - cimgui.c.igRender(); + cimgui.c.ImGui_Render(); } // MTLRenderCommandEncoder @@ -1057,7 +1057,7 @@ pub const Inspector = struct { ); defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); cimgui.ImGui_ImplMetal_RenderDrawData( - cimgui.c.igGetDrawData(), + cimgui.c.ImGui_GetDrawData(), command_buffer.value, encoder.value, ); @@ -1065,22 +1065,21 @@ pub const Inspector = struct { pub fn updateContentScale(self: *Inspector, x: f64, y: f64) void { _ = y; - cimgui.c.igSetCurrentContext(self.ig_ctx); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); // Cache our scale because we use it for cursor position calculations. self.content_scale = x; // Setup a new style and scale it appropriately. - const style = cimgui.c.ImGuiStyle_ImGuiStyle(); - defer cimgui.c.ImGuiStyle_destroy(style); - cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatCast(x)); - const active_style = cimgui.c.igGetStyle(); - active_style.* = style.*; + var style: cimgui.c.ImGuiStyle = .{}; + cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatCast(x)); + const active_style = cimgui.c.ImGui_GetStyle(); + active_style.* = style; } pub fn updateSize(self: *Inspector, width: u32, height: u32) void { - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) }; } @@ -1093,8 +1092,8 @@ pub const Inspector = struct { _ = mods; self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const imgui_button = switch (button) { .left => cimgui.c.ImGuiMouseButton_Left, @@ -1115,8 +1114,8 @@ pub const Inspector = struct { _ = mods; self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddMouseWheelEvent( io, @floatCast(xoff), @@ -1126,8 +1125,8 @@ pub const Inspector = struct { pub fn cursorPosCallback(self: *Inspector, x: f64, y: f64) void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddMousePosEvent( io, @floatCast(x * self.content_scale), @@ -1137,15 +1136,15 @@ pub const Inspector = struct { pub fn focusCallback(self: *Inspector, focused: bool) void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, focused); } pub fn textCallback(self: *Inspector, text: [:0]const u8) void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, text.ptr); } @@ -1156,8 +1155,8 @@ pub const Inspector = struct { mods: input.Mods, ) !void { self.queueRender(); - cimgui.c.igSetCurrentContext(self.ig_ctx); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + cimgui.c.ImGui_SetCurrentContext(self.ig_ctx); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Update all our modifiers cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift); @@ -1176,7 +1175,7 @@ pub const Inspector = struct { } fn newFrame(self: *Inspector) !void { - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Determine our delta time const now = try std.time.Instant.now(); diff --git a/src/apprt/gtk/class/imgui_widget.zig b/src/apprt/gtk/class/imgui_widget.zig index ef1ca05c9..474efaa91 100644 --- a/src/apprt/gtk/class/imgui_widget.zig +++ b/src/apprt/gtk/class/imgui_widget.zig @@ -126,7 +126,7 @@ pub const ImguiWidget = extern struct { log.warn("Dear ImGui context not initialized", .{}); return error.ContextNotInitialized; }; - cimgui.c.igSetCurrentContext(ig_context); + cimgui.c.ImGui_SetCurrentContext(ig_context); } /// Initialize the frame. Expects that the context is already current. @@ -137,7 +137,7 @@ pub const ImguiWidget = extern struct { const priv = self.private(); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Determine our delta time const now = std.time.Instant.now() catch unreachable; @@ -163,7 +163,7 @@ pub const ImguiWidget = extern struct { self.setCurrentContext() catch return false; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const mods = key.translateMods(gtk_mods); cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift); @@ -219,14 +219,14 @@ pub const ImguiWidget = extern struct { return; } - priv.ig_context = cimgui.c.igCreateContext(null) orelse { + priv.ig_context = cimgui.c.ImGui_CreateContext(null) orelse { log.warn("unable to initialize Dear ImGui context", .{}); return; }; self.setCurrentContext() catch return; // Setup some basic config - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); io.BackendPlatformName = "ghostty_gtk"; // Realize means that our OpenGL context is ready, so we can now @@ -247,7 +247,7 @@ pub const ImguiWidget = extern struct { /// Handle a request to resize the GLArea fn glAreaResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *Self) callconv(.c) void { self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const scale_factor = area.as(gtk.Widget).getScaleFactor(); // Our display size is always unscaled. We'll do the scaling in the @@ -256,11 +256,10 @@ pub const ImguiWidget = extern struct { io.DisplayFramebufferScale = .{ .x = 1, .y = 1 }; // Setup a new style and scale it appropriately. - const style = cimgui.c.ImGuiStyle_ImGuiStyle(); - defer cimgui.c.ImGuiStyle_destroy(style); - cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor)); - const active_style = cimgui.c.igGetStyle(); - active_style.* = style.*; + var style: cimgui.c.ImGuiStyle = .{}; + cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatFromInt(scale_factor)); + const active_style = cimgui.c.ImGui_GetStyle(); + active_style.* = style; } /// Handle a request to render the contents of our GLArea @@ -273,33 +272,33 @@ pub const ImguiWidget = extern struct { for (0..2) |_| { cimgui.ImGui_ImplOpenGL3_NewFrame(); self.newFrame(); - cimgui.c.igNewFrame(); + cimgui.c.ImGui_NewFrame(); // Call the virtual method to draw the UI. self.render(); // Render - cimgui.c.igRender(); + cimgui.c.ImGui_Render(); } // OpenGL final render gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0); gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData()); + cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.ImGui_GetDrawData()); return @intFromBool(true); } fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, true); self.queueRender(); } fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void { self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddFocusEvent(io, false); self.queueRender(); } @@ -345,7 +344,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton(); if (translateMouseButton(gdk_button)) |button| { cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true); @@ -361,7 +360,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton(); if (translateMouseButton(gdk_button)) |button| { cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false); @@ -376,7 +375,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); const scale_factor = self.getScaleFactor(); cimgui.c.ImGuiIO_AddMousePosEvent( io, @@ -393,7 +392,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) c_int { self.queueRender(); self.setCurrentContext() catch return @intFromBool(false); - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddMouseWheelEvent( io, @floatCast(x), @@ -409,7 +408,7 @@ pub const ImguiWidget = extern struct { ) callconv(.c) void { self.queueRender(); self.setCurrentContext() catch return; - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes); } diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 5e2cd40b9..f96a269bd 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -479,15 +479,17 @@ pub fn add( } // cimgui - if (b.lazyDependency("cimgui", .{ + if (b.lazyDependency("dcimgui", .{ .target = target, .optimize = optimize, - })) |cimgui_dep| { - step.root_module.addImport("cimgui", cimgui_dep.module("cimgui")); - step.linkLibrary(cimgui_dep.artifact("cimgui")); + .@"backend-metal" = target.result.os.tag.isDarwin(), + .@"backend-osx" = target.result.os.tag == .macos, + })) |dep| { + step.root_module.addImport("dcimgui", dep.module("dcimgui")); + step.linkLibrary(dep.artifact("dcimgui")); try static_libs.append( b.allocator, - cimgui_dep.artifact("cimgui").getEmittedBin(), + dep.artifact("dcimgui").getEmittedBin(), ); } diff --git a/src/input/key.zig b/src/input/key.zig index 54c7491ae..6445871eb 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const OptionAsAlt = @import("config.zig").OptionAsAlt; /// A generic key input event. This is the information that is necessary @@ -696,7 +696,7 @@ pub const Key = enum(c_int) { } /// Returns the cimgui key constant for this key. - pub fn imguiKey(self: Key) ?c_uint { + pub fn imguiKey(self: Key) ?c_int { return switch (self) { .key_a => cimgui.c.ImGuiKey_A, .key_b => cimgui.c.ImGuiKey_B, diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index dc498b58d..b1ac473b8 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -7,7 +7,7 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const builtin = @import("builtin"); -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const Surface = @import("../Surface.zig"); const font = @import("../font/main.zig"); const input = @import("../input.zig"); @@ -126,7 +126,7 @@ const CellInspect = union(enum) { /// Setup the ImGui state. This requires an ImGui context to be set. pub fn setup() void { - const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); + const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO(); // Enable docking, which we use heavily for the UI. io.ConfigFlags |= cimgui.c.ImGuiConfigFlags_DockingEnable; @@ -144,15 +144,14 @@ pub fn setup() void { // This is currently hardcoded to a 2x content scale. const font_size = 16 * 2; - const font_config: *cimgui.c.ImFontConfig = cimgui.c.ImFontConfig_ImFontConfig(); - defer cimgui.c.ImFontConfig_destroy(font_config); + var font_config: cimgui.c.ImFontConfig = .{}; font_config.FontDataOwnedByAtlas = false; _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( io.Fonts, @ptrCast(@constCast(font.embedded.regular)), font.embedded.regular.len, font_size, - font_config, + &font_config, null, ); } @@ -221,11 +220,7 @@ pub fn recordPtyRead(self: *Inspector, data: []const u8) !void { /// Render the frame. pub fn render(self: *Inspector) void { - const dock_id = cimgui.c.igDockSpaceOverViewport( - cimgui.c.igGetMainViewport(), - cimgui.c.ImGuiDockNodeFlags_None, - null, - ); + const dock_id = cimgui.c.ImGui_DockSpaceOverViewport(); // Render all of our data. We hold the mutex for this duration. This is // expensive but this is an initial implementation until it doesn't work @@ -245,7 +240,7 @@ pub fn render(self: *Inspector) void { // widgets and such. if (builtin.mode == .Debug) { var show: bool = true; - cimgui.c.igShowDemoWindow(&show); + cimgui.c.ImGui_ShowDemoWindow(&show); } // On first render we set up the layout. We can actually do this at @@ -261,7 +256,7 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { _ = self; // Our initial focus - cimgui.c.igSetWindowFocus_Str(window_screen); + cimgui.c.ImGui_SetWindowFocusStr(window_screen); // Setup our initial layout. const dock_id: struct { @@ -270,7 +265,7 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { } = dock_id: { var dock_id_left: cimgui.c.ImGuiID = undefined; var dock_id_right: cimgui.c.ImGuiID = undefined; - _ = cimgui.c.igDockBuilderSplitNode( + _ = cimgui.ImGui_DockBuilderSplitNode( dock_id_main, cimgui.c.ImGuiDir_Left, 0.7, @@ -284,20 +279,20 @@ fn setupLayout(self: *Inspector, dock_id_main: cimgui.c.ImGuiID) void { }; }; - cimgui.c.igDockBuilderDockWindow(window_cell, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_modes, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_keyboard, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_termio, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_screen, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_imgui_demo, dock_id.left); - cimgui.c.igDockBuilderDockWindow(window_size, dock_id.right); - cimgui.c.igDockBuilderFinish(dock_id_main); + cimgui.ImGui_DockBuilderDockWindow(window_cell, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_modes, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_keyboard, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_termio, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_screen, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_imgui_demo, dock_id.left); + cimgui.ImGui_DockBuilderDockWindow(window_size, dock_id.right); + cimgui.ImGui_DockBuilderFinish(dock_id_main); } fn renderScreenWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_screen, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -307,76 +302,70 @@ fn renderScreenWindow(self: *Inspector) void { const screen: *terminal.Screen = t.screens.active; { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_screen", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Active Screen"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Active Screen"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(t.screens.active_key).ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(t.screens.active_key).ptr); } } } - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Cursor", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_cursor", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); inspector.cursor.renderInTable( self.surface.renderer_state.terminal, &screen.cursor, ); } // table - cimgui.c.igTextDisabled("(Any styles not shown are not currently set)"); + cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)"); } // cursor - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Keyboard", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_keyboard", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const kitty_flags = screen.kitty_keyboard.current(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Mode"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Mode"); } { - _ = cimgui.c.igTableSetColumnIndex(1); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); const mode = if (kitty_flags.int() != 0) "kitty" else "legacy"; - cimgui.c.igText("%s", mode.ptr); + cimgui.c.ImGui_Text("%s", mode.ptr); } } @@ -386,15 +375,15 @@ fn renderScreenWindow(self: *Inspector) void { { const value = @field(kitty_flags, field.name); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); const name = std.fmt.comptimePrint("{s}", .{field.name}); - cimgui.c.igText("%s", name.ptr); + cimgui.c.ImGui_Text("%s", name.ptr); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%s", if (value) "true".ptr else "false".ptr, ); @@ -403,14 +392,14 @@ fn renderScreenWindow(self: *Inspector) void { } } else { { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Xterm modify keys"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Xterm modify keys"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%s", if (t.flags.modify_other_keys_2) "true".ptr else "false".ptr, ); @@ -420,143 +409,139 @@ fn renderScreenWindow(self: *Inspector) void { } // table } // keyboard - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Kitty Graphics", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) kitty_gfx: { if (!screen.kitty_images.enabled()) { - cimgui.c.igTextDisabled("(Kitty graphics are disabled)"); + cimgui.c.ImGui_TextDisabled("(Kitty graphics are disabled)"); break :kitty_gfx; } { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##kitty_graphics", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const kitty_images = &screen.kitty_images; { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Usage"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Usage"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes)); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_bytes, units.toKibiBytes(kitty_images.total_bytes)); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Limit"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Limit"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit)); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", kitty_images.total_limit, units.toKibiBytes(kitty_images.total_limit)); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Image Count"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Image Count"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", kitty_images.images.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", kitty_images.images.count()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Placement Count"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Placement Count"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", kitty_images.placements.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", kitty_images.placements.count()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Image Loading"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Image Loading"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", if (kitty_images.loading != null) "true".ptr else "false".ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", if (kitty_images.loading != null) "true".ptr else "false".ptr); } } } // table } // kitty graphics - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Internal Terminal State", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { const pages = &screen.pages; { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##terminal_state", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Usage"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Usage"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size)); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.page_size, units.toKibiBytes(pages.page_size)); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Limit"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Limit"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize())); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", pages.maxSize(), units.toKibiBytes(pages.maxSize())); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Viewport Location"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Viewport Location"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(pages.viewport).ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(pages.viewport).ptr); } } } // table // - if (cimgui.c.igCollapsingHeader_TreeNodeFlags( + if (cimgui.c.ImGui_CollapsingHeader( "Active Page", cimgui.c.ImGuiTreeNodeFlags_DefaultOpen, )) { @@ -569,28 +554,26 @@ fn renderScreenWindow(self: *Inspector) void { /// users to toggle them on and off. fn renderModesWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_modes, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, )) return; - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_modes", 3, cimgui.c.ImGuiTableFlags_SizingFixedFit | cimgui.c.ImGuiTableFlags_RowBg, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - _ = cimgui.c.igTableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_NoResize, 0, 0); - _ = cimgui.c.igTableSetupColumn("Number", cimgui.c.ImGuiTableColumnFlags_PreferSortAscending, 0, 0); - _ = cimgui.c.igTableSetupColumn("Name", cimgui.c.ImGuiTableColumnFlags_WidthStretch, 0, 0); - cimgui.c.igTableHeadersRow(); + cimgui.c.ImGui_TableSetupColumn("", cimgui.c.ImGuiTableColumnFlags_NoResize); + cimgui.c.ImGui_TableSetupColumn("Number", cimgui.c.ImGuiTableColumnFlags_PreferSortAscending); + cimgui.c.ImGui_TableSetupColumn("Name", cimgui.c.ImGuiTableColumnFlags_WidthStretch); + cimgui.c.ImGui_TableHeadersRow(); } const t = self.surface.renderer_state.terminal; @@ -598,59 +581,57 @@ fn renderModesWindow(self: *Inspector) void { @setEvalBranchQuota(6000); const tag: terminal.modes.ModeTag = @bitCast(@as(terminal.modes.ModeTag.Backing, field.value)); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); var value: bool = t.modes.get(@field(terminal.Mode, field.name)); - _ = cimgui.c.igCheckbox("", &value); + _ = cimgui.c.ImGui_Checkbox("", &value); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%s%d", if (tag.ansi) "" else "?", @as(u32, @intCast(tag.value)), ); } { - _ = cimgui.c.igTableSetColumnIndex(2); + _ = cimgui.c.ImGui_TableSetColumnIndex(2); const name = std.fmt.comptimePrint("{s}", .{field.name}); - cimgui.c.igText("%s", name.ptr); + cimgui.c.ImGui_Text("%s", name.ptr); } } } fn renderSizeWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_size, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, )) return; - cimgui.c.igSeparatorText("Dimensions"); + cimgui.c.ImGui_SeparatorText("Dimensions"); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_size", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); // Screen Size { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Screen Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Screen Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%dpx x %dpx", self.surface.size.screen.width, self.surface.size.screen.height, @@ -660,15 +641,15 @@ fn renderSizeWindow(self: *Inspector) void { // Grid Size { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grid Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grid Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); const grid_size = self.surface.size.grid(); - cimgui.c.igText( + cimgui.c.ImGui_Text( "%dc x %dr", grid_size.columns, grid_size.rows, @@ -678,14 +659,14 @@ fn renderSizeWindow(self: *Inspector) void { // Cell Size { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Cell Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Cell Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%dpx x %dpx", self.surface.size.cell.width, self.surface.size.cell.height, @@ -695,14 +676,14 @@ fn renderSizeWindow(self: *Inspector) void { // Padding { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Window Padding"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Window Padding"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "T=%d B=%d L=%d R=%d px", self.surface.size.padding.top, self.surface.size.padding.bottom, @@ -713,27 +694,25 @@ fn renderSizeWindow(self: *Inspector) void { } } - cimgui.c.igSeparatorText("Font"); + cimgui.c.ImGui_SeparatorText("Font"); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_font", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Size (Points)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Size (Points)"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%.2f pt", self.surface.font_size.points, ); @@ -741,14 +720,14 @@ fn renderSizeWindow(self: *Inspector) void { } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Size (Pixels)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Size (Pixels)"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "%.2f px", self.surface.font_size.pixels(), ); @@ -756,17 +735,15 @@ fn renderSizeWindow(self: *Inspector) void { } } - cimgui.c.igSeparatorText("Mouse"); + cimgui.c.ImGui_SeparatorText("Mouse"); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_mouse", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const mouse = &self.surface.mouse; const t = self.surface.renderer_state.terminal; @@ -781,14 +758,14 @@ fn renderSizeWindow(self: *Inspector) void { break :pt pt.coord(); }; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Hover Grid"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Hover Grid"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "row=%d, col=%d", hover_point.y, hover_point.x, @@ -804,14 +781,14 @@ fn renderSizeWindow(self: *Inspector) void { }, }).convert(.terminal, self.surface.size).terminal; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Hover Point"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Hover Point"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "(%dpx, %dpx)", @as(i64, @intFromFloat(coord.x)), @as(i64, @intFromFloat(coord.y)), @@ -824,23 +801,23 @@ fn renderSizeWindow(self: *Inspector) void { } else false; click: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Click State"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Click State"); } { - _ = cimgui.c.igTableSetColumnIndex(1); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); if (!any_click) { - cimgui.c.igText("none"); + cimgui.c.ImGui_Text("none"); break :click; } for (mouse.click_state, 0..) |state, i| { if (state != .press) continue; const button: input.MouseButton = @enumFromInt(i); - cimgui.c.igSameLine(0, 0); - cimgui.c.igText("%s", (switch (button) { + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("%s", (switch (button) { .unknown => "?", .left => "L", .middle => "M", @@ -868,14 +845,14 @@ fn renderSizeWindow(self: *Inspector) void { break :pt pt.coord(); }; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Click Grid"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Click Grid"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "row=%d, col=%d", left_click_point.y, left_click_point.x, @@ -884,14 +861,14 @@ fn renderSizeWindow(self: *Inspector) void { } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Click Point"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Click Point"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "(%dpx, %dpx)", @as(u32, @intFromFloat(mouse.left_click_xpos)), @as(u32, @intFromFloat(mouse.left_click_ypos)), @@ -903,8 +880,8 @@ fn renderSizeWindow(self: *Inspector) void { fn renderCellWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_cell, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -913,45 +890,45 @@ fn renderCellWindow(self: *Inspector) void { // Our popup for the picker const popup_picker = "Cell Picker"; - if (cimgui.c.igButton("Picker", .{ .x = 0, .y = 0 })) { + if (cimgui.c.ImGui_Button("Picker")) { // Request a cell self.cell.request(); - cimgui.c.igOpenPopup_Str( + cimgui.c.ImGui_OpenPopup( popup_picker, cimgui.c.ImGuiPopupFlags_None, ); } - if (cimgui.c.igBeginPopupModal( + if (cimgui.c.ImGui_BeginPopupModal( popup_picker, null, cimgui.c.ImGuiWindowFlags_AlwaysAutoResize, )) popup: { - defer cimgui.c.igEndPopup(); + defer cimgui.c.ImGui_EndPopup(); // Once we select a cell, close this popup. if (self.cell == .selected) { - cimgui.c.igCloseCurrentPopup(); + cimgui.c.ImGui_CloseCurrentPopup(); break :popup; } - cimgui.c.igText( + cimgui.c.ImGui_Text( "Click on a cell in the terminal to inspect it.\n" ++ "The click will be intercepted by the picker, \n" ++ "so it won't be sent to the terminal.", ); - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); - if (cimgui.c.igButton("Cancel", .{ .x = 0, .y = 0 })) { - cimgui.c.igCloseCurrentPopup(); + if (cimgui.c.ImGui_Button("Cancel")) { + cimgui.c.ImGui_CloseCurrentPopup(); } } // cell pick popup - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); if (self.cell != .selected) { - cimgui.c.igText("No cell selected."); + cimgui.c.ImGui_Text("No cell selected."); return; } @@ -965,8 +942,8 @@ fn renderCellWindow(self: *Inspector) void { fn renderKeyboardWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_keyboard, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -974,47 +951,44 @@ fn renderKeyboardWindow(self: *Inspector) void { list: { if (self.key_events.empty()) { - cimgui.c.igText("No recorded key events. Press a key with the " ++ + cimgui.c.ImGui_Text("No recorded key events. Press a key with the " ++ "terminal focused to record it."); break :list; } - if (cimgui.c.igButton("Clear", .{ .x = 0, .y = 0 })) { + if (cimgui.c.ImGui_Button("Clear")) { var it = self.key_events.iterator(.forward); while (it.next()) |v| v.deinit(self.surface.alloc); self.key_events.clear(); self.vt_stream.handler.current_seq = 1; } - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_key_events", 1, //cimgui.c.ImGuiTableFlags_ScrollY | cimgui.c.ImGuiTableFlags_RowBg | cimgui.c.ImGuiTableFlags_Borders, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); var it = self.key_events.iterator(.reverse); while (it.next()) |ev| { // Need to push an ID so that our selectable is unique. - cimgui.c.igPushID_Ptr(ev); - defer cimgui.c.igPopID(); + cimgui.c.ImGui_PushIDPtr(ev); + defer cimgui.c.ImGui_PopID(); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); var buf: [1024]u8 = undefined; const label = ev.label(&buf) catch "Key Event"; - _ = cimgui.c.igSelectable_BoolPtr( + _ = cimgui.c.ImGui_SelectableBoolPtr( label.ptr, &ev.imgui_state.selected, cimgui.c.ImGuiSelectableFlags_None, - .{ .x = 0, .y = 0 }, ); if (!ev.imgui_state.selected) continue; @@ -1034,7 +1008,7 @@ fn getKeyAction(self: *Inspector) KeyAction { }; inline for (keys) |k| { - if (cimgui.c.igIsKeyPressed_Bool(k.key, false)) { + if (cimgui.c.ImGui_IsKeyPressed(k.key)) { return k.action; } } @@ -1043,8 +1017,8 @@ fn getKeyAction(self: *Inspector) KeyAction { fn renderTermioWindow(self: *Inspector) void { // Start our window. If we're collapsed we do nothing. - defer cimgui.c.igEnd(); - if (!cimgui.c.igBegin( + defer cimgui.c.ImGui_End(); + if (!cimgui.c.ImGui_Begin( window_termio, null, cimgui.c.ImGuiWindowFlags_NoFocusOnAppearing, @@ -1057,21 +1031,21 @@ fn renderTermioWindow(self: *Inspector) void { "Pause##pause_play" else "Resume##pause_play"; - if (cimgui.c.igButton(pause_play.ptr, .{ .x = 0, .y = 0 })) { + if (cimgui.c.ImGui_Button(pause_play.ptr)) { self.vt_stream.handler.active = !self.vt_stream.handler.active; } - cimgui.c.igSameLine(0, cimgui.c.igGetStyle().*.ItemInnerSpacing.x); - if (cimgui.c.igButton("Filter", .{ .x = 0, .y = 0 })) { - cimgui.c.igOpenPopup_Str( + cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x); + if (cimgui.c.ImGui_Button("Filter")) { + cimgui.c.ImGui_OpenPopup( popup_filter, cimgui.c.ImGuiPopupFlags_None, ); } if (!self.vt_events.empty()) { - cimgui.c.igSameLine(0, cimgui.c.igGetStyle().*.ItemInnerSpacing.x); - if (cimgui.c.igButton("Clear", .{ .x = 0, .y = 0 })) { + cimgui.c.ImGui_SameLineEx(0, cimgui.c.ImGui_GetStyle().*.ItemInnerSpacing.x); + if (cimgui.c.ImGui_Button("Clear")) { var it = self.vt_events.iterator(.forward); while (it.next()) |v| v.deinit(self.surface.alloc); self.vt_events.clear(); @@ -1081,44 +1055,36 @@ fn renderTermioWindow(self: *Inspector) void { } } - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); if (self.vt_events.empty()) { - cimgui.c.igText("Waiting for events..."); + cimgui.c.ImGui_Text("Waiting for events..."); break :list; } - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_vt_events", 3, cimgui.c.ImGuiTableFlags_RowBg | cimgui.c.ImGuiTableFlags_Borders, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); - cimgui.c.igTableSetupColumn( + cimgui.c.ImGui_TableSetupColumn( "Seq", cimgui.c.ImGuiTableColumnFlags_WidthFixed, - 0, - 0, ); - cimgui.c.igTableSetupColumn( + cimgui.c.ImGui_TableSetupColumn( "Kind", cimgui.c.ImGuiTableColumnFlags_WidthFixed, - 0, - 0, ); - cimgui.c.igTableSetupColumn( + cimgui.c.ImGui_TableSetupColumn( "Description", cimgui.c.ImGuiTableColumnFlags_WidthStretch, - 0, - 0, ); // Handle keyboard navigation when window is focused - if (cimgui.c.igIsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) { + if (cimgui.c.ImGui_IsWindowFocused(cimgui.c.ImGuiFocusedFlags_RootAndChildWindows)) { const key_pressed = self.getKeyAction(); switch (key_pressed) { @@ -1174,11 +1140,11 @@ fn renderTermioWindow(self: *Inspector) void { var it = self.vt_events.iterator(.reverse); while (it.next()) |ev| { // Need to push an ID so that our selectable is unique. - cimgui.c.igPushID_Ptr(ev); - defer cimgui.c.igPopID(); + cimgui.c.ImGui_PushIDPtr(ev); + defer cimgui.c.ImGui_PopID(); - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableNextColumn(); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableNextColumn(); // Store the previous selection state to detect changes const was_selected = ev.imgui_selected; @@ -1189,11 +1155,10 @@ fn renderTermioWindow(self: *Inspector) void { } // Handle selectable widget - if (cimgui.c.igSelectable_BoolPtr( + if (cimgui.c.ImGui_SelectableBoolPtr( "##select", &ev.imgui_selected, cimgui.c.ImGuiSelectableFlags_SpanAllColumns, - .{ .x = 0, .y = 0 }, )) { // If selection state changed, update keyboard navigation state if (ev.imgui_selected != was_selected) { @@ -1205,40 +1170,38 @@ fn renderTermioWindow(self: *Inspector) void { } } - cimgui.c.igSameLine(0, 0); - cimgui.c.igText("%d", ev.seq); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", @tagName(ev.kind).ptr); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", ev.str.ptr); + cimgui.c.ImGui_SameLine(); + cimgui.c.ImGui_Text("%d", ev.seq); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", @tagName(ev.kind).ptr); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", ev.str.ptr); // If the event is selected, we render info about it. For now // we put this in the last column because that's the widest and // imgui has no way to make a column span. if (ev.imgui_selected) { { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "details", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); inspector.cursor.renderInTable( self.surface.renderer_state.terminal, &ev.cursor, ); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Scroll Region"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Scroll Region"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text( "T=%d B=%d L=%d R=%d", ev.scrolling_region.top, ev.scrolling_region.bottom, @@ -1253,51 +1216,49 @@ fn renderTermioWindow(self: *Inspector) void { var buf: [256]u8 = undefined; const key = std.fmt.bufPrintZ(&buf, "{s}", .{entry.key_ptr.*}) catch ""; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", key.ptr); - _ = cimgui.c.igTableNextColumn(); - cimgui.c.igText("%s", entry.value_ptr.ptr); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", key.ptr); + _ = cimgui.c.ImGui_TableNextColumn(); + cimgui.c.ImGui_Text("%s", entry.value_ptr.ptr); } } // If this is the selected event and scrolling is needed, scroll to it if (self.need_scroll_to_selected and self.is_keyboard_selection) { - cimgui.c.igSetScrollHereY(0.5); + cimgui.c.ImGui_SetScrollHereY(0.5); self.need_scroll_to_selected = false; } } } } // table - if (cimgui.c.igBeginPopupModal( + if (cimgui.c.ImGui_BeginPopupModal( popup_filter, null, cimgui.c.ImGuiWindowFlags_AlwaysAutoResize, )) { - defer cimgui.c.igEndPopup(); + defer cimgui.c.ImGui_EndPopup(); - cimgui.c.igText("Changed filter settings will only affect future events."); + cimgui.c.ImGui_Text("Changed filter settings will only affect future events."); - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_filter_kind", 3, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); inline for (@typeInfo(terminal.Parser.Action.Tag).@"enum".fields) |field| { const tag = @field(terminal.Parser.Action.Tag, field.name); if (tag == .apc_put or tag == .dcs_put) continue; - _ = cimgui.c.igTableNextColumn(); + _ = cimgui.c.ImGui_TableNextColumn(); var value = !self.vt_stream.handler.filter_exclude.contains(tag); - if (cimgui.c.igCheckbox(@tagName(tag).ptr, &value)) { + if (cimgui.c.ImGui_Checkbox(@tagName(tag).ptr, &value)) { if (value) { self.vt_stream.handler.filter_exclude.remove(tag); } else { @@ -1307,22 +1268,22 @@ fn renderTermioWindow(self: *Inspector) void { } } // Filter kind table - cimgui.c.igSeparator(); + cimgui.c.ImGui_Separator(); - cimgui.c.igText( + cimgui.c.ImGui_Text( "Filter by string. Empty displays all, \"abc\" finds lines\n" ++ "containing \"abc\", \"abc,xyz\" finds lines containing \"abc\"\n" ++ "or \"xyz\", \"-abc\" excludes lines containing \"abc\".", ); _ = cimgui.c.ImGuiTextFilter_Draw( - self.vt_stream.handler.filter_text, + &self.vt_stream.handler.filter_text, "##filter_text", 0, ); - cimgui.c.igSeparator(); - if (cimgui.c.igButton("Close", .{ .x = 0, .y = 0 })) { - cimgui.c.igCloseCurrentPopup(); + cimgui.c.ImGui_Separator(); + if (cimgui.c.ImGui_Button("Close")) { + cimgui.c.ImGui_CloseCurrentPopup(); } } // filter popup } diff --git a/src/inspector/cell.zig b/src/inspector/cell.zig index 2f72556bd..540e044fd 100644 --- a/src/inspector/cell.zig +++ b/src/inspector/cell.zig @@ -1,7 +1,7 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); /// A cell being inspected. This duplicates much of the data in @@ -55,24 +55,22 @@ pub const Cell = struct { y: usize, ) void { // We have a selected cell, show information about it. - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "table_cursor", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grid Position"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grid Position"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("row=%d col=%d", y, x); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("row=%d col=%d", y, x); } } @@ -82,18 +80,18 @@ pub const Cell = struct { // the single glyph in an image view so it looks _identical_ to the // terminal. codepoint: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Codepoints"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Codepoints"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - if (cimgui.c.igBeginListBox("##codepoints", .{ .x = 0, .y = 0 })) { - defer cimgui.c.igEndListBox(); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (cimgui.c.ImGui_BeginListBox("##codepoints", .{ .x = 0, .y = 0 })) { + defer cimgui.c.ImGui_EndListBox(); if (self.codepoint == 0) { - _ = cimgui.c.igSelectable_Bool("(empty)", false, 0, .{}); + _ = cimgui.c.ImGui_SelectableEx("(empty)", false, 0, .{}); break :codepoint; } @@ -102,42 +100,42 @@ pub const Cell = struct { { const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{self.codepoint}) catch ""; - _ = cimgui.c.igSelectable_Bool(key.ptr, false, 0, .{}); + _ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{}); } // All extras for (self.cps) |cp| { const key = std.fmt.bufPrintZ(&buf, "U+{X}", .{cp}) catch ""; - _ = cimgui.c.igSelectable_Bool(key.ptr, false, 0, .{}); + _ = cimgui.c.ImGui_SelectableEx(key.ptr, false, 0, .{}); } } } } // Character width property - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Width Property"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText(@tagName(self.wide)); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Width Property"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text(@tagName(self.wide)); // If we have a color then we show the color - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Foreground Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Foreground Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (self.style.fg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -152,7 +150,7 @@ pub const Cell = struct { @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -162,21 +160,21 @@ pub const Cell = struct { }, } - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Background Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Background Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (self.style.bg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -191,7 +189,7 @@ pub const Cell = struct { @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -209,17 +207,17 @@ pub const Cell = struct { inline for (styles) |style| style: { if (!@field(self.style.flags, style)) break :style; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText(style.ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text(style.ptr); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("true"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); } } - cimgui.c.igTextDisabled("(Any styles not shown are not currently set)"); + cimgui.c.ImGui_TextDisabled("(Any styles not shown are not currently set)"); } }; diff --git a/src/inspector/cursor.zig b/src/inspector/cursor.zig index 756898252..4f8bfb2e0 100644 --- a/src/inspector/cursor.zig +++ b/src/inspector/cursor.zig @@ -1,4 +1,4 @@ -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); /// Render cursor information with a table already open. @@ -7,57 +7,57 @@ pub fn renderInTable( cursor: *const terminal.Screen.Cursor, ) void { { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Position (x, y)"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Position (x, y)"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("(%d, %d)", cursor.x, cursor.y); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("(%d, %d)", cursor.x, cursor.y); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Style"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Style"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(cursor.cursor_style).ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(cursor.cursor_style).ptr); } } if (cursor.pending_wrap) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Pending Wrap"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Pending Wrap"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", if (cursor.pending_wrap) "true".ptr else "false".ptr); } } // If we have a color then we show the color - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Foreground Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Foreground Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (cursor.style.fg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -72,7 +72,7 @@ pub fn renderInTable( @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_fg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -82,21 +82,21 @@ pub fn renderInTable( }, } - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Background Color"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Background Color"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); switch (cursor.style.bg_color) { - .none => cimgui.c.igText("default"), + .none => cimgui.c.ImGui_Text("default"), .palette => |idx| { const rgb = t.colors.palette.current[idx]; - cimgui.c.igValue_Int("Palette", idx); + cimgui.c.ImGui_Text("Palette %d", idx); var color: [3]f32 = .{ @as(f32, @floatFromInt(rgb.r)) / 255, @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -111,7 +111,7 @@ pub fn renderInTable( @as(f32, @floatFromInt(rgb.g)) / 255, @as(f32, @floatFromInt(rgb.b)) / 255, }; - _ = cimgui.c.igColorEdit3( + _ = cimgui.c.ImGui_ColorEdit3( "color_bg", &color, cimgui.c.ImGuiColorEditFlags_DisplayHex | @@ -129,14 +129,14 @@ pub fn renderInTable( inline for (styles) |style| style: { if (!@field(cursor.style.flags, style)) break :style; - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText(style.ptr); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text(style.ptr); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("true"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); } } } diff --git a/src/inspector/key.zig b/src/inspector/key.zig index e42e4f23c..12d91a107 100644 --- a/src/inspector/key.zig +++ b/src/inspector/key.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const input = @import("../input.zig"); const CircBuf = @import("../datastruct/main.zig").CircBuf; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); /// Circular buffer of key events. pub const EventRing = CircBuf(Event, undefined); @@ -72,30 +72,28 @@ pub const Event = struct { /// Render this event in the inspector GUI. pub fn render(self: *const Event) void { - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##event", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); if (self.binding.len > 0) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Triggered Binding"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Triggered Binding"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); const height: f32 = height: { const item_count: f32 = @floatFromInt(@min(self.binding.len, 5)); - const padding = cimgui.c.igGetStyle().*.FramePadding.y * 2; - break :height cimgui.c.igGetTextLineHeightWithSpacing() * item_count + padding; + const padding = cimgui.c.ImGui_GetStyle().*.FramePadding.y * 2; + break :height cimgui.c.ImGui_GetTextLineHeightWithSpacing() * item_count + padding; }; - if (cimgui.c.igBeginListBox("##bindings", .{ .x = 0, .y = height })) { - defer cimgui.c.igEndListBox(); + if (cimgui.c.ImGui_BeginListBox("##bindings", .{ .x = 0, .y = height })) { + defer cimgui.c.ImGui_EndListBox(); for (self.binding) |action| { - _ = cimgui.c.igSelectable_Bool( + _ = cimgui.c.ImGui_SelectableEx( @tagName(action).ptr, false, cimgui.c.ImGuiSelectableFlags_None, @@ -106,64 +104,64 @@ pub const Event = struct { } pty: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Encoding to Pty"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Encoding to Pty"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); if (self.pty.len == 0) { - cimgui.c.igTextDisabled("(no data)"); + cimgui.c.ImGui_TextDisabled("(no data)"); break :pty; } self.renderPty() catch { - cimgui.c.igTextDisabled("(error rendering pty data)"); + cimgui.c.ImGui_TextDisabled("(error rendering pty data)"); break :pty; }; } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Action"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(self.event.action).ptr); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Action"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(self.event.action).ptr); } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Key"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(self.event.key).ptr); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Key"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%s", @tagName(self.event.key).ptr); } if (!self.event.mods.empty()) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Mods"); - _ = cimgui.c.igTableSetColumnIndex(1); - if (self.event.mods.shift) cimgui.c.igText("shift "); - if (self.event.mods.ctrl) cimgui.c.igText("ctrl "); - if (self.event.mods.alt) cimgui.c.igText("alt "); - if (self.event.mods.super) cimgui.c.igText("super "); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Mods"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + if (self.event.mods.shift) cimgui.c.ImGui_Text("shift "); + if (self.event.mods.ctrl) cimgui.c.ImGui_Text("ctrl "); + if (self.event.mods.alt) cimgui.c.ImGui_Text("alt "); + if (self.event.mods.super) cimgui.c.ImGui_Text("super "); } if (self.event.composing) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Composing"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("true"); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Composing"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("true"); } utf8: { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("UTF-8"); - _ = cimgui.c.igTableSetColumnIndex(1); + cimgui.c.ImGui_TableNextRow(); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("UTF-8"); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); if (self.event.utf8.len == 0) { - cimgui.c.igTextDisabled("(empty)"); + cimgui.c.ImGui_TextDisabled("(empty)"); break :utf8; } self.renderUtf8(self.event.utf8) catch { - cimgui.c.igTextDisabled("(error rendering utf-8)"); + cimgui.c.ImGui_TextDisabled("(error rendering utf-8)"); break :utf8; }; } @@ -187,13 +185,11 @@ pub const Event = struct { try writer.writeByte(0); // Render as a textbox - _ = cimgui.c.igInputText( + _ = cimgui.c.ImGui_InputText( "##utf8", &buf, buf_stream.getWritten().len - 1, cimgui.c.ImGuiInputTextFlags_ReadOnly, - null, - null, ); } @@ -223,13 +219,11 @@ pub const Event = struct { try writer.writeByte(0); // Render as a textbox - _ = cimgui.c.igInputText( + _ = cimgui.c.ImGui_InputText( "##pty", &buf, buf_stream.getWritten().len - 1, cimgui.c.ImGuiInputTextFlags_ReadOnly, - null, - null, ); } }; diff --git a/src/inspector/page.zig b/src/inspector/page.zig index 7da469e21..fd9d3bfb4 100644 --- a/src/inspector/page.zig +++ b/src/inspector/page.zig @@ -1,167 +1,161 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); const units = @import("units.zig"); pub fn render(page: *const terminal.Page) void { - cimgui.c.igPushID_Ptr(page); - defer cimgui.c.igPopID(); + cimgui.c.ImGui_PushIDPtr(page); + defer cimgui.c.ImGui_PopID(); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_BeginTable( "##page_state", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Memory Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Memory Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len)); - cimgui.c.igText("%d VM pages", page.memory.len / std.heap.page_size_min); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d bytes (%d KiB)", page.memory.len, units.toKibiBytes(page.memory.len)); + cimgui.c.ImGui_Text("%d VM pages", page.memory.len / std.heap.page_size_min); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Unique Styles"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Unique Styles"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", page.styles.count()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.styles.count()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grapheme Entries"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grapheme Entries"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", page.graphemeCount()); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", page.graphemeCount()); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Capacity"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Capacity"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + _ = cimgui.c.ImGui_BeginTable( "##capacity", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const cap = page.capacity; { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Columns"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Columns"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(cap.cols))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.cols))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Rows"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Rows"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(cap.rows))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.rows))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Unique Styles"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Unique Styles"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(cap.styles))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(cap.styles))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Grapheme Bytes"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Grapheme Bytes"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", cap.grapheme_bytes); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", cap.grapheme_bytes); } } } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Size"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Size"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - _ = cimgui.c.igBeginTable( + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + _ = cimgui.c.ImGui_BeginTable( "##size", 2, cimgui.c.ImGuiTableFlags_None, - .{ .x = 0, .y = 0 }, - 0, ); - defer cimgui.c.igEndTable(); + defer cimgui.c.ImGui_EndTable(); const size = page.size; { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Columns"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Columns"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(size.cols))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.cols))); } } { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); + cimgui.c.ImGui_TableNextRow(); { - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Rows"); + _ = cimgui.c.ImGui_TableSetColumnIndex(0); + cimgui.c.ImGui_Text("Rows"); } { - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%d", @as(u32, @intCast(size.rows))); + _ = cimgui.c.ImGui_TableSetColumnIndex(1); + cimgui.c.ImGui_Text("%d", @as(u32, @intCast(size.rows))); } } } diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 7e2b51ee1..9f55e6019 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -1,6 +1,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const terminal = @import("../terminal/main.zig"); const CircBuf = @import("../datastruct/main.zig").CircBuf; const Surface = @import("../Surface.zig"); @@ -83,7 +83,7 @@ pub const VTEvent = struct { /// Returns true if the event passes the given filter. pub fn passFilter( self: *const VTEvent, - filter: *cimgui.c.ImGuiTextFilter, + filter: *const cimgui.c.ImGuiTextFilter, ) bool { // Check our main string if (cimgui.c.ImGuiTextFilter_PassFilter( @@ -318,19 +318,18 @@ pub const VTHandler = struct { /// Exclude certain actions by tag. filter_exclude: ActionTagSet = .initMany(&.{.print}), - filter_text: *cimgui.c.ImGuiTextFilter, + filter_text: cimgui.c.ImGuiTextFilter = .{}, const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag); pub fn init(surface: *Surface) VTHandler { return .{ .surface = surface, - .filter_text = cimgui.c.ImGuiTextFilter_ImGuiTextFilter(""), }; } pub fn deinit(self: *VTHandler) void { - cimgui.c.ImGuiTextFilter_destroy(self.filter_text); + _ = self; } pub fn vt( @@ -371,7 +370,7 @@ pub const VTHandler = struct { errdefer ev.deinit(alloc); // Check if the event passes the filter - if (!ev.passFilter(self.filter_text)) { + if (!ev.passFilter(&self.filter_text)) { ev.deinit(alloc); return true; } -- 2.51.2 From 896361f426d29efc4a28f69261095dfeeef3733a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 11:00:21 -0800 Subject: [PATCH 138/349] Fix up API calls for initialization --- pkg/dcimgui/build.zig | 5 ++++ pkg/dcimgui/ext.cpp | 30 ++++++++++++++++++++++++ pkg/dcimgui/main.zig | 6 +++++ src/apprt/embedded.zig | 7 ++++-- src/apprt/gtk/class/imgui_widget.zig | 7 ++++-- src/inspector/Inspector.zig | 35 ++++++++++++++-------------- 6 files changed, 69 insertions(+), 21 deletions(-) create mode 100644 pkg/dcimgui/ext.cpp diff --git a/pkg/dcimgui/build.zig b/pkg/dcimgui/build.zig index 2d1594cba..4e5184920 100644 --- a/pkg/dcimgui/build.zig +++ b/pkg/dcimgui/build.zig @@ -133,6 +133,11 @@ pub fn build(b: *std.Build) !void { }, .flags = flags.items, }); + lib.addCSourceFiles(.{ + .root = b.path(""), + .files = &.{"ext.cpp"}, + .flags = flags.items, + }); lib.installHeadersDirectory( upstream.path(""), diff --git a/pkg/dcimgui/ext.cpp b/pkg/dcimgui/ext.cpp new file mode 100644 index 000000000..d4732e0fa --- /dev/null +++ b/pkg/dcimgui/ext.cpp @@ -0,0 +1,30 @@ +#include "imgui.h" + +// This file contains custom extensions for functionality that isn't +// properly supported by Dear Bindings yet. Namely: +// https://github.com/dearimgui/dear_bindings/issues/55 + +// Wrap this in a namespace to keep it separate from the C++ API +namespace cimgui +{ +#include "dcimgui.h" +} + +extern "C" +{ +CIMGUI_API void ImFontConfig_ImFontConfig(cimgui::ImFontConfig* self) +{ + static_assert(sizeof(cimgui::ImFontConfig) == sizeof(::ImFontConfig), "ImFontConfig size mismatch"); + static_assert(alignof(cimgui::ImFontConfig) == alignof(::ImFontConfig), "ImFontConfig alignment mismatch"); + ::ImFontConfig defaults; + *reinterpret_cast<::ImFontConfig*>(self) = defaults; +} + +CIMGUI_API void ImGuiStyle_ImGuiStyle(cimgui::ImGuiStyle* self) +{ + static_assert(sizeof(cimgui::ImGuiStyle) == sizeof(::ImGuiStyle), "ImGuiStyle size mismatch"); + static_assert(alignof(cimgui::ImGuiStyle) == alignof(::ImGuiStyle), "ImGuiStyle alignment mismatch"); + ::ImGuiStyle defaults; + *reinterpret_cast<::ImGuiStyle*>(self) = defaults; +} +} diff --git a/pkg/dcimgui/main.zig b/pkg/dcimgui/main.zig index 7ea135cdb..00a3c739d 100644 --- a/pkg/dcimgui/main.zig +++ b/pkg/dcimgui/main.zig @@ -28,6 +28,12 @@ pub extern fn ImGui_DockBuilderDockWindow(window_name: [*:0]const u8, node_id: c pub extern fn ImGui_DockBuilderSplitNode(node_id: c.ImGuiID, split_dir: c.ImGuiDir, size_ratio_for_node_at_dir: f32, out_id_at_dir: *c.ImGuiID, out_id_at_opposite_dir: *c.ImGuiID) callconv(.c) c.ImGuiID; pub extern fn ImGui_DockBuilderFinish(node_id: c.ImGuiID) callconv(.c) void; +// Extension functions from ext.cpp +pub const ext = struct { + pub extern fn ImFontConfig_ImFontConfig(self: *c.ImFontConfig) callconv(.c) void; + pub extern fn ImGuiStyle_ImGuiStyle(self: *c.ImGuiStyle) callconv(.c) void; +}; + test { _ = c; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c6c4d2ab8..27f07604a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1070,8 +1070,11 @@ pub const Inspector = struct { // Cache our scale because we use it for cursor position calculations. self.content_scale = x; - // Setup a new style and scale it appropriately. - var style: cimgui.c.ImGuiStyle = .{}; + // Setup a new style and scale it appropriately. We must use the + // ImGuiStyle constructor to get proper default values (e.g., + // CurveTessellationTol) rather than zero-initialized values. + var style: cimgui.c.ImGuiStyle = undefined; + cimgui.ext.ImGuiStyle_ImGuiStyle(&style); cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatCast(x)); const active_style = cimgui.c.ImGui_GetStyle(); active_style.* = style; diff --git a/src/apprt/gtk/class/imgui_widget.zig b/src/apprt/gtk/class/imgui_widget.zig index 474efaa91..39a8cef63 100644 --- a/src/apprt/gtk/class/imgui_widget.zig +++ b/src/apprt/gtk/class/imgui_widget.zig @@ -255,8 +255,11 @@ pub const ImguiWidget = extern struct { io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) }; io.DisplayFramebufferScale = .{ .x = 1, .y = 1 }; - // Setup a new style and scale it appropriately. - var style: cimgui.c.ImGuiStyle = .{}; + // Setup a new style and scale it appropriately. We must use the + // ImGuiStyle constructor to get proper default values (e.g., + // CurveTessellationTol) rather than zero-initialized values. + var style: cimgui.c.ImGuiStyle = undefined; + cimgui.ext.ImGuiStyle_ImGuiStyle(&style); cimgui.c.ImGuiStyle_ScaleAllSizes(&style, @floatFromInt(scale_factor)); const active_style = cimgui.c.ImGui_GetStyle(); active_style.* = style; diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index b1ac473b8..7029b7d3a 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -138,23 +138,24 @@ pub fn setup() void { io.IniFilename = null; io.LogFilename = null; - // Use our own embedded font - { - // TODO: This will have to be recalculated for different screen DPIs. - // This is currently hardcoded to a 2x content scale. - const font_size = 16 * 2; - - var font_config: cimgui.c.ImFontConfig = .{}; - font_config.FontDataOwnedByAtlas = false; - _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( - io.Fonts, - @ptrCast(@constCast(font.embedded.regular)), - font.embedded.regular.len, - font_size, - &font_config, - null, - ); - } + // // Use our own embedded font + // { + // // TODO: This will have to be recalculated for different screen DPIs. + // // This is currently hardcoded to a 2x content scale. + // const font_size = 16 * 2; + // + // var font_config: cimgui.c.ImFontConfig = .{}; + // cimgui.ext.ImFontConfig_ImFontConfig(&font_config); + // font_config.FontDataOwnedByAtlas = false; + // _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( + // io.Fonts, + // @ptrCast(@constCast(font.embedded.regular.ptr)), + // @intCast(font.embedded.regular.len), + // font_size, + // &font_config, + // null, + // ); + // } } pub fn init(surface: *Surface) !Inspector { -- 2.51.2 From f1ba5297b8b35b6a952ff0312b3e0fd4564d0908 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 13:20:01 -0800 Subject: [PATCH 139/349] build: fix imgui on GTK --- src/apprt/gtk/class/imgui_widget.zig | 2 +- src/build/SharedDeps.zig | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/imgui_widget.zig b/src/apprt/gtk/class/imgui_widget.zig index 39a8cef63..79e85fad2 100644 --- a/src/apprt/gtk/class/imgui_widget.zig +++ b/src/apprt/gtk/class/imgui_widget.zig @@ -1,7 +1,7 @@ const std = @import("std"); const assert = @import("../../../quirks.zig").inlineAssert; -const cimgui = @import("cimgui"); +const cimgui = @import("dcimgui"); const gl = @import("opengl"); const adw = @import("adw"); const gdk = @import("gdk"); diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index f96a269bd..ce0d5d479 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -484,6 +484,7 @@ pub fn add( .optimize = optimize, .@"backend-metal" = target.result.os.tag.isDarwin(), .@"backend-osx" = target.result.os.tag == .macos, + .@"backend-opengl3" = target.result.os.tag != .macos, })) |dep| { step.root_module.addImport("dcimgui", dep.module("dcimgui")); step.linkLibrary(dep.artifact("dcimgui")); -- 2.51.2 From 965ffb1750b2373c4e25cdf3a03b160635ad1dc4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 13:12:55 -0800 Subject: [PATCH 140/349] pkg/dcimgui: add freetype --- pkg/dcimgui/build.zig | 37 +++++++++++++++++++++++++++++++++++ src/build/SharedDeps.zig | 42 ++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/pkg/dcimgui/build.zig b/pkg/dcimgui/build.zig index 4e5184920..683f0be92 100644 --- a/pkg/dcimgui/build.zig +++ b/pkg/dcimgui/build.zig @@ -4,12 +4,14 @@ const NativeTargetInfo = std.zig.system.NativeTargetInfo; pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const freetype = b.option(bool, "freetype", "Use Freetype") orelse false; const backend_opengl3 = b.option(bool, "backend-opengl3", "OpenGL3 backend") orelse false; const backend_metal = b.option(bool, "backend-metal", "Metal backend") orelse false; const backend_osx = b.option(bool, "backend-osx", "OSX backend") orelse false; // Build options const options = b.addOptions(); + options.addOption(bool, "freetype", freetype); options.addOption(bool, "backend_opengl3", backend_opengl3); options.addOption(bool, "backend_metal", backend_metal); options.addOption(bool, "backend_osx", backend_osx); @@ -50,6 +52,9 @@ pub fn build(b: *std.Build) !void { "-DIMGUI_USE_WCHAR32=1", "-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1", }); + if (freetype) try flags.appendSlice(b.allocator, &.{ + "-DIMGUI_ENABLE_FREETYPE=1", + }); if (target.result.os.tag == .windows) { try flags.appendSlice(b.allocator, &.{ "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", @@ -84,6 +89,30 @@ pub fn build(b: *std.Build) !void { .{ .include_extensions = &.{".h"} }, ); + if (freetype) { + lib.addCSourceFile(.{ + .file = upstream.path("misc/freetype/imgui_freetype.cpp"), + .flags = flags.items, + }); + + if (b.systemIntegrationOption("freetype", .{})) { + lib.linkSystemLibrary2("freetype2", dynamic_link_opts); + } else { + const freetype_dep = b.dependency("freetype", .{ + .target = target, + .optimize = optimize, + .@"enable-libpng" = true, + }); + lib.linkLibrary(freetype_dep.artifact("freetype")); + if (freetype_dep.builder.lazyDependency( + "freetype", + .{}, + )) |freetype_upstream| { + mod.addIncludePath(freetype_upstream.path("include")); + } + } + } + if (backend_metal) { lib.addCSourceFiles(.{ .root = upstream.path("backends"), @@ -160,3 +189,11 @@ pub fn build(b: *std.Build) !void { const test_step = b.step("test", "Run tests"); test_step.dependOn(&tests_run.step); } + +// For dynamic linking, we prefer dynamic linking and to search by +// mode first. Mode first will search all paths for a dynamic library +// before falling back to static. +const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ + .preferred_link_mode = .dynamic, + .search_strategy = .mode_first, +}; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index ce0d5d479..b1c084002 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -135,29 +135,28 @@ pub fn add( // Every exe needs the terminal options self.config.terminalOptions().add(b, step.root_module); - // Freetype + // Freetype. We always include this even if our font backend doesn't + // use it because Dear Imgui uses Freetype. _ = b.systemIntegrationOption("freetype", .{}); // Shows it in help - if (self.config.font_backend.hasFreetype()) { - if (b.lazyDependency("freetype", .{ - .target = target, - .optimize = optimize, - .@"enable-libpng" = true, - })) |freetype_dep| { - step.root_module.addImport( - "freetype", - freetype_dep.module("freetype"), - ); + if (b.lazyDependency("freetype", .{ + .target = target, + .optimize = optimize, + .@"enable-libpng" = true, + })) |freetype_dep| { + step.root_module.addImport( + "freetype", + freetype_dep.module("freetype"), + ); - if (b.systemIntegrationOption("freetype", .{})) { - step.linkSystemLibrary2("bzip2", dynamic_link_opts); - step.linkSystemLibrary2("freetype2", dynamic_link_opts); - } else { - step.linkLibrary(freetype_dep.artifact("freetype")); - try static_libs.append( - b.allocator, - freetype_dep.artifact("freetype").getEmittedBin(), - ); - } + if (b.systemIntegrationOption("freetype", .{})) { + step.linkSystemLibrary2("bzip2", dynamic_link_opts); + step.linkSystemLibrary2("freetype2", dynamic_link_opts); + } else { + step.linkLibrary(freetype_dep.artifact("freetype")); + try static_libs.append( + b.allocator, + freetype_dep.artifact("freetype").getEmittedBin(), + ); } } @@ -482,6 +481,7 @@ pub fn add( if (b.lazyDependency("dcimgui", .{ .target = target, .optimize = optimize, + .freetype = true, .@"backend-metal" = target.result.os.tag.isDarwin(), .@"backend-osx" = target.result.os.tag == .macos, .@"backend-opengl3" = target.result.os.tag != .macos, -- 2.51.2 From f2bc722a582862f94a7ad4d3a4e658ba33b1de5b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 13:35:37 -0800 Subject: [PATCH 141/349] pkg/dcimgui: fix wchar size mismatch --- pkg/dcimgui/main.zig | 4 ++++ src/inspector/Inspector.zig | 36 ++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pkg/dcimgui/main.zig b/pkg/dcimgui/main.zig index 00a3c739d..e709158f5 100644 --- a/pkg/dcimgui/main.zig +++ b/pkg/dcimgui/main.zig @@ -1,6 +1,10 @@ pub const build_options = @import("build_options"); pub const c = @cImport({ + // This is set during the build so it also has to be set + // during import time to get the right types. Without this + // you get stack size mismatches on some structs. + @cDefine("IMGUI_USE_WCHAR32", "1"); @cInclude("dcimgui.h"); }); diff --git a/src/inspector/Inspector.zig b/src/inspector/Inspector.zig index 7029b7d3a..156e2cb18 100644 --- a/src/inspector/Inspector.zig +++ b/src/inspector/Inspector.zig @@ -138,24 +138,24 @@ pub fn setup() void { io.IniFilename = null; io.LogFilename = null; - // // Use our own embedded font - // { - // // TODO: This will have to be recalculated for different screen DPIs. - // // This is currently hardcoded to a 2x content scale. - // const font_size = 16 * 2; - // - // var font_config: cimgui.c.ImFontConfig = .{}; - // cimgui.ext.ImFontConfig_ImFontConfig(&font_config); - // font_config.FontDataOwnedByAtlas = false; - // _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( - // io.Fonts, - // @ptrCast(@constCast(font.embedded.regular.ptr)), - // @intCast(font.embedded.regular.len), - // font_size, - // &font_config, - // null, - // ); - // } + // Use our own embedded font + { + // TODO: This will have to be recalculated for different screen DPIs. + // This is currently hardcoded to a 2x content scale. + const font_size = 16 * 2; + + var font_config: cimgui.c.ImFontConfig = undefined; + cimgui.ext.ImFontConfig_ImFontConfig(&font_config); + font_config.FontDataOwnedByAtlas = false; + _ = cimgui.c.ImFontAtlas_AddFontFromMemoryTTF( + io.Fonts, + @ptrCast(@constCast(font.embedded.regular.ptr)), + @intCast(font.embedded.regular.len), + font_size, + &font_config, + null, + ); + } } pub fn init(surface: *Surface) !Inspector { -- 2.51.2 From 82e585ad9a8e6c1fcec5689ec2e603d1d5fa3be8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 13:38:39 -0800 Subject: [PATCH 142/349] remove pkg/cimgui --- build.zig.zon | 1 - pkg/cimgui/build.zig | 129 - pkg/cimgui/build.zig.zon | 19 - pkg/cimgui/c.zig | 4 - pkg/cimgui/main.zig | 20 - pkg/cimgui/vendor/cimgui.cpp | 5943 ------------------------------ pkg/cimgui/vendor/cimgui.h | 6554 ---------------------------------- 7 files changed, 12670 deletions(-) delete mode 100644 pkg/cimgui/build.zig delete mode 100644 pkg/cimgui/build.zig.zon delete mode 100644 pkg/cimgui/c.zig delete mode 100644 pkg/cimgui/main.zig delete mode 100644 pkg/cimgui/vendor/cimgui.cpp delete mode 100644 pkg/cimgui/vendor/cimgui.h diff --git a/build.zig.zon b/build.zig.zon index eff6dc12e..c9c3093b6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -63,7 +63,6 @@ }, // C libs - .cimgui = .{ .path = "./pkg/cimgui", .lazy = true }, .dcimgui = .{ .path = "./pkg/dcimgui", .lazy = true }, .fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true }, .freetype = .{ .path = "./pkg/freetype", .lazy = true }, diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig deleted file mode 100644 index 890873ef9..000000000 --- a/pkg/cimgui/build.zig +++ /dev/null @@ -1,129 +0,0 @@ -const std = @import("std"); -const NativeTargetInfo = std.zig.system.NativeTargetInfo; - -pub fn build(b: *std.Build) !void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const module = b.addModule("cimgui", .{ - .root_source_file = b.path("main.zig"), - .target = target, - .optimize = optimize, - }); - - const imgui_ = b.lazyDependency("imgui", .{}); - const lib = b.addLibrary(.{ - .name = "cimgui", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - }), - .linkage = .static, - }); - lib.linkLibC(); - lib.linkLibCpp(); - if (target.result.os.tag == .windows) { - lib.linkSystemLibrary("imm32"); - } - - // For dynamic linking, we prefer dynamic linking and to search by - // mode first. Mode first will search all paths for a dynamic library - // before falling back to static. - const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{ - .preferred_link_mode = .dynamic, - .search_strategy = .mode_first, - }; - - if (b.systemIntegrationOption("freetype", .{})) { - lib.linkSystemLibrary2("freetype2", dynamic_link_opts); - } else { - const freetype = b.dependency("freetype", .{ - .target = target, - .optimize = optimize, - .@"enable-libpng" = true, - }); - lib.linkLibrary(freetype.artifact("freetype")); - - if (freetype.builder.lazyDependency( - "freetype", - .{}, - )) |freetype_dep| { - module.addIncludePath(freetype_dep.path("include")); - } - } - - if (imgui_) |imgui| lib.addIncludePath(imgui.path("")); - module.addIncludePath(b.path("vendor")); - - 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(b.allocator, &.{ - "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", - }); - } else { - try flags.appendSlice(b.allocator, &.{ - "-DIMGUI_IMPL_API=extern\t\"C\"", - }); - } - - if (target.result.os.tag == .freebsd) { - try flags.append(b.allocator, "-fPIC"); - } - - if (imgui_) |imgui| { - lib.addCSourceFile(.{ .file = b.path("vendor/cimgui.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_draw.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_demo.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_widgets.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("imgui_tables.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ .file = imgui.path("misc/freetype/imgui_freetype.cpp"), .flags = flags.items }); - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_opengl3.cpp"), - .flags = flags.items, - }); - - if (target.result.os.tag.isDarwin()) { - if (!target.query.isNative()) { - try @import("apple_sdk").addPaths(b, lib); - } - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_metal.mm"), - .flags = flags.items, - }); - if (target.result.os.tag == .macos) { - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_osx.mm"), - .flags = flags.items, - }); - } - } - } - - lib.installHeadersDirectory( - b.path("vendor"), - "", - .{ .include_extensions = &.{".h"} }, - ); - - b.installArtifact(lib); - - const test_exe = b.addTest(.{ - .name = "test", - .root_module = b.createModule(.{ - .root_source_file = b.path("main.zig"), - .target = target, - .optimize = optimize, - }), - }); - test_exe.linkLibrary(lib); - const tests_run = b.addRunArtifact(test_exe); - const test_step = b.step("test", "Run tests"); - test_step.dependOn(&tests_run.step); -} diff --git a/pkg/cimgui/build.zig.zon b/pkg/cimgui/build.zig.zon deleted file mode 100644 index f539a8fd6..000000000 --- a/pkg/cimgui/build.zig.zon +++ /dev/null @@ -1,19 +0,0 @@ -.{ - .name = .cimgui, - .version = "1.90.6", // -docking branch - .fingerprint = 0x49726f5f8acbc90d, - .paths = .{""}, - .dependencies = .{ - // This should be kept in sync with the submodule in the cimgui source - // code in ./vendor/ to be safe that they're compatible. - .imgui = .{ - // ocornut/imgui - .url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", - .hash = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3", - .lazy = true, - }, - - .apple_sdk = .{ .path = "../apple-sdk" }, - .freetype = .{ .path = "../freetype" }, - }, -} diff --git a/pkg/cimgui/c.zig b/pkg/cimgui/c.zig deleted file mode 100644 index f9b8ff920..000000000 --- a/pkg/cimgui/c.zig +++ /dev/null @@ -1,4 +0,0 @@ -pub const c = @cImport({ - @cDefine("CIMGUI_DEFINE_ENUMS_AND_STRUCTS", "1"); - @cInclude("cimgui.h"); -}); diff --git a/pkg/cimgui/main.zig b/pkg/cimgui/main.zig deleted file mode 100644 index b890a49ee..000000000 --- a/pkg/cimgui/main.zig +++ /dev/null @@ -1,20 +0,0 @@ -pub const c = @import("c.zig").c; - -// OpenGL -pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.c) bool; -pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void; -pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void; -pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.c) void; - -// Metal -pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.c) bool; -pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void; -pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.c) void; -pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.c) void; - -// OSX -pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool; -pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void; -pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void; - -test {} diff --git a/pkg/cimgui/vendor/cimgui.cpp b/pkg/cimgui/vendor/cimgui.cpp deleted file mode 100644 index 3b36df7d9..000000000 --- a/pkg/cimgui/vendor/cimgui.cpp +++ /dev/null @@ -1,5943 +0,0 @@ -// This file is automatically generated by generator.lua from -// https://github.com/cimgui/cimgui based on imgui.h file version "1.90.6" 19060 -// from Dear ImGui https://github.com/ocornut/imgui with imgui_internal.h api -// docking branch -#define IMGUI_ENABLE_FREETYPE -#ifdef IMGUI_ENABLE_FREETYPE -#ifndef CIMGUI_FREETYPE -#error "IMGUI_FREETYPE should be defined for Freetype linking" -#endif -#else -#ifdef CIMGUI_FREETYPE -#error "IMGUI_FREETYPE should not be defined without freetype generated cimgui" -#endif -#endif -#include "imgui.h" -#ifdef IMGUI_ENABLE_FREETYPE -#include "misc/freetype/imgui_freetype.h" -#endif -#include "imgui_internal.h" - -#include "cimgui.h" - -CIMGUI_API ImVec2* ImVec2_ImVec2_Nil(void) { - return IM_NEW(ImVec2)(); -} -CIMGUI_API void ImVec2_destroy(ImVec2* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec2* ImVec2_ImVec2_Float(float _x, float _y) { - return IM_NEW(ImVec2)(_x, _y); -} -CIMGUI_API ImVec4* ImVec4_ImVec4_Nil(void) { - return IM_NEW(ImVec4)(); -} -CIMGUI_API void ImVec4_destroy(ImVec4* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec4* ImVec4_ImVec4_Float(float _x, float _y, float _z, float _w) { - return IM_NEW(ImVec4)(_x, _y, _z, _w); -} -CIMGUI_API ImGuiContext* igCreateContext(ImFontAtlas* shared_font_atlas) { - return ImGui::CreateContext(shared_font_atlas); -} -CIMGUI_API void igDestroyContext(ImGuiContext* ctx) { - return ImGui::DestroyContext(ctx); -} -CIMGUI_API ImGuiContext* igGetCurrentContext() { - return ImGui::GetCurrentContext(); -} -CIMGUI_API void igSetCurrentContext(ImGuiContext* ctx) { - return ImGui::SetCurrentContext(ctx); -} -CIMGUI_API ImGuiIO* igGetIO() { - return &ImGui::GetIO(); -} -CIMGUI_API ImGuiStyle* igGetStyle() { - return &ImGui::GetStyle(); -} -CIMGUI_API void igNewFrame() { - return ImGui::NewFrame(); -} -CIMGUI_API void igEndFrame() { - return ImGui::EndFrame(); -} -CIMGUI_API void igRender() { - return ImGui::Render(); -} -CIMGUI_API ImDrawData* igGetDrawData() { - return ImGui::GetDrawData(); -} -CIMGUI_API void igShowDemoWindow(bool* p_open) { - return ImGui::ShowDemoWindow(p_open); -} -CIMGUI_API void igShowMetricsWindow(bool* p_open) { - return ImGui::ShowMetricsWindow(p_open); -} -CIMGUI_API void igShowDebugLogWindow(bool* p_open) { - return ImGui::ShowDebugLogWindow(p_open); -} -CIMGUI_API void igShowIDStackToolWindow(bool* p_open) { - return ImGui::ShowIDStackToolWindow(p_open); -} -CIMGUI_API void igShowAboutWindow(bool* p_open) { - return ImGui::ShowAboutWindow(p_open); -} -CIMGUI_API void igShowStyleEditor(ImGuiStyle* ref) { - return ImGui::ShowStyleEditor(ref); -} -CIMGUI_API bool igShowStyleSelector(const char* label) { - return ImGui::ShowStyleSelector(label); -} -CIMGUI_API void igShowFontSelector(const char* label) { - return ImGui::ShowFontSelector(label); -} -CIMGUI_API void igShowUserGuide() { - return ImGui::ShowUserGuide(); -} -CIMGUI_API const char* igGetVersion() { - return ImGui::GetVersion(); -} -CIMGUI_API void igStyleColorsDark(ImGuiStyle* dst) { - return ImGui::StyleColorsDark(dst); -} -CIMGUI_API void igStyleColorsLight(ImGuiStyle* dst) { - return ImGui::StyleColorsLight(dst); -} -CIMGUI_API void igStyleColorsClassic(ImGuiStyle* dst) { - return ImGui::StyleColorsClassic(dst); -} -CIMGUI_API bool igBegin(const char* name, - bool* p_open, - ImGuiWindowFlags flags) { - return ImGui::Begin(name, p_open, flags); -} -CIMGUI_API void igEnd() { - return ImGui::End(); -} -CIMGUI_API bool igBeginChild_Str(const char* str_id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) { - return ImGui::BeginChild(str_id, size, child_flags, window_flags); -} -CIMGUI_API bool igBeginChild_ID(ImGuiID id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) { - return ImGui::BeginChild(id, size, child_flags, window_flags); -} -CIMGUI_API void igEndChild() { - return ImGui::EndChild(); -} -CIMGUI_API bool igIsWindowAppearing() { - return ImGui::IsWindowAppearing(); -} -CIMGUI_API bool igIsWindowCollapsed() { - return ImGui::IsWindowCollapsed(); -} -CIMGUI_API bool igIsWindowFocused(ImGuiFocusedFlags flags) { - return ImGui::IsWindowFocused(flags); -} -CIMGUI_API bool igIsWindowHovered(ImGuiHoveredFlags flags) { - return ImGui::IsWindowHovered(flags); -} -CIMGUI_API ImDrawList* igGetWindowDrawList() { - return ImGui::GetWindowDrawList(); -} -CIMGUI_API float igGetWindowDpiScale() { - return ImGui::GetWindowDpiScale(); -} -CIMGUI_API void igGetWindowPos(ImVec2* pOut) { - *pOut = ImGui::GetWindowPos(); -} -CIMGUI_API void igGetWindowSize(ImVec2* pOut) { - *pOut = ImGui::GetWindowSize(); -} -CIMGUI_API float igGetWindowWidth() { - return ImGui::GetWindowWidth(); -} -CIMGUI_API float igGetWindowHeight() { - return ImGui::GetWindowHeight(); -} -CIMGUI_API ImGuiViewport* igGetWindowViewport() { - return ImGui::GetWindowViewport(); -} -CIMGUI_API void igSetNextWindowPos(const ImVec2 pos, - ImGuiCond cond, - const ImVec2 pivot) { - return ImGui::SetNextWindowPos(pos, cond, pivot); -} -CIMGUI_API void igSetNextWindowSize(const ImVec2 size, ImGuiCond cond) { - return ImGui::SetNextWindowSize(size, cond); -} -CIMGUI_API void igSetNextWindowSizeConstraints( - const ImVec2 size_min, - const ImVec2 size_max, - ImGuiSizeCallback custom_callback, - void* custom_callback_data) { - return ImGui::SetNextWindowSizeConstraints( - size_min, size_max, custom_callback, custom_callback_data); -} -CIMGUI_API void igSetNextWindowContentSize(const ImVec2 size) { - return ImGui::SetNextWindowContentSize(size); -} -CIMGUI_API void igSetNextWindowCollapsed(bool collapsed, ImGuiCond cond) { - return ImGui::SetNextWindowCollapsed(collapsed, cond); -} -CIMGUI_API void igSetNextWindowFocus() { - return ImGui::SetNextWindowFocus(); -} -CIMGUI_API void igSetNextWindowScroll(const ImVec2 scroll) { - return ImGui::SetNextWindowScroll(scroll); -} -CIMGUI_API void igSetNextWindowBgAlpha(float alpha) { - return ImGui::SetNextWindowBgAlpha(alpha); -} -CIMGUI_API void igSetNextWindowViewport(ImGuiID viewport_id) { - return ImGui::SetNextWindowViewport(viewport_id); -} -CIMGUI_API void igSetWindowPos_Vec2(const ImVec2 pos, ImGuiCond cond) { - return ImGui::SetWindowPos(pos, cond); -} -CIMGUI_API void igSetWindowSize_Vec2(const ImVec2 size, ImGuiCond cond) { - return ImGui::SetWindowSize(size, cond); -} -CIMGUI_API void igSetWindowCollapsed_Bool(bool collapsed, ImGuiCond cond) { - return ImGui::SetWindowCollapsed(collapsed, cond); -} -CIMGUI_API void igSetWindowFocus_Nil() { - return ImGui::SetWindowFocus(); -} -CIMGUI_API void igSetWindowFontScale(float scale) { - return ImGui::SetWindowFontScale(scale); -} -CIMGUI_API void igSetWindowPos_Str(const char* name, - const ImVec2 pos, - ImGuiCond cond) { - return ImGui::SetWindowPos(name, pos, cond); -} -CIMGUI_API void igSetWindowSize_Str(const char* name, - const ImVec2 size, - ImGuiCond cond) { - return ImGui::SetWindowSize(name, size, cond); -} -CIMGUI_API void igSetWindowCollapsed_Str(const char* name, - bool collapsed, - ImGuiCond cond) { - return ImGui::SetWindowCollapsed(name, collapsed, cond); -} -CIMGUI_API void igSetWindowFocus_Str(const char* name) { - return ImGui::SetWindowFocus(name); -} -CIMGUI_API void igGetContentRegionAvail(ImVec2* pOut) { - *pOut = ImGui::GetContentRegionAvail(); -} -CIMGUI_API void igGetContentRegionMax(ImVec2* pOut) { - *pOut = ImGui::GetContentRegionMax(); -} -CIMGUI_API void igGetWindowContentRegionMin(ImVec2* pOut) { - *pOut = ImGui::GetWindowContentRegionMin(); -} -CIMGUI_API void igGetWindowContentRegionMax(ImVec2* pOut) { - *pOut = ImGui::GetWindowContentRegionMax(); -} -CIMGUI_API float igGetScrollX() { - return ImGui::GetScrollX(); -} -CIMGUI_API float igGetScrollY() { - return ImGui::GetScrollY(); -} -CIMGUI_API void igSetScrollX_Float(float scroll_x) { - return ImGui::SetScrollX(scroll_x); -} -CIMGUI_API void igSetScrollY_Float(float scroll_y) { - return ImGui::SetScrollY(scroll_y); -} -CIMGUI_API float igGetScrollMaxX() { - return ImGui::GetScrollMaxX(); -} -CIMGUI_API float igGetScrollMaxY() { - return ImGui::GetScrollMaxY(); -} -CIMGUI_API void igSetScrollHereX(float center_x_ratio) { - return ImGui::SetScrollHereX(center_x_ratio); -} -CIMGUI_API void igSetScrollHereY(float center_y_ratio) { - return ImGui::SetScrollHereY(center_y_ratio); -} -CIMGUI_API void igSetScrollFromPosX_Float(float local_x, float center_x_ratio) { - return ImGui::SetScrollFromPosX(local_x, center_x_ratio); -} -CIMGUI_API void igSetScrollFromPosY_Float(float local_y, float center_y_ratio) { - return ImGui::SetScrollFromPosY(local_y, center_y_ratio); -} -CIMGUI_API void igPushFont(ImFont* font) { - return ImGui::PushFont(font); -} -CIMGUI_API void igPopFont() { - return ImGui::PopFont(); -} -CIMGUI_API void igPushStyleColor_U32(ImGuiCol idx, ImU32 col) { - return ImGui::PushStyleColor(idx, col); -} -CIMGUI_API void igPushStyleColor_Vec4(ImGuiCol idx, const ImVec4 col) { - return ImGui::PushStyleColor(idx, col); -} -CIMGUI_API void igPopStyleColor(int count) { - return ImGui::PopStyleColor(count); -} -CIMGUI_API void igPushStyleVar_Float(ImGuiStyleVar idx, float val) { - return ImGui::PushStyleVar(idx, val); -} -CIMGUI_API void igPushStyleVar_Vec2(ImGuiStyleVar idx, const ImVec2 val) { - return ImGui::PushStyleVar(idx, val); -} -CIMGUI_API void igPopStyleVar(int count) { - return ImGui::PopStyleVar(count); -} -CIMGUI_API void igPushTabStop(bool tab_stop) { - return ImGui::PushTabStop(tab_stop); -} -CIMGUI_API void igPopTabStop() { - return ImGui::PopTabStop(); -} -CIMGUI_API void igPushButtonRepeat(bool repeat) { - return ImGui::PushButtonRepeat(repeat); -} -CIMGUI_API void igPopButtonRepeat() { - return ImGui::PopButtonRepeat(); -} -CIMGUI_API void igPushItemWidth(float item_width) { - return ImGui::PushItemWidth(item_width); -} -CIMGUI_API void igPopItemWidth() { - return ImGui::PopItemWidth(); -} -CIMGUI_API void igSetNextItemWidth(float item_width) { - return ImGui::SetNextItemWidth(item_width); -} -CIMGUI_API float igCalcItemWidth() { - return ImGui::CalcItemWidth(); -} -CIMGUI_API void igPushTextWrapPos(float wrap_local_pos_x) { - return ImGui::PushTextWrapPos(wrap_local_pos_x); -} -CIMGUI_API void igPopTextWrapPos() { - return ImGui::PopTextWrapPos(); -} -CIMGUI_API ImFont* igGetFont() { - return ImGui::GetFont(); -} -CIMGUI_API float igGetFontSize() { - return ImGui::GetFontSize(); -} -CIMGUI_API void igGetFontTexUvWhitePixel(ImVec2* pOut) { - *pOut = ImGui::GetFontTexUvWhitePixel(); -} -CIMGUI_API ImU32 igGetColorU32_Col(ImGuiCol idx, float alpha_mul) { - return ImGui::GetColorU32(idx, alpha_mul); -} -CIMGUI_API ImU32 igGetColorU32_Vec4(const ImVec4 col) { - return ImGui::GetColorU32(col); -} -CIMGUI_API ImU32 igGetColorU32_U32(ImU32 col, float alpha_mul) { - return ImGui::GetColorU32(col, alpha_mul); -} -CIMGUI_API const ImVec4* igGetStyleColorVec4(ImGuiCol idx) { - return &ImGui::GetStyleColorVec4(idx); -} -CIMGUI_API void igGetCursorScreenPos(ImVec2* pOut) { - *pOut = ImGui::GetCursorScreenPos(); -} -CIMGUI_API void igSetCursorScreenPos(const ImVec2 pos) { - return ImGui::SetCursorScreenPos(pos); -} -CIMGUI_API void igGetCursorPos(ImVec2* pOut) { - *pOut = ImGui::GetCursorPos(); -} -CIMGUI_API float igGetCursorPosX() { - return ImGui::GetCursorPosX(); -} -CIMGUI_API float igGetCursorPosY() { - return ImGui::GetCursorPosY(); -} -CIMGUI_API void igSetCursorPos(const ImVec2 local_pos) { - return ImGui::SetCursorPos(local_pos); -} -CIMGUI_API void igSetCursorPosX(float local_x) { - return ImGui::SetCursorPosX(local_x); -} -CIMGUI_API void igSetCursorPosY(float local_y) { - return ImGui::SetCursorPosY(local_y); -} -CIMGUI_API void igGetCursorStartPos(ImVec2* pOut) { - *pOut = ImGui::GetCursorStartPos(); -} -CIMGUI_API void igSeparator() { - return ImGui::Separator(); -} -CIMGUI_API void igSameLine(float offset_from_start_x, float spacing) { - return ImGui::SameLine(offset_from_start_x, spacing); -} -CIMGUI_API void igNewLine() { - return ImGui::NewLine(); -} -CIMGUI_API void igSpacing() { - return ImGui::Spacing(); -} -CIMGUI_API void igDummy(const ImVec2 size) { - return ImGui::Dummy(size); -} -CIMGUI_API void igIndent(float indent_w) { - return ImGui::Indent(indent_w); -} -CIMGUI_API void igUnindent(float indent_w) { - return ImGui::Unindent(indent_w); -} -CIMGUI_API void igBeginGroup() { - return ImGui::BeginGroup(); -} -CIMGUI_API void igEndGroup() { - return ImGui::EndGroup(); -} -CIMGUI_API void igAlignTextToFramePadding() { - return ImGui::AlignTextToFramePadding(); -} -CIMGUI_API float igGetTextLineHeight() { - return ImGui::GetTextLineHeight(); -} -CIMGUI_API float igGetTextLineHeightWithSpacing() { - return ImGui::GetTextLineHeightWithSpacing(); -} -CIMGUI_API float igGetFrameHeight() { - return ImGui::GetFrameHeight(); -} -CIMGUI_API float igGetFrameHeightWithSpacing() { - return ImGui::GetFrameHeightWithSpacing(); -} -CIMGUI_API void igPushID_Str(const char* str_id) { - return ImGui::PushID(str_id); -} -CIMGUI_API void igPushID_StrStr(const char* str_id_begin, - const char* str_id_end) { - return ImGui::PushID(str_id_begin, str_id_end); -} -CIMGUI_API void igPushID_Ptr(const void* ptr_id) { - return ImGui::PushID(ptr_id); -} -CIMGUI_API void igPushID_Int(int int_id) { - return ImGui::PushID(int_id); -} -CIMGUI_API void igPopID() { - return ImGui::PopID(); -} -CIMGUI_API ImGuiID igGetID_Str(const char* str_id) { - return ImGui::GetID(str_id); -} -CIMGUI_API ImGuiID igGetID_StrStr(const char* str_id_begin, - const char* str_id_end) { - return ImGui::GetID(str_id_begin, str_id_end); -} -CIMGUI_API ImGuiID igGetID_Ptr(const void* ptr_id) { - return ImGui::GetID(ptr_id); -} -CIMGUI_API void igTextUnformatted(const char* text, const char* text_end) { - return ImGui::TextUnformatted(text, text_end); -} -CIMGUI_API void igText(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextV(fmt, args); - va_end(args); -} -CIMGUI_API void igTextV(const char* fmt, va_list args) { - return ImGui::TextV(fmt, args); -} -CIMGUI_API void igTextColored(const ImVec4 col, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextColoredV(col, fmt, args); - va_end(args); -} -CIMGUI_API void igTextColoredV(const ImVec4 col, - const char* fmt, - va_list args) { - return ImGui::TextColoredV(col, fmt, args); -} -CIMGUI_API void igTextDisabled(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextDisabledV(fmt, args); - va_end(args); -} -CIMGUI_API void igTextDisabledV(const char* fmt, va_list args) { - return ImGui::TextDisabledV(fmt, args); -} -CIMGUI_API void igTextWrapped(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::TextWrappedV(fmt, args); - va_end(args); -} -CIMGUI_API void igTextWrappedV(const char* fmt, va_list args) { - return ImGui::TextWrappedV(fmt, args); -} -CIMGUI_API void igLabelText(const char* label, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::LabelTextV(label, fmt, args); - va_end(args); -} -CIMGUI_API void igLabelTextV(const char* label, const char* fmt, va_list args) { - return ImGui::LabelTextV(label, fmt, args); -} -CIMGUI_API void igBulletText(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::BulletTextV(fmt, args); - va_end(args); -} -CIMGUI_API void igBulletTextV(const char* fmt, va_list args) { - return ImGui::BulletTextV(fmt, args); -} -CIMGUI_API void igSeparatorText(const char* label) { - return ImGui::SeparatorText(label); -} -CIMGUI_API bool igButton(const char* label, const ImVec2 size) { - return ImGui::Button(label, size); -} -CIMGUI_API bool igSmallButton(const char* label) { - return ImGui::SmallButton(label); -} -CIMGUI_API bool igInvisibleButton(const char* str_id, - const ImVec2 size, - ImGuiButtonFlags flags) { - return ImGui::InvisibleButton(str_id, size, flags); -} -CIMGUI_API bool igArrowButton(const char* str_id, ImGuiDir dir) { - return ImGui::ArrowButton(str_id, dir); -} -CIMGUI_API bool igCheckbox(const char* label, bool* v) { - return ImGui::Checkbox(label, v); -} -CIMGUI_API bool igCheckboxFlags_IntPtr(const char* label, - int* flags, - int flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igCheckboxFlags_UintPtr(const char* label, - unsigned int* flags, - unsigned int flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igRadioButton_Bool(const char* label, bool active) { - return ImGui::RadioButton(label, active); -} -CIMGUI_API bool igRadioButton_IntPtr(const char* label, int* v, int v_button) { - return ImGui::RadioButton(label, v, v_button); -} -CIMGUI_API void igProgressBar(float fraction, - const ImVec2 size_arg, - const char* overlay) { - return ImGui::ProgressBar(fraction, size_arg, overlay); -} -CIMGUI_API void igBullet() { - return ImGui::Bullet(); -} -CIMGUI_API void igImage(ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 tint_col, - const ImVec4 border_col) { - return ImGui::Image(user_texture_id, image_size, uv0, uv1, tint_col, - border_col); -} -CIMGUI_API bool igImageButton(const char* str_id, - ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col) { - return ImGui::ImageButton(str_id, user_texture_id, image_size, uv0, uv1, - bg_col, tint_col); -} -CIMGUI_API bool igBeginCombo(const char* label, - const char* preview_value, - ImGuiComboFlags flags) { - return ImGui::BeginCombo(label, preview_value, flags); -} -CIMGUI_API void igEndCombo() { - return ImGui::EndCombo(); -} -CIMGUI_API bool igCombo_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int popup_max_height_in_items) { - return ImGui::Combo(label, current_item, items, items_count, - popup_max_height_in_items); -} -CIMGUI_API bool igCombo_Str(const char* label, - int* current_item, - const char* items_separated_by_zeros, - int popup_max_height_in_items) { - return ImGui::Combo(label, current_item, items_separated_by_zeros, - popup_max_height_in_items); -} -CIMGUI_API bool igCombo_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int popup_max_height_in_items) { - return ImGui::Combo(label, current_item, getter, user_data, items_count, - popup_max_height_in_items); -} -CIMGUI_API bool igDragFloat(const char* label, - float* v, - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloat2(const char* label, - float v[2], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat2(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloat3(const char* label, - float v[3], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat3(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloat4(const char* label, - float v[4], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragFloat4(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragFloatRange2(const char* label, - float* v_current_min, - float* v_current_max, - float v_speed, - float v_min, - float v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags) { - return ImGui::DragFloatRange2(label, v_current_min, v_current_max, v_speed, - v_min, v_max, format, format_max, flags); -} -CIMGUI_API bool igDragInt(const char* label, - int* v, - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragInt2(const char* label, - int v[2], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt2(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragInt3(const char* label, - int v[3], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt3(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragInt4(const char* label, - int v[4], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragInt4(label, v, v_speed, v_min, v_max, format, flags); -} -CIMGUI_API bool igDragIntRange2(const char* label, - int* v_current_min, - int* v_current_max, - float v_speed, - int v_min, - int v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags) { - return ImGui::DragIntRange2(label, v_current_min, v_current_max, v_speed, - v_min, v_max, format, format_max, flags); -} -CIMGUI_API bool igDragScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragScalar(label, data_type, p_data, v_speed, p_min, p_max, - format, flags); -} -CIMGUI_API bool igDragScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragScalarN(label, data_type, p_data, components, v_speed, - p_min, p_max, format, flags); -} -CIMGUI_API bool igSliderFloat(const char* label, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderFloat2(const char* label, - float v[2], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat2(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderFloat3(const char* label, - float v[3], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat3(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderFloat4(const char* label, - float v[4], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderFloat4(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderAngle(const char* label, - float* v_rad, - float v_degrees_min, - float v_degrees_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderAngle(label, v_rad, v_degrees_min, v_degrees_max, format, - flags); -} -CIMGUI_API bool igSliderInt(const char* label, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderInt2(const char* label, - int v[2], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt2(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderInt3(const char* label, - int v[3], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt3(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderInt4(const char* label, - int v[4], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderInt4(label, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igSliderScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderScalar(label, data_type, p_data, p_min, p_max, format, - flags); -} -CIMGUI_API bool igSliderScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::SliderScalarN(label, data_type, p_data, components, p_min, - p_max, format, flags); -} -CIMGUI_API bool igVSliderFloat(const char* label, - const ImVec2 size, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::VSliderFloat(label, size, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igVSliderInt(const char* label, - const ImVec2 size, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::VSliderInt(label, size, v, v_min, v_max, format, flags); -} -CIMGUI_API bool igVSliderScalar(const char* label, - const ImVec2 size, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::VSliderScalar(label, size, data_type, p_data, p_min, p_max, - format, flags); -} -CIMGUI_API bool igInputText(const char* label, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputText(label, buf, buf_size, flags, callback, user_data); -} -CIMGUI_API bool igInputTextMultiline(const char* label, - char* buf, - size_t buf_size, - const ImVec2 size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputTextMultiline(label, buf, buf_size, size, flags, callback, - user_data); -} -CIMGUI_API bool igInputTextWithHint(const char* label, - const char* hint, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputTextWithHint(label, hint, buf, buf_size, flags, callback, - user_data); -} -CIMGUI_API bool igInputFloat(const char* label, - float* v, - float step, - float step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat(label, v, step, step_fast, format, flags); -} -CIMGUI_API bool igInputFloat2(const char* label, - float v[2], - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat2(label, v, format, flags); -} -CIMGUI_API bool igInputFloat3(const char* label, - float v[3], - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat3(label, v, format, flags); -} -CIMGUI_API bool igInputFloat4(const char* label, - float v[4], - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputFloat4(label, v, format, flags); -} -CIMGUI_API bool igInputInt(const char* label, - int* v, - int step, - int step_fast, - ImGuiInputTextFlags flags) { - return ImGui::InputInt(label, v, step, step_fast, flags); -} -CIMGUI_API bool igInputInt2(const char* label, - int v[2], - ImGuiInputTextFlags flags) { - return ImGui::InputInt2(label, v, flags); -} -CIMGUI_API bool igInputInt3(const char* label, - int v[3], - ImGuiInputTextFlags flags) { - return ImGui::InputInt3(label, v, flags); -} -CIMGUI_API bool igInputInt4(const char* label, - int v[4], - ImGuiInputTextFlags flags) { - return ImGui::InputInt4(label, v, flags); -} -CIMGUI_API bool igInputDouble(const char* label, - double* v, - double step, - double step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputDouble(label, v, step, step_fast, format, flags); -} -CIMGUI_API bool igInputScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputScalar(label, data_type, p_data, p_step, p_step_fast, - format, flags); -} -CIMGUI_API bool igInputScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags) { - return ImGui::InputScalarN(label, data_type, p_data, components, p_step, - p_step_fast, format, flags); -} -CIMGUI_API bool igColorEdit3(const char* label, - float col[3], - ImGuiColorEditFlags flags) { - return ImGui::ColorEdit3(label, col, flags); -} -CIMGUI_API bool igColorEdit4(const char* label, - float col[4], - ImGuiColorEditFlags flags) { - return ImGui::ColorEdit4(label, col, flags); -} -CIMGUI_API bool igColorPicker3(const char* label, - float col[3], - ImGuiColorEditFlags flags) { - return ImGui::ColorPicker3(label, col, flags); -} -CIMGUI_API bool igColorPicker4(const char* label, - float col[4], - ImGuiColorEditFlags flags, - const float* ref_col) { - return ImGui::ColorPicker4(label, col, flags, ref_col); -} -CIMGUI_API bool igColorButton(const char* desc_id, - const ImVec4 col, - ImGuiColorEditFlags flags, - const ImVec2 size) { - return ImGui::ColorButton(desc_id, col, flags, size); -} -CIMGUI_API void igSetColorEditOptions(ImGuiColorEditFlags flags) { - return ImGui::SetColorEditOptions(flags); -} -CIMGUI_API bool igTreeNode_Str(const char* label) { - return ImGui::TreeNode(label); -} -CIMGUI_API bool igTreeNode_StrStr(const char* str_id, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeV(str_id, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNode_Ptr(const void* ptr_id, const char* fmt, ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeV(ptr_id, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNodeV_Str(const char* str_id, - const char* fmt, - va_list args) { - return ImGui::TreeNodeV(str_id, fmt, args); -} -CIMGUI_API bool igTreeNodeV_Ptr(const void* ptr_id, - const char* fmt, - va_list args) { - return ImGui::TreeNodeV(ptr_id, fmt, args); -} -CIMGUI_API bool igTreeNodeEx_Str(const char* label, ImGuiTreeNodeFlags flags) { - return ImGui::TreeNodeEx(label, flags); -} -CIMGUI_API bool igTreeNodeEx_StrStr(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeExV(str_id, flags, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNodeEx_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - bool ret = ImGui::TreeNodeExV(ptr_id, flags, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API bool igTreeNodeExV_Str(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args) { - return ImGui::TreeNodeExV(str_id, flags, fmt, args); -} -CIMGUI_API bool igTreeNodeExV_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args) { - return ImGui::TreeNodeExV(ptr_id, flags, fmt, args); -} -CIMGUI_API void igTreePush_Str(const char* str_id) { - return ImGui::TreePush(str_id); -} -CIMGUI_API void igTreePush_Ptr(const void* ptr_id) { - return ImGui::TreePush(ptr_id); -} -CIMGUI_API void igTreePop() { - return ImGui::TreePop(); -} -CIMGUI_API float igGetTreeNodeToLabelSpacing() { - return ImGui::GetTreeNodeToLabelSpacing(); -} -CIMGUI_API bool igCollapsingHeader_TreeNodeFlags(const char* label, - ImGuiTreeNodeFlags flags) { - return ImGui::CollapsingHeader(label, flags); -} -CIMGUI_API bool igCollapsingHeader_BoolPtr(const char* label, - bool* p_visible, - ImGuiTreeNodeFlags flags) { - return ImGui::CollapsingHeader(label, p_visible, flags); -} -CIMGUI_API void igSetNextItemOpen(bool is_open, ImGuiCond cond) { - return ImGui::SetNextItemOpen(is_open, cond); -} -CIMGUI_API bool igSelectable_Bool(const char* label, - bool selected, - ImGuiSelectableFlags flags, - const ImVec2 size) { - return ImGui::Selectable(label, selected, flags, size); -} -CIMGUI_API bool igSelectable_BoolPtr(const char* label, - bool* p_selected, - ImGuiSelectableFlags flags, - const ImVec2 size) { - return ImGui::Selectable(label, p_selected, flags, size); -} -CIMGUI_API bool igBeginListBox(const char* label, const ImVec2 size) { - return ImGui::BeginListBox(label, size); -} -CIMGUI_API void igEndListBox() { - return ImGui::EndListBox(); -} -CIMGUI_API bool igListBox_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int height_in_items) { - return ImGui::ListBox(label, current_item, items, items_count, - height_in_items); -} -CIMGUI_API bool igListBox_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int height_in_items) { - return ImGui::ListBox(label, current_item, getter, user_data, items_count, - height_in_items); -} -CIMGUI_API void igPlotLines_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride) { - return ImGui::PlotLines(label, values, values_count, values_offset, - overlay_text, scale_min, scale_max, graph_size, - stride); -} -CIMGUI_API void igPlotLines_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size) { - return ImGui::PlotLines(label, values_getter, data, values_count, - values_offset, overlay_text, scale_min, scale_max, - graph_size); -} -CIMGUI_API void igPlotHistogram_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride) { - return ImGui::PlotHistogram(label, values, values_count, values_offset, - overlay_text, scale_min, scale_max, graph_size, - stride); -} -CIMGUI_API void igPlotHistogram_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size) { - return ImGui::PlotHistogram(label, values_getter, data, values_count, - values_offset, overlay_text, scale_min, scale_max, - graph_size); -} -CIMGUI_API void igValue_Bool(const char* prefix, bool b) { - return ImGui::Value(prefix, b); -} -CIMGUI_API void igValue_Int(const char* prefix, int v) { - return ImGui::Value(prefix, v); -} -CIMGUI_API void igValue_Uint(const char* prefix, unsigned int v) { - return ImGui::Value(prefix, v); -} -CIMGUI_API void igValue_Float(const char* prefix, - float v, - const char* float_format) { - return ImGui::Value(prefix, v, float_format); -} -CIMGUI_API bool igBeginMenuBar() { - return ImGui::BeginMenuBar(); -} -CIMGUI_API void igEndMenuBar() { - return ImGui::EndMenuBar(); -} -CIMGUI_API bool igBeginMainMenuBar() { - return ImGui::BeginMainMenuBar(); -} -CIMGUI_API void igEndMainMenuBar() { - return ImGui::EndMainMenuBar(); -} -CIMGUI_API bool igBeginMenu(const char* label, bool enabled) { - return ImGui::BeginMenu(label, enabled); -} -CIMGUI_API void igEndMenu() { - return ImGui::EndMenu(); -} -CIMGUI_API bool igMenuItem_Bool(const char* label, - const char* shortcut, - bool selected, - bool enabled) { - return ImGui::MenuItem(label, shortcut, selected, enabled); -} -CIMGUI_API bool igMenuItem_BoolPtr(const char* label, - const char* shortcut, - bool* p_selected, - bool enabled) { - return ImGui::MenuItem(label, shortcut, p_selected, enabled); -} -CIMGUI_API bool igBeginTooltip() { - return ImGui::BeginTooltip(); -} -CIMGUI_API void igEndTooltip() { - return ImGui::EndTooltip(); -} -CIMGUI_API void igSetTooltip(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::SetTooltipV(fmt, args); - va_end(args); -} -CIMGUI_API void igSetTooltipV(const char* fmt, va_list args) { - return ImGui::SetTooltipV(fmt, args); -} -CIMGUI_API bool igBeginItemTooltip() { - return ImGui::BeginItemTooltip(); -} -CIMGUI_API void igSetItemTooltip(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::SetItemTooltipV(fmt, args); - va_end(args); -} -CIMGUI_API void igSetItemTooltipV(const char* fmt, va_list args) { - return ImGui::SetItemTooltipV(fmt, args); -} -CIMGUI_API bool igBeginPopup(const char* str_id, ImGuiWindowFlags flags) { - return ImGui::BeginPopup(str_id, flags); -} -CIMGUI_API bool igBeginPopupModal(const char* name, - bool* p_open, - ImGuiWindowFlags flags) { - return ImGui::BeginPopupModal(name, p_open, flags); -} -CIMGUI_API void igEndPopup() { - return ImGui::EndPopup(); -} -CIMGUI_API void igOpenPopup_Str(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopup(str_id, popup_flags); -} -CIMGUI_API void igOpenPopup_ID(ImGuiID id, ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopup(id, popup_flags); -} -CIMGUI_API void igOpenPopupOnItemClick(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopupOnItemClick(str_id, popup_flags); -} -CIMGUI_API void igCloseCurrentPopup() { - return ImGui::CloseCurrentPopup(); -} -CIMGUI_API bool igBeginPopupContextItem(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::BeginPopupContextItem(str_id, popup_flags); -} -CIMGUI_API bool igBeginPopupContextWindow(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::BeginPopupContextWindow(str_id, popup_flags); -} -CIMGUI_API bool igBeginPopupContextVoid(const char* str_id, - ImGuiPopupFlags popup_flags) { - return ImGui::BeginPopupContextVoid(str_id, popup_flags); -} -CIMGUI_API bool igIsPopupOpen_Str(const char* str_id, ImGuiPopupFlags flags) { - return ImGui::IsPopupOpen(str_id, flags); -} -CIMGUI_API bool igBeginTable(const char* str_id, - int column, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width) { - return ImGui::BeginTable(str_id, column, flags, outer_size, inner_width); -} -CIMGUI_API void igEndTable() { - return ImGui::EndTable(); -} -CIMGUI_API void igTableNextRow(ImGuiTableRowFlags row_flags, - float min_row_height) { - return ImGui::TableNextRow(row_flags, min_row_height); -} -CIMGUI_API bool igTableNextColumn() { - return ImGui::TableNextColumn(); -} -CIMGUI_API bool igTableSetColumnIndex(int column_n) { - return ImGui::TableSetColumnIndex(column_n); -} -CIMGUI_API void igTableSetupColumn(const char* label, - ImGuiTableColumnFlags flags, - float init_width_or_weight, - ImGuiID user_id) { - return ImGui::TableSetupColumn(label, flags, init_width_or_weight, user_id); -} -CIMGUI_API void igTableSetupScrollFreeze(int cols, int rows) { - return ImGui::TableSetupScrollFreeze(cols, rows); -} -CIMGUI_API void igTableHeader(const char* label) { - return ImGui::TableHeader(label); -} -CIMGUI_API void igTableHeadersRow() { - return ImGui::TableHeadersRow(); -} -CIMGUI_API void igTableAngledHeadersRow() { - return ImGui::TableAngledHeadersRow(); -} -CIMGUI_API ImGuiTableSortSpecs* igTableGetSortSpecs() { - return ImGui::TableGetSortSpecs(); -} -CIMGUI_API int igTableGetColumnCount() { - return ImGui::TableGetColumnCount(); -} -CIMGUI_API int igTableGetColumnIndex() { - return ImGui::TableGetColumnIndex(); -} -CIMGUI_API int igTableGetRowIndex() { - return ImGui::TableGetRowIndex(); -} -CIMGUI_API const char* igTableGetColumnName_Int(int column_n) { - return ImGui::TableGetColumnName(column_n); -} -CIMGUI_API ImGuiTableColumnFlags igTableGetColumnFlags(int column_n) { - return ImGui::TableGetColumnFlags(column_n); -} -CIMGUI_API void igTableSetColumnEnabled(int column_n, bool v) { - return ImGui::TableSetColumnEnabled(column_n, v); -} -CIMGUI_API void igTableSetBgColor(ImGuiTableBgTarget target, - ImU32 color, - int column_n) { - return ImGui::TableSetBgColor(target, color, column_n); -} -CIMGUI_API void igColumns(int count, const char* id, bool border) { - return ImGui::Columns(count, id, border); -} -CIMGUI_API void igNextColumn() { - return ImGui::NextColumn(); -} -CIMGUI_API int igGetColumnIndex() { - return ImGui::GetColumnIndex(); -} -CIMGUI_API float igGetColumnWidth(int column_index) { - return ImGui::GetColumnWidth(column_index); -} -CIMGUI_API void igSetColumnWidth(int column_index, float width) { - return ImGui::SetColumnWidth(column_index, width); -} -CIMGUI_API float igGetColumnOffset(int column_index) { - return ImGui::GetColumnOffset(column_index); -} -CIMGUI_API void igSetColumnOffset(int column_index, float offset_x) { - return ImGui::SetColumnOffset(column_index, offset_x); -} -CIMGUI_API int igGetColumnsCount() { - return ImGui::GetColumnsCount(); -} -CIMGUI_API bool igBeginTabBar(const char* str_id, ImGuiTabBarFlags flags) { - return ImGui::BeginTabBar(str_id, flags); -} -CIMGUI_API void igEndTabBar() { - return ImGui::EndTabBar(); -} -CIMGUI_API bool igBeginTabItem(const char* label, - bool* p_open, - ImGuiTabItemFlags flags) { - return ImGui::BeginTabItem(label, p_open, flags); -} -CIMGUI_API void igEndTabItem() { - return ImGui::EndTabItem(); -} -CIMGUI_API bool igTabItemButton(const char* label, ImGuiTabItemFlags flags) { - return ImGui::TabItemButton(label, flags); -} -CIMGUI_API void igSetTabItemClosed(const char* tab_or_docked_window_label) { - return ImGui::SetTabItemClosed(tab_or_docked_window_label); -} -CIMGUI_API ImGuiID igDockSpace(ImGuiID id, - const ImVec2 size, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class) { - return ImGui::DockSpace(id, size, flags, window_class); -} -CIMGUI_API ImGuiID -igDockSpaceOverViewport(const ImGuiViewport* viewport, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class) { - return ImGui::DockSpaceOverViewport(viewport, flags, window_class); -} -CIMGUI_API void igSetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond) { - return ImGui::SetNextWindowDockID(dock_id, cond); -} -CIMGUI_API void igSetNextWindowClass(const ImGuiWindowClass* window_class) { - return ImGui::SetNextWindowClass(window_class); -} -CIMGUI_API ImGuiID igGetWindowDockID() { - return ImGui::GetWindowDockID(); -} -CIMGUI_API bool igIsWindowDocked() { - return ImGui::IsWindowDocked(); -} -CIMGUI_API void igLogToTTY(int auto_open_depth) { - return ImGui::LogToTTY(auto_open_depth); -} -CIMGUI_API void igLogToFile(int auto_open_depth, const char* filename) { - return ImGui::LogToFile(auto_open_depth, filename); -} -CIMGUI_API void igLogToClipboard(int auto_open_depth) { - return ImGui::LogToClipboard(auto_open_depth); -} -CIMGUI_API void igLogFinish() { - return ImGui::LogFinish(); -} -CIMGUI_API void igLogButtons() { - return ImGui::LogButtons(); -} -CIMGUI_API void igLogTextV(const char* fmt, va_list args) { - return ImGui::LogTextV(fmt, args); -} -CIMGUI_API bool igBeginDragDropSource(ImGuiDragDropFlags flags) { - return ImGui::BeginDragDropSource(flags); -} -CIMGUI_API bool igSetDragDropPayload(const char* type, - const void* data, - size_t sz, - ImGuiCond cond) { - return ImGui::SetDragDropPayload(type, data, sz, cond); -} -CIMGUI_API void igEndDragDropSource() { - return ImGui::EndDragDropSource(); -} -CIMGUI_API bool igBeginDragDropTarget() { - return ImGui::BeginDragDropTarget(); -} -CIMGUI_API const ImGuiPayload* igAcceptDragDropPayload( - const char* type, - ImGuiDragDropFlags flags) { - return ImGui::AcceptDragDropPayload(type, flags); -} -CIMGUI_API void igEndDragDropTarget() { - return ImGui::EndDragDropTarget(); -} -CIMGUI_API const ImGuiPayload* igGetDragDropPayload() { - return ImGui::GetDragDropPayload(); -} -CIMGUI_API void igBeginDisabled(bool disabled) { - return ImGui::BeginDisabled(disabled); -} -CIMGUI_API void igEndDisabled() { - return ImGui::EndDisabled(); -} -CIMGUI_API void igPushClipRect(const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect) { - return ImGui::PushClipRect(clip_rect_min, clip_rect_max, - intersect_with_current_clip_rect); -} -CIMGUI_API void igPopClipRect() { - return ImGui::PopClipRect(); -} -CIMGUI_API void igSetItemDefaultFocus() { - return ImGui::SetItemDefaultFocus(); -} -CIMGUI_API void igSetKeyboardFocusHere(int offset) { - return ImGui::SetKeyboardFocusHere(offset); -} -CIMGUI_API void igSetNextItemAllowOverlap() { - return ImGui::SetNextItemAllowOverlap(); -} -CIMGUI_API bool igIsItemHovered(ImGuiHoveredFlags flags) { - return ImGui::IsItemHovered(flags); -} -CIMGUI_API bool igIsItemActive() { - return ImGui::IsItemActive(); -} -CIMGUI_API bool igIsItemFocused() { - return ImGui::IsItemFocused(); -} -CIMGUI_API bool igIsItemClicked(ImGuiMouseButton mouse_button) { - return ImGui::IsItemClicked(mouse_button); -} -CIMGUI_API bool igIsItemVisible() { - return ImGui::IsItemVisible(); -} -CIMGUI_API bool igIsItemEdited() { - return ImGui::IsItemEdited(); -} -CIMGUI_API bool igIsItemActivated() { - return ImGui::IsItemActivated(); -} -CIMGUI_API bool igIsItemDeactivated() { - return ImGui::IsItemDeactivated(); -} -CIMGUI_API bool igIsItemDeactivatedAfterEdit() { - return ImGui::IsItemDeactivatedAfterEdit(); -} -CIMGUI_API bool igIsItemToggledOpen() { - return ImGui::IsItemToggledOpen(); -} -CIMGUI_API bool igIsAnyItemHovered() { - return ImGui::IsAnyItemHovered(); -} -CIMGUI_API bool igIsAnyItemActive() { - return ImGui::IsAnyItemActive(); -} -CIMGUI_API bool igIsAnyItemFocused() { - return ImGui::IsAnyItemFocused(); -} -CIMGUI_API ImGuiID igGetItemID() { - return ImGui::GetItemID(); -} -CIMGUI_API void igGetItemRectMin(ImVec2* pOut) { - *pOut = ImGui::GetItemRectMin(); -} -CIMGUI_API void igGetItemRectMax(ImVec2* pOut) { - *pOut = ImGui::GetItemRectMax(); -} -CIMGUI_API void igGetItemRectSize(ImVec2* pOut) { - *pOut = ImGui::GetItemRectSize(); -} -CIMGUI_API ImGuiViewport* igGetMainViewport() { - return ImGui::GetMainViewport(); -} -CIMGUI_API ImDrawList* igGetBackgroundDrawList_Nil() { - return ImGui::GetBackgroundDrawList(); -} -CIMGUI_API ImDrawList* igGetForegroundDrawList_Nil() { - return ImGui::GetForegroundDrawList(); -} -CIMGUI_API ImDrawList* igGetBackgroundDrawList_ViewportPtr( - ImGuiViewport* viewport) { - return ImGui::GetBackgroundDrawList(viewport); -} -CIMGUI_API ImDrawList* igGetForegroundDrawList_ViewportPtr( - ImGuiViewport* viewport) { - return ImGui::GetForegroundDrawList(viewport); -} -CIMGUI_API bool igIsRectVisible_Nil(const ImVec2 size) { - return ImGui::IsRectVisible(size); -} -CIMGUI_API bool igIsRectVisible_Vec2(const ImVec2 rect_min, - const ImVec2 rect_max) { - return ImGui::IsRectVisible(rect_min, rect_max); -} -CIMGUI_API double igGetTime() { - return ImGui::GetTime(); -} -CIMGUI_API int igGetFrameCount() { - return ImGui::GetFrameCount(); -} -CIMGUI_API ImDrawListSharedData* igGetDrawListSharedData() { - return ImGui::GetDrawListSharedData(); -} -CIMGUI_API const char* igGetStyleColorName(ImGuiCol idx) { - return ImGui::GetStyleColorName(idx); -} -CIMGUI_API void igSetStateStorage(ImGuiStorage* storage) { - return ImGui::SetStateStorage(storage); -} -CIMGUI_API ImGuiStorage* igGetStateStorage() { - return ImGui::GetStateStorage(); -} -CIMGUI_API void igCalcTextSize(ImVec2* pOut, - const char* text, - const char* text_end, - bool hide_text_after_double_hash, - float wrap_width) { - *pOut = ImGui::CalcTextSize(text, text_end, hide_text_after_double_hash, - wrap_width); -} -CIMGUI_API void igColorConvertU32ToFloat4(ImVec4* pOut, ImU32 in) { - *pOut = ImGui::ColorConvertU32ToFloat4(in); -} -CIMGUI_API ImU32 igColorConvertFloat4ToU32(const ImVec4 in) { - return ImGui::ColorConvertFloat4ToU32(in); -} -CIMGUI_API void igColorConvertRGBtoHSV(float r, - float g, - float b, - float* out_h, - float* out_s, - float* out_v) { - return ImGui::ColorConvertRGBtoHSV(r, g, b, *out_h, *out_s, *out_v); -} -CIMGUI_API void igColorConvertHSVtoRGB(float h, - float s, - float v, - float* out_r, - float* out_g, - float* out_b) { - return ImGui::ColorConvertHSVtoRGB(h, s, v, *out_r, *out_g, *out_b); -} -CIMGUI_API bool igIsKeyDown_Nil(ImGuiKey key) { - return ImGui::IsKeyDown(key); -} -CIMGUI_API bool igIsKeyPressed_Bool(ImGuiKey key, bool repeat) { - return ImGui::IsKeyPressed(key, repeat); -} -CIMGUI_API bool igIsKeyReleased_Nil(ImGuiKey key) { - return ImGui::IsKeyReleased(key); -} -CIMGUI_API bool igIsKeyChordPressed_Nil(ImGuiKeyChord key_chord) { - return ImGui::IsKeyChordPressed(key_chord); -} -CIMGUI_API int igGetKeyPressedAmount(ImGuiKey key, - float repeat_delay, - float rate) { - return ImGui::GetKeyPressedAmount(key, repeat_delay, rate); -} -CIMGUI_API const char* igGetKeyName(ImGuiKey key) { - return ImGui::GetKeyName(key); -} -CIMGUI_API void igSetNextFrameWantCaptureKeyboard(bool want_capture_keyboard) { - return ImGui::SetNextFrameWantCaptureKeyboard(want_capture_keyboard); -} -CIMGUI_API bool igIsMouseDown_Nil(ImGuiMouseButton button) { - return ImGui::IsMouseDown(button); -} -CIMGUI_API bool igIsMouseClicked_Bool(ImGuiMouseButton button, bool repeat) { - return ImGui::IsMouseClicked(button, repeat); -} -CIMGUI_API bool igIsMouseReleased_Nil(ImGuiMouseButton button) { - return ImGui::IsMouseReleased(button); -} -CIMGUI_API bool igIsMouseDoubleClicked_Nil(ImGuiMouseButton button) { - return ImGui::IsMouseDoubleClicked(button); -} -CIMGUI_API int igGetMouseClickedCount(ImGuiMouseButton button) { - return ImGui::GetMouseClickedCount(button); -} -CIMGUI_API bool igIsMouseHoveringRect(const ImVec2 r_min, - const ImVec2 r_max, - bool clip) { - return ImGui::IsMouseHoveringRect(r_min, r_max, clip); -} -CIMGUI_API bool igIsMousePosValid(const ImVec2* mouse_pos) { - return ImGui::IsMousePosValid(mouse_pos); -} -CIMGUI_API bool igIsAnyMouseDown() { - return ImGui::IsAnyMouseDown(); -} -CIMGUI_API void igGetMousePos(ImVec2* pOut) { - *pOut = ImGui::GetMousePos(); -} -CIMGUI_API void igGetMousePosOnOpeningCurrentPopup(ImVec2* pOut) { - *pOut = ImGui::GetMousePosOnOpeningCurrentPopup(); -} -CIMGUI_API bool igIsMouseDragging(ImGuiMouseButton button, - float lock_threshold) { - return ImGui::IsMouseDragging(button, lock_threshold); -} -CIMGUI_API void igGetMouseDragDelta(ImVec2* pOut, - ImGuiMouseButton button, - float lock_threshold) { - *pOut = ImGui::GetMouseDragDelta(button, lock_threshold); -} -CIMGUI_API void igResetMouseDragDelta(ImGuiMouseButton button) { - return ImGui::ResetMouseDragDelta(button); -} -CIMGUI_API ImGuiMouseCursor igGetMouseCursor() { - return ImGui::GetMouseCursor(); -} -CIMGUI_API void igSetMouseCursor(ImGuiMouseCursor cursor_type) { - return ImGui::SetMouseCursor(cursor_type); -} -CIMGUI_API void igSetNextFrameWantCaptureMouse(bool want_capture_mouse) { - return ImGui::SetNextFrameWantCaptureMouse(want_capture_mouse); -} -CIMGUI_API const char* igGetClipboardText() { - return ImGui::GetClipboardText(); -} -CIMGUI_API void igSetClipboardText(const char* text) { - return ImGui::SetClipboardText(text); -} -CIMGUI_API void igLoadIniSettingsFromDisk(const char* ini_filename) { - return ImGui::LoadIniSettingsFromDisk(ini_filename); -} -CIMGUI_API void igLoadIniSettingsFromMemory(const char* ini_data, - size_t ini_size) { - return ImGui::LoadIniSettingsFromMemory(ini_data, ini_size); -} -CIMGUI_API void igSaveIniSettingsToDisk(const char* ini_filename) { - return ImGui::SaveIniSettingsToDisk(ini_filename); -} -CIMGUI_API const char* igSaveIniSettingsToMemory(size_t* out_ini_size) { - return ImGui::SaveIniSettingsToMemory(out_ini_size); -} -CIMGUI_API void igDebugTextEncoding(const char* text) { - return ImGui::DebugTextEncoding(text); -} -CIMGUI_API void igDebugFlashStyleColor(ImGuiCol idx) { - return ImGui::DebugFlashStyleColor(idx); -} -CIMGUI_API void igDebugStartItemPicker() { - return ImGui::DebugStartItemPicker(); -} -CIMGUI_API bool igDebugCheckVersionAndDataLayout(const char* version_str, - size_t sz_io, - size_t sz_style, - size_t sz_vec2, - size_t sz_vec4, - size_t sz_drawvert, - size_t sz_drawidx) { - return ImGui::DebugCheckVersionAndDataLayout( - version_str, sz_io, sz_style, sz_vec2, sz_vec4, sz_drawvert, sz_drawidx); -} -CIMGUI_API void igSetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, - ImGuiMemFreeFunc free_func, - void* user_data) { - return ImGui::SetAllocatorFunctions(alloc_func, free_func, user_data); -} -CIMGUI_API void igGetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, - ImGuiMemFreeFunc* p_free_func, - void** p_user_data) { - return ImGui::GetAllocatorFunctions(p_alloc_func, p_free_func, p_user_data); -} -CIMGUI_API void* igMemAlloc(size_t size) { - return ImGui::MemAlloc(size); -} -CIMGUI_API void igMemFree(void* ptr) { - return ImGui::MemFree(ptr); -} -CIMGUI_API ImGuiPlatformIO* igGetPlatformIO() { - return &ImGui::GetPlatformIO(); -} -CIMGUI_API void igUpdatePlatformWindows() { - return ImGui::UpdatePlatformWindows(); -} -CIMGUI_API void igRenderPlatformWindowsDefault(void* platform_render_arg, - void* renderer_render_arg) { - return ImGui::RenderPlatformWindowsDefault(platform_render_arg, - renderer_render_arg); -} -CIMGUI_API void igDestroyPlatformWindows() { - return ImGui::DestroyPlatformWindows(); -} -CIMGUI_API ImGuiViewport* igFindViewportByID(ImGuiID id) { - return ImGui::FindViewportByID(id); -} -CIMGUI_API ImGuiViewport* igFindViewportByPlatformHandle( - void* platform_handle) { - return ImGui::FindViewportByPlatformHandle(platform_handle); -} -CIMGUI_API ImGuiTableSortSpecs* ImGuiTableSortSpecs_ImGuiTableSortSpecs(void) { - return IM_NEW(ImGuiTableSortSpecs)(); -} -CIMGUI_API void ImGuiTableSortSpecs_destroy(ImGuiTableSortSpecs* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumnSortSpecs* -ImGuiTableColumnSortSpecs_ImGuiTableColumnSortSpecs(void) { - return IM_NEW(ImGuiTableColumnSortSpecs)(); -} -CIMGUI_API void ImGuiTableColumnSortSpecs_destroy( - ImGuiTableColumnSortSpecs* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStyle* ImGuiStyle_ImGuiStyle(void) { - return IM_NEW(ImGuiStyle)(); -} -CIMGUI_API void ImGuiStyle_destroy(ImGuiStyle* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiStyle_ScaleAllSizes(ImGuiStyle* self, float scale_factor) { - return self->ScaleAllSizes(scale_factor); -} -CIMGUI_API void ImGuiIO_AddKeyEvent(ImGuiIO* self, ImGuiKey key, bool down) { - return self->AddKeyEvent(key, down); -} -CIMGUI_API void ImGuiIO_AddKeyAnalogEvent(ImGuiIO* self, - ImGuiKey key, - bool down, - float v) { - return self->AddKeyAnalogEvent(key, down, v); -} -CIMGUI_API void ImGuiIO_AddMousePosEvent(ImGuiIO* self, float x, float y) { - return self->AddMousePosEvent(x, y); -} -CIMGUI_API void ImGuiIO_AddMouseButtonEvent(ImGuiIO* self, - int button, - bool down) { - return self->AddMouseButtonEvent(button, down); -} -CIMGUI_API void ImGuiIO_AddMouseWheelEvent(ImGuiIO* self, - float wheel_x, - float wheel_y) { - return self->AddMouseWheelEvent(wheel_x, wheel_y); -} -CIMGUI_API void ImGuiIO_AddMouseSourceEvent(ImGuiIO* self, - ImGuiMouseSource source) { - return self->AddMouseSourceEvent(source); -} -CIMGUI_API void ImGuiIO_AddMouseViewportEvent(ImGuiIO* self, ImGuiID id) { - return self->AddMouseViewportEvent(id); -} -CIMGUI_API void ImGuiIO_AddFocusEvent(ImGuiIO* self, bool focused) { - return self->AddFocusEvent(focused); -} -CIMGUI_API void ImGuiIO_AddInputCharacter(ImGuiIO* self, unsigned int c) { - return self->AddInputCharacter(c); -} -CIMGUI_API void ImGuiIO_AddInputCharacterUTF16(ImGuiIO* self, ImWchar16 c) { - return self->AddInputCharacterUTF16(c); -} -CIMGUI_API void ImGuiIO_AddInputCharactersUTF8(ImGuiIO* self, const char* str) { - return self->AddInputCharactersUTF8(str); -} -CIMGUI_API void ImGuiIO_SetKeyEventNativeData(ImGuiIO* self, - ImGuiKey key, - int native_keycode, - int native_scancode, - int native_legacy_index) { - return self->SetKeyEventNativeData(key, native_keycode, native_scancode, - native_legacy_index); -} -CIMGUI_API void ImGuiIO_SetAppAcceptingEvents(ImGuiIO* self, - bool accepting_events) { - return self->SetAppAcceptingEvents(accepting_events); -} -CIMGUI_API void ImGuiIO_ClearEventsQueue(ImGuiIO* self) { - return self->ClearEventsQueue(); -} -CIMGUI_API void ImGuiIO_ClearInputKeys(ImGuiIO* self) { - return self->ClearInputKeys(); -} -CIMGUI_API ImGuiIO* ImGuiIO_ImGuiIO(void) { - return IM_NEW(ImGuiIO)(); -} -CIMGUI_API void ImGuiIO_destroy(ImGuiIO* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiInputTextCallbackData* -ImGuiInputTextCallbackData_ImGuiInputTextCallbackData(void) { - return IM_NEW(ImGuiInputTextCallbackData)(); -} -CIMGUI_API void ImGuiInputTextCallbackData_destroy( - ImGuiInputTextCallbackData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiInputTextCallbackData_DeleteChars( - ImGuiInputTextCallbackData* self, - int pos, - int bytes_count) { - return self->DeleteChars(pos, bytes_count); -} -CIMGUI_API void ImGuiInputTextCallbackData_InsertChars( - ImGuiInputTextCallbackData* self, - int pos, - const char* text, - const char* text_end) { - return self->InsertChars(pos, text, text_end); -} -CIMGUI_API void ImGuiInputTextCallbackData_SelectAll( - ImGuiInputTextCallbackData* self) { - return self->SelectAll(); -} -CIMGUI_API void ImGuiInputTextCallbackData_ClearSelection( - ImGuiInputTextCallbackData* self) { - return self->ClearSelection(); -} -CIMGUI_API bool ImGuiInputTextCallbackData_HasSelection( - ImGuiInputTextCallbackData* self) { - return self->HasSelection(); -} -CIMGUI_API ImGuiWindowClass* ImGuiWindowClass_ImGuiWindowClass(void) { - return IM_NEW(ImGuiWindowClass)(); -} -CIMGUI_API void ImGuiWindowClass_destroy(ImGuiWindowClass* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPayload* ImGuiPayload_ImGuiPayload(void) { - return IM_NEW(ImGuiPayload)(); -} -CIMGUI_API void ImGuiPayload_destroy(ImGuiPayload* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiPayload_Clear(ImGuiPayload* self) { - return self->Clear(); -} -CIMGUI_API bool ImGuiPayload_IsDataType(ImGuiPayload* self, const char* type) { - return self->IsDataType(type); -} -CIMGUI_API bool ImGuiPayload_IsPreview(ImGuiPayload* self) { - return self->IsPreview(); -} -CIMGUI_API bool ImGuiPayload_IsDelivery(ImGuiPayload* self) { - return self->IsDelivery(); -} -CIMGUI_API ImGuiOnceUponAFrame* ImGuiOnceUponAFrame_ImGuiOnceUponAFrame(void) { - return IM_NEW(ImGuiOnceUponAFrame)(); -} -CIMGUI_API void ImGuiOnceUponAFrame_destroy(ImGuiOnceUponAFrame* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTextFilter* ImGuiTextFilter_ImGuiTextFilter( - const char* default_filter) { - return IM_NEW(ImGuiTextFilter)(default_filter); -} -CIMGUI_API void ImGuiTextFilter_destroy(ImGuiTextFilter* self) { - IM_DELETE(self); -} -CIMGUI_API bool ImGuiTextFilter_Draw(ImGuiTextFilter* self, - const char* label, - float width) { - return self->Draw(label, width); -} -CIMGUI_API bool ImGuiTextFilter_PassFilter(ImGuiTextFilter* self, - const char* text, - const char* text_end) { - return self->PassFilter(text, text_end); -} -CIMGUI_API void ImGuiTextFilter_Build(ImGuiTextFilter* self) { - return self->Build(); -} -CIMGUI_API void ImGuiTextFilter_Clear(ImGuiTextFilter* self) { - return self->Clear(); -} -CIMGUI_API bool ImGuiTextFilter_IsActive(ImGuiTextFilter* self) { - return self->IsActive(); -} -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Nil(void) { - return IM_NEW(ImGuiTextRange)(); -} -CIMGUI_API void ImGuiTextRange_destroy(ImGuiTextRange* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Str(const char* _b, - const char* _e) { - return IM_NEW(ImGuiTextRange)(_b, _e); -} -CIMGUI_API bool ImGuiTextRange_empty(ImGuiTextRange* self) { - return self->empty(); -} -CIMGUI_API void ImGuiTextRange_split(ImGuiTextRange* self, - char separator, - ImVector_ImGuiTextRange* out) { - return self->split(separator, out); -} -CIMGUI_API ImGuiTextBuffer* ImGuiTextBuffer_ImGuiTextBuffer(void) { - return IM_NEW(ImGuiTextBuffer)(); -} -CIMGUI_API void ImGuiTextBuffer_destroy(ImGuiTextBuffer* self) { - IM_DELETE(self); -} -CIMGUI_API const char* ImGuiTextBuffer_begin(ImGuiTextBuffer* self) { - return self->begin(); -} -CIMGUI_API const char* ImGuiTextBuffer_end(ImGuiTextBuffer* self) { - return self->end(); -} -CIMGUI_API int ImGuiTextBuffer_size(ImGuiTextBuffer* self) { - return self->size(); -} -CIMGUI_API bool ImGuiTextBuffer_empty(ImGuiTextBuffer* self) { - return self->empty(); -} -CIMGUI_API void ImGuiTextBuffer_clear(ImGuiTextBuffer* self) { - return self->clear(); -} -CIMGUI_API void ImGuiTextBuffer_reserve(ImGuiTextBuffer* self, int capacity) { - return self->reserve(capacity); -} -CIMGUI_API const char* ImGuiTextBuffer_c_str(ImGuiTextBuffer* self) { - return self->c_str(); -} -CIMGUI_API void ImGuiTextBuffer_append(ImGuiTextBuffer* self, - const char* str, - const char* str_end) { - return self->append(str, str_end); -} -CIMGUI_API void ImGuiTextBuffer_appendfv(ImGuiTextBuffer* self, - const char* fmt, - va_list args) { - return self->appendfv(fmt, args); -} -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Int(ImGuiID _key, - int _val) { - return IM_NEW(ImGuiStoragePair)(_key, _val); -} -CIMGUI_API void ImGuiStoragePair_destroy(ImGuiStoragePair* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Float( - ImGuiID _key, - float _val) { - return IM_NEW(ImGuiStoragePair)(_key, _val); -} -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Ptr(ImGuiID _key, - void* _val) { - return IM_NEW(ImGuiStoragePair)(_key, _val); -} -CIMGUI_API void ImGuiStorage_Clear(ImGuiStorage* self) { - return self->Clear(); -} -CIMGUI_API int ImGuiStorage_GetInt(ImGuiStorage* self, - ImGuiID key, - int default_val) { - return self->GetInt(key, default_val); -} -CIMGUI_API void ImGuiStorage_SetInt(ImGuiStorage* self, ImGuiID key, int val) { - return self->SetInt(key, val); -} -CIMGUI_API bool ImGuiStorage_GetBool(ImGuiStorage* self, - ImGuiID key, - bool default_val) { - return self->GetBool(key, default_val); -} -CIMGUI_API void ImGuiStorage_SetBool(ImGuiStorage* self, - ImGuiID key, - bool val) { - return self->SetBool(key, val); -} -CIMGUI_API float ImGuiStorage_GetFloat(ImGuiStorage* self, - ImGuiID key, - float default_val) { - return self->GetFloat(key, default_val); -} -CIMGUI_API void ImGuiStorage_SetFloat(ImGuiStorage* self, - ImGuiID key, - float val) { - return self->SetFloat(key, val); -} -CIMGUI_API void* ImGuiStorage_GetVoidPtr(ImGuiStorage* self, ImGuiID key) { - return self->GetVoidPtr(key); -} -CIMGUI_API void ImGuiStorage_SetVoidPtr(ImGuiStorage* self, - ImGuiID key, - void* val) { - return self->SetVoidPtr(key, val); -} -CIMGUI_API int* ImGuiStorage_GetIntRef(ImGuiStorage* self, - ImGuiID key, - int default_val) { - return self->GetIntRef(key, default_val); -} -CIMGUI_API bool* ImGuiStorage_GetBoolRef(ImGuiStorage* self, - ImGuiID key, - bool default_val) { - return self->GetBoolRef(key, default_val); -} -CIMGUI_API float* ImGuiStorage_GetFloatRef(ImGuiStorage* self, - ImGuiID key, - float default_val) { - return self->GetFloatRef(key, default_val); -} -CIMGUI_API void** ImGuiStorage_GetVoidPtrRef(ImGuiStorage* self, - ImGuiID key, - void* default_val) { - return self->GetVoidPtrRef(key, default_val); -} -CIMGUI_API void ImGuiStorage_BuildSortByKey(ImGuiStorage* self) { - return self->BuildSortByKey(); -} -CIMGUI_API void ImGuiStorage_SetAllInt(ImGuiStorage* self, int val) { - return self->SetAllInt(val); -} -CIMGUI_API ImGuiListClipper* ImGuiListClipper_ImGuiListClipper(void) { - return IM_NEW(ImGuiListClipper)(); -} -CIMGUI_API void ImGuiListClipper_destroy(ImGuiListClipper* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiListClipper_Begin(ImGuiListClipper* self, - int items_count, - float items_height) { - return self->Begin(items_count, items_height); -} -CIMGUI_API void ImGuiListClipper_End(ImGuiListClipper* self) { - return self->End(); -} -CIMGUI_API bool ImGuiListClipper_Step(ImGuiListClipper* self) { - return self->Step(); -} -CIMGUI_API void ImGuiListClipper_IncludeItemByIndex(ImGuiListClipper* self, - int item_index) { - return self->IncludeItemByIndex(item_index); -} -CIMGUI_API void ImGuiListClipper_IncludeItemsByIndex(ImGuiListClipper* self, - int item_begin, - int item_end) { - return self->IncludeItemsByIndex(item_begin, item_end); -} -CIMGUI_API ImColor* ImColor_ImColor_Nil(void) { - return IM_NEW(ImColor)(); -} -CIMGUI_API void ImColor_destroy(ImColor* self) { - IM_DELETE(self); -} -CIMGUI_API ImColor* ImColor_ImColor_Float(float r, float g, float b, float a) { - return IM_NEW(ImColor)(r, g, b, a); -} -CIMGUI_API ImColor* ImColor_ImColor_Vec4(const ImVec4 col) { - return IM_NEW(ImColor)(col); -} -CIMGUI_API ImColor* ImColor_ImColor_Int(int r, int g, int b, int a) { - return IM_NEW(ImColor)(r, g, b, a); -} -CIMGUI_API ImColor* ImColor_ImColor_U32(ImU32 rgba) { - return IM_NEW(ImColor)(rgba); -} -CIMGUI_API void ImColor_SetHSV(ImColor* self, - float h, - float s, - float v, - float a) { - return self->SetHSV(h, s, v, a); -} -CIMGUI_API void ImColor_HSV(ImColor* pOut, float h, float s, float v, float a) { - *pOut = ImColor::HSV(h, s, v, a); -} -CIMGUI_API ImDrawCmd* ImDrawCmd_ImDrawCmd(void) { - return IM_NEW(ImDrawCmd)(); -} -CIMGUI_API void ImDrawCmd_destroy(ImDrawCmd* self) { - IM_DELETE(self); -} -CIMGUI_API ImTextureID ImDrawCmd_GetTexID(ImDrawCmd* self) { - return self->GetTexID(); -} -CIMGUI_API ImDrawListSplitter* ImDrawListSplitter_ImDrawListSplitter(void) { - return IM_NEW(ImDrawListSplitter)(); -} -CIMGUI_API void ImDrawListSplitter_destroy(ImDrawListSplitter* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawListSplitter_Clear(ImDrawListSplitter* self) { - return self->Clear(); -} -CIMGUI_API void ImDrawListSplitter_ClearFreeMemory(ImDrawListSplitter* self) { - return self->ClearFreeMemory(); -} -CIMGUI_API void ImDrawListSplitter_Split(ImDrawListSplitter* self, - ImDrawList* draw_list, - int count) { - return self->Split(draw_list, count); -} -CIMGUI_API void ImDrawListSplitter_Merge(ImDrawListSplitter* self, - ImDrawList* draw_list) { - return self->Merge(draw_list); -} -CIMGUI_API void ImDrawListSplitter_SetCurrentChannel(ImDrawListSplitter* self, - ImDrawList* draw_list, - int channel_idx) { - return self->SetCurrentChannel(draw_list, channel_idx); -} -CIMGUI_API ImDrawList* ImDrawList_ImDrawList( - ImDrawListSharedData* shared_data) { - return IM_NEW(ImDrawList)(shared_data); -} -CIMGUI_API void ImDrawList_destroy(ImDrawList* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawList_PushClipRect(ImDrawList* self, - const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect) { - return self->PushClipRect(clip_rect_min, clip_rect_max, - intersect_with_current_clip_rect); -} -CIMGUI_API void ImDrawList_PushClipRectFullScreen(ImDrawList* self) { - return self->PushClipRectFullScreen(); -} -CIMGUI_API void ImDrawList_PopClipRect(ImDrawList* self) { - return self->PopClipRect(); -} -CIMGUI_API void ImDrawList_PushTextureID(ImDrawList* self, - ImTextureID texture_id) { - return self->PushTextureID(texture_id); -} -CIMGUI_API void ImDrawList_PopTextureID(ImDrawList* self) { - return self->PopTextureID(); -} -CIMGUI_API void ImDrawList_GetClipRectMin(ImVec2* pOut, ImDrawList* self) { - *pOut = self->GetClipRectMin(); -} -CIMGUI_API void ImDrawList_GetClipRectMax(ImVec2* pOut, ImDrawList* self) { - *pOut = self->GetClipRectMax(); -} -CIMGUI_API void ImDrawList_AddLine(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - ImU32 col, - float thickness) { - return self->AddLine(p1, p2, col, thickness); -} -CIMGUI_API void ImDrawList_AddRect(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags, - float thickness) { - return self->AddRect(p_min, p_max, col, rounding, flags, thickness); -} -CIMGUI_API void ImDrawList_AddRectFilled(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags) { - return self->AddRectFilled(p_min, p_max, col, rounding, flags); -} -CIMGUI_API void ImDrawList_AddRectFilledMultiColor(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col_upr_left, - ImU32 col_upr_right, - ImU32 col_bot_right, - ImU32 col_bot_left) { - return self->AddRectFilledMultiColor( - p_min, p_max, col_upr_left, col_upr_right, col_bot_right, col_bot_left); -} -CIMGUI_API void ImDrawList_AddQuad(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness) { - return self->AddQuad(p1, p2, p3, p4, col, thickness); -} -CIMGUI_API void ImDrawList_AddQuadFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col) { - return self->AddQuadFilled(p1, p2, p3, p4, col); -} -CIMGUI_API void ImDrawList_AddTriangle(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness) { - return self->AddTriangle(p1, p2, p3, col, thickness); -} -CIMGUI_API void ImDrawList_AddTriangleFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col) { - return self->AddTriangleFilled(p1, p2, p3, col); -} -CIMGUI_API void ImDrawList_AddCircle(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness) { - return self->AddCircle(center, radius, col, num_segments, thickness); -} -CIMGUI_API void ImDrawList_AddCircleFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments) { - return self->AddCircleFilled(center, radius, col, num_segments); -} -CIMGUI_API void ImDrawList_AddNgon(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness) { - return self->AddNgon(center, radius, col, num_segments, thickness); -} -CIMGUI_API void ImDrawList_AddNgonFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments) { - return self->AddNgonFilled(center, radius, col, num_segments); -} -CIMGUI_API void ImDrawList_AddEllipse(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments, - float thickness) { - return self->AddEllipse(center, radius, col, rot, num_segments, thickness); -} -CIMGUI_API void ImDrawList_AddEllipseFilled(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments) { - return self->AddEllipseFilled(center, radius, col, rot, num_segments); -} -CIMGUI_API void ImDrawList_AddText_Vec2(ImDrawList* self, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end) { - return self->AddText(pos, col, text_begin, text_end); -} -CIMGUI_API void ImDrawList_AddText_FontPtr(ImDrawList* self, - const ImFont* font, - float font_size, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end, - float wrap_width, - const ImVec4* cpu_fine_clip_rect) { - return self->AddText(font, font_size, pos, col, text_begin, text_end, - wrap_width, cpu_fine_clip_rect); -} -CIMGUI_API void ImDrawList_AddBezierCubic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness, - int num_segments) { - return self->AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); -} -CIMGUI_API void ImDrawList_AddBezierQuadratic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness, - int num_segments) { - return self->AddBezierQuadratic(p1, p2, p3, col, thickness, num_segments); -} -CIMGUI_API void ImDrawList_AddPolyline(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col, - ImDrawFlags flags, - float thickness) { - return self->AddPolyline(points, num_points, col, flags, thickness); -} -CIMGUI_API void ImDrawList_AddConvexPolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col) { - return self->AddConvexPolyFilled(points, num_points, col); -} -CIMGUI_API void ImDrawList_AddConcavePolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col) { - return self->AddConcavePolyFilled(points, num_points, col); -} -CIMGUI_API void ImDrawList_AddImage(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col) { - return self->AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col); -} -CIMGUI_API void ImDrawList_AddImageQuad(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 uv1, - const ImVec2 uv2, - const ImVec2 uv3, - const ImVec2 uv4, - ImU32 col) { - return self->AddImageQuad(user_texture_id, p1, p2, p3, p4, uv1, uv2, uv3, uv4, - col); -} -CIMGUI_API void ImDrawList_AddImageRounded(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col, - float rounding, - ImDrawFlags flags) { - return self->AddImageRounded(user_texture_id, p_min, p_max, uv_min, uv_max, - col, rounding, flags); -} -CIMGUI_API void ImDrawList_PathClear(ImDrawList* self) { - return self->PathClear(); -} -CIMGUI_API void ImDrawList_PathLineTo(ImDrawList* self, const ImVec2 pos) { - return self->PathLineTo(pos); -} -CIMGUI_API void ImDrawList_PathLineToMergeDuplicate(ImDrawList* self, - const ImVec2 pos) { - return self->PathLineToMergeDuplicate(pos); -} -CIMGUI_API void ImDrawList_PathFillConvex(ImDrawList* self, ImU32 col) { - return self->PathFillConvex(col); -} -CIMGUI_API void ImDrawList_PathFillConcave(ImDrawList* self, ImU32 col) { - return self->PathFillConcave(col); -} -CIMGUI_API void ImDrawList_PathStroke(ImDrawList* self, - ImU32 col, - ImDrawFlags flags, - float thickness) { - return self->PathStroke(col, flags, thickness); -} -CIMGUI_API void ImDrawList_PathArcTo(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments) { - return self->PathArcTo(center, radius, a_min, a_max, num_segments); -} -CIMGUI_API void ImDrawList_PathArcToFast(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_of_12, - int a_max_of_12) { - return self->PathArcToFast(center, radius, a_min_of_12, a_max_of_12); -} -CIMGUI_API void ImDrawList_PathEllipticalArcTo(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - float rot, - float a_min, - float a_max, - int num_segments) { - return self->PathEllipticalArcTo(center, radius, rot, a_min, a_max, - num_segments); -} -CIMGUI_API void ImDrawList_PathBezierCubicCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - int num_segments) { - return self->PathBezierCubicCurveTo(p2, p3, p4, num_segments); -} -CIMGUI_API void ImDrawList_PathBezierQuadraticCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - int num_segments) { - return self->PathBezierQuadraticCurveTo(p2, p3, num_segments); -} -CIMGUI_API void ImDrawList_PathRect(ImDrawList* self, - const ImVec2 rect_min, - const ImVec2 rect_max, - float rounding, - ImDrawFlags flags) { - return self->PathRect(rect_min, rect_max, rounding, flags); -} -CIMGUI_API void ImDrawList_AddCallback(ImDrawList* self, - ImDrawCallback callback, - void* callback_data) { - return self->AddCallback(callback, callback_data); -} -CIMGUI_API void ImDrawList_AddDrawCmd(ImDrawList* self) { - return self->AddDrawCmd(); -} -CIMGUI_API ImDrawList* ImDrawList_CloneOutput(ImDrawList* self) { - return self->CloneOutput(); -} -CIMGUI_API void ImDrawList_ChannelsSplit(ImDrawList* self, int count) { - return self->ChannelsSplit(count); -} -CIMGUI_API void ImDrawList_ChannelsMerge(ImDrawList* self) { - return self->ChannelsMerge(); -} -CIMGUI_API void ImDrawList_ChannelsSetCurrent(ImDrawList* self, int n) { - return self->ChannelsSetCurrent(n); -} -CIMGUI_API void ImDrawList_PrimReserve(ImDrawList* self, - int idx_count, - int vtx_count) { - return self->PrimReserve(idx_count, vtx_count); -} -CIMGUI_API void ImDrawList_PrimUnreserve(ImDrawList* self, - int idx_count, - int vtx_count) { - return self->PrimUnreserve(idx_count, vtx_count); -} -CIMGUI_API void ImDrawList_PrimRect(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - ImU32 col) { - return self->PrimRect(a, b, col); -} -CIMGUI_API void ImDrawList_PrimRectUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - ImU32 col) { - return self->PrimRectUV(a, b, uv_a, uv_b, col); -} -CIMGUI_API void ImDrawList_PrimQuadUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 d, - const ImVec2 uv_a, - const ImVec2 uv_b, - const ImVec2 uv_c, - const ImVec2 uv_d, - ImU32 col) { - return self->PrimQuadUV(a, b, c, d, uv_a, uv_b, uv_c, uv_d, col); -} -CIMGUI_API void ImDrawList_PrimWriteVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col) { - return self->PrimWriteVtx(pos, uv, col); -} -CIMGUI_API void ImDrawList_PrimWriteIdx(ImDrawList* self, ImDrawIdx idx) { - return self->PrimWriteIdx(idx); -} -CIMGUI_API void ImDrawList_PrimVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col) { - return self->PrimVtx(pos, uv, col); -} -CIMGUI_API void ImDrawList__ResetForNewFrame(ImDrawList* self) { - return self->_ResetForNewFrame(); -} -CIMGUI_API void ImDrawList__ClearFreeMemory(ImDrawList* self) { - return self->_ClearFreeMemory(); -} -CIMGUI_API void ImDrawList__PopUnusedDrawCmd(ImDrawList* self) { - return self->_PopUnusedDrawCmd(); -} -CIMGUI_API void ImDrawList__TryMergeDrawCmds(ImDrawList* self) { - return self->_TryMergeDrawCmds(); -} -CIMGUI_API void ImDrawList__OnChangedClipRect(ImDrawList* self) { - return self->_OnChangedClipRect(); -} -CIMGUI_API void ImDrawList__OnChangedTextureID(ImDrawList* self) { - return self->_OnChangedTextureID(); -} -CIMGUI_API void ImDrawList__OnChangedVtxOffset(ImDrawList* self) { - return self->_OnChangedVtxOffset(); -} -CIMGUI_API int ImDrawList__CalcCircleAutoSegmentCount(ImDrawList* self, - float radius) { - return self->_CalcCircleAutoSegmentCount(radius); -} -CIMGUI_API void ImDrawList__PathArcToFastEx(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_sample, - int a_max_sample, - int a_step) { - return self->_PathArcToFastEx(center, radius, a_min_sample, a_max_sample, - a_step); -} -CIMGUI_API void ImDrawList__PathArcToN(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments) { - return self->_PathArcToN(center, radius, a_min, a_max, num_segments); -} -CIMGUI_API ImDrawData* ImDrawData_ImDrawData(void) { - return IM_NEW(ImDrawData)(); -} -CIMGUI_API void ImDrawData_destroy(ImDrawData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawData_Clear(ImDrawData* self) { - return self->Clear(); -} -CIMGUI_API void ImDrawData_AddDrawList(ImDrawData* self, - ImDrawList* draw_list) { - return self->AddDrawList(draw_list); -} -CIMGUI_API void ImDrawData_DeIndexAllBuffers(ImDrawData* self) { - return self->DeIndexAllBuffers(); -} -CIMGUI_API void ImDrawData_ScaleClipRects(ImDrawData* self, - const ImVec2 fb_scale) { - return self->ScaleClipRects(fb_scale); -} -CIMGUI_API ImFontConfig* ImFontConfig_ImFontConfig(void) { - return IM_NEW(ImFontConfig)(); -} -CIMGUI_API void ImFontConfig_destroy(ImFontConfig* self) { - IM_DELETE(self); -} -CIMGUI_API ImFontGlyphRangesBuilder* -ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder(void) { - return IM_NEW(ImFontGlyphRangesBuilder)(); -} -CIMGUI_API void ImFontGlyphRangesBuilder_destroy( - ImFontGlyphRangesBuilder* self) { - IM_DELETE(self); -} -CIMGUI_API void ImFontGlyphRangesBuilder_Clear(ImFontGlyphRangesBuilder* self) { - return self->Clear(); -} -CIMGUI_API bool ImFontGlyphRangesBuilder_GetBit(ImFontGlyphRangesBuilder* self, - size_t n) { - return self->GetBit(n); -} -CIMGUI_API void ImFontGlyphRangesBuilder_SetBit(ImFontGlyphRangesBuilder* self, - size_t n) { - return self->SetBit(n); -} -CIMGUI_API void ImFontGlyphRangesBuilder_AddChar(ImFontGlyphRangesBuilder* self, - ImWchar c) { - return self->AddChar(c); -} -CIMGUI_API void ImFontGlyphRangesBuilder_AddText(ImFontGlyphRangesBuilder* self, - const char* text, - const char* text_end) { - return self->AddText(text, text_end); -} -CIMGUI_API void ImFontGlyphRangesBuilder_AddRanges( - ImFontGlyphRangesBuilder* self, - const ImWchar* ranges) { - return self->AddRanges(ranges); -} -CIMGUI_API void ImFontGlyphRangesBuilder_BuildRanges( - ImFontGlyphRangesBuilder* self, - ImVector_ImWchar* out_ranges) { - return self->BuildRanges(out_ranges); -} -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlasCustomRect_ImFontAtlasCustomRect( - void) { - return IM_NEW(ImFontAtlasCustomRect)(); -} -CIMGUI_API void ImFontAtlasCustomRect_destroy(ImFontAtlasCustomRect* self) { - IM_DELETE(self); -} -CIMGUI_API bool ImFontAtlasCustomRect_IsPacked(ImFontAtlasCustomRect* self) { - return self->IsPacked(); -} -CIMGUI_API ImFontAtlas* ImFontAtlas_ImFontAtlas(void) { - return IM_NEW(ImFontAtlas)(); -} -CIMGUI_API void ImFontAtlas_destroy(ImFontAtlas* self) { - IM_DELETE(self); -} -CIMGUI_API ImFont* ImFontAtlas_AddFont(ImFontAtlas* self, - const ImFontConfig* font_cfg) { - return self->AddFont(font_cfg); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontDefault(ImFontAtlas* self, - const ImFontConfig* font_cfg) { - return self->AddFontDefault(font_cfg); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromFileTTF(ImFontAtlas* self, - const char* filename, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromFileTTF(filename, size_pixels, font_cfg, - glyph_ranges); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryTTF( - ImFontAtlas* self, - void* font_data, - int font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromMemoryTTF(font_data, font_data_size, size_pixels, - font_cfg, glyph_ranges); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedTTF( - ImFontAtlas* self, - const void* compressed_font_data, - int compressed_font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromMemoryCompressedTTF( - compressed_font_data, compressed_font_data_size, size_pixels, font_cfg, - glyph_ranges); -} -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedBase85TTF( - ImFontAtlas* self, - const char* compressed_font_data_base85, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges) { - return self->AddFontFromMemoryCompressedBase85TTF( - compressed_font_data_base85, size_pixels, font_cfg, glyph_ranges); -} -CIMGUI_API void ImFontAtlas_ClearInputData(ImFontAtlas* self) { - return self->ClearInputData(); -} -CIMGUI_API void ImFontAtlas_ClearTexData(ImFontAtlas* self) { - return self->ClearTexData(); -} -CIMGUI_API void ImFontAtlas_ClearFonts(ImFontAtlas* self) { - return self->ClearFonts(); -} -CIMGUI_API void ImFontAtlas_Clear(ImFontAtlas* self) { - return self->Clear(); -} -CIMGUI_API bool ImFontAtlas_Build(ImFontAtlas* self) { - return self->Build(); -} -CIMGUI_API void ImFontAtlas_GetTexDataAsAlpha8(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel) { - return self->GetTexDataAsAlpha8(out_pixels, out_width, out_height, - out_bytes_per_pixel); -} -CIMGUI_API void ImFontAtlas_GetTexDataAsRGBA32(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel) { - return self->GetTexDataAsRGBA32(out_pixels, out_width, out_height, - out_bytes_per_pixel); -} -CIMGUI_API bool ImFontAtlas_IsBuilt(ImFontAtlas* self) { - return self->IsBuilt(); -} -CIMGUI_API void ImFontAtlas_SetTexID(ImFontAtlas* self, ImTextureID id) { - return self->SetTexID(id); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesDefault(ImFontAtlas* self) { - return self->GetGlyphRangesDefault(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesGreek(ImFontAtlas* self) { - return self->GetGlyphRangesGreek(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesKorean(ImFontAtlas* self) { - return self->GetGlyphRangesKorean(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesJapanese( - ImFontAtlas* self) { - return self->GetGlyphRangesJapanese(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseFull( - ImFontAtlas* self) { - return self->GetGlyphRangesChineseFull(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseSimplifiedCommon( - ImFontAtlas* self) { - return self->GetGlyphRangesChineseSimplifiedCommon(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesCyrillic( - ImFontAtlas* self) { - return self->GetGlyphRangesCyrillic(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesThai(ImFontAtlas* self) { - return self->GetGlyphRangesThai(); -} -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesVietnamese( - ImFontAtlas* self) { - return self->GetGlyphRangesVietnamese(); -} -CIMGUI_API int ImFontAtlas_AddCustomRectRegular(ImFontAtlas* self, - int width, - int height) { - return self->AddCustomRectRegular(width, height); -} -CIMGUI_API int ImFontAtlas_AddCustomRectFontGlyph(ImFontAtlas* self, - ImFont* font, - ImWchar id, - int width, - int height, - float advance_x, - const ImVec2 offset) { - return self->AddCustomRectFontGlyph(font, id, width, height, advance_x, - offset); -} -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlas_GetCustomRectByIndex( - ImFontAtlas* self, - int index) { - return self->GetCustomRectByIndex(index); -} -CIMGUI_API void ImFontAtlas_CalcCustomRectUV(ImFontAtlas* self, - const ImFontAtlasCustomRect* rect, - ImVec2* out_uv_min, - ImVec2* out_uv_max) { - return self->CalcCustomRectUV(rect, out_uv_min, out_uv_max); -} -CIMGUI_API bool ImFontAtlas_GetMouseCursorTexData(ImFontAtlas* self, - ImGuiMouseCursor cursor, - ImVec2* out_offset, - ImVec2* out_size, - ImVec2 out_uv_border[2], - ImVec2 out_uv_fill[2]) { - return self->GetMouseCursorTexData(cursor, out_offset, out_size, - out_uv_border, out_uv_fill); -} -CIMGUI_API ImFont* ImFont_ImFont(void) { - return IM_NEW(ImFont)(); -} -CIMGUI_API void ImFont_destroy(ImFont* self) { - IM_DELETE(self); -} -CIMGUI_API const ImFontGlyph* ImFont_FindGlyph(ImFont* self, ImWchar c) { - return self->FindGlyph(c); -} -CIMGUI_API const ImFontGlyph* ImFont_FindGlyphNoFallback(ImFont* self, - ImWchar c) { - return self->FindGlyphNoFallback(c); -} -CIMGUI_API float ImFont_GetCharAdvance(ImFont* self, ImWchar c) { - return self->GetCharAdvance(c); -} -CIMGUI_API bool ImFont_IsLoaded(ImFont* self) { - return self->IsLoaded(); -} -CIMGUI_API const char* ImFont_GetDebugName(ImFont* self) { - return self->GetDebugName(); -} -CIMGUI_API void ImFont_CalcTextSizeA(ImVec2* pOut, - ImFont* self, - float size, - float max_width, - float wrap_width, - const char* text_begin, - const char* text_end, - const char** remaining) { - *pOut = self->CalcTextSizeA(size, max_width, wrap_width, text_begin, text_end, - remaining); -} -CIMGUI_API const char* ImFont_CalcWordWrapPositionA(ImFont* self, - float scale, - const char* text, - const char* text_end, - float wrap_width) { - return self->CalcWordWrapPositionA(scale, text, text_end, wrap_width); -} -CIMGUI_API void ImFont_RenderChar(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - ImWchar c) { - return self->RenderChar(draw_list, size, pos, col, c); -} -CIMGUI_API void ImFont_RenderText(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - const ImVec4 clip_rect, - const char* text_begin, - const char* text_end, - float wrap_width, - bool cpu_fine_clip) { - return self->RenderText(draw_list, size, pos, col, clip_rect, text_begin, - text_end, wrap_width, cpu_fine_clip); -} -CIMGUI_API void ImFont_BuildLookupTable(ImFont* self) { - return self->BuildLookupTable(); -} -CIMGUI_API void ImFont_ClearOutputData(ImFont* self) { - return self->ClearOutputData(); -} -CIMGUI_API void ImFont_GrowIndex(ImFont* self, int new_size) { - return self->GrowIndex(new_size); -} -CIMGUI_API void ImFont_AddGlyph(ImFont* self, - const ImFontConfig* src_cfg, - ImWchar c, - float x0, - float y0, - float x1, - float y1, - float u0, - float v0, - float u1, - float v1, - float advance_x) { - return self->AddGlyph(src_cfg, c, x0, y0, x1, y1, u0, v0, u1, v1, advance_x); -} -CIMGUI_API void ImFont_AddRemapChar(ImFont* self, - ImWchar dst, - ImWchar src, - bool overwrite_dst) { - return self->AddRemapChar(dst, src, overwrite_dst); -} -CIMGUI_API void ImFont_SetGlyphVisible(ImFont* self, ImWchar c, bool visible) { - return self->SetGlyphVisible(c, visible); -} -CIMGUI_API bool ImFont_IsGlyphRangeUnused(ImFont* self, - unsigned int c_begin, - unsigned int c_last) { - return self->IsGlyphRangeUnused(c_begin, c_last); -} -CIMGUI_API ImGuiViewport* ImGuiViewport_ImGuiViewport(void) { - return IM_NEW(ImGuiViewport)(); -} -CIMGUI_API void ImGuiViewport_destroy(ImGuiViewport* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiViewport_GetCenter(ImVec2* pOut, ImGuiViewport* self) { - *pOut = self->GetCenter(); -} -CIMGUI_API void ImGuiViewport_GetWorkCenter(ImVec2* pOut, ImGuiViewport* self) { - *pOut = self->GetWorkCenter(); -} -CIMGUI_API ImGuiPlatformIO* ImGuiPlatformIO_ImGuiPlatformIO(void) { - return IM_NEW(ImGuiPlatformIO)(); -} -CIMGUI_API void ImGuiPlatformIO_destroy(ImGuiPlatformIO* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPlatformMonitor* ImGuiPlatformMonitor_ImGuiPlatformMonitor( - void) { - return IM_NEW(ImGuiPlatformMonitor)(); -} -CIMGUI_API void ImGuiPlatformMonitor_destroy(ImGuiPlatformMonitor* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPlatformImeData* ImGuiPlatformImeData_ImGuiPlatformImeData( - void) { - return IM_NEW(ImGuiPlatformImeData)(); -} -CIMGUI_API void ImGuiPlatformImeData_destroy(ImGuiPlatformImeData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiID igImHashData(const void* data, - size_t data_size, - ImGuiID seed) { - return ImHashData(data, data_size, seed); -} -CIMGUI_API ImGuiID igImHashStr(const char* data, - size_t data_size, - ImGuiID seed) { - return ImHashStr(data, data_size, seed); -} -CIMGUI_API void igImQsort(void* base, - size_t count, - size_t size_of_element, - int (*compare_func)(void const*, void const*)) { - return ImQsort(base, count, size_of_element, compare_func); -} -CIMGUI_API ImU32 igImAlphaBlendColors(ImU32 col_a, ImU32 col_b) { - return ImAlphaBlendColors(col_a, col_b); -} -CIMGUI_API bool igImIsPowerOfTwo_Int(int v) { - return ImIsPowerOfTwo(v); -} -CIMGUI_API bool igImIsPowerOfTwo_U64(ImU64 v) { - return ImIsPowerOfTwo(v); -} -CIMGUI_API int igImUpperPowerOfTwo(int v) { - return ImUpperPowerOfTwo(v); -} -CIMGUI_API int igImStricmp(const char* str1, const char* str2) { - return ImStricmp(str1, str2); -} -CIMGUI_API int igImStrnicmp(const char* str1, const char* str2, size_t count) { - return ImStrnicmp(str1, str2, count); -} -CIMGUI_API void igImStrncpy(char* dst, const char* src, size_t count) { - return ImStrncpy(dst, src, count); -} -CIMGUI_API char* igImStrdup(const char* str) { - return ImStrdup(str); -} -CIMGUI_API char* igImStrdupcpy(char* dst, size_t* p_dst_size, const char* str) { - return ImStrdupcpy(dst, p_dst_size, str); -} -CIMGUI_API const char* igImStrchrRange(const char* str_begin, - const char* str_end, - char c) { - return ImStrchrRange(str_begin, str_end, c); -} -CIMGUI_API const char* igImStreolRange(const char* str, const char* str_end) { - return ImStreolRange(str, str_end); -} -CIMGUI_API const char* igImStristr(const char* haystack, - const char* haystack_end, - const char* needle, - const char* needle_end) { - return ImStristr(haystack, haystack_end, needle, needle_end); -} -CIMGUI_API void igImStrTrimBlanks(char* str) { - return ImStrTrimBlanks(str); -} -CIMGUI_API const char* igImStrSkipBlank(const char* str) { - return ImStrSkipBlank(str); -} -CIMGUI_API int igImStrlenW(const ImWchar* str) { - return ImStrlenW(str); -} -CIMGUI_API const ImWchar* igImStrbolW(const ImWchar* buf_mid_line, - const ImWchar* buf_begin) { - return ImStrbolW(buf_mid_line, buf_begin); -} -CIMGUI_API char igImToUpper(char c) { - return ImToUpper(c); -} -CIMGUI_API bool igImCharIsBlankA(char c) { - return ImCharIsBlankA(c); -} -CIMGUI_API bool igImCharIsBlankW(unsigned int c) { - return ImCharIsBlankW(c); -} -CIMGUI_API int igImFormatString(char* buf, - size_t buf_size, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - int ret = ImFormatStringV(buf, buf_size, fmt, args); - va_end(args); - return ret; -} -CIMGUI_API int igImFormatStringV(char* buf, - size_t buf_size, - const char* fmt, - va_list args) { - return ImFormatStringV(buf, buf_size, fmt, args); -} -CIMGUI_API void igImFormatStringToTempBuffer(const char** out_buf, - const char** out_buf_end, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); - va_end(args); -} -CIMGUI_API void igImFormatStringToTempBufferV(const char** out_buf, - const char** out_buf_end, - const char* fmt, - va_list args) { - return ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); -} -CIMGUI_API const char* igImParseFormatFindStart(const char* format) { - return ImParseFormatFindStart(format); -} -CIMGUI_API const char* igImParseFormatFindEnd(const char* format) { - return ImParseFormatFindEnd(format); -} -CIMGUI_API const char* igImParseFormatTrimDecorations(const char* format, - char* buf, - size_t buf_size) { - return ImParseFormatTrimDecorations(format, buf, buf_size); -} -CIMGUI_API void igImParseFormatSanitizeForPrinting(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size) { - return ImParseFormatSanitizeForPrinting(fmt_in, fmt_out, fmt_out_size); -} -CIMGUI_API const char* igImParseFormatSanitizeForScanning(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size) { - return ImParseFormatSanitizeForScanning(fmt_in, fmt_out, fmt_out_size); -} -CIMGUI_API int igImParseFormatPrecision(const char* format, int default_value) { - return ImParseFormatPrecision(format, default_value); -} -CIMGUI_API const char* igImTextCharToUtf8(char out_buf[5], unsigned int c) { - return ImTextCharToUtf8(out_buf, c); -} -CIMGUI_API int igImTextStrToUtf8(char* out_buf, - int out_buf_size, - const ImWchar* in_text, - const ImWchar* in_text_end) { - return ImTextStrToUtf8(out_buf, out_buf_size, in_text, in_text_end); -} -CIMGUI_API int igImTextCharFromUtf8(unsigned int* out_char, - const char* in_text, - const char* in_text_end) { - return ImTextCharFromUtf8(out_char, in_text, in_text_end); -} -CIMGUI_API int igImTextStrFromUtf8(ImWchar* out_buf, - int out_buf_size, - const char* in_text, - const char* in_text_end, - const char** in_remaining) { - return ImTextStrFromUtf8(out_buf, out_buf_size, in_text, in_text_end, - in_remaining); -} -CIMGUI_API int igImTextCountCharsFromUtf8(const char* in_text, - const char* in_text_end) { - return ImTextCountCharsFromUtf8(in_text, in_text_end); -} -CIMGUI_API int igImTextCountUtf8BytesFromChar(const char* in_text, - const char* in_text_end) { - return ImTextCountUtf8BytesFromChar(in_text, in_text_end); -} -CIMGUI_API int igImTextCountUtf8BytesFromStr(const ImWchar* in_text, - const ImWchar* in_text_end) { - return ImTextCountUtf8BytesFromStr(in_text, in_text_end); -} -CIMGUI_API const char* igImTextFindPreviousUtf8Codepoint( - const char* in_text_start, - const char* in_text_curr) { - return ImTextFindPreviousUtf8Codepoint(in_text_start, in_text_curr); -} -CIMGUI_API int igImTextCountLines(const char* in_text, - const char* in_text_end) { - return ImTextCountLines(in_text, in_text_end); -} -CIMGUI_API ImFileHandle igImFileOpen(const char* filename, const char* mode) { - return ImFileOpen(filename, mode); -} -CIMGUI_API bool igImFileClose(ImFileHandle file) { - return ImFileClose(file); -} -CIMGUI_API ImU64 igImFileGetSize(ImFileHandle file) { - return ImFileGetSize(file); -} -CIMGUI_API ImU64 igImFileRead(void* data, - ImU64 size, - ImU64 count, - ImFileHandle file) { - return ImFileRead(data, size, count, file); -} -CIMGUI_API ImU64 igImFileWrite(const void* data, - ImU64 size, - ImU64 count, - ImFileHandle file) { - return ImFileWrite(data, size, count, file); -} -CIMGUI_API void* igImFileLoadToMemory(const char* filename, - const char* mode, - size_t* out_file_size, - int padding_bytes) { - return ImFileLoadToMemory(filename, mode, out_file_size, padding_bytes); -} -CIMGUI_API float igImPow_Float(float x, float y) { - return ImPow(x, y); -} -CIMGUI_API double igImPow_double(double x, double y) { - return ImPow(x, y); -} -CIMGUI_API float igImLog_Float(float x) { - return ImLog(x); -} -CIMGUI_API double igImLog_double(double x) { - return ImLog(x); -} -CIMGUI_API int igImAbs_Int(int x) { - return ImAbs(x); -} -CIMGUI_API float igImAbs_Float(float x) { - return ImAbs(x); -} -CIMGUI_API double igImAbs_double(double x) { - return ImAbs(x); -} -CIMGUI_API float igImSign_Float(float x) { - return ImSign(x); -} -CIMGUI_API double igImSign_double(double x) { - return ImSign(x); -} -CIMGUI_API float igImRsqrt_Float(float x) { - return ImRsqrt(x); -} -CIMGUI_API double igImRsqrt_double(double x) { - return ImRsqrt(x); -} -CIMGUI_API void igImMin(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs) { - *pOut = ImMin(lhs, rhs); -} -CIMGUI_API void igImMax(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs) { - *pOut = ImMax(lhs, rhs); -} -CIMGUI_API void igImClamp(ImVec2* pOut, - const ImVec2 v, - const ImVec2 mn, - ImVec2 mx) { - *pOut = ImClamp(v, mn, mx); -} -CIMGUI_API void igImLerp_Vec2Float(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - float t) { - *pOut = ImLerp(a, b, t); -} -CIMGUI_API void igImLerp_Vec2Vec2(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 t) { - *pOut = ImLerp(a, b, t); -} -CIMGUI_API void igImLerp_Vec4(ImVec4* pOut, - const ImVec4 a, - const ImVec4 b, - float t) { - *pOut = ImLerp(a, b, t); -} -CIMGUI_API float igImSaturate(float f) { - return ImSaturate(f); -} -CIMGUI_API float igImLengthSqr_Vec2(const ImVec2 lhs) { - return ImLengthSqr(lhs); -} -CIMGUI_API float igImLengthSqr_Vec4(const ImVec4 lhs) { - return ImLengthSqr(lhs); -} -CIMGUI_API float igImInvLength(const ImVec2 lhs, float fail_value) { - return ImInvLength(lhs, fail_value); -} -CIMGUI_API float igImTrunc_Float(float f) { - return ImTrunc(f); -} -CIMGUI_API void igImTrunc_Vec2(ImVec2* pOut, const ImVec2 v) { - *pOut = ImTrunc(v); -} -CIMGUI_API float igImFloor_Float(float f) { - return ImFloor(f); -} -CIMGUI_API void igImFloor_Vec2(ImVec2* pOut, const ImVec2 v) { - *pOut = ImFloor(v); -} -CIMGUI_API int igImModPositive(int a, int b) { - return ImModPositive(a, b); -} -CIMGUI_API float igImDot(const ImVec2 a, const ImVec2 b) { - return ImDot(a, b); -} -CIMGUI_API void igImRotate(ImVec2* pOut, - const ImVec2 v, - float cos_a, - float sin_a) { - *pOut = ImRotate(v, cos_a, sin_a); -} -CIMGUI_API float igImLinearSweep(float current, float target, float speed) { - return ImLinearSweep(current, target, speed); -} -CIMGUI_API void igImMul(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs) { - *pOut = ImMul(lhs, rhs); -} -CIMGUI_API bool igImIsFloatAboveGuaranteedIntegerPrecision(float f) { - return ImIsFloatAboveGuaranteedIntegerPrecision(f); -} -CIMGUI_API float igImExponentialMovingAverage(float avg, float sample, int n) { - return ImExponentialMovingAverage(avg, sample, n); -} -CIMGUI_API void igImBezierCubicCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - float t) { - *pOut = ImBezierCubicCalc(p1, p2, p3, p4, t); -} -CIMGUI_API void igImBezierCubicClosestPoint(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - int num_segments) { - *pOut = ImBezierCubicClosestPoint(p1, p2, p3, p4, p, num_segments); -} -CIMGUI_API void igImBezierCubicClosestPointCasteljau(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - float tess_tol) { - *pOut = ImBezierCubicClosestPointCasteljau(p1, p2, p3, p4, p, tess_tol); -} -CIMGUI_API void igImBezierQuadraticCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - float t) { - *pOut = ImBezierQuadraticCalc(p1, p2, p3, t); -} -CIMGUI_API void igImLineClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 p) { - *pOut = ImLineClosestPoint(a, b, p); -} -CIMGUI_API bool igImTriangleContainsPoint(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p) { - return ImTriangleContainsPoint(a, b, c, p); -} -CIMGUI_API void igImTriangleClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p) { - *pOut = ImTriangleClosestPoint(a, b, c, p); -} -CIMGUI_API void igImTriangleBarycentricCoords(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p, - float* out_u, - float* out_v, - float* out_w) { - return ImTriangleBarycentricCoords(a, b, c, p, *out_u, *out_v, *out_w); -} -CIMGUI_API float igImTriangleArea(const ImVec2 a, - const ImVec2 b, - const ImVec2 c) { - return ImTriangleArea(a, b, c); -} -CIMGUI_API bool igImTriangleIsClockwise(const ImVec2 a, - const ImVec2 b, - const ImVec2 c) { - return ImTriangleIsClockwise(a, b, c); -} -CIMGUI_API ImVec1* ImVec1_ImVec1_Nil(void) { - return IM_NEW(ImVec1)(); -} -CIMGUI_API void ImVec1_destroy(ImVec1* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec1* ImVec1_ImVec1_Float(float _x) { - return IM_NEW(ImVec1)(_x); -} -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Nil(void) { - return IM_NEW(ImVec2ih)(); -} -CIMGUI_API void ImVec2ih_destroy(ImVec2ih* self) { - IM_DELETE(self); -} -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_short(short _x, short _y) { - return IM_NEW(ImVec2ih)(_x, _y); -} -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Vec2(const ImVec2 rhs) { - return IM_NEW(ImVec2ih)(rhs); -} -CIMGUI_API ImRect* ImRect_ImRect_Nil(void) { - return IM_NEW(ImRect)(); -} -CIMGUI_API void ImRect_destroy(ImRect* self) { - IM_DELETE(self); -} -CIMGUI_API ImRect* ImRect_ImRect_Vec2(const ImVec2 min, const ImVec2 max) { - return IM_NEW(ImRect)(min, max); -} -CIMGUI_API ImRect* ImRect_ImRect_Vec4(const ImVec4 v) { - return IM_NEW(ImRect)(v); -} -CIMGUI_API ImRect* ImRect_ImRect_Float(float x1, float y1, float x2, float y2) { - return IM_NEW(ImRect)(x1, y1, x2, y2); -} -CIMGUI_API void ImRect_GetCenter(ImVec2* pOut, ImRect* self) { - *pOut = self->GetCenter(); -} -CIMGUI_API void ImRect_GetSize(ImVec2* pOut, ImRect* self) { - *pOut = self->GetSize(); -} -CIMGUI_API float ImRect_GetWidth(ImRect* self) { - return self->GetWidth(); -} -CIMGUI_API float ImRect_GetHeight(ImRect* self) { - return self->GetHeight(); -} -CIMGUI_API float ImRect_GetArea(ImRect* self) { - return self->GetArea(); -} -CIMGUI_API void ImRect_GetTL(ImVec2* pOut, ImRect* self) { - *pOut = self->GetTL(); -} -CIMGUI_API void ImRect_GetTR(ImVec2* pOut, ImRect* self) { - *pOut = self->GetTR(); -} -CIMGUI_API void ImRect_GetBL(ImVec2* pOut, ImRect* self) { - *pOut = self->GetBL(); -} -CIMGUI_API void ImRect_GetBR(ImVec2* pOut, ImRect* self) { - *pOut = self->GetBR(); -} -CIMGUI_API bool ImRect_Contains_Vec2(ImRect* self, const ImVec2 p) { - return self->Contains(p); -} -CIMGUI_API bool ImRect_Contains_Rect(ImRect* self, const ImRect r) { - return self->Contains(r); -} -CIMGUI_API bool ImRect_ContainsWithPad(ImRect* self, - const ImVec2 p, - const ImVec2 pad) { - return self->ContainsWithPad(p, pad); -} -CIMGUI_API bool ImRect_Overlaps(ImRect* self, const ImRect r) { - return self->Overlaps(r); -} -CIMGUI_API void ImRect_Add_Vec2(ImRect* self, const ImVec2 p) { - return self->Add(p); -} -CIMGUI_API void ImRect_Add_Rect(ImRect* self, const ImRect r) { - return self->Add(r); -} -CIMGUI_API void ImRect_Expand_Float(ImRect* self, const float amount) { - return self->Expand(amount); -} -CIMGUI_API void ImRect_Expand_Vec2(ImRect* self, const ImVec2 amount) { - return self->Expand(amount); -} -CIMGUI_API void ImRect_Translate(ImRect* self, const ImVec2 d) { - return self->Translate(d); -} -CIMGUI_API void ImRect_TranslateX(ImRect* self, float dx) { - return self->TranslateX(dx); -} -CIMGUI_API void ImRect_TranslateY(ImRect* self, float dy) { - return self->TranslateY(dy); -} -CIMGUI_API void ImRect_ClipWith(ImRect* self, const ImRect r) { - return self->ClipWith(r); -} -CIMGUI_API void ImRect_ClipWithFull(ImRect* self, const ImRect r) { - return self->ClipWithFull(r); -} -CIMGUI_API void ImRect_Floor(ImRect* self) { - return self->Floor(); -} -CIMGUI_API bool ImRect_IsInverted(ImRect* self) { - return self->IsInverted(); -} -CIMGUI_API void ImRect_ToVec4(ImVec4* pOut, ImRect* self) { - *pOut = self->ToVec4(); -} -CIMGUI_API size_t igImBitArrayGetStorageSizeInBytes(int bitcount) { - return ImBitArrayGetStorageSizeInBytes(bitcount); -} -CIMGUI_API void igImBitArrayClearAllBits(ImU32* arr, int bitcount) { - return ImBitArrayClearAllBits(arr, bitcount); -} -CIMGUI_API bool igImBitArrayTestBit(const ImU32* arr, int n) { - return ImBitArrayTestBit(arr, n); -} -CIMGUI_API void igImBitArrayClearBit(ImU32* arr, int n) { - return ImBitArrayClearBit(arr, n); -} -CIMGUI_API void igImBitArraySetBit(ImU32* arr, int n) { - return ImBitArraySetBit(arr, n); -} -CIMGUI_API void igImBitArraySetBitRange(ImU32* arr, int n, int n2) { - return ImBitArraySetBitRange(arr, n, n2); -} -CIMGUI_API void ImBitVector_Create(ImBitVector* self, int sz) { - return self->Create(sz); -} -CIMGUI_API void ImBitVector_Clear(ImBitVector* self) { - return self->Clear(); -} -CIMGUI_API bool ImBitVector_TestBit(ImBitVector* self, int n) { - return self->TestBit(n); -} -CIMGUI_API void ImBitVector_SetBit(ImBitVector* self, int n) { - return self->SetBit(n); -} -CIMGUI_API void ImBitVector_ClearBit(ImBitVector* self, int n) { - return self->ClearBit(n); -} -CIMGUI_API void ImGuiTextIndex_clear(ImGuiTextIndex* self) { - return self->clear(); -} -CIMGUI_API int ImGuiTextIndex_size(ImGuiTextIndex* self) { - return self->size(); -} -CIMGUI_API const char* ImGuiTextIndex_get_line_begin(ImGuiTextIndex* self, - const char* base, - int n) { - return self->get_line_begin(base, n); -} -CIMGUI_API const char* ImGuiTextIndex_get_line_end(ImGuiTextIndex* self, - const char* base, - int n) { - return self->get_line_end(base, n); -} -CIMGUI_API void ImGuiTextIndex_append(ImGuiTextIndex* self, - const char* base, - int old_size, - int new_size) { - return self->append(base, old_size, new_size); -} -CIMGUI_API ImDrawListSharedData* ImDrawListSharedData_ImDrawListSharedData( - void) { - return IM_NEW(ImDrawListSharedData)(); -} -CIMGUI_API void ImDrawListSharedData_destroy(ImDrawListSharedData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImDrawListSharedData_SetCircleTessellationMaxError( - ImDrawListSharedData* self, - float max_error) { - return self->SetCircleTessellationMaxError(max_error); -} -CIMGUI_API ImDrawDataBuilder* ImDrawDataBuilder_ImDrawDataBuilder(void) { - return IM_NEW(ImDrawDataBuilder)(); -} -CIMGUI_API void ImDrawDataBuilder_destroy(ImDrawDataBuilder* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Int(ImGuiStyleVar idx, - int v) { - return IM_NEW(ImGuiStyleMod)(idx, v); -} -CIMGUI_API void ImGuiStyleMod_destroy(ImGuiStyleMod* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Float(ImGuiStyleVar idx, - float v) { - return IM_NEW(ImGuiStyleMod)(idx, v); -} -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Vec2(ImGuiStyleVar idx, - ImVec2 v) { - return IM_NEW(ImGuiStyleMod)(idx, v); -} -CIMGUI_API ImGuiComboPreviewData* ImGuiComboPreviewData_ImGuiComboPreviewData( - void) { - return IM_NEW(ImGuiComboPreviewData)(); -} -CIMGUI_API void ImGuiComboPreviewData_destroy(ImGuiComboPreviewData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiMenuColumns* ImGuiMenuColumns_ImGuiMenuColumns(void) { - return IM_NEW(ImGuiMenuColumns)(); -} -CIMGUI_API void ImGuiMenuColumns_destroy(ImGuiMenuColumns* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiMenuColumns_Update(ImGuiMenuColumns* self, - float spacing, - bool window_reappearing) { - return self->Update(spacing, window_reappearing); -} -CIMGUI_API float ImGuiMenuColumns_DeclColumns(ImGuiMenuColumns* self, - float w_icon, - float w_label, - float w_shortcut, - float w_mark) { - return self->DeclColumns(w_icon, w_label, w_shortcut, w_mark); -} -CIMGUI_API void ImGuiMenuColumns_CalcNextTotalWidth(ImGuiMenuColumns* self, - bool update_offsets) { - return self->CalcNextTotalWidth(update_offsets); -} -CIMGUI_API ImGuiInputTextDeactivatedState* -ImGuiInputTextDeactivatedState_ImGuiInputTextDeactivatedState(void) { - return IM_NEW(ImGuiInputTextDeactivatedState)(); -} -CIMGUI_API void ImGuiInputTextDeactivatedState_destroy( - ImGuiInputTextDeactivatedState* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiInputTextDeactivatedState_ClearFreeMemory( - ImGuiInputTextDeactivatedState* self) { - return self->ClearFreeMemory(); -} -CIMGUI_API ImGuiInputTextState* ImGuiInputTextState_ImGuiInputTextState(void) { - return IM_NEW(ImGuiInputTextState)(); -} -CIMGUI_API void ImGuiInputTextState_destroy(ImGuiInputTextState* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiInputTextState_ClearText(ImGuiInputTextState* self) { - return self->ClearText(); -} -CIMGUI_API void ImGuiInputTextState_ClearFreeMemory(ImGuiInputTextState* self) { - return self->ClearFreeMemory(); -} -CIMGUI_API int ImGuiInputTextState_GetUndoAvailCount( - ImGuiInputTextState* self) { - return self->GetUndoAvailCount(); -} -CIMGUI_API int ImGuiInputTextState_GetRedoAvailCount( - ImGuiInputTextState* self) { - return self->GetRedoAvailCount(); -} -CIMGUI_API void ImGuiInputTextState_OnKeyPressed(ImGuiInputTextState* self, - int key) { - return self->OnKeyPressed(key); -} -CIMGUI_API void ImGuiInputTextState_CursorAnimReset(ImGuiInputTextState* self) { - return self->CursorAnimReset(); -} -CIMGUI_API void ImGuiInputTextState_CursorClamp(ImGuiInputTextState* self) { - return self->CursorClamp(); -} -CIMGUI_API bool ImGuiInputTextState_HasSelection(ImGuiInputTextState* self) { - return self->HasSelection(); -} -CIMGUI_API void ImGuiInputTextState_ClearSelection(ImGuiInputTextState* self) { - return self->ClearSelection(); -} -CIMGUI_API int ImGuiInputTextState_GetCursorPos(ImGuiInputTextState* self) { - return self->GetCursorPos(); -} -CIMGUI_API int ImGuiInputTextState_GetSelectionStart( - ImGuiInputTextState* self) { - return self->GetSelectionStart(); -} -CIMGUI_API int ImGuiInputTextState_GetSelectionEnd(ImGuiInputTextState* self) { - return self->GetSelectionEnd(); -} -CIMGUI_API void ImGuiInputTextState_SelectAll(ImGuiInputTextState* self) { - return self->SelectAll(); -} -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndSelectAll( - ImGuiInputTextState* self) { - return self->ReloadUserBufAndSelectAll(); -} -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndKeepSelection( - ImGuiInputTextState* self) { - return self->ReloadUserBufAndKeepSelection(); -} -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndMoveToEnd( - ImGuiInputTextState* self) { - return self->ReloadUserBufAndMoveToEnd(); -} -CIMGUI_API ImGuiNextWindowData* ImGuiNextWindowData_ImGuiNextWindowData(void) { - return IM_NEW(ImGuiNextWindowData)(); -} -CIMGUI_API void ImGuiNextWindowData_destroy(ImGuiNextWindowData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiNextWindowData_ClearFlags(ImGuiNextWindowData* self) { - return self->ClearFlags(); -} -CIMGUI_API ImGuiNextItemData* ImGuiNextItemData_ImGuiNextItemData(void) { - return IM_NEW(ImGuiNextItemData)(); -} -CIMGUI_API void ImGuiNextItemData_destroy(ImGuiNextItemData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiNextItemData_ClearFlags(ImGuiNextItemData* self) { - return self->ClearFlags(); -} -CIMGUI_API ImGuiLastItemData* ImGuiLastItemData_ImGuiLastItemData(void) { - return IM_NEW(ImGuiLastItemData)(); -} -CIMGUI_API void ImGuiLastItemData_destroy(ImGuiLastItemData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStackSizes* ImGuiStackSizes_ImGuiStackSizes(void) { - return IM_NEW(ImGuiStackSizes)(); -} -CIMGUI_API void ImGuiStackSizes_destroy(ImGuiStackSizes* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiStackSizes_SetToContextState(ImGuiStackSizes* self, - ImGuiContext* ctx) { - return self->SetToContextState(ctx); -} -CIMGUI_API void ImGuiStackSizes_CompareWithContextState(ImGuiStackSizes* self, - ImGuiContext* ctx) { - return self->CompareWithContextState(ctx); -} -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Ptr(void* ptr) { - return IM_NEW(ImGuiPtrOrIndex)(ptr); -} -CIMGUI_API void ImGuiPtrOrIndex_destroy(ImGuiPtrOrIndex* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Int(int index) { - return IM_NEW(ImGuiPtrOrIndex)(index); -} -CIMGUI_API void* ImGuiDataVarInfo_GetVarPtr(ImGuiDataVarInfo* self, - void* parent) { - return self->GetVarPtr(parent); -} -CIMGUI_API ImGuiPopupData* ImGuiPopupData_ImGuiPopupData(void) { - return IM_NEW(ImGuiPopupData)(); -} -CIMGUI_API void ImGuiPopupData_destroy(ImGuiPopupData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiInputEvent* ImGuiInputEvent_ImGuiInputEvent(void) { - return IM_NEW(ImGuiInputEvent)(); -} -CIMGUI_API void ImGuiInputEvent_destroy(ImGuiInputEvent* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiKeyRoutingData* ImGuiKeyRoutingData_ImGuiKeyRoutingData(void) { - return IM_NEW(ImGuiKeyRoutingData)(); -} -CIMGUI_API void ImGuiKeyRoutingData_destroy(ImGuiKeyRoutingData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiKeyRoutingTable* ImGuiKeyRoutingTable_ImGuiKeyRoutingTable( - void) { - return IM_NEW(ImGuiKeyRoutingTable)(); -} -CIMGUI_API void ImGuiKeyRoutingTable_destroy(ImGuiKeyRoutingTable* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiKeyRoutingTable_Clear(ImGuiKeyRoutingTable* self) { - return self->Clear(); -} -CIMGUI_API ImGuiKeyOwnerData* ImGuiKeyOwnerData_ImGuiKeyOwnerData(void) { - return IM_NEW(ImGuiKeyOwnerData)(); -} -CIMGUI_API void ImGuiKeyOwnerData_destroy(ImGuiKeyOwnerData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiListClipperRange ImGuiListClipperRange_FromIndices(int min, - int max) { - return ImGuiListClipperRange::FromIndices(min, max); -} -CIMGUI_API ImGuiListClipperRange -ImGuiListClipperRange_FromPositions(float y1, - float y2, - int off_min, - int off_max) { - return ImGuiListClipperRange::FromPositions(y1, y2, off_min, off_max); -} -CIMGUI_API ImGuiListClipperData* ImGuiListClipperData_ImGuiListClipperData( - void) { - return IM_NEW(ImGuiListClipperData)(); -} -CIMGUI_API void ImGuiListClipperData_destroy(ImGuiListClipperData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiListClipperData_Reset(ImGuiListClipperData* self, - ImGuiListClipper* clipper) { - return self->Reset(clipper); -} -CIMGUI_API ImGuiNavItemData* ImGuiNavItemData_ImGuiNavItemData(void) { - return IM_NEW(ImGuiNavItemData)(); -} -CIMGUI_API void ImGuiNavItemData_destroy(ImGuiNavItemData* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiNavItemData_Clear(ImGuiNavItemData* self) { - return self->Clear(); -} -CIMGUI_API ImGuiTypingSelectState* -ImGuiTypingSelectState_ImGuiTypingSelectState(void) { - return IM_NEW(ImGuiTypingSelectState)(); -} -CIMGUI_API void ImGuiTypingSelectState_destroy(ImGuiTypingSelectState* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiTypingSelectState_Clear(ImGuiTypingSelectState* self) { - return self->Clear(); -} -CIMGUI_API ImGuiOldColumnData* ImGuiOldColumnData_ImGuiOldColumnData(void) { - return IM_NEW(ImGuiOldColumnData)(); -} -CIMGUI_API void ImGuiOldColumnData_destroy(ImGuiOldColumnData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiOldColumns* ImGuiOldColumns_ImGuiOldColumns(void) { - return IM_NEW(ImGuiOldColumns)(); -} -CIMGUI_API void ImGuiOldColumns_destroy(ImGuiOldColumns* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiDockNode* ImGuiDockNode_ImGuiDockNode(ImGuiID id) { - return IM_NEW(ImGuiDockNode)(id); -} -CIMGUI_API void ImGuiDockNode_destroy(ImGuiDockNode* self) { - IM_DELETE(self); -} -CIMGUI_API bool ImGuiDockNode_IsRootNode(ImGuiDockNode* self) { - return self->IsRootNode(); -} -CIMGUI_API bool ImGuiDockNode_IsDockSpace(ImGuiDockNode* self) { - return self->IsDockSpace(); -} -CIMGUI_API bool ImGuiDockNode_IsFloatingNode(ImGuiDockNode* self) { - return self->IsFloatingNode(); -} -CIMGUI_API bool ImGuiDockNode_IsCentralNode(ImGuiDockNode* self) { - return self->IsCentralNode(); -} -CIMGUI_API bool ImGuiDockNode_IsHiddenTabBar(ImGuiDockNode* self) { - return self->IsHiddenTabBar(); -} -CIMGUI_API bool ImGuiDockNode_IsNoTabBar(ImGuiDockNode* self) { - return self->IsNoTabBar(); -} -CIMGUI_API bool ImGuiDockNode_IsSplitNode(ImGuiDockNode* self) { - return self->IsSplitNode(); -} -CIMGUI_API bool ImGuiDockNode_IsLeafNode(ImGuiDockNode* self) { - return self->IsLeafNode(); -} -CIMGUI_API bool ImGuiDockNode_IsEmpty(ImGuiDockNode* self) { - return self->IsEmpty(); -} -CIMGUI_API void ImGuiDockNode_Rect(ImRect* pOut, ImGuiDockNode* self) { - *pOut = self->Rect(); -} -CIMGUI_API void ImGuiDockNode_SetLocalFlags(ImGuiDockNode* self, - ImGuiDockNodeFlags flags) { - return self->SetLocalFlags(flags); -} -CIMGUI_API void ImGuiDockNode_UpdateMergedFlags(ImGuiDockNode* self) { - return self->UpdateMergedFlags(); -} -CIMGUI_API ImGuiDockContext* ImGuiDockContext_ImGuiDockContext(void) { - return IM_NEW(ImGuiDockContext)(); -} -CIMGUI_API void ImGuiDockContext_destroy(ImGuiDockContext* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiViewportP* ImGuiViewportP_ImGuiViewportP(void) { - return IM_NEW(ImGuiViewportP)(); -} -CIMGUI_API void ImGuiViewportP_destroy(ImGuiViewportP* self) { - IM_DELETE(self); -} -CIMGUI_API void ImGuiViewportP_ClearRequestFlags(ImGuiViewportP* self) { - return self->ClearRequestFlags(); -} -CIMGUI_API void ImGuiViewportP_CalcWorkRectPos(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min) { - *pOut = self->CalcWorkRectPos(off_min); -} -CIMGUI_API void ImGuiViewportP_CalcWorkRectSize(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min, - const ImVec2 off_max) { - *pOut = self->CalcWorkRectSize(off_min, off_max); -} -CIMGUI_API void ImGuiViewportP_UpdateWorkRect(ImGuiViewportP* self) { - return self->UpdateWorkRect(); -} -CIMGUI_API void ImGuiViewportP_GetMainRect(ImRect* pOut, ImGuiViewportP* self) { - *pOut = self->GetMainRect(); -} -CIMGUI_API void ImGuiViewportP_GetWorkRect(ImRect* pOut, ImGuiViewportP* self) { - *pOut = self->GetWorkRect(); -} -CIMGUI_API void ImGuiViewportP_GetBuildWorkRect(ImRect* pOut, - ImGuiViewportP* self) { - *pOut = self->GetBuildWorkRect(); -} -CIMGUI_API ImGuiWindowSettings* ImGuiWindowSettings_ImGuiWindowSettings(void) { - return IM_NEW(ImGuiWindowSettings)(); -} -CIMGUI_API void ImGuiWindowSettings_destroy(ImGuiWindowSettings* self) { - IM_DELETE(self); -} -CIMGUI_API char* ImGuiWindowSettings_GetName(ImGuiWindowSettings* self) { - return self->GetName(); -} -CIMGUI_API ImGuiSettingsHandler* ImGuiSettingsHandler_ImGuiSettingsHandler( - void) { - return IM_NEW(ImGuiSettingsHandler)(); -} -CIMGUI_API void ImGuiSettingsHandler_destroy(ImGuiSettingsHandler* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiDebugAllocInfo* ImGuiDebugAllocInfo_ImGuiDebugAllocInfo(void) { - return IM_NEW(ImGuiDebugAllocInfo)(); -} -CIMGUI_API void ImGuiDebugAllocInfo_destroy(ImGuiDebugAllocInfo* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiStackLevelInfo* ImGuiStackLevelInfo_ImGuiStackLevelInfo(void) { - return IM_NEW(ImGuiStackLevelInfo)(); -} -CIMGUI_API void ImGuiStackLevelInfo_destroy(ImGuiStackLevelInfo* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiIDStackTool* ImGuiIDStackTool_ImGuiIDStackTool(void) { - return IM_NEW(ImGuiIDStackTool)(); -} -CIMGUI_API void ImGuiIDStackTool_destroy(ImGuiIDStackTool* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiContextHook* ImGuiContextHook_ImGuiContextHook(void) { - return IM_NEW(ImGuiContextHook)(); -} -CIMGUI_API void ImGuiContextHook_destroy(ImGuiContextHook* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiContext* ImGuiContext_ImGuiContext( - ImFontAtlas* shared_font_atlas) { - return IM_NEW(ImGuiContext)(shared_font_atlas); -} -CIMGUI_API void ImGuiContext_destroy(ImGuiContext* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiWindow* ImGuiWindow_ImGuiWindow(ImGuiContext* context, - const char* name) { - return IM_NEW(ImGuiWindow)(context, name); -} -CIMGUI_API void ImGuiWindow_destroy(ImGuiWindow* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiID ImGuiWindow_GetID_Str(ImGuiWindow* self, - const char* str, - const char* str_end) { - return self->GetID(str, str_end); -} -CIMGUI_API ImGuiID ImGuiWindow_GetID_Ptr(ImGuiWindow* self, const void* ptr) { - return self->GetID(ptr); -} -CIMGUI_API ImGuiID ImGuiWindow_GetID_Int(ImGuiWindow* self, int n) { - return self->GetID(n); -} -CIMGUI_API ImGuiID ImGuiWindow_GetIDFromRectangle(ImGuiWindow* self, - const ImRect r_abs) { - return self->GetIDFromRectangle(r_abs); -} -CIMGUI_API void ImGuiWindow_Rect(ImRect* pOut, ImGuiWindow* self) { - *pOut = self->Rect(); -} -CIMGUI_API float ImGuiWindow_CalcFontSize(ImGuiWindow* self) { - return self->CalcFontSize(); -} -CIMGUI_API float ImGuiWindow_TitleBarHeight(ImGuiWindow* self) { - return self->TitleBarHeight(); -} -CIMGUI_API void ImGuiWindow_TitleBarRect(ImRect* pOut, ImGuiWindow* self) { - *pOut = self->TitleBarRect(); -} -CIMGUI_API float ImGuiWindow_MenuBarHeight(ImGuiWindow* self) { - return self->MenuBarHeight(); -} -CIMGUI_API void ImGuiWindow_MenuBarRect(ImRect* pOut, ImGuiWindow* self) { - *pOut = self->MenuBarRect(); -} -CIMGUI_API ImGuiTabItem* ImGuiTabItem_ImGuiTabItem(void) { - return IM_NEW(ImGuiTabItem)(); -} -CIMGUI_API void ImGuiTabItem_destroy(ImGuiTabItem* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTabBar* ImGuiTabBar_ImGuiTabBar(void) { - return IM_NEW(ImGuiTabBar)(); -} -CIMGUI_API void ImGuiTabBar_destroy(ImGuiTabBar* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumn* ImGuiTableColumn_ImGuiTableColumn(void) { - return IM_NEW(ImGuiTableColumn)(); -} -CIMGUI_API void ImGuiTableColumn_destroy(ImGuiTableColumn* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableInstanceData* -ImGuiTableInstanceData_ImGuiTableInstanceData(void) { - return IM_NEW(ImGuiTableInstanceData)(); -} -CIMGUI_API void ImGuiTableInstanceData_destroy(ImGuiTableInstanceData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTable* ImGuiTable_ImGuiTable(void) { - return IM_NEW(ImGuiTable)(); -} -CIMGUI_API void ImGuiTable_destroy(ImGuiTable* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableTempData* ImGuiTableTempData_ImGuiTableTempData(void) { - return IM_NEW(ImGuiTableTempData)(); -} -CIMGUI_API void ImGuiTableTempData_destroy(ImGuiTableTempData* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumnSettings* -ImGuiTableColumnSettings_ImGuiTableColumnSettings(void) { - return IM_NEW(ImGuiTableColumnSettings)(); -} -CIMGUI_API void ImGuiTableColumnSettings_destroy( - ImGuiTableColumnSettings* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableSettings* ImGuiTableSettings_ImGuiTableSettings(void) { - return IM_NEW(ImGuiTableSettings)(); -} -CIMGUI_API void ImGuiTableSettings_destroy(ImGuiTableSettings* self) { - IM_DELETE(self); -} -CIMGUI_API ImGuiTableColumnSettings* ImGuiTableSettings_GetColumnSettings( - ImGuiTableSettings* self) { - return self->GetColumnSettings(); -} -CIMGUI_API ImGuiWindow* igGetCurrentWindowRead() { - return ImGui::GetCurrentWindowRead(); -} -CIMGUI_API ImGuiWindow* igGetCurrentWindow() { - return ImGui::GetCurrentWindow(); -} -CIMGUI_API ImGuiWindow* igFindWindowByID(ImGuiID id) { - return ImGui::FindWindowByID(id); -} -CIMGUI_API ImGuiWindow* igFindWindowByName(const char* name) { - return ImGui::FindWindowByName(name); -} -CIMGUI_API void igUpdateWindowParentAndRootLinks(ImGuiWindow* window, - ImGuiWindowFlags flags, - ImGuiWindow* parent_window) { - return ImGui::UpdateWindowParentAndRootLinks(window, flags, parent_window); -} -CIMGUI_API void igUpdateWindowSkipRefresh(ImGuiWindow* window) { - return ImGui::UpdateWindowSkipRefresh(window); -} -CIMGUI_API void igCalcWindowNextAutoFitSize(ImVec2* pOut, ImGuiWindow* window) { - *pOut = ImGui::CalcWindowNextAutoFitSize(window); -} -CIMGUI_API bool igIsWindowChildOf(ImGuiWindow* window, - ImGuiWindow* potential_parent, - bool popup_hierarchy, - bool dock_hierarchy) { - return ImGui::IsWindowChildOf(window, potential_parent, popup_hierarchy, - dock_hierarchy); -} -CIMGUI_API bool igIsWindowWithinBeginStackOf(ImGuiWindow* window, - ImGuiWindow* potential_parent) { - return ImGui::IsWindowWithinBeginStackOf(window, potential_parent); -} -CIMGUI_API bool igIsWindowAbove(ImGuiWindow* potential_above, - ImGuiWindow* potential_below) { - return ImGui::IsWindowAbove(potential_above, potential_below); -} -CIMGUI_API bool igIsWindowNavFocusable(ImGuiWindow* window) { - return ImGui::IsWindowNavFocusable(window); -} -CIMGUI_API void igSetWindowPos_WindowPtr(ImGuiWindow* window, - const ImVec2 pos, - ImGuiCond cond) { - return ImGui::SetWindowPos(window, pos, cond); -} -CIMGUI_API void igSetWindowSize_WindowPtr(ImGuiWindow* window, - const ImVec2 size, - ImGuiCond cond) { - return ImGui::SetWindowSize(window, size, cond); -} -CIMGUI_API void igSetWindowCollapsed_WindowPtr(ImGuiWindow* window, - bool collapsed, - ImGuiCond cond) { - return ImGui::SetWindowCollapsed(window, collapsed, cond); -} -CIMGUI_API void igSetWindowHitTestHole(ImGuiWindow* window, - const ImVec2 pos, - const ImVec2 size) { - return ImGui::SetWindowHitTestHole(window, pos, size); -} -CIMGUI_API void igSetWindowHiddenAndSkipItemsForCurrentFrame( - ImGuiWindow* window) { - return ImGui::SetWindowHiddenAndSkipItemsForCurrentFrame(window); -} -CIMGUI_API void igSetWindowParentWindowForFocusRoute( - ImGuiWindow* window, - ImGuiWindow* parent_window) { - return ImGui::SetWindowParentWindowForFocusRoute(window, parent_window); -} -CIMGUI_API void igWindowRectAbsToRel(ImRect* pOut, - ImGuiWindow* window, - const ImRect r) { - *pOut = ImGui::WindowRectAbsToRel(window, r); -} -CIMGUI_API void igWindowRectRelToAbs(ImRect* pOut, - ImGuiWindow* window, - const ImRect r) { - *pOut = ImGui::WindowRectRelToAbs(window, r); -} -CIMGUI_API void igWindowPosRelToAbs(ImVec2* pOut, - ImGuiWindow* window, - const ImVec2 p) { - *pOut = ImGui::WindowPosRelToAbs(window, p); -} -CIMGUI_API void igFocusWindow(ImGuiWindow* window, - ImGuiFocusRequestFlags flags) { - return ImGui::FocusWindow(window, flags); -} -CIMGUI_API void igFocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, - ImGuiWindow* ignore_window, - ImGuiViewport* filter_viewport, - ImGuiFocusRequestFlags flags) { - return ImGui::FocusTopMostWindowUnderOne(under_this_window, ignore_window, - filter_viewport, flags); -} -CIMGUI_API void igBringWindowToFocusFront(ImGuiWindow* window) { - return ImGui::BringWindowToFocusFront(window); -} -CIMGUI_API void igBringWindowToDisplayFront(ImGuiWindow* window) { - return ImGui::BringWindowToDisplayFront(window); -} -CIMGUI_API void igBringWindowToDisplayBack(ImGuiWindow* window) { - return ImGui::BringWindowToDisplayBack(window); -} -CIMGUI_API void igBringWindowToDisplayBehind(ImGuiWindow* window, - ImGuiWindow* above_window) { - return ImGui::BringWindowToDisplayBehind(window, above_window); -} -CIMGUI_API int igFindWindowDisplayIndex(ImGuiWindow* window) { - return ImGui::FindWindowDisplayIndex(window); -} -CIMGUI_API ImGuiWindow* igFindBottomMostVisibleWindowWithinBeginStack( - ImGuiWindow* window) { - return ImGui::FindBottomMostVisibleWindowWithinBeginStack(window); -} -CIMGUI_API void igSetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags) { - return ImGui::SetNextWindowRefreshPolicy(flags); -} -CIMGUI_API void igSetCurrentFont(ImFont* font) { - return ImGui::SetCurrentFont(font); -} -CIMGUI_API ImFont* igGetDefaultFont() { - return ImGui::GetDefaultFont(); -} -CIMGUI_API ImDrawList* igGetForegroundDrawList_WindowPtr(ImGuiWindow* window) { - return ImGui::GetForegroundDrawList(window); -} -CIMGUI_API void igAddDrawListToDrawDataEx(ImDrawData* draw_data, - ImVector_ImDrawListPtr* out_list, - ImDrawList* draw_list) { - return ImGui::AddDrawListToDrawDataEx(draw_data, out_list, draw_list); -} -CIMGUI_API void igInitialize() { - return ImGui::Initialize(); -} -CIMGUI_API void igShutdown() { - return ImGui::Shutdown(); -} -CIMGUI_API void igUpdateInputEvents(bool trickle_fast_inputs) { - return ImGui::UpdateInputEvents(trickle_fast_inputs); -} -CIMGUI_API void igUpdateHoveredWindowAndCaptureFlags() { - return ImGui::UpdateHoveredWindowAndCaptureFlags(); -} -CIMGUI_API void igStartMouseMovingWindow(ImGuiWindow* window) { - return ImGui::StartMouseMovingWindow(window); -} -CIMGUI_API void igStartMouseMovingWindowOrNode(ImGuiWindow* window, - ImGuiDockNode* node, - bool undock) { - return ImGui::StartMouseMovingWindowOrNode(window, node, undock); -} -CIMGUI_API void igUpdateMouseMovingWindowNewFrame() { - return ImGui::UpdateMouseMovingWindowNewFrame(); -} -CIMGUI_API void igUpdateMouseMovingWindowEndFrame() { - return ImGui::UpdateMouseMovingWindowEndFrame(); -} -CIMGUI_API ImGuiID igAddContextHook(ImGuiContext* context, - const ImGuiContextHook* hook) { - return ImGui::AddContextHook(context, hook); -} -CIMGUI_API void igRemoveContextHook(ImGuiContext* context, - ImGuiID hook_to_remove) { - return ImGui::RemoveContextHook(context, hook_to_remove); -} -CIMGUI_API void igCallContextHooks(ImGuiContext* context, - ImGuiContextHookType type) { - return ImGui::CallContextHooks(context, type); -} -CIMGUI_API void igTranslateWindowsInViewport(ImGuiViewportP* viewport, - const ImVec2 old_pos, - const ImVec2 new_pos) { - return ImGui::TranslateWindowsInViewport(viewport, old_pos, new_pos); -} -CIMGUI_API void igScaleWindowsInViewport(ImGuiViewportP* viewport, - float scale) { - return ImGui::ScaleWindowsInViewport(viewport, scale); -} -CIMGUI_API void igDestroyPlatformWindow(ImGuiViewportP* viewport) { - return ImGui::DestroyPlatformWindow(viewport); -} -CIMGUI_API void igSetWindowViewport(ImGuiWindow* window, - ImGuiViewportP* viewport) { - return ImGui::SetWindowViewport(window, viewport); -} -CIMGUI_API void igSetCurrentViewport(ImGuiWindow* window, - ImGuiViewportP* viewport) { - return ImGui::SetCurrentViewport(window, viewport); -} -CIMGUI_API const ImGuiPlatformMonitor* igGetViewportPlatformMonitor( - ImGuiViewport* viewport) { - return ImGui::GetViewportPlatformMonitor(viewport); -} -CIMGUI_API ImGuiViewportP* igFindHoveredViewportFromPlatformWindowStack( - const ImVec2 mouse_platform_pos) { - return ImGui::FindHoveredViewportFromPlatformWindowStack(mouse_platform_pos); -} -CIMGUI_API void igMarkIniSettingsDirty_Nil() { - return ImGui::MarkIniSettingsDirty(); -} -CIMGUI_API void igMarkIniSettingsDirty_WindowPtr(ImGuiWindow* window) { - return ImGui::MarkIniSettingsDirty(window); -} -CIMGUI_API void igClearIniSettings() { - return ImGui::ClearIniSettings(); -} -CIMGUI_API void igAddSettingsHandler(const ImGuiSettingsHandler* handler) { - return ImGui::AddSettingsHandler(handler); -} -CIMGUI_API void igRemoveSettingsHandler(const char* type_name) { - return ImGui::RemoveSettingsHandler(type_name); -} -CIMGUI_API ImGuiSettingsHandler* igFindSettingsHandler(const char* type_name) { - return ImGui::FindSettingsHandler(type_name); -} -CIMGUI_API ImGuiWindowSettings* igCreateNewWindowSettings(const char* name) { - return ImGui::CreateNewWindowSettings(name); -} -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByID(ImGuiID id) { - return ImGui::FindWindowSettingsByID(id); -} -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByWindow( - ImGuiWindow* window) { - return ImGui::FindWindowSettingsByWindow(window); -} -CIMGUI_API void igClearWindowSettings(const char* name) { - return ImGui::ClearWindowSettings(name); -} -CIMGUI_API void igLocalizeRegisterEntries(const ImGuiLocEntry* entries, - int count) { - return ImGui::LocalizeRegisterEntries(entries, count); -} -CIMGUI_API const char* igLocalizeGetMsg(ImGuiLocKey key) { - return ImGui::LocalizeGetMsg(key); -} -CIMGUI_API void igSetScrollX_WindowPtr(ImGuiWindow* window, float scroll_x) { - return ImGui::SetScrollX(window, scroll_x); -} -CIMGUI_API void igSetScrollY_WindowPtr(ImGuiWindow* window, float scroll_y) { - return ImGui::SetScrollY(window, scroll_y); -} -CIMGUI_API void igSetScrollFromPosX_WindowPtr(ImGuiWindow* window, - float local_x, - float center_x_ratio) { - return ImGui::SetScrollFromPosX(window, local_x, center_x_ratio); -} -CIMGUI_API void igSetScrollFromPosY_WindowPtr(ImGuiWindow* window, - float local_y, - float center_y_ratio) { - return ImGui::SetScrollFromPosY(window, local_y, center_y_ratio); -} -CIMGUI_API void igScrollToItem(ImGuiScrollFlags flags) { - return ImGui::ScrollToItem(flags); -} -CIMGUI_API void igScrollToRect(ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags) { - return ImGui::ScrollToRect(window, rect, flags); -} -CIMGUI_API void igScrollToRectEx(ImVec2* pOut, - ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags) { - *pOut = ImGui::ScrollToRectEx(window, rect, flags); -} -CIMGUI_API void igScrollToBringRectIntoView(ImGuiWindow* window, - const ImRect rect) { - return ImGui::ScrollToBringRectIntoView(window, rect); -} -CIMGUI_API ImGuiItemStatusFlags igGetItemStatusFlags() { - return ImGui::GetItemStatusFlags(); -} -CIMGUI_API ImGuiItemFlags igGetItemFlags() { - return ImGui::GetItemFlags(); -} -CIMGUI_API ImGuiID igGetActiveID() { - return ImGui::GetActiveID(); -} -CIMGUI_API ImGuiID igGetFocusID() { - return ImGui::GetFocusID(); -} -CIMGUI_API void igSetActiveID(ImGuiID id, ImGuiWindow* window) { - return ImGui::SetActiveID(id, window); -} -CIMGUI_API void igSetFocusID(ImGuiID id, ImGuiWindow* window) { - return ImGui::SetFocusID(id, window); -} -CIMGUI_API void igClearActiveID() { - return ImGui::ClearActiveID(); -} -CIMGUI_API ImGuiID igGetHoveredID() { - return ImGui::GetHoveredID(); -} -CIMGUI_API void igSetHoveredID(ImGuiID id) { - return ImGui::SetHoveredID(id); -} -CIMGUI_API void igKeepAliveID(ImGuiID id) { - return ImGui::KeepAliveID(id); -} -CIMGUI_API void igMarkItemEdited(ImGuiID id) { - return ImGui::MarkItemEdited(id); -} -CIMGUI_API void igPushOverrideID(ImGuiID id) { - return ImGui::PushOverrideID(id); -} -CIMGUI_API ImGuiID igGetIDWithSeed_Str(const char* str_id_begin, - const char* str_id_end, - ImGuiID seed) { - return ImGui::GetIDWithSeed(str_id_begin, str_id_end, seed); -} -CIMGUI_API ImGuiID igGetIDWithSeed_Int(int n, ImGuiID seed) { - return ImGui::GetIDWithSeed(n, seed); -} -CIMGUI_API void igItemSize_Vec2(const ImVec2 size, float text_baseline_y) { - return ImGui::ItemSize(size, text_baseline_y); -} -CIMGUI_API void igItemSize_Rect(const ImRect bb, float text_baseline_y) { - return ImGui::ItemSize(bb, text_baseline_y); -} -CIMGUI_API bool igItemAdd(const ImRect bb, - ImGuiID id, - const ImRect* nav_bb, - ImGuiItemFlags extra_flags) { - return ImGui::ItemAdd(bb, id, nav_bb, extra_flags); -} -CIMGUI_API bool igItemHoverable(const ImRect bb, - ImGuiID id, - ImGuiItemFlags item_flags) { - return ImGui::ItemHoverable(bb, id, item_flags); -} -CIMGUI_API bool igIsWindowContentHoverable(ImGuiWindow* window, - ImGuiHoveredFlags flags) { - return ImGui::IsWindowContentHoverable(window, flags); -} -CIMGUI_API bool igIsClippedEx(const ImRect bb, ImGuiID id) { - return ImGui::IsClippedEx(bb, id); -} -CIMGUI_API void igSetLastItemData(ImGuiID item_id, - ImGuiItemFlags in_flags, - ImGuiItemStatusFlags status_flags, - const ImRect item_rect) { - return ImGui::SetLastItemData(item_id, in_flags, status_flags, item_rect); -} -CIMGUI_API void igCalcItemSize(ImVec2* pOut, - ImVec2 size, - float default_w, - float default_h) { - *pOut = ImGui::CalcItemSize(size, default_w, default_h); -} -CIMGUI_API float igCalcWrapWidthForPos(const ImVec2 pos, float wrap_pos_x) { - return ImGui::CalcWrapWidthForPos(pos, wrap_pos_x); -} -CIMGUI_API void igPushMultiItemsWidths(int components, float width_full) { - return ImGui::PushMultiItemsWidths(components, width_full); -} -CIMGUI_API bool igIsItemToggledSelection() { - return ImGui::IsItemToggledSelection(); -} -CIMGUI_API void igGetContentRegionMaxAbs(ImVec2* pOut) { - *pOut = ImGui::GetContentRegionMaxAbs(); -} -CIMGUI_API void igShrinkWidths(ImGuiShrinkWidthItem* items, - int count, - float width_excess) { - return ImGui::ShrinkWidths(items, count, width_excess); -} -CIMGUI_API void igPushItemFlag(ImGuiItemFlags option, bool enabled) { - return ImGui::PushItemFlag(option, enabled); -} -CIMGUI_API void igPopItemFlag() { - return ImGui::PopItemFlag(); -} -CIMGUI_API const ImGuiDataVarInfo* igGetStyleVarInfo(ImGuiStyleVar idx) { - return ImGui::GetStyleVarInfo(idx); -} -CIMGUI_API void igLogBegin(ImGuiLogType type, int auto_open_depth) { - return ImGui::LogBegin(type, auto_open_depth); -} -CIMGUI_API void igLogToBuffer(int auto_open_depth) { - return ImGui::LogToBuffer(auto_open_depth); -} -CIMGUI_API void igLogRenderedText(const ImVec2* ref_pos, - const char* text, - const char* text_end) { - return ImGui::LogRenderedText(ref_pos, text, text_end); -} -CIMGUI_API void igLogSetNextTextDecoration(const char* prefix, - const char* suffix) { - return ImGui::LogSetNextTextDecoration(prefix, suffix); -} -CIMGUI_API bool igBeginChildEx(const char* name, - ImGuiID id, - const ImVec2 size_arg, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags) { - return ImGui::BeginChildEx(name, id, size_arg, child_flags, window_flags); -} -CIMGUI_API void igOpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) { - return ImGui::OpenPopupEx(id, popup_flags); -} -CIMGUI_API void igClosePopupToLevel(int remaining, - bool restore_focus_to_window_under_popup) { - return ImGui::ClosePopupToLevel(remaining, - restore_focus_to_window_under_popup); -} -CIMGUI_API void igClosePopupsOverWindow( - ImGuiWindow* ref_window, - bool restore_focus_to_window_under_popup) { - return ImGui::ClosePopupsOverWindow(ref_window, - restore_focus_to_window_under_popup); -} -CIMGUI_API void igClosePopupsExceptModals() { - return ImGui::ClosePopupsExceptModals(); -} -CIMGUI_API bool igIsPopupOpen_ID(ImGuiID id, ImGuiPopupFlags popup_flags) { - return ImGui::IsPopupOpen(id, popup_flags); -} -CIMGUI_API bool igBeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags) { - return ImGui::BeginPopupEx(id, extra_flags); -} -CIMGUI_API bool igBeginTooltipEx(ImGuiTooltipFlags tooltip_flags, - ImGuiWindowFlags extra_window_flags) { - return ImGui::BeginTooltipEx(tooltip_flags, extra_window_flags); -} -CIMGUI_API bool igBeginTooltipHidden() { - return ImGui::BeginTooltipHidden(); -} -CIMGUI_API void igGetPopupAllowedExtentRect(ImRect* pOut, ImGuiWindow* window) { - *pOut = ImGui::GetPopupAllowedExtentRect(window); -} -CIMGUI_API ImGuiWindow* igGetTopMostPopupModal() { - return ImGui::GetTopMostPopupModal(); -} -CIMGUI_API ImGuiWindow* igGetTopMostAndVisiblePopupModal() { - return ImGui::GetTopMostAndVisiblePopupModal(); -} -CIMGUI_API ImGuiWindow* igFindBlockingModal(ImGuiWindow* window) { - return ImGui::FindBlockingModal(window); -} -CIMGUI_API void igFindBestWindowPosForPopup(ImVec2* pOut, ImGuiWindow* window) { - *pOut = ImGui::FindBestWindowPosForPopup(window); -} -CIMGUI_API void igFindBestWindowPosForPopupEx(ImVec2* pOut, - const ImVec2 ref_pos, - const ImVec2 size, - ImGuiDir* last_dir, - const ImRect r_outer, - const ImRect r_avoid, - ImGuiPopupPositionPolicy policy) { - *pOut = ImGui::FindBestWindowPosForPopupEx(ref_pos, size, last_dir, r_outer, - r_avoid, policy); -} -CIMGUI_API bool igBeginViewportSideBar(const char* name, - ImGuiViewport* viewport, - ImGuiDir dir, - float size, - ImGuiWindowFlags window_flags) { - return ImGui::BeginViewportSideBar(name, viewport, dir, size, window_flags); -} -CIMGUI_API bool igBeginMenuEx(const char* label, - const char* icon, - bool enabled) { - return ImGui::BeginMenuEx(label, icon, enabled); -} -CIMGUI_API bool igMenuItemEx(const char* label, - const char* icon, - const char* shortcut, - bool selected, - bool enabled) { - return ImGui::MenuItemEx(label, icon, shortcut, selected, enabled); -} -CIMGUI_API bool igBeginComboPopup(ImGuiID popup_id, - const ImRect bb, - ImGuiComboFlags flags) { - return ImGui::BeginComboPopup(popup_id, bb, flags); -} -CIMGUI_API bool igBeginComboPreview() { - return ImGui::BeginComboPreview(); -} -CIMGUI_API void igEndComboPreview() { - return ImGui::EndComboPreview(); -} -CIMGUI_API void igNavInitWindow(ImGuiWindow* window, bool force_reinit) { - return ImGui::NavInitWindow(window, force_reinit); -} -CIMGUI_API void igNavInitRequestApplyResult() { - return ImGui::NavInitRequestApplyResult(); -} -CIMGUI_API bool igNavMoveRequestButNoResultYet() { - return ImGui::NavMoveRequestButNoResultYet(); -} -CIMGUI_API void igNavMoveRequestSubmit(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags) { - return ImGui::NavMoveRequestSubmit(move_dir, clip_dir, move_flags, - scroll_flags); -} -CIMGUI_API void igNavMoveRequestForward(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags) { - return ImGui::NavMoveRequestForward(move_dir, clip_dir, move_flags, - scroll_flags); -} -CIMGUI_API void igNavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) { - return ImGui::NavMoveRequestResolveWithLastItem(result); -} -CIMGUI_API void igNavMoveRequestResolveWithPastTreeNode( - ImGuiNavItemData* result, - ImGuiNavTreeNodeData* tree_node_data) { - return ImGui::NavMoveRequestResolveWithPastTreeNode(result, tree_node_data); -} -CIMGUI_API void igNavMoveRequestCancel() { - return ImGui::NavMoveRequestCancel(); -} -CIMGUI_API void igNavMoveRequestApplyResult() { - return ImGui::NavMoveRequestApplyResult(); -} -CIMGUI_API void igNavMoveRequestTryWrapping(ImGuiWindow* window, - ImGuiNavMoveFlags move_flags) { - return ImGui::NavMoveRequestTryWrapping(window, move_flags); -} -CIMGUI_API void igNavHighlightActivated(ImGuiID id) { - return ImGui::NavHighlightActivated(id); -} -CIMGUI_API void igNavClearPreferredPosForAxis(ImGuiAxis axis) { - return ImGui::NavClearPreferredPosForAxis(axis); -} -CIMGUI_API void igNavRestoreHighlightAfterMove() { - return ImGui::NavRestoreHighlightAfterMove(); -} -CIMGUI_API void igNavUpdateCurrentWindowIsScrollPushableX() { - return ImGui::NavUpdateCurrentWindowIsScrollPushableX(); -} -CIMGUI_API void igSetNavWindow(ImGuiWindow* window) { - return ImGui::SetNavWindow(window); -} -CIMGUI_API void igSetNavID(ImGuiID id, - ImGuiNavLayer nav_layer, - ImGuiID focus_scope_id, - const ImRect rect_rel) { - return ImGui::SetNavID(id, nav_layer, focus_scope_id, rect_rel); -} -CIMGUI_API void igSetNavFocusScope(ImGuiID focus_scope_id) { - return ImGui::SetNavFocusScope(focus_scope_id); -} -CIMGUI_API void igFocusItem() { - return ImGui::FocusItem(); -} -CIMGUI_API void igActivateItemByID(ImGuiID id) { - return ImGui::ActivateItemByID(id); -} -CIMGUI_API bool igIsNamedKey(ImGuiKey key) { - return ImGui::IsNamedKey(key); -} -CIMGUI_API bool igIsNamedKeyOrModKey(ImGuiKey key) { - return ImGui::IsNamedKeyOrModKey(key); -} -CIMGUI_API bool igIsLegacyKey(ImGuiKey key) { - return ImGui::IsLegacyKey(key); -} -CIMGUI_API bool igIsKeyboardKey(ImGuiKey key) { - return ImGui::IsKeyboardKey(key); -} -CIMGUI_API bool igIsGamepadKey(ImGuiKey key) { - return ImGui::IsGamepadKey(key); -} -CIMGUI_API bool igIsMouseKey(ImGuiKey key) { - return ImGui::IsMouseKey(key); -} -CIMGUI_API bool igIsAliasKey(ImGuiKey key) { - return ImGui::IsAliasKey(key); -} -CIMGUI_API bool igIsModKey(ImGuiKey key) { - return ImGui::IsModKey(key); -} -CIMGUI_API ImGuiKeyChord igFixupKeyChord(ImGuiContext* ctx, - ImGuiKeyChord key_chord) { - return ImGui::FixupKeyChord(ctx, key_chord); -} -CIMGUI_API ImGuiKey igConvertSingleModFlagToKey(ImGuiContext* ctx, - ImGuiKey key) { - return ImGui::ConvertSingleModFlagToKey(ctx, key); -} -CIMGUI_API ImGuiKeyData* igGetKeyData_ContextPtr(ImGuiContext* ctx, - ImGuiKey key) { - return ImGui::GetKeyData(ctx, key); -} -CIMGUI_API ImGuiKeyData* igGetKeyData_Key(ImGuiKey key) { - return ImGui::GetKeyData(key); -} -CIMGUI_API const char* igGetKeyChordName(ImGuiKeyChord key_chord) { - return ImGui::GetKeyChordName(key_chord); -} -CIMGUI_API ImGuiKey igMouseButtonToKey(ImGuiMouseButton button) { - return ImGui::MouseButtonToKey(button); -} -CIMGUI_API bool igIsMouseDragPastThreshold(ImGuiMouseButton button, - float lock_threshold) { - return ImGui::IsMouseDragPastThreshold(button, lock_threshold); -} -CIMGUI_API void igGetKeyMagnitude2d(ImVec2* pOut, - ImGuiKey key_left, - ImGuiKey key_right, - ImGuiKey key_up, - ImGuiKey key_down) { - *pOut = ImGui::GetKeyMagnitude2d(key_left, key_right, key_up, key_down); -} -CIMGUI_API float igGetNavTweakPressedAmount(ImGuiAxis axis) { - return ImGui::GetNavTweakPressedAmount(axis); -} -CIMGUI_API int igCalcTypematicRepeatAmount(float t0, - float t1, - float repeat_delay, - float repeat_rate) { - return ImGui::CalcTypematicRepeatAmount(t0, t1, repeat_delay, repeat_rate); -} -CIMGUI_API void igGetTypematicRepeatRate(ImGuiInputFlags flags, - float* repeat_delay, - float* repeat_rate) { - return ImGui::GetTypematicRepeatRate(flags, repeat_delay, repeat_rate); -} -CIMGUI_API void igTeleportMousePos(const ImVec2 pos) { - return ImGui::TeleportMousePos(pos); -} -CIMGUI_API void igSetActiveIdUsingAllKeyboardKeys() { - return ImGui::SetActiveIdUsingAllKeyboardKeys(); -} -CIMGUI_API bool igIsActiveIdUsingNavDir(ImGuiDir dir) { - return ImGui::IsActiveIdUsingNavDir(dir); -} -CIMGUI_API ImGuiID igGetKeyOwner(ImGuiKey key) { - return ImGui::GetKeyOwner(key); -} -CIMGUI_API void igSetKeyOwner(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::SetKeyOwner(key, owner_id, flags); -} -CIMGUI_API void igSetKeyOwnersForKeyChord(ImGuiKeyChord key, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::SetKeyOwnersForKeyChord(key, owner_id, flags); -} -CIMGUI_API void igSetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) { - return ImGui::SetItemKeyOwner(key, flags); -} -CIMGUI_API bool igTestKeyOwner(ImGuiKey key, ImGuiID owner_id) { - return ImGui::TestKeyOwner(key, owner_id); -} -CIMGUI_API ImGuiKeyOwnerData* igGetKeyOwnerData(ImGuiContext* ctx, - ImGuiKey key) { - return ImGui::GetKeyOwnerData(ctx, key); -} -CIMGUI_API bool igIsKeyDown_ID(ImGuiKey key, ImGuiID owner_id) { - return ImGui::IsKeyDown(key, owner_id); -} -CIMGUI_API bool igIsKeyPressed_ID(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::IsKeyPressed(key, owner_id, flags); -} -CIMGUI_API bool igIsKeyReleased_ID(ImGuiKey key, ImGuiID owner_id) { - return ImGui::IsKeyReleased(key, owner_id); -} -CIMGUI_API bool igIsMouseDown_ID(ImGuiMouseButton button, ImGuiID owner_id) { - return ImGui::IsMouseDown(button, owner_id); -} -CIMGUI_API bool igIsMouseClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::IsMouseClicked(button, owner_id, flags); -} -CIMGUI_API bool igIsMouseReleased_ID(ImGuiMouseButton button, - ImGuiID owner_id) { - return ImGui::IsMouseReleased(button, owner_id); -} -CIMGUI_API bool igIsMouseDoubleClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id) { - return ImGui::IsMouseDoubleClicked(button, owner_id); -} -CIMGUI_API bool igIsKeyChordPressed_ID(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::IsKeyChordPressed(key_chord, owner_id, flags); -} -CIMGUI_API void igSetNextItemShortcut(ImGuiKeyChord key_chord) { - return ImGui::SetNextItemShortcut(key_chord); -} -CIMGUI_API bool igShortcut(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::Shortcut(key_chord, owner_id, flags); -} -CIMGUI_API bool igSetShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags) { - return ImGui::SetShortcutRouting(key_chord, owner_id, flags); -} -CIMGUI_API bool igTestShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id) { - return ImGui::TestShortcutRouting(key_chord, owner_id); -} -CIMGUI_API ImGuiKeyRoutingData* igGetShortcutRoutingData( - ImGuiKeyChord key_chord) { - return ImGui::GetShortcutRoutingData(key_chord); -} -CIMGUI_API void igDockContextInitialize(ImGuiContext* ctx) { - return ImGui::DockContextInitialize(ctx); -} -CIMGUI_API void igDockContextShutdown(ImGuiContext* ctx) { - return ImGui::DockContextShutdown(ctx); -} -CIMGUI_API void igDockContextClearNodes(ImGuiContext* ctx, - ImGuiID root_id, - bool clear_settings_refs) { - return ImGui::DockContextClearNodes(ctx, root_id, clear_settings_refs); -} -CIMGUI_API void igDockContextRebuildNodes(ImGuiContext* ctx) { - return ImGui::DockContextRebuildNodes(ctx); -} -CIMGUI_API void igDockContextNewFrameUpdateUndocking(ImGuiContext* ctx) { - return ImGui::DockContextNewFrameUpdateUndocking(ctx); -} -CIMGUI_API void igDockContextNewFrameUpdateDocking(ImGuiContext* ctx) { - return ImGui::DockContextNewFrameUpdateDocking(ctx); -} -CIMGUI_API void igDockContextEndFrame(ImGuiContext* ctx) { - return ImGui::DockContextEndFrame(ctx); -} -CIMGUI_API ImGuiID igDockContextGenNodeID(ImGuiContext* ctx) { - return ImGui::DockContextGenNodeID(ctx); -} -CIMGUI_API void igDockContextQueueDock(ImGuiContext* ctx, - ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload, - ImGuiDir split_dir, - float split_ratio, - bool split_outer) { - return ImGui::DockContextQueueDock(ctx, target, target_node, payload, - split_dir, split_ratio, split_outer); -} -CIMGUI_API void igDockContextQueueUndockWindow(ImGuiContext* ctx, - ImGuiWindow* window) { - return ImGui::DockContextQueueUndockWindow(ctx, window); -} -CIMGUI_API void igDockContextQueueUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node) { - return ImGui::DockContextQueueUndockNode(ctx, node); -} -CIMGUI_API void igDockContextProcessUndockWindow( - ImGuiContext* ctx, - ImGuiWindow* window, - bool clear_persistent_docking_ref) { - return ImGui::DockContextProcessUndockWindow(ctx, window, - clear_persistent_docking_ref); -} -CIMGUI_API void igDockContextProcessUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node) { - return ImGui::DockContextProcessUndockNode(ctx, node); -} -CIMGUI_API bool igDockContextCalcDropPosForDocking(ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload_window, - ImGuiDockNode* payload_node, - ImGuiDir split_dir, - bool split_outer, - ImVec2* out_pos) { - return ImGui::DockContextCalcDropPosForDocking( - target, target_node, payload_window, payload_node, split_dir, split_outer, - out_pos); -} -CIMGUI_API ImGuiDockNode* igDockContextFindNodeByID(ImGuiContext* ctx, - ImGuiID id) { - return ImGui::DockContextFindNodeByID(ctx, id); -} -CIMGUI_API void igDockNodeWindowMenuHandler_Default(ImGuiContext* ctx, - ImGuiDockNode* node, - ImGuiTabBar* tab_bar) { - return ImGui::DockNodeWindowMenuHandler_Default(ctx, node, tab_bar); -} -CIMGUI_API bool igDockNodeBeginAmendTabBar(ImGuiDockNode* node) { - return ImGui::DockNodeBeginAmendTabBar(node); -} -CIMGUI_API void igDockNodeEndAmendTabBar() { - return ImGui::DockNodeEndAmendTabBar(); -} -CIMGUI_API ImGuiDockNode* igDockNodeGetRootNode(ImGuiDockNode* node) { - return ImGui::DockNodeGetRootNode(node); -} -CIMGUI_API bool igDockNodeIsInHierarchyOf(ImGuiDockNode* node, - ImGuiDockNode* parent) { - return ImGui::DockNodeIsInHierarchyOf(node, parent); -} -CIMGUI_API int igDockNodeGetDepth(const ImGuiDockNode* node) { - return ImGui::DockNodeGetDepth(node); -} -CIMGUI_API ImGuiID igDockNodeGetWindowMenuButtonId(const ImGuiDockNode* node) { - return ImGui::DockNodeGetWindowMenuButtonId(node); -} -CIMGUI_API ImGuiDockNode* igGetWindowDockNode() { - return ImGui::GetWindowDockNode(); -} -CIMGUI_API bool igGetWindowAlwaysWantOwnTabBar(ImGuiWindow* window) { - return ImGui::GetWindowAlwaysWantOwnTabBar(window); -} -CIMGUI_API void igBeginDocked(ImGuiWindow* window, bool* p_open) { - return ImGui::BeginDocked(window, p_open); -} -CIMGUI_API void igBeginDockableDragDropSource(ImGuiWindow* window) { - return ImGui::BeginDockableDragDropSource(window); -} -CIMGUI_API void igBeginDockableDragDropTarget(ImGuiWindow* window) { - return ImGui::BeginDockableDragDropTarget(window); -} -CIMGUI_API void igSetWindowDock(ImGuiWindow* window, - ImGuiID dock_id, - ImGuiCond cond) { - return ImGui::SetWindowDock(window, dock_id, cond); -} -CIMGUI_API void igDockBuilderDockWindow(const char* window_name, - ImGuiID node_id) { - return ImGui::DockBuilderDockWindow(window_name, node_id); -} -CIMGUI_API ImGuiDockNode* igDockBuilderGetNode(ImGuiID node_id) { - return ImGui::DockBuilderGetNode(node_id); -} -CIMGUI_API ImGuiDockNode* igDockBuilderGetCentralNode(ImGuiID node_id) { - return ImGui::DockBuilderGetCentralNode(node_id); -} -CIMGUI_API ImGuiID igDockBuilderAddNode(ImGuiID node_id, - ImGuiDockNodeFlags flags) { - return ImGui::DockBuilderAddNode(node_id, flags); -} -CIMGUI_API void igDockBuilderRemoveNode(ImGuiID node_id) { - return ImGui::DockBuilderRemoveNode(node_id); -} -CIMGUI_API void igDockBuilderRemoveNodeDockedWindows(ImGuiID node_id, - bool clear_settings_refs) { - return ImGui::DockBuilderRemoveNodeDockedWindows(node_id, - clear_settings_refs); -} -CIMGUI_API void igDockBuilderRemoveNodeChildNodes(ImGuiID node_id) { - return ImGui::DockBuilderRemoveNodeChildNodes(node_id); -} -CIMGUI_API void igDockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos) { - return ImGui::DockBuilderSetNodePos(node_id, pos); -} -CIMGUI_API void igDockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size) { - return ImGui::DockBuilderSetNodeSize(node_id, size); -} -CIMGUI_API ImGuiID igDockBuilderSplitNode(ImGuiID node_id, - ImGuiDir split_dir, - float size_ratio_for_node_at_dir, - ImGuiID* out_id_at_dir, - ImGuiID* out_id_at_opposite_dir) { - return ImGui::DockBuilderSplitNode(node_id, split_dir, - size_ratio_for_node_at_dir, out_id_at_dir, - out_id_at_opposite_dir); -} -CIMGUI_API void igDockBuilderCopyDockSpace( - ImGuiID src_dockspace_id, - ImGuiID dst_dockspace_id, - ImVector_const_charPtr* in_window_remap_pairs) { - return ImGui::DockBuilderCopyDockSpace(src_dockspace_id, dst_dockspace_id, - in_window_remap_pairs); -} -CIMGUI_API void igDockBuilderCopyNode(ImGuiID src_node_id, - ImGuiID dst_node_id, - ImVector_ImGuiID* out_node_remap_pairs) { - return ImGui::DockBuilderCopyNode(src_node_id, dst_node_id, - out_node_remap_pairs); -} -CIMGUI_API void igDockBuilderCopyWindowSettings(const char* src_name, - const char* dst_name) { - return ImGui::DockBuilderCopyWindowSettings(src_name, dst_name); -} -CIMGUI_API void igDockBuilderFinish(ImGuiID node_id) { - return ImGui::DockBuilderFinish(node_id); -} -CIMGUI_API void igPushFocusScope(ImGuiID id) { - return ImGui::PushFocusScope(id); -} -CIMGUI_API void igPopFocusScope() { - return ImGui::PopFocusScope(); -} -CIMGUI_API ImGuiID igGetCurrentFocusScope() { - return ImGui::GetCurrentFocusScope(); -} -CIMGUI_API bool igIsDragDropActive() { - return ImGui::IsDragDropActive(); -} -CIMGUI_API bool igBeginDragDropTargetCustom(const ImRect bb, ImGuiID id) { - return ImGui::BeginDragDropTargetCustom(bb, id); -} -CIMGUI_API void igClearDragDrop() { - return ImGui::ClearDragDrop(); -} -CIMGUI_API bool igIsDragDropPayloadBeingAccepted() { - return ImGui::IsDragDropPayloadBeingAccepted(); -} -CIMGUI_API void igRenderDragDropTargetRect(const ImRect bb, - const ImRect item_clip_rect) { - return ImGui::RenderDragDropTargetRect(bb, item_clip_rect); -} -CIMGUI_API ImGuiTypingSelectRequest* igGetTypingSelectRequest( - ImGuiTypingSelectFlags flags) { - return ImGui::GetTypingSelectRequest(flags); -} -CIMGUI_API int igTypingSelectFindMatch(ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, - int), - void* user_data, - int nav_item_idx) { - return ImGui::TypingSelectFindMatch(req, items_count, get_item_name_func, - user_data, nav_item_idx); -} -CIMGUI_API int igTypingSelectFindNextSingleCharMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data, - int nav_item_idx) { - return ImGui::TypingSelectFindNextSingleCharMatch( - req, items_count, get_item_name_func, user_data, nav_item_idx); -} -CIMGUI_API int igTypingSelectFindBestLeadingMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data) { - return ImGui::TypingSelectFindBestLeadingMatch(req, items_count, - get_item_name_func, user_data); -} -CIMGUI_API void igSetWindowClipRectBeforeSetChannel(ImGuiWindow* window, - const ImRect clip_rect) { - return ImGui::SetWindowClipRectBeforeSetChannel(window, clip_rect); -} -CIMGUI_API void igBeginColumns(const char* str_id, - int count, - ImGuiOldColumnFlags flags) { - return ImGui::BeginColumns(str_id, count, flags); -} -CIMGUI_API void igEndColumns() { - return ImGui::EndColumns(); -} -CIMGUI_API void igPushColumnClipRect(int column_index) { - return ImGui::PushColumnClipRect(column_index); -} -CIMGUI_API void igPushColumnsBackground() { - return ImGui::PushColumnsBackground(); -} -CIMGUI_API void igPopColumnsBackground() { - return ImGui::PopColumnsBackground(); -} -CIMGUI_API ImGuiID igGetColumnsID(const char* str_id, int count) { - return ImGui::GetColumnsID(str_id, count); -} -CIMGUI_API ImGuiOldColumns* igFindOrCreateColumns(ImGuiWindow* window, - ImGuiID id) { - return ImGui::FindOrCreateColumns(window, id); -} -CIMGUI_API float igGetColumnOffsetFromNorm(const ImGuiOldColumns* columns, - float offset_norm) { - return ImGui::GetColumnOffsetFromNorm(columns, offset_norm); -} -CIMGUI_API float igGetColumnNormFromOffset(const ImGuiOldColumns* columns, - float offset) { - return ImGui::GetColumnNormFromOffset(columns, offset); -} -CIMGUI_API void igTableOpenContextMenu(int column_n) { - return ImGui::TableOpenContextMenu(column_n); -} -CIMGUI_API void igTableSetColumnWidth(int column_n, float width) { - return ImGui::TableSetColumnWidth(column_n, width); -} -CIMGUI_API void igTableSetColumnSortDirection(int column_n, - ImGuiSortDirection sort_direction, - bool append_to_sort_specs) { - return ImGui::TableSetColumnSortDirection(column_n, sort_direction, - append_to_sort_specs); -} -CIMGUI_API int igTableGetHoveredColumn() { - return ImGui::TableGetHoveredColumn(); -} -CIMGUI_API int igTableGetHoveredRow() { - return ImGui::TableGetHoveredRow(); -} -CIMGUI_API float igTableGetHeaderRowHeight() { - return ImGui::TableGetHeaderRowHeight(); -} -CIMGUI_API float igTableGetHeaderAngledMaxLabelWidth() { - return ImGui::TableGetHeaderAngledMaxLabelWidth(); -} -CIMGUI_API void igTablePushBackgroundChannel() { - return ImGui::TablePushBackgroundChannel(); -} -CIMGUI_API void igTablePopBackgroundChannel() { - return ImGui::TablePopBackgroundChannel(); -} -CIMGUI_API void igTableAngledHeadersRowEx(ImGuiID row_id, - float angle, - float max_label_width, - const ImGuiTableHeaderData* data, - int data_count) { - return ImGui::TableAngledHeadersRowEx(row_id, angle, max_label_width, data, - data_count); -} -CIMGUI_API ImGuiTable* igGetCurrentTable() { - return ImGui::GetCurrentTable(); -} -CIMGUI_API ImGuiTable* igTableFindByID(ImGuiID id) { - return ImGui::TableFindByID(id); -} -CIMGUI_API bool igBeginTableEx(const char* name, - ImGuiID id, - int columns_count, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width) { - return ImGui::BeginTableEx(name, id, columns_count, flags, outer_size, - inner_width); -} -CIMGUI_API void igTableBeginInitMemory(ImGuiTable* table, int columns_count) { - return ImGui::TableBeginInitMemory(table, columns_count); -} -CIMGUI_API void igTableBeginApplyRequests(ImGuiTable* table) { - return ImGui::TableBeginApplyRequests(table); -} -CIMGUI_API void igTableSetupDrawChannels(ImGuiTable* table) { - return ImGui::TableSetupDrawChannels(table); -} -CIMGUI_API void igTableUpdateLayout(ImGuiTable* table) { - return ImGui::TableUpdateLayout(table); -} -CIMGUI_API void igTableUpdateBorders(ImGuiTable* table) { - return ImGui::TableUpdateBorders(table); -} -CIMGUI_API void igTableUpdateColumnsWeightFromWidth(ImGuiTable* table) { - return ImGui::TableUpdateColumnsWeightFromWidth(table); -} -CIMGUI_API void igTableDrawBorders(ImGuiTable* table) { - return ImGui::TableDrawBorders(table); -} -CIMGUI_API void igTableDrawDefaultContextMenu( - ImGuiTable* table, - ImGuiTableFlags flags_for_section_to_display) { - return ImGui::TableDrawDefaultContextMenu(table, - flags_for_section_to_display); -} -CIMGUI_API bool igTableBeginContextMenuPopup(ImGuiTable* table) { - return ImGui::TableBeginContextMenuPopup(table); -} -CIMGUI_API void igTableMergeDrawChannels(ImGuiTable* table) { - return ImGui::TableMergeDrawChannels(table); -} -CIMGUI_API ImGuiTableInstanceData* igTableGetInstanceData(ImGuiTable* table, - int instance_no) { - return ImGui::TableGetInstanceData(table, instance_no); -} -CIMGUI_API ImGuiID igTableGetInstanceID(ImGuiTable* table, int instance_no) { - return ImGui::TableGetInstanceID(table, instance_no); -} -CIMGUI_API void igTableSortSpecsSanitize(ImGuiTable* table) { - return ImGui::TableSortSpecsSanitize(table); -} -CIMGUI_API void igTableSortSpecsBuild(ImGuiTable* table) { - return ImGui::TableSortSpecsBuild(table); -} -CIMGUI_API ImGuiSortDirection -igTableGetColumnNextSortDirection(ImGuiTableColumn* column) { - return ImGui::TableGetColumnNextSortDirection(column); -} -CIMGUI_API void igTableFixColumnSortDirection(ImGuiTable* table, - ImGuiTableColumn* column) { - return ImGui::TableFixColumnSortDirection(table, column); -} -CIMGUI_API float igTableGetColumnWidthAuto(ImGuiTable* table, - ImGuiTableColumn* column) { - return ImGui::TableGetColumnWidthAuto(table, column); -} -CIMGUI_API void igTableBeginRow(ImGuiTable* table) { - return ImGui::TableBeginRow(table); -} -CIMGUI_API void igTableEndRow(ImGuiTable* table) { - return ImGui::TableEndRow(table); -} -CIMGUI_API void igTableBeginCell(ImGuiTable* table, int column_n) { - return ImGui::TableBeginCell(table, column_n); -} -CIMGUI_API void igTableEndCell(ImGuiTable* table) { - return ImGui::TableEndCell(table); -} -CIMGUI_API void igTableGetCellBgRect(ImRect* pOut, - const ImGuiTable* table, - int column_n) { - *pOut = ImGui::TableGetCellBgRect(table, column_n); -} -CIMGUI_API const char* igTableGetColumnName_TablePtr(const ImGuiTable* table, - int column_n) { - return ImGui::TableGetColumnName(table, column_n); -} -CIMGUI_API ImGuiID igTableGetColumnResizeID(ImGuiTable* table, - int column_n, - int instance_no) { - return ImGui::TableGetColumnResizeID(table, column_n, instance_no); -} -CIMGUI_API float igTableGetMaxColumnWidth(const ImGuiTable* table, - int column_n) { - return ImGui::TableGetMaxColumnWidth(table, column_n); -} -CIMGUI_API void igTableSetColumnWidthAutoSingle(ImGuiTable* table, - int column_n) { - return ImGui::TableSetColumnWidthAutoSingle(table, column_n); -} -CIMGUI_API void igTableSetColumnWidthAutoAll(ImGuiTable* table) { - return ImGui::TableSetColumnWidthAutoAll(table); -} -CIMGUI_API void igTableRemove(ImGuiTable* table) { - return ImGui::TableRemove(table); -} -CIMGUI_API void igTableGcCompactTransientBuffers_TablePtr(ImGuiTable* table) { - return ImGui::TableGcCompactTransientBuffers(table); -} -CIMGUI_API void igTableGcCompactTransientBuffers_TableTempDataPtr( - ImGuiTableTempData* table) { - return ImGui::TableGcCompactTransientBuffers(table); -} -CIMGUI_API void igTableGcCompactSettings() { - return ImGui::TableGcCompactSettings(); -} -CIMGUI_API void igTableLoadSettings(ImGuiTable* table) { - return ImGui::TableLoadSettings(table); -} -CIMGUI_API void igTableSaveSettings(ImGuiTable* table) { - return ImGui::TableSaveSettings(table); -} -CIMGUI_API void igTableResetSettings(ImGuiTable* table) { - return ImGui::TableResetSettings(table); -} -CIMGUI_API ImGuiTableSettings* igTableGetBoundSettings(ImGuiTable* table) { - return ImGui::TableGetBoundSettings(table); -} -CIMGUI_API void igTableSettingsAddSettingsHandler() { - return ImGui::TableSettingsAddSettingsHandler(); -} -CIMGUI_API ImGuiTableSettings* igTableSettingsCreate(ImGuiID id, - int columns_count) { - return ImGui::TableSettingsCreate(id, columns_count); -} -CIMGUI_API ImGuiTableSettings* igTableSettingsFindByID(ImGuiID id) { - return ImGui::TableSettingsFindByID(id); -} -CIMGUI_API ImGuiTabBar* igGetCurrentTabBar() { - return ImGui::GetCurrentTabBar(); -} -CIMGUI_API bool igBeginTabBarEx(ImGuiTabBar* tab_bar, - const ImRect bb, - ImGuiTabBarFlags flags) { - return ImGui::BeginTabBarEx(tab_bar, bb, flags); -} -CIMGUI_API ImGuiTabItem* igTabBarFindTabByID(ImGuiTabBar* tab_bar, - ImGuiID tab_id) { - return ImGui::TabBarFindTabByID(tab_bar, tab_id); -} -CIMGUI_API ImGuiTabItem* igTabBarFindTabByOrder(ImGuiTabBar* tab_bar, - int order) { - return ImGui::TabBarFindTabByOrder(tab_bar, order); -} -CIMGUI_API ImGuiTabItem* igTabBarFindMostRecentlySelectedTabForActiveWindow( - ImGuiTabBar* tab_bar) { - return ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(tab_bar); -} -CIMGUI_API ImGuiTabItem* igTabBarGetCurrentTab(ImGuiTabBar* tab_bar) { - return ImGui::TabBarGetCurrentTab(tab_bar); -} -CIMGUI_API int igTabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - return ImGui::TabBarGetTabOrder(tab_bar, tab); -} -CIMGUI_API const char* igTabBarGetTabName(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab) { - return ImGui::TabBarGetTabName(tab_bar, tab); -} -CIMGUI_API void igTabBarAddTab(ImGuiTabBar* tab_bar, - ImGuiTabItemFlags tab_flags, - ImGuiWindow* window) { - return ImGui::TabBarAddTab(tab_bar, tab_flags, window); -} -CIMGUI_API void igTabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) { - return ImGui::TabBarRemoveTab(tab_bar, tab_id); -} -CIMGUI_API void igTabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - return ImGui::TabBarCloseTab(tab_bar, tab); -} -CIMGUI_API void igTabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - return ImGui::TabBarQueueFocus(tab_bar, tab); -} -CIMGUI_API void igTabBarQueueReorder(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - int offset) { - return ImGui::TabBarQueueReorder(tab_bar, tab, offset); -} -CIMGUI_API void igTabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - ImVec2 mouse_pos) { - return ImGui::TabBarQueueReorderFromMousePos(tab_bar, tab, mouse_pos); -} -CIMGUI_API bool igTabBarProcessReorder(ImGuiTabBar* tab_bar) { - return ImGui::TabBarProcessReorder(tab_bar); -} -CIMGUI_API bool igTabItemEx(ImGuiTabBar* tab_bar, - const char* label, - bool* p_open, - ImGuiTabItemFlags flags, - ImGuiWindow* docked_window) { - return ImGui::TabItemEx(tab_bar, label, p_open, flags, docked_window); -} -CIMGUI_API void igTabItemCalcSize_Str(ImVec2* pOut, - const char* label, - bool has_close_button_or_unsaved_marker) { - *pOut = ImGui::TabItemCalcSize(label, has_close_button_or_unsaved_marker); -} -CIMGUI_API void igTabItemCalcSize_WindowPtr(ImVec2* pOut, ImGuiWindow* window) { - *pOut = ImGui::TabItemCalcSize(window); -} -CIMGUI_API void igTabItemBackground(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImU32 col) { - return ImGui::TabItemBackground(draw_list, bb, flags, col); -} -CIMGUI_API void igTabItemLabelAndCloseButton(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImVec2 frame_padding, - const char* label, - ImGuiID tab_id, - ImGuiID close_button_id, - bool is_contents_visible, - bool* out_just_closed, - bool* out_text_clipped) { - return ImGui::TabItemLabelAndCloseButton( - draw_list, bb, flags, frame_padding, label, tab_id, close_button_id, - is_contents_visible, out_just_closed, out_text_clipped); -} -CIMGUI_API void igRenderText(ImVec2 pos, - const char* text, - const char* text_end, - bool hide_text_after_hash) { - return ImGui::RenderText(pos, text, text_end, hide_text_after_hash); -} -CIMGUI_API void igRenderTextWrapped(ImVec2 pos, - const char* text, - const char* text_end, - float wrap_width) { - return ImGui::RenderTextWrapped(pos, text, text_end, wrap_width); -} -CIMGUI_API void igRenderTextClipped(const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect) { - return ImGui::RenderTextClipped(pos_min, pos_max, text, text_end, - text_size_if_known, align, clip_rect); -} -CIMGUI_API void igRenderTextClippedEx(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect) { - return ImGui::RenderTextClippedEx(draw_list, pos_min, pos_max, text, text_end, - text_size_if_known, align, clip_rect); -} -CIMGUI_API void igRenderTextEllipsis(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - float clip_max_x, - float ellipsis_max_x, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known) { - return ImGui::RenderTextEllipsis(draw_list, pos_min, pos_max, clip_max_x, - ellipsis_max_x, text, text_end, - text_size_if_known); -} -CIMGUI_API void igRenderFrame(ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - bool border, - float rounding) { - return ImGui::RenderFrame(p_min, p_max, fill_col, border, rounding); -} -CIMGUI_API void igRenderFrameBorder(ImVec2 p_min, - ImVec2 p_max, - float rounding) { - return ImGui::RenderFrameBorder(p_min, p_max, rounding); -} -CIMGUI_API void igRenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, - ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - float grid_step, - ImVec2 grid_off, - float rounding, - ImDrawFlags flags) { - return ImGui::RenderColorRectWithAlphaCheckerboard( - draw_list, p_min, p_max, fill_col, grid_step, grid_off, rounding, flags); -} -CIMGUI_API void igRenderNavHighlight(const ImRect bb, - ImGuiID id, - ImGuiNavHighlightFlags flags) { - return ImGui::RenderNavHighlight(bb, id, flags); -} -CIMGUI_API const char* igFindRenderedTextEnd(const char* text, - const char* text_end) { - return ImGui::FindRenderedTextEnd(text, text_end); -} -CIMGUI_API void igRenderMouseCursor(ImVec2 pos, - float scale, - ImGuiMouseCursor mouse_cursor, - ImU32 col_fill, - ImU32 col_border, - ImU32 col_shadow) { - return ImGui::RenderMouseCursor(pos, scale, mouse_cursor, col_fill, - col_border, col_shadow); -} -CIMGUI_API void igRenderArrow(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - ImGuiDir dir, - float scale) { - return ImGui::RenderArrow(draw_list, pos, col, dir, scale); -} -CIMGUI_API void igRenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) { - return ImGui::RenderBullet(draw_list, pos, col); -} -CIMGUI_API void igRenderCheckMark(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - float sz) { - return ImGui::RenderCheckMark(draw_list, pos, col, sz); -} -CIMGUI_API void igRenderArrowPointingAt(ImDrawList* draw_list, - ImVec2 pos, - ImVec2 half_sz, - ImGuiDir direction, - ImU32 col) { - return ImGui::RenderArrowPointingAt(draw_list, pos, half_sz, direction, col); -} -CIMGUI_API void igRenderArrowDockMenu(ImDrawList* draw_list, - ImVec2 p_min, - float sz, - ImU32 col) { - return ImGui::RenderArrowDockMenu(draw_list, p_min, sz, col); -} -CIMGUI_API void igRenderRectFilledRangeH(ImDrawList* draw_list, - const ImRect rect, - ImU32 col, - float x_start_norm, - float x_end_norm, - float rounding) { - return ImGui::RenderRectFilledRangeH(draw_list, rect, col, x_start_norm, - x_end_norm, rounding); -} -CIMGUI_API void igRenderRectFilledWithHole(ImDrawList* draw_list, - const ImRect outer, - const ImRect inner, - ImU32 col, - float rounding) { - return ImGui::RenderRectFilledWithHole(draw_list, outer, inner, col, - rounding); -} -CIMGUI_API ImDrawFlags igCalcRoundingFlagsForRectInRect(const ImRect r_in, - const ImRect r_outer, - float threshold) { - return ImGui::CalcRoundingFlagsForRectInRect(r_in, r_outer, threshold); -} -CIMGUI_API void igTextEx(const char* text, - const char* text_end, - ImGuiTextFlags flags) { - return ImGui::TextEx(text, text_end, flags); -} -CIMGUI_API bool igButtonEx(const char* label, - const ImVec2 size_arg, - ImGuiButtonFlags flags) { - return ImGui::ButtonEx(label, size_arg, flags); -} -CIMGUI_API bool igArrowButtonEx(const char* str_id, - ImGuiDir dir, - ImVec2 size_arg, - ImGuiButtonFlags flags) { - return ImGui::ArrowButtonEx(str_id, dir, size_arg, flags); -} -CIMGUI_API bool igImageButtonEx(ImGuiID id, - ImTextureID texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col, - ImGuiButtonFlags flags) { - return ImGui::ImageButtonEx(id, texture_id, image_size, uv0, uv1, bg_col, - tint_col, flags); -} -CIMGUI_API void igSeparatorEx(ImGuiSeparatorFlags flags, float thickness) { - return ImGui::SeparatorEx(flags, thickness); -} -CIMGUI_API void igSeparatorTextEx(ImGuiID id, - const char* label, - const char* label_end, - float extra_width) { - return ImGui::SeparatorTextEx(id, label, label_end, extra_width); -} -CIMGUI_API bool igCheckboxFlags_S64Ptr(const char* label, - ImS64* flags, - ImS64 flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igCheckboxFlags_U64Ptr(const char* label, - ImU64* flags, - ImU64 flags_value) { - return ImGui::CheckboxFlags(label, flags, flags_value); -} -CIMGUI_API bool igCloseButton(ImGuiID id, const ImVec2 pos) { - return ImGui::CloseButton(id, pos); -} -CIMGUI_API bool igCollapseButton(ImGuiID id, - const ImVec2 pos, - ImGuiDockNode* dock_node) { - return ImGui::CollapseButton(id, pos, dock_node); -} -CIMGUI_API void igScrollbar(ImGuiAxis axis) { - return ImGui::Scrollbar(axis); -} -CIMGUI_API bool igScrollbarEx(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - ImS64* p_scroll_v, - ImS64 avail_v, - ImS64 contents_v, - ImDrawFlags flags) { - return ImGui::ScrollbarEx(bb, id, axis, p_scroll_v, avail_v, contents_v, - flags); -} -CIMGUI_API void igGetWindowScrollbarRect(ImRect* pOut, - ImGuiWindow* window, - ImGuiAxis axis) { - *pOut = ImGui::GetWindowScrollbarRect(window, axis); -} -CIMGUI_API ImGuiID igGetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis) { - return ImGui::GetWindowScrollbarID(window, axis); -} -CIMGUI_API ImGuiID igGetWindowResizeCornerID(ImGuiWindow* window, int n) { - return ImGui::GetWindowResizeCornerID(window, n); -} -CIMGUI_API ImGuiID igGetWindowResizeBorderID(ImGuiWindow* window, - ImGuiDir dir) { - return ImGui::GetWindowResizeBorderID(window, dir); -} -CIMGUI_API bool igButtonBehavior(const ImRect bb, - ImGuiID id, - bool* out_hovered, - bool* out_held, - ImGuiButtonFlags flags) { - return ImGui::ButtonBehavior(bb, id, out_hovered, out_held, flags); -} -CIMGUI_API bool igDragBehavior(ImGuiID id, - ImGuiDataType data_type, - void* p_v, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags) { - return ImGui::DragBehavior(id, data_type, p_v, v_speed, p_min, p_max, format, - flags); -} -CIMGUI_API bool igSliderBehavior(const ImRect bb, - ImGuiID id, - ImGuiDataType data_type, - void* p_v, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags, - ImRect* out_grab_bb) { - return ImGui::SliderBehavior(bb, id, data_type, p_v, p_min, p_max, format, - flags, out_grab_bb); -} -CIMGUI_API bool igSplitterBehavior(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - float* size1, - float* size2, - float min_size1, - float min_size2, - float hover_extend, - float hover_visibility_delay, - ImU32 bg_col) { - return ImGui::SplitterBehavior(bb, id, axis, size1, size2, min_size1, - min_size2, hover_extend, - hover_visibility_delay, bg_col); -} -CIMGUI_API bool igTreeNodeBehavior(ImGuiID id, - ImGuiTreeNodeFlags flags, - const char* label, - const char* label_end) { - return ImGui::TreeNodeBehavior(id, flags, label, label_end); -} -CIMGUI_API void igTreePushOverrideID(ImGuiID id) { - return ImGui::TreePushOverrideID(id); -} -CIMGUI_API void igTreeNodeSetOpen(ImGuiID id, bool open) { - return ImGui::TreeNodeSetOpen(id, open); -} -CIMGUI_API bool igTreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags) { - return ImGui::TreeNodeUpdateNextOpen(id, flags); -} -CIMGUI_API void igSetNextItemSelectionUserData( - ImGuiSelectionUserData selection_user_data) { - return ImGui::SetNextItemSelectionUserData(selection_user_data); -} -CIMGUI_API const ImGuiDataTypeInfo* igDataTypeGetInfo(ImGuiDataType data_type) { - return ImGui::DataTypeGetInfo(data_type); -} -CIMGUI_API int igDataTypeFormatString(char* buf, - int buf_size, - ImGuiDataType data_type, - const void* p_data, - const char* format) { - return ImGui::DataTypeFormatString(buf, buf_size, data_type, p_data, format); -} -CIMGUI_API void igDataTypeApplyOp(ImGuiDataType data_type, - int op, - void* output, - const void* arg_1, - const void* arg_2) { - return ImGui::DataTypeApplyOp(data_type, op, output, arg_1, arg_2); -} -CIMGUI_API bool igDataTypeApplyFromText(const char* buf, - ImGuiDataType data_type, - void* p_data, - const char* format) { - return ImGui::DataTypeApplyFromText(buf, data_type, p_data, format); -} -CIMGUI_API int igDataTypeCompare(ImGuiDataType data_type, - const void* arg_1, - const void* arg_2) { - return ImGui::DataTypeCompare(data_type, arg_1, arg_2); -} -CIMGUI_API bool igDataTypeClamp(ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max) { - return ImGui::DataTypeClamp(data_type, p_data, p_min, p_max); -} -CIMGUI_API bool igInputTextEx(const char* label, - const char* hint, - char* buf, - int buf_size, - const ImVec2 size_arg, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data) { - return ImGui::InputTextEx(label, hint, buf, buf_size, size_arg, flags, - callback, user_data); -} -CIMGUI_API void igInputTextDeactivateHook(ImGuiID id) { - return ImGui::InputTextDeactivateHook(id); -} -CIMGUI_API bool igTempInputText(const ImRect bb, - ImGuiID id, - const char* label, - char* buf, - int buf_size, - ImGuiInputTextFlags flags) { - return ImGui::TempInputText(bb, id, label, buf, buf_size, flags); -} -CIMGUI_API bool igTempInputScalar(const ImRect bb, - ImGuiID id, - const char* label, - ImGuiDataType data_type, - void* p_data, - const char* format, - const void* p_clamp_min, - const void* p_clamp_max) { - return ImGui::TempInputScalar(bb, id, label, data_type, p_data, format, - p_clamp_min, p_clamp_max); -} -CIMGUI_API bool igTempInputIsActive(ImGuiID id) { - return ImGui::TempInputIsActive(id); -} -CIMGUI_API ImGuiInputTextState* igGetInputTextState(ImGuiID id) { - return ImGui::GetInputTextState(id); -} -CIMGUI_API void igColorTooltip(const char* text, - const float* col, - ImGuiColorEditFlags flags) { - return ImGui::ColorTooltip(text, col, flags); -} -CIMGUI_API void igColorEditOptionsPopup(const float* col, - ImGuiColorEditFlags flags) { - return ImGui::ColorEditOptionsPopup(col, flags); -} -CIMGUI_API void igColorPickerOptionsPopup(const float* ref_col, - ImGuiColorEditFlags flags) { - return ImGui::ColorPickerOptionsPopup(ref_col, flags); -} -CIMGUI_API int igPlotEx(ImGuiPlotType plot_type, - const char* label, - float (*values_getter)(void* data, int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - const ImVec2 size_arg) { - return ImGui::PlotEx(plot_type, label, values_getter, data, values_count, - values_offset, overlay_text, scale_min, scale_max, - size_arg); -} -CIMGUI_API void igShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - ImVec2 gradient_p0, - ImVec2 gradient_p1, - ImU32 col0, - ImU32 col1) { - return ImGui::ShadeVertsLinearColorGradientKeepAlpha( - draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col0, - col1); -} -CIMGUI_API void igShadeVertsLinearUV(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - bool clamp) { - return ImGui::ShadeVertsLinearUV(draw_list, vert_start_idx, vert_end_idx, a, - b, uv_a, uv_b, clamp); -} -CIMGUI_API void igShadeVertsTransformPos(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 pivot_in, - float cos_a, - float sin_a, - const ImVec2 pivot_out) { - return ImGui::ShadeVertsTransformPos(draw_list, vert_start_idx, vert_end_idx, - pivot_in, cos_a, sin_a, pivot_out); -} -CIMGUI_API void igGcCompactTransientMiscBuffers() { - return ImGui::GcCompactTransientMiscBuffers(); -} -CIMGUI_API void igGcCompactTransientWindowBuffers(ImGuiWindow* window) { - return ImGui::GcCompactTransientWindowBuffers(window); -} -CIMGUI_API void igGcAwakeTransientWindowBuffers(ImGuiWindow* window) { - return ImGui::GcAwakeTransientWindowBuffers(window); -} -CIMGUI_API void igDebugLog(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - ImGui::DebugLogV(fmt, args); - va_end(args); -} -CIMGUI_API void igDebugLogV(const char* fmt, va_list args) { - return ImGui::DebugLogV(fmt, args); -} -CIMGUI_API void igDebugAllocHook(ImGuiDebugAllocInfo* info, - int frame_count, - void* ptr, - size_t size) { - return ImGui::DebugAllocHook(info, frame_count, ptr, size); -} -CIMGUI_API void igErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, - void* user_data) { - return ImGui::ErrorCheckEndFrameRecover(log_callback, user_data); -} -CIMGUI_API void igErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, - void* user_data) { - return ImGui::ErrorCheckEndWindowRecover(log_callback, user_data); -} -CIMGUI_API void igErrorCheckUsingSetCursorPosToExtendParentBoundaries() { - return ImGui::ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); -} -CIMGUI_API void igDebugDrawCursorPos(ImU32 col) { - return ImGui::DebugDrawCursorPos(col); -} -CIMGUI_API void igDebugDrawLineExtents(ImU32 col) { - return ImGui::DebugDrawLineExtents(col); -} -CIMGUI_API void igDebugDrawItemRect(ImU32 col) { - return ImGui::DebugDrawItemRect(col); -} -CIMGUI_API void igDebugLocateItem(ImGuiID target_id) { - return ImGui::DebugLocateItem(target_id); -} -CIMGUI_API void igDebugLocateItemOnHover(ImGuiID target_id) { - return ImGui::DebugLocateItemOnHover(target_id); -} -CIMGUI_API void igDebugLocateItemResolveWithLastItem() { - return ImGui::DebugLocateItemResolveWithLastItem(); -} -CIMGUI_API void igDebugBreakClearData() { - return ImGui::DebugBreakClearData(); -} -CIMGUI_API bool igDebugBreakButton(const char* label, - const char* description_of_location) { - return ImGui::DebugBreakButton(label, description_of_location); -} -CIMGUI_API void igDebugBreakButtonTooltip(bool keyboard_only, - const char* description_of_location) { - return ImGui::DebugBreakButtonTooltip(keyboard_only, description_of_location); -} -CIMGUI_API void igShowFontAtlas(ImFontAtlas* atlas) { - return ImGui::ShowFontAtlas(atlas); -} -CIMGUI_API void igDebugHookIdInfo(ImGuiID id, - ImGuiDataType data_type, - const void* data_id, - const void* data_id_end) { - return ImGui::DebugHookIdInfo(id, data_type, data_id, data_id_end); -} -CIMGUI_API void igDebugNodeColumns(ImGuiOldColumns* columns) { - return ImGui::DebugNodeColumns(columns); -} -CIMGUI_API void igDebugNodeDockNode(ImGuiDockNode* node, const char* label) { - return ImGui::DebugNodeDockNode(node, label); -} -CIMGUI_API void igDebugNodeDrawList(ImGuiWindow* window, - ImGuiViewportP* viewport, - const ImDrawList* draw_list, - const char* label) { - return ImGui::DebugNodeDrawList(window, viewport, draw_list, label); -} -CIMGUI_API void igDebugNodeDrawCmdShowMeshAndBoundingBox( - ImDrawList* out_draw_list, - const ImDrawList* draw_list, - const ImDrawCmd* draw_cmd, - bool show_mesh, - bool show_aabb) { - return ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox( - out_draw_list, draw_list, draw_cmd, show_mesh, show_aabb); -} -CIMGUI_API void igDebugNodeFont(ImFont* font) { - return ImGui::DebugNodeFont(font); -} -CIMGUI_API void igDebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph) { - return ImGui::DebugNodeFontGlyph(font, glyph); -} -CIMGUI_API void igDebugNodeStorage(ImGuiStorage* storage, const char* label) { - return ImGui::DebugNodeStorage(storage, label); -} -CIMGUI_API void igDebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) { - return ImGui::DebugNodeTabBar(tab_bar, label); -} -CIMGUI_API void igDebugNodeTable(ImGuiTable* table) { - return ImGui::DebugNodeTable(table); -} -CIMGUI_API void igDebugNodeTableSettings(ImGuiTableSettings* settings) { - return ImGui::DebugNodeTableSettings(settings); -} -CIMGUI_API void igDebugNodeInputTextState(ImGuiInputTextState* state) { - return ImGui::DebugNodeInputTextState(state); -} -CIMGUI_API void igDebugNodeTypingSelectState(ImGuiTypingSelectState* state) { - return ImGui::DebugNodeTypingSelectState(state); -} -CIMGUI_API void igDebugNodeWindow(ImGuiWindow* window, const char* label) { - return ImGui::DebugNodeWindow(window, label); -} -CIMGUI_API void igDebugNodeWindowSettings(ImGuiWindowSettings* settings) { - return ImGui::DebugNodeWindowSettings(settings); -} -CIMGUI_API void igDebugNodeWindowsList(ImVector_ImGuiWindowPtr* windows, - const char* label) { - return ImGui::DebugNodeWindowsList(windows, label); -} -CIMGUI_API void igDebugNodeWindowsListByBeginStackParent( - ImGuiWindow** windows, - int windows_size, - ImGuiWindow* parent_in_begin_stack) { - return ImGui::DebugNodeWindowsListByBeginStackParent(windows, windows_size, - parent_in_begin_stack); -} -CIMGUI_API void igDebugNodeViewport(ImGuiViewportP* viewport) { - return ImGui::DebugNodeViewport(viewport); -} -CIMGUI_API void igDebugRenderKeyboardPreview(ImDrawList* draw_list) { - return ImGui::DebugRenderKeyboardPreview(draw_list); -} -CIMGUI_API void igDebugRenderViewportThumbnail(ImDrawList* draw_list, - ImGuiViewportP* viewport, - const ImRect bb) { - return ImGui::DebugRenderViewportThumbnail(draw_list, viewport, bb); -} - -CIMGUI_API void igImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas) { - return ImFontAtlasUpdateConfigDataPointers(atlas); -} -CIMGUI_API void igImFontAtlasBuildInit(ImFontAtlas* atlas) { - return ImFontAtlasBuildInit(atlas); -} -CIMGUI_API void igImFontAtlasBuildSetupFont(ImFontAtlas* atlas, - ImFont* font, - ImFontConfig* font_config, - float ascent, - float descent) { - return ImFontAtlasBuildSetupFont(atlas, font, font_config, ascent, descent); -} -CIMGUI_API void igImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, - void* stbrp_context_opaque) { - return ImFontAtlasBuildPackCustomRects(atlas, stbrp_context_opaque); -} -CIMGUI_API void igImFontAtlasBuildFinish(ImFontAtlas* atlas) { - return ImFontAtlasBuildFinish(atlas); -} -CIMGUI_API void igImFontAtlasBuildRender8bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned char in_marker_pixel_value) { - return ImFontAtlasBuildRender8bppRectFromString( - atlas, x, y, w, h, in_str, in_marker_char, in_marker_pixel_value); -} -CIMGUI_API void igImFontAtlasBuildRender32bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned int in_marker_pixel_value) { - return ImFontAtlasBuildRender32bppRectFromString( - atlas, x, y, w, h, in_str, in_marker_char, in_marker_pixel_value); -} -CIMGUI_API void igImFontAtlasBuildMultiplyCalcLookupTable( - unsigned char out_table[256], - float in_multiply_factor) { - return ImFontAtlasBuildMultiplyCalcLookupTable(out_table, in_multiply_factor); -} -CIMGUI_API void igImFontAtlasBuildMultiplyRectAlpha8( - const unsigned char table[256], - unsigned char* pixels, - int x, - int y, - int w, - int h, - int stride) { - return ImFontAtlasBuildMultiplyRectAlpha8(table, pixels, x, y, w, h, stride); -} - -/////////////////////////////manual written functions -CIMGUI_API void igLogText(CONST char* fmt, ...) { - char buffer[256]; - va_list args; - va_start(args, fmt); - vsnprintf(buffer, 256, fmt, args); - va_end(args); - - ImGui::LogText("%s", buffer); -} -CIMGUI_API void ImGuiTextBuffer_appendf(struct ImGuiTextBuffer* buffer, - const char* fmt, - ...) { - va_list args; - va_start(args, fmt); - buffer->appendfv(fmt, args); - va_end(args); -} - -CIMGUI_API float igGET_FLT_MAX() { - return FLT_MAX; -} - -CIMGUI_API float igGET_FLT_MIN() { - return FLT_MIN; -} - -CIMGUI_API ImVector_ImWchar* ImVector_ImWchar_create() { - return IM_NEW(ImVector)(); -} - -CIMGUI_API void ImVector_ImWchar_destroy(ImVector_ImWchar* self) { - IM_DELETE(self); -} - -CIMGUI_API void ImVector_ImWchar_Init(ImVector_ImWchar* p) { - IM_PLACEMENT_NEW(p) ImVector(); -} -CIMGUI_API void ImVector_ImWchar_UnInit(ImVector_ImWchar* p) { - p->~ImVector(); -} - -#ifdef IMGUI_HAS_DOCK - -// NOTE: Some function pointers in the ImGuiPlatformIO structure are not -// C-compatible because of their use of a complex return type. To work around -// this, we store a custom CimguiStorage object inside -// ImGuiIO::BackendLanguageUserData, which contains C-compatible function -// pointer variants for these functions. When a user function pointer is -// provided, we hook up the underlying ImGuiPlatformIO function pointer to a -// thunk which accesses the user function pointer through CimguiStorage. - -struct CimguiStorage { - void (*Platform_GetWindowPos)(ImGuiViewport* vp, ImVec2* out_pos); - void (*Platform_GetWindowSize)(ImGuiViewport* vp, ImVec2* out_pos); -}; - -// Gets a reference to the CimguiStorage object stored in the current ImGui -// context's BackendLanguageUserData. -CimguiStorage& GetCimguiStorage() { - ImGuiIO& io = ImGui::GetIO(); - if (io.BackendLanguageUserData == NULL) { - io.BackendLanguageUserData = new CimguiStorage(); - } - - return *(CimguiStorage*)io.BackendLanguageUserData; -} - -// Thunk satisfying the signature of ImGuiPlatformIO::Platform_GetWindowPos. -ImVec2 Platform_GetWindowPos_hook(ImGuiViewport* vp) { - ImVec2 pos; - GetCimguiStorage().Platform_GetWindowPos(vp, &pos); - return pos; -}; - -// Fully C-compatible function pointer setter for -// ImGuiPlatformIO::Platform_GetWindowPos. -CIMGUI_API void ImGuiPlatformIO_Set_Platform_GetWindowPos( - ImGuiPlatformIO* platform_io, - void (*user_callback)(ImGuiViewport* vp, ImVec2* out_pos)) { - CimguiStorage& storage = GetCimguiStorage(); - storage.Platform_GetWindowPos = user_callback; - platform_io->Platform_GetWindowPos = &Platform_GetWindowPos_hook; -} - -// Thunk satisfying the signature of ImGuiPlatformIO::Platform_GetWindowSize. -ImVec2 Platform_GetWindowSize_hook(ImGuiViewport* vp) { - ImVec2 size; - GetCimguiStorage().Platform_GetWindowSize(vp, &size); - return size; -}; - -// Fully C-compatible function pointer setter for -// ImGuiPlatformIO::Platform_GetWindowSize. -CIMGUI_API void ImGuiPlatformIO_Set_Platform_GetWindowSize( - ImGuiPlatformIO* platform_io, - void (*user_callback)(ImGuiViewport* vp, ImVec2* out_size)) { - CimguiStorage& storage = GetCimguiStorage(); - storage.Platform_GetWindowSize = user_callback; - platform_io->Platform_GetWindowSize = &Platform_GetWindowSize_hook; -} - -#endif diff --git a/pkg/cimgui/vendor/cimgui.h b/pkg/cimgui/vendor/cimgui.h deleted file mode 100644 index f00b4d9b7..000000000 --- a/pkg/cimgui/vendor/cimgui.h +++ /dev/null @@ -1,6554 +0,0 @@ -// This file is automatically generated by generator.lua from -// https://github.com/cimgui/cimgui based on imgui.h file version "1.90.6" 19060 -// from Dear ImGui https://github.com/ocornut/imgui with imgui_internal.h api -// docking branch -#ifndef CIMGUI_INCLUDED -#define CIMGUI_INCLUDED -#include -#include -#if defined _WIN32 || defined __CYGWIN__ -#ifdef CIMGUI_NO_EXPORT -#define API -#else -#define API __declspec(dllexport) -#endif -#else -#ifdef __GNUC__ -#define API __attribute__((__visibility__("default"))) -#else -#define API -#endif -#endif - -#if defined __cplusplus -#define EXTERN extern "C" -#else -#include -#include -#define EXTERN extern -#endif - -#define CIMGUI_API EXTERN API -#define CONST const - -#ifdef _MSC_VER -typedef unsigned __int64 ImU64; -#else -// typedef unsigned long long ImU64; -#endif - -#ifdef CIMGUI_DEFINE_ENUMS_AND_STRUCTS - -typedef struct ImDrawChannel ImDrawChannel; -typedef struct ImDrawCmd ImDrawCmd; -typedef struct ImDrawData ImDrawData; -typedef struct ImDrawList ImDrawList; -typedef struct ImDrawListSharedData ImDrawListSharedData; -typedef struct ImDrawListSplitter ImDrawListSplitter; -typedef struct ImDrawVert ImDrawVert; -typedef struct ImFont ImFont; -typedef struct ImFontAtlas ImFontAtlas; -typedef struct ImFontBuilderIO ImFontBuilderIO; -typedef struct ImFontConfig ImFontConfig; -typedef struct ImFontGlyph ImFontGlyph; -typedef struct ImFontGlyphRangesBuilder ImFontGlyphRangesBuilder; -typedef struct ImColor ImColor; -typedef struct ImGuiContext ImGuiContext; -typedef struct ImGuiIO ImGuiIO; -typedef struct ImGuiInputTextCallbackData ImGuiInputTextCallbackData; -typedef struct ImGuiKeyData ImGuiKeyData; -typedef struct ImGuiListClipper ImGuiListClipper; -typedef struct ImGuiOnceUponAFrame ImGuiOnceUponAFrame; -typedef struct ImGuiPayload ImGuiPayload; -typedef struct ImGuiPlatformIO ImGuiPlatformIO; -typedef struct ImGuiPlatformMonitor ImGuiPlatformMonitor; -typedef struct ImGuiPlatformImeData ImGuiPlatformImeData; -typedef struct ImGuiSizeCallbackData ImGuiSizeCallbackData; -typedef struct ImGuiStorage ImGuiStorage; -typedef struct ImGuiStyle ImGuiStyle; -typedef struct ImGuiTableSortSpecs ImGuiTableSortSpecs; -typedef struct ImGuiTableColumnSortSpecs ImGuiTableColumnSortSpecs; -typedef struct ImGuiTextBuffer ImGuiTextBuffer; -typedef struct ImGuiTextFilter ImGuiTextFilter; -typedef struct ImGuiViewport ImGuiViewport; -typedef struct ImGuiWindowClass ImGuiWindowClass; -typedef struct ImBitVector ImBitVector; -typedef struct ImRect ImRect; -typedef struct ImDrawDataBuilder ImDrawDataBuilder; -typedef struct ImGuiColorMod ImGuiColorMod; -typedef struct ImGuiContextHook ImGuiContextHook; -typedef struct ImGuiDataVarInfo ImGuiDataVarInfo; -typedef struct ImGuiDataTypeInfo ImGuiDataTypeInfo; -typedef struct ImGuiDockContext ImGuiDockContext; -typedef struct ImGuiDockRequest ImGuiDockRequest; -typedef struct ImGuiDockNode ImGuiDockNode; -typedef struct ImGuiDockNodeSettings ImGuiDockNodeSettings; -typedef struct ImGuiGroupData ImGuiGroupData; -typedef struct ImGuiInputTextState ImGuiInputTextState; -typedef struct ImGuiInputTextDeactivateData ImGuiInputTextDeactivateData; -typedef struct ImGuiLastItemData ImGuiLastItemData; -typedef struct ImGuiLocEntry ImGuiLocEntry; -typedef struct ImGuiMenuColumns ImGuiMenuColumns; -typedef struct ImGuiNavItemData ImGuiNavItemData; -typedef struct ImGuiNavTreeNodeData ImGuiNavTreeNodeData; -typedef struct ImGuiMetricsConfig ImGuiMetricsConfig; -typedef struct ImGuiNextWindowData ImGuiNextWindowData; -typedef struct ImGuiNextItemData ImGuiNextItemData; -typedef struct ImGuiOldColumnData ImGuiOldColumnData; -typedef struct ImGuiOldColumns ImGuiOldColumns; -typedef struct ImGuiPopupData ImGuiPopupData; -typedef struct ImGuiSettingsHandler ImGuiSettingsHandler; -typedef struct ImGuiStackSizes ImGuiStackSizes; -typedef struct ImGuiStyleMod ImGuiStyleMod; -typedef struct ImGuiTabBar ImGuiTabBar; -typedef struct ImGuiTabItem ImGuiTabItem; -typedef struct ImGuiTable ImGuiTable; -typedef struct ImGuiTableHeaderData ImGuiTableHeaderData; -typedef struct ImGuiTableColumn ImGuiTableColumn; -typedef struct ImGuiTableInstanceData ImGuiTableInstanceData; -typedef struct ImGuiTableTempData ImGuiTableTempData; -typedef struct ImGuiTableSettings ImGuiTableSettings; -typedef struct ImGuiTableColumnsSettings ImGuiTableColumnsSettings; -typedef struct ImGuiTypingSelectState ImGuiTypingSelectState; -typedef struct ImGuiTypingSelectRequest ImGuiTypingSelectRequest; -typedef struct ImGuiWindow ImGuiWindow; -typedef struct ImGuiWindowDockStyle ImGuiWindowDockStyle; -typedef struct ImGuiWindowTempData ImGuiWindowTempData; -typedef struct ImGuiWindowSettings ImGuiWindowSettings; -typedef struct ImVector_const_charPtr { - int Size; - int Capacity; - const char** Data; -} ImVector_const_charPtr; - -struct ImDrawChannel; -struct ImDrawCmd; -struct ImDrawData; -struct ImDrawList; -struct ImDrawListSharedData; -struct ImDrawListSplitter; -struct ImDrawVert; -struct ImFont; -struct ImFontAtlas; -struct ImFontBuilderIO; -struct ImFontConfig; -struct ImFontGlyph; -struct ImFontGlyphRangesBuilder; -struct ImColor; -struct ImGuiContext; -struct ImGuiIO; -struct ImGuiInputTextCallbackData; -struct ImGuiKeyData; -struct ImGuiListClipper; -struct ImGuiOnceUponAFrame; -struct ImGuiPayload; -struct ImGuiPlatformIO; -struct ImGuiPlatformMonitor; -struct ImGuiPlatformImeData; -struct ImGuiSizeCallbackData; -struct ImGuiStorage; -struct ImGuiStyle; -struct ImGuiTableSortSpecs; -struct ImGuiTableColumnSortSpecs; -struct ImGuiTextBuffer; -struct ImGuiTextFilter; -struct ImGuiViewport; -struct ImGuiWindowClass; -typedef int ImGuiCol; -typedef int ImGuiCond; -typedef int ImGuiDataType; -typedef int ImGuiDir; -typedef int ImGuiMouseButton; -typedef int ImGuiMouseCursor; -typedef int ImGuiSortDirection; -typedef int ImGuiStyleVar; -typedef int ImGuiTableBgTarget; -typedef int ImDrawFlags; -typedef int ImDrawListFlags; -typedef int ImFontAtlasFlags; -typedef int ImGuiBackendFlags; -typedef int ImGuiButtonFlags; -typedef int ImGuiChildFlags; -typedef int ImGuiColorEditFlags; -typedef int ImGuiConfigFlags; -typedef int ImGuiComboFlags; -typedef int ImGuiDockNodeFlags; -typedef int ImGuiDragDropFlags; -typedef int ImGuiFocusedFlags; -typedef int ImGuiHoveredFlags; -typedef int ImGuiInputTextFlags; -typedef int ImGuiKeyChord; -typedef int ImGuiPopupFlags; -typedef int ImGuiSelectableFlags; -typedef int ImGuiSliderFlags; -typedef int ImGuiTabBarFlags; -typedef int ImGuiTabItemFlags; -typedef int ImGuiTableFlags; -typedef int ImGuiTableColumnFlags; -typedef int ImGuiTableRowFlags; -typedef int ImGuiTreeNodeFlags; -typedef int ImGuiViewportFlags; -typedef int ImGuiWindowFlags; -typedef void* ImTextureID; -typedef unsigned short ImDrawIdx; -typedef unsigned int ImGuiID; -typedef signed char ImS8; -typedef unsigned char ImU8; -typedef signed short ImS16; -typedef unsigned short ImU16; -typedef signed int ImS32; -typedef unsigned int ImU32; -typedef signed long long ImS64; -typedef unsigned long long ImU64; -typedef unsigned int ImWchar32; -typedef unsigned short ImWchar16; -typedef ImWchar16 ImWchar; -typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); -typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); -typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); -typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); -typedef struct ImVec2 ImVec2; -struct ImVec2 { - float x, y; -}; -typedef struct ImVec4 ImVec4; -struct ImVec4 { - float x, y, z, w; -}; -typedef enum { - ImGuiWindowFlags_None = 0, - ImGuiWindowFlags_NoTitleBar = 1 << 0, - ImGuiWindowFlags_NoResize = 1 << 1, - ImGuiWindowFlags_NoMove = 1 << 2, - ImGuiWindowFlags_NoScrollbar = 1 << 3, - ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, - ImGuiWindowFlags_NoCollapse = 1 << 5, - ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, - ImGuiWindowFlags_NoBackground = 1 << 7, - ImGuiWindowFlags_NoSavedSettings = 1 << 8, - ImGuiWindowFlags_NoMouseInputs = 1 << 9, - ImGuiWindowFlags_MenuBar = 1 << 10, - ImGuiWindowFlags_HorizontalScrollbar = 1 << 11, - ImGuiWindowFlags_NoFocusOnAppearing = 1 << 12, - ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, - ImGuiWindowFlags_AlwaysVerticalScrollbar = 1 << 14, - ImGuiWindowFlags_AlwaysHorizontalScrollbar = 1 << 15, - ImGuiWindowFlags_NoNavInputs = 1 << 16, - ImGuiWindowFlags_NoNavFocus = 1 << 17, - ImGuiWindowFlags_UnsavedDocument = 1 << 18, - ImGuiWindowFlags_NoDocking = 1 << 19, - ImGuiWindowFlags_NoNav = - ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, - ImGuiWindowFlags_NoDecoration = - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, - ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | - ImGuiWindowFlags_NoNavInputs | - ImGuiWindowFlags_NoNavFocus, - ImGuiWindowFlags_NavFlattened = 1 << 23, - ImGuiWindowFlags_ChildWindow = 1 << 24, - ImGuiWindowFlags_Tooltip = 1 << 25, - ImGuiWindowFlags_Popup = 1 << 26, - ImGuiWindowFlags_Modal = 1 << 27, - ImGuiWindowFlags_ChildMenu = 1 << 28, - ImGuiWindowFlags_DockNodeHost = 1 << 29, -} ImGuiWindowFlags_; -typedef enum { - ImGuiChildFlags_None = 0, - ImGuiChildFlags_Border = 1 << 0, - ImGuiChildFlags_AlwaysUseWindowPadding = 1 << 1, - ImGuiChildFlags_ResizeX = 1 << 2, - ImGuiChildFlags_ResizeY = 1 << 3, - ImGuiChildFlags_AutoResizeX = 1 << 4, - ImGuiChildFlags_AutoResizeY = 1 << 5, - ImGuiChildFlags_AlwaysAutoResize = 1 << 6, - ImGuiChildFlags_FrameStyle = 1 << 7, -} ImGuiChildFlags_; -typedef enum { - ImGuiInputTextFlags_None = 0, - ImGuiInputTextFlags_CharsDecimal = 1 << 0, - ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, - ImGuiInputTextFlags_CharsUppercase = 1 << 2, - ImGuiInputTextFlags_CharsNoBlank = 1 << 3, - ImGuiInputTextFlags_AutoSelectAll = 1 << 4, - ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, - ImGuiInputTextFlags_CallbackCompletion = 1 << 6, - ImGuiInputTextFlags_CallbackHistory = 1 << 7, - ImGuiInputTextFlags_CallbackAlways = 1 << 8, - ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, - ImGuiInputTextFlags_AllowTabInput = 1 << 10, - ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, - ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, - ImGuiInputTextFlags_AlwaysOverwrite = 1 << 13, - ImGuiInputTextFlags_ReadOnly = 1 << 14, - ImGuiInputTextFlags_Password = 1 << 15, - ImGuiInputTextFlags_NoUndoRedo = 1 << 16, - ImGuiInputTextFlags_CharsScientific = 1 << 17, - ImGuiInputTextFlags_CallbackResize = 1 << 18, - ImGuiInputTextFlags_CallbackEdit = 1 << 19, - ImGuiInputTextFlags_EscapeClearsAll = 1 << 20, -} ImGuiInputTextFlags_; -typedef enum { - ImGuiTreeNodeFlags_None = 0, - ImGuiTreeNodeFlags_Selected = 1 << 0, - ImGuiTreeNodeFlags_Framed = 1 << 1, - ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, - ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, - ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, - ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, - ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, - ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, - ImGuiTreeNodeFlags_Leaf = 1 << 8, - ImGuiTreeNodeFlags_Bullet = 1 << 9, - ImGuiTreeNodeFlags_FramePadding = 1 << 10, - ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, - ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, - ImGuiTreeNodeFlags_SpanTextWidth = 1 << 13, - ImGuiTreeNodeFlags_SpanAllColumns = 1 << 14, - ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 15, - ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | - ImGuiTreeNodeFlags_NoTreePushOnOpen | - ImGuiTreeNodeFlags_NoAutoOpenOnLog, -} ImGuiTreeNodeFlags_; -typedef enum { - ImGuiPopupFlags_None = 0, - ImGuiPopupFlags_MouseButtonLeft = 0, - ImGuiPopupFlags_MouseButtonRight = 1, - ImGuiPopupFlags_MouseButtonMiddle = 2, - ImGuiPopupFlags_MouseButtonMask_ = 0x1F, - ImGuiPopupFlags_MouseButtonDefault_ = 1, - ImGuiPopupFlags_NoReopen = 1 << 5, - ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 7, - ImGuiPopupFlags_NoOpenOverItems = 1 << 8, - ImGuiPopupFlags_AnyPopupId = 1 << 10, - ImGuiPopupFlags_AnyPopupLevel = 1 << 11, - ImGuiPopupFlags_AnyPopup = - ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, -} ImGuiPopupFlags_; -typedef enum { - ImGuiSelectableFlags_None = 0, - ImGuiSelectableFlags_DontClosePopups = 1 << 0, - ImGuiSelectableFlags_SpanAllColumns = 1 << 1, - ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, - ImGuiSelectableFlags_Disabled = 1 << 3, - ImGuiSelectableFlags_AllowOverlap = 1 << 4, -} ImGuiSelectableFlags_; -typedef enum { - ImGuiComboFlags_None = 0, - ImGuiComboFlags_PopupAlignLeft = 1 << 0, - ImGuiComboFlags_HeightSmall = 1 << 1, - ImGuiComboFlags_HeightRegular = 1 << 2, - ImGuiComboFlags_HeightLarge = 1 << 3, - ImGuiComboFlags_HeightLargest = 1 << 4, - ImGuiComboFlags_NoArrowButton = 1 << 5, - ImGuiComboFlags_NoPreview = 1 << 6, - ImGuiComboFlags_WidthFitPreview = 1 << 7, - ImGuiComboFlags_HeightMask_ = - ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | - ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, -} ImGuiComboFlags_; -typedef enum { - ImGuiTabBarFlags_None = 0, - ImGuiTabBarFlags_Reorderable = 1 << 0, - ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, - ImGuiTabBarFlags_TabListPopupButton = 1 << 2, - ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, - ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, - ImGuiTabBarFlags_NoTooltip = 1 << 5, - ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 6, - ImGuiTabBarFlags_FittingPolicyScroll = 1 << 7, - ImGuiTabBarFlags_FittingPolicyMask_ = - ImGuiTabBarFlags_FittingPolicyResizeDown | - ImGuiTabBarFlags_FittingPolicyScroll, - ImGuiTabBarFlags_FittingPolicyDefault_ = - ImGuiTabBarFlags_FittingPolicyResizeDown, -} ImGuiTabBarFlags_; -typedef enum { - ImGuiTabItemFlags_None = 0, - ImGuiTabItemFlags_UnsavedDocument = 1 << 0, - ImGuiTabItemFlags_SetSelected = 1 << 1, - ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, - ImGuiTabItemFlags_NoPushId = 1 << 3, - ImGuiTabItemFlags_NoTooltip = 1 << 4, - ImGuiTabItemFlags_NoReorder = 1 << 5, - ImGuiTabItemFlags_Leading = 1 << 6, - ImGuiTabItemFlags_Trailing = 1 << 7, - ImGuiTabItemFlags_NoAssumedClosure = 1 << 8, -} ImGuiTabItemFlags_; -typedef enum { - ImGuiFocusedFlags_None = 0, - ImGuiFocusedFlags_ChildWindows = 1 << 0, - ImGuiFocusedFlags_RootWindow = 1 << 1, - ImGuiFocusedFlags_AnyWindow = 1 << 2, - ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, - ImGuiFocusedFlags_DockHierarchy = 1 << 4, - ImGuiFocusedFlags_RootAndChildWindows = - ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, -} ImGuiFocusedFlags_; -typedef enum { - ImGuiHoveredFlags_None = 0, - ImGuiHoveredFlags_ChildWindows = 1 << 0, - ImGuiHoveredFlags_RootWindow = 1 << 1, - ImGuiHoveredFlags_AnyWindow = 1 << 2, - ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, - ImGuiHoveredFlags_DockHierarchy = 1 << 4, - ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, - ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, - ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, - ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, - ImGuiHoveredFlags_NoNavOverride = 1 << 11, - ImGuiHoveredFlags_AllowWhenOverlapped = - ImGuiHoveredFlags_AllowWhenOverlappedByItem | - ImGuiHoveredFlags_AllowWhenOverlappedByWindow, - ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | - ImGuiHoveredFlags_AllowWhenOverlapped, - ImGuiHoveredFlags_RootAndChildWindows = - ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, - ImGuiHoveredFlags_ForTooltip = 1 << 12, - ImGuiHoveredFlags_Stationary = 1 << 13, - ImGuiHoveredFlags_DelayNone = 1 << 14, - ImGuiHoveredFlags_DelayShort = 1 << 15, - ImGuiHoveredFlags_DelayNormal = 1 << 16, - ImGuiHoveredFlags_NoSharedDelay = 1 << 17, -} ImGuiHoveredFlags_; -typedef enum { - ImGuiDockNodeFlags_None = 0, - ImGuiDockNodeFlags_KeepAliveOnly = 1 << 0, - ImGuiDockNodeFlags_NoDockingOverCentralNode = 1 << 2, - ImGuiDockNodeFlags_PassthruCentralNode = 1 << 3, - ImGuiDockNodeFlags_NoDockingSplit = 1 << 4, - ImGuiDockNodeFlags_NoResize = 1 << 5, - ImGuiDockNodeFlags_AutoHideTabBar = 1 << 6, - ImGuiDockNodeFlags_NoUndocking = 1 << 7, -} ImGuiDockNodeFlags_; -typedef enum { - ImGuiDragDropFlags_None = 0, - ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, - ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, - ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, - ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, - ImGuiDragDropFlags_SourceExtern = 1 << 4, - ImGuiDragDropFlags_SourceAutoExpirePayload = 1 << 5, - ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, - ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, - ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, - ImGuiDragDropFlags_AcceptPeekOnly = - ImGuiDragDropFlags_AcceptBeforeDelivery | - ImGuiDragDropFlags_AcceptNoDrawDefaultRect, -} ImGuiDragDropFlags_; -typedef enum { - ImGuiDataType_S8, - ImGuiDataType_U8, - ImGuiDataType_S16, - ImGuiDataType_U16, - ImGuiDataType_S32, - ImGuiDataType_U32, - ImGuiDataType_S64, - ImGuiDataType_U64, - ImGuiDataType_Float, - ImGuiDataType_Double, - ImGuiDataType_COUNT -} ImGuiDataType_; -typedef enum { - ImGuiDir_None = -1, - ImGuiDir_Left = 0, - ImGuiDir_Right = 1, - ImGuiDir_Up = 2, - ImGuiDir_Down = 3, - ImGuiDir_COUNT -} ImGuiDir_; -typedef enum { - ImGuiSortDirection_None = 0, - ImGuiSortDirection_Ascending = 1, - ImGuiSortDirection_Descending = 2 -} ImGuiSortDirection_; -typedef enum { - ImGuiKey_None = 0, - ImGuiKey_Tab = 512, - ImGuiKey_LeftArrow = 513, - ImGuiKey_RightArrow = 514, - ImGuiKey_UpArrow = 515, - ImGuiKey_DownArrow = 516, - ImGuiKey_PageUp = 517, - ImGuiKey_PageDown = 518, - ImGuiKey_Home = 519, - ImGuiKey_End = 520, - ImGuiKey_Insert = 521, - ImGuiKey_Delete = 522, - ImGuiKey_Backspace = 523, - ImGuiKey_Space = 524, - ImGuiKey_Enter = 525, - ImGuiKey_Escape = 526, - ImGuiKey_LeftCtrl = 527, - ImGuiKey_LeftShift = 528, - ImGuiKey_LeftAlt = 529, - ImGuiKey_LeftSuper = 530, - ImGuiKey_RightCtrl = 531, - ImGuiKey_RightShift = 532, - ImGuiKey_RightAlt = 533, - ImGuiKey_RightSuper = 534, - ImGuiKey_Menu = 535, - ImGuiKey_0 = 536, - ImGuiKey_1 = 537, - ImGuiKey_2 = 538, - ImGuiKey_3 = 539, - ImGuiKey_4 = 540, - ImGuiKey_5 = 541, - ImGuiKey_6 = 542, - ImGuiKey_7 = 543, - ImGuiKey_8 = 544, - ImGuiKey_9 = 545, - ImGuiKey_A = 546, - ImGuiKey_B = 547, - ImGuiKey_C = 548, - ImGuiKey_D = 549, - ImGuiKey_E = 550, - ImGuiKey_F = 551, - ImGuiKey_G = 552, - ImGuiKey_H = 553, - ImGuiKey_I = 554, - ImGuiKey_J = 555, - ImGuiKey_K = 556, - ImGuiKey_L = 557, - ImGuiKey_M = 558, - ImGuiKey_N = 559, - ImGuiKey_O = 560, - ImGuiKey_P = 561, - ImGuiKey_Q = 562, - ImGuiKey_R = 563, - ImGuiKey_S = 564, - ImGuiKey_T = 565, - ImGuiKey_U = 566, - ImGuiKey_V = 567, - ImGuiKey_W = 568, - ImGuiKey_X = 569, - ImGuiKey_Y = 570, - ImGuiKey_Z = 571, - ImGuiKey_F1 = 572, - ImGuiKey_F2 = 573, - ImGuiKey_F3 = 574, - ImGuiKey_F4 = 575, - ImGuiKey_F5 = 576, - ImGuiKey_F6 = 577, - ImGuiKey_F7 = 578, - ImGuiKey_F8 = 579, - ImGuiKey_F9 = 580, - ImGuiKey_F10 = 581, - ImGuiKey_F11 = 582, - ImGuiKey_F12 = 583, - ImGuiKey_F13 = 584, - ImGuiKey_F14 = 585, - ImGuiKey_F15 = 586, - ImGuiKey_F16 = 587, - ImGuiKey_F17 = 588, - ImGuiKey_F18 = 589, - ImGuiKey_F19 = 590, - ImGuiKey_F20 = 591, - ImGuiKey_F21 = 592, - ImGuiKey_F22 = 593, - ImGuiKey_F23 = 594, - ImGuiKey_F24 = 595, - ImGuiKey_Apostrophe = 596, - ImGuiKey_Comma = 597, - ImGuiKey_Minus = 598, - ImGuiKey_Period = 599, - ImGuiKey_Slash = 600, - ImGuiKey_Semicolon = 601, - ImGuiKey_Equal = 602, - ImGuiKey_LeftBracket = 603, - ImGuiKey_Backslash = 604, - ImGuiKey_RightBracket = 605, - ImGuiKey_GraveAccent = 606, - ImGuiKey_CapsLock = 607, - ImGuiKey_ScrollLock = 608, - ImGuiKey_NumLock = 609, - ImGuiKey_PrintScreen = 610, - ImGuiKey_Pause = 611, - ImGuiKey_Keypad0 = 612, - ImGuiKey_Keypad1 = 613, - ImGuiKey_Keypad2 = 614, - ImGuiKey_Keypad3 = 615, - ImGuiKey_Keypad4 = 616, - ImGuiKey_Keypad5 = 617, - ImGuiKey_Keypad6 = 618, - ImGuiKey_Keypad7 = 619, - ImGuiKey_Keypad8 = 620, - ImGuiKey_Keypad9 = 621, - ImGuiKey_KeypadDecimal = 622, - ImGuiKey_KeypadDivide = 623, - ImGuiKey_KeypadMultiply = 624, - ImGuiKey_KeypadSubtract = 625, - ImGuiKey_KeypadAdd = 626, - ImGuiKey_KeypadEnter = 627, - ImGuiKey_KeypadEqual = 628, - ImGuiKey_AppBack = 629, - ImGuiKey_AppForward = 630, - ImGuiKey_GamepadStart = 631, - ImGuiKey_GamepadBack = 632, - ImGuiKey_GamepadFaceLeft = 633, - ImGuiKey_GamepadFaceRight = 634, - ImGuiKey_GamepadFaceUp = 635, - ImGuiKey_GamepadFaceDown = 636, - ImGuiKey_GamepadDpadLeft = 637, - ImGuiKey_GamepadDpadRight = 638, - ImGuiKey_GamepadDpadUp = 639, - ImGuiKey_GamepadDpadDown = 640, - ImGuiKey_GamepadL1 = 641, - ImGuiKey_GamepadR1 = 642, - ImGuiKey_GamepadL2 = 643, - ImGuiKey_GamepadR2 = 644, - ImGuiKey_GamepadL3 = 645, - ImGuiKey_GamepadR3 = 646, - ImGuiKey_GamepadLStickLeft = 647, - ImGuiKey_GamepadLStickRight = 648, - ImGuiKey_GamepadLStickUp = 649, - ImGuiKey_GamepadLStickDown = 650, - ImGuiKey_GamepadRStickLeft = 651, - ImGuiKey_GamepadRStickRight = 652, - ImGuiKey_GamepadRStickUp = 653, - ImGuiKey_GamepadRStickDown = 654, - ImGuiKey_MouseLeft = 655, - ImGuiKey_MouseRight = 656, - ImGuiKey_MouseMiddle = 657, - ImGuiKey_MouseX1 = 658, - ImGuiKey_MouseX2 = 659, - ImGuiKey_MouseWheelX = 660, - ImGuiKey_MouseWheelY = 661, - ImGuiKey_ReservedForModCtrl = 662, - ImGuiKey_ReservedForModShift = 663, - ImGuiKey_ReservedForModAlt = 664, - ImGuiKey_ReservedForModSuper = 665, - ImGuiKey_COUNT = 666, - ImGuiMod_None = 0, - ImGuiMod_Ctrl = 1 << 12, - ImGuiMod_Shift = 1 << 13, - ImGuiMod_Alt = 1 << 14, - ImGuiMod_Super = 1 << 15, - ImGuiMod_Shortcut = 1 << 11, - ImGuiMod_Mask_ = 0xF800, - ImGuiKey_NamedKey_BEGIN = 512, - ImGuiKey_NamedKey_END = ImGuiKey_COUNT, - ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, - ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, - ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN, -} ImGuiKey; -typedef enum { - ImGuiConfigFlags_None = 0, - ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, - ImGuiConfigFlags_NavEnableGamepad = 1 << 1, - ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, - ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, - ImGuiConfigFlags_NoMouse = 1 << 4, - ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, - ImGuiConfigFlags_DockingEnable = 1 << 6, - ImGuiConfigFlags_ViewportsEnable = 1 << 10, - ImGuiConfigFlags_DpiEnableScaleViewports = 1 << 14, - ImGuiConfigFlags_DpiEnableScaleFonts = 1 << 15, - ImGuiConfigFlags_IsSRGB = 1 << 20, - ImGuiConfigFlags_IsTouchScreen = 1 << 21, -} ImGuiConfigFlags_; -typedef enum { - ImGuiBackendFlags_None = 0, - ImGuiBackendFlags_HasGamepad = 1 << 0, - ImGuiBackendFlags_HasMouseCursors = 1 << 1, - ImGuiBackendFlags_HasSetMousePos = 1 << 2, - ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, - ImGuiBackendFlags_PlatformHasViewports = 1 << 10, - ImGuiBackendFlags_HasMouseHoveredViewport = 1 << 11, - ImGuiBackendFlags_RendererHasViewports = 1 << 12, -} ImGuiBackendFlags_; -typedef enum { - ImGuiCol_Text, - ImGuiCol_TextDisabled, - ImGuiCol_WindowBg, - ImGuiCol_ChildBg, - ImGuiCol_PopupBg, - ImGuiCol_Border, - ImGuiCol_BorderShadow, - ImGuiCol_FrameBg, - ImGuiCol_FrameBgHovered, - ImGuiCol_FrameBgActive, - ImGuiCol_TitleBg, - ImGuiCol_TitleBgActive, - ImGuiCol_TitleBgCollapsed, - ImGuiCol_MenuBarBg, - ImGuiCol_ScrollbarBg, - ImGuiCol_ScrollbarGrab, - ImGuiCol_ScrollbarGrabHovered, - ImGuiCol_ScrollbarGrabActive, - ImGuiCol_CheckMark, - ImGuiCol_SliderGrab, - ImGuiCol_SliderGrabActive, - ImGuiCol_Button, - ImGuiCol_ButtonHovered, - ImGuiCol_ButtonActive, - ImGuiCol_Header, - ImGuiCol_HeaderHovered, - ImGuiCol_HeaderActive, - ImGuiCol_Separator, - ImGuiCol_SeparatorHovered, - ImGuiCol_SeparatorActive, - ImGuiCol_ResizeGrip, - ImGuiCol_ResizeGripHovered, - ImGuiCol_ResizeGripActive, - ImGuiCol_Tab, - ImGuiCol_TabHovered, - ImGuiCol_TabActive, - ImGuiCol_TabUnfocused, - ImGuiCol_TabUnfocusedActive, - ImGuiCol_DockingPreview, - ImGuiCol_DockingEmptyBg, - ImGuiCol_PlotLines, - ImGuiCol_PlotLinesHovered, - ImGuiCol_PlotHistogram, - ImGuiCol_PlotHistogramHovered, - ImGuiCol_TableHeaderBg, - ImGuiCol_TableBorderStrong, - ImGuiCol_TableBorderLight, - ImGuiCol_TableRowBg, - ImGuiCol_TableRowBgAlt, - ImGuiCol_TextSelectedBg, - ImGuiCol_DragDropTarget, - ImGuiCol_NavHighlight, - ImGuiCol_NavWindowingHighlight, - ImGuiCol_NavWindowingDimBg, - ImGuiCol_ModalWindowDimBg, - ImGuiCol_COUNT -} ImGuiCol_; -typedef enum { - ImGuiStyleVar_Alpha, - ImGuiStyleVar_DisabledAlpha, - ImGuiStyleVar_WindowPadding, - ImGuiStyleVar_WindowRounding, - ImGuiStyleVar_WindowBorderSize, - ImGuiStyleVar_WindowMinSize, - ImGuiStyleVar_WindowTitleAlign, - ImGuiStyleVar_ChildRounding, - ImGuiStyleVar_ChildBorderSize, - ImGuiStyleVar_PopupRounding, - ImGuiStyleVar_PopupBorderSize, - ImGuiStyleVar_FramePadding, - ImGuiStyleVar_FrameRounding, - ImGuiStyleVar_FrameBorderSize, - ImGuiStyleVar_ItemSpacing, - ImGuiStyleVar_ItemInnerSpacing, - ImGuiStyleVar_IndentSpacing, - ImGuiStyleVar_CellPadding, - ImGuiStyleVar_ScrollbarSize, - ImGuiStyleVar_ScrollbarRounding, - ImGuiStyleVar_GrabMinSize, - ImGuiStyleVar_GrabRounding, - ImGuiStyleVar_TabRounding, - ImGuiStyleVar_TabBorderSize, - ImGuiStyleVar_TabBarBorderSize, - ImGuiStyleVar_TableAngledHeadersAngle, - ImGuiStyleVar_TableAngledHeadersTextAlign, - ImGuiStyleVar_ButtonTextAlign, - ImGuiStyleVar_SelectableTextAlign, - ImGuiStyleVar_SeparatorTextBorderSize, - ImGuiStyleVar_SeparatorTextAlign, - ImGuiStyleVar_SeparatorTextPadding, - ImGuiStyleVar_DockingSeparatorSize, - ImGuiStyleVar_COUNT -} ImGuiStyleVar_; -typedef enum { - ImGuiButtonFlags_None = 0, - ImGuiButtonFlags_MouseButtonLeft = 1 << 0, - ImGuiButtonFlags_MouseButtonRight = 1 << 1, - ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, - ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | - ImGuiButtonFlags_MouseButtonRight | - ImGuiButtonFlags_MouseButtonMiddle, - ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft, -} ImGuiButtonFlags_; -typedef enum { - ImGuiColorEditFlags_None = 0, - ImGuiColorEditFlags_NoAlpha = 1 << 1, - ImGuiColorEditFlags_NoPicker = 1 << 2, - ImGuiColorEditFlags_NoOptions = 1 << 3, - ImGuiColorEditFlags_NoSmallPreview = 1 << 4, - ImGuiColorEditFlags_NoInputs = 1 << 5, - ImGuiColorEditFlags_NoTooltip = 1 << 6, - ImGuiColorEditFlags_NoLabel = 1 << 7, - ImGuiColorEditFlags_NoSidePreview = 1 << 8, - ImGuiColorEditFlags_NoDragDrop = 1 << 9, - ImGuiColorEditFlags_NoBorder = 1 << 10, - ImGuiColorEditFlags_AlphaBar = 1 << 16, - ImGuiColorEditFlags_AlphaPreview = 1 << 17, - ImGuiColorEditFlags_AlphaPreviewHalf = 1 << 18, - ImGuiColorEditFlags_HDR = 1 << 19, - ImGuiColorEditFlags_DisplayRGB = 1 << 20, - ImGuiColorEditFlags_DisplayHSV = 1 << 21, - ImGuiColorEditFlags_DisplayHex = 1 << 22, - ImGuiColorEditFlags_Uint8 = 1 << 23, - ImGuiColorEditFlags_Float = 1 << 24, - ImGuiColorEditFlags_PickerHueBar = 1 << 25, - ImGuiColorEditFlags_PickerHueWheel = 1 << 26, - ImGuiColorEditFlags_InputRGB = 1 << 27, - ImGuiColorEditFlags_InputHSV = 1 << 28, - ImGuiColorEditFlags_DefaultOptions_ = - ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | - ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, - ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | - ImGuiColorEditFlags_DisplayHSV | - ImGuiColorEditFlags_DisplayHex, - ImGuiColorEditFlags_DataTypeMask_ = - ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, - ImGuiColorEditFlags_PickerMask_ = - ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, - ImGuiColorEditFlags_InputMask_ = - ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV, -} ImGuiColorEditFlags_; -typedef enum { - ImGuiSliderFlags_None = 0, - ImGuiSliderFlags_AlwaysClamp = 1 << 4, - ImGuiSliderFlags_Logarithmic = 1 << 5, - ImGuiSliderFlags_NoRoundToFormat = 1 << 6, - ImGuiSliderFlags_NoInput = 1 << 7, - ImGuiSliderFlags_InvalidMask_ = 0x7000000F, -} ImGuiSliderFlags_; -typedef enum { - ImGuiMouseButton_Left = 0, - ImGuiMouseButton_Right = 1, - ImGuiMouseButton_Middle = 2, - ImGuiMouseButton_COUNT = 5 -} ImGuiMouseButton_; -typedef enum { - ImGuiMouseCursor_None = -1, - ImGuiMouseCursor_Arrow = 0, - ImGuiMouseCursor_TextInput, - ImGuiMouseCursor_ResizeAll, - ImGuiMouseCursor_ResizeNS, - ImGuiMouseCursor_ResizeEW, - ImGuiMouseCursor_ResizeNESW, - ImGuiMouseCursor_ResizeNWSE, - ImGuiMouseCursor_Hand, - ImGuiMouseCursor_NotAllowed, - ImGuiMouseCursor_COUNT -} ImGuiMouseCursor_; -typedef enum { - ImGuiMouseSource_Mouse = 0, - ImGuiMouseSource_TouchScreen = 1, - ImGuiMouseSource_Pen = 2, - ImGuiMouseSource_COUNT = 3, -} ImGuiMouseSource; -typedef enum { - ImGuiCond_None = 0, - ImGuiCond_Always = 1 << 0, - ImGuiCond_Once = 1 << 1, - ImGuiCond_FirstUseEver = 1 << 2, - ImGuiCond_Appearing = 1 << 3, -} ImGuiCond_; -typedef enum { - ImGuiTableFlags_None = 0, - ImGuiTableFlags_Resizable = 1 << 0, - ImGuiTableFlags_Reorderable = 1 << 1, - ImGuiTableFlags_Hideable = 1 << 2, - ImGuiTableFlags_Sortable = 1 << 3, - ImGuiTableFlags_NoSavedSettings = 1 << 4, - ImGuiTableFlags_ContextMenuInBody = 1 << 5, - ImGuiTableFlags_RowBg = 1 << 6, - ImGuiTableFlags_BordersInnerH = 1 << 7, - ImGuiTableFlags_BordersOuterH = 1 << 8, - ImGuiTableFlags_BordersInnerV = 1 << 9, - ImGuiTableFlags_BordersOuterV = 1 << 10, - ImGuiTableFlags_BordersH = - ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, - ImGuiTableFlags_BordersV = - ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, - ImGuiTableFlags_BordersInner = - ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, - ImGuiTableFlags_BordersOuter = - ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, - ImGuiTableFlags_Borders = - ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, - ImGuiTableFlags_NoBordersInBody = 1 << 11, - ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, - ImGuiTableFlags_SizingFixedFit = 1 << 13, - ImGuiTableFlags_SizingFixedSame = 2 << 13, - ImGuiTableFlags_SizingStretchProp = 3 << 13, - ImGuiTableFlags_SizingStretchSame = 4 << 13, - ImGuiTableFlags_NoHostExtendX = 1 << 16, - ImGuiTableFlags_NoHostExtendY = 1 << 17, - ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, - ImGuiTableFlags_PreciseWidths = 1 << 19, - ImGuiTableFlags_NoClip = 1 << 20, - ImGuiTableFlags_PadOuterX = 1 << 21, - ImGuiTableFlags_NoPadOuterX = 1 << 22, - ImGuiTableFlags_NoPadInnerX = 1 << 23, - ImGuiTableFlags_ScrollX = 1 << 24, - ImGuiTableFlags_ScrollY = 1 << 25, - ImGuiTableFlags_SortMulti = 1 << 26, - ImGuiTableFlags_SortTristate = 1 << 27, - ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, - ImGuiTableFlags_SizingMask_ = - ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | - ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, -} ImGuiTableFlags_; -typedef enum { - ImGuiTableColumnFlags_None = 0, - ImGuiTableColumnFlags_Disabled = 1 << 0, - ImGuiTableColumnFlags_DefaultHide = 1 << 1, - ImGuiTableColumnFlags_DefaultSort = 1 << 2, - ImGuiTableColumnFlags_WidthStretch = 1 << 3, - ImGuiTableColumnFlags_WidthFixed = 1 << 4, - ImGuiTableColumnFlags_NoResize = 1 << 5, - ImGuiTableColumnFlags_NoReorder = 1 << 6, - ImGuiTableColumnFlags_NoHide = 1 << 7, - ImGuiTableColumnFlags_NoClip = 1 << 8, - ImGuiTableColumnFlags_NoSort = 1 << 9, - ImGuiTableColumnFlags_NoSortAscending = 1 << 10, - ImGuiTableColumnFlags_NoSortDescending = 1 << 11, - ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, - ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, - ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, - ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, - ImGuiTableColumnFlags_IndentEnable = 1 << 16, - ImGuiTableColumnFlags_IndentDisable = 1 << 17, - ImGuiTableColumnFlags_AngledHeader = 1 << 18, - ImGuiTableColumnFlags_IsEnabled = 1 << 24, - ImGuiTableColumnFlags_IsVisible = 1 << 25, - ImGuiTableColumnFlags_IsSorted = 1 << 26, - ImGuiTableColumnFlags_IsHovered = 1 << 27, - ImGuiTableColumnFlags_WidthMask_ = - ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, - ImGuiTableColumnFlags_IndentMask_ = - ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, - ImGuiTableColumnFlags_StatusMask_ = - ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | - ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, - ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, -} ImGuiTableColumnFlags_; -typedef enum { - ImGuiTableRowFlags_None = 0, - ImGuiTableRowFlags_Headers = 1 << 0, -} ImGuiTableRowFlags_; -typedef enum { - ImGuiTableBgTarget_None = 0, - ImGuiTableBgTarget_RowBg0 = 1, - ImGuiTableBgTarget_RowBg1 = 2, - ImGuiTableBgTarget_CellBg = 3, -} ImGuiTableBgTarget_; -struct ImGuiTableSortSpecs { - const ImGuiTableColumnSortSpecs* Specs; - int SpecsCount; - bool SpecsDirty; -}; -struct ImGuiTableColumnSortSpecs { - ImGuiID ColumnUserID; - ImS16 ColumnIndex; - ImS16 SortOrder; - ImGuiSortDirection SortDirection : 8; -}; -struct ImGuiStyle { - float Alpha; - float DisabledAlpha; - ImVec2 WindowPadding; - float WindowRounding; - float WindowBorderSize; - ImVec2 WindowMinSize; - ImVec2 WindowTitleAlign; - ImGuiDir WindowMenuButtonPosition; - float ChildRounding; - float ChildBorderSize; - float PopupRounding; - float PopupBorderSize; - ImVec2 FramePadding; - float FrameRounding; - float FrameBorderSize; - ImVec2 ItemSpacing; - ImVec2 ItemInnerSpacing; - ImVec2 CellPadding; - ImVec2 TouchExtraPadding; - float IndentSpacing; - float ColumnsMinSpacing; - float ScrollbarSize; - float ScrollbarRounding; - float GrabMinSize; - float GrabRounding; - float LogSliderDeadzone; - float TabRounding; - float TabBorderSize; - float TabMinWidthForCloseButton; - float TabBarBorderSize; - float TableAngledHeadersAngle; - ImVec2 TableAngledHeadersTextAlign; - ImGuiDir ColorButtonPosition; - ImVec2 ButtonTextAlign; - ImVec2 SelectableTextAlign; - float SeparatorTextBorderSize; - ImVec2 SeparatorTextAlign; - ImVec2 SeparatorTextPadding; - ImVec2 DisplayWindowPadding; - ImVec2 DisplaySafeAreaPadding; - float DockingSeparatorSize; - float MouseCursorScale; - bool AntiAliasedLines; - bool AntiAliasedLinesUseTex; - bool AntiAliasedFill; - float CurveTessellationTol; - float CircleTessellationMaxError; - ImVec4 Colors[ImGuiCol_COUNT]; - float HoverStationaryDelay; - float HoverDelayShort; - float HoverDelayNormal; - ImGuiHoveredFlags HoverFlagsForTooltipMouse; - ImGuiHoveredFlags HoverFlagsForTooltipNav; -}; -struct ImGuiKeyData { - bool Down; - float DownDuration; - float DownDurationPrev; - float AnalogValue; -}; -typedef struct ImVector_ImWchar { - int Size; - int Capacity; - ImWchar* Data; -} ImVector_ImWchar; - -struct ImGuiIO { - ImGuiConfigFlags ConfigFlags; - ImGuiBackendFlags BackendFlags; - ImVec2 DisplaySize; - float DeltaTime; - float IniSavingRate; - const char* IniFilename; - const char* LogFilename; - void* UserData; - ImFontAtlas* Fonts; - float FontGlobalScale; - bool FontAllowUserScaling; - ImFont* FontDefault; - ImVec2 DisplayFramebufferScale; - bool ConfigDockingNoSplit; - bool ConfigDockingWithShift; - bool ConfigDockingAlwaysTabBar; - bool ConfigDockingTransparentPayload; - bool ConfigViewportsNoAutoMerge; - bool ConfigViewportsNoTaskBarIcon; - bool ConfigViewportsNoDecoration; - bool ConfigViewportsNoDefaultParent; - bool MouseDrawCursor; - bool ConfigMacOSXBehaviors; - bool ConfigInputTrickleEventQueue; - bool ConfigInputTextCursorBlink; - bool ConfigInputTextEnterKeepActive; - bool ConfigDragClickToInputText; - bool ConfigWindowsResizeFromEdges; - bool ConfigWindowsMoveFromTitleBarOnly; - float ConfigMemoryCompactTimer; - float MouseDoubleClickTime; - float MouseDoubleClickMaxDist; - float MouseDragThreshold; - float KeyRepeatDelay; - float KeyRepeatRate; - bool ConfigDebugIsDebuggerPresent; - bool ConfigDebugBeginReturnValueOnce; - bool ConfigDebugBeginReturnValueLoop; - bool ConfigDebugIgnoreFocusLoss; - bool ConfigDebugIniSettings; - const char* BackendPlatformName; - const char* BackendRendererName; - void* BackendPlatformUserData; - void* BackendRendererUserData; - void* BackendLanguageUserData; - const char* (*GetClipboardTextFn)(void* user_data); - void (*SetClipboardTextFn)(void* user_data, const char* text); - void* ClipboardUserData; - void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, - ImGuiPlatformImeData* data); - ImWchar PlatformLocaleDecimalPoint; - bool WantCaptureMouse; - bool WantCaptureKeyboard; - bool WantTextInput; - bool WantSetMousePos; - bool WantSaveIniSettings; - bool NavActive; - bool NavVisible; - float Framerate; - int MetricsRenderVertices; - int MetricsRenderIndices; - int MetricsRenderWindows; - int MetricsActiveWindows; - ImVec2 MouseDelta; - ImGuiContext* Ctx; - ImVec2 MousePos; - bool MouseDown[5]; - float MouseWheel; - float MouseWheelH; - ImGuiMouseSource MouseSource; - ImGuiID MouseHoveredViewport; - bool KeyCtrl; - bool KeyShift; - bool KeyAlt; - bool KeySuper; - ImGuiKeyChord KeyMods; - ImGuiKeyData KeysData[ImGuiKey_KeysData_SIZE]; - bool WantCaptureMouseUnlessPopupClose; - ImVec2 MousePosPrev; - ImVec2 MouseClickedPos[5]; - double MouseClickedTime[5]; - bool MouseClicked[5]; - bool MouseDoubleClicked[5]; - ImU16 MouseClickedCount[5]; - ImU16 MouseClickedLastCount[5]; - bool MouseReleased[5]; - bool MouseDownOwned[5]; - bool MouseDownOwnedUnlessPopupClose[5]; - bool MouseWheelRequestAxisSwap; - float MouseDownDuration[5]; - float MouseDownDurationPrev[5]; - ImVec2 MouseDragMaxDistanceAbs[5]; - float MouseDragMaxDistanceSqr[5]; - float PenPressure; - bool AppFocusLost; - bool AppAcceptingEvents; - ImS8 BackendUsingLegacyKeyArrays; - bool BackendUsingLegacyNavInputArray; - ImWchar16 InputQueueSurrogate; - ImVector_ImWchar InputQueueCharacters; -}; -struct ImGuiInputTextCallbackData { - ImGuiContext* Ctx; - ImGuiInputTextFlags EventFlag; - ImGuiInputTextFlags Flags; - void* UserData; - ImWchar EventChar; - ImGuiKey EventKey; - char* Buf; - int BufTextLen; - int BufSize; - bool BufDirty; - int CursorPos; - int SelectionStart; - int SelectionEnd; -}; -struct ImGuiSizeCallbackData { - void* UserData; - ImVec2 Pos; - ImVec2 CurrentSize; - ImVec2 DesiredSize; -}; -struct ImGuiWindowClass { - ImGuiID ClassId; - ImGuiID ParentViewportId; - ImGuiID FocusRouteParentWindowId; - ImGuiViewportFlags ViewportFlagsOverrideSet; - ImGuiViewportFlags ViewportFlagsOverrideClear; - ImGuiTabItemFlags TabItemFlagsOverrideSet; - ImGuiDockNodeFlags DockNodeFlagsOverrideSet; - bool DockingAlwaysTabBar; - bool DockingAllowUnclassed; -}; -struct ImGuiPayload { - void* Data; - int DataSize; - ImGuiID SourceId; - ImGuiID SourceParentId; - int DataFrameCount; - char DataType[32 + 1]; - bool Preview; - bool Delivery; -}; -struct ImGuiOnceUponAFrame { - int RefFrame; -}; -struct ImGuiTextRange { - const char* b; - const char* e; -}; -typedef struct ImGuiTextRange ImGuiTextRange; - -typedef struct ImVector_ImGuiTextRange { - int Size; - int Capacity; - ImGuiTextRange* Data; -} ImVector_ImGuiTextRange; - -struct ImGuiTextFilter { - char InputBuf[256]; - ImVector_ImGuiTextRange Filters; - int CountGrep; -}; -typedef struct ImGuiTextRange ImGuiTextRange; -typedef struct ImVector_char { - int Size; - int Capacity; - char* Data; -} ImVector_char; - -struct ImGuiTextBuffer { - ImVector_char Buf; -}; -struct ImGuiStoragePair { - ImGuiID key; - union { - int val_i; - float val_f; - void* val_p; - }; -}; -typedef struct ImGuiStoragePair ImGuiStoragePair; - -typedef struct ImVector_ImGuiStoragePair { - int Size; - int Capacity; - ImGuiStoragePair* Data; -} ImVector_ImGuiStoragePair; - -struct ImGuiStorage { - ImVector_ImGuiStoragePair Data; -}; -typedef struct ImGuiStoragePair ImGuiStoragePair; -struct ImGuiListClipper { - ImGuiContext* Ctx; - int DisplayStart; - int DisplayEnd; - int ItemsCount; - float ItemsHeight; - float StartPosY; - void* TempData; -}; -struct ImColor { - ImVec4 Value; -}; -typedef void (*ImDrawCallback)(const ImDrawList* parent_list, - const ImDrawCmd* cmd); -struct ImDrawCmd { - ImVec4 ClipRect; - ImTextureID TextureId; - unsigned int VtxOffset; - unsigned int IdxOffset; - unsigned int ElemCount; - ImDrawCallback UserCallback; - void* UserCallbackData; -}; -struct ImDrawVert { - ImVec2 pos; - ImVec2 uv; - ImU32 col; -}; -typedef struct ImDrawCmdHeader ImDrawCmdHeader; -struct ImDrawCmdHeader { - ImVec4 ClipRect; - ImTextureID TextureId; - unsigned int VtxOffset; -}; -typedef struct ImVector_ImDrawCmd { - int Size; - int Capacity; - ImDrawCmd* Data; -} ImVector_ImDrawCmd; - -typedef struct ImVector_ImDrawIdx { - int Size; - int Capacity; - ImDrawIdx* Data; -} ImVector_ImDrawIdx; - -struct ImDrawChannel { - ImVector_ImDrawCmd _CmdBuffer; - ImVector_ImDrawIdx _IdxBuffer; -}; -typedef struct ImVector_ImDrawChannel { - int Size; - int Capacity; - ImDrawChannel* Data; -} ImVector_ImDrawChannel; - -struct ImDrawListSplitter { - int _Current; - int _Count; - ImVector_ImDrawChannel _Channels; -}; -typedef enum { - ImDrawFlags_None = 0, - ImDrawFlags_Closed = 1 << 0, - ImDrawFlags_RoundCornersTopLeft = 1 << 4, - ImDrawFlags_RoundCornersTopRight = 1 << 5, - ImDrawFlags_RoundCornersBottomLeft = 1 << 6, - ImDrawFlags_RoundCornersBottomRight = 1 << 7, - ImDrawFlags_RoundCornersNone = 1 << 8, - ImDrawFlags_RoundCornersTop = - ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, - ImDrawFlags_RoundCornersBottom = - ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, - ImDrawFlags_RoundCornersLeft = - ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, - ImDrawFlags_RoundCornersRight = - ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, - ImDrawFlags_RoundCornersAll = - ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | - ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, - ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, - ImDrawFlags_RoundCornersMask_ = - ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, -} ImDrawFlags_; -typedef enum { - ImDrawListFlags_None = 0, - ImDrawListFlags_AntiAliasedLines = 1 << 0, - ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, - ImDrawListFlags_AntiAliasedFill = 1 << 2, - ImDrawListFlags_AllowVtxOffset = 1 << 3, -} ImDrawListFlags_; -typedef struct ImVector_ImDrawVert { - int Size; - int Capacity; - ImDrawVert* Data; -} ImVector_ImDrawVert; - -typedef struct ImVector_ImVec2 { - int Size; - int Capacity; - ImVec2* Data; -} ImVector_ImVec2; - -typedef struct ImVector_ImVec4 { - int Size; - int Capacity; - ImVec4* Data; -} ImVector_ImVec4; - -typedef struct ImVector_ImTextureID { - int Size; - int Capacity; - ImTextureID* Data; -} ImVector_ImTextureID; - -struct ImDrawList { - ImVector_ImDrawCmd CmdBuffer; - ImVector_ImDrawIdx IdxBuffer; - ImVector_ImDrawVert VtxBuffer; - ImDrawListFlags Flags; - unsigned int _VtxCurrentIdx; - ImDrawListSharedData* _Data; - ImDrawVert* _VtxWritePtr; - ImDrawIdx* _IdxWritePtr; - ImVector_ImVec2 _Path; - ImDrawCmdHeader _CmdHeader; - ImDrawListSplitter _Splitter; - ImVector_ImVec4 _ClipRectStack; - ImVector_ImTextureID _TextureIdStack; - float _FringeScale; - const char* _OwnerName; -}; -typedef struct ImVector_ImDrawListPtr { - int Size; - int Capacity; - ImDrawList** Data; -} ImVector_ImDrawListPtr; - -struct ImDrawData { - bool Valid; - int CmdListsCount; - int TotalIdxCount; - int TotalVtxCount; - ImVector_ImDrawListPtr CmdLists; - ImVec2 DisplayPos; - ImVec2 DisplaySize; - ImVec2 FramebufferScale; - ImGuiViewport* OwnerViewport; -}; -struct ImFontConfig { - void* FontData; - int FontDataSize; - bool FontDataOwnedByAtlas; - int FontNo; - float SizePixels; - int OversampleH; - int OversampleV; - bool PixelSnapH; - ImVec2 GlyphExtraSpacing; - ImVec2 GlyphOffset; - const ImWchar* GlyphRanges; - float GlyphMinAdvanceX; - float GlyphMaxAdvanceX; - bool MergeMode; - unsigned int FontBuilderFlags; - float RasterizerMultiply; - float RasterizerDensity; - ImWchar EllipsisChar; - char Name[40]; - ImFont* DstFont; -}; -struct ImFontGlyph { - unsigned int Colored : 1; - unsigned int Visible : 1; - unsigned int Codepoint : 30; - float AdvanceX; - float X0, Y0, X1, Y1; - float U0, V0, U1, V1; -}; -typedef struct ImVector_ImU32 { - int Size; - int Capacity; - ImU32* Data; -} ImVector_ImU32; - -struct ImFontGlyphRangesBuilder { - ImVector_ImU32 UsedChars; -}; -typedef struct ImFontAtlasCustomRect ImFontAtlasCustomRect; -struct ImFontAtlasCustomRect { - unsigned short Width, Height; - unsigned short X, Y; - unsigned int GlyphID; - float GlyphAdvanceX; - ImVec2 GlyphOffset; - ImFont* Font; -}; -typedef enum { - ImFontAtlasFlags_None = 0, - ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, - ImFontAtlasFlags_NoMouseCursors = 1 << 1, - ImFontAtlasFlags_NoBakedLines = 1 << 2, -} ImFontAtlasFlags_; -typedef struct ImVector_ImFontPtr { - int Size; - int Capacity; - ImFont** Data; -} ImVector_ImFontPtr; - -typedef struct ImVector_ImFontAtlasCustomRect { - int Size; - int Capacity; - ImFontAtlasCustomRect* Data; -} ImVector_ImFontAtlasCustomRect; - -typedef struct ImVector_ImFontConfig { - int Size; - int Capacity; - ImFontConfig* Data; -} ImVector_ImFontConfig; - -struct ImFontAtlas { - ImFontAtlasFlags Flags; - ImTextureID TexID; - int TexDesiredWidth; - int TexGlyphPadding; - bool Locked; - void* UserData; - bool TexReady; - bool TexPixelsUseColors; - unsigned char* TexPixelsAlpha8; - unsigned int* TexPixelsRGBA32; - int TexWidth; - int TexHeight; - ImVec2 TexUvScale; - ImVec2 TexUvWhitePixel; - ImVector_ImFontPtr Fonts; - ImVector_ImFontAtlasCustomRect CustomRects; - ImVector_ImFontConfig ConfigData; - ImVec4 TexUvLines[(63) + 1]; - const ImFontBuilderIO* FontBuilderIO; - unsigned int FontBuilderFlags; - int PackIdMouseCursors; - int PackIdLines; -}; -typedef struct ImVector_float { - int Size; - int Capacity; - float* Data; -} ImVector_float; - -typedef struct ImVector_ImFontGlyph { - int Size; - int Capacity; - ImFontGlyph* Data; -} ImVector_ImFontGlyph; - -struct ImFont { - ImVector_float IndexAdvanceX; - float FallbackAdvanceX; - float FontSize; - ImVector_ImWchar IndexLookup; - ImVector_ImFontGlyph Glyphs; - const ImFontGlyph* FallbackGlyph; - ImFontAtlas* ContainerAtlas; - const ImFontConfig* ConfigData; - short ConfigDataCount; - ImWchar FallbackChar; - ImWchar EllipsisChar; - short EllipsisCharCount; - float EllipsisWidth; - float EllipsisCharStep; - bool DirtyLookupTables; - float Scale; - float Ascent, Descent; - int MetricsTotalSurface; - ImU8 Used4kPagesMap[(0xFFFF + 1) / 4096 / 8]; -}; -typedef enum { - ImGuiViewportFlags_None = 0, - ImGuiViewportFlags_IsPlatformWindow = 1 << 0, - ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, - ImGuiViewportFlags_OwnedByApp = 1 << 2, - ImGuiViewportFlags_NoDecoration = 1 << 3, - ImGuiViewportFlags_NoTaskBarIcon = 1 << 4, - ImGuiViewportFlags_NoFocusOnAppearing = 1 << 5, - ImGuiViewportFlags_NoFocusOnClick = 1 << 6, - ImGuiViewportFlags_NoInputs = 1 << 7, - ImGuiViewportFlags_NoRendererClear = 1 << 8, - ImGuiViewportFlags_NoAutoMerge = 1 << 9, - ImGuiViewportFlags_TopMost = 1 << 10, - ImGuiViewportFlags_CanHostOtherWindows = 1 << 11, - ImGuiViewportFlags_IsMinimized = 1 << 12, - ImGuiViewportFlags_IsFocused = 1 << 13, -} ImGuiViewportFlags_; -struct ImGuiViewport { - ImGuiID ID; - ImGuiViewportFlags Flags; - ImVec2 Pos; - ImVec2 Size; - ImVec2 WorkPos; - ImVec2 WorkSize; - float DpiScale; - ImGuiID ParentViewportId; - ImDrawData* DrawData; - void* RendererUserData; - void* PlatformUserData; - void* PlatformHandle; - void* PlatformHandleRaw; - bool PlatformWindowCreated; - bool PlatformRequestMove; - bool PlatformRequestResize; - bool PlatformRequestClose; -}; -typedef struct ImVector_ImGuiPlatformMonitor { - int Size; - int Capacity; - ImGuiPlatformMonitor* Data; -} ImVector_ImGuiPlatformMonitor; - -typedef struct ImVector_ImGuiViewportPtr { - int Size; - int Capacity; - ImGuiViewport** Data; -} ImVector_ImGuiViewportPtr; - -struct ImGuiPlatformIO { - void (*Platform_CreateWindow)(ImGuiViewport* vp); - void (*Platform_DestroyWindow)(ImGuiViewport* vp); - void (*Platform_ShowWindow)(ImGuiViewport* vp); - void (*Platform_SetWindowPos)(ImGuiViewport* vp, ImVec2 pos); - ImVec2 (*Platform_GetWindowPos)(ImGuiViewport* vp); - void (*Platform_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); - ImVec2 (*Platform_GetWindowSize)(ImGuiViewport* vp); - void (*Platform_SetWindowFocus)(ImGuiViewport* vp); - bool (*Platform_GetWindowFocus)(ImGuiViewport* vp); - bool (*Platform_GetWindowMinimized)(ImGuiViewport* vp); - void (*Platform_SetWindowTitle)(ImGuiViewport* vp, const char* str); - void (*Platform_SetWindowAlpha)(ImGuiViewport* vp, float alpha); - void (*Platform_UpdateWindow)(ImGuiViewport* vp); - void (*Platform_RenderWindow)(ImGuiViewport* vp, void* render_arg); - void (*Platform_SwapBuffers)(ImGuiViewport* vp, void* render_arg); - float (*Platform_GetWindowDpiScale)(ImGuiViewport* vp); - void (*Platform_OnChangedViewport)(ImGuiViewport* vp); - int (*Platform_CreateVkSurface)(ImGuiViewport* vp, - ImU64 vk_inst, - const void* vk_allocators, - ImU64* out_vk_surface); - void (*Renderer_CreateWindow)(ImGuiViewport* vp); - void (*Renderer_DestroyWindow)(ImGuiViewport* vp); - void (*Renderer_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); - void (*Renderer_RenderWindow)(ImGuiViewport* vp, void* render_arg); - void (*Renderer_SwapBuffers)(ImGuiViewport* vp, void* render_arg); - ImVector_ImGuiPlatformMonitor Monitors; - ImVector_ImGuiViewportPtr Viewports; -}; -struct ImGuiPlatformMonitor { - ImVec2 MainPos, MainSize; - ImVec2 WorkPos, WorkSize; - float DpiScale; - void* PlatformHandle; -}; -struct ImGuiPlatformImeData { - bool WantVisible; - ImVec2 InputPos; - float InputLineHeight; -}; -struct ImBitVector; -struct ImRect; -struct ImDrawDataBuilder; -struct ImDrawListSharedData; -struct ImGuiColorMod; -struct ImGuiContext; -struct ImGuiContextHook; -struct ImGuiDataVarInfo; -struct ImGuiDataTypeInfo; -struct ImGuiDockContext; -struct ImGuiDockRequest; -struct ImGuiDockNode; -struct ImGuiDockNodeSettings; -struct ImGuiGroupData; -struct ImGuiInputTextState; -struct ImGuiInputTextDeactivateData; -struct ImGuiLastItemData; -struct ImGuiLocEntry; -struct ImGuiMenuColumns; -struct ImGuiNavItemData; -struct ImGuiNavTreeNodeData; -struct ImGuiMetricsConfig; -struct ImGuiNextWindowData; -struct ImGuiNextItemData; -struct ImGuiOldColumnData; -struct ImGuiOldColumns; -struct ImGuiPopupData; -struct ImGuiSettingsHandler; -struct ImGuiStackSizes; -struct ImGuiStyleMod; -struct ImGuiTabBar; -struct ImGuiTabItem; -struct ImGuiTable; -struct ImGuiTableHeaderData; -struct ImGuiTableColumn; -struct ImGuiTableInstanceData; -struct ImGuiTableTempData; -struct ImGuiTableSettings; -struct ImGuiTableColumnsSettings; -struct ImGuiTypingSelectState; -struct ImGuiTypingSelectRequest; -struct ImGuiWindow; -struct ImGuiWindowDockStyle; -struct ImGuiWindowTempData; -struct ImGuiWindowSettings; -typedef int ImGuiDataAuthority; -typedef int ImGuiLayoutType; -typedef int ImGuiActivateFlags; -typedef int ImGuiDebugLogFlags; -typedef int ImGuiFocusRequestFlags; -typedef int ImGuiInputFlags; -typedef int ImGuiItemFlags; -typedef int ImGuiItemStatusFlags; -typedef int ImGuiOldColumnFlags; -typedef int ImGuiNavHighlightFlags; -typedef int ImGuiNavMoveFlags; -typedef int ImGuiNextItemDataFlags; -typedef int ImGuiNextWindowDataFlags; -typedef int ImGuiScrollFlags; -typedef int ImGuiSeparatorFlags; -typedef int ImGuiTextFlags; -typedef int ImGuiTooltipFlags; -typedef int ImGuiTypingSelectFlags; -typedef int ImGuiWindowRefreshFlags; -typedef void (*ImGuiErrorLogCallback)(void* user_data, const char* fmt, ...); -extern ImGuiContext* GImGui; -typedef struct StbUndoRecord StbUndoRecord; -struct StbUndoRecord { - int where; - int insert_length; - int delete_length; - int char_storage; -}; -typedef struct StbUndoState StbUndoState; -struct StbUndoState { - StbUndoRecord undo_rec[99]; - ImWchar undo_char[999]; - short undo_point, redo_point; - int undo_char_point, redo_char_point; -}; -typedef struct STB_TexteditState STB_TexteditState; -struct STB_TexteditState { - int cursor; - int select_start; - int select_end; - unsigned char insert_mode; - int row_count_per_page; - unsigned char cursor_at_end_of_line; - unsigned char initialized; - unsigned char has_preferred_x; - unsigned char single_line; - unsigned char padding1, padding2, padding3; - float preferred_x; - StbUndoState undostate; -}; -typedef struct StbTexteditRow StbTexteditRow; -struct StbTexteditRow { - float x0, x1; - float baseline_y_delta; - float ymin, ymax; - int num_chars; -}; -typedef FILE* ImFileHandle; -typedef struct ImVec1 ImVec1; -struct ImVec1 { - float x; -}; -typedef struct ImVec2ih ImVec2ih; -struct ImVec2ih { - short x, y; -}; -struct ImRect { - ImVec2 Min; - ImVec2 Max; -}; -typedef ImU32* ImBitArrayPtr; -struct ImBitVector { - ImVector_ImU32 Storage; -}; -typedef int ImPoolIdx; -typedef struct ImGuiTextIndex ImGuiTextIndex; -typedef struct ImVector_int { - int Size; - int Capacity; - int* Data; -} ImVector_int; - -struct ImGuiTextIndex { - ImVector_int LineOffsets; - int EndOffset; -}; -struct ImDrawListSharedData { - ImVec2 TexUvWhitePixel; - ImFont* Font; - float FontSize; - float CurveTessellationTol; - float CircleSegmentMaxError; - ImVec4 ClipRectFullscreen; - ImDrawListFlags InitialFlags; - ImVector_ImVec2 TempBuffer; - ImVec2 ArcFastVtx[48]; - float ArcFastRadiusCutoff; - ImU8 CircleSegmentCounts[64]; - const ImVec4* TexUvLines; -}; -struct ImDrawDataBuilder { - ImVector_ImDrawListPtr* Layers[2]; - ImVector_ImDrawListPtr LayerData1; -}; -typedef enum { - ImGuiItemFlags_None = 0, - ImGuiItemFlags_NoTabStop = 1 << 0, - ImGuiItemFlags_ButtonRepeat = 1 << 1, - ImGuiItemFlags_Disabled = 1 << 2, - ImGuiItemFlags_NoNav = 1 << 3, - ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, - ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, - ImGuiItemFlags_MixedValue = 1 << 6, - ImGuiItemFlags_ReadOnly = 1 << 7, - ImGuiItemFlags_NoWindowHoverableCheck = 1 << 8, - ImGuiItemFlags_AllowOverlap = 1 << 9, - ImGuiItemFlags_Inputable = 1 << 10, - ImGuiItemFlags_HasSelectionUserData = 1 << 11, -} ImGuiItemFlags_; -typedef enum { - ImGuiItemStatusFlags_None = 0, - ImGuiItemStatusFlags_HoveredRect = 1 << 0, - ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, - ImGuiItemStatusFlags_Edited = 1 << 2, - ImGuiItemStatusFlags_ToggledSelection = 1 << 3, - ImGuiItemStatusFlags_ToggledOpen = 1 << 4, - ImGuiItemStatusFlags_HasDeactivated = 1 << 5, - ImGuiItemStatusFlags_Deactivated = 1 << 6, - ImGuiItemStatusFlags_HoveredWindow = 1 << 7, - ImGuiItemStatusFlags_Visible = 1 << 8, - ImGuiItemStatusFlags_HasClipRect = 1 << 9, -} ImGuiItemStatusFlags_; -typedef enum { - ImGuiHoveredFlags_DelayMask_ = - ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | - ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay, - ImGuiHoveredFlags_AllowedMaskForIsWindowHovered = - ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | - ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_NoPopupHierarchy | - ImGuiHoveredFlags_DockHierarchy | - ImGuiHoveredFlags_AllowWhenBlockedByPopup | - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | - ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary, - ImGuiHoveredFlags_AllowedMaskForIsItemHovered = - ImGuiHoveredFlags_AllowWhenBlockedByPopup | - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | - ImGuiHoveredFlags_AllowWhenOverlapped | - ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoNavOverride | - ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary | - ImGuiHoveredFlags_DelayMask_, -} ImGuiHoveredFlagsPrivate_; -typedef enum { - ImGuiInputTextFlags_Multiline = 1 << 26, - ImGuiInputTextFlags_NoMarkEdited = 1 << 27, - ImGuiInputTextFlags_MergedItem = 1 << 28, - ImGuiInputTextFlags_LocalizeDecimalPoint = 1 << 29, -} ImGuiInputTextFlagsPrivate_; -typedef enum { - ImGuiButtonFlags_PressedOnClick = 1 << 4, - ImGuiButtonFlags_PressedOnClickRelease = 1 << 5, - ImGuiButtonFlags_PressedOnClickReleaseAnywhere = 1 << 6, - ImGuiButtonFlags_PressedOnRelease = 1 << 7, - ImGuiButtonFlags_PressedOnDoubleClick = 1 << 8, - ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, - ImGuiButtonFlags_Repeat = 1 << 10, - ImGuiButtonFlags_FlattenChildren = 1 << 11, - ImGuiButtonFlags_AllowOverlap = 1 << 12, - ImGuiButtonFlags_DontClosePopups = 1 << 13, - ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, - ImGuiButtonFlags_NoKeyModifiers = 1 << 16, - ImGuiButtonFlags_NoHoldingActiveId = 1 << 17, - ImGuiButtonFlags_NoNavFocus = 1 << 18, - ImGuiButtonFlags_NoHoveredOnFocus = 1 << 19, - ImGuiButtonFlags_NoSetKeyOwner = 1 << 20, - ImGuiButtonFlags_NoTestKeyOwner = 1 << 21, - ImGuiButtonFlags_PressedOnMask_ = - ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | - ImGuiButtonFlags_PressedOnClickReleaseAnywhere | - ImGuiButtonFlags_PressedOnRelease | - ImGuiButtonFlags_PressedOnDoubleClick | - ImGuiButtonFlags_PressedOnDragDropHold, - ImGuiButtonFlags_PressedOnDefault_ = ImGuiButtonFlags_PressedOnClickRelease, -} ImGuiButtonFlagsPrivate_; -typedef enum { - ImGuiComboFlags_CustomPreview = 1 << 20, -} ImGuiComboFlagsPrivate_; -typedef enum { - ImGuiSliderFlags_Vertical = 1 << 20, - ImGuiSliderFlags_ReadOnly = 1 << 21, -} ImGuiSliderFlagsPrivate_; -typedef enum { - ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, - ImGuiSelectableFlags_SelectOnNav = 1 << 21, - ImGuiSelectableFlags_SelectOnClick = 1 << 22, - ImGuiSelectableFlags_SelectOnRelease = 1 << 23, - ImGuiSelectableFlags_SpanAvailWidth = 1 << 24, - ImGuiSelectableFlags_SetNavIdOnHover = 1 << 25, - ImGuiSelectableFlags_NoPadWithHalfSpacing = 1 << 26, - ImGuiSelectableFlags_NoSetKeyOwner = 1 << 27, -} ImGuiSelectableFlagsPrivate_; -typedef enum { - ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 20, - ImGuiTreeNodeFlags_UpsideDownArrow = 1 << 21, -} ImGuiTreeNodeFlagsPrivate_; -typedef enum { - ImGuiSeparatorFlags_None = 0, - ImGuiSeparatorFlags_Horizontal = 1 << 0, - ImGuiSeparatorFlags_Vertical = 1 << 1, - ImGuiSeparatorFlags_SpanAllColumns = 1 << 2, -} ImGuiSeparatorFlags_; -typedef enum { - ImGuiFocusRequestFlags_None = 0, - ImGuiFocusRequestFlags_RestoreFocusedChild = 1 << 0, - ImGuiFocusRequestFlags_UnlessBelowModal = 1 << 1, -} ImGuiFocusRequestFlags_; -typedef enum { - ImGuiTextFlags_None = 0, - ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0, -} ImGuiTextFlags_; -typedef enum { - ImGuiTooltipFlags_None = 0, - ImGuiTooltipFlags_OverridePrevious = 1 << 1, -} ImGuiTooltipFlags_; -typedef enum { - ImGuiLayoutType_Horizontal = 0, - ImGuiLayoutType_Vertical = 1 -} ImGuiLayoutType_; -typedef enum { - ImGuiLogType_None = 0, - ImGuiLogType_TTY, - ImGuiLogType_File, - ImGuiLogType_Buffer, - ImGuiLogType_Clipboard, -} ImGuiLogType; -typedef enum { - ImGuiAxis_None = -1, - ImGuiAxis_X = 0, - ImGuiAxis_Y = 1 -} ImGuiAxis; -typedef enum { - ImGuiPlotType_Lines, - ImGuiPlotType_Histogram, -} ImGuiPlotType; -struct ImGuiColorMod { - ImGuiCol Col; - ImVec4 BackupValue; -}; -struct ImGuiStyleMod { - ImGuiStyleVar VarIdx; - union { - int BackupInt[2]; - float BackupFloat[2]; - }; -}; -typedef struct ImGuiComboPreviewData ImGuiComboPreviewData; -struct ImGuiComboPreviewData { - ImRect PreviewRect; - ImVec2 BackupCursorPos; - ImVec2 BackupCursorMaxPos; - ImVec2 BackupCursorPosPrevLine; - float BackupPrevLineTextBaseOffset; - ImGuiLayoutType BackupLayout; -}; -struct ImGuiGroupData { - ImGuiID WindowID; - ImVec2 BackupCursorPos; - ImVec2 BackupCursorMaxPos; - ImVec2 BackupCursorPosPrevLine; - ImVec1 BackupIndent; - ImVec1 BackupGroupOffset; - ImVec2 BackupCurrLineSize; - float BackupCurrLineTextBaseOffset; - ImGuiID BackupActiveIdIsAlive; - bool BackupActiveIdPreviousFrameIsAlive; - bool BackupHoveredIdIsAlive; - bool BackupIsSameLine; - bool EmitItem; -}; -struct ImGuiMenuColumns { - ImU32 TotalWidth; - ImU32 NextTotalWidth; - ImU16 Spacing; - ImU16 OffsetIcon; - ImU16 OffsetLabel; - ImU16 OffsetShortcut; - ImU16 OffsetMark; - ImU16 Widths[4]; -}; -typedef struct ImGuiInputTextDeactivatedState ImGuiInputTextDeactivatedState; -struct ImGuiInputTextDeactivatedState { - ImGuiID ID; - ImVector_char TextA; -}; -struct ImGuiInputTextState { - ImGuiContext* Ctx; - ImGuiID ID; - int CurLenW, CurLenA; - ImVector_ImWchar TextW; - ImVector_char TextA; - ImVector_char InitialTextA; - bool TextAIsValid; - int BufCapacityA; - float ScrollX; - STB_TexteditState Stb; - float CursorAnim; - bool CursorFollow; - bool SelectedAllMouseLock; - bool Edited; - ImGuiInputTextFlags Flags; - bool ReloadUserBuf; - int ReloadSelectionStart; - int ReloadSelectionEnd; -}; -typedef enum { - ImGuiWindowRefreshFlags_None = 0, - ImGuiWindowRefreshFlags_TryToAvoidRefresh = 1 << 0, - ImGuiWindowRefreshFlags_RefreshOnHover = 1 << 1, - ImGuiWindowRefreshFlags_RefreshOnFocus = 1 << 2, -} ImGuiWindowRefreshFlags_; -typedef enum { - ImGuiNextWindowDataFlags_None = 0, - ImGuiNextWindowDataFlags_HasPos = 1 << 0, - ImGuiNextWindowDataFlags_HasSize = 1 << 1, - ImGuiNextWindowDataFlags_HasContentSize = 1 << 2, - ImGuiNextWindowDataFlags_HasCollapsed = 1 << 3, - ImGuiNextWindowDataFlags_HasSizeConstraint = 1 << 4, - ImGuiNextWindowDataFlags_HasFocus = 1 << 5, - ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, - ImGuiNextWindowDataFlags_HasScroll = 1 << 7, - ImGuiNextWindowDataFlags_HasChildFlags = 1 << 8, - ImGuiNextWindowDataFlags_HasRefreshPolicy = 1 << 9, - ImGuiNextWindowDataFlags_HasViewport = 1 << 10, - ImGuiNextWindowDataFlags_HasDock = 1 << 11, - ImGuiNextWindowDataFlags_HasWindowClass = 1 << 12, -} ImGuiNextWindowDataFlags_; -struct ImGuiNextWindowData { - ImGuiNextWindowDataFlags Flags; - ImGuiCond PosCond; - ImGuiCond SizeCond; - ImGuiCond CollapsedCond; - ImGuiCond DockCond; - ImVec2 PosVal; - ImVec2 PosPivotVal; - ImVec2 SizeVal; - ImVec2 ContentSizeVal; - ImVec2 ScrollVal; - ImGuiChildFlags ChildFlags; - bool PosUndock; - bool CollapsedVal; - ImRect SizeConstraintRect; - ImGuiSizeCallback SizeCallback; - void* SizeCallbackUserData; - float BgAlphaVal; - ImGuiID ViewportId; - ImGuiID DockId; - ImGuiWindowClass WindowClass; - ImVec2 MenuBarOffsetMinVal; - ImGuiWindowRefreshFlags RefreshFlagsVal; -}; -typedef ImS64 ImGuiSelectionUserData; -typedef enum { - ImGuiNextItemDataFlags_None = 0, - ImGuiNextItemDataFlags_HasWidth = 1 << 0, - ImGuiNextItemDataFlags_HasOpen = 1 << 1, - ImGuiNextItemDataFlags_HasShortcut = 1 << 2, -} ImGuiNextItemDataFlags_; -struct ImGuiNextItemData { - ImGuiNextItemDataFlags Flags; - ImGuiItemFlags ItemFlags; - ImGuiSelectionUserData SelectionUserData; - float Width; - ImGuiKeyChord Shortcut; - bool OpenVal; - ImGuiCond OpenCond : 8; -}; -struct ImGuiLastItemData { - ImGuiID ID; - ImGuiItemFlags InFlags; - ImGuiItemStatusFlags StatusFlags; - ImRect Rect; - ImRect NavRect; - ImRect DisplayRect; - ImRect ClipRect; -}; -struct ImGuiNavTreeNodeData { - ImGuiID ID; - ImGuiItemFlags InFlags; - ImRect NavRect; -}; -struct ImGuiStackSizes { - short SizeOfIDStack; - short SizeOfColorStack; - short SizeOfStyleVarStack; - short SizeOfFontStack; - short SizeOfFocusScopeStack; - short SizeOfGroupStack; - short SizeOfItemFlagsStack; - short SizeOfBeginPopupStack; - short SizeOfDisabledStack; -}; -typedef struct ImGuiWindowStackData ImGuiWindowStackData; -struct ImGuiWindowStackData { - ImGuiWindow* Window; - ImGuiLastItemData ParentLastItemDataBackup; - ImGuiStackSizes StackSizesOnBegin; -}; -typedef struct ImGuiShrinkWidthItem ImGuiShrinkWidthItem; -struct ImGuiShrinkWidthItem { - int Index; - float Width; - float InitialWidth; -}; -typedef struct ImGuiPtrOrIndex ImGuiPtrOrIndex; -struct ImGuiPtrOrIndex { - void* Ptr; - int Index; -}; -struct ImGuiDataVarInfo { - ImGuiDataType Type; - ImU32 Count; - ImU32 Offset; -}; -typedef struct ImGuiDataTypeTempStorage ImGuiDataTypeTempStorage; -struct ImGuiDataTypeTempStorage { - ImU8 Data[8]; -}; -struct ImGuiDataTypeInfo { - size_t Size; - const char* Name; - const char* PrintFmt; - const char* ScanFmt; -}; -typedef enum { - ImGuiDataType_String = ImGuiDataType_COUNT + 1, - ImGuiDataType_Pointer, - ImGuiDataType_ID, -} ImGuiDataTypePrivate_; -typedef enum { - ImGuiPopupPositionPolicy_Default, - ImGuiPopupPositionPolicy_ComboBox, - ImGuiPopupPositionPolicy_Tooltip, -} ImGuiPopupPositionPolicy; -struct ImGuiPopupData { - ImGuiID PopupId; - ImGuiWindow* Window; - ImGuiWindow* RestoreNavWindow; - int ParentNavLayer; - int OpenFrameCount; - ImGuiID OpenParentId; - ImVec2 OpenPopupPos; - ImVec2 OpenMousePos; -}; -typedef struct ImBitArray_ImGuiKey_NamedKey_COUNT__lessImGuiKey_NamedKey_BEGIN { - ImU32 Storage[(ImGuiKey_NamedKey_COUNT + 31) >> 5]; -} ImBitArray_ImGuiKey_NamedKey_COUNT__lessImGuiKey_NamedKey_BEGIN; - -typedef ImBitArray_ImGuiKey_NamedKey_COUNT__lessImGuiKey_NamedKey_BEGIN - ImBitArrayForNamedKeys; -typedef enum { - ImGuiInputEventType_None = 0, - ImGuiInputEventType_MousePos, - ImGuiInputEventType_MouseWheel, - ImGuiInputEventType_MouseButton, - ImGuiInputEventType_MouseViewport, - ImGuiInputEventType_Key, - ImGuiInputEventType_Text, - ImGuiInputEventType_Focus, - ImGuiInputEventType_COUNT -} ImGuiInputEventType; -typedef enum { - ImGuiInputSource_None = 0, - ImGuiInputSource_Mouse, - ImGuiInputSource_Keyboard, - ImGuiInputSource_Gamepad, - ImGuiInputSource_COUNT -} ImGuiInputSource; -typedef struct ImGuiInputEventMousePos ImGuiInputEventMousePos; -struct ImGuiInputEventMousePos { - float PosX, PosY; - ImGuiMouseSource MouseSource; -}; -typedef struct ImGuiInputEventMouseWheel ImGuiInputEventMouseWheel; -struct ImGuiInputEventMouseWheel { - float WheelX, WheelY; - ImGuiMouseSource MouseSource; -}; -typedef struct ImGuiInputEventMouseButton ImGuiInputEventMouseButton; -struct ImGuiInputEventMouseButton { - int Button; - bool Down; - ImGuiMouseSource MouseSource; -}; -typedef struct ImGuiInputEventMouseViewport ImGuiInputEventMouseViewport; -struct ImGuiInputEventMouseViewport { - ImGuiID HoveredViewportID; -}; -typedef struct ImGuiInputEventKey ImGuiInputEventKey; -struct ImGuiInputEventKey { - ImGuiKey Key; - bool Down; - float AnalogValue; -}; -typedef struct ImGuiInputEventText ImGuiInputEventText; -struct ImGuiInputEventText { - unsigned int Char; -}; -typedef struct ImGuiInputEventAppFocused ImGuiInputEventAppFocused; -struct ImGuiInputEventAppFocused { - bool Focused; -}; -typedef struct ImGuiInputEvent ImGuiInputEvent; -struct ImGuiInputEvent { - ImGuiInputEventType Type; - ImGuiInputSource Source; - ImU32 EventId; - union { - ImGuiInputEventMousePos MousePos; - ImGuiInputEventMouseWheel MouseWheel; - ImGuiInputEventMouseButton MouseButton; - ImGuiInputEventMouseViewport MouseViewport; - ImGuiInputEventKey Key; - ImGuiInputEventText Text; - ImGuiInputEventAppFocused AppFocused; - }; - bool AddedByTestEngine; -}; -typedef ImS16 ImGuiKeyRoutingIndex; -typedef struct ImGuiKeyRoutingData ImGuiKeyRoutingData; -struct ImGuiKeyRoutingData { - ImGuiKeyRoutingIndex NextEntryIndex; - ImU16 Mods; - ImU8 RoutingCurrScore; - ImU8 RoutingNextScore; - ImGuiID RoutingCurr; - ImGuiID RoutingNext; -}; -typedef struct ImGuiKeyRoutingTable ImGuiKeyRoutingTable; -typedef struct ImVector_ImGuiKeyRoutingData { - int Size; - int Capacity; - ImGuiKeyRoutingData* Data; -} ImVector_ImGuiKeyRoutingData; - -struct ImGuiKeyRoutingTable { - ImGuiKeyRoutingIndex Index[ImGuiKey_NamedKey_COUNT]; - ImVector_ImGuiKeyRoutingData Entries; - ImVector_ImGuiKeyRoutingData EntriesNext; -}; -typedef struct ImGuiKeyOwnerData ImGuiKeyOwnerData; -struct ImGuiKeyOwnerData { - ImGuiID OwnerCurr; - ImGuiID OwnerNext; - bool LockThisFrame; - bool LockUntilRelease; -}; -typedef enum { - ImGuiInputFlags_None = 0, - ImGuiInputFlags_Repeat = 1 << 0, - ImGuiInputFlags_RepeatRateDefault = 1 << 1, - ImGuiInputFlags_RepeatRateNavMove = 1 << 2, - ImGuiInputFlags_RepeatRateNavTweak = 1 << 3, - ImGuiInputFlags_RepeatUntilRelease = 1 << 4, - ImGuiInputFlags_RepeatUntilKeyModsChange = 1 << 5, - ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone = 1 << 6, - ImGuiInputFlags_RepeatUntilOtherKeyPress = 1 << 7, - ImGuiInputFlags_CondHovered = 1 << 8, - ImGuiInputFlags_CondActive = 1 << 9, - ImGuiInputFlags_CondDefault_ = - ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, - ImGuiInputFlags_LockThisFrame = 1 << 10, - ImGuiInputFlags_LockUntilRelease = 1 << 11, - ImGuiInputFlags_RouteFocused = 1 << 12, - ImGuiInputFlags_RouteGlobalLow = 1 << 13, - ImGuiInputFlags_RouteGlobal = 1 << 14, - ImGuiInputFlags_RouteGlobalHigh = 1 << 15, - ImGuiInputFlags_RouteAlways = 1 << 16, - ImGuiInputFlags_RouteUnlessBgFocused = 1 << 17, - ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | - ImGuiInputFlags_RepeatRateNavMove | - ImGuiInputFlags_RepeatRateNavTweak, - ImGuiInputFlags_RepeatUntilMask_ = - ImGuiInputFlags_RepeatUntilRelease | - ImGuiInputFlags_RepeatUntilKeyModsChange | - ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone | - ImGuiInputFlags_RepeatUntilOtherKeyPress, - ImGuiInputFlags_RepeatMask_ = ImGuiInputFlags_Repeat | - ImGuiInputFlags_RepeatRateMask_ | - ImGuiInputFlags_RepeatUntilMask_, - ImGuiInputFlags_CondMask_ = - ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, - ImGuiInputFlags_RouteMask_ = - ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | - ImGuiInputFlags_RouteGlobalLow | ImGuiInputFlags_RouteGlobalHigh, - ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_RepeatMask_, - ImGuiInputFlags_SupportedByIsMouseClicked = ImGuiInputFlags_Repeat, - ImGuiInputFlags_SupportedByShortcut = - ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteMask_ | - ImGuiInputFlags_RouteAlways | ImGuiInputFlags_RouteUnlessBgFocused, - ImGuiInputFlags_SupportedBySetKeyOwner = - ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease, - ImGuiInputFlags_SupportedBySetItemKeyOwner = - ImGuiInputFlags_SupportedBySetKeyOwner | ImGuiInputFlags_CondMask_, -} ImGuiInputFlags_; -typedef struct ImGuiListClipperRange ImGuiListClipperRange; -struct ImGuiListClipperRange { - int Min; - int Max; - bool PosToIndexConvert; - ImS8 PosToIndexOffsetMin; - ImS8 PosToIndexOffsetMax; -}; -typedef struct ImGuiListClipperData ImGuiListClipperData; -typedef struct ImVector_ImGuiListClipperRange { - int Size; - int Capacity; - ImGuiListClipperRange* Data; -} ImVector_ImGuiListClipperRange; - -struct ImGuiListClipperData { - ImGuiListClipper* ListClipper; - float LossynessOffset; - int StepNo; - int ItemsFrozen; - ImVector_ImGuiListClipperRange Ranges; -}; -typedef enum { - ImGuiActivateFlags_None = 0, - ImGuiActivateFlags_PreferInput = 1 << 0, - ImGuiActivateFlags_PreferTweak = 1 << 1, - ImGuiActivateFlags_TryToPreserveState = 1 << 2, - ImGuiActivateFlags_FromTabbing = 1 << 3, - ImGuiActivateFlags_FromShortcut = 1 << 4, -} ImGuiActivateFlags_; -typedef enum { - ImGuiScrollFlags_None = 0, - ImGuiScrollFlags_KeepVisibleEdgeX = 1 << 0, - ImGuiScrollFlags_KeepVisibleEdgeY = 1 << 1, - ImGuiScrollFlags_KeepVisibleCenterX = 1 << 2, - ImGuiScrollFlags_KeepVisibleCenterY = 1 << 3, - ImGuiScrollFlags_AlwaysCenterX = 1 << 4, - ImGuiScrollFlags_AlwaysCenterY = 1 << 5, - ImGuiScrollFlags_NoScrollParent = 1 << 6, - ImGuiScrollFlags_MaskX_ = ImGuiScrollFlags_KeepVisibleEdgeX | - ImGuiScrollFlags_KeepVisibleCenterX | - ImGuiScrollFlags_AlwaysCenterX, - ImGuiScrollFlags_MaskY_ = ImGuiScrollFlags_KeepVisibleEdgeY | - ImGuiScrollFlags_KeepVisibleCenterY | - ImGuiScrollFlags_AlwaysCenterY, -} ImGuiScrollFlags_; -typedef enum { - ImGuiNavHighlightFlags_None = 0, - ImGuiNavHighlightFlags_Compact = 1 << 1, - ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, - ImGuiNavHighlightFlags_NoRounding = 1 << 3, -} ImGuiNavHighlightFlags_; -typedef enum { - ImGuiNavMoveFlags_None = 0, - ImGuiNavMoveFlags_LoopX = 1 << 0, - ImGuiNavMoveFlags_LoopY = 1 << 1, - ImGuiNavMoveFlags_WrapX = 1 << 2, - ImGuiNavMoveFlags_WrapY = 1 << 3, - ImGuiNavMoveFlags_WrapMask_ = - ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_LoopY | - ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_WrapY, - ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, - ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, - ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, - ImGuiNavMoveFlags_Forwarded = 1 << 7, - ImGuiNavMoveFlags_DebugNoResult = 1 << 8, - ImGuiNavMoveFlags_FocusApi = 1 << 9, - ImGuiNavMoveFlags_IsTabbing = 1 << 10, - ImGuiNavMoveFlags_IsPageMove = 1 << 11, - ImGuiNavMoveFlags_Activate = 1 << 12, - ImGuiNavMoveFlags_NoSelect = 1 << 13, - ImGuiNavMoveFlags_NoSetNavHighlight = 1 << 14, - ImGuiNavMoveFlags_NoClearActiveId = 1 << 15, -} ImGuiNavMoveFlags_; -typedef enum { - ImGuiNavLayer_Main = 0, - ImGuiNavLayer_Menu = 1, - ImGuiNavLayer_COUNT -} ImGuiNavLayer; -struct ImGuiNavItemData { - ImGuiWindow* Window; - ImGuiID ID; - ImGuiID FocusScopeId; - ImRect RectRel; - ImGuiItemFlags InFlags; - float DistBox; - float DistCenter; - float DistAxial; - ImGuiSelectionUserData SelectionUserData; -}; -typedef struct ImGuiFocusScopeData ImGuiFocusScopeData; -struct ImGuiFocusScopeData { - ImGuiID ID; - ImGuiID WindowID; -}; -typedef enum { - ImGuiTypingSelectFlags_None = 0, - ImGuiTypingSelectFlags_AllowBackspace = 1 << 0, - ImGuiTypingSelectFlags_AllowSingleCharMode = 1 << 1, -} ImGuiTypingSelectFlags_; -struct ImGuiTypingSelectRequest { - ImGuiTypingSelectFlags Flags; - int SearchBufferLen; - const char* SearchBuffer; - bool SelectRequest; - bool SingleCharMode; - ImS8 SingleCharSize; -}; -struct ImGuiTypingSelectState { - ImGuiTypingSelectRequest Request; - char SearchBuffer[64]; - ImGuiID FocusScope; - int LastRequestFrame; - float LastRequestTime; - bool SingleCharModeLock; -}; -typedef enum { - ImGuiOldColumnFlags_None = 0, - ImGuiOldColumnFlags_NoBorder = 1 << 0, - ImGuiOldColumnFlags_NoResize = 1 << 1, - ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, - ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, - ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, -} ImGuiOldColumnFlags_; -struct ImGuiOldColumnData { - float OffsetNorm; - float OffsetNormBeforeResize; - ImGuiOldColumnFlags Flags; - ImRect ClipRect; -}; -typedef struct ImVector_ImGuiOldColumnData { - int Size; - int Capacity; - ImGuiOldColumnData* Data; -} ImVector_ImGuiOldColumnData; - -struct ImGuiOldColumns { - ImGuiID ID; - ImGuiOldColumnFlags Flags; - bool IsFirstFrame; - bool IsBeingResized; - int Current; - int Count; - float OffMinX, OffMaxX; - float LineMinY, LineMaxY; - float HostCursorPosY; - float HostCursorMaxPosX; - ImRect HostInitialClipRect; - ImRect HostBackupClipRect; - ImRect HostBackupParentWorkRect; - ImVector_ImGuiOldColumnData Columns; - ImDrawListSplitter Splitter; -}; -typedef enum { - ImGuiDockNodeFlags_DockSpace = 1 << 10, - ImGuiDockNodeFlags_CentralNode = 1 << 11, - ImGuiDockNodeFlags_NoTabBar = 1 << 12, - ImGuiDockNodeFlags_HiddenTabBar = 1 << 13, - ImGuiDockNodeFlags_NoWindowMenuButton = 1 << 14, - ImGuiDockNodeFlags_NoCloseButton = 1 << 15, - ImGuiDockNodeFlags_NoResizeX = 1 << 16, - ImGuiDockNodeFlags_NoResizeY = 1 << 17, - ImGuiDockNodeFlags_DockedWindowsInFocusRoute = 1 << 18, - ImGuiDockNodeFlags_NoDockingSplitOther = 1 << 19, - ImGuiDockNodeFlags_NoDockingOverMe = 1 << 20, - ImGuiDockNodeFlags_NoDockingOverOther = 1 << 21, - ImGuiDockNodeFlags_NoDockingOverEmpty = 1 << 22, - ImGuiDockNodeFlags_NoDocking = ImGuiDockNodeFlags_NoDockingOverMe | - ImGuiDockNodeFlags_NoDockingOverOther | - ImGuiDockNodeFlags_NoDockingOverEmpty | - ImGuiDockNodeFlags_NoDockingSplit | - ImGuiDockNodeFlags_NoDockingSplitOther, - ImGuiDockNodeFlags_SharedFlagsInheritMask_ = ~0, - ImGuiDockNodeFlags_NoResizeFlagsMask_ = ImGuiDockNodeFlags_NoResize | - ImGuiDockNodeFlags_NoResizeX | - ImGuiDockNodeFlags_NoResizeY, - ImGuiDockNodeFlags_LocalFlagsTransferMask_ = - ImGuiDockNodeFlags_NoDockingSplit | - ImGuiDockNodeFlags_NoResizeFlagsMask_ | - ImGuiDockNodeFlags_AutoHideTabBar | ImGuiDockNodeFlags_CentralNode | - ImGuiDockNodeFlags_NoTabBar | ImGuiDockNodeFlags_HiddenTabBar | - ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton, - ImGuiDockNodeFlags_SavedFlagsMask_ = - ImGuiDockNodeFlags_NoResizeFlagsMask_ | ImGuiDockNodeFlags_DockSpace | - ImGuiDockNodeFlags_CentralNode | ImGuiDockNodeFlags_NoTabBar | - ImGuiDockNodeFlags_HiddenTabBar | ImGuiDockNodeFlags_NoWindowMenuButton | - ImGuiDockNodeFlags_NoCloseButton, -} ImGuiDockNodeFlagsPrivate_; -typedef enum { - ImGuiDataAuthority_Auto, - ImGuiDataAuthority_DockNode, - ImGuiDataAuthority_Window, -} ImGuiDataAuthority_; -typedef enum { - ImGuiDockNodeState_Unknown, - ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow, - ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing, - ImGuiDockNodeState_HostWindowVisible, -} ImGuiDockNodeState; -typedef struct ImVector_ImGuiWindowPtr { - int Size; - int Capacity; - ImGuiWindow** Data; -} ImVector_ImGuiWindowPtr; - -struct ImGuiDockNode { - ImGuiID ID; - ImGuiDockNodeFlags SharedFlags; - ImGuiDockNodeFlags LocalFlags; - ImGuiDockNodeFlags LocalFlagsInWindows; - ImGuiDockNodeFlags MergedFlags; - ImGuiDockNodeState State; - ImGuiDockNode* ParentNode; - ImGuiDockNode* ChildNodes[2]; - ImVector_ImGuiWindowPtr Windows; - ImGuiTabBar* TabBar; - ImVec2 Pos; - ImVec2 Size; - ImVec2 SizeRef; - ImGuiAxis SplitAxis; - ImGuiWindowClass WindowClass; - ImU32 LastBgColor; - ImGuiWindow* HostWindow; - ImGuiWindow* VisibleWindow; - ImGuiDockNode* CentralNode; - ImGuiDockNode* OnlyNodeWithWindows; - int CountNodeWithWindows; - int LastFrameAlive; - int LastFrameActive; - int LastFrameFocused; - ImGuiID LastFocusedNodeId; - ImGuiID SelectedTabId; - ImGuiID WantCloseTabId; - ImGuiID RefViewportId; - ImGuiDataAuthority AuthorityForPos : 3; - ImGuiDataAuthority AuthorityForSize : 3; - ImGuiDataAuthority AuthorityForViewport : 3; - bool IsVisible : 1; - bool IsFocused : 1; - bool IsBgDrawnThisFrame : 1; - bool HasCloseButton : 1; - bool HasWindowMenuButton : 1; - bool HasCentralNodeChild : 1; - bool WantCloseAll : 1; - bool WantLockSizeOnce : 1; - bool WantMouseMove : 1; - bool WantHiddenTabBarUpdate : 1; - bool WantHiddenTabBarToggle : 1; -}; -typedef enum { - ImGuiWindowDockStyleCol_Text, - ImGuiWindowDockStyleCol_Tab, - ImGuiWindowDockStyleCol_TabHovered, - ImGuiWindowDockStyleCol_TabActive, - ImGuiWindowDockStyleCol_TabUnfocused, - ImGuiWindowDockStyleCol_TabUnfocusedActive, - ImGuiWindowDockStyleCol_COUNT -} ImGuiWindowDockStyleCol; -struct ImGuiWindowDockStyle { - ImU32 Colors[ImGuiWindowDockStyleCol_COUNT]; -}; -typedef struct ImVector_ImGuiDockRequest { - int Size; - int Capacity; - ImGuiDockRequest* Data; -} ImVector_ImGuiDockRequest; - -typedef struct ImVector_ImGuiDockNodeSettings { - int Size; - int Capacity; - ImGuiDockNodeSettings* Data; -} ImVector_ImGuiDockNodeSettings; - -struct ImGuiDockContext { - ImGuiStorage Nodes; - ImVector_ImGuiDockRequest Requests; - ImVector_ImGuiDockNodeSettings NodesSettings; - bool WantFullRebuild; -}; -typedef struct ImGuiViewportP ImGuiViewportP; -struct ImGuiViewportP { - ImGuiViewport _ImGuiViewport; - ImGuiWindow* Window; - int Idx; - int LastFrameActive; - int LastFocusedStampCount; - ImGuiID LastNameHash; - ImVec2 LastPos; - float Alpha; - float LastAlpha; - bool LastFocusedHadNavWindow; - short PlatformMonitor; - int BgFgDrawListsLastFrame[2]; - ImDrawList* BgFgDrawLists[2]; - ImDrawData DrawDataP; - ImDrawDataBuilder DrawDataBuilder; - ImVec2 LastPlatformPos; - ImVec2 LastPlatformSize; - ImVec2 LastRendererSize; - ImVec2 WorkOffsetMin; - ImVec2 WorkOffsetMax; - ImVec2 BuildWorkOffsetMin; - ImVec2 BuildWorkOffsetMax; -}; -struct ImGuiWindowSettings { - ImGuiID ID; - ImVec2ih Pos; - ImVec2ih Size; - ImVec2ih ViewportPos; - ImGuiID ViewportId; - ImGuiID DockId; - ImGuiID ClassId; - short DockOrder; - bool Collapsed; - bool IsChild; - bool WantApply; - bool WantDelete; -}; -struct ImGuiSettingsHandler { - const char* TypeName; - ImGuiID TypeHash; - void (*ClearAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); - void (*ReadInitFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); - void* (*ReadOpenFn)(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - const char* name); - void (*ReadLineFn)(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - void* entry, - const char* line); - void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); - void (*WriteAllFn)(ImGuiContext* ctx, - ImGuiSettingsHandler* handler, - ImGuiTextBuffer* out_buf); - void* UserData; -}; -typedef enum { - ImGuiLocKey_VersionStr = 0, - ImGuiLocKey_TableSizeOne = 1, - ImGuiLocKey_TableSizeAllFit = 2, - ImGuiLocKey_TableSizeAllDefault = 3, - ImGuiLocKey_TableResetOrder = 4, - ImGuiLocKey_WindowingMainMenuBar = 5, - ImGuiLocKey_WindowingPopup = 6, - ImGuiLocKey_WindowingUntitled = 7, - ImGuiLocKey_DockingHideTabBar = 8, - ImGuiLocKey_DockingHoldShiftToDock = 9, - ImGuiLocKey_DockingDragToUndockOrMoveNode = 10, - ImGuiLocKey_COUNT = 11, -} ImGuiLocKey; -struct ImGuiLocEntry { - ImGuiLocKey Key; - const char* Text; -}; -typedef enum { - ImGuiDebugLogFlags_None = 0, - ImGuiDebugLogFlags_EventActiveId = 1 << 0, - ImGuiDebugLogFlags_EventFocus = 1 << 1, - ImGuiDebugLogFlags_EventPopup = 1 << 2, - ImGuiDebugLogFlags_EventNav = 1 << 3, - ImGuiDebugLogFlags_EventClipper = 1 << 4, - ImGuiDebugLogFlags_EventSelection = 1 << 5, - ImGuiDebugLogFlags_EventIO = 1 << 6, - ImGuiDebugLogFlags_EventInputRouting = 1 << 7, - ImGuiDebugLogFlags_EventDocking = 1 << 8, - ImGuiDebugLogFlags_EventViewport = 1 << 9, - ImGuiDebugLogFlags_EventMask_ = - ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | - ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | - ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | - ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventInputRouting | - ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, - ImGuiDebugLogFlags_OutputToTTY = 1 << 20, - ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, -} ImGuiDebugLogFlags_; -typedef struct ImGuiDebugAllocEntry ImGuiDebugAllocEntry; -struct ImGuiDebugAllocEntry { - int FrameCount; - ImS16 AllocCount; - ImS16 FreeCount; -}; -typedef struct ImGuiDebugAllocInfo ImGuiDebugAllocInfo; -struct ImGuiDebugAllocInfo { - int TotalAllocCount; - int TotalFreeCount; - ImS16 LastEntriesIdx; - ImGuiDebugAllocEntry LastEntriesBuf[6]; -}; -struct ImGuiMetricsConfig { - bool ShowDebugLog; - bool ShowIDStackTool; - bool ShowWindowsRects; - bool ShowWindowsBeginOrder; - bool ShowTablesRects; - bool ShowDrawCmdMesh; - bool ShowDrawCmdBoundingBoxes; - bool ShowTextEncodingViewer; - bool ShowAtlasTintedWithTextColor; - bool ShowDockingNodes; - int ShowWindowsRectsType; - int ShowTablesRectsType; - int HighlightMonitorIdx; - ImGuiID HighlightViewportID; -}; -typedef struct ImGuiStackLevelInfo ImGuiStackLevelInfo; -struct ImGuiStackLevelInfo { - ImGuiID ID; - ImS8 QueryFrameCount; - bool QuerySuccess; - ImGuiDataType DataType : 8; - char Desc[57]; -}; -typedef struct ImGuiIDStackTool ImGuiIDStackTool; -typedef struct ImVector_ImGuiStackLevelInfo { - int Size; - int Capacity; - ImGuiStackLevelInfo* Data; -} ImVector_ImGuiStackLevelInfo; - -struct ImGuiIDStackTool { - int LastActiveFrame; - int StackLevel; - ImGuiID QueryId; - ImVector_ImGuiStackLevelInfo Results; - bool CopyToClipboardOnCtrlC; - float CopyToClipboardLastTime; -}; -typedef void (*ImGuiContextHookCallback)(ImGuiContext* ctx, - ImGuiContextHook* hook); -typedef enum { - ImGuiContextHookType_NewFramePre, - ImGuiContextHookType_NewFramePost, - ImGuiContextHookType_EndFramePre, - ImGuiContextHookType_EndFramePost, - ImGuiContextHookType_RenderPre, - ImGuiContextHookType_RenderPost, - ImGuiContextHookType_Shutdown, - ImGuiContextHookType_PendingRemoval_ -} ImGuiContextHookType; -struct ImGuiContextHook { - ImGuiID HookId; - ImGuiContextHookType Type; - ImGuiID Owner; - ImGuiContextHookCallback Callback; - void* UserData; -}; -typedef struct ImVector_ImGuiInputEvent { - int Size; - int Capacity; - ImGuiInputEvent* Data; -} ImVector_ImGuiInputEvent; - -typedef struct ImVector_ImGuiWindowStackData { - int Size; - int Capacity; - ImGuiWindowStackData* Data; -} ImVector_ImGuiWindowStackData; - -typedef struct ImVector_ImGuiColorMod { - int Size; - int Capacity; - ImGuiColorMod* Data; -} ImVector_ImGuiColorMod; - -typedef struct ImVector_ImGuiStyleMod { - int Size; - int Capacity; - ImGuiStyleMod* Data; -} ImVector_ImGuiStyleMod; - -typedef struct ImVector_ImGuiFocusScopeData { - int Size; - int Capacity; - ImGuiFocusScopeData* Data; -} ImVector_ImGuiFocusScopeData; - -typedef struct ImVector_ImGuiItemFlags { - int Size; - int Capacity; - ImGuiItemFlags* Data; -} ImVector_ImGuiItemFlags; - -typedef struct ImVector_ImGuiGroupData { - int Size; - int Capacity; - ImGuiGroupData* Data; -} ImVector_ImGuiGroupData; - -typedef struct ImVector_ImGuiPopupData { - int Size; - int Capacity; - ImGuiPopupData* Data; -} ImVector_ImGuiPopupData; - -typedef struct ImVector_ImGuiNavTreeNodeData { - int Size; - int Capacity; - ImGuiNavTreeNodeData* Data; -} ImVector_ImGuiNavTreeNodeData; - -typedef struct ImVector_ImGuiViewportPPtr { - int Size; - int Capacity; - ImGuiViewportP** Data; -} ImVector_ImGuiViewportPPtr; - -typedef struct ImVector_unsigned_char { - int Size; - int Capacity; - unsigned char* Data; -} ImVector_unsigned_char; - -typedef struct ImVector_ImGuiListClipperData { - int Size; - int Capacity; - ImGuiListClipperData* Data; -} ImVector_ImGuiListClipperData; - -typedef struct ImVector_ImGuiTableTempData { - int Size; - int Capacity; - ImGuiTableTempData* Data; -} ImVector_ImGuiTableTempData; - -typedef struct ImVector_ImGuiTable { - int Size; - int Capacity; - ImGuiTable* Data; -} ImVector_ImGuiTable; - -typedef struct ImPool_ImGuiTable { - ImVector_ImGuiTable Buf; - ImGuiStorage Map; - ImPoolIdx FreeIdx; - ImPoolIdx AliveCount; -} ImPool_ImGuiTable; - -typedef struct ImVector_ImGuiTabBar { - int Size; - int Capacity; - ImGuiTabBar* Data; -} ImVector_ImGuiTabBar; - -typedef struct ImPool_ImGuiTabBar { - ImVector_ImGuiTabBar Buf; - ImGuiStorage Map; - ImPoolIdx FreeIdx; - ImPoolIdx AliveCount; -} ImPool_ImGuiTabBar; - -typedef struct ImVector_ImGuiPtrOrIndex { - int Size; - int Capacity; - ImGuiPtrOrIndex* Data; -} ImVector_ImGuiPtrOrIndex; - -typedef struct ImVector_ImGuiShrinkWidthItem { - int Size; - int Capacity; - ImGuiShrinkWidthItem* Data; -} ImVector_ImGuiShrinkWidthItem; - -typedef struct ImVector_ImGuiID { - int Size; - int Capacity; - ImGuiID* Data; -} ImVector_ImGuiID; - -typedef struct ImVector_ImGuiSettingsHandler { - int Size; - int Capacity; - ImGuiSettingsHandler* Data; -} ImVector_ImGuiSettingsHandler; - -typedef struct ImChunkStream_ImGuiWindowSettings { - ImVector_char Buf; -} ImChunkStream_ImGuiWindowSettings; - -typedef struct ImChunkStream_ImGuiTableSettings { - ImVector_char Buf; -} ImChunkStream_ImGuiTableSettings; - -typedef struct ImVector_ImGuiContextHook { - int Size; - int Capacity; - ImGuiContextHook* Data; -} ImVector_ImGuiContextHook; - -struct ImGuiContext { - bool Initialized; - bool FontAtlasOwnedByContext; - ImGuiIO IO; - ImGuiPlatformIO PlatformIO; - ImGuiStyle Style; - ImGuiConfigFlags ConfigFlagsCurrFrame; - ImGuiConfigFlags ConfigFlagsLastFrame; - ImFont* Font; - float FontSize; - float FontBaseSize; - ImDrawListSharedData DrawListSharedData; - double Time; - int FrameCount; - int FrameCountEnded; - int FrameCountPlatformEnded; - int FrameCountRendered; - bool WithinFrameScope; - bool WithinFrameScopeWithImplicitWindow; - bool WithinEndChild; - bool GcCompactAll; - bool TestEngineHookItems; - void* TestEngine; - ImVector_ImGuiInputEvent InputEventsQueue; - ImVector_ImGuiInputEvent InputEventsTrail; - ImGuiMouseSource InputEventsNextMouseSource; - ImU32 InputEventsNextEventId; - ImVector_ImGuiWindowPtr Windows; - ImVector_ImGuiWindowPtr WindowsFocusOrder; - ImVector_ImGuiWindowPtr WindowsTempSortBuffer; - ImVector_ImGuiWindowStackData CurrentWindowStack; - ImGuiStorage WindowsById; - int WindowsActiveCount; - ImVec2 WindowsHoverPadding; - ImGuiID DebugBreakInWindow; - ImGuiWindow* CurrentWindow; - ImGuiWindow* HoveredWindow; - ImGuiWindow* HoveredWindowUnderMovingWindow; - ImGuiWindow* MovingWindow; - ImGuiWindow* WheelingWindow; - ImVec2 WheelingWindowRefMousePos; - int WheelingWindowStartFrame; - int WheelingWindowScrolledFrame; - float WheelingWindowReleaseTimer; - ImVec2 WheelingWindowWheelRemainder; - ImVec2 WheelingAxisAvg; - ImGuiID DebugHookIdInfo; - ImGuiID HoveredId; - ImGuiID HoveredIdPreviousFrame; - bool HoveredIdAllowOverlap; - bool HoveredIdDisabled; - float HoveredIdTimer; - float HoveredIdNotActiveTimer; - ImGuiID ActiveId; - ImGuiID ActiveIdIsAlive; - float ActiveIdTimer; - bool ActiveIdIsJustActivated; - bool ActiveIdAllowOverlap; - bool ActiveIdNoClearOnFocusLoss; - bool ActiveIdHasBeenPressedBefore; - bool ActiveIdHasBeenEditedBefore; - bool ActiveIdHasBeenEditedThisFrame; - bool ActiveIdFromShortcut; - int ActiveIdMouseButton : 8; - ImVec2 ActiveIdClickOffset; - ImGuiWindow* ActiveIdWindow; - ImGuiInputSource ActiveIdSource; - ImGuiID ActiveIdPreviousFrame; - bool ActiveIdPreviousFrameIsAlive; - bool ActiveIdPreviousFrameHasBeenEditedBefore; - ImGuiWindow* ActiveIdPreviousFrameWindow; - ImGuiID LastActiveId; - float LastActiveIdTimer; - double LastKeyModsChangeTime; - double LastKeyModsChangeFromNoneTime; - double LastKeyboardKeyPressTime; - ImBitArrayForNamedKeys KeysMayBeCharInput; - ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; - ImGuiKeyRoutingTable KeysRoutingTable; - ImU32 ActiveIdUsingNavDirMask; - bool ActiveIdUsingAllKeyboardKeys; - ImGuiKeyChord DebugBreakInShortcutRouting; - ImGuiID CurrentFocusScopeId; - ImGuiItemFlags CurrentItemFlags; - ImGuiID DebugLocateId; - ImGuiNextItemData NextItemData; - ImGuiLastItemData LastItemData; - ImGuiNextWindowData NextWindowData; - bool DebugShowGroupRects; - ImGuiCol DebugFlashStyleColorIdx; - ImVector_ImGuiColorMod ColorStack; - ImVector_ImGuiStyleMod StyleVarStack; - ImVector_ImFontPtr FontStack; - ImVector_ImGuiFocusScopeData FocusScopeStack; - ImVector_ImGuiItemFlags ItemFlagsStack; - ImVector_ImGuiGroupData GroupStack; - ImVector_ImGuiPopupData OpenPopupStack; - ImVector_ImGuiPopupData BeginPopupStack; - ImVector_ImGuiNavTreeNodeData NavTreeNodeStack; - ImVector_ImGuiViewportPPtr Viewports; - float CurrentDpiScale; - ImGuiViewportP* CurrentViewport; - ImGuiViewportP* MouseViewport; - ImGuiViewportP* MouseLastHoveredViewport; - ImGuiID PlatformLastFocusedViewportId; - ImGuiPlatformMonitor FallbackMonitor; - ImRect PlatformMonitorsFullWorkRect; - int ViewportCreatedCount; - int PlatformWindowsCreatedCount; - int ViewportFocusedStampCount; - ImGuiWindow* NavWindow; - ImGuiID NavId; - ImGuiID NavFocusScopeId; - ImVector_ImGuiFocusScopeData NavFocusRoute; - ImGuiID NavActivateId; - ImGuiID NavActivateDownId; - ImGuiID NavActivatePressedId; - ImGuiActivateFlags NavActivateFlags; - ImGuiID NavHighlightActivatedId; - float NavHighlightActivatedTimer; - ImGuiID NavJustMovedToId; - ImGuiID NavJustMovedToFocusScopeId; - ImGuiKeyChord NavJustMovedToKeyMods; - ImGuiID NavNextActivateId; - ImGuiActivateFlags NavNextActivateFlags; - ImGuiInputSource NavInputSource; - ImGuiNavLayer NavLayer; - ImGuiSelectionUserData NavLastValidSelectionUserData; - bool NavIdIsAlive; - bool NavMousePosDirty; - bool NavDisableHighlight; - bool NavDisableMouseHover; - bool NavAnyRequest; - bool NavInitRequest; - bool NavInitRequestFromMove; - ImGuiNavItemData NavInitResult; - bool NavMoveSubmitted; - bool NavMoveScoringItems; - bool NavMoveForwardToNextFrame; - ImGuiNavMoveFlags NavMoveFlags; - ImGuiScrollFlags NavMoveScrollFlags; - ImGuiKeyChord NavMoveKeyMods; - ImGuiDir NavMoveDir; - ImGuiDir NavMoveDirForDebug; - ImGuiDir NavMoveClipDir; - ImRect NavScoringRect; - ImRect NavScoringNoClipRect; - int NavScoringDebugCount; - int NavTabbingDir; - int NavTabbingCounter; - ImGuiNavItemData NavMoveResultLocal; - ImGuiNavItemData NavMoveResultLocalVisible; - ImGuiNavItemData NavMoveResultOther; - ImGuiNavItemData NavTabbingResultFirst; - ImGuiKeyChord ConfigNavWindowingKeyNext; - ImGuiKeyChord ConfigNavWindowingKeyPrev; - ImGuiWindow* NavWindowingTarget; - ImGuiWindow* NavWindowingTargetAnim; - ImGuiWindow* NavWindowingListWindow; - float NavWindowingTimer; - float NavWindowingHighlightAlpha; - bool NavWindowingToggleLayer; - ImGuiKey NavWindowingToggleKey; - ImVec2 NavWindowingAccumDeltaPos; - ImVec2 NavWindowingAccumDeltaSize; - float DimBgRatio; - bool DragDropActive; - bool DragDropWithinSource; - bool DragDropWithinTarget; - ImGuiDragDropFlags DragDropSourceFlags; - int DragDropSourceFrameCount; - int DragDropMouseButton; - ImGuiPayload DragDropPayload; - ImRect DragDropTargetRect; - ImRect DragDropTargetClipRect; - ImGuiID DragDropTargetId; - ImGuiDragDropFlags DragDropAcceptFlags; - float DragDropAcceptIdCurrRectSurface; - ImGuiID DragDropAcceptIdCurr; - ImGuiID DragDropAcceptIdPrev; - int DragDropAcceptFrameCount; - ImGuiID DragDropHoldJustPressedId; - ImVector_unsigned_char DragDropPayloadBufHeap; - unsigned char DragDropPayloadBufLocal[16]; - int ClipperTempDataStacked; - ImVector_ImGuiListClipperData ClipperTempData; - ImGuiTable* CurrentTable; - ImGuiID DebugBreakInTable; - int TablesTempDataStacked; - ImVector_ImGuiTableTempData TablesTempData; - ImPool_ImGuiTable Tables; - ImVector_float TablesLastTimeActive; - ImVector_ImDrawChannel DrawChannelsTempMergeBuffer; - ImGuiTabBar* CurrentTabBar; - ImPool_ImGuiTabBar TabBars; - ImVector_ImGuiPtrOrIndex CurrentTabBarStack; - ImVector_ImGuiShrinkWidthItem ShrinkWidthBuffer; - ImGuiID HoverItemDelayId; - ImGuiID HoverItemDelayIdPreviousFrame; - float HoverItemDelayTimer; - float HoverItemDelayClearTimer; - ImGuiID HoverItemUnlockedStationaryId; - ImGuiID HoverWindowUnlockedStationaryId; - ImGuiMouseCursor MouseCursor; - float MouseStationaryTimer; - ImVec2 MouseLastValidPos; - ImGuiInputTextState InputTextState; - ImGuiInputTextDeactivatedState InputTextDeactivatedState; - ImFont InputTextPasswordFont; - ImGuiID TempInputId; - int BeginMenuDepth; - int BeginComboDepth; - ImGuiColorEditFlags ColorEditOptions; - ImGuiID ColorEditCurrentID; - ImGuiID ColorEditSavedID; - float ColorEditSavedHue; - float ColorEditSavedSat; - ImU32 ColorEditSavedColor; - ImVec4 ColorPickerRef; - ImGuiComboPreviewData ComboPreviewData; - ImRect WindowResizeBorderExpectedRect; - bool WindowResizeRelativeMode; - float SliderGrabClickOffset; - float SliderCurrentAccum; - bool SliderCurrentAccumDirty; - bool DragCurrentAccumDirty; - float DragCurrentAccum; - float DragSpeedDefaultRatio; - float ScrollbarClickDeltaToGrabCenter; - float DisabledAlphaBackup; - short DisabledStackSize; - short LockMarkEdited; - short TooltipOverrideCount; - ImVector_char ClipboardHandlerData; - ImVector_ImGuiID MenusIdSubmittedThisFrame; - ImGuiTypingSelectState TypingSelectState; - ImGuiPlatformImeData PlatformImeData; - ImGuiPlatformImeData PlatformImeDataPrev; - ImGuiID PlatformImeViewport; - ImGuiDockContext DockContext; - void (*DockNodeWindowMenuHandler)(ImGuiContext* ctx, - ImGuiDockNode* node, - ImGuiTabBar* tab_bar); - bool SettingsLoaded; - float SettingsDirtyTimer; - ImGuiTextBuffer SettingsIniData; - ImVector_ImGuiSettingsHandler SettingsHandlers; - ImChunkStream_ImGuiWindowSettings SettingsWindows; - ImChunkStream_ImGuiTableSettings SettingsTables; - ImVector_ImGuiContextHook Hooks; - ImGuiID HookIdNext; - const char* LocalizationTable[ImGuiLocKey_COUNT]; - bool LogEnabled; - ImGuiLogType LogType; - ImFileHandle LogFile; - ImGuiTextBuffer LogBuffer; - const char* LogNextPrefix; - const char* LogNextSuffix; - float LogLinePosY; - bool LogLineFirstItem; - int LogDepthRef; - int LogDepthToExpand; - int LogDepthToExpandDefault; - ImGuiDebugLogFlags DebugLogFlags; - ImGuiTextBuffer DebugLogBuf; - ImGuiTextIndex DebugLogIndex; - ImGuiDebugLogFlags DebugLogAutoDisableFlags; - ImU8 DebugLogAutoDisableFrames; - ImU8 DebugLocateFrames; - bool DebugBreakInLocateId; - ImGuiKeyChord DebugBreakKeyChord; - ImS8 DebugBeginReturnValueCullDepth; - bool DebugItemPickerActive; - ImU8 DebugItemPickerMouseButton; - ImGuiID DebugItemPickerBreakId; - float DebugFlashStyleColorTime; - ImVec4 DebugFlashStyleColorBackup; - ImGuiMetricsConfig DebugMetricsConfig; - ImGuiIDStackTool DebugIDStackTool; - ImGuiDebugAllocInfo DebugAllocInfo; - ImGuiDockNode* DebugHoveredDockNode; - float FramerateSecPerFrame[60]; - int FramerateSecPerFrameIdx; - int FramerateSecPerFrameCount; - float FramerateSecPerFrameAccum; - int WantCaptureMouseNextFrame; - int WantCaptureKeyboardNextFrame; - int WantTextInputNextFrame; - ImVector_char TempBuffer; - char TempKeychordName[64]; -}; -struct ImGuiWindowTempData { - ImVec2 CursorPos; - ImVec2 CursorPosPrevLine; - ImVec2 CursorStartPos; - ImVec2 CursorMaxPos; - ImVec2 IdealMaxPos; - ImVec2 CurrLineSize; - ImVec2 PrevLineSize; - float CurrLineTextBaseOffset; - float PrevLineTextBaseOffset; - bool IsSameLine; - bool IsSetPos; - ImVec1 Indent; - ImVec1 ColumnsOffset; - ImVec1 GroupOffset; - ImVec2 CursorStartPosLossyness; - ImGuiNavLayer NavLayerCurrent; - short NavLayersActiveMask; - short NavLayersActiveMaskNext; - bool NavIsScrollPushableX; - bool NavHideHighlightOneFrame; - bool NavWindowHasScrollY; - bool MenuBarAppending; - ImVec2 MenuBarOffset; - ImGuiMenuColumns MenuColumns; - int TreeDepth; - ImU32 TreeJumpToParentOnPopMask; - ImVector_ImGuiWindowPtr ChildWindows; - ImGuiStorage* StateStorage; - ImGuiOldColumns* CurrentColumns; - int CurrentTableIdx; - ImGuiLayoutType LayoutType; - ImGuiLayoutType ParentLayoutType; - ImU32 ModalDimBgColor; - float ItemWidth; - float TextWrapPos; - ImVector_float ItemWidthStack; - ImVector_float TextWrapPosStack; -}; -typedef struct ImVector_ImGuiOldColumns { - int Size; - int Capacity; - ImGuiOldColumns* Data; -} ImVector_ImGuiOldColumns; - -struct ImGuiWindow { - ImGuiContext* Ctx; - char* Name; - ImGuiID ID; - ImGuiWindowFlags Flags, FlagsPreviousFrame; - ImGuiChildFlags ChildFlags; - ImGuiWindowClass WindowClass; - ImGuiViewportP* Viewport; - ImGuiID ViewportId; - ImVec2 ViewportPos; - int ViewportAllowPlatformMonitorExtend; - ImVec2 Pos; - ImVec2 Size; - ImVec2 SizeFull; - ImVec2 ContentSize; - ImVec2 ContentSizeIdeal; - ImVec2 ContentSizeExplicit; - ImVec2 WindowPadding; - float WindowRounding; - float WindowBorderSize; - float DecoOuterSizeX1, DecoOuterSizeY1; - float DecoOuterSizeX2, DecoOuterSizeY2; - float DecoInnerSizeX1, DecoInnerSizeY1; - int NameBufLen; - ImGuiID MoveId; - ImGuiID TabId; - ImGuiID ChildId; - ImVec2 Scroll; - ImVec2 ScrollMax; - ImVec2 ScrollTarget; - ImVec2 ScrollTargetCenterRatio; - ImVec2 ScrollTargetEdgeSnapDist; - ImVec2 ScrollbarSizes; - bool ScrollbarX, ScrollbarY; - bool ViewportOwned; - bool Active; - bool WasActive; - bool WriteAccessed; - bool Collapsed; - bool WantCollapseToggle; - bool SkipItems; - bool SkipRefresh; - bool Appearing; - bool Hidden; - bool IsFallbackWindow; - bool IsExplicitChild; - bool HasCloseButton; - signed char ResizeBorderHovered; - signed char ResizeBorderHeld; - short BeginCount; - short BeginCountPreviousFrame; - short BeginOrderWithinParent; - short BeginOrderWithinContext; - short FocusOrder; - ImGuiID PopupId; - ImS8 AutoFitFramesX, AutoFitFramesY; - bool AutoFitOnlyGrows; - ImGuiDir AutoPosLastDirection; - ImS8 HiddenFramesCanSkipItems; - ImS8 HiddenFramesCannotSkipItems; - ImS8 HiddenFramesForRenderOnly; - ImS8 DisableInputsFrames; - ImGuiCond SetWindowPosAllowFlags : 8; - ImGuiCond SetWindowSizeAllowFlags : 8; - ImGuiCond SetWindowCollapsedAllowFlags : 8; - ImGuiCond SetWindowDockAllowFlags : 8; - ImVec2 SetWindowPosVal; - ImVec2 SetWindowPosPivot; - ImVector_ImGuiID IDStack; - ImGuiWindowTempData DC; - ImRect OuterRectClipped; - ImRect InnerRect; - ImRect InnerClipRect; - ImRect WorkRect; - ImRect ParentWorkRect; - ImRect ClipRect; - ImRect ContentRegionRect; - ImVec2ih HitTestHoleSize; - ImVec2ih HitTestHoleOffset; - int LastFrameActive; - int LastFrameJustFocused; - float LastTimeActive; - float ItemWidthDefault; - ImGuiStorage StateStorage; - ImVector_ImGuiOldColumns ColumnsStorage; - float FontWindowScale; - float FontDpiScale; - int SettingsOffset; - ImDrawList* DrawList; - ImDrawList DrawListInst; - ImGuiWindow* ParentWindow; - ImGuiWindow* ParentWindowInBeginStack; - ImGuiWindow* RootWindow; - ImGuiWindow* RootWindowPopupTree; - ImGuiWindow* RootWindowDockTree; - ImGuiWindow* RootWindowForTitleBarHighlight; - ImGuiWindow* RootWindowForNav; - ImGuiWindow* ParentWindowForFocusRoute; - ImGuiWindow* NavLastChildNavWindow; - ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; - ImRect NavRectRel[ImGuiNavLayer_COUNT]; - ImVec2 NavPreferredScoringPosRel[ImGuiNavLayer_COUNT]; - ImGuiID NavRootFocusScopeId; - int MemoryDrawListIdxCapacity; - int MemoryDrawListVtxCapacity; - bool MemoryCompacted; - bool DockIsActive : 1; - bool DockNodeIsVisible : 1; - bool DockTabIsVisible : 1; - bool DockTabWantClose : 1; - short DockOrder; - ImGuiWindowDockStyle DockStyle; - ImGuiDockNode* DockNode; - ImGuiDockNode* DockNodeAsHost; - ImGuiID DockId; - ImGuiItemStatusFlags DockTabItemStatusFlags; - ImRect DockTabItemRect; -}; -typedef enum { - ImGuiTabBarFlags_DockNode = 1 << 20, - ImGuiTabBarFlags_IsFocused = 1 << 21, - ImGuiTabBarFlags_SaveSettings = 1 << 22, -} ImGuiTabBarFlagsPrivate_; -typedef enum { - ImGuiTabItemFlags_SectionMask_ = - ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, - ImGuiTabItemFlags_NoCloseButton = 1 << 20, - ImGuiTabItemFlags_Button = 1 << 21, - ImGuiTabItemFlags_Unsorted = 1 << 22, -} ImGuiTabItemFlagsPrivate_; -struct ImGuiTabItem { - ImGuiID ID; - ImGuiTabItemFlags Flags; - ImGuiWindow* Window; - int LastFrameVisible; - int LastFrameSelected; - float Offset; - float Width; - float ContentWidth; - float RequestedWidth; - ImS32 NameOffset; - ImS16 BeginOrder; - ImS16 IndexDuringLayout; - bool WantClose; -}; -typedef struct ImVector_ImGuiTabItem { - int Size; - int Capacity; - ImGuiTabItem* Data; -} ImVector_ImGuiTabItem; - -struct ImGuiTabBar { - ImVector_ImGuiTabItem Tabs; - ImGuiTabBarFlags Flags; - ImGuiID ID; - ImGuiID SelectedTabId; - ImGuiID NextSelectedTabId; - ImGuiID VisibleTabId; - int CurrFrameVisible; - int PrevFrameVisible; - ImRect BarRect; - float CurrTabsContentsHeight; - float PrevTabsContentsHeight; - float WidthAllTabs; - float WidthAllTabsIdeal; - float ScrollingAnim; - float ScrollingTarget; - float ScrollingTargetDistToVisibility; - float ScrollingSpeed; - float ScrollingRectMinX; - float ScrollingRectMaxX; - float SeparatorMinX; - float SeparatorMaxX; - ImGuiID ReorderRequestTabId; - ImS16 ReorderRequestOffset; - ImS8 BeginCount; - bool WantLayout; - bool VisibleTabWasSubmitted; - bool TabsAddedNew; - ImS16 TabsActiveCount; - ImS16 LastTabItemIdx; - float ItemSpacingY; - ImVec2 FramePadding; - ImVec2 BackupCursorPos; - ImGuiTextBuffer TabsNames; -}; -typedef ImS16 ImGuiTableColumnIdx; -typedef ImU16 ImGuiTableDrawChannelIdx; -struct ImGuiTableColumn { - ImGuiTableColumnFlags Flags; - float WidthGiven; - float MinX; - float MaxX; - float WidthRequest; - float WidthAuto; - float StretchWeight; - float InitStretchWeightOrWidth; - ImRect ClipRect; - ImGuiID UserID; - float WorkMinX; - float WorkMaxX; - float ItemWidth; - float ContentMaxXFrozen; - float ContentMaxXUnfrozen; - float ContentMaxXHeadersUsed; - float ContentMaxXHeadersIdeal; - ImS16 NameOffset; - ImGuiTableColumnIdx DisplayOrder; - ImGuiTableColumnIdx IndexWithinEnabledSet; - ImGuiTableColumnIdx PrevEnabledColumn; - ImGuiTableColumnIdx NextEnabledColumn; - ImGuiTableColumnIdx SortOrder; - ImGuiTableDrawChannelIdx DrawChannelCurrent; - ImGuiTableDrawChannelIdx DrawChannelFrozen; - ImGuiTableDrawChannelIdx DrawChannelUnfrozen; - bool IsEnabled; - bool IsUserEnabled; - bool IsUserEnabledNextFrame; - bool IsVisibleX; - bool IsVisibleY; - bool IsRequestOutput; - bool IsSkipItems; - bool IsPreserveWidthAuto; - ImS8 NavLayerCurrent; - ImU8 AutoFitQueue; - ImU8 CannotSkipItemsQueue; - ImU8 SortDirection : 2; - ImU8 SortDirectionsAvailCount : 2; - ImU8 SortDirectionsAvailMask : 4; - ImU8 SortDirectionsAvailList; -}; -typedef struct ImGuiTableCellData ImGuiTableCellData; -struct ImGuiTableCellData { - ImU32 BgColor; - ImGuiTableColumnIdx Column; -}; -struct ImGuiTableHeaderData { - ImGuiTableColumnIdx Index; - ImU32 TextColor; - ImU32 BgColor0; - ImU32 BgColor1; -}; -struct ImGuiTableInstanceData { - ImGuiID TableInstanceID; - float LastOuterHeight; - float LastTopHeadersRowHeight; - float LastFrozenHeight; - int HoveredRowLast; - int HoveredRowNext; -}; -typedef struct ImSpan_ImGuiTableColumn { - ImGuiTableColumn* Data; - ImGuiTableColumn* DataEnd; -} ImSpan_ImGuiTableColumn; - -typedef struct ImSpan_ImGuiTableColumnIdx { - ImGuiTableColumnIdx* Data; - ImGuiTableColumnIdx* DataEnd; -} ImSpan_ImGuiTableColumnIdx; - -typedef struct ImSpan_ImGuiTableCellData { - ImGuiTableCellData* Data; - ImGuiTableCellData* DataEnd; -} ImSpan_ImGuiTableCellData; - -typedef struct ImVector_ImGuiTableInstanceData { - int Size; - int Capacity; - ImGuiTableInstanceData* Data; -} ImVector_ImGuiTableInstanceData; - -typedef struct ImVector_ImGuiTableColumnSortSpecs { - int Size; - int Capacity; - ImGuiTableColumnSortSpecs* Data; -} ImVector_ImGuiTableColumnSortSpecs; - -struct ImGuiTable { - ImGuiID ID; - ImGuiTableFlags Flags; - void* RawData; - ImGuiTableTempData* TempData; - ImSpan_ImGuiTableColumn Columns; - ImSpan_ImGuiTableColumnIdx DisplayOrderToIndex; - ImSpan_ImGuiTableCellData RowCellData; - ImBitArrayPtr EnabledMaskByDisplayOrder; - ImBitArrayPtr EnabledMaskByIndex; - ImBitArrayPtr VisibleMaskByIndex; - ImGuiTableFlags SettingsLoadedFlags; - int SettingsOffset; - int LastFrameActive; - int ColumnsCount; - int CurrentRow; - int CurrentColumn; - ImS16 InstanceCurrent; - ImS16 InstanceInteracted; - float RowPosY1; - float RowPosY2; - float RowMinHeight; - float RowCellPaddingY; - float RowTextBaseline; - float RowIndentOffsetX; - ImGuiTableRowFlags RowFlags : 16; - ImGuiTableRowFlags LastRowFlags : 16; - int RowBgColorCounter; - ImU32 RowBgColor[2]; - ImU32 BorderColorStrong; - ImU32 BorderColorLight; - float BorderX1; - float BorderX2; - float HostIndentX; - float MinColumnWidth; - float OuterPaddingX; - float CellPaddingX; - float CellSpacingX1; - float CellSpacingX2; - float InnerWidth; - float ColumnsGivenWidth; - float ColumnsAutoFitWidth; - float ColumnsStretchSumWeights; - float ResizedColumnNextWidth; - float ResizeLockMinContentsX2; - float RefScale; - float AngledHeadersHeight; - float AngledHeadersSlope; - ImRect OuterRect; - ImRect InnerRect; - ImRect WorkRect; - ImRect InnerClipRect; - ImRect BgClipRect; - ImRect Bg0ClipRectForDrawCmd; - ImRect Bg2ClipRectForDrawCmd; - ImRect HostClipRect; - ImRect HostBackupInnerClipRect; - ImGuiWindow* OuterWindow; - ImGuiWindow* InnerWindow; - ImGuiTextBuffer ColumnsNames; - ImDrawListSplitter* DrawSplitter; - ImGuiTableInstanceData InstanceDataFirst; - ImVector_ImGuiTableInstanceData InstanceDataExtra; - ImGuiTableColumnSortSpecs SortSpecsSingle; - ImVector_ImGuiTableColumnSortSpecs SortSpecsMulti; - ImGuiTableSortSpecs SortSpecs; - ImGuiTableColumnIdx SortSpecsCount; - ImGuiTableColumnIdx ColumnsEnabledCount; - ImGuiTableColumnIdx ColumnsEnabledFixedCount; - ImGuiTableColumnIdx DeclColumnsCount; - ImGuiTableColumnIdx AngledHeadersCount; - ImGuiTableColumnIdx HoveredColumnBody; - ImGuiTableColumnIdx HoveredColumnBorder; - ImGuiTableColumnIdx HighlightColumnHeader; - ImGuiTableColumnIdx AutoFitSingleColumn; - ImGuiTableColumnIdx ResizedColumn; - ImGuiTableColumnIdx LastResizedColumn; - ImGuiTableColumnIdx HeldHeaderColumn; - ImGuiTableColumnIdx ReorderColumn; - ImGuiTableColumnIdx ReorderColumnDir; - ImGuiTableColumnIdx LeftMostEnabledColumn; - ImGuiTableColumnIdx RightMostEnabledColumn; - ImGuiTableColumnIdx LeftMostStretchedColumn; - ImGuiTableColumnIdx RightMostStretchedColumn; - ImGuiTableColumnIdx ContextPopupColumn; - ImGuiTableColumnIdx FreezeRowsRequest; - ImGuiTableColumnIdx FreezeRowsCount; - ImGuiTableColumnIdx FreezeColumnsRequest; - ImGuiTableColumnIdx FreezeColumnsCount; - ImGuiTableColumnIdx RowCellDataCurrent; - ImGuiTableDrawChannelIdx DummyDrawChannel; - ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; - ImGuiTableDrawChannelIdx Bg2DrawChannelUnfrozen; - bool IsLayoutLocked; - bool IsInsideRow; - bool IsInitializing; - bool IsSortSpecsDirty; - bool IsUsingHeaders; - bool IsContextPopupOpen; - bool DisableDefaultContextMenu; - bool IsSettingsRequestLoad; - bool IsSettingsDirty; - bool IsDefaultDisplayOrder; - bool IsResetAllRequest; - bool IsResetDisplayOrderRequest; - bool IsUnfrozenRows; - bool IsDefaultSizingPolicy; - bool IsActiveIdAliveBeforeTable; - bool IsActiveIdInTable; - bool HasScrollbarYCurr; - bool HasScrollbarYPrev; - bool MemoryCompacted; - bool HostSkipItems; -}; -typedef struct ImVector_ImGuiTableHeaderData { - int Size; - int Capacity; - ImGuiTableHeaderData* Data; -} ImVector_ImGuiTableHeaderData; - -struct ImGuiTableTempData { - int TableIndex; - float LastTimeActive; - float AngledHeadersExtraWidth; - ImVector_ImGuiTableHeaderData AngledHeadersRequests; - ImVec2 UserOuterSize; - ImDrawListSplitter DrawSplitter; - ImRect HostBackupWorkRect; - ImRect HostBackupParentWorkRect; - ImVec2 HostBackupPrevLineSize; - ImVec2 HostBackupCurrLineSize; - ImVec2 HostBackupCursorMaxPos; - ImVec1 HostBackupColumnsOffset; - float HostBackupItemWidth; - int HostBackupItemWidthStackSize; -}; -typedef struct ImGuiTableColumnSettings ImGuiTableColumnSettings; -struct ImGuiTableColumnSettings { - float WidthOrWeight; - ImGuiID UserID; - ImGuiTableColumnIdx Index; - ImGuiTableColumnIdx DisplayOrder; - ImGuiTableColumnIdx SortOrder; - ImU8 SortDirection : 2; - ImU8 IsEnabled : 1; - ImU8 IsStretch : 1; -}; -struct ImGuiTableSettings { - ImGuiID ID; - ImGuiTableFlags SaveFlags; - float RefScale; - ImGuiTableColumnIdx ColumnsCount; - ImGuiTableColumnIdx ColumnsCountMax; - bool WantApply; -}; -struct ImFontBuilderIO { - bool (*FontBuilder_Build)(ImFontAtlas* atlas); -}; -#define IMGUI_HAS_DOCK 1 - -#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) - -#else -struct GLFWwindow; -struct SDL_Window; -typedef union SDL_Event SDL_Event; -#endif // CIMGUI_DEFINE_ENUMS_AND_STRUCTS - -#ifndef CIMGUI_DEFINE_ENUMS_AND_STRUCTS -typedef struct ImGuiStorage::ImGuiStoragePair ImGuiStoragePair; -typedef struct ImGuiTextFilter::ImGuiTextRange ImGuiTextRange; -typedef ImStb::STB_TexteditState STB_TexteditState; -typedef ImStb::StbTexteditRow StbTexteditRow; -typedef ImStb::StbUndoRecord StbUndoRecord; -typedef ImStb::StbUndoState StbUndoState; -typedef ImChunkStream ImChunkStream_ImGuiTableSettings; -typedef ImChunkStream ImChunkStream_ImGuiWindowSettings; -typedef ImPool ImPool_ImGuiTabBar; -typedef ImPool ImPool_ImGuiTable; -typedef ImSpan ImSpan_ImGuiTableCellData; -typedef ImSpan ImSpan_ImGuiTableColumn; -typedef ImSpan ImSpan_ImGuiTableColumnIdx; -typedef ImVector ImVector_ImDrawChannel; -typedef ImVector ImVector_ImDrawCmd; -typedef ImVector ImVector_ImDrawIdx; -typedef ImVector ImVector_ImDrawListPtr; -typedef ImVector ImVector_ImDrawVert; -typedef ImVector ImVector_ImFontPtr; -typedef ImVector ImVector_ImFontAtlasCustomRect; -typedef ImVector ImVector_ImFontConfig; -typedef ImVector ImVector_ImFontGlyph; -typedef ImVector ImVector_ImGuiColorMod; -typedef ImVector ImVector_ImGuiContextHook; -typedef ImVector ImVector_ImGuiDockNodeSettings; -typedef ImVector ImVector_ImGuiDockRequest; -typedef ImVector ImVector_ImGuiFocusScopeData; -typedef ImVector ImVector_ImGuiGroupData; -typedef ImVector ImVector_ImGuiID; -typedef ImVector ImVector_ImGuiInputEvent; -typedef ImVector ImVector_ImGuiItemFlags; -typedef ImVector ImVector_ImGuiKeyRoutingData; -typedef ImVector ImVector_ImGuiListClipperData; -typedef ImVector ImVector_ImGuiListClipperRange; -typedef ImVector ImVector_ImGuiNavTreeNodeData; -typedef ImVector ImVector_ImGuiOldColumnData; -typedef ImVector ImVector_ImGuiOldColumns; -typedef ImVector ImVector_ImGuiPlatformMonitor; -typedef ImVector ImVector_ImGuiPopupData; -typedef ImVector ImVector_ImGuiPtrOrIndex; -typedef ImVector ImVector_ImGuiSettingsHandler; -typedef ImVector ImVector_ImGuiShrinkWidthItem; -typedef ImVector ImVector_ImGuiStackLevelInfo; -typedef ImVector ImVector_ImGuiStoragePair; -typedef ImVector ImVector_ImGuiStyleMod; -typedef ImVector ImVector_ImGuiTabItem; -typedef ImVector ImVector_ImGuiTableColumnSortSpecs; -typedef ImVector ImVector_ImGuiTableHeaderData; -typedef ImVector ImVector_ImGuiTableInstanceData; -typedef ImVector ImVector_ImGuiTableTempData; -typedef ImVector ImVector_ImGuiTextRange; -typedef ImVector ImVector_ImGuiViewportPtr; -typedef ImVector ImVector_ImGuiViewportPPtr; -typedef ImVector ImVector_ImGuiWindowPtr; -typedef ImVector ImVector_ImGuiWindowStackData; -typedef ImVector ImVector_ImTextureID; -typedef ImVector ImVector_ImU32; -typedef ImVector ImVector_ImVec2; -typedef ImVector ImVector_ImVec4; -typedef ImVector ImVector_ImWchar; -typedef ImVector ImVector_char; -typedef ImVector ImVector_const_charPtr; -typedef ImVector ImVector_float; -typedef ImVector ImVector_int; -typedef ImVector ImVector_unsigned_char; -#endif // CIMGUI_DEFINE_ENUMS_AND_STRUCTS -CIMGUI_API ImVec2* ImVec2_ImVec2_Nil(void); -CIMGUI_API void ImVec2_destroy(ImVec2* self); -CIMGUI_API ImVec2* ImVec2_ImVec2_Float(float _x, float _y); -CIMGUI_API ImVec4* ImVec4_ImVec4_Nil(void); -CIMGUI_API void ImVec4_destroy(ImVec4* self); -CIMGUI_API ImVec4* ImVec4_ImVec4_Float(float _x, float _y, float _z, float _w); -CIMGUI_API ImGuiContext* igCreateContext(ImFontAtlas* shared_font_atlas); -CIMGUI_API void igDestroyContext(ImGuiContext* ctx); -CIMGUI_API ImGuiContext* igGetCurrentContext(void); -CIMGUI_API void igSetCurrentContext(ImGuiContext* ctx); -CIMGUI_API ImGuiIO* igGetIO(void); -CIMGUI_API ImGuiStyle* igGetStyle(void); -CIMGUI_API void igNewFrame(void); -CIMGUI_API void igEndFrame(void); -CIMGUI_API void igRender(void); -CIMGUI_API ImDrawData* igGetDrawData(void); -CIMGUI_API void igShowDemoWindow(bool* p_open); -CIMGUI_API void igShowMetricsWindow(bool* p_open); -CIMGUI_API void igShowDebugLogWindow(bool* p_open); -CIMGUI_API void igShowIDStackToolWindow(bool* p_open); -CIMGUI_API void igShowAboutWindow(bool* p_open); -CIMGUI_API void igShowStyleEditor(ImGuiStyle* ref); -CIMGUI_API bool igShowStyleSelector(const char* label); -CIMGUI_API void igShowFontSelector(const char* label); -CIMGUI_API void igShowUserGuide(void); -CIMGUI_API const char* igGetVersion(void); -CIMGUI_API void igStyleColorsDark(ImGuiStyle* dst); -CIMGUI_API void igStyleColorsLight(ImGuiStyle* dst); -CIMGUI_API void igStyleColorsClassic(ImGuiStyle* dst); -CIMGUI_API bool igBegin(const char* name, bool* p_open, ImGuiWindowFlags flags); -CIMGUI_API void igEnd(void); -CIMGUI_API bool igBeginChild_Str(const char* str_id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags); -CIMGUI_API bool igBeginChild_ID(ImGuiID id, - const ImVec2 size, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags); -CIMGUI_API void igEndChild(void); -CIMGUI_API bool igIsWindowAppearing(void); -CIMGUI_API bool igIsWindowCollapsed(void); -CIMGUI_API bool igIsWindowFocused(ImGuiFocusedFlags flags); -CIMGUI_API bool igIsWindowHovered(ImGuiHoveredFlags flags); -CIMGUI_API ImDrawList* igGetWindowDrawList(void); -CIMGUI_API float igGetWindowDpiScale(void); -CIMGUI_API void igGetWindowPos(ImVec2* pOut); -CIMGUI_API void igGetWindowSize(ImVec2* pOut); -CIMGUI_API float igGetWindowWidth(void); -CIMGUI_API float igGetWindowHeight(void); -CIMGUI_API ImGuiViewport* igGetWindowViewport(void); -CIMGUI_API void igSetNextWindowPos(const ImVec2 pos, - ImGuiCond cond, - const ImVec2 pivot); -CIMGUI_API void igSetNextWindowSize(const ImVec2 size, ImGuiCond cond); -CIMGUI_API void igSetNextWindowSizeConstraints( - const ImVec2 size_min, - const ImVec2 size_max, - ImGuiSizeCallback custom_callback, - void* custom_callback_data); -CIMGUI_API void igSetNextWindowContentSize(const ImVec2 size); -CIMGUI_API void igSetNextWindowCollapsed(bool collapsed, ImGuiCond cond); -CIMGUI_API void igSetNextWindowFocus(void); -CIMGUI_API void igSetNextWindowScroll(const ImVec2 scroll); -CIMGUI_API void igSetNextWindowBgAlpha(float alpha); -CIMGUI_API void igSetNextWindowViewport(ImGuiID viewport_id); -CIMGUI_API void igSetWindowPos_Vec2(const ImVec2 pos, ImGuiCond cond); -CIMGUI_API void igSetWindowSize_Vec2(const ImVec2 size, ImGuiCond cond); -CIMGUI_API void igSetWindowCollapsed_Bool(bool collapsed, ImGuiCond cond); -CIMGUI_API void igSetWindowFocus_Nil(void); -CIMGUI_API void igSetWindowFontScale(float scale); -CIMGUI_API void igSetWindowPos_Str(const char* name, - const ImVec2 pos, - ImGuiCond cond); -CIMGUI_API void igSetWindowSize_Str(const char* name, - const ImVec2 size, - ImGuiCond cond); -CIMGUI_API void igSetWindowCollapsed_Str(const char* name, - bool collapsed, - ImGuiCond cond); -CIMGUI_API void igSetWindowFocus_Str(const char* name); -CIMGUI_API void igGetContentRegionAvail(ImVec2* pOut); -CIMGUI_API void igGetContentRegionMax(ImVec2* pOut); -CIMGUI_API void igGetWindowContentRegionMin(ImVec2* pOut); -CIMGUI_API void igGetWindowContentRegionMax(ImVec2* pOut); -CIMGUI_API float igGetScrollX(void); -CIMGUI_API float igGetScrollY(void); -CIMGUI_API void igSetScrollX_Float(float scroll_x); -CIMGUI_API void igSetScrollY_Float(float scroll_y); -CIMGUI_API float igGetScrollMaxX(void); -CIMGUI_API float igGetScrollMaxY(void); -CIMGUI_API void igSetScrollHereX(float center_x_ratio); -CIMGUI_API void igSetScrollHereY(float center_y_ratio); -CIMGUI_API void igSetScrollFromPosX_Float(float local_x, float center_x_ratio); -CIMGUI_API void igSetScrollFromPosY_Float(float local_y, float center_y_ratio); -CIMGUI_API void igPushFont(ImFont* font); -CIMGUI_API void igPopFont(void); -CIMGUI_API void igPushStyleColor_U32(ImGuiCol idx, ImU32 col); -CIMGUI_API void igPushStyleColor_Vec4(ImGuiCol idx, const ImVec4 col); -CIMGUI_API void igPopStyleColor(int count); -CIMGUI_API void igPushStyleVar_Float(ImGuiStyleVar idx, float val); -CIMGUI_API void igPushStyleVar_Vec2(ImGuiStyleVar idx, const ImVec2 val); -CIMGUI_API void igPopStyleVar(int count); -CIMGUI_API void igPushTabStop(bool tab_stop); -CIMGUI_API void igPopTabStop(void); -CIMGUI_API void igPushButtonRepeat(bool repeat); -CIMGUI_API void igPopButtonRepeat(void); -CIMGUI_API void igPushItemWidth(float item_width); -CIMGUI_API void igPopItemWidth(void); -CIMGUI_API void igSetNextItemWidth(float item_width); -CIMGUI_API float igCalcItemWidth(void); -CIMGUI_API void igPushTextWrapPos(float wrap_local_pos_x); -CIMGUI_API void igPopTextWrapPos(void); -CIMGUI_API ImFont* igGetFont(void); -CIMGUI_API float igGetFontSize(void); -CIMGUI_API void igGetFontTexUvWhitePixel(ImVec2* pOut); -CIMGUI_API ImU32 igGetColorU32_Col(ImGuiCol idx, float alpha_mul); -CIMGUI_API ImU32 igGetColorU32_Vec4(const ImVec4 col); -CIMGUI_API ImU32 igGetColorU32_U32(ImU32 col, float alpha_mul); -CIMGUI_API const ImVec4* igGetStyleColorVec4(ImGuiCol idx); -CIMGUI_API void igGetCursorScreenPos(ImVec2* pOut); -CIMGUI_API void igSetCursorScreenPos(const ImVec2 pos); -CIMGUI_API void igGetCursorPos(ImVec2* pOut); -CIMGUI_API float igGetCursorPosX(void); -CIMGUI_API float igGetCursorPosY(void); -CIMGUI_API void igSetCursorPos(const ImVec2 local_pos); -CIMGUI_API void igSetCursorPosX(float local_x); -CIMGUI_API void igSetCursorPosY(float local_y); -CIMGUI_API void igGetCursorStartPos(ImVec2* pOut); -CIMGUI_API void igSeparator(void); -CIMGUI_API void igSameLine(float offset_from_start_x, float spacing); -CIMGUI_API void igNewLine(void); -CIMGUI_API void igSpacing(void); -CIMGUI_API void igDummy(const ImVec2 size); -CIMGUI_API void igIndent(float indent_w); -CIMGUI_API void igUnindent(float indent_w); -CIMGUI_API void igBeginGroup(void); -CIMGUI_API void igEndGroup(void); -CIMGUI_API void igAlignTextToFramePadding(void); -CIMGUI_API float igGetTextLineHeight(void); -CIMGUI_API float igGetTextLineHeightWithSpacing(void); -CIMGUI_API float igGetFrameHeight(void); -CIMGUI_API float igGetFrameHeightWithSpacing(void); -CIMGUI_API void igPushID_Str(const char* str_id); -CIMGUI_API void igPushID_StrStr(const char* str_id_begin, - const char* str_id_end); -CIMGUI_API void igPushID_Ptr(const void* ptr_id); -CIMGUI_API void igPushID_Int(int int_id); -CIMGUI_API void igPopID(void); -CIMGUI_API ImGuiID igGetID_Str(const char* str_id); -CIMGUI_API ImGuiID igGetID_StrStr(const char* str_id_begin, - const char* str_id_end); -CIMGUI_API ImGuiID igGetID_Ptr(const void* ptr_id); -CIMGUI_API void igTextUnformatted(const char* text, const char* text_end); -CIMGUI_API void igText(const char* fmt, ...); -CIMGUI_API void igTextV(const char* fmt, va_list args); -CIMGUI_API void igTextColored(const ImVec4 col, const char* fmt, ...); -CIMGUI_API void igTextColoredV(const ImVec4 col, const char* fmt, va_list args); -CIMGUI_API void igTextDisabled(const char* fmt, ...); -CIMGUI_API void igTextDisabledV(const char* fmt, va_list args); -CIMGUI_API void igTextWrapped(const char* fmt, ...); -CIMGUI_API void igTextWrappedV(const char* fmt, va_list args); -CIMGUI_API void igLabelText(const char* label, const char* fmt, ...); -CIMGUI_API void igLabelTextV(const char* label, const char* fmt, va_list args); -CIMGUI_API void igBulletText(const char* fmt, ...); -CIMGUI_API void igBulletTextV(const char* fmt, va_list args); -CIMGUI_API void igSeparatorText(const char* label); -CIMGUI_API bool igButton(const char* label, const ImVec2 size); -CIMGUI_API bool igSmallButton(const char* label); -CIMGUI_API bool igInvisibleButton(const char* str_id, - const ImVec2 size, - ImGuiButtonFlags flags); -CIMGUI_API bool igArrowButton(const char* str_id, ImGuiDir dir); -CIMGUI_API bool igCheckbox(const char* label, bool* v); -CIMGUI_API bool igCheckboxFlags_IntPtr(const char* label, - int* flags, - int flags_value); -CIMGUI_API bool igCheckboxFlags_UintPtr(const char* label, - unsigned int* flags, - unsigned int flags_value); -CIMGUI_API bool igRadioButton_Bool(const char* label, bool active); -CIMGUI_API bool igRadioButton_IntPtr(const char* label, int* v, int v_button); -CIMGUI_API void igProgressBar(float fraction, - const ImVec2 size_arg, - const char* overlay); -CIMGUI_API void igBullet(void); -CIMGUI_API void igImage(ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 tint_col, - const ImVec4 border_col); -CIMGUI_API bool igImageButton(const char* str_id, - ImTextureID user_texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col); -CIMGUI_API bool igBeginCombo(const char* label, - const char* preview_value, - ImGuiComboFlags flags); -CIMGUI_API void igEndCombo(void); -CIMGUI_API bool igCombo_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int popup_max_height_in_items); -CIMGUI_API bool igCombo_Str(const char* label, - int* current_item, - const char* items_separated_by_zeros, - int popup_max_height_in_items); -CIMGUI_API bool igCombo_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int popup_max_height_in_items); -CIMGUI_API bool igDragFloat(const char* label, - float* v, - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloat2(const char* label, - float v[2], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloat3(const char* label, - float v[3], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloat4(const char* label, - float v[4], - float v_speed, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragFloatRange2(const char* label, - float* v_current_min, - float* v_current_max, - float v_speed, - float v_min, - float v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt(const char* label, - int* v, - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt2(const char* label, - int v[2], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt3(const char* label, - int v[3], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragInt4(const char* label, - int v[4], - float v_speed, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragIntRange2(const char* label, - int* v_current_min, - int* v_current_max, - float v_speed, - int v_min, - int v_max, - const char* format, - const char* format_max, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igDragScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat(const char* label, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat2(const char* label, - float v[2], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat3(const char* label, - float v[3], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderFloat4(const char* label, - float v[4], - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderAngle(const char* label, - float* v_rad, - float v_degrees_min, - float v_degrees_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt(const char* label, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt2(const char* label, - int v[2], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt3(const char* label, - int v[3], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderInt4(const char* label, - int v[4], - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igVSliderFloat(const char* label, - const ImVec2 size, - float* v, - float v_min, - float v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igVSliderInt(const char* label, - const ImVec2 size, - int* v, - int v_min, - int v_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igVSliderScalar(const char* label, - const ImVec2 size, - ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igInputText(const char* label, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API bool igInputTextMultiline(const char* label, - char* buf, - size_t buf_size, - const ImVec2 size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API bool igInputTextWithHint(const char* label, - const char* hint, - char* buf, - size_t buf_size, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API bool igInputFloat(const char* label, - float* v, - float step, - float step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputFloat2(const char* label, - float v[2], - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputFloat3(const char* label, - float v[3], - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputFloat4(const char* label, - float v[4], - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt(const char* label, - int* v, - int step, - int step_fast, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt2(const char* label, - int v[2], - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt3(const char* label, - int v[3], - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputInt4(const char* label, - int v[4], - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputDouble(const char* label, - double* v, - double step, - double step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputScalar(const char* label, - ImGuiDataType data_type, - void* p_data, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igInputScalarN(const char* label, - ImGuiDataType data_type, - void* p_data, - int components, - const void* p_step, - const void* p_step_fast, - const char* format, - ImGuiInputTextFlags flags); -CIMGUI_API bool igColorEdit3(const char* label, - float col[3], - ImGuiColorEditFlags flags); -CIMGUI_API bool igColorEdit4(const char* label, - float col[4], - ImGuiColorEditFlags flags); -CIMGUI_API bool igColorPicker3(const char* label, - float col[3], - ImGuiColorEditFlags flags); -CIMGUI_API bool igColorPicker4(const char* label, - float col[4], - ImGuiColorEditFlags flags, - const float* ref_col); -CIMGUI_API bool igColorButton(const char* desc_id, - const ImVec4 col, - ImGuiColorEditFlags flags, - const ImVec2 size); -CIMGUI_API void igSetColorEditOptions(ImGuiColorEditFlags flags); -CIMGUI_API bool igTreeNode_Str(const char* label); -CIMGUI_API bool igTreeNode_StrStr(const char* str_id, const char* fmt, ...); -CIMGUI_API bool igTreeNode_Ptr(const void* ptr_id, const char* fmt, ...); -CIMGUI_API bool igTreeNodeV_Str(const char* str_id, - const char* fmt, - va_list args); -CIMGUI_API bool igTreeNodeV_Ptr(const void* ptr_id, - const char* fmt, - va_list args); -CIMGUI_API bool igTreeNodeEx_Str(const char* label, ImGuiTreeNodeFlags flags); -CIMGUI_API bool igTreeNodeEx_StrStr(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...); -CIMGUI_API bool igTreeNodeEx_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - ...); -CIMGUI_API bool igTreeNodeExV_Str(const char* str_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args); -CIMGUI_API bool igTreeNodeExV_Ptr(const void* ptr_id, - ImGuiTreeNodeFlags flags, - const char* fmt, - va_list args); -CIMGUI_API void igTreePush_Str(const char* str_id); -CIMGUI_API void igTreePush_Ptr(const void* ptr_id); -CIMGUI_API void igTreePop(void); -CIMGUI_API float igGetTreeNodeToLabelSpacing(void); -CIMGUI_API bool igCollapsingHeader_TreeNodeFlags(const char* label, - ImGuiTreeNodeFlags flags); -CIMGUI_API bool igCollapsingHeader_BoolPtr(const char* label, - bool* p_visible, - ImGuiTreeNodeFlags flags); -CIMGUI_API void igSetNextItemOpen(bool is_open, ImGuiCond cond); -CIMGUI_API bool igSelectable_Bool(const char* label, - bool selected, - ImGuiSelectableFlags flags, - const ImVec2 size); -CIMGUI_API bool igSelectable_BoolPtr(const char* label, - bool* p_selected, - ImGuiSelectableFlags flags, - const ImVec2 size); -CIMGUI_API bool igBeginListBox(const char* label, const ImVec2 size); -CIMGUI_API void igEndListBox(void); -CIMGUI_API bool igListBox_Str_arr(const char* label, - int* current_item, - const char* const items[], - int items_count, - int height_in_items); -CIMGUI_API bool igListBox_FnStrPtr(const char* label, - int* current_item, - const char* (*getter)(void* user_data, - int idx), - void* user_data, - int items_count, - int height_in_items); -CIMGUI_API void igPlotLines_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride); -CIMGUI_API void igPlotLines_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size); -CIMGUI_API void igPlotHistogram_FloatPtr(const char* label, - const float* values, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size, - int stride); -CIMGUI_API void igPlotHistogram_FnFloatPtr(const char* label, - float (*values_getter)(void* data, - int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - ImVec2 graph_size); -CIMGUI_API void igValue_Bool(const char* prefix, bool b); -CIMGUI_API void igValue_Int(const char* prefix, int v); -CIMGUI_API void igValue_Uint(const char* prefix, unsigned int v); -CIMGUI_API void igValue_Float(const char* prefix, - float v, - const char* float_format); -CIMGUI_API bool igBeginMenuBar(void); -CIMGUI_API void igEndMenuBar(void); -CIMGUI_API bool igBeginMainMenuBar(void); -CIMGUI_API void igEndMainMenuBar(void); -CIMGUI_API bool igBeginMenu(const char* label, bool enabled); -CIMGUI_API void igEndMenu(void); -CIMGUI_API bool igMenuItem_Bool(const char* label, - const char* shortcut, - bool selected, - bool enabled); -CIMGUI_API bool igMenuItem_BoolPtr(const char* label, - const char* shortcut, - bool* p_selected, - bool enabled); -CIMGUI_API bool igBeginTooltip(void); -CIMGUI_API void igEndTooltip(void); -CIMGUI_API void igSetTooltip(const char* fmt, ...); -CIMGUI_API void igSetTooltipV(const char* fmt, va_list args); -CIMGUI_API bool igBeginItemTooltip(void); -CIMGUI_API void igSetItemTooltip(const char* fmt, ...); -CIMGUI_API void igSetItemTooltipV(const char* fmt, va_list args); -CIMGUI_API bool igBeginPopup(const char* str_id, ImGuiWindowFlags flags); -CIMGUI_API bool igBeginPopupModal(const char* name, - bool* p_open, - ImGuiWindowFlags flags); -CIMGUI_API void igEndPopup(void); -CIMGUI_API void igOpenPopup_Str(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API void igOpenPopup_ID(ImGuiID id, ImGuiPopupFlags popup_flags); -CIMGUI_API void igOpenPopupOnItemClick(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API void igCloseCurrentPopup(void); -CIMGUI_API bool igBeginPopupContextItem(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API bool igBeginPopupContextWindow(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API bool igBeginPopupContextVoid(const char* str_id, - ImGuiPopupFlags popup_flags); -CIMGUI_API bool igIsPopupOpen_Str(const char* str_id, ImGuiPopupFlags flags); -CIMGUI_API bool igBeginTable(const char* str_id, - int column, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width); -CIMGUI_API void igEndTable(void); -CIMGUI_API void igTableNextRow(ImGuiTableRowFlags row_flags, - float min_row_height); -CIMGUI_API bool igTableNextColumn(void); -CIMGUI_API bool igTableSetColumnIndex(int column_n); -CIMGUI_API void igTableSetupColumn(const char* label, - ImGuiTableColumnFlags flags, - float init_width_or_weight, - ImGuiID user_id); -CIMGUI_API void igTableSetupScrollFreeze(int cols, int rows); -CIMGUI_API void igTableHeader(const char* label); -CIMGUI_API void igTableHeadersRow(void); -CIMGUI_API void igTableAngledHeadersRow(void); -CIMGUI_API ImGuiTableSortSpecs* igTableGetSortSpecs(void); -CIMGUI_API int igTableGetColumnCount(void); -CIMGUI_API int igTableGetColumnIndex(void); -CIMGUI_API int igTableGetRowIndex(void); -CIMGUI_API const char* igTableGetColumnName_Int(int column_n); -CIMGUI_API ImGuiTableColumnFlags igTableGetColumnFlags(int column_n); -CIMGUI_API void igTableSetColumnEnabled(int column_n, bool v); -CIMGUI_API void igTableSetBgColor(ImGuiTableBgTarget target, - ImU32 color, - int column_n); -CIMGUI_API void igColumns(int count, const char* id, bool border); -CIMGUI_API void igNextColumn(void); -CIMGUI_API int igGetColumnIndex(void); -CIMGUI_API float igGetColumnWidth(int column_index); -CIMGUI_API void igSetColumnWidth(int column_index, float width); -CIMGUI_API float igGetColumnOffset(int column_index); -CIMGUI_API void igSetColumnOffset(int column_index, float offset_x); -CIMGUI_API int igGetColumnsCount(void); -CIMGUI_API bool igBeginTabBar(const char* str_id, ImGuiTabBarFlags flags); -CIMGUI_API void igEndTabBar(void); -CIMGUI_API bool igBeginTabItem(const char* label, - bool* p_open, - ImGuiTabItemFlags flags); -CIMGUI_API void igEndTabItem(void); -CIMGUI_API bool igTabItemButton(const char* label, ImGuiTabItemFlags flags); -CIMGUI_API void igSetTabItemClosed(const char* tab_or_docked_window_label); -CIMGUI_API ImGuiID igDockSpace(ImGuiID id, - const ImVec2 size, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class); -CIMGUI_API ImGuiID -igDockSpaceOverViewport(const ImGuiViewport* viewport, - ImGuiDockNodeFlags flags, - const ImGuiWindowClass* window_class); -CIMGUI_API void igSetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond); -CIMGUI_API void igSetNextWindowClass(const ImGuiWindowClass* window_class); -CIMGUI_API ImGuiID igGetWindowDockID(void); -CIMGUI_API bool igIsWindowDocked(void); -CIMGUI_API void igLogToTTY(int auto_open_depth); -CIMGUI_API void igLogToFile(int auto_open_depth, const char* filename); -CIMGUI_API void igLogToClipboard(int auto_open_depth); -CIMGUI_API void igLogFinish(void); -CIMGUI_API void igLogButtons(void); -CIMGUI_API void igLogTextV(const char* fmt, va_list args); -CIMGUI_API bool igBeginDragDropSource(ImGuiDragDropFlags flags); -CIMGUI_API bool igSetDragDropPayload(const char* type, - const void* data, - size_t sz, - ImGuiCond cond); -CIMGUI_API void igEndDragDropSource(void); -CIMGUI_API bool igBeginDragDropTarget(void); -CIMGUI_API const ImGuiPayload* igAcceptDragDropPayload( - const char* type, - ImGuiDragDropFlags flags); -CIMGUI_API void igEndDragDropTarget(void); -CIMGUI_API const ImGuiPayload* igGetDragDropPayload(void); -CIMGUI_API void igBeginDisabled(bool disabled); -CIMGUI_API void igEndDisabled(void); -CIMGUI_API void igPushClipRect(const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect); -CIMGUI_API void igPopClipRect(void); -CIMGUI_API void igSetItemDefaultFocus(void); -CIMGUI_API void igSetKeyboardFocusHere(int offset); -CIMGUI_API void igSetNextItemAllowOverlap(void); -CIMGUI_API bool igIsItemHovered(ImGuiHoveredFlags flags); -CIMGUI_API bool igIsItemActive(void); -CIMGUI_API bool igIsItemFocused(void); -CIMGUI_API bool igIsItemClicked(ImGuiMouseButton mouse_button); -CIMGUI_API bool igIsItemVisible(void); -CIMGUI_API bool igIsItemEdited(void); -CIMGUI_API bool igIsItemActivated(void); -CIMGUI_API bool igIsItemDeactivated(void); -CIMGUI_API bool igIsItemDeactivatedAfterEdit(void); -CIMGUI_API bool igIsItemToggledOpen(void); -CIMGUI_API bool igIsAnyItemHovered(void); -CIMGUI_API bool igIsAnyItemActive(void); -CIMGUI_API bool igIsAnyItemFocused(void); -CIMGUI_API ImGuiID igGetItemID(void); -CIMGUI_API void igGetItemRectMin(ImVec2* pOut); -CIMGUI_API void igGetItemRectMax(ImVec2* pOut); -CIMGUI_API void igGetItemRectSize(ImVec2* pOut); -CIMGUI_API ImGuiViewport* igGetMainViewport(void); -CIMGUI_API ImDrawList* igGetBackgroundDrawList_Nil(void); -CIMGUI_API ImDrawList* igGetForegroundDrawList_Nil(void); -CIMGUI_API ImDrawList* igGetBackgroundDrawList_ViewportPtr( - ImGuiViewport* viewport); -CIMGUI_API ImDrawList* igGetForegroundDrawList_ViewportPtr( - ImGuiViewport* viewport); -CIMGUI_API bool igIsRectVisible_Nil(const ImVec2 size); -CIMGUI_API bool igIsRectVisible_Vec2(const ImVec2 rect_min, - const ImVec2 rect_max); -CIMGUI_API double igGetTime(void); -CIMGUI_API int igGetFrameCount(void); -CIMGUI_API ImDrawListSharedData* igGetDrawListSharedData(void); -CIMGUI_API const char* igGetStyleColorName(ImGuiCol idx); -CIMGUI_API void igSetStateStorage(ImGuiStorage* storage); -CIMGUI_API ImGuiStorage* igGetStateStorage(void); -CIMGUI_API void igCalcTextSize(ImVec2* pOut, - const char* text, - const char* text_end, - bool hide_text_after_double_hash, - float wrap_width); -CIMGUI_API void igColorConvertU32ToFloat4(ImVec4* pOut, ImU32 in); -CIMGUI_API ImU32 igColorConvertFloat4ToU32(const ImVec4 in); -CIMGUI_API void igColorConvertRGBtoHSV(float r, - float g, - float b, - float* out_h, - float* out_s, - float* out_v); -CIMGUI_API void igColorConvertHSVtoRGB(float h, - float s, - float v, - float* out_r, - float* out_g, - float* out_b); -CIMGUI_API bool igIsKeyDown_Nil(ImGuiKey key); -CIMGUI_API bool igIsKeyPressed_Bool(ImGuiKey key, bool repeat); -CIMGUI_API bool igIsKeyReleased_Nil(ImGuiKey key); -CIMGUI_API bool igIsKeyChordPressed_Nil(ImGuiKeyChord key_chord); -CIMGUI_API int igGetKeyPressedAmount(ImGuiKey key, - float repeat_delay, - float rate); -CIMGUI_API const char* igGetKeyName(ImGuiKey key); -CIMGUI_API void igSetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); -CIMGUI_API bool igIsMouseDown_Nil(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseClicked_Bool(ImGuiMouseButton button, bool repeat); -CIMGUI_API bool igIsMouseReleased_Nil(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseDoubleClicked_Nil(ImGuiMouseButton button); -CIMGUI_API int igGetMouseClickedCount(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseHoveringRect(const ImVec2 r_min, - const ImVec2 r_max, - bool clip); -CIMGUI_API bool igIsMousePosValid(const ImVec2* mouse_pos); -CIMGUI_API bool igIsAnyMouseDown(void); -CIMGUI_API void igGetMousePos(ImVec2* pOut); -CIMGUI_API void igGetMousePosOnOpeningCurrentPopup(ImVec2* pOut); -CIMGUI_API bool igIsMouseDragging(ImGuiMouseButton button, - float lock_threshold); -CIMGUI_API void igGetMouseDragDelta(ImVec2* pOut, - ImGuiMouseButton button, - float lock_threshold); -CIMGUI_API void igResetMouseDragDelta(ImGuiMouseButton button); -CIMGUI_API ImGuiMouseCursor igGetMouseCursor(void); -CIMGUI_API void igSetMouseCursor(ImGuiMouseCursor cursor_type); -CIMGUI_API void igSetNextFrameWantCaptureMouse(bool want_capture_mouse); -CIMGUI_API const char* igGetClipboardText(void); -CIMGUI_API void igSetClipboardText(const char* text); -CIMGUI_API void igLoadIniSettingsFromDisk(const char* ini_filename); -CIMGUI_API void igLoadIniSettingsFromMemory(const char* ini_data, - size_t ini_size); -CIMGUI_API void igSaveIniSettingsToDisk(const char* ini_filename); -CIMGUI_API const char* igSaveIniSettingsToMemory(size_t* out_ini_size); -CIMGUI_API void igDebugTextEncoding(const char* text); -CIMGUI_API void igDebugFlashStyleColor(ImGuiCol idx); -CIMGUI_API void igDebugStartItemPicker(void); -CIMGUI_API bool igDebugCheckVersionAndDataLayout(const char* version_str, - size_t sz_io, - size_t sz_style, - size_t sz_vec2, - size_t sz_vec4, - size_t sz_drawvert, - size_t sz_drawidx); -CIMGUI_API void igSetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, - ImGuiMemFreeFunc free_func, - void* user_data); -CIMGUI_API void igGetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, - ImGuiMemFreeFunc* p_free_func, - void** p_user_data); -CIMGUI_API void* igMemAlloc(size_t size); -CIMGUI_API void igMemFree(void* ptr); -CIMGUI_API ImGuiPlatformIO* igGetPlatformIO(void); -CIMGUI_API void igUpdatePlatformWindows(void); -CIMGUI_API void igRenderPlatformWindowsDefault(void* platform_render_arg, - void* renderer_render_arg); -CIMGUI_API void igDestroyPlatformWindows(void); -CIMGUI_API ImGuiViewport* igFindViewportByID(ImGuiID id); -CIMGUI_API ImGuiViewport* igFindViewportByPlatformHandle(void* platform_handle); -CIMGUI_API ImGuiTableSortSpecs* ImGuiTableSortSpecs_ImGuiTableSortSpecs(void); -CIMGUI_API void ImGuiTableSortSpecs_destroy(ImGuiTableSortSpecs* self); -CIMGUI_API ImGuiTableColumnSortSpecs* -ImGuiTableColumnSortSpecs_ImGuiTableColumnSortSpecs(void); -CIMGUI_API void ImGuiTableColumnSortSpecs_destroy( - ImGuiTableColumnSortSpecs* self); -CIMGUI_API ImGuiStyle* ImGuiStyle_ImGuiStyle(void); -CIMGUI_API void ImGuiStyle_destroy(ImGuiStyle* self); -CIMGUI_API void ImGuiStyle_ScaleAllSizes(ImGuiStyle* self, float scale_factor); -CIMGUI_API void ImGuiIO_AddKeyEvent(ImGuiIO* self, ImGuiKey key, bool down); -CIMGUI_API void ImGuiIO_AddKeyAnalogEvent(ImGuiIO* self, - ImGuiKey key, - bool down, - float v); -CIMGUI_API void ImGuiIO_AddMousePosEvent(ImGuiIO* self, float x, float y); -CIMGUI_API void ImGuiIO_AddMouseButtonEvent(ImGuiIO* self, - int button, - bool down); -CIMGUI_API void ImGuiIO_AddMouseWheelEvent(ImGuiIO* self, - float wheel_x, - float wheel_y); -CIMGUI_API void ImGuiIO_AddMouseSourceEvent(ImGuiIO* self, - ImGuiMouseSource source); -CIMGUI_API void ImGuiIO_AddMouseViewportEvent(ImGuiIO* self, ImGuiID id); -CIMGUI_API void ImGuiIO_AddFocusEvent(ImGuiIO* self, bool focused); -CIMGUI_API void ImGuiIO_AddInputCharacter(ImGuiIO* self, unsigned int c); -CIMGUI_API void ImGuiIO_AddInputCharacterUTF16(ImGuiIO* self, ImWchar16 c); -CIMGUI_API void ImGuiIO_AddInputCharactersUTF8(ImGuiIO* self, const char* str); -CIMGUI_API void ImGuiIO_SetKeyEventNativeData(ImGuiIO* self, - ImGuiKey key, - int native_keycode, - int native_scancode, - int native_legacy_index); -CIMGUI_API void ImGuiIO_SetAppAcceptingEvents(ImGuiIO* self, - bool accepting_events); -CIMGUI_API void ImGuiIO_ClearEventsQueue(ImGuiIO* self); -CIMGUI_API void ImGuiIO_ClearInputKeys(ImGuiIO* self); -CIMGUI_API ImGuiIO* ImGuiIO_ImGuiIO(void); -CIMGUI_API void ImGuiIO_destroy(ImGuiIO* self); -CIMGUI_API ImGuiInputTextCallbackData* -ImGuiInputTextCallbackData_ImGuiInputTextCallbackData(void); -CIMGUI_API void ImGuiInputTextCallbackData_destroy( - ImGuiInputTextCallbackData* self); -CIMGUI_API void ImGuiInputTextCallbackData_DeleteChars( - ImGuiInputTextCallbackData* self, - int pos, - int bytes_count); -CIMGUI_API void ImGuiInputTextCallbackData_InsertChars( - ImGuiInputTextCallbackData* self, - int pos, - const char* text, - const char* text_end); -CIMGUI_API void ImGuiInputTextCallbackData_SelectAll( - ImGuiInputTextCallbackData* self); -CIMGUI_API void ImGuiInputTextCallbackData_ClearSelection( - ImGuiInputTextCallbackData* self); -CIMGUI_API bool ImGuiInputTextCallbackData_HasSelection( - ImGuiInputTextCallbackData* self); -CIMGUI_API ImGuiWindowClass* ImGuiWindowClass_ImGuiWindowClass(void); -CIMGUI_API void ImGuiWindowClass_destroy(ImGuiWindowClass* self); -CIMGUI_API ImGuiPayload* ImGuiPayload_ImGuiPayload(void); -CIMGUI_API void ImGuiPayload_destroy(ImGuiPayload* self); -CIMGUI_API void ImGuiPayload_Clear(ImGuiPayload* self); -CIMGUI_API bool ImGuiPayload_IsDataType(ImGuiPayload* self, const char* type); -CIMGUI_API bool ImGuiPayload_IsPreview(ImGuiPayload* self); -CIMGUI_API bool ImGuiPayload_IsDelivery(ImGuiPayload* self); -CIMGUI_API ImGuiOnceUponAFrame* ImGuiOnceUponAFrame_ImGuiOnceUponAFrame(void); -CIMGUI_API void ImGuiOnceUponAFrame_destroy(ImGuiOnceUponAFrame* self); -CIMGUI_API ImGuiTextFilter* ImGuiTextFilter_ImGuiTextFilter( - const char* default_filter); -CIMGUI_API void ImGuiTextFilter_destroy(ImGuiTextFilter* self); -CIMGUI_API bool ImGuiTextFilter_Draw(ImGuiTextFilter* self, - const char* label, - float width); -CIMGUI_API bool ImGuiTextFilter_PassFilter(ImGuiTextFilter* self, - const char* text, - const char* text_end); -CIMGUI_API void ImGuiTextFilter_Build(ImGuiTextFilter* self); -CIMGUI_API void ImGuiTextFilter_Clear(ImGuiTextFilter* self); -CIMGUI_API bool ImGuiTextFilter_IsActive(ImGuiTextFilter* self); -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Nil(void); -CIMGUI_API void ImGuiTextRange_destroy(ImGuiTextRange* self); -CIMGUI_API ImGuiTextRange* ImGuiTextRange_ImGuiTextRange_Str(const char* _b, - const char* _e); -CIMGUI_API bool ImGuiTextRange_empty(ImGuiTextRange* self); -CIMGUI_API void ImGuiTextRange_split(ImGuiTextRange* self, - char separator, - ImVector_ImGuiTextRange* out); -CIMGUI_API ImGuiTextBuffer* ImGuiTextBuffer_ImGuiTextBuffer(void); -CIMGUI_API void ImGuiTextBuffer_destroy(ImGuiTextBuffer* self); -CIMGUI_API const char* ImGuiTextBuffer_begin(ImGuiTextBuffer* self); -CIMGUI_API const char* ImGuiTextBuffer_end(ImGuiTextBuffer* self); -CIMGUI_API int ImGuiTextBuffer_size(ImGuiTextBuffer* self); -CIMGUI_API bool ImGuiTextBuffer_empty(ImGuiTextBuffer* self); -CIMGUI_API void ImGuiTextBuffer_clear(ImGuiTextBuffer* self); -CIMGUI_API void ImGuiTextBuffer_reserve(ImGuiTextBuffer* self, int capacity); -CIMGUI_API const char* ImGuiTextBuffer_c_str(ImGuiTextBuffer* self); -CIMGUI_API void ImGuiTextBuffer_append(ImGuiTextBuffer* self, - const char* str, - const char* str_end); -CIMGUI_API void ImGuiTextBuffer_appendfv(ImGuiTextBuffer* self, - const char* fmt, - va_list args); -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Int(ImGuiID _key, - int _val); -CIMGUI_API void ImGuiStoragePair_destroy(ImGuiStoragePair* self); -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Float( - ImGuiID _key, - float _val); -CIMGUI_API ImGuiStoragePair* ImGuiStoragePair_ImGuiStoragePair_Ptr(ImGuiID _key, - void* _val); -CIMGUI_API void ImGuiStorage_Clear(ImGuiStorage* self); -CIMGUI_API int ImGuiStorage_GetInt(ImGuiStorage* self, - ImGuiID key, - int default_val); -CIMGUI_API void ImGuiStorage_SetInt(ImGuiStorage* self, ImGuiID key, int val); -CIMGUI_API bool ImGuiStorage_GetBool(ImGuiStorage* self, - ImGuiID key, - bool default_val); -CIMGUI_API void ImGuiStorage_SetBool(ImGuiStorage* self, ImGuiID key, bool val); -CIMGUI_API float ImGuiStorage_GetFloat(ImGuiStorage* self, - ImGuiID key, - float default_val); -CIMGUI_API void ImGuiStorage_SetFloat(ImGuiStorage* self, - ImGuiID key, - float val); -CIMGUI_API void* ImGuiStorage_GetVoidPtr(ImGuiStorage* self, ImGuiID key); -CIMGUI_API void ImGuiStorage_SetVoidPtr(ImGuiStorage* self, - ImGuiID key, - void* val); -CIMGUI_API int* ImGuiStorage_GetIntRef(ImGuiStorage* self, - ImGuiID key, - int default_val); -CIMGUI_API bool* ImGuiStorage_GetBoolRef(ImGuiStorage* self, - ImGuiID key, - bool default_val); -CIMGUI_API float* ImGuiStorage_GetFloatRef(ImGuiStorage* self, - ImGuiID key, - float default_val); -CIMGUI_API void** ImGuiStorage_GetVoidPtrRef(ImGuiStorage* self, - ImGuiID key, - void* default_val); -CIMGUI_API void ImGuiStorage_BuildSortByKey(ImGuiStorage* self); -CIMGUI_API void ImGuiStorage_SetAllInt(ImGuiStorage* self, int val); -CIMGUI_API ImGuiListClipper* ImGuiListClipper_ImGuiListClipper(void); -CIMGUI_API void ImGuiListClipper_destroy(ImGuiListClipper* self); -CIMGUI_API void ImGuiListClipper_Begin(ImGuiListClipper* self, - int items_count, - float items_height); -CIMGUI_API void ImGuiListClipper_End(ImGuiListClipper* self); -CIMGUI_API bool ImGuiListClipper_Step(ImGuiListClipper* self); -CIMGUI_API void ImGuiListClipper_IncludeItemByIndex(ImGuiListClipper* self, - int item_index); -CIMGUI_API void ImGuiListClipper_IncludeItemsByIndex(ImGuiListClipper* self, - int item_begin, - int item_end); -CIMGUI_API ImColor* ImColor_ImColor_Nil(void); -CIMGUI_API void ImColor_destroy(ImColor* self); -CIMGUI_API ImColor* ImColor_ImColor_Float(float r, float g, float b, float a); -CIMGUI_API ImColor* ImColor_ImColor_Vec4(const ImVec4 col); -CIMGUI_API ImColor* ImColor_ImColor_Int(int r, int g, int b, int a); -CIMGUI_API ImColor* ImColor_ImColor_U32(ImU32 rgba); -CIMGUI_API void ImColor_SetHSV(ImColor* self, - float h, - float s, - float v, - float a); -CIMGUI_API void ImColor_HSV(ImColor* pOut, float h, float s, float v, float a); -CIMGUI_API ImDrawCmd* ImDrawCmd_ImDrawCmd(void); -CIMGUI_API void ImDrawCmd_destroy(ImDrawCmd* self); -CIMGUI_API ImTextureID ImDrawCmd_GetTexID(ImDrawCmd* self); -CIMGUI_API ImDrawListSplitter* ImDrawListSplitter_ImDrawListSplitter(void); -CIMGUI_API void ImDrawListSplitter_destroy(ImDrawListSplitter* self); -CIMGUI_API void ImDrawListSplitter_Clear(ImDrawListSplitter* self); -CIMGUI_API void ImDrawListSplitter_ClearFreeMemory(ImDrawListSplitter* self); -CIMGUI_API void ImDrawListSplitter_Split(ImDrawListSplitter* self, - ImDrawList* draw_list, - int count); -CIMGUI_API void ImDrawListSplitter_Merge(ImDrawListSplitter* self, - ImDrawList* draw_list); -CIMGUI_API void ImDrawListSplitter_SetCurrentChannel(ImDrawListSplitter* self, - ImDrawList* draw_list, - int channel_idx); -CIMGUI_API ImDrawList* ImDrawList_ImDrawList(ImDrawListSharedData* shared_data); -CIMGUI_API void ImDrawList_destroy(ImDrawList* self); -CIMGUI_API void ImDrawList_PushClipRect(ImDrawList* self, - const ImVec2 clip_rect_min, - const ImVec2 clip_rect_max, - bool intersect_with_current_clip_rect); -CIMGUI_API void ImDrawList_PushClipRectFullScreen(ImDrawList* self); -CIMGUI_API void ImDrawList_PopClipRect(ImDrawList* self); -CIMGUI_API void ImDrawList_PushTextureID(ImDrawList* self, - ImTextureID texture_id); -CIMGUI_API void ImDrawList_PopTextureID(ImDrawList* self); -CIMGUI_API void ImDrawList_GetClipRectMin(ImVec2* pOut, ImDrawList* self); -CIMGUI_API void ImDrawList_GetClipRectMax(ImVec2* pOut, ImDrawList* self); -CIMGUI_API void ImDrawList_AddLine(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - ImU32 col, - float thickness); -CIMGUI_API void ImDrawList_AddRect(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags, - float thickness); -CIMGUI_API void ImDrawList_AddRectFilled(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col, - float rounding, - ImDrawFlags flags); -CIMGUI_API void ImDrawList_AddRectFilledMultiColor(ImDrawList* self, - const ImVec2 p_min, - const ImVec2 p_max, - ImU32 col_upr_left, - ImU32 col_upr_right, - ImU32 col_bot_right, - ImU32 col_bot_left); -CIMGUI_API void ImDrawList_AddQuad(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness); -CIMGUI_API void ImDrawList_AddQuadFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col); -CIMGUI_API void ImDrawList_AddTriangle(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness); -CIMGUI_API void ImDrawList_AddTriangleFilled(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col); -CIMGUI_API void ImDrawList_AddCircle(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness); -CIMGUI_API void ImDrawList_AddCircleFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments); -CIMGUI_API void ImDrawList_AddNgon(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments, - float thickness); -CIMGUI_API void ImDrawList_AddNgonFilled(ImDrawList* self, - const ImVec2 center, - float radius, - ImU32 col, - int num_segments); -CIMGUI_API void ImDrawList_AddEllipse(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments, - float thickness); -CIMGUI_API void ImDrawList_AddEllipseFilled(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - ImU32 col, - float rot, - int num_segments); -CIMGUI_API void ImDrawList_AddText_Vec2(ImDrawList* self, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end); -CIMGUI_API void ImDrawList_AddText_FontPtr(ImDrawList* self, - const ImFont* font, - float font_size, - const ImVec2 pos, - ImU32 col, - const char* text_begin, - const char* text_end, - float wrap_width, - const ImVec4* cpu_fine_clip_rect); -CIMGUI_API void ImDrawList_AddBezierCubic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - ImU32 col, - float thickness, - int num_segments); -CIMGUI_API void ImDrawList_AddBezierQuadratic(ImDrawList* self, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - ImU32 col, - float thickness, - int num_segments); -CIMGUI_API void ImDrawList_AddPolyline(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col, - ImDrawFlags flags, - float thickness); -CIMGUI_API void ImDrawList_AddConvexPolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col); -CIMGUI_API void ImDrawList_AddConcavePolyFilled(ImDrawList* self, - const ImVec2* points, - int num_points, - ImU32 col); -CIMGUI_API void ImDrawList_AddImage(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col); -CIMGUI_API void ImDrawList_AddImageQuad(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 uv1, - const ImVec2 uv2, - const ImVec2 uv3, - const ImVec2 uv4, - ImU32 col); -CIMGUI_API void ImDrawList_AddImageRounded(ImDrawList* self, - ImTextureID user_texture_id, - const ImVec2 p_min, - const ImVec2 p_max, - const ImVec2 uv_min, - const ImVec2 uv_max, - ImU32 col, - float rounding, - ImDrawFlags flags); -CIMGUI_API void ImDrawList_PathClear(ImDrawList* self); -CIMGUI_API void ImDrawList_PathLineTo(ImDrawList* self, const ImVec2 pos); -CIMGUI_API void ImDrawList_PathLineToMergeDuplicate(ImDrawList* self, - const ImVec2 pos); -CIMGUI_API void ImDrawList_PathFillConvex(ImDrawList* self, ImU32 col); -CIMGUI_API void ImDrawList_PathFillConcave(ImDrawList* self, ImU32 col); -CIMGUI_API void ImDrawList_PathStroke(ImDrawList* self, - ImU32 col, - ImDrawFlags flags, - float thickness); -CIMGUI_API void ImDrawList_PathArcTo(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments); -CIMGUI_API void ImDrawList_PathArcToFast(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_of_12, - int a_max_of_12); -CIMGUI_API void ImDrawList_PathEllipticalArcTo(ImDrawList* self, - const ImVec2 center, - const ImVec2 radius, - float rot, - float a_min, - float a_max, - int num_segments); -CIMGUI_API void ImDrawList_PathBezierCubicCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - int num_segments); -CIMGUI_API void ImDrawList_PathBezierQuadraticCurveTo(ImDrawList* self, - const ImVec2 p2, - const ImVec2 p3, - int num_segments); -CIMGUI_API void ImDrawList_PathRect(ImDrawList* self, - const ImVec2 rect_min, - const ImVec2 rect_max, - float rounding, - ImDrawFlags flags); -CIMGUI_API void ImDrawList_AddCallback(ImDrawList* self, - ImDrawCallback callback, - void* callback_data); -CIMGUI_API void ImDrawList_AddDrawCmd(ImDrawList* self); -CIMGUI_API ImDrawList* ImDrawList_CloneOutput(ImDrawList* self); -CIMGUI_API void ImDrawList_ChannelsSplit(ImDrawList* self, int count); -CIMGUI_API void ImDrawList_ChannelsMerge(ImDrawList* self); -CIMGUI_API void ImDrawList_ChannelsSetCurrent(ImDrawList* self, int n); -CIMGUI_API void ImDrawList_PrimReserve(ImDrawList* self, - int idx_count, - int vtx_count); -CIMGUI_API void ImDrawList_PrimUnreserve(ImDrawList* self, - int idx_count, - int vtx_count); -CIMGUI_API void ImDrawList_PrimRect(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - ImU32 col); -CIMGUI_API void ImDrawList_PrimRectUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - ImU32 col); -CIMGUI_API void ImDrawList_PrimQuadUV(ImDrawList* self, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 d, - const ImVec2 uv_a, - const ImVec2 uv_b, - const ImVec2 uv_c, - const ImVec2 uv_d, - ImU32 col); -CIMGUI_API void ImDrawList_PrimWriteVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col); -CIMGUI_API void ImDrawList_PrimWriteIdx(ImDrawList* self, ImDrawIdx idx); -CIMGUI_API void ImDrawList_PrimVtx(ImDrawList* self, - const ImVec2 pos, - const ImVec2 uv, - ImU32 col); -CIMGUI_API void ImDrawList__ResetForNewFrame(ImDrawList* self); -CIMGUI_API void ImDrawList__ClearFreeMemory(ImDrawList* self); -CIMGUI_API void ImDrawList__PopUnusedDrawCmd(ImDrawList* self); -CIMGUI_API void ImDrawList__TryMergeDrawCmds(ImDrawList* self); -CIMGUI_API void ImDrawList__OnChangedClipRect(ImDrawList* self); -CIMGUI_API void ImDrawList__OnChangedTextureID(ImDrawList* self); -CIMGUI_API void ImDrawList__OnChangedVtxOffset(ImDrawList* self); -CIMGUI_API int ImDrawList__CalcCircleAutoSegmentCount(ImDrawList* self, - float radius); -CIMGUI_API void ImDrawList__PathArcToFastEx(ImDrawList* self, - const ImVec2 center, - float radius, - int a_min_sample, - int a_max_sample, - int a_step); -CIMGUI_API void ImDrawList__PathArcToN(ImDrawList* self, - const ImVec2 center, - float radius, - float a_min, - float a_max, - int num_segments); -CIMGUI_API ImDrawData* ImDrawData_ImDrawData(void); -CIMGUI_API void ImDrawData_destroy(ImDrawData* self); -CIMGUI_API void ImDrawData_Clear(ImDrawData* self); -CIMGUI_API void ImDrawData_AddDrawList(ImDrawData* self, ImDrawList* draw_list); -CIMGUI_API void ImDrawData_DeIndexAllBuffers(ImDrawData* self); -CIMGUI_API void ImDrawData_ScaleClipRects(ImDrawData* self, - const ImVec2 fb_scale); -CIMGUI_API ImFontConfig* ImFontConfig_ImFontConfig(void); -CIMGUI_API void ImFontConfig_destroy(ImFontConfig* self); -CIMGUI_API ImFontGlyphRangesBuilder* -ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder(void); -CIMGUI_API void ImFontGlyphRangesBuilder_destroy( - ImFontGlyphRangesBuilder* self); -CIMGUI_API void ImFontGlyphRangesBuilder_Clear(ImFontGlyphRangesBuilder* self); -CIMGUI_API bool ImFontGlyphRangesBuilder_GetBit(ImFontGlyphRangesBuilder* self, - size_t n); -CIMGUI_API void ImFontGlyphRangesBuilder_SetBit(ImFontGlyphRangesBuilder* self, - size_t n); -CIMGUI_API void ImFontGlyphRangesBuilder_AddChar(ImFontGlyphRangesBuilder* self, - ImWchar c); -CIMGUI_API void ImFontGlyphRangesBuilder_AddText(ImFontGlyphRangesBuilder* self, - const char* text, - const char* text_end); -CIMGUI_API void ImFontGlyphRangesBuilder_AddRanges( - ImFontGlyphRangesBuilder* self, - const ImWchar* ranges); -CIMGUI_API void ImFontGlyphRangesBuilder_BuildRanges( - ImFontGlyphRangesBuilder* self, - ImVector_ImWchar* out_ranges); -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlasCustomRect_ImFontAtlasCustomRect( - void); -CIMGUI_API void ImFontAtlasCustomRect_destroy(ImFontAtlasCustomRect* self); -CIMGUI_API bool ImFontAtlasCustomRect_IsPacked(ImFontAtlasCustomRect* self); -CIMGUI_API ImFontAtlas* ImFontAtlas_ImFontAtlas(void); -CIMGUI_API void ImFontAtlas_destroy(ImFontAtlas* self); -CIMGUI_API ImFont* ImFontAtlas_AddFont(ImFontAtlas* self, - const ImFontConfig* font_cfg); -CIMGUI_API ImFont* ImFontAtlas_AddFontDefault(ImFontAtlas* self, - const ImFontConfig* font_cfg); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromFileTTF(ImFontAtlas* self, - const char* filename, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryTTF( - ImFontAtlas* self, - void* font_data, - int font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedTTF( - ImFontAtlas* self, - const void* compressed_font_data, - int compressed_font_data_size, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API ImFont* ImFontAtlas_AddFontFromMemoryCompressedBase85TTF( - ImFontAtlas* self, - const char* compressed_font_data_base85, - float size_pixels, - const ImFontConfig* font_cfg, - const ImWchar* glyph_ranges); -CIMGUI_API void ImFontAtlas_ClearInputData(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_ClearTexData(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_ClearFonts(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_Clear(ImFontAtlas* self); -CIMGUI_API bool ImFontAtlas_Build(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_GetTexDataAsAlpha8(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel); -CIMGUI_API void ImFontAtlas_GetTexDataAsRGBA32(ImFontAtlas* self, - unsigned char** out_pixels, - int* out_width, - int* out_height, - int* out_bytes_per_pixel); -CIMGUI_API bool ImFontAtlas_IsBuilt(ImFontAtlas* self); -CIMGUI_API void ImFontAtlas_SetTexID(ImFontAtlas* self, ImTextureID id); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesDefault(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesGreek(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesKorean(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesJapanese(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseFull( - ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesChineseSimplifiedCommon( - ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesCyrillic(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesThai(ImFontAtlas* self); -CIMGUI_API const ImWchar* ImFontAtlas_GetGlyphRangesVietnamese( - ImFontAtlas* self); -CIMGUI_API int ImFontAtlas_AddCustomRectRegular(ImFontAtlas* self, - int width, - int height); -CIMGUI_API int ImFontAtlas_AddCustomRectFontGlyph(ImFontAtlas* self, - ImFont* font, - ImWchar id, - int width, - int height, - float advance_x, - const ImVec2 offset); -CIMGUI_API ImFontAtlasCustomRect* ImFontAtlas_GetCustomRectByIndex( - ImFontAtlas* self, - int index); -CIMGUI_API void ImFontAtlas_CalcCustomRectUV(ImFontAtlas* self, - const ImFontAtlasCustomRect* rect, - ImVec2* out_uv_min, - ImVec2* out_uv_max); -CIMGUI_API bool ImFontAtlas_GetMouseCursorTexData(ImFontAtlas* self, - ImGuiMouseCursor cursor, - ImVec2* out_offset, - ImVec2* out_size, - ImVec2 out_uv_border[2], - ImVec2 out_uv_fill[2]); -CIMGUI_API ImFont* ImFont_ImFont(void); -CIMGUI_API void ImFont_destroy(ImFont* self); -CIMGUI_API const ImFontGlyph* ImFont_FindGlyph(ImFont* self, ImWchar c); -CIMGUI_API const ImFontGlyph* ImFont_FindGlyphNoFallback(ImFont* self, - ImWchar c); -CIMGUI_API float ImFont_GetCharAdvance(ImFont* self, ImWchar c); -CIMGUI_API bool ImFont_IsLoaded(ImFont* self); -CIMGUI_API const char* ImFont_GetDebugName(ImFont* self); -CIMGUI_API void ImFont_CalcTextSizeA(ImVec2* pOut, - ImFont* self, - float size, - float max_width, - float wrap_width, - const char* text_begin, - const char* text_end, - const char** remaining); -CIMGUI_API const char* ImFont_CalcWordWrapPositionA(ImFont* self, - float scale, - const char* text, - const char* text_end, - float wrap_width); -CIMGUI_API void ImFont_RenderChar(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - ImWchar c); -CIMGUI_API void ImFont_RenderText(ImFont* self, - ImDrawList* draw_list, - float size, - const ImVec2 pos, - ImU32 col, - const ImVec4 clip_rect, - const char* text_begin, - const char* text_end, - float wrap_width, - bool cpu_fine_clip); -CIMGUI_API void ImFont_BuildLookupTable(ImFont* self); -CIMGUI_API void ImFont_ClearOutputData(ImFont* self); -CIMGUI_API void ImFont_GrowIndex(ImFont* self, int new_size); -CIMGUI_API void ImFont_AddGlyph(ImFont* self, - const ImFontConfig* src_cfg, - ImWchar c, - float x0, - float y0, - float x1, - float y1, - float u0, - float v0, - float u1, - float v1, - float advance_x); -CIMGUI_API void ImFont_AddRemapChar(ImFont* self, - ImWchar dst, - ImWchar src, - bool overwrite_dst); -CIMGUI_API void ImFont_SetGlyphVisible(ImFont* self, ImWchar c, bool visible); -CIMGUI_API bool ImFont_IsGlyphRangeUnused(ImFont* self, - unsigned int c_begin, - unsigned int c_last); -CIMGUI_API ImGuiViewport* ImGuiViewport_ImGuiViewport(void); -CIMGUI_API void ImGuiViewport_destroy(ImGuiViewport* self); -CIMGUI_API void ImGuiViewport_GetCenter(ImVec2* pOut, ImGuiViewport* self); -CIMGUI_API void ImGuiViewport_GetWorkCenter(ImVec2* pOut, ImGuiViewport* self); -CIMGUI_API ImGuiPlatformIO* ImGuiPlatformIO_ImGuiPlatformIO(void); -CIMGUI_API void ImGuiPlatformIO_destroy(ImGuiPlatformIO* self); -CIMGUI_API ImGuiPlatformMonitor* ImGuiPlatformMonitor_ImGuiPlatformMonitor( - void); -CIMGUI_API void ImGuiPlatformMonitor_destroy(ImGuiPlatformMonitor* self); -CIMGUI_API ImGuiPlatformImeData* ImGuiPlatformImeData_ImGuiPlatformImeData( - void); -CIMGUI_API void ImGuiPlatformImeData_destroy(ImGuiPlatformImeData* self); -CIMGUI_API ImGuiID igImHashData(const void* data, - size_t data_size, - ImGuiID seed); -CIMGUI_API ImGuiID igImHashStr(const char* data, - size_t data_size, - ImGuiID seed); -CIMGUI_API void igImQsort(void* base, - size_t count, - size_t size_of_element, - int (*compare_func)(void const*, void const*)); -CIMGUI_API ImU32 igImAlphaBlendColors(ImU32 col_a, ImU32 col_b); -CIMGUI_API bool igImIsPowerOfTwo_Int(int v); -CIMGUI_API bool igImIsPowerOfTwo_U64(ImU64 v); -CIMGUI_API int igImUpperPowerOfTwo(int v); -CIMGUI_API int igImStricmp(const char* str1, const char* str2); -CIMGUI_API int igImStrnicmp(const char* str1, const char* str2, size_t count); -CIMGUI_API void igImStrncpy(char* dst, const char* src, size_t count); -CIMGUI_API char* igImStrdup(const char* str); -CIMGUI_API char* igImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); -CIMGUI_API const char* igImStrchrRange(const char* str_begin, - const char* str_end, - char c); -CIMGUI_API const char* igImStreolRange(const char* str, const char* str_end); -CIMGUI_API const char* igImStristr(const char* haystack, - const char* haystack_end, - const char* needle, - const char* needle_end); -CIMGUI_API void igImStrTrimBlanks(char* str); -CIMGUI_API const char* igImStrSkipBlank(const char* str); -CIMGUI_API int igImStrlenW(const ImWchar* str); -CIMGUI_API const ImWchar* igImStrbolW(const ImWchar* buf_mid_line, - const ImWchar* buf_begin); -CIMGUI_API char igImToUpper(char c); -CIMGUI_API bool igImCharIsBlankA(char c); -CIMGUI_API bool igImCharIsBlankW(unsigned int c); -CIMGUI_API int igImFormatString(char* buf, - size_t buf_size, - const char* fmt, - ...); -CIMGUI_API int igImFormatStringV(char* buf, - size_t buf_size, - const char* fmt, - va_list args); -CIMGUI_API void igImFormatStringToTempBuffer(const char** out_buf, - const char** out_buf_end, - const char* fmt, - ...); -CIMGUI_API void igImFormatStringToTempBufferV(const char** out_buf, - const char** out_buf_end, - const char* fmt, - va_list args); -CIMGUI_API const char* igImParseFormatFindStart(const char* format); -CIMGUI_API const char* igImParseFormatFindEnd(const char* format); -CIMGUI_API const char* igImParseFormatTrimDecorations(const char* format, - char* buf, - size_t buf_size); -CIMGUI_API void igImParseFormatSanitizeForPrinting(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size); -CIMGUI_API const char* igImParseFormatSanitizeForScanning(const char* fmt_in, - char* fmt_out, - size_t fmt_out_size); -CIMGUI_API int igImParseFormatPrecision(const char* format, int default_value); -CIMGUI_API const char* igImTextCharToUtf8(char out_buf[5], unsigned int c); -CIMGUI_API int igImTextStrToUtf8(char* out_buf, - int out_buf_size, - const ImWchar* in_text, - const ImWchar* in_text_end); -CIMGUI_API int igImTextCharFromUtf8(unsigned int* out_char, - const char* in_text, - const char* in_text_end); -CIMGUI_API int igImTextStrFromUtf8(ImWchar* out_buf, - int out_buf_size, - const char* in_text, - const char* in_text_end, - const char** in_remaining); -CIMGUI_API int igImTextCountCharsFromUtf8(const char* in_text, - const char* in_text_end); -CIMGUI_API int igImTextCountUtf8BytesFromChar(const char* in_text, - const char* in_text_end); -CIMGUI_API int igImTextCountUtf8BytesFromStr(const ImWchar* in_text, - const ImWchar* in_text_end); -CIMGUI_API const char* igImTextFindPreviousUtf8Codepoint( - const char* in_text_start, - const char* in_text_curr); -CIMGUI_API int igImTextCountLines(const char* in_text, const char* in_text_end); -CIMGUI_API ImFileHandle igImFileOpen(const char* filename, const char* mode); -CIMGUI_API bool igImFileClose(ImFileHandle file); -CIMGUI_API ImU64 igImFileGetSize(ImFileHandle file); -CIMGUI_API ImU64 igImFileRead(void* data, - ImU64 size, - ImU64 count, - ImFileHandle file); -CIMGUI_API ImU64 igImFileWrite(const void* data, - ImU64 size, - ImU64 count, - ImFileHandle file); -CIMGUI_API void* igImFileLoadToMemory(const char* filename, - const char* mode, - size_t* out_file_size, - int padding_bytes); -CIMGUI_API float igImPow_Float(float x, float y); -CIMGUI_API double igImPow_double(double x, double y); -CIMGUI_API float igImLog_Float(float x); -CIMGUI_API double igImLog_double(double x); -CIMGUI_API int igImAbs_Int(int x); -CIMGUI_API float igImAbs_Float(float x); -CIMGUI_API double igImAbs_double(double x); -CIMGUI_API float igImSign_Float(float x); -CIMGUI_API double igImSign_double(double x); -CIMGUI_API float igImRsqrt_Float(float x); -CIMGUI_API double igImRsqrt_double(double x); -CIMGUI_API void igImMin(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs); -CIMGUI_API void igImMax(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs); -CIMGUI_API void igImClamp(ImVec2* pOut, - const ImVec2 v, - const ImVec2 mn, - ImVec2 mx); -CIMGUI_API void igImLerp_Vec2Float(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - float t); -CIMGUI_API void igImLerp_Vec2Vec2(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 t); -CIMGUI_API void igImLerp_Vec4(ImVec4* pOut, - const ImVec4 a, - const ImVec4 b, - float t); -CIMGUI_API float igImSaturate(float f); -CIMGUI_API float igImLengthSqr_Vec2(const ImVec2 lhs); -CIMGUI_API float igImLengthSqr_Vec4(const ImVec4 lhs); -CIMGUI_API float igImInvLength(const ImVec2 lhs, float fail_value); -CIMGUI_API float igImTrunc_Float(float f); -CIMGUI_API void igImTrunc_Vec2(ImVec2* pOut, const ImVec2 v); -CIMGUI_API float igImFloor_Float(float f); -CIMGUI_API void igImFloor_Vec2(ImVec2* pOut, const ImVec2 v); -CIMGUI_API int igImModPositive(int a, int b); -CIMGUI_API float igImDot(const ImVec2 a, const ImVec2 b); -CIMGUI_API void igImRotate(ImVec2* pOut, - const ImVec2 v, - float cos_a, - float sin_a); -CIMGUI_API float igImLinearSweep(float current, float target, float speed); -CIMGUI_API void igImMul(ImVec2* pOut, const ImVec2 lhs, const ImVec2 rhs); -CIMGUI_API bool igImIsFloatAboveGuaranteedIntegerPrecision(float f); -CIMGUI_API float igImExponentialMovingAverage(float avg, float sample, int n); -CIMGUI_API void igImBezierCubicCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - float t); -CIMGUI_API void igImBezierCubicClosestPoint(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - int num_segments); -CIMGUI_API void igImBezierCubicClosestPointCasteljau(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - const ImVec2 p4, - const ImVec2 p, - float tess_tol); -CIMGUI_API void igImBezierQuadraticCalc(ImVec2* pOut, - const ImVec2 p1, - const ImVec2 p2, - const ImVec2 p3, - float t); -CIMGUI_API void igImLineClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 p); -CIMGUI_API bool igImTriangleContainsPoint(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p); -CIMGUI_API void igImTriangleClosestPoint(ImVec2* pOut, - const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p); -CIMGUI_API void igImTriangleBarycentricCoords(const ImVec2 a, - const ImVec2 b, - const ImVec2 c, - const ImVec2 p, - float* out_u, - float* out_v, - float* out_w); -CIMGUI_API float igImTriangleArea(const ImVec2 a, - const ImVec2 b, - const ImVec2 c); -CIMGUI_API bool igImTriangleIsClockwise(const ImVec2 a, - const ImVec2 b, - const ImVec2 c); -CIMGUI_API ImVec1* ImVec1_ImVec1_Nil(void); -CIMGUI_API void ImVec1_destroy(ImVec1* self); -CIMGUI_API ImVec1* ImVec1_ImVec1_Float(float _x); -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Nil(void); -CIMGUI_API void ImVec2ih_destroy(ImVec2ih* self); -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_short(short _x, short _y); -CIMGUI_API ImVec2ih* ImVec2ih_ImVec2ih_Vec2(const ImVec2 rhs); -CIMGUI_API ImRect* ImRect_ImRect_Nil(void); -CIMGUI_API void ImRect_destroy(ImRect* self); -CIMGUI_API ImRect* ImRect_ImRect_Vec2(const ImVec2 min, const ImVec2 max); -CIMGUI_API ImRect* ImRect_ImRect_Vec4(const ImVec4 v); -CIMGUI_API ImRect* ImRect_ImRect_Float(float x1, float y1, float x2, float y2); -CIMGUI_API void ImRect_GetCenter(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetSize(ImVec2* pOut, ImRect* self); -CIMGUI_API float ImRect_GetWidth(ImRect* self); -CIMGUI_API float ImRect_GetHeight(ImRect* self); -CIMGUI_API float ImRect_GetArea(ImRect* self); -CIMGUI_API void ImRect_GetTL(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetTR(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetBL(ImVec2* pOut, ImRect* self); -CIMGUI_API void ImRect_GetBR(ImVec2* pOut, ImRect* self); -CIMGUI_API bool ImRect_Contains_Vec2(ImRect* self, const ImVec2 p); -CIMGUI_API bool ImRect_Contains_Rect(ImRect* self, const ImRect r); -CIMGUI_API bool ImRect_ContainsWithPad(ImRect* self, - const ImVec2 p, - const ImVec2 pad); -CIMGUI_API bool ImRect_Overlaps(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_Add_Vec2(ImRect* self, const ImVec2 p); -CIMGUI_API void ImRect_Add_Rect(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_Expand_Float(ImRect* self, const float amount); -CIMGUI_API void ImRect_Expand_Vec2(ImRect* self, const ImVec2 amount); -CIMGUI_API void ImRect_Translate(ImRect* self, const ImVec2 d); -CIMGUI_API void ImRect_TranslateX(ImRect* self, float dx); -CIMGUI_API void ImRect_TranslateY(ImRect* self, float dy); -CIMGUI_API void ImRect_ClipWith(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_ClipWithFull(ImRect* self, const ImRect r); -CIMGUI_API void ImRect_Floor(ImRect* self); -CIMGUI_API bool ImRect_IsInverted(ImRect* self); -CIMGUI_API void ImRect_ToVec4(ImVec4* pOut, ImRect* self); -CIMGUI_API size_t igImBitArrayGetStorageSizeInBytes(int bitcount); -CIMGUI_API void igImBitArrayClearAllBits(ImU32* arr, int bitcount); -CIMGUI_API bool igImBitArrayTestBit(const ImU32* arr, int n); -CIMGUI_API void igImBitArrayClearBit(ImU32* arr, int n); -CIMGUI_API void igImBitArraySetBit(ImU32* arr, int n); -CIMGUI_API void igImBitArraySetBitRange(ImU32* arr, int n, int n2); -CIMGUI_API void ImBitVector_Create(ImBitVector* self, int sz); -CIMGUI_API void ImBitVector_Clear(ImBitVector* self); -CIMGUI_API bool ImBitVector_TestBit(ImBitVector* self, int n); -CIMGUI_API void ImBitVector_SetBit(ImBitVector* self, int n); -CIMGUI_API void ImBitVector_ClearBit(ImBitVector* self, int n); -CIMGUI_API void ImGuiTextIndex_clear(ImGuiTextIndex* self); -CIMGUI_API int ImGuiTextIndex_size(ImGuiTextIndex* self); -CIMGUI_API const char* ImGuiTextIndex_get_line_begin(ImGuiTextIndex* self, - const char* base, - int n); -CIMGUI_API const char* ImGuiTextIndex_get_line_end(ImGuiTextIndex* self, - const char* base, - int n); -CIMGUI_API void ImGuiTextIndex_append(ImGuiTextIndex* self, - const char* base, - int old_size, - int new_size); -CIMGUI_API ImDrawListSharedData* ImDrawListSharedData_ImDrawListSharedData( - void); -CIMGUI_API void ImDrawListSharedData_destroy(ImDrawListSharedData* self); -CIMGUI_API void ImDrawListSharedData_SetCircleTessellationMaxError( - ImDrawListSharedData* self, - float max_error); -CIMGUI_API ImDrawDataBuilder* ImDrawDataBuilder_ImDrawDataBuilder(void); -CIMGUI_API void ImDrawDataBuilder_destroy(ImDrawDataBuilder* self); -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Int(ImGuiStyleVar idx, - int v); -CIMGUI_API void ImGuiStyleMod_destroy(ImGuiStyleMod* self); -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Float(ImGuiStyleVar idx, - float v); -CIMGUI_API ImGuiStyleMod* ImGuiStyleMod_ImGuiStyleMod_Vec2(ImGuiStyleVar idx, - ImVec2 v); -CIMGUI_API ImGuiComboPreviewData* ImGuiComboPreviewData_ImGuiComboPreviewData( - void); -CIMGUI_API void ImGuiComboPreviewData_destroy(ImGuiComboPreviewData* self); -CIMGUI_API ImGuiMenuColumns* ImGuiMenuColumns_ImGuiMenuColumns(void); -CIMGUI_API void ImGuiMenuColumns_destroy(ImGuiMenuColumns* self); -CIMGUI_API void ImGuiMenuColumns_Update(ImGuiMenuColumns* self, - float spacing, - bool window_reappearing); -CIMGUI_API float ImGuiMenuColumns_DeclColumns(ImGuiMenuColumns* self, - float w_icon, - float w_label, - float w_shortcut, - float w_mark); -CIMGUI_API void ImGuiMenuColumns_CalcNextTotalWidth(ImGuiMenuColumns* self, - bool update_offsets); -CIMGUI_API ImGuiInputTextDeactivatedState* -ImGuiInputTextDeactivatedState_ImGuiInputTextDeactivatedState(void); -CIMGUI_API void ImGuiInputTextDeactivatedState_destroy( - ImGuiInputTextDeactivatedState* self); -CIMGUI_API void ImGuiInputTextDeactivatedState_ClearFreeMemory( - ImGuiInputTextDeactivatedState* self); -CIMGUI_API ImGuiInputTextState* ImGuiInputTextState_ImGuiInputTextState(void); -CIMGUI_API void ImGuiInputTextState_destroy(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ClearText(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ClearFreeMemory(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetUndoAvailCount(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetRedoAvailCount(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_OnKeyPressed(ImGuiInputTextState* self, - int key); -CIMGUI_API void ImGuiInputTextState_CursorAnimReset(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_CursorClamp(ImGuiInputTextState* self); -CIMGUI_API bool ImGuiInputTextState_HasSelection(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ClearSelection(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetCursorPos(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetSelectionStart(ImGuiInputTextState* self); -CIMGUI_API int ImGuiInputTextState_GetSelectionEnd(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_SelectAll(ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndSelectAll( - ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndKeepSelection( - ImGuiInputTextState* self); -CIMGUI_API void ImGuiInputTextState_ReloadUserBufAndMoveToEnd( - ImGuiInputTextState* self); -CIMGUI_API ImGuiNextWindowData* ImGuiNextWindowData_ImGuiNextWindowData(void); -CIMGUI_API void ImGuiNextWindowData_destroy(ImGuiNextWindowData* self); -CIMGUI_API void ImGuiNextWindowData_ClearFlags(ImGuiNextWindowData* self); -CIMGUI_API ImGuiNextItemData* ImGuiNextItemData_ImGuiNextItemData(void); -CIMGUI_API void ImGuiNextItemData_destroy(ImGuiNextItemData* self); -CIMGUI_API void ImGuiNextItemData_ClearFlags(ImGuiNextItemData* self); -CIMGUI_API ImGuiLastItemData* ImGuiLastItemData_ImGuiLastItemData(void); -CIMGUI_API void ImGuiLastItemData_destroy(ImGuiLastItemData* self); -CIMGUI_API ImGuiStackSizes* ImGuiStackSizes_ImGuiStackSizes(void); -CIMGUI_API void ImGuiStackSizes_destroy(ImGuiStackSizes* self); -CIMGUI_API void ImGuiStackSizes_SetToContextState(ImGuiStackSizes* self, - ImGuiContext* ctx); -CIMGUI_API void ImGuiStackSizes_CompareWithContextState(ImGuiStackSizes* self, - ImGuiContext* ctx); -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Ptr(void* ptr); -CIMGUI_API void ImGuiPtrOrIndex_destroy(ImGuiPtrOrIndex* self); -CIMGUI_API ImGuiPtrOrIndex* ImGuiPtrOrIndex_ImGuiPtrOrIndex_Int(int index); -CIMGUI_API void* ImGuiDataVarInfo_GetVarPtr(ImGuiDataVarInfo* self, - void* parent); -CIMGUI_API ImGuiPopupData* ImGuiPopupData_ImGuiPopupData(void); -CIMGUI_API void ImGuiPopupData_destroy(ImGuiPopupData* self); -CIMGUI_API ImGuiInputEvent* ImGuiInputEvent_ImGuiInputEvent(void); -CIMGUI_API void ImGuiInputEvent_destroy(ImGuiInputEvent* self); -CIMGUI_API ImGuiKeyRoutingData* ImGuiKeyRoutingData_ImGuiKeyRoutingData(void); -CIMGUI_API void ImGuiKeyRoutingData_destroy(ImGuiKeyRoutingData* self); -CIMGUI_API ImGuiKeyRoutingTable* ImGuiKeyRoutingTable_ImGuiKeyRoutingTable( - void); -CIMGUI_API void ImGuiKeyRoutingTable_destroy(ImGuiKeyRoutingTable* self); -CIMGUI_API void ImGuiKeyRoutingTable_Clear(ImGuiKeyRoutingTable* self); -CIMGUI_API ImGuiKeyOwnerData* ImGuiKeyOwnerData_ImGuiKeyOwnerData(void); -CIMGUI_API void ImGuiKeyOwnerData_destroy(ImGuiKeyOwnerData* self); -CIMGUI_API ImGuiListClipperRange ImGuiListClipperRange_FromIndices(int min, - int max); -CIMGUI_API ImGuiListClipperRange -ImGuiListClipperRange_FromPositions(float y1, - float y2, - int off_min, - int off_max); -CIMGUI_API ImGuiListClipperData* ImGuiListClipperData_ImGuiListClipperData( - void); -CIMGUI_API void ImGuiListClipperData_destroy(ImGuiListClipperData* self); -CIMGUI_API void ImGuiListClipperData_Reset(ImGuiListClipperData* self, - ImGuiListClipper* clipper); -CIMGUI_API ImGuiNavItemData* ImGuiNavItemData_ImGuiNavItemData(void); -CIMGUI_API void ImGuiNavItemData_destroy(ImGuiNavItemData* self); -CIMGUI_API void ImGuiNavItemData_Clear(ImGuiNavItemData* self); -CIMGUI_API ImGuiTypingSelectState* -ImGuiTypingSelectState_ImGuiTypingSelectState(void); -CIMGUI_API void ImGuiTypingSelectState_destroy(ImGuiTypingSelectState* self); -CIMGUI_API void ImGuiTypingSelectState_Clear(ImGuiTypingSelectState* self); -CIMGUI_API ImGuiOldColumnData* ImGuiOldColumnData_ImGuiOldColumnData(void); -CIMGUI_API void ImGuiOldColumnData_destroy(ImGuiOldColumnData* self); -CIMGUI_API ImGuiOldColumns* ImGuiOldColumns_ImGuiOldColumns(void); -CIMGUI_API void ImGuiOldColumns_destroy(ImGuiOldColumns* self); -CIMGUI_API ImGuiDockNode* ImGuiDockNode_ImGuiDockNode(ImGuiID id); -CIMGUI_API void ImGuiDockNode_destroy(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsRootNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsDockSpace(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsFloatingNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsCentralNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsHiddenTabBar(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsNoTabBar(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsSplitNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsLeafNode(ImGuiDockNode* self); -CIMGUI_API bool ImGuiDockNode_IsEmpty(ImGuiDockNode* self); -CIMGUI_API void ImGuiDockNode_Rect(ImRect* pOut, ImGuiDockNode* self); -CIMGUI_API void ImGuiDockNode_SetLocalFlags(ImGuiDockNode* self, - ImGuiDockNodeFlags flags); -CIMGUI_API void ImGuiDockNode_UpdateMergedFlags(ImGuiDockNode* self); -CIMGUI_API ImGuiDockContext* ImGuiDockContext_ImGuiDockContext(void); -CIMGUI_API void ImGuiDockContext_destroy(ImGuiDockContext* self); -CIMGUI_API ImGuiViewportP* ImGuiViewportP_ImGuiViewportP(void); -CIMGUI_API void ImGuiViewportP_destroy(ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_ClearRequestFlags(ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_CalcWorkRectPos(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min); -CIMGUI_API void ImGuiViewportP_CalcWorkRectSize(ImVec2* pOut, - ImGuiViewportP* self, - const ImVec2 off_min, - const ImVec2 off_max); -CIMGUI_API void ImGuiViewportP_UpdateWorkRect(ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_GetMainRect(ImRect* pOut, ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_GetWorkRect(ImRect* pOut, ImGuiViewportP* self); -CIMGUI_API void ImGuiViewportP_GetBuildWorkRect(ImRect* pOut, - ImGuiViewportP* self); -CIMGUI_API ImGuiWindowSettings* ImGuiWindowSettings_ImGuiWindowSettings(void); -CIMGUI_API void ImGuiWindowSettings_destroy(ImGuiWindowSettings* self); -CIMGUI_API char* ImGuiWindowSettings_GetName(ImGuiWindowSettings* self); -CIMGUI_API ImGuiSettingsHandler* ImGuiSettingsHandler_ImGuiSettingsHandler( - void); -CIMGUI_API void ImGuiSettingsHandler_destroy(ImGuiSettingsHandler* self); -CIMGUI_API ImGuiDebugAllocInfo* ImGuiDebugAllocInfo_ImGuiDebugAllocInfo(void); -CIMGUI_API void ImGuiDebugAllocInfo_destroy(ImGuiDebugAllocInfo* self); -CIMGUI_API ImGuiStackLevelInfo* ImGuiStackLevelInfo_ImGuiStackLevelInfo(void); -CIMGUI_API void ImGuiStackLevelInfo_destroy(ImGuiStackLevelInfo* self); -CIMGUI_API ImGuiIDStackTool* ImGuiIDStackTool_ImGuiIDStackTool(void); -CIMGUI_API void ImGuiIDStackTool_destroy(ImGuiIDStackTool* self); -CIMGUI_API ImGuiContextHook* ImGuiContextHook_ImGuiContextHook(void); -CIMGUI_API void ImGuiContextHook_destroy(ImGuiContextHook* self); -CIMGUI_API ImGuiContext* ImGuiContext_ImGuiContext( - ImFontAtlas* shared_font_atlas); -CIMGUI_API void ImGuiContext_destroy(ImGuiContext* self); -CIMGUI_API ImGuiWindow* ImGuiWindow_ImGuiWindow(ImGuiContext* context, - const char* name); -CIMGUI_API void ImGuiWindow_destroy(ImGuiWindow* self); -CIMGUI_API ImGuiID ImGuiWindow_GetID_Str(ImGuiWindow* self, - const char* str, - const char* str_end); -CIMGUI_API ImGuiID ImGuiWindow_GetID_Ptr(ImGuiWindow* self, const void* ptr); -CIMGUI_API ImGuiID ImGuiWindow_GetID_Int(ImGuiWindow* self, int n); -CIMGUI_API ImGuiID ImGuiWindow_GetIDFromRectangle(ImGuiWindow* self, - const ImRect r_abs); -CIMGUI_API void ImGuiWindow_Rect(ImRect* pOut, ImGuiWindow* self); -CIMGUI_API float ImGuiWindow_CalcFontSize(ImGuiWindow* self); -CIMGUI_API float ImGuiWindow_TitleBarHeight(ImGuiWindow* self); -CIMGUI_API void ImGuiWindow_TitleBarRect(ImRect* pOut, ImGuiWindow* self); -CIMGUI_API float ImGuiWindow_MenuBarHeight(ImGuiWindow* self); -CIMGUI_API void ImGuiWindow_MenuBarRect(ImRect* pOut, ImGuiWindow* self); -CIMGUI_API ImGuiTabItem* ImGuiTabItem_ImGuiTabItem(void); -CIMGUI_API void ImGuiTabItem_destroy(ImGuiTabItem* self); -CIMGUI_API ImGuiTabBar* ImGuiTabBar_ImGuiTabBar(void); -CIMGUI_API void ImGuiTabBar_destroy(ImGuiTabBar* self); -CIMGUI_API ImGuiTableColumn* ImGuiTableColumn_ImGuiTableColumn(void); -CIMGUI_API void ImGuiTableColumn_destroy(ImGuiTableColumn* self); -CIMGUI_API ImGuiTableInstanceData* -ImGuiTableInstanceData_ImGuiTableInstanceData(void); -CIMGUI_API void ImGuiTableInstanceData_destroy(ImGuiTableInstanceData* self); -CIMGUI_API ImGuiTable* ImGuiTable_ImGuiTable(void); -CIMGUI_API void ImGuiTable_destroy(ImGuiTable* self); -CIMGUI_API ImGuiTableTempData* ImGuiTableTempData_ImGuiTableTempData(void); -CIMGUI_API void ImGuiTableTempData_destroy(ImGuiTableTempData* self); -CIMGUI_API ImGuiTableColumnSettings* -ImGuiTableColumnSettings_ImGuiTableColumnSettings(void); -CIMGUI_API void ImGuiTableColumnSettings_destroy( - ImGuiTableColumnSettings* self); -CIMGUI_API ImGuiTableSettings* ImGuiTableSettings_ImGuiTableSettings(void); -CIMGUI_API void ImGuiTableSettings_destroy(ImGuiTableSettings* self); -CIMGUI_API ImGuiTableColumnSettings* ImGuiTableSettings_GetColumnSettings( - ImGuiTableSettings* self); -CIMGUI_API ImGuiWindow* igGetCurrentWindowRead(void); -CIMGUI_API ImGuiWindow* igGetCurrentWindow(void); -CIMGUI_API ImGuiWindow* igFindWindowByID(ImGuiID id); -CIMGUI_API ImGuiWindow* igFindWindowByName(const char* name); -CIMGUI_API void igUpdateWindowParentAndRootLinks(ImGuiWindow* window, - ImGuiWindowFlags flags, - ImGuiWindow* parent_window); -CIMGUI_API void igUpdateWindowSkipRefresh(ImGuiWindow* window); -CIMGUI_API void igCalcWindowNextAutoFitSize(ImVec2* pOut, ImGuiWindow* window); -CIMGUI_API bool igIsWindowChildOf(ImGuiWindow* window, - ImGuiWindow* potential_parent, - bool popup_hierarchy, - bool dock_hierarchy); -CIMGUI_API bool igIsWindowWithinBeginStackOf(ImGuiWindow* window, - ImGuiWindow* potential_parent); -CIMGUI_API bool igIsWindowAbove(ImGuiWindow* potential_above, - ImGuiWindow* potential_below); -CIMGUI_API bool igIsWindowNavFocusable(ImGuiWindow* window); -CIMGUI_API void igSetWindowPos_WindowPtr(ImGuiWindow* window, - const ImVec2 pos, - ImGuiCond cond); -CIMGUI_API void igSetWindowSize_WindowPtr(ImGuiWindow* window, - const ImVec2 size, - ImGuiCond cond); -CIMGUI_API void igSetWindowCollapsed_WindowPtr(ImGuiWindow* window, - bool collapsed, - ImGuiCond cond); -CIMGUI_API void igSetWindowHitTestHole(ImGuiWindow* window, - const ImVec2 pos, - const ImVec2 size); -CIMGUI_API void igSetWindowHiddenAndSkipItemsForCurrentFrame( - ImGuiWindow* window); -CIMGUI_API void igSetWindowParentWindowForFocusRoute( - ImGuiWindow* window, - ImGuiWindow* parent_window); -CIMGUI_API void igWindowRectAbsToRel(ImRect* pOut, - ImGuiWindow* window, - const ImRect r); -CIMGUI_API void igWindowRectRelToAbs(ImRect* pOut, - ImGuiWindow* window, - const ImRect r); -CIMGUI_API void igWindowPosRelToAbs(ImVec2* pOut, - ImGuiWindow* window, - const ImVec2 p); -CIMGUI_API void igFocusWindow(ImGuiWindow* window, - ImGuiFocusRequestFlags flags); -CIMGUI_API void igFocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, - ImGuiWindow* ignore_window, - ImGuiViewport* filter_viewport, - ImGuiFocusRequestFlags flags); -CIMGUI_API void igBringWindowToFocusFront(ImGuiWindow* window); -CIMGUI_API void igBringWindowToDisplayFront(ImGuiWindow* window); -CIMGUI_API void igBringWindowToDisplayBack(ImGuiWindow* window); -CIMGUI_API void igBringWindowToDisplayBehind(ImGuiWindow* window, - ImGuiWindow* above_window); -CIMGUI_API int igFindWindowDisplayIndex(ImGuiWindow* window); -CIMGUI_API ImGuiWindow* igFindBottomMostVisibleWindowWithinBeginStack( - ImGuiWindow* window); -CIMGUI_API void igSetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); -CIMGUI_API void igSetCurrentFont(ImFont* font); -CIMGUI_API ImFont* igGetDefaultFont(void); -CIMGUI_API ImDrawList* igGetForegroundDrawList_WindowPtr(ImGuiWindow* window); -CIMGUI_API void igAddDrawListToDrawDataEx(ImDrawData* draw_data, - ImVector_ImDrawListPtr* out_list, - ImDrawList* draw_list); -CIMGUI_API void igInitialize(void); -CIMGUI_API void igShutdown(void); -CIMGUI_API void igUpdateInputEvents(bool trickle_fast_inputs); -CIMGUI_API void igUpdateHoveredWindowAndCaptureFlags(void); -CIMGUI_API void igStartMouseMovingWindow(ImGuiWindow* window); -CIMGUI_API void igStartMouseMovingWindowOrNode(ImGuiWindow* window, - ImGuiDockNode* node, - bool undock); -CIMGUI_API void igUpdateMouseMovingWindowNewFrame(void); -CIMGUI_API void igUpdateMouseMovingWindowEndFrame(void); -CIMGUI_API ImGuiID igAddContextHook(ImGuiContext* context, - const ImGuiContextHook* hook); -CIMGUI_API void igRemoveContextHook(ImGuiContext* context, - ImGuiID hook_to_remove); -CIMGUI_API void igCallContextHooks(ImGuiContext* context, - ImGuiContextHookType type); -CIMGUI_API void igTranslateWindowsInViewport(ImGuiViewportP* viewport, - const ImVec2 old_pos, - const ImVec2 new_pos); -CIMGUI_API void igScaleWindowsInViewport(ImGuiViewportP* viewport, float scale); -CIMGUI_API void igDestroyPlatformWindow(ImGuiViewportP* viewport); -CIMGUI_API void igSetWindowViewport(ImGuiWindow* window, - ImGuiViewportP* viewport); -CIMGUI_API void igSetCurrentViewport(ImGuiWindow* window, - ImGuiViewportP* viewport); -CIMGUI_API const ImGuiPlatformMonitor* igGetViewportPlatformMonitor( - ImGuiViewport* viewport); -CIMGUI_API ImGuiViewportP* igFindHoveredViewportFromPlatformWindowStack( - const ImVec2 mouse_platform_pos); -CIMGUI_API void igMarkIniSettingsDirty_Nil(void); -CIMGUI_API void igMarkIniSettingsDirty_WindowPtr(ImGuiWindow* window); -CIMGUI_API void igClearIniSettings(void); -CIMGUI_API void igAddSettingsHandler(const ImGuiSettingsHandler* handler); -CIMGUI_API void igRemoveSettingsHandler(const char* type_name); -CIMGUI_API ImGuiSettingsHandler* igFindSettingsHandler(const char* type_name); -CIMGUI_API ImGuiWindowSettings* igCreateNewWindowSettings(const char* name); -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByID(ImGuiID id); -CIMGUI_API ImGuiWindowSettings* igFindWindowSettingsByWindow( - ImGuiWindow* window); -CIMGUI_API void igClearWindowSettings(const char* name); -CIMGUI_API void igLocalizeRegisterEntries(const ImGuiLocEntry* entries, - int count); -CIMGUI_API const char* igLocalizeGetMsg(ImGuiLocKey key); -CIMGUI_API void igSetScrollX_WindowPtr(ImGuiWindow* window, float scroll_x); -CIMGUI_API void igSetScrollY_WindowPtr(ImGuiWindow* window, float scroll_y); -CIMGUI_API void igSetScrollFromPosX_WindowPtr(ImGuiWindow* window, - float local_x, - float center_x_ratio); -CIMGUI_API void igSetScrollFromPosY_WindowPtr(ImGuiWindow* window, - float local_y, - float center_y_ratio); -CIMGUI_API void igScrollToItem(ImGuiScrollFlags flags); -CIMGUI_API void igScrollToRect(ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags); -CIMGUI_API void igScrollToRectEx(ImVec2* pOut, - ImGuiWindow* window, - const ImRect rect, - ImGuiScrollFlags flags); -CIMGUI_API void igScrollToBringRectIntoView(ImGuiWindow* window, - const ImRect rect); -CIMGUI_API ImGuiItemStatusFlags igGetItemStatusFlags(void); -CIMGUI_API ImGuiItemFlags igGetItemFlags(void); -CIMGUI_API ImGuiID igGetActiveID(void); -CIMGUI_API ImGuiID igGetFocusID(void); -CIMGUI_API void igSetActiveID(ImGuiID id, ImGuiWindow* window); -CIMGUI_API void igSetFocusID(ImGuiID id, ImGuiWindow* window); -CIMGUI_API void igClearActiveID(void); -CIMGUI_API ImGuiID igGetHoveredID(void); -CIMGUI_API void igSetHoveredID(ImGuiID id); -CIMGUI_API void igKeepAliveID(ImGuiID id); -CIMGUI_API void igMarkItemEdited(ImGuiID id); -CIMGUI_API void igPushOverrideID(ImGuiID id); -CIMGUI_API ImGuiID igGetIDWithSeed_Str(const char* str_id_begin, - const char* str_id_end, - ImGuiID seed); -CIMGUI_API ImGuiID igGetIDWithSeed_Int(int n, ImGuiID seed); -CIMGUI_API void igItemSize_Vec2(const ImVec2 size, float text_baseline_y); -CIMGUI_API void igItemSize_Rect(const ImRect bb, float text_baseline_y); -CIMGUI_API bool igItemAdd(const ImRect bb, - ImGuiID id, - const ImRect* nav_bb, - ImGuiItemFlags extra_flags); -CIMGUI_API bool igItemHoverable(const ImRect bb, - ImGuiID id, - ImGuiItemFlags item_flags); -CIMGUI_API bool igIsWindowContentHoverable(ImGuiWindow* window, - ImGuiHoveredFlags flags); -CIMGUI_API bool igIsClippedEx(const ImRect bb, ImGuiID id); -CIMGUI_API void igSetLastItemData(ImGuiID item_id, - ImGuiItemFlags in_flags, - ImGuiItemStatusFlags status_flags, - const ImRect item_rect); -CIMGUI_API void igCalcItemSize(ImVec2* pOut, - ImVec2 size, - float default_w, - float default_h); -CIMGUI_API float igCalcWrapWidthForPos(const ImVec2 pos, float wrap_pos_x); -CIMGUI_API void igPushMultiItemsWidths(int components, float width_full); -CIMGUI_API bool igIsItemToggledSelection(void); -CIMGUI_API void igGetContentRegionMaxAbs(ImVec2* pOut); -CIMGUI_API void igShrinkWidths(ImGuiShrinkWidthItem* items, - int count, - float width_excess); -CIMGUI_API void igPushItemFlag(ImGuiItemFlags option, bool enabled); -CIMGUI_API void igPopItemFlag(void); -CIMGUI_API const ImGuiDataVarInfo* igGetStyleVarInfo(ImGuiStyleVar idx); -CIMGUI_API void igLogBegin(ImGuiLogType type, int auto_open_depth); -CIMGUI_API void igLogToBuffer(int auto_open_depth); -CIMGUI_API void igLogRenderedText(const ImVec2* ref_pos, - const char* text, - const char* text_end); -CIMGUI_API void igLogSetNextTextDecoration(const char* prefix, - const char* suffix); -CIMGUI_API bool igBeginChildEx(const char* name, - ImGuiID id, - const ImVec2 size_arg, - ImGuiChildFlags child_flags, - ImGuiWindowFlags window_flags); -CIMGUI_API void igOpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags); -CIMGUI_API void igClosePopupToLevel(int remaining, - bool restore_focus_to_window_under_popup); -CIMGUI_API void igClosePopupsOverWindow( - ImGuiWindow* ref_window, - bool restore_focus_to_window_under_popup); -CIMGUI_API void igClosePopupsExceptModals(void); -CIMGUI_API bool igIsPopupOpen_ID(ImGuiID id, ImGuiPopupFlags popup_flags); -CIMGUI_API bool igBeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); -CIMGUI_API bool igBeginTooltipEx(ImGuiTooltipFlags tooltip_flags, - ImGuiWindowFlags extra_window_flags); -CIMGUI_API bool igBeginTooltipHidden(void); -CIMGUI_API void igGetPopupAllowedExtentRect(ImRect* pOut, ImGuiWindow* window); -CIMGUI_API ImGuiWindow* igGetTopMostPopupModal(void); -CIMGUI_API ImGuiWindow* igGetTopMostAndVisiblePopupModal(void); -CIMGUI_API ImGuiWindow* igFindBlockingModal(ImGuiWindow* window); -CIMGUI_API void igFindBestWindowPosForPopup(ImVec2* pOut, ImGuiWindow* window); -CIMGUI_API void igFindBestWindowPosForPopupEx(ImVec2* pOut, - const ImVec2 ref_pos, - const ImVec2 size, - ImGuiDir* last_dir, - const ImRect r_outer, - const ImRect r_avoid, - ImGuiPopupPositionPolicy policy); -CIMGUI_API bool igBeginViewportSideBar(const char* name, - ImGuiViewport* viewport, - ImGuiDir dir, - float size, - ImGuiWindowFlags window_flags); -CIMGUI_API bool igBeginMenuEx(const char* label, - const char* icon, - bool enabled); -CIMGUI_API bool igMenuItemEx(const char* label, - const char* icon, - const char* shortcut, - bool selected, - bool enabled); -CIMGUI_API bool igBeginComboPopup(ImGuiID popup_id, - const ImRect bb, - ImGuiComboFlags flags); -CIMGUI_API bool igBeginComboPreview(void); -CIMGUI_API void igEndComboPreview(void); -CIMGUI_API void igNavInitWindow(ImGuiWindow* window, bool force_reinit); -CIMGUI_API void igNavInitRequestApplyResult(void); -CIMGUI_API bool igNavMoveRequestButNoResultYet(void); -CIMGUI_API void igNavMoveRequestSubmit(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags); -CIMGUI_API void igNavMoveRequestForward(ImGuiDir move_dir, - ImGuiDir clip_dir, - ImGuiNavMoveFlags move_flags, - ImGuiScrollFlags scroll_flags); -CIMGUI_API void igNavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); -CIMGUI_API void igNavMoveRequestResolveWithPastTreeNode( - ImGuiNavItemData* result, - ImGuiNavTreeNodeData* tree_node_data); -CIMGUI_API void igNavMoveRequestCancel(void); -CIMGUI_API void igNavMoveRequestApplyResult(void); -CIMGUI_API void igNavMoveRequestTryWrapping(ImGuiWindow* window, - ImGuiNavMoveFlags move_flags); -CIMGUI_API void igNavHighlightActivated(ImGuiID id); -CIMGUI_API void igNavClearPreferredPosForAxis(ImGuiAxis axis); -CIMGUI_API void igNavRestoreHighlightAfterMove(void); -CIMGUI_API void igNavUpdateCurrentWindowIsScrollPushableX(void); -CIMGUI_API void igSetNavWindow(ImGuiWindow* window); -CIMGUI_API void igSetNavID(ImGuiID id, - ImGuiNavLayer nav_layer, - ImGuiID focus_scope_id, - const ImRect rect_rel); -CIMGUI_API void igSetNavFocusScope(ImGuiID focus_scope_id); -CIMGUI_API void igFocusItem(void); -CIMGUI_API void igActivateItemByID(ImGuiID id); -CIMGUI_API bool igIsNamedKey(ImGuiKey key); -CIMGUI_API bool igIsNamedKeyOrModKey(ImGuiKey key); -CIMGUI_API bool igIsLegacyKey(ImGuiKey key); -CIMGUI_API bool igIsKeyboardKey(ImGuiKey key); -CIMGUI_API bool igIsGamepadKey(ImGuiKey key); -CIMGUI_API bool igIsMouseKey(ImGuiKey key); -CIMGUI_API bool igIsAliasKey(ImGuiKey key); -CIMGUI_API bool igIsModKey(ImGuiKey key); -CIMGUI_API ImGuiKeyChord igFixupKeyChord(ImGuiContext* ctx, - ImGuiKeyChord key_chord); -CIMGUI_API ImGuiKey igConvertSingleModFlagToKey(ImGuiContext* ctx, - ImGuiKey key); -CIMGUI_API ImGuiKeyData* igGetKeyData_ContextPtr(ImGuiContext* ctx, - ImGuiKey key); -CIMGUI_API ImGuiKeyData* igGetKeyData_Key(ImGuiKey key); -CIMGUI_API const char* igGetKeyChordName(ImGuiKeyChord key_chord); -CIMGUI_API ImGuiKey igMouseButtonToKey(ImGuiMouseButton button); -CIMGUI_API bool igIsMouseDragPastThreshold(ImGuiMouseButton button, - float lock_threshold); -CIMGUI_API void igGetKeyMagnitude2d(ImVec2* pOut, - ImGuiKey key_left, - ImGuiKey key_right, - ImGuiKey key_up, - ImGuiKey key_down); -CIMGUI_API float igGetNavTweakPressedAmount(ImGuiAxis axis); -CIMGUI_API int igCalcTypematicRepeatAmount(float t0, - float t1, - float repeat_delay, - float repeat_rate); -CIMGUI_API void igGetTypematicRepeatRate(ImGuiInputFlags flags, - float* repeat_delay, - float* repeat_rate); -CIMGUI_API void igTeleportMousePos(const ImVec2 pos); -CIMGUI_API void igSetActiveIdUsingAllKeyboardKeys(void); -CIMGUI_API bool igIsActiveIdUsingNavDir(ImGuiDir dir); -CIMGUI_API ImGuiID igGetKeyOwner(ImGuiKey key); -CIMGUI_API void igSetKeyOwner(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API void igSetKeyOwnersForKeyChord(ImGuiKeyChord key, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API void igSetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags); -CIMGUI_API bool igTestKeyOwner(ImGuiKey key, ImGuiID owner_id); -CIMGUI_API ImGuiKeyOwnerData* igGetKeyOwnerData(ImGuiContext* ctx, - ImGuiKey key); -CIMGUI_API bool igIsKeyDown_ID(ImGuiKey key, ImGuiID owner_id); -CIMGUI_API bool igIsKeyPressed_ID(ImGuiKey key, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igIsKeyReleased_ID(ImGuiKey key, ImGuiID owner_id); -CIMGUI_API bool igIsMouseDown_ID(ImGuiMouseButton button, ImGuiID owner_id); -CIMGUI_API bool igIsMouseClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igIsMouseReleased_ID(ImGuiMouseButton button, ImGuiID owner_id); -CIMGUI_API bool igIsMouseDoubleClicked_ID(ImGuiMouseButton button, - ImGuiID owner_id); -CIMGUI_API bool igIsKeyChordPressed_ID(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API void igSetNextItemShortcut(ImGuiKeyChord key_chord); -CIMGUI_API bool igShortcut(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igSetShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id, - ImGuiInputFlags flags); -CIMGUI_API bool igTestShortcutRouting(ImGuiKeyChord key_chord, - ImGuiID owner_id); -CIMGUI_API ImGuiKeyRoutingData* igGetShortcutRoutingData( - ImGuiKeyChord key_chord); -CIMGUI_API void igDockContextInitialize(ImGuiContext* ctx); -CIMGUI_API void igDockContextShutdown(ImGuiContext* ctx); -CIMGUI_API void igDockContextClearNodes(ImGuiContext* ctx, - ImGuiID root_id, - bool clear_settings_refs); -CIMGUI_API void igDockContextRebuildNodes(ImGuiContext* ctx); -CIMGUI_API void igDockContextNewFrameUpdateUndocking(ImGuiContext* ctx); -CIMGUI_API void igDockContextNewFrameUpdateDocking(ImGuiContext* ctx); -CIMGUI_API void igDockContextEndFrame(ImGuiContext* ctx); -CIMGUI_API ImGuiID igDockContextGenNodeID(ImGuiContext* ctx); -CIMGUI_API void igDockContextQueueDock(ImGuiContext* ctx, - ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload, - ImGuiDir split_dir, - float split_ratio, - bool split_outer); -CIMGUI_API void igDockContextQueueUndockWindow(ImGuiContext* ctx, - ImGuiWindow* window); -CIMGUI_API void igDockContextQueueUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node); -CIMGUI_API void igDockContextProcessUndockWindow( - ImGuiContext* ctx, - ImGuiWindow* window, - bool clear_persistent_docking_ref); -CIMGUI_API void igDockContextProcessUndockNode(ImGuiContext* ctx, - ImGuiDockNode* node); -CIMGUI_API bool igDockContextCalcDropPosForDocking(ImGuiWindow* target, - ImGuiDockNode* target_node, - ImGuiWindow* payload_window, - ImGuiDockNode* payload_node, - ImGuiDir split_dir, - bool split_outer, - ImVec2* out_pos); -CIMGUI_API ImGuiDockNode* igDockContextFindNodeByID(ImGuiContext* ctx, - ImGuiID id); -CIMGUI_API void igDockNodeWindowMenuHandler_Default(ImGuiContext* ctx, - ImGuiDockNode* node, - ImGuiTabBar* tab_bar); -CIMGUI_API bool igDockNodeBeginAmendTabBar(ImGuiDockNode* node); -CIMGUI_API void igDockNodeEndAmendTabBar(void); -CIMGUI_API ImGuiDockNode* igDockNodeGetRootNode(ImGuiDockNode* node); -CIMGUI_API bool igDockNodeIsInHierarchyOf(ImGuiDockNode* node, - ImGuiDockNode* parent); -CIMGUI_API int igDockNodeGetDepth(const ImGuiDockNode* node); -CIMGUI_API ImGuiID igDockNodeGetWindowMenuButtonId(const ImGuiDockNode* node); -CIMGUI_API ImGuiDockNode* igGetWindowDockNode(void); -CIMGUI_API bool igGetWindowAlwaysWantOwnTabBar(ImGuiWindow* window); -CIMGUI_API void igBeginDocked(ImGuiWindow* window, bool* p_open); -CIMGUI_API void igBeginDockableDragDropSource(ImGuiWindow* window); -CIMGUI_API void igBeginDockableDragDropTarget(ImGuiWindow* window); -CIMGUI_API void igSetWindowDock(ImGuiWindow* window, - ImGuiID dock_id, - ImGuiCond cond); -CIMGUI_API void igDockBuilderDockWindow(const char* window_name, - ImGuiID node_id); -CIMGUI_API ImGuiDockNode* igDockBuilderGetNode(ImGuiID node_id); -CIMGUI_API ImGuiDockNode* igDockBuilderGetCentralNode(ImGuiID node_id); -CIMGUI_API ImGuiID igDockBuilderAddNode(ImGuiID node_id, - ImGuiDockNodeFlags flags); -CIMGUI_API void igDockBuilderRemoveNode(ImGuiID node_id); -CIMGUI_API void igDockBuilderRemoveNodeDockedWindows(ImGuiID node_id, - bool clear_settings_refs); -CIMGUI_API void igDockBuilderRemoveNodeChildNodes(ImGuiID node_id); -CIMGUI_API void igDockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos); -CIMGUI_API void igDockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size); -CIMGUI_API ImGuiID igDockBuilderSplitNode(ImGuiID node_id, - ImGuiDir split_dir, - float size_ratio_for_node_at_dir, - ImGuiID* out_id_at_dir, - ImGuiID* out_id_at_opposite_dir); -CIMGUI_API void igDockBuilderCopyDockSpace( - ImGuiID src_dockspace_id, - ImGuiID dst_dockspace_id, - ImVector_const_charPtr* in_window_remap_pairs); -CIMGUI_API void igDockBuilderCopyNode(ImGuiID src_node_id, - ImGuiID dst_node_id, - ImVector_ImGuiID* out_node_remap_pairs); -CIMGUI_API void igDockBuilderCopyWindowSettings(const char* src_name, - const char* dst_name); -CIMGUI_API void igDockBuilderFinish(ImGuiID node_id); -CIMGUI_API void igPushFocusScope(ImGuiID id); -CIMGUI_API void igPopFocusScope(void); -CIMGUI_API ImGuiID igGetCurrentFocusScope(void); -CIMGUI_API bool igIsDragDropActive(void); -CIMGUI_API bool igBeginDragDropTargetCustom(const ImRect bb, ImGuiID id); -CIMGUI_API void igClearDragDrop(void); -CIMGUI_API bool igIsDragDropPayloadBeingAccepted(void); -CIMGUI_API void igRenderDragDropTargetRect(const ImRect bb, - const ImRect item_clip_rect); -CIMGUI_API ImGuiTypingSelectRequest* igGetTypingSelectRequest( - ImGuiTypingSelectFlags flags); -CIMGUI_API int igTypingSelectFindMatch(ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, - int), - void* user_data, - int nav_item_idx); -CIMGUI_API int igTypingSelectFindNextSingleCharMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data, - int nav_item_idx); -CIMGUI_API int igTypingSelectFindBestLeadingMatch( - ImGuiTypingSelectRequest* req, - int items_count, - const char* (*get_item_name_func)(void*, int), - void* user_data); -CIMGUI_API void igSetWindowClipRectBeforeSetChannel(ImGuiWindow* window, - const ImRect clip_rect); -CIMGUI_API void igBeginColumns(const char* str_id, - int count, - ImGuiOldColumnFlags flags); -CIMGUI_API void igEndColumns(void); -CIMGUI_API void igPushColumnClipRect(int column_index); -CIMGUI_API void igPushColumnsBackground(void); -CIMGUI_API void igPopColumnsBackground(void); -CIMGUI_API ImGuiID igGetColumnsID(const char* str_id, int count); -CIMGUI_API ImGuiOldColumns* igFindOrCreateColumns(ImGuiWindow* window, - ImGuiID id); -CIMGUI_API float igGetColumnOffsetFromNorm(const ImGuiOldColumns* columns, - float offset_norm); -CIMGUI_API float igGetColumnNormFromOffset(const ImGuiOldColumns* columns, - float offset); -CIMGUI_API void igTableOpenContextMenu(int column_n); -CIMGUI_API void igTableSetColumnWidth(int column_n, float width); -CIMGUI_API void igTableSetColumnSortDirection(int column_n, - ImGuiSortDirection sort_direction, - bool append_to_sort_specs); -CIMGUI_API int igTableGetHoveredColumn(void); -CIMGUI_API int igTableGetHoveredRow(void); -CIMGUI_API float igTableGetHeaderRowHeight(void); -CIMGUI_API float igTableGetHeaderAngledMaxLabelWidth(void); -CIMGUI_API void igTablePushBackgroundChannel(void); -CIMGUI_API void igTablePopBackgroundChannel(void); -CIMGUI_API void igTableAngledHeadersRowEx(ImGuiID row_id, - float angle, - float max_label_width, - const ImGuiTableHeaderData* data, - int data_count); -CIMGUI_API ImGuiTable* igGetCurrentTable(void); -CIMGUI_API ImGuiTable* igTableFindByID(ImGuiID id); -CIMGUI_API bool igBeginTableEx(const char* name, - ImGuiID id, - int columns_count, - ImGuiTableFlags flags, - const ImVec2 outer_size, - float inner_width); -CIMGUI_API void igTableBeginInitMemory(ImGuiTable* table, int columns_count); -CIMGUI_API void igTableBeginApplyRequests(ImGuiTable* table); -CIMGUI_API void igTableSetupDrawChannels(ImGuiTable* table); -CIMGUI_API void igTableUpdateLayout(ImGuiTable* table); -CIMGUI_API void igTableUpdateBorders(ImGuiTable* table); -CIMGUI_API void igTableUpdateColumnsWeightFromWidth(ImGuiTable* table); -CIMGUI_API void igTableDrawBorders(ImGuiTable* table); -CIMGUI_API void igTableDrawDefaultContextMenu( - ImGuiTable* table, - ImGuiTableFlags flags_for_section_to_display); -CIMGUI_API bool igTableBeginContextMenuPopup(ImGuiTable* table); -CIMGUI_API void igTableMergeDrawChannels(ImGuiTable* table); -CIMGUI_API ImGuiTableInstanceData* igTableGetInstanceData(ImGuiTable* table, - int instance_no); -CIMGUI_API ImGuiID igTableGetInstanceID(ImGuiTable* table, int instance_no); -CIMGUI_API void igTableSortSpecsSanitize(ImGuiTable* table); -CIMGUI_API void igTableSortSpecsBuild(ImGuiTable* table); -CIMGUI_API ImGuiSortDirection -igTableGetColumnNextSortDirection(ImGuiTableColumn* column); -CIMGUI_API void igTableFixColumnSortDirection(ImGuiTable* table, - ImGuiTableColumn* column); -CIMGUI_API float igTableGetColumnWidthAuto(ImGuiTable* table, - ImGuiTableColumn* column); -CIMGUI_API void igTableBeginRow(ImGuiTable* table); -CIMGUI_API void igTableEndRow(ImGuiTable* table); -CIMGUI_API void igTableBeginCell(ImGuiTable* table, int column_n); -CIMGUI_API void igTableEndCell(ImGuiTable* table); -CIMGUI_API void igTableGetCellBgRect(ImRect* pOut, - const ImGuiTable* table, - int column_n); -CIMGUI_API const char* igTableGetColumnName_TablePtr(const ImGuiTable* table, - int column_n); -CIMGUI_API ImGuiID igTableGetColumnResizeID(ImGuiTable* table, - int column_n, - int instance_no); -CIMGUI_API float igTableGetMaxColumnWidth(const ImGuiTable* table, - int column_n); -CIMGUI_API void igTableSetColumnWidthAutoSingle(ImGuiTable* table, - int column_n); -CIMGUI_API void igTableSetColumnWidthAutoAll(ImGuiTable* table); -CIMGUI_API void igTableRemove(ImGuiTable* table); -CIMGUI_API void igTableGcCompactTransientBuffers_TablePtr(ImGuiTable* table); -CIMGUI_API void igTableGcCompactTransientBuffers_TableTempDataPtr( - ImGuiTableTempData* table); -CIMGUI_API void igTableGcCompactSettings(void); -CIMGUI_API void igTableLoadSettings(ImGuiTable* table); -CIMGUI_API void igTableSaveSettings(ImGuiTable* table); -CIMGUI_API void igTableResetSettings(ImGuiTable* table); -CIMGUI_API ImGuiTableSettings* igTableGetBoundSettings(ImGuiTable* table); -CIMGUI_API void igTableSettingsAddSettingsHandler(void); -CIMGUI_API ImGuiTableSettings* igTableSettingsCreate(ImGuiID id, - int columns_count); -CIMGUI_API ImGuiTableSettings* igTableSettingsFindByID(ImGuiID id); -CIMGUI_API ImGuiTabBar* igGetCurrentTabBar(void); -CIMGUI_API bool igBeginTabBarEx(ImGuiTabBar* tab_bar, - const ImRect bb, - ImGuiTabBarFlags flags); -CIMGUI_API ImGuiTabItem* igTabBarFindTabByID(ImGuiTabBar* tab_bar, - ImGuiID tab_id); -CIMGUI_API ImGuiTabItem* igTabBarFindTabByOrder(ImGuiTabBar* tab_bar, - int order); -CIMGUI_API ImGuiTabItem* igTabBarFindMostRecentlySelectedTabForActiveWindow( - ImGuiTabBar* tab_bar); -CIMGUI_API ImGuiTabItem* igTabBarGetCurrentTab(ImGuiTabBar* tab_bar); -CIMGUI_API int igTabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); -CIMGUI_API const char* igTabBarGetTabName(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab); -CIMGUI_API void igTabBarAddTab(ImGuiTabBar* tab_bar, - ImGuiTabItemFlags tab_flags, - ImGuiWindow* window); -CIMGUI_API void igTabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); -CIMGUI_API void igTabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); -CIMGUI_API void igTabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); -CIMGUI_API void igTabBarQueueReorder(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - int offset); -CIMGUI_API void igTabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, - ImGuiTabItem* tab, - ImVec2 mouse_pos); -CIMGUI_API bool igTabBarProcessReorder(ImGuiTabBar* tab_bar); -CIMGUI_API bool igTabItemEx(ImGuiTabBar* tab_bar, - const char* label, - bool* p_open, - ImGuiTabItemFlags flags, - ImGuiWindow* docked_window); -CIMGUI_API void igTabItemCalcSize_Str(ImVec2* pOut, - const char* label, - bool has_close_button_or_unsaved_marker); -CIMGUI_API void igTabItemCalcSize_WindowPtr(ImVec2* pOut, ImGuiWindow* window); -CIMGUI_API void igTabItemBackground(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImU32 col); -CIMGUI_API void igTabItemLabelAndCloseButton(ImDrawList* draw_list, - const ImRect bb, - ImGuiTabItemFlags flags, - ImVec2 frame_padding, - const char* label, - ImGuiID tab_id, - ImGuiID close_button_id, - bool is_contents_visible, - bool* out_just_closed, - bool* out_text_clipped); -CIMGUI_API void igRenderText(ImVec2 pos, - const char* text, - const char* text_end, - bool hide_text_after_hash); -CIMGUI_API void igRenderTextWrapped(ImVec2 pos, - const char* text, - const char* text_end, - float wrap_width); -CIMGUI_API void igRenderTextClipped(const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect); -CIMGUI_API void igRenderTextClippedEx(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known, - const ImVec2 align, - const ImRect* clip_rect); -CIMGUI_API void igRenderTextEllipsis(ImDrawList* draw_list, - const ImVec2 pos_min, - const ImVec2 pos_max, - float clip_max_x, - float ellipsis_max_x, - const char* text, - const char* text_end, - const ImVec2* text_size_if_known); -CIMGUI_API void igRenderFrame(ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - bool border, - float rounding); -CIMGUI_API void igRenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding); -CIMGUI_API void igRenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, - ImVec2 p_min, - ImVec2 p_max, - ImU32 fill_col, - float grid_step, - ImVec2 grid_off, - float rounding, - ImDrawFlags flags); -CIMGUI_API void igRenderNavHighlight(const ImRect bb, - ImGuiID id, - ImGuiNavHighlightFlags flags); -CIMGUI_API const char* igFindRenderedTextEnd(const char* text, - const char* text_end); -CIMGUI_API void igRenderMouseCursor(ImVec2 pos, - float scale, - ImGuiMouseCursor mouse_cursor, - ImU32 col_fill, - ImU32 col_border, - ImU32 col_shadow); -CIMGUI_API void igRenderArrow(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - ImGuiDir dir, - float scale); -CIMGUI_API void igRenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); -CIMGUI_API void igRenderCheckMark(ImDrawList* draw_list, - ImVec2 pos, - ImU32 col, - float sz); -CIMGUI_API void igRenderArrowPointingAt(ImDrawList* draw_list, - ImVec2 pos, - ImVec2 half_sz, - ImGuiDir direction, - ImU32 col); -CIMGUI_API void igRenderArrowDockMenu(ImDrawList* draw_list, - ImVec2 p_min, - float sz, - ImU32 col); -CIMGUI_API void igRenderRectFilledRangeH(ImDrawList* draw_list, - const ImRect rect, - ImU32 col, - float x_start_norm, - float x_end_norm, - float rounding); -CIMGUI_API void igRenderRectFilledWithHole(ImDrawList* draw_list, - const ImRect outer, - const ImRect inner, - ImU32 col, - float rounding); -CIMGUI_API ImDrawFlags igCalcRoundingFlagsForRectInRect(const ImRect r_in, - const ImRect r_outer, - float threshold); -CIMGUI_API void igTextEx(const char* text, - const char* text_end, - ImGuiTextFlags flags); -CIMGUI_API bool igButtonEx(const char* label, - const ImVec2 size_arg, - ImGuiButtonFlags flags); -CIMGUI_API bool igArrowButtonEx(const char* str_id, - ImGuiDir dir, - ImVec2 size_arg, - ImGuiButtonFlags flags); -CIMGUI_API bool igImageButtonEx(ImGuiID id, - ImTextureID texture_id, - const ImVec2 image_size, - const ImVec2 uv0, - const ImVec2 uv1, - const ImVec4 bg_col, - const ImVec4 tint_col, - ImGuiButtonFlags flags); -CIMGUI_API void igSeparatorEx(ImGuiSeparatorFlags flags, float thickness); -CIMGUI_API void igSeparatorTextEx(ImGuiID id, - const char* label, - const char* label_end, - float extra_width); -CIMGUI_API bool igCheckboxFlags_S64Ptr(const char* label, - ImS64* flags, - ImS64 flags_value); -CIMGUI_API bool igCheckboxFlags_U64Ptr(const char* label, - ImU64* flags, - ImU64 flags_value); -CIMGUI_API bool igCloseButton(ImGuiID id, const ImVec2 pos); -CIMGUI_API bool igCollapseButton(ImGuiID id, - const ImVec2 pos, - ImGuiDockNode* dock_node); -CIMGUI_API void igScrollbar(ImGuiAxis axis); -CIMGUI_API bool igScrollbarEx(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - ImS64* p_scroll_v, - ImS64 avail_v, - ImS64 contents_v, - ImDrawFlags flags); -CIMGUI_API void igGetWindowScrollbarRect(ImRect* pOut, - ImGuiWindow* window, - ImGuiAxis axis); -CIMGUI_API ImGuiID igGetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis); -CIMGUI_API ImGuiID igGetWindowResizeCornerID(ImGuiWindow* window, int n); -CIMGUI_API ImGuiID igGetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir); -CIMGUI_API bool igButtonBehavior(const ImRect bb, - ImGuiID id, - bool* out_hovered, - bool* out_held, - ImGuiButtonFlags flags); -CIMGUI_API bool igDragBehavior(ImGuiID id, - ImGuiDataType data_type, - void* p_v, - float v_speed, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags); -CIMGUI_API bool igSliderBehavior(const ImRect bb, - ImGuiID id, - ImGuiDataType data_type, - void* p_v, - const void* p_min, - const void* p_max, - const char* format, - ImGuiSliderFlags flags, - ImRect* out_grab_bb); -CIMGUI_API bool igSplitterBehavior(const ImRect bb, - ImGuiID id, - ImGuiAxis axis, - float* size1, - float* size2, - float min_size1, - float min_size2, - float hover_extend, - float hover_visibility_delay, - ImU32 bg_col); -CIMGUI_API bool igTreeNodeBehavior(ImGuiID id, - ImGuiTreeNodeFlags flags, - const char* label, - const char* label_end); -CIMGUI_API void igTreePushOverrideID(ImGuiID id); -CIMGUI_API void igTreeNodeSetOpen(ImGuiID id, bool open); -CIMGUI_API bool igTreeNodeUpdateNextOpen(ImGuiID id, ImGuiTreeNodeFlags flags); -CIMGUI_API void igSetNextItemSelectionUserData( - ImGuiSelectionUserData selection_user_data); -CIMGUI_API const ImGuiDataTypeInfo* igDataTypeGetInfo(ImGuiDataType data_type); -CIMGUI_API int igDataTypeFormatString(char* buf, - int buf_size, - ImGuiDataType data_type, - const void* p_data, - const char* format); -CIMGUI_API void igDataTypeApplyOp(ImGuiDataType data_type, - int op, - void* output, - const void* arg_1, - const void* arg_2); -CIMGUI_API bool igDataTypeApplyFromText(const char* buf, - ImGuiDataType data_type, - void* p_data, - const char* format); -CIMGUI_API int igDataTypeCompare(ImGuiDataType data_type, - const void* arg_1, - const void* arg_2); -CIMGUI_API bool igDataTypeClamp(ImGuiDataType data_type, - void* p_data, - const void* p_min, - const void* p_max); -CIMGUI_API bool igInputTextEx(const char* label, - const char* hint, - char* buf, - int buf_size, - const ImVec2 size_arg, - ImGuiInputTextFlags flags, - ImGuiInputTextCallback callback, - void* user_data); -CIMGUI_API void igInputTextDeactivateHook(ImGuiID id); -CIMGUI_API bool igTempInputText(const ImRect bb, - ImGuiID id, - const char* label, - char* buf, - int buf_size, - ImGuiInputTextFlags flags); -CIMGUI_API bool igTempInputScalar(const ImRect bb, - ImGuiID id, - const char* label, - ImGuiDataType data_type, - void* p_data, - const char* format, - const void* p_clamp_min, - const void* p_clamp_max); -CIMGUI_API bool igTempInputIsActive(ImGuiID id); -CIMGUI_API ImGuiInputTextState* igGetInputTextState(ImGuiID id); -CIMGUI_API void igColorTooltip(const char* text, - const float* col, - ImGuiColorEditFlags flags); -CIMGUI_API void igColorEditOptionsPopup(const float* col, - ImGuiColorEditFlags flags); -CIMGUI_API void igColorPickerOptionsPopup(const float* ref_col, - ImGuiColorEditFlags flags); -CIMGUI_API int igPlotEx(ImGuiPlotType plot_type, - const char* label, - float (*values_getter)(void* data, int idx), - void* data, - int values_count, - int values_offset, - const char* overlay_text, - float scale_min, - float scale_max, - const ImVec2 size_arg); -CIMGUI_API void igShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - ImVec2 gradient_p0, - ImVec2 gradient_p1, - ImU32 col0, - ImU32 col1); -CIMGUI_API void igShadeVertsLinearUV(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 a, - const ImVec2 b, - const ImVec2 uv_a, - const ImVec2 uv_b, - bool clamp); -CIMGUI_API void igShadeVertsTransformPos(ImDrawList* draw_list, - int vert_start_idx, - int vert_end_idx, - const ImVec2 pivot_in, - float cos_a, - float sin_a, - const ImVec2 pivot_out); -CIMGUI_API void igGcCompactTransientMiscBuffers(void); -CIMGUI_API void igGcCompactTransientWindowBuffers(ImGuiWindow* window); -CIMGUI_API void igGcAwakeTransientWindowBuffers(ImGuiWindow* window); -CIMGUI_API void igDebugLog(const char* fmt, ...); -CIMGUI_API void igDebugLogV(const char* fmt, va_list args); -CIMGUI_API void igDebugAllocHook(ImGuiDebugAllocInfo* info, - int frame_count, - void* ptr, - size_t size); -CIMGUI_API void igErrorCheckEndFrameRecover(ImGuiErrorLogCallback log_callback, - void* user_data); -CIMGUI_API void igErrorCheckEndWindowRecover(ImGuiErrorLogCallback log_callback, - void* user_data); -CIMGUI_API void igErrorCheckUsingSetCursorPosToExtendParentBoundaries(void); -CIMGUI_API void igDebugDrawCursorPos(ImU32 col); -CIMGUI_API void igDebugDrawLineExtents(ImU32 col); -CIMGUI_API void igDebugDrawItemRect(ImU32 col); -CIMGUI_API void igDebugLocateItem(ImGuiID target_id); -CIMGUI_API void igDebugLocateItemOnHover(ImGuiID target_id); -CIMGUI_API void igDebugLocateItemResolveWithLastItem(void); -CIMGUI_API void igDebugBreakClearData(void); -CIMGUI_API bool igDebugBreakButton(const char* label, - const char* description_of_location); -CIMGUI_API void igDebugBreakButtonTooltip(bool keyboard_only, - const char* description_of_location); -CIMGUI_API void igShowFontAtlas(ImFontAtlas* atlas); -CIMGUI_API void igDebugHookIdInfo(ImGuiID id, - ImGuiDataType data_type, - const void* data_id, - const void* data_id_end); -CIMGUI_API void igDebugNodeColumns(ImGuiOldColumns* columns); -CIMGUI_API void igDebugNodeDockNode(ImGuiDockNode* node, const char* label); -CIMGUI_API void igDebugNodeDrawList(ImGuiWindow* window, - ImGuiViewportP* viewport, - const ImDrawList* draw_list, - const char* label); -CIMGUI_API void igDebugNodeDrawCmdShowMeshAndBoundingBox( - ImDrawList* out_draw_list, - const ImDrawList* draw_list, - const ImDrawCmd* draw_cmd, - bool show_mesh, - bool show_aabb); -CIMGUI_API void igDebugNodeFont(ImFont* font); -CIMGUI_API void igDebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph); -CIMGUI_API void igDebugNodeStorage(ImGuiStorage* storage, const char* label); -CIMGUI_API void igDebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); -CIMGUI_API void igDebugNodeTable(ImGuiTable* table); -CIMGUI_API void igDebugNodeTableSettings(ImGuiTableSettings* settings); -CIMGUI_API void igDebugNodeInputTextState(ImGuiInputTextState* state); -CIMGUI_API void igDebugNodeTypingSelectState(ImGuiTypingSelectState* state); -CIMGUI_API void igDebugNodeWindow(ImGuiWindow* window, const char* label); -CIMGUI_API void igDebugNodeWindowSettings(ImGuiWindowSettings* settings); -CIMGUI_API void igDebugNodeWindowsList(ImVector_ImGuiWindowPtr* windows, - const char* label); -CIMGUI_API void igDebugNodeWindowsListByBeginStackParent( - ImGuiWindow** windows, - int windows_size, - ImGuiWindow* parent_in_begin_stack); -CIMGUI_API void igDebugNodeViewport(ImGuiViewportP* viewport); -CIMGUI_API void igDebugRenderKeyboardPreview(ImDrawList* draw_list); -CIMGUI_API void igDebugRenderViewportThumbnail(ImDrawList* draw_list, - ImGuiViewportP* viewport, - const ImRect bb); -CIMGUI_API void igImFontAtlasUpdateConfigDataPointers(ImFontAtlas* atlas); -CIMGUI_API void igImFontAtlasBuildInit(ImFontAtlas* atlas); -CIMGUI_API void igImFontAtlasBuildSetupFont(ImFontAtlas* atlas, - ImFont* font, - ImFontConfig* font_config, - float ascent, - float descent); -CIMGUI_API void igImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, - void* stbrp_context_opaque); -CIMGUI_API void igImFontAtlasBuildFinish(ImFontAtlas* atlas); -CIMGUI_API void igImFontAtlasBuildRender8bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned char in_marker_pixel_value); -CIMGUI_API void igImFontAtlasBuildRender32bppRectFromString( - ImFontAtlas* atlas, - int x, - int y, - int w, - int h, - const char* in_str, - char in_marker_char, - unsigned int in_marker_pixel_value); -CIMGUI_API void igImFontAtlasBuildMultiplyCalcLookupTable( - unsigned char out_table[256], - float in_multiply_factor); -CIMGUI_API void igImFontAtlasBuildMultiplyRectAlpha8( - const unsigned char table[256], - unsigned char* pixels, - int x, - int y, - int w, - int h, - int stride); - -/////////////////////////hand written functions -// no LogTextV -CIMGUI_API void igLogText(CONST char* fmt, ...); -// no appendfV -CIMGUI_API void ImGuiTextBuffer_appendf(struct ImGuiTextBuffer* buffer, - const char* fmt, - ...); -// for getting FLT_MAX in bindings -CIMGUI_API float igGET_FLT_MAX(void); -// for getting FLT_MIN in bindings -CIMGUI_API float igGET_FLT_MIN(void); - -CIMGUI_API ImVector_ImWchar* ImVector_ImWchar_create(void); -CIMGUI_API void ImVector_ImWchar_destroy(ImVector_ImWchar* self); -CIMGUI_API void ImVector_ImWchar_Init(ImVector_ImWchar* p); -CIMGUI_API void ImVector_ImWchar_UnInit(ImVector_ImWchar* p); - -#endif // CIMGUI_INCLUDED -- 2.51.2 From 8449c3efb8a9179fcde7263a9b9c273914bfcc29 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 13:39:17 -0800 Subject: [PATCH 143/349] update all deps --- build.zig.zon.json | 11 ++++++++--- build.zig.zon.nix | 14 +++++++++++--- build.zig.zon.txt | 3 ++- flatpak/zig-packages.json | 12 +++++++++--- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/build.zig.zon.json b/build.zig.zon.json index 6ab567279..28fd4927e 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -1,4 +1,9 @@ { + "N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr": { + "name": "bindings", + "url": "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz", + "hash": "sha256-i/7FAOAJJvZ5hT7iPWfMOS08MYFzPKRwRzhlHT9wuqM=" + }, "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ": { "name": "breakpad", "url": "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz", @@ -44,10 +49,10 @@ "url": "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz", "hash": "sha256-h9T4iT704I8iSXNgj/6/lCaKgTgLp5wS6IQZaMgKohI=" }, - "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3": { + "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI": { "name": "imgui", - "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", - "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" + "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + "hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs=" }, "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU": { "name": "iterm2_themes", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 905ee1ec3..3cfa8f280 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -82,6 +82,14 @@ fetcher.${proto}; in linkFarm name [ + { + name = "N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr"; + path = fetchZigArtifact { + name = "bindings"; + url = "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz"; + hash = "sha256-i/7FAOAJJvZ5hT7iPWfMOS08MYFzPKRwRzhlHT9wuqM="; + }; + } { name = "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ"; path = fetchZigArtifact { @@ -155,11 +163,11 @@ in }; } { - name = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3"; + name = "N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI"; path = fetchZigArtifact { name = "imgui"; - url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz"; - hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="; + url = "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz"; + hash = "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 09cf8007c..c84b24638 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -1,4 +1,5 @@ git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732 +https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.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 @@ -11,7 +12,6 @@ https://deps.files.ghostty.org/gobject-2025-11-08-23-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/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz @@ -32,4 +32,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/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 03907fba1..b511a4f27 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -1,4 +1,10 @@ [ + { + "type": "archive", + "url": "https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz", + "dest": "vendor/p/N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr", + "sha256": "8bfec500e00926f679853ee23d67cc392d3c3181733ca4704738651d3f70baa3" + }, { "type": "archive", "url": "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz", @@ -55,9 +61,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", - "dest": "vendor/p/N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3", - "sha256": "a05fd01e04cf11ab781e28387c621d2e420f1e6044c8e27a25e603ea99ef7860" + "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", + "dest": "vendor/p/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI", + "sha256": "c816c20e8c75f3e15ae867350e79925502d1a6a85938bb1a73b8927e5f31f9cb" }, { "type": "archive", -- 2.51.2 From c694517432adf6a2bd28633de52b0193153282fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Dec 2025 13:41:58 -0800 Subject: [PATCH 144/349] update gitattributes, removed file --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 87f1eb32e..9fe672044 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,6 @@ build.zig.zon.json linguist-generated=true vendor/** linguist-vendored website/** linguist-documentation pkg/breakpad/vendor/** linguist-vendored -pkg/cimgui/vendor/** linguist-vendored pkg/glfw/wayland-headers/** linguist-vendored pkg/libintl/config.h linguist-generated=true pkg/libintl/libintl.h linguist-generated=true -- 2.51.2 From 74fc48682a26bfde50ba9085568178b5713ecd86 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:31:11 +0100 Subject: [PATCH 145/349] macOS: remove unused file --- macos/Ghostty.xcodeproj/project.pbxproj | 1 - .../Sources/Helpers/DraggableWindowView.swift | 19 ------------------- 2 files changed, 20 deletions(-) delete mode 100644 macos/Sources/Helpers/DraggableWindowView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index ad2c54ca9..9bd36eaad 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -152,7 +152,6 @@ Helpers/AppInfo.swift, Helpers/CodableBridge.swift, Helpers/Cursor.swift, - Helpers/DraggableWindowView.swift, Helpers/ExpiringUndoManager.swift, "Helpers/Extensions/Double+Extension.swift", "Helpers/Extensions/EventModifiers+Extension.swift", diff --git a/macos/Sources/Helpers/DraggableWindowView.swift b/macos/Sources/Helpers/DraggableWindowView.swift deleted file mode 100644 index 8d88e2f66..000000000 --- a/macos/Sources/Helpers/DraggableWindowView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Cocoa -import SwiftUI - -struct DraggableWindowView: NSViewRepresentable { - func makeNSView(context: Context) -> DraggableWindowNSView { - return DraggableWindowNSView() - } - - func updateNSView(_ nsView: DraggableWindowNSView, context: Context) { - // No need to update anything here - } -} - -class DraggableWindowNSView: NSView { - override func mouseDown(with event: NSEvent) { - guard let window = self.window else { return } - window.performDrag(with: event) - } -} -- 2.51.2 From 1249f3b88cda09ca81a2d3c983036093983982e8 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:37:08 +0100 Subject: [PATCH 146/349] macOS: temporarily disable `window.isMovable` to fix #10110 --- .../Ghostty/Surface View/SurfaceDragSource.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 21416ac75..059428b15 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -104,7 +104,11 @@ extension Ghostty { /// Whether the current drag was cancelled by pressing escape. private var dragCancelledByEscape: Bool = false - + + /// Original value of `window.isMovable` to restore + /// when the mouse exits. + private var isWindowMovable: Bool? + deinit { if let escapeMonitor { NSEvent.removeMonitor(escapeMonitor) @@ -131,10 +135,18 @@ extension Ghostty { } override func mouseEntered(with event: NSEvent) { + // Temporarily disable `isMovable` to fix + // https://github.com/ghostty-org/ghostty/issues/10110 + isWindowMovable = window?.isMovable + window?.isMovable = false onHoverChanged?(true) } override func mouseExited(with event: NSEvent) { + if let isWindowMovable { + window?.isMovable = isWindowMovable + self.isWindowMovable = nil + } onHoverChanged?(false) } -- 2.51.2 From 12024ed8310c73e793bf286a18c226135487ab46 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 1 Jan 2026 08:52:53 -0500 Subject: [PATCH 147/349] macos: simplify .keyDown guard condition This condition is more naturally expressed as a `guard`. --- .../Ghostty/Surface View/SurfaceView_AppKit.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index f9a5480ec..fcff6cd8b 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1181,16 +1181,9 @@ extension Ghostty { /// Special case handling for some control keys override func performKeyEquivalent(with event: NSEvent) -> Bool { - switch (event.type) { - case .keyDown: - // Continue, we care about key down events - break - - default: - // Any other key event we don't care about. I don't think its even - // possible to receive any other event type. - return false - } + // We only care about key down events. It might not even be possible + // to receive any other event type here. + guard event.type == .keyDown else { return false } // Only process events if we're focused. Some key events like C-/ macOS // appears to send to the first view in the hierarchy rather than the -- 2.51.2 From c081adce052ac431bfaf80ff32835c2f301ba30b Mon Sep 17 00:00:00 2001 From: Eric Bower Date: Thu, 1 Jan 2026 10:47:40 -0500 Subject: [PATCH 148/349] chore: write page vt test bg color with trailing row The issue is in ghostty_src/src/terminal/formatter.zig#L1117-L1129: - Cells without text are treated as "blank" (line 1117-1119) - this includes cells that only have background colors - When blank cells are emitted, they're plain spaces (line 1129) - writer.splatByteAll(' ', blank_cells) outputs spaces without any SGR styling - Background-only cells (bg_color_palette, bg_color_rgb) are marked unreachable (lines 1233-1235) because the code assumes hasText() already filtered them This means when htop draws a row like: `[green bg]CPU: 45%[red bg] [default]` The trailing cells with red background but no text get accumulated as blanks and emitted as plain spaces - losing the background color. --- src/terminal/formatter.zig | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 74bbfe482..d0e248d72 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -5819,3 +5819,57 @@ test "Page codepoint_map empty map" { const output = builder.writer.buffered(); try testing.expectEqualStrings("hello world", output); } + +test "Page VT background color on trailing blank cells" { + // This test reproduces a bug where trailing cells with background color + // but no text are emitted as plain spaces without SGR sequences. + // This causes TUIs like htop to lose background colors on rehydration. + const testing = std.testing; + const alloc = testing.allocator; + + var builder: std.Io.Writer.Allocating = .init(alloc); + defer builder.deinit(); + + var t = try Terminal.init(alloc, .{ + .cols = 20, + .rows = 5, + }); + defer t.deinit(alloc); + + var s = t.vtStream(); + defer s.deinit(); + + // Simulate a TUI row: "CPU:" with text, then trailing cells with red background + // to end of line (no text after the colored region). + // \x1b[41m sets red background, then EL fills rest of row with that bg. + try s.nextSlice("CPU:\x1b[41m\x1b[K"); + // Reset colors and move to next line with different content + try s.nextSlice("\x1b[0m\r\nline2"); + + const pages = &t.screens.active.pages; + const page = &pages.pages.last.?.data; + + var formatter: PageFormatter = .init(page, .vt); + formatter.opts.trim = false; // Don't trim so we can see the trailing behavior + + try formatter.format(&builder.writer); + const output = builder.writer.buffered(); + + // The output should preserve the red background SGR for trailing cells on line 1. + // Bug: the first row outputs "CPU:\r\n" only - losing the background color fill. + // The red background should appear BEFORE the newline, not after. + + // Find position of CRLF + const crlf_pos = std.mem.indexOf(u8, output, "\r\n") orelse { + // No CRLF found, fail the test + return error.TestUnexpectedResult; + }; + + // Check that red background (48;5;1) appears BEFORE the newline (on line 1) + const line1 = output[0..crlf_pos]; + const has_red_bg_line1 = std.mem.indexOf(u8, line1, "\x1b[41m") != null or + std.mem.indexOf(u8, line1, "\x1b[48;5;1m") != null; + + // This should be true but currently fails due to the bug + try testing.expect(has_red_bg_line1); +} -- 2.51.2 From c89627fe752804c678b8caf4ddb2214de54289c3 Mon Sep 17 00:00:00 2001 From: Ivan Buiko Date: Wed, 31 Dec 2025 20:55:18 +0100 Subject: [PATCH 149/349] macOS: Add menu shortcut handling in macOS key event processing Allow menu bar to flash for shortcuts and handle key equivalents before checking for Ghostty key bindings --- .../Ghostty/Surface View/SurfaceView_AppKit.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index fcff6cd8b..7f33df45a 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1195,7 +1195,15 @@ extension Ghostty { return false } - // If this event as-is would result in a key binding then we send it. + // Let the menu system handle this event if we're not in a key sequence or key table. + // This allows the menu bar to flash for shortcuts like Command+V. + if keySequence.isEmpty && keyTables.isEmpty { + if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { + return true + } + } + + // If the menu didn't handle it, check Ghostty bindings for custom shortcuts. if let surface { var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) let match = (event.characters ?? "").withCString { ptr in @@ -2209,7 +2217,7 @@ extension Ghostty.SurfaceView { return NSAttributedString(string: plainString, attributes: attributes) } - + } /// Caches a value for some period of time, evicting it automatically when that time expires. -- 2.51.2 From ec2612f9ce199b88f96f70f1b5cef9477534bae6 Mon Sep 17 00:00:00 2001 From: Martin Emde Date: Wed, 31 Dec 2025 19:59:28 -0800 Subject: [PATCH 150/349] Add iTimeFocus shader uniform to track time since focus --- src/config/Config.zig | 16 ++++++++ src/renderer/generic.zig | 38 +++++++++++++---- src/renderer/shaders/shadertoy_prefix.glsl | 2 + .../shaders/test_shadertoy_focus.glsl | 41 +++++++++++++++++++ src/renderer/shadertoy.zig | 3 ++ 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/renderer/shaders/test_shadertoy_focus.glsl diff --git a/src/config/Config.zig b/src/config/Config.zig index 92caa5744..88f3d5375 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2783,6 +2783,22 @@ keybind: Keybinds = .{}, /// the same time as the `iTime` uniform, allowing you to compute the /// time since the change by subtracting this from `iTime`. /// +/// * `float iTimeFocus` - Timestamp when the surface last gained iFocus. +/// +/// When the surface gains focus, this is set to the current value of +/// `iTime`, similar to how `iTimeCursorChange` works. This allows you +/// to compute the time since focus was gained or lost by calculating +/// `iTime - iTimeFocus`. Use this to create animations that restart +/// when the terminal regains focus. +/// +/// * `int iFocus` - Current focus state of the surface. +/// +/// Set to 1.0 when the surface is focused, 0.0 when unfocused. This +/// allows shaders to detect unfocused state and avoid animation artifacts +/// from large time deltas caused by infrequent "deceptive frames" +/// (e.g., modifier key presses, link hover events in unfocused split panes). +/// Check `iFocus > 0` to determine if the surface is currently focused. +/// /// If the shader fails to compile, the shader will be ignored. Any errors /// related to shader compilation will not show up as configuration errors /// and only show up in the log, since shader compilation happens after diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 4ebe501f7..5fa3432a6 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -116,6 +116,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// True if the window is focused focused: bool, + /// Flag to indicate that our focus state changed for custom + /// shaders to update their state. + custom_shader_focused_changed: bool = false, + /// 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. @@ -746,6 +750,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .current_cursor_color = @splat(0), .previous_cursor_color = @splat(0), .cursor_change_time = 0, + .time_focus = 0, + .focus = 1, // assume focused initially }, .bg_image_buffer = undefined, @@ -1008,8 +1014,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// /// Must be called on the render thread. pub fn setFocus(self: *Self, focus: bool) !void { + assert(self.focused != focus); + self.focused = focus; + // Flag that we need to update our custom shaders + self.custom_shader_focused_changed = true; + // If we're not focused, then we want to stop the display link // because it is a waste of resources and we can move to pure // change-driven updates. @@ -2255,6 +2266,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return; + const uniforms = &self.custom_shader_uniforms; + const now = try std.time.Instant.now(); defer self.last_frame_time = now; const first_frame_time = self.first_frame_time orelse t: { @@ -2264,23 +2277,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const last_frame_time = self.last_frame_time orelse now; const since_ns: f32 = @floatFromInt(now.since(first_frame_time)); - self.custom_shader_uniforms.time = since_ns / std.time.ns_per_s; + uniforms.time = since_ns / std.time.ns_per_s; const delta_ns: f32 = @floatFromInt(now.since(last_frame_time)); - self.custom_shader_uniforms.time_delta = delta_ns / std.time.ns_per_s; + uniforms.time_delta = delta_ns / std.time.ns_per_s; - self.custom_shader_uniforms.frame += 1; + uniforms.frame += 1; const screen = self.size.screen; const padding = self.size.padding; const cell = self.size.cell; - self.custom_shader_uniforms.resolution = .{ + uniforms.resolution = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, }; - self.custom_shader_uniforms.channel_resolution[0] = .{ + uniforms.channel_resolution[0] = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, @@ -2345,8 +2358,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { @as(f32, @floatFromInt(cursor.color[3])) / 255.0, }; - const uniforms = &self.custom_shader_uniforms; - const cursor_changed: bool = !std.meta.eql(new_cursor, uniforms.current_cursor) or !std.meta.eql(cursor_color, uniforms.current_cursor_color); @@ -2359,6 +2370,19 @@ pub fn Renderer(comptime GraphicsAPI: type) type { uniforms.cursor_change_time = uniforms.time; } } + + // Update focus uniforms + uniforms.focus = @intFromBool(self.focused); + + // If we need to update the time our focus state changed + // then update it to our current frame time. This may not be + // exactly correct since it is frame time, not exact focus + // time, but focus time on its own isn't exactly correct anyways + // since it comes async from a message. + if (self.custom_shader_focused_changed and self.focused) { + uniforms.time_focus = uniforms.time; + self.custom_shader_focused_changed = false; + } } /// Convert the terminal state to GPU cells stored in CPU memory. These diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index 6d9cf0f68..c984334f4 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -16,6 +16,8 @@ layout(binding = 1, std140) uniform Globals { uniform vec4 iCurrentCursorColor; uniform vec4 iPreviousCursorColor; uniform float iTimeCursorChange; + uniform float iTimeFocus; + uniform int iFocus; }; layout(binding = 0) uniform sampler2D iChannel0; diff --git a/src/renderer/shaders/test_shadertoy_focus.glsl b/src/renderer/shaders/test_shadertoy_focus.glsl new file mode 100644 index 000000000..9fc2304df --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_focus.glsl @@ -0,0 +1,41 @@ +// Test shader for iTimeFocus and iFocus +// Shows border when focused, green fade that restarts on each focus gain +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 uv = fragCoord / iResolution.xy; + + // Sample the terminal content + vec4 terminal = texture2D(iChannel0, uv); + vec3 color = terminal.rgb; + + if (iFocus > 0) { + // FOCUSED: Add border and fading green overlay + + // Calculate time since focus was gained + float timeSinceFocus = iTime - iTimeFocus; + + // Green fade: starts at 1.0 (full green), fades to 0.0 over 3 seconds + float fadeOut = max(0.0, 1.0 - (timeSinceFocus / 3.0)); + + // Add green overlay that fades out + color = mix(color, vec3(0.0, 1.0, 0.0), fadeOut * 0.4); + + // Add border (5 pixels) + float borderSize = 5.0; + vec2 pixelCoord = fragCoord; + bool isBorder = pixelCoord.x < borderSize || + pixelCoord.x > iResolution.x - borderSize || + pixelCoord.y < borderSize || + pixelCoord.y > iResolution.y - borderSize; + + if (isBorder) { + // Bright cyan border that pulses subtly + float pulse = sin(timeSinceFocus * 2.0) * 0.1 + 0.9; + color = vec3(0.0, 1.0, 1.0) * pulse; + } + } else { + // UNFOCUSED: Solid red overlay (no border) + color = mix(color, vec3(1.0, 0.0, 0.0), 0.3); + } + + fragColor = vec4(color, 1.0); +} diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 0d096c0fc..f71200610 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -25,6 +25,8 @@ pub const Uniforms = extern struct { current_cursor_color: [4]f32 align(16), previous_cursor_color: [4]f32 align(16), cursor_change_time: f32 align(4), + time_focus: f32 align(4), + focus: i32 align(4), }; /// The target to load shaders for. @@ -412,3 +414,4 @@ test "shadertoy to glsl" { const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl"); +const test_focus = @embedFile("shaders/test_shadertoy_focus.glsl"); -- 2.51.2 From c384cd050e747579b78c738a09061e1815c5c46a Mon Sep 17 00:00:00 2001 From: Martin Emde Date: Thu, 1 Jan 2026 15:27:26 -0800 Subject: [PATCH 151/349] Fix Mac window becomes unmovable after pane rearrangement After rearranging panes, the window becomes permanently unmovable. Grab handles temporarily set `window.isMovable = false` on hover to prevent window dragging from interfering with pane dragging. Override `viewWillMove(toWindow:)` to catch when the view is being removed from the window. This lifecycle method is called before the window reference becomes nil, allowing us to restore `window.isMovable`. --- .../Surface View/SurfaceDragSource.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 059428b15..37a69852e 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -105,22 +105,30 @@ extension Ghostty { /// Whether the current drag was cancelled by pressing escape. private var dragCancelledByEscape: Bool = false - /// Original value of `window.isMovable` to restore - /// when the mouse exits. - private var isWindowMovable: Bool? - deinit { if let escapeMonitor { NSEvent.removeMonitor(escapeMonitor) } } - + + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + // Ensure this view gets the mouse event before window dragging handlers + return true + } + + override func mouseDown(with event: NSEvent) { + // Consume the mouseDown event to prevent it from propagating to the + // window's drag handler. This fixes issue #10110 where grab handles + // would drag the window instead of initiating pane drags. + // Don't call super - the drag will be initiated in mouseDragged. + } + override func updateTrackingAreas() { super.updateTrackingAreas() - + // To update our tracking area we just recreate it all. trackingAreas.forEach { removeTrackingArea($0) } - + // Add our tracking area for mouse events addTrackingArea(NSTrackingArea( rect: bounds, @@ -135,18 +143,10 @@ extension Ghostty { } override func mouseEntered(with event: NSEvent) { - // Temporarily disable `isMovable` to fix - // https://github.com/ghostty-org/ghostty/issues/10110 - isWindowMovable = window?.isMovable - window?.isMovable = false onHoverChanged?(true) } override func mouseExited(with event: NSEvent) { - if let isWindowMovable { - window?.isMovable = isWindowMovable - self.isWindowMovable = nil - } onHoverChanged?(false) } @@ -237,7 +237,7 @@ extension Ghostty { NSEvent.removeMonitor(escapeMonitor) self.escapeMonitor = nil } - + if operation == [] && !dragCancelledByEscape { let endsInWindow = NSApplication.shared.windows.contains { window in window.isVisible && window.frame.contains(screenPoint) @@ -250,7 +250,7 @@ extension Ghostty { ) } } - + isTracking = false onDragStateChanged?(false) } -- 2.51.2 From 6fdbc29b9ae0eb72ca7904eedda7702aa6f0c5c7 Mon Sep 17 00:00:00 2001 From: Eric Bower Date: Thu, 1 Jan 2026 20:07:27 -0500 Subject: [PATCH 152/349] fix(formatter): preserve background colors on cells without text The VT formatter was treating cells without text as blank and emitting them as plain spaces, losing any background color styling. This caused TUIs like htop to lose their background colors when rehydrating terminal state (e.g., after detach/reattach in zmx). For styled formats (VT/HTML), cells with background colors or style_id are now emitted with proper SGR sequences and a space character instead of being accumulated as unstyled blanks. Adds handling for bg_color_palette and bg_color_rgb content tags which were previously unreachable. Reference: https://ampcode.com/threads/T-019b7a35-c3f3-73fc-adfa-00bbe9dbda3c --- src/terminal/formatter.zig | 97 +++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 11 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index d0e248d72..a107b0535 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -1113,12 +1113,16 @@ pub const PageFormatter = struct { // If we have a zero value, then we accumulate a counter. We // only want to turn zero values into spaces if we have a non-zero - // char sometime later. - if (!cell.hasText()) { + // char sometime later. However, for styled formats (VT, HTML), if + // the cell has styling (e.g., background color), we must emit it + // to preserve the visual appearance. + const dominated_by_style = self.opts.emit.styled() and + (!cell.isEmpty() or cell.hasStyling()); + if (!dominated_by_style and !cell.hasText()) { blank_cells += 1; continue; } - if (cell.codepoint() == ' ' and self.opts.trim) { + if (cell.codepoint() == ' ' and self.opts.trim and !dominated_by_style) { blank_cells += 1; continue; } @@ -1215,24 +1219,46 @@ pub const PageFormatter = struct { } } - try self.writeCell(tag, writer, cell); + // For styled cells without text, emit a space to carry the styling + if (cell.hasText()) { + try self.writeCell(tag, writer, cell); + } else { + try writer.writeByte(' '); + } // If we have a point map, all codepoints map to this // cell. if (self.point_map) |*map| { - var discarding: std.Io.Writer.Discarding = .init(&.{}); - try self.writeCell(tag, &discarding.writer, cell); - for (0..discarding.count) |_| map.map.append(map.alloc, .{ + const byte_count: usize = if (cell.hasText()) count: { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + try self.writeCell(tag, &discarding.writer, cell); + break :count discarding.count; + } else 1; + for (0..byte_count) |_| map.map.append(map.alloc, .{ .x = x, .y = y, }) catch return error.WriteFailed; } }, - // Unreachable since we do hasText() above - .bg_color_palette, - .bg_color_rgb, - => unreachable, + // Cells with only background color (no text). Emit a space + // with the appropriate background color SGR sequence. + .bg_color_palette => { + const index = cell.content.color_palette; + try self.emitBgColorSgr(writer, index, null, &style); + try writer.writeByte(' '); + if (self.point_map) |*map| { + map.map.append(map.alloc, .{ .x = x, .y = y }) catch return error.WriteFailed; + } + }, + .bg_color_rgb => { + const rgb = cell.content.color_rgb; + try self.emitBgColorSgr(writer, null, rgb, &style); + try writer.writeByte(' '); + if (self.point_map) |*map| { + map.map.append(map.alloc, .{ .x = x, .y = y }) catch return error.WriteFailed; + } + }, } } } @@ -1348,6 +1374,55 @@ pub const PageFormatter = struct { } } + /// Emit background color SGR sequence for bg_color_* content tags. + /// Updates the style tracking to reflect the emitted background. + fn emitBgColorSgr( + self: PageFormatter, + writer: *std.Io.Writer, + palette_index: ?u8, + rgb: ?Cell.RGB, + style: *Style, + ) std.Io.Writer.Error!void { + switch (self.opts.emit) { + .plain => {}, + .vt => { + // Close previous style if non-default + if (!style.default()) try writer.writeAll("\x1b[0m"); + // Emit background color + if (palette_index) |idx| { + try writer.print("\x1b[48;5;{d}m", .{idx}); + } else if (rgb) |c| { + try writer.print("\x1b[48;2;{d};{d};{d}m", .{ c.r, c.g, c.b }); + } + // Update style tracking - set bg_color so we know to reset later + style.* = .{}; + style.bg_color = if (palette_index) |idx| + .{ .palette = idx } + else if (rgb) |c| + .{ .rgb = .{ .r = c.r, .g = c.g, .b = c.b } } + else + .none; + }, + .html => { + // Close previous tag if needed + if (!style.default()) try writer.writeAll(""); + // Emit background color as inline style + if (palette_index) |idx| { + try writer.print("
", .{idx}); + } else if (rgb) |c| { + try writer.print("
2}{x:0>2}{x:0>2};\">", .{ c.r, c.g, c.b }); + } + style.* = .{}; + style.bg_color = if (palette_index) |idx| + .{ .palette = idx } + else if (rgb) |c| + .{ .rgb = .{ .r = c.r, .g = c.g, .b = c.b } } + else + .none; + }, + } + } + fn formatStyleOpen( self: PageFormatter, writer: *std.Io.Writer, -- 2.51.2 From ea03c5c8a20ccb508f62d9da8ea54fb2d9a34729 Mon Sep 17 00:00:00 2001 From: Eric Bower Date: Thu, 1 Jan 2026 20:15:41 -0500 Subject: [PATCH 153/349] fix: reset style to prevent bleeding bg colors newline --- src/terminal/formatter.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index a107b0535..4c7fadc00 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -1042,6 +1042,13 @@ pub const PageFormatter = struct { } if (blank_rows > 0) { + // Reset style before emitting newlines to prevent background + // colors from bleeding into the next line's leading cells. + if (!style.default()) { + try self.formatStyleClose(writer); + style = .{}; + } + const sequence: []const u8 = switch (self.opts.emit) { // Plaintext just uses standard newlines because newlines // on their own usually move the cursor back in anywhere -- 2.51.2 From f316c969a5e72029df0454d791109964c9244639 Mon Sep 17 00:00:00 2001 From: Eric Bower Date: Thu, 1 Jan 2026 20:24:33 -0500 Subject: [PATCH 154/349] chore: update test expectation --- src/terminal/formatter.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 4c7fadc00..2e846f7c1 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -3427,7 +3427,9 @@ test "Page VT multi-line with styles" { try formatter.format(&builder.writer); const output = builder.writer.buffered(); - try testing.expectEqualStrings("\x1b[0m\x1b[1mfirst\r\n\x1b[0m\x1b[3msecond\x1b[0m", output); + // Note: style is reset before newline to prevent background colors from + // bleeding to the next line's leading cells. + try testing.expectEqualStrings("\x1b[0m\x1b[1mfirst\x1b[0m\r\n\x1b[0m\x1b[3msecond\x1b[0m", output); // Verify point map matches output length try testing.expectEqual(output.len, point_map.items.len); -- 2.51.2 From 4ea669562e4104b6991bd5a0c190a098c27a0660 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Sat, 3 Jan 2026 03:57:44 +0100 Subject: [PATCH 155/349] fix: use flush instead of end on stdout in code generators for Windows compatibility --- src/unicode/props_uucode.zig | 4 +++- src/unicode/symbols_uucode.zig | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index 2440d437c..9813ecf43 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -87,7 +87,9 @@ pub fn main() !void { var buf: [4096]u8 = undefined; var stdout = std.fs.File.stdout().writer(&buf); try t.writeZig(&stdout.interface); - try stdout.end(); + // Use flush instead of end because stdout is a pipe when captured by + // the build system, and pipes cannot be truncated (especially on Windows). + try stdout.interface.flush(); // 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 8cbd59211..1a7e20252 100644 --- a/src/unicode/symbols_uucode.zig +++ b/src/unicode/symbols_uucode.zig @@ -34,7 +34,9 @@ pub fn main() !void { var buf: [4096]u8 = undefined; var stdout = std.fs.File.stdout().writer(&buf); try t.writeZig(&stdout.interface); - try stdout.end(); + // Use flush instead of end because stdout is a pipe when captured by + // the build system, and pipes cannot be truncated (especially on Windows). + try stdout.interface.flush(); // Uncomment when manually debugging to see our table sizes. // std.log.warn("stage1={} stage2={} stage3={}", .{ -- 2.51.2 From e2de0bfd9338321e59c0e157bab0696f4ce8a9a8 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Sat, 3 Jan 2026 13:44:29 +0100 Subject: [PATCH 156/349] fix: clarify error codes in comment --- src/unicode/props_uucode.zig | 3 ++- src/unicode/symbols_uucode.zig | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index 9813ecf43..bee942422 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -88,7 +88,8 @@ pub fn main() !void { var stdout = std.fs.File.stdout().writer(&buf); try t.writeZig(&stdout.interface); // Use flush instead of end because stdout is a pipe when captured by - // the build system, and pipes cannot be truncated (especially on Windows). + // the build system, and pipes cannot be truncated (Windows returns + // INVALID_PARAMETER, Linux returns EINVAL). try stdout.interface.flush(); // Uncomment when manually debugging to see our table sizes. diff --git a/src/unicode/symbols_uucode.zig b/src/unicode/symbols_uucode.zig index 1a7e20252..794ca5bab 100644 --- a/src/unicode/symbols_uucode.zig +++ b/src/unicode/symbols_uucode.zig @@ -35,7 +35,8 @@ pub fn main() !void { var stdout = std.fs.File.stdout().writer(&buf); try t.writeZig(&stdout.interface); // Use flush instead of end because stdout is a pipe when captured by - // the build system, and pipes cannot be truncated (especially on Windows). + // the build system, and pipes cannot be truncated (Windows returns + // INVALID_PARAMETER, Linux returns EINVAL). try stdout.interface.flush(); // Uncomment when manually debugging to see our table sizes. -- 2.51.2 From d7972cb8b7219c0a9a489a8f46abf4939dfb34d4 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 3 Jan 2026 18:36:43 +0100 Subject: [PATCH 157/349] gtk: Session Search Gtk implementation of #9945. Fixes #9948. This adds session search to the command palette on Gtk, allowing you to jump to any surface by title or working directory. The main difference to the Mac OS implementation is that tabs do not have colors by which to search. --- src/apprt/gtk/class/application.zig | 30 ++ src/apprt/gtk/class/command_palette.zig | 394 +++++++++++++++++++++--- src/apprt/gtk/class/split_tree.zig | 6 + src/apprt/gtk/class/window.zig | 52 ++++ 4 files changed, 438 insertions(+), 44 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index b16bce049..eb83fa8a2 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1176,6 +1176,36 @@ pub const Application = extern struct { return self.private().config.ref(); } + /// Collect all surfaces from all windows in the application. + /// The caller must unref each surface and window and deinit the list. + pub fn collectAllSurfaces( + self: *Self, + alloc: Allocator, + ) !std.ArrayList(Window.SurfaceInfo) { + var all_surfaces: std.ArrayList(Window.SurfaceInfo) = .{}; + errdefer { + for (all_surfaces.items) |info| { + info.surface.unref(); + info.window.unref(); + } + all_surfaces.deinit(alloc); + } + + const windows = self.as(gtk.Application).getWindows(); + var it: ?*glib.List = windows; + while (it) |node| : (it = node.f_next) { + const window_widget = @as(*gtk.Window, @ptrCast(@alignCast(node.f_data))); + const window = gobject.ext.cast(Window, window_widget) orelse continue; + + var window_surfaces = try window.collectSurfaces(alloc); + defer window_surfaces.deinit(alloc); + + try all_surfaces.appendSlice(alloc, window_surfaces.items); + } + + return all_surfaces; + } + /// Set the configuration for this application. The reference count /// is increased on the new configuration and the old one is /// unreferenced. diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 6da49115e..b5d4b34d4 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -13,6 +13,8 @@ const key = @import("../key.zig"); const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Window = @import("window.zig").Window; +const Surface = @import("surface.zig").Surface; +const Tab = @import("tab.zig").Tab; const Config = @import("config.zig").Config; const log = std.log.scoped(.gtk_ghostty_command_palette); @@ -146,34 +148,155 @@ pub const CommandPalette = extern struct { return; }; - const cfg = config.get(); - // Clear existing binds priv.source.removeAll(); + const alloc = Application.default().allocator(); + var commands: std.ArrayList(*Command) = .{}; + defer { + for (commands.items) |cmd| cmd.unref(); + commands.deinit(alloc); + } + + self.collectJumpCommands(config, &commands) catch |err| { + log.warn("failed to collect jump commands: {}", .{err}); + }; + + self.collectRegularCommands(config, &commands, alloc); + + // Sort commands + std.mem.sort(*Command, commands.items, {}, struct { + fn lessThan(_: void, a: *Command, b: *Command) bool { + return compareCommands(a, b); + } + }.lessThan); + + for (commands.items) |cmd| { + const cmd_ref = cmd.as(gobject.Object); + priv.source.append(cmd_ref); + } + } + + /// Collect regular commands from configuration, filtering out unsupported actions. + fn collectRegularCommands( + self: *CommandPalette, + config: *Config, + commands: *std.ArrayList(*Command), + alloc: std.mem.Allocator, + ) void { + _ = self; + const cfg = config.get(); + for (cfg.@"command-palette-entry".value.items) |command| { // Filter out actions that are not implemented or don't make sense // for GTK. - switch (command.action) { - .close_all_windows, - .toggle_secure_input, - .check_for_updates, - .redo, - .undo, - .reset_window_size, - .toggle_window_float_on_top, - => continue, - - else => {}, - } + if (!isActionSupportedOnGtk(command.action)) continue; const cmd = Command.new(config, command); - const cmd_ref = cmd.as(gobject.Object); - priv.source.append(cmd_ref); - cmd_ref.unref(); + commands.append(alloc, cmd) catch |err| { + log.warn("failed to add command to list: {}", .{err}); + cmd.unref(); + continue; + }; + } + } + + /// Check if an action is supported on GTK. + fn isActionSupportedOnGtk(action: input.Binding.Action) bool { + return switch (action) { + .close_all_windows, + .toggle_secure_input, + .check_for_updates, + .redo, + .undo, + .reset_window_size, + .toggle_window_float_on_top, + => false, + + else => true, + }; + } + + /// Collect jump commands for all surfaces across all windows. + fn collectJumpCommands( + self: *CommandPalette, + config: *Config, + commands: *std.ArrayList(*Command), + ) !void { + _ = self; + const app = Application.default(); + const alloc = app.allocator(); + + // Collect all surfaces from all windows + var surfaces = app.collectAllSurfaces(alloc) catch |err| { + log.warn("failed to collect surfaces for jump commands: {}", .{err}); + return; + }; + defer { + for (surfaces.items) |info| { + info.surface.unref(); + info.window.unref(); + } + surfaces.deinit(alloc); + } + + for (surfaces.items) |info| { + const cmd = Command.newJump(config, info.surface, info.window); + errdefer cmd.unref(); + try commands.append(alloc, cmd); } } + /// Compare two commands for sorting. + /// Sorts alphabetically by title (case-insensitive), with colon normalization + /// so "Foo:" sorts before "Foo Bar:". Uses sort_key as tie-breaker. + fn compareCommands(a: *Command, b: *Command) bool { + const a_title = a.propGetTitle() orelse return false; + const b_title = b.propGetTitle() orelse return true; + + // Compare case-insensitively with colon normalization + var i: usize = 0; + var j: usize = 0; + while (i < a_title.len and j < b_title.len) { + // Get characters, replacing ':' with '\t' + const a_char = if (a_title[i] == ':') '\t' else a_title[i]; + const b_char = if (b_title[j] == ':') '\t' else b_title[j]; + + const a_lower = std.ascii.toLower(a_char); + const b_lower = std.ascii.toLower(b_char); + + if (a_lower != b_lower) { + return a_lower < b_lower; + } + + i += 1; + j += 1; + } + + // If one title is a prefix of the other, shorter one comes first + if (a_title.len != b_title.len) { + return a_title.len < b_title.len; + } + + // Titles are equal - use sort_key as tie-breaker if both have one + const a_priv = a.private(); + const b_priv = b.private(); + + const a_sort_key = switch (a_priv.data) { + .regular => 0, + .jump => |*ja| ja.sort_key, + }; + const b_sort_key = switch (b_priv.data) { + .regular => 0, + .jump => |*jb| jb.sort_key, + }; + + if (a_sort_key != 0 and b_sort_key != 0) { + return a_sort_key < b_sort_key; + } + return false; + } + fn close(self: *CommandPalette) void { const priv = self.private(); _ = priv.dialog.close(); @@ -234,6 +357,16 @@ pub const CommandPalette = extern struct { self.close(); const cmd = gobject.ext.cast(Command, object_ orelse return) orelse return; + + // Handle jump commands differently + if (cmd.isJump()) { + const surface = cmd.getJumpSurface() orelse return; + const window = cmd.getJumpWindow() orelse return; + focusSurface(surface, window); + return; + } + + // Regular command - emit trigger signal const action = cmd.getAction() orelse return; // Signal that an an action has been selected. Signals are synchronous @@ -413,22 +546,30 @@ const Command = extern struct { }; pub const Private = struct { - /// The configuration we should use to get keybindings. config: ?*Config = null, - - /// Arena used to manage our allocations. arena: ArenaAllocator, + data: CommandData, - /// The command. - command: ?input.Command = null, + pub var offset: c_int = 0; - /// Cache the formatted action. - action: ?[:0]const u8 = null, + pub const CommandData = union(enum) { + regular: RegularData, + jump: JumpData, + }; - /// Cache the formatted action_key. - action_key: ?[:0]const u8 = null, + pub const RegularData = struct { + command: input.Command, + action: ?[:0]const u8 = null, + action_key: ?[:0]const u8 = null, + }; - pub var offset: c_int = 0; + pub const JumpData = struct { + surface: *Surface, + window: *Window, + title: ?[:0]const u8 = null, + description: ?[:0]const u8 = null, + sort_key: usize, + }; }; pub fn new(config: *Config, command: input.Command) *Self { @@ -437,7 +578,34 @@ const Command = extern struct { }); const priv = self.private(); - priv.command = command.clone(priv.arena.allocator()) catch null; + const cloned = command.clone(priv.arena.allocator()) catch { + self.unref(); + return undefined; + }; + + priv.data = .{ + .regular = .{ + .command = cloned, + }, + }; + + return self; + } + + /// Create a new jump command that focuses a specific surface. + pub fn newJump(config: *Config, surface: *Surface, window: *Window) *Self { + const self = gobject.ext.newInstance(Self, .{ + .config = config, + }); + + const priv = self.private(); + priv.data = .{ + .jump = .{ + .surface = surface.ref(), + .window = window.ref(), + .sort_key = @intFromPtr(surface), + }, + }; return self; } @@ -459,6 +627,14 @@ const Command = extern struct { priv.config = null; } + switch (priv.data) { + .regular => {}, + .jump => |*j| { + j.surface.unref(); + j.window.unref(); + }, + } + gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), @@ -481,52 +657,116 @@ const Command = extern struct { fn propGetActionKey(self: *Self) ?[:0]const u8 { const priv = self.private(); - if (priv.action_key) |action_key| return action_key; + const regular = switch (priv.data) { + .regular => |*r| r, + .jump => return null, + }; - const command = priv.command orelse return null; + if (regular.action_key) |action_key| return action_key; - priv.action_key = std.fmt.allocPrintSentinel( + regular.action_key = std.fmt.allocPrintSentinel( priv.arena.allocator(), "{f}", - .{command.action}, + .{regular.command.action}, 0, ) catch null; - return priv.action_key; + return regular.action_key; } fn propGetAction(self: *Self) ?[:0]const u8 { const priv = self.private(); - if (priv.action) |action| return action; + const regular = switch (priv.data) { + .regular => |*r| r, + .jump => return null, + }; - const command = priv.command orelse return null; + if (regular.action) |action| return action; const cfg = if (priv.config) |config| config.get() else return null; const keybinds = cfg.keybind.set; const alloc = priv.arena.allocator(); - priv.action = action: { + regular.action = action: { var buf: [64]u8 = undefined; - const trigger = keybinds.getTrigger(command.action) orelse break :action null; + const trigger = keybinds.getTrigger(regular.command.action) orelse break :action null; const accel = (key.accelFromTrigger(&buf, trigger) catch break :action null) orelse break :action null; break :action alloc.dupeZ(u8, accel) catch return null; }; - return priv.action; + return regular.action; } fn propGetTitle(self: *Self) ?[:0]const u8 { const priv = self.private(); - const command = priv.command orelse return null; - return command.title; + + switch (priv.data) { + .regular => |*r| return r.command.title, + .jump => |*j| { + if (j.title) |title| return title; + + const alloc = priv.arena.allocator(); + + var val = gobject.ext.Value.new(?[:0]const u8); + defer val.unset(); + gobject.Object.getProperty( + j.surface.as(gobject.Object), + "title", + &val, + ); + const surface_title = gobject.ext.Value.get(&val, ?[:0]const u8) orelse "Untitled"; + + j.title = std.fmt.allocPrintSentinel( + alloc, + "Focus: {s}", + .{surface_title}, + 0, + ) catch null; + + return j.title; + }, + } } fn propGetDescription(self: *Self) ?[:0]const u8 { const priv = self.private(); - const command = priv.command orelse return null; - return command.description; + + switch (priv.data) { + .regular => |*r| return r.command.description, + .jump => |*j| { + if (j.description) |desc| return desc; + + const alloc = priv.arena.allocator(); + + var title_val = gobject.ext.Value.new(?[:0]const u8); + defer title_val.unset(); + gobject.Object.getProperty( + j.surface.as(gobject.Object), + "title", + &title_val, + ); + const title = gobject.ext.Value.get(&title_val, ?[:0]const u8) orelse "Untitled"; + + var pwd_val = gobject.ext.Value.new(?[:0]const u8); + defer pwd_val.unset(); + gobject.Object.getProperty( + j.surface.as(gobject.Object), + "pwd", + &pwd_val, + ); + const pwd = gobject.ext.Value.get(&pwd_val, ?[:0]const u8); + + if (pwd) |p| { + if (std.mem.indexOf(u8, title, p) == null) { + j.description = alloc.dupeZ(u8, p) catch null; + } + } + + return j.description; + }, + } } //--------------------------------------------------------------- @@ -536,8 +776,34 @@ const Command = extern struct { /// allocated data that will be freed when this object is. pub fn getAction(self: *Self) ?input.Binding.Action { const priv = self.private(); - const command = priv.command orelse return null; - return command.action; + return switch (priv.data) { + .regular => |*r| r.command.action, + .jump => null, + }; + } + + /// Check if this is a jump command. + pub fn isJump(self: *Self) bool { + const priv = self.private(); + return priv.data == .jump; + } + + /// Get the jump surface. + pub fn getJumpSurface(self: *Self) ?*Surface { + const priv = self.private(); + return switch (priv.data) { + .regular => null, + .jump => |*j| j.surface, + }; + } + + /// Get the jump window. + pub fn getJumpWindow(self: *Self) ?*Window { + const priv = self.private(); + return switch (priv.data) { + .regular => null, + .jump => |*j| j.window, + }; } //--------------------------------------------------------------- @@ -567,3 +833,43 @@ const Command = extern struct { } }; }; + +/// Focus a surface in a window, bringing the window to front and switching +/// to the appropriate tab if needed. +fn focusSurface(surface: *Surface, window: *Window) void { + window.as(gtk.Window).present(); + + // Find the tab containing this surface + const tab_view = window.getTabView(); + const n = tab_view.getNPages(); + if (n < 0) return; + + for (0..@intCast(n)) |i| { + const page = tab_view.getNthPage(@intCast(i)); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse continue; + + // Check if this tab contains the surface + const tree = tab.getSurfaceTree() orelse continue; + var it = tree.iterator(); + var found = false; + while (it.next()) |entry| { + if (entry.view == surface) { + found = true; + break; + } + } + + if (found) { + // Switch to this tab + tab_view.setSelectedPage(page); + + // Look up the split tree and update last focused surface + const split_tree = tab.getSplitTree(); + split_tree.setLastFocusedSurface(surface); + + surface.grabFocus(); + break; + } + } +} diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 46b3268d9..e203879ca 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -478,6 +478,12 @@ pub const SplitTree = extern struct { return surface; } + /// Sets the last focused surface in the tree. This is used to track + /// which surface should be considered "active" for the split tree. + pub fn setLastFocusedSurface(self: *Self, surface: ?*Surface) void { + self.private().last_focused.set(surface); + } + /// Returns whether any of the surfaces in the tree have a parent. /// This is important because we can only rebuild the widget tree /// when every surface has no parent. diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 77fd2eea5..638db87b8 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -804,6 +804,58 @@ pub const Window = extern struct { return self.private().config; } + /// Information about a surface in a window. + pub const SurfaceInfo = struct { + surface: *Surface, + window: *Window, + }; + + /// Collect all surfaces from all tabs in this window. + /// The caller must unref each surface and window when done. + pub fn collectSurfaces( + self: *Self, + alloc: std.mem.Allocator, + ) !std.ArrayList(SurfaceInfo) { + var surfaces: std.ArrayList(SurfaceInfo) = .{}; + errdefer { + for (surfaces.items) |info| { + info.surface.unref(); + info.window.unref(); + } + surfaces.deinit(alloc); + } + + const priv = self.private(); + const n = priv.tab_view.getNPages(); + if (n < 0) return surfaces; + + for (0..@intCast(n)) |i| { + const page = priv.tab_view.getNthPage(@intCast(i)); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse { + log.warn("unexpected non-Tab child in tab view", .{}); + continue; + }; + + const tree = tab.getSurfaceTree() orelse continue; + + var it = tree.iterator(); + while (it.next()) |entry| { + try surfaces.append(alloc, .{ + .surface = entry.view.ref(), + .window = self.ref(), + }); + } + } + + return surfaces; + } + + /// Get the tab view for this window. + pub fn getTabView(self: *Self) *adw.TabView { + return self.private().tab_view; + } + /// Get the current window decoration value for this window. pub fn getWindowDecoration(self: *Self) configpkg.WindowDecoration { const priv = self.private(); -- 2.51.2 From 8754c53e0e538feb5ea74b9dac274784465f6ba0 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 3 Jan 2026 22:07:57 +0100 Subject: [PATCH 158/349] gtk: Get jump command title from Surface title --- src/apprt/gtk/class/command_palette.zig | 29 +++---------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index b5d4b34d4..f022f13a7 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -708,15 +708,7 @@ const Command = extern struct { if (j.title) |title| return title; const alloc = priv.arena.allocator(); - - var val = gobject.ext.Value.new(?[:0]const u8); - defer val.unset(); - gobject.Object.getProperty( - j.surface.as(gobject.Object), - "title", - &val, - ); - const surface_title = gobject.ext.Value.get(&val, ?[:0]const u8) orelse "Untitled"; + const surface_title = j.surface.getTitle() orelse "Untitled"; j.title = std.fmt.allocPrintSentinel( alloc, @@ -740,23 +732,8 @@ const Command = extern struct { const alloc = priv.arena.allocator(); - var title_val = gobject.ext.Value.new(?[:0]const u8); - defer title_val.unset(); - gobject.Object.getProperty( - j.surface.as(gobject.Object), - "title", - &title_val, - ); - const title = gobject.ext.Value.get(&title_val, ?[:0]const u8) orelse "Untitled"; - - var pwd_val = gobject.ext.Value.new(?[:0]const u8); - defer pwd_val.unset(); - gobject.Object.getProperty( - j.surface.as(gobject.Object), - "pwd", - &pwd_val, - ); - const pwd = gobject.ext.Value.get(&pwd_val, ?[:0]const u8); + const title = j.surface.getTitle() orelse "Untitled"; + const pwd = j.surface.getPwd(); if (pwd) |p| { if (std.mem.indexOf(u8, title, p) == null) { -- 2.51.2 From d3aa68413974c6e5b932d52fdf04372c6e494c8b Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sat, 3 Jan 2026 22:15:23 +0100 Subject: [PATCH 159/349] gtk: Remove window reference from jump commands Removes redundant implementations that is already present in the core application to work with surfaces. --- src/apprt/gtk/class/application.zig | 30 -------- src/apprt/gtk/class/command_palette.zig | 93 +++++-------------------- src/apprt/gtk/class/split_tree.zig | 6 -- src/apprt/gtk/class/window.zig | 47 ------------- 4 files changed, 16 insertions(+), 160 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index eb83fa8a2..b16bce049 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1176,36 +1176,6 @@ pub const Application = extern struct { return self.private().config.ref(); } - /// Collect all surfaces from all windows in the application. - /// The caller must unref each surface and window and deinit the list. - pub fn collectAllSurfaces( - self: *Self, - alloc: Allocator, - ) !std.ArrayList(Window.SurfaceInfo) { - var all_surfaces: std.ArrayList(Window.SurfaceInfo) = .{}; - errdefer { - for (all_surfaces.items) |info| { - info.surface.unref(); - info.window.unref(); - } - all_surfaces.deinit(alloc); - } - - const windows = self.as(gtk.Application).getWindows(); - var it: ?*glib.List = windows; - while (it) |node| : (it = node.f_next) { - const window_widget = @as(*gtk.Window, @ptrCast(@alignCast(node.f_data))); - const window = gobject.ext.cast(Window, window_widget) orelse continue; - - var window_surfaces = try window.collectSurfaces(alloc); - defer window_surfaces.deinit(alloc); - - try all_surfaces.appendSlice(alloc, window_surfaces.items); - } - - return all_surfaces; - } - /// Set the configuration for this application. The reference count /// is increased on the new configuration and the old one is /// unreferenced. diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index f022f13a7..5e942e4a3 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -192,10 +192,14 @@ pub const CommandPalette = extern struct { // for GTK. if (!isActionSupportedOnGtk(command.action)) continue; - const cmd = Command.new(config, command); + const cmd = Command.new(config, command) catch |err| { + log.warn("failed to create command: {}", .{err}); + continue; + }; + errdefer cmd.unref(); + commands.append(alloc, cmd) catch |err| { log.warn("failed to add command to list: {}", .{err}); - cmd.unref(); continue; }; } @@ -227,21 +231,11 @@ pub const CommandPalette = extern struct { const app = Application.default(); const alloc = app.allocator(); - // Collect all surfaces from all windows - var surfaces = app.collectAllSurfaces(alloc) catch |err| { - log.warn("failed to collect surfaces for jump commands: {}", .{err}); - return; - }; - defer { - for (surfaces.items) |info| { - info.surface.unref(); - info.window.unref(); - } - surfaces.deinit(alloc); - } - - for (surfaces.items) |info| { - const cmd = Command.newJump(config, info.surface, info.window); + // Get all surfaces from the core app + const core_app = app.core(); + for (core_app.surfaces.items) |apprt_surface| { + const surface = apprt_surface.gobj(); + const cmd = Command.newJump(config, surface); errdefer cmd.unref(); try commands.append(alloc, cmd); } @@ -361,8 +355,7 @@ pub const CommandPalette = extern struct { // Handle jump commands differently if (cmd.isJump()) { const surface = cmd.getJumpSurface() orelse return; - const window = cmd.getJumpWindow() orelse return; - focusSurface(surface, window); + surface.present(); return; } @@ -565,23 +558,20 @@ const Command = extern struct { pub const JumpData = struct { surface: *Surface, - window: *Window, title: ?[:0]const u8 = null, description: ?[:0]const u8 = null, sort_key: usize, }; }; - pub fn new(config: *Config, command: input.Command) *Self { + pub fn new(config: *Config, command: input.Command) Allocator.Error!*Self { const self = gobject.ext.newInstance(Self, .{ .config = config, }); + errdefer self.unref(); const priv = self.private(); - const cloned = command.clone(priv.arena.allocator()) catch { - self.unref(); - return undefined; - }; + const cloned = try command.clone(priv.arena.allocator()); priv.data = .{ .regular = .{ @@ -593,7 +583,7 @@ const Command = extern struct { } /// Create a new jump command that focuses a specific surface. - pub fn newJump(config: *Config, surface: *Surface, window: *Window) *Self { + pub fn newJump(config: *Config, surface: *Surface) *Self { const self = gobject.ext.newInstance(Self, .{ .config = config, }); @@ -602,7 +592,6 @@ const Command = extern struct { priv.data = .{ .jump = .{ .surface = surface.ref(), - .window = window.ref(), .sort_key = @intFromPtr(surface), }, }; @@ -631,7 +620,6 @@ const Command = extern struct { .regular => {}, .jump => |*j| { j.surface.unref(); - j.window.unref(); }, } @@ -774,15 +762,6 @@ const Command = extern struct { }; } - /// Get the jump window. - pub fn getJumpWindow(self: *Self) ?*Window { - const priv = self.private(); - return switch (priv.data) { - .regular => null, - .jump => |*j| j.window, - }; - } - //--------------------------------------------------------------- const C = Common(Self, Private); @@ -810,43 +789,3 @@ const Command = extern struct { } }; }; - -/// Focus a surface in a window, bringing the window to front and switching -/// to the appropriate tab if needed. -fn focusSurface(surface: *Surface, window: *Window) void { - window.as(gtk.Window).present(); - - // Find the tab containing this surface - const tab_view = window.getTabView(); - const n = tab_view.getNPages(); - if (n < 0) return; - - for (0..@intCast(n)) |i| { - const page = tab_view.getNthPage(@intCast(i)); - const child = page.getChild(); - const tab = gobject.ext.cast(Tab, child) orelse continue; - - // Check if this tab contains the surface - const tree = tab.getSurfaceTree() orelse continue; - var it = tree.iterator(); - var found = false; - while (it.next()) |entry| { - if (entry.view == surface) { - found = true; - break; - } - } - - if (found) { - // Switch to this tab - tab_view.setSelectedPage(page); - - // Look up the split tree and update last focused surface - const split_tree = tab.getSplitTree(); - split_tree.setLastFocusedSurface(surface); - - surface.grabFocus(); - break; - } - } -} diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index e203879ca..46b3268d9 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -478,12 +478,6 @@ pub const SplitTree = extern struct { return surface; } - /// Sets the last focused surface in the tree. This is used to track - /// which surface should be considered "active" for the split tree. - pub fn setLastFocusedSurface(self: *Self, surface: ?*Surface) void { - self.private().last_focused.set(surface); - } - /// Returns whether any of the surfaces in the tree have a parent. /// This is important because we can only rebuild the widget tree /// when every surface has no parent. diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 638db87b8..8df250447 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -804,53 +804,6 @@ pub const Window = extern struct { return self.private().config; } - /// Information about a surface in a window. - pub const SurfaceInfo = struct { - surface: *Surface, - window: *Window, - }; - - /// Collect all surfaces from all tabs in this window. - /// The caller must unref each surface and window when done. - pub fn collectSurfaces( - self: *Self, - alloc: std.mem.Allocator, - ) !std.ArrayList(SurfaceInfo) { - var surfaces: std.ArrayList(SurfaceInfo) = .{}; - errdefer { - for (surfaces.items) |info| { - info.surface.unref(); - info.window.unref(); - } - surfaces.deinit(alloc); - } - - const priv = self.private(); - const n = priv.tab_view.getNPages(); - if (n < 0) return surfaces; - - for (0..@intCast(n)) |i| { - const page = priv.tab_view.getNthPage(@intCast(i)); - const child = page.getChild(); - const tab = gobject.ext.cast(Tab, child) orelse { - log.warn("unexpected non-Tab child in tab view", .{}); - continue; - }; - - const tree = tab.getSurfaceTree() orelse continue; - - var it = tree.iterator(); - while (it.next()) |entry| { - try surfaces.append(alloc, .{ - .surface = entry.view.ref(), - .window = self.ref(), - }); - } - } - - return surfaces; - } - /// Get the tab view for this window. pub fn getTabView(self: *Self) *adw.TabView { return self.private().tab_view; -- 2.51.2 From bd20f844aa80e5b4a9960c24e6378f12f88e06e1 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 4 Jan 2026 00:16:28 +0000 Subject: [PATCH 160/349] 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 c9c3093b6..c3e2de9f8 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/ghostty-themes-release-20251222-150520-0add1e1.tgz", - .hash = "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU", + .url = "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz", + .hash = "N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 28fd4927e..022a7401e 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", "hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs=" }, - "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU": { + "N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV": { "name": "iterm2_themes", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz", - "hash": "sha256-cMIEDZFYdilCavhL5pWQ6jRerGUFSZIjLwbxNossSeg=" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz", + "hash": "sha256-bWKQxRggz/ZLr6w0Zt/hTnnAAb13VQWV70ScCsNFIZk=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 3cfa8f280..d845ea10e 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -171,11 +171,11 @@ in }; } { - name = "N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU"; + name = "N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz"; - hash = "sha256-cMIEDZFYdilCavhL5pWQ6jRerGUFSZIjLwbxNossSeg="; + url = "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz"; + hash = "sha256-bWKQxRggz/ZLr6w0Zt/hTnnAAb13VQWV70ScCsNFIZk="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index c84b24638..0eeb7c5f1 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -6,7 +6,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918 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/ghostty-themes-release-20251222-150520-0add1e1.tgz +https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index b511a4f27..7749eb67e 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251222-150520-0add1e1.tgz", - "dest": "vendor/p/N-V-__8AAIdIAwAOceDblkuOARUyuTKbDdGPjPClPLhMeIfU", - "sha256": "70c2040d91587629426af84be69590ea345eac65054992232f06f1368b2c49e8" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz", + "dest": "vendor/p/N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV", + "sha256": "6d6290c51820cff64bafac3466dfe14e79c001bd77550595ef449c0ac3452199" }, { "type": "archive", -- 2.51.2 From f5d7108c51240724e71ed85de06ee64d8cba014f Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Sun, 4 Jan 2026 08:36:40 +0100 Subject: [PATCH 161/349] gtk: Remove strong reference to surface from command palette --- src/apprt/gtk/class/command_palette.zig | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index 5e942e4a3..af1ef8e5e 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -10,6 +10,7 @@ const gtk = @import("gtk"); const input = @import("../../../input.zig"); const gresource = @import("../build/gresource.zig"); const key = @import("../key.zig"); +const WeakRef = @import("../weak_ref.zig").WeakRef; const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Window = @import("window.zig").Window; @@ -355,6 +356,7 @@ pub const CommandPalette = extern struct { // Handle jump commands differently if (cmd.isJump()) { const surface = cmd.getJumpSurface() orelse return; + defer surface.unref(); surface.present(); return; } @@ -557,7 +559,7 @@ const Command = extern struct { }; pub const JumpData = struct { - surface: *Surface, + surface: WeakRef(Surface) = .empty, title: ?[:0]const u8 = null, description: ?[:0]const u8 = null, sort_key: usize, @@ -591,10 +593,10 @@ const Command = extern struct { const priv = self.private(); priv.data = .{ .jump = .{ - .surface = surface.ref(), .sort_key = @intFromPtr(surface), }, }; + priv.data.jump.surface.set(surface); return self; } @@ -619,7 +621,7 @@ const Command = extern struct { switch (priv.data) { .regular => {}, .jump => |*j| { - j.surface.unref(); + j.surface.set(null); }, } @@ -695,8 +697,11 @@ const Command = extern struct { .jump => |*j| { if (j.title) |title| return title; + const surface = j.surface.get() orelse return null; + defer surface.unref(); + const alloc = priv.arena.allocator(); - const surface_title = j.surface.getTitle() orelse "Untitled"; + const surface_title = surface.getTitle() orelse "Untitled"; j.title = std.fmt.allocPrintSentinel( alloc, @@ -718,10 +723,13 @@ const Command = extern struct { .jump => |*j| { if (j.description) |desc| return desc; + const surface = j.surface.get() orelse return null; + defer surface.unref(); + const alloc = priv.arena.allocator(); - const title = j.surface.getTitle() orelse "Untitled"; - const pwd = j.surface.getPwd(); + const title = surface.getTitle() orelse "Untitled"; + const pwd = surface.getPwd(); if (pwd) |p| { if (std.mem.indexOf(u8, title, p) == null) { @@ -753,12 +761,13 @@ const Command = extern struct { return priv.data == .jump; } - /// Get the jump surface. + /// Get the jump surface. Returns a strong reference that the caller + /// must unref when done, or null if the surface has been destroyed. pub fn getJumpSurface(self: *Self) ?*Surface { const priv = self.private(); return switch (priv.data) { .regular => null, - .jump => |*j| j.surface, + .jump => |*j| j.surface.get(), }; } -- 2.51.2 From 742c9ca390a199eae1f1796ebb6469b96b2089d3 Mon Sep 17 00:00:00 2001 From: Jan Klass Date: Sun, 4 Jan 2026 23:44:04 +0100 Subject: [PATCH 162/349] i18n: Add missing German translation --- po/de_DE.UTF-8.po | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index c7fc6643f..27b353e38 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -3,6 +3,7 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Robin Pfรคffle , 2025. +# Jan Klass , 2026. # msgid "" msgstr "" @@ -320,4 +321,4 @@ msgstr "Ghostty-Entwickler" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "" +msgstr "Ghostty: Terminal Inspektor" -- 2.51.2 From 1d4a5d91e08eb09eb1b1b984278c8ec980a3b2ca Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 4 Jan 2026 21:15:29 -0500 Subject: [PATCH 163/349] More debugging for position.y differences --- src/font/shaper/coretext.zig | 80 ++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 09f123b4b..4b8b05419 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -382,15 +382,16 @@ pub const Shaper = struct { const line = typesetter.createLine(.{ .location = 0, .length = 0 }); self.cf_release_pool.appendAssumeCapacity(line); - // This keeps track of the current x offset within a run. + // This keeps track of the current x offset (sum of advance.width) for + // a run. var run_offset_x: f64 = 0.0; - // This keeps track of the current x offset and cluster for a cell. + // This keeps track of the cell starting x and cluster. var cell_offset: CellOffset = .{}; // For debugging positions, turn this on: - //var run_offset_y: f64 = 0.0; - //var cell_offset_y: f64 = 0.0; + var run_offset_y: f64 = 0.0; + var cell_offset_y: f64 = 0.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 @@ -448,11 +449,11 @@ pub const Shaper = struct { }; // For debugging positions, turn this on: - //cell_offset_y = run_offset_y; + cell_offset_y = run_offset_y; } // For debugging positions, turn this on: - //try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); + try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); const x_offset = position.x - cell_offset.x; @@ -468,7 +469,7 @@ pub const Shaper = struct { run_offset_x += advance.width; // For debugging positions, turn this on: - //run_offset_y += advance.height; + run_offset_y += advance.height; } } @@ -654,21 +655,31 @@ pub const Shaper = struct { const advance_y_offset = run_offset_y - cell_offset_y; const x_offset_diff = x_offset - advance_x_offset; const y_offset_diff = position.y - advance_y_offset; + const positions_differ = @abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001; + const old_offset_y = position.y - cell_offset_y; + const position_y_differs = @abs(cell_offset_y) > 0.0001; - if (@abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001) { + if (positions_differ or position_y_differs) { var allocating = std.Io.Writer.Allocating.init(alloc); const writer = &allocating.writer; const codepoints = state.codepoints.items; + var last_cluster: ?u32 = null; for (codepoints) |cp| { - if (cp.cluster == cell_offset.cluster and + if ((cp.cluster == cell_offset.cluster or cp.cluster == cell_offset.cluster - 1 or cp.cluster == cell_offset.cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { + if (last_cluster) |last| { + if (cp.cluster != last) { + try writer.writeAll(" "); + } + } try writer.print("\\u{{{x}}}", .{cp.codepoint}); + last_cluster = cp.cluster; } } try writer.writeAll(" โ†’ "); for (codepoints) |cp| { - if (cp.cluster == cell_offset.cluster and + if ((cp.cluster == cell_offset.cluster or cp.cluster == cell_offset.cluster - 1 or cp.cluster == cell_offset.cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); @@ -676,27 +687,34 @@ pub const Shaper = struct { } const formatted_cps = try allocating.toOwnedSlice(); - // Note that the codepoints from `start_index .. end_index + 1` - // might not include all the codepoints being shaped. Sometimes a - // codepoint gets represented in a glyph with a later codepoint - // such that the index for the former codepoint is skipped and just - // the index for the latter codepoint is used. Additionally, this - // gets called as we iterate through the glyphs, so it won't - // include the codepoints that come later that might be affecting - // positions for the current glyph. Usually though, for that case - // the positions of the later glyphs will also be affected and show - // up in the logs. - log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) current cp={x}, cps={s}", .{ - cell_offset.cluster, - x_offset, - position.y, - advance_x_offset, - advance_y_offset, - x_offset_diff, - y_offset_diff, - state.codepoints.items[index].codepoint, - formatted_cps, - }); + if (positions_differ) { + log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) current cp={x}, cps={s}", .{ + cell_offset.cluster, + x_offset, + position.y, + advance_x_offset, + advance_y_offset, + x_offset_diff, + y_offset_diff, + state.codepoints.items[index].codepoint, + formatted_cps, + }); + } + + if (position_y_differs) { + log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} current cp={x}, cps={s}", .{ + cell_offset.cluster, + x_offset, + position.y, + run_offset_x, + run_offset_y, + cell_offset.x, + cell_offset_y, + old_offset_y, + state.codepoints.items[index].codepoint, + formatted_cps, + }); + } } } }; -- 2.51.2 From b830a8397ce411e2546282798058779b3ace0b23 Mon Sep 17 00:00:00 2001 From: Jan Klass Date: Mon, 5 Jan 2026 10:48:09 +0100 Subject: [PATCH 164/349] Update po/de_DE.UTF-8.po Co-authored-by: Klaus Hipp --- po/de_DE.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 27b353e38..82d186967 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -321,4 +321,4 @@ msgstr "Ghostty-Entwickler" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Terminal Inspektor" +msgstr "Ghostty: Terminalinspektor" -- 2.51.2 From f36abed35a52f7cfd0e21debbf90a5705abc7acc Mon Sep 17 00:00:00 2001 From: Peter Cardenas <16930781+PeterCardenas@users.noreply.github.com> Date: Mon, 5 Jan 2026 05:26:33 -0800 Subject: [PATCH 165/349] fix: reset progress bar on reset terminal --- src/termio/stream_handler.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 182770339..c647e3ba2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -952,6 +952,9 @@ pub const StreamHandler = struct { // Reset resets our palette so we report it for mode 2031. self.surfaceMessageWriter(.{ .report_color_scheme = false }); + + // Clear the progress bar + self.progressReport(.{ .state = .remove }); } pub fn queryKittyKeyboard(self: *StreamHandler) !void { -- 2.51.2 From d38558aee10fb444c6eb7e9d38b04b777035cacd Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 5 Jan 2026 09:34:17 -0500 Subject: [PATCH 166/349] =?UTF-8?q?Show=20current=20cp=20with=20=E2=96=B8?= =?UTF-8?q?=20in=20list=20of=20cps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/font/shaper/coretext.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 4b8b05419..f1c643848 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -663,6 +663,7 @@ pub const Shaper = struct { var allocating = std.Io.Writer.Allocating.init(alloc); const writer = &allocating.writer; const codepoints = state.codepoints.items; + const current_cp = state.codepoints.items[index].codepoint; var last_cluster: ?u32 = null; for (codepoints) |cp| { if ((cp.cluster == cell_offset.cluster or cp.cluster == cell_offset.cluster - 1 or cp.cluster == cell_offset.cluster + 1) and @@ -673,6 +674,9 @@ pub const Shaper = struct { try writer.writeAll(" "); } } + if (cp.cluster == cell_offset.cluster and cp.codepoint == current_cp) { + try writer.writeAll("โ–ธ"); + } try writer.print("\\u{{{x}}}", .{cp.codepoint}); last_cluster = cp.cluster; } @@ -688,7 +692,7 @@ pub const Shaper = struct { const formatted_cps = try allocating.toOwnedSlice(); if (positions_differ) { - log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) current cp={x}, cps={s}", .{ + log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) cps = {s}", .{ cell_offset.cluster, x_offset, position.y, @@ -696,13 +700,12 @@ pub const Shaper = struct { advance_y_offset, x_offset_diff, y_offset_diff, - state.codepoints.items[index].codepoint, formatted_cps, }); } if (position_y_differs) { - log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} current cp={x}, cps={s}", .{ + log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} cps = {s}", .{ cell_offset.cluster, x_offset, position.y, @@ -711,7 +714,6 @@ pub const Shaper = struct { cell_offset.x, cell_offset_y, old_offset_y, - state.codepoints.items[index].codepoint, formatted_cps, }); } -- 2.51.2 From 41f63384f54c429ee873faeed41c9aba53218441 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 5 Jan 2026 09:59:37 -0500 Subject: [PATCH 167/349] Turn off debugging --- src/font/shaper/coretext.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index f1c643848..a1e0c0129 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -390,8 +390,8 @@ pub const Shaper = struct { var cell_offset: CellOffset = .{}; // For debugging positions, turn this on: - var run_offset_y: f64 = 0.0; - var cell_offset_y: f64 = 0.0; + //var run_offset_y: f64 = 0.0; + //var cell_offset_y: f64 = 0.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 @@ -449,11 +449,11 @@ pub const Shaper = struct { }; // For debugging positions, turn this on: - cell_offset_y = run_offset_y; + //cell_offset_y = run_offset_y; } // For debugging positions, turn this on: - try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); + //try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); const x_offset = position.x - cell_offset.x; @@ -469,7 +469,7 @@ pub const Shaper = struct { run_offset_x += advance.width; // For debugging positions, turn this on: - run_offset_y += advance.height; + //run_offset_y += advance.height; } } -- 2.51.2 From 7aec7effea33b39bb8caabda26e6eaad0486815f Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 5 Jan 2026 10:12:05 -0500 Subject: [PATCH 168/349] Add test for Tai Tham letter position.y difference --- src/font/shaper/coretext.zig | 60 ++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index a1e0c0129..bcf1fe879 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1533,6 +1533,66 @@ test "shape Tai Tham vowels (position differs from advance)" { try testing.expectEqual(@as(usize, 1), count); } +test "shape Tai Tham letters (position.y differs from advance)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Tai Tham for this to work, if we can't find + // Noto Sans Tai Tham, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Tai Tham", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x1a49, buf[buf_idx..]); // HA + buf_idx += try std.unicode.utf8Encode(0x1a60, buf[buf_idx..]); // SAKOT + // Second grapheme cluster, combining with the first in a ligature: + buf_idx += try std.unicode.utf8Encode(0x1a3f, buf[buf_idx..]); // YA + buf_idx += try std.unicode.utf8Encode(0x1a69, buf[buf_idx..]); // U + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 3), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 1), cells[2].x); // U from second grapheme + + // The U glyph renders at a y below zero + try testing.expectEqual(@as(i16, -3), cells[2].y_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape box glyphs" { const testing = std.testing; const alloc = testing.allocator; -- 2.51.2 From 15899b70fb111d5ba6206a5cb329444ed64f49c2 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 5 Jan 2026 10:35:22 -0500 Subject: [PATCH 169/349] simplify run_offset_x comment --- src/font/shaper/coretext.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index bcf1fe879..c8822a373 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -382,8 +382,7 @@ pub const Shaper = struct { const line = typesetter.createLine(.{ .location = 0, .length = 0 }); self.cf_release_pool.appendAssumeCapacity(line); - // This keeps track of the current x offset (sum of advance.width) for - // a run. + // This keeps track of the current x offset (sum of advance.width) var run_offset_x: f64 = 0.0; // This keeps track of the cell starting x and cluster. -- 2.51.2 From f3e90e23d92cc9c829bd8e4ee35f091062647897 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 5 Jan 2026 15:47:07 -0600 Subject: [PATCH 170/349] Lower unimplemented OSC from warning to debug --- src/terminal/stream.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index ba6b57d5c..665b54284 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2108,7 +2108,7 @@ pub fn Stream(comptime Handler: type) type { .conemu_wait_input, .conemu_guimacro, => { - log.warn("unimplemented OSC callback: {}", .{cmd}); + log.debug("unimplemented OSC callback: {}", .{cmd}); }, .invalid => { -- 2.51.2 From 87fc5357eb20d174e9fc67525fa0cb08f5b67caa Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 21:36:04 -0700 Subject: [PATCH 171/349] Add config entries for tab and split inheritance --- src/config/Config.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 88f3d5375..f4ec21684 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1845,11 +1845,21 @@ keybind: Keybinds = .{}, /// This setting is only supported currently on macOS. @"window-vsync": bool = true, -/// If true, new windows and tabs will inherit the working directory of the +/// If true, new windows will inherit the working directory of the /// previously focused window. If no window was previously focused, the default /// working directory will be used (the `working-directory` option). @"window-inherit-working-directory": bool = true, +/// If true, new tabs will inherit the working directory of the +/// previously focused window. If no window was previously focused, the default +/// working directory will be used (the `working-directory` option). +@"tab-inherit-working-directory": bool = true, + +/// If true, new split panes will inherit the working directory of the +/// previously focused window. If no window was previously focused, the default +/// working directory will be used (the `working-directory` option). +@"split-inherit-working-directory": bool = true, + /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default font size /// will be used. If this is false, the default font size specified in the -- 2.51.2 From 02e1a68263763e8e64bc86365227ea46855a9ca5 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:08:30 -0700 Subject: [PATCH 172/349] Use config to determine cwd inheritance for windows, tabs, and splits - Define NewSurfaceContext enum (window, tab, split) - Add shouldInheritWorkingDirectory helper function - Thread surface context through newConfig and newSurfaceOptions - Replace window-inherit-working-directory checks with context-aware logic - Add context to embedded CAPI and GTK Surface structs --- src/apprt/embedded.zig | 32 ++++++++++++++++++++++++++++++-- src/apprt/gtk/class/surface.zig | 14 ++++++-------- src/apprt/surface.zig | 18 +++++++++++++++++- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 27f07604a..2c329af7c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -456,6 +456,15 @@ pub const Surface = struct { /// Wait after the command exits wait_after_command: bool = false, + + /// Context for the new surface + context: Context = .c_window, + + pub const Context = enum(c_int) { + c_window = 0, + c_tab = 1, + c_split = 2, + }; }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -477,7 +486,13 @@ pub const Surface = struct { errdefer app.core_app.deleteSurface(self); // Shallow copy the config so that we can modify it. - var config = try apprt.surface.newConfig(app.core_app, &app.config); + const surface_context: apprt.surface.NewSurfaceContext = switch (opts.context) { + .c_window => .window, + .c_tab => .tab, + .c_split => .split, + }; + + var config = try apprt.surface.newConfig(app.core_app, &app.config, surface_context); defer config.deinit(); // If we have a working directory from the options then we set it. @@ -894,14 +909,27 @@ pub const Surface = struct { }; } - pub fn newSurfaceOptions(self: *const Surface) apprt.Surface.Options { + pub fn newSurfaceOptions(self: *const Surface, context: apprt.surface.NewSurfaceContext) apprt.Surface.Options { const font_size: f32 = font_size: { if (!self.app.config.@"window-inherit-font-size") break :font_size 0; break :font_size self.core_surface.font_size.points; }; + const working_directory: ?[*:0]const u8 = wd: { + if (!apprt.surface.shouldInheritWorkingDirectory(context, &self.app.config)) break :wd null; + const cwd = self.core_surface.pwd(self.app.core_app.alloc) catch null orelse break :wd null; + defer self.app.core_app.alloc.free(cwd); + break :wd self.app.core_app.alloc.dupeZ(u8, cwd) catch null; + }; + return .{ .font_size = font_size, + .working_directory = working_directory, + .context = switch (context) { + .window => .c_window, + .tab => .c_tab, + .split => .c_split, + }, }; } diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cb5122314..f5bf01d06 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -671,6 +671,9 @@ pub const Surface = extern struct { error_page: *adw.StatusPage, terminal_page: *gtk.Overlay, + /// The context for this surface (window, tab, or split) + context: apprt.surface.NewSurfaceContext = .window, + pub var offset: c_int = 0; }; @@ -716,10 +719,8 @@ pub const Surface = extern struct { // Remainder needs a config. If there is no config we just assume // we aren't inheriting any of these values. if (priv.config) |config_obj| { - const config = config_obj.get(); - - // Setup our pwd if configured to inherit - if (config.@"window-inherit-working-directory") { + // Setup our cwd if configured to inherit + if (apprt.surface.shouldInheritWorkingDirectory(context, config_obj.get())) { if (parent.rt_surface.surface.getPwd()) |pwd| { priv.pwd = glib.ext.dupeZ(u8, pwd); self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec); @@ -3203,10 +3204,7 @@ pub const Surface = extern struct { errdefer app.core().deleteSurface(self.rt()); // Initialize our surface configuration. - var config = try apprt.surface.newConfig( - app.core(), - priv.config.?.get(), - ); + var config = try apprt.surface.newConfig(app.core(), priv.config.?.get(), priv.context); defer config.deinit(); // Properties that can impact surface init diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 45a847493..56dfae31b 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -159,12 +159,28 @@ pub const Mailbox = struct { } }; +/// Context for new surface creation to determine inheritance behavior +pub const NewSurfaceContext = enum { + window, + tab, + split, +}; + +pub fn shouldInheritWorkingDirectory(context: NewSurfaceContext, config: *const Config) bool { + return switch (context) { + .window => config.@"window-inherit-working-directory", + .tab => config.@"tab-inherit-working-directory", + .split => config.@"split-inherit-working-directory", + }; +} + /// Returns a new config for a surface for the given app that should be /// used for any new surfaces. The resulting config should be deinitialized /// after the surface is initialized. pub fn newConfig( app: *const App, config: *const Config, + context: NewSurfaceContext, ) Allocator.Error!Config { // Create a shallow clone var copy = config.shallowClone(app.alloc); @@ -175,7 +191,7 @@ pub fn newConfig( // Get our previously focused surface for some inherited values. const prev = app.focusedSurface(); if (prev) |p| { - if (config.@"window-inherit-working-directory") { + if (shouldInheritWorkingDirectory(context, config)) { if (try p.pwd(alloc)) |pwd| { copy.@"working-directory" = pwd; } -- 2.51.2 From 7cf4c8dc538e3fbc3fb37d399e30776fb298acfc Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:12:10 -0700 Subject: [PATCH 173/349] Add context field to C config struct --- include/ghostty.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/ghostty.h b/include/ghostty.h index 0ad15cf69..ef7c0e4c8 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -428,6 +428,7 @@ typedef struct { size_t env_var_count; const char* initial_input; bool wait_after_command; + int context; // 0=window, 1=tab, 2=split } ghostty_surface_config_s; typedef struct { -- 2.51.2 From dba0ff5339e0d49e16d3b74686a7282333d90501 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:14:00 -0700 Subject: [PATCH 174/349] Add C API function to handle new surfaces of different types --- include/ghostty.h | 5 ++++- macos/Sources/Ghostty/Ghostty.App.swift | 6 +++--- src/apprt/embedded.zig | 18 +++++++++++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index ef7c0e4c8..ea7fd07aa 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1036,7 +1036,10 @@ ghostty_surface_t ghostty_surface_new(ghostty_app_t, void ghostty_surface_free(ghostty_surface_t); void* ghostty_surface_userdata(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); -ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); /* deprecated: use context-specific functions */ +ghostty_surface_config_s ghostty_surface_inherited_config_window(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config_tab(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config_split(ghostty_surface_t); void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); bool ghostty_surface_process_exited(ghostty_surface_t); diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 4e9166168..e23bf0457 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -773,7 +773,7 @@ extension Ghostty { name: Notification.ghosttyNewWindow, object: surfaceView, userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config_window(surface)), ] ) @@ -810,7 +810,7 @@ extension Ghostty { name: Notification.ghosttyNewTab, object: surfaceView, userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config_tab(surface)), ] ) @@ -839,7 +839,7 @@ extension Ghostty { object: surfaceView, userInfo: [ "direction": direction, - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config_split(surface)), ] ) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 2c329af7c..8836db403 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1551,8 +1551,24 @@ pub const CAPI = struct { } /// Returns the config to use for surfaces that inherit from this one. + /// Deprecated: Use ghostty_surface_inherited_config_window/tab/split instead. export fn ghostty_surface_inherited_config(surface: *Surface) Surface.Options { - return surface.newSurfaceOptions(); + return surface.newSurfaceOptions(.window); + } + + /// Returns the config to use for new windows that inherit from this surface. + export fn ghostty_surface_inherited_config_window(surface: *Surface) Surface.Options { + return surface.newSurfaceOptions(.window); + } + + /// Returns the config to use for new tabs that inherit from this surface. + export fn ghostty_surface_inherited_config_tab(surface: *Surface) Surface.Options { + return surface.newSurfaceOptions(.tab); + } + + /// Returns the config to use for new splits that inherit from this surface. + export fn ghostty_surface_inherited_config_split(surface: *Surface) Surface.Options { + return surface.newSurfaceOptions(.split); } /// Update the configuration to the provided config for only this surface. -- 2.51.2 From 496f5b3ed72751727a26b363692ad3c82cd2995d Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:17:38 -0700 Subject: [PATCH 175/349] Add the context to the Swift layer. - Define NewSurfaceContext to match the Zig enum name and avoid magic numbers. --- .../Sources/Ghostty/Surface View/SurfaceView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index c224d373e..3fa98837f 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -625,6 +625,13 @@ extension Ghostty { #endif } + /// Context for surface creation, matching the Zig NewSurfaceContext enum + enum NewSurfaceContext: Int32 { + case window = 0 + case tab = 1 + case split = 2 + } + /// The configuration for a surface. For any configuration not set, defaults will be chosen from /// libghostty, usually from the Ghostty configuration. struct SurfaceConfiguration { @@ -646,6 +653,9 @@ extension Ghostty { /// Wait after the command var waitAfterCommand: Bool = false + /// Context for surface creation + var context: NewSurfaceContext = .window + init() {} init(from config: ghostty_surface_config_s) { @@ -667,6 +677,7 @@ extension Ghostty { } } } + self.context = NewSurfaceContext(rawValue: config.context) ?? .window } /// Provides a C-compatible ghostty configuration within a closure. The configuration @@ -700,6 +711,9 @@ extension Ghostty { // Set wait after command config.wait_after_command = waitAfterCommand + // Set context + config.context = context.rawValue + // Use withCString to ensure strings remain valid for the duration of the closure return try workingDirectory.withCString { cWorkingDir in config.working_directory = cWorkingDir -- 2.51.2 From 05229502bf14758b77cb0ad4ef27132734906537 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:19:17 -0700 Subject: [PATCH 176/349] Add the surface context to the Surface's setParent In order to set the private context variable so that initiSurface can use it. --- src/apprt/gtk/class/surface.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index f5bf01d06..01569dab0 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -699,6 +699,7 @@ pub const Surface = extern struct { pub fn setParent( self: *Self, parent: *CoreSurface, + context: apprt.surface.NewSurfaceContext, ) void { const priv = self.private(); @@ -709,6 +710,9 @@ pub const Surface = extern struct { return; } + // Store the context so initSurface can use it + priv.context = context; + // Setup our font size const font_size_ptr = glib.ext.create(font.face.DesiredSize); errdefer glib.ext.destroy(font_size_ptr); -- 2.51.2 From 82614511ab1bd889e8870abed01d7c3db062bdd4 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:21:45 -0700 Subject: [PATCH 177/349] Use the new GTK Surface::setParent from the tab and split --- src/apprt/gtk/class/split_tree.zig | 2 +- src/apprt/gtk/class/tab.zig | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 46b3268d9..8d859adca 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -219,7 +219,7 @@ pub const SplitTree = extern struct { // Inherit properly if we were asked to. if (parent_) |p| { if (p.core()) |core| { - surface.setParent(core); + surface.setParent(core, .split); } } diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index fb3b8b0ef..ae05cd1ad 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -161,8 +161,12 @@ pub const Tab = extern struct { /// ever created for a tab. If a surface was already created this does /// nothing. pub fn setParent(self: *Self, parent: *CoreSurface) void { + self.setParentWithContext(parent, .tab); + } + + pub fn setParentWithContext(self: *Self, parent: *CoreSurface, context: apprt.surface.NewSurfaceContext) void { if (self.getActiveSurface()) |surface| { - surface.setParent(parent); + surface.setParent(parent, context); } } -- 2.51.2 From 0af2a3f693fbca383c8bce9db2a9870fb71953e9 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sat, 11 Oct 2025 22:23:08 -0700 Subject: [PATCH 178/349] Enable distinguishing between a new tab in a new window (which should inherit based on the window setting), and a new tab in an existing window (which should inherit base on tab setting) --- src/apprt/gtk/class/application.zig | 4 ++-- src/apprt/gtk/class/window.zig | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index b16bce049..848aa22db 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -2238,8 +2238,8 @@ const Action = struct { .{}, ); - // Create a new tab - win.newTab(parent); + // Create a new tab with window context (first tab in new window) + win.newTabForWindow(parent); // Show the window gtk.Window.present(win.as(gtk.Window)); diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 77fd2eea5..c2a8924f2 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -361,10 +361,14 @@ pub const Window = extern struct { /// at the position dictated by the `window-new-tab-position` config. /// The new tab will be selected. pub fn newTab(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_); + _ = self.newTabPage(parent_, .tab); } - fn newTabPage(self: *Self, parent_: ?*CoreSurface) *adw.TabPage { + pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void { + _ = self.newTabPage(parent_, .window); + } + + fn newTabPage(self: *Self, parent_: ?*CoreSurface, context: apprt.surface.NewSurfaceContext) *adw.TabPage { const priv = self.private(); const tab_view = priv.tab_view; @@ -372,7 +376,9 @@ pub const Window = extern struct { const tab = gobject.ext.newInstance(Tab, .{ .config = priv.config, }); - if (parent_) |p| tab.setParent(p); + if (parent_) |p| { + tab.setParentWithContext(p, context); + } // Get the position that we should insert the new tab at. const config = if (priv.config) |v| v.get() else { @@ -1231,7 +1237,7 @@ pub const Window = extern struct { _: *adw.TabOverview, self: *Self, ) callconv(.c) *adw.TabPage { - return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null); + return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab); } fn tabOverviewOpen( -- 2.51.2 From c035fb53855a35cd06e1b3d5f3813029803f37e4 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sun, 12 Oct 2025 22:05:21 -0700 Subject: [PATCH 179/349] Add an enum type for the C API --- include/ghostty.h | 8 ++++++- .../Ghostty/Surface View/SurfaceView.swift | 22 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index ea7fd07aa..7258c8838 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -416,6 +416,12 @@ typedef union { ghostty_platform_ios_s ios; } ghostty_platform_u; +typedef enum { + GHOSTTY_SURFACE_CONTEXT_WINDOW = 0, + GHOSTTY_SURFACE_CONTEXT_TAB = 1, + GHOSTTY_SURFACE_CONTEXT_SPLIT = 2, +} ghostty_surface_context_e; + typedef struct { ghostty_platform_e platform_tag; ghostty_platform_u platform; @@ -428,7 +434,7 @@ typedef struct { size_t env_var_count; const char* initial_input; bool wait_after_command; - int context; // 0=window, 1=tab, 2=split + ghostty_surface_context_e context; } ghostty_surface_config_s; typedef struct { diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 3fa98837f..095441192 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -625,11 +625,19 @@ extension Ghostty { #endif } - /// Context for surface creation, matching the Zig NewSurfaceContext enum - enum NewSurfaceContext: Int32 { - case window = 0 - case tab = 1 - case split = 2 + /// Context for surface creation, matching ghostty_surface_context_e + enum NewSurfaceContext: ghostty_surface_context_e.RawValue { + case window = 0 // GHOSTTY_SURFACE_CONTEXT_WINDOW + case tab = 1 // GHOSTTY_SURFACE_CONTEXT_TAB + case split = 2 // GHOSTTY_SURFACE_CONTEXT_SPLIT + + init(_ cValue: ghostty_surface_context_e) { + self.init(rawValue: cValue.rawValue)! + } + + var cValue: ghostty_surface_context_e { + ghostty_surface_context_e(rawValue: self.rawValue) + } } /// The configuration for a surface. For any configuration not set, defaults will be chosen from @@ -677,7 +685,7 @@ extension Ghostty { } } } - self.context = NewSurfaceContext(rawValue: config.context) ?? .window + self.context = NewSurfaceContext(config.context) } /// Provides a C-compatible ghostty configuration within a closure. The configuration @@ -712,7 +720,7 @@ extension Ghostty { config.wait_after_command = waitAfterCommand // Set context - config.context = context.rawValue + config.context = context.cValue // Use withCString to ensure strings remain valid for the duration of the closure return try workingDirectory.withCString { cWorkingDir in -- 2.51.2 From d6607997235ad5059f3c7692df2fd2e9154127a6 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Mon, 13 Oct 2025 10:59:01 -0700 Subject: [PATCH 180/349] Consolidate the several ghostty_surface_inherited_config functions back into a single function with a second parameter for the source context. --- include/ghostty.h | 5 +---- macos/Sources/Ghostty/Ghostty.App.swift | 6 +++--- src/apprt/embedded.zig | 28 +++++++++---------------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 7258c8838..c390e2102 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1042,10 +1042,7 @@ ghostty_surface_t ghostty_surface_new(ghostty_app_t, void ghostty_surface_free(ghostty_surface_t); void* ghostty_surface_userdata(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); -ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); /* deprecated: use context-specific functions */ -ghostty_surface_config_s ghostty_surface_inherited_config_window(ghostty_surface_t); -ghostty_surface_config_s ghostty_surface_inherited_config_tab(ghostty_surface_t); -ghostty_surface_config_s ghostty_surface_inherited_config_split(ghostty_surface_t); +ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, ghostty_surface_context_e); void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); bool ghostty_surface_process_exited(ghostty_surface_t); diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e23bf0457..191da9aca 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -773,7 +773,7 @@ extension Ghostty { name: Notification.ghosttyNewWindow, object: surfaceView, userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config_window(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_WINDOW)), ] ) @@ -810,7 +810,7 @@ extension Ghostty { name: Notification.ghosttyNewTab, object: surfaceView, userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config_tab(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_TAB)), ] ) @@ -839,7 +839,7 @@ extension Ghostty { object: surfaceView, userInfo: [ "direction": direction, - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config_split(surface)), + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: ghostty_surface_inherited_config(surface, GHOSTTY_SURFACE_CONTEXT_SPLIT)), ] ) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 8836db403..186b0c2e4 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1551,24 +1551,16 @@ pub const CAPI = struct { } /// Returns the config to use for surfaces that inherit from this one. - /// Deprecated: Use ghostty_surface_inherited_config_window/tab/split instead. - export fn ghostty_surface_inherited_config(surface: *Surface) Surface.Options { - return surface.newSurfaceOptions(.window); - } - - /// Returns the config to use for new windows that inherit from this surface. - export fn ghostty_surface_inherited_config_window(surface: *Surface) Surface.Options { - return surface.newSurfaceOptions(.window); - } - - /// Returns the config to use for new tabs that inherit from this surface. - export fn ghostty_surface_inherited_config_tab(surface: *Surface) Surface.Options { - return surface.newSurfaceOptions(.tab); - } - - /// Returns the config to use for new splits that inherit from this surface. - export fn ghostty_surface_inherited_config_split(surface: *Surface) Surface.Options { - return surface.newSurfaceOptions(.split); + export fn ghostty_surface_inherited_config( + surface: *Surface, + source: Surface.Options.Context, + ) Surface.Options { + const context: apprt.surface.NewSurfaceContext = switch (source) { + .c_window => .window, + .c_tab => .tab, + .c_split => .split, + }; + return surface.newSurfaceOptions(context); } /// Update the configuration to the provided config for only this surface. -- 2.51.2 From b119bc6089f5a7e672ab8672f9de25b46a017ac0 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Mon, 13 Oct 2025 19:12:03 -0700 Subject: [PATCH 181/349] consolidated enums --- .../Ghostty/Surface View/SurfaceView.swift | 21 ++----------- src/apprt/embedded.zig | 31 +++---------------- src/apprt/surface.zig | 8 ++--- 3 files changed, 12 insertions(+), 48 deletions(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 095441192..357e82a19 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -625,21 +625,6 @@ extension Ghostty { #endif } - /// Context for surface creation, matching ghostty_surface_context_e - enum NewSurfaceContext: ghostty_surface_context_e.RawValue { - case window = 0 // GHOSTTY_SURFACE_CONTEXT_WINDOW - case tab = 1 // GHOSTTY_SURFACE_CONTEXT_TAB - case split = 2 // GHOSTTY_SURFACE_CONTEXT_SPLIT - - init(_ cValue: ghostty_surface_context_e) { - self.init(rawValue: cValue.rawValue)! - } - - var cValue: ghostty_surface_context_e { - ghostty_surface_context_e(rawValue: self.rawValue) - } - } - /// The configuration for a surface. For any configuration not set, defaults will be chosen from /// libghostty, usually from the Ghostty configuration. struct SurfaceConfiguration { @@ -662,7 +647,7 @@ extension Ghostty { var waitAfterCommand: Bool = false /// Context for surface creation - var context: NewSurfaceContext = .window + var context: ghostty_surface_context_e = GHOSTTY_SURFACE_CONTEXT_WINDOW init() {} @@ -685,7 +670,7 @@ extension Ghostty { } } } - self.context = NewSurfaceContext(config.context) + self.context = config.context } /// Provides a C-compatible ghostty configuration within a closure. The configuration @@ -720,7 +705,7 @@ extension Ghostty { config.wait_after_command = waitAfterCommand // Set context - config.context = context.cValue + config.context = context // Use withCString to ensure strings remain valid for the duration of the closure return try workingDirectory.withCString { cWorkingDir in diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 186b0c2e4..d1d38c24d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -458,13 +458,7 @@ pub const Surface = struct { wait_after_command: bool = false, /// Context for the new surface - context: Context = .c_window, - - pub const Context = enum(c_int) { - c_window = 0, - c_tab = 1, - c_split = 2, - }; + context: apprt.surface.NewSurfaceContext = .window, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -486,13 +480,7 @@ pub const Surface = struct { errdefer app.core_app.deleteSurface(self); // Shallow copy the config so that we can modify it. - const surface_context: apprt.surface.NewSurfaceContext = switch (opts.context) { - .c_window => .window, - .c_tab => .tab, - .c_split => .split, - }; - - var config = try apprt.surface.newConfig(app.core_app, &app.config, surface_context); + var config = try apprt.surface.newConfig(app.core_app, &app.config, opts.context); defer config.deinit(); // If we have a working directory from the options then we set it. @@ -925,11 +913,7 @@ pub const Surface = struct { return .{ .font_size = font_size, .working_directory = working_directory, - .context = switch (context) { - .window => .c_window, - .tab => .c_tab, - .split => .c_split, - }, + .context = context, }; } @@ -1553,14 +1537,9 @@ pub const CAPI = struct { /// Returns the config to use for surfaces that inherit from this one. export fn ghostty_surface_inherited_config( surface: *Surface, - source: Surface.Options.Context, + source: apprt.surface.NewSurfaceContext, ) Surface.Options { - const context: apprt.surface.NewSurfaceContext = switch (source) { - .c_window => .window, - .c_tab => .tab, - .c_split => .split, - }; - return surface.newSurfaceOptions(context); + return surface.newSurfaceOptions(source); } /// Update the configuration to the provided config for only this surface. diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 56dfae31b..be2f59149 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -160,10 +160,10 @@ pub const Mailbox = struct { }; /// Context for new surface creation to determine inheritance behavior -pub const NewSurfaceContext = enum { - window, - tab, - split, +pub const NewSurfaceContext = enum(c_int) { + window = 0, + tab = 1, + split = 2, }; pub fn shouldInheritWorkingDirectory(context: NewSurfaceContext, config: *const Config) bool { -- 2.51.2 From 55285fee28bf11e12e9d82b028c44c761b33713f Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Wed, 3 Dec 2025 11:15:14 -0800 Subject: [PATCH 182/349] preserve multi-line formatting --- src/apprt/gtk/class/surface.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 01569dab0..0d6c64433 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3208,7 +3208,11 @@ pub const Surface = extern struct { errdefer app.core().deleteSurface(self.rt()); // Initialize our surface configuration. - var config = try apprt.surface.newConfig(app.core(), priv.config.?.get(), priv.context); + var config = try apprt.surface.newConfig( + app.core(), + priv.config.?.get(), + priv.context, + ); defer config.deinit(); // Properties that can impact surface init -- 2.51.2 From 93f33bc0d65222f8ecdd3c470c8281b35c17c43d Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Sun, 28 Dec 2025 14:45:15 -0800 Subject: [PATCH 183/349] clarify config documentation around previously focused windows/tabs/splits --- 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 f4ec21684..eef23cccb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1851,12 +1851,12 @@ keybind: Keybinds = .{}, @"window-inherit-working-directory": bool = true, /// If true, new tabs will inherit the working directory of the -/// previously focused window. If no window was previously focused, the default +/// previously focused tab. If no tab was previously focused, the default /// working directory will be used (the `working-directory` option). @"tab-inherit-working-directory": bool = true, /// If true, new split panes will inherit the working directory of the -/// previously focused window. If no window was previously focused, the default +/// previously focused split. If no split was previously focused, the default /// working directory will be used (the `working-directory` option). @"split-inherit-working-directory": bool = true, -- 2.51.2 From 1c7ba3dbe01da392c3588ee0e9885d7d259dbb2d Mon Sep 17 00:00:00 2001 From: Jan Klass Date: Tue, 6 Jan 2026 10:26:40 +0100 Subject: [PATCH 184/349] Update rev date and last translator --- po/de_DE.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 82d186967..f73f1c251 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -10,8 +10,8 @@ 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-08-25 19:38+0100\n" -"Last-Translator: Robin \n" +"PO-Revision-Date: 2026-01-06 10:25+0100\n" +"Last-Translator: Jan Klass \n" "Language-Team: German \n" "Language: de\n" "MIME-Version: 1.0\n" -- 2.51.2 From 9b6a3be99339bcefcc49b7791b7b9761d24e6093 Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Tue, 6 Jan 2026 22:15:19 +0800 Subject: [PATCH 185/349] macOS: Selection for Find feature Adds the `selection_for_search` action, with Cmd+E keybind by default. This action inputs the currently selected text into the search field without changing focus, matching standard macOS behavior. --- include/ghostty.h | 9 ++++- macos/Sources/App/macOS/AppDelegate.swift | 2 ++ macos/Sources/App/macOS/MainMenu.xib | 11 ++++-- .../Terminal/BaseTerminalController.swift | 6 +++- macos/Sources/Ghostty/Ghostty.Action.swift | 12 +++++++ macos/Sources/Ghostty/Ghostty.App.swift | 35 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 1 + .../Ghostty/Surface View/SurfaceView.swift | 11 +++++- .../Surface View/SurfaceView_AppKit.swift | 8 +++++ src/Surface.zig | 9 +++++ src/apprt/action.zig | 19 ++++++++++ src/config/Config.zig | 6 ++++ src/input/Binding.zig | 4 +++ src/input/command.zig | 6 ++++ 14 files changed, 134 insertions(+), 5 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 0ad15cf69..726b368e7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -810,6 +810,11 @@ typedef struct { ssize_t selected; } ghostty_action_search_selected_s; +// apprt.action.SelectionForSearch +typedef struct { + const char* text; +} ghostty_action_selection_for_search_s; + // terminal.Scrollbar typedef struct { uint64_t total; @@ -878,11 +883,12 @@ typedef enum { GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, GHOSTTY_ACTION_COMMAND_FINISHED, GHOSTTY_ACTION_START_SEARCH, + GHOSTTY_ACTION_SELECTION_FOR_SEARCH, GHOSTTY_ACTION_END_SEARCH, GHOSTTY_ACTION_SEARCH_TOTAL, GHOSTTY_ACTION_SEARCH_SELECTED, GHOSTTY_ACTION_READONLY, - } ghostty_action_tag_e; +} ghostty_action_tag_e; typedef union { ghostty_action_split_direction_e new_split; @@ -919,6 +925,7 @@ typedef union { ghostty_action_progress_report_s progress_report; ghostty_action_command_finished_s command_finished; ghostty_action_start_search_s start_search; + ghostty_action_selection_for_search_s selection_for_search; ghostty_action_search_total_s search_total; ghostty_action_search_selected_s search_selected; ghostty_action_readonly_e readonly; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 57bfba828..c365fb935 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -46,6 +46,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuSelectAll: NSMenuItem? @IBOutlet private var menuFindParent: NSMenuItem? @IBOutlet private var menuFind: NSMenuItem? + @IBOutlet private var menuSelectionForFind: NSMenuItem? @IBOutlet private var menuFindNext: NSMenuItem? @IBOutlet private var menuFindPrevious: NSMenuItem? @IBOutlet private var menuHideFindBar: NSMenuItem? @@ -615,6 +616,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) + syncMenuShortcut(config, action: "selection_for_search", menuItem: self.menuSelectionForFind) syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index a321061dd..248063f89 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -58,6 +58,7 @@ + @@ -262,6 +263,12 @@ + + + + + + diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index fb86ce8f7..a4e0da7ee 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1383,7 +1383,11 @@ class BaseTerminalController: NSWindowController, @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } - + + @IBAction func selectionForFind(_ sender: Any) { + focusedSurface?.selectionForFind(sender) + } + @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 91f1491dd..c04c7d958 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -128,6 +128,18 @@ extension Ghostty.Action { } } + struct SelectionForSearch { + let text: String? + + init(c: ghostty_action_selection_for_search_s) { + if let contentCString = c.text { + self.text = String(cString: contentCString) + } else { + self.text = nil + } + } + } + enum PromptTitle { case surface case tab diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 4e9166168..69788c194 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -621,6 +621,9 @@ extension Ghostty { case GHOSTTY_ACTION_START_SEARCH: startSearch(app, target: target, v: action.action.start_search) + case GHOSTTY_ACTION_SELECTION_FOR_SEARCH: + selectionForSearch(app, target: target, v: action.action.selection_for_search) + case GHOSTTY_ACTION_END_SEARCH: endSearch(app, target: target) @@ -1881,6 +1884,38 @@ extension Ghostty { } } + private static func selectionForSearch( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_selection_for_search_s + ) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("selection_for_search 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 selectionForSearch = Ghostty.Action.SelectionForSearch(c: v) + DispatchQueue.main.async { + if surfaceView.searchState != nil, let text = selectionForSearch.text { + NotificationCenter.default.post( + name: .ghosttySelectionForSearch, + object: surfaceView, + userInfo: [ + "text": text + ] + ) + } + } + + default: + assertionFailure() + } + } + private static func endSearch( _ app: ghostty_app_t, target: ghostty_target_s) { diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index aa62c16f7..dbe9c173b 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -406,6 +406,7 @@ extension Notification.Name { /// Focus the search field static let ghosttySearchFocus = Notification.Name("com.mitchellh.ghostty.searchFocus") + static let ghosttySelectionForSearch = Notification.Name("com.mitchellh.ghostty.selectionForSearch") } // NOTE: I am moving all of these to Notification.Name extensions over time. This diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index c224d373e..b3717d4c5 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -475,7 +475,16 @@ extension Ghostty { } .onReceive(NotificationCenter.default.publisher(for: .ghosttySearchFocus)) { notification in guard notification.object as? SurfaceView === surfaceView else { return } - isSearchFieldFocused = true + DispatchQueue.main.async { + isSearchFieldFocused = true + } + } + .onReceive(NotificationCenter.default.publisher(for: .ghosttySelectionForSearch)) { notification in + guard notification.object as? SurfaceView === surfaceView else { return } + if let userInfo = notification.userInfo, let text = userInfo["text"] as? String { + searchState.needle = text + // We do not focus the textfield after the action to match macOS behavior + } } .background( GeometryReader { barGeo in diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 7f33df45a..1fc43ac82 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1519,6 +1519,14 @@ extension Ghostty { } } + @IBAction func selectionForFind(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "selection_for_search" + if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" diff --git a/src/Surface.zig b/src/Surface.zig index 43ee440c2..68cf46045 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5163,6 +5163,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ); }, + .selection_for_search => { + const selection = try self.selectionString(self.alloc) orelse return false; + return try self.rt_app.performAction( + .{ .surface = self }, + .selection_for_search, + .{ .text = selection }, + ); + }, + .end_search => { // We only return that this was performed if we actually // stopped a search, but we also send the apprt end_search so diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 25fc6f08a..7fdaabf08 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -316,6 +316,9 @@ pub const Action = union(Key) { /// Start the search overlay with an optional initial needle. start_search: StartSearch, + /// Input the selected text into the search field. + selection_for_search: SelectionForSearch, + /// End the search overlay, clearing the search state and hiding it. end_search, @@ -389,6 +392,7 @@ pub const Action = union(Key) { show_on_screen_keyboard, command_finished, start_search, + selection_for_search, end_search, search_total, search_selected, @@ -914,3 +918,18 @@ pub const SearchSelected = struct { }; } }; + +pub const SelectionForSearch = struct { + text: [:0]const u8, + + // Sync with: ghostty_action_selection_for_search_s + pub const C = extern struct { + text: [*:0]const u8, + }; + + pub fn cval(self: SelectionForSearch) C { + return .{ + .text = self.text.ptr, + }; + } +}; diff --git a/src/config/Config.zig b/src/config/Config.zig index 88f3d5375..698831ec1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6585,6 +6585,12 @@ pub const Keybinds = struct { .start_search, .{ .performable = true }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'e' }, .mods = .{ .super = true } }, + .selection_for_search, + .{ .performable = true }, + ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .shift = true } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index d5b24c61b..0ef5208bc 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -368,6 +368,9 @@ pub const Action = union(enum) { /// If a previous search is active, it is replaced. search: []const u8, + /// Input the selected text into the search field. + selection_for_search, + /// Navigate the search results. If there is no active search, this /// is not performed. navigate_search: NavigateSearch, @@ -1284,6 +1287,7 @@ pub const Action = union(enum) { .cursor_key, .search, .navigate_search, + .selection_for_search, .start_search, .end_search, .reset, diff --git a/src/input/command.zig b/src/input/command.zig index f089112db..3fc7b29f6 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -189,6 +189,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Start a search if one isn't already active.", }}, + .selection_for_search => comptime &.{.{ + .action = .selection_for_search, + .title = "Selection for Search", + .description = "Input the selected text into the search field.", + }}, + .end_search => comptime &.{.{ .action = .end_search, .title = "End Search", -- 2.51.2 From f258265ef0b858472f24851f9616c7f823a4467c Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 6 Jan 2026 10:30:13 -0500 Subject: [PATCH 186/349] font/shaper: keep codepoints in same cell if detecting ligature --- src/font/shaper/coretext.zig | 95 +++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index c8822a373..6d17fc014 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -389,8 +389,8 @@ pub const Shaper = struct { var cell_offset: CellOffset = .{}; // For debugging positions, turn this on: - //var run_offset_y: f64 = 0.0; - //var cell_offset_y: f64 = 0.0; + var run_offset_y: f64 = 0.0; + var cell_offset_y: f64 = 0.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 @@ -410,8 +410,8 @@ pub const Shaper = struct { // other so we can iterate over them and just append to our // cell buffer. const runs = line.getGlyphRuns(); - for (0..runs.getCount()) |i| { - const ctrun = runs.getValueAtIndex(macos.text.Run, i); + for (0..runs.getCount()) |run_i| { + const ctrun = runs.getValueAtIndex(macos.text.Run, run_i); const status = ctrun.getStatus(); if (status.non_monotonic or status.right_to_left) non_ltr = true; @@ -434,30 +434,45 @@ pub const Shaper = struct { // Our cluster is also our cell X position. If the cluster changes // then we need to reset our current cell offsets. const cluster = state.codepoints.items[index].cluster; - if (cell_offset.cluster != cluster) pad: { - // We previously asserted this but for rtl text this is - // not true. So we check for this and break out. In the - // future we probably need to reverse pad for rtl but - // I don't have a solid test case for this yet so let's - // wait for that. - if (cell_offset.cluster > cluster) break :pad; - - cell_offset = .{ - .cluster = cluster, - .x = run_offset_x, + if (cell_offset.cluster != cluster) { + // We previously asserted that the new cluster is greater + // than cell_offset.cluster, but for rtl text this is not + // true. We then used to break out of this block if cluster + // was less than cell_offset.cluster, but now this would + // fail to reset cell_offset.x and cell_offset.cluster and + // lead to incorrect shape.Cell `x` and `x_offset`. We + // don't have a test case for RTL, yet. + + const is_codepoint_first_in_cluster = blk: { + var i = index; + while (i > 0) { + i -= 1; + const codepoint = state.codepoints.items[i]; + + // Skip surrogate pair padding + if (codepoint.codepoint == 0) continue; + break :blk codepoint.cluster != cluster; + } else break :blk true; }; - // For debugging positions, turn this on: - //cell_offset_y = run_offset_y; + if (is_codepoint_first_in_cluster) { + cell_offset = .{ + .cluster = cluster, + .x = run_offset_x, + }; + + // For debugging positions, turn this on: + cell_offset_y = run_offset_y; + } } // For debugging positions, turn this on: - //try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); + try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); const x_offset = position.x - cell_offset.x; self.cell_buf.appendAssumeCapacity(.{ - .x = @intCast(cluster), + .x = @intCast(cell_offset.cluster), .x_offset = @intFromFloat(@round(x_offset)), .y_offset = @intFromFloat(@round(position.y)), .glyph_index = glyph, @@ -468,7 +483,7 @@ pub const Shaper = struct { run_offset_x += advance.width; // For debugging positions, turn this on: - //run_offset_y += advance.height; + run_offset_y += advance.height; } } @@ -657,15 +672,20 @@ pub const Shaper = struct { const positions_differ = @abs(x_offset_diff) > 0.0001 or @abs(y_offset_diff) > 0.0001; const old_offset_y = position.y - cell_offset_y; const position_y_differs = @abs(cell_offset_y) > 0.0001; + const cluster = state.codepoints.items[index].cluster; + const cluster_differs = cluster != cell_offset.cluster; - if (positions_differ or position_y_differs) { + if (positions_differ or position_y_differs or cluster_differs) { var allocating = std.Io.Writer.Allocating.init(alloc); const writer = &allocating.writer; const codepoints = state.codepoints.items; - const current_cp = state.codepoints.items[index].codepoint; var last_cluster: ?u32 = null; - for (codepoints) |cp| { - if ((cp.cluster == cell_offset.cluster or cp.cluster == cell_offset.cluster - 1 or cp.cluster == cell_offset.cluster + 1) and + for (codepoints, 0..) |cp, i| { + if ((cp.cluster == cluster - 3 or + cp.cluster == cluster - 2 or + cp.cluster == cluster - 1 or + cp.cluster == cluster or + cp.cluster == cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { if (last_cluster) |last| { @@ -673,7 +693,7 @@ pub const Shaper = struct { try writer.writeAll(" "); } } - if (cp.cluster == cell_offset.cluster and cp.codepoint == current_cp) { + if (i == index) { try writer.writeAll("โ–ธ"); } try writer.print("\\u{{{x}}}", .{cp.codepoint}); @@ -682,7 +702,11 @@ pub const Shaper = struct { } try writer.writeAll(" โ†’ "); for (codepoints) |cp| { - if ((cp.cluster == cell_offset.cluster or cp.cluster == cell_offset.cluster - 1 or cp.cluster == cell_offset.cluster + 1) and + if ((cp.cluster == cluster - 3 or + cp.cluster == cluster - 2 or + cp.cluster == cluster - 1 or + cp.cluster == cluster or + cp.cluster == cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); @@ -692,7 +716,7 @@ pub const Shaper = struct { if (positions_differ) { log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) cps = {s}", .{ - cell_offset.cluster, + cluster, x_offset, position.y, advance_x_offset, @@ -705,7 +729,7 @@ pub const Shaper = struct { if (position_y_differs) { log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} cps = {s}", .{ - cell_offset.cluster, + cluster, x_offset, position.y, run_offset_x, @@ -716,6 +740,21 @@ pub const Shaper = struct { formatted_cps, }); } + + if (cluster_differs) { + log.warn("cell_offset.cluster differs from cluster (potential ligature detected) cell_offset.cluster={d} cluster={d} diff={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) cps = {s}", .{ + cell_offset.cluster, + cluster, + cluster - cell_offset.cluster, + x_offset, + position.y, + run_offset_x, + run_offset_y, + cell_offset.x, + cell_offset_y, + formatted_cps, + }); + } } } }; -- 2.51.2 From 896615c00448516560d52494990d8e85b1b3a5f1 Mon Sep 17 00:00:00 2001 From: Britt Binler Date: Tue, 6 Jan 2026 10:15:06 -0500 Subject: [PATCH 187/349] font: add bitmap font tests for BDF, PCF, and OTB formats Add test coverage for bitmap font rendering using the Spleen 8x16 font in three formats: BDF (Bitmap Distribution Format), PCF (Portable Compiled Format), and OTB (OpenType Bitmap). Tests validate glyph rendering against expected pixel patterns. Addresses #8524. --- .gitattributes | 1 + src/font/embedded.zig | 6 + src/font/face/freetype.zig | 150 + src/font/res/BSD-2-Clause.txt | 24 + src/font/res/README.md | 6 +- src/font/res/spleen-8x16.bdf | 22328 ++++++++++++++++++++++++++++++++ src/font/res/spleen-8x16.otb | Bin 0 -> 25912 bytes src/font/res/spleen-8x16.pcf | Bin 0 -> 206164 bytes typos.toml | 1 + 9 files changed, 22515 insertions(+), 1 deletion(-) create mode 100644 src/font/res/BSD-2-Clause.txt create mode 100644 src/font/res/spleen-8x16.bdf create mode 100644 src/font/res/spleen-8x16.otb create mode 100644 src/font/res/spleen-8x16.pcf diff --git a/.gitattributes b/.gitattributes index 9fe672044..9158b3979 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,5 @@ 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/font/res/** linguist-vendored src/terminal/res/** linguist-vendored diff --git a/src/font/embedded.zig b/src/font/embedded.zig index 1e496075d..8e0ae33b8 100644 --- a/src/font/embedded.zig +++ b/src/font/embedded.zig @@ -47,3 +47,9 @@ pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf"); /// Terminus TTF is a scalable font with bitmap glyphs at various sizes. pub const terminus_ttf = @embedFile("res/TerminusTTF-Regular.ttf"); + +/// Spleen is a monospaced bitmap font available in multiple formats. +/// Used for testing bitmap font support across different file formats. +pub const spleen_bdf = @embedFile("res/spleen-8x16.bdf"); +pub const spleen_pcf = @embedFile("res/spleen-8x16.pcf"); +pub const spleen_otb = @embedFile("res/spleen-8x16.otb"); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index a6ef52c39..827753254 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -1284,3 +1284,153 @@ test "bitmap glyph" { } } } + +// Expected pixel pattern for Spleen 8x16 'A' (glyph index from char 'A') +// Derived from BDF BITMAP data: 00,00,7C,C6,C6,C6,FE,C6,C6,C6,C6,C6,00,00,00,00 +const spleen_A = + \\........ + \\........ + \\.#####.. + \\##...##. + \\##...##. + \\##...##. + \\#######. + \\##...##. + \\##...##. + \\##...##. + \\##...##. + \\##...##. + \\........ + \\........ + \\........ + \\........ +; +// Including the newline +const spleen_A_pitch = 9; +// Test parameters for bitmap font tests +const spleen_test_point_size = 12; +const spleen_test_dpi = 96; + +test "bitmap glyph BDF" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_bdf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + // Spleen 8x16 is a pure bitmap font at 16px height + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + // Get glyph index for 'A' + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + // Verify dimensions match Spleen 8x16 + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + // Verify pixel-perfect rendering + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +test "bitmap glyph PCF" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_pcf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +test "bitmap glyph OTB" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_otb; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} diff --git a/src/font/res/BSD-2-Clause.txt b/src/font/res/BSD-2-Clause.txt new file mode 100644 index 000000000..4387948e8 --- /dev/null +++ b/src/font/res/BSD-2-Clause.txt @@ -0,0 +1,24 @@ +Copyright (c) 2018-2024, Frederic Cambus +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/font/res/README.md b/src/font/res/README.md index 5ad4b274f..b4d77a783 100644 --- a/src/font/res/README.md +++ b/src/font/res/README.md @@ -1,6 +1,6 @@ # Fonts and Licenses -This project uses several fonts which fall under the SIL Open Font License (OFL-1.1) and MIT License: +This project uses several fonts which fall under the SIL Open Font License (OFL-1.1), MIT License, and BSD 2-Clause License: - Code New Roman (OFL-1.1) - [ยฉ 2014 Sam Radian. All Rights Reserved.](https://github.com/chrissimpkins/codeface/blob/master/fonts/code-new-roman/license.txt) @@ -28,8 +28,12 @@ This project uses several fonts which fall under the SIL Open Font License (OFL- - Terminus TTF (OFL-1.1) - [Copyright (c) 2010-2020 Dimitar Toshkov Zhekov with Reserved Font Name "Terminus Font"](https://sourceforge.net/projects/terminus-font/) - [Copyright (c) 2011-2023 Tilman Blumenbach with Reserved Font Name "Terminus (TTF)"](https://files.ax86.net/terminus-ttf/) +- Spleen (BSD 2-Clause) + - [Copyright (c) 2018-2024, Frederic Cambus](https://github.com/fcambus/spleen) A full copy of the OFL license can be found at [OFL.txt](./OFL.txt). An accompanying FAQ is also available at . A full copy of the MIT license can be found at [MIT.txt](./MIT.txt). + +A full copy of the BSD 2-Clause license can be found at [BSD-2-Clause.txt](./BSD-2-Clause.txt). diff --git a/src/font/res/spleen-8x16.bdf b/src/font/res/spleen-8x16.bdf new file mode 100644 index 000000000..5c7c2684b --- /dev/null +++ b/src/font/res/spleen-8x16.bdf @@ -0,0 +1,22328 @@ +STARTFONT 2.1 +COMMENT /* +COMMENT * Spleen 8x16 2.1.0 +COMMENT * Copyright (c) 2018-2024, Frederic Cambus +COMMENT * https://www.cambus.net/ +COMMENT * +COMMENT * Created: 2018-08-11 +COMMENT * Last Updated: 2024-01-27 +COMMENT * +COMMENT * Spleen is released under the BSD 2-Clause license. +COMMENT * See LICENSE file for details. +COMMENT * +COMMENT * SPDX-License-Identifier: BSD-2-Clause +COMMENT */ +FONT -misc-spleen-medium-r-normal--16-160-72-72-C-80-ISO10646-1 +SIZE 16 72 72 +FONTBOUNDINGBOX 8 16 0 -4 +STARTPROPERTIES 20 +FAMILY_NAME "Spleen" +WEIGHT_NAME "Medium" +FONT_VERSION "2.1.0" +FOUNDRY "misc" +SLANT "R" +SETWIDTH_NAME "Normal" +PIXEL_SIZE 16 +POINT_SIZE 160 +RESOLUTION_X 72 +RESOLUTION_Y 72 +SPACING "C" +AVERAGE_WIDTH 80 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +MIN_SPACE 8 +FONT_ASCENT 12 +FONT_DESCENT 4 +COPYRIGHT "Copyright (c) 2018-2024, Frederic Cambus" +DEFAULT_CHAR 32 +_GBDFED_INFO "Edited with gbdfed 1.6." +ENDPROPERTIES +CHARS 969 +STARTCHAR SPACE +ENCODING 32 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EXCLAMATION MARK +ENCODING 33 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +18 +18 +00 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUOTATION MARK +ENCODING 34 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR NUMBER SIGN +ENCODING 35 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +FE +6C +6C +6C +6C +FE +6C +6C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOLLAR SIGN +ENCODING 36 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +7E +D0 +D0 +D0 +7C +16 +16 +16 +16 +FC +10 +00 +00 +00 +ENDCHAR +STARTCHAR PERCENT SIGN +ENCODING 37 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +66 +6C +0C +18 +18 +30 +36 +66 +60 +00 +00 +00 +00 +ENDCHAR +STARTCHAR AMPERSAND +ENCODING 38 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +6C +6C +38 +70 +DA +CC +CC +7A +00 +00 +00 +00 +ENDCHAR +STARTCHAR APOSTROPHE +ENCODING 39 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT PARENTHESIS +ENCODING 40 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0E +18 +30 +30 +60 +60 +60 +60 +30 +30 +18 +0E +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT PARENTHESIS +ENCODING 41 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +70 +18 +0C +0C +06 +06 +06 +06 +0C +0C +18 +70 +00 +00 +00 +ENDCHAR +STARTCHAR ASTERISK +ENCODING 42 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +3C +18 +FF +18 +3C +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR PLUS SIGN +ENCODING 43 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +7E +18 +18 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMMA +ENCODING 44 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +30 +00 +00 +00 +ENDCHAR +STARTCHAR HYPHEN-MINUS +ENCODING 45 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +7E +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR FULL STOP +ENCODING 46 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SOLIDUS +ENCODING 47 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +0C +0C +18 +18 +30 +30 +60 +60 +C0 +C0 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT ZERO +ENCODING 48 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +CE +DE +F6 +E6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT ONE +ENCODING 49 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +38 +78 +58 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT TWO +ENCODING 50 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +06 +06 +0C +18 +30 +60 +C6 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT THREE +ENCODING 51 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +06 +06 +3C +06 +06 +06 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT FOUR +ENCODING 52 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +CC +CC +CC +CC +FE +0C +0C +0C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT FIVE +ENCODING 53 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +C0 +C0 +FC +06 +06 +06 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT SIX +ENCODING 54 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C0 +C0 +FC +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT SEVEN +ENCODING 55 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +06 +06 +0C +18 +30 +30 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT EIGHT +ENCODING 56 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +7C +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIGIT NINE +ENCODING 57 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +7E +06 +06 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR COLON +ENCODING 58 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SEMICOLON +ENCODING 59 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +18 +18 +30 +00 +00 +00 +ENDCHAR +STARTCHAR LESS-THAN SIGN +ENCODING 60 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +0C +18 +30 +60 +60 +30 +18 +0C +06 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EQUALS SIGN +ENCODING 61 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREATER-THAN SIGN +ENCODING 62 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +60 +30 +18 +0C +06 +06 +0C +18 +30 +60 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUESTION MARK +ENCODING 63 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +06 +0C +18 +30 +30 +00 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMMERCIAL AT +ENCODING 64 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +C2 +DA +DA +DA +DA +DE +C0 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A +ENCODING 65 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER B +ENCODING 66 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C +ENCODING 67 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER D +ENCODING 68 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E +ENCODING 69 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER F +ENCODING 70 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G +ENCODING 71 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H +ENCODING 72 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I +ENCODING 73 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER J +ENCODING 74 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +F0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER K +ENCODING 75 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L +ENCODING 76 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER M +ENCODING 77 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +EE +FE +D6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N +ENCODING 78 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O +ENCODING 79 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER P +ENCODING 80 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Q +ENCODING 81 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +D6 +D6 +7C +18 +0C +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R +ENCODING 82 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S +ENCODING 83 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T +ENCODING 84 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U +ENCODING 85 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER V +ENCODING 86 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +6C +38 +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER W +ENCODING 87 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +D6 +FE +EE +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER X +ENCODING 88 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +6C +38 +6C +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y +ENCODING 89 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z +ENCODING 90 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +06 +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT SQUARE BRACKET +ENCODING 91 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +3E +30 +30 +30 +30 +30 +30 +30 +30 +30 +30 +3E +00 +00 +00 +ENDCHAR +STARTCHAR REVERSE SOLIDUS +ENCODING 92 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +C0 +C0 +60 +60 +30 +30 +18 +18 +0C +0C +06 +06 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT SQUARE BRACKET +ENCODING 93 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +0C +0C +0C +0C +0C +0C +0C +0C +0C +0C +7C +00 +00 +00 +ENDCHAR +STARTCHAR CIRCUMFLEX ACCENT +ENCODING 94 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +C6 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LOW LINE +ENCODING 95 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FE +00 +ENDCHAR +STARTCHAR GRAVE ACCENT +ENCODING 96 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +30 +18 +0C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A +ENCODING 97 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER B +ENCODING 98 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C +ENCODING 99 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER D +ENCODING 100 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +06 +06 +7E +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E +ENCODING 101 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER F +ENCODING 102 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1E +30 +30 +30 +7C +30 +30 +30 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G +ENCODING 103 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H +ENCODING 104 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I +ENCODING 105 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER J +ENCODING 106 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +70 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER K +ENCODING 107 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +CC +D8 +F0 +F0 +D8 +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L +ENCODING 108 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +30 +30 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER M +ENCODING 109 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +EC +D6 +D6 +D6 +D6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N +ENCODING 110 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O +ENCODING 111 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER P +ENCODING 112 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Q +ENCODING 113 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R +ENCODING 114 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S +ENCODING 115 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T +ENCODING 116 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +7C +30 +30 +30 +30 +30 +1E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U +ENCODING 117 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER V +ENCODING 118 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +6C +38 +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER W +ENCODING 119 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +D6 +D6 +D6 +D6 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER X +ENCODING 120 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +6C +38 +38 +6C +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y +ENCODING 121 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z +ENCODING 122 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT CURLY BRACKET +ENCODING 123 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0E +18 +18 +18 +18 +70 +70 +18 +18 +18 +18 +0E +00 +00 +00 +ENDCHAR +STARTCHAR VERTICAL LINE +ENCODING 124 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT CURLY BRACKET +ENCODING 125 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +70 +18 +18 +18 +18 +0E +0E +18 +18 +18 +18 +70 +00 +00 +00 +ENDCHAR +STARTCHAR TILDE +ENCODING 126 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +32 +7E +4C +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR +ENCODING 127 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR NO-BREAK SPACE +ENCODING 160 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR INVERTED EXCLAMATION MARK +ENCODING 161 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CENT SIGN +ENCODING 162 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +08 +7E +C8 +C8 +C8 +C8 +C8 +7E +08 +00 +00 +00 +ENDCHAR +STARTCHAR POUND SIGN +ENCODING 163 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +60 +60 +60 +F8 +60 +60 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR CURRENCY SIGN +ENCODING 164 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +3C +66 +66 +66 +3C +66 +00 +00 +00 +00 +ENDCHAR +STARTCHAR YEN SIGN +ENCODING 165 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C3 +C3 +66 +3C +18 +3C +18 +3C +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BROKEN BAR +ENCODING 166 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +00 +00 +18 +18 +18 +18 +18 +00 +00 +ENDCHAR +STARTCHAR SECTION SIGN +ENCODING 167 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +3C +66 +60 +30 +3C +66 +66 +66 +66 +3C +0C +06 +66 +3C +00 +ENDCHAR +STARTCHAR DIAERESIS +ENCODING 168 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COPYRIGHT SIGN +ENCODING 169 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +82 +9A +A2 +A2 +A2 +9A +82 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR FEMININE ORDINAL INDICATOR +ENCODING 170 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +0C +3C +4C +3C +00 +7C +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +ENCODING 171 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +33 +66 +CC +66 +33 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR NOT SIGN +ENCODING 172 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FE +06 +06 +06 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SOFT HYPHEN +ENCODING 173 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +3C +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR REGISTERED SIGN +ENCODING 174 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +82 +BA +AA +B2 +AA +AA +82 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR MACRON +ENCODING 175 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DEGREE SIGN +ENCODING 176 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +38 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR PLUS-MINUS SIGN +ENCODING 177 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +18 +18 +7E +18 +18 +00 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT TWO +ENCODING 178 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +4C +0C +38 +60 +7C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT THREE +ENCODING 179 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +26 +0C +06 +26 +1C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR ACUTE ACCENT +ENCODING 180 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MICRO SIGN +ENCODING 181 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +CC +CC +CC +CC +CC +CC +F6 +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR PILCROW SIGN +ENCODING 182 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +D6 +D6 +D6 +76 +16 +16 +16 +16 +16 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MIDDLE DOT +ENCODING 183 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +18 +18 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CEDILLA +ENCODING 184 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +30 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT ONE +ENCODING 185 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +38 +18 +18 +18 +3C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MASCULINE ORDINAL INDICATOR +ENCODING 186 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +38 +00 +00 +7C +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +ENCODING 187 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +CC +66 +33 +66 +CC +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR VULGAR FRACTION ONE QUARTER +ENCODING 188 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +40 +C0 +40 +42 +46 +EC +18 +30 +70 +D4 +94 +1E +04 +04 +00 +ENDCHAR +STARTCHAR VULGAR FRACTION ONE HALF +ENCODING 189 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +40 +C0 +40 +42 +46 +EC +18 +30 +6C +D2 +82 +0C +10 +1E +00 +ENDCHAR +STARTCHAR VULGAR FRACTION THREE QUARTERS +ENCODING 190 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +90 +20 +12 +96 +6C +18 +30 +70 +D4 +94 +1E +04 +04 +00 +ENDCHAR +STARTCHAR INVERTED QUESTION MARK +ENCODING 191 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +18 +18 +30 +60 +C0 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH GRAVE +ENCODING 192 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH ACUTE +ENCODING 193 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH CIRCUMFLEX +ENCODING 194 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH TILDE +ENCODING 195 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH DIAERESIS +ENCODING 196 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH RING ABOVE +ENCODING 197 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +38 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER AE +ENCODING 198 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +D8 +D8 +D8 +FE +D8 +D8 +D8 +D8 +DE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH CEDILLA +ENCODING 199 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH GRAVE +ENCODING 200 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH ACUTE +ENCODING 201 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH CIRCUMFLEX +ENCODING 202 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH DIAERESIS +ENCODING 203 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH GRAVE +ENCODING 204 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH ACUTE +ENCODING 205 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0C +18 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH CIRCUMFLEX +ENCODING 206 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH DIAERESIS +ENCODING 207 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER ETH +ENCODING 208 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +66 +66 +66 +F6 +66 +66 +66 +66 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH TILDE +ENCODING 209 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH GRAVE +ENCODING 210 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH ACUTE +ENCODING 211 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH CIRCUMFLEX +ENCODING 212 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH TILDE +ENCODING 213 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DIAERESIS +ENCODING 214 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR MULTIPLICATION SIGN +ENCODING 215 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +6C +38 +38 +6C +C6 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH STROKE +ENCODING 216 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +02 +7C +C6 +CE +CE +D6 +D6 +E6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH GRAVE +ENCODING 217 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +30 +18 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH ACUTE +ENCODING 218 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH CIRCUMFLEX +ENCODING 219 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH DIAERESIS +ENCODING 220 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y WITH ACUTE +ENCODING 221 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER THORN +ENCODING 222 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER SHARP S +ENCODING 223 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +78 +CC +CC +CC +D8 +CC +C6 +C6 +D6 +DC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH GRAVE +ENCODING 224 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH ACUTE +ENCODING 225 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH CIRCUMFLEX +ENCODING 226 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH TILDE +ENCODING 227 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH DIAERESIS +ENCODING 228 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH RING ABOVE +ENCODING 229 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +38 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER AE +ENCODING 230 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +6E +16 +16 +7E +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH CEDILLA +ENCODING 231 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH GRAVE +ENCODING 232 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH ACUTE +ENCODING 233 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH CIRCUMFLEX +ENCODING 234 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH DIAERESIS +ENCODING 235 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH GRAVE +ENCODING 236 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +30 +18 +0C +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH ACUTE +ENCODING 237 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH CIRCUMFLEX +ENCODING 238 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +3C +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH DIAERESIS +ENCODING 239 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER ETH +ENCODING 240 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +70 +D8 +0C +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH TILDE +ENCODING 241 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH GRAVE +ENCODING 242 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH ACUTE +ENCODING 243 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH CIRCUMFLEX +ENCODING 244 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH TILDE +ENCODING 245 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DIAERESIS +ENCODING 246 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DIVISION SIGN +ENCODING 247 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +18 +18 +00 +7E +00 +18 +18 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH STROKE +ENCODING 248 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +02 +7C +C6 +CE +D6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH GRAVE +ENCODING 249 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +30 +18 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH ACUTE +ENCODING 250 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH CIRCUMFLEX +ENCODING 251 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH DIAERESIS +ENCODING 252 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y WITH ACUTE +ENCODING 253 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER THORN +ENCODING 254 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y WITH DIAERESIS +ENCODING 255 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH MACRON +ENCODING 256 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH MACRON +ENCODING 257 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH BREVE +ENCODING 258 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH BREVE +ENCODING 259 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH OGONEK +ENCODING 260 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH OGONEK +ENCODING 261 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +06 +7E +C6 +C6 +C6 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH ACUTE +ENCODING 262 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH ACUTE +ENCODING 263 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH CIRCUMFLEX +ENCODING 264 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH CIRCUMFLEX +ENCODING 265 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH DOT ABOVE +ENCODING 266 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH DOT ABOVE +ENCODING 267 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER C WITH CARON +ENCODING 268 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER C WITH CARON +ENCODING 269 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER D WITH CARON +ENCODING 270 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FC +C6 +C6 +C6 +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER D WITH CARON +ENCODING 271 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +16 +06 +06 +7E +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER D WITH STROKE +ENCODING 272 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +66 +66 +66 +F6 +66 +66 +66 +66 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER D WITH STROKE +ENCODING 273 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +1F +06 +7E +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH MACRON +ENCODING 274 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH MACRON +ENCODING 275 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH BREVE +ENCODING 276 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH BREVE +ENCODING 277 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH DOT ABOVE +ENCODING 278 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH DOT ABOVE +ENCODING 279 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH OGONEK +ENCODING 280 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH OGONEK +ENCODING 281 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH CARON +ENCODING 282 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH CARON +ENCODING 283 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH CIRCUMFLEX +ENCODING 284 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH CIRCUMFLEX +ENCODING 285 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH BREVE +ENCODING 286 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH BREVE +ENCODING 287 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH DOT ABOVE +ENCODING 288 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH DOT ABOVE +ENCODING 289 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH CEDILLA +ENCODING 290 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +C6 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH CEDILLA +ENCODING 291 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +30 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H WITH CIRCUMFLEX +ENCODING 292 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H WITH CIRCUMFLEX +ENCODING 293 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H WITH STROKE +ENCODING 294 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +FF +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H WITH STROKE +ENCODING 295 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +F0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH TILDE +ENCODING 296 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH TILDE +ENCODING 297 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH MACRON +ENCODING 298 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7E +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH MACRON +ENCODING 299 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7E +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH BREVE +ENCODING 300 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +3C +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH BREVE +ENCODING 301 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +3C +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH OGONEK +ENCODING 302 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH OGONEK +ENCODING 303 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +38 +18 +18 +18 +18 +18 +1C +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH DOT ABOVE +ENCODING 304 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER DOTLESS I +ENCODING 305 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LIGATURE IJ +ENCODING 306 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +F7 +66 +66 +66 +66 +66 +66 +66 +66 +EC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LIGATURE IJ +ENCODING 307 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +00 +E6 +66 +66 +66 +66 +66 +76 +06 +06 +1C +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER J WITH CIRCUMFLEX +ENCODING 308 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +18 +18 +18 +18 +18 +18 +18 +F0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER J WITH CIRCUMFLEX +ENCODING 309 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +3C +66 +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +70 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER K WITH CEDILLA +ENCODING 310 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER K WITH CEDILLA +ENCODING 311 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +CC +D8 +F0 +F0 +D8 +CC +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER KRA +ENCODING 312 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +CC +D8 +F0 +D8 +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH ACUTE +ENCODING 313 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH ACUTE +ENCODING 314 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +30 +30 +30 +30 +30 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH CEDILLA +ENCODING 315 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH CEDILLA +ENCODING 316 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +30 +30 +30 +30 +30 +1C +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH CARON +ENCODING 317 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH CARON +ENCODING 318 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +30 +30 +30 +30 +30 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH MIDDLE DOT +ENCODING 319 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +C0 +C0 +C0 +CC +CC +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH MIDDLE DOT +ENCODING 320 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +36 +36 +30 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER L WITH STROKE +ENCODING 321 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +60 +60 +68 +78 +70 +E0 +E0 +60 +60 +3E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER L WITH STROKE +ENCODING 322 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +34 +3C +38 +70 +70 +30 +30 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH ACUTE +ENCODING 323 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH ACUTE +ENCODING 324 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH CEDILLA +ENCODING 325 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH CEDILLA +ENCODING 326 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER N WITH CARON +ENCODING 327 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N WITH CARON +ENCODING 328 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER N PRECEDED BY APOSTROPHE +ENCODING 329 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +C0 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER ENG +ENCODING 330 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +E6 +E6 +D6 +D6 +CE +CE +C6 +C6 +06 +06 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER ENG +ENCODING 331 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +06 +06 +0C +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH MACRON +ENCODING 332 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH MACRON +ENCODING 333 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH BREVE +ENCODING 334 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH BREVE +ENCODING 335 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DOUBLE ACUTE +ENCODING 336 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +CC +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DOUBLE ACUTE +ENCODING 337 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +36 +6C +D8 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LIGATURE OE +ENCODING 338 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +D8 +D8 +D8 +DE +D8 +D8 +D8 +D8 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LIGATURE OE +ENCODING 339 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +6E +D6 +D6 +DE +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH ACUTE +ENCODING 340 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH ACUTE +ENCODING 341 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH CEDILLA +ENCODING 342 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +C6 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH CEDILLA +ENCODING 343 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH CARON +ENCODING 344 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH CARON +ENCODING 345 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH ACUTE +ENCODING 346 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH ACUTE +ENCODING 347 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH CIRCUMFLEX +ENCODING 348 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH CIRCUMFLEX +ENCODING 349 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH CEDILLA +ENCODING 350 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +7C +06 +06 +06 +06 +FC +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH CEDILLA +ENCODING 351 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +7C +06 +06 +FC +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER S WITH CARON +ENCODING 352 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +7C +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER S WITH CARON +ENCODING 353 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C0 +C0 +7C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T WITH CEDILLA +ENCODING 354 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +18 +0C +0C +18 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T WITH CEDILLA +ENCODING 355 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +7C +30 +30 +30 +30 +30 +1E +0C +0C +18 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T WITH CARON +ENCODING 356 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FF +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T WITH CARON +ENCODING 357 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +30 +30 +7C +30 +30 +30 +30 +30 +1E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER T WITH STROKE +ENCODING 358 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +7E +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER T WITH STROKE +ENCODING 359 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +7C +30 +7C +30 +30 +30 +1E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH TILDE +ENCODING 360 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH TILDE +ENCODING 361 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +32 +7E +4C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH MACRON +ENCODING 362 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH MACRON +ENCODING 363 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH BREVE +ENCODING 364 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH BREVE +ENCODING 365 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH RING ABOVE +ENCODING 366 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +38 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH RING ABOVE +ENCODING 367 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH DOUBLE ACUTE +ENCODING 368 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +CC +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH DOUBLE ACUTE +ENCODING 369 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +36 +6C +D8 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH OGONEK +ENCODING 370 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH OGONEK +ENCODING 371 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +0C +08 +06 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER W WITH CIRCUMFLEX +ENCODING 372 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +D6 +FE +EE +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER W WITH CIRCUMFLEX +ENCODING 373 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +C6 +C6 +D6 +D6 +D6 +D6 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y WITH CIRCUMFLEX +ENCODING 374 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Y WITH CIRCUMFLEX +ENCODING 375 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +10 +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Y WITH DIAERESIS +ENCODING 376 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z WITH ACUTE +ENCODING 377 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +FE +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z WITH ACUTE +ENCODING 378 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z WITH DOT ABOVE +ENCODING 379 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +FE +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z WITH DOT ABOVE +ENCODING 380 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER Z WITH CARON +ENCODING 381 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +FE +06 +0C +18 +30 +60 +C0 +C0 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER Z WITH CARON +ENCODING 382 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +FE +06 +0C +18 +30 +60 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER LONG S +ENCODING 383 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1E +30 +30 +70 +30 +30 +30 +30 +30 +30 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER F WITH HOOK +ENCODING 402 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +0E +1B +1B +18 +18 +18 +7E +18 +18 +18 +D8 +D8 +70 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH CARON +ENCODING 461 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH CARON +ENCODING 462 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH CARON +ENCODING 463 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH CARON +ENCODING 464 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH CARON +ENCODING 465 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH CARON +ENCODING 466 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH CARON +ENCODING 467 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH CARON +ENCODING 468 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER AE WITH MACRON +ENCODING 482 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7E +D8 +D8 +D8 +FE +D8 +D8 +D8 +DE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER AE WITH MACRON +ENCODING 483 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +6E +16 +16 +7E +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH CARON +ENCODING 486 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH CARON +ENCODING 487 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER K WITH CARON +ENCODING 488 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER K WITH CARON +ENCODING 489 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C0 +C0 +CC +D8 +F0 +F0 +D8 +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH OGONEK +ENCODING 490 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH OGONEK +ENCODING 491 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH OGONEK AND MACRON +ENCODING 492 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH OGONEK AND MACRON +ENCODING 493 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +18 +10 +0C +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER J WITH CARON +ENCODING 496 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +38 +10 +00 +18 +18 +18 +18 +18 +18 +18 +18 +18 +70 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER G WITH ACUTE +ENCODING 500 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +C0 +C0 +C0 +DE +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER G WITH ACUTE +ENCODING 501 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +7E +C6 +C6 +C6 +C6 +C6 +7C +06 +06 +FC +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER AE WITH ACUTE +ENCODING 508 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +00 +7E +D8 +D8 +D8 +FE +D8 +D8 +D8 +DE +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER AE WITH ACUTE +ENCODING 509 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +00 +6E +16 +16 +7E +D0 +D0 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH STROKE AND ACUTE +ENCODING 510 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +30 +02 +7C +CE +CE +D6 +D6 +E6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH STROKE AND ACUTE +ENCODING 511 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0C +18 +30 +02 +7C +C6 +CE +D6 +E6 +C6 +7C +80 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH DOUBLE GRAVE +ENCODING 512 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH DOUBLE GRAVE +ENCODING 513 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH INVERTED BREVE +ENCODING 514 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH INVERTED BREVE +ENCODING 515 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH DOUBLE GRAVE +ENCODING 516 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH DOUBLE GRAVE +ENCODING 517 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH INVERTED BREVE +ENCODING 518 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH INVERTED BREVE +ENCODING 519 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH DOUBLE GRAVE +ENCODING 520 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH DOUBLE GRAVE +ENCODING 521 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER I WITH INVERTED BREVE +ENCODING 522 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +3C +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER I WITH INVERTED BREVE +ENCODING 523 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +3C +66 +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DOUBLE GRAVE +ENCODING 524 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DOUBLE GRAVE +ENCODING 525 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH INVERTED BREVE +ENCODING 526 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH INVERTED BREVE +ENCODING 527 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH DOUBLE GRAVE +ENCODING 528 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH DOUBLE GRAVE +ENCODING 529 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER R WITH INVERTED BREVE +ENCODING 530 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER R WITH INVERTED BREVE +ENCODING 531 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +7E +C6 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH DOUBLE GRAVE +ENCODING 532 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +CC +66 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH DOUBLE GRAVE +ENCODING 533 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +D8 +6C +36 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER U WITH INVERTED BREVE +ENCODING 534 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +38 +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER U WITH INVERTED BREVE +ENCODING 535 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +38 +6C +6C +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER H WITH CARON +ENCODING 542 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER H WITH CARON +ENCODING 543 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +10 +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER A WITH DOT ABOVE +ENCODING 550 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER A WITH DOT ABOVE +ENCODING 551 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER E WITH CEDILLA +ENCODING 552 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER E WITH CEDILLA +ENCODING 553 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +18 +18 +30 +00 +ENDCHAR +STARTCHAR LATIN CAPITAL LETTER O WITH DOT ABOVE +ENCODING 558 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LATIN SMALL LETTER O WITH DOT ABOVE +ENCODING 559 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BREVE +ENCODING 728 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOT ABOVE +ENCODING 729 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR OGONEK +ENCODING 731 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +0C +08 +06 +00 +ENDCHAR +STARTCHAR SMALL TILDE +ENCODING 732 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +32 +4C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOUBLE ACUTE ACCENT +ENCODING 733 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +CC +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMBINING BREVE +ENCODING 774 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR COMBINING DIAERESIS +ENCODING 776 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER GAMMA +ENCODING 915 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER THETA +ENCODING 920 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +C6 +C6 +FE +C6 +C6 +C6 +6C +38 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER SIGMA +ENCODING 931 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +60 +30 +18 +18 +30 +60 +C6 +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER PHI +ENCODING 934 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +10 +7C +D6 +D6 +D6 +D6 +D6 +D6 +7C +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK CAPITAL LETTER OMEGA +ENCODING 937 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +38 +6C +C6 +C6 +C6 +C6 +C6 +6C +6C +EE +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER ALPHA +ENCODING 945 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +76 +DC +D8 +D8 +D8 +DC +76 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER DELTA +ENCODING 948 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1E +30 +18 +0C +3C +66 +66 +66 +66 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER EPSILON +ENCODING 949 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C0 +70 +C0 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER PI +ENCODING 960 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FE +6C +6C +6C +6C +6C +6C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER SIGMA +ENCODING 963 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +D8 +D8 +D8 +D8 +D8 +70 +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER TAU +ENCODING 964 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +18 +18 +18 +18 +18 +0C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREEK SMALL LETTER PHI +ENCODING 966 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +4C +D6 +D6 +D6 +D6 +D6 +7C +10 +10 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER IO +ENCODING 1025 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +6C +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I +ENCODING 1030 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +18 +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YI +ENCODING 1031 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +66 +66 +00 +7E +18 +18 +18 +18 +18 +18 +18 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHORT U +ENCODING 1038 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER A +ENCODING 1040 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER BE +ENCODING 1041 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C0 +C0 +C0 +FC +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER VE +ENCODING 1042 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C6 +C6 +C6 +C6 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER GHE +ENCODING 1043 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER DE +ENCODING 1044 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +1C +3C +6C +6C +6C +6C +6C +6C +FE +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER IE +ENCODING 1045 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +F8 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ZHE +ENCODING 1046 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +D6 +D6 +D6 +7C +38 +7C +D6 +D6 +D6 +D6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ZE +ENCODING 1047 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +06 +06 +06 +3C +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER I +ENCODING 1048 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +CE +CE +D6 +D6 +E6 +E6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHORT I +ENCODING 1049 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +6C +38 +00 +C6 +CE +CE +D6 +D6 +E6 +E6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER KA +ENCODING 1050 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +CC +F8 +CC +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EL +ENCODING 1051 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EM +ENCODING 1052 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +EE +FE +D6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EN +ENCODING 1053 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +FE +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER O +ENCODING 1054 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER PE +ENCODING 1055 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FE +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ER +ENCODING 1056 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +C6 +C6 +C6 +FC +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER ES +ENCODING 1057 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER TE +ENCODING 1058 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER U +ENCODING 1059 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER EF +ENCODING 1060 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +10 +7C +D6 +D6 +D6 +D6 +D6 +D6 +7C +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER HA +ENCODING 1061 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +6C +38 +6C +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER TSE +ENCODING 1062 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER CHE +ENCODING 1063 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +7E +06 +06 +06 +06 +06 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHA +ENCODING 1064 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SHCHA +ENCODING 1065 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +D6 +7F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER HARD SIGN +ENCODING 1066 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +F0 +F0 +30 +30 +3C +36 +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YERU +ENCODING 1067 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C6 +C6 +C6 +C6 +F6 +DE +DE +DE +DE +F6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER SOFT SIGN +ENCODING 1068 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +30 +30 +30 +30 +3C +36 +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER E +ENCODING 1069 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FC +06 +06 +06 +3E +06 +06 +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YU +ENCODING 1070 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +CC +D6 +D6 +D6 +F6 +F6 +D6 +D6 +D6 +CC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER YA +ENCODING 1071 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +C6 +C6 +C6 +7E +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER A +ENCODING 1072 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +06 +7E +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER BE +ENCODING 1073 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C0 +FC +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER VE +ENCODING 1074 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +70 +D8 +D8 +D8 +FC +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER GHE +ENCODING 1075 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +06 +06 +7C +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER DE +ENCODING 1076 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER IE +ENCODING 1077 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ZHE +ENCODING 1078 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +D6 +7C +38 +38 +7C +D6 +D6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ZE +ENCODING 1079 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +06 +06 +3C +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER I +ENCODING 1080 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHORT I +ENCODING 1081 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER KA +ENCODING 1082 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +CC +CC +D8 +F0 +F0 +D8 +CC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EL +ENCODING 1083 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EM +ENCODING 1084 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +EE +FE +D6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EN +ENCODING 1085 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +FE +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER O +ENCODING 1086 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER PE +ENCODING 1087 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ER +ENCODING 1088 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +C6 +C6 +C6 +C6 +C6 +FC +C0 +C0 +C0 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER ES +ENCODING 1089 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C0 +C0 +C0 +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER TE +ENCODING 1090 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +EC +D6 +D6 +D6 +D6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER U +ENCODING 1091 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER EF +ENCODING 1092 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7C +D6 +D6 +D6 +D6 +D6 +7C +10 +10 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER HA +ENCODING 1093 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +6C +38 +38 +6C +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER TSE +ENCODING 1094 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER CHE +ENCODING 1095 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +C6 +7E +06 +06 +06 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHA +ENCODING 1096 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +D6 +D6 +D6 +D6 +6E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHCHA +ENCODING 1097 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +D6 +D6 +D6 +D6 +6F +03 +03 +06 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER HARD SIGN +ENCODING 1098 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +F0 +30 +3C +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YERU +ENCODING 1099 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +C6 +C6 +F6 +DE +DE +DE +F6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SOFT SIGN +ENCODING 1100 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +30 +30 +3C +36 +36 +36 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER E +ENCODING 1101 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FC +06 +06 +3E +06 +06 +FC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YU +ENCODING 1102 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +CC +D6 +D6 +F6 +D6 +D6 +CC +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YA +ENCODING 1103 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +C6 +C6 +7E +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER IO +ENCODING 1105 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +6C +6C +00 +7E +C6 +C6 +FE +C0 +C0 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I +ENCODING 1110 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER YI +ENCODING 1111 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +00 +38 +18 +18 +18 +18 +18 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER SHORT U +ENCODING 1118 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +6C +6C +38 +00 +C6 +C6 +C6 +C6 +C6 +C6 +7E +06 +06 +FC +00 +ENDCHAR +STARTCHAR CYRILLIC CAPITAL LETTER GHE WITH UPTURN +ENCODING 1168 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +06 +06 +7C +C0 +C0 +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR CYRILLIC SMALL LETTER GHE WITH UPTURN +ENCODING 1169 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +06 +06 +7C +C0 +C0 +C0 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOUBLE VERTICAL LINE +ENCODING 8214 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +66 +66 +66 +66 +66 +66 +66 +66 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT SINGLE QUOTATION MARK +ENCODING 8216 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT SINGLE QUOTATION MARK +ENCODING 8217 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT DOUBLE QUOTATION MARK +ENCODING 8220 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT DOUBLE QUOTATION MARK +ENCODING 8221 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BULLET +ENCODING 8226 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +18 +3C +3C +18 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR HORIZONTAL ELLIPSIS +ENCODING 8230 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +DB +DB +00 +00 +00 +00 +ENDCHAR +STARTCHAR SINGLE LEFT-POINTING ANGLE QUOTATION MARK +ENCODING 8249 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +0C +18 +30 +18 +0C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +ENCODING 8250 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +30 +18 +0C +18 +30 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOUBLE EXCLAMATION MARK +ENCODING 8252 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +66 +66 +66 +66 +66 +66 +66 +00 +66 +66 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SUPERSCRIPT LATIN SMALL LETTER N +ENCODING 8319 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +78 +6C +6C +6C +6C +6C +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR PESETA SIGN +ENCODING 8359 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +F8 +CC +CC +F8 +C0 +CC +DE +CC +CC +CC +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EURO SIGN +ENCODING 8364 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +1C +36 +60 +F8 +60 +F8 +60 +36 +1C +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFTWARDS ARROW +ENCODING 8592 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +20 +40 +FE +40 +20 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UPWARDS ARROW +ENCODING 8593 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +5A +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHTWARDS ARROW +ENCODING 8594 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +08 +04 +FE +04 +08 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOWNWARDS ARROW +ENCODING 8595 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +18 +18 +5A +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT RIGHT ARROW +ENCODING 8596 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +28 +44 +FE +44 +28 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UP DOWN ARROW +ENCODING 8597 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +5A +18 +18 +18 +18 +5A +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UP DOWN ARROW WITH BASE +ENCODING 8616 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +5A +18 +18 +18 +18 +5A +3C +18 +7E +00 +00 +00 +ENDCHAR +STARTCHAR BULLET OPERATOR +ENCODING 8729 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +38 +38 +38 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR SQUARE ROOT +ENCODING 8730 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +0F +0C +0C +0C +0C +0C +CC +6C +3C +1C +0C +00 +00 +00 +00 +ENDCHAR +STARTCHAR INFINITY +ENCODING 8734 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +7E +DB +DB +DB +7E +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR INTERSECTION +ENCODING 8745 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +7C +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UNION +ENCODING 8746 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +C6 +7C +00 +00 +00 +00 +ENDCHAR +STARTCHAR ALMOST EQUAL TO +ENCODING 8776 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +32 +4C +00 +32 +4C +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR IDENTICAL TO +ENCODING 8801 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LESS-THAN OR EQUAL TO +ENCODING 8804 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +0C +18 +30 +18 +0C +00 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR GREATER-THAN OR EQUAL TO +ENCODING 8805 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +30 +18 +0C +18 +30 +00 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR HOUSE +ENCODING 8962 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +6C +C6 +C6 +C6 +FE +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR REVERSED NOT SIGN +ENCODING 8976 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FE +C0 +C0 +C0 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TURNED NOT SIGN +ENCODING 8985 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +C0 +C0 +C0 +FE +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TOP HALF INTEGRAL +ENCODING 8992 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +0E +1B +1B +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOTTOM HALF INTEGRAL +ENCODING 8993 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +18 +D8 +D8 +70 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT HORIZONTAL +ENCODING 9472 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY HORIZONTAL +ENCODING 9473 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL +ENCODING 9474 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL +ENCODING 9475 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT TRIPLE DASH HORIZONTAL +ENCODING 9476 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +DB +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY TRIPLE DASH HORIZONTAL +ENCODING 9477 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +DB +DB +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT TRIPLE DASH VERTICAL +ENCODING 9478 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +00 +00 +18 +18 +18 +00 +00 +18 +18 +18 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY TRIPLE DASH VERTICAL +ENCODING 9479 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +1C +1C +00 +00 +1C +1C +1C +00 +00 +1C +1C +1C +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT QUADRUPLE DASH HORIZONTAL +ENCODING 9480 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +AA +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY QUADRUPLE DASH HORIZONTAL +ENCODING 9481 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +AA +AA +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT QUADRUPLE DASH VERTICAL +ENCODING 9482 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +00 +00 +18 +18 +00 +00 +18 +18 +00 +00 +18 +18 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY QUADRUPLE DASH VERTICAL +ENCODING 9483 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +1C +00 +00 +1C +1C +00 +00 +1C +1C +00 +00 +1C +1C +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN AND RIGHT +ENCODING 9484 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND RIGHT HEAVY +ENCODING 9485 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND RIGHT LIGHT +ENCODING 9486 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN AND RIGHT +ENCODING 9487 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN AND LEFT +ENCODING 9488 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F8 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND LEFT HEAVY +ENCODING 9489 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F8 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND LEFT LIGHT +ENCODING 9490 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FC +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN AND LEFT +ENCODING 9491 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FC +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND RIGHT +ENCODING 9492 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND RIGHT HEAVY +ENCODING 9493 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND RIGHT LIGHT +ENCODING 9494 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND RIGHT +ENCODING 9495 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND LEFT +ENCODING 9496 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND LEFT HEAVY +ENCODING 9497 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +F8 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND LEFT LIGHT +ENCODING 9498 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND LEFT +ENCODING 9499 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +FC +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL AND RIGHT +ENCODING 9500 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL LIGHT AND RIGHT HEAVY +ENCODING 9501 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND RIGHT DOWN LIGHT +ENCODING 9502 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND RIGHT UP LIGHT +ENCODING 9503 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL HEAVY AND RIGHT LIGHT +ENCODING 9504 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND RIGHT UP HEAVY +ENCODING 9505 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND RIGHT DOWN HEAVY +ENCODING 9506 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +1F +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL AND RIGHT +ENCODING 9507 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1F +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL AND LEFT +ENCODING 9508 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL LIGHT AND LEFT HEAVY +ENCODING 9509 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F8 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND LEFT DOWN LIGHT +ENCODING 9510 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND LEFT UP LIGHT +ENCODING 9511 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FC +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL HEAVY AND LEFT LIGHT +ENCODING 9512 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND LEFT UP HEAVY +ENCODING 9513 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +FC +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND LEFT DOWN HEAVY +ENCODING 9514 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FC +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL AND LEFT +ENCODING 9515 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FC +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN AND HORIZONTAL +ENCODING 9516 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT HEAVY AND RIGHT DOWN LIGHT +ENCODING 9517 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F8 +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT HEAVY AND LEFT DOWN LIGHT +ENCODING 9518 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +1F +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND HORIZONTAL HEAVY +ENCODING 9519 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND HORIZONTAL LIGHT +ENCODING 9520 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT LIGHT AND LEFT DOWN HEAVY +ENCODING 9521 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT LIGHT AND RIGHT DOWN HEAVY +ENCODING 9522 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN AND HORIZONTAL +ENCODING 9523 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +FF +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND HORIZONTAL +ENCODING 9524 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT HEAVY AND RIGHT UP LIGHT +ENCODING 9525 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +F8 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT HEAVY AND LEFT UP LIGHT +ENCODING 9526 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND HORIZONTAL HEAVY +ENCODING 9527 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND HORIZONTAL LIGHT +ENCODING 9528 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT LIGHT AND LEFT UP HEAVY +ENCODING 9529 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FC +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT LIGHT AND RIGHT UP HEAVY +ENCODING 9530 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND HORIZONTAL +ENCODING 9531 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL +ENCODING 9532 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT HEAVY AND RIGHT VERTICAL LIGHT +ENCODING 9533 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT HEAVY AND LEFT VERTICAL LIGHT +ENCODING 9534 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL LIGHT AND HORIZONTAL HEAVY +ENCODING 9535 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP HEAVY AND DOWN HORIZONTAL LIGHT +ENCODING 9536 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN HEAVY AND UP HORIZONTAL LIGHT +ENCODING 9537 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL HEAVY AND HORIZONTAL LIGHT +ENCODING 9538 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT UP HEAVY AND RIGHT DOWN LIGHT +ENCODING 9539 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT UP HEAVY AND LEFT DOWN LIGHT +ENCODING 9540 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT DOWN HEAVY AND RIGHT UP LIGHT +ENCODING 9541 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT DOWN HEAVY AND LEFT UP LIGHT +ENCODING 9542 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN LIGHT AND UP HORIZONTAL HEAVY +ENCODING 9543 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS UP LIGHT AND DOWN HORIZONTAL HEAVY +ENCODING 9544 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +FF +FF +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS RIGHT LIGHT AND LEFT VERTICAL HEAVY +ENCODING 9545 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FC +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LEFT LIGHT AND RIGHT VERTICAL HEAVY +ENCODING 9546 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +1F +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY VERTICAL AND HORIZONTAL +ENCODING 9547 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +FF +FF +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL +ENCODING 9548 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +EE +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOUBLE DASH HORIZONTAL +ENCODING 9549 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +EE +EE +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL +ENCODING 9550 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +18 +18 +18 +18 +18 +18 +00 +00 +18 +18 +18 +18 +18 +18 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOUBLE DASH VERTICAL +ENCODING 9551 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +1C +1C +1C +1C +1C +1C +00 +00 +1C +1C +1C +1C +1C +1C +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE HORIZONTAL +ENCODING 9552 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +00 +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL +ENCODING 9553 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN SINGLE AND RIGHT DOUBLE +ENCODING 9554 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +1F +18 +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN DOUBLE AND RIGHT SINGLE +ENCODING 9555 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +3F +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE DOWN AND RIGHT +ENCODING 9556 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +3F +30 +37 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN SINGLE AND LEFT DOUBLE +ENCODING 9557 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +F8 +18 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN DOUBLE AND LEFT SINGLE +ENCODING 9558 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FE +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE DOWN AND LEFT +ENCODING 9559 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FE +06 +F6 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS UP SINGLE AND RIGHT DOUBLE +ENCODING 9560 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +1F +18 +1F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP DOUBLE AND RIGHT SINGLE +ENCODING 9561 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +3F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE UP AND RIGHT +ENCODING 9562 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +37 +30 +3F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP SINGLE AND LEFT DOUBLE +ENCODING 9563 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +F8 +18 +F8 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP DOUBLE AND LEFT SINGLE +ENCODING 9564 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +FE +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE UP AND LEFT +ENCODING 9565 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F6 +06 +FE +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL SINGLE AND RIGHT DOUBLE +ENCODING 9566 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +1F +18 +1F +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL DOUBLE AND RIGHT SINGLE +ENCODING 9567 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +37 +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL AND RIGHT +ENCODING 9568 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +37 +30 +37 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL SINGLE AND LEFT DOUBLE +ENCODING 9569 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +F8 +18 +F8 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL DOUBLE AND LEFT SINGLE +ENCODING 9570 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +F6 +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL AND LEFT +ENCODING 9571 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F6 +06 +F6 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN SINGLE AND HORIZONTAL DOUBLE +ENCODING 9572 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +00 +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS DOWN DOUBLE AND HORIZONTAL SINGLE +ENCODING 9573 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE DOWN AND HORIZONTAL +ENCODING 9574 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +00 +F7 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS UP SINGLE AND HORIZONTAL DOUBLE +ENCODING 9575 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +FF +00 +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS UP DOUBLE AND HORIZONTAL SINGLE +ENCODING 9576 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE UP AND HORIZONTAL +ENCODING 9577 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F7 +00 +FF +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL SINGLE AND HORIZONTAL DOUBLE +ENCODING 9578 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +FF +18 +FF +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS VERTICAL DOUBLE AND HORIZONTAL SINGLE +ENCODING 9579 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +36 +FF +36 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL +ENCODING 9580 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +36 +36 +36 +36 +36 +36 +F7 +00 +F7 +36 +36 +36 +36 +36 +36 +36 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC DOWN AND RIGHT +ENCODING 9581 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +0F +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC DOWN AND LEFT +ENCODING 9582 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F0 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC UP AND LEFT +ENCODING 9583 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +F0 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT ARC UP AND RIGHT +ENCODING 9584 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +0F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT +ENCODING 9585 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +03 +03 +06 +06 +0C +0C +18 +18 +30 +30 +60 +60 +C0 +C0 +80 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT +ENCODING 9586 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +C0 +C0 +60 +60 +30 +30 +18 +18 +0C +0C +06 +06 +03 +03 +01 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DIAGONAL CROSS +ENCODING 9587 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +81 +C3 +C3 +66 +66 +3C +3C +18 +18 +3C +3C +66 +66 +C3 +C3 +81 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT LEFT +ENCODING 9588 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F0 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP +ENCODING 9589 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT RIGHT +ENCODING 9590 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +0F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT DOWN +ENCODING 9591 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY LEFT +ENCODING 9592 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +F0 +F0 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP +ENCODING 9593 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1C +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY RIGHT +ENCODING 9594 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +0F +0F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY DOWN +ENCODING 9595 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT LEFT AND HEAVY RIGHT +ENCODING 9596 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +0F +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS LIGHT UP AND HEAVY DOWN +ENCODING 9597 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +18 +18 +18 +18 +18 +18 +18 +18 +1C +1C +1C +1C +1C +1C +1C +1C +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY LEFT AND LIGHT RIGHT +ENCODING 9598 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +FF +F0 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BOX DRAWINGS HEAVY UP AND LIGHT DOWN +ENCODING 9599 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +1C +1C +1C +1C +1C +1C +1C +1C +18 +18 +18 +18 +18 +18 +18 +18 +ENDCHAR +STARTCHAR UPPER HALF BLOCK +ENCODING 9600 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LOWER ONE EIGHTH BLOCK +ENCODING 9601 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +ENDCHAR +STARTCHAR LOWER ONE QUARTER BLOCK +ENCODING 9602 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER THREE EIGHTHS BLOCK +ENCODING 9603 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER HALF BLOCK +ENCODING 9604 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER FIVE EIGHTHS BLOCK +ENCODING 9605 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER THREE QUARTERS BLOCK +ENCODING 9606 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LOWER SEVEN EIGHTHS BLOCK +ENCODING 9607 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR FULL BLOCK +ENCODING 9608 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR LEFT SEVEN EIGHTHS BLOCK +ENCODING 9609 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +FE +ENDCHAR +STARTCHAR LEFT THREE QUARTERS BLOCK +ENCODING 9610 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +FC +ENDCHAR +STARTCHAR LEFT FIVE EIGHTHS BLOCK +ENCODING 9611 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +F8 +ENDCHAR +STARTCHAR LEFT HALF BLOCK +ENCODING 9612 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR LEFT THREE EIGHTHS BLOCK +ENCODING 9613 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +E0 +ENDCHAR +STARTCHAR LEFT ONE QUARTER BLOCK +ENCODING 9614 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +C0 +ENDCHAR +STARTCHAR LEFT ONE EIGHTH BLOCK +ENCODING 9615 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR RIGHT HALF BLOCK +ENCODING 9616 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR LIGHT SHADE +ENCODING 9617 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +11 +44 +ENDCHAR +STARTCHAR MEDIUM SHADE +ENCODING 9618 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +55 +AA +ENDCHAR +STARTCHAR DARK SHADE +ENCODING 9619 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +DD +77 +ENDCHAR +STARTCHAR UPPER ONE EIGHTH BLOCK +ENCODING 9620 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHT ONE EIGHTH BLOCK +ENCODING 9621 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +01 +ENDCHAR +STARTCHAR QUADRANT LOWER LEFT +ENCODING 9622 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR QUADRANT LOWER RIGHT +ENCODING 9623 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT +ENCODING 9624 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT +ENCODING 9625 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND LOWER RIGHT +ENCODING 9626 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT +ENCODING 9627 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT +ENCODING 9628 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +FF +FF +0F +0F +0F +0F +0F +0F +0F +0F +ENDCHAR +STARTCHAR QUADRANT UPPER RIGHT +ENCODING 9629 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR QUADRANT UPPER RIGHT AND LOWER LEFT +ENCODING 9630 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +F0 +F0 +F0 +F0 +F0 +F0 +F0 +F0 +ENDCHAR +STARTCHAR QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT +ENCODING 9631 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +0F +0F +0F +0F +0F +0F +0F +0F +FF +FF +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR BLACK SQUARE +ENCODING 9632 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7C +7C +7C +7C +7C +7C +7C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK RECTANGLE +ENCODING 9644 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +FE +FE +FE +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK UP-POINTING TRIANGLE +ENCODING 9650 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +10 +38 +38 +7C +7C +FE +FE +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK DOWN-POINTING TRIANGLE +ENCODING 9660 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +FE +FE +7C +7C +38 +38 +10 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK DIAMOND +ENCODING 9670 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +7C +FE +7C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LOZENGE +ENCODING 9674 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +6C +C6 +6C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR WHITE CIRCLE +ENCODING 9675 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +3C +66 +66 +66 +66 +3C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK CIRCLE +ENCODING 9679 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +3C +7E +7E +7E +7E +3C +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR INVERSE BULLET +ENCODING 9688 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +FF +E7 +C3 +C3 +E7 +FF +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR INVERSE WHITE CIRCLE +ENCODING 9689 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FF +FF +FF +FF +C3 +99 +99 +99 +99 +C3 +FF +FF +FF +FF +FF +ENDCHAR +STARTCHAR BLACK LOWER RIGHT TRIANGLE +ENCODING 9698 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +80 +C0 +E0 +F0 +F8 +FC +FE +FF +ENDCHAR +STARTCHAR BLACK LOWER LEFT TRIANGLE +ENCODING 9699 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +01 +03 +07 +0F +1F +3F +7F +FF +ENDCHAR +STARTCHAR BLACK UPPER LEFT TRIANGLE +ENCODING 9700 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +FE +FC +F8 +F0 +E0 +C0 +80 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK UPPER RIGHT TRIANGLE +ENCODING 9701 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +FF +7F +3F +1F +0F +07 +03 +01 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR HEAVEN +ENCODING 9776 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR LAKE +ENCODING 9777 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +7E +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR FIRE +ENCODING 9778 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +66 +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR THUNDER +ENCODING 9779 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +66 +00 +00 +7E +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR WIND +ENCODING 9780 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +7E +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR WATER +ENCODING 9781 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +7E +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR MOUNTAIN +ENCODING 9782 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +7E +00 +00 +66 +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR TRIGRAM FOR EARTH +ENCODING 9783 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +66 +00 +00 +66 +00 +00 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR WHITE SMILING FACE +ENCODING 9786 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +81 +A5 +81 +81 +A5 +99 +81 +81 +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK SMILING FACE +ENCODING 9787 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +FF +DB +FF +FF +DB +E7 +FF +FF +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR WHITE SUN WITH RAYS +ENCODING 9788 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +18 +18 +DB +3C +E7 +3C +DB +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR FEMALE SIGN +ENCODING 9792 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +3C +66 +66 +66 +66 +3C +18 +3C +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR MALE SIGN +ENCODING 9794 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +0F +07 +0D +19 +3C +66 +66 +66 +66 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK SPADE SUIT +ENCODING 9824 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +18 +3C +7E +FF +FF +7E +18 +18 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK CLUB SUIT +ENCODING 9827 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +18 +3C +3C +E7 +E7 +E7 +18 +18 +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK HEART SUIT +ENCODING 9829 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +6C +FE +FE +FE +7C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BLACK DIAMOND SUIT +ENCODING 9830 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +10 +38 +7C +FE +7C +38 +10 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR EIGHTH NOTE +ENCODING 9834 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +66 +7E +60 +60 +60 +60 +60 +C0 +C0 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BEAMED EIGHTH NOTES +ENCODING 9835 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +7E +66 +7E +66 +66 +66 +66 +66 +6E +CC +C0 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN BLANK +ENCODING 10240 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1 +ENCODING 10241 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2 +ENCODING 10242 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12 +ENCODING 10243 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3 +ENCODING 10244 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13 +ENCODING 10245 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23 +ENCODING 10246 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123 +ENCODING 10247 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4 +ENCODING 10248 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14 +ENCODING 10249 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24 +ENCODING 10250 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124 +ENCODING 10251 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34 +ENCODING 10252 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134 +ENCODING 10253 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234 +ENCODING 10254 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234 +ENCODING 10255 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-5 +ENCODING 10256 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-15 +ENCODING 10257 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-25 +ENCODING 10258 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-125 +ENCODING 10259 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-35 +ENCODING 10260 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-135 +ENCODING 10261 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-235 +ENCODING 10262 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1235 +ENCODING 10263 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-45 +ENCODING 10264 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-145 +ENCODING 10265 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-245 +ENCODING 10266 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1245 +ENCODING 10267 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-345 +ENCODING 10268 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1345 +ENCODING 10269 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2345 +ENCODING 10270 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12345 +ENCODING 10271 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-6 +ENCODING 10272 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-16 +ENCODING 10273 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-26 +ENCODING 10274 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-126 +ENCODING 10275 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-36 +ENCODING 10276 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-136 +ENCODING 10277 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-236 +ENCODING 10278 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1236 +ENCODING 10279 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-46 +ENCODING 10280 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-146 +ENCODING 10281 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-246 +ENCODING 10282 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1246 +ENCODING 10283 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-346 +ENCODING 10284 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1346 +ENCODING 10285 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2346 +ENCODING 10286 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12346 +ENCODING 10287 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-56 +ENCODING 10288 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-156 +ENCODING 10289 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-256 +ENCODING 10290 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1256 +ENCODING 10291 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-356 +ENCODING 10292 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1356 +ENCODING 10293 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2356 +ENCODING 10294 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12356 +ENCODING 10295 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-456 +ENCODING 10296 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1456 +ENCODING 10297 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2456 +ENCODING 10298 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12456 +ENCODING 10299 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3456 +ENCODING 10300 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13456 +ENCODING 10301 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23456 +ENCODING 10302 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123456 +ENCODING 10303 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-7 +ENCODING 10304 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-17 +ENCODING 10305 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-27 +ENCODING 10306 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-127 +ENCODING 10307 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-37 +ENCODING 10308 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-137 +ENCODING 10309 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-237 +ENCODING 10310 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1237 +ENCODING 10311 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-47 +ENCODING 10312 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-147 +ENCODING 10313 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-247 +ENCODING 10314 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1247 +ENCODING 10315 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-347 +ENCODING 10316 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1347 +ENCODING 10317 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2347 +ENCODING 10318 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12347 +ENCODING 10319 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-57 +ENCODING 10320 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-157 +ENCODING 10321 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-257 +ENCODING 10322 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1257 +ENCODING 10323 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-357 +ENCODING 10324 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1357 +ENCODING 10325 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2357 +ENCODING 10326 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12357 +ENCODING 10327 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-457 +ENCODING 10328 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1457 +ENCODING 10329 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2457 +ENCODING 10330 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12457 +ENCODING 10331 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3457 +ENCODING 10332 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13457 +ENCODING 10333 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23457 +ENCODING 10334 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123457 +ENCODING 10335 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-67 +ENCODING 10336 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-167 +ENCODING 10337 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-267 +ENCODING 10338 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1267 +ENCODING 10339 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-367 +ENCODING 10340 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1367 +ENCODING 10341 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2367 +ENCODING 10342 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12367 +ENCODING 10343 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-467 +ENCODING 10344 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1467 +ENCODING 10345 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2467 +ENCODING 10346 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12467 +ENCODING 10347 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3467 +ENCODING 10348 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13467 +ENCODING 10349 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23467 +ENCODING 10350 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123467 +ENCODING 10351 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-567 +ENCODING 10352 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1567 +ENCODING 10353 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2567 +ENCODING 10354 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12567 +ENCODING 10355 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3567 +ENCODING 10356 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13567 +ENCODING 10357 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23567 +ENCODING 10358 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123567 +ENCODING 10359 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4567 +ENCODING 10360 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14567 +ENCODING 10361 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24567 +ENCODING 10362 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124567 +ENCODING 10363 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34567 +ENCODING 10364 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134567 +ENCODING 10365 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234567 +ENCODING 10366 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234567 +ENCODING 10367 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-8 +ENCODING 10368 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-18 +ENCODING 10369 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-28 +ENCODING 10370 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-128 +ENCODING 10371 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-38 +ENCODING 10372 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-138 +ENCODING 10373 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-238 +ENCODING 10374 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1238 +ENCODING 10375 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-48 +ENCODING 10376 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-148 +ENCODING 10377 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-248 +ENCODING 10378 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1248 +ENCODING 10379 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-348 +ENCODING 10380 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1348 +ENCODING 10381 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2348 +ENCODING 10382 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12348 +ENCODING 10383 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-58 +ENCODING 10384 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-158 +ENCODING 10385 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-258 +ENCODING 10386 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1258 +ENCODING 10387 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-358 +ENCODING 10388 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1358 +ENCODING 10389 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2358 +ENCODING 10390 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12358 +ENCODING 10391 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-458 +ENCODING 10392 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1458 +ENCODING 10393 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2458 +ENCODING 10394 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12458 +ENCODING 10395 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3458 +ENCODING 10396 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13458 +ENCODING 10397 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23458 +ENCODING 10398 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123458 +ENCODING 10399 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-68 +ENCODING 10400 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-168 +ENCODING 10401 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-268 +ENCODING 10402 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1268 +ENCODING 10403 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-368 +ENCODING 10404 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1368 +ENCODING 10405 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2368 +ENCODING 10406 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12368 +ENCODING 10407 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-468 +ENCODING 10408 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1468 +ENCODING 10409 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2468 +ENCODING 10410 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12468 +ENCODING 10411 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3468 +ENCODING 10412 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13468 +ENCODING 10413 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23468 +ENCODING 10414 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123468 +ENCODING 10415 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-568 +ENCODING 10416 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1568 +ENCODING 10417 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2568 +ENCODING 10418 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12568 +ENCODING 10419 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3568 +ENCODING 10420 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13568 +ENCODING 10421 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23568 +ENCODING 10422 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123568 +ENCODING 10423 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4568 +ENCODING 10424 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14568 +ENCODING 10425 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24568 +ENCODING 10426 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124568 +ENCODING 10427 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34568 +ENCODING 10428 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134568 +ENCODING 10429 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234568 +ENCODING 10430 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234568 +ENCODING 10431 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-78 +ENCODING 10432 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-178 +ENCODING 10433 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-278 +ENCODING 10434 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1278 +ENCODING 10435 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-378 +ENCODING 10436 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1378 +ENCODING 10437 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2378 +ENCODING 10438 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12378 +ENCODING 10439 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-478 +ENCODING 10440 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1478 +ENCODING 10441 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2478 +ENCODING 10442 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12478 +ENCODING 10443 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3478 +ENCODING 10444 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13478 +ENCODING 10445 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23478 +ENCODING 10446 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123478 +ENCODING 10447 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-578 +ENCODING 10448 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1578 +ENCODING 10449 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2578 +ENCODING 10450 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12578 +ENCODING 10451 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3578 +ENCODING 10452 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13578 +ENCODING 10453 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23578 +ENCODING 10454 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123578 +ENCODING 10455 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4578 +ENCODING 10456 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14578 +ENCODING 10457 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24578 +ENCODING 10458 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124578 +ENCODING 10459 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34578 +ENCODING 10460 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134578 +ENCODING 10461 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234578 +ENCODING 10462 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234578 +ENCODING 10463 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-678 +ENCODING 10464 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1678 +ENCODING 10465 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2678 +ENCODING 10466 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12678 +ENCODING 10467 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-3678 +ENCODING 10468 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-13678 +ENCODING 10469 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-23678 +ENCODING 10470 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-123678 +ENCODING 10471 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-4678 +ENCODING 10472 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-14678 +ENCODING 10473 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-24678 +ENCODING 10474 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-124678 +ENCODING 10475 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-34678 +ENCODING 10476 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-134678 +ENCODING 10477 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-234678 +ENCODING 10478 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1234678 +ENCODING 10479 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-5678 +ENCODING 10480 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-15678 +ENCODING 10481 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-25678 +ENCODING 10482 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-125678 +ENCODING 10483 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-35678 +ENCODING 10484 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-135678 +ENCODING 10485 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-235678 +ENCODING 10486 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1235678 +ENCODING 10487 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +60 +60 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-45678 +ENCODING 10488 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-145678 +ENCODING 10489 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-245678 +ENCODING 10490 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1245678 +ENCODING 10491 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-345678 +ENCODING 10492 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-1345678 +ENCODING 10493 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-2345678 +ENCODING 10494 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +06 +06 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR BRAILLE PATTERN DOTS-12345678 +ENCODING 10495 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +00 +66 +66 +00 +ENDCHAR +STARTCHAR UPWARDS BLACK ARROW +ENCODING 11014 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +7E +FF +3C +3C +3C +3C +3C +3C +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOWNWARDS BLACK ARROW +ENCODING 11015 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +3C +3C +3C +3C +3C +3C +FF +7E +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFTWARDS TRIANGLE-HEADED ARROW +ENCODING 11104 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +20 +60 +FE +60 +20 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UPWARDS TRIANGLE-HEADED ARROW +ENCODING 11105 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +7E +18 +18 +18 +18 +18 +18 +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR RIGHTWARDS TRIANGLE-HEADED ARROW +ENCODING 11106 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +08 +0C +FE +0C +08 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR DOWNWARDS TRIANGLE-HEADED ARROW +ENCODING 11107 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +18 +18 +18 +18 +18 +18 +7E +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR LEFT RIGHT TRIANGLE-HEADED ARROW +ENCODING 11108 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +00 +00 +00 +28 +6C +FE +6C +28 +00 +00 +00 +00 +00 +00 +ENDCHAR +STARTCHAR UP DOWN TRIANGLE-HEADED ARROW +ENCODING 11109 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +3C +7E +18 +18 +18 +18 +7E +3C +18 +00 +00 +00 +00 +ENDCHAR +STARTCHAR char57504 +ENCODING 57504 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +C0 +CC +DE +CC +CC +CC +98 +30 +60 +C0 +C0 +C0 +00 +00 +ENDCHAR +STARTCHAR char57505 +ENCODING 57505 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +40 +40 +40 +40 +40 +38 +00 +12 +1A +1A +16 +16 +12 +00 +00 +ENDCHAR +STARTCHAR char57506 +ENCODING 57506 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +00 +00 +18 +24 +24 +24 +7E +7E +66 +66 +7E +7E +00 +00 +00 +00 +ENDCHAR +STARTCHAR char57520 +ENCODING 57520 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +C0 +E0 +F0 +F8 +FC +FE +FF +FF +FE +FC +F8 +F0 +E0 +C0 +80 +ENDCHAR +STARTCHAR char57521 +ENCODING 57521 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +80 +C0 +60 +30 +18 +0C +06 +03 +03 +06 +0C +18 +30 +60 +C0 +80 +ENDCHAR +STARTCHAR char57522 +ENCODING 57522 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +03 +07 +0F +1F +3F +7F +FF +FF +7F +3F +1F +0F +07 +03 +01 +ENDCHAR +STARTCHAR char57523 +ENCODING 57523 +SWIDTH 500 0 +DWIDTH 8 0 +BBX 8 16 0 -4 +BITMAP +01 +03 +06 +0C +18 +30 +60 +C0 +C0 +60 +30 +18 +0C +06 +03 +01 +ENDCHAR +ENDFONT diff --git a/src/font/res/spleen-8x16.otb b/src/font/res/spleen-8x16.otb new file mode 100644 index 0000000000000000000000000000000000000000..229b7ab02a40482f9f9a1ec332a38ada6806a267 GIT binary patch literal 25912 zcmZQzWME+6WoTevW(aU~atY~6%>2o~z;K6w!GD^olaI5DsG>OoLqG`w14D>^u)b0M z&8Fa)MBFfh$Ypf0D}-P znUR{9qR!#M!@vOMn`LCACUP+`F)=WNSTHa!sAS}pRIq?mhQu&1u+PZJPfi4L8KS>1 zFtAU{O{^$jJ`Hklat{LogF;?nZmP7S`2z-qkdEx*a1*?f>nXEqpSP@QjDw; zq#Y~>ieXsLfdub?v?Hqo@iClK2aa?J36yw;3w}Uy=dokQ&ipt7G6YEwjV)fjt2=GJC zAQGY-#HwMdJ9g|CvfmFJH~)K z_A1CugzX@^j~%OFWBUVjCj;Deu*aY(B_tRuz;P#oq+LRS0W1b03Xrs)x$)t{jWfvF z!4`uEWbF(L3~#PoyLJsFDKaoHK>UuZoq+*d3ZWKOaFw7$3bP&JcTjk;v9X~Dg2N8U z@gS844uCz5h&Xs4$)TzQB`1tP1_`33PX-2tW5+;V%0tthV*yH`Xo4X9pfVI5@(c_N z|3C#>0#f?q0|#yah(uIpARY|E9S>0hQ4bdcS;=5j=L4qU85YWeQqU-d@}Lw{5D~>7 zai|~z14rEn5U2yS6`+C^ISB~~KN1p1~1V5n)DwP?|zSxum<2~}yqW8-7P02TnL zhN)y=U@%TQlV%JOgQ^5G|AA5rn1!U0fx!kWgb2Etrd_Kxty+a_5!?!hQiyhooE%Uy z4I+fl4)P+Xb%B&OEqr(^5)fuUw9BaRu&IG_3)lpRAP>0t0Tuue5J3@c6h91rmk+@TPaV4Y{ujML75 zSr9>o0}f7ZZzK!~u1t|*VSzdcCYW=niAO*VDwr@qL1S~qi%KvP z>?8)Jnq%kAUAy+|*|C}iSfVwMfHxWtewQ%7B509=MKA}OAXGc3(SY1yVSv}gptd{2 zNemTd&YZb%=Gd`ocR&t>2qu8S30xZ^oWuaiuo(5b04SW$Rf5WB43!L^a6(sUkz>Jt zsgi+#Ax})K?!pDMst1xp5$TfwoVhDnqgdqrO+ki?82?=nC1ql$4 znHYkg&K8CsLs}YU>*7Yi4IWTNM5**ap@h*q0Qm?*CCGLRL6DCy1VLtE2!gzcDF`k+ z>p;a5BHkGoz)AEPa{2@ZGAMDQv>8B7!Vm;G2}2NMJBA>Vc5JOR+of!85a)?g_B zLG3Dxv;<0C$BrFCD*!>Y8D^>j1rb=`F&+-&<`z^XTo76Sg33y?+yPCW@bCncU?61& zP@1oxP(oKJ0V!!gijm9&MGm^k918)65Q>?grW-^Up*_b!0Ghmz+TJ-9V%VD5SjtZZ z1~z$cc14Nz8Z6~JIONguQH}*Rm7tJER|)kyYApjvo#vfyNc!=@aTBG?n1kL^Bgo?Sd2@ zLn_ul6)<{V23q)o6DY{bU?*ARAbL1(K?a6n|Dj19x!-Z%1GFQC5H#|^R+)mz5R782 z4x38Qh!MKVG#hLx)6#4(OGUUs3FO#B^v>Y12?|e)mH^1VSj_wmY6XDs8;}BUC^3M_ z~qRha+Kn8A6j)ef&K$L8rr8;z#7C9CWA(RRl)S`w6Bg{-YgUw7( z`b1X=8uNjO9YZdYLB$$a7$FF)kB*@ZV1nWuebfl560I`=Q3)~>(XN9w^-xtx7(fS- zP=-1|NfCW~NkRgfN>H_nt}@3$03w89CMbbHgptOq7~~8L3LxbwB8nOKq@_XQ+Y%BI zH*P?yi5v?7P`Q9wL4!*$Xaf?i9hAJGl?)<|z$(!zQ>dAsP(n&eAa{bSLM^NWK&n6( z%}kIGN(!lg_y3W*{|q%4eJ+qY4;(lEYMr2&iQYH?mEN$Sd}MFJt4O#YZ15A5Wh4ZU z3u>s35Q0brHA0Yq0VZ675CjDXG(h3CHnapob37>7pgSI<5;`D)6d)j#=$%jr1Ev~` z{yV5&4C>dTbehhjfs1~$#Bd|W45Nnv>OW)lP%N-?md~Vt3d#d$ZCa3-(AFGLPHKIia}&-K1*h)o^nINYlXBlV-1`V@L2d*ag9nMiv_nTtp+eAsR;(7Gj(S5?GBAMVcEM4EJnaaY zYXD^uGLt8qXbD1G<9!-K9zY4HYkcP6XYXkNsAB!H%>6; ziy`GXDA6NXbPW4^F_wNYBx```BP8vRU_&z#6l|d20}Y-+BM#zsP#7U~?w}1waJh

3W5+hkp(VVBIj=C2XQV{s&LABRighVBrZe{~u`T9mGHuJOJ$~!$S$u1%?K`1lV>M2iz$}@+Qcv{~!W{!74!v z1_p-zFj24|TsxEj7L-8)U_q#MaM}d1Q3PS45J7OtKoAf?85tP{1_S{S1R2D@unI!K zy$R;7f{8*@f*Q(57$OKU5@akGLj=LP8RVfJf`k%8j6ohIilP$cTNFWgs3@}F4{T=s zfQcfj`~%f3gCYnM1q(vWl7~7C67MiUm?&5!)GU}7L?uiRCJI&w)&2vUnLl8nV3knq z;I=17HHsii6f6ie6Bcri@PrA%M8PVdX2Qf!RYD~p0RrW~!WdN$rXOr3+#;wJuppEz z57Q48gsOza28!*daRk-=2dV=_B`k_ioP=EvrXSh%KQN_WL6}7-f}qd_WkVF(p@Kia zqo5KJ5GO$e<*^C=$0`W55aM{K_WxLIM+rKZ%KuRPV8=t<2P-WgsSYNHl9phC*peb@ zX#+JM)l8TmN~(hiqL>L2gy~0dC)5>?po4P$!%_o^N^A)XH9TP!L5)E*6DkP_WGDv~ za;SnRw!>6nwH+mnptk-;i6fYHlz4}!gjo;vCQK!YnJ_`@WghW3M;6}|0pV9w!+Lt*8U%>?Jyl+mH(jtYBt30Fb;(N{~xRp z!hw(=O=yB3AvBc`ArwKF5RxE52rh^$1QkRTf(rga17IipK?7jHA7}t9_yG-o1s|XR zupqcgibQ|~8_)n)kRJ_z1qEGDAy_bU6)FS^-YrLkU_p4z3<-Ql95JE+uu7-`2AIcC z1Ys@%3&K>ubf5^r0vkmTMLSFd%owmrm>`Nu7zf27m>`OFm>|p;u$drjH4p$|Li`S9 zftJw!`v)$T;VKyfEG%kj&;|eftEsU-*o%)?+NSP*152sa#f@Zrawf2f&@k(r%e z-o73p`2XLZA0HkZ0Ih`uX@FW(Z!gc!&J5a}h!6y^>ag{8)36EFF)%>HkvpMj*aRVF zqHUUk=tUERn29C`F%w0wu5oE&OlJq6Hte^)9S$EIeBM56EiT2KrVq}h+qPElOviSh>IkFP>CT3QVGIP34}@vL6`uB zN{|2u!{eO|Jn#ro55^EPA)``Y0T2NZ1gV5-MF@gaVhDm%f|NidAlgAHF$6&>L8?F) z?j(>(kN^n71wmRtszEed5TpV_5TpWR1c*jai6ID50n!dK21O-?AV?)hJBCUOL6BAu z2I+vC2~ve22oeBEV5r0p1gQjJ6x%_nFa$vYAPEeW7=ln+89+MV?u2SX6$GgSNnohN z5Co|NVUT4QW}*m!RDrak2!aGa7)20jE2@*ADo_PMDnZ&oG~9NON(@1$3ec)YkV8<@ zCrAfeJ4hu+JBlEPgCPhq2&5W>;o3o}Kms5P7X+yU8G#}QQi&l5QVCK8l7MRmsl*Tj zsRXG4VHA}h0T6}@g0#XgTo9xJLl7hYlR#04AqY|l!XOD0l^B9B0SuKO0T7032l)tw z6A~a59|Oqm7=j=Hm;{PS3_*}e5C%!0sKgLN5&&5WNg+^eAOR4D2!ebBQVqgzGod~K ziJ__l837VSQHdc4GZQ3%q7teV(@YGNPyvKWkX9sfA&v*B!VmqWK>{EN43!vy zAX`Bgqyz3wkTwiKkN`*mLnVeFNF@kEEknc+h9F1)q#Z*gh9JmRkZKSOcPB_2h9Fc0 zxKKq=2{IEZ32_pL1JVvM21O-?AV?)hJBCUOL6BCEY7mB-2~q_T0AaWwNF~TT6hV+m z3_*}ekSdS_Tsuf5h9F2KNEHa9s00asFkBF%6^7x0AQc#bAOV;Jib@PYkV+5+Nua32 z5QGU}s00asFkCyxM=%WE91Ze2h9F1)CV`?7LlC4Agh3K0Dlr6M0vIYm0w4_64)PI5 zH3-86K|TQqfG}JTq!OeRMG&MCLlC4AqzWVf*A7yNAqY|lQU$^&DnSAu3>O4x1*wM8 zkcqWK>{EN43!vyAX`Bgqyz3wkTwiKkN`*mLnVeFNF@k^EWNBmlxFf*@N#szEf|Oppo;L68cNb`XuC5CT3vK^!fgyB8{*$xr_VYnbjD-wp7 z2~`2I8H6E%AXP{b5J8Xt2!kXs%!EoHRDx8Y2*LzV1VJ1ahMNiU5e%oLVQGKD1wpDn zUIo!`L686lqX>dHAPkZ~QHdc46Tnaj5&&Vic94%iszDeo2=Wa`0EFR!AeA7kD1soB z7=j>`AXOjDnZ&YRALB%w1O~H2gFQ}Dhxr807wEuC59kKB?zO~4pN072oeBE zV5r0p1lbD0P#q9=g0x`>f&@Sk7%DLYK`KEQWEnh=L8>qWK>{EN43!vyAX`DIK{VV< zkTwiKkP47?5RIY|LlC3_q#a}oib@PYkV=qt43!vyAgv(PAPhGXqzWVe!f-*5N|1Rd zf*_R`f*_SpRj4T$LlC4Aq#cS8u?ZDGXa{M9VYrh(sxSmW0x$^_l^B8`l^_g~Kv9Vy zh$Mh;5>y*V0E8iq2M@m5fC0#JP(d)?=6@af8Z(82e+lU8)*uVRQHFUL7#KKs{=p8d zgPIAQ%)&5JBj;ZZ`nol+nW!oui|b|>B!CxnLCkaj0Sg8pDJd~AA^6C%go=tvU0qsQ z9b$|cHi8curw40qH~>D!jF}mHyw3s1TqJB*|37r_9;}iPssyS5G@{Q03N6q%ehi>h zbD$M^AW;UMKOA5|kSsErf#LsO0Rcuv1_tJnpwMAp0xJU@6(|a}f`NyDft7`UaRLJ? z0|Vns#+eWjLNh#pSO+o{fmD7c(zoUdgA|Spr$YS>jkGDu^jaD#$7*DX1w}DcC5~ zD=b%7qc~M@g_4w#oRXH3hf<DfL&+_ci{xZ6-tsya!Q(D7p6d6 zsG!OKci}13^Qt#gAE_CrS;1YHs+Og}pv|V8@L<7%4Imf(fB664|MmY%|7ZM9`=9bZ z@qhIH(EtAbz5gx#x9DHhzskSw|GxhF@bCSSl+Daw#gGMMvoP>5 zEP=9F8N?ViK-nA&91O>xY%T^fhI0(g4EYQN43!K;44Dk+3>ge13FN!V3rYs34;!U0)rbv5ko3N3PUPbPcnl7gEK=SLoP!SLn%WsLoh=DLk>eK zLn=caLl8qMLpnn#Lk>eCLlKIM0z(9Y9)mwa5koqI0z(o*CPN8BE<++i0fPcVF+(Op z6+M#SFC#2 zKzs%Uh8jjjh7>Ud1_2PCk%0ju|3I9PK>@^PU|?(i;=+r%Gcp%V0iL|iJ<|? z|D(acAn=C?>;OKPeX7i0w=gg;h%rFj#>2qCAPjP(JOcxR8i--cz`$S$VmLA|Ft~vj zJ`4;Dejr8+0|P@cNGHfuc_79D1_p*o5W|CkfuRk?m<&=pn}LC0E=VMRfq`K)NMsKK z1H(}e;~E14!wrxW$hVI`B5xQN7)2Qw7#Nip7#OWU40{F!#ww5+E(Qk1Mi3*Jfq}7y z;s5{t{}>n;r-DR4+8G!ZxH^bGY37~K3r{ak_~8FDj=lNo}29Q{HVf*68bL&7~>LOdXv{ql=) z6LS~>JR@9v;)6Y-Tp0rVJ^ex;%plibf1l70Pk+Do2nHlhBtvk3qqC=St09B0r(ZnCAXf%w z|A5FKkdGLg^9w4AGSf3k6f}}G6^slFEp&|xj7)SC+=^0DQj0Q^6`T`ulS+#j;@zEG z++1DaJ^kGL8C+8`OHxx5$}>wc6w;GY(o$0t4E4-GepFDfPS<8=V*~?Gq%nY! z94Og?I2;Td3_RdW0807@HUk64|9}6#F)%Q&Ga#!0>A{LYb}*l8=;-Ka86`%;U^E?! zW`oh9V6-G0EeS_U!qJj&v?LrY2}eu9(UNeqBpfXXM@z!dl5n&n91bNRbS#(oBy>Ow zG!zUP{yhL5^kralU|?WuU|?W802y^=a$sO!YG7bsI>5ld%)r3F?7+al+`z!Vd;l^E z&Emkoz|z3Lz;b|rft7)Qfz^S5fwh5wf%O0b0~-Sa1DgW_16uyUco`TNcpVrRcpDfPcn?5E(D@t~ z82B0(82An_Fz_=lFz`DtFz`1pFz_FMjFJmDFfa%-Ffa%lU|fDpw7U+pzgrHpx(g1pniaXL4$#TLBoN8L8F0zLE``e zgC+w5gQf!mgJuH*gXRGS1}z2#1}z5$2CW7L2CV}O4B89~4B8G14B8D04B7`67<3pI z7<3#M7<3vK7<3LWFz7NcFz7lkFz7ZgFz6m&V9;Y=V9;}5V9;w|V9-0jz@X2-z@YEI zz@Xp2z@UGCfx&=*fx*Cmfx)1Gfx+Ma1A`$01B0Oh1A}1$1B2lK1_mPr1_mPs1_q-B z1_q-83=E(-4r2!f2IB?>2IB(^3?>W=3?>c?3?>Z>3?>H{7)%)$7)%`)7)%=&7)%c^ zFqkngFqkqfPuk9fq@~=fq@~gfq^0L00To10|P^l0|P@)0|P_Q0S1O(1_p*;2L^`V z1_p-U0}Koy3=9k*4h#$-4Gat+2N)Pa85kHs9T*rw8yFZu4=^xTLBFWG^CA-REpA^8Ag zHZsM5fgz=Vfg$Ap14AkU14F6<14C*914HTo28J{S28J{T28Of-28Of)3=HWE3=HWG z3=HWF3=HW97#K1b7#K1f7#K1d7#K1RFfe2?Ffe2~Ffe2`Ffe2uU|`5%U|`5{U|`5< zU|`5Pz`&5rz`&60z`&5*z`&4wfPo>0fq@~%fq@~Xfq^0C00ToV0|P^@0|P^D0|P_u z0R{%pTw|UC14CW|14G^c28MhF28MhG28R3w28R3t3=9Pf3=9Ph3=9Pg3=9Pa7#Io} z7#Ip27#Ip07#IoE1q28J08 z3=A_4FfhzyU|^W(z`!uGfq`M>0S1Oy3=9mj92gj8H83#DI>5jHnj4?(z`!uOfq`N6 z0S1OS3=9l&92gkpG%zsCIRKeApXq$VeSD2hItGO4D%cq80IxFFw8r^z%ZYI zfnmM_1H=3V28Q_u7#Kj)@CzIm7#1`zFf2I0z_5^kfnlKo1H-}w28M+P7#J2YFfc4~ zU|?9(z`(HR00YBf1_p-34h#&78yFZCA7Efu!oa|=#DRffNdp7Jk^>A3OBom#mO3yn zENx(5SbBhgVHpDh!!id3hGh*549gBMFf3Hq`7YS5AZ2L^`K4Gavc z4=^ySVPIfbf36wG0dlYaJLE);2IOtUbWMu#SO&VVwg5!@33rhII!R z7}hf|FsyfAU|8S4z_9)R1H%Re28Im|3=A6@7#KDjU|`tDz`(H4fq`LT0|Udx0}KqC z7#J8fIWRD6YG7d4bbx_jGXn#|W(Nj_%?%6;n-4HBY++zv*y6yzu%&^4VaovqhOG}p_O*mZz`VK)N~UaV*wetku;%~+!(IjkhP@6940{_G81^1uVA#jNz_8DOfni?* z1H--p3=I1j7#Q|DFfi+P7FdT7UU^vpiz;NUM z1H)0!N(~1FhNBG(3`Y+zFdSoGU^wQ$z;LXAf#KKz28QDd3=GE|7#NN>FfbfHz`$^V zfq~(K0|Uc}1_p)`2N)PYU6PXy3=Ah57#L0-U|=}Kz`$_Ifq~&v0|UdU0}KqO85kH& zJ1{VuZeU{f#JLZ1H<_S28Qzo7#J=vFfd$jU|_h=z`$_f00YBC1_p+U4h#$z z8yFZa9$;X&#K6FC$$^34QUe3Sr2`BMml+rsE;}$VTy9`sxO{+t;R*u-!xaYxhARyW z3|9^?FkEF|V7ThQz;LyJf#K=_1_scAm1_)V7Sh}z;NAxf#G@s1H<(L zki{%F92gjGG%zsSIKaSglYxQZrUL`R%?1XBn+F&eZZR-0+;U)GxYfYGaO(gA!)*oz zhT9Gd47VE?7;Yb6V7SA;z;MTbf#FUA1H+vI3=DS}7#Qw4FfiP0U|_g=fPvv20|Uc7 z2L^_F4GawT4lpp>XJBBs@4&!tzkz|_{s9Jt2Mi1h4;&a69yBm8JUGC>@Q{Il;h_Tq z!@~v!hKC0j7#=Y&Fg$W#V0hHP!0_k*1H)qm28PED3=EGO7#JQOU|@K{z`*dtfq~&k z0|Ud80}KpL85kIzIxsLiZD3${dVqo983O~uGY1BSXAKMt&kis!JZE5Fc<#W!@VtS6 z;rRguh8GMB3@;oQ7+y3mFuXXx!0?iRf#IbC1H;P(28Ndh7#LnLFfhDwU|@LFz`*e8 z00YBo1_p-L4h#&h8yFZ~A7Eg3!@$7s#({z1O#=hNn*$6CZy6XE-a0Taylr4$czb|> z;T;15!#f8ChIb7N4DSvwFuZ4AV0iDq!0^6-f#LlD28Is|3=AI}7#Kb@Ffe>Lz`*d4 zfq~(p0|Udy1_p+Y2N)PWf!fy-7#J!Q7#Kb|FffAr3GFpgNP&BPpxz%A3^E!f4`QQZ zaH|T`vVvlCd1CnBmJ4HFO>BnHADH6XR< z`jPpdaI%4>q5n`C#0QBZtHZ|z*#p9$I0exlj88o=av(E67!(e~s0XP5*$<+rfkAeI zFgCM5VGP2cFa}|eIuHiA6@+1N39cQn830Z1;MxsT!+=Uz1_lPuvNKSL2NSQsp%)|! zvJaGIK|KIaFA&5>$N0hnWH!hikUu~e<`0;$<5hCynvVURkI9WZl1Wd+P`5Dk*UXFo^|qy||(h>wgxYOvW25`)N55mY|xY!_dAdD;z;)5_KERe-vY>*p~!wi;ZKd!pzsIL$l@S2F8!deC4}+0A7nPDbq)$QP%jC@$A)p)4^s=OZ$TJj1_*qw9n5VeZ36gWL#Hk51#$2kH}m+E$=` z3c5ZRAD4bmxP#IpC~bn&z|^DDAUPO@*$1LwVlWzAAB+!^htVLvB8NSQjW7H`ZUm_T zVNm)2#Q~_k0@Zo2cm=6}*@sMnW`#g%iNPSX$a+C+nE4>PKp18Y$PFM2at{cD+y=rR zJ;)f8)A3PTthnZ~6bSsY{!$Zm8DG85)U^st1v5k!OJ(DlOj$o3$!LFosC zL1Lh^3}T~W(Cj`)9vgsBZo0A zHa_ur9dtmxNeg@eE!XWn}t3zfZ$2mw3G9ShU&Ime$S!nqL2A+UW8;HX>VU!vv?2tX{9qG<`4i+W7#kZ6lE;TZTbV)r2G68{X48-` zNE{@OO+PH1g3<{LgZh9lHmE$nm44981C2F<#t%U>48!>7YGHivOd@m+0HhY=cXS#g z22+ntgXGXLObxm?h!2wk(I9(3?gaT2lviMUkQfYu#6bB8l&?TyAU;Sh41@AG2*dc~ z(4cY?w88>3%MYGu2hFyFX8b{`E}&z~*vtjR9ms7UH-XYShz6+xnFpfD!Jw6GAb*1D zeGm=elVT1it$@M`hDp(jtQO`MWHDT9kbgnp2bv`ZtrP&w)`Mse28n~@vFXJnj!i8# zF;Mt};vR%SY;0;s5d)=95C-K}5Dmg0{UAPwM#msKLGcbs10Xgi9h2fGP`eDYG83d9 z6ki}VNDSQ`Tzrr|kQ+gH2bUUB#6f8e8wSm%fXX2lMm7@^HlR8hhCwT>VDpa1G_oE- zY>=Non2G0M2l)%M@&c5H zL1_)tjsuwk(+^7Tpp{#oGfhA%v_Y$^KzfL^A7lZR#)i?LF@2C)Y#5{;6wmlDNDU~yKo}NhAR5F6nF*3d)(>hwA@f1< zAa%$Xq!(lcNDL&8tezPCpm2kQKRNnAdO?_6`$298`2nUM#Kwg|?gObq#-RKRQV$XX z$%ELqFi0Mx4jH5SAD3Q`I5z#DvJHemW`g8F>TqF@JV+fj3`)Bob6^-$SHajY8l(nY zFLM3^l@Xx13lN6YV}#}}ki#EjKFADQ7$lEwKgh2jGeF`Xd0cuy;vjjDI%JF-{vdgf zI$RhekE|EOM)p5Q9+zH_I7l9(2N{FXE(n9fK=L4UxG+c_q>c~<=?9q!@*4<)*dQ@n z`aoxKfy@S}fyEar{y=dF!XP=28qmfnWOG1a4~hej7)T91j4l0v^n%PFS3k^uqKTJP}2Ju1i=onT8g3JKPk*Xim9zu>sQuTxKAjoaV z?Eq5T4{|do4S+C+O{)Dcw}WVM^n>&uW6(Gqtj!Pd7i?W3NE}%mE;dLHES(_JAaPI} zgD@+gdSQHYy&yh%`UBYmQ;$r8#6jxOG04rJFocCaNFB01 z7#k#xt{22dPk$hFFg?gLNF1ad9i#gnqz+jhj13Y;*9+r=(k=*t_#kyKJ;*dj93+nq zgTeuXL25wx07QfMAPiy?!su-QP`!b#{s)B8ffVnT<<7Deec=OCSu&BOtqA7*@uDXpkJTy)ZUN93&6JFufoe z`bX^{Itc7rg;ERb3dhNUYIjjRV$b|dpaY>;|f7^EL02ckiCfb0Y5fng9I zB#(?iZU&WCATf|Wm|74`toFUZ{>3=#*)BdbSegXBT#uwjrHAPkZNsYBNb;)BFN^2iwEcTgCB`~Z>% zsl$aq^2mB&Y-BS)Vjy+c%m9glQhmNt?4^j`pAUz;I!!Wwv zL1ux}fb0NaSe*&d3*v(?ObnzBq#hk(vmX>@ps)j(gRUM_cA$%a)FESJ{V=zKXi)s3 z>jm+V^&+!Da-cQHAR2_R*$om$)`QFj*$G2?7K`%dH`eEuoe0=t!>qpiHG9SjrMuX%*<`csB?8l|| zJ9O;~C>(KN(3~tT{V+Wsc~E~9#z&?>bLyb77ldJH5#&}_{s;LFo4;XVp!@|gA030_ zL2(bV3nT~P6T+bJ7LZyP2IWl{294W->OK$+6NAw(b75s2Og|{!!!Sr6Xew#KM%E892OoyT8Au;G#$_k6 zILLmGeIN|72iXh|pAZJw57G<5AhjU*5(mj6V^Ez1 zDknfEN0A7no^`;g55$>XyhB#(@d-2&2w9A2RG z0?Ml}3~~>$UKkr&_`}RWHUpb}kl#V>2bo2#{h+YJWey?xLH2?$A-h3g0CGPlyg(Qh ze;~c+7}+dz`$2j^7^WZOcaYmben8d-3O{7MF#FN%0`ZZ}0P&H{K{f;Aevmvi4C?!W z+(ih3;u?fO>X6L=`3HtUcEZ@mG-%BwNG}M(^dq~S7&jo>31TDbhuMYCevlq)7@OUo za0Iy%8DrB65`)_avK#D}Rzr?KfruK&^1qw}%p2ib>S|D)@J@p0(~nE_LSj|S-n-mm7Unk0qI4?pmqt!9%PJ7AI$wA_kzk@7{*n8;<6uR4va=N4`vo9 z?SU}JO)zyZ8d*Ka4KO_*wJ;1Sn?Pj?hz|-UkUCHt!Ng(x0#JPdk_V|n$H?kIav*is z%mImmadvu5(mj6V^G+E;t&)EAbD*1 zKz4&LNFG@)jE!stNDQP7n>iqHkUTaFD%(Nv0K&+9W)L44W77)~1MO7=pT-V7a}5+0 zAPh!f#MUJer)3p$a+EUMs_bc8zc`h9~~o`0g?l$L)Q!9gTz7d z$QW5KNDV$rtp9PDflEKkT_AZ_`T)_O_y^H24C;#^yB(bkGY3>og66Y97-S~C{u{FY zL25zy42D7WA^Qa+k8UT3k8B2r4^oG$55@+$50oxJ7*-Y{>xHQU(V#vxwlQ3g`N$Yq zKg``Ay`V4yjlF^}hz$}0nTxC!nGJFqNFE;ssR5;HP+0{kbCLBUvq9>xnFA8XrXM5@ z(hs7s>4S-Z=hsJdB3vL8d|S*z_ZZAxJ++4IvCGzd-6hFgD2jptJ+Ru=Inj4;0rhOlF(|x1X#ms?1<8TT z0fjLrK4JPmG>8w9N5-JC9;6o}2GR#o2gAhL4>JSV9AfQ9_CLsdpmYbrpz;I82GJmS zkUP;ay8l7yko^GC3&J3AbiFXQgTz7dAayWIto<-EVEzZ0L#+Mi{s*N;To`07$erjI z-Txr>U^4?-{SBIHfTcf>`PlTq#6a@sc7ymZGhpVBVn4F~(bFF+3}G}#9%LUnM)yBR z9ZU~04H8G!3vw$g-azum>e1OCd360CKFkb4=3vv0?0-<1h-@c1A0!8|4;`cX9~4g@ zwXk%IOrz_Cxg8{qZXY@yWF|-*I)<46^FJx}Bl{m@K0XXG7u{|UAKm}B%)q7}=5~-d zxb(rqvFQitgP8&IKTJJ3jZHsD4@f_#O@gkTnDQ4{KXN={i#w3{Aon6;Sl$MSfzl^P z4Ga@wKS&=4qnkr)`47?y!l1B0c0YRA3z7q=f!PCVvw-xXV~{#zKY;XtFi0F-Ka3AD z1Ks~HGhj4G9;6N%CdM3W`eE^h%}ip%knJT#J*8?uZ5`zCC(zg@Y}}I^vq5zRs0<)S zFGvri;Rn(W!XUFK!LYmnN@w^mE;~SF0LUG%v<5Q|#s|rP)Zo$&Qx6(%0_6=@x&X03 zVjy!sYH;ZXl|P{L32Lum!yq}38eIB8ZUeayWDhz9iGwg`3=x-pWO2|sCD2|?P`M7n zAU;SOSsgMPHvR+hHzFa^dO6a!WraW zY#3P`E;gu~g83Vn2Dt^79%ONleo#1|V^FvttHZ?xLm& z2g!rfVZ$IfboZk3L2A(Tg5ni~LE<2JbUny?kUU5oG6uOHly*U4pg2axAoZYn42D5< zDo7nR3`!fwdO>LbnGa)Q(+d&4#_ATf|% zLG}{DAhp;qsBZzvU!d^BhGF4>O)p3c-G21619BHg4M;6EjGSIUaRG``5REJjVq?<_ z5(9-7ES^C$$S)uo8H3b-)MCS+@C9K|pC5!lc7ZUc?+?NtIS@wI3koZcTS4L=d5~TZ z2I&Q1kX{f5$$>CP9X1S-gJDp83JO0E4HJXW*z|(LKw%40gO3KO#fCw8K;=6qo37YC-0pi-Gtcwb(Eyu0itnFh~tHy&y4M_Mw{xQi~0P(gw0x5FeCg(J?l? zATdz*qpL;dgVcc3V#C+Yk#9%Z?EjA33$47&1O9bC|485BN*&K9z*!&L)17tnOY;1l; z7XyU>vL0kMx>{^}eC`L0lY=m*y$8ae@(VN`h>SsE$aW&L(ftqF{{|ZWMaCd8WIf1i zke$dFWEZmCAT~%0Sr0NBl(sp^CN z;u|>((D^VqWIedppm+wk8QlyJAD13vagcsc-Us;?W-o{ai6g5+W`o*>$ZbV*y@d25 zn+4Jfaz6-z>;$<5go$-ONFB&NbPN&)nF+!mJCN-}W`oqh^n%J|kli2*5=T~tiw%l% zkh?+kDhPwvATbcer3YCYR`~b=mAT~%0 zghBFzFi0IXjIaJgHWOJdvY$czMplQ54buTt0^X$utAAdK7&0Ey$$ zgDegz=U`zD@&k+w5=T~tiw%l%SQsF)L1MV{fYK-kgXEF*fXXje9tMen_JFGvhD1_Ht$HmUkS?gn9yK9GKpKe1tu`$2I6!su}U5(8mS9*6lC8x1la z9|oBL!nn-ArWfRIQZTw1$YBg(qstTGgY+O{Lh8}w(cO+NPl!*cer)E0`~%9bAPiz- zQ$vav_@-kLG=Rn`LH2^m5fBaHlc1deSszG0h>uS{dKe<}LHb~P5E~tX_WObI4?0Fy zhl>vzs{xfyAT}u9fY_k27evD_C{N-t10)VAA3)_SXuKa}4l)fA2g!rf5yGHzm_cVc zgUT)t4dN5h0}5kMT8TniKM4{?#)QlP$%E#XK<7(?><7^xK1duNMz#Z;4RRC6Z6I-c zX29e$7#J8_pm!;QXb>MJhl>W84YC7d7l@6FL1G{|kQk-AR5F+SA&fYD)T{U93RG}4l zK1?k>8hK6=nT<~mOb*$vxY#hYxM)x~gVGA9%mUFMK1eMn9zZl9jI0l&AH)agAx1x_ zZ4OE|pm+y`84QE+BFGG4F-SeA9D`wE^}*D^^nqxQSuk;2G_LjAApcW}L49Ws292@c zvLBl`XiOHHII&_N{UD4_J*9pFqw7KDgY<(ix%xqUG>{wd zVPyNz*&zKOj88v4w}HYA-Ar^oA^n8Rz~?qXdO>a>q@R!(_}mYQJCGgtFd_Yf%)sY< zP~73O8ze_aKOr;lxgQjE_~Z%6f%JngK6UuqPe?C5b3t+-{rJo$Bu5SXATvO2AqRu> zf-oU>;*tmH1z}wBAaQ)|0;OGi@`U6-`au|!k+hF3j^n%1eaZkt$kUCr#ly)i657G<5 zFt=i(aoG(L2kC`{6D}I04i^ULr!@RQVSvkSkT@>8LE<3$K^UYC7seJQFfou`kU97; zOf5DVq#u+9DA5n{KPCD>ZosD>SGa)03F!ry3sQ#*gY<*qAEXW!#-$e|4$@D^{~&d^ zFi1ZL9)Ekc+9IpHTSYa~sHhO2Z$N{_)uX3MX6`W^S0gVqn=GXvxXY#0{qAR6QrTo|9(pzz0~4abx@xP!u6oxQ1sWeD0DEvV*KE2r71QG*A4#%4E645SyvCY6S{A4KER3-Ti_44Rk5=4NCu zTzWy`pmlJ_YDr~-^n=X7hw+&Saz8%3AUQ?`1_tCceV{xCn(qZ+nAxCm1cX8BIY1aR zCkn!#vq(S~#0FuI90-Hdf-p=!$Se>BnGM1)`#^R#GB7Y4hGN(n7tlO02*dP)*dPp( zhp7kY1^EFNrZoIO_Tw^}7;$34pO85qH{iqg^n?5l!uZtSYFom@@#!Tb2l77%6Hhs$1=I3fEf4S$du@L`Z2@nL-KqeKp7C#hz@^ucIi^~2Po z(}eV+t4HVK(vPkV7av_eE_JxXVft~&(^?$VzNG|%#s?_DpmruD7?i&#!LU0L$fZH` z9wiu5e^P=$@)4;ts0~61CS)I> zv`I)mp}3{gd_wwR;fsqVq@R%cahVGfCuBZM9WI)X{eMMcR z=<@jZAU!h~7#NT-NDP}EboZe1L3JCbegxI8pu7yK>p|@WP~3pbLsyT^2bImB{vtaA z0|O|pfX356V~U`?bRhj83^EI3Cdh1%9U!|vc7p6iHxHc;(ho8VWG2XLkR2eqKz4%c zMpsXW4~jFG8eD9cJT@AXUSQ(n=ttIr%Pg4r*l1)u$ZTYDvFU?}A*(}XBbxe_gL28ioptE89M36pEc>&T3qCtF^9J(GDAEXZCUt|mt1L=d&AT~@rIt|M= zybKHsk_-$CAU()tf!H81bbZKtkoh1rAb*1V55gceNF0Pga>(k@*)TOAbs)dPFi0GP zLE`9o(D|V99#o#dFuC;yNFT@y5KT<|0rDp(j6gJK3=JKF$}f5Joqb zSUxPR5vv|u9msAFhKUn$FH9YbMz^0-{UEzR7-k+iO~^cS`$^RgvKxe9`q1q|=fl*& zXmtD0)uZ!a`f$-8w}3D%d314f^Few+7+oE)e02X1(uZz8sru2)2e}=DVdCiK!^B}U zx;#iP2;-86iQ}Tt-H&cQx;}JyQuTw}3c@gRL2M9)@d?rB_JQ<*Fd=;~d5~HVhVjwO zgNfs!(bc2#ap{AJgX{)jTzvHKg{gzl==Ook0%4duA^jk=APiHFZXO~1=<3n=g!IGg z#YKbM2Ew@H37HSl3&Obcql=@P2ht0|=;~m6boJD8k4{tDe02AN>;_?UdtrQZ`$^RgG8=?p=AqN* z=2Kf9WH$)I%td!UsrHkqA7(F%2Dt@iOboJh|h4^s!D(d7y8Vfx6W(bF3`pOCqP)RQWY zt{$C_?jAz&gw&(!N9U8OpH%he_EE|Qr9BWvcLy#$x_#*B4VONcIJ$n2-5`uh9wrW> z(e2cyyDL3V>MOdg#^SC7s|x1Ut~AiF^rrVrhGLVR@l38_byhuI6GL2dzI z7$2QR*N@Hz=>=hQeK0<{dYCwjM)wa)97dz-N9U8OA7(C$2Du%CVSIG+3GqRCL6}(m zFm*5*-Tj2@LzgF2Kgg{h46`4^24NTx1#p^@G%cFialZ{V;JDjjkVE zJvtwz4@RTw2iXn6FnJIggkgMi_2_(b`#^d@m{|QFwIB>rk8U2iedv62_2_(b_2_(< zy|`$Q+dvp5kFFn`4^s!DL3%+L#wVm6T^^(tgmLLdSC7sI=>=h2`eEX@XiCGAkbZP~ z(cMo-Ke~E!J|X>}umNFocfj}{IS_{N37HR52ctoHK^VqI*AJ2dVRGe3^&cVoVeWv@ zgv>{mhv|dS=<qj>qq!)yV)elk&!Z7uO%p+t!sro^7gD}i~5F3PHe01{&*$>hS!o=zasRdzN z>PfX9T|GJ<-Tg3oiKRhq1!0(aQq2SD1!0(ebefQPAiW?A(}%7eoext7qd|H>7{(_= zqniiP3&Jpc=<+ae7>zCu(hI^cd2||GJvtwx7ldK@(B%p7L3%+LrXR!xVHlqf4N?oj zFm>qZ3nq?>2I&Q1m^_FL!Z1F%`(ffR8l)D4VSIG^N!1TB3xr|jq0{K*fz*O9OdY!Y zFmV`7NI%Fd5XPmRkbZRg(bc2#L3V*K%szA)BnQIi;xIl)4uoNRYMYPlADBHb8e}&J z!}#d-ld2zP4vYqw4Z<)!y7?eE5T=$q%p4ev?tW73CsjYlFCYxFAH)V>7$2Pm$$>CT z9Nj*U90R>cTF9;LMClvnZ_JPa> zVVL=Z^n=ubFibtVd4%+%t4HS((hsv27Y%Y72*cz-Y!HU=(and6!)TCN5XQxaiNk1g z^FVq*7$y&5gD{Mbt{$BaQVYT`^&mC~!}x?~^za1f1!0&zbo*f9Fq)8lm^v7ZZXO~1 z=<3n=g!F^_0>Uu+(P@wz2*bodY!HU=(and6!)TCN5GIz7?tYM75XPk+-F{N_gUkkD znEB{5y7?frAPiH7E)Nrj(dhCZy&w#e2eCmI#z%KQA^qs`r0R#+3!_190byeK==Op1 zf-tfAVd`Ksx_>}sfiO%S#0Fs)AKiXZ^~218(IB%y7{(`L9=iP?y&z1iewaEKjcy-0 zA7nNN!_=eOkFFk_57G<5F#RAl2$RdFw)v!*hprx-5Aqub)5?E@>?0%(avKQ4+=s3n zoexqA!o;d4q#tGuu{64Q=;@D;Iq3S))e|xgT|c^dbUwQI==#y+(fR1+q3cJNCsjWo z_2}jkvL9VNIv-s>C@q07x;t?3(eM z%zShj-8^*r(D|h52iXn6Fn#Fuq4Ux0L+7KbN9UufhuI6GL2dzI7$2QR*N<*KNG}MZ z>x1#p^~1zrG^z6F_LHg~W-p8exgCUId_v}<%Y*cSFtPe!>R>dw`_av(wmitKAPln~ z#0Fs)ADu?GADs`<3&QC7V0=RA(dA+KU^KcsAwEnWj7C=vaw`bKb!Z7>L%_F2AT|GLVkbaoGxM+~uKo}-Z$b6VO7!A@3!Z1F%ewa9nMt3hrF9^fr z(e!boUdo4_%&A{V;oBG{`L=jEhgmK9F7zCRRU89gIddA7mB?S8K^VqIcMm!r-F}c>5GGbXOdX6yH=ht6-F`yq(d9vI1!0)|AT|i&;-l+F z=Y#ZuFtPeUYC#yLo{)Ly@`U&xvq6|x^FeAs7?*lV?L*fOvI~UCwGZ7qQu!czVdo=& z&Jh5ey8t>X0UHLX#fGt|1&M*o0+|V7gY3Yj7hMc=-zz9hgU(L?(I7s$8gxF$Y>*uw zyFhGQ7+oJWKIpzz(0QdG`$05_k4+u27%uxj;vhGH+y-JJ>%qka-S-MRhXO={__)-< z#6f0*>;TyXVk2Xa7)TBz22+ntgU*EjoezP2=kE*#2Jl_Rpm+eOfnj9*pgVfeG3d+- zkQyNd1_mi87Gq#w0AY|ANDdi;+@`|7zyQM_H-j)NErZw~jI0O52A%N(auevxAJEx9 zpfi9#X90oY4wUvl7{msNf#g7HKx#pHKzczK#0L2fc4jRo&O!MRbOsOzgV-Q3kQ+g2 zKx#pHKzc!E1cB6m&I|&bfdV>12y~VZ=u9Dy-5?BNgTz2`AT=PhAUz=8U6GR)>0EAvJ7eCS5+R@(7)Y{PwG79XB z_F5DJIzc9bT}6mM(MCXV6OyYsAfCpfqX`rwkZ@`1=xzkDKw(wa+|<_5UD43q(GGVc zG7ILQ_GYkcO>LlXZtdto;==TTwLu7wELaDG3yK-A)1X{b9bm;ECEbn9t&n&FM_yYk zIBLNJIDQ-2J32rPhXip$1IW`Li6&6gHaE6)bbE=6bq>%~TZqo)JpK5%TIDS;ROvJD(0U;?ZJoJqi*0dv6;;538~Lt}xB zLK6dXyC7);l*U0mLNghrq#crqvDyK0A6OkYrok+*gTMqrtOsrp*wf%>>;?rBnCJwt z5Mo`;O&}}5ao*Gh%5R_q42o`uZJ;0qvA}kK32@BxfFc0u0#Nn?Dd_>FU5Ho%$WV|k zyFg-SK>{%aq5~EhXrT+{VhbU#1lT;NGhhZ_GZgF=unq(ZQ(Z?#S2L(UX>RC%rF(ED z28n^ZfY1vz3a6_O>M-qqW*n3_YVQD>2i6Am5ts#zBS=y~3L$XxfO0&lgP=NKVSwZ` zxL&XlgoD5?=mZr@U=~;wi3K(uRGJ|&F~X6ASP(}ddy9|`SfPMT9oT7L0vtF9FF?f_ z+d6v6np-=1%39hx8bC4FQwA~*RFw8W2v8~p`3PbFDytoo4ZsrMgajo(^)Eu9TM=;&wyl{szoATFq(1TI>-t5FhvdmSv-Vz59GjZLi`ZM6*@ zpq$sy(Ow6xf58MO;z4>lK*qO&ECPEPTw%0<)V0+$cXV{MG_-bri`Q0A)!SMNN`EcQ zZ5=%=4ILc_vF@hkwvKL4Ftye+w0D5jbvJ{=nwvX1T3gVFhSrW&kjbqr4Q(J6h-mKU z=&1n_wIBk*0$E-IB5E7jI(lkA1XvbiUJn5Rqzz<856BLD1jv0L&wzb|m*@e7K@TW6 zkqB&JAXC60L=B=H6wat&3Tjo=Hnn$v!=SCD3FL*Qj*d2vf7@C>5db3Ez#&xI1PWLX zQ40zZP!p^N6h}RfkOWD94Cn#r?Ez_n5n%H`CWCAP`v~lFgnz+e5CR;(uvmpeBUl|o z7F5r)G_-Vp+xI;+EufUs+}hCs7OMrNCa@TY)dJGd3SxnS4#5J4eH}=)0Yo%`2(aQd z5UT@3bb$z%>%mF16_id}KtwZ$XaW(99X%~A9UVO_jUWQzG#IO_9n5O!XaOk!yANzH z*e%Ee$N(e{Hnw$uGfoS*$bb`IF-RW=lz-dnQEUUd9!x;&sA+76bcnz@@DSk28b*Ly zH(d>l;PSMiqZ3q0fvZKRocQiv-pt1?h z!Vm+Q2bV>55W*-h3*M}0FWtUyFNi0B3p%^(8If{C?vbc1SV zB8YalI{Yjmi~=iZ#$p~eF|cAH2>h->$P!^xcXMkyDEy#-gV2lNK2UtX{ej11Q2fHx zALtFvLhO55p~>^oQw3;K1O>;Kbm};KJa_;Ktz2;KAU@;Kkt0;KSg{;K$(4 z5Wo<~5X2D75W*135XKPB5Wx`15XBJ95W^755XTTtq4)nYBrqg0Brzm2q%fp1q%ov3 zWH4khWHDqjbTM=@^f2@?^fB}^OkkMEFo|I@!xV<84AU5loHEY+%^Pu!&(a z!xo0E4BHsCGwfj4$*_xIH^Ux=y$t&p_A?w{ILL5_;V{DyhNBF}7>+ZXU^vNeis3ZF z8HTeA=NQg2Twu7!aEakE!xe_B4A&U0Gu&Xf$#9F|Hp3l;yA1aj?lU}Kc*yXG;W5J# zhNleA7@jk{V0g*!is3cG8-}+G?-<@Qd|>#<@QL9w!xx6H4Br^OGyGuq$?%KeH^U!> zzYPBv{xdQ#GBPqTGBdI;vNEzUvNLipax!u;ax?NU@-p%<@-qrB3Ni{Y3Nwl@iZY5Z ziZeN4sv>N6TJ z8ZsI&8Z(+OnlhR(nloB3S~6NOS~J=(+A`WP+7lE1j1I)8qP7}FM@A<`XGRxBS4KBR zcSaA~<}-RSdVypay%~KNeHr~2{TTxo0~v$BV!fND z3$u~2kFg)d2GNWY{{Ls3$T*2{GUF7+sf^PYr!&rAoXI$gaW>-|#<`6180RxCU|h(! zh;cFF62_&B%NUn4u3%irxQcN#;~K`bjO!TJGj3qq$he7dGvgMpE5pUe9riS@g*_-V0=Z4s-dig@ipTci2E7eLTFTy@g3uP zR1s2mj2}o*4N}Ya5lnt!{0yQ;G3g<|_yxBf#;@pdjNgz&7{4?AK<1CKM?+vV1cpus zF#crx#rT`?5943Pe~kZ`7?>EDn3$NESeRIu*qGRvIG8w@xR|(^c$j#Z_?Y;a1egSw zgqVbxM3_XG#F)gHB$yOqfiW%$UrXESM~rteC8sY?y4B?3nDC9GD!LoS2-MT$o&$ z+?d>%JeWM0yqLV1e3*Qh{FwZi0+<4sf|!DtLYP9C!kEIDBA6nXqL`wYVwhr?;+W!@ z5||R1l9-a2QkYVi(wNejGMF-%vY4`&a+q?N@|g0O3YZF+ikOO-N|;KS%9zTTDwryn zs+g*oYM5%7>X_=88kicHnwXlIT9{gy+L+pzI+!|{x|q6|dYF2d`k4BeCNNE8n#44j zX$sR+rfE#mnPxD}#2(j7vk>A;vk`02b+Rt===^)b~ zro&7}n2s_XV>-@sg6SmFDW=m*XPC}1ont!Bbb;w2(ZXM^nvLk(+|Jy=+{xU<+|As>+{@g@ z+|N9Lc_Q;9=E=-cn5QyNW1h}DgLx+NEautFbC~Bc&tsm?ynuNj^CITO%uAS;GB0CZ z&b)$oCG#rg)y!*{*D|kTUeCONc_Z^C=FQAon71--W8TiZgL$-^#Fsfn3IXO_ z%)5u0cbWGv?`7V{yr201^TDC!_EGnahQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By z2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1J zhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kin zXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mk zz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2By z2#kinXb6mkz-S1JhQMeDjE2A{7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7 zfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c z4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C z(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R z7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7 zfzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c1*0J_8UmvsFd71*Aut*OqaiRF0;3@?8Umvs zFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF z0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71* zAut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@? z8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O zqaiRF0;3@?8UmvsFd71*Aut*OqaiRF0;3@?8UiCV1egyoA7(y++xN^znU66aXFh>j z#lXriFz_*e0P{%(2F5R=cr*+~)4*sN7)=ACX<#%BjHZFnG%%V5M$^D(8W>FjqiJ9? z4KRK|^p!zN3iT2?GP86$1mK0|Nu28v_HQ4+8^Z5Ca2a1Oo$O90LPm z3IhXU76Sug0RsbL83O}j4FdyX69WTd2Ll6R9|Hs96b1&ySquz}^BEWzmoqRhu4Q0g z+`+)WxQBs(@el(8;|T@^#&Zk|j8_;K7;iB!Fg{>lV0^~F!1#uNf$HUk5*Ap--mIRgW;Jp%)?8v_Hg9|Hq(6axcu3IhXkF#`j06$1lvBLf3- z2Ll81L%F4jN%FV#QD#*aV zD$c;bD$BsYs?5N^s>#5>s?Wf{YRbUCYR$mF>d3&r>dwHx>dU~u8qC1J8p*)G8qdJM zn##byn$5t#TFAh_TF$`0TFbz|+RVVf+R4Dc+RwniI+cNebv6S7>p}(w*5wQgtZNw< zST{2;uB z*c%xb*xMNx*n1fm*e5eEu+L;*V4u&xz`m4$fqgXt1N%k>2KMa?4D5Ru7}yUpFtDFw zU|>Jbz`%Z$fr0%t0|Wa*1_t)$3=HgV85r0$iTo+&cMJ?%fP_V%)r3W$-uzT&%nSjm4Sg{HUk64LIwto||iz*w4Vgag>39<1_;U$3+GPj_V8z9CsNQI36=FaJ*z-;CRo#!10xVf#Wv= z11BQ`11CEJ11B#71E(+p1E(Yd1E)L#1E(qj1E)3v1E(Pa1E)Cy1E(zm1E(_s1E(hg z1E)U&17|1$17|b?17{)w17|t|17|J+17|S<17{@z17|%017|Y>17{Zl1Lp(=2F~dW z44exY7&uomFmP^UVBp-&z`(hefr0Zd0|Vzt1_sXa3=EuC85lTkGca&IWMJTY&cML= zmVtruGXn$X9|i_4Rt5$xJ_ZIZaRvr11qKE#O$G)oBL)U8YX$}`7X}6{Uj_!QFa`#$ zcm@Wp3ofxc z*A)f^uDc8jT+bL7xZX1`aQ$Fl;AUiC;O1gr;1*_J;Fe)v;8tZ|;MQYc;5KJq;C5hO z;Pzx-;0|J7;ErZs;7(y+;Lc@W;4Wie;I3z2;O<~x;GW39z&(qBfqO9n1NRyR2JWp4 z4BYz|7`Tr!FmPXBVBo&Vz`*^8fr0xq0|WOL1_tiG3=BMM3=BN{3=BLH3=BMq3=BM4 z3=BNR3=BLr3=BN33=BMe3=BNs3=BL83=BM(3=BL)3=BNg3=BLi3=BNI3=BL|7#MhF zGBEJWXJFu2!oa|@ih+S=BLf4^9tH-U!wd{Orx+M`E;2Ci++|?kdBVWJ^OAvq=Q9HX z&u<0>UN!~>UI7LMUQq@HUI_*UUTFpfUL^(wUQGrDULytuUV8=xURMSNULOVq-Y^CR z-UJ2)-dqL--eLv@-Wmo5-ev{{-gX8C-aZBf-boA$yweyMcxN#%@Xljk;9bPPz`Klr zfp--H1MfNp2Hs5!47}SI7ao z<6~gp6JlWC6JucDlVV`tlVf1uQ(|D?Q)6J@(_&!Y(_>)ZGh$%iGh<-jvtnT2vtwZ3 zb7ElNb7NrO^I~A&^J8G(3u0j43u9p5i(+8li(_EmOJZQ)OJiW*%VJ>Q%VS{RD`H^a zD`Q~bt72f_t7Bl`YhqyFYhz&G>tbNw>tkTxo5aAtH;sXTZx#as-#i8ezC{cSe9IUZ z_*OA6@U3HD;M>H&z_*Qofo~TB1K&Od2EIcK41C8J82C;xFz}sYVBovNz`%Enfr0N9 z0|VbZ1_r)I3=DkF7#R3oF);AGV_@L>#K6G!je&vh7Xt&|KL!SVCI$w6HUyU=WaDU=UDcU=Yw_U=T28U=VO%U=Z+RU=RpmU=WCAU=T=Q zU=YY+U=S!^U=S!{U=XNbU=V0xU=ZkHU=WzVz#uSazA-Qe{9|AcWMNu`)MH=}G+|&6v}RxsbYfr-^kQHT3}RpqjACFAOk!XV%wk{= zEMi~~tYTmgY+_&#>|$ULoW#H&IE#Tna1jH8;3@_N!A%Sdg1Zj-o(Hlyo-TB_z(kw@F@lc;Y$n* z!nYV0gdZ_52)|-r5dOr#ApDDgL4=8cL4=EeK}3jwK}3pyK}3mxK}3szLBxoGLBxuI zLBxrHLBxxJK_rNQK_rTSK_rQRK_rWTL8OR*L8OX-L8OU+L8Oa;L1YpGgUBof29ZS! z3?i!-7(_NPFo^78U=TUPz#wvpfkEUF1B1vd1_qHw3=AT#7#Ku8F))byVqg$uVqg&E zVqg#zVqg%JVqg$eVqg%}Vqg$8Vqg%pVqg$;Vqg&UVqg#rVqg%BVqg$WVqg%>Vqg$0 zVqg%hVqg$$Vqg&MVqg%R#K0gri-AFO5d(whDh3A8O$-d8yBHWm4>2%^o?>7Sy~MyE zdW(TU^brGt=qm;W(N7EvqQ4jz#F!Wu#JCt3#Do|a#H1J)#FQ8q#IzU~#Ecji#H<(? z#GDuy#Jm_7#DW+Y#G)7&#F7{o#IhI|#EKXg#Htt=#F`iw#JU(5#3nH?h|OYP5L?8+ zAhwEuL2MHPgV-(x2C+j73}UAk7{o3yFo@k^U=Vx6z##UDfkEsO1B2Kv1_p5^1_p61 z1_p5<1_p5{1_p5@1_p601_p5>1_p5}1_p5_1_p621_tpU1_tpc1_tpY1_tpg1_tpW z1_tpe1_tpa1_tpi1_tp-3=HD47#PGCF))a)Vqg&8#K0iFi-AG>5CenwDFz1dOAHL+ zw-^}2A2Be9zhYnz|HQx`{)>S@f{B4af{TGcLWqGuLW+SwLWzMvLW_Yx!ia%E!is@G z!ij-F!i#}HB8Y)OB8q`QB8h=PB8!1RqKJV(qKbh*qKSb)qKkn+ViE&`#4H8|iA4+y z5~~;(BsMWHNbF)@kT}G^AaRO;LE;hvgTyTc28l-u3=*#x7$iP1Fi8AjV31^DV36cu zV2~7IV33qzV31T|V35>eV30IoV34$8V32fTV371;V2})9V33SsV315>V2~_iV34e4 zV32HJV32HPV36!(V33^7z#zGhfkARL1B2vN1_sIf3=EPdg999$T^U>>oP8X99YZ|* z{TKp6{X-xuzffN%*B}NLe;*&mAcg?fAZJ&<5C%uz0N0>kM?V(^#{mD}kRbm64_5{s zSGN#`AWwIX5C+HK5Z55jU~h&1pU_|iXMbN`M+T3`01sC`U0+YX&|n6)P#+(LV1FM^ zm(XAa7f*N35Hd+;e;ED#5D*a78vRp z4E7+%Nv=W8o{l~YK8_)teuI?+2dQhAYf!K&G-#bYgPcQs-F#dl7<~N08Qg;$!(0ce z>meZv@83qqYu;*A)Y=it_(KG`FSNp`8jqBe*U^nL9UM844!@hHtucLF2zaN8(t9y{ED>$)2(`RrfD1kc% zc?N_qAhR8vLql8{d_A3m{22l~eVl{*!x?-%U0i%z8Jt~RJbio|QIz{S20MrPfZPIh z8p1JQp+4@8K@6xA-1CDcN&1HRgm?z{csfH$gTdo4QeEQW8Ri)bD%wcZII4a$1V%$( zGz4f70!~4$VXh1={vizh?*4wR-VDLMjy^sNF8-lTpbEv`*9laSxq})|P!_13@&>hO zTqz@+BZEABd_0{QMj4}lFsM=jH1Q*KCXkp;p*}vYAq*b=L7q|mej$!N48fj$?mn(C z3aS~o#TMWi>>A?8;2Ij_5ALCaI|jJ~GlT{}XmGO(!gTQu_k*y&7KR2ega$A`Ees9} zbqsQ4@bq)@^z#gfWbpJ00X4}XZO~9ZP`kp>$JakNgu&Cr)eq9rMf5~ax*#6@p`f-r zq)qR_5E2^X=jy@`;vc}^Jx#Nu9w5 z+&}kqb@2@KWpHr}@@9b82ayd7b#w`G^b292I>X7w(b<~;7wO|4Nt1BoRJs6x^9erJ07@UF} zJ$-y!N9oaU7)=MG`Czmh7%c}z%YhM84#2t-sC@`XZvrL6I2nHY?0}lfO0}GhNz`()4!N3FMA+s46 QIR5|p|BZoxfgPa+06 Date: Tue, 6 Jan 2026 14:08:34 -0800 Subject: [PATCH 188/349] rename the selection search binding, unify into start_search action --- include/ghostty.h | 7 ---- macos/Sources/App/macOS/AppDelegate.swift | 2 +- macos/Sources/Ghostty/Ghostty.Action.swift | 12 ------- macos/Sources/Ghostty/Ghostty.App.swift | 35 ------------------- .../Surface View/SurfaceView_AppKit.swift | 2 +- src/Surface.zig | 6 ++-- src/apprt/action.zig | 17 --------- src/config/Config.zig | 2 +- src/input/Binding.zig | 8 +++-- src/input/command.zig | 8 ++--- 10 files changed, 15 insertions(+), 84 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 726b368e7..5fc3a7433 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -810,11 +810,6 @@ typedef struct { ssize_t selected; } ghostty_action_search_selected_s; -// apprt.action.SelectionForSearch -typedef struct { - const char* text; -} ghostty_action_selection_for_search_s; - // terminal.Scrollbar typedef struct { uint64_t total; @@ -883,7 +878,6 @@ typedef enum { GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, GHOSTTY_ACTION_COMMAND_FINISHED, GHOSTTY_ACTION_START_SEARCH, - GHOSTTY_ACTION_SELECTION_FOR_SEARCH, GHOSTTY_ACTION_END_SEARCH, GHOSTTY_ACTION_SEARCH_TOTAL, GHOSTTY_ACTION_SEARCH_SELECTED, @@ -925,7 +919,6 @@ typedef union { ghostty_action_progress_report_s progress_report; ghostty_action_command_finished_s command_finished; ghostty_action_start_search_s start_search; - ghostty_action_selection_for_search_s selection_for_search; ghostty_action_search_total_s search_total; ghostty_action_search_selected_s search_selected; ghostty_action_readonly_e readonly; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c365fb935..c0886607c 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -616,7 +616,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) - syncMenuShortcut(config, action: "selection_for_search", menuItem: self.menuSelectionForFind) + syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind) syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index c04c7d958..91f1491dd 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -128,18 +128,6 @@ extension Ghostty.Action { } } - struct SelectionForSearch { - let text: String? - - init(c: ghostty_action_selection_for_search_s) { - if let contentCString = c.text { - self.text = String(cString: contentCString) - } else { - self.text = nil - } - } - } - enum PromptTitle { case surface case tab diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 69788c194..4e9166168 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -621,9 +621,6 @@ extension Ghostty { case GHOSTTY_ACTION_START_SEARCH: startSearch(app, target: target, v: action.action.start_search) - case GHOSTTY_ACTION_SELECTION_FOR_SEARCH: - selectionForSearch(app, target: target, v: action.action.selection_for_search) - case GHOSTTY_ACTION_END_SEARCH: endSearch(app, target: target) @@ -1884,38 +1881,6 @@ extension Ghostty { } } - private static func selectionForSearch( - _ app: ghostty_app_t, - target: ghostty_target_s, - v: ghostty_action_selection_for_search_s - ) { - switch (target.tag) { - case GHOSTTY_TARGET_APP: - Ghostty.logger.warning("selection_for_search 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 selectionForSearch = Ghostty.Action.SelectionForSearch(c: v) - DispatchQueue.main.async { - if surfaceView.searchState != nil, let text = selectionForSearch.text { - NotificationCenter.default.post( - name: .ghosttySelectionForSearch, - object: surfaceView, - userInfo: [ - "text": text - ] - ) - } - } - - default: - assertionFailure() - } - } - private static func endSearch( _ app: ghostty_app_t, target: ghostty_target_s) { diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 1fc43ac82..a5ba62571 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1521,7 +1521,7 @@ extension Ghostty { @IBAction func selectionForFind(_ sender: Any?) { guard let surface = self.surface else { return } - let action = "selection_for_search" + let action = "search_selection" if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { AppDelegate.logger.warning("action failed action=\(action)") } diff --git a/src/Surface.zig b/src/Surface.zig index 68cf46045..1f3e4da8b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5163,12 +5163,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ); }, - .selection_for_search => { + .search_selection => { const selection = try self.selectionString(self.alloc) orelse return false; return try self.rt_app.performAction( .{ .surface = self }, - .selection_for_search, - .{ .text = selection }, + .start_search, + .{ .needle = selection }, ); }, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 7fdaabf08..f00ab16ef 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -316,9 +316,6 @@ pub const Action = union(Key) { /// Start the search overlay with an optional initial needle. start_search: StartSearch, - /// Input the selected text into the search field. - selection_for_search: SelectionForSearch, - /// End the search overlay, clearing the search state and hiding it. end_search, @@ -392,7 +389,6 @@ pub const Action = union(Key) { show_on_screen_keyboard, command_finished, start_search, - selection_for_search, end_search, search_total, search_selected, @@ -919,17 +915,4 @@ pub const SearchSelected = struct { } }; -pub const SelectionForSearch = struct { - text: [:0]const u8, - - // Sync with: ghostty_action_selection_for_search_s - pub const C = extern struct { - text: [*:0]const u8, - }; - pub fn cval(self: SelectionForSearch) C { - return .{ - .text = self.text.ptr, - }; - } -}; diff --git a/src/config/Config.zig b/src/config/Config.zig index 698831ec1..ef6132912 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6588,7 +6588,7 @@ pub const Keybinds = struct { try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'e' }, .mods = .{ .super = true } }, - .selection_for_search, + .search_selection, .{ .performable = true }, ); try self.set.putFlags( diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 0ef5208bc..08f5fdf7c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -368,8 +368,10 @@ pub const Action = union(enum) { /// If a previous search is active, it is replaced. search: []const u8, - /// Input the selected text into the search field. - selection_for_search, + /// Start a search for the current text selection. If there is no + /// selection, this does nothing. If a search is already active, this + /// changes the search terms. + search_selection, /// Navigate the search results. If there is no active search, this /// is not performed. @@ -1287,7 +1289,7 @@ pub const Action = union(enum) { .cursor_key, .search, .navigate_search, - .selection_for_search, + .search_selection, .start_search, .end_search, .reset, diff --git a/src/input/command.zig b/src/input/command.zig index 3fc7b29f6..d6d2b0247 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -189,10 +189,10 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Start a search if one isn't already active.", }}, - .selection_for_search => comptime &.{.{ - .action = .selection_for_search, - .title = "Selection for Search", - .description = "Input the selected text into the search field.", + .search_selection => comptime &.{.{ + .action = .search_selection, + .title = "Search Selection", + .description = "Start a search for the current text selection.", }}, .end_search => comptime &.{.{ -- 2.51.2 From f07d600e43d10b67b596b2739440d2679a8754b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Jan 2026 14:12:26 -0800 Subject: [PATCH 189/349] macos: start_search with needle changes needle --- macos/Sources/Ghostty/Ghostty.App.swift | 8 ++++++-- src/apprt/action.zig | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 4e9166168..959f78197 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1869,11 +1869,15 @@ extension Ghostty { let startSearch = Ghostty.Action.StartSearch(c: v) DispatchQueue.main.async { - if surfaceView.searchState != nil { - NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) + if let searchState = surfaceView.searchState { + if let needle = startSearch.needle, !needle.isEmpty { + searchState.needle = needle + } } else { surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch) } + + NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) } default: diff --git a/src/apprt/action.zig b/src/apprt/action.zig index f00ab16ef..4dd9d2994 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -313,7 +313,9 @@ pub const Action = union(Key) { /// A command has finished, command_finished: CommandFinished, - /// Start the search overlay with an optional initial needle. + /// Start the search overlay with an optional initial needle. If the + /// search is already active and the needle is non-empty, update the + /// current search needle and focus the search input. start_search: StartSearch, /// End the search overlay, clearing the search state and hiding it. -- 2.51.2 From 05a41c77726bea3b3bb6271d60b006ad4ec02733 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Jan 2026 14:20:30 -0800 Subject: [PATCH 190/349] macos: clean up menu --- macos/Sources/App/macOS/MainMenu.xib | 13 +++++++------ macos/Sources/Ghostty/Package.swift | 1 - .../Sources/Ghostty/Surface View/SurfaceView.swift | 7 ------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 248063f89..aa9aca952 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -263,12 +263,6 @@ - - - - - - @@ -288,6 +282,13 @@ + + + + + + + diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index dbe9c173b..aa62c16f7 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -406,7 +406,6 @@ extension Notification.Name { /// Focus the search field static let ghosttySearchFocus = Notification.Name("com.mitchellh.ghostty.searchFocus") - static let ghosttySelectionForSearch = Notification.Name("com.mitchellh.ghostty.selectionForSearch") } // NOTE: I am moving all of these to Notification.Name extensions over time. This diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index b3717d4c5..5609f36b7 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -479,13 +479,6 @@ extension Ghostty { isSearchFieldFocused = true } } - .onReceive(NotificationCenter.default.publisher(for: .ghosttySelectionForSearch)) { notification in - guard notification.object as? SurfaceView === surfaceView else { return } - if let userInfo = notification.userInfo, let text = userInfo["text"] as? String { - searchState.needle = text - // We do not focus the textfield after the action to match macOS behavior - } - } .background( GeometryReader { barGeo in Color.clear.onAppear { -- 2.51.2 From 3ba4f17f0df0b284016470ac7635a289b95aef5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Jan 2026 14:21:39 -0800 Subject: [PATCH 191/349] zig fmt --- src/apprt/action.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 4dd9d2994..78f4bef54 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -916,5 +916,3 @@ pub const SearchSelected = struct { }; } }; - - -- 2.51.2 From 7d0157e69a7b8082b4c56baa466304768f68cbc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 Jan 2026 14:28:54 -0800 Subject: [PATCH 192/349] macOS: add Cmd+J "Jump to Selection" menu item and default binding This matches other built-in macOS apps like Terminal, Notes, Safari. We already had the binding, just needed to create the menu. https://ampcode.com/threads/T-019b956a-f4e6-71b4-87fa-4162258d33ff --- macos/Sources/App/macOS/AppDelegate.swift | 2 ++ macos/Sources/App/macOS/MainMenu.xib | 6 ++++++ .../Features/Terminal/BaseTerminalController.swift | 4 ++++ .../Sources/Ghostty/Surface View/SurfaceView_AppKit.swift | 8 ++++++++ src/config/Config.zig | 6 ++++++ 5 files changed, 26 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c0886607c..2e62d033b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -47,6 +47,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuFindParent: NSMenuItem? @IBOutlet private var menuFind: NSMenuItem? @IBOutlet private var menuSelectionForFind: NSMenuItem? + @IBOutlet private var menuScrollToSelection: NSMenuItem? @IBOutlet private var menuFindNext: NSMenuItem? @IBOutlet private var menuFindPrevious: NSMenuItem? @IBOutlet private var menuHideFindBar: NSMenuItem? @@ -617,6 +618,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind) syncMenuShortcut(config, action: "search_selection", menuItem: self.menuSelectionForFind) + syncMenuShortcut(config, action: "scroll_to_selection", menuItem: self.menuScrollToSelection) syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext) syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index aa9aca952..e28344098 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -289,6 +289,12 @@ + + + + + + diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index a4e0da7ee..98175eabc 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1388,6 +1388,10 @@ class BaseTerminalController: NSWindowController, focusedSurface?.selectionForFind(sender) } + @IBAction func scrollToSelection(_ sender: Any) { + focusedSurface?.scrollToSelection(sender) + } + @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index a5ba62571..ca74c7815 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1527,6 +1527,14 @@ extension Ghostty { } } + @IBAction func scrollToSelection(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "scroll_to_selection" + if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" diff --git a/src/config/Config.zig b/src/config/Config.zig index ef6132912..1aacf78fd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6446,6 +6446,12 @@ pub const Keybinds = struct { .{ .key = .{ .physical = .page_down }, .mods = .{ .super = true } }, .{ .scroll_page_down = {} }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'j' }, .mods = .{ .super = true } }, + .{ .scroll_to_selection = {} }, + .{ .performable = true }, + ); // Semantic prompts try self.set.put( -- 2.51.2 From 61394d5213ab23a815df50b49b4c407cc4f107c2 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Wed, 7 Jan 2026 00:53:59 +0100 Subject: [PATCH 193/349] build: add -fPIC for musl targets in C++ dependencies --- .github/workflows/test.yml | 1 + pkg/dcimgui/build.zig | 2 +- pkg/freetype/build.zig | 2 +- pkg/glslang/build.zig | 2 +- pkg/highway/build.zig | 2 +- pkg/simdutf/build.zig | 2 +- pkg/spirv-cross/build.zig | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b91555f2f..45127e032 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -217,6 +217,7 @@ jobs: x86_64-macos, aarch64-linux, x86_64-linux, + x86_64-linux-musl, x86_64-windows, wasm32-freestanding, ] diff --git a/pkg/dcimgui/build.zig b/pkg/dcimgui/build.zig index 683f0be92..95c4af303 100644 --- a/pkg/dcimgui/build.zig +++ b/pkg/dcimgui/build.zig @@ -64,7 +64,7 @@ pub fn build(b: *std.Build) !void { "-DIMGUI_IMPL_API=extern\t\"C\"", }); } - if (target.result.os.tag == .freebsd) { + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); } diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index e0a041be7..ecb22cb6c 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -90,7 +90,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu "-fno-sanitize=undefined", }); - if (target.result.os.tag == .freebsd) { + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); } diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index da9a82e31..c41e05217 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -66,7 +66,7 @@ fn buildGlslang( "-fno-sanitize-trap=undefined", }); - if (target.result.os.tag == .freebsd) { + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); } diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index 04fe70853..3715baf4a 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -73,7 +73,7 @@ pub fn build(b: *std.Build) !void { "-fno-sanitize-trap=undefined", }); - if (target.result.os.tag == .freebsd) { + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); } diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index 2b157d1a9..3123cab21 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -32,7 +32,7 @@ pub fn build(b: *std.Build) !void { "-fno-sanitize-trap=undefined", }); - if (target.result.os.tag == .freebsd) { + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); } diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index 31af1974e..f85e74adf 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -74,7 +74,7 @@ fn buildSpirvCross( "-fno-sanitize-trap=undefined", }); - if (target.result.os.tag == .freebsd) { + if (target.result.os.tag == .freebsd or target.result.abi == .musl) { try flags.append(b.allocator, "-fPIC"); } -- 2.51.2 From 48dd6314dc1470805d319b8d015d69c6a74ff078 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 7 Jan 2026 09:57:16 -0500 Subject: [PATCH 194/349] Use Python syntax for easier debugging --- src/font/shaper/coretext.zig | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 6d17fc014..5d6cdb00e 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -681,11 +681,8 @@ pub const Shaper = struct { const codepoints = state.codepoints.items; var last_cluster: ?u32 = null; for (codepoints, 0..) |cp, i| { - if ((cp.cluster == cluster - 3 or - cp.cluster == cluster - 2 or - cp.cluster == cluster - 1 or - cp.cluster == cluster or - cp.cluster == cluster + 1) and + if ((cp.cluster >= cell_offset.cluster - 1 and + cp.cluster <= cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { if (last_cluster) |last| { @@ -696,17 +693,19 @@ pub const Shaper = struct { if (i == index) { try writer.writeAll("โ–ธ"); } - try writer.print("\\u{{{x}}}", .{cp.codepoint}); + // Using Python syntax for easier debugging + if (cp.codepoint > 0xFFFF) { + try writer.print("\\U{x:0>8}", .{cp.codepoint}); + } else { + try writer.print("\\u{x:0>4}", .{cp.codepoint}); + } last_cluster = cp.cluster; } } try writer.writeAll(" โ†’ "); for (codepoints) |cp| { - if ((cp.cluster == cluster - 3 or - cp.cluster == cluster - 2 or - cp.cluster == cluster - 1 or - cp.cluster == cluster or - cp.cluster == cluster + 1) and + if ((cp.cluster >= cell_offset.cluster - 1 and + cp.cluster <= cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); -- 2.51.2 From 13e125a05755a0ccf312006b89d72ce93d5d682b Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 7 Jan 2026 10:13:33 -0500 Subject: [PATCH 195/349] Add a big comment for the heuristic to detect ligatures. --- src/font/shaper/coretext.zig | 38 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 5d6cdb00e..c292104e2 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -389,8 +389,8 @@ pub const Shaper = struct { var cell_offset: CellOffset = .{}; // For debugging positions, turn this on: - var run_offset_y: f64 = 0.0; - var cell_offset_y: f64 = 0.0; + //var run_offset_y: f64 = 0.0; + //var cell_offset_y: f64 = 0.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 @@ -437,13 +437,13 @@ pub const Shaper = struct { if (cell_offset.cluster != cluster) { // We previously asserted that the new cluster is greater // than cell_offset.cluster, but for rtl text this is not - // true. We then used to break out of this block if cluster - // was less than cell_offset.cluster, but now this would - // fail to reset cell_offset.x and cell_offset.cluster and - // lead to incorrect shape.Cell `x` and `x_offset`. We - // don't have a test case for RTL, yet. + // true. We then used to break out of this block if the new + // cluster was less than cell_offset.cluster, but now this + // would fail to reset cell_offset.x and + // cell_offset.cluster and lead to incorrect shape.Cell `x` + // and `x_offset`. We don't have a test case for RTL, yet. - const is_codepoint_first_in_cluster = blk: { + const is_first_codepoint_in_cluster = blk: { var i = index; while (i > 0) { i -= 1; @@ -455,19 +455,33 @@ pub const Shaper = struct { } else break :blk true; }; - if (is_codepoint_first_in_cluster) { + // We need to reset the `cell_offset` at the start of a new + // cluster, but we do that conditionally if the codepoint + // `is_first_codepoint_in_cluster`, which is a heuristic to + // detect ligatures and avoid positioning glyphs that mark + // ligatures incorrectly. The idea is that if the first + // codepoint in a cluster doesn't appear in the stream, + // it's very likely that it combined with codepoints from a + // previous cluster into a ligature. Then, the subsequent + // codepoints are very likely marking glyphs that are + // placed relative to that ligature, so if we were to reset + // the `cell_offset` to align it with the grid, the + // positions would be off. (TBD if there are exceptions to + // this heuristic, but using the logging below seems to + // show it works well.) + if (is_first_codepoint_in_cluster) { cell_offset = .{ .cluster = cluster, .x = run_offset_x, }; // For debugging positions, turn this on: - cell_offset_y = run_offset_y; + //cell_offset_y = run_offset_y; } } // For debugging positions, turn this on: - try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); + //try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); const x_offset = position.x - cell_offset.x; @@ -483,7 +497,7 @@ pub const Shaper = struct { run_offset_x += advance.width; // For debugging positions, turn this on: - run_offset_y += advance.height; + //run_offset_y += advance.height; } } -- 2.51.2 From 8ebb8470b73521a1d3ce5aa5716d68b5c1ee3bd1 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Wed, 7 Jan 2026 10:46:01 -0500 Subject: [PATCH 196/349] Fix unsigned subtraction from zero --- src/font/shaper/coretext.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index c292104e2..b61c8a62d 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -695,7 +695,7 @@ pub const Shaper = struct { const codepoints = state.codepoints.items; var last_cluster: ?u32 = null; for (codepoints, 0..) |cp, i| { - if ((cp.cluster >= cell_offset.cluster - 1 and + if ((@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and cp.cluster <= cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { @@ -718,7 +718,7 @@ pub const Shaper = struct { } try writer.writeAll(" โ†’ "); for (codepoints) |cp| { - if ((cp.cluster >= cell_offset.cluster - 1 and + if ((@as(i32, @intCast(cp.cluster)) >= @as(i32, @intCast(cell_offset.cluster)) - 1 and cp.cluster <= cluster + 1) and cp.codepoint != 0 // Skip surrogate pair padding ) { -- 2.51.2 From 02fc0f502f8634c64037bb03d1d5c99e3272e2eb Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:56:12 +0100 Subject: [PATCH 197/349] macOS: rename function to avoid mutating misunderstanding --- macos/Sources/Features/Splits/SplitTree.swift | 24 +++++++++---------- .../Terminal/BaseTerminalController.swift | 22 ++++++++--------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 23b597591..2fb83e64c 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -121,10 +121,10 @@ extension SplitTree { /// Insert a new view at the given view point by creating a split in the given direction. /// This will always reset the zoomed state of the tree. - func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { + func inserting(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { guard let root else { throw SplitError.viewNotFound } return .init( - root: try root.insert(view: view, at: at, direction: direction), + root: try root.inserting(view: view, at: at, direction: direction), zoomed: nil) } /// Find a node containing a view with the specified ID. @@ -137,7 +137,7 @@ extension SplitTree { /// Remove a node from the tree. If the node being removed is part of a split, /// the sibling node takes the place of the parent split. - func remove(_ target: Node) -> Self { + func removing(_ target: Node) -> Self { guard let root else { return self } // If we're removing the root itself, return an empty tree @@ -155,7 +155,7 @@ extension SplitTree { } /// Replace a node in the tree with a new node. - func replace(node: Node, with newNode: Node) throws -> Self { + func replacing(node: Node, with newNode: Node) throws -> Self { guard let root else { throw SplitError.viewNotFound } // Get the path to the node we want to replace @@ -164,7 +164,7 @@ extension SplitTree { } // Replace the node - let newRoot = try root.replaceNode(at: path, with: newNode) + let newRoot = try root.replacingNode(at: path, with: newNode) // Update zoomed if it was the replaced node let newZoomed = (zoomed == node) ? newNode : zoomed @@ -232,7 +232,7 @@ extension SplitTree { /// Equalize all splits in the tree so that each split's ratio is based on the /// relative weight (number of leaves) of its children. - func equalize() -> Self { + func equalized() -> Self { guard let root else { return self } let newRoot = root.equalize() return .init(root: newRoot, zoomed: zoomed) @@ -255,7 +255,7 @@ extension SplitTree { /// - bounds: The bounds used to construct the spatial tree representation /// - Returns: A new SplitTree with the adjusted split ratios /// - Throws: SplitError.viewNotFound if the node is not found in the tree or no suitable parent split exists - func resize(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self { + func resizing(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self { guard let root else { throw SplitError.viewNotFound } // Find the path to the target node @@ -327,7 +327,7 @@ extension SplitTree { ) // Replace the split node with the new one - let newRoot = try root.replaceNode(at: splitPath, with: .split(newSplit)) + let newRoot = try root.replacingNode(at: splitPath, with: .split(newSplit)) return .init(root: newRoot, zoomed: nil) } @@ -508,7 +508,7 @@ extension SplitTree.Node { /// /// - Note: If the existing view (`at`) is not found in the tree, this method does nothing. We should /// maybe throw instead but at the moment we just do nothing. - func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { + func inserting(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { // Get the path to our insertion point. If it doesn't exist we do // nothing. guard let path = path(to: .leaf(view: at)) else { @@ -544,11 +544,11 @@ extension SplitTree.Node { )) // Replace the node at the path with the new split - return try replaceNode(at: path, with: newSplit) + return try replacingNode(at: path, with: newSplit) } /// Helper function to replace a node at the given path from the root - func replaceNode(at path: Path, with newNode: Self) throws -> Self { + func replacingNode(at path: Path, with newNode: Self) throws -> Self { // If path is empty, replace the root if path.isEmpty { return newNode @@ -635,7 +635,7 @@ extension SplitTree.Node { /// Resize a split node to the specified ratio. /// For leaf nodes, this returns the node unchanged. /// For split nodes, this creates a new split with the updated ratio. - func resize(to ratio: Double) -> Self { + func resizing(to ratio: Double) -> Self { switch self { case .leaf: // Leaf nodes don't have a ratio to resize diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 98175eabc..ecf72dd34 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -240,7 +240,7 @@ class BaseTerminalController: NSWindowController, // Do the split let newTree: SplitTree do { - newTree = try surfaceTree.insert( + newTree = try surfaceTree.inserting( view: newView, at: oldView, direction: direction) @@ -450,7 +450,7 @@ class BaseTerminalController: NSWindowController, } replaceSurfaceTree( - surfaceTree.remove(node), + surfaceTree.removing(node), moveFocusTo: nextFocus, moveFocusFrom: focusedSurface, undoAction: "Close Terminal" @@ -614,7 +614,7 @@ class BaseTerminalController: NSWindowController, guard surfaceTree.contains(target) else { return } // Equalize the splits - surfaceTree = surfaceTree.equalize() + surfaceTree = surfaceTree.equalized() } @objc private func ghosttyDidFocusSplit(_ notification: Notification) { @@ -704,7 +704,7 @@ class BaseTerminalController: NSWindowController, // Perform the resize using the new SplitTree resize method do { - surfaceTree = try surfaceTree.resize(node: targetNode, by: amount, in: spatialDirection, with: bounds) + surfaceTree = try surfaceTree.resizing(node: targetNode, by: amount, in: spatialDirection, with: bounds) } catch { Ghostty.logger.warning("failed to resize split: \(error)") } @@ -742,7 +742,7 @@ class BaseTerminalController: NSWindowController, } // Remove the surface from our tree - let removedTree = surfaceTree.remove(targetNode) + let removedTree = surfaceTree.removing(targetNode) // Create a new tree with the dragged surface and open a new window let newTree = SplitTree(view: target) @@ -868,9 +868,9 @@ class BaseTerminalController: NSWindowController, } private func splitDidResize(node: SplitTree.Node, to newRatio: Double) { - let resizedNode = node.resize(to: newRatio) + let resizedNode = node.resizing(to: newRatio) do { - surfaceTree = try surfaceTree.replace(node: node, with: resizedNode) + surfaceTree = try surfaceTree.replacing(node: node, with: resizedNode) } catch { Ghostty.logger.warning("failed to replace node during split resize: \(error)") } @@ -892,10 +892,10 @@ class BaseTerminalController: NSWindowController, // Check if source is in our tree if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move - let treeWithoutSource = surfaceTree.remove(sourceNode) + let treeWithoutSource = surfaceTree.removing(sourceNode) let newTree: SplitTree do { - newTree = try treeWithoutSource.insert(view: source, at: destination, direction: direction) + newTree = try treeWithoutSource.inserting(view: source, at: destination, direction: direction) } catch { Ghostty.logger.warning("failed to insert surface during drop: \(error)") return @@ -930,10 +930,10 @@ class BaseTerminalController: NSWindowController, // Remove from source controller's tree and add it to our tree. // We do this first because if there is an error then we can // abort. - let sourceTreeWithoutNode = sourceController.surfaceTree.remove(sourceNode) + let sourceTreeWithoutNode = sourceController.surfaceTree.removing(sourceNode) let newTree: SplitTree do { - newTree = try surfaceTree.insert(view: source, at: destination, direction: direction) + newTree = try surfaceTree.inserting(view: source, at: destination, direction: direction) } catch { Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") return -- 2.51.2 From a265462aa65976e5d858ad13ca6fba7bf155be28 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:57:15 +0100 Subject: [PATCH 198/349] macOS: moving a focused split to another tab should also update the previous tab --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ecf72dd34..71daafaeb 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -961,10 +961,9 @@ class BaseTerminalController: NSWindowController, confirmUndo: false) } } else { - // The source isn't empty so we can do a simple replace which will handle + // The source isn't empty so we can do a simple remove which will handle // the undo properly. - sourceController.replaceSurfaceTree( - sourceTreeWithoutNode) + sourceController.removeSurfaceNode(sourceNode) } // Add in the surface to our tree -- 2.51.2 From 323d362bc18003ef94a1b69ce27b9833573a2aa1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Jan 2026 09:33:32 -0800 Subject: [PATCH 199/349] macos: dragging last window out of quick terminal works --- .../Terminal/BaseTerminalController.swift | 24 +------ .../Terminal/TerminalController.swift | 64 ++++++++++++------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 71daafaeb..88d8e39d8 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -457,7 +457,7 @@ class BaseTerminalController: NSWindowController, ) } - private func replaceSurfaceTree( + func replaceSurfaceTree( _ newTree: SplitTree, moveFocusTo newView: Ghostty.SurfaceView? = nil, moveFocusFrom oldView: Ghostty.SurfaceView? = nil, @@ -930,7 +930,6 @@ class BaseTerminalController: NSWindowController, // Remove from source controller's tree and add it to our tree. // We do this first because if there is an error then we can // abort. - let sourceTreeWithoutNode = sourceController.surfaceTree.removing(sourceNode) let newTree: SplitTree do { newTree = try surfaceTree.inserting(view: source, at: destination, direction: direction) @@ -946,25 +945,8 @@ class BaseTerminalController: NSWindowController, undoManager?.endUndoGrouping() } - if sourceTreeWithoutNode.isEmpty { - // If our source tree is becoming empty, then we're closing this terminal. - // We need to handle this carefully to get undo to work properly. If the - // controller is a TerminalController this is easy because it has a way - // to do this. - if let c = sourceController as? TerminalController { - c.closeTabImmediately() - } else { - // Not a TerminalController so we always undo into a new window. - _ = TerminalController.newWindow( - sourceController.ghostty, - tree: sourceController.surfaceTree, - confirmUndo: false) - } - } else { - // The source isn't empty so we can do a simple remove which will handle - // the undo properly. - sourceController.removeSurfaceNode(sourceNode) - } + // Remove the node from the source. + sourceController.removeSurfaceNode(sourceNode) // Add in the surface to our tree replaceSurfaceTree( diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index abaedbe41..c7f9fe086 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -8,16 +8,16 @@ import GhosttyKit class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" - + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config - + // If we have no window decorations, there's no reason to do anything but // the default titlebar (because there will be no titlebar). if !config.windowDecorations { return defaultValue } - + let nib = switch config.macosTitlebarStyle { case "native": "Terminal" case "hidden": "TerminalHiddenTitlebar" @@ -34,33 +34,33 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr #endif default: defaultValue } - + return nib } - + /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail /// early if we don't care. private var tabListenForFrame: Bool = false - + /// This is the hash value of the last tabGroup.windows array. We use this to detect order /// changes in the list. private var tabWindowsHash: Int = 0 - + /// This is set to false by init if the window managed by this controller should not be restorable. /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - - + + /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] - + /// This will be set to the initial frame of the window from the xib on load. private var initialFrame: NSRect? = nil - + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: SplitTree? = nil, @@ -72,12 +72,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // as the script. We may want to revisit this behavior when we have scrollback // restoration. self.restorable = (base?.command ?? "") == "" - + // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - + super.init(ghostty, baseConfig: base, surfaceTree: tree) - + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -134,36 +134,56 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr object: nil ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } - + // MARK: Base Controller Overrides - + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) - + // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() - + // Update our zoom state if let window = window as? TerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } - + // If our surface tree is now nil then we close our window. if (to.isEmpty) { self.window?.close() } } + + override func replaceSurfaceTree( + _ newTree: SplitTree, + moveFocusTo newView: Ghostty.SurfaceView? = nil, + moveFocusFrom oldView: Ghostty.SurfaceView? = nil, + undoAction: String? = nil + ) { + // We have a special case if our tree is empty to close our tab immediately. + // This makes it so that undo is handled properly. + if newTree.isEmpty { + closeTabImmediately() + return + } + + super.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: oldView, + undoAction: undoAction) + } // MARK: Terminal Creation -- 2.51.2 From 5a042570c84c759d1ab653d2376ddc53297fdf0f Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Thu, 1 Jan 2026 17:40:36 -0500 Subject: [PATCH 200/349] feat: select entire URL on double-click When double-clicking text, first check if the position is part of a URL using the default URL regex pattern. If a URL is detected, select the entire URL instead of just the word. This follows the feedback from PR #2324 to modify the selection behavior rather than introducing a separate selectLink function. The implementation uses the existing URL regex from config/url.zig which already handles various URL schemes (http, https, ftp, ssh, etc.) and file paths. The URL detection runs before the normal word selection, falling back to selectWord if no URL is found at the clicked position. --- src/Surface.zig | 44 ++++++++++--- src/terminal/StringMap.zig | 128 +++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 1f3e4da8b..11fa42e35 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4141,9 +4141,18 @@ pub fn mouseButtonCallback( } }, - // Double click, select the word under our mouse + // Double click, select the word under our mouse. + // First try to detect if we're clicking on a URL to select the entire URL. 2 => { - const sel_ = self.io.terminal.screens.active.selectWord(pin.*); + const sel_ = sel: { + // Try link detection without requiring modifier keys + const link_result = self.linkAtPin(pin.*, null) catch null; + if (link_result) |result| { + // Only select URLs (links with .open action) + if (result[0] == .open) break :sel result[1]; + } + break :sel self.io.terminal.screens.active.selectWord(pin.*); + }; if (sel_) |sel| { try self.io.terminal.screens.active.select(sel); try self.queueRender(); @@ -4364,11 +4373,26 @@ fn linkAtPos( return .{ ._open_osc8, sel }; } - // If we have no OSC8 links then we fallback to regex-based URL detection. - // If we have no configured links we can save a lot of work going forward. + // Fall back to regex-based link detection + return self.linkAtPin(mouse_pin, mouse_mods); +} + +/// Core link detection at a pin position using regex patterns. +/// When mouse_mods is null, skips highlight/modifier checks (for double-click). +/// +/// Requires the renderer state mutex is held. +fn linkAtPin( + self: *Surface, + mouse_pin: terminal.Pin, + mouse_mods: ?input.Mods, +) !?struct { + input.Link.Action, + terminal.Selection, +} { + const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; + if (self.config.links.len == 0) return null; - // Get the line we're hovering over. const line = screen.selectLine(.{ .pin = mouse_pin, .whitespace = null, @@ -4383,11 +4407,13 @@ fn linkAtPos( })); defer strmap.deinit(self.alloc); - // Go through each link and see if we clicked it for (self.config.links) |link| { - switch (link.highlight) { - .always, .hover => {}, - .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, + // Skip highlight/mods check when mouse_mods is null (double-click mode) + if (mouse_mods) |mods| { + switch (link.highlight) { + .always, .hover => {}, + .always_mods, .hover_mods => |v| if (!v.equal(mods)) continue, + } } var it = strmap.searchIterator(link.regex); diff --git a/src/terminal/StringMap.zig b/src/terminal/StringMap.zig index 4ac47eeab..f7d88d1c8 100644 --- a/src/terminal/StringMap.zig +++ b/src/terminal/StringMap.zig @@ -147,3 +147,131 @@ test "StringMap searchIterator" { try testing.expect(try it.next() == null); } + +test "StringMap searchIterator URL detection" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const url = @import("../config/url.zig"); + + // Initialize URL regex + try oni.testing.ensureInit(); + var re = try oni.Regex.init( + url.regex, + .{}, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Initialize our screen with text containing a URL + var s = try Screen.init(alloc, .{ .cols = 40, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello https://example.com/path world"); + + // Get the line + const line = s.selectLine(.{ + .pin = s.pages.pin(.{ .active = .{ + .x = 10, + .y = 0, + } }).?, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + defer map.deinit(alloc); + + // Search for URL match + var it = map.searchIterator(re); + { + var match = (try it.next()).?; + defer match.deinit(); + + const sel = match.selection(); + // URL should start at x=6 ("https://example.com/path" starts after "hello ") + try testing.expectEqual(point.Point{ .screen = .{ + .x = 6, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + // URL should end at x=29 (end of "/path") + try testing.expectEqual(point.Point{ .screen = .{ + .x = 29, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + } + + try testing.expect(try it.next() == null); +} + +test "StringMap searchIterator URL with click position" { + if (comptime !build_options.oniguruma) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const url = @import("../config/url.zig"); + + // Initialize URL regex + try oni.testing.ensureInit(); + var re = try oni.Regex.init( + url.regex, + .{}, + oni.Encoding.utf8, + oni.Syntax.default, + null, + ); + defer re.deinit(); + + // Initialize our screen with text containing a URL + var s = try Screen.init(alloc, .{ .cols = 40, .rows = 5, .max_scrollback = 0 }); + defer s.deinit(); + try s.testWriteString("hello https://example.com world"); + + // Simulate clicking on "example" (x=14) + const click_pin = s.pages.pin(.{ .active = .{ + .x = 14, + .y = 0, + } }).?; + + // Get the line + const line = s.selectLine(.{ + .pin = click_pin, + }).?; + var map: StringMap = undefined; + const sel_str = try s.selectionString(alloc, .{ + .sel = line, + .trim = false, + .map = &map, + }); + alloc.free(sel_str); + defer map.deinit(alloc); + + // Search for URL match and verify click position is within URL + var it = map.searchIterator(re); + var found_url = false; + while (true) { + var match = (try it.next()) orelse break; + defer match.deinit(); + + const sel = match.selection(); + if (sel.contains(&s, click_pin)) { + found_url = true; + // Verify URL bounds + try testing.expectEqual(point.Point{ .screen = .{ + .x = 6, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.start()).?); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 24, + .y = 0, + } }, s.pages.pointFromPin(.screen, sel.end()).?); + break; + } + } + try testing.expect(found_url); +} -- 2.51.2 From 66593157602ee440e4b19734ccabeb8349b3cdd6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Jan 2026 10:01:45 -0800 Subject: [PATCH 201/349] tweaks to link detection --- src/Surface.zig | 83 +++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 11fa42e35..7e9a307e5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -327,7 +327,7 @@ const DerivedConfig = struct { window_width: u32, title: ?[:0]const u8, title_report: bool, - links: []Link, + links: []DerivedConfig.Link, link_previews: configpkg.LinkPreviews, scroll_to_bottom: configpkg.Config.ScrollToBottom, notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, @@ -347,7 +347,7 @@ const DerivedConfig = struct { // Build all of our links const links = links: { - var links: std.ArrayList(Link) = .empty; + var links: std.ArrayList(DerivedConfig.Link) = .empty; defer links.deinit(alloc); for (config.link.links.items) |link| { var regex = try link.oniRegex(); @@ -1599,10 +1599,10 @@ fn mouseRefreshLinks( } const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false }; - switch (link[0]) { + switch (link.action) { .open => { const str = try self.io.terminal.screens.active.selectionString(alloc, .{ - .sel = link[1], + .sel = link.selection, .trim = false, }); break :link .{ @@ -1613,7 +1613,7 @@ fn mouseRefreshLinks( ._open_osc8 => { // Show the URL in the status bar - const pin = link[1].start(); + const pin = link.selection.start(); const uri = self.osc8URI(pin) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); break :link .{ null, false }; @@ -4146,11 +4146,17 @@ pub fn mouseButtonCallback( 2 => { const sel_ = sel: { // Try link detection without requiring modifier keys - const link_result = self.linkAtPin(pin.*, null) catch null; - if (link_result) |result| { - // Only select URLs (links with .open action) - if (result[0] == .open) break :sel result[1]; + if (self.linkAtPin( + pin.*, + null, + )) |result_| { + if (result_) |result| { + break :sel result.selection; + } + } else |_| { + // Ignore any errors, likely regex errors. } + break :sel self.io.terminal.screens.active.selectWord(pin.*); }; if (sel_) |sel| { @@ -4340,16 +4346,18 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void { } } +const Link = struct { + action: input.Link.Action, + selection: terminal.Selection, +}; + /// Returns the link at the given cursor position, if any. /// /// Requires the renderer mutex is held. fn linkAtPos( self: *Surface, pos: apprt.CursorPos, -) !?struct { - input.Link.Action, - terminal.Selection, -} { +) !?Link { // Convert our cursor position to a screen point. const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; const mouse_pin: terminal.Pin = mouse_pin: { @@ -4370,29 +4378,27 @@ fn linkAtPos( const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; const sel = terminal.Selection.init(mouse_pin, mouse_pin, false); - return .{ ._open_osc8, sel }; + return .{ .action = ._open_osc8, .selection = sel }; } - // Fall back to regex-based link detection - return self.linkAtPin(mouse_pin, mouse_mods); + // Fall back to configured links + return try self.linkAtPin(mouse_pin, mouse_mods); } -/// Core link detection at a pin position using regex patterns. -/// When mouse_mods is null, skips highlight/modifier checks (for double-click). +/// Detects if a link is present at the given pin. +/// +/// If mouse mods is null then mouse mod requirements are ignored (all +/// configured links are checked). /// /// Requires the renderer state mutex is held. fn linkAtPin( self: *Surface, mouse_pin: terminal.Pin, mouse_mods: ?input.Mods, -) !?struct { - input.Link.Action, - terminal.Selection, -} { - const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; - +) !?Link { if (self.config.links.len == 0) return null; + const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; const line = screen.selectLine(.{ .pin = mouse_pin, .whitespace = null, @@ -4409,12 +4415,10 @@ fn linkAtPin( for (self.config.links) |link| { // Skip highlight/mods check when mouse_mods is null (double-click mode) - if (mouse_mods) |mods| { - switch (link.highlight) { - .always, .hover => {}, - .always_mods, .hover_mods => |v| if (!v.equal(mods)) continue, - } - } + if (mouse_mods) |mods| switch (link.highlight) { + .always, .hover => {}, + .always_mods, .hover_mods => |v| if (!v.equal(mods)) continue, + }; var it = strmap.searchIterator(link.regex); while (true) { @@ -4422,7 +4426,10 @@ fn linkAtPin( defer match.deinit(); const sel = match.selection(); if (!sel.contains(screen, mouse_pin)) continue; - return .{ link.action, sel }; + return .{ + .action = link.action, + .selection = sel, + }; } } @@ -4453,11 +4460,11 @@ fn mouseModsWithCapture(self: *Surface, mods: input.Mods) input.Mods { /// /// Requires the renderer state mutex is held. fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { - const action, const sel = try self.linkAtPos(pos) orelse return false; - switch (action) { + const link = try self.linkAtPos(pos) orelse return false; + switch (link.action) { .open => { const str = try self.io.terminal.screens.active.selectionString(self.alloc, .{ - .sel = sel, + .sel = link.selection, .trim = false, }); defer self.alloc.free(str); @@ -4470,7 +4477,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { }, ._open_osc8 => { - const uri = self.osc8URI(sel.start()) orelse { + const uri = self.osc8URI(link.selection.start()) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; @@ -5313,11 +5320,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); if (try self.linkAtPos(pos)) |link_info| { - const url_text = switch (link_info[0]) { + const url_text = switch (link_info.action) { .open => url_text: { // For regex links, get the text from selection break :url_text (self.io.terminal.screens.active.selectionString(self.alloc, .{ - .sel = link_info[1], + .sel = link_info.selection, .trim = self.config.clipboard_trim_trailing_spaces, })) catch |err| { log.err("error reading url string err={}", .{err}); @@ -5327,7 +5334,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool ._open_osc8 => url_text: { // For OSC8 links, get the URI directly from hyperlink data - const uri = self.osc8URI(link_info[1].start()) orelse { + const uri = self.osc8URI(link_info.selection.start()) orelse { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; -- 2.51.2 From 795de7938d1087eaa324a4e4829a7039714e494d Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 24 Dec 2025 12:34:39 -0500 Subject: [PATCH 202/349] shell-integration: better shell detection and setup Command-based shell detection has been extracted to its own function (detectShell), which is nicer for testing. It now uses argIterator to determine the command's executable, rather than the previous string operations, which allows us to handle command strings containing quotes and spaces. Also, our shell-specific setup functions now use a consistent signature, which simplifies the calling code quite a bit. --- src/termio/shell_integration.zig | 197 ++++++++++++++++--------------- 1 file changed, 102 insertions(+), 95 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index b803d1897..b9477e090 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -45,95 +45,37 @@ pub fn setup( env: *EnvMap, force_shell: ?Shell, ) !?ShellIntegration { - const exe = if (force_shell) |shell| switch (shell) { - .bash => "bash", - .elvish => "elvish", - .fish => "fish", - .zsh => "zsh", - } else switch (command) { - .direct => |v| std.fs.path.basename(v[0]), - .shell => |v| exe: { - // Shell strings can include spaces so we want to only - // look up to the space if it exists. No shell that we integrate - // has spaces. - const idx = std.mem.indexOfScalar(u8, v, ' ') orelse v.len; - break :exe std.fs.path.basename(v[0..idx]); - }, - }; - - const result = try setupShell( - alloc_arena, - resource_dir, - command, - env, - exe, - ); - - return result; -} - -fn setupShell( - alloc_arena: Allocator, - resource_dir: []const u8, - command: config.Command, - env: *EnvMap, - exe: []const u8, -) !?ShellIntegration { - if (std.mem.eql(u8, "bash", exe)) { - // Apple distributes their own patched version of Bash 3.2 - // on macOS that disables the ENV-based POSIX startup path. - // This means we're unable to perform our automatic shell - // integration sequence in this specific environment. - // - // If we're running "/bin/bash" on Darwin, we can assume - // we're using Apple's Bash because /bin is non-writable - // on modern macOS due to System Integrity Protection. - if (comptime builtin.target.os.tag.isDarwin()) { - if (std.mem.eql(u8, "/bin/bash", switch (command) { - .direct => |v| v[0], - .shell => |v| v, - })) { - return null; - } - } + const shell: Shell = force_shell orelse + try detectShell(alloc_arena, command) orelse + return null; - const new_command = try setupBash( + const new_command: config.Command = switch (shell) { + .bash => try setupBash( alloc_arena, command, resource_dir, env, - ) orelse return null; - return .{ - .shell = .bash, - .command = new_command, - }; - } - - if (std.mem.eql(u8, "elvish", exe)) { - if (!try setupXdgDataDirs(alloc_arena, resource_dir, env)) return null; - return .{ - .shell = .elvish, - .command = try command.clone(alloc_arena), - }; - } + ), - if (std.mem.eql(u8, "fish", exe)) { - if (!try setupXdgDataDirs(alloc_arena, resource_dir, env)) return null; - return .{ - .shell = .fish, - .command = try command.clone(alloc_arena), - }; - } + .elvish, .fish => try setupXdgDataDirs( + alloc_arena, + command, + resource_dir, + env, + ), - if (std.mem.eql(u8, "zsh", exe)) { - if (!try setupZsh(resource_dir, env)) return null; - return .{ - .shell = .zsh, - .command = try command.clone(alloc_arena), - }; - } + .zsh => try setupZsh( + alloc_arena, + command, + resource_dir, + env, + ), + } orelse return null; - return null; + return .{ + .shell = shell, + .command = new_command, + }; } test "force shell" { @@ -185,6 +127,55 @@ test "shell integration failure" { try testing.expectEqual(0, env.count()); } +fn detectShell(alloc: Allocator, command: config.Command) !?Shell { + var arg_iter = try command.argIterator(alloc); + defer arg_iter.deinit(); + + const arg0 = arg_iter.next() orelse return null; + const exe = std.fs.path.basename(arg0); + + if (std.mem.eql(u8, "bash", exe)) { + // Apple distributes their own patched version of Bash 3.2 + // on macOS that disables the ENV-based POSIX startup path. + // This means we're unable to perform our automatic shell + // integration sequence in this specific environment. + // + // If we're running "/bin/bash" on Darwin, we can assume + // we're using Apple's Bash because /bin is non-writable + // on modern macOS due to System Integrity Protection. + if (comptime builtin.target.os.tag.isDarwin()) { + if (std.mem.eql(u8, "/bin/bash", arg0)) { + return null; + } + } + return .bash; + } + + if (std.mem.eql(u8, "elvish", exe)) return .elvish; + if (std.mem.eql(u8, "fish", exe)) return .fish; + if (std.mem.eql(u8, "zsh", exe)) return .zsh; + + return null; +} + +test detectShell { + const testing = std.testing; + const alloc = testing.allocator; + + try testing.expect(try detectShell(alloc, .{ .shell = "sh" }) == null); + try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash" })); + try testing.expectEqual(.elvish, try detectShell(alloc, .{ .shell = "elvish" })); + try testing.expectEqual(.fish, try detectShell(alloc, .{ .shell = "fish" })); + try testing.expectEqual(.zsh, try detectShell(alloc, .{ .shell = "zsh" })); + + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expect(try detectShell(alloc, .{ .shell = "/bin/bash" }) == null); + } + + try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash -c 'command'" })); + try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "\"/a b/bash\"" })); +} + /// Set up the shell integration features environment variable. pub fn setupFeatures( env: *EnvMap, @@ -603,10 +594,11 @@ test "bash: missing resources" { /// so that the shell can refer to it and safely remove this directory /// from `XDG_DATA_DIRS` when integration is complete. fn setupXdgDataDirs( - alloc_arena: Allocator, + alloc: Allocator, + command: config.Command, resource_dir: []const u8, env: *EnvMap, -) !bool { +) !?config.Command { var path_buf: [std.fs.max_path_bytes]u8 = undefined; // Get our path to the shell integration directory. @@ -617,7 +609,7 @@ fn setupXdgDataDirs( ); var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { log.warn("unable to open {s}: {}", .{ integ_path, err }); - return false; + return null; }; integ_dir.close(); @@ -631,7 +623,7 @@ fn setupXdgDataDirs( // 4K is a reasonable size for this for most cases. However, env // vars can be significantly larger so if we have to we fall // back to a heap allocated value. - var stack_alloc_state = std.heap.stackFallback(4096, alloc_arena); + var stack_alloc_state = std.heap.stackFallback(4096, alloc); const stack_alloc = stack_alloc_state.get(); // If no XDG_DATA_DIRS set use the default value as specified. @@ -648,7 +640,7 @@ fn setupXdgDataDirs( ), ); - return true; + return try command.clone(alloc); } test "xdg: empty XDG_DATA_DIRS" { @@ -664,7 +656,8 @@ test "xdg: empty XDG_DATA_DIRS" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); + const command = try setupXdgDataDirs(alloc, .{ .shell = "xdg" }, res.path, &env); + try testing.expectEqualStrings("xdg", command.?.shell); var path_buf: [std.fs.max_path_bytes]u8 = undefined; try testing.expectEqualStrings( @@ -691,7 +684,9 @@ test "xdg: existing XDG_DATA_DIRS" { defer env.deinit(); try env.put("XDG_DATA_DIRS", "/opt/share"); - try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); + + const command = try setupXdgDataDirs(alloc, .{ .shell = "xdg" }, res.path, &env); + try testing.expectEqualStrings("xdg", command.?.shell); var path_buf: [std.fs.max_path_bytes]u8 = undefined; try testing.expectEqualStrings( @@ -719,7 +714,7 @@ test "xdg: missing resources" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(!try setupXdgDataDirs(alloc, resources_dir, &env)); + try testing.expect(try setupXdgDataDirs(alloc, .{ .shell = "xdg" }, resources_dir, &env) == null); try testing.expectEqual(0, env.count()); } @@ -727,9 +722,11 @@ test "xdg: missing resources" { /// ZDOTDIR to our resources dir so that zsh will load our config. This /// config then loads the true user config. fn setupZsh( + alloc: Allocator, + command: config.Command, resource_dir: []const u8, env: *EnvMap, -) !bool { +) !?config.Command { // Preserve an existing ZDOTDIR value. We're about to overwrite it. if (env.get("ZDOTDIR")) |old| { try env.put("GHOSTTY_ZSH_ZDOTDIR", old); @@ -744,24 +741,29 @@ fn setupZsh( ); var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { log.warn("unable to open {s}: {}", .{ integ_path, err }); - return false; + return null; }; integ_dir.close(); try env.put("ZDOTDIR", integ_path); - return true; + return try command.clone(alloc); } test "zsh" { const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(testing.allocator, .zsh); defer res.deinit(); var env = EnvMap.init(testing.allocator); defer env.deinit(); - try testing.expect(try setupZsh(res.path, &env)); + const command = try setupZsh(alloc, .{ .shell = "zsh" }, res.path, &env); + try testing.expectEqualStrings("zsh", command.?.shell); try testing.expectEqualStrings(res.shell_path, env.get("ZDOTDIR").?); try testing.expect(env.get("GHOSTTY_ZSH_ZDOTDIR") == null); } @@ -769,6 +771,10 @@ test "zsh" { test "zsh: ZDOTDIR" { const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + var res: TmpResourcesDir = try .init(testing.allocator, .zsh); defer res.deinit(); @@ -777,7 +783,8 @@ test "zsh: ZDOTDIR" { try env.put("ZDOTDIR", "$HOME/.config/zsh"); - try testing.expect(try setupZsh(res.path, &env)); + const command = try setupZsh(alloc, .{ .shell = "zsh" }, res.path, &env); + try testing.expectEqualStrings("zsh", command.?.shell); try testing.expectEqualStrings(res.shell_path, env.get("ZDOTDIR").?); try testing.expectEqualStrings("$HOME/.config/zsh", env.get("GHOSTTY_ZSH_ZDOTDIR").?); } @@ -797,7 +804,7 @@ test "zsh: missing resources" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(!try setupZsh(resources_dir, &env)); + try testing.expect(try setupZsh(alloc, .{ .shell = "zsh" }, resources_dir, &env) == null); try testing.expectEqual(0, env.count()); } -- 2.51.2 From 7bfcaef1e894eacede1208ab1dc99ca727010386 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Jan 2026 12:51:40 -0800 Subject: [PATCH 203/349] terminal: formatting feedback --- src/terminal/formatter.zig | 262 +++++++++++++++++-------------------- 1 file changed, 121 insertions(+), 141 deletions(-) diff --git a/src/terminal/formatter.zig b/src/terminal/formatter.zig index 2e846f7c1..4249187a7 100644 --- a/src/terminal/formatter.zig +++ b/src/terminal/formatter.zig @@ -1120,18 +1120,27 @@ pub const PageFormatter = struct { // If we have a zero value, then we accumulate a counter. We // only want to turn zero values into spaces if we have a non-zero - // char sometime later. However, for styled formats (VT, HTML), if - // the cell has styling (e.g., background color), we must emit it - // to preserve the visual appearance. - const dominated_by_style = self.opts.emit.styled() and - (!cell.isEmpty() or cell.hasStyling()); - if (!dominated_by_style and !cell.hasText()) { - blank_cells += 1; - continue; - } - if (cell.codepoint() == ' ' and self.opts.trim and !dominated_by_style) { - blank_cells += 1; - continue; + // char sometime later. + blank: { + // If we're emitting styled output (not plaintext) and + // the cell has some kind of styling or is not empty + // then this isn't blank. + if (self.opts.emit.styled() and + (!cell.isEmpty() or cell.hasStyling())) break :blank; + + // Cells with no text are blank + if (!cell.hasText()) { + blank_cells += 1; + continue; + } + + // Trailing spaces are blank. We know it is trailing + // because if we get a non-empty cell later we'll + // fill the blanks. + if (cell.codepoint() == ' ' and self.opts.trim) { + blank_cells += 1; + continue; + } } // This cell is not blank. If we have accumulated blank cells @@ -1169,79 +1178,72 @@ pub const PageFormatter = struct { blank_cells = 0; } + style: { + // If we aren't emitting styled output then we don't + // have to worry about styles. + if (!self.opts.emit.styled()) break :style; + + // Get our cell style. + const cell_style = self.cellStyle(cell); + + // If the style hasn't changed, don't bloat output. + if (cell_style.eql(style)) break :style; + + // If we had a previous style, we need to close it, + // because we've confirmed we have some new style + // (which is maybe default). + if (!style.default()) switch (self.opts.emit) { + .html => try self.formatStyleClose(writer), + + // For VT, we only close if we're switching to a default + // style because any non-default style will emit + // a \x1b[0m as the start of a VT coloring sequence. + .vt => if (cell_style.default()) try self.formatStyleClose(writer), + + // Unreachable because of the styled() check at the + // top of this block. + .plain => unreachable, + }; + + // At this point, we can copy our style over + style = cell_style; + + // If we're just the default style now, we're done. + if (cell_style.default()) break :style; + + // New style, emit it. + try self.formatStyleOpen( + writer, + &style, + ); + + // If we have a point map, we map the style to + // this cell. + if (self.point_map) |*map| { + var discarding: std.Io.Writer.Discarding = .init(&.{}); + try self.formatStyleOpen( + &discarding.writer, + &style, + ); + for (0..discarding.count) |_| map.map.append(map.alloc, .{ + .x = x, + .y = y, + }) catch return error.WriteFailed; + } + } + switch (cell.content_tag) { // We combine codepoint and graphemes because both have // shared style handling. We use comptime to dup it. inline .codepoint, .codepoint_grapheme => |tag| { - // Handle closing our styling if we go back to unstyled - // content. - if (self.opts.emit.styled() and - !cell.hasStyling() and - !style.default()) - { - try self.formatStyleClose(writer); - style = .{}; - } - - // If we're emitting styling and we have styles, then - // we need to load the style and emit any sequences - // as necessary. - if (self.opts.emit.styled() and cell.hasStyling()) style: { - // Get the style. - const cell_style = self.page.styles.get( - self.page.memory, - cell.style_id, - ); - - // If the style hasn't changed since our last - // emitted style, don't bloat the output. - if (cell_style.eql(style)) break :style; - - // We need to emit a closing tag if the style - // was non-default before, which means we set - // styles once. - const closing = !style.default(); - - // New style, emit it. - style = cell_style.*; - try self.formatStyleOpen( - writer, - &style, - closing, - ); - - // If we have a point map, we map the style to - // this cell. - if (self.point_map) |*map| { - var discarding: std.Io.Writer.Discarding = .init(&.{}); - try self.formatStyleOpen( - &discarding.writer, - &style, - closing, - ); - for (0..discarding.count) |_| map.map.append(map.alloc, .{ - .x = x, - .y = y, - }) catch return error.WriteFailed; - } - } - - // For styled cells without text, emit a space to carry the styling - if (cell.hasText()) { - try self.writeCell(tag, writer, cell); - } else { - try writer.writeByte(' '); - } + try self.writeCell(tag, writer, cell); // If we have a point map, all codepoints map to this // cell. if (self.point_map) |*map| { - const byte_count: usize = if (cell.hasText()) count: { - var discarding: std.Io.Writer.Discarding = .init(&.{}); - try self.writeCell(tag, &discarding.writer, cell); - break :count discarding.count; - } else 1; - for (0..byte_count) |_| map.map.append(map.alloc, .{ + var discarding: std.Io.Writer.Discarding = .init(&.{}); + try self.writeCell(tag, &discarding.writer, cell); + for (0..discarding.count) |_| map.map.append(map.alloc, .{ .x = x, .y = y, }) catch return error.WriteFailed; @@ -1250,21 +1252,12 @@ pub const PageFormatter = struct { // Cells with only background color (no text). Emit a space // with the appropriate background color SGR sequence. - .bg_color_palette => { - const index = cell.content.color_palette; - try self.emitBgColorSgr(writer, index, null, &style); - try writer.writeByte(' '); - if (self.point_map) |*map| { - map.map.append(map.alloc, .{ .x = x, .y = y }) catch return error.WriteFailed; - } - }, - .bg_color_rgb => { - const rgb = cell.content.color_rgb; - try self.emitBgColorSgr(writer, null, rgb, &style); + .bg_color_palette, .bg_color_rgb => { try writer.writeByte(' '); - if (self.point_map) |*map| { - map.map.append(map.alloc, .{ .x = x, .y = y }) catch return error.WriteFailed; - } + if (self.point_map) |*map| map.map.append( + map.alloc, + .{ .x = x, .y = y }, + ) catch return error.WriteFailed; }, } } @@ -1298,6 +1291,14 @@ pub const PageFormatter = struct { writer: *std.Io.Writer, cell: *const Cell, ) !void { + // Blank cells get an empty space that isn't replaced by anything + // because it isn't really a space. We do this so that formatting + // is preserved if we're emitting styles. + if (!cell.hasText()) { + try writer.writeByte(' '); + return; + } + try self.writeCodepointWithReplacement(writer, cell.content.codepoint); if (comptime tag == .codepoint_grapheme) { for (self.page.lookupGrapheme(cell).?) |cp| { @@ -1381,67 +1382,47 @@ pub const PageFormatter = struct { } } - /// Emit background color SGR sequence for bg_color_* content tags. - /// Updates the style tracking to reflect the emitted background. - fn emitBgColorSgr( - self: PageFormatter, - writer: *std.Io.Writer, - palette_index: ?u8, - rgb: ?Cell.RGB, - style: *Style, - ) std.Io.Writer.Error!void { - switch (self.opts.emit) { - .plain => {}, - .vt => { - // Close previous style if non-default - if (!style.default()) try writer.writeAll("\x1b[0m"); - // Emit background color - if (palette_index) |idx| { - try writer.print("\x1b[48;5;{d}m", .{idx}); - } else if (rgb) |c| { - try writer.print("\x1b[48;2;{d};{d};{d}m", .{ c.r, c.g, c.b }); - } - // Update style tracking - set bg_color so we know to reset later - style.* = .{}; - style.bg_color = if (palette_index) |idx| - .{ .palette = idx } - else if (rgb) |c| - .{ .rgb = .{ .r = c.r, .g = c.g, .b = c.b } } - else - .none; + /// Returns the style for the given cell. If there is no styling this + /// will return the default style. + fn cellStyle( + self: *const PageFormatter, + cell: *const Cell, + ) Style { + return switch (cell.content_tag) { + inline .codepoint, .codepoint_grapheme => if (!cell.hasStyling()) + .{} + else + self.page.styles.get( + self.page.memory, + cell.style_id, + ).*, + + .bg_color_palette => .{ + .bg_color = .{ + .palette = cell.content.color_palette, + }, }, - .html => { - // Close previous tag if needed - if (!style.default()) try writer.writeAll("

"); - // Emit background color as inline style - if (palette_index) |idx| { - try writer.print("
", .{idx}); - } else if (rgb) |c| { - try writer.print("
2}{x:0>2}{x:0>2};\">", .{ c.r, c.g, c.b }); - } - style.* = .{}; - style.bg_color = if (palette_index) |idx| - .{ .palette = idx } - else if (rgb) |c| - .{ .rgb = .{ .r = c.r, .g = c.g, .b = c.b } } - else - .none; + + .bg_color_rgb => .{ + .bg_color = .{ + .rgb = .{ + .r = cell.content.color_rgb.r, + .g = cell.content.color_rgb.g, + .b = cell.content.color_rgb.b, + }, + }, }, - } + }; } fn formatStyleOpen( self: PageFormatter, writer: *std.Io.Writer, style: *const Style, - closing: bool, ) std.Io.Writer.Error!void { switch (self.opts.emit) { .plain => unreachable, - // Note: we don't use closing on purpose because VT sequences - // always reset the prior style. Our formatter always emits a - // \x1b[0m before emitting a new style if necessary. .vt => { var formatter = style.formatterVt(); formatter.palette = self.opts.palette; @@ -1451,7 +1432,6 @@ pub const PageFormatter = struct { // We use `display: inline` so that the div doesn't impact // layout since we're primarily using it as a CSS wrapper. .html => { - if (closing) try writer.writeAll("
"); var formatter = style.formatterHtml(); formatter.palette = self.opts.palette; try writer.print( -- 2.51.2 From 5a7fdf735ed97b2448d6f7f7f2eceb1684e316d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Jan 2026 13:32:54 -0800 Subject: [PATCH 204/349] macos: custom tab title shows bell if active Fixes #10210 --- .../Features/Terminal/BaseTerminalController.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 88d8e39d8..b739e9ed1 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -834,7 +834,15 @@ class BaseTerminalController: NSWindowController, private func applyTitleToWindow() { guard let window else { return } - window.title = titleOverride ?? lastComputedTitle + + if let titleOverride { + window.title = computeTitle( + title: titleOverride, + bell: focusedSurface?.bell ?? false) + return + } + + window.title = lastComputedTitle } func pwdDidChange(to: URL?) { -- 2.51.2 From 111b0996d23e997d4b10411b10be04a6067bd9da Mon Sep 17 00:00:00 2001 From: Jagjeevan Kashid Date: Fri, 26 Dec 2025 19:33:50 +0530 Subject: [PATCH 205/349] feat: key-remap configuration to remap modifiers at the app-level Signed-off-by: Jagjeevan Kashid --- src/Surface.zig | 22 ++- src/config/Config.zig | 138 +++++++++++++++++ src/input.zig | 1 + src/input/KeyRemap.zig | 327 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 src/input/KeyRemap.zig diff --git a/src/Surface.zig b/src/Surface.zig index 7e9a307e5..c0c933d1c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -333,6 +333,7 @@ const DerivedConfig = struct { notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, notify_on_command_finish_action: configpkg.Config.NotifyOnCommandFinishAction, notify_on_command_finish_after: Duration, + key_remaps: []const input.KeyRemap, const Link = struct { regex: oni.Regex, @@ -408,6 +409,7 @@ const DerivedConfig = struct { .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", + .key_remaps = config.@"key-remap".value.items, // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -2576,8 +2578,14 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void { /// then Ghosty will act as though the binding does not exist. pub fn keyEventIsBinding( self: *Surface, - event: input.KeyEvent, + event_orig: input.KeyEvent, ) bool { + // Apply key remappings for consistency with keyCallback + var event = event_orig; + if (self.config.key_remaps.len > 0) { + event.mods = input.KeyRemap.applyRemaps(self.config.key_remaps, event_orig.mods); + } + switch (event.action) { .release => return false, .press, .repeat => {}, @@ -2605,9 +2613,16 @@ pub fn keyEventIsBinding( /// sending to the terminal, etc. pub fn keyCallback( self: *Surface, - event: input.KeyEvent, + event_orig: input.KeyEvent, ) !InputEffect { - // log.warn("text keyCallback event={}", .{event}); + // log.warn("text keyCallback event={}", .{event_orig}); + + // Apply key remappings to transform modifiers before any processing. + // This allows users to remap modifier keys at the app level. + var event = event_orig; + if (self.config.key_remaps.len > 0) { + event.mods = input.KeyRemap.applyRemaps(self.config.key_remaps, event_orig.mods); + } // Crash metadata in case we crash in here crash.sentry.thread_state = self.crashThreadState(); @@ -2645,7 +2660,6 @@ pub fn keyCallback( event, if (insp_ev) |*ev| ev else null, )) |v| return v; - // If we allow KAM and KAM is enabled then we do nothing. if (self.config.vt_kam_allowed) { self.renderer_state.mutex.lock(); diff --git a/src/config/Config.zig b/src/config/Config.zig index d9066b06d..2f0be6ab3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1757,6 +1757,38 @@ class: ?[:0]const u8 = null, /// Key tables are available since Ghostty 1.3.0. keybind: Keybinds = .{}, +/// Remap modifier keys within Ghostty. This allows you to swap or reassign +/// modifier keys at the application level without affecting system-wide +/// settings. +/// +/// The format is `from=to` where both `from` and `to` are modifier key names. +/// You can use generic names like `ctrl`, `alt`, `shift`, `super` (macOS: +/// `cmd`/`command`) or sided names like `left_ctrl`, `right_alt`, etc. +/// +/// Example: +/// +/// key-remap = ctrl=super +/// key-remap = left_control=right_alt +/// +/// Important notes: +/// +/// * This is a one-way remap. If you remap `ctrl=super`, then the physical +/// Ctrl key acts as Super, but the Super key remains Super. +/// +/// * Remaps are not transitive. If you remap `ctrl=super` and `alt=ctrl`, +/// pressing Alt will produce Ctrl, NOT Super. +/// +/// * This affects both keybind matching and terminal input encoding. +/// +/// * Generic modifiers (e.g. `ctrl`) match both left and right physical keys. +/// Use sided names (e.g. `left_ctrl`) to remap only one side. +/// +/// This configuration can be repeated to specify multiple remaps. +/// +/// Currently only supported on macOS. Linux/GTK support is planned for +/// a future release. +@"key-remap": RepeatableKeyRemap = .{}, + /// Horizontal window padding. This applies padding between the terminal cells /// and the left and right window borders. The value is in points, meaning that /// it will be scaled appropriately for screen DPI. @@ -8021,6 +8053,112 @@ pub const RepeatableLink = struct { } }; +/// RepeatableKeyRemap is used for the key-remap configuration which +/// allows remapping modifier keys within Ghostty. +pub const RepeatableKeyRemap = struct { + const Self = @This(); + + value: std.ArrayListUnmanaged(inputpkg.KeyRemap) = .empty, + + pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { + // Empty/unset input clears the list + const input = input_ orelse ""; + if (input.len == 0) { + self.value.clearRetainingCapacity(); + return; + } + + // Parse the key remap + const remap = inputpkg.KeyRemap.parse(input) catch |err| switch (err) { + error.InvalidFormat => return error.InvalidValue, + error.InvalidModifier => return error.InvalidValue, + }; + + // Reserve space and append + try self.value.ensureUnusedCapacity(alloc, 1); + self.value.appendAssumeCapacity(remap); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + return .{ + .value = try self.value.clone(alloc), + }; + } + + /// Compare if two values are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + if (self.value.items.len != other.value.items.len) return false; + for (self.value.items, other.value.items) |a, b| { + if (!a.equal(b)) return false; + } + return true; + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + if (self.value.items.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + for (self.value.items) |item| { + // Format as "from=to" + var buf: [64]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + writer.print("{s}={s}", .{ @tagName(item.from), @tagName(item.to) }) catch + return error.OutOfMemory; + try formatter.formatEntry([]const u8, fbs.getWritten()); + } + } + + test "RepeatableKeyRemap parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableKeyRemap = .{}; + + try list.parseCLI(alloc, "ctrl=super"); + try testing.expectEqual(@as(usize, 1), list.value.items.len); + try testing.expectEqual(inputpkg.KeyRemap.ModKey.ctrl, list.value.items[0].from); + try testing.expectEqual(inputpkg.KeyRemap.ModKey.super, list.value.items[0].to); + + try list.parseCLI(alloc, "alt=shift"); + try testing.expectEqual(@as(usize, 2), list.value.items.len); + } + + test "RepeatableKeyRemap parseCLI clear" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableKeyRemap = .{}; + + try list.parseCLI(alloc, "ctrl=super"); + try testing.expectEqual(@as(usize, 1), list.value.items.len); + + // Empty clears the list + try list.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), list.value.items.len); + } + + test "RepeatableKeyRemap parseCLI invalid" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableKeyRemap = .{}; + + try testing.expectError(error.InvalidValue, list.parseCLI(alloc, "foo=bar")); + try testing.expectError(error.InvalidValue, list.parseCLI(alloc, "ctrl")); + } +}; + /// Options for copy on select behavior. pub const CopyOnSelect = enum { /// Disables copy on select entirely. diff --git a/src/input.zig b/src/input.zig index be84a60d6..fe981ccc9 100644 --- a/src/input.zig +++ b/src/input.zig @@ -31,6 +31,7 @@ pub const ScrollMods = mouse.ScrollMods; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; pub const Trigger = Binding.Trigger; +pub const KeyRemap = @import("input/KeyRemap.zig"); // Keymap is only available on macOS right now. We could implement it // in theory for XKB too on Linux but we don't need it right now. diff --git a/src/input/KeyRemap.zig b/src/input/KeyRemap.zig new file mode 100644 index 000000000..de13ab70d --- /dev/null +++ b/src/input/KeyRemap.zig @@ -0,0 +1,327 @@ +//! Key remapping support for modifier keys within Ghostty. +//! +//! This module allows users to remap modifier keys (ctrl, alt, shift, super) +//! at the application level without affecting system-wide settings. +//! +//! Syntax: `key-remap = from=to` +//! +//! Examples: +//! key-remap = ctrl=super -- Ctrl acts as Super +//! key-remap = left_alt=ctrl -- Left Alt acts as Ctrl +//! +//! Remapping is one-way and non-transitive: +//! - `ctrl=super` means Ctrlโ†’Super, but Super stays Super +//! - `ctrl=super` + `alt=ctrl` means Altโ†’Ctrl (NOT Super) + +const KeyRemap = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const key = @import("key.zig"); +const Mods = key.Mods; + +from: ModKey, +to: ModKey, + +pub const ModKey = enum { + ctrl, + alt, + shift, + super, + left_ctrl, + left_alt, + left_shift, + left_super, + right_ctrl, + right_alt, + right_shift, + right_super, + + pub fn isGeneric(self: ModKey) bool { + return switch (self) { + .ctrl, .alt, .shift, .super => true, + else => false, + }; + } + + pub fn parse(input: []const u8) ?ModKey { + const map = std.StaticStringMap(ModKey).initComptime(.{ + .{ "ctrl", .ctrl }, + .{ "control", .ctrl }, + .{ "alt", .alt }, + .{ "opt", .alt }, + .{ "option", .alt }, + .{ "shift", .shift }, + .{ "super", .super }, + .{ "cmd", .super }, + .{ "command", .super }, + .{ "left_ctrl", .left_ctrl }, + .{ "left_control", .left_ctrl }, + .{ "leftctrl", .left_ctrl }, + .{ "leftcontrol", .left_ctrl }, + .{ "left_alt", .left_alt }, + .{ "left_opt", .left_alt }, + .{ "left_option", .left_alt }, + .{ "leftalt", .left_alt }, + .{ "leftopt", .left_alt }, + .{ "leftoption", .left_alt }, + .{ "left_shift", .left_shift }, + .{ "leftshift", .left_shift }, + .{ "left_super", .left_super }, + .{ "left_cmd", .left_super }, + .{ "left_command", .left_super }, + .{ "leftsuper", .left_super }, + .{ "leftcmd", .left_super }, + .{ "leftcommand", .left_super }, + .{ "right_ctrl", .right_ctrl }, + .{ "right_control", .right_ctrl }, + .{ "rightctrl", .right_ctrl }, + .{ "rightcontrol", .right_ctrl }, + .{ "right_alt", .right_alt }, + .{ "right_opt", .right_alt }, + .{ "right_option", .right_alt }, + .{ "rightalt", .right_alt }, + .{ "rightopt", .right_alt }, + .{ "rightoption", .right_alt }, + .{ "right_shift", .right_shift }, + .{ "rightshift", .right_shift }, + .{ "right_super", .right_super }, + .{ "right_cmd", .right_super }, + .{ "right_command", .right_super }, + .{ "rightsuper", .right_super }, + .{ "rightcmd", .right_super }, + .{ "rightcommand", .right_super }, + }); + + var buf: [32]u8 = undefined; + if (input.len > buf.len) return null; + const lower = std.ascii.lowerString(&buf, input); + return map.get(lower); + } +}; + +pub fn parse(input: []const u8) !KeyRemap { + const eql_idx = std.mem.indexOf(u8, input, "=") orelse + return error.InvalidFormat; + + const from_str = std.mem.trim(u8, input[0..eql_idx], " \t"); + const to_str = std.mem.trim(u8, input[eql_idx + 1 ..], " \t"); + + if (from_str.len == 0 or to_str.len == 0) { + return error.InvalidFormat; + } + + const from = ModKey.parse(from_str) orelse return error.InvalidModifier; + const to = ModKey.parse(to_str) orelse return error.InvalidModifier; + + return .{ .from = from, .to = to }; +} + +pub fn apply(self: KeyRemap, mods: Mods) ?Mods { + var result = mods; + var matched = false; + + switch (self.from) { + .ctrl => if (mods.ctrl) { + result.ctrl = false; + matched = true; + }, + .left_ctrl => if (mods.ctrl and mods.sides.ctrl == .left) { + result.ctrl = false; + matched = true; + }, + .right_ctrl => if (mods.ctrl and mods.sides.ctrl == .right) { + result.ctrl = false; + matched = true; + }, + .alt => if (mods.alt) { + result.alt = false; + matched = true; + }, + .left_alt => if (mods.alt and mods.sides.alt == .left) { + result.alt = false; + matched = true; + }, + .right_alt => if (mods.alt and mods.sides.alt == .right) { + result.alt = false; + matched = true; + }, + .shift => if (mods.shift) { + result.shift = false; + matched = true; + }, + .left_shift => if (mods.shift and mods.sides.shift == .left) { + result.shift = false; + matched = true; + }, + .right_shift => if (mods.shift and mods.sides.shift == .right) { + result.shift = false; + matched = true; + }, + .super => if (mods.super) { + result.super = false; + matched = true; + }, + .left_super => if (mods.super and mods.sides.super == .left) { + result.super = false; + matched = true; + }, + .right_super => if (mods.super and mods.sides.super == .right) { + result.super = false; + matched = true; + }, + } + + if (!matched) return null; + + switch (self.to) { + .ctrl, .left_ctrl => { + result.ctrl = true; + result.sides.ctrl = .left; + }, + .right_ctrl => { + result.ctrl = true; + result.sides.ctrl = .right; + }, + .alt, .left_alt => { + result.alt = true; + result.sides.alt = .left; + }, + .right_alt => { + result.alt = true; + result.sides.alt = .right; + }, + .shift, .left_shift => { + result.shift = true; + result.sides.shift = .left; + }, + .right_shift => { + result.shift = true; + result.sides.shift = .right; + }, + .super, .left_super => { + result.super = true; + result.sides.super = .left; + }, + .right_super => { + result.super = true; + result.sides.super = .right; + }, + } + + return result; +} + +/// Apply remaps non-transitively: each remap checks the original mods. +pub fn applyRemaps(remaps: []const KeyRemap, mods: Mods) Mods { + var result = mods; + for (remaps) |remap| { + if (remap.apply(mods)) |_| { + switch (remap.from) { + .ctrl, .left_ctrl, .right_ctrl => result.ctrl = false, + .alt, .left_alt, .right_alt => result.alt = false, + .shift, .left_shift, .right_shift => result.shift = false, + .super, .left_super, .right_super => result.super = false, + } + switch (remap.to) { + .ctrl, .left_ctrl => { + result.ctrl = true; + result.sides.ctrl = .left; + }, + .right_ctrl => { + result.ctrl = true; + result.sides.ctrl = .right; + }, + .alt, .left_alt => { + result.alt = true; + result.sides.alt = .left; + }, + .right_alt => { + result.alt = true; + result.sides.alt = .right; + }, + .shift, .left_shift => { + result.shift = true; + result.sides.shift = .left; + }, + .right_shift => { + result.shift = true; + result.sides.shift = .right; + }, + .super, .left_super => { + result.super = true; + result.sides.super = .left; + }, + .right_super => { + result.super = true; + result.sides.super = .right; + }, + } + } + } + return result; +} + +pub fn clone(self: KeyRemap, alloc: Allocator) Allocator.Error!KeyRemap { + _ = alloc; + return self; +} + +pub fn equal(self: KeyRemap, other: KeyRemap) bool { + return self.from == other.from and self.to == other.to; +} + +test "ModKey.parse" { + const testing = std.testing; + + try testing.expectEqual(ModKey.ctrl, ModKey.parse("ctrl").?); + try testing.expectEqual(ModKey.ctrl, ModKey.parse("control").?); + try testing.expectEqual(ModKey.ctrl, ModKey.parse("CTRL").?); + try testing.expectEqual(ModKey.alt, ModKey.parse("alt").?); + try testing.expectEqual(ModKey.super, ModKey.parse("cmd").?); + try testing.expectEqual(ModKey.left_ctrl, ModKey.parse("left_ctrl").?); + try testing.expectEqual(ModKey.right_alt, ModKey.parse("right_alt").?); + try testing.expect(ModKey.parse("foo") == null); +} + +test "parse" { + const testing = std.testing; + + const remap = try parse("ctrl=super"); + try testing.expectEqual(ModKey.ctrl, remap.from); + try testing.expectEqual(ModKey.super, remap.to); + + const spaced = try parse(" ctrl = super "); + try testing.expectEqual(ModKey.ctrl, spaced.from); + + try testing.expectError(error.InvalidFormat, parse("ctrl")); + try testing.expectError(error.InvalidModifier, parse("foo=bar")); +} + +test "apply" { + const testing = std.testing; + + const remap = try parse("ctrl=super"); + const mods = Mods{ .ctrl = true }; + const result = remap.apply(mods).?; + + try testing.expect(!result.ctrl); + try testing.expect(result.super); + try testing.expect(remap.apply(Mods{ .alt = true }) == null); +} + +test "applyRemaps non-transitive" { + const testing = std.testing; + + const remaps = [_]KeyRemap{ + try parse("ctrl=super"), + try parse("alt=ctrl"), + }; + + const mods = Mods{ .alt = true }; + const result = applyRemaps(&remaps, mods); + + try testing.expect(!result.alt); + try testing.expect(result.ctrl); + try testing.expect(!result.super); +} -- 2.51.2 From 8415d8215b5eafe5cdc6905daf13e365476448c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 06:52:02 -0800 Subject: [PATCH 206/349] comments --- src/config/Config.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 2f0be6ab3..8a314346d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1765,6 +1765,11 @@ keybind: Keybinds = .{}, /// You can use generic names like `ctrl`, `alt`, `shift`, `super` (macOS: /// `cmd`/`command`) or sided names like `left_ctrl`, `right_alt`, etc. /// +/// This will NOT change keyboard layout or key encodings outside of Ghostty. +/// For example, on macOS, `option+a` may still produce `รฅ` even if `option` is +/// remapped to `ctrl`. Desktop environments usually handle key layout long +/// before Ghostty receives the key events. +/// /// Example: /// /// key-remap = ctrl=super @@ -1779,6 +1784,9 @@ keybind: Keybinds = .{}, /// pressing Alt will produce Ctrl, NOT Super. /// /// * This affects both keybind matching and terminal input encoding. +/// This does NOT impact keyboard layout or how keys are interpreted +/// prior to Ghostty receiving them. For example, `option+a` on macOS +/// may still produce `รฅ` even if `option` is remapped to `ctrl`. /// /// * Generic modifiers (e.g. `ctrl`) match both left and right physical keys. /// Use sided names (e.g. `left_ctrl`) to remap only one side. -- 2.51.2 From 619427c84c477ea3de9794b219156a0f410fe568 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 07:02:46 -0800 Subject: [PATCH 207/349] input: move mods out to key_mods.zig --- src/input.zig | 3 +- src/input/Binding.zig | 15 ++-- src/input/key.zig | 157 +--------------------------------- src/input/key_mods.zig | 185 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 166 deletions(-) create mode 100644 src/input/key_mods.zig diff --git a/src/input.zig b/src/input.zig index fe981ccc9..df636c122 100644 --- a/src/input.zig +++ b/src/input.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const config = @import("input/config.zig"); const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); +const key_mods = @import("input/key_mods.zig"); const keyboard = @import("input/keyboard.zig"); pub const command = @import("input/command.zig"); @@ -22,7 +23,7 @@ pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; pub const KeyEvent = key.KeyEvent; pub const InspectorMode = Binding.Action.InspectorMode; -pub const Mods = key.Mods; +pub const Mods = key_mods.Mods; pub const MouseButton = mouse.Button; pub const MouseButtonState = mouse.ButtonState; pub const MousePressureStage = mouse.PressureStage; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 08f5fdf7c..3197bb7d1 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -9,6 +9,7 @@ const build_config = @import("../build_config.zig"); const uucode = @import("uucode"); const EntryFormatter = @import("../config/formatter.zig").EntryFormatter; const key = @import("key.zig"); +const key_mods = @import("key_mods.zig"); const KeyEvent = key.KeyEvent; /// The trigger that needs to be performed to execute the action. @@ -1640,18 +1641,12 @@ pub const Trigger = struct { } // Alias modifiers - const alias_mods = .{ - .{ "cmd", "super" }, - .{ "command", "super" }, - .{ "opt", "alt" }, - .{ "option", "alt" }, - .{ "control", "ctrl" }, - }; - inline for (alias_mods) |pair| { + inline for (key_mods.alias) |pair| { if (std.mem.eql(u8, part, pair[0])) { // Repeat not allowed - if (@field(result.mods, pair[1])) return Error.InvalidFormat; - @field(result.mods, pair[1]) = true; + const field = @tagName(pair[1]); + if (@field(result.mods, field)) return Error.InvalidFormat; + @field(result.mods, field) = true; continue :loop; } } diff --git a/src/input/key.zig b/src/input/key.zig index 6445871eb..a929a0323 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -4,6 +4,8 @@ const Allocator = std.mem.Allocator; const cimgui = @import("dcimgui"); const OptionAsAlt = @import("config.zig").OptionAsAlt; +pub const Mods = @import("key_mods.zig").Mods; + /// A generic key input event. This is the information that is necessary /// regardless of apprt in order to generate the proper terminal /// control sequences for a given key press. @@ -76,161 +78,6 @@ pub const KeyEvent = struct { } }; -/// A bitmask for all key modifiers. -/// -/// IMPORTANT: Any changes here update include/ghostty.h -pub const Mods = packed struct(Mods.Backing) { - pub const Backing = u16; - - shift: bool = false, - ctrl: bool = false, - alt: bool = false, - super: bool = false, - caps_lock: bool = false, - num_lock: bool = false, - sides: side = .{}, - _padding: u6 = 0, - - /// Tracks the side that is active for any given modifier. Note - /// that this doesn't confirm a modifier is pressed; you must check - /// the bool for that in addition to this. - /// - /// Not all platforms support this, check apprt for more info. - pub const side = packed struct(u4) { - shift: Side = .left, - ctrl: Side = .left, - alt: Side = .left, - super: Side = .left, - }; - - pub const Side = enum(u1) { left, right }; - - /// Integer value of this struct. - pub fn int(self: Mods) Backing { - return @bitCast(self); - } - - /// Returns true if no modifiers are set. - pub fn empty(self: Mods) bool { - return self.int() == 0; - } - - /// Returns true if two mods are equal. - pub fn equal(self: Mods, other: Mods) bool { - return self.int() == other.int(); - } - - /// Return mods that are only relevant for bindings. - pub fn binding(self: Mods) Mods { - return .{ - .shift = self.shift, - .ctrl = self.ctrl, - .alt = self.alt, - .super = self.super, - }; - } - - /// Perform `self &~ other` to remove the other mods from self. - pub fn unset(self: Mods, other: Mods) Mods { - return @bitCast(self.int() & ~other.int()); - } - - /// Returns the mods without locks set. - pub fn withoutLocks(self: Mods) Mods { - var copy = self; - copy.caps_lock = false; - copy.num_lock = false; - return copy; - } - - /// 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: OptionAsAlt) Mods { - var result = self; - - // macos-option-as-alt for darwin - if (comptime builtin.target.os.tag.isDarwin()) alt: { - // Alt has to be set only on the correct side - switch (option_as_alt) { - .false => break :alt, - .true => {}, - .left => if (self.sides.alt == .right) break :alt, - .right => if (self.sides.alt == .left) break :alt, - } - - // Unset alt - result.alt = false; - } - - return result; - } - - /// Checks to see if super is on (MacOS) or ctrl. - pub fn ctrlOrSuper(self: Mods) bool { - if (comptime builtin.target.os.tag.isDarwin()) { - return self.super; - } - return self.ctrl; - } - - // For our own understanding - test { - const testing = std.testing; - try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0)); - try testing.expectEqual( - @as(Backing, @bitCast(Mods{ .shift = true })), - @as(Backing, 0b0000_0001), - ); - } - - test "translation macos-option-as-alt" { - if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; - - const testing = std.testing; - - // Unset - { - const mods: Mods = .{}; - const result = mods.translation(.true); - try testing.expectEqual(result, mods); - } - - // Set - { - const mods: Mods = .{ .alt = true }; - const result = mods.translation(.true); - try testing.expectEqual(Mods{}, result); - } - - // Set but disabled - { - const mods: Mods = .{ .alt = true }; - const result = mods.translation(.false); - try testing.expectEqual(result, mods); - } - - // Set wrong side - { - const mods: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; - const result = mods.translation(.left); - try testing.expectEqual(result, mods); - } - { - const mods: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; - const result = mods.translation(.right); - try testing.expectEqual(result, mods); - } - - // Set with other mods - { - const mods: Mods = .{ .alt = true, .shift = true }; - const result = mods.translation(.true); - try testing.expectEqual(Mods{ .shift = true }, result); - } - } -}; - /// The action associated with an input event. This is backed by a c_int /// so that we can use the enum as-is for our embedding API. /// diff --git a/src/input/key_mods.zig b/src/input/key_mods.zig new file mode 100644 index 000000000..885bceda1 --- /dev/null +++ b/src/input/key_mods.zig @@ -0,0 +1,185 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const OptionAsAlt = @import("config.zig").OptionAsAlt; + +/// Aliases for modifier names. +pub const alias: []const struct { []const u8, Mod } = &.{ + .{ "cmd", .super }, + .{ "command", .super }, + .{ "opt", .alt }, + .{ "option", .alt }, + .{ "control", .ctrl }, +}; + +/// Single modifier +pub const Mod = enum { + shift, + ctrl, + alt, + super, + + pub const Side = enum(u1) { left, right }; +}; + +/// A bitmask for all key modifiers. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const Mods = packed struct(Mods.Backing) { + pub const Backing = u16; + + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + super: bool = false, + caps_lock: bool = false, + num_lock: bool = false, + sides: side = .{}, + _padding: u6 = 0, + + /// Tracks the side that is active for any given modifier. Note + /// that this doesn't confirm a modifier is pressed; you must check + /// the bool for that in addition to this. + /// + /// Not all platforms support this, check apprt for more info. + pub const side = packed struct(u4) { + shift: Mod.Side = .left, + ctrl: Mod.Side = .left, + alt: Mod.Side = .left, + super: Mod.Side = .left, + }; + + /// Integer value of this struct. + pub fn int(self: Mods) Backing { + return @bitCast(self); + } + + /// Returns true if no modifiers are set. + pub fn empty(self: Mods) bool { + return self.int() == 0; + } + + /// Returns true if two mods are equal. + pub fn equal(self: Mods, other: Mods) bool { + return self.int() == other.int(); + } + + /// Return mods that are only relevant for bindings. + pub fn binding(self: Mods) Mods { + return .{ + .shift = self.shift, + .ctrl = self.ctrl, + .alt = self.alt, + .super = self.super, + }; + } + + /// Perform `self &~ other` to remove the other mods from self. + pub fn unset(self: Mods, other: Mods) Mods { + return @bitCast(self.int() & ~other.int()); + } + + /// Returns the mods without locks set. + pub fn withoutLocks(self: Mods) Mods { + var copy = self; + copy.caps_lock = false; + copy.num_lock = false; + return copy; + } + + /// 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: OptionAsAlt) Mods { + var result = self; + + // macos-option-as-alt for darwin + if (comptime builtin.target.os.tag.isDarwin()) alt: { + // Alt has to be set only on the correct side + switch (option_as_alt) { + .false => break :alt, + .true => {}, + .left => if (self.sides.alt == .right) break :alt, + .right => if (self.sides.alt == .left) break :alt, + } + + // Unset alt + result.alt = false; + } + + return result; + } + + /// Checks to see if super is on (MacOS) or ctrl. + pub fn ctrlOrSuper(self: Mods) bool { + if (comptime builtin.target.os.tag.isDarwin()) { + return self.super; + } + return self.ctrl; + } + + // For our own understanding + test { + const testing = std.testing; + try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0)); + try testing.expectEqual( + @as(Backing, @bitCast(Mods{ .shift = true })), + @as(Backing, 0b0000_0001), + ); + } + + test "translation macos-option-as-alt" { + if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; + + const testing = std.testing; + + // Unset + { + const mods: Mods = .{}; + const result = mods.translation(.true); + try testing.expectEqual(result, mods); + } + + // Set + { + const mods: Mods = .{ .alt = true }; + const result = mods.translation(.true); + try testing.expectEqual(Mods{}, result); + } + + // Set but disabled + { + const mods: Mods = .{ .alt = true }; + const result = mods.translation(.false); + try testing.expectEqual(result, mods); + } + + // Set wrong side + { + const mods: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; + const result = mods.translation(.left); + try testing.expectEqual(result, mods); + } + { + const mods: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const result = mods.translation(.right); + try testing.expectEqual(result, mods); + } + + // Set with other mods + { + const mods: Mods = .{ .alt = true, .shift = true }; + const result = mods.translation(.true); + try testing.expectEqual(Mods{ .shift = true }, result); + } + } +}; + +/// Modifier remapping. See `key-remap` in Config.zig for detailed docs. +pub const RemapSet = struct { + /// Available mappings. + map: std.ArrayHashMapUnmanaged(Mods, Mods), + + /// The mask of remapped modifiers that can be used to quickly + /// check if some input mods need remapping. + mask: Mods.Backing, +}; -- 2.51.2 From f804a4344e9748bafaf5ad4d4eea4800f941d77d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 08:15:06 -0800 Subject: [PATCH 208/349] input: RemapSet --- src/config/Config.zig | 3 +- src/input.zig | 1 + src/input/key_mods.zig | 424 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 423 insertions(+), 5 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 8a314346d..0d883fddc 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -37,6 +37,7 @@ const RepeatableStringMap = @import("RepeatableStringMap.zig"); pub const Path = @import("path.zig").Path; pub const RepeatablePath = @import("path.zig").RepeatablePath; const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig"); +const KeyRemapSet = @import("key_mods.zig").RemapSet; // We do this instead of importing all of terminal/main.zig to // limit the dependency graph. This is important because some things @@ -1789,7 +1790,7 @@ keybind: Keybinds = .{}, /// may still produce `รฅ` even if `option` is remapped to `ctrl`. /// /// * Generic modifiers (e.g. `ctrl`) match both left and right physical keys. -/// Use sided names (e.g. `left_ctrl`) to remap only one side. +/// Use sided names (e.g. `left_ctrl`) to remap only one side. /// /// /// This configuration can be repeated to specify multiple remaps. /// diff --git a/src/input.zig b/src/input.zig index df636c122..232e03580 100644 --- a/src/input.zig +++ b/src/input.zig @@ -22,6 +22,7 @@ pub const Link = @import("input/Link.zig"); pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; pub const KeyEvent = key.KeyEvent; +pub const KeyRemapSet = key_mods.RemapSet; pub const InspectorMode = Binding.Action.InspectorMode; pub const Mods = key_mods.Mods; pub const MouseButton = mouse.Button; diff --git a/src/input/key_mods.zig b/src/input/key_mods.zig index 885bceda1..885fe357b 100644 --- a/src/input/key_mods.zig +++ b/src/input/key_mods.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const OptionAsAlt = @import("config.zig").OptionAsAlt; @@ -33,19 +35,46 @@ pub const Mods = packed struct(Mods.Backing) { super: bool = false, caps_lock: bool = false, num_lock: bool = false, - sides: side = .{}, + sides: Side = .{}, _padding: u6 = 0, + /// The standard modifier keys only. Does not include the lock keys, + /// only standard bindable keys. + pub const Keys = packed struct(u4) { + shift: bool = false, + ctrl: bool = false, + alt: bool = false, + super: bool = false, + + pub const Backing = @typeInfo(Keys).@"struct".backing_integer.?; + + pub inline fn int(self: Keys) Keys.Backing { + return @bitCast(self); + } + }; + /// Tracks the side that is active for any given modifier. Note /// that this doesn't confirm a modifier is pressed; you must check /// the bool for that in addition to this. /// /// Not all platforms support this, check apprt for more info. - pub const side = packed struct(u4) { + pub const Side = packed struct(u4) { shift: Mod.Side = .left, ctrl: Mod.Side = .left, alt: Mod.Side = .left, super: Mod.Side = .left, + + pub const Backing = @typeInfo(Side).@"struct".backing_integer.?; + }; + + /// The mask that has all the side bits set. + pub const side_mask: Mods = .{ + .sides = .{ + .shift = .right, + .ctrl = .right, + .alt = .right, + .super = .right, + }, }; /// Integer value of this struct. @@ -63,6 +92,16 @@ pub const Mods = packed struct(Mods.Backing) { return self.int() == other.int(); } + /// Returns only the keys. + /// + /// In the future I want to remove `binding` for this. I didn't want + /// to do that all in one PR where I added this because its a bigger + /// change. + pub fn keys(self: Mods) Keys { + const backing: Keys.Backing = @truncate(self.int()); + return @bitCast(backing); + } + /// Return mods that are only relevant for bindings. pub fn binding(self: Mods) Mods { return .{ @@ -177,9 +216,386 @@ pub const Mods = packed struct(Mods.Backing) { /// Modifier remapping. See `key-remap` in Config.zig for detailed docs. pub const RemapSet = struct { /// Available mappings. - map: std.ArrayHashMapUnmanaged(Mods, Mods), + map: std.AutoArrayHashMapUnmanaged(Mods, Mods), /// The mask of remapped modifiers that can be used to quickly /// check if some input mods need remapping. - mask: Mods.Backing, + mask: Mask, + + pub const empty: RemapSet = .{ + .map = .{}, + .mask = .{}, + }; + + pub const ParseError = Allocator.Error || error{ + MissingAssignment, + InvalidMod, + }; + + pub fn deinit(self: *RemapSet, alloc: Allocator) void { + self.map.deinit(alloc); + } + + /// Parse a modifier remap and add it to the set. + pub fn parse( + self: *RemapSet, + alloc: Allocator, + input: []const u8, + ) ParseError!void { + // Find the assignment point ('=') + const eql_idx = std.mem.indexOfScalar( + u8, + input, + '=', + ) orelse return error.MissingAssignment; + + // The to side defaults to "left" if no explicit side is given. + // This is because this is the default unsided value provided by + // the apprts in the current Mods layout. + const to: Mods = to: { + const raw = try parseMod(input[eql_idx + 1 ..]); + break :to initMods(raw[0], raw[1] orelse .left); + }; + + // The from side, if sided, is easy and we put it directly into + // the map. + const from_raw = try parseMod(input[0..eql_idx]); + if (from_raw[1]) |from_side| { + const from: Mods = initMods(from_raw[0], from_side); + try self.map.put( + alloc, + from, + to, + ); + errdefer comptime unreachable; + self.mask.update(from); + return; + } + + // We need to do some combinatorial explosion here for unsided + // from in order to assign all possible sides. + const from_left = initMods(from_raw[0], .left); + const from_right = initMods(from_raw[0], .right); + try self.map.put( + alloc, + from_left, + to, + ); + errdefer _ = self.map.swapRemove(from_left); + try self.map.put( + alloc, + from_right, + to, + ); + errdefer _ = self.map.swapRemove(from_right); + + errdefer comptime unreachable; + self.mask.update(from_left); + self.mask.update(from_right); + } + + /// Must be called prior to any remappings so that the mapping + /// is sorted properly. Otherwise, you will get invalid results. + pub fn finalize(self: *RemapSet) void { + const Context = struct { + keys: []const Mods, + + pub fn lessThan( + ctx: @This(), + a_index: usize, + b_index: usize, + ) bool { + _ = b_index; + + // Mods with any right sides prioritize + const side_mask = comptime Mods.side_mask.int(); + const a = ctx.keys[a_index]; + return a.int() & side_mask != 0; + } + }; + + self.map.sort(Context{ .keys = self.map.keys() }); + } + + /// Parses a single mode in a single remapping string. E.g. + /// `ctrl` or `left_shift`. + fn parseMod(input: []const u8) error{InvalidMod}!struct { Mod, ?Mod.Side } { + const side_str, const mod_str = if (std.mem.indexOfScalar( + u8, + input, + '_', + )) |idx| .{ + input[0..idx], + input[idx + 1 ..], + } else .{ + "", + input, + }; + + return .{ + std.meta.stringToEnum( + Mod, + mod_str, + ) orelse return error.InvalidMod, + if (side_str.len > 0) std.meta.stringToEnum( + Mod.Side, + side_str, + ) orelse return error.InvalidMod else null, + }; + } + + fn initMods(mod: Mod, side: Mod.Side) Mods { + switch (mod) { + inline else => |tag| { + var mods: Mods = .{}; + @field(mods, @tagName(tag)) = true; + @field(mods.sides, @tagName(tag)) = side; + return mods; + }, + } + } + + /// Returns true if the given mods need remapping. + pub fn isRemapped(self: *const RemapSet, mods: Mods) bool { + return self.mask.match(mods); + } + + /// Apply a remap to the given mods. + pub fn apply(self: *const RemapSet, mods: Mods) Mods { + if (!self.isRemapped(mods)) return mods; + + const mods_binding: Mods.Keys.Backing = @truncate(mods.int()); + const mods_sides: Mods.Side.Backing = @bitCast(mods.sides); + + var it = self.map.iterator(); + while (it.next()) |entry| { + const from = entry.key_ptr.*; + const from_binding: Mods.Keys.Backing = @truncate(from.int()); + if (mods_binding & from_binding != from_binding) continue; + const from_sides: Mods.Side.Backing = @bitCast(from.sides); + if ((mods_sides ^ from_sides) & from_binding != 0) continue; + + var mods_int = mods.int(); + mods_int &= ~from.int(); + mods_int |= entry.value_ptr.int(); + return @bitCast(mods_int); + } + + unreachable; + } + + /// Tracks which modifier keys and sides have remappings registered. + /// Used as a fast pre-check before doing expensive map lookups. + /// + /// The mask uses separate tracking for left and right sides because + /// remappings can be side-specific (e.g., only remap left_ctrl). + /// + /// Note: `left_sides` uses inverted logic where 1 means "left is remapped" + /// even though `Mod.Side.left = 0`. This allows efficient bitwise matching + /// since we can AND directly with the side bits. + pub const Mask = packed struct(u12) { + /// Which modifier keys (shift/ctrl/alt/super) have any remapping. + keys: Mods.Keys = .{}, + /// Which modifiers have left-side remappings (inverted: 1 = left remapped). + left_sides: Mods.Side = .{}, + /// Which modifiers have right-side remappings (1 = right remapped). + right_sides: Mods.Side = .{}, + + /// Adds a modifier to the mask, marking it as having a remapping. + pub fn update(self: *Mask, mods: Mods) void { + const keys_int: Mods.Keys.Backing = mods.keys().int(); + + // OR the new keys into our existing keys mask. + // Example: keys=0b0000, new ctrl โ†’ keys=0b0010 + self.keys = @bitCast(self.keys.int() | keys_int); + + // Both Keys and Side are u4 with matching bit positions. + // This lets us use keys_int to select which side bits to update. + const sides: Mods.Side.Backing = @bitCast(mods.sides); + const left_int: Mods.Side.Backing = @bitCast(self.left_sides); + const right_int: Mods.Side.Backing = @bitCast(self.right_sides); + + // Update left_sides: set bit if this key is active AND side is left. + // Since Side.left=0, we invert sides (~sides) so left becomes 1. + // keys_int masks to only affect the modifier being added. + // Example: left_ctrl โ†’ keys_int=0b0010, ~sides=0b1111 (left=0 inverted) + // result: left_int | (0b0010 & 0b1111) = left_int | 0b0010 + self.left_sides = @bitCast(left_int | (keys_int & ~sides)); + + // Update right_sides: set bit if this key is active AND side is right. + // Since Side.right=1, we use sides directly. + // Example: right_ctrl โ†’ keys_int=0b0010, sides=0b0010 (right=1) + // result: right_int | (0b0010 & 0b0010) = right_int | 0b0010 + self.right_sides = @bitCast(right_int | (keys_int & sides)); + } + + /// Returns true if the given mods match any remapping in this mask. + /// This is a fast check to avoid expensive map lookups when no + /// remapping could possibly apply. + /// + /// Checks both that the modifier key is remapped AND that the + /// specific side (left/right) being pressed has a remapping. + pub fn match(self: *const Mask, mods: Mods) bool { + // Find which pressed keys have remappings registered. + // Example: pressed={ctrl,alt}, mask={ctrl} โ†’ active=0b0010 (just ctrl) + const active = mods.keys().int() & self.keys.int(); + if (active == 0) return false; + + // Check if the pressed side matches a remapped side. + // For left (sides bit = 0): check against left_int (where 1 = left remapped) + // ~sides inverts so left becomes 1, then AND with left_int + // For right (sides bit = 1): check against right_int directly + // + // Example: pressing left_ctrl (sides.ctrl=0, left_int.ctrl=1) + // ~sides = 0b1111, left_int = 0b0010 + // (~sides & left_int) = 0b0010 โœ“ matches + // + // Example: pressing right_ctrl but only left_ctrl is remapped + // sides = 0b0010, left_int = 0b0010, right_int = 0b0000 + // (~0b0010 & 0b0010) | (0b0010 & 0b0000) = 0b0000 โœ— no match + const sides: Mods.Side.Backing = @bitCast(mods.sides); + const left_int: Mods.Side.Backing = @bitCast(self.left_sides); + const right_int: Mods.Side.Backing = @bitCast(self.right_sides); + const side_match = (~sides & left_int) | (sides & right_int); + + // Final check: is any active (pressed + remapped) key also side-matched? + return (active & side_match) != 0; + } + }; }; + +test "RemapSet: unsided remap creates both left and right mappings" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + try set.parse(alloc, "ctrl=super"); + set.finalize(); + try testing.expectEqual( + Mods{ + .super = true, + .sides = .{ .super = .left }, + }, + set.apply(.{ + .ctrl = true, + .sides = .{ .ctrl = .left }, + }), + ); + try testing.expectEqual( + Mods{ + .super = true, + .sides = .{ .super = .left }, + }, + set.apply(.{ + .ctrl = true, + .sides = .{ .ctrl = .right }, + }), + ); +} + +test "RemapSet: sided from only maps that side" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "left_alt=ctrl"); + set.finalize(); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + try testing.expectEqual(left_ctrl, set.apply(left_alt)); + + const right_alt: Mods = .{ .alt = true, .sides = .{ .alt = .right } }; + try testing.expectEqual(right_alt, set.apply(right_alt)); +} + +test "RemapSet: sided to" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "ctrl=right_super"); + set.finalize(); + + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + const right_super: Mods = .{ .super = true, .sides = .{ .super = .right } }; + try testing.expectEqual(right_super, set.apply(left_ctrl)); +} + +test "RemapSet: both sides specified" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "left_shift=right_ctrl"); + set.finalize(); + + const left_shift: Mods = .{ .shift = true, .sides = .{ .shift = .left } }; + const right_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .right } }; + try testing.expectEqual(right_ctrl, set.apply(left_shift)); +} + +test "RemapSet: multiple parses accumulate" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "left_ctrl=super"); + try set.parse(alloc, "left_alt=ctrl"); + set.finalize(); + + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_ctrl)); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_ctrl_result: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + try testing.expectEqual(left_ctrl_result, set.apply(left_alt)); +} + +test "RemapSet: error on missing assignment" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try testing.expectError(error.MissingAssignment, set.parse(alloc, "ctrl")); + try testing.expectError(error.MissingAssignment, set.parse(alloc, "")); +} + +test "RemapSet: error on invalid modifier" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try testing.expectError(error.InvalidMod, set.parse(alloc, "invalid=ctrl")); + try testing.expectError(error.InvalidMod, set.parse(alloc, "ctrl=invalid")); + try testing.expectError(error.InvalidMod, set.parse(alloc, "middle_ctrl=super")); +} + +test "RemapSet: isRemapped checks mask" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "ctrl=super"); + set.finalize(); + + try testing.expect(set.isRemapped(.{ .ctrl = true })); + try testing.expect(!set.isRemapped(.{ .alt = true })); + try testing.expect(!set.isRemapped(.{ .shift = true })); +} -- 2.51.2 From 5b24aebcab75e52b7c04c507d9a7c0e8785b8855 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 10:18:56 -0800 Subject: [PATCH 209/349] update to use new RemapSet --- src/Surface.zig | 12 +- src/config/Config.zig | 113 +------------- src/input.zig | 1 - src/input/KeyRemap.zig | 327 ----------------------------------------- src/input/key_mods.zig | 223 +++++++++++++++++++++++++++- 5 files changed, 232 insertions(+), 444 deletions(-) delete mode 100644 src/input/KeyRemap.zig diff --git a/src/Surface.zig b/src/Surface.zig index c0c933d1c..cc727826f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -333,7 +333,7 @@ const DerivedConfig = struct { notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, notify_on_command_finish_action: configpkg.Config.NotifyOnCommandFinishAction, notify_on_command_finish_after: Duration, - key_remaps: []const input.KeyRemap, + key_remaps: input.KeyRemapSet, const Link = struct { regex: oni.Regex, @@ -409,7 +409,7 @@ const DerivedConfig = struct { .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", - .key_remaps = config.@"key-remap".value.items, + .key_remaps = try config.@"key-remap".clone(alloc), // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -2582,8 +2582,8 @@ pub fn keyEventIsBinding( ) bool { // Apply key remappings for consistency with keyCallback var event = event_orig; - if (self.config.key_remaps.len > 0) { - event.mods = input.KeyRemap.applyRemaps(self.config.key_remaps, event_orig.mods); + if (self.config.key_remaps.isRemapped(event_orig.mods)) { + event.mods = self.config.key_remaps.apply(event_orig.mods); } switch (event.action) { @@ -2620,8 +2620,8 @@ pub fn keyCallback( // Apply key remappings to transform modifiers before any processing. // This allows users to remap modifier keys at the app level. var event = event_orig; - if (self.config.key_remaps.len > 0) { - event.mods = input.KeyRemap.applyRemaps(self.config.key_remaps, event_orig.mods); + if (self.config.key_remaps.isRemapped(event_orig.mods)) { + event.mods = self.config.key_remaps.apply(event_orig.mods); } // Crash metadata in case we crash in here diff --git a/src/config/Config.zig b/src/config/Config.zig index 0d883fddc..3ff31ae0e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -37,7 +37,7 @@ const RepeatableStringMap = @import("RepeatableStringMap.zig"); pub const Path = @import("path.zig").Path; pub const RepeatablePath = @import("path.zig").RepeatablePath; const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig"); -const KeyRemapSet = @import("key_mods.zig").RemapSet; +const KeyRemapSet = @import("../input/key_mods.zig").RemapSet; // We do this instead of importing all of terminal/main.zig to // limit the dependency graph. This is important because some things @@ -1796,7 +1796,7 @@ keybind: Keybinds = .{}, /// /// Currently only supported on macOS. Linux/GTK support is planned for /// a future release. -@"key-remap": RepeatableKeyRemap = .{}, +@"key-remap": KeyRemapSet = .empty, /// Horizontal window padding. This applies padding between the terminal cells /// and the left and right window borders. The value is in points, meaning that @@ -4477,6 +4477,9 @@ pub fn finalize(self: *Config) !void { } self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0); + + // Finalize key remapping set for efficient lookups + self.@"key-remap".finalize(); } /// Callback for src/cli/args.zig to allow us to handle special cases @@ -8062,112 +8065,6 @@ pub const RepeatableLink = struct { } }; -/// RepeatableKeyRemap is used for the key-remap configuration which -/// allows remapping modifier keys within Ghostty. -pub const RepeatableKeyRemap = struct { - const Self = @This(); - - value: std.ArrayListUnmanaged(inputpkg.KeyRemap) = .empty, - - pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void { - // Empty/unset input clears the list - const input = input_ orelse ""; - if (input.len == 0) { - self.value.clearRetainingCapacity(); - return; - } - - // Parse the key remap - const remap = inputpkg.KeyRemap.parse(input) catch |err| switch (err) { - error.InvalidFormat => return error.InvalidValue, - error.InvalidModifier => return error.InvalidValue, - }; - - // Reserve space and append - try self.value.ensureUnusedCapacity(alloc, 1); - self.value.appendAssumeCapacity(remap); - } - - /// Deep copy of the struct. Required by Config. - pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { - return .{ - .value = try self.value.clone(alloc), - }; - } - - /// Compare if two values are equal. Required by Config. - pub fn equal(self: Self, other: Self) bool { - if (self.value.items.len != other.value.items.len) return false; - for (self.value.items, other.value.items) |a, b| { - if (!a.equal(b)) return false; - } - return true; - } - - /// Used by Formatter - pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { - if (self.value.items.len == 0) { - try formatter.formatEntry(void, {}); - return; - } - - for (self.value.items) |item| { - // Format as "from=to" - var buf: [64]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - writer.print("{s}={s}", .{ @tagName(item.from), @tagName(item.to) }) catch - return error.OutOfMemory; - try formatter.formatEntry([]const u8, fbs.getWritten()); - } - } - - test "RepeatableKeyRemap parseCLI" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: RepeatableKeyRemap = .{}; - - try list.parseCLI(alloc, "ctrl=super"); - try testing.expectEqual(@as(usize, 1), list.value.items.len); - try testing.expectEqual(inputpkg.KeyRemap.ModKey.ctrl, list.value.items[0].from); - try testing.expectEqual(inputpkg.KeyRemap.ModKey.super, list.value.items[0].to); - - try list.parseCLI(alloc, "alt=shift"); - try testing.expectEqual(@as(usize, 2), list.value.items.len); - } - - test "RepeatableKeyRemap parseCLI clear" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: RepeatableKeyRemap = .{}; - - try list.parseCLI(alloc, "ctrl=super"); - try testing.expectEqual(@as(usize, 1), list.value.items.len); - - // Empty clears the list - try list.parseCLI(alloc, ""); - try testing.expectEqual(@as(usize, 0), list.value.items.len); - } - - test "RepeatableKeyRemap parseCLI invalid" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: RepeatableKeyRemap = .{}; - - try testing.expectError(error.InvalidValue, list.parseCLI(alloc, "foo=bar")); - try testing.expectError(error.InvalidValue, list.parseCLI(alloc, "ctrl")); - } -}; - /// Options for copy on select behavior. pub const CopyOnSelect = enum { /// Disables copy on select entirely. diff --git a/src/input.zig b/src/input.zig index 232e03580..bad3ac1f3 100644 --- a/src/input.zig +++ b/src/input.zig @@ -33,7 +33,6 @@ pub const ScrollMods = mouse.ScrollMods; pub const SplitFocusDirection = Binding.Action.SplitFocusDirection; pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; pub const Trigger = Binding.Trigger; -pub const KeyRemap = @import("input/KeyRemap.zig"); // Keymap is only available on macOS right now. We could implement it // in theory for XKB too on Linux but we don't need it right now. diff --git a/src/input/KeyRemap.zig b/src/input/KeyRemap.zig deleted file mode 100644 index de13ab70d..000000000 --- a/src/input/KeyRemap.zig +++ /dev/null @@ -1,327 +0,0 @@ -//! Key remapping support for modifier keys within Ghostty. -//! -//! This module allows users to remap modifier keys (ctrl, alt, shift, super) -//! at the application level without affecting system-wide settings. -//! -//! Syntax: `key-remap = from=to` -//! -//! Examples: -//! key-remap = ctrl=super -- Ctrl acts as Super -//! key-remap = left_alt=ctrl -- Left Alt acts as Ctrl -//! -//! Remapping is one-way and non-transitive: -//! - `ctrl=super` means Ctrlโ†’Super, but Super stays Super -//! - `ctrl=super` + `alt=ctrl` means Altโ†’Ctrl (NOT Super) - -const KeyRemap = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const key = @import("key.zig"); -const Mods = key.Mods; - -from: ModKey, -to: ModKey, - -pub const ModKey = enum { - ctrl, - alt, - shift, - super, - left_ctrl, - left_alt, - left_shift, - left_super, - right_ctrl, - right_alt, - right_shift, - right_super, - - pub fn isGeneric(self: ModKey) bool { - return switch (self) { - .ctrl, .alt, .shift, .super => true, - else => false, - }; - } - - pub fn parse(input: []const u8) ?ModKey { - const map = std.StaticStringMap(ModKey).initComptime(.{ - .{ "ctrl", .ctrl }, - .{ "control", .ctrl }, - .{ "alt", .alt }, - .{ "opt", .alt }, - .{ "option", .alt }, - .{ "shift", .shift }, - .{ "super", .super }, - .{ "cmd", .super }, - .{ "command", .super }, - .{ "left_ctrl", .left_ctrl }, - .{ "left_control", .left_ctrl }, - .{ "leftctrl", .left_ctrl }, - .{ "leftcontrol", .left_ctrl }, - .{ "left_alt", .left_alt }, - .{ "left_opt", .left_alt }, - .{ "left_option", .left_alt }, - .{ "leftalt", .left_alt }, - .{ "leftopt", .left_alt }, - .{ "leftoption", .left_alt }, - .{ "left_shift", .left_shift }, - .{ "leftshift", .left_shift }, - .{ "left_super", .left_super }, - .{ "left_cmd", .left_super }, - .{ "left_command", .left_super }, - .{ "leftsuper", .left_super }, - .{ "leftcmd", .left_super }, - .{ "leftcommand", .left_super }, - .{ "right_ctrl", .right_ctrl }, - .{ "right_control", .right_ctrl }, - .{ "rightctrl", .right_ctrl }, - .{ "rightcontrol", .right_ctrl }, - .{ "right_alt", .right_alt }, - .{ "right_opt", .right_alt }, - .{ "right_option", .right_alt }, - .{ "rightalt", .right_alt }, - .{ "rightopt", .right_alt }, - .{ "rightoption", .right_alt }, - .{ "right_shift", .right_shift }, - .{ "rightshift", .right_shift }, - .{ "right_super", .right_super }, - .{ "right_cmd", .right_super }, - .{ "right_command", .right_super }, - .{ "rightsuper", .right_super }, - .{ "rightcmd", .right_super }, - .{ "rightcommand", .right_super }, - }); - - var buf: [32]u8 = undefined; - if (input.len > buf.len) return null; - const lower = std.ascii.lowerString(&buf, input); - return map.get(lower); - } -}; - -pub fn parse(input: []const u8) !KeyRemap { - const eql_idx = std.mem.indexOf(u8, input, "=") orelse - return error.InvalidFormat; - - const from_str = std.mem.trim(u8, input[0..eql_idx], " \t"); - const to_str = std.mem.trim(u8, input[eql_idx + 1 ..], " \t"); - - if (from_str.len == 0 or to_str.len == 0) { - return error.InvalidFormat; - } - - const from = ModKey.parse(from_str) orelse return error.InvalidModifier; - const to = ModKey.parse(to_str) orelse return error.InvalidModifier; - - return .{ .from = from, .to = to }; -} - -pub fn apply(self: KeyRemap, mods: Mods) ?Mods { - var result = mods; - var matched = false; - - switch (self.from) { - .ctrl => if (mods.ctrl) { - result.ctrl = false; - matched = true; - }, - .left_ctrl => if (mods.ctrl and mods.sides.ctrl == .left) { - result.ctrl = false; - matched = true; - }, - .right_ctrl => if (mods.ctrl and mods.sides.ctrl == .right) { - result.ctrl = false; - matched = true; - }, - .alt => if (mods.alt) { - result.alt = false; - matched = true; - }, - .left_alt => if (mods.alt and mods.sides.alt == .left) { - result.alt = false; - matched = true; - }, - .right_alt => if (mods.alt and mods.sides.alt == .right) { - result.alt = false; - matched = true; - }, - .shift => if (mods.shift) { - result.shift = false; - matched = true; - }, - .left_shift => if (mods.shift and mods.sides.shift == .left) { - result.shift = false; - matched = true; - }, - .right_shift => if (mods.shift and mods.sides.shift == .right) { - result.shift = false; - matched = true; - }, - .super => if (mods.super) { - result.super = false; - matched = true; - }, - .left_super => if (mods.super and mods.sides.super == .left) { - result.super = false; - matched = true; - }, - .right_super => if (mods.super and mods.sides.super == .right) { - result.super = false; - matched = true; - }, - } - - if (!matched) return null; - - switch (self.to) { - .ctrl, .left_ctrl => { - result.ctrl = true; - result.sides.ctrl = .left; - }, - .right_ctrl => { - result.ctrl = true; - result.sides.ctrl = .right; - }, - .alt, .left_alt => { - result.alt = true; - result.sides.alt = .left; - }, - .right_alt => { - result.alt = true; - result.sides.alt = .right; - }, - .shift, .left_shift => { - result.shift = true; - result.sides.shift = .left; - }, - .right_shift => { - result.shift = true; - result.sides.shift = .right; - }, - .super, .left_super => { - result.super = true; - result.sides.super = .left; - }, - .right_super => { - result.super = true; - result.sides.super = .right; - }, - } - - return result; -} - -/// Apply remaps non-transitively: each remap checks the original mods. -pub fn applyRemaps(remaps: []const KeyRemap, mods: Mods) Mods { - var result = mods; - for (remaps) |remap| { - if (remap.apply(mods)) |_| { - switch (remap.from) { - .ctrl, .left_ctrl, .right_ctrl => result.ctrl = false, - .alt, .left_alt, .right_alt => result.alt = false, - .shift, .left_shift, .right_shift => result.shift = false, - .super, .left_super, .right_super => result.super = false, - } - switch (remap.to) { - .ctrl, .left_ctrl => { - result.ctrl = true; - result.sides.ctrl = .left; - }, - .right_ctrl => { - result.ctrl = true; - result.sides.ctrl = .right; - }, - .alt, .left_alt => { - result.alt = true; - result.sides.alt = .left; - }, - .right_alt => { - result.alt = true; - result.sides.alt = .right; - }, - .shift, .left_shift => { - result.shift = true; - result.sides.shift = .left; - }, - .right_shift => { - result.shift = true; - result.sides.shift = .right; - }, - .super, .left_super => { - result.super = true; - result.sides.super = .left; - }, - .right_super => { - result.super = true; - result.sides.super = .right; - }, - } - } - } - return result; -} - -pub fn clone(self: KeyRemap, alloc: Allocator) Allocator.Error!KeyRemap { - _ = alloc; - return self; -} - -pub fn equal(self: KeyRemap, other: KeyRemap) bool { - return self.from == other.from and self.to == other.to; -} - -test "ModKey.parse" { - const testing = std.testing; - - try testing.expectEqual(ModKey.ctrl, ModKey.parse("ctrl").?); - try testing.expectEqual(ModKey.ctrl, ModKey.parse("control").?); - try testing.expectEqual(ModKey.ctrl, ModKey.parse("CTRL").?); - try testing.expectEqual(ModKey.alt, ModKey.parse("alt").?); - try testing.expectEqual(ModKey.super, ModKey.parse("cmd").?); - try testing.expectEqual(ModKey.left_ctrl, ModKey.parse("left_ctrl").?); - try testing.expectEqual(ModKey.right_alt, ModKey.parse("right_alt").?); - try testing.expect(ModKey.parse("foo") == null); -} - -test "parse" { - const testing = std.testing; - - const remap = try parse("ctrl=super"); - try testing.expectEqual(ModKey.ctrl, remap.from); - try testing.expectEqual(ModKey.super, remap.to); - - const spaced = try parse(" ctrl = super "); - try testing.expectEqual(ModKey.ctrl, spaced.from); - - try testing.expectError(error.InvalidFormat, parse("ctrl")); - try testing.expectError(error.InvalidModifier, parse("foo=bar")); -} - -test "apply" { - const testing = std.testing; - - const remap = try parse("ctrl=super"); - const mods = Mods{ .ctrl = true }; - const result = remap.apply(mods).?; - - try testing.expect(!result.ctrl); - try testing.expect(result.super); - try testing.expect(remap.apply(Mods{ .alt = true }) == null); -} - -test "applyRemaps non-transitive" { - const testing = std.testing; - - const remaps = [_]KeyRemap{ - try parse("ctrl=super"), - try parse("alt=ctrl"), - }; - - const mods = Mods{ .alt = true }; - const result = applyRemaps(&remaps, mods); - - try testing.expect(!result.alt); - try testing.expect(result.ctrl); - try testing.expect(!result.super); -} diff --git a/src/input/key_mods.zig b/src/input/key_mods.zig index 885fe357b..be759ae1b 100644 --- a/src/input/key_mods.zig +++ b/src/input/key_mods.zig @@ -232,8 +232,21 @@ pub const RemapSet = struct { InvalidMod, }; - pub fn deinit(self: *RemapSet, alloc: Allocator) void { - self.map.deinit(alloc); + /// Parse from CLI input. Required by Config. + pub fn parseCLI(self: *RemapSet, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse ""; + + // Empty value resets the set + if (value.len == 0) { + self.map.clearRetainingCapacity(); + self.mask = .{}; + return; + } + + self.parse(alloc, value) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.MissingAssignment, error.InvalidMod => return error.InvalidValue, + }; } /// Parse a modifier remap and add it to the set. @@ -294,6 +307,10 @@ pub const RemapSet = struct { self.mask.update(from_right); } + pub fn deinit(self: *RemapSet, alloc: Allocator) void { + self.map.deinit(alloc); + } + /// Must be called prior to any remappings so that the mapping /// is sorted properly. Otherwise, you will get invalid results. pub fn finalize(self: *RemapSet) void { @@ -317,6 +334,67 @@ pub const RemapSet = struct { self.map.sort(Context{ .keys = self.map.keys() }); } + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const RemapSet, alloc: Allocator) Allocator.Error!RemapSet { + return .{ + .map = try self.map.clone(alloc), + .mask = self.mask, + }; + } + + /// Compare if two RemapSets are equal. Required by Config. + pub fn equal(self: RemapSet, other: RemapSet) bool { + if (self.map.count() != other.map.count()) return false; + + var it = self.map.iterator(); + while (it.next()) |entry| { + const other_value = other.map.get(entry.key_ptr.*) orelse return false; + if (!entry.value_ptr.equal(other_value)) return false; + } + + return true; + } + + /// Used by Formatter. Required by Config. + pub fn formatEntry(self: RemapSet, formatter: anytype) !void { + if (self.map.count() == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var it = self.map.iterator(); + while (it.next()) |entry| { + const from = entry.key_ptr.*; + const to = entry.value_ptr.*; + + var buf: [64]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + formatMod(writer, from) catch return error.OutOfMemory; + writer.writeByte('=') catch return error.OutOfMemory; + formatMod(writer, to) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, fbs.getWritten()); + } + } + + fn formatMod(writer: anytype, mods: Mods) !void { + // Check which mod is set and format it with optional side prefix + inline for (.{ "shift", "ctrl", "alt", "super" }) |name| { + if (@field(mods, name)) { + const side = @field(mods.sides, name); + if (side == .right) { + try writer.writeAll("right_"); + } else { + // Only write left_ if we need to distinguish + // For now, always write left_ if it's a sided mapping + try writer.writeAll("left_"); + } + try writer.writeAll(name); + return; + } + } + } + /// Parses a single mode in a single remapping string. E.g. /// `ctrl` or `left_shift`. fn parseMod(input: []const u8) error{InvalidMod}!struct { Mod, ?Mod.Side } { @@ -599,3 +677,144 @@ test "RemapSet: isRemapped checks mask" { try testing.expect(!set.isRemapped(.{ .alt = true })); try testing.expect(!set.isRemapped(.{ .shift = true })); } + +test "RemapSet: clone creates independent copy" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "ctrl=super"); + set.finalize(); + + var cloned = try set.clone(alloc); + defer cloned.deinit(alloc); + + try testing.expect(set.equal(cloned)); + try testing.expect(cloned.isRemapped(.{ .ctrl = true })); +} + +test "RemapSet: equal compares correctly" { + const testing = std.testing; + const alloc = testing.allocator; + + var set1: RemapSet = .empty; + defer set1.deinit(alloc); + + var set2: RemapSet = .empty; + defer set2.deinit(alloc); + + try testing.expect(set1.equal(set2)); + + try set1.parse(alloc, "ctrl=super"); + try testing.expect(!set1.equal(set2)); + + try set2.parse(alloc, "ctrl=super"); + try testing.expect(set1.equal(set2)); + + try set1.parse(alloc, "alt=shift"); + try testing.expect(!set1.equal(set2)); +} + +test "RemapSet: parseCLI basic" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parseCLI(alloc, "ctrl=super"); + try testing.expectEqual(@as(usize, 2), set.map.count()); +} + +test "RemapSet: parseCLI empty clears" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parseCLI(alloc, "ctrl=super"); + try testing.expectEqual(@as(usize, 2), set.map.count()); + + try set.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), set.map.count()); +} + +test "RemapSet: parseCLI invalid" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "foo=bar")); + try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "ctrl")); +} + +test "RemapSet: formatEntry empty" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + const set: RemapSet = .empty; + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + try testing.expectEqualSlices(u8, "key-remap = \n", buf.written()); +} + +test "RemapSet: formatEntry single sided" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var set: RemapSet = .empty; + defer set.deinit(testing.allocator); + + try set.parse(testing.allocator, "left_ctrl=super"); + set.finalize(); + + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + try testing.expectEqualSlices(u8, "key-remap = left_ctrl=left_super\n", buf.written()); +} + +test "RemapSet: formatEntry unsided creates two entries" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var set: RemapSet = .empty; + defer set.deinit(testing.allocator); + + try set.parse(testing.allocator, "ctrl=super"); + set.finalize(); + + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + // Unsided creates both left and right mappings + const written = buf.written(); + try testing.expect(std.mem.indexOf(u8, written, "left_ctrl=left_super") != null); + try testing.expect(std.mem.indexOf(u8, written, "right_ctrl=left_super") != null); +} + +test "RemapSet: formatEntry right sided" { + const testing = std.testing; + const formatterpkg = @import("../config/formatter.zig"); + + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + var set: RemapSet = .empty; + defer set.deinit(testing.allocator); + + try set.parse(testing.allocator, "left_alt=right_ctrl"); + set.finalize(); + + try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer)); + try testing.expectEqualSlices(u8, "key-remap = left_alt=right_ctrl\n", buf.written()); +} -- 2.51.2 From 21d9d89d32b2da0426fcecd6eb3ab3723e648367 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 10:26:45 -0800 Subject: [PATCH 210/349] input: RemapSet should support aliased mods --- src/input/key_mods.zig | 102 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/src/input/key_mods.zig b/src/input/key_mods.zig index be759ae1b..35e1c1038 100644 --- a/src/input/key_mods.zig +++ b/src/input/key_mods.zig @@ -410,11 +410,21 @@ pub const RemapSet = struct { input, }; + const mod: Mod = if (std.meta.stringToEnum( + Mod, + mod_str, + )) |mod| mod else mod: { + inline for (alias) |pair| { + if (std.mem.eql(u8, mod_str, pair[0])) { + break :mod pair[1]; + } + } + + return error.InvalidMod; + }; + return .{ - std.meta.stringToEnum( - Mod, - mod_str, - ) orelse return error.InvalidMod, + mod, if (side_str.len > 0) std.meta.stringToEnum( Mod.Side, side_str, @@ -753,6 +763,90 @@ test "RemapSet: parseCLI invalid" { try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "ctrl")); } +test "RemapSet: parse aliased modifiers" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "cmd=ctrl"); + set.finalize(); + + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + try testing.expectEqual(left_ctrl, set.apply(left_super)); +} + +test "RemapSet: parse aliased modifiers command" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "command=alt"); + set.finalize(); + + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + try testing.expectEqual(left_alt, set.apply(left_super)); +} + +test "RemapSet: parse aliased modifiers opt and option" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "opt=super"); + set.finalize(); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_alt)); + + set.deinit(alloc); + set = .empty; + + try set.parse(alloc, "option=shift"); + set.finalize(); + + const left_shift: Mods = .{ .shift = true, .sides = .{ .shift = .left } }; + try testing.expectEqual(left_shift, set.apply(left_alt)); +} + +test "RemapSet: parse aliased modifiers control" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "control=super"); + set.finalize(); + + const left_ctrl: Mods = .{ .ctrl = true, .sides = .{ .ctrl = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_ctrl)); +} + +test "RemapSet: parse aliased modifiers on target side" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: RemapSet = .empty; + defer set.deinit(alloc); + + try set.parse(alloc, "alt=cmd"); + set.finalize(); + + const left_alt: Mods = .{ .alt = true, .sides = .{ .alt = .left } }; + const left_super: Mods = .{ .super = true, .sides = .{ .super = .left } }; + try testing.expectEqual(left_super, set.apply(left_alt)); +} + test "RemapSet: formatEntry empty" { const testing = std.testing; const formatterpkg = @import("../config/formatter.zig"); -- 2.51.2 From a6d36b5e6d8c3dbf8bdb8d57bab8ed893eed11cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 10:45:45 -0800 Subject: [PATCH 211/349] config: add more details to the key-remap feature --- src/config/Config.zig | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 3ff31ae0e..2f0bef6ff 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1790,12 +1790,18 @@ keybind: Keybinds = .{}, /// may still produce `รฅ` even if `option` is remapped to `ctrl`. /// /// * Generic modifiers (e.g. `ctrl`) match both left and right physical keys. -/// Use sided names (e.g. `left_ctrl`) to remap only one side. /// +/// Use sided names (e.g. `left_ctrl`) to remap only one side. /// -/// This configuration can be repeated to specify multiple remaps. +/// There are other edge case scenarios that may not behave as expected +/// but are working as intended the way this feature is designed: +/// +/// * On macOS, bindings in the main menu will trigger before any remapping +/// is done. This is because macOS itself handles menu activation and +/// this happens before Ghostty receives the key event. To workaround +/// this, you should unbind the menu items and rebind them using your +/// desired modifier. /// -/// Currently only supported on macOS. Linux/GTK support is planned for -/// a future release. +/// This configuration can be repeated to specify multiple remaps. @"key-remap": KeyRemapSet = .empty, /// Horizontal window padding. This applies padding between the terminal cells -- 2.51.2 From fb1268a9089c3be639241402016842fb9e4c9f0d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 17 Dec 2025 12:57:27 -0600 Subject: [PATCH 212/349] benchmark: add doNotOptimizeAway to OSC benchmark --- src/benchmark/OscParser.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/benchmark/OscParser.zig b/src/benchmark/OscParser.zig index 6243aba7d..bd82a3534 100644 --- a/src/benchmark/OscParser.zig +++ b/src/benchmark/OscParser.zig @@ -101,7 +101,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { }; for (osc_buf[0..len]) |c| self.parser.next(c); - _ = self.parser.end(std.ascii.control_code.bel); + std.mem.doNotOptimizeAway(self.parser.end(std.ascii.control_code.bel)); self.parser.reset(); } } -- 2.51.2 From d32a94a06ac3172fc86d0b366a48dbcee853daa6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 17 Dec 2025 17:36:18 -0600 Subject: [PATCH 213/349] core: add new OSC parser This replaces the OSC parser with one that only uses a state machine to determine which OSC is being handled, rather than parsing the whole OSC. Once the OSC command is determined the remainder of the data is stored in a buffer until the terminator is found. The data is then parsed to determine the final OSC command. --- src/terminal/kitty/color.zig | 4 + src/terminal/osc.zig | 2144 +++++++++++++++------------------- 2 files changed, 945 insertions(+), 1203 deletions(-) diff --git a/src/terminal/kitty/color.zig b/src/terminal/kitty/color.zig index deeabcfb7..c1072c390 100644 --- a/src/terminal/kitty/color.zig +++ b/src/terminal/kitty/color.zig @@ -17,6 +17,10 @@ pub const OSC = struct { /// request. terminator: Terminator = .st, + pub fn deinit(self: *OSC, alloc: std.mem.Allocator) void { + self.list.deinit(alloc); + } + /// We don't currently support encoding this to C in any way. pub const C = void; diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index f62b7a6cd..d81244b9f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -309,170 +309,90 @@ pub const Terminator = enum { }; pub const Parser = struct { + /// Maximum size of a "normal" OSC. + const MAX_BUF = 2048; + /// Optional allocator used to accept data longer than MAX_BUF. /// This only applies to some commands (e.g. OSC 52) that can /// reasonably exceed MAX_BUF. - alloc: ?Allocator, + alloc: ?Allocator = null, /// Current state of the parser. - state: State, - - /// Current command of the parser, this accumulates. - command: Command, + state: State = .start, - /// Buffer that stores the input we see for a single OSC command. - /// Slices in Command are offsets into this buffer. - buf: [MAX_BUF]u8, - buf_start: usize, - buf_idx: usize, - buf_dynamic: ?*std.ArrayListUnmanaged(u8), + /// Buffer for temporary storage of OSC data + buffer: [MAX_BUF]u8 = undefined, + /// Fixed writer for accumulating OSC data + fixed: ?std.Io.Writer = null, + /// Allocating writer for accumulating OSC data + allocating: ?std.Io.Writer.Allocating = null, + /// Pointer to the active writer for accumulating OSC data + writer: ?*std.Io.Writer = null, - /// True when a command is complete/valid to return. - complete: bool, - - /// Temporary state that is dependent on the current state. - temp_state: union { - /// Current string parameter being populated - str: *[:0]const u8, - - /// Current numeric parameter being populated - num: u16, - - /// Temporary state for key/value pairs - key: []const u8, - }, - - // Maximum length of a single OSC command. This is the full OSC command - // sequence length (excluding ESC ]). This is arbitrary, I couldn't find - // any definitive resource on how long this should be. - // - // NOTE: This does mean certain OSC sequences such as OSC 8 (hyperlinks) - // won't work if their parameters are larger than fit in the buffer. - const MAX_BUF = 2048; + /// The command that is the result of parsing. + command: Command = .invalid, pub const State = enum { - empty, + start, invalid, - swallow, - // Command prefixes. We could just accumulate and compare (mem.eql) - // but the state space is small enough that we just build it up this way. + // OSC command prefixes. Not all of these are valid OSCs, but may be + // needed to "bridge" to a valid OSC (e.g. to support OSC 777 we need to + // have a state "77" even though there is no OSC 77). @"0", @"1", + @"2", + @"4", + @"5", + @"7", + @"8", + @"9", @"10", - @"104", @"11", @"12", @"13", - @"133", @"14", @"15", @"16", @"17", @"18", @"19", - @"2", @"21", @"22", - @"4", - @"5", @"52", - @"7", @"77", + @"104", + @"110", + @"111", + @"112", + @"113", + @"114", + @"115", + @"116", + @"117", + @"118", + @"119", + @"133", @"777", - @"8", - @"9", - - // We're in a semantic prompt OSC command but we aren't sure - // what the command is yet, i.e. `133;` - semantic_prompt, - semantic_option_start, - semantic_option_key, - semantic_option_value, - semantic_exit_code_start, - semantic_exit_code, - - // Get/set clipboard states - clipboard_kind, - clipboard_kind_end, - - // OSC color operation. - osc_color, - - // Hyperlinks - hyperlink_param_key, - hyperlink_param_value, - hyperlink_uri, - - // rxvt extension. Only used for OSC 777 and only the value "notify" is - // supported - rxvt_extension, - - // Title of a desktop notification - notification_title, - - // Expect a string parameter. param_str must be set as well as - // buf_start. - string, - - // A string that can grow beyond MAX_BUF. This uses the allocator. - // If the parser has no allocator then it is treated as if the - // buffer is full. - allocable_string, - - // Kitty color protocol - // https://sw.kovidgoyal.net/kitty/color-stack/#id1 - kitty_color_protocol_key, - kitty_color_protocol_value, - - // OSC 9 is used by ConEmu and iTerm2 for different things. - // iTerm2 uses it to post a notification[1]. - // ConEmu uses it to implement many custom functions[2]. - // - // Some Linux applications (namely systemd and flatpak) have - // adopted the ConEmu implementation but this causes bogus - // notifications on iTerm2 compatible terminal emulators. - // - // Ghostty supports both by disallowing ConEmu-specific commands - // from being shown as desktop notifications. - // - // [1]: https://iterm2.com/documentation-escape-codes.html - // [2]: https://conemu.github.io/en/AnsiEscapeCodes.html#OSC_Operating_system_commands - osc_9, - - // ConEmu specific substates - conemu_sleep, - conemu_sleep_value, - conemu_message_box, - conemu_tab, - conemu_tab_txt, - conemu_progress_prestate, - conemu_progress_state, - conemu_progress_prevalue, - conemu_progress_value, - conemu_guimacro, }; pub fn init(alloc: ?Allocator) Parser { var result: Parser = .{ .alloc = alloc, - .state = .empty, + .state = .start, + .fixed = null, + .allocating = null, + .writer = null, .command = .invalid, - .buf_start = 0, - .buf_idx = 0, - .buf_dynamic = null, - .complete = false, // Keeping all our undefined values together so we can // visually easily duplicate them in the Valgrind check below. - .buf = undefined, - .temp_state = undefined, + .buffer = undefined, }; if (std.valgrind.runningOnValgrind() > 0) { // Initialize our undefined fields so Valgrind can catch it. // https://github.com/ziglang/zig/issues/19148 - result.buf = undefined; - result.temp_state = undefined; + result.buffer = undefined; } return result; @@ -485,75 +405,108 @@ pub const Parser = struct { /// 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. - if (self.state == .empty) { - assert(self.buf_start == 0); - assert(self.buf_idx == 0); - assert(!self.complete); - assert(self.buf_dynamic == null); - return; - } + // If we set up an allocating writer, free up that memory. + if (self.allocating) |*allocating| allocating.deinit(); - // Some commands have their own memory management we need to clear. + // Handle any cleanup that individual OSCs require. switch (self.command) { - .kitty_color_protocol => |*v| v.list.deinit(self.alloc.?), - .color_operation => |*v| v.requests.deinit(self.alloc.?), - else => {}, + .kitty_color_protocol => |*v| kitty_color_protocol: { + v.deinit(self.alloc orelse break :kitty_color_protocol); + }, + .change_window_icon, + .change_window_title, + .clipboard_contents, + .color_operation, + .conemu_change_tab_title, + .conemu_guimacro, + .conemu_progress_report, + .conemu_show_message_box, + .conemu_sleep, + .conemu_wait_input, + .end_of_command, + .end_of_input, + .hyperlink_end, + .hyperlink_start, + .invalid, + .mouse_shape, + .prompt_end, + .prompt_start, + .report_pwd, + .show_desktop_notification, + => {}, } - self.state = .empty; - self.buf_start = 0; - self.buf_idx = 0; + self.state = .start; + self.fixed = null; + self.allocating = null; + self.writer = null; self.command = .invalid; - self.complete = false; - if (self.buf_dynamic) |ptr| { - const alloc = self.alloc.?; - ptr.deinit(alloc); - alloc.destroy(ptr); - self.buf_dynamic = null; + + if (std.valgrind.runningOnValgrind() > 0) { + // Initialize our undefined fields so Valgrind can catch it. + // https://github.com/ziglang/zig/issues/19148 + self.buffer = undefined; } } - /// Consume the next character c and advance the parser state. - pub fn next(self: *Parser, c: u8) void { - // 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. - // - // We always keep space for 1 byte at the end to null-terminate - // values. - if (self.buf_idx >= self.buf.len - 1) { - @branchHint(.cold); - if (self.state != .invalid) { - log.warn( - "OSC sequence too long (> {d}), ignoring. state={}", - .{ self.buf.len, self.state }, - ); - } + /// Make sure that we have an allocator. If we don't, set the state to + /// invalid so that any additional OSC data is discarded. + pub inline fn ensureAllocator(self: *Parser) bool { + if (self.alloc != null) return true; + log.warn("An allocator is required to process OSC {t} but none was provided.", .{self.state}); + self.state = .invalid; + return false; + } - self.state = .invalid; + /// Set up a fixed Writer to collect the rest of the OSC data. + pub inline fn writeToFixed(self: *Parser) void { + self.fixed = .fixed(&self.buffer); + self.writer = &self.fixed.?; + } - // We have to do this here because it will never reach the - // switch statement below, since our buf_idx will always be - // too high after this. - self.complete = false; + /// Set up an allocating Writer to collect the rest of the OSC data. If we + /// don't have an allocator or setting up the allocator fails, fall back to + /// writing to a fixed buffer and hope that it's big enough. + pub inline fn writeToAllocating(self: *Parser) void { + const alloc = self.alloc orelse { + // We don't have an allocator - fall back to a fixed buffer and hope + // that it's big enough. + self.writeToFixed(); return; - } + }; + + self.allocating = std.Io.Writer.Allocating.initCapacity(alloc, 2048) catch { + // The allocator failed for some reason, fall back to a fixed buffer + // and hope that it's big enough. + self.writeToFixed(); + return; + }; - // We store everything in the buffer so we can do a better job - // logging if we get to an invalid command. - self.buf[self.buf_idx] = c; - self.buf_idx += 1; + self.writer = &self.allocating.?.writer; + } - // log.warn("state = {} c = {x}", .{ self.state, c }); + /// Consume the next character c and advance the parser state. + pub inline fn next(self: *Parser, c: u8) void { + // If the state becomes invalid for any reason, just discard + // any further input. + if (self.state == .invalid) return; + + // If a writer has been initialized, we just accumulate the rest of the + // OSC sequence in the writer's buffer and skip the state machine. + if (self.writer) |writer| { + writer.writeByte(c) catch |err| switch (err) { + // We have overflowed our buffer or had some other error, set the + // state to invalid so that we discard any further input. + error.WriteFailed => self.state = .invalid, + }; + return; + } switch (self.state) { - // If we get something during the invalid state, we've - // ruined our entry. - .invalid => self.complete = false, + // handled above, so should never be here + .invalid => unreachable, - .empty => switch (c) { + .start => switch (c) { '0' => self.state = .@"0", '1' => self.state = .@"1", '2' => self.state = .@"2", @@ -565,27 +518,13 @@ pub const Parser = struct { else => self.state = .invalid, }, - .swallow => {}, - .@"0" => switch (c) { - ';' => { - self.command = .{ .change_window_title = undefined }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"1" => switch (c) { - ';' => { - self.command = .{ .change_window_icon = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_icon }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), '0' => self.state = .@"10", '1' => self.state = .@"11", '2' => self.state = .@"12", @@ -600,390 +539,162 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => osc_10: { - if (self.alloc == null) { - log.warn("OSC 10 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_10; - } - self.command = .{ .color_operation = .{ - .op = .osc_10, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '4' => { - self.state = .@"104"; - // If we have an allocator, then we can complete the OSC104 - if (self.alloc != null) self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), + '4' => self.state = .@"104", else => self.state = .invalid, }, .@"104" => switch (c) { - ';' => osc_104: { - if (self.alloc == null) { - log.warn("OSC 104 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_104; - } - self.command = .{ - .color_operation = .{ - .op = .osc_104, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"11" => switch (c) { - ';' => osc_11: { - if (self.alloc == null) { - log.warn("OSC 11 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_11; - } - self.command = .{ .color_operation = .{ - .op = .osc_11, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '0'...'9' => blk: { - if (self.alloc == null) { - log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); - self.state = .invalid; - break :blk; - } + ';' => if (self.ensureAllocator()) self.writeToFixed(), + '0' => self.state = .@"110", + '1' => self.state = .@"111", + '2' => self.state = .@"112", + '3' => self.state = .@"113", + '4' => self.state = .@"114", + '5' => self.state = .@"115", + '6' => self.state = .@"116", + '7' => self.state = .@"117", + '8' => self.state = .@"118", + '9' => self.state = .@"119", + else => self.state = .invalid, + }, - self.command = .{ - .color_operation = .{ - .op = switch (c) { - '0' => .osc_110, - '1' => .osc_111, - '2' => .osc_112, - '3' => .osc_113, - '4' => .osc_114, - '5' => .osc_115, - '6' => .osc_116, - '7' => .osc_117, - '8' => .osc_118, - '9' => .osc_119, - else => unreachable, - }, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + .@"110" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"111" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"112" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"113" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"114" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"115" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"116" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"117" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"118" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), + else => self.state = .invalid, + }, + + .@"119" => switch (c) { + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"12" => switch (c) { - ';' => osc_12: { - if (self.alloc == null) { - log.warn("OSC 12 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_12; - } - self.command = .{ .color_operation = .{ - .op = .osc_12, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"13" => switch (c) { - ';' => osc_13: { - if (self.alloc == null) { - log.warn("OSC 13 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_13; - } - self.command = .{ .color_operation = .{ - .op = .osc_13, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), '3' => self.state = .@"133", else => self.state = .invalid, }, .@"133" => switch (c) { - ';' => self.state = .semantic_prompt, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"14" => switch (c) { - ';' => osc_14: { - if (self.alloc == null) { - log.warn("OSC 14 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_14; - } - self.command = .{ .color_operation = .{ - .op = .osc_14, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"15" => switch (c) { - ';' => osc_15: { - if (self.alloc == null) { - log.warn("OSC 15 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_15; - } - self.command = .{ .color_operation = .{ - .op = .osc_15, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"16" => switch (c) { - ';' => osc_16: { - if (self.alloc == null) { - log.warn("OSC 16 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_16; - } - self.command = .{ .color_operation = .{ - .op = .osc_16, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"17" => switch (c) { - ';' => osc_17: { - if (self.alloc == null) { - log.warn("OSC 17 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_17; - } - self.command = .{ .color_operation = .{ - .op = .osc_17, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"18" => switch (c) { - ';' => osc_18: { - if (self.alloc == null) { - log.warn("OSC 18 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_18; - } - self.command = .{ .color_operation = .{ - .op = .osc_18, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"19" => switch (c) { - ';' => osc_19: { - if (self.alloc == null) { - log.warn("OSC 19 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_19; - } - self.command = .{ .color_operation = .{ - .op = .osc_19, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, - .osc_color => {}, - .@"2" => switch (c) { + ';' => self.writeToFixed(), '1' => self.state = .@"21", '2' => self.state = .@"22", - ';' => { - self.command = .{ .change_window_title = undefined }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.change_window_title }; - self.buf_start = self.buf_idx; - }, else => self.state = .invalid, }, .@"21" => switch (c) { - ';' => kitty: { - 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 = .empty, - }, - }; - - self.temp_state = .{ .key = "" }; - self.state = .kitty_color_protocol_key; - self.complete = true; - self.buf_start = self.buf_idx; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, - .kitty_color_protocol_key => switch (c) { - ';' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.endKittyColorProtocolOption(.key_only, false); - self.state = .kitty_color_protocol_key; - self.buf_start = self.buf_idx; - }, - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .kitty_color_protocol_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .kitty_color_protocol_value => switch (c) { - ';' => { - self.endKittyColorProtocolOption(.key_and_value, false); - self.state = .kitty_color_protocol_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - .@"22" => switch (c) { - ';' => { - self.command = .{ .mouse_shape = undefined }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.mouse_shape.value }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"4" => switch (c) { - ';' => osc_4: { - if (self.alloc == null) { - log.info("OSC 4 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_4; - } - self.command = .{ - .color_operation = .{ - .op = .osc_4, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, .@"5" => switch (c) { - ';' => osc_5: { - if (self.alloc == null) { - log.info("OSC 5 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_5; - } - self.command = .{ - .color_operation = .{ - .op = .osc_5, - }, - }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.complete = true; - }, + ';' => if (self.ensureAllocator()) self.writeToFixed(), '2' => self.state = .@"52", else => self.state = .invalid, }, .@"52" => switch (c) { - ';' => { - self.command = .{ .clipboard_contents = undefined }; - self.state = .clipboard_kind; - }, - else => self.state = .invalid, - }, - - .clipboard_kind => switch (c) { - ';' => { - self.command.clipboard_contents.kind = 'c'; - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - - // See clipboard_kind_end - self.complete = true; - }, - else => { - self.command.clipboard_contents.kind = c; - self.state = .clipboard_kind_end; - }, - }, - - .clipboard_kind_end => switch (c) { - ';' => { - self.temp_state = .{ .str = &self.command.clipboard_contents.data }; - self.buf_start = self.buf_idx; - self.prepAllocableString(); - - // OSC 52 can have empty payloads (quoting xterm ctlseqs): - // "If the second parameter is neither a base64 string nor ?, - // then the selection is cleared." - self.complete = true; - }, + ';' => self.writeToAllocating(), else => self.state = .invalid, }, .@"7" => switch (c) { - ';' => { - self.command = .{ .report_pwd = .{ .value = "" } }; - self.complete = true; - self.state = .string; - self.temp_state = .{ .str = &self.command.report_pwd.value }; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), '7' => self.state = .@"77", else => self.state = .invalid, }, @@ -994,776 +705,791 @@ pub const Parser = struct { }, .@"777" => switch (c) { - ';' => { - self.state = .rxvt_extension; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, .@"8" => switch (c) { - ';' => { - self.command = .{ .hyperlink_start = .{ - .uri = "", - } }; - - self.state = .hyperlink_param_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .hyperlink_param_key => switch (c) { - ';' => { - self.complete = true; - self.state = .hyperlink_uri; - self.buf_start = self.buf_idx; - }, - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .hyperlink_param_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .hyperlink_param_value => switch (c) { - ':' => { - self.endHyperlinkOptionValue(); - self.state = .hyperlink_param_key; - self.buf_start = self.buf_idx; - }, - ';' => { - self.endHyperlinkOptionValue(); - self.state = .string; - self.temp_state = .{ .str = &self.command.hyperlink_start.uri }; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .hyperlink_uri => {}, - - .rxvt_extension => switch (c) { - 'a'...'z' => {}, - ';' => { - const ext = self.buf[self.buf_start .. self.buf_idx - 1]; - if (!std.mem.eql(u8, ext, "notify")) { - @branchHint(.cold); - log.warn("unknown rxvt extension: {s}", .{ext}); - self.state = .invalid; - return; - } - - self.command = .{ .show_desktop_notification = undefined }; - self.buf_start = self.buf_idx; - self.state = .notification_title; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, - .notification_title => switch (c) { - ';' => { - 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; - }, - else => {}, - }, - .@"9" => switch (c) { - ';' => { - self.buf_start = self.buf_idx; - self.state = .osc_9; - }, - else => self.state = .invalid, - }, - - .osc_9 => switch (c) { - '1' => { - self.state = .conemu_sleep; - // This will end up being either a ConEmu sleep OSC 9;1, - // or a desktop notification OSC 9 that begins with '1', so - // mark as complete. - self.complete = true; - }, - '2' => { - self.state = .conemu_message_box; - // This will end up being either a ConEmu message box OSC 9;2, - // or a desktop notification OSC 9 that begins with '2', so - // mark as complete. - self.complete = true; - }, - '3' => { - self.state = .conemu_tab; - // This will end up being either a ConEmu message box OSC 9;3, - // or a desktop notification OSC 9 that begins with '3', so - // mark as complete. - self.complete = true; - }, - '4' => { - self.state = .conemu_progress_prestate; - // This will end up being either a ConEmu progress report - // OSC 9;4, or a desktop notification OSC 9 that begins with - // '4', so mark as complete. - self.complete = true; - }, - '5' => { - // Note that sending an OSC 9 desktop notification that - // starts with 5 is impossible due to this. - self.state = .swallow; - self.command = .conemu_wait_input; - self.complete = true; - }, - '6' => { - self.state = .conemu_guimacro; - // This will end up being either a ConEmu GUI macro OSC 9;6, - // or a desktop notification OSC 9 that begins with '6', so - // mark as complete. - self.complete = true; - }, - - // Todo: parse out other ConEmu operating system commands. Even - // if we don't support them we probably don't want them showing - // up as desktop notifications. - - else => self.showDesktopNotification(), - }, - - .conemu_sleep => switch (c) { - ';' => { - self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } }; - self.buf_start = self.buf_idx; - self.complete = true; - self.state = .conemu_sleep_value; - }, - - // OSC 9;1 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_sleep_value => switch (c) { - else => self.complete = true, - }, - - .conemu_message_box => switch (c) { - ';' => { - self.command = .{ .conemu_show_message_box = undefined }; - self.temp_state = .{ .str = &self.command.conemu_show_message_box }; - self.buf_start = self.buf_idx; - self.complete = true; - self.prepAllocableString(); - }, - - // OSC 9;2 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_tab => switch (c) { - ';' => { - self.state = .conemu_tab_txt; - self.command = .{ .conemu_change_tab_title = .reset }; - self.buf_start = self.buf_idx; - self.complete = true; - }, - - // OSC 9;3 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_tab_txt => { - self.command = .{ .conemu_change_tab_title = .{ .value = undefined } }; - self.temp_state = .{ .str = &self.command.conemu_change_tab_title.value }; - self.complete = true; - self.prepAllocableString(); - }, - - .conemu_progress_prestate => switch (c) { - ';' => { - self.command = .{ .conemu_progress_report = .{ - .state = undefined, - } }; - self.state = .conemu_progress_state; - }, - - // OSC 9;4 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_state => switch (c) { - '0' => { - self.command.conemu_progress_report.state = .remove; - self.state = .swallow; - self.complete = true; - }, - '1' => { - self.command.conemu_progress_report.state = .set; - self.command.conemu_progress_report.progress = 0; - self.state = .conemu_progress_prevalue; - }, - '2' => { - self.command.conemu_progress_report.state = .@"error"; - self.complete = true; - self.state = .conemu_progress_prevalue; - }, - '3' => { - self.command.conemu_progress_report.state = .indeterminate; - self.complete = true; - self.state = .swallow; - }, - '4' => { - self.command.conemu_progress_report.state = .pause; - self.complete = true; - self.state = .conemu_progress_prevalue; - }, - - // OSC 9;4; is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_prevalue => switch (c) { - ';' => { - self.state = .conemu_progress_value; - }, - - // OSC 9;4;<0-4> is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .conemu_progress_value => switch (c) { - '0'...'9' => value: { - // No matter what substate we're in, a number indicates - // a completed ConEmu progress command. - self.complete = true; - - // If we aren't a set substate, then we don't care - // about the value. - const p = &self.command.conemu_progress_report; - switch (p.state) { - .remove, - .indeterminate, - => break :value, - .set, - .@"error", - .pause, - => {}, - } - - if (p.state == .set) - assert(p.progress != null) - else if (p.progress == null) - p.progress = 0; - - // If we're over 100% we're done. - if (p.progress.? >= 100) break :value; - - // If we're over 10 then any new digit forces us to - // be 100. - if (p.progress.? >= 10) - p.progress = 100 - else { - const d = std.fmt.charToDigit(c, 10) catch 0; - p.progress = @min(100, (p.progress.? * 10) + d); - } - }, - - else => { - self.state = .swallow; - self.complete = true; - }, - }, - - .conemu_guimacro => switch (c) { - ';' => { - self.command = .{ .conemu_guimacro = undefined }; - self.temp_state = .{ .str = &self.command.conemu_guimacro }; - self.buf_start = self.buf_idx; - self.state = .string; - self.complete = true; - }, - - // OSC 9;6 is a desktop - // notification. - else => self.showDesktopNotification(), - }, - - .semantic_prompt => switch (c) { - 'A' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_start = .{} }; - self.complete = true; - }, - - 'B' => { - self.state = .semantic_option_start; - self.command = .{ .prompt_end = {} }; - self.complete = true; - }, - - 'C' => { - self.state = .semantic_option_start; - self.command = .{ .end_of_input = .{} }; - self.complete = true; - }, - - 'D' => { - self.state = .semantic_exit_code_start; - self.command = .{ .end_of_command = .{} }; - self.complete = true; - }, - - else => self.state = .invalid, - }, - - .semantic_option_start => switch (c) { - ';' => { - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_option_key => switch (c) { - '=' => { - self.temp_state = .{ .key = self.buf[self.buf_start .. self.buf_idx - 1] }; - self.state = .semantic_option_value; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_option_value => switch (c) { - ';' => { - self.endSemanticOptionValue(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, - else => {}, - }, - - .semantic_exit_code_start => switch (c) { - ';' => { - // No longer complete, if ';' shows up we expect some code. - self.complete = false; - self.state = .semantic_exit_code; - self.temp_state = .{ .num = 0 }; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .semantic_exit_code => switch (c) { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { - self.complete = true; - - const idx = self.buf_idx - self.buf_start; - if (idx > 0) self.temp_state.num *|= 10; - self.temp_state.num +|= c - '0'; - }, - ';' => { - self.endSemanticExitCode(); - self.state = .semantic_option_key; - self.buf_start = self.buf_idx; - }, + ';' => self.writeToFixed(), else => self.state = .invalid, }, + } + } - .allocable_string => { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, c) catch { - self.state = .invalid; - return; - }; - - // Never consume buffer space for allocable strings - self.buf_idx -= 1; - - // We can complete at any time - self.complete = true; - }, + /// End the sequence and return the command, if any. If the return value + /// 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. + /// + /// 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 { + return switch (self.state) { + .start => null, + .invalid => null, + .@"0" => self.parseChangeWindowTitle(terminator_ch), + .@"1" => self.parseChangeWindowIcon(terminator_ch), + .@"2" => self.parseChangeWindowTitle(terminator_ch), + .@"4" => self.parseOscColor(terminator_ch), + .@"5" => self.parseOscColor(terminator_ch), + .@"7" => self.parseReportPwd(terminator_ch), + .@"8" => self.parseHyperlink(terminator_ch), + .@"9" => self.parseOsc9(terminator_ch), + .@"10" => self.parseOscColor(terminator_ch), + .@"11" => self.parseOscColor(terminator_ch), + .@"12" => self.parseOscColor(terminator_ch), + .@"13" => self.parseOscColor(terminator_ch), + .@"14" => self.parseOscColor(terminator_ch), + .@"15" => self.parseOscColor(terminator_ch), + .@"16" => self.parseOscColor(terminator_ch), + .@"17" => self.parseOscColor(terminator_ch), + .@"18" => self.parseOscColor(terminator_ch), + .@"19" => self.parseOscColor(terminator_ch), + .@"21" => self.parseKittyColorProtocol(terminator_ch), + .@"22" => self.parseMouseShape(terminator_ch), + .@"52" => self.parseClipboardOperation(terminator_ch), + .@"77" => null, + .@"104" => self.parseOscColor(terminator_ch), + .@"110" => self.parseOscColor(terminator_ch), + .@"111" => self.parseOscColor(terminator_ch), + .@"112" => self.parseOscColor(terminator_ch), + .@"113" => self.parseOscColor(terminator_ch), + .@"114" => self.parseOscColor(terminator_ch), + .@"115" => self.parseOscColor(terminator_ch), + .@"116" => self.parseOscColor(terminator_ch), + .@"117" => self.parseOscColor(terminator_ch), + .@"118" => self.parseOscColor(terminator_ch), + .@"119" => self.parseOscColor(terminator_ch), + .@"133" => self.parseSemanticPrompt(terminator_ch), + .@"777" => self.parseRxvtExtension(terminator_ch), + }; + } - .string => self.complete = true, - } + /// Parse OSC 0 and OSC 2 + fn parseChangeWindowTitle(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + self.command = .{ + .change_window_title = data[0 .. data.len - 1 :0], + }; + return &self.command; } - fn showDesktopNotification(self: *Parser) void { - self.command = .{ .show_desktop_notification = .{ - .title = "", - .body = undefined, - } }; - - self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; - self.state = .string; - // Set as complete as we've already seen one character that should be - // part of the notification. If we wait for another character to set - // `complete` when the state is `.string` we won't be able to send any - // single character notifications. - self.complete = true; + /// Parse OSC 1 + fn parseChangeWindowIcon(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + self.command = .{ + .change_window_icon = data[0 .. data.len - 1 :0], + }; + return &self.command; } - fn prepAllocableString(self: *Parser) void { - assert(self.buf_dynamic == null); - - // We need an allocator. If we don't have an allocator, we - // pretend we're just a fixed buffer string and hope we fit! + /// Parse OSCs 4, 5, 10-19, 104, 110-119 + fn parseOscColor(self: *Parser, terminator_ch: ?u8) ?*Command { const alloc = self.alloc orelse { - self.state = .string; - return; + self.state = .invalid; + return null; }; - - // Allocate our dynamic buffer - const list = alloc.create(std.ArrayListUnmanaged(u8)) catch { - self.state = .string; - return; + // If we've collected any extra data parse that, otherwise use an empty + // string. + const data = data: { + const writer = self.writer orelse break :data ""; + break :data writer.buffered(); + }; + // Check and make sure that we're parsing the correct OSCs + const op: osc_color.Operation = switch (self.state) { + .@"4" => .osc_4, + .@"5" => .osc_5, + .@"10" => .osc_10, + .@"11" => .osc_11, + .@"12" => .osc_12, + .@"13" => .osc_13, + .@"14" => .osc_14, + .@"15" => .osc_15, + .@"16" => .osc_16, + .@"17" => .osc_17, + .@"18" => .osc_18, + .@"19" => .osc_19, + .@"104" => .osc_104, + .@"110" => .osc_110, + .@"111" => .osc_111, + .@"112" => .osc_112, + .@"113" => .osc_113, + .@"114" => .osc_114, + .@"115" => .osc_115, + .@"116" => .osc_116, + .@"117" => .osc_117, + .@"118" => .osc_118, + .@"119" => .osc_119, + else => { + self.state = .invalid; + return null; + }, + }; + self.command = .{ + .color_operation = .{ + .op = op, + .requests = osc_color.parse(alloc, op, data) catch |err| list: { + log.info( + "failed to parse OSC {t} color request err={} data={s}", + .{ self.state, err, data }, + ); + break :list .{}; + }, + .terminator = .init(terminator_ch), + }, }; - list.* = .{}; + return &self.command; + } - self.buf_dynamic = list; - self.state = .allocable_string; + /// Parse OSC 7 + fn parseReportPwd(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + self.command = .{ + .report_pwd = .{ + .value = data[0 .. data.len - 1 :0], + }, + }; + return &self.command; } - fn endHyperlink(self: *Parser) void { - switch (self.command) { - .hyperlink_start => |*v| { - 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; - } + /// Parse OSC 8 hyperlinks + fn parseHyperlink(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + const s = std.mem.indexOfScalar(u8, data, ';') orelse { + self.state = .invalid; + return null; + }; - v.uri = value; + self.command = .{ + .hyperlink_start = .{ + .uri = data[s + 1 .. data.len - 1 :0], }, + }; - else => unreachable, + data[s] = 0; + const kvs = data[0 .. s + 1]; + std.mem.replaceScalar(u8, kvs, ':', 0); + var kv_start: usize = 0; + while (kv_start < kvs.len) { + const kv_end = std.mem.indexOfScalarPos(u8, kvs, kv_start + 1, 0) orelse break; + const kv = data[kv_start .. kv_end + 1]; + const v = std.mem.indexOfScalar(u8, kv, '=') orelse break; + const key = kv[0..v]; + const value = kv[v + 1 .. kv.len - 1 :0]; + if (std.mem.eql(u8, key, "id")) { + if (value.len > 0) self.command.hyperlink_start.id = value; + } else { + log.warn("unknown hyperlink option: '{s}'", .{key}); + } + kv_start = kv_end + 1; } - } - - fn endHyperlinkOptionValue(self: *Parser) void { - const value: [:0]const u8 = if (self.buf_start == self.buf_idx) - "" - 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) { - .hyperlink_start => |*v| { - // We treat empty IDs as null ids so that we can - // auto-assign. - if (value.len > 0) v.id = value; - }, - else => {}, + if (self.command.hyperlink_start.uri.len == 0) { + if (self.command.hyperlink_start.id != null) { + self.state = .invalid; + return null; } - } else log.info("unknown hyperlink option: {s}", .{self.temp_state.key}); + self.command = .hyperlink_end; + } + + return &self.command; } - fn endSemanticOptionValue(self: *Parser) void { - 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]; + /// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. + fn parseOsc9(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; }; - if (mem.eql(u8, self.temp_state.key, "aid")) { - switch (self.command) { - .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 = 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 = string_encoding.urlPercentDecode(value) catch null, - else => {}, - } - } else if (mem.eql(u8, self.temp_state.key, "redraw")) { - // 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.redraw = false, - '1' => v.redraw = true, - else => break :valid false, - } - - break :valid true; - } else false; - - if (!valid) { - log.info("OSC 133 A invalid redraw value: {s}", .{value}); + // Check first to see if this is a ConEmu OSC + // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + conemu: { + var data = writer.buffered(); + if (data.len == 0) break :conemu; + switch (data[0]) { + // Check for OSC 9;1 9;10 9;12 + '1' => { + if (data.len < 2) break :conemu; + switch (data[1]) { + // OSC 9;1 + ';' => { + self.command = .{ + .conemu_sleep = .{ + .duration_ms = if (std.fmt.parseUnsigned(u16, data[2..], 10)) |num| @min(num, 10_000) else |_| 100, + }, + }; + return &self.command; + }, + // OSC 9;10 + '0' => { + self.state = .invalid; + return null; + }, + // OSC 9;12 + '2' => { + self.command = .{ + .prompt_start = .{}, + }; + return &self.command; + }, + else => break :conemu, } }, - 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}); + // OSC 9;2 + '2' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + data = writer.buffered(); + self.command = .{ + .conemu_show_message_box = data[2 .. data.len - 1 :0], + }; + return &self.command; + }, + // OSC 9;3 + '3' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + if (data.len == 2) { + self.command = .{ + .conemu_change_tab_title = .reset, + }; + return &self.command; } + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + data = writer.buffered(); + self.command = .{ + .conemu_change_tab_title = .{ + .value = data[2 .. data.len - 1 :0], + }, + }; + return &self.command; }, - 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}); + // OSC 9;4 + '4' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + if (data.len < 3) break :conemu; + switch (data[2]) { + '0' => { + self.command = .{ + .conemu_progress_report = .{ + .state = .remove, + }, + }; + }, + '1' => { + self.command = .{ + .conemu_progress_report = .{ + .state = .set, + .progress = 0, + }, + }; + }, + '2' => { + self.command = .{ + .conemu_progress_report = .{ + .state = .@"error", + }, + }; + }, + '3' => { + self.command = .{ + .conemu_progress_report = .{ + .state = .indeterminate, + }, + }; + }, + '4' => { + self.command = .{ + .conemu_progress_report = .{ + .state = .pause, + }, + }; + }, + else => break :conemu, + } + switch (self.command.conemu_progress_report.state) { + .remove, .indeterminate => {}, + .set, .@"error", .pause => progress: { + if (data.len < 4) break :progress; + if (data[3] != ';') break :progress; + // parse the progress value + self.command.conemu_progress_report.progress = value: { + break :value @intCast(std.math.clamp( + std.fmt.parseUnsigned(usize, data[4..], 10) catch break :value null, + 0, + 100, + )); + }; + }, } + return &self.command; }, - 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. - switch (self.command) { - .prompt_start => |*v| if (value.len == 1) { - v.kind = switch (value[0]) { - 'c' => .continuation, - 's' => .secondary, - 'r' => .right, - 'i' => .primary, - else => .primary, + // OSC 9;5 + '5' => { + self.command = .conemu_wait_input; + return &self.command; + }, + // OSC 9;6 + '6' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + data = writer.buffered(); + self.command = .{ + .conemu_guimacro = data[2 .. data.len - 1 :0], }; + return &self.command; + }, + // OSC 9;7 + '7' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + self.state = .invalid; + return null; + }, + // OSC 9;8 + '8' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + self.state = .invalid; + return null; }, - else => {}, + // OSC 9;9 + '9' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + self.state = .invalid; + return null; + }, + else => break :conemu, } - } else log.info("unknown semantic prompts option: {s}", .{self.temp_state.key}); - } - - fn endSemanticExitCode(self: *Parser) void { - switch (self.command) { - .end_of_command => |*v| v.exit_code = @truncate(self.temp_state.num), - else => {}, } - } - fn endString(self: *Parser) void { - self.buf[self.buf_idx] = 0; - defer self.buf_idx += 1; - self.temp_state.str.* = self.buf[self.buf_start..self.buf_idx :0]; - } + // If it's not a ConEmu OSC, it's an iTerm2 notification - fn endConEmuSleepValue(self: *Parser) void { - switch (self.command) { - .conemu_sleep => |*v| v.duration_ms = value: { - const str = self.buf[self.buf_start..self.buf_idx]; - if (str.len == 0) break :value 100; - - if (std.fmt.parseUnsigned(u16, str, 10)) |num| { - break :value @min(num, 10_000); - } else |_| { - break :value 100; - } + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + self.command = .{ + .show_desktop_notification = .{ + .title = "", + .body = data[0 .. data.len - 1 :0], }, - else => {}, - } + }; + return &self.command; } - fn endKittyColorProtocolOption(self: *Parser, kind: enum { key_only, key_and_value }, final: bool) void { - if (self.temp_state.key.len == 0) { - @branchHint(.cold); - log.warn("zero length key in kitty color protocol", .{}); - return; + /// Parse OSC 21, the Kitty Color Protocol. + fn parseKittyColorProtocol(self: *Parser, terminator_ch: ?u8) ?*Command { + assert(self.state == .@"21"); + const alloc = self.alloc orelse { + self.state = .invalid; + return null; + }; + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + self.command = .{ + .kitty_color_protocol = .{ + .list = .empty, + .terminator = .init(terminator_ch), + }, + }; + const list = &self.command.kitty_color_protocol.list; + const data = writer.buffered(); + var kv_it = std.mem.splitScalar(u8, data, ';'); + while (kv_it.next()) |kv| { + if (list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { + log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); + self.state = .invalid; + return null; + } + var it = std.mem.splitScalar(u8, kv, '='); + const k = it.next() orelse continue; + if (k.len == 0) { + log.warn("zero length key in kitty color protocol", .{}); + continue; + } + const key = kitty_color.Kind.parse(k) orelse { + log.warn("unknown key in kitty color protocol: {s}", .{k}); + continue; + }; + const value = std.mem.trim(u8, it.rest(), " "); + if (value.len == 0) { + list.append(alloc, .{ .reset = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } else if (mem.eql(u8, "?", value)) { + list.append(alloc, .{ .query = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } else { + list.append(alloc, .{ + .set = .{ + .key = key, + .color = RGB.parse(value) catch |err| switch (err) { + error.InvalidFormat => { + log.warn("invalid color format in kitty color protocol: {s}", .{value}); + continue; + }, + }, + }, + }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } } + return &self.command; + } - const key = kitty_color.Kind.parse(self.temp_state.key) orelse { - @branchHint(.cold); - log.warn("unknown key in kitty color protocol: {s}", .{self.temp_state.key}); - return; + // Parse OSC 22 + fn parseMouseShape(self: *Parser, _: ?u8) ?*Command { + assert(self.state == .@"22"); + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + self.command = .{ + .mouse_shape = .{ + .value = data[0 .. data.len - 1 :0], + }, }; + return &self.command; + } - const value = value: { - if (self.buf_start == self.buf_idx) break :value ""; - if (final) break :value std.mem.trim(u8, self.buf[self.buf_start..self.buf_idx], " "); - break :value std.mem.trim(u8, self.buf[self.buf_start .. self.buf_idx - 1], " "); + /// Parse OSC 52 + fn parseClipboardOperation(self: *Parser, _: ?u8) ?*Command { + assert(self.state == .@"52"); + const writer = self.writer orelse { + self.state = .invalid; + return null; }; + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 1) { + self.state = .invalid; + return null; + } + if (data[0] == ';') { + self.command = .{ + .clipboard_contents = .{ + .kind = 'c', + .data = data[1 .. data.len - 1 :0], + }, + }; + } else { + if (data.len < 2) { + self.state = .invalid; + return null; + } + if (data[1] != ';') { + self.state = .invalid; + return null; + } + self.command = .{ + .clipboard_contents = .{ + .kind = data[0], + .data = data[2 .. data.len - 1 :0], + }, + }; + } + return &self.command; + } - switch (self.command) { - .kitty_color_protocol => |*v| { - // Cap our allocation amount for our list. - if (v.list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { - @branchHint(.cold); + /// Parse OSC 133, semantic prompts + fn parseSemanticPrompt(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 0) { + self.state = .invalid; + return null; + } + switch (data[0]) { + 'A' => prompt_start: { + self.command = .{ + .prompt_start = .{}, + }; + if (data.len == 1) break :prompt_start; + if (data[1] != ';') { self.state = .invalid; - log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); - return; + return null; } - - // 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(alloc, .{ .reset = key }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } else if (mem.eql(u8, "?", value)) { - v.list.append(alloc, .{ .query = key }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; - } else { - v.list.append(alloc, .{ - .set = .{ - .key = key, - .color = RGB.parse(value) catch |err| switch (err) { - error.InvalidFormat => { - log.warn("invalid color format in kitty color protocol: {s}", .{value}); - return; - }, - }, - }, - }) catch |err| { - @branchHint(.cold); - log.warn("unable to append kitty color protocol option: {}", .{err}); - return; - }; + var it = SemanticPromptKVIterator.init(writer) catch { + self.state = .invalid; + return null; + }; + while (it.next()) |kv| { + if (std.mem.eql(u8, kv.key, "aid")) { + self.command.prompt_start.aid = kv.value; + } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + // 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. + self.command.prompt_start.redraw = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A: invalid redraw value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + self.command.prompt_start.special_key = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A invalid special_key value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "click_events")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + self.command.prompt_start.click_events = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A invalid click_events value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "k")) k: { + // 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. + if (kv.value.len != 1) break :k; + self.command.prompt_start.kind = switch (kv.value[0]) { + 'c' => .continuation, + 's' => .secondary, + 'r' => .right, + 'i' => .primary, + else => .primary, + }; + } else log.info("OSC 133 A: unknown semantic prompt option: {s}", .{kv.key}); + } + }, + 'B' => prompt_end: { + self.command = .prompt_end; + if (data.len == 1) break :prompt_end; + if (data[1] != ';') { + self.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + self.state = .invalid; + return null; + }; + while (it.next()) |kv| { + log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); + } + }, + 'C' => end_of_input: { + self.command = .{ + .end_of_input = .{}, + }; + if (data.len == 1) break :end_of_input; + if (data[1] != ';') { + self.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + self.state = .invalid; + return null; + }; + while (it.next()) |kv| { + if (std.mem.eql(u8, kv.key, "cmdline")) { + self.command.end_of_input.cmdline = string_encoding.printfQDecode(kv.value) catch null; + } else if (std.mem.eql(u8, kv.key, "cmdline_url")) { + self.command.end_of_input.cmdline = string_encoding.urlPercentDecode(kv.value) catch null; + } else { + log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); + } } }, - else => {}, + 'D' => { + const exit_code: ?u8 = exit_code: { + if (data.len == 1) break :exit_code null; + if (data[1] != ';') { + self.state = .invalid; + return null; + } + break :exit_code std.fmt.parseUnsigned(u8, data[2..], 10) catch null; + }; + self.command = .{ + .end_of_command = .{ + .exit_code = exit_code, + }, + }; + }, + else => { + self.state = .invalid; + return null; + }, } + return &self.command; } - fn endOscColor(self: *Parser) void { - const alloc = self.alloc.?; - assert(self.command == .color_operation); - const data = self.buf[self.buf_start..self.buf_idx]; - self.command.color_operation.requests = osc_color.parse( - alloc, - self.command.color_operation.op, - data, - ) catch |err| list: { - log.info( - "failed to parse OSC color request err={} data={s}", - .{ err, data }, - ); - break :list .{}; - }; - } + const SemanticPromptKVIterator = struct { + index: usize, + string: []u8, - fn endAllocableString(self: *Parser) void { - const alloc = self.alloc.?; - const list = self.buf_dynamic.?; - list.append(alloc, 0) catch { - @branchHint(.cold); - log.warn("allocation failed on allocable string termination", .{}); - self.temp_state.str.* = ""; - return; + pub const SemanticPromptKV = struct { + key: [:0]u8, + value: [:0]u8, }; - 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 - /// 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. - /// - /// 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}", - .{self.buf[0..self.buf_idx]}, - ); - return null; + pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { + // add a semicolon to make it easier to find and sentinel terminate the values + try writer.writeByte(';'); + return .{ + .index = 0, + .string = writer.buffered()[2..], + }; } - // Other cleanup we may have to do depending on state. - switch (self.state) { - .allocable_string => self.endAllocableString(), - .semantic_exit_code => self.endSemanticExitCode(), - .semantic_option_value => self.endSemanticOptionValue(), - .hyperlink_uri => self.endHyperlink(), - .string => self.endString(), - .conemu_sleep_value => self.endConEmuSleepValue(), - .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), - .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), - .osc_color => self.endOscColor(), - - // 104 abruptly ended turns into a reset palette command. - .@"104" => { - self.command = .{ .color_operation = .{ - .op = .osc_104, - } }; - self.state = .osc_color; - self.buf_start = self.buf_idx; - self.endOscColor(); - }, + pub fn next(self: *SemanticPromptKVIterator) ?SemanticPromptKV { + if (self.index >= self.string.len) return null; - // We received OSC 9;X ST, but nothing else, finish off as a - // desktop notification with "X" as the body. - .conemu_sleep, - .conemu_message_box, - .conemu_tab, - .conemu_progress_prestate, - .conemu_progress_state, - .conemu_guimacro, - => { - self.showDesktopNotification(); - self.endString(); - }, + const kv = kv: { + const index = std.mem.indexOfScalarPos(u8, self.string, self.index, ';') orelse { + self.index = self.string.len; + return null; + }; + self.string[index] = 0; + const kv = self.string[self.index..index :0]; + self.index = index + 1; + break :kv kv; + }; - // A ConEmu progress report that has reached these states is - // complete, don't do anything to them. - .conemu_progress_prevalue, - .conemu_progress_value, - => {}, + const key = key: { + const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; + kv[index] = 0; + const key = kv[0..index :0]; + break :key key; + }; - else => {}, - } + const value = kv[key.len + 1 .. :0]; - switch (self.command) { - .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), - .color_operation => |*c| c.terminator = .init(terminator_ch), - else => {}, + return .{ + .key = key, + .value = value, + }; } + }; + /// Parse OSC 777 + fn parseRxvtExtension(self: *Parser, _: ?u8) ?*Command { + const writer = self.writer orelse { + self.state = .invalid; + return null; + }; + // ensure that we are sentinel terminated + writer.writeByte(0) catch { + self.state = .invalid; + return null; + }; + const data = writer.buffered(); + const k = std.mem.indexOfScalar(u8, data, ';') orelse { + self.state = .invalid; + return null; + }; + const ext = data[0..k]; + if (!std.mem.eql(u8, ext, "notify")) { + log.warn("unknown rxvt extension: {s}", .{ext}); + self.state = .invalid; + return null; + } + const t = std.mem.indexOfScalarPos(u8, data, k + 1, ';') orelse { + log.warn("rxvt notify extension is missing the title", .{}); + self.state = .invalid; + return null; + }; + data[t] = 0; + const title = data[k + 1 .. t :0]; + const body = data[t + 1 .. data.len - 1 :0]; + self.command = .{ + .show_desktop_notification = .{ + .title = title, + .body = body, + }, + }; return &self.command; } }; @@ -1794,7 +1520,6 @@ test "OSC 0: longer than buffer" { 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" { @@ -1803,7 +1528,7 @@ test "OSC 0: one shorter than buffer length" { var p: Parser = .init(null); const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len - 1); + const title = "a" ** (Parser.MAX_BUF - 1); const input = prefix ++ title; for (input) |ch| p.next(ch); @@ -1818,13 +1543,12 @@ test "OSC 0: exactly at buffer length" { var p: Parser = .init(null); const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - prefix.len); + const title = "a" ** Parser.MAX_BUF; 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" { @@ -2731,7 +2455,7 @@ test "OSC 21: kitty color protocol reset after invalid" { p.reset(); - try testing.expectEqual(Parser.State.empty, p.state); + try testing.expectEqual(Parser.State.start, p.state); p.next('X'); try testing.expectEqual(Parser.State.invalid, p.state); @@ -2874,6 +2598,20 @@ test "OSC 133: prompt_start with single option" { try testing.expectEqualStrings("14", cmd.prompt_start.aid.?); } +test "OSC 133: prompt_start with '=' in aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=a=b;redraw=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expectEqualStrings("a=b", cmd.prompt_start.aid.?); + try testing.expect(!cmd.prompt_start.redraw); +} + test "OSC 133: prompt_start with redraw disabled" { const testing = std.testing; -- 2.51.2 From 2805c1e405b8b3e0e96a468b1014107e1b51eac1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 7 Jan 2026 12:36:23 -0600 Subject: [PATCH 214/349] osc: collapse switch cases --- src/terminal/osc.zig | 209 ++++++++++++++----------------------------- 1 file changed, 67 insertions(+), 142 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index d81244b9f..571d123d8 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -518,11 +518,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"0" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - .@"1" => switch (c) { ';' => self.writeToFixed(), '0' => self.state = .@"10", @@ -564,57 +559,26 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"110" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"111" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"112" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"113" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"114" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"115" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"116" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"117" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"118" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"119" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"12" => switch (c) { + .@"4", + .@"12", + .@"14", + .@"15", + .@"16", + .@"17", + .@"18", + .@"19", + .@"21", + .@"110", + .@"111", + .@"112", + .@"113", + .@"114", + .@"115", + .@"116", + .@"117", + .@"118", + .@"119", + => switch (c) { ';' => if (self.ensureAllocator()) self.writeToFixed(), else => self.state = .invalid, }, @@ -625,41 +589,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"133" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"14" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"15" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"16" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"17" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"18" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"19" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - .@"2" => switch (c) { ';' => self.writeToFixed(), '1' => self.state = .@"21", @@ -667,21 +596,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"21" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"22" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"4" => switch (c) { - ';' => if (self.ensureAllocator()) self.writeToFixed(), - else => self.state = .invalid, - }, - .@"5" => switch (c) { ';' => if (self.ensureAllocator()) self.writeToFixed(), '2' => self.state = .@"52", @@ -704,17 +618,13 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"777" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"8" => switch (c) { - ';' => self.writeToFixed(), - else => self.state = .invalid, - }, - - .@"9" => switch (c) { + .@"0", + .@"133", + .@"22", + .@"777", + .@"8", + .@"9", + => switch (c) { ';' => self.writeToFixed(), else => self.state = .invalid, }, @@ -731,41 +641,56 @@ pub const Parser = struct { pub fn end(self: *Parser, terminator_ch: ?u8) ?*Command { return switch (self.state) { .start => null, + .invalid => null, - .@"0" => self.parseChangeWindowTitle(terminator_ch), + + .@"0", + .@"2", + => self.parseChangeWindowTitle(terminator_ch), + .@"1" => self.parseChangeWindowIcon(terminator_ch), - .@"2" => self.parseChangeWindowTitle(terminator_ch), - .@"4" => self.parseOscColor(terminator_ch), - .@"5" => self.parseOscColor(terminator_ch), + + .@"4", + .@"5", + .@"10", + .@"11", + .@"12", + .@"13", + .@"14", + .@"15", + .@"16", + .@"17", + .@"18", + .@"19", + .@"104", + .@"110", + .@"111", + .@"112", + .@"113", + .@"114", + .@"115", + .@"116", + .@"117", + .@"118", + .@"119", + => self.parseOscColor(terminator_ch), + .@"7" => self.parseReportPwd(terminator_ch), + .@"8" => self.parseHyperlink(terminator_ch), + .@"9" => self.parseOsc9(terminator_ch), - .@"10" => self.parseOscColor(terminator_ch), - .@"11" => self.parseOscColor(terminator_ch), - .@"12" => self.parseOscColor(terminator_ch), - .@"13" => self.parseOscColor(terminator_ch), - .@"14" => self.parseOscColor(terminator_ch), - .@"15" => self.parseOscColor(terminator_ch), - .@"16" => self.parseOscColor(terminator_ch), - .@"17" => self.parseOscColor(terminator_ch), - .@"18" => self.parseOscColor(terminator_ch), - .@"19" => self.parseOscColor(terminator_ch), + .@"21" => self.parseKittyColorProtocol(terminator_ch), + .@"22" => self.parseMouseShape(terminator_ch), + .@"52" => self.parseClipboardOperation(terminator_ch), + .@"77" => null, - .@"104" => self.parseOscColor(terminator_ch), - .@"110" => self.parseOscColor(terminator_ch), - .@"111" => self.parseOscColor(terminator_ch), - .@"112" => self.parseOscColor(terminator_ch), - .@"113" => self.parseOscColor(terminator_ch), - .@"114" => self.parseOscColor(terminator_ch), - .@"115" => self.parseOscColor(terminator_ch), - .@"116" => self.parseOscColor(terminator_ch), - .@"117" => self.parseOscColor(terminator_ch), - .@"118" => self.parseOscColor(terminator_ch), - .@"119" => self.parseOscColor(terminator_ch), + .@"133" => self.parseSemanticPrompt(terminator_ch), + .@"777" => self.parseRxvtExtension(terminator_ch), }; } -- 2.51.2 From 0b9b17cbe0c14a7f4ce56840b5708af9563ebbea Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 13:40:21 -0600 Subject: [PATCH 215/349] osc: remove pub from internal parser functions --- src/terminal/osc.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 571d123d8..dd31224e2 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -451,7 +451,7 @@ pub const Parser = struct { /// Make sure that we have an allocator. If we don't, set the state to /// invalid so that any additional OSC data is discarded. - pub inline fn ensureAllocator(self: *Parser) bool { + inline fn ensureAllocator(self: *Parser) bool { if (self.alloc != null) return true; log.warn("An allocator is required to process OSC {t} but none was provided.", .{self.state}); self.state = .invalid; @@ -459,7 +459,7 @@ pub const Parser = struct { } /// Set up a fixed Writer to collect the rest of the OSC data. - pub inline fn writeToFixed(self: *Parser) void { + inline fn writeToFixed(self: *Parser) void { self.fixed = .fixed(&self.buffer); self.writer = &self.fixed.?; } @@ -467,7 +467,7 @@ pub const Parser = struct { /// Set up an allocating Writer to collect the rest of the OSC data. If we /// don't have an allocator or setting up the allocator fails, fall back to /// writing to a fixed buffer and hope that it's big enough. - pub inline fn writeToAllocating(self: *Parser) void { + inline fn writeToAllocating(self: *Parser) void { const alloc = self.alloc orelse { // We don't have an allocator - fall back to a fixed buffer and hope // that it's big enough. -- 2.51.2 From 6ee1b3998e55fe66584b968aab342b422d8bb502 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 13:50:33 -0600 Subject: [PATCH 216/349] osc: no defaults on Parser fields --- src/terminal/osc.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index dd31224e2..64b409cdd 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -315,22 +315,22 @@ pub const Parser = struct { /// Optional allocator used to accept data longer than MAX_BUF. /// This only applies to some commands (e.g. OSC 52) that can /// reasonably exceed MAX_BUF. - alloc: ?Allocator = null, + alloc: ?Allocator, /// Current state of the parser. - state: State = .start, + state: State, /// Buffer for temporary storage of OSC data - buffer: [MAX_BUF]u8 = undefined, + buffer: [MAX_BUF]u8, /// Fixed writer for accumulating OSC data - fixed: ?std.Io.Writer = null, + fixed: ?std.Io.Writer, /// Allocating writer for accumulating OSC data - allocating: ?std.Io.Writer.Allocating = null, + allocating: ?std.Io.Writer.Allocating, /// Pointer to the active writer for accumulating OSC data - writer: ?*std.Io.Writer = null, + writer: ?*std.Io.Writer, /// The command that is the result of parsing. - command: Command = .invalid, + command: Command, pub const State = enum { start, -- 2.51.2 From f180f1c9b8b5e4f1fca505ace5485b13762bef32 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 14:12:16 -0600 Subject: [PATCH 217/349] osc: remove inline from Parser.next --- src/benchmark/OscParser.zig | 2 +- src/terminal/Parser.zig | 2 +- src/terminal/osc.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/benchmark/OscParser.zig b/src/benchmark/OscParser.zig index bd82a3534..d4b416de8 100644 --- a/src/benchmark/OscParser.zig +++ b/src/benchmark/OscParser.zig @@ -100,7 +100,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void { error.ReadFailed => return error.BenchmarkFailed, }; - for (osc_buf[0..len]) |c| self.parser.next(c); + for (osc_buf[0..len]) |c| @call(.always_inline, Parser.next, .{ &self.parser, c }); std.mem.doNotOptimizeAway(self.parser.end(std.ascii.control_code.bel)); self.parser.reset(); } diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 980906e49..34a23787f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -359,7 +359,7 @@ inline fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { break :param null; }, .osc_put => osc_put: { - self.osc_parser.next(c); + @call(.always_inline, osc.Parser.next, .{ &self.osc_parser, c }); break :osc_put null; }, .csi_dispatch => csi_dispatch: { diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 64b409cdd..56184df46 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -486,7 +486,7 @@ pub const Parser = struct { } /// Consume the next character c and advance the parser state. - pub inline fn next(self: *Parser, c: u8) void { + pub fn next(self: *Parser, c: u8) void { // If the state becomes invalid for any reason, just discard // any further input. if (self.state == .invalid) return; -- 2.51.2 From caa6b958d77dbfe62abd4ef69989fd76a719286a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 14:07:43 -0800 Subject: [PATCH 218/349] apprt/embedded: escape the initial input string Fixes #10214 --- src/apprt/embedded.zig | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index d1d38c24d..6c1d46722 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -542,13 +542,20 @@ pub const Surface = struct { // If we have an initial input then we set it. if (opts.initial_input) |c_input| { const alloc = config.arenaAlloc(); + + // We need to escape the string because the "raw" field + // expects a Zig string. + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try std.zig.stringEscape( + std.mem.sliceTo(c_input, 0), + &buf.writer, + ); + config.input.list.clearRetainingCapacity(); try config.input.list.append( alloc, - .{ .raw = try alloc.dupeZ(u8, std.mem.sliceTo( - c_input, - 0, - )) }, + .{ .raw = try buf.toOwnedSliceSentinel(0) }, ); } -- 2.51.2 From 794c47425e77bc1c3563e7cdd5fdb7d5f4e88303 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 14:27:35 -0800 Subject: [PATCH 219/349] terminal: PageList shouldn't allow any scrolling with max_size=0 Partial #10227 This fixes the scrollbar part of #10227, but not the search part. The way PageList works is that max_size is advisory: we always allocate on page boundaries so we always have _some_ extra space (usually, unless you ask for a byte-perfect max size). Normally this is fine, it doesn't cause any real issues. But with the introduction of scrollbars (and search), we were exposing this hidden space to the user. To fix this, the easiest approach is to special-case the zero-scrollback scenario, since it is already documented that scrollback limit is not _exact_ and is subject to some minimum allocations. But with zero-scrollback we really expect NOTHING. --- src/terminal/PageList.zig | 69 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 910083b7b..bebe6b700 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2000,6 +2000,12 @@ pub const Scroll = union(enum) { pub fn scroll(self: *PageList, behavior: Scroll) void { defer self.assertIntegrity(); + // Special case no-scrollback mode to never allow scrolling. + if (self.explicit_max_size == 0) { + self.viewport = .active; + return; + } + switch (behavior) { .active => self.viewport = .active, .top => self.viewport = .top, @@ -2322,6 +2328,17 @@ pub const Scrollbar = struct { /// 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 { + // If we have no scrollback, special case no scrollbar. + // We need to do this because the way PageList works is that + // it always has SOME extra space (due to the way we allocate by page). + // So even with no scrollback we have some growth. It is architecturally + // much simpler to just hide that for no-scrollback cases. + if (self.explicit_max_size == 0) return .{ + .total = self.rows, + .offset = 0, + .len = self.rows, + }; + return .{ .total = self.total_rows, .offset = self.viewportRowOffset(), @@ -4762,7 +4779,8 @@ test "PageList grow prune required with a single page" { const testing = std.testing; const alloc = testing.allocator; - var s = try init(alloc, 80, 24, 0); + // Need scrollback > 0 to have a scrollbar to test + var s = try init(alloc, 80, 24, null); defer s.deinit(); // This block is all test setup. There is nothing required about this @@ -4810,6 +4828,47 @@ test "PageList grow prune required with a single page" { }, s.scrollbar()); } +test "PageList scrollbar with max_size 0 after grow" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + // Grow some rows (simulates normal terminal output) + try s.growRows(10); + + const sb = s.scrollbar(); + + // With no scrollback (max_size = 0), total should equal rows + try testing.expectEqual(s.rows, sb.total); + + // With no scrollback, offset should be 0 (nowhere to scroll back to) + try testing.expectEqual(@as(usize, 0), sb.offset); +} + +test "PageList scroll with max_size 0 no history" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + try s.growRows(10); + + // Remember initial viewport position + const pt_before = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + + // Try to scroll backwards into "history" - should be no-op + s.scroll(.{ .delta_row = -5 }); + try testing.expect(s.viewport == .active); + + // Scroll to top - should also be no-op with no scrollback + s.scroll(.{ .top = {} }); + const pt_after = s.getCell(.{ .viewport = .{} }).?.screenPoint(); + try testing.expectEqual(pt_before, pt_after); +} + test "PageList scroll top" { const testing = std.testing; const alloc = testing.allocator; @@ -5785,8 +5844,8 @@ test "PageList grow prune scrollback" { 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); + // Use std_size to limit scrollback so pruning is triggered. + var s = try init(alloc, 80, 24, std_size); defer s.deinit(); // Grow to capacity @@ -5854,8 +5913,8 @@ 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); + // Use std_size to limit scrollback so pruning is triggered. + var s = try init(alloc, 80, 24, std_size); defer s.deinit(); // Grow to capacity of first page -- 2.51.2 From 5bfbadbc7044977fcfe6767b79857ead625b6463 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 Jan 2026 20:22:22 -0800 Subject: [PATCH 220/349] terminal/search: screen search prunes history for no-scrollback screens The big comment in `search/screen.zig` describes the solution well. The problem is that our search is discrete by page and a page can contain some amount of history as well. For zero-scrollback screens, we need to fully prune any history lines. For everyone else, everything in the PageList is scrollable and visible so we should search it. --- src/terminal/highlight.zig | 9 ++++ src/terminal/search/screen.zig | 79 +++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/terminal/highlight.zig b/src/terminal/highlight.zig index 582ef6f06..bc3a1758e 100644 --- a/src/terminal/highlight.zig +++ b/src/terminal/highlight.zig @@ -180,6 +180,15 @@ pub const Flattened = struct { }; } + pub fn endPin(self: Flattened) Pin { + const slice = self.chunks.slice(); + return .{ + .node = slice.items(.node)[slice.len - 1], + .x = self.bot_x, + .y = slice.items(.end)[slice.len - 1] - 1, + }; + } + /// Convert to an Untracked highlight. pub fn untracked(self: Flattened) Untracked { // Note: we don't use startPin/endPin here because it is slightly diff --git a/src/terminal/search/screen.zig b/src/terminal/search/screen.zig index 0ae7f8a1f..179e7da87 100644 --- a/src/terminal/search/screen.zig +++ b/src/terminal/search/screen.zig @@ -412,6 +412,12 @@ pub const ScreenSearch = struct { // pages then we need to re-search the pages and add it to // our history results. + // If our screen has no scrollback then we have no history. + if (self.screen.no_scrollback) { + assert(self.history == null); + break :history; + } + const history_: ?*HistorySearch = if (self.history) |*h| state: { // If our start pin became garbage, it means we pruned all // the way up through it, so we have no history anymore. @@ -575,8 +581,43 @@ pub const ScreenSearch = struct { }, } - // Active area search was successful. Now we have to fixup our - // selection if we had one. + // If we have no scrollback, we need to prune any active results + // that aren't in the actual active area. We only do this for the + // no scrollback scenario because with scrollback we actually + // rely on our active search searching by page to find history + // items as well. This is all related to the fact that PageList + // scrollback limits are discrete by page size except we special + // case zero. + if (self.screen.no_scrollback and + self.active_results.items.len > 0) + active_prune: { + const items = self.active_results.items; + const tl = self.screen.pages.getTopLeft(.active); + for (0.., items) |i, *hl| { + if (!tl.before(hl.endPin())) { + // Deinit because its going to be pruned no matter + // what at some point for not being in the active area. + hl.deinit(alloc); + continue; + } + + // In the active area! Since our results are sorted + // that means everything after this is also in the active + // area, so we prune up to this i. + if (i > 0) self.active_results.replaceRangeAssumeCapacity( + 0, + i, + &.{}, + ); + + break :active_prune; + } + + // None are in the active area... + self.active_results.clearRetainingCapacity(); + } + + // Now we have to fixup our selection if we had one. fixup: { const old_idx = old_selection_idx orelse break :fixup; const m = if (self.selected) |*m| m else break :fixup; @@ -1333,3 +1374,37 @@ test "select prev with history" { } }, t.screens.active.pages.pointFromPin(.active, sel.end).?); } } + +test "screen search no scrollback has no history" { + const alloc = testing.allocator; + var t: Terminal = try .init(alloc, .{ + .cols = 10, + .rows = 2, + .max_scrollback = 0, + }); + defer t.deinit(alloc); + + // Alt screen has no scrollback + _ = try t.switchScreen(.alternate); + + var s = t.vtStream(); + defer s.deinit(); + + // This will probably stop working at some point and we'll have + // no way to test it using public APIs, but at the time of writing + // this test, CSI 22 J (scroll complete) pushes into scrollback + // with alt screen. + try s.nextSlice("Fizz\r\n"); + try s.nextSlice("\x1b[22J"); + try s.nextSlice("hello."); + + var search: ScreenSearch = try .init(alloc, t.screens.active, "Fizz"); + defer search.deinit(); + try search.searchAll(); + try testing.expectEqual(0, search.active_results.items.len); + + // Get all matches + const matches = try search.matches(alloc); + defer alloc.free(matches); + try testing.expectEqual(0, matches.len); +} -- 2.51.2 From 93b4b08b5270d10c440b64483bd9ba39e2787082 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 8 Jan 2026 23:07:57 -0600 Subject: [PATCH 221/349] osc: refactor parsing helper functions into separate files Following up on #9950, refactor the parsing helper functions into separate files. --- src/terminal/osc.zig | 2337 +---------------- src/terminal/osc/parsers.zig | 27 + .../osc/parsers/change_window_icon.zig | 33 + .../osc/parsers/change_window_title.zig | 119 + .../osc/parsers/clipboard_operation.zig | 106 + src/terminal/osc/{ => parsers}/color.zig | 123 +- src/terminal/osc/parsers/hyperlink.zig | 164 ++ src/terminal/osc/parsers/kitty_color.zig | 212 ++ src/terminal/osc/parsers/mouse_shape.zig | 39 + src/terminal/osc/parsers/osc9.zig | 766 ++++++ src/terminal/osc/parsers/report_pwd.zig | 48 + src/terminal/osc/parsers/rxvt_extension.zig | 59 + src/terminal/osc/parsers/semantic_prompt.zig | 694 +++++ src/terminal/stream_readonly.zig | 2 +- 14 files changed, 2384 insertions(+), 2345 deletions(-) create mode 100644 src/terminal/osc/parsers.zig create mode 100644 src/terminal/osc/parsers/change_window_icon.zig create mode 100644 src/terminal/osc/parsers/change_window_title.zig create mode 100644 src/terminal/osc/parsers/clipboard_operation.zig rename src/terminal/osc/{ => parsers}/color.zig (86%) create mode 100644 src/terminal/osc/parsers/hyperlink.zig create mode 100644 src/terminal/osc/parsers/kitty_color.zig create mode 100644 src/terminal/osc/parsers/mouse_shape.zig create mode 100644 src/terminal/osc/parsers/osc9.zig create mode 100644 src/terminal/osc/parsers/report_pwd.zig create mode 100644 src/terminal/osc/parsers/rxvt_extension.zig create mode 100644 src/terminal/osc/parsers/semantic_prompt.zig diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 56184df46..1f4489961 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -12,11 +12,9 @@ const mem = std.mem; const assert = @import("../quirks.zig").inlineAssert; const Allocator = mem.Allocator; 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 parsers = @import("osc/parsers.zig"); +pub const color = parsers.color; const log = std.log.scoped(.osc); @@ -146,8 +144,8 @@ pub const Command = union(Key) { /// /// 4, 5, 10-19, 104, 105, 110-119 color_operation: struct { - op: osc_color.Operation, - requests: osc_color.List = .{}, + op: color.Operation, + requests: color.List = .{}, terminator: Terminator = .st, }, @@ -310,7 +308,7 @@ pub const Terminator = enum { pub const Parser = struct { /// Maximum size of a "normal" OSC. - const MAX_BUF = 2048; + pub const MAX_BUF = 2048; /// Optional allocator used to accept data longer than MAX_BUF. /// This only applies to some commands (e.g. OSC 52) that can @@ -646,9 +644,9 @@ pub const Parser = struct { .@"0", .@"2", - => self.parseChangeWindowTitle(terminator_ch), + => parsers.change_window_title.parse(self, terminator_ch), - .@"1" => self.parseChangeWindowIcon(terminator_ch), + .@"1" => parsers.change_window_icon.parse(self, terminator_ch), .@"4", .@"5", @@ -673,2330 +671,29 @@ pub const Parser = struct { .@"117", .@"118", .@"119", - => self.parseOscColor(terminator_ch), + => parsers.color.parse(self, terminator_ch), - .@"7" => self.parseReportPwd(terminator_ch), + .@"7" => parsers.report_pwd.parse(self, terminator_ch), - .@"8" => self.parseHyperlink(terminator_ch), + .@"8" => parsers.hyperlink.parse(self, terminator_ch), - .@"9" => self.parseOsc9(terminator_ch), + .@"9" => parsers.osc9.parse(self, terminator_ch), - .@"21" => self.parseKittyColorProtocol(terminator_ch), + .@"21" => parsers.kitty_color.parse(self, terminator_ch), - .@"22" => self.parseMouseShape(terminator_ch), + .@"22" => parsers.mouse_shape.parse(self, terminator_ch), - .@"52" => self.parseClipboardOperation(terminator_ch), + .@"52" => parsers.clipboard_operation.parse(self, terminator_ch), .@"77" => null, - .@"133" => self.parseSemanticPrompt(terminator_ch), + .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), - .@"777" => self.parseRxvtExtension(terminator_ch), + .@"777" => parsers.rxvt_extension.parse(self, terminator_ch), }; } - - /// Parse OSC 0 and OSC 2 - fn parseChangeWindowTitle(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .change_window_title = data[0 .. data.len - 1 :0], - }; - return &self.command; - } - - /// Parse OSC 1 - fn parseChangeWindowIcon(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .change_window_icon = data[0 .. data.len - 1 :0], - }; - return &self.command; - } - - /// Parse OSCs 4, 5, 10-19, 104, 110-119 - fn parseOscColor(self: *Parser, terminator_ch: ?u8) ?*Command { - const alloc = self.alloc orelse { - self.state = .invalid; - return null; - }; - // If we've collected any extra data parse that, otherwise use an empty - // string. - const data = data: { - const writer = self.writer orelse break :data ""; - break :data writer.buffered(); - }; - // Check and make sure that we're parsing the correct OSCs - const op: osc_color.Operation = switch (self.state) { - .@"4" => .osc_4, - .@"5" => .osc_5, - .@"10" => .osc_10, - .@"11" => .osc_11, - .@"12" => .osc_12, - .@"13" => .osc_13, - .@"14" => .osc_14, - .@"15" => .osc_15, - .@"16" => .osc_16, - .@"17" => .osc_17, - .@"18" => .osc_18, - .@"19" => .osc_19, - .@"104" => .osc_104, - .@"110" => .osc_110, - .@"111" => .osc_111, - .@"112" => .osc_112, - .@"113" => .osc_113, - .@"114" => .osc_114, - .@"115" => .osc_115, - .@"116" => .osc_116, - .@"117" => .osc_117, - .@"118" => .osc_118, - .@"119" => .osc_119, - else => { - self.state = .invalid; - return null; - }, - }; - self.command = .{ - .color_operation = .{ - .op = op, - .requests = osc_color.parse(alloc, op, data) catch |err| list: { - log.info( - "failed to parse OSC {t} color request err={} data={s}", - .{ self.state, err, data }, - ); - break :list .{}; - }, - .terminator = .init(terminator_ch), - }, - }; - return &self.command; - } - - /// Parse OSC 7 - fn parseReportPwd(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .report_pwd = .{ - .value = data[0 .. data.len - 1 :0], - }, - }; - return &self.command; - } - - /// Parse OSC 8 hyperlinks - fn parseHyperlink(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - const s = std.mem.indexOfScalar(u8, data, ';') orelse { - self.state = .invalid; - return null; - }; - - self.command = .{ - .hyperlink_start = .{ - .uri = data[s + 1 .. data.len - 1 :0], - }, - }; - - data[s] = 0; - const kvs = data[0 .. s + 1]; - std.mem.replaceScalar(u8, kvs, ':', 0); - var kv_start: usize = 0; - while (kv_start < kvs.len) { - const kv_end = std.mem.indexOfScalarPos(u8, kvs, kv_start + 1, 0) orelse break; - const kv = data[kv_start .. kv_end + 1]; - const v = std.mem.indexOfScalar(u8, kv, '=') orelse break; - const key = kv[0..v]; - const value = kv[v + 1 .. kv.len - 1 :0]; - if (std.mem.eql(u8, key, "id")) { - if (value.len > 0) self.command.hyperlink_start.id = value; - } else { - log.warn("unknown hyperlink option: '{s}'", .{key}); - } - kv_start = kv_end + 1; - } - - if (self.command.hyperlink_start.uri.len == 0) { - if (self.command.hyperlink_start.id != null) { - self.state = .invalid; - return null; - } - self.command = .hyperlink_end; - } - - return &self.command; - } - - /// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. - fn parseOsc9(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - - // Check first to see if this is a ConEmu OSC - // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC - conemu: { - var data = writer.buffered(); - if (data.len == 0) break :conemu; - switch (data[0]) { - // Check for OSC 9;1 9;10 9;12 - '1' => { - if (data.len < 2) break :conemu; - switch (data[1]) { - // OSC 9;1 - ';' => { - self.command = .{ - .conemu_sleep = .{ - .duration_ms = if (std.fmt.parseUnsigned(u16, data[2..], 10)) |num| @min(num, 10_000) else |_| 100, - }, - }; - return &self.command; - }, - // OSC 9;10 - '0' => { - self.state = .invalid; - return null; - }, - // OSC 9;12 - '2' => { - self.command = .{ - .prompt_start = .{}, - }; - return &self.command; - }, - else => break :conemu, - } - }, - // OSC 9;2 - '2' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - data = writer.buffered(); - self.command = .{ - .conemu_show_message_box = data[2 .. data.len - 1 :0], - }; - return &self.command; - }, - // OSC 9;3 - '3' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - if (data.len == 2) { - self.command = .{ - .conemu_change_tab_title = .reset, - }; - return &self.command; - } - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - data = writer.buffered(); - self.command = .{ - .conemu_change_tab_title = .{ - .value = data[2 .. data.len - 1 :0], - }, - }; - return &self.command; - }, - // OSC 9;4 - '4' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - if (data.len < 3) break :conemu; - switch (data[2]) { - '0' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .remove, - }, - }; - }, - '1' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .set, - .progress = 0, - }, - }; - }, - '2' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .@"error", - }, - }; - }, - '3' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .indeterminate, - }, - }; - }, - '4' => { - self.command = .{ - .conemu_progress_report = .{ - .state = .pause, - }, - }; - }, - else => break :conemu, - } - switch (self.command.conemu_progress_report.state) { - .remove, .indeterminate => {}, - .set, .@"error", .pause => progress: { - if (data.len < 4) break :progress; - if (data[3] != ';') break :progress; - // parse the progress value - self.command.conemu_progress_report.progress = value: { - break :value @intCast(std.math.clamp( - std.fmt.parseUnsigned(usize, data[4..], 10) catch break :value null, - 0, - 100, - )); - }; - }, - } - return &self.command; - }, - // OSC 9;5 - '5' => { - self.command = .conemu_wait_input; - return &self.command; - }, - // OSC 9;6 - '6' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - data = writer.buffered(); - self.command = .{ - .conemu_guimacro = data[2 .. data.len - 1 :0], - }; - return &self.command; - }, - // OSC 9;7 - '7' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - self.state = .invalid; - return null; - }, - // OSC 9;8 - '8' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - self.state = .invalid; - return null; - }, - // OSC 9;9 - '9' => { - if (data.len < 2) break :conemu; - if (data[1] != ';') break :conemu; - self.state = .invalid; - return null; - }, - else => break :conemu, - } - } - - // If it's not a ConEmu OSC, it's an iTerm2 notification - - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .show_desktop_notification = .{ - .title = "", - .body = data[0 .. data.len - 1 :0], - }, - }; - return &self.command; - } - - /// Parse OSC 21, the Kitty Color Protocol. - fn parseKittyColorProtocol(self: *Parser, terminator_ch: ?u8) ?*Command { - assert(self.state == .@"21"); - const alloc = self.alloc orelse { - self.state = .invalid; - return null; - }; - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - self.command = .{ - .kitty_color_protocol = .{ - .list = .empty, - .terminator = .init(terminator_ch), - }, - }; - const list = &self.command.kitty_color_protocol.list; - const data = writer.buffered(); - var kv_it = std.mem.splitScalar(u8, data, ';'); - while (kv_it.next()) |kv| { - if (list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { - log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); - self.state = .invalid; - return null; - } - var it = std.mem.splitScalar(u8, kv, '='); - const k = it.next() orelse continue; - if (k.len == 0) { - log.warn("zero length key in kitty color protocol", .{}); - continue; - } - const key = kitty_color.Kind.parse(k) orelse { - log.warn("unknown key in kitty color protocol: {s}", .{k}); - continue; - }; - const value = std.mem.trim(u8, it.rest(), " "); - if (value.len == 0) { - list.append(alloc, .{ .reset = key }) catch |err| { - log.warn("unable to append kitty color protocol option: {}", .{err}); - continue; - }; - } else if (mem.eql(u8, "?", value)) { - list.append(alloc, .{ .query = key }) catch |err| { - log.warn("unable to append kitty color protocol option: {}", .{err}); - continue; - }; - } else { - list.append(alloc, .{ - .set = .{ - .key = key, - .color = RGB.parse(value) catch |err| switch (err) { - error.InvalidFormat => { - log.warn("invalid color format in kitty color protocol: {s}", .{value}); - continue; - }, - }, - }, - }) catch |err| { - log.warn("unable to append kitty color protocol option: {}", .{err}); - continue; - }; - } - } - return &self.command; - } - - // Parse OSC 22 - fn parseMouseShape(self: *Parser, _: ?u8) ?*Command { - assert(self.state == .@"22"); - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - self.command = .{ - .mouse_shape = .{ - .value = data[0 .. data.len - 1 :0], - }, - }; - return &self.command; - } - - /// Parse OSC 52 - fn parseClipboardOperation(self: *Parser, _: ?u8) ?*Command { - assert(self.state == .@"52"); - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - if (data.len == 1) { - self.state = .invalid; - return null; - } - if (data[0] == ';') { - self.command = .{ - .clipboard_contents = .{ - .kind = 'c', - .data = data[1 .. data.len - 1 :0], - }, - }; - } else { - if (data.len < 2) { - self.state = .invalid; - return null; - } - if (data[1] != ';') { - self.state = .invalid; - return null; - } - self.command = .{ - .clipboard_contents = .{ - .kind = data[0], - .data = data[2 .. data.len - 1 :0], - }, - }; - } - return &self.command; - } - - /// Parse OSC 133, semantic prompts - fn parseSemanticPrompt(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - if (data.len == 0) { - self.state = .invalid; - return null; - } - switch (data[0]) { - 'A' => prompt_start: { - self.command = .{ - .prompt_start = .{}, - }; - if (data.len == 1) break :prompt_start; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - var it = SemanticPromptKVIterator.init(writer) catch { - self.state = .invalid; - return null; - }; - while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "aid")) { - self.command.prompt_start.aid = kv.value; - } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - // 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. - self.command.prompt_start.redraw = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { - '0' => break :value false, - '1' => break :value true, - else => break :value null, - } - }) orelse { - log.info("OSC 133 A: invalid redraw value: {s}", .{kv.value}); - break :redraw; - }; - } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - self.command.prompt_start.special_key = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { - '0' => break :value false, - '1' => break :value true, - else => break :value null, - } - }) orelse { - log.info("OSC 133 A invalid special_key value: {s}", .{kv.value}); - break :redraw; - }; - } else if (std.mem.eql(u8, kv.key, "click_events")) redraw: { - // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers - self.command.prompt_start.click_events = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { - '0' => break :value false, - '1' => break :value true, - else => break :value null, - } - }) orelse { - log.info("OSC 133 A invalid click_events value: {s}", .{kv.value}); - break :redraw; - }; - } else if (std.mem.eql(u8, kv.key, "k")) k: { - // 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. - if (kv.value.len != 1) break :k; - self.command.prompt_start.kind = switch (kv.value[0]) { - 'c' => .continuation, - 's' => .secondary, - 'r' => .right, - 'i' => .primary, - else => .primary, - }; - } else log.info("OSC 133 A: unknown semantic prompt option: {s}", .{kv.key}); - } - }, - 'B' => prompt_end: { - self.command = .prompt_end; - if (data.len == 1) break :prompt_end; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - var it = SemanticPromptKVIterator.init(writer) catch { - self.state = .invalid; - return null; - }; - while (it.next()) |kv| { - log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); - } - }, - 'C' => end_of_input: { - self.command = .{ - .end_of_input = .{}, - }; - if (data.len == 1) break :end_of_input; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - var it = SemanticPromptKVIterator.init(writer) catch { - self.state = .invalid; - return null; - }; - while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "cmdline")) { - self.command.end_of_input.cmdline = string_encoding.printfQDecode(kv.value) catch null; - } else if (std.mem.eql(u8, kv.key, "cmdline_url")) { - self.command.end_of_input.cmdline = string_encoding.urlPercentDecode(kv.value) catch null; - } else { - log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); - } - } - }, - 'D' => { - const exit_code: ?u8 = exit_code: { - if (data.len == 1) break :exit_code null; - if (data[1] != ';') { - self.state = .invalid; - return null; - } - break :exit_code std.fmt.parseUnsigned(u8, data[2..], 10) catch null; - }; - self.command = .{ - .end_of_command = .{ - .exit_code = exit_code, - }, - }; - }, - else => { - self.state = .invalid; - return null; - }, - } - return &self.command; - } - - const SemanticPromptKVIterator = struct { - index: usize, - string: []u8, - - pub const SemanticPromptKV = struct { - key: [:0]u8, - value: [:0]u8, - }; - - pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { - // add a semicolon to make it easier to find and sentinel terminate the values - try writer.writeByte(';'); - return .{ - .index = 0, - .string = writer.buffered()[2..], - }; - } - - pub fn next(self: *SemanticPromptKVIterator) ?SemanticPromptKV { - if (self.index >= self.string.len) return null; - - const kv = kv: { - const index = std.mem.indexOfScalarPos(u8, self.string, self.index, ';') orelse { - self.index = self.string.len; - return null; - }; - self.string[index] = 0; - const kv = self.string[self.index..index :0]; - self.index = index + 1; - break :kv kv; - }; - - const key = key: { - const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; - kv[index] = 0; - const key = kv[0..index :0]; - break :key key; - }; - - const value = kv[key.len + 1 .. :0]; - - return .{ - .key = key, - .value = value, - }; - } - }; - - /// Parse OSC 777 - fn parseRxvtExtension(self: *Parser, _: ?u8) ?*Command { - const writer = self.writer orelse { - self.state = .invalid; - return null; - }; - // ensure that we are sentinel terminated - writer.writeByte(0) catch { - self.state = .invalid; - return null; - }; - const data = writer.buffered(); - const k = std.mem.indexOfScalar(u8, data, ';') orelse { - self.state = .invalid; - return null; - }; - const ext = data[0..k]; - if (!std.mem.eql(u8, ext, "notify")) { - log.warn("unknown rxvt extension: {s}", .{ext}); - self.state = .invalid; - return null; - } - const t = std.mem.indexOfScalarPos(u8, data, k + 1, ';') orelse { - log.warn("rxvt notify extension is missing the title", .{}); - self.state = .invalid; - return null; - }; - data[t] = 0; - const title = data[k + 1 .. t :0]; - const body = data[t + 1 .. data.len - 1 :0]; - self.command = .{ - .show_desktop_notification = .{ - .title = title, - .body = body, - }, - }; - return &self.command; - } }; test { - _ = osc_color; -} - -test "OSC 0: change_window_title" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('0'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("ab", cmd.change_window_title); -} - -test "OSC 0: longer than buffer" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); - for (input) |ch| p.next(ch); - - try testing.expect(p.end(null) == null); -} - -test "OSC 0: one shorter than buffer length" { - const testing = std.testing; - - var p: Parser = .init(null); - - const prefix = "0;"; - const title = "a" ** (Parser.MAX_BUF - 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(null); - - const prefix = "0;"; - const title = "a" ** Parser.MAX_BUF; - 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); -} - -test "OSC 1: change_window_icon" { - const testing = std.testing; - - var p: Parser = .init(null); - 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(null); - p.next('2'); - p.next(';'); - p.next('a'); - p.next('b'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("ab", cmd.change_window_title); -} - -test "OSC 2: change_window_title with utf8" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('2'); - p.next(';'); - // 'โ€”' EM DASH U+2014 (E2 80 94) - p.next(0xE2); - p.next(0x80); - p.next(0x94); - - p.next(' '); - // 'โ€' HYPHEN U+2010 (E2 80 90) - // Intententionally chosen to conflict with the 0x90 C1 control - p.next(0xE2); - p.next(0x80); - p.next(0x90); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("โ€” โ€", cmd.change_window_title); -} - -test "OSC 2: change_window_title empty" { - const testing = std.testing; - - var p: Parser = .init(null); - p.next('2'); - p.next(';'); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .change_window_title); - try testing.expectEqualStrings("", cmd.change_window_title); -} - -test "OSC 4: empty param" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "4;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); -} - -// See src/terminal/osc/color.zig for more OSC 4 tests. - -// See src/terminal/osc/color.zig for OSC 5 tests. - -test "OSC 7: report pwd" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "7;file:///tmp/example"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .report_pwd); - try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); -} - -test "OSC 7: report pwd empty" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "7;"; - for (input) |ch| p.next(ch); - const cmd = p.end(null).?.*; - try testing.expect(cmd == .report_pwd); - try testing.expectEqualStrings("", cmd.report_pwd.value); -} - -test "OSC 8: hyperlink" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;;http://example.com"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_start); - try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); -} - -test "OSC 8: hyperlink with id set" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id=foo;http://example.com"; - for (input) |ch| p.next(ch); - - 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"); -} - -test "OSC 8: hyperlink with empty id" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id=;http://example.com"; - for (input) |ch| p.next(ch); - - 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"); -} - -test "OSC 8: hyperlink with incomplete key" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id;http://example.com"; - for (input) |ch| p.next(ch); - - 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"); -} - -test "OSC 8: hyperlink with empty key" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;=value;http://example.com"; - for (input) |ch| p.next(ch); - - 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"); -} - -test "OSC 8: hyperlink with empty key and id" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;=value:id=foo;http://example.com"; - for (input) |ch| p.next(ch); - - 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"); -} - -test "OSC 8: hyperlink with empty uri" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;id=foo;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); -} - -test "OSC 8: hyperlink end" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "8;;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .hyperlink_end); -} - -test "OSC 9: show desktop notification" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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 = .init(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 = .init(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 = .init(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; - - 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"; - for (input) |ch| p.next(ch); - - 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); - { - const item = cmd.kitty_color_protocol.list.items[0]; - try testing.expect(item == .query); - try testing.expectEqual(Kind{ .special = .foreground }, item.query); - } - { - const item = cmd.kitty_color_protocol.list.items[1]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .special = .background }, item.set.key); - try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); - try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); - try testing.expectEqual(@as(u8, 0xff), item.set.color.b); - } - { - const item = cmd.kitty_color_protocol.list.items[2]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .special = .cursor }, item.set.key); - try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); - try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); - try testing.expectEqual(@as(u8, 0xff), item.set.color.b); - } - { - const item = cmd.kitty_color_protocol.list.items[3]; - try testing.expect(item == .reset); - try testing.expectEqual(Kind{ .special = .cursor_text }, item.reset); - } - { - const item = cmd.kitty_color_protocol.list.items[4]; - try testing.expect(item == .reset); - try testing.expectEqual(Kind{ .special = .visual_bell }, item.reset); - } - { - const item = cmd.kitty_color_protocol.list.items[5]; - try testing.expect(item == .query); - try testing.expectEqual(Kind{ .special = .selection_background }, item.query); - } - { - const item = cmd.kitty_color_protocol.list.items[6]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .special = .selection_background }, item.set.key); - try testing.expectEqual(@as(u8, 0xaa), item.set.color.r); - try testing.expectEqual(@as(u8, 0xbb), item.set.color.g); - try testing.expectEqual(@as(u8, 0xcc), item.set.color.b); - } - { - const item = cmd.kitty_color_protocol.list.items[7]; - try testing.expect(item == .query); - try testing.expectEqual(Kind{ .palette = 2 }, item.query); - } - { - const item = cmd.kitty_color_protocol.list.items[8]; - try testing.expect(item == .set); - try testing.expectEqual(Kind{ .palette = 3 }, item.set.key); - try testing.expectEqual(@as(u8, 0xff), item.set.color.r); - try testing.expectEqual(@as(u8, 0xff), item.set.color.g); - try testing.expectEqual(@as(u8, 0xff), item.set.color.b); - } -} - -test "OSC 21: kitty color protocol without allocator" { - const testing = std.testing; - - var p: Parser = .init(null); - defer p.deinit(); - - const input = "21;foreground=?"; - for (input) |ch| p.next(ch); - try testing.expect(p.end('\x1b') == null); -} - -test "OSC 21: kitty color protocol double reset" { - const testing = std.testing; - - 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"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - - p.reset(); - p.reset(); -} - -test "OSC 21: kitty color protocol reset after invalid" { - const testing = std.testing; - - 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"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - - p.reset(); - - try testing.expectEqual(Parser.State.start, p.state); - p.next('X'); - try testing.expectEqual(Parser.State.invalid, p.state); - - p.reset(); -} - -test "OSC 21: kitty color protocol no key" { - const testing = std.testing; - - var p: Parser = .init(testing.allocator); - defer p.deinit(); - - const input = "21;"; - for (input) |ch| p.next(ch); - - const cmd = p.end('\x1b').?.*; - try testing.expect(cmd == .kitty_color_protocol); - try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); -} - -test "OSC 22: pointer cursor" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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 52: get/set clipboard" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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: get/set clipboard (optional parameter)" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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 = .init(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 52: clear clipboard" { - const testing = std.testing; - - var p: Parser = .init(null); - 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(null); - - 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(null); - - 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 '=' in aid" { - const testing = std.testing; - - var p: Parser = .init(null); - - const input = "133;A;aid=a=b;redraw=0"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .prompt_start); - try testing.expectEqualStrings("a=b", cmd.prompt_start.aid.?); - try testing.expect(!cmd.prompt_start.redraw); -} - -test "OSC 133: prompt_start with redraw disabled" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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(null); - - 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(null); - - 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(null); - - 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: prompt_start with special_key" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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; - - var p: Parser = .init(null); - - 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(null); - - 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(null); - - 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(null); - - const input = "133;C"; - for (input) |ch| p.next(ch); - - const cmd = p.end(null).?.*; - try testing.expect(cmd == .end_of_input); -} - -test "OSC 133: end_of_input with cmdline 1" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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 2" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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.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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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(null); - - 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" { - const testing = std.testing; - - var p: Parser = .init(null); - - 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"); + _ = parsers; } diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig new file mode 100644 index 000000000..152276af2 --- /dev/null +++ b/src/terminal/osc/parsers.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub const change_window_icon = @import("parsers/change_window_icon.zig"); +pub const change_window_title = @import("parsers/change_window_title.zig"); +pub const clipboard_operation = @import("parsers/clipboard_operation.zig"); +pub const color = @import("parsers/color.zig"); +pub const hyperlink = @import("parsers/hyperlink.zig"); +pub const kitty_color = @import("parsers/kitty_color.zig"); +pub const mouse_shape = @import("parsers/mouse_shape.zig"); +pub const osc9 = @import("parsers/osc9.zig"); +pub const report_pwd = @import("parsers/report_pwd.zig"); +pub const rxvt_extension = @import("parsers/rxvt_extension.zig"); +pub const semantic_prompt = @import("parsers/semantic_prompt.zig"); + +test { + _ = change_window_icon; + _ = change_window_title; + _ = clipboard_operation; + _ = color; + _ = hyperlink; + _ = kitty_color; + _ = mouse_shape; + _ = osc9; + _ = report_pwd; + _ = rxvt_extension; + _ = semantic_prompt; +} diff --git a/src/terminal/osc/parsers/change_window_icon.zig b/src/terminal/osc/parsers/change_window_icon.zig new file mode 100644 index 000000000..aefe17696 --- /dev/null +++ b/src/terminal/osc/parsers/change_window_icon.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 1 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .change_window_icon = data[0 .. data.len - 1 :0], + }; + return &parser.command; +} + +test "OSC 1: change_window_icon" { + const testing = std.testing; + + var p: Parser = .init(null); + 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); +} diff --git a/src/terminal/osc/parsers/change_window_title.zig b/src/terminal/osc/parsers/change_window_title.zig new file mode 100644 index 000000000..b0bf44dd3 --- /dev/null +++ b/src/terminal/osc/parsers/change_window_title.zig @@ -0,0 +1,119 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 0 and OSC 2 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .change_window_title = data[0 .. data.len - 1 :0], + }; + return &parser.command; +} + +test "OSC 0: change_window_title" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('0'); + p.next(';'); + p.next('a'); + p.next('b'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("ab", cmd.change_window_title); +} + +test "OSC 0: longer than buffer" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "0;" ++ "a" ** (Parser.MAX_BUF + 2); + for (input) |ch| p.next(ch); + + try testing.expect(p.end(null) == null); +} + +test "OSC 0: one shorter than buffer length" { + const testing = std.testing; + + var p: Parser = .init(null); + + const prefix = "0;"; + const title = "a" ** (Parser.MAX_BUF - 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(null); + + const prefix = "0;"; + const title = "a" ** Parser.MAX_BUF; + 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); +} +test "OSC 2: change_window_title with 2" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('2'); + p.next(';'); + p.next('a'); + p.next('b'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("ab", cmd.change_window_title); +} + +test "OSC 2: change_window_title with utf8" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('2'); + p.next(';'); + // 'โ€”' EM DASH U+2014 (E2 80 94) + p.next(0xE2); + p.next(0x80); + p.next(0x94); + + p.next(' '); + // 'โ€' HYPHEN U+2010 (E2 80 90) + // Intententionally chosen to conflict with the 0x90 C1 control + p.next(0xE2); + p.next(0x80); + p.next(0x90); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("โ€” โ€", cmd.change_window_title); +} + +test "OSC 2: change_window_title empty" { + const testing = std.testing; + + var p: Parser = .init(null); + p.next('2'); + p.next(';'); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .change_window_title); + try testing.expectEqualStrings("", cmd.change_window_title); +} diff --git a/src/terminal/osc/parsers/clipboard_operation.zig b/src/terminal/osc/parsers/clipboard_operation.zig new file mode 100644 index 000000000..59a8831bc --- /dev/null +++ b/src/terminal/osc/parsers/clipboard_operation.zig @@ -0,0 +1,106 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 52 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"52"); + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 1) { + parser.state = .invalid; + return null; + } + if (data[0] == ';') { + parser.command = .{ + .clipboard_contents = .{ + .kind = 'c', + .data = data[1 .. data.len - 1 :0], + }, + }; + } else { + if (data.len < 2) { + parser.state = .invalid; + return null; + } + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + parser.command = .{ + .clipboard_contents = .{ + .kind = data[0], + .data = data[2 .. data.len - 1 :0], + }, + }; + } + return &parser.command; +} + +test "OSC 52: get/set clipboard" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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: get/set clipboard (optional parameter)" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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 = .init(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 52: clear clipboard" { + const testing = std.testing; + + var p: Parser = .init(null); + 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); +} diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/parsers/color.zig similarity index 86% rename from src/terminal/osc/color.zig rename to src/terminal/osc/parsers/color.zig index 9fd81ed63..7d3dc68c0 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/parsers/color.zig @@ -1,10 +1,15 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const DynamicColor = @import("../color.zig").Dynamic; -const SpecialColor = @import("../color.zig").Special; -const RGB = @import("../color.zig").RGB; -pub const ParseError = Allocator.Error || error{ +const DynamicColor = @import("../../color.zig").Dynamic; +const SpecialColor = @import("../../color.zig").Special; +const RGB = @import("../../color.zig").RGB; +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_color); + +const ParseError = Allocator.Error || error{ MissingOperation, }; @@ -36,6 +41,76 @@ pub const Operation = enum { osc_119, }; +/// Parse OSCs 4, 5, 10-19, 104, 110-119 +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { + const alloc = parser.alloc orelse { + parser.state = .invalid; + return null; + }; + // If we've collected any extra data parse that, otherwise use an empty + // string. + const data = data: { + const writer = parser.writer orelse break :data ""; + break :data writer.buffered(); + }; + // Check and make sure that we're parsing the correct OSCs + const op: Operation = switch (parser.state) { + .@"4" => .osc_4, + .@"5" => .osc_5, + .@"10" => .osc_10, + .@"11" => .osc_11, + .@"12" => .osc_12, + .@"13" => .osc_13, + .@"14" => .osc_14, + .@"15" => .osc_15, + .@"16" => .osc_16, + .@"17" => .osc_17, + .@"18" => .osc_18, + .@"19" => .osc_19, + .@"104" => .osc_104, + .@"110" => .osc_110, + .@"111" => .osc_111, + .@"112" => .osc_112, + .@"113" => .osc_113, + .@"114" => .osc_114, + .@"115" => .osc_115, + .@"116" => .osc_116, + .@"117" => .osc_117, + .@"118" => .osc_118, + .@"119" => .osc_119, + else => { + parser.state = .invalid; + return null; + }, + }; + parser.command = .{ + .color_operation = .{ + .op = op, + .requests = parseColor(alloc, op, data) catch |err| list: { + log.info( + "failed to parse OSC {t} color request err={} data={s}", + .{ parser.state, err, data }, + ); + break :list .{}; + }, + .terminator = .init(terminator_ch), + }, + }; + return &parser.command; +} + +test "OSC 4: empty param" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "4;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b'); + try testing.expect(cmd == null); +} + /// Parse any color operation string. This should NOT include the operation /// itself, but only the body of the operation. e.g. for "4;a;b;c" the body /// should be "a;b;c" and the operation should be set accordingly. @@ -46,7 +121,7 @@ pub const Operation = enum { /// request) but grants us an easier to understand and testable implementation. /// /// If color changing ends up being a bottleneck we can optimize this later. -pub fn parse( +fn parseColor( alloc: Allocator, op: Operation, buf: []const u8, @@ -295,7 +370,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -317,7 +392,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -336,7 +411,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -360,7 +435,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -387,7 +462,7 @@ test "OSC 4:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_4, body); + var list = try parseColor(alloc, .osc_4, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -419,7 +494,7 @@ test "OSC 5:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_5, body); + var list = try parseColor(alloc, .osc_5, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -439,7 +514,7 @@ test "OSC 4: multiple requests" { // printf '\e]4;0;red;1;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_4, "0;red;1;blue", @@ -465,7 +540,7 @@ test "OSC 4: multiple requests" { // Multiple requests with same index overwrite each other // printf '\e]4;0;red;0;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_4, "0;red;0;blue", @@ -505,7 +580,7 @@ test "OSC 104:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_104, body); + var list = try parseColor(alloc, .osc_104, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -529,7 +604,7 @@ test "OSC 104:" { ); defer alloc.free(body); - var list = try parse(alloc, .osc_104, body); + var list = try parseColor(alloc, .osc_104, body); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -544,7 +619,7 @@ test "OSC 104: empty index" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, "0;;1"); + var list = try parseColor(alloc, .osc_104, "0;;1"); defer list.deinit(alloc); try testing.expectEqual(2, list.count()); try testing.expectEqual( @@ -561,7 +636,7 @@ test "OSC 104: invalid index" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, "ffff;1"); + var list = try parseColor(alloc, .osc_104, "ffff;1"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -574,7 +649,7 @@ test "OSC 104: reset all" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_104, ""); + var list = try parseColor(alloc, .osc_104, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -587,7 +662,7 @@ test "OSC 105: reset all" { const testing = std.testing; const alloc = testing.allocator; - var list = try parse(alloc, .osc_105, ""); + var list = try parseColor(alloc, .osc_105, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -611,7 +686,7 @@ test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: // Example script: // printf '\e]10;red\e\\' { - var list = try parse(alloc, op, "red"); + var list = try parseColor(alloc, op, "red"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -632,7 +707,7 @@ test "OSC 10: OSC 11: OSC 12: OSC: 13: OSC 14: OSC 15: OSC: 16: OSC 17: OSC 18: // Example script: // printf '\e]11;red;blue\e\\' { - var list = try parse( + var list = try parseColor( alloc, .osc_11, "red;blue", @@ -671,7 +746,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // Example script: // printf '\e]110\e\\' { - var list = try parse(alloc, op, ""); + var list = try parseColor(alloc, op, ""); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -684,7 +759,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // // printf '\e]110;\e\\' { - var list = try parse(alloc, op, ";"); + var list = try parseColor(alloc, op, ";"); defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( @@ -697,7 +772,7 @@ test "OSC 110: OSC 111: OSC 112: OSC: 113: OSC 114: OSC 115: OSC: 116: OSC 117: // // printf '\e]110 \e\\' { - var list = try parse(alloc, op, " "); + var list = try parseColor(alloc, op, " "); defer list.deinit(alloc); try testing.expectEqual(0, list.count()); } diff --git a/src/terminal/osc/parsers/hyperlink.zig b/src/terminal/osc/parsers/hyperlink.zig new file mode 100644 index 000000000..cf328beb5 --- /dev/null +++ b/src/terminal/osc/parsers/hyperlink.zig @@ -0,0 +1,164 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_hyperlink); + +/// Parse OSC 8 hyperlinks +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + const s = std.mem.indexOfScalar(u8, data, ';') orelse { + parser.state = .invalid; + return null; + }; + + parser.command = .{ + .hyperlink_start = .{ + .uri = data[s + 1 .. data.len - 1 :0], + }, + }; + + data[s] = 0; + const kvs = data[0 .. s + 1]; + std.mem.replaceScalar(u8, kvs, ':', 0); + var kv_start: usize = 0; + while (kv_start < kvs.len) { + const kv_end = std.mem.indexOfScalarPos(u8, kvs, kv_start + 1, 0) orelse break; + const kv = data[kv_start .. kv_end + 1]; + const v = std.mem.indexOfScalar(u8, kv, '=') orelse break; + const key = kv[0..v]; + const value = kv[v + 1 .. kv.len - 1 :0]; + if (std.mem.eql(u8, key, "id")) { + if (value.len > 0) parser.command.hyperlink_start.id = value; + } else { + log.warn("unknown hyperlink option: '{s}'", .{key}); + } + kv_start = kv_end + 1; + } + + if (parser.command.hyperlink_start.uri.len == 0) { + if (parser.command.hyperlink_start.id != null) { + parser.state = .invalid; + return null; + } + parser.command = .hyperlink_end; + } + + return &parser.command; +} + +test "OSC 8: hyperlink" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;;http://example.com"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_start); + try testing.expectEqualStrings(cmd.hyperlink_start.uri, "http://example.com"); +} + +test "OSC 8: hyperlink with id set" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id=foo;http://example.com"; + for (input) |ch| p.next(ch); + + 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"); +} + +test "OSC 8: hyperlink with empty id" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id=;http://example.com"; + for (input) |ch| p.next(ch); + + 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"); +} + +test "OSC 8: hyperlink with incomplete key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id;http://example.com"; + for (input) |ch| p.next(ch); + + 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"); +} + +test "OSC 8: hyperlink with empty key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;=value;http://example.com"; + for (input) |ch| p.next(ch); + + 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"); +} + +test "OSC 8: hyperlink with empty key and id" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;=value:id=foo;http://example.com"; + for (input) |ch| p.next(ch); + + 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"); +} + +test "OSC 8: hyperlink with empty uri" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;id=foo;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b'); + try testing.expect(cmd == null); +} + +test "OSC 8: hyperlink end" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "8;;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .hyperlink_end); +} diff --git a/src/terminal/osc/parsers/kitty_color.zig b/src/terminal/osc/parsers/kitty_color.zig new file mode 100644 index 000000000..30a7fe77f --- /dev/null +++ b/src/terminal/osc/parsers/kitty_color.zig @@ -0,0 +1,212 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; +const kitty_color = @import("../../kitty/color.zig"); +const RGB = @import("../../color.zig").RGB; + +const log = std.log.scoped(.osc_kitty_color); + +/// Parse OSC 21, the Kitty Color Protocol. +pub fn parse(parser: *Parser, terminator_ch: ?u8) ?*Command { + assert(parser.state == .@"21"); + + const alloc = parser.alloc orelse { + parser.state = .invalid; + return null; + }; + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + parser.command = .{ + .kitty_color_protocol = .{ + .list = .empty, + .terminator = .init(terminator_ch), + }, + }; + const list = &parser.command.kitty_color_protocol.list; + const data = writer.buffered(); + var kv_it = std.mem.splitScalar(u8, data, ';'); + while (kv_it.next()) |kv| { + if (list.items.len >= @as(usize, kitty_color.Kind.max) * 2) { + log.warn("exceeded limit for number of keys in kitty color protocol, ignoring", .{}); + parser.state = .invalid; + return null; + } + var it = std.mem.splitScalar(u8, kv, '='); + const k = it.next() orelse continue; + if (k.len == 0) { + log.warn("zero length key in kitty color protocol", .{}); + continue; + } + const key = kitty_color.Kind.parse(k) orelse { + log.warn("unknown key in kitty color protocol: {s}", .{k}); + continue; + }; + const value = std.mem.trim(u8, it.rest(), " "); + if (value.len == 0) { + list.append(alloc, .{ .reset = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } else if (std.mem.eql(u8, "?", value)) { + list.append(alloc, .{ .query = key }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } else { + list.append(alloc, .{ + .set = .{ + .key = key, + .color = RGB.parse(value) catch |err| switch (err) { + error.InvalidFormat => { + log.warn("invalid color format in kitty color protocol: {s}", .{value}); + continue; + }, + }, + }, + }) catch |err| { + log.warn("unable to append kitty color protocol option: {}", .{err}); + continue; + }; + } + } + return &parser.command; +} + +test "OSC 21: kitty color protocol" { + const testing = std.testing; + const Kind = kitty_color.Kind; + + 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"; + for (input) |ch| p.next(ch); + + 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); + { + const item = cmd.kitty_color_protocol.list.items[0]; + try testing.expect(item == .query); + try testing.expectEqual(Kind{ .special = .foreground }, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[1]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .special = .background }, item.set.key); + try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); + try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[2]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .special = .cursor }, item.set.key); + try testing.expectEqual(@as(u8, 0xf0), item.set.color.r); + try testing.expectEqual(@as(u8, 0xf8), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[3]; + try testing.expect(item == .reset); + try testing.expectEqual(Kind{ .special = .cursor_text }, item.reset); + } + { + const item = cmd.kitty_color_protocol.list.items[4]; + try testing.expect(item == .reset); + try testing.expectEqual(Kind{ .special = .visual_bell }, item.reset); + } + { + const item = cmd.kitty_color_protocol.list.items[5]; + try testing.expect(item == .query); + try testing.expectEqual(Kind{ .special = .selection_background }, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[6]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .special = .selection_background }, item.set.key); + try testing.expectEqual(@as(u8, 0xaa), item.set.color.r); + try testing.expectEqual(@as(u8, 0xbb), item.set.color.g); + try testing.expectEqual(@as(u8, 0xcc), item.set.color.b); + } + { + const item = cmd.kitty_color_protocol.list.items[7]; + try testing.expect(item == .query); + try testing.expectEqual(Kind{ .palette = 2 }, item.query); + } + { + const item = cmd.kitty_color_protocol.list.items[8]; + try testing.expect(item == .set); + try testing.expectEqual(Kind{ .palette = 3 }, item.set.key); + try testing.expectEqual(@as(u8, 0xff), item.set.color.r); + try testing.expectEqual(@as(u8, 0xff), item.set.color.g); + try testing.expectEqual(@as(u8, 0xff), item.set.color.b); + } +} + +test "OSC 21: kitty color protocol without allocator" { + const testing = std.testing; + + var p: Parser = .init(null); + defer p.deinit(); + + const input = "21;foreground=?"; + for (input) |ch| p.next(ch); + try testing.expect(p.end('\x1b') == null); +} + +test "OSC 21: kitty color protocol double reset" { + const testing = std.testing; + + 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"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + + p.reset(); + p.reset(); +} + +test "OSC 21: kitty color protocol reset after invalid" { + const testing = std.testing; + + 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"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + + p.reset(); + + try testing.expectEqual(Parser.State.start, p.state); + p.next('X'); + try testing.expectEqual(Parser.State.invalid, p.state); + + p.reset(); +} + +test "OSC 21: kitty color protocol no key" { + const testing = std.testing; + + var p: Parser = .init(testing.allocator); + defer p.deinit(); + + const input = "21;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_color_protocol); + try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); +} diff --git a/src/terminal/osc/parsers/mouse_shape.zig b/src/terminal/osc/parsers/mouse_shape.zig new file mode 100644 index 000000000..91c5ab270 --- /dev/null +++ b/src/terminal/osc/parsers/mouse_shape.zig @@ -0,0 +1,39 @@ +const std = @import("std"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +// Parse OSC 22 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"22"); + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .mouse_shape = .{ + .value = data[0 .. data.len - 1 :0], + }, + }; + return &parser.command; +} + +test "OSC 22: pointer cursor" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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); +} diff --git a/src/terminal/osc/parsers/osc9.zig b/src/terminal/osc/parsers/osc9.zig new file mode 100644 index 000000000..1ca7ba5a0 --- /dev/null +++ b/src/terminal/osc/parsers/osc9.zig @@ -0,0 +1,766 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 9, which could be an iTerm2 notification or a ConEmu extension. +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + + // Check first to see if this is a ConEmu OSC + // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + conemu: { + var data = writer.buffered(); + if (data.len == 0) break :conemu; + switch (data[0]) { + // Check for OSC 9;1 9;10 9;12 + '1' => { + if (data.len < 2) break :conemu; + switch (data[1]) { + // OSC 9;1 + ';' => { + parser.command = .{ + .conemu_sleep = .{ + .duration_ms = if (std.fmt.parseUnsigned(u16, data[2..], 10)) |num| @min(num, 10_000) else |_| 100, + }, + }; + return &parser.command; + }, + // OSC 9;10 + '0' => { + parser.state = .invalid; + return null; + }, + // OSC 9;12 + '2' => { + parser.command = .{ + .prompt_start = .{}, + }; + return &parser.command; + }, + else => break :conemu, + } + }, + // OSC 9;2 + '2' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_show_message_box = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;3 + '3' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + if (data.len == 2) { + parser.command = .{ + .conemu_change_tab_title = .reset, + }; + return &parser.command; + } + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_change_tab_title = .{ + .value = data[2 .. data.len - 1 :0], + }, + }; + return &parser.command; + }, + // OSC 9;4 + '4' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + if (data.len < 3) break :conemu; + switch (data[2]) { + '0' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .remove, + }, + }; + }, + '1' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .set, + .progress = 0, + }, + }; + }, + '2' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .@"error", + }, + }; + }, + '3' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .indeterminate, + }, + }; + }, + '4' => { + parser.command = .{ + .conemu_progress_report = .{ + .state = .pause, + }, + }; + }, + else => break :conemu, + } + switch (parser.command.conemu_progress_report.state) { + .remove, .indeterminate => {}, + .set, .@"error", .pause => progress: { + if (data.len < 4) break :progress; + if (data[3] != ';') break :progress; + // parse the progress value + parser.command.conemu_progress_report.progress = value: { + break :value @intCast(std.math.clamp( + std.fmt.parseUnsigned(usize, data[4..], 10) catch break :value null, + 0, + 100, + )); + }; + }, + } + return &parser.command; + }, + // OSC 9;5 + '5' => { + parser.command = .conemu_wait_input; + return &parser.command; + }, + // OSC 9;6 + '6' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + data = writer.buffered(); + parser.command = .{ + .conemu_guimacro = data[2 .. data.len - 1 :0], + }; + return &parser.command; + }, + // OSC 9;7 + '7' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + parser.state = .invalid; + return null; + }, + // OSC 9;8 + '8' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + parser.state = .invalid; + return null; + }, + // OSC 9;9 + '9' => { + if (data.len < 2) break :conemu; + if (data[1] != ';') break :conemu; + parser.state = .invalid; + return null; + }, + else => break :conemu, + } + } + + // If it's not a ConEmu OSC, it's an iTerm2 notification + + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .show_desktop_notification = .{ + .title = "", + .body = data[0 .. data.len - 1 :0], + }, + }; + return &parser.command; +} + +test "OSC 9: show desktop notification" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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 = .init(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 = .init(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 = .init(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); +} diff --git a/src/terminal/osc/parsers/report_pwd.zig b/src/terminal/osc/parsers/report_pwd.zig new file mode 100644 index 000000000..080b9cbb0 --- /dev/null +++ b/src/terminal/osc/parsers/report_pwd.zig @@ -0,0 +1,48 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +/// Parse OSC 7 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + parser.command = .{ + .report_pwd = .{ + .value = data[0 .. data.len - 1 :0], + }, + }; + return &parser.command; +} + +test "OSC 7: report pwd" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "7;file:///tmp/example"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("file:///tmp/example", cmd.report_pwd.value); +} + +test "OSC 7: report pwd empty" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "7;"; + for (input) |ch| p.next(ch); + const cmd = p.end(null).?.*; + try testing.expect(cmd == .report_pwd); + try testing.expectEqualStrings("", cmd.report_pwd.value); +} diff --git a/src/terminal/osc/parsers/rxvt_extension.zig b/src/terminal/osc/parsers/rxvt_extension.zig new file mode 100644 index 000000000..94a0961d2 --- /dev/null +++ b/src/terminal/osc/parsers/rxvt_extension.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_rxvt_extension); + +/// Parse OSC 777 +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + // ensure that we are sentinel terminated + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + const k = std.mem.indexOfScalar(u8, data, ';') orelse { + parser.state = .invalid; + return null; + }; + const ext = data[0..k]; + if (!std.mem.eql(u8, ext, "notify")) { + log.warn("unknown rxvt extension: {s}", .{ext}); + parser.state = .invalid; + return null; + } + const t = std.mem.indexOfScalarPos(u8, data, k + 1, ';') orelse { + log.warn("rxvt notify extension is missing the title", .{}); + parser.state = .invalid; + return null; + }; + data[t] = 0; + const title = data[k + 1 .. t :0]; + const body = data[t + 1 .. data.len - 1 :0]; + parser.command = .{ + .show_desktop_notification = .{ + .title = title, + .body = body, + }, + }; + return &parser.command; +} + +test "OSC: OSC 777 show desktop notification with title" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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"); +} diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig new file mode 100644 index 000000000..510fe3447 --- /dev/null +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -0,0 +1,694 @@ +const std = @import("std"); + +const string_encoding = @import("../../../os/string_encoding.zig"); + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; + +const log = std.log.scoped(.osc_semantic_prompt); + +/// Parse OSC 133, semantic prompts +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + if (data.len == 0) { + parser.state = .invalid; + return null; + } + switch (data[0]) { + 'A' => prompt_start: { + parser.command = .{ + .prompt_start = .{}, + }; + if (data.len == 1) break :prompt_start; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + parser.state = .invalid; + return null; + }; + while (it.next()) |kv| { + if (std.mem.eql(u8, kv.key, "aid")) { + parser.command.prompt_start.aid = kv.value; + } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + // 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. + parser.command.prompt_start.redraw = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A: invalid redraw value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + parser.command.prompt_start.special_key = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A invalid special_key value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "click_events")) redraw: { + // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers + parser.command.prompt_start.click_events = (value: { + if (kv.value.len != 1) break :value null; + switch (kv.value[0]) { + '0' => break :value false, + '1' => break :value true, + else => break :value null, + } + }) orelse { + log.info("OSC 133 A invalid click_events value: {s}", .{kv.value}); + break :redraw; + }; + } else if (std.mem.eql(u8, kv.key, "k")) k: { + // 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. + if (kv.value.len != 1) break :k; + parser.command.prompt_start.kind = switch (kv.value[0]) { + 'c' => .continuation, + 's' => .secondary, + 'r' => .right, + 'i' => .primary, + else => .primary, + }; + } else log.info("OSC 133 A: unknown semantic prompt option: {s}", .{kv.key}); + } + }, + 'B' => prompt_end: { + parser.command = .prompt_end; + if (data.len == 1) break :prompt_end; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + parser.state = .invalid; + return null; + }; + while (it.next()) |kv| { + log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); + } + }, + 'C' => end_of_input: { + parser.command = .{ + .end_of_input = .{}, + }; + if (data.len == 1) break :end_of_input; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + var it = SemanticPromptKVIterator.init(writer) catch { + parser.state = .invalid; + return null; + }; + while (it.next()) |kv| { + if (std.mem.eql(u8, kv.key, "cmdline")) { + parser.command.end_of_input.cmdline = string_encoding.printfQDecode(kv.value) catch null; + } else if (std.mem.eql(u8, kv.key, "cmdline_url")) { + parser.command.end_of_input.cmdline = string_encoding.urlPercentDecode(kv.value) catch null; + } else { + log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); + } + } + }, + 'D' => { + const exit_code: ?u8 = exit_code: { + if (data.len == 1) break :exit_code null; + if (data[1] != ';') { + parser.state = .invalid; + return null; + } + break :exit_code std.fmt.parseUnsigned(u8, data[2..], 10) catch null; + }; + parser.command = .{ + .end_of_command = .{ + .exit_code = exit_code, + }, + }; + }, + else => { + parser.state = .invalid; + return null; + }, + } + return &parser.command; +} + +const SemanticPromptKVIterator = struct { + index: usize, + string: []u8, + + pub const SemanticPromptKV = struct { + key: [:0]u8, + value: [:0]u8, + }; + + pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { + // add a semicolon to make it easier to find and sentinel terminate the values + try writer.writeByte(';'); + return .{ + .index = 0, + .string = writer.buffered()[2..], + }; + } + + pub fn next(self: *SemanticPromptKVIterator) ?SemanticPromptKV { + if (self.index >= self.string.len) return null; + + const kv = kv: { + const index = std.mem.indexOfScalarPos(u8, self.string, self.index, ';') orelse { + self.index = self.string.len; + return null; + }; + self.string[index] = 0; + const kv = self.string[self.index..index :0]; + self.index = index + 1; + break :kv kv; + }; + + const key = key: { + const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; + kv[index] = 0; + const key = kv[0..index :0]; + break :key key; + }; + + const value = kv[key.len + 1 .. :0]; + + return .{ + .key = key, + .value = value, + }; + } +}; + +test "OSC 133: prompt_start" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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(null); + + 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 '=' in aid" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;aid=a=b;redraw=0"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); + try testing.expectEqualStrings("a=b", cmd.prompt_start.aid.?); + try testing.expect(!cmd.prompt_start.redraw); +} + +test "OSC 133: prompt_start with redraw disabled" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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(null); + + 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(null); + + 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(null); + + 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: prompt_start with special_key" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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; + + var p: Parser = .init(null); + + 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(null); + + 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(null); + + 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(null); + + const input = "133;C"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .end_of_input); +} + +test "OSC 133: end_of_input with cmdline 1" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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 2" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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.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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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(null); + + 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); +} diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index c33dba1bb..1ee4f3f08 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -4,7 +4,7 @@ const stream = @import("stream.zig"); const Action = stream.Action; const Screen = @import("Screen.zig"); const modes = @import("modes.zig"); -const osc_color = @import("osc/color.zig"); +const osc_color = @import("osc/parsers/color.zig"); const kitty_color = @import("kitty/color.zig"); const Terminal = @import("Terminal.zig"); -- 2.51.2 From 856ef1fc1bed143a84c88c32812758828520de7e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 06:32:37 -0800 Subject: [PATCH 222/349] input: change the key_is_binding to return some information --- include/ghostty.h | 11 ++++++++++- src/Surface.zig | 39 ++++++++++++++++++++++++--------------- src/apprt/embedded.zig | 7 ++++++- src/input/Binding.zig | 21 +++++++++++++++++++++ 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index ff28a0cff..b884ebc08 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -102,6 +102,13 @@ typedef enum { GHOSTTY_MODS_SUPER_RIGHT = 1 << 9, } ghostty_input_mods_e; +typedef enum { + GHOSTTY_BINDING_FLAGS_CONSUMED = 1 << 0, + GHOSTTY_BINDING_FLAGS_ALL = 1 << 1, + GHOSTTY_BINDING_FLAGS_GLOBAL = 1 << 2, + GHOSTTY_BINDING_FLAGS_PERFORMABLE = 1 << 3, +} ghostty_binding_flags_e; + typedef enum { GHOSTTY_ACTION_RELEASE, GHOSTTY_ACTION_PRESS, @@ -1058,7 +1065,9 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); -bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s); +bool ghostty_surface_key_is_binding(ghostty_surface_t, + ghostty_input_key_s, + ghostty_binding_flags_e*); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); bool ghostty_surface_mouse_captured(ghostty_surface_t); diff --git a/src/Surface.zig b/src/Surface.zig index cc727826f..9998922b9 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2579,7 +2579,7 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void { pub fn keyEventIsBinding( self: *Surface, event_orig: input.KeyEvent, -) bool { +) ?input.Binding.Flags { // Apply key remappings for consistency with keyCallback var event = event_orig; if (self.config.key_remaps.isRemapped(event_orig.mods)) { @@ -2587,26 +2587,35 @@ pub fn keyEventIsBinding( } switch (event.action) { - .release => return false, + .release => return null, .press, .repeat => {}, } - // If we're in a sequence, check the sequence set - if (self.keyboard.sequence_set) |set| { - return set.getEvent(event) != null; - } + // Look up our entry + const entry: input.Binding.Set.Entry = entry: { + // If we're in a sequence, check the sequence set + if (self.keyboard.sequence_set) |set| { + break :entry set.getEvent(event) orelse return null; + } - // Check active key tables (inner-most to outer-most) - const table_items = self.keyboard.table_stack.items; - for (0..table_items.len) |i| { - const rev_i: usize = table_items.len - 1 - i; - if (table_items[rev_i].set.getEvent(event) != null) { - return true; + // Check active key tables (inner-most to outer-most) + const table_items = self.keyboard.table_stack.items; + for (0..table_items.len) |i| { + const rev_i: usize = table_items.len - 1 - i; + if (table_items[rev_i].set.getEvent(event)) |entry| { + break :entry entry; + } } - } - // Check the root set - return self.config.keybind.set.getEvent(event) != null; + // Check the root set + break :entry self.config.keybind.set.getEvent(event) orelse return null; + }; + + // Return flags based on the + return switch (entry.value_ptr.*) { + .leader => .{}, + inline .leaf, .leaf_chained => |v| v.flags, + }; } /// Called for any key events. This handles keybindings, encoding and diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 6c1d46722..364a1bec1 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1751,13 +1751,18 @@ pub const CAPI = struct { export fn ghostty_surface_key_is_binding( surface: *Surface, event: KeyEvent, + c_flags: ?*input.Binding.Flags.C, ) bool { const core_event = event.keyEvent().core() orelse { log.warn("error processing key event", .{}); return false; }; - return surface.core_surface.keyEventIsBinding(core_event); + const flags = surface.core_surface.keyEventIsBinding( + core_event, + ) orelse return false; + if (c_flags) |ptr| ptr.* = flags.cval(); + return true; } /// Send raw text to the terminal. This is treated like a paste diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 3197bb7d1..08475c7e1 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -45,6 +45,27 @@ pub const Flags = packed struct { /// performed. If the action can't be performed then the binding acts as /// if it doesn't exist. performable: bool = false, + + /// C type + pub const C = u8; + + /// Converts this to a C-compatible value. + /// + /// Sync with ghostty.h for enums. + pub fn cval(self: Flags) C { + const Backing = @typeInfo(Flags).@"struct".backing_integer.?; + return @as(Backing, @bitCast(self)); + } + + test "cval" { + const testing = std.testing; + try testing.expectEqual(@as(u8, 0b0001), (Flags{}).cval()); + try testing.expectEqual(@as(u8, 0b0000), (Flags{ .consumed = false }).cval()); + try testing.expectEqual(@as(u8, 0b0011), (Flags{ .all = true }).cval()); + try testing.expectEqual(@as(u8, 0b0101), (Flags{ .global = true }).cval()); + try testing.expectEqual(@as(u8, 0b1001), (Flags{ .performable = true }).cval()); + try testing.expectEqual(@as(u8, 0b1111), (Flags{ .consumed = true, .all = true, .global = true, .performable = true }).cval()); + } }; /// Full binding parser. The binding parser is implemented as an iterator -- 2.51.2 From 49768c646496200110a60f3be0b6ffe2a369e476 Mon Sep 17 00:00:00 2001 From: Maciek Borzecki Date: Fri, 9 Jan 2026 15:48:41 +0100 Subject: [PATCH 223/349] snap: fix handling of nonexistent last_revision file Assuming /bin/sh is symlinked to bash, the handling of special builtin 'source' is slightly different between bash and bash-in-POSIX-mode (as a result of being invoked through /bin/sh). Specifically errors in builtin 'source' cannot be masked with `|| true`. Compare $ ls -l /bin/sh lrwxrwxrwx 1 root root 4 Dec 11 11:00 /bin/sh -> bash $ /bin/sh -c 'set -e ; source nofile || true; echo ok' /bin/sh: line 1: source: nofile: file not found $ /bin/bash -c 'set -e ; source nofile || true; echo ok' /bin/bash: line 1: nofile: No such file or directory ok Thus ghostty from snap would not start at all when $SNAP_USER_DATA/.last_revision does not exist causign the launcher script to exit prematurely. Signed-off-by: Maciek Borzecki --- snap/local/launcher | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/snap/local/launcher b/snap/local/launcher index 89e0d1709..71b92f5bb 100755 --- a/snap/local/launcher +++ b/snap/local/launcher @@ -14,7 +14,14 @@ if [ -z "$XDG_DATA_HOME" ]; then export XDG_DATA_HOME="$SNAP_REAL_HOME/.local/share" fi -source "$SNAP_USER_DATA/.last_revision" 2>/dev/null || true +if [ -f "$SNAP_USER_DATA/.last_revision" ]; then + if ! source "$SNAP_USER_DATA/.last_revision" 2>/dev/null; then + # file exist but sourcing it fails, so it's likely + # not good anyway + rm -f "$SNAP_USER_DATA/.last_revision" + fi +fi + if [ "$LAST_REVISION" = "$SNAP_REVISION" ]; then needs_update=false else -- 2.51.2 From f34c69147a7c583a7cde667f7cd586a55c5b592b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 06:58:48 -0800 Subject: [PATCH 224/349] macos: use the new binding flags information value to trigger menu --- macos/Sources/Ghostty/Ghostty.Input.swift | 26 +++++++++++ macos/Sources/Ghostty/Ghostty.Surface.swift | 20 +++++++++ .../Surface View/SurfaceView_AppKit.swift | 43 +++++++++++-------- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 44011d5b9..4b3fb9937 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -100,6 +100,32 @@ extension Ghostty { ] } +// MARK: Ghostty.Input.BindingFlags + +extension Ghostty.Input { + /// `ghostty_binding_flags_e` + struct BindingFlags: OptionSet, Sendable { + let rawValue: UInt32 + + static let consumed = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_CONSUMED.rawValue) + static let all = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_ALL.rawValue) + static let global = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_GLOBAL.rawValue) + static let performable = BindingFlags(rawValue: GHOSTTY_BINDING_FLAGS_PERFORMABLE.rawValue) + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + init(cFlags: ghostty_binding_flags_e) { + self.rawValue = cFlags.rawValue + } + + var cFlags: ghostty_binding_flags_e { + ghostty_binding_flags_e(rawValue) + } + } +} + // MARK: Ghostty.Input.KeyEvent extension Ghostty.Input { diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index e86952e50..7cb32ed71 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -62,6 +62,26 @@ extension Ghostty { } } + /// Check if a key event matches a keybinding. + /// + /// This checks whether the given key event would trigger a keybinding in the terminal. + /// If it matches, returns the binding flags indicating properties of the matched binding. + /// + /// - Parameter event: The key event to check + /// - Returns: The binding flags if a binding matches, or nil if no binding matches + @MainActor + func keyIsBinding(_ event: ghostty_input_key_s) -> Input.BindingFlags? { + var flags = ghostty_binding_flags_e(0) + guard ghostty_surface_key_is_binding(surface, event, &flags) else { return nil } + return Input.BindingFlags(cFlags: flags) + } + + /// See `keyIsBinding(_ event: ghostty_input_key_s)`. + @MainActor + func keyIsBinding(_ event: Input.KeyEvent) -> Input.BindingFlags? { + event.withCValue { keyIsBinding($0) } + } + /// Whether the terminal has captured mouse input. /// /// When the mouse is captured, the terminal application is receiving mouse events diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index ca74c7815..80de2e823 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1184,7 +1184,7 @@ extension Ghostty { // We only care about key down events. It might not even be possible // to receive any other event type here. guard event.type == .keyDown else { return false } - + // Only process events if we're focused. Some key events like C-/ macOS // appears to send to the first view in the hierarchy rather than the // the first responder (I don't know why). This prevents us from handling it. @@ -1194,26 +1194,35 @@ extension Ghostty { if (!focused) { return false } - - // Let the menu system handle this event if we're not in a key sequence or key table. - // This allows the menu bar to flash for shortcuts like Command+V. - if keySequence.isEmpty && keyTables.isEmpty { - if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { - return true - } - } - - // If the menu didn't handle it, check Ghostty bindings for custom shortcuts. - if let surface { + + // Get information about if this is a binding. + let bindingFlags = surfaceModel.flatMap { surface in var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) - let match = (event.characters ?? "").withCString { ptr in + return (event.characters ?? "").withCString { ptr in ghosttyEvent.text = ptr - return ghostty_surface_key_is_binding(surface, ghosttyEvent) + return surface.keyIsBinding(ghosttyEvent) } - if match { - self.keyDown(with: event) - return true + } + + // If this is a binding then we want to perform it. + if let bindingFlags { + // Attempt to trigger a menu item for this key binding. We only do this if: + // - We're not in a key sequence or table (those are separate bindings) + // - The binding is NOT `all` (menu uses FirstResponder chain) + // - The binding is NOT `performable` (menu will always consume) + // - The binding is `consumed` (unconsumed bindings should pass through + // to the terminal, so we must not intercept them for the menu) + if keySequence.isEmpty, + keyTables.isEmpty, + bindingFlags.isDisjoint(with: [.all, .performable]), + bindingFlags.contains(.consumed) { + if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { + return true + } } + + self.keyDown(with: event) + return true } let equivalent: String -- 2.51.2 From 115351db87cc1c1c89e2d217e82e48d484a4b25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20M=C3=BCller?= Date: Fri, 9 Jan 2026 16:19:50 +0100 Subject: [PATCH 225/349] docs: bell border feature is available on macOS As of commit fe55d90 and PR #8768 this feature is also available on macOS. --- src/config/Config.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 2f0bef6ff..b0ef26554 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2934,8 +2934,6 @@ keybind: Keybinds = .{}, /// Display a border around the alerted surface until the terminal is /// re-focused or interacted with (such as on keyboard input). /// -/// GTK only. -/// /// Example: `audio`, `no-audio`, `system`, `no-system` /// /// Available since: 1.2.0 -- 2.51.2 From c179de62a70ebd848a63bfbb34459d47b0ac918d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 08:27:27 -0800 Subject: [PATCH 226/349] extract deepEqual --- src/config/Config.zig | 94 +---------- src/datastruct/comparison.zig | 297 +++++++++++++++++++++++++++++++++- src/datastruct/main.zig | 2 + 3 files changed, 297 insertions(+), 96 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 2f0bef6ff..ac52595be 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -17,6 +17,7 @@ const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const global_state = &@import("../global.zig").state; +const deepEqual = @import("../datastruct/comparison.zig").deepEqual; const fontpkg = @import("../font/main.zig"); const inputpkg = @import("../input.zig"); const internal_os = @import("../os/main.zig"); @@ -4120,7 +4121,7 @@ pub fn changeConditionalState( // Conditional set contains the keys that this config uses. So we // only continue if we use this key. - if (self._conditional_set.contains(key) and !equalField( + if (self._conditional_set.contains(key) and !deepEqual( @TypeOf(@field(self._conditional_state, field.name)), @field(self._conditional_state, field.name), @field(new, field.name), @@ -4826,7 +4827,7 @@ pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool const old_value = @field(self, field.name); const new_value = @field(new, field.name); - return !equalField(field.type, old_value, new_value); + return !deepEqual(field.type, old_value, new_value); } /// This yields a key for every changed field between old and new. @@ -4854,91 +4855,6 @@ pub const ChangeIterator = struct { } }; -/// A config-specific helper to determine if two values of the same -/// type are equal. This isn't the same as std.mem.eql or std.testing.equals -/// because we expect structs to implement their own equality. -/// -/// This also doesn't support ALL Zig types, because we only add to it -/// as we need types for the config. -fn equalField(comptime T: type, old: T, new: T) bool { - // Do known named types first - switch (T) { - inline []const u8, - [:0]const u8, - => return std.mem.eql(u8, old, new), - - []const [:0]const u8, - => { - if (old.len != new.len) return false; - for (old, new) |a, b| { - if (!std.mem.eql(u8, a, b)) return false; - } - - return true; - }, - - else => {}, - } - - // Back into types of types - switch (@typeInfo(T)) { - .void => return true, - - inline .bool, - .int, - .float, - .@"enum", - => return old == new, - - .optional => |info| { - if (old == null and new == null) return true; - if (old == null or new == null) return false; - return equalField(info.child, old.?, new.?); - }, - - .@"struct" => |info| { - if (@hasDecl(T, "equal")) return old.equal(new); - - // If a struct doesn't declare an "equal" function, we fall back - // to a recursive field-by-field compare. - inline for (info.fields) |field_info| { - if (!equalField( - field_info.type, - @field(old, field_info.name), - @field(new, field_info.name), - )) return false; - } - return true; - }, - - .@"union" => |info| { - if (@hasDecl(T, "equal")) return old.equal(new); - - const tag_type = info.tag_type.?; - const old_tag = std.meta.activeTag(old); - const new_tag = std.meta.activeTag(new); - if (old_tag != new_tag) return false; - - inline for (info.fields) |field_info| { - if (@field(tag_type, field_info.name) == old_tag) { - return equalField( - field_info.type, - @field(old, field_info.name), - @field(new, field_info.name), - ); - } - } - - unreachable; - }, - - else => { - @compileLog(T); - @compileError("unsupported field type"); - }, - } -} - /// This runs a heuristic to determine if we are likely running /// Ghostty in a CLI environment. We need this to change some behaviors. /// We should keep the set of behaviors that depend on this as small @@ -6885,7 +6801,7 @@ pub const Keybinds = struct { const self_leaf = self_entry.value_ptr.*.leaf; const other_leaf = other_entry.value_ptr.*.leaf; - if (!equalField( + if (!deepEqual( inputpkg.Binding.Set.Leaf, self_leaf, other_leaf, @@ -6899,7 +6815,7 @@ pub const Keybinds = struct { if (self_chain.flags != other_chain.flags) return false; if (self_chain.actions.items.len != other_chain.actions.items.len) return false; for (self_chain.actions.items, other_chain.actions.items) |a1, a2| { - if (!equalField( + if (!deepEqual( inputpkg.Binding.Action, a1, a2, diff --git a/src/datastruct/comparison.zig b/src/datastruct/comparison.zig index 4427c143c..6a862bb9c 100644 --- a/src/datastruct/comparison.zig +++ b/src/datastruct/comparison.zig @@ -1,11 +1,102 @@ // 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"); +const testing = std.testing; + +/// A deep equality comparison function that works for most types. We +/// add types as necessary. It defers to `equal` decls on types that support +/// decls. +pub fn deepEqual(comptime T: type, old: T, new: T) bool { + // Do known named types first + switch (T) { + inline []const u8, + [:0]const u8, + => return std.mem.eql(u8, old, new), + + []const [:0]const u8, + => { + if (old.len != new.len) return false; + for (old, new) |a, b| { + if (!std.mem.eql(u8, a, b)) return false; + } + + return true; + }, + + else => {}, + } + + // Back into types of types + switch (@typeInfo(T)) { + .void => return true, + + inline .bool, + .int, + .float, + .@"enum", + => return old == new, + + .optional => |info| { + if (old == null and new == null) return true; + if (old == null or new == null) return false; + return deepEqual(info.child, old.?, new.?); + }, + + .array => |info| for (old, new) |old_elem, new_elem| { + if (!deepEqual( + info.child, + old_elem, + new_elem, + )) return false; + } else return true, + + .@"struct" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + // If a struct doesn't declare an "equal" function, we fall back + // to a recursive field-by-field compare. + inline for (info.fields) |field_info| { + if (!deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + )) return false; + } + return true; + }, + + .@"union" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + + const tag_type = info.tag_type.?; + const old_tag = std.meta.activeTag(old); + const new_tag = std.meta.activeTag(new); + if (old_tag != new_tag) return false; + + inline for (info.fields) |field_info| { + if (@field(tag_type, field_info.name) == old_tag) { + return deepEqual( + field_info.type, + @field(old, field_info.name), + @field(new, field_info.name), + ); + } + } + + unreachable; + }, + + else => { + @compileLog(T); + @compileError("unsupported field type"); + }, + } +} /// Generic, recursive equality testing utility using approximate comparison for /// floats and equality for everything else /// -/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`. +/// Based on `testing.expectEqual` and `testing.expectEqualSlices`. /// /// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { @@ -59,7 +150,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { 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 { + testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }; } @@ -69,7 +160,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { const expectedTag = @as(Tag, expected); const actualTag = @as(Tag, actual); - std.testing.expectEqual(expectedTag, actualTag) catch { + testing.expectEqual(expectedTag, actualTag) catch { return error.TestExpectedApproxEqual; }; @@ -84,23 +175,23 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { }; // 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 { + testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }; }, // fall back to expectEqual for everything else - else => std.testing.expectEqual(expected, actual) catch { + else => testing.expectEqual(expected, actual) catch { return error.TestExpectedApproxEqual; }, } } -/// Copy of std.testing.print (not public) +/// Copy of testing.print (not public) fn print(comptime fmt: []const u8, args: anytype) void { if (@inComptime()) { @compileError(std.fmt.comptimePrint(fmt, args)); - } else if (std.testing.backend_can_print) { + } else if (testing.backend_can_print) { std.debug.print(fmt, args); } } @@ -145,3 +236,195 @@ test "expectApproxEqual struct" { try expectApproxEqual(a, b); } + +test "deepEqual void" { + try testing.expect(deepEqual(void, {}, {})); +} + +test "deepEqual bool" { + try testing.expect(deepEqual(bool, true, true)); + try testing.expect(deepEqual(bool, false, false)); + try testing.expect(!deepEqual(bool, true, false)); + try testing.expect(!deepEqual(bool, false, true)); +} + +test "deepEqual int" { + try testing.expect(deepEqual(i32, 42, 42)); + try testing.expect(deepEqual(i32, -100, -100)); + try testing.expect(!deepEqual(i32, 42, 43)); + try testing.expect(deepEqual(u64, 0, 0)); + try testing.expect(!deepEqual(u64, 0, 1)); +} + +test "deepEqual float" { + try testing.expect(deepEqual(f32, 1.0, 1.0)); + try testing.expect(!deepEqual(f32, 1.0, 1.1)); + try testing.expect(deepEqual(f64, 3.14159, 3.14159)); + try testing.expect(!deepEqual(f64, 3.14159, 3.14158)); +} + +test "deepEqual enum" { + const Color = enum { red, green, blue }; + try testing.expect(deepEqual(Color, .red, .red)); + try testing.expect(deepEqual(Color, .blue, .blue)); + try testing.expect(!deepEqual(Color, .red, .green)); + try testing.expect(!deepEqual(Color, .green, .blue)); +} + +test "deepEqual []const u8" { + try testing.expect(deepEqual([]const u8, "hello", "hello")); + try testing.expect(deepEqual([]const u8, "", "")); + try testing.expect(!deepEqual([]const u8, "hello", "world")); + try testing.expect(!deepEqual([]const u8, "hello", "hell")); + try testing.expect(!deepEqual([]const u8, "hello", "hello!")); +} + +test "deepEqual [:0]const u8" { + try testing.expect(deepEqual([:0]const u8, "foo", "foo")); + try testing.expect(!deepEqual([:0]const u8, "foo", "bar")); + try testing.expect(!deepEqual([:0]const u8, "foo", "fo")); +} + +test "deepEqual []const [:0]const u8" { + const a: []const [:0]const u8 = &.{ "one", "two", "three" }; + const b: []const [:0]const u8 = &.{ "one", "two", "three" }; + const c: []const [:0]const u8 = &.{ "one", "two" }; + const d: []const [:0]const u8 = &.{ "one", "two", "four" }; + const e: []const [:0]const u8 = &.{}; + + try testing.expect(deepEqual([]const [:0]const u8, a, b)); + try testing.expect(!deepEqual([]const [:0]const u8, a, c)); + try testing.expect(!deepEqual([]const [:0]const u8, a, d)); + try testing.expect(deepEqual([]const [:0]const u8, e, e)); + try testing.expect(!deepEqual([]const [:0]const u8, a, e)); +} + +test "deepEqual optional" { + try testing.expect(deepEqual(?i32, null, null)); + try testing.expect(deepEqual(?i32, 42, 42)); + try testing.expect(!deepEqual(?i32, null, 42)); + try testing.expect(!deepEqual(?i32, 42, null)); + try testing.expect(!deepEqual(?i32, 42, 43)); +} + +test "deepEqual optional nested" { + const Nested = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(?Nested, null, null)); + try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 })); + try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null)); +} + +test "deepEqual array" { + try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 })); + try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 })); + try testing.expect(deepEqual([0]i32, .{}, .{})); +} + +test "deepEqual nested array" { + const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } }; + const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } }; + + try testing.expect(deepEqual([2][2]i32, a, b)); + try testing.expect(!deepEqual([2][2]i32, a, c)); +} + +test "deepEqual struct" { + const Point = struct { x: i32, y: i32 }; + try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 })); + try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 })); +} + +test "deepEqual struct nested" { + const Inner = struct { value: i32 }; + const Outer = struct { a: Inner, b: Inner }; + + const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } }; + const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } }; + + try testing.expect(deepEqual(Outer, x, y)); + try testing.expect(!deepEqual(Outer, x, z)); +} + +test "deepEqual struct with equal decl" { + const Custom = struct { + value: i32, + + pub fn equal(self: @This(), other: @This()) bool { + return @mod(self.value, 10) == @mod(other.value, 10); + } + }; + + try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 })); + try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 })); + try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 })); +} + +test "deepEqual union" { + const Value = union(enum) { + int: i32, + float: f32, + none, + }; + + try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 })); + try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 })); + try testing.expect(deepEqual(Value, .none, .none)); + try testing.expect(!deepEqual(Value, .none, .{ .int = 0 })); +} + +test "deepEqual union with equal decl" { + const Value = union(enum) { + num: i32, + str: []const u8, + + pub fn equal(self: @This(), other: @This()) bool { + return switch (self) { + .num => |n| switch (other) { + .num => |m| @mod(n, 10) == @mod(m, 10), + else => false, + }, + .str => |s| switch (other) { + .str => |t| s.len == t.len, + else => false, + }, + }; + } + }; + + try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 })); + try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 })); + try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" })); + try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" })); +} + +test "deepEqual array of structs" { + const Item = struct { id: i32, name: []const u8 }; + const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } }; + const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } }; + + try testing.expect(deepEqual([2]Item, a, b)); + try testing.expect(!deepEqual([2]Item, a, c)); +} + +test "deepEqual struct with optional field" { + const Config = struct { name: []const u8, port: ?u16 }; + + try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 })); + try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null })); + try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 })); +} + +test "deepEqual struct with array field" { + const Data = struct { values: [3]i32 }; + + try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } })); + try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } })); +} diff --git a/src/datastruct/main.zig b/src/datastruct/main.zig index 64a29269e..bfee23427 100644 --- a/src/datastruct/main.zig +++ b/src/datastruct/main.zig @@ -19,4 +19,6 @@ pub const SplitTree = split_tree.SplitTree; test { @import("std").testing.refAllDecls(@This()); + + _ = @import("comparison.zig"); } -- 2.51.2 From 201198c74a8f6facc844c1564d1ebc48fa779bd6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 08:27:27 -0800 Subject: [PATCH 227/349] input: do value comparison for Set hash maps We previously only compared the hashes for triggers and actions for hash map equality. I'm genuinely surprised this never bit us before because it can result in false positives when two different values have the same hash. Fix that up! --- src/input/Binding.zig | 64 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 08475c7e1..14db736ae 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -8,6 +8,7 @@ const assert = @import("../quirks.zig").inlineAssert; const build_config = @import("../build_config.zig"); const uucode = @import("uucode"); const EntryFormatter = @import("../config/formatter.zig").EntryFormatter; +const deepEqual = @import("../datastruct/comparison.zig").deepEqual; const key = @import("key.zig"); const key_mods = @import("key_mods.zig"); const KeyEvent = key.KeyEvent; @@ -1587,6 +1588,24 @@ pub const Action = union(enum) { }, } } + + /// Compares two actions for equality. + pub fn equal(self: Action, other: Action) bool { + if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false; + return switch (self) { + inline else => |field_self, tag| { + const field_other = @field(other, @tagName(tag)); + return deepEqual( + @TypeOf(field_self), + field_self, + field_other, + ); + }, + }; + } + + /// For the Set.Context + const bindingSetEqual = equal; }; /// Trigger is the associated key state that can trigger an action. @@ -1908,7 +1927,7 @@ pub const Trigger = struct { } /// Returns true if two triggers are equal. - pub fn eql(self: Trigger, other: Trigger) bool { + pub fn equal(self: Trigger, other: Trigger) bool { if (self.mods != other.mods) return false; const self_tag = std.meta.activeTag(self.key); const other_tag = std.meta.activeTag(other.key); @@ -1920,6 +1939,26 @@ pub const Trigger = struct { }; } + /// Returns true if two triggers are equal using folded codepoints. + pub fn foldedEqual(self: Trigger, other: Trigger) bool { + if (self.mods != other.mods) return false; + const self_tag = std.meta.activeTag(self.key); + const other_tag = std.meta.activeTag(other.key); + if (self_tag != other_tag) return false; + return switch (self.key) { + .physical => |v| v == other.key.physical, + .unicode => |v| deepEqual( + [3]u21, + foldedCodepoint(v), + foldedCodepoint(other.key.unicode), + ), + .catch_all => true, + }; + } + + /// For the Set.Context + const bindingSetEqual = foldedEqual; + /// Convert the trigger to a C API compatible trigger. pub fn cval(self: Trigger) C { return .{ @@ -2674,7 +2713,7 @@ pub const Set = struct { // If our value is not the same as the old trigger, we can // ignore it because our reverse mapping points somewhere else. - if (!entry.value_ptr.eql(old)) return; + if (!entry.value_ptr.equal(old)) return; // It is the same trigger, so let's now go through our bindings // and try to find another trigger that maps to the same action. @@ -2745,7 +2784,8 @@ pub const Set = struct { } pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { - return ctx.hash(a) == ctx.hash(b); + _ = ctx; + return a.bindingSetEqual(b); } }; } @@ -3110,63 +3150,63 @@ test "parse: all triggers" { } } -test "Trigger: eql" { +test "Trigger: equal" { const testing = std.testing; // Equal physical keys { const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; - try testing.expect(t1.eql(t2)); + try testing.expect(t1.equal(t2)); } // Different physical keys { const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; const t2: Trigger = .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true } }; - try testing.expect(!t1.eql(t2)); + try testing.expect(!t1.equal(t2)); } // Different mods { const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } }; const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true } }; - try testing.expect(!t1.eql(t2)); + try testing.expect(!t1.equal(t2)); } // Equal unicode keys { const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; const t2: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; - try testing.expect(t1.eql(t2)); + try testing.expect(t1.equal(t2)); } // Different unicode keys { const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; const t2: Trigger = .{ .key = .{ .unicode = 'b' }, .mods = .{} }; - try testing.expect(!t1.eql(t2)); + try testing.expect(!t1.equal(t2)); } // Different key types { const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} }; const t2: Trigger = .{ .key = .{ .physical = .key_a }, .mods = .{} }; - try testing.expect(!t1.eql(t2)); + try testing.expect(!t1.equal(t2)); } // catch_all { const t1: Trigger = .{ .key = .catch_all, .mods = .{} }; const t2: Trigger = .{ .key = .catch_all, .mods = .{} }; - try testing.expect(t1.eql(t2)); + try testing.expect(t1.equal(t2)); } // catch_all with different mods { const t1: Trigger = .{ .key = .catch_all, .mods = .{} }; const t2: Trigger = .{ .key = .catch_all, .mods = .{ .alt = true } }; - try testing.expect(!t1.eql(t2)); + try testing.expect(!t1.equal(t2)); } } -- 2.51.2 From 0e9ce7e450fbe4ef296a71d569c92fdd9c1756e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 08:46:35 -0800 Subject: [PATCH 228/349] input: change our binding set to use array hash map This is recommended for ongoing performance: https://github.com/ziglang/zig/issues/17851 Likely not an issue for this particular use case which is why it never bit us; we don't actively modify this map much once it is created. But, its still good hygiene and ArrayHashMap made some of the API usage nicer. --- src/config/Config.zig | 8 ++++---- src/input/Binding.zig | 43 +++++++++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ac52595be..4cd918ea6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6830,7 +6830,7 @@ pub const Keybinds = struct { /// Like formatEntry but has an option to include docs. pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void { - if (self.set.bindings.size == 0 and self.tables.count() == 0) { + if (self.set.bindings.count() == 0 and self.tables.count() == 0) { try formatter.formatEntry(void, {}); return; } @@ -6932,8 +6932,8 @@ pub const Keybinds = struct { // Note they turn into translated keys because they match // their ASCII mapping. const want = - \\keybind = ctrl+z>2=goto_tab:2 \\keybind = ctrl+z>1=goto_tab:1 + \\keybind = ctrl+z>2=goto_tab:2 \\ ; try std.testing.expectEqualStrings(want, buf.written()); @@ -6957,9 +6957,9 @@ pub const Keybinds = struct { // NB: This does not currently retain the order of the keybinds. const want = - \\a = ctrl+a>ctrl+c>t=new_tab - \\a = ctrl+a>ctrl+b>w=close_window \\a = ctrl+a>ctrl+b>n=new_window + \\a = ctrl+a>ctrl+b>w=close_window + \\a = ctrl+a>ctrl+c>t=new_tab \\a = ctrl+b>ctrl+d>a=previous_tab \\ ; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 14db736ae..57414d764 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1998,18 +1998,18 @@ pub const Trigger = struct { /// The use case is that this will be called on EVERY key input to look /// for an associated action so it must be fast. pub const Set = struct { - const HashMap = std.HashMapUnmanaged( + const HashMap = std.ArrayHashMapUnmanaged( Trigger, Value, Context(Trigger), - std.hash_map.default_max_load_percentage, + true, ); - const ReverseMap = std.HashMapUnmanaged( + const ReverseMap = std.ArrayHashMapUnmanaged( Action, Trigger, Context(Action), - std.hash_map.default_max_load_percentage, + true, ); /// The set of bindings. @@ -2503,11 +2503,10 @@ pub const Set = struct { // update the reverse mapping to remove the old action. .leaf => if (track_reverse) { const t_hash = t.hash(); - var it = self.reverse.iterator(); - while (it.next()) |reverse_entry| it: { - if (t_hash == reverse_entry.value_ptr.hash()) { - self.reverse.removeByPtr(reverse_entry.key_ptr); - break :it; + for (0.., self.reverse.values()) |i, *value| { + if (t_hash == value.hash()) { + self.reverse.swapRemoveAt(i); + break; } } }, @@ -2523,7 +2522,7 @@ pub const Set = struct { .action = action, .flags = flags, } }; - errdefer _ = self.bindings.remove(t); + errdefer _ = self.bindings.swapRemove(t); if (track_reverse) try self.reverse.put(alloc, action, t); errdefer if (track_reverse) self.reverse.remove(action); @@ -2663,7 +2662,7 @@ pub const Set = struct { self.chain_parent = null; var entry = self.bindings.get(t) orelse return; - _ = self.bindings.remove(t); + _ = self.bindings.swapRemove(t); switch (entry) { // For a leader removal, we need to deallocate our child set. @@ -2733,7 +2732,7 @@ pub const Set = struct { // No other trigger points to this action so we remove // the reverse mapping completely. - _ = self.reverse.remove(action); + _ = self.reverse.swapRemove(action); } /// Deep clone the set. @@ -2766,9 +2765,8 @@ pub const Set = struct { // We need to clone the action keys in the reverse map since // they may contain allocated values. - { - var it = result.reverse.keyIterator(); - while (it.next()) |action| action.* = try action.clone(alloc); + for (result.reverse.keys()) |*action| { + action.* = try action.clone(alloc); } return result; @@ -2778,13 +2776,22 @@ pub const Set = struct { /// gets the hash key and checks for equality. fn Context(comptime KeyType: type) type { return struct { - pub fn hash(ctx: @This(), k: KeyType) u64 { + pub fn hash(ctx: @This(), k: KeyType) u32 { _ = ctx; - return k.hash(); + // This seems crazy at first glance but this is also how + // the Zig standard library handles hashing for array + // hash maps! + return @truncate(k.hash()); } - pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { + pub fn eql( + ctx: @This(), + a: KeyType, + b: KeyType, + b_index: usize, + ) bool { _ = ctx; + _ = b_index; return a.bindingSetEqual(b); } }; -- 2.51.2 From d94ba5cf105948ddd2b2fc4160a2a4cdb472df4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20M=C3=BCller?= Date: Fri, 9 Jan 2026 18:46:00 +0100 Subject: [PATCH 229/349] docs: add bell border feature version availability Commit 22fc90f (PR #8222) on GTK and commit fe55d90 (PR #8768) on macOS. --- src/config/Config.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index b0ef26554..7958c52f1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2934,6 +2934,8 @@ keybind: Keybinds = .{}, /// Display a border around the alerted surface until the terminal is /// re-focused or interacted with (such as on keyboard input). /// +/// Available since: 1.2.0 on GTK, 1.2.1 on macOS +/// /// Example: `audio`, `no-audio`, `system`, `no-system` /// /// Available since: 1.2.0 -- 2.51.2 From 6f1544b4a3c68c80dd70dfb9acd6edc4d55cac60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 20:17:33 -0800 Subject: [PATCH 230/349] os: add mach VM tags --- src/os/mach.zig | 35 +++++++++++++++++++++++++++++++++++ src/os/main.zig | 4 ++++ 2 files changed, 39 insertions(+) create mode 100644 src/os/mach.zig diff --git a/src/os/mach.zig b/src/os/mach.zig new file mode 100644 index 000000000..3f90c05c5 --- /dev/null +++ b/src/os/mach.zig @@ -0,0 +1,35 @@ +const std = @import("std"); + +/// macOS virtual memory tags for use with mach_vm_map/mach_vm_allocate. +/// These identify memory regions in tools like vmmap and Instruments. +pub const VMTag = enum(u8) { + application_specific_1 = 240, + application_specific_2 = 241, + application_specific_3 = 242, + application_specific_4 = 243, + application_specific_5 = 244, + application_specific_6 = 245, + application_specific_7 = 246, + application_specific_8 = 247, + application_specific_9 = 248, + application_specific_10 = 249, + application_specific_11 = 250, + application_specific_12 = 251, + application_specific_13 = 252, + application_specific_14 = 253, + application_specific_15 = 254, + application_specific_16 = 255, + + // We ignore the rest because we never realistic set them. + _, + + /// Converts the tag to the format expected by mach_vm_map/mach_vm_allocate. + /// Equivalent to C macro: VM_MAKE_TAG(tag) + pub fn make(self: VMTag) i32 { + return @bitCast(@as(u32, @intFromEnum(self)) << 24); + } +}; + +test "VMTag.make" { + try std.testing.expectEqual(@as(i32, @bitCast(@as(u32, 240) << 24)), VMTag.application_specific_1.make()); +} diff --git a/src/os/main.zig b/src/os/main.zig index c105f6143..2aadabac5 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -23,6 +23,7 @@ pub const args = @import("args.zig"); pub const cgroup = @import("cgroup.zig"); pub const hostname = @import("hostname.zig"); pub const i18n = @import("i18n.zig"); +pub const mach = @import("mach.zig"); pub const path = @import("path.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); @@ -73,5 +74,8 @@ test { if (comptime builtin.os.tag == .linux) { _ = kernel_info; + } else if (comptime builtin.os.tag.isDarwin()) { + _ = mach; + _ = macos; } } -- 2.51.2 From b426a682973202bdc09328c7bc5751ac9d6fa66c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 20:17:33 -0800 Subject: [PATCH 231/349] os: mach taggedPageAllocator --- src/os/mach.zig | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/src/os/mach.zig b/src/os/mach.zig index 3f90c05c5..4477bd128 100644 --- a/src/os/mach.zig +++ b/src/os/mach.zig @@ -1,4 +1,7 @@ const std = @import("std"); +const assert = @import("../quirks.zig").inlineAssert; +const mem = std.mem; +const Allocator = std.mem.Allocator; /// macOS virtual memory tags for use with mach_vm_map/mach_vm_allocate. /// These identify memory regions in tools like vmmap and Instruments. @@ -30,6 +33,118 @@ pub const VMTag = enum(u8) { } }; +/// Creates a page allocator that tags all allocated memory with the given +/// VMTag. +pub fn taggedPageAllocator(tag: VMTag) Allocator { + return .{ + // We smuggle the tag in as the context pointer. + .ptr = @ptrFromInt(@as(usize, @intFromEnum(tag))), + .vtable = &TaggedPageAllocator.vtable, + }; +} + +/// This is based heavily on the Zig 0.15.2 PageAllocator implementation, +/// with only the posix implementation. Zig 0.15.2 is MIT licensed. +const TaggedPageAllocator = struct { + pub const vtable: Allocator.VTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + fn alloc(context: *anyopaque, n: usize, alignment: mem.Alignment, ra: usize) ?[*]u8 { + _ = ra; + assert(n > 0); + const tag: VMTag = @enumFromInt(@as(u8, @truncate(@intFromPtr(context)))); + return map(n, alignment, tag); + } + + fn resize(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) bool { + _ = context; + _ = alignment; + _ = return_address; + return realloc(memory, new_len, false) != null; + } + + fn remap(context: *anyopaque, memory: []u8, alignment: mem.Alignment, new_len: usize, return_address: usize) ?[*]u8 { + _ = context; + _ = alignment; + _ = return_address; + return realloc(memory, new_len, true); + } + + fn free(context: *anyopaque, memory: []u8, alignment: mem.Alignment, return_address: usize) void { + _ = context; + _ = alignment; + _ = return_address; + return unmap(@alignCast(memory)); + } + + pub fn map(n: usize, alignment: mem.Alignment, tag: VMTag) ?[*]u8 { + const page_size = std.heap.pageSize(); + if (n >= std.math.maxInt(usize) - page_size) return null; + const alignment_bytes = alignment.toByteUnits(); + + const aligned_len = mem.alignForward(usize, n, page_size); + const max_drop_len = alignment_bytes - @min(alignment_bytes, page_size); + const overalloc_len = if (max_drop_len <= aligned_len - n) + aligned_len + else + mem.alignForward(usize, aligned_len + max_drop_len, page_size); + const hint = @atomicLoad(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, .unordered); + const slice = std.posix.mmap( + hint, + overalloc_len, + std.posix.PROT.READ | std.posix.PROT.WRITE, + .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + tag.make(), + 0, + ) catch return null; + const result_ptr = mem.alignPointer(slice.ptr, alignment_bytes) orelse return null; + // Unmap the extra bytes that were only requested in order to guarantee + // that the range of memory we were provided had a proper alignment in it + // somewhere. The extra bytes could be at the beginning, or end, or both. + const drop_len = result_ptr - slice.ptr; + if (drop_len != 0) std.posix.munmap(slice[0..drop_len]); + const remaining_len = overalloc_len - drop_len; + if (remaining_len > aligned_len) std.posix.munmap(@alignCast(result_ptr[aligned_len..remaining_len])); + const new_hint: [*]align(std.heap.page_size_min) u8 = @alignCast(result_ptr + aligned_len); + _ = @cmpxchgStrong(@TypeOf(std.heap.next_mmap_addr_hint), &std.heap.next_mmap_addr_hint, hint, new_hint, .monotonic, .monotonic); + return result_ptr; + } + + pub fn unmap(memory: []align(std.heap.page_size_min) u8) void { + const page_aligned_len = mem.alignForward(usize, memory.len, std.heap.pageSize()); + std.posix.munmap(memory.ptr[0..page_aligned_len]); + } + + pub fn realloc(uncasted_memory: []u8, new_len: usize, may_move: bool) ?[*]u8 { + const memory: []align(std.heap.page_size_min) u8 = @alignCast(uncasted_memory); + const page_size = std.heap.pageSize(); + const new_size_aligned = mem.alignForward(usize, new_len, page_size); + + const page_aligned_len = mem.alignForward(usize, memory.len, page_size); + if (new_size_aligned == page_aligned_len) + return memory.ptr; + + if (std.posix.MREMAP != void) { + // TODO: if the next_mmap_addr_hint is within the remapped range, update it + const new_memory = std.posix.mremap(memory.ptr, memory.len, new_len, .{ .MAYMOVE = may_move }, null) catch return null; + return new_memory.ptr; + } + + if (new_size_aligned < page_aligned_len) { + const ptr = memory.ptr + new_size_aligned; + // TODO: if the next_mmap_addr_hint is within the unmapped range, update it + std.posix.munmap(@alignCast(ptr[0 .. page_aligned_len - new_size_aligned])); + return memory.ptr; + } + + return null; + } +}; + test "VMTag.make" { try std.testing.expectEqual(@as(i32, @bitCast(@as(u32, 240) << 24)), VMTag.application_specific_1.make()); } -- 2.51.2 From 1d63045c2ffe006847716bb39720259e670ea96d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 20:17:33 -0800 Subject: [PATCH 232/349] terminal: use tagged memory for PageList ops --- src/terminal/PageList.zig | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index bebe6b700..71f5ff24a 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -4,6 +4,7 @@ const PageList = @This(); const std = @import("std"); +const builtin = @import("builtin"); const build_options = @import("terminal_options"); const Allocator = std.mem.Allocator; const assert = @import("../quirks.zig").inlineAssert; @@ -255,6 +256,18 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize { return PagePool.item_size * pages; } +/// This is the page allocator we'll use for all our underlying +/// VM page allocations. +inline fn pageAllocator() Allocator { + // On non-macOS we use our standard Zig page allocator. + if (!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator; + + // On macOS we want to tag our memory so we can assign it to our + // core terminal usage. + const mach = @import("../os/mach.zig"); + return mach.taggedPageAllocator(.application_specific_1); +} + /// Initialize the page. The top of the first page in the list is always the /// top of the active area of the screen (important knowledge for quickly /// setting up cursors in Screen). @@ -280,7 +293,11 @@ pub fn init( // The screen starts with a single page that is the entire viewport, // and we'll split it thereafter if it gets too large and add more as // necessary. - var pool = try MemoryPool.init(alloc, std.heap.page_allocator, page_preheat); + var pool = try MemoryPool.init( + alloc, + pageAllocator(), + page_preheat, + ); errdefer pool.deinit(); var page_serial: u64 = 0; const page_list, const page_size = try initPages( @@ -669,7 +686,7 @@ pub fn clone( // Setup our pools break :alloc try .init( alloc, - std.heap.page_allocator, + pageAllocator(), page_count, ); }, -- 2.51.2 From 9ee78d82c02f3b3c3e79c20c02406f2de09196b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 Jan 2026 20:17:33 -0800 Subject: [PATCH 233/349] terminal: fix memory leak when grow attempts to reuse non-standard page --- src/terminal/PageList.zig | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 71f5ff24a..cd4064445 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2509,13 +2509,26 @@ pub fn grow(self: *PageList) !?*List.Node { // satisfied then we do not prune. if (self.growRequiredForActive()) break :prune; - const layout = Page.layout(try std_capacity.adjust(.{ .cols = self.cols })); - - // Get our first page and reset it to prepare for reuse. const first = self.pages.popFirst().?; assert(first != last); + + // If our first node has non-standard memory size, we can't reuse + // it. This is because our initBuf below would change the underlying + // memory length which would break our memory free outside the pool. + // It is easiest in this case to prune the node. + if (first.data.memory.len > std_size) { + // Node is already removed so we can just destroy it. + self.destroyNode(first); + break :prune; + } + + // Get the layout first so our failable work is done early. + const layout = Page.layout(try std_capacity.adjust(.{ .cols = self.cols })); + + // Reset our memory const buf = first.data.memory; @memset(buf, 0); + assert(buf.len <= std_size); // Decrease our total row count from the pruned page and then // add one for our new row. -- 2.51.2 From 235eebfa9295ef2877321656b35f160d8a3d0b5c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2026 06:55:21 -0800 Subject: [PATCH 234/349] terminal: during test, use the testing allocator for pages --- src/terminal/PageList.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index cd4064445..2acdfc9c3 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -259,6 +259,9 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize { /// This is the page allocator we'll use for all our underlying /// VM page allocations. inline fn pageAllocator() Allocator { + // In tests we use our testing allocator so we can detect leaks. + if (builtin.is_test) return std.testing.allocator; + // On non-macOS we use our standard Zig page allocator. if (!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator; -- 2.51.2 From 509f0733667d7631266f787ea800ba05d3672cec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2026 06:58:19 -0800 Subject: [PATCH 235/349] terminal: fix up our total row and pin accounting during reuse --- src/terminal/PageList.zig | 111 ++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 23 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 2acdfc9c3..1cf4b376c 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2496,6 +2496,10 @@ pub fn grow(self: *PageList) !?*List.Node { // Slower path: we have no space, we need to allocate a new page. + // Get the layout first so our failable work is done early. + // We'll need this for both paths. + const cap = try std_capacity.adjust(.{ .cols = self.cols }); + // If allocation would exceed our max size, we prune the first page. // We don't need to reallocate because we can simply reuse that first // page. @@ -2515,28 +2519,8 @@ pub fn grow(self: *PageList) !?*List.Node { const first = self.pages.popFirst().?; assert(first != last); - // If our first node has non-standard memory size, we can't reuse - // it. This is because our initBuf below would change the underlying - // memory length which would break our memory free outside the pool. - // It is easiest in this case to prune the node. - if (first.data.memory.len > std_size) { - // Node is already removed so we can just destroy it. - self.destroyNode(first); - break :prune; - } - - // Get the layout first so our failable work is done early. - const layout = Page.layout(try std_capacity.adjust(.{ .cols = self.cols })); - - // Reset our memory - const buf = first.data.memory; - @memset(buf, 0); - assert(buf.len <= std_size); - - // Decrease our total row count from the pruned page and then - // add one for our new row. + // Decrease our total row count from the pruned page 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: { @@ -2554,10 +2538,26 @@ pub fn grow(self: *PageList) !?*List.Node { } } + // If our first node has non-standard memory size, we can't reuse + // it. This is because our initBuf below would change the underlying + // memory length which would break our memory free outside the pool. + // It is easiest in this case to prune the node. + if (first.data.memory.len > std_size) { + // Node is already removed so we can just destroy it. + self.destroyNode(first); + break :prune; + } + + // Reset our memory + const buf = first.data.memory; + @memset(buf, 0); + assert(buf.len <= std_size); + // Initialize our new page and reinsert it as the last - first.data = .initBuf(.init(buf), layout); + first.data = .initBuf(.init(buf), Page.layout(cap)); first.data.size.rows = 1; self.pages.insertAfter(last, first); + self.total_rows += 1; // We also need to reset the serial number. Since this is the only // place we ever reuse a serial number, we also can safely set @@ -2587,7 +2587,7 @@ pub fn grow(self: *PageList) !?*List.Node { } // We need to allocate a new memory buffer. - const next_node = try self.createPage(try std_capacity.adjust(.{ .cols = self.cols })); + const next_node = try self.createPage(cap); // we don't errdefer this because we've added it to the linked // list and its fine to have dangling unused pages. self.pages.append(next_node); @@ -11018,3 +11018,68 @@ test "PageList resize grow cols with unwrap fixes viewport pin" { const br_after = s.getBottomRight(.viewport); try testing.expect(br_after != null); } + +test "PageList grow reuses non-standard page without leak" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create a PageList with 3 * std_size max so we can fit multiple pages + // but will still trigger reuse. + var s = try init(alloc, 80, 24, 3 * std_size); + defer s.deinit(); + + // Save the first page node before adjustment + const first_before = s.pages.first.?; + + // Adjust the first page to have non-standard capacity. We use a small + // increase that makes it just slightly larger than std_size. + _ = try s.adjustCapacity(first_before, .{ .grapheme_bytes = std_size + 1 }); + + // The first page should now have non-standard memory size. + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + // First, fill up the first page's capacity + const first_page = s.pages.first.?; + while (first_page.data.size.rows < first_page.data.capacity.rows) { + _ = try s.grow(); + } + + // Now grow to create a second page + _ = try s.grow(); + try testing.expect(s.pages.first != s.pages.last); + + // Continue growing until we exceed max_size AND the last page is full + while (s.page_size + PagePool.item_size <= s.maxSize() or + s.pages.last.?.data.size.rows < s.pages.last.?.data.capacity.rows) + { + _ = try s.grow(); + } + + // The first page should still be non-standard + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + // Verify we have enough rows for active area (so prune path isn't skipped) + try testing.expect(!s.growRequiredForActive()); + + // Verify last page is full (so grow will need to allocate/reuse) + try testing.expect(s.pages.last.?.data.size.rows == s.pages.last.?.data.capacity.rows); + + // Remember the first page memory pointer before the reuse attempt + const first_page_ptr = s.pages.first.?; + const first_page_mem_ptr = s.pages.first.?.data.memory.ptr; + + // Now grow one more time to trigger the reuse path. Since the first page + // is non-standard, it should be destroyed (not reused). The testing + // allocator will detect a leak if destroyNode doesn't properly free + // the non-standard memory. + _ = try s.grow(); + + // After grow, check if the first page is a different one + // (meaning the non-standard page was pruned, not reused at the end) + // The original first page should no longer be the first page + try testing.expect(s.pages.first.? != first_page_ptr); + + // If the non-standard page was properly destroyed and not reused, + // the last page should not have the same memory pointer + try testing.expect(s.pages.last.?.data.memory.ptr != first_page_mem_ptr); +} -- 2.51.2 From 50516ed581a80f78492424b1e6d75d97146c2654 Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:42:30 -0600 Subject: [PATCH 236/349] add search selection for GTK --- src/Surface.zig | 1 + src/apprt/gtk/class/application.zig | 8 ++++---- src/apprt/gtk/class/search_overlay.zig | 7 +++++++ src/apprt/gtk/class/surface.zig | 6 +++++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 9998922b9..4103b91fb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5221,6 +5221,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .search_selection => { const selection = try self.selectionString(self.alloc) orelse return false; + defer self.alloc.free(selection); return try self.rt_app.performAction( .{ .surface = self }, .start_search, diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 848aa22db..bc83c09a4 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -734,7 +734,7 @@ pub const Application = extern struct { .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target), .command_finished => return Action.commandFinished(target, value), - .start_search => Action.startSearch(target), + .start_search => Action.startSearch(target, value), .end_search => Action.endSearch(target), .search_total => Action.searchTotal(target, value), .search_selected => Action.searchSelected(target, value), @@ -2439,17 +2439,17 @@ const Action = struct { } } - pub fn startSearch(target: apprt.Target) void { + pub fn startSearch(target: apprt.Target, value: apprt.action.StartSearch) void { switch (target) { .app => {}, - .surface => |v| v.rt_surface.surface.setSearchActive(true), + .surface => |v| v.rt_surface.surface.setSearchActive(true, value.needle), } } pub fn endSearch(target: apprt.Target) void { switch (target) { .app => {}, - .surface => |v| v.rt_surface.surface.setSearchActive(false), + .surface => |v| v.rt_surface.surface.setSearchActive(false, ""), } } diff --git a/src/apprt/gtk/class/search_overlay.zig b/src/apprt/gtk/class/search_overlay.zig index 4936cd967..a81115462 100644 --- a/src/apprt/gtk/class/search_overlay.zig +++ b/src/apprt/gtk/class/search_overlay.zig @@ -250,6 +250,13 @@ pub const SearchOverlay = extern struct { priv.active = active; } + // Set contents of search + pub fn setSearchContents(self: *Self, content: [:0]const u8) void { + const priv = self.private(); + priv.search_entry.as(gtk.Editable).setText(content); + signals.@"search-changed".impl.emit(self, null, .{content}, null); + } + /// Set the total number of search matches. pub fn setSearchTotal(self: *Self, total: ?usize) void { const priv = self.private(); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 0d6c64433..d1eb144e9 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -2091,7 +2091,7 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec); } - pub fn setSearchActive(self: *Self, active: bool) void { + pub fn setSearchActive(self: *Self, active: bool, needle: [:0]const u8) void { const priv = self.private(); var value = gobject.ext.Value.newFrom(active); defer value.unset(); @@ -2101,6 +2101,10 @@ pub const Surface = extern struct { &value, ); + if (!std.mem.eql(u8, needle, "")) { + priv.search_overlay.setSearchContents(needle); + } + if (active) { priv.search_overlay.grabFocus(); } -- 2.51.2 From 6037e2194ae301e82ac88e74a3fa8ddefe73727b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 Jan 2026 20:28:49 -0800 Subject: [PATCH 237/349] terminal: remove the ability to reuse a pool from PageList This complicates logic and is unused. --- src/terminal/PageList.zig | 90 +++++++++++---------------------------- src/terminal/Screen.zig | 19 +-------- 2 files changed, 27 insertions(+), 82 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 1cf4b376c..1d2eda6b9 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -109,7 +109,6 @@ pub const MemoryPool = struct { /// The memory pool we get page nodes, pages from. pool: MemoryPool, -pool_owned: bool, /// The list of pages in the screen. pages: List, @@ -324,7 +323,6 @@ pub fn init( .cols = cols, .rows = rows, .pool = pool, - .pool_owned = true, .pages = page_list, .page_serial = page_serial, .page_serial_min = 0, @@ -519,11 +517,7 @@ pub fn deinit(self: *PageList) void { // Deallocate all the pages. We don't need to deallocate the list or // nodes because they all reside in the pool. - if (self.pool_owned) { - self.pool.deinit(); - } else { - self.pool.reset(.{ .retain_capacity = {} }); - } + self.pool.deinit(); } /// Reset the PageList back to an empty state. This is similar to @@ -641,14 +635,6 @@ pub const Clone = struct { top: point.Point, bot: ?point.Point = null, - /// The allocator source for the clone operation. If this is alloc - /// then the cloned pagelist will own and dealloc the memory on deinit. - /// If this is pool then the caller owns the memory. - memory: union(enum) { - alloc: Allocator, - pool: *MemoryPool, - }, - // If this is non-null then cloning will attempt to remap the tracked // pins into the new cloned area and will keep track of the old to // new mapping in this map. If this is null, the cloned pagelist will @@ -670,37 +656,26 @@ pub const Clone = struct { /// rows will be added to the bottom of the region to make up the difference. pub fn clone( self: *const PageList, + alloc: Allocator, opts: Clone, ) !PageList { var it = self.pageIterator(.right_down, opts.top, opts.bot); - // Setup our own memory pool if we have to. - var owned_pool: ?MemoryPool = switch (opts.memory) { - .pool => null, - .alloc => |alloc| alloc: { - // First, count our pages so our preheat is exactly what we need. - var it_copy = it; - const page_count: usize = page_count: { - var count: usize = 0; - while (it_copy.next()) |_| count += 1; - break :page_count count; - }; - - // Setup our pools - break :alloc try .init( - alloc, - pageAllocator(), - page_count, - ); - }, + // First, count our pages so our preheat is exactly what we need. + var it_copy = it; + const page_count: usize = page_count: { + var count: usize = 0; + while (it_copy.next()) |_| count += 1; + break :page_count count; }; - errdefer if (owned_pool) |*pool| pool.deinit(); - // Create our memory pool we use - const pool: *MemoryPool = switch (opts.memory) { - .pool => |v| v, - .alloc => &owned_pool.?, - }; + // Setup our pool + var pool: MemoryPool = try .init( + alloc, + pageAllocator(), + page_count, + ); + errdefer pool.deinit(); // Our viewport pin is always undefined since our viewport in a clones // goes back to the top @@ -729,7 +704,7 @@ pub fn clone( // Clone the page. We have to use createPageExt here because // we don't know if the source page has a standard size. const node = try createPageExt( - pool, + &pool, chunk.node.data.capacity, &page_serial, &page_size, @@ -770,11 +745,7 @@ pub fn clone( } var result: PageList = .{ - .pool = pool.*, - .pool_owned = switch (opts.memory) { - .pool => false, - .alloc => true, - }, + .pool = pool, .pages = page_list, .page_serial = page_serial, .page_serial_min = 0, @@ -7141,9 +7112,8 @@ test "PageList clone" { defer s.deinit(); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{} }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); @@ -7158,10 +7128,9 @@ test "PageList clone partial trimmed right" { try testing.expectEqual(@as(usize, s.rows), s.totalRows()); try s.growRows(30); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{} }, .bot = .{ .screen = .{ .y = 39 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 40), s2.totalRows()); @@ -7176,9 +7145,8 @@ test "PageList clone partial trimmed left" { try testing.expectEqual(@as(usize, s.rows), s.totalRows()); try s.growRows(30); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{ .y = 10 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 40), s2.totalRows()); @@ -7220,9 +7188,8 @@ test "PageList clone partial trimmed left reclaims styles" { try testing.expectEqual(1, page.styles.count()); } - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{ .y = 10 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 40), s2.totalRows()); @@ -7243,10 +7210,9 @@ test "PageList clone partial trimmed both" { try testing.expectEqual(@as(usize, s.rows), s.totalRows()); try s.growRows(30); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{ .y = 10 } }, .bot = .{ .screen = .{ .y = 35 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, 26), s2.totalRows()); @@ -7260,9 +7226,8 @@ test "PageList clone less than active" { defer s.deinit(); try testing.expectEqual(@as(usize, s.rows), s.totalRows()); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .active = .{ .y = 5 } }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); @@ -7282,9 +7247,8 @@ test "PageList clone remap tracked pin" { var pin_remap = Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit(); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .active = .{ .y = 5 } }, - .memory = .{ .alloc = alloc }, .tracked_pins = &pin_remap, }); defer s2.deinit(); @@ -7311,9 +7275,8 @@ test "PageList clone remap tracked pin not in cloned area" { var pin_remap = Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit(); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .active = .{ .y = 5 } }, - .memory = .{ .alloc = alloc }, .tracked_pins = &pin_remap, }); defer s2.deinit(); @@ -7335,9 +7298,8 @@ test "PageList clone full dirty" { s.markDirty(.{ .active = .{ .x = 0, .y = 12 } }); s.markDirty(.{ .active = .{ .x = 0, .y = 23 } }); - var s2 = try s.clone(.{ + var s2 = try s.clone(alloc, .{ .top = .{ .screen = .{} }, - .memory = .{ .alloc = alloc }, }); defer s2.deinit(); try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index ba2af2473..2861e02e5 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -396,18 +396,6 @@ pub fn clone( alloc: Allocator, top: point.Point, bot: ?point.Point, -) !Screen { - return try self.clonePool(alloc, null, top, bot); -} - -/// Same as clone but you can specify a custom memory pool to use for -/// the screen. -pub fn clonePool( - self: *const Screen, - alloc: Allocator, - pool: ?*PageList.MemoryPool, - top: point.Point, - bot: ?point.Point, ) !Screen { // Create a tracked pin remapper for our selection and cursor. Note // that we may want to expose this generally in the future but at the @@ -415,14 +403,9 @@ pub fn clonePool( var pin_remap = PageList.Clone.TrackedPinsRemap.init(alloc); defer pin_remap.deinit(); - var pages = try self.pages.clone(.{ + var pages = try self.pages.clone(alloc, .{ .top = top, .bot = bot, - .memory = if (pool) |p| .{ - .pool = p, - } else .{ - .alloc = alloc, - }, .tracked_pins = &pin_remap, }); errdefer pages.deinit(); -- 2.51.2 From 7e3c9f4d5a9638b95e6d3b1d2237658989f2c723 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 11 Jan 2026 10:57:10 -0500 Subject: [PATCH 238/349] shell-integration: initial nushell shell integration Nushell is a modern interactive shell that provides many shell features out-of-the-box, like `title` support. Our shell integration therefore focuses on Ghostty-specific features like `sudo`. We use Nushell's module system to provide a `ghostty` module containing our shell integration features. This module is automatically loaded from $XDG_DATA_DIRS/nushell/vendor/autoload/ when `nushell` shell integration is enabled. Exported module functions need to be explicitly "used" before they're available to the interactive shell environment. We do that automatically by adding `--execute "use ghostty *"` to the `nu` command line. This imports all available functions, and individual shell features are runtime-guarded by the script code (using $GHOSTTY_SHELL_FEATURES). We can consider further refining this later. When automatic shell integration is disabled, users can still manually source and enable the shell integration module: source $GHOSTTY_RESOURCES_DIR/shell-integration/nushell/vendor/autoload/ghostty.nu use ghostty * This initial work implements our TERMINFO-aware `sudo` wrapper (via the `sudo` shell feature). Support for additional features, like `ssh-env` and `ssh-terminfo`, will follow. --- src/config/Config.zig | 3 +- src/shell-integration/README.md | 18 ++ .../nushell/vendor/autoload/ghostty.nu | 35 ++++ src/termio/Exec.zig | 1 + src/termio/shell_integration.zig | 171 ++++++++++++++++-- 5 files changed, 212 insertions(+), 16 deletions(-) create mode 100644 src/shell-integration/nushell/vendor/autoload/ghostty.nu diff --git a/src/config/Config.zig b/src/config/Config.zig index f13eba358..7689899de 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2667,7 +2667,7 @@ keybind: Keybinds = .{}, /// /// * `detect` - Detect the shell based on the filename. /// -/// * `bash`, `elvish`, `fish`, `zsh` - Use this specific shell injection scheme. +/// * `bash`, `elvish`, `fish`, `nushell`, `zsh` - Use this specific shell injection scheme. /// /// The default value is `detect`. @"shell-integration": ShellIntegration = .detect, @@ -8027,6 +8027,7 @@ pub const ShellIntegration = enum { bash, elvish, fish, + nushell, zsh, }; diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 9c422ef26..8809134d2 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -76,6 +76,24 @@ allowing us to automatically integrate with the shell. For details on the Fish startup process, see the [Fish documentation](https://fishshell.com/docs/current/language.html). +### Nushell + +For [Nushell](https://www.nushell.sh/), Ghostty prepends to the +`XDG_DATA_DIRS` directory, making the `ghostty` module available through +Nushell's vendor autoload mechanism. Ghostty then automatically imports +the module using the `-e "use ghostty *"` flag when starting Nushell. + +Nushell provides many shell features itself, such as `title` and `cursor`, +so our integration focuses on Ghostty-specific features like `sudo`. + +The shell integration is automatically enabled when running Nushell in Ghostty, +but you can also load it manually is shell integration is disabled: + +```nushell +source $GHOSTTY_RESOURCES_DIR/shell-integration/nushell/vendor/autoload/ghostty.nu +use ghostty * +``` + ### Zsh Automatic [Zsh](https://www.zsh.org/) integration works by temporarily setting diff --git a/src/shell-integration/nushell/vendor/autoload/ghostty.nu b/src/shell-integration/nushell/vendor/autoload/ghostty.nu new file mode 100644 index 000000000..467e3f529 --- /dev/null +++ b/src/shell-integration/nushell/vendor/autoload/ghostty.nu @@ -0,0 +1,35 @@ +# Ghostty shell integration +export module ghostty { + def has_feature [feature: string] { + $feature in ($env.GHOSTTY_SHELL_FEATURES | default "" | split row ',') + } + + # Wrap `sudo` to preserve Ghostty's TERMINFO environment variable + export def --wrapped sudo [ + ...args # Arguments to pass to `sudo` + ] { + mut sudo_args = $args + + if (has_feature "sudo") { + # Extract just the sudo options (before the command) + let sudo_options = ($args | take until {|arg| + not (($arg | str starts-with "-") or ($arg | str contains "=")) + }) + + # Prepend TERMINFO preservation flag if not using sudoedit + if (not ("-e" in $sudo_options or "--edit" in $sudo_options)) { + $sudo_args = ($args | prepend "--preserve-env=TERMINFO") + } + } + + ^sudo ...$sudo_args + } +} + +# Clean up XDG_DATA_DIRS by removing GHOSTTY_SHELL_INTEGRATION_XDG_DIR +if 'GHOSTTY_SHELL_INTEGRATION_XDG_DIR' in $env { + if 'XDG_DATA_DIRS' in $env { + $env.XDG_DATA_DIRS = ($env.XDG_DATA_DIRS | str replace $"($env.GHOSTTY_SHELL_INTEGRATION_XDG_DIR):" "") + } + hide-env GHOSTTY_SHELL_INTEGRATION_XDG_DIR +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 93ad835c5..0e7cdc172 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -770,6 +770,7 @@ const Subprocess = struct { .bash => .bash, .elvish => .elvish, .fish => .fish, + .nushell => .nushell, .zsh => .zsh, }; diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index b9477e090..94000110a 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -14,6 +14,7 @@ pub const Shell = enum { bash, elvish, fish, + nushell, zsh, }; @@ -57,7 +58,7 @@ pub fn setup( env, ), - .elvish, .fish => try setupXdgDataDirs( + .nushell => try setupNushell( alloc_arena, command, resource_dir, @@ -70,6 +71,11 @@ pub fn setup( resource_dir, env, ), + + .elvish, .fish => xdg: { + if (!try setupXdgDataDirs(alloc_arena, resource_dir, env)) return null; + break :xdg try command.clone(alloc_arena); + }, } orelse return null; return .{ @@ -153,6 +159,7 @@ fn detectShell(alloc: Allocator, command: config.Command) !?Shell { if (std.mem.eql(u8, "elvish", exe)) return .elvish; if (std.mem.eql(u8, "fish", exe)) return .fish; + if (std.mem.eql(u8, "nu", exe)) return .nushell; if (std.mem.eql(u8, "zsh", exe)) return .zsh; return null; @@ -166,6 +173,7 @@ test detectShell { try testing.expectEqual(.bash, try detectShell(alloc, .{ .shell = "bash" })); try testing.expectEqual(.elvish, try detectShell(alloc, .{ .shell = "elvish" })); try testing.expectEqual(.fish, try detectShell(alloc, .{ .shell = "fish" })); + try testing.expectEqual(.nushell, try detectShell(alloc, .{ .shell = "nu" })); try testing.expectEqual(.zsh, try detectShell(alloc, .{ .shell = "zsh" })); if (comptime builtin.target.os.tag.isDarwin()) { @@ -373,11 +381,8 @@ fn setupBash( } } - // Get the command string from the builder, then copy it to the arena - // allocator. The stackFallback allocator's memory becomes invalid after - // this function returns, so we must copy to the arena. - const cmd_str = try cmd.toOwnedSlice(); - return .{ .shell = try alloc.dupeZ(u8, cmd_str) }; + // Return a copy of our modified command line to use as the shell command. + return .{ .shell = try alloc.dupeZ(u8, try cmd.toOwnedSlice()) }; } test "bash" { @@ -595,10 +600,9 @@ test "bash: missing resources" { /// from `XDG_DATA_DIRS` when integration is complete. fn setupXdgDataDirs( alloc: Allocator, - command: config.Command, resource_dir: []const u8, env: *EnvMap, -) !?config.Command { +) !bool { var path_buf: [std.fs.max_path_bytes]u8 = undefined; // Get our path to the shell integration directory. @@ -609,7 +613,7 @@ fn setupXdgDataDirs( ); var integ_dir = std.fs.openDirAbsolute(integ_path, .{}) catch |err| { log.warn("unable to open {s}: {}", .{ integ_path, err }); - return null; + return false; }; integ_dir.close(); @@ -640,7 +644,7 @@ fn setupXdgDataDirs( ), ); - return try command.clone(alloc); + return true; } test "xdg: empty XDG_DATA_DIRS" { @@ -656,8 +660,7 @@ test "xdg: empty XDG_DATA_DIRS" { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupXdgDataDirs(alloc, .{ .shell = "xdg" }, res.path, &env); - try testing.expectEqualStrings("xdg", command.?.shell); + try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); var path_buf: [std.fs.max_path_bytes]u8 = undefined; try testing.expectEqualStrings( @@ -685,8 +688,7 @@ test "xdg: existing XDG_DATA_DIRS" { try env.put("XDG_DATA_DIRS", "/opt/share"); - const command = try setupXdgDataDirs(alloc, .{ .shell = "xdg" }, res.path, &env); - try testing.expectEqualStrings("xdg", command.?.shell); + try testing.expect(try setupXdgDataDirs(alloc, res.path, &env)); var path_buf: [std.fs.max_path_bytes]u8 = undefined; try testing.expectEqualStrings( @@ -714,7 +716,146 @@ test "xdg: missing resources" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(try setupXdgDataDirs(alloc, .{ .shell = "xdg" }, resources_dir, &env) == null); + try testing.expect(!try setupXdgDataDirs(alloc, resources_dir, &env)); + try testing.expectEqual(0, env.count()); +} + +/// Set up automatic Nushell shell integration. This works by adding our +/// shell resource directory to the `XDG_DATA_DIRS` environment variable, +/// which Nushell will use to load `nushell/vendor/autoload/ghostty.nu`. +/// +/// We then add `--execute 'use ghostty ...'` to the nu command line to +/// automatically enable our shelll features. +fn setupNushell( + alloc: Allocator, + command: config.Command, + resource_dir: []const u8, + env: *EnvMap, +) !?config.Command { + var stack_fallback = std.heap.stackFallback(4096, alloc); + var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get()); + defer cmd.deinit(); + + // Iterator that yields each argument in the original command line. + // This will allocate once proportionate to the command line length. + var iter = try command.argIterator(alloc); + defer iter.deinit(); + + // Start accumulating arguments with the executable and initial flags. + if (iter.next()) |exe| { + try cmd.appendArg(exe); + } else return null; + + // Tell nu to immediately "use" all of the exported functions in our + // 'ghostty' module. + // + // We can consider making this more specific based on the set of + // enabled shell features (e.g. `use ghostty sudo`). At the moment, + // shell features are all runtime-guarded in the nushell script. + try cmd.appendArg("--execute 'use ghostty *'"); + + // Walk through the rest of the given arguments. If we see an option that + // would require complex or unsupported integration behavior, we bail out + // and skip loading our shell integration. Users can still manually source + // the shell integration module. + // + // Unsupported options: + // -c / --command -c is always non-interactive + // --lsp --lsp starts the language server + while (iter.next()) |arg| { + if (std.mem.eql(u8, arg, "--command") or std.mem.eql(u8, arg, "--lsp")) { + return null; + } else if (arg.len > 1 and arg[0] == '-' and arg[1] != '-') { + if (std.mem.indexOfScalar(u8, arg, 'c') != null) { + return null; + } + try cmd.appendArg(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 cmd.appendArg(arg); + while (iter.next()) |remaining_arg| { + try cmd.appendArg(remaining_arg); + } + break; + } else { + try cmd.appendArg(arg); + } + } + + if (!try setupXdgDataDirs(alloc, resource_dir, env)) return null; + + // Return a copy of our modified command line to use as the shell command. + return .{ .shell = try alloc.dupeZ(u8, try cmd.toOwnedSlice()) }; +} + +test "nushell" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var res: TmpResourcesDir = try .init(alloc, .nushell); + defer res.deinit(); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + const command = try setupNushell(alloc, .{ .shell = "nu" }, res.path, &env); + try testing.expectEqualStrings("nu --execute 'use ghostty *'", command.?.shell); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?, + ); + try testing.expectStringStartsWith( + env.get("XDG_DATA_DIRS").?, + try std.fmt.bufPrint(&path_buf, "{s}/shell-integration", .{res.path}), + ); +} + +test "nushell: unsupported options" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var res: TmpResourcesDir = try .init(alloc, .nushell); + defer res.deinit(); + + const cmdlines = [_][:0]const u8{ + "nu --command exit", + "nu --lsp", + "nu -c script.sh", + "nu -ic script.sh", + }; + + for (cmdlines) |cmdline| { + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupNushell(alloc, .{ .shell = cmdline }, res.path, &env) == null); + try testing.expectEqual(0, env.count()); + } +} + +test "nushell: missing resources" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const resources_dir = try tmp_dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(resources_dir); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try testing.expect(try setupNushell(alloc, .{ .shell = "nu" }, resources_dir, &env) == null); try testing.expectEqual(0, env.count()); } -- 2.51.2 From 9434203725f68283b8346705bd1a339b18eabf85 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 11 Jan 2026 17:05:08 -0500 Subject: [PATCH 239/349] shell-integration: always set up XDG_DATA_DIRS for nushell This makes our 'ghostty' module available even if the rest of our automatic integration steps fail, which is convenient for manual "use"-age. This is safe because autoload-ing our module doesn't have any side effects other than cleaning up the XDG_DATA_DIRS environment variable. --- src/termio/shell_integration.zig | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 94000110a..ab6dcd6ff 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -732,6 +732,11 @@ fn setupNushell( resource_dir: []const u8, env: *EnvMap, ) !?config.Command { + // Add our XDG_DATA_DIRS entry (for nushell/vendor/autoload/). This + // makes our 'ghostty' module automatically available, even if any + // of the later checks abort the rest of our automatic integration. + if (!try setupXdgDataDirs(alloc, resource_dir, env)) return null; + var stack_fallback = std.heap.stackFallback(4096, alloc); var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get()); defer cmd.deinit(); @@ -783,8 +788,6 @@ fn setupNushell( } } - if (!try setupXdgDataDirs(alloc, resource_dir, env)) return null; - // Return a copy of our modified command line to use as the shell command. return .{ .shell = try alloc.dupeZ(u8, try cmd.toOwnedSlice()) }; } @@ -836,7 +839,8 @@ test "nushell: unsupported options" { defer env.deinit(); try testing.expect(try setupNushell(alloc, .{ .shell = cmdline }, res.path, &env) == null); - try testing.expectEqual(0, env.count()); + try testing.expect(env.get("XDG_DATA_DIRS") != null); + try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR") != null); } } -- 2.51.2 From b995595953bdd7d6537a553827308c803539f49d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Jan 2026 14:24:24 -0800 Subject: [PATCH 240/349] terminal: tracked pins need to be invalidated for the non-std page Fixes a regression from #10251 Thanks to @grishy again for finding this. Updated tests to catch it, too. --- src/terminal/PageList.zig | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 1d2eda6b9..a96aa1975 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2509,6 +2509,18 @@ pub fn grow(self: *PageList) !?*List.Node { } } + // Update any tracked pins that point to this page to point to the + // new first page to the top-left, and mark them as garbage. + const pin_keys = self.tracked_pins.keys(); + for (pin_keys) |p| { + if (p.node != first) continue; + p.node = self.pages.first.?; + p.y = 0; + p.x = 0; + p.garbage = true; + } + self.viewport_pin.garbage = false; + // If our first node has non-standard memory size, we can't reuse // it. This is because our initBuf below would change the underlying // memory length which would break our memory free outside the pool. @@ -2538,18 +2550,6 @@ pub fn grow(self: *PageList) !?*List.Node { first.serial = self.page_serial; self.page_serial += 1; - // Update any tracked pins that point to this page to point to the - // new first page to the top-left. - const pin_keys = self.tracked_pins.keys(); - for (pin_keys) |p| { - if (p.node != first) continue; - p.node = self.pages.first.?; - p.y = 0; - p.x = 0; - p.garbage = true; - } - self.viewport_pin.garbage = false; - // In this case we do NOT need to update page_size because // we're reusing an existing page so nothing has changed. @@ -11030,6 +11030,10 @@ test "PageList grow reuses non-standard page without leak" { const first_page_ptr = s.pages.first.?; const first_page_mem_ptr = s.pages.first.?.data.memory.ptr; + // Create a tracked pin pointing to the non-standard first page + const tracked_pin = try s.trackPin(.{ .node = first_page_ptr, .x = 0, .y = 0 }); + defer s.untrackPin(tracked_pin); + // Now grow one more time to trigger the reuse path. Since the first page // is non-standard, it should be destroyed (not reused). The testing // allocator will detect a leak if destroyNode doesn't properly free @@ -11044,4 +11048,10 @@ test "PageList grow reuses non-standard page without leak" { // If the non-standard page was properly destroyed and not reused, // the last page should not have the same memory pointer try testing.expect(s.pages.last.?.data.memory.ptr != first_page_mem_ptr); + + // The tracked pin should have been moved to the new first page and marked as garbage + try testing.expectEqual(s.pages.first.?, tracked_pin.node); + try testing.expectEqual(0, tracked_pin.x); + try testing.expectEqual(0, tracked_pin.y); + try testing.expect(tracked_pin.garbage); } -- 2.51.2 From 2bdbda20fd105d193f106116f7358c6c38ca030a Mon Sep 17 00:00:00 2001 From: Peter Cardenas <16930781+PeterCardenas@users.noreply.github.com> Date: Sun, 4 Jan 2026 18:57:27 -0800 Subject: [PATCH 241/349] fix(completions.fish): add +help and +version to completions --- src/extra/fish.zig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/extra/fish.zig b/src/extra/fish.zig index 12343c62f..73fa9a706 100644 --- a/src/extra/fish.zig +++ b/src/extra/fish.zig @@ -28,8 +28,6 @@ fn writeCompletions(writer: *std.Io.Writer) !void { try writer.writeAll("set -l commands \""); var count: usize = 0; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; if (count > 0) try writer.writeAll(" "); try writer.writeAll("+"); try writer.writeAll(field.name); @@ -98,8 +96,6 @@ fn writeCompletions(writer: *std.Io.Writer) !void { try writer.writeAll("complete -c ghostty -n \"string match -q -- '+*' (commandline -pt)\" -f -a \""); var count: usize = 0; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; if (count > 0) try writer.writeAll(" "); try writer.writeAll("+"); try writer.writeAll(field.name); -- 2.51.2 From e47272878d5da048125b49b882518e483a27da9d Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 11 Jan 2026 19:26:27 -0500 Subject: [PATCH 242/349] extra: enable +help and +version in bash and zsh --- src/extra/bash.zig | 9 --------- src/extra/zsh.zig | 6 ------ 2 files changed, 15 deletions(-) diff --git a/src/extra/bash.zig b/src/extra/bash.zig index ee9a7895c..14bf3a225 100644 --- a/src/extra/bash.zig +++ b/src/extra/bash.zig @@ -158,9 +158,6 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { ); for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - const options = @field(Action, field.name).options(); // assumes options will never be created with only <_name> members if (@typeInfo(options).@"struct".fields.len == 0) continue; @@ -194,9 +191,6 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { ); for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - const options = @field(Action, field.name).options(); if (@typeInfo(options).@"struct".fields.len == 0) continue; @@ -272,9 +266,6 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { ); for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - try writer.writeAll(pad1 ++ "topLevel+=\" +" ++ field.name ++ "\"\n"); } diff --git a/src/extra/zsh.zig b/src/extra/zsh.zig index 2fad4234a..376db807f 100644 --- a/src/extra/zsh.zig +++ b/src/extra/zsh.zig @@ -139,9 +139,6 @@ fn writeZshCompletions(writer: *std.Io.Writer) !void { var count: usize = 0; const padding = " "; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - try writer.writeAll(padding ++ "'+"); try writer.writeAll(field.name); try writer.writeAll("'\n"); @@ -168,9 +165,6 @@ fn writeZshCompletions(writer: *std.Io.Writer) !void { { const padding = " "; for (@typeInfo(Action).@"enum".fields) |field| { - if (std.mem.eql(u8, "help", field.name)) continue; - if (std.mem.eql(u8, "version", field.name)) continue; - const options = @field(Action, field.name).options(); // assumes options will never be created with only <_name> members if (@typeInfo(options).@"struct".fields.len == 0) continue; -- 2.51.2 From 5c8c7c627c3f186b1acae2d84ceae9b5e2e6dc0b Mon Sep 17 00:00:00 2001 From: MithicSpirit Date: Sun, 11 Jan 2026 17:37:57 -0500 Subject: [PATCH 243/349] nix: update nixpkgs, remove zig.hook, and remove x11-gnome As of NixOS/nixpkgs#473413[1], `zig.hook` no longer supports `zig_default_flags`, and now they can and must be provided in `zigBuildFlags` instead. Updating also requires removing gnome-xorg since it has been removed from nixpkgs. [1] https://github.com/NixOS/nixpkgs/pull/473413 --- flake.lock | 15 +++++++-------- flake.nix | 3 +-- nix/package.nix | 16 +++++----------- nix/tests.nix | 2 +- nix/vm/common-gnome.nix | 2 +- nix/vm/x11-gnome.nix | 9 --------- 6 files changed, 15 insertions(+), 32 deletions(-) delete mode 100644 nix/vm/x11-gnome.nix diff --git a/flake.lock b/flake.lock index a80c2f8ae..853fb27b6 100644 --- a/flake.lock +++ b/flake.lock @@ -41,27 +41,26 @@ ] }, "locked": { - "lastModified": 1755776884, - "narHash": "sha256-CPM7zm6csUx7vSfKvzMDIjepEJv1u/usmaT7zydzbuI=", + "lastModified": 1768068402, + "narHash": "sha256-bAXnnJZKJiF7Xr6eNW6+PhBf1lg2P1aFUO9+xgWkXfA=", "owner": "nix-community", "repo": "home-manager", - "rev": "4fb695d10890e9fc6a19deadf85ff79ffb78da86", + "rev": "8bc5473b6bc2b6e1529a9c4040411e1199c43b4c", "type": "github" }, "original": { "owner": "nix-community", - "ref": "release-25.05", "repo": "home-manager", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1763191728, - "narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=", - "rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c", + "lastModified": 1768032153, + "narHash": "sha256-zvxtwlM8ZlulmZKyYCQAPpkm5dngSEnnHjmjV7Teloc=", + "rev": "3146c6aa9995e7351a398e17470e15305e6e18ff", "type": "tarball", - "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre896415.1d4c88323ac3/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre925418.3146c6aa9995/nixexprs.tar.xz" }, "original": { "type": "tarball", diff --git a/flake.nix b/flake.nix index d70f23513..58e250aec 100644 --- a/flake.nix +++ b/flake.nix @@ -35,7 +35,7 @@ }; home-manager = { - url = "github:nix-community/home-manager?ref=release-25.05"; + url = "github:nix-community/home-manager"; inputs = { nixpkgs.follows = "nixpkgs"; }; @@ -117,7 +117,6 @@ wayland-gnome = runVM ./nix/vm/wayland-gnome.nix; wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix; x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix; - x11-gnome = runVM ./nix/vm/x11-gnome.nix; x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix; x11-xfce = runVM ./nix/vm/x11-xfce.nix; }; diff --git a/nix/package.nix b/nix/package.nix index 3d00648ec..b2decc7bc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -20,16 +20,6 @@ wayland-scanner, pkgs, }: let - # The Zig hook has no way to select the release type without actual - # overriding of the default flags. - # - # TODO: Once - # 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_15.hook.overrideAttrs { - zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off"; - }; gi_typelib_path = import ./build-support/gi-typelib-path.nix { inherit pkgs lib stdenv; }; @@ -73,7 +63,7 @@ in ncurses pandoc pkg-config - zig_hook + zig_0_15 gobject-introspection wrapGAppsHook4 blueprint-compiler @@ -92,12 +82,16 @@ in GI_TYPELIB_PATH = gi_typelib_path; + dontSetZigDefaultFlags = true; + zigBuildFlags = [ "--system" "${finalAttrs.deps}" "-Dversion-string=${finalAttrs.version}-${revision}-nix" "-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-wayland=${lib.boolToString enableWayland}" + "-Dcpu=baseline" + "-Doptimize=${optimize}" "-Dstrip=${lib.boolToString strip}" ]; diff --git a/nix/tests.nix b/nix/tests.nix index a9970e80c..3949877cf 100644 --- a/nix/tests.nix +++ b/nix/tests.nix @@ -274,7 +274,7 @@ in { client.succeed("${su "${ghostty} +new-window"}") client.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'") - with subtest("SSH from client to server and verify that the Ghostty terminfo is copied.") + with subtest("SSH from client to server and verify that the Ghostty terminfo is copied."): client.sleep(2) client.send_chars("ssh ghostty@server\n") server.wait_for_file("${user.home}/.terminfo/x/xterm-ghostty", timeout=30) diff --git a/nix/vm/common-gnome.nix b/nix/vm/common-gnome.nix index ab4aab9e9..d8d484071 100644 --- a/nix/vm/common-gnome.nix +++ b/nix/vm/common-gnome.nix @@ -8,7 +8,7 @@ ./common.nix ]; - services.xserver = { + services = { displayManager = { gdm = { enable = true; diff --git a/nix/vm/x11-gnome.nix b/nix/vm/x11-gnome.nix deleted file mode 100644 index 1994aea82..000000000 --- a/nix/vm/x11-gnome.nix +++ /dev/null @@ -1,9 +0,0 @@ -{...}: { - imports = [ - ./common-gnome.nix - ]; - - services.displayManager = { - defaultSession = "gnome-xorg"; - }; -} -- 2.51.2 From 87b11e08929a7e8d5b80dcb8b14f18a25e969883 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 11 Jan 2026 22:54:03 -0800 Subject: [PATCH 244/349] Add failing tests for #10265 --- src/terminal/Terminal.zig | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 8bb167cd1..903e427d4 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -5419,6 +5419,52 @@ test "Terminal: insertLines top/bottom scroll region" { } } +test "Terminal: insertLines across page boundary marks all shifted rows dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); + defer t.deinit(alloc); + + const first_page = t.screens.active.pages.pages.first.?; + const first_page_nrows = first_page.data.capacity.rows; + + // Fill up the first page minus 3 rows + for (0..first_page_nrows - 3) |_| try t.linefeed(); + + // Add content that will cross a page boundary + try t.printString("1AAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("2BBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("3CCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("4DDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("5EEEE"); + + // Verify we now have a second page + try testing.expect(first_page.next != null); + + t.setCursorPos(1, 1); + t.clearDirty(); + t.insertLines(1); + + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", str); + } +} + test "Terminal: insertLines (legacy test)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 2, .rows = 5 }); @@ -8042,6 +8088,52 @@ test "Terminal: deleteLines colors with bg color" { } } +test "Terminal: deleteLines across page boundary marks all shifted rows dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 1024 }); + defer t.deinit(alloc); + + const first_page = t.screens.active.pages.pages.first.?; + const first_page_nrows = first_page.data.capacity.rows; + + // Fill up the first page minus 3 rows + for (0..first_page_nrows - 3) |_| try t.linefeed(); + + // Add content that will cross a page boundary + try t.printString("1AAAA"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("2BBBB"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("3CCCC"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("4DDDD"); + t.carriageReturn(); + try t.linefeed(); + try t.printString("5EEEE"); + + // Verify we now have a second page + try testing.expect(first_page.next != null); + + t.setCursorPos(1, 1); + t.clearDirty(); + t.deleteLines(1); + + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("2BBBB\n3CCCC\n4DDDD\n5EEEE", str); + } +} + test "Terminal: deleteLines (legacy)" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); -- 2.51.2 From 095c82910b2df5302c75934cb5f7b13bdd759efc Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 11 Jan 2026 22:56:03 -0800 Subject: [PATCH 245/349] Terminal: keep cross-boundary rows dirty in {insert,delete}Lines --- src/terminal/Terminal.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 903e427d4..7b384f34e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1690,6 +1690,10 @@ pub fn insertLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; + + // The clone operation may overwrite the dirty flag, so make + // sure the row is still marked dirty. + dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the @@ -1888,6 +1892,10 @@ pub fn deleteLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; + + // The clone operation may overwrite the dirty flag, so make + // sure the row is still marked dirty. + dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the -- 2.51.2 From f37b0c56ecfc3b6ce3b7156c26cd1416652bcbdf Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 12 Jan 2026 09:56:31 -0500 Subject: [PATCH 246/349] Keep track of run's max cluster seen. --- src/font/shaper/coretext.zig | 411 ++++++++++++++++++++++++++++------- 1 file changed, 338 insertions(+), 73 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index b61c8a62d..d36151698 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -6,6 +6,7 @@ const macos = @import("macos"); const font = @import("../main.zig"); const os = @import("../../os/main.zig"); const terminal = @import("../../terminal/main.zig"); +const unicode = @import("../../unicode/main.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -103,7 +104,7 @@ pub const Shaper = struct { } }; - const CellOffset = struct { + const Offset = struct { cluster: u32 = 0, x: f64 = 0, }; @@ -382,11 +383,12 @@ pub const Shaper = struct { const line = typesetter.createLine(.{ .location = 0, .length = 0 }); self.cf_release_pool.appendAssumeCapacity(line); - // This keeps track of the current x offset (sum of advance.width) - var run_offset_x: f64 = 0.0; + // This keeps track of the current x offset (sum of advance.width) and + // the furthest cluster we've seen so far (max). + var run_offset: Offset = .{}; // This keeps track of the cell starting x and cluster. - var cell_offset: CellOffset = .{}; + var cell_offset: Offset = .{}; // For debugging positions, turn this on: //var run_offset_y: f64 = 0.0; @@ -436,12 +438,12 @@ pub const Shaper = struct { const cluster = state.codepoints.items[index].cluster; if (cell_offset.cluster != cluster) { // We previously asserted that the new cluster is greater - // than cell_offset.cluster, but for rtl text this is not - // true. We then used to break out of this block if the new - // cluster was less than cell_offset.cluster, but now this - // would fail to reset cell_offset.x and - // cell_offset.cluster and lead to incorrect shape.Cell `x` - // and `x_offset`. We don't have a test case for RTL, yet. + // than cell_offset.cluster, but this isn't always true. + // See e.g. the "shape Chakma vowel sign with ligature + // (vowel sign renders first)" test. + + const is_after_glyph_from_current_or_next_clusters = + cluster <= run_offset.cluster; const is_first_codepoint_in_cluster = blk: { var i = index; @@ -457,22 +459,38 @@ pub const Shaper = struct { // We need to reset the `cell_offset` at the start of a new // cluster, but we do that conditionally if the codepoint - // `is_first_codepoint_in_cluster`, which is a heuristic to - // detect ligatures and avoid positioning glyphs that mark - // ligatures incorrectly. The idea is that if the first - // codepoint in a cluster doesn't appear in the stream, - // it's very likely that it combined with codepoints from a - // previous cluster into a ligature. Then, the subsequent - // codepoints are very likely marking glyphs that are - // placed relative to that ligature, so if we were to reset - // the `cell_offset` to align it with the grid, the - // positions would be off. (TBD if there are exceptions to - // this heuristic, but using the logging below seems to - // show it works well.) - if (is_first_codepoint_in_cluster) { + // `is_first_codepoint_in_cluster` and the cluster is not + // `is_after_glyph_from_current_or_next_clusters`, which is + // a heuristic to detect ligatures and avoid positioning + // glyphs that mark ligatures incorrectly. The idea is that + // if the first codepoint in a cluster doesn't appear in + // the stream, it's very likely that it combined with + // codepoints from a previous cluster into a ligature. + // Then, the subsequent codepoints are very likely marking + // glyphs that are placed relative to that ligature, so if + // we were to reset the `cell_offset` to align it with the + // grid, the positions would be off. The + // `!is_after_glyph_from_current_or_next_clusters` check is + // needed in case these marking glyphs come from a later + // cluster but are rendered first (see the Chakma and + // Bengali tests). In that case when we get to the + // codepoint that `is_first_codepoint_in_cluster`, but in a + // cluster that + // `is_after_glyph_from_current_or_next_clusters`, we don't + // want to reset to the grid and cause the positions to be + // off. (Note that we could go back and align the cells to + // the grid starting from the one from the cluster that + // rendered out of order, but that is more complicated so + // we don't do that for now. Also, it's TBD if there are + // exceptions to this heuristic for detecting ligatures, + // but using the logging below seems to show it works + // well.) + if (is_first_codepoint_in_cluster and + !is_after_glyph_from_current_or_next_clusters) + { cell_offset = .{ .cluster = cluster, - .x = run_offset_x, + .x = run_offset.x, }; // For debugging positions, turn this on: @@ -481,7 +499,7 @@ pub const Shaper = struct { } // For debugging positions, turn this on: - //try self.debugPositions(alloc, run_offset_x, run_offset_y, cell_offset, cell_offset_y, position, index); + //try self.debugPositions(alloc, run_offset, run_offset_y, cell_offset, cell_offset_y, position, index); const x_offset = position.x - cell_offset.x; @@ -494,7 +512,8 @@ pub const Shaper = struct { // Add our advances to keep track of our run offsets. // Advances apply to the NEXT cell. - run_offset_x += advance.width; + run_offset.x += advance.width; + run_offset.cluster = @max(run_offset.cluster, cluster); // For debugging positions, turn this on: //run_offset_y += advance.height; @@ -670,16 +689,16 @@ pub const Shaper = struct { fn debugPositions( self: *Shaper, alloc: Allocator, - run_offset_x: f64, + run_offset: Offset, run_offset_y: f64, - cell_offset: CellOffset, + cell_offset: Offset, cell_offset_y: f64, position: macos.graphics.Point, index: usize, ) !void { const state = &self.run_state; const x_offset = position.x - cell_offset.x; - const advance_x_offset = run_offset_x - cell_offset.x; + const advance_x_offset = run_offset.x - cell_offset.x; const advance_y_offset = run_offset_y - cell_offset_y; const x_offset_diff = x_offset - advance_x_offset; const y_offset_diff = position.y - advance_y_offset; @@ -689,7 +708,30 @@ pub const Shaper = struct { const cluster = state.codepoints.items[index].cluster; const cluster_differs = cluster != cell_offset.cluster; - if (positions_differ or position_y_differs or cluster_differs) { + // To debug every loop, flip this to true: + const extra_debugging = false; + + const is_previous_codepoint_prepend = if (cluster_differs or + extra_debugging) + blk: { + var i = index; + while (i > 0) { + i -= 1; + const codepoint = state.codepoints.items[i]; + + // Skip surrogate pair padding + if (codepoint.codepoint == 0) continue; + + break :blk unicode.table.get(@intCast(codepoint.codepoint)).grapheme_boundary_class == .prepend; + } + break :blk false; + } else false; + + const formatted_cps = if (positions_differ or + position_y_differs or + cluster_differs or + extra_debugging) + blk: { var allocating = std.Io.Writer.Allocating.init(alloc); const writer = &allocating.writer; const codepoints = state.codepoints.items; @@ -725,49 +767,69 @@ pub const Shaper = struct { try writer.print("{u}", .{@as(u21, @intCast(cp.codepoint))}); } } - const formatted_cps = try allocating.toOwnedSlice(); - - if (positions_differ) { - log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) cps = {s}", .{ - cluster, - x_offset, - position.y, - advance_x_offset, - advance_y_offset, - x_offset_diff, - y_offset_diff, - formatted_cps, - }); - } + break :blk try allocating.toOwnedSlice(); + } else ""; + + if (extra_debugging) { + log.warn("extra debugging of positions index={d} cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) is_prev_prepend={} cps = {s}", .{ + index, + cell_offset.cluster, + cluster, + run_offset.cluster, + cluster - cell_offset.cluster, + x_offset, + position.y, + run_offset.x, + run_offset_y, + cell_offset.x, + cell_offset_y, + is_previous_codepoint_prepend, + formatted_cps, + }); + } - if (position_y_differs) { - log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} cps = {s}", .{ - cluster, - x_offset, - position.y, - run_offset_x, - run_offset_y, - cell_offset.x, - cell_offset_y, - old_offset_y, - formatted_cps, - }); - } + if (positions_differ) { + log.warn("position differs from advance: cluster={d} pos=({d:.2},{d:.2}) adv=({d:.2},{d:.2}) diff=({d:.2},{d:.2}) cps = {s}", .{ + cluster, + x_offset, + position.y, + advance_x_offset, + advance_y_offset, + x_offset_diff, + y_offset_diff, + formatted_cps, + }); + } - if (cluster_differs) { - log.warn("cell_offset.cluster differs from cluster (potential ligature detected) cell_offset.cluster={d} cluster={d} diff={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) cps = {s}", .{ - cell_offset.cluster, - cluster, - cluster - cell_offset.cluster, - x_offset, - position.y, - run_offset_x, - run_offset_y, - cell_offset.x, - cell_offset_y, - formatted_cps, - }); - } + if (position_y_differs) { + log.warn("position.y differs from old offset.y: cluster={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) old offset.y={d:.2} cps = {s}", .{ + cluster, + x_offset, + position.y, + run_offset.x, + run_offset_y, + cell_offset.x, + cell_offset_y, + old_offset_y, + formatted_cps, + }); + } + + if (cluster_differs) { + log.warn("cell_offset.cluster differs from cluster (potential ligature detected) cell_offset.cluster={d} cluster={d} run_offset.cluster={d} diff={d} pos=({d:.2},{d:.2}) run_offset=({d:.2},{d:.2}) cell_offset=({d:.2},{d:.2}) is_prev_prepend={} cps = {s}", .{ + cell_offset.cluster, + cluster, + run_offset.cluster, + cluster - cell_offset.cluster, + x_offset, + position.y, + run_offset.x, + run_offset_y, + cell_offset.x, + cell_offset_y, + is_previous_codepoint_prepend, + formatted_cps, + }); } } }; @@ -1636,7 +1698,7 @@ test "shape Tai Tham letters (position.y differs from advance)" { try testing.expectEqual(@as(usize, 3), cells.len); try testing.expectEqual(@as(u16, 0), cells[0].x); try testing.expectEqual(@as(u16, 0), cells[1].x); - try testing.expectEqual(@as(u16, 1), cells[2].x); // U from second grapheme + try testing.expectEqual(@as(u16, 0), cells[2].x); // U from second grapheme // The U glyph renders at a y below zero try testing.expectEqual(@as(i16, -3), cells[2].y_offset); @@ -1644,6 +1706,209 @@ test "shape Tai Tham letters (position.y differs from advance)" { try testing.expectEqual(@as(usize, 1), count); } +test "shape Javanese ligatures" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Javanese for this to work, if we can't find + // Noto Sans Javanese Regular, which is a system font on macOS, we just + // skip the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Javanese", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0xa9a4, buf[buf_idx..]); // NA + buf_idx += try std.unicode.utf8Encode(0xa9c0, buf[buf_idx..]); // PANGKON + // Second grapheme cluster, combining with the first in a ligature: + buf_idx += try std.unicode.utf8Encode(0xa9b2, buf[buf_idx..]); // HA + buf_idx += try std.unicode.utf8Encode(0xa9b8, buf[buf_idx..]); // Vowel sign SUKU + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + const cell_width = run.grid.metrics.cell_width; + try testing.expectEqual(@as(usize, 3), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); + + // The vowel sign SUKU renders with correct x_offset + try testing.expect(cells[2].x_offset > 3 * cell_width); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Chakma vowel sign with ligature (vowel sign renders first)" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Chakma for this to work, if we can't find + // Noto Sans Chakma Regular, which is a system font on macOS, we just skip + // the test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Noto Sans Chakma", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x1111d, buf[buf_idx..]); // BAA + // Second grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x11116, buf[buf_idx..]); // TAA + buf_idx += try std.unicode.utf8Encode(0x11133, buf[buf_idx..]); // Virama + // Third grapheme cluster, combining with the second in a ligature: + buf_idx += try std.unicode.utf8Encode(0x11120, buf[buf_idx..]); // YYAA + buf_idx += try std.unicode.utf8Encode(0x1112c, buf[buf_idx..]); // Vowel Sign U + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 4), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + // See the giant "We need to reset the `cell_offset`" comment, but here + // we should technically have the rest of these be `x` of 1, but that + // would require going back in the stream to adjust past cells, and + // don't take on that complexity. + try testing.expectEqual(@as(u16, 0), cells[1].x); + try testing.expectEqual(@as(u16, 0), cells[2].x); + try testing.expectEqual(@as(u16, 0), cells[3].x); + + // The vowel sign U renders before the TAA: + try testing.expect(cells[1].x_offset < cells[2].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + +test "shape Bengali ligatures with out of order vowels" { + const testing = std.testing; + const alloc = testing.allocator; + + // We need a font that supports Bengali for this to work, if we can't find + // Arial Unicode MS, which is a system font on macOS, we just skip the + // test. + var testdata = testShaperWithDiscoveredFont( + alloc, + "Arial Unicode MS", + ) catch return error.SkipZigTest; + defer testdata.deinit(); + + var buf: [32]u8 = undefined; + var buf_idx: usize = 0; + + // First grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA + buf_idx += try std.unicode.utf8Encode(0x09be, buf[buf_idx..]); // Vowel sign AA + // Second grapheme cluster: + buf_idx += try std.unicode.utf8Encode(0x09b7, buf[buf_idx..]); // SSA + buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama + // Third grapheme cluster, combining with the second in a ligature: + buf_idx += try std.unicode.utf8Encode(0x099f, buf[buf_idx..]); // TTA + buf_idx += try std.unicode.utf8Encode(0x09cd, buf[buf_idx..]); // Virama + // Fourth grapheme cluster, combining with the previous two in a ligature: + buf_idx += try std.unicode.utf8Encode(0x09b0, buf[buf_idx..]); // RA + buf_idx += try std.unicode.utf8Encode(0x09c7, buf[buf_idx..]); // Vowel sign E + + // Make a screen with some data + var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 }); + defer t.deinit(alloc); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + var s = t.vtStream(); + defer s.deinit(); + try s.nextSlice(buf[0..buf_idx]); + + var state: terminal.RenderState = .empty; + defer state.deinit(alloc); + try state.update(alloc, &t); + + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .cells = state.row_data.get(0).cells.slice(), + }); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + + const cells = try shaper.shape(run); + try testing.expectEqual(@as(usize, 8), cells.len); + try testing.expectEqual(@as(u16, 0), cells[0].x); + try testing.expectEqual(@as(u16, 0), cells[1].x); + // See the giant "We need to reset the `cell_offset`" comment, but here + // we should technically have the rest of these be `x` of 1, but that + // would require going back in the stream to adjust past cells, and + // don't take on that complexity. + try testing.expectEqual(@as(u16, 0), cells[2].x); + try testing.expectEqual(@as(u16, 0), cells[3].x); + try testing.expectEqual(@as(u16, 0), cells[4].x); + try testing.expectEqual(@as(u16, 0), cells[5].x); + try testing.expectEqual(@as(u16, 0), cells[6].x); + try testing.expectEqual(@as(u16, 0), cells[7].x); + + // The vowel sign E renders before the SSA: + try testing.expect(cells[2].x_offset < cells[3].x_offset); + } + try testing.expectEqual(@as(usize, 1), count); +} + test "shape box glyphs" { const testing = std.testing; const alloc = testing.allocator; @@ -2381,7 +2646,7 @@ fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestS .monospace = false, }); defer disco_it.deinit(); - var face: font.DeferredFace = (try disco_it.next()).?; + var face: font.DeferredFace = (try disco_it.next()) orelse return error.FontNotFound; errdefer face.deinit(); _ = try c.add( alloc, -- 2.51.2 From 2a0a57506524999a21192410eb06e630f2a9052b Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 12 Jan 2026 10:22:37 -0500 Subject: [PATCH 247/349] comment fixup --- src/font/shaper/coretext.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index d36151698..223b2382b 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1823,7 +1823,7 @@ test "shape Chakma vowel sign with ligature (vowel sign renders first)" { // See the giant "We need to reset the `cell_offset`" comment, but here // we should technically have the rest of these be `x` of 1, but that // would require going back in the stream to adjust past cells, and - // don't take on that complexity. + // we don't take on that complexity. try testing.expectEqual(@as(u16, 0), cells[1].x); try testing.expectEqual(@as(u16, 0), cells[2].x); try testing.expectEqual(@as(u16, 0), cells[3].x); @@ -1895,7 +1895,7 @@ test "shape Bengali ligatures with out of order vowels" { // See the giant "We need to reset the `cell_offset`" comment, but here // we should technically have the rest of these be `x` of 1, but that // would require going back in the stream to adjust past cells, and - // don't take on that complexity. + // we don't take on that complexity. try testing.expectEqual(@as(u16, 0), cells[2].x); try testing.expectEqual(@as(u16, 0), cells[3].x); try testing.expectEqual(@as(u16, 0), cells[4].x); -- 2.51.2 From 7ed19689b9791ab2f64fdaa4c088da31507a1d71 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 Jan 2026 08:35:15 -0800 Subject: [PATCH 248/349] terminal: add Capacity.maxCols --- src/terminal/PageList.zig | 8 +++ src/terminal/page.zig | 113 +++++++++++++++++++++++++++++--------- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index a96aa1975..546f6c2e2 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -49,7 +49,15 @@ const Node = struct { /// The memory pool we get page nodes from. const NodePool = std.heap.MemoryPool(List.Node); +/// The standard page capacity that we use as a starting point for +/// all pages. This is chosen as a sane default that fits most terminal +/// usage to support using our pool. const std_capacity = pagepkg.std_capacity; + +/// The maximum columns we can support with the standard capacity. +const std_max_cols = std_capacity.maxCols().?; + +/// The byte size required for a standard page. const std_size = Page.layout(std_capacity).total_size; /// The memory pool we use for page memory buffers. We use a separate pool diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 124ff2545..6e6416e4e 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1662,43 +1662,42 @@ pub const Capacity = struct { cols: ?size.CellCountInt = null, }; + /// Returns the maximum number of columns that can be used with this + /// capacity while still fitting at least one row. Returns null if even + /// a single column cannot fit (which would indicate an unusable capacity). + /// + /// Note that this is the maximum number of columns that never increases + /// the amount of memory the original capacity will take. If you modify + /// the original capacity to add rows, then you can fit more columns. + pub fn maxCols(self: Capacity) ?size.CellCountInt { + const available_bits = self.availableBitsForGrid(); + + // If we can't even fit the row metadata, return null + if (available_bits <= @bitSizeOf(Row)) return null; + + // We do the math of how many columns we can fit in the remaining + // bits ignoring the metadat of a row. + const remaining_bits = available_bits - @bitSizeOf(Row); + const max_cols = remaining_bits / @bitSizeOf(Cell); + + // Clamp to CellCountInt max + return @min(std.math.maxInt(size.CellCountInt), max_cols); + } + /// Adjust the capacity parameters while retaining the same total size. + /// /// Adjustments always happen by limiting the rows in the page. Everything /// else can grow. If it is impossible to achieve the desired adjustment, /// OutOfMemory is returned. pub fn adjust(self: Capacity, req: Adjustment) Allocator.Error!Capacity { var adjusted = self; if (req.cols) |cols| { - // The math below only works if there is no alignment gap between - // the end of the rows array and the start of the cells array. - // - // To guarantee this, we assert that Row's size is a multiple of - // Cell's alignment, so that any length array of Rows will end on - // a valid alignment for the start of the Cell array. - assert(@sizeOf(Row) % @alignOf(Cell) == 0); - - const layout = Page.layout(self); - - // In order to determine the amount of space in the page available - // 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.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, StyleSet.base_align.toByteUnits()); + const available_bits = self.availableBitsForGrid(); // The size per row is: // - The row metadata itself // - The cells per row (n=cols) - const bits_per_row: usize = size: { - var bits: usize = @bitSizeOf(Row); // Row metadata - bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols) - break :size bits; - }; - const available_bits: usize = styles_start * 8; + const bits_per_row: usize = @bitSizeOf(Row) + @bitSizeOf(Cell) * @as(usize, @intCast(cols)); const new_rows: usize = @divFloor(available_bits, bits_per_row); // If our rows go to zero then we can't fit any row metadata @@ -1711,6 +1710,34 @@ pub const Capacity = struct { return adjusted; } + + /// Computes the number of bits available for rows and cells in the page. + /// + /// This is done by laying out the "meta" members (styles, graphemes, + /// hyperlinks, strings) from the end of the page and finding where they + /// start, which gives us the space available for rows and cells. + fn availableBitsForGrid(self: Capacity) usize { + // The math below only works if there is no alignment gap between + // the end of the rows array and the start of the cells array. + // + // To guarantee this, we assert that Row's size is a multiple of + // Cell's alignment, so that any length array of Rows will end on + // a valid alignment for the start of the Cell array. + assert(@sizeOf(Row) % @alignOf(Cell) == 0); + + const l = Page.layout(self); + + // Layout meta members from the end to find styles_start + const hyperlink_map_start = alignBackward(usize, l.total_size - l.hyperlink_map_layout.total_size, hyperlink.Map.base_align.toByteUnits()); + const hyperlink_set_start = alignBackward(usize, hyperlink_map_start - l.hyperlink_set_layout.total_size, hyperlink.Set.base_align.toByteUnits()); + const string_alloc_start = alignBackward(usize, hyperlink_set_start - l.string_alloc_layout.total_size, StringAlloc.base_align.toByteUnits()); + const grapheme_map_start = alignBackward(usize, string_alloc_start - l.grapheme_map_layout.total_size, GraphemeMap.base_align.toByteUnits()); + const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - l.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align.toByteUnits()); + const styles_start = alignBackward(usize, grapheme_alloc_start - l.styles_layout.total_size, StyleSet.base_align.toByteUnits()); + + // Multiply by 8 to convert bytes to bits + return styles_start * 8; + } }; pub const Row = packed struct(u64) { @@ -2070,6 +2097,40 @@ test "Page capacity adjust cols too high" { ); } +test "Capacity maxCols basic" { + const cap = std_capacity; + const max = cap.maxCols().?; + + // maxCols should be >= current cols (since current capacity is valid) + try testing.expect(max >= cap.cols); + + // Adjusting to maxCols should succeed with at least 1 row + const adjusted = try cap.adjust(.{ .cols = max }); + try testing.expect(adjusted.rows >= 1); + + // Adjusting to maxCols + 1 should fail + try testing.expectError( + error.OutOfMemory, + cap.adjust(.{ .cols = max + 1 }), + ); +} + +test "Capacity maxCols preserves total size" { + const cap = std_capacity; + const original_size = Page.layout(cap).total_size; + const max = cap.maxCols().?; + const adjusted = try cap.adjust(.{ .cols = max }); + const adjusted_size = Page.layout(adjusted).total_size; + try testing.expectEqual(original_size, adjusted_size); +} + +test "Capacity maxCols with 1 row exactly" { + const cap = std_capacity; + const max = cap.maxCols().?; + const adjusted = try cap.adjust(.{ .cols = max }); + try testing.expectEqual(@as(size.CellCountInt, 1), adjusted.rows); +} + test "Page init" { var page = try Page.init(.{ .cols = 120, -- 2.51.2 From 257aafb7b44db09252e1ae5a8d63a9e8920f788d Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 12 Jan 2026 09:32:33 -0800 Subject: [PATCH 249/349] Consolidate dirty marking in insertLines/deleteLines --- src/terminal/Terminal.zig | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 7b384f34e..d717a9724 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1602,9 +1602,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row; - // Mark the row as dirty - cur_p.markDirty(); - // If this is one of the lines we need to shift, do so if (y > adjusted_count) { const off_p = cur_p.up(adjusted_count).?; @@ -1690,10 +1687,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; - - // The clone operation may overwrite the dirty flag, so make - // sure the row is still marked dirty. - dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the @@ -1703,9 +1696,6 @@ pub fn insertLines(self: *Terminal, count: usize) void { dst_row.* = src_row.*; src_row.* = dst; - // Make sure the row is marked as dirty though. - dst_row.dirty = true; - // Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { @@ -1732,6 +1722,9 @@ pub fn insertLines(self: *Terminal, count: usize) void { ); } + // Mark the row as dirty + cur_p.markDirty(); + // We have successfully processed a line. y -= 1; // Move our pin up to the next row. @@ -1809,9 +1802,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { const cur_rac = cur_p.rowAndCell(); const cur_row: *Row = cur_rac.row; - // Mark the row as dirty - cur_p.markDirty(); - // If this is one of the lines we need to shift, do so if (y < rem - adjusted_count) { const off_p = cur_p.down(adjusted_count).?; @@ -1892,10 +1882,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { // Continue the loop to try handling this row again. continue; }; - - // The clone operation may overwrite the dirty flag, so make - // sure the row is still marked dirty. - dst_row.dirty = true; } else { if (!left_right) { // Swap the src/dst cells. This ensures that our dst gets the @@ -1905,9 +1891,6 @@ pub fn deleteLines(self: *Terminal, count: usize) void { dst_row.* = src_row.*; src_row.* = dst; - // Make sure the row is marked as dirty though. - dst_row.dirty = true; - // Ensure what we did didn't corrupt the page cur_p.node.data.assertIntegrity(); } else { @@ -1934,6 +1917,9 @@ pub fn deleteLines(self: *Terminal, count: usize) void { ); } + // Mark the row as dirty + cur_p.markDirty(); + // We have successfully processed a line. y += 1; // Move our pin down to the next row. -- 2.51.2 From d512de7e7839d580d02cb2560fc6733c81b7d058 Mon Sep 17 00:00:00 2001 From: MithicSpirit Date: Mon, 12 Jan 2026 00:22:18 -0500 Subject: [PATCH 250/349] nix: update zon2nix After the last commit, zon2nix required recompiling zig, which caused slow build times and CI failures. --- flake.lock | 8 ++++---- flake.nix | 2 +- nix/devShell.nix | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/flake.lock b/flake.lock index 853fb27b6..be298785c 100644 --- a/flake.lock +++ b/flake.lock @@ -125,17 +125,17 @@ ] }, "locked": { - "lastModified": 1758405547, - "narHash": "sha256-WgaDgvIZMPvlZcZrpPMjkaalTBnGF2lTG+62znXctWM=", + "lastModified": 1768231828, + "narHash": "sha256-wL/8Iij4T2OLkhHcc4NieOjf7YeJffaUYbCiCqKv/+0=", "owner": "jcollie", "repo": "zon2nix", - "rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245", + "rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071", "type": "github" }, "original": { "owner": "jcollie", "repo": "zon2nix", - "rev": "bf983aa90ff169372b9fa8c02e57ea75e0b42245", + "rev": "c28e93f3ba133d4c1b1d65224e2eebede61fd071", "type": "github" } } diff --git a/flake.nix b/flake.nix index 58e250aec..a854f6ea3 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,7 @@ }; zon2nix = { - url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245"; + url = "github:jcollie/zon2nix?rev=c28e93f3ba133d4c1b1d65224e2eebede61fd071"; inputs = { nixpkgs.follows = "nixpkgs"; }; diff --git a/nix/devShell.nix b/nix/devShell.nix index d37107133..90059a730 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -26,6 +26,7 @@ wasmtime, wraptest, zig, + zig_0_15, zip, llvmPackages_latest, bzip2, -- 2.51.2 From 5817e1dc5f4a12d2d68b9865053a64bdbd43ebd9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 Jan 2026 09:13:52 -0800 Subject: [PATCH 251/349] terminal: PageList can initialize with memory requirements > std --- src/terminal/PageList.zig | 136 ++++++++++++++++++++++++++++++++------ src/terminal/page.zig | 7 +- 2 files changed, 118 insertions(+), 25 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 546f6c2e2..fc680b971 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -54,9 +54,6 @@ const NodePool = std.heap.MemoryPool(List.Node); /// usage to support using our pool. const std_capacity = pagepkg.std_capacity; -/// The maximum columns we can support with the standard capacity. -const std_max_cols = std_capacity.maxCols().?; - /// The byte size required for a standard page. const std_size = Page.layout(std_capacity).total_size; @@ -231,19 +228,30 @@ pub const Viewport = union(enum) { /// But this gives us a nice fast heuristic for determining min/max size. /// Therefore, if the page size is violated you should always also verify /// that we have enough space for the active area. -fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize { +fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) usize { + // Invariant required to ensure our divCeil below cannot overflow. + comptime { + const max_rows = std.math.maxInt(size.CellCountInt); + _ = std.math.divCeil(usize, max_rows, 1) catch unreachable; + } + // Get our capacity to fit our rows. If the cols are too big, it may // force less rows than we want meaning we need more than one page to // represent a viewport. - const cap = try std_capacity.adjust(.{ .cols = cols }); + const cap = initialCapacity(cols); // Calculate the number of standard sized pages we need to represent // an active area. - const pages_exact = if (cap.rows >= rows) 1 else try std.math.divCeil( + const pages_exact = if (cap.rows >= rows) 1 else std.math.divCeil( usize, rows, cap.rows, - ); + ) catch { + // Not possible: + // - initialCapacity guarantees at least 1 row + // - numerator/denominator can't overflow because of comptime check above + unreachable; + }; // We always need at least one page extra so that we // can fit partial pages to spread our active area across two pages. @@ -263,6 +271,49 @@ fn minMaxSize(cols: size.CellCountInt, rows: size.CellCountInt) !usize { return PagePool.item_size * pages; } +/// Calculates the initial capacity for a new page for a given column +/// count. This will attempt to fit within std_size at all times so we +/// can use our memory pool, but if cols is too big, this will return a +/// larger capacity. +/// +/// The returned capacity is always guaranteed to layout properly (not +/// overflow). We are able to support capacities up to the maximum int +/// value of cols, so this will never overflow. +fn initialCapacity(cols: size.CellCountInt) Capacity { + // This is an important invariant that ensures that this function + // can never return an error. We verify here that our standard capacity + // when increased to maximum possible columns can always support at + // least one row in memory. + // + // IF THIS EVER FAILS: We probably need to modify our logic below + // to reduce other elements of the capacity (styles, graphemes, etc.). + // But, instead, I recommend taking a step back and re-evaluating + // life choices. + comptime { + var cap = std_capacity; + cap.cols = std.math.maxInt(size.CellCountInt); + _ = Page.layout(cap); + } + + if (std_capacity.adjust( + .{ .cols = cols }, + )) |cap| { + // If we can adjust our standard capacity, we fit within the + // standard size and we're good! + return cap; + } else |err| { + // Ensure our error set doesn't change. + comptime assert(@TypeOf(err) == error{OutOfMemory}); + } + + // This code path means that our standard capacity can't even + // accommodate our column count! The only solution is to increase + // our capacity and go non-standard. + var cap: Capacity = std_capacity; + cap.cols = cols; + return cap; +} + /// This is the page allocator we'll use for all our underlying /// VM page allocations. inline fn pageAllocator() Allocator { @@ -318,7 +369,7 @@ pub fn init( ); // Get our minimum max size, see doc comments for more details. - const min_max_size = try minMaxSize(cols, rows); + const min_max_size = minMaxSize(cols, rows); // We always track our viewport pin to ensure this is never an allocation const viewport_pin = try pool.pins.create(); @@ -352,17 +403,31 @@ fn initPages( serial: *u64, cols: size.CellCountInt, rows: size.CellCountInt, -) !struct { List, usize } { +) Allocator.Error!struct { List, usize } { var page_list: List = .{}; var page_size: usize = 0; // Add pages as needed to create our initial viewport. - const cap = try std_capacity.adjust(.{ .cols = cols }); + const cap = initialCapacity(cols); + const layout = Page.layout(cap); + const pooled = layout.total_size <= std_size; + const page_alloc = pool.pages.arena.child_allocator; + var rem = rows; while (rem > 0) { const node = try pool.nodes.create(); - const page_buf = try pool.pages.create(); - // no errdefer because the pool deinit will clean these up + const page_buf = if (pooled) + try pool.pages.create() + else + try page_alloc.alignedAlloc( + u8, + .fromByteUnits(std.heap.page_size_min), + layout.total_size, + ); + errdefer if (pooled) + pool.pages.destroy(page_buf) + else + page_alloc.free(page_buf); // In runtime safety modes we have to memset because the Zig allocator // interface will always memset to 0xAA for undefined. In non-safe modes @@ -372,10 +437,7 @@ fn initPages( // Initialize the first set of pages to contain our viewport so that // the top of the first page is always the active area. node.* = .{ - .data = .initBuf( - .init(page_buf), - Page.layout(cap), - ), + .data = .initBuf(.init(page_buf), layout), .serial = serial.*, }; node.data.size.rows = @min(rem, node.data.capacity.rows); @@ -541,10 +603,8 @@ pub fn reset(self: *PageList) void { // 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. - const cap = std_capacity.adjust( - .{ .cols = self.cols }, - ) catch @panic("reset: std_capacity.adjust failed"); - assert(cap.rows > 0); // adjust should never return 0 rows + const cap = initialCapacity(self.cols); + assert(cap.rows > 0); // The number of pages we need is the number of rows in the active // area divided by the row capacity of a page. @@ -836,7 +896,7 @@ pub fn resize(self: *PageList, opts: Resize) !void { // when increasing beyond our initial minimum max size or explicit max // size to fit the active area. const old_min_max_size = self.min_max_size; - self.min_max_size = try minMaxSize( + self.min_max_size = minMaxSize( opts.cols orelse self.cols, opts.rows orelse self.rows, ); @@ -1600,7 +1660,7 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { // We only set the new min_max_size if we're not reflowing. If we are // reflowing, then resize handles this for us. const old_min_max_size = self.min_max_size; - self.min_max_size = if (!opts.reflow) try minMaxSize( + self.min_max_size = if (!opts.reflow) minMaxSize( opts.cols orelse self.cols, opts.rows orelse self.rows, ) else old_min_max_size; @@ -4559,6 +4619,38 @@ test "PageList init rows across two pages" { }, s.scrollbar()); } +test "PageList init more than max cols" { + const testing = std.testing; + const alloc = testing.allocator; + + // Initialize with more columns than we can fit in our standard + // capacity. This is going to force us to go to a non-standard page + // immediately. + var s = try init( + alloc, + std_capacity.maxCols().? + 1, + 80, + null, + ); + defer s.deinit(); + try testing.expect(s.viewport == .active); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // We expect a single, non-standard page + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + // 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" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 6e6416e4e..848123405 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -196,7 +196,8 @@ pub const Page = struct { // We need to go through and initialize all the rows so that // they point to a valid offset into the cells, since the rows // zero-initialized aren't valid. - const cells_ptr = cells.ptr(buf)[0 .. cap.cols * cap.rows]; + const cells_len = @as(usize, cap.cols) * @as(usize, cap.rows); + const cells_ptr = cells.ptr(buf)[0..cells_len]; for (rows.ptr(buf)[0..cap.rows], 0..) |*row, y| { const start = y * cap.cols; row.* = .{ @@ -1556,7 +1557,7 @@ pub const Page = struct { const rows_start = 0; const rows_end: usize = rows_start + (rows_count * @sizeOf(Row)); - const cells_count: usize = @intCast(cap.cols * cap.rows); + const cells_count: usize = @as(usize, cap.cols) * @as(usize, cap.rows); const cells_start = alignForward(usize, rows_end, @alignOf(Cell)); const cells_end = cells_start + (cells_count * @sizeOf(Cell)); @@ -1676,7 +1677,7 @@ pub const Capacity = struct { if (available_bits <= @bitSizeOf(Row)) return null; // We do the math of how many columns we can fit in the remaining - // bits ignoring the metadat of a row. + // bits ignoring the metadata of a row. const remaining_bits = available_bits - @bitSizeOf(Row); const max_cols = remaining_bits / @bitSizeOf(Cell); -- 2.51.2 From 2af6e255e4dd4ea685610f7445d7b69ba4410e49 Mon Sep 17 00:00:00 2001 From: MithicSpirit Date: Mon, 12 Jan 2026 13:13:17 -0500 Subject: [PATCH 252/349] chore: fix typo curor->cursor (2x) Detected by CI (typos) after nixpkgs update. --- src/extra/bash.zig | 2 +- src/terminal/stream.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extra/bash.zig b/src/extra/bash.zig index ee9a7895c..ee672a964 100644 --- a/src/extra/bash.zig +++ b/src/extra/bash.zig @@ -296,7 +296,7 @@ fn writeBashCompletions(writer: *std.Io.Writer) !void { \\ else prev="${COMP_WORDS[COMP_CWORD-1]}" \\ fi \\ - \\ # current completion is double quoted add a space so the curor progresses + \\ # current completion is double quoted add a space so the cursor progresses \\ if [[ "$2" == \"*\" ]]; then \\ COMPREPLY=( "$cur " ); \\ return; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 665b54284..eef249327 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1641,7 +1641,7 @@ pub fn Stream(comptime Handler: type) type { }, }, else => { - log.warn("invalid set curor style command: {f}", .{input}); + log.warn("invalid set cursor style command: {f}", .{input}); return; }, }; -- 2.51.2 From 8f4bfeece56c1d31b920116e9baf1b7ee2edfedd Mon Sep 17 00:00:00 2001 From: MithicSpirit Date: Mon, 12 Jan 2026 13:14:10 -0500 Subject: [PATCH 253/349] ci: fix ryand56/r2-upload-action version comment Does not entail any actual changes in the version, merely in the comment indicating the used version. Detected by CI (GitHub Action Pins) after nixpkgs update. --- .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 c433e7484..acb1ab1f1 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -64,7 +64,7 @@ jobs: mkdir blob mv appcast.xml blob/appcast.xml - name: Upload Appcast to R2 - uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1 + uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 with: r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }} r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }} -- 2.51.2 From 1537590a5f25bca16f2516efda33a5571f100008 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 12 Jan 2026 11:40:22 -0500 Subject: [PATCH 254/349] macos: cycle through our icons in the About view Clicking on the icon immediately advances to the next one. Hovering on the icon pauses the automatic cycling, and the "help" tooltip displays the icon's configuration name (for `macos-icon`). --- macos/Ghostty.xcodeproj/project.pbxproj | 1 + macos/Sources/App/macOS/AppDelegate.swift | 34 +++------- macos/Sources/Features/About/AboutView.swift | 5 +- .../Features/About/CyclingIconView.swift | 62 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 16 +++++ 5 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 macos/Sources/Features/About/CyclingIconView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 9bd36eaad..0b3432362 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ Features/About/About.xib, Features/About/AboutController.swift, Features/About/AboutView.swift, + Features/About/CyclingIconView.swift, "Features/App Intents/CloseTerminalIntent.swift", "Features/App Intents/CommandPaletteIntent.swift", "Features/App Intents/Entities/CommandEntity.swift", diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 2e62d033b..e8c56e9d9 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -946,33 +946,8 @@ class AppDelegate: NSObject, var appIconName: String? = config.macosIcon.rawValue switch (config.macosIcon) { - case .official: - // Discard saved icon name - appIconName = nil - break - case .blueprint: - appIcon = NSImage(named: "BlueprintImage")! - - case .chalkboard: - appIcon = NSImage(named: "ChalkboardImage")! - - case .glass: - appIcon = NSImage(named: "GlassImage")! - - case .holographic: - appIcon = NSImage(named: "HolographicImage")! - - case .microchip: - appIcon = NSImage(named: "MicrochipImage")! - - case .paper: - appIcon = NSImage(named: "PaperImage")! - - case .retro: - appIcon = NSImage(named: "RetroImage")! - - case .xray: - appIcon = NSImage(named: "XrayImage")! + case let icon where icon.assetName != nil: + appIcon = NSImage(named: icon.assetName!)! case .custom: if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) { @@ -982,6 +957,7 @@ class AppDelegate: NSObject, appIcon = nil // Revert back to official icon if invalid location appIconName = nil // Discard saved icon name } + case .customStyle: // Discard saved icon name // if no valid colours were found @@ -997,6 +973,10 @@ class AppDelegate: NSObject, let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString) appIconName = (colorStrings + [config.macosIconFrame.rawValue]) .joined(separator: "_") + + default: + // Discard saved icon name + appIconName = nil } // Only change the icon if it has actually changed from the current one, diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 6ed3285ed..967eb16b0 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -44,10 +44,7 @@ struct AboutView: View { var body: some View { VStack(alignment: .center) { - ghosttyIconImage() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 128) + CyclingIconView() VStack(alignment: .center, spacing: 32) { VStack(alignment: .center, spacing: 8) { diff --git a/macos/Sources/Features/About/CyclingIconView.swift b/macos/Sources/Features/About/CyclingIconView.swift new file mode 100644 index 000000000..4274278e0 --- /dev/null +++ b/macos/Sources/Features/About/CyclingIconView.swift @@ -0,0 +1,62 @@ +import SwiftUI +import GhosttyKit + +/// A view that cycles through Ghostty's official icon variants. +struct CyclingIconView: View { + @State private var currentIcon: Ghostty.MacOSIcon = .official + @State private var isHovering: Bool = false + + private let icons: [Ghostty.MacOSIcon] = [ + .official, + .blueprint, + .chalkboard, + .microchip, + .glass, + .holographic, + .paper, + .retro, + .xray, + ] + private let timerPublisher = Timer.publish(every: 3, on: .main, in: .common) + + var body: some View { + ZStack { + iconView(for: currentIcon) + .id(currentIcon) + } + .animation(.easeInOut(duration: 0.5), value: currentIcon) + .frame(height: 128) + .onReceive(timerPublisher.autoconnect()) { _ in + if !isHovering { + advanceToNextIcon() + } + } + .onHover { hovering in + isHovering = hovering + } + .onTapGesture { + advanceToNextIcon() + } + .help("macos-icon = \(currentIcon.rawValue)") + .accessibilityLabel("Ghostty Application Icon") + .accessibilityHint("Click to cycle through icon variants") + } + + @ViewBuilder + private func iconView(for icon: Ghostty.MacOSIcon) -> some View { + let iconImage: Image = switch icon.assetName { + case let assetName?: Image(assetName) + case nil: ghosttyIconImage() + } + + iconImage + .resizable() + .aspectRatio(contentMode: .fit) + } + + private func advanceToNextIcon() { + let currentIndex = icons.firstIndex(of: currentIcon) ?? 0 + let nextIndex = icons.indexWrapping(after: currentIndex) + currentIcon = icons[nextIndex] + } +} diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index aa62c16f7..15cb3a51e 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -330,6 +330,22 @@ extension Ghostty { case xray case custom case customStyle = "custom-style" + + /// Bundled asset name for built-in icons + var assetName: String? { + switch self { + case .official: return nil + case .blueprint: return "BlueprintImage" + case .chalkboard: return "ChalkboardImage" + case .microchip: return "MicrochipImage" + case .glass: return "GlassImage" + case .holographic: return "HolographicImage" + case .paper: return "PaperImage" + case .retro: return "RetroImage" + case .xray: return "XrayImage" + case .custom, .customStyle: return nil + } + } } /// macos-icon-frame -- 2.51.2 From 55583d9f27ff9165286c45df8db4f2c999583468 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Tue, 13 Jan 2026 10:36:20 -0500 Subject: [PATCH 255/349] fix Devanagari test --- src/font/shaper/coretext.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 223b2382b..cc05022c4 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -776,7 +776,7 @@ pub const Shaper = struct { cell_offset.cluster, cluster, run_offset.cluster, - cluster - cell_offset.cluster, + @as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)), x_offset, position.y, run_offset.x, @@ -820,7 +820,7 @@ pub const Shaper = struct { cell_offset.cluster, cluster, run_offset.cluster, - cluster - cell_offset.cluster, + @as(isize, @intCast(cluster)) - @as(isize, @intCast(cell_offset.cluster)), x_offset, position.y, run_offset.x, @@ -1577,11 +1577,13 @@ test "shape Devanagari string" { try testing.expect(run != null); const cells = try shaper.shape(run.?); + // To understand the `x`/`cluster` assertions here, run with the "For + // debugging positions" code turned on and `extra_debugging` set to true. try testing.expectEqual(@as(usize, 8), cells.len); try testing.expectEqual(@as(u16, 0), cells[0].x); try testing.expectEqual(@as(u16, 1), cells[1].x); try testing.expectEqual(@as(u16, 2), cells[2].x); - try testing.expectEqual(@as(u16, 3), cells[3].x); + try testing.expectEqual(@as(u16, 4), cells[3].x); try testing.expectEqual(@as(u16, 4), cells[4].x); try testing.expectEqual(@as(u16, 5), cells[5].x); try testing.expectEqual(@as(u16, 5), cells[6].x); -- 2.51.2 From 38117f54452072dbc689d0c74dad91270472788b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:15:44 +0000 Subject: [PATCH 256/349] build(deps): bump namespacelabs/nscloud-setup-buildx-action Bumps [namespacelabs/nscloud-setup-buildx-action](https://github.com/namespacelabs/nscloud-setup-buildx-action) from 0.0.20 to 0.0.21. - [Release notes](https://github.com/namespacelabs/nscloud-setup-buildx-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-setup-buildx-action/compare/91c2e6537780e3b092cb8476406be99a8f91bd5e...a7e525416136ee2842da3c800e7067b72a27200e) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-setup-buildx-action dependency-version: 0.0.21 dependency-type: direct:production update-type: version-update:semver-patch ... 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 45127e032..a9adcfbc2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1083,7 +1083,7 @@ jobs: uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 - name: Configure Namespace powered Buildx - uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20 + uses: namespacelabs/nscloud-setup-buildx-action@a7e525416136ee2842da3c800e7067b72a27200e # v0.0.21 - name: Download Source Tarball Artifacts uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 -- 2.51.2 From 2587a2efb44038e2b02aa1c97cfc5d98e65dafe4 Mon Sep 17 00:00:00 2001 From: Mike Kasberg Date: Tue, 21 Oct 2025 17:25:54 -0600 Subject: [PATCH 257/349] feat: Select/Copy Links On Right Click If Present This is a solution for #2107. When a user right-clicks, and there's no existing selection, the existing behavior is to try to select the word under the cursor: https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/Surface.zig#L3740-L3742 This PR tweaks that behavior _slightly_: If there's a link under our cursor, as determined by `linkAtPos`, select the link (to copy with the right-click context menu). Otherwise, select the word as before. As noted in #2107, this matches the behavior of iTerm and Gnome Terminal. It's worth noting that `linkAtPos` already does the right thing in terms of checking the links from config and their highlight/hover states (modified by Ctrl or Super depending on platform). https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/Surface.zig#L3896-L3901 It also therefore respects `link-url` from config. https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/config/Config.zig#L3411-L3416 By using `linkAtPos`, we get all that behavior for free. In practical terms, that means: - If I'm holding Ctrl so a link is underlined and I right click on it, it selects the underlined link. - If I'm not holding Ctrl and I right click on a link that is not underlined, it selects the word as before. - This behavior respects per-platform key bindings and user config settings. `linkAtPos` requires that the render state mutex is held. I believe it's safe to call because we're inside a block holding the mutex: https://github.com/ghostty-org/ghostty/blob/3548acfac63e7674b5e25896f6b393474fe8ea65/src/Surface.zig#L3702-L3704 **AI Disclosure:** I used Gemini CLI to help me with this PR because while I have many years of programming experience, this is my first time writing Zig. I prototyped a couple different approaches with AI before landing on this one, so AI generated various prototypes and I chose the final imlementation. I've verified that my code compiles and works as intended. --- src/Surface.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 4103b91fb..1f50cb681 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4225,8 +4225,8 @@ pub fn mouseButtonCallback( // Get our viewport pin const screen: *terminal.Screen = self.renderer_state.terminal.screens.active; + const pos = try self.rt_surface.getCursorPos(); const pin = pin: { - const pos = try self.rt_surface.getCursorPos(); const pt_viewport = self.posToViewport(pos.x, pos.y); const pin = screen.pages.pin(.{ .viewport = .{ @@ -4257,8 +4257,14 @@ pub fn mouseButtonCallback( // word selection where we clicked. } - const sel = screen.selectWord(pin) orelse break :sel; - try self.setSelection(sel); + // If there is a link at this position, we want to + // select the link. Otherwise, select the word. + if (try self.linkAtPos(pos)) |link| { + try self.setSelection(link.selection); + } else { + const sel = screen.selectWord(pin) orelse break :sel; + try self.setSelection(sel); + } try self.queueRender(); // Don't consume so that we show the context menu in apprt. -- 2.51.2 From 916b99df7c65b79ed8970d12f7bad06c67bc77c4 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 14 Jan 2026 22:51:53 +0800 Subject: [PATCH 258/349] terminal: parse kitty text sizing protocol (OSC 66), redux #9845 redone to use the new OSC parser Implements the VT side of #5563 --- src/inspector/termio.zig | 17 +- src/terminal/osc.zig | 24 +- src/terminal/osc/encoding.zig | 38 +++ src/terminal/osc/parsers.zig | 2 + .../osc/parsers/kitty_text_sizing.zig | 250 ++++++++++++++++++ src/terminal/stream.zig | 1 + 6 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/terminal/osc/encoding.zig create mode 100644 src/terminal/osc/parsers/kitty_text_sizing.zig diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 9f55e6019..934bb6e2d 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -286,18 +286,19 @@ pub const VTEvent = struct { ), else => switch (Value) { - u8, u16 => try md.put( - key, - try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0), - ), - []const u8, [:0]const u8, => try md.put(key, try alloc.dupeZ(u8, value)), - else => |T| { - @compileLog(T); - @compileError("unsupported type, see log"); + else => |T| switch (@typeInfo(T)) { + .int => try md.put( + key, + try std.fmt.allocPrintSentinel(alloc, "{}", .{value}, 0), + ), + else => { + @compileLog(T); + @compileError("unsupported type, see log"); + }, }, }, } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 1f4489961..14d501eaa 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -14,6 +14,8 @@ const Allocator = mem.Allocator; const LibEnum = @import("../lib/enum.zig").Enum; const kitty_color = @import("kitty/color.zig"); const parsers = @import("osc/parsers.zig"); +const encoding = @import("osc/encoding.zig"); + pub const color = parsers.color; const log = std.log.scoped(.osc); @@ -191,6 +193,9 @@ pub const Command = union(Key) { /// ConEmu GUI macro (OSC 9;6) conemu_guimacro: [:0]const u8, + /// Kitty text sizing protocol (OSC 66) + kitty_text_sizing: parsers.kitty_text_sizing.OSC, + pub const Key = LibEnum( if (build_options.c_abi) .c else .zig, // NOTE: Order matters, see LibEnum documentation. @@ -216,6 +221,7 @@ pub const Command = union(Key) { "conemu_progress_report", "conemu_wait_input", "conemu_guimacro", + "kitty_text_sizing", }, ); @@ -342,6 +348,7 @@ pub const Parser = struct { @"2", @"4", @"5", + @"6", @"7", @"8", @"9", @@ -358,6 +365,7 @@ pub const Parser = struct { @"21", @"22", @"52", + @"66", @"77", @"104", @"110", @@ -431,6 +439,7 @@ pub const Parser = struct { .prompt_start, .report_pwd, .show_desktop_notification, + .kitty_text_sizing, => {}, } @@ -510,6 +519,7 @@ pub const Parser = struct { '2' => self.state = .@"2", '4' => self.state = .@"4", '5' => self.state = .@"5", + '6' => self.state = .@"6", '7' => self.state = .@"7", '8' => self.state = .@"8", '9' => self.state = .@"9", @@ -600,7 +610,14 @@ pub const Parser = struct { else => self.state = .invalid, }, - .@"52" => switch (c) { + .@"6" => switch (c) { + '6' => self.state = .@"66", + else => self.state = .invalid, + }, + + .@"52", + .@"66", + => switch (c) { ';' => self.writeToAllocating(), else => self.state = .invalid, }, @@ -685,6 +702,10 @@ pub const Parser = struct { .@"52" => parsers.clipboard_operation.parse(self, terminator_ch), + .@"6" => null, + + .@"66" => parsers.kitty_text_sizing.parse(self, terminator_ch), + .@"77" => null, .@"133" => parsers.semantic_prompt.parse(self, terminator_ch), @@ -696,4 +717,5 @@ pub const Parser = struct { test { _ = parsers; + _ = encoding; } diff --git a/src/terminal/osc/encoding.zig b/src/terminal/osc/encoding.zig new file mode 100644 index 000000000..7491d10c2 --- /dev/null +++ b/src/terminal/osc/encoding.zig @@ -0,0 +1,38 @@ +//! Specialized encodings used in some OSC protocols. +const std = @import("std"); + +/// Kitty defines "Escape code safe UTF-8" as valid UTF-8 with the +/// additional requirement of not containing any C0 escape codes +/// (0x00-0x1f), DEL (0x7f) and C1 escape codes (0x80-0x9f). +/// +/// Used by OSC 66 (text sizing) and OSC 99 (Kitty notifications). +/// +/// See: https://sw.kovidgoyal.net/kitty/desktop-notifications/#safe-utf8 +pub fn isSafeUtf8(s: []const u8) bool { + const utf8 = std.unicode.Utf8View.init(s) catch { + @branchHint(.cold); + return false; + }; + + var it = utf8.iterator(); + while (it.nextCodepoint()) |cp| switch (cp) { + 0x00...0x1f, 0x7f, 0x80...0x9f => { + @branchHint(.cold); + return false; + }, + else => {}, + }; + + return true; +} + +test isSafeUtf8 { + const testing = std.testing; + + try testing.expect(isSafeUtf8("Hello world!")); + try testing.expect(isSafeUtf8("ๅฎ‰ๅ…จ็š„ใƒฆใƒ‹ใ‚ณใƒผใƒ‰โ˜€๏ธ")); + try testing.expect(!isSafeUtf8("No linebreaks\nallowed")); + try testing.expect(!isSafeUtf8("\x07no bells")); + try testing.expect(!isSafeUtf8("\x1b]9;no OSCs\x1b\\\x1b[m")); + try testing.expect(!isSafeUtf8("\x9f8-bit escapes are clever, but no")); +} diff --git a/src/terminal/osc/parsers.zig b/src/terminal/osc/parsers.zig index 152276af2..9c1c39b2c 100644 --- a/src/terminal/osc/parsers.zig +++ b/src/terminal/osc/parsers.zig @@ -6,6 +6,7 @@ pub const clipboard_operation = @import("parsers/clipboard_operation.zig"); pub const color = @import("parsers/color.zig"); pub const hyperlink = @import("parsers/hyperlink.zig"); pub const kitty_color = @import("parsers/kitty_color.zig"); +pub const kitty_text_sizing = @import("parsers/kitty_text_sizing.zig"); pub const mouse_shape = @import("parsers/mouse_shape.zig"); pub const osc9 = @import("parsers/osc9.zig"); pub const report_pwd = @import("parsers/report_pwd.zig"); @@ -19,6 +20,7 @@ test { _ = color; _ = hyperlink; _ = kitty_color; + _ = kitty_text_sizing; _ = mouse_shape; _ = osc9; _ = report_pwd; diff --git a/src/terminal/osc/parsers/kitty_text_sizing.zig b/src/terminal/osc/parsers/kitty_text_sizing.zig new file mode 100644 index 000000000..2c2d1b8fd --- /dev/null +++ b/src/terminal/osc/parsers/kitty_text_sizing.zig @@ -0,0 +1,250 @@ +//! Kitty's text sizing protocol (OSC 66) +//! Specification: https://sw.kovidgoyal.net/kitty/text-sizing-protocol/ + +const std = @import("std"); +const build_options = @import("terminal_options"); + +const assert = @import("../../../quirks.zig").inlineAssert; + +const Parser = @import("../../osc.zig").Parser; +const Command = @import("../../osc.zig").Command; +const encoding = @import("../encoding.zig"); +const lib = @import("../../../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + +const log = std.log.scoped(.kitty_text_sizing); + +pub const max_payload_length = 4096; + +pub const VAlign = lib.Enum(lib_target, &.{ + "top", + "bottom", + "center", +}); + +pub const HAlign = lib.Enum(lib_target, &.{ + "left", + "right", + "center", +}); + +pub const OSC = struct { + scale: u3 = 1, // 1 - 7 + width: u3 = 0, // 0 - 7 (0 means default) + numerator: u4 = 0, + denominator: u4 = 0, + valign: VAlign = .top, + halign: HAlign = .left, + text: [:0]const u8, + + /// We don't currently support encoding this to C in any way. + pub const C = void; + + pub fn cval(_: OSC) C { + return {}; + } + + fn update(self: *OSC, key: u8, value: []const u8) !void { + // All values are numeric, so we can do a small hack here + const v = try std.fmt.parseInt(u4, value, 10); + + switch (key) { + 's' => { + if (v == 0) return error.InvalidValue; + self.scale = std.math.cast(u3, v) orelse return error.Overflow; + }, + 'w' => self.width = std.math.cast(u3, v) orelse return error.Overflow, + 'n' => self.numerator = v, + 'd' => self.denominator = v, + 'v' => self.valign = std.enums.fromInt(VAlign, v) orelse return error.InvalidValue, + 'h' => self.halign = std.enums.fromInt(HAlign, v) orelse return error.InvalidValue, + else => return error.UnknownKey, + } + } +}; + +pub fn parse(parser: *Parser, _: ?u8) ?*Command { + assert(parser.state == .@"66"); + + const writer = parser.writer orelse { + parser.state = .invalid; + return null; + }; + + // Write a NUL byte to ensure that `text` is NUL-terminated + writer.writeByte(0) catch { + parser.state = .invalid; + return null; + }; + const data = writer.buffered(); + + const payload_start = std.mem.indexOfScalar(u8, data, ';') orelse { + log.warn("missing semicolon before payload", .{}); + parser.state = .invalid; + return null; + }; + const payload = data[payload_start + 1 .. data.len - 1 :0]; + + // Payload has to be a URL-safe UTF-8 string, + // and be under the size limit. + if (payload.len > max_payload_length) { + log.warn("payload is too long", .{}); + parser.state = .invalid; + return null; + } + if (!encoding.isSafeUtf8(payload)) { + log.warn("payload is not escape code safe UTF-8", .{}); + parser.state = .invalid; + return null; + } + + parser.command = .{ + .kitty_text_sizing = .{ .text = payload }, + }; + const cmd = &parser.command.kitty_text_sizing; + + // Parse any arguments if given + if (payload_start > 0) { + var kv_it = std.mem.splitScalar( + u8, + data[0..payload_start], + ':', + ); + + while (kv_it.next()) |kv| { + var it = std.mem.splitScalar(u8, kv, '='); + const k = it.next() orelse { + log.warn("missing key", .{}); + continue; + }; + if (k.len != 1) { + log.warn("key must be a single character", .{}); + continue; + } + + const value = it.next() orelse { + log.warn("missing value", .{}); + continue; + }; + + cmd.update(k[0], value) catch |err| { + switch (err) { + error.UnknownKey => log.warn("unknown key: '{c}'", .{k[0]}), + else => log.warn("invalid value for key '{c}': {}", .{ k[0], err }), + } + continue; + }; + } + } + + return &parser.command; +} + +test "OSC 66: empty parameters" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;bobr"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(1, cmd.kitty_text_sizing.scale); + try testing.expectEqualStrings("bobr", cmd.kitty_text_sizing.text); +} + +test "OSC 66: single parameter" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;s=2;kurwa"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(2, cmd.kitty_text_sizing.scale); + try testing.expectEqualStrings("kurwa", cmd.kitty_text_sizing.text); +} + +test "OSC 66: multiple parameters" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;s=2:w=7:n=13:d=15:v=1:h=2;long"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(2, cmd.kitty_text_sizing.scale); + try testing.expectEqual(7, cmd.kitty_text_sizing.width); + try testing.expectEqual(13, cmd.kitty_text_sizing.numerator); + try testing.expectEqual(15, cmd.kitty_text_sizing.denominator); + try testing.expectEqual(.bottom, cmd.kitty_text_sizing.valign); + try testing.expectEqual(.center, cmd.kitty_text_sizing.halign); + try testing.expectEqualStrings("long", cmd.kitty_text_sizing.text); +} + +test "OSC 66: scale is zero" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;s=0;nope"; + for (input) |ch| p.next(ch); + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(1, cmd.kitty_text_sizing.scale); +} + +test "OSC 66: invalid parameters" { + const testing = std.testing; + + var p: Parser = .init(null); + + for ("66;w=8:v=3:n=16;") |ch| p.next(ch); + const cmd = p.end('\x1b').?.*; + + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqual(0, cmd.kitty_text_sizing.width); + try testing.expect(cmd.kitty_text_sizing.valign == .top); + try testing.expectEqual(0, cmd.kitty_text_sizing.numerator); +} + +test "OSC 66: UTF-8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;๐Ÿ‘ป้ญ‘้ญ…้ญ้ญ‰ใ‚ดใƒผใ‚นใƒƒใƒ†ใ‚ฃ"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?.*; + try testing.expect(cmd == .kitty_text_sizing); + try testing.expectEqualStrings("๐Ÿ‘ป้ญ‘้ญ…้ญ้ญ‰ใ‚ดใƒผใ‚นใƒƒใƒ†ใ‚ฃ", cmd.kitty_text_sizing.text); +} + +test "OSC 66: unsafe UTF-8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;\n"; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} + +test "OSC 66: overlong UTF-8" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "66;;" ++ "bobr" ** 1025; + for (input) |ch| p.next(ch); + + try testing.expect(p.end('\x1b') == null); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index eef249327..74a01e8a6 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2107,6 +2107,7 @@ pub fn Stream(comptime Handler: type) type { .conemu_change_tab_title, .conemu_wait_input, .conemu_guimacro, + .kitty_text_sizing, => { log.debug("unimplemented OSC callback: {}", .{cmd}); }, -- 2.51.2 From 78a503491e619ea9c69ee307167bc398f9f4cc28 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Thu, 15 Jan 2026 10:53:00 -0300 Subject: [PATCH 259/349] initial commit --- src/apprt/gtk/class/surface.zig | 33 ++++++++++++++++++++++++++------ src/apprt/gtk/gsettings.zig | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 src/apprt/gtk/gsettings.zig diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index d1eb144e9..8f219677d 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -19,6 +19,7 @@ const terminal = @import("../../../terminal/main.zig"); const CoreSurface = @import("../../../Surface.zig"); const gresource = @import("../build/gresource.zig"); const ext = @import("../ext.zig"); +const gsettings = @import("../gsettings.zig"); const gtk_key = @import("../key.zig"); const ApprtSurface = @import("../Surface.zig"); const Common = @import("../class.zig").Common; @@ -674,6 +675,11 @@ pub const Surface = extern struct { /// The context for this surface (window, tab, or split) context: apprt.surface.NewSurfaceContext = .window, + /// Whether primary paste (middle-click paste) is enabled via GNOME settings. + /// If null, the setting could not be read (non-GNOME system or schema missing). + /// If true, middle-click paste is enabled. If false, it's disabled. + gtk_enable_primary_paste: ?bool = null, + pub var offset: c_int = 0; }; @@ -1511,12 +1517,10 @@ pub const Surface = extern struct { const xft_dpi_scale = xft_scale: { // gtk-xft-dpi is font DPI multiplied by 1024. See // https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html - const settings = gtk.Settings.getDefault() orelse break :xft_scale 1.0; - var value = std.mem.zeroes(gobject.Value); - defer value.unset(); - _ = value.init(gobject.ext.typeFor(c_int)); - settings.as(gobject.Object).getProperty("gtk-xft-dpi", &value); - const gtk_xft_dpi = value.getInt(); + const gtk_xft_dpi = gsettings.readSetting(c_int, "gtk-xft-dpi") orelse { + log.warn("gtk-xft-dpi was not set, using default value", .{}); + break :xft_scale 1.0; + }; // Use a value of 1.0 for the XFT DPI scale if the setting is <= 0 // See: @@ -1767,6 +1771,10 @@ pub const Surface = extern struct { priv.im_composing = false; priv.im_len = 0; + // Read GNOME desktop interface settings for primary paste (middle-click) + // This is only relevant on Linux systems with GNOME settings available + priv.gtk_enable_primary_paste = gsettings.readSetting(bool, "gtk-enable-primary-paste"); + // Set up to handle items being dropped on our surface. Files can be dropped // from Nautilus and strings can be dropped from many programs. The order // of these types matter. @@ -2685,6 +2693,13 @@ pub const Surface = extern struct { // Report the event const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); + + // Check if middle button paste should be disabled based on GNOME settings + // If gtk_enable_primary_paste is explicitly false, skip processing middle button + if (button == .middle and priv.gtk_enable_primary_paste == false) { + return; + } + const consumed = consumed: { const gtk_mods = event.getModifierState(); const mods = gtk_key.translateMods(gtk_mods); @@ -2736,6 +2751,12 @@ pub const Surface = extern struct { const gtk_mods = event.getModifierState(); const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); + // Check if middle button paste should be disabled based on GNOME settings + // If gtk_enable_primary_paste is explicitly false, skip processing middle button + if (button == .middle and priv.gtk_enable_primary_paste == false) { + return; + } + const mods = gtk_key.translateMods(gtk_mods); const consumed = surface.mouseButtonCallback( .release, diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig new file mode 100644 index 000000000..1e9f14a1f --- /dev/null +++ b/src/apprt/gtk/gsettings.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const gtk = @import("gtk"); +const gobject = @import("gobject"); + +/// Reads a GTK setting using the GTK Settings API. +/// This automatically uses XDG Desktop Portal in Flatpak environments. +/// Returns null if not on a GTK-supported platform or if the setting cannot be read. +/// +/// Supported platforms: Linux, FreeBSD +/// Supported types: bool, c_int +/// +/// Example usage: +/// const enabled = readSetting(bool, "gtk-enable-primary-paste"); +/// const dpi = readSetting(c_int, "gtk-xft-dpi"); +pub fn readSetting(comptime T: type, key: [*:0]const u8) ?T { + // Only available on systems that use GTK (Linux, FreeBSD) + if (comptime builtin.os.tag != .linux and builtin.os.tag != .freebsd) return null; + + const settings = gtk.Settings.getDefault() orelse return null; + + // For bool and c_int, we use c_int as the underlying GObject type + // because GTK boolean properties are stored as integers + var value = gobject.ext.Value.new(c_int); + defer value.unset(); + + settings.as(gobject.Object).getProperty(key, &value); + + return switch (T) { + bool => value.getInt() != 0, + c_int => value.getInt(), + else => @compileError("Unsupported type '" ++ @typeName(T) ++ "' for GTK setting. Supported types: bool, c_int"), + }; +} -- 2.51.2 From 7a306e52c2b4ca4bef038e1c9f576ffaea5b52d9 Mon Sep 17 00:00:00 2001 From: Everton Correia <1169768+evertonstz@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:49:23 -0300 Subject: [PATCH 260/349] Update src/apprt/gtk/class/surface.zig Co-authored-by: Leah Amelia Chen --- src/apprt/gtk/class/surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 8f219677d..cb7ac7d5f 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -2753,7 +2753,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and priv.gtk_enable_primary_paste == false) { + if (button == .middle and !priv.gtk_enable_primary_paste) { return; } -- 2.51.2 From 29adcf4b6481d849896d9d0fe9bbcb61dc786e38 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Thu, 15 Jan 2026 17:16:11 -0300 Subject: [PATCH 261/349] Enhance GTK settings handling with well-defined types and utility functions --- src/apprt/gtk/gsettings.zig | 145 +++++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 17 deletions(-) diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig index 1e9f14a1f..2964f3b62 100644 --- a/src/apprt/gtk/gsettings.zig +++ b/src/apprt/gtk/gsettings.zig @@ -3,32 +3,143 @@ const builtin = @import("builtin"); const gtk = @import("gtk"); const gobject = @import("gobject"); -/// Reads a GTK setting using the GTK Settings API. +/// GTK Settings keys with well-defined types. +pub const Key = enum { + gtk_enable_primary_paste, + gtk_xft_dpi, + gtk_font_name, + + fn Type(comptime self: Key) type { + return switch (self) { + .gtk_enable_primary_paste => bool, + .gtk_xft_dpi => c_int, + .gtk_font_name => []const u8, + }; + } + + fn GValueType(comptime self: Key) type { + return switch (self) { + // Booleans are stored as integers in GTK's internal representation + .gtk_enable_primary_paste, + => c_int, + + // Integer types + .gtk_xft_dpi, + => c_int, + + // String types (returned as null-terminated C strings from GTK) + .gtk_font_name, + => ?[*:0]const u8, + }; + } + + fn propertyName(comptime self: Key) [*:0]const u8 { + return switch (self) { + .gtk_enable_primary_paste => "gtk-enable-primary-paste", + .gtk_xft_dpi => "gtk-xft-dpi", + .gtk_font_name => "gtk-font-name", + }; + } + + /// Returns true if this setting type requires memory allocation. + /// this is defensive: types that do not need allocation need to be + /// explicitly marked here + fn requiresAllocation(comptime self: Key) bool { + const T = self.Type(); + return switch (T) { + bool, c_int => false, + else => true, + }; + } +}; + +/// Reads a GTK setting using the GTK Settings API for non-allocating types. /// This automatically uses XDG Desktop Portal in Flatpak environments. -/// Returns null if not on a GTK-supported platform or if the setting cannot be read. /// -/// Supported platforms: Linux, FreeBSD -/// Supported types: bool, c_int +/// No allocator is required or used. Returns null if the setting is not available or cannot be read. /// /// Example usage: -/// const enabled = readSetting(bool, "gtk-enable-primary-paste"); -/// const dpi = readSetting(c_int, "gtk-xft-dpi"); -pub fn readSetting(comptime T: type, key: [*:0]const u8) ?T { - // Only available on systems that use GTK (Linux, FreeBSD) - if (comptime builtin.os.tag != .linux and builtin.os.tag != .freebsd) return null; +/// const enabled = get(.gtk_enable_primary_paste); +/// const dpi = get(.gtk_xft_dpi); +pub fn get(comptime key: Key) ?key.Type() { + comptime { + if (key.requiresAllocation()) { + @compileError("Allocating types require an allocator; use getAlloc() instead"); + } + } + const settings = gtk.Settings.getDefault() orelse return null; + return getImpl(settings, null, key) catch unreachable; +} +/// Reads a GTK setting using the GTK Settings API, allocating if necessary. +/// This automatically uses XDG Desktop Portal in Flatpak environments. +/// +/// The caller must free any returned allocated memory with the provided allocator. +/// Returns null if the setting is not available or cannot be read. +/// May return an allocation error if memory allocation fails. +/// +/// Example usage: +/// const theme = try getAlloc(allocator, .gtk_theme_name); +/// defer if (theme) |t| allocator.free(t); +pub fn getAlloc(allocator: std.mem.Allocator, comptime key: Key) !?key.Type() { const settings = gtk.Settings.getDefault() orelse return null; + return getImpl(settings, allocator, key); +} - // For bool and c_int, we use c_int as the underlying GObject type - // because GTK boolean properties are stored as integers - var value = gobject.ext.Value.new(c_int); +/// Shared implementation for reading GTK settings. +/// If allocator is null, only non-allocating types can be used. +/// Note: When adding a new type, research if it requires allocation (strings and boxed types do) +/// if allocation is NOT needed, list it inside the switch statement in the function requiresAllocation() +fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: Key) !?key.Type() { + const GValType = key.GValueType(); + var value = gobject.ext.Value.new(GValType); defer value.unset(); - settings.as(gobject.Object).getProperty(key, &value); + settings.as(gobject.Object).getProperty(key.propertyName(), &value); + + return switch (key) { + // Booleans are stored as integers in GTK, convert to bool + .gtk_enable_primary_paste, + => value.getInt() != 0, - return switch (T) { - bool => value.getInt() != 0, - c_int => value.getInt(), - else => @compileError("Unsupported type '" ++ @typeName(T) ++ "' for GTK setting. Supported types: bool, c_int"), + // Integer types are returned directly + .gtk_xft_dpi, + => value.getInt(), + + // Strings: GTK owns the GValue's pointer, so we must duplicate it + // before the GValue is destroyed by defer value.unset() + .gtk_font_name, + => blk: { + // This is defensive: we have already checked at compile-time that + // an allocator is provided for allocating types + const alloc = allocator.?; + const ptr = value.getString() orelse break :blk null; + const str = std.mem.span(ptr); + break :blk try alloc.dupe(u8, str); + }, }; } + +test "Key.Type returns correct types" { + try std.testing.expectEqual(bool, Key.gtk_enable_primary_paste.Type()); + try std.testing.expectEqual(c_int, Key.gtk_xft_dpi.Type()); + try std.testing.expectEqual([]const u8, Key.gtk_font_name.Type()); +} + +test "Key.requiresAllocation identifies allocating types" { + try std.testing.expectEqual(false, Key.gtk_enable_primary_paste.requiresAllocation()); + try std.testing.expectEqual(false, Key.gtk_xft_dpi.requiresAllocation()); + try std.testing.expectEqual(true, Key.gtk_font_name.requiresAllocation()); +} + +test "Key.GValueType returns correct GObject types" { + try std.testing.expectEqual(c_int, Key.gtk_enable_primary_paste.GValueType()); + try std.testing.expectEqual(c_int, Key.gtk_xft_dpi.GValueType()); + try std.testing.expectEqual(?[*:0]const u8, Key.gtk_font_name.GValueType()); +} + +test "Key.propertyName returns correct GTK property names" { + try std.testing.expectEqualSlices(u8, "gtk-enable-primary-paste", std.mem.span(Key.gtk_enable_primary_paste.propertyName())); + try std.testing.expectEqualSlices(u8, "gtk-xft-dpi", std.mem.span(Key.gtk_xft_dpi.propertyName())); + try std.testing.expectEqualSlices(u8, "gtk-font-name", std.mem.span(Key.gtk_font_name.propertyName())); +} -- 2.51.2 From db7df92a81adb4c1ad9951afc0009d0dcb5f2cec Mon Sep 17 00:00:00 2001 From: evertonstz Date: Thu, 15 Jan 2026 18:15:31 -0300 Subject: [PATCH 262/349] Refactor gsettings usage for gtk-xft-dpi and gtk-enable-primary-paste with improved logging --- src/apprt/gtk/class/surface.zig | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index cb7ac7d5f..7ba4b9c51 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1517,7 +1517,7 @@ pub const Surface = extern struct { const xft_dpi_scale = xft_scale: { // gtk-xft-dpi is font DPI multiplied by 1024. See // https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html - const gtk_xft_dpi = gsettings.readSetting(c_int, "gtk-xft-dpi") orelse { + const gtk_xft_dpi = gsettings.get(.gtk_xft_dpi) orelse { log.warn("gtk-xft-dpi was not set, using default value", .{}); break :xft_scale 1.0; }; @@ -1527,7 +1527,7 @@ pub const Surface = extern struct { // https://gitlab.gnome.org/GNOME/libadwaita/-/commit/a7738a4d269bfdf4d8d5429ca73ccdd9b2450421 // https://gitlab.gnome.org/GNOME/libadwaita/-/commit/9759d3fd81129608dd78116001928f2aed974ead if (gtk_xft_dpi <= 0) { - log.warn("gtk-xft-dpi was not set, using default value", .{}); + log.warn("gtk-xft-dpi has invalid value ({}), using default", .{gtk_xft_dpi}); break :xft_scale 1.0; } @@ -1773,7 +1773,10 @@ pub const Surface = extern struct { // Read GNOME desktop interface settings for primary paste (middle-click) // This is only relevant on Linux systems with GNOME settings available - priv.gtk_enable_primary_paste = gsettings.readSetting(bool, "gtk-enable-primary-paste"); + priv.gtk_enable_primary_paste = gsettings.get(.gtk_enable_primary_paste) orelse blk: { + log.warn("gtk-enable-primary-paste was not set, using default value", .{}); + break :blk false; + }; // Set up to handle items being dropped on our surface. Files can be dropped // from Nautilus and strings can be dropped from many programs. The order @@ -2696,7 +2699,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and priv.gtk_enable_primary_paste == false) { + if (button == .middle and !(priv.gtk_enable_primary_paste orelse true)) { return; } @@ -2753,7 +2756,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and !priv.gtk_enable_primary_paste) { + if (button == .middle and !(priv.gtk_enable_primary_paste orelse false)) { return; } -- 2.51.2 From 04a7bcd138dcbc69a8b62294b67d813253b98b24 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Thu, 15 Jan 2026 18:20:44 -0300 Subject: [PATCH 263/349] Fix middle button paste condition to respect GNOME settings --- src/apprt/gtk/class/surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 7ba4b9c51..360ce3f88 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -2756,7 +2756,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and !(priv.gtk_enable_primary_paste orelse false)) { + if (button == .middle and !(priv.gtk_enable_primary_paste orelse true)) { return; } -- 2.51.2 From c553296d7a8721e9445b7369771d81de09fa2bc8 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Thu, 15 Jan 2026 19:08:37 -0300 Subject: [PATCH 264/349] Remove unused import of 'builtin' in gsettings.zig --- src/apprt/gtk/gsettings.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig index 2964f3b62..97b671c7a 100644 --- a/src/apprt/gtk/gsettings.zig +++ b/src/apprt/gtk/gsettings.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); const gtk = @import("gtk"); const gobject = @import("gobject"); -- 2.51.2 From 3ca8b97ca7d18efe763af47cd00b212b4f9d6e07 Mon Sep 17 00:00:00 2001 From: Julian Haag Date: Fri, 16 Jan 2026 10:53:50 +0100 Subject: [PATCH 265/349] fix(zsh): strip control characters from window title The zsh shell integration was using `${(V)1}` parameter expansion to set the window title, which converts control characters to their visible escape sequence representations. This caused commands ending with a newline to display as `command\n` in the title bar. Changed to use `${1//[[:cntrl:]]}` which strips control characters entirely, matching the behavior of the bash integration. --- src/shell-integration/zsh/ghostty-integration | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index febf3e59c..3648a80c0 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -204,8 +204,10 @@ _ghostty_deferred_init() { # Enable terminal title changes. functions[_ghostty_precmd]+=" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|โ€ฆ/%3~|%~)}\"\$'\\a'" + # Strip control characters from the command to avoid displaying escape + # sequences like \n in the title. functions[_ghostty_preexec]+=" - builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'" + builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'" fi if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then -- 2.51.2 From ca924f4f4506e72ddaf40d323932750c2ffbfcff Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Fri, 16 Jan 2026 08:53:18 -0500 Subject: [PATCH 266/349] zsh: improve title-related comment --- src/shell-integration/zsh/ghostty-integration | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 3648a80c0..3fb3ec19b 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -201,11 +201,9 @@ _ghostty_deferred_init() { _ghostty_report_pwd if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then - # Enable terminal title changes. + # Enable terminal title changes, formatted for user-friendly display. functions[_ghostty_precmd]+=" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|โ€ฆ/%3~|%~)}\"\$'\\a'" - # Strip control characters from the command to avoid displaying escape - # sequences like \n in the title. functions[_ghostty_preexec]+=" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${1//[[:cntrl:]]}\"\$'\\a'" fi -- 2.51.2 From bf1ca59196d8dec6bd3953fa42eea0d544f02dcf Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Fri, 16 Jan 2026 09:28:19 -0500 Subject: [PATCH 267/349] shellcheck: move common directives to .shellcheckrc This simplifies our CI command line and makes it easier to document expected usage (in HACKING.md). There unfortunately isn't a way to set --checked-sourced or our default warning level in .shellcheckrc, and our `find` command is still a bit unwieldy, but this is still a net improvement. --- .github/workflows/test.yml | 2 -- .shellcheckrc | 8 ++++++++ HACKING.md | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .shellcheckrc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a9adcfbc2..fbac22a47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -978,8 +978,6 @@ jobs: --check-sourced \ --color=always \ --severity=warning \ - --shell=bash \ - --external-sources \ $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) translations: diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 000000000..919cc175d --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,8 @@ +# ShellCheck +# https://github.com/koalaman/shellcheck/wiki/Directive#shellcheckrc-file + +# Allow opening any 'source'd file, even if not specified as input +external-sources=true + +# Assume bash by default +shell=bash diff --git a/HACKING.md b/HACKING.md index bde50ec99..0abb3a2d8 100644 --- a/HACKING.md +++ b/HACKING.md @@ -164,6 +164,28 @@ alejandra . Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). +### ShellCheck + +Bash scripts are checked with [ShellCheck](https://www.shellcheck.net/) in CI. + +Nix users can use the following command to run ShellCheck over all of our scripts: + +``` +nix develop -c shellcheck \ + --check-sourced \ + --severity=warning \ + $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) +``` + +Non-Nix users can [install ShellCheck](https://github.com/koalaman/shellcheck#user-content-installing) and then run: + +``` +shellcheck \ + --check-sourced \ + --severity=warning \ + $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) +``` + ### Updating the Zig Cache Fixed-Output Derivation Hash The Nix package depends on a [fixed-output -- 2.51.2 From 60da5eb3a671bb6889adc6c76b090fdb148c003b Mon Sep 17 00:00:00 2001 From: Everton Correia <1169768+evertonstz@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:25:49 -0300 Subject: [PATCH 268/349] Apply suggestion from @pluiedev Co-authored-by: Leah Amelia Chen --- src/apprt/gtk/gsettings.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig index 97b671c7a..f96e9f01c 100644 --- a/src/apprt/gtk/gsettings.zig +++ b/src/apprt/gtk/gsettings.zig @@ -61,10 +61,8 @@ pub const Key = enum { /// const enabled = get(.gtk_enable_primary_paste); /// const dpi = get(.gtk_xft_dpi); pub fn get(comptime key: Key) ?key.Type() { - comptime { - if (key.requiresAllocation()) { - @compileError("Allocating types require an allocator; use getAlloc() instead"); - } + if (comptime key.requiresAllocation()) { + @compileError("Allocating types require an allocator; use getAlloc() instead"); } const settings = gtk.Settings.getDefault() orelse return null; return getImpl(settings, null, key) catch unreachable; -- 2.51.2 From 1c2c2257d4aff4924afdb2c152106de8d7df89fa Mon Sep 17 00:00:00 2001 From: evertonstz Date: Fri, 16 Jan 2026 12:29:31 -0300 Subject: [PATCH 269/349] Set default value for gtk_enable_primary_paste to true and simplify condition checks --- src/apprt/gtk/class/surface.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 360ce3f88..ea5ca203f 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -676,9 +676,8 @@ pub const Surface = extern struct { context: apprt.surface.NewSurfaceContext = .window, /// Whether primary paste (middle-click paste) is enabled via GNOME settings. - /// If null, the setting could not be read (non-GNOME system or schema missing). /// If true, middle-click paste is enabled. If false, it's disabled. - gtk_enable_primary_paste: ?bool = null, + gtk_enable_primary_paste: ?bool = true, pub var offset: c_int = 0; }; @@ -2699,7 +2698,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and !(priv.gtk_enable_primary_paste orelse true)) { + if (button == .middle and !(priv.gtk_enable_primary_paste)) { return; } @@ -2756,7 +2755,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and !(priv.gtk_enable_primary_paste orelse true)) { + if (button == .middle and !(priv.gtk_enable_primary_paste)) { return; } -- 2.51.2 From f1dbdc796564c0d73c97808c1bfd87ac9d29fc5e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 12:46:34 -0800 Subject: [PATCH 270/349] terminal: fix stale cursor pin usage after cursorChangePin Fixes #10282 The function `cursorChangePin` is supposed to be called anytime the cursor page pin changes, but it itself may alter the page pin if setting up the underlying managed memory forces a page size adjustment. Multiple callers to this function were erroneously reusing the old page pin value. --- src/terminal/Screen.zig | 147 ++++++++++++++++++++++++++++++++-------- 1 file changed, 117 insertions(+), 30 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 2861e02e5..fed0a8c51 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -640,9 +640,8 @@ pub fn cursorUp(self: *Screen, n: size.CellCountInt) void { defer self.assertIntegrity(); self.cursor.y -= n; // Must be set before cursorChangePin - const page_pin = self.cursor.page_pin.up(n).?; - self.cursorChangePin(page_pin); - const page_rac = page_pin.rowAndCell(); + self.cursorChangePin(self.cursor.page_pin.up(n).?); + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; } @@ -667,9 +666,8 @@ pub fn cursorDown(self: *Screen, n: size.CellCountInt) void { // We move the offset into our page list to the next row and then // get the pointers to the row/cell and set all the cursor state up. - const page_pin = self.cursor.page_pin.down(n).?; - self.cursorChangePin(page_pin); - const page_rac = page_pin.rowAndCell(); + self.cursorChangePin(self.cursor.page_pin.down(n).?); + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; } @@ -800,31 +798,37 @@ pub fn cursorDownScroll(self: *Screen) !void { // allocate, prune scrollback, whatever. _ = try self.pages.grow(); - // If our pin page change it means that the page that the pin - // was on was pruned. In this case, grow() moves the pin to - // the top-left of the new page. This effectively moves it by - // one already, we just need to fix up the x value. - const page_pin = if (old_pin.node == self.cursor.page_pin.node) - self.cursor.page_pin.down(1).? - else reuse: { - var pin = self.cursor.page_pin.*; - pin.x = self.cursor.x; - break :reuse pin; - }; + self.cursorChangePin(new_pin: { + // We do this all in a block here because referencing this pin + // after cursorChangePin is unsafe, and we want to keep it out + // of scope. + + // If our pin page change it means that the page that the pin + // was on was pruned. In this case, grow() moves the pin to + // the top-left of the new page. This effectively moves it by + // one already, we just need to fix up the x value. + const page_pin = if (old_pin.node == self.cursor.page_pin.node) + self.cursor.page_pin.down(1).? + else reuse: { + var pin = self.cursor.page_pin.*; + pin.x = self.cursor.x; + break :reuse pin; + }; - // These assertions help catch some pagelist math errors. Our - // x/y should be unchanged after the grow. - if (build_options.slow_runtime_safety) { - const active = self.pages.pointFromPin( - .active, - page_pin, - ).?.active; - assert(active.x == self.cursor.x); - assert(active.y == self.cursor.y); - } + // These assertions help catch some pagelist math errors. Our + // x/y should be unchanged after the grow. + if (build_options.slow_runtime_safety) { + const active = self.pages.pointFromPin( + .active, + page_pin, + ).?.active; + assert(active.x == self.cursor.x); + assert(active.y == self.cursor.y); + } - self.cursorChangePin(page_pin); - const page_rac = page_pin.rowAndCell(); + break :new_pin page_pin; + }); + const page_rac = self.cursor.page_pin.rowAndCell(); self.cursor.page_row = page_rac.row; self.cursor.page_cell = page_rac.cell; @@ -834,7 +838,7 @@ pub fn cursorDownScroll(self: *Screen) !void { // Clear the new row so it gets our bg color. We only do this // if we have a bg color at all. if (self.cursor.style.bg_color != .none) { - const page: *Page = &page_pin.node.data; + const page: *Page = &self.cursor.page_pin.node.data; self.clearCells( page, self.cursor.page_row, @@ -1081,6 +1085,11 @@ 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. +/// +/// Note that this can change the cursor pin AGAIN if the process of +/// setting up our cursor forces a capacity adjustment of the underlying +/// cursor page, so any references to the page pin should be re-read +/// from `self.cursor.page_pin` after calling this. 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 @@ -9016,3 +9025,81 @@ test "Screen: adjustCapacity cursor style exceeds style set capacity" { try testing.expect(s.cursor.style.default()); try testing.expectEqual(style.default_id, s.cursor.style_id); } + +test "Screen: cursorDown to page with insufficient capacity" { + // Regression test for https://github.com/ghostty-org/ghostty/issues/10282 + // + // This test exposes a use-after-realloc bug in cursorDown (and similar + // cursor movement functions). The bug pattern: + // + // 1. cursorDown creates a by-value copy of the pin via page_pin.down(n) + // 2. cursorChangePin is called, which may trigger adjustCapacity + // if the target page's style map is full + // 3. adjustCapacity frees the old page and creates a new one + // 4. The local pin copy still points to the freed page + // 5. rowAndCell() on the stale pin accesses freed memory + + const testing = std.testing; + const alloc = testing.allocator; + + // Small screen to make page boundary crossing easy to set up + var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 }); + defer s.deinit(); + + // Scroll down enough to create a second page + const start_page = &s.pages.pages.last.?.data; + const rem = start_page.capacity.rows; + start_page.pauseIntegrityChecks(true); + for (0..rem) |_| try s.cursorDownOrScroll(); + start_page.pauseIntegrityChecks(false); + + // Cursor should now be on a new page + const new_page = &s.cursor.page_pin.node.data; + try testing.expect(start_page != new_page); + + // Fill new_page's style map to capacity. When we move INTO this page + // with a style set, adjustCapacity will be triggered. + { + new_page.pauseIntegrityChecks(true); + defer new_page.pauseIntegrityChecks(false); + defer new_page.assertIntegrity(); + + var n: u24 = 1; + while (new_page.styles.add( + new_page.memory, + .{ .bg_color = .{ .rgb = @bitCast(n) } }, + )) |_| n += 1 else |_| {} + } + + // Move cursor to start of active area and set a style + s.cursorAbsolute(0, 0); + try s.setAttribute(.bold); + try testing.expect(s.cursor.style.flags.bold); + try testing.expect(s.cursor.style_id != style.default_id); + + // Find the row just before the page boundary + for (0..s.pages.rows - 1) |row| { + s.cursorAbsolute(0, @intCast(row)); + const cur_node = s.cursor.page_pin.node; + if (s.cursor.page_pin.down(1)) |next_pin| { + if (next_pin.node != cur_node) { + // Cursor is at 'row', moving down crosses to new_page + try testing.expect(&next_pin.node.data == new_page); + + // This cursorDown triggers the bug: the local page_pin copy + // becomes stale after adjustCapacity, causing rowAndCell() + // to access freed memory. + s.cursorDown(1); + + // If the fix is applied, verify correct state + try testing.expect(s.cursor.y == row + 1); + try testing.expect(s.cursor.style.flags.bold); + + break; + } + } + } else { + // Didn't find boundary + try testing.expect(false); + } +} -- 2.51.2 From 95a23f756d0160f814f1043b7b2ebe96f0941482 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Jan 2026 13:09:41 -0800 Subject: [PATCH 271/349] terminal: more strict sizing for page capacities, max cap can fit 64-bit --- src/terminal/PageList.zig | 24 ++++++++++-------------- src/terminal/hyperlink.zig | 5 +++-- src/terminal/page.zig | 34 +++++++++++++++++++++++++++++----- src/terminal/size.zig | 27 +++++++++++++++++++++++++-- src/terminal/style.zig | 2 +- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index fc680b971..4592b3db6 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1318,7 +1318,7 @@ const ReflowCursor = struct { // Grow our capacity until we can // definitely fit the extra bytes. const required = cps.len * @sizeOf(u21); - var new_grapheme_capacity: usize = cap.grapheme_bytes; + var new_grapheme_capacity: size.GraphemeBytesInt = cap.grapheme_bytes; while (new_grapheme_capacity - cap.grapheme_bytes < required) { new_grapheme_capacity *= 2; } @@ -1362,7 +1362,7 @@ const ReflowCursor = struct { } else |_| { // Grow our capacity until we can // definitely fit the extra bytes. - var new_string_capacity: usize = cap.string_bytes; + var new_string_capacity: size.StringBytesInt = cap.string_bytes; while (new_string_capacity - cap.string_bytes < additional_required_string_capacity) { new_string_capacity *= 2; } @@ -2647,16 +2647,16 @@ pub const AdjustCapacity = struct { /// Adjust the number of styles in the page. This may be /// rounded up if necessary to fit alignment requirements, /// but it will never be rounded down. - styles: ?usize = null, + styles: ?size.StyleCountInt = null, /// Adjust the number of available grapheme bytes in the page. - grapheme_bytes: ?usize = null, + grapheme_bytes: ?size.GraphemeBytesInt = null, /// Adjust the number of available hyperlink bytes in the page. - hyperlink_bytes: ?usize = null, + hyperlink_bytes: ?size.HyperlinkCountInt = null, /// Adjust the number of available string bytes in the page. - string_bytes: ?usize = null, + string_bytes: ?size.StringBytesInt = null, }; pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError; @@ -2692,23 +2692,19 @@ pub fn adjustCapacity( // All ceilPowerOfTwo is unreachable because we're always same or less // bit width so maxInt is always possible. if (adjustment.styles) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; + const aligned = std.math.ceilPowerOfTwo(size.StyleCountInt, v) catch unreachable; cap.styles = @max(cap.styles, aligned); } if (adjustment.grapheme_bytes) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; + const aligned = std.math.ceilPowerOfTwo(size.GraphemeBytesInt, v) catch unreachable; cap.grapheme_bytes = @max(cap.grapheme_bytes, aligned); } if (adjustment.hyperlink_bytes) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; + const aligned = std.math.ceilPowerOfTwo(size.HyperlinkCountInt, v) catch unreachable; cap.hyperlink_bytes = @max(cap.hyperlink_bytes, aligned); } if (adjustment.string_bytes) |v| { - comptime assert(@bitSizeOf(@TypeOf(v)) <= @bitSizeOf(usize)); - const aligned = std.math.ceilPowerOfTwo(usize, v) catch unreachable; + const aligned = std.math.ceilPowerOfTwo(size.StringBytesInt, v) catch unreachable; cap.string_bytes = @max(cap.string_bytes, aligned); } diff --git a/src/terminal/hyperlink.zig b/src/terminal/hyperlink.zig index 975e6f30e..94f86466c 100644 --- a/src/terminal/hyperlink.zig +++ b/src/terminal/hyperlink.zig @@ -13,8 +13,9 @@ const autoHash = std.hash.autoHash; const autoHashStrat = std.hash.autoHashStrat; /// The unique identifier for a hyperlink. This is at most the number of cells -/// that can fit in a single terminal page. -pub const Id = size.CellCountInt; +/// that can fit in a single terminal page, since each cell can only contain +/// at most one hyperlink. +pub const Id = size.HyperlinkCountInt; // The mapping of cell to hyperlink. We use an offset hash map to save space // since its very unlikely a cell is a hyperlink, so its a waste to store diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 848123405..559f536f1 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1569,7 +1569,10 @@ pub const Page = struct { 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_count = std.math.ceilPowerOfTwo( + usize, + @divFloor(cap.grapheme_bytes, grapheme_chunk), + ) catch unreachable; const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count)); 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; @@ -1639,25 +1642,33 @@ pub const Size = struct { }; /// Capacity of this page. +/// +/// This capacity can be maxed out (every field max) and still fit +/// within a 64-bit memory space. If you need more than this, you will +/// need to split data across separate pages. +/// +/// For 32-bit systems, it is possible to overflow the addressable +/// space and this is something we still need to address in the future +/// likely by limiting the maximum capacity on 32-bit systems further. pub const Capacity = struct { /// Number of columns and rows we can know about. cols: size.CellCountInt, rows: size.CellCountInt, /// Number of unique styles that can be used on this page. - styles: usize = 16, + styles: size.StyleCountInt = 16, /// Number of bytes to allocate for hyperlink data. Note that the /// amount of data used for hyperlinks in total is more than this because /// hyperlinks use string data as well as a small amount of lookup metadata. /// This number is a rough approximation. - hyperlink_bytes: usize = hyperlink_bytes_default, + hyperlink_bytes: size.HyperlinkCountInt = hyperlink_bytes_default, /// Number of bytes to allocate for grapheme data. - grapheme_bytes: usize = grapheme_bytes_default, + grapheme_bytes: size.GraphemeBytesInt = grapheme_bytes_default, /// Number of bytes to allocate for strings. - string_bytes: usize = string_bytes_default, + string_bytes: size.StringBytesInt = string_bytes_default, pub const Adjustment = struct { cols: ?size.CellCountInt = null, @@ -2025,6 +2036,19 @@ pub const Cell = packed struct(u64) { // //const pages = total_size / std.heap.page_size_min; // } +test "Page.layout can take a maxed capacity" { + // Our intention is for a maxed-out capacity to always fit + // within a page layout without trigering runtime safety on any + // overflow. This simplifies some of our handling downstream of the + // call (relevant to: https://github.com/ghostty-org/ghostty/issues/10258) + var cap: Capacity = undefined; + inline for (@typeInfo(Capacity).@"struct".fields) |field| { + @field(cap, field.name) = std.math.maxInt(field.type); + } + + _ = Page.layout(cap); +} + test "Cell is zero by default" { const cell = Cell.init(0); const cell_int: u64 = @bitCast(cell); diff --git a/src/terminal/size.zig b/src/terminal/size.zig index 0dedfcc14..7be09739e 100644 --- a/src/terminal/size.zig +++ b/src/terminal/size.zig @@ -11,9 +11,32 @@ pub const max_page_size = std.math.maxInt(u32); /// derived from the maximum terminal page size. pub const OffsetInt = std.math.IntFittingRange(0, max_page_size - 1); -/// The int type that can contain the maximum number of cells in a page. -pub const CellCountInt = u16; // TODO: derive +/// Int types for maximum values of things. A lot of these sizes are +/// based on "X is enough for any reasonable use case" principles. +// The goal is that a user can have the maxInt amount of all of these +// present at one time and be able to address them in a single Page.zig. + +// Total number of cells that are possible in each dimension (row/col). +// Based on 2^16 being enough for any reasonable terminal size and allowing +// IDs to remain 16-bit. +pub const CellCountInt = u16; + +// Total number of styles and hyperlinks that are possible in a page. +// We match CellCountInt here because each cell in a single row can have at +// most one style, making it simple to split a page by splitting rows. // +// Note due to the way RefCountedSet works, we are short one value, but +// this is a theoretical limit we accept. A page with a single row max +// columns wide would be one short of having every cell have a unique style. +pub const StyleCountInt = CellCountInt; +pub const HyperlinkCountInt = CellCountInt; + +// Total number of bytes that can be taken up by grapheme data and string +// data. Both of these technically unlimited with malicious input, but +// we choose a reasonable limit of 2^32 (4GB) per. +pub const GraphemeBytesInt = u32; +pub const StringBytesInt = u32; + /// The offset from the base address of the page to the start of some data. /// This is typed for ease of use. /// diff --git a/src/terminal/style.zig b/src/terminal/style.zig index e5c47b9fe..7908beefa 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -11,7 +11,7 @@ const RefCountedSet = @import("ref_counted_set.zig").RefCountedSet; /// The unique identifier for a style. This is at most the number of cells /// that can fit into a terminal page. -pub const Id = size.CellCountInt; +pub const Id = size.StyleCountInt; /// The Id to use for default styling. pub const default_id: Id = 0; -- 2.51.2 From 8306f96d9460a440579d002bfcea254fac5889ab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Jan 2026 13:41:11 -0800 Subject: [PATCH 272/349] terminal: PageList.increaseCapacity --- src/terminal/PageList.zig | 399 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 4592b3db6..9c63fd578 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2736,6 +2736,110 @@ pub fn adjustCapacity( return new_node; } +/// Possible dimensions to increase capacity for. +pub const IncreaseCapacity = enum { + styles, + grapheme_bytes, + hyperlink_bytes, + string_bytes, +}; + +pub const IncreaseCapacityError = error{ + // An actual system OOM trying to allocate memory. + OutOfMemory, + + // The existing page is already at max capacity for the given + // adjustment. The caller must create a new page, remove data from + // the old page, etc. (up to the caller). + OutOfSpace, +}; + +/// Increase the capacity of the given page node in the given direction. +/// This will always allocate a new node and remove the old node, so the +/// existing node pointer will be invalid after this call. The newly created +/// node on success is returned. +/// +/// The increase amount is at the control of the PageList implementation, +/// but is guaranteed to always increase by at least one unit in the +/// given dimension. Practically, we'll always increase by much more +/// (we currently double every time) but callers shouldn't depend on that. +/// The only guarantee is some amount of growth. +/// +/// Adjustment can be null if you want to recreate, reclone the page +/// with the same capacity. This is a special case used for rehashing since +/// the logic is otherwise the same. In this case, OutOfMemory is the +/// only possible error. +pub fn increaseCapacity( + self: *PageList, + node: *List.Node, + adjustment: ?IncreaseCapacity, +) IncreaseCapacityError!*List.Node { + defer self.assertIntegrity(); + const page: *Page = &node.data; + + // Apply our adjustment + var cap = page.capacity; + if (adjustment) |v| switch (v) { + inline else => |tag| { + const field_name = @tagName(tag); + const Int = @FieldType(Capacity, field_name); + const old = @field(cap, field_name); + + // We use checked math to prevent overflow. If there is an + // overflow it means we're out of space in this dimension, + // since pages can take up to their maxInt capacity in any + // category. + const new = std.math.mul(Int, old, 2) catch |err| { + comptime assert(@TypeOf(err) == error{Overflow}); + return error.OutOfSpace; + }; + @field(cap, field_name) = new; + }, + }; + + log.info("adjusting page capacity={}", .{cap}); + + // Create our new page and clone the old page into it. + const new_node = try self.createPage(cap); + errdefer self.destroyNode(new_node); + const new_page: *Page = &new_node.data; + assert(new_page.capacity.rows >= page.capacity.rows); + assert(new_page.capacity.cols >= page.capacity.cols); + new_page.size.rows = page.size.rows; + new_page.size.cols = page.size.cols; + new_page.cloneFrom( + page, + 0, + page.size.rows, + ) catch |err| { + // cloneFrom only errors if there isn't capacity for the data + // from the source page but we're only increasing capacity so + // this should never be possible. If it happens, we should crash + // because we're in no man's land and can't safely recover. + log.err("increaseCapacity clone failed err={}", .{err}); + @panic("unexpected clone failure"); + }; + + // Must not fail after this because the operations we do after this + // can't be recovered. + errdefer comptime unreachable; + + // Fix up all our tracked pins to point to the new page. + const pin_keys = self.tracked_pins.keys(); + for (pin_keys) |p| { + if (p.node != node) continue; + p.node = new_node; + } + + // Insert this page and destroy the old page + self.pages.insertBefore(node, new_node); + self.pages.remove(node); + self.destroyNode(node); + + new_page.assertIntegrity(); + return new_node; +} + /// 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. inline fn createPage( @@ -6482,6 +6586,301 @@ test "PageList adjustCapacity after col shrink" { } } +test "PageList increaseCapacity to increase styles" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + const original_styles_cap = s.pages.first.?.data.capacity.styles; + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Write all our data so we can assert its the same after + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x) }, + }; + } + } + } + + // Increase our styles + _ = try s.increaseCapacity(s.pages.first.?, .styles); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // Verify capacity doubled + try testing.expectEqual( + original_styles_cap * 2, + page.capacity.styles, + ); + + // Verify data preserved + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x)), + rac.cell.content.codepoint, + ); + } + } + } +} + +test "PageList increaseCapacity to increase graphemes" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + const original_cap = s.pages.first.?.data.capacity.grapheme_bytes; + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x) }, + }; + } + } + } + + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + try testing.expectEqual(original_cap * 2, page.capacity.grapheme_bytes); + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x)), + rac.cell.content.codepoint, + ); + } + } + } +} + +test "PageList increaseCapacity to increase hyperlinks" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + const original_cap = s.pages.first.?.data.capacity.hyperlink_bytes; + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x) }, + }; + } + } + } + + _ = try s.increaseCapacity(s.pages.first.?, .hyperlink_bytes); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + try testing.expectEqual(original_cap * 2, page.capacity.hyperlink_bytes); + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x)), + rac.cell.content.codepoint, + ); + } + } + } +} + +test "PageList increaseCapacity to increase string_bytes" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + const original_cap = s.pages.first.?.data.capacity.string_bytes; + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x) }, + }; + } + } + } + + _ = try s.increaseCapacity(s.pages.first.?, .string_bytes); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + try testing.expectEqual(original_cap * 2, page.capacity.string_bytes); + + for (0..s.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x)), + rac.cell.content.codepoint, + ); + } + } + } +} + +test "PageList increaseCapacity tracked pins" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + // Create a tracked pin on the first page + const tracked = try s.trackPin(s.pin(.{ .active = .{ .x = 1, .y = 1 } }).?); + defer s.untrackPin(tracked); + + const old_node = s.pages.first.?; + try testing.expectEqual(old_node, tracked.node); + + // Increase capacity + const new_node = try s.increaseCapacity(s.pages.first.?, .styles); + + // Pin should now point to the new node + try testing.expectEqual(new_node, tracked.node); + try testing.expectEqual(@as(size.CellCountInt, 1), tracked.x); + try testing.expectEqual(@as(size.CellCountInt, 1), tracked.y); +} + +test "PageList increaseCapacity returns OutOfSpace at max capacity" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 2, 0); + defer s.deinit(); + + // Set styles capacity to a value that will overflow when doubled + // We need to create a page with capacity at more than half of max + const max_styles = std.math.maxInt(size.StyleCountInt); + const half_max = max_styles / 2 + 1; + + // Adjust capacity to near-max first + _ = try s.adjustCapacity(s.pages.first.?, .{ .styles = half_max }); + + // Now increaseCapacity should fail with OutOfSpace + try testing.expectError( + error.OutOfSpace, + s.increaseCapacity(s.pages.first.?, .styles), + ); +} + +test "PageList increaseCapacity after col shrink" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 2, 0); + defer s.deinit(); + + // Shrink columns + try s.resize(.{ .cols = 5, .reflow = false }); + try testing.expectEqual(5, s.cols); + + { + const page = &s.pages.first.?.data; + try testing.expectEqual(5, page.size.cols); + try testing.expect(page.capacity.cols >= 10); + } + + // Increase capacity + _ = try s.increaseCapacity(s.pages.first.?, .styles); + + { + const page = &s.pages.first.?.data; + // size.cols should still be 5, not reverted to capacity.cols + try testing.expectEqual(5, page.size.cols); + try testing.expectEqual(5, s.cols); + } +} + +test "PageList increaseCapacity multi-page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow to create a second page + const page1_node = s.pages.last.?; + page1_node.data.pauseIntegrityChecks(true); + for (0..page1_node.data.capacity.rows - page1_node.data.size.rows) |_| { + try testing.expect(try s.grow() == null); + } + page1_node.data.pauseIntegrityChecks(false); + try testing.expect(try s.grow() != null); + + // Now we have two pages + try testing.expect(s.pages.first != s.pages.last); + const page2_node = s.pages.last.?; + + const page1_styles_cap = s.pages.first.?.data.capacity.styles; + const page2_styles_cap = page2_node.data.capacity.styles; + + // Increase capacity on the first page only + _ = try s.increaseCapacity(s.pages.first.?, .styles); + + // First page capacity should be doubled + try testing.expectEqual( + page1_styles_cap * 2, + s.pages.first.?.data.capacity.styles, + ); + + // Second page should be unchanged + try testing.expectEqual( + page2_styles_cap, + s.pages.last.?.data.capacity.styles, + ); +} + test "PageList pageIterator single page" { const testing = std.testing; const alloc = testing.allocator; -- 2.51.2 From 1e5973386b5eb262526b4990102500e556f6f2ea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Jan 2026 13:41:11 -0800 Subject: [PATCH 273/349] terminal: Screen.increaseCapacity --- src/terminal/Screen.zig | 312 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 3 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index fed0a8c51..8edb72ba5 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -588,6 +588,76 @@ pub fn adjustCapacity( return new_node; } +pub fn increaseCapacity( + self: *Screen, + node: *PageList.List.Node, + adjustment: ?PageList.IncreaseCapacity, +) PageList.IncreaseCapacityError!*PageList.List.Node { + // If the page being modified isn't our cursor page then + // this is a quick operation because we have no additional + // accounting. We have to do this check here BEFORE calling + // increaseCapacity because increaseCapacity will update all + // our tracked pins (including our cursor). + if (node != self.cursor.page_pin.node) return try self.pages.increaseCapacity( + node, + adjustment, + ); + + // We're modifying the cursor page. When we increase the + // capacity below it will be short the ref count on our + // current style and hyperlink, so we need to init those. + const new_node = try self.pages.increaseCapacity(node, adjustment); + const new_page: *Page = &new_node.data; + + // Re-add the style, if the page somehow doesn't have enough + // memory to add it, we emit a warning and gracefully degrade + // to the default style for the cursor. + if (self.cursor.style_id != style.default_id) { + self.cursor.style_id = new_page.styles.add( + new_page.memory, + self.cursor.style, + ) catch |err| id: { + // TODO: Should we increase the capacity further in this case? + log.warn( + "(Screen.increaseCapacity) Failed to add cursor style back to page, err={}", + .{err}, + ); + + // Reset the cursor style. + self.cursor.style = .{}; + break :id style.default_id; + }; + } + + // Re-add the hyperlink, if the page somehow doesn't have enough + // memory to add it, we emit a warning and gracefully degrade to + // no hyperlink. + if (self.cursor.hyperlink) |link| { + // So we don't attempt to free any memory in the replaced page. + self.cursor.hyperlink_id = 0; + self.cursor.hyperlink = null; + + // Re-add + self.startHyperlinkOnce(link.*) catch |err| { + // TODO: Should we increase the capacity further in this case? + log.warn( + "(Screen.increaseCapacity) Failed to add cursor hyperlink back to page, err={}", + .{err}, + ); + }; + + // Remove our old link + link.deinit(self.alloc); + self.alloc.destroy(link); + } + + // Reload the cursor information because the pin changed. + // So our page row/cell and so on are all off. + self.cursorReload(); + + return new_node; +} + 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); @@ -3007,15 +3077,15 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { .protected = self.cursor.protected, }; - // If we have a hyperlink, add it to the cell. - if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink(); - // If we have a ref-counted style, increase. if (self.cursor.style_id != style.default_id) { const page = self.cursor.page_pin.node.data; page.styles.use(page.memory, self.cursor.style_id); self.cursor.page_row.styled = true; } + + // If we have a hyperlink, add it to the cell. + if (self.cursor.hyperlink_id > 0) try self.cursorSetHyperlink(); }, 2 => { @@ -9103,3 +9173,239 @@ test "Screen: cursorDown to page with insufficient capacity" { try testing.expect(false); } } + +test "Screen: increaseCapacity cursor style ref count preserved" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 5, + .rows = 5, + .max_scrollback = 0, + }); + defer s.deinit(); + try s.setAttribute(.bold); + try s.testWriteString("1ABCD"); + + // We should have one page and it should be our cursor page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.pages.pages.first == s.cursor.page_pin.node); + + const old_style = s.cursor.style; + + { + const page = &s.pages.pages.last.?.data; + // 5 chars + cursor = 6 refs + try testing.expectEqual( + 6, + page.styles.refCount(page.memory, s.cursor.style_id), + ); + } + + // This forces the page to change via increaseCapacity. + const new_node = try s.increaseCapacity( + s.cursor.page_pin.node, + .grapheme_bytes, + ); + + // Cursor's page_pin should now point to the new node + try testing.expect(s.cursor.page_pin.node == new_node); + + // Verify cursor's page_cell and page_row are correctly reloaded from the pin + const page_rac = s.cursor.page_pin.rowAndCell(); + try testing.expect(s.cursor.page_row == page_rac.row); + try testing.expect(s.cursor.page_cell == page_rac.cell); + + // Style should be preserved + try testing.expectEqual(old_style, s.cursor.style); + try testing.expect(s.cursor.style_id != style.default_id); + + // After increaseCapacity, the 5 chars are cloned (5 refs) and + // the cursor's style is re-added (1 ref) = 6 total. + { + const page = &s.pages.pages.last.?.data; + const ref_count = page.styles.refCount(page.memory, s.cursor.style_id); + try testing.expectEqual(6, ref_count); + } +} + +test "Screen: increaseCapacity cursor hyperlink ref count preserved" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 5, + .rows = 5, + .max_scrollback = 0, + }); + defer s.deinit(); + try s.startHyperlink("https://example.com/", null); + try s.testWriteString("1ABCD"); + + // We should have one page and it should be our cursor page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.pages.pages.first == s.cursor.page_pin.node); + + { + const page = &s.pages.pages.last.?.data; + // Cursor has the hyperlink active = 1 count in hyperlink_set + try testing.expectEqual(1, page.hyperlink_set.count()); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expect(s.cursor.hyperlink != null); + } + + // This forces the page to change via increaseCapacity. + _ = try s.increaseCapacity( + s.cursor.page_pin.node, + .grapheme_bytes, + ); + + // Hyperlink should be preserved with correct URI + try testing.expect(s.cursor.hyperlink != null); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); + + // After increaseCapacity, the hyperlink is re-added to the new page. + { + const page = &s.pages.pages.last.?.data; + try testing.expectEqual(1, page.hyperlink_set.count()); + } +} + +test "Screen: increaseCapacity cursor with both style and hyperlink preserved" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 5, + .rows = 5, + .max_scrollback = 0, + }); + defer s.deinit(); + + // Set both a non-default style AND an active hyperlink. + // Write one character first with bold to mark the row as styled, + // then start the hyperlink and write more characters. + try s.setAttribute(.bold); + try s.startHyperlink("https://example.com/", null); + try s.testWriteString("1ABCD"); + + // We should have one page and it should be our cursor page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.pages.pages.first == s.cursor.page_pin.node); + + const old_style = s.cursor.style; + + { + const page = &s.pages.pages.last.?.data; + // 5 chars + cursor = 6 refs for bold style + try testing.expectEqual( + 6, + page.styles.refCount(page.memory, s.cursor.style_id), + ); + // Cursor has the hyperlink active = 1 count in hyperlink_set + try testing.expectEqual(1, page.hyperlink_set.count()); + try testing.expect(s.cursor.style_id != style.default_id); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expect(s.cursor.hyperlink != null); + } + + // This forces the page to change via increaseCapacity. + _ = try s.increaseCapacity( + s.cursor.page_pin.node, + .grapheme_bytes, + ); + + // Style should be preserved + try testing.expectEqual(old_style, s.cursor.style); + try testing.expect(s.cursor.style_id != style.default_id); + + // Hyperlink should be preserved with correct URI + try testing.expect(s.cursor.hyperlink != null); + try testing.expect(s.cursor.hyperlink_id != 0); + try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); + + // After increaseCapacity, both style and hyperlink are re-added to the new page. + { + const page = &s.pages.pages.last.?.data; + const ref_count = page.styles.refCount(page.memory, s.cursor.style_id); + try testing.expectEqual(6, ref_count); + try testing.expectEqual(1, page.hyperlink_set.count()); + } +} + +test "Screen: increaseCapacity non-cursor page returns early" { + // Test that calling increaseCapacity on a page that is NOT the cursor's + // page properly delegates to pages.increaseCapacity without doing the + // extra cursor accounting (style/hyperlink re-adding). + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 80, + .rows = 24, + .max_scrollback = 10000, + }); + defer s.deinit(); + + // Set up a custom style and hyperlink on the cursor + try s.setAttribute(.bold); + try s.startHyperlink("https://example.com/", null); + try s.testWriteString("Hello"); + + // Store cursor state before growing pages + const old_style = s.cursor.style; + const old_style_id = s.cursor.style_id; + const old_hyperlink = s.cursor.hyperlink; + const old_hyperlink_id = s.cursor.hyperlink_id; + + // The cursor is on the first (and only) page + try testing.expect(s.pages.pages.first == s.pages.pages.last); + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); + + // Grow pages until we have multiple pages. The cursor's pin stays on + // the first page since we're just adding rows. + const first_page_node = s.pages.pages.first.?; + first_page_node.data.pauseIntegrityChecks(true); + for (0..first_page_node.data.capacity.rows - first_page_node.data.size.rows) |_| { + _ = try s.pages.grow(); + } + first_page_node.data.pauseIntegrityChecks(false); + _ = try s.pages.grow(); + + // Now we have two pages + try testing.expect(s.pages.pages.first != s.pages.pages.last); + const second_page = s.pages.pages.last.?; + + // Cursor should still be on the first page (where it was created) + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); + try testing.expect(s.cursor.page_pin.node != second_page); + + const second_page_styles_cap = second_page.data.capacity.styles; + const cursor_page_styles_cap = s.cursor.page_pin.node.data.capacity.styles; + + // Call increaseCapacity on the second page (NOT the cursor's page) + const new_second_page = try s.increaseCapacity(second_page, .styles); + + // The second page should have increased capacity + try testing.expectEqual( + second_page_styles_cap * 2, + new_second_page.data.capacity.styles, + ); + + // The cursor's page (first page) should be unchanged + try testing.expectEqual( + cursor_page_styles_cap, + s.cursor.page_pin.node.data.capacity.styles, + ); + + // Cursor state should be completely unchanged since we didn't touch its page + try testing.expectEqual(old_style, s.cursor.style); + try testing.expectEqual(old_style_id, s.cursor.style_id); + try testing.expectEqual(old_hyperlink, s.cursor.hyperlink); + try testing.expectEqual(old_hyperlink_id, s.cursor.hyperlink_id); + + // Verify hyperlink is still valid + try testing.expect(s.cursor.hyperlink != null); + try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); +} -- 2.51.2 From 29d4aba03337d7e9d6a0c357969e8924240b84dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 09:07:44 -0800 Subject: [PATCH 274/349] terminal: Screen replace adjust with increaseCapacity --- src/terminal/Screen.zig | 101 ++++++++++++++++++++++++++++------------ src/terminal/page.zig | 2 +- 2 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8edb72ba5..66dc37006 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1176,14 +1176,19 @@ inline fn cursorChangePin(self: *Screen, new: Pin) void { return; } - // If we have a old style then we need to release it from the old page. + // If we have an old style then we need to release it from the old page. const old_style_: ?style.Style = if (self.cursor.style_id == style.default_id) null else self.cursor.style; if (old_style_ != null) { + // Release the style directly from the old page instead of going through + // manualStyleUpdate, because the cursor position may have already been + // updated but the pin has not, which would fail integrity checks. + const old_page: *Page = &self.cursor.page_pin.node.data; + old_page.styles.release(old_page.memory, self.cursor.style_id); self.cursor.style = .{}; - self.manualStyleUpdate() catch unreachable; // Removing a style should never fail + self.cursor.style_id = style.default_id; } // If we have a hyperlink then we need to release it from the old page. @@ -2000,7 +2005,17 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { } /// Call this whenever you manually change the cursor style. -pub fn manualStyleUpdate(self: *Screen) !void { +/// +/// Note that this can return any PageList capacity error, because it +/// is possible for the internal pagelist to not accommodate the new style +/// at all. This WILL attempt to resize our internal pages to fit the style +/// but it is possible that it cannot be done, in which case upstream callers +/// need to split the page or do something else. +/// +/// NOTE(mitchellh): I think in the future we'll do page splitting +/// automatically here and remove this failure scenario. +pub fn manualStyleUpdate(self: *Screen) PageList.IncreaseCapacityError!void { + defer self.assertIntegrity(); var page: *Page = &self.cursor.page_pin.node.data; // std.log.warn("active styles={}", .{page.styles.count()}); @@ -2019,6 +2034,9 @@ pub fn manualStyleUpdate(self: *Screen) !void { // Clear the cursor style ID to prevent weird things from happening // if the page capacity has to be adjusted which would end up calling // manualStyleUpdate again. + // + // This also ensures that if anything fails below, we fall back to + // clearing our style. self.cursor.style_id = style.default_id; // After setting the style, we need to update our style map. @@ -2030,30 +2048,50 @@ pub fn manualStyleUpdate(self: *Screen) !void { page.memory, self.cursor.style, ) catch |err| id: { - // Our style map is full or needs to be rehashed, - // so we allocate a new page, which will rehash, - // and double the style capacity for it if it was - // full. - const node = try self.adjustCapacity( + // Our style map is full or needs to be rehashed, so we need to + // increase style capacity (or rehash). + const node = try self.increaseCapacity( self.cursor.page_pin.node, switch (err) { - error.OutOfMemory => .{ .styles = page.capacity.styles * 2 }, - error.NeedsRehash => .{}, + error.OutOfMemory => .styles, + error.NeedsRehash => null, }, ); page = &node.data; - break :id try page.styles.add( + break :id page.styles.add( page.memory, self.cursor.style, - ); + ) catch |err2| switch (err2) { + error.OutOfMemory => { + // This shouldn't happen because increaseCapacity is + // guaranteed to increase our capacity by at least one and + // we only need one space, but again, I don't want to crash + // here so let's log loudly and reset. + log.err("style addition failed after capacity increase", .{}); + return error.OutOfMemory; + }, + error.NeedsRehash => { + // This should be impossible because we rehash above + // and rehashing should never result in a duplicate. But + // we don't want to simply hard crash so log it and + // clear our style. + log.err("style rehash resulted in needs rehash", .{}); + return; + }, + }; }; + errdefer page.styles.release(page.memory, id); + self.cursor.style_id = id; - self.assertIntegrity(); } /// Append a grapheme to the given cell within the current cursor row. -pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { +pub fn appendGrapheme( + self: *Screen, + cell: *Cell, + cp: u21, +) PageList.IncreaseCapacityError!void { defer self.cursor.page_pin.node.data.assertIntegrity(); self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, @@ -2073,11 +2111,9 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { // Adjust our capacity. This will update our cursor page pin and // force us to reload. - const original_node = self.cursor.page_pin.node; - const new_bytes = original_node.data.capacity.grapheme_bytes * 2; - _ = try self.adjustCapacity( - original_node, - .{ .grapheme_bytes = new_bytes }, + _ = try self.increaseCapacity( + self.cursor.page_pin.node, + .grapheme_bytes, ); // The cell pointer is now invalid, so we need to get it from @@ -2088,17 +2124,22 @@ pub fn appendGrapheme(self: *Screen, cell: *Cell, cp: u21) !void { .gt => self.cursorCellRight(@intCast(cell_idx - self.cursor.x)), }; - try self.cursor.page_pin.node.data.appendGrapheme( + self.cursor.page_pin.node.data.appendGrapheme( self.cursor.page_row, reloaded_cell, cp, - ); + ) catch |err2| { + comptime assert(@TypeOf(err2) == error{OutOfMemory}); + // This should never happen because we just increased capacity. + // Log loudly but still return an error so we don't just + // crash. + log.err("grapheme append failed after capacity increase", .{}); + return err2; + }; }, }; } -pub const StartHyperlinkError = Allocator.Error || PageList.AdjustCapacityError; - /// Start the hyperlink state. Future cells will be marked as hyperlinks with /// this state. Note that various terminal operations may clear the hyperlink /// state, such as switching screens (alt screen). @@ -2106,7 +2147,7 @@ pub fn startHyperlink( self: *Screen, uri: []const u8, id_: ?[]const u8, -) StartHyperlinkError!void { +) PageList.IncreaseCapacityError!void { // Create our pending entry. const link: hyperlink.Hyperlink = .{ .uri = uri, @@ -2131,21 +2172,21 @@ pub fn startHyperlink( error.OutOfMemory => return error.OutOfMemory, // strings table is out of memory, adjust it up - error.StringsOutOfMemory => _ = try self.adjustCapacity( + error.StringsOutOfMemory => _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{ .string_bytes = self.cursor.page_pin.node.data.capacity.string_bytes * 2 }, + .string_bytes, ), // hyperlink set is out of memory, adjust it up - error.SetOutOfMemory => _ = try self.adjustCapacity( + error.SetOutOfMemory => _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{ .hyperlink_bytes = self.cursor.page_pin.node.data.capacity.hyperlink_bytes * 2 }, + .hyperlink_bytes, ), // hyperlink set is too full, rehash it - error.SetNeedsRehash => _ = try self.adjustCapacity( + error.SetNeedsRehash => _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{}, + null, ), } diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 559f536f1..1150027a4 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -2038,7 +2038,7 @@ pub const Cell = packed struct(u64) { test "Page.layout can take a maxed capacity" { // Our intention is for a maxed-out capacity to always fit - // within a page layout without trigering runtime safety on any + // within a page layout without triggering runtime safety on any // overflow. This simplifies some of our handling downstream of the // call (relevant to: https://github.com/ghostty-org/ghostty/issues/10258) var cap: Capacity = undefined; -- 2.51.2 From 25b7cc9f2cc28071d9d07f3a96ab86c811f1d1e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 10:56:32 -0800 Subject: [PATCH 275/349] terminal: hyperlink state uses increaseCapacity on screen --- src/terminal/Screen.zig | 48 ++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 66dc37006..f524bd7f4 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2248,7 +2248,7 @@ pub fn endHyperlink(self: *Screen) void { } /// Set the current hyperlink state on the current cell. -pub fn cursorSetHyperlink(self: *Screen) !void { +pub fn cursorSetHyperlink(self: *Screen) PageList.IncreaseCapacityError!void { assert(self.cursor.hyperlink_id != 0); var page = &self.cursor.page_pin.node.data; @@ -2263,10 +2263,6 @@ pub fn cursorSetHyperlink(self: *Screen) !void { } else |err| switch (err) { // hyperlink_map is out of space, realloc the page to be larger error.HyperlinkMapOutOfMemory => { - const uri_size = if (self.cursor.hyperlink) |link| link.uri.len else 0; - - var string_bytes = page.capacity.string_bytes; - // Attempt to allocate the space that would be required to // insert a new copy of the cursor hyperlink uri in to the // string alloc, since right now adjustCapacity always just @@ -2274,29 +2270,31 @@ pub fn cursorSetHyperlink(self: *Screen) !void { // If this alloc fails then we know we also need to grow our // string bytes. // - // FIXME: This SUCKS - if (page.string_alloc.alloc( - u8, - page.memory, - uri_size, - )) |slice| { - // We don't bother freeing because we're - // about to free the entire page anyway. - _ = &slice; - } else |_| { - // We didn't have enough room, let's just double our - // string bytes until there's definitely enough room - // for our uri. - const before = string_bytes; - while (string_bytes - before < uri_size) string_bytes *= 2; + // FIXME: increaseCapacity should not do this. + while (self.cursor.hyperlink) |link| { + if (page.string_alloc.alloc( + u8, + page.memory, + link.uri.len, + )) |slice| { + // We don't bother freeing because we're + // about to free the entire page anyway. + _ = slice; + break; + } else |_| {} + + // We didn't have enough room, let's increase string bytes + const new_node = try self.increaseCapacity( + self.cursor.page_pin.node, + .string_bytes, + ); + assert(new_node == self.cursor.page_pin.node); + page = &new_node.data; } - _ = try self.adjustCapacity( + _ = try self.increaseCapacity( self.cursor.page_pin.node, - .{ - .hyperlink_bytes = page.capacity.hyperlink_bytes * 2, - .string_bytes = string_bytes, - }, + .hyperlink_bytes, ); // Retry -- 2.51.2 From c8afc423082ef764c9f5e62b5269cc5dc2fcff22 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 11:37:58 -0800 Subject: [PATCH 276/349] terminal: switch to increaseCapacity --- src/terminal/Terminal.zig | 72 ++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d717a9724..310adaf29 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1634,54 +1634,48 @@ pub fn insertLines(self: *Terminal, count: usize) void { self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { - const cap = dst_p.node.data.capacity; // Adjust our page capacity to make // room for we didn't have space for - _ = self.screens.active.adjustCapacity( + _ = self.screens.active.increaseCapacity( dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, error.HyperlinkSetNeedsRehash, - => .{}, + => null, // Increase style memory error.StyleSetOutOfMemory, - => .{ .styles = cap.styles * 2 }, + => .styles, // Increase string memory error.StringAllocOutOfMemory, - => .{ .string_bytes = cap.string_bytes * 2 }, + => .string_bytes, // Increase hyperlink memory error.HyperlinkSetOutOfMemory, error.HyperlinkMapOutOfMemory, - => .{ .hyperlink_bytes = cap.hyperlink_bytes * 2 }, + => .hyperlink_bytes, // Increase grapheme memory error.GraphemeMapOutOfMemory, error.GraphemeAllocOutOfMemory, - => .{ .grapheme_bytes = cap.grapheme_bytes * 2 }, + => .grapheme_bytes, }, ) catch |e| switch (e) { - // This shouldn't be possible because above we're only - // adjusting capacity _upwards_. So it should have all - // the existing capacity it had to fit the adjusted - // data. Panic since we don't expect this. - error.StyleSetOutOfMemory, - error.StyleSetNeedsRehash, - error.StringAllocOutOfMemory, - error.HyperlinkSetOutOfMemory, - error.HyperlinkSetNeedsRehash, - error.HyperlinkMapOutOfMemory, - error.GraphemeMapOutOfMemory, - error.GraphemeAllocOutOfMemory, - => @panic("adjustCapacity resulted in capacity errors"), - - // The system allocator is OOM. We can't currently do - // anything graceful here. We panic. + // System OOM. We have no way to recover from this + // currently. We should probably change insertLines + // to raise an error here. error.OutOfMemory, - => @panic("adjustCapacity system allocator OOM"), + => @panic("increaseCapacity system allocator OOM"), + + // The page can't accomodate the managed memory required + // for this operation. We previously just corrupted + // memory here so a crash is better. The right long + // term solution is to allocate a new page here + // move this row to the new page, and start over. + error.OutOfSpace, + => @panic("increaseCapacity OutOfSpace"), }; // Continue the loop to try handling this row again. @@ -1834,49 +1828,41 @@ pub fn deleteLines(self: *Terminal, count: usize) void { self.scrolling_region.left, self.scrolling_region.right + 1, ) catch |err| { - const cap = dst_p.node.data.capacity; // Adjust our page capacity to make // room for we didn't have space for - _ = self.screens.active.adjustCapacity( + _ = self.screens.active.increaseCapacity( dst_p.node, switch (err) { // Rehash the sets error.StyleSetNeedsRehash, error.HyperlinkSetNeedsRehash, - => .{}, + => null, // Increase style memory error.StyleSetOutOfMemory, - => .{ .styles = cap.styles * 2 }, + => .styles, // Increase string memory error.StringAllocOutOfMemory, - => .{ .string_bytes = cap.string_bytes * 2 }, + => .string_bytes, // Increase hyperlink memory error.HyperlinkSetOutOfMemory, error.HyperlinkMapOutOfMemory, - => .{ .hyperlink_bytes = cap.hyperlink_bytes * 2 }, + => .hyperlink_bytes, // Increase grapheme memory error.GraphemeMapOutOfMemory, error.GraphemeAllocOutOfMemory, - => .{ .grapheme_bytes = cap.grapheme_bytes * 2 }, + => .grapheme_bytes, }, ) catch |e| switch (e) { - // See insertLines which has the same error capture. - error.StyleSetOutOfMemory, - error.StyleSetNeedsRehash, - error.StringAllocOutOfMemory, - error.HyperlinkSetOutOfMemory, - error.HyperlinkSetNeedsRehash, - error.HyperlinkMapOutOfMemory, - error.GraphemeMapOutOfMemory, - error.GraphemeAllocOutOfMemory, - => @panic("adjustCapacity resulted in capacity errors"), - + // See insertLines error.OutOfMemory, - => @panic("adjustCapacity system allocator OOM"), + => @panic("increaseCapacity system allocator OOM"), + + error.OutOfSpace, + => @panic("increaseCapacity OutOfSpace"), }; // Continue the loop to try handling this row again. -- 2.51.2 From b59ac60a8772ae1e259966efc0eb9feb5c4167cb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 11:40:39 -0800 Subject: [PATCH 277/349] terminal: remove Screen.adjustCapacity --- src/terminal/Screen.zig | 359 +++++++++----------------------------- src/terminal/Terminal.zig | 2 +- 2 files changed, 80 insertions(+), 281 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index f524bd7f4..1538da5da 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -517,77 +517,6 @@ pub fn clone( result.assertIntegrity(); return result; } - -/// Adjust the capacity of a page within the pagelist of this screen. -/// This handles some accounting if the page being modified is the -/// cursor page. -pub fn adjustCapacity( - self: *Screen, - node: *PageList.List.Node, - adjustment: PageList.AdjustCapacity, -) PageList.AdjustCapacityError!*PageList.List.Node { - // If the page being modified isn't our cursor page then - // this is a quick operation because we have no additional - // accounting. - if (node != self.cursor.page_pin.node) { - return try self.pages.adjustCapacity(node, adjustment); - } - - // We're modifying the cursor page. When we adjust the - // capacity below it will be short the ref count on our - // current style and hyperlink, so we need to init those. - const new_node = try self.pages.adjustCapacity(node, adjustment); - const new_page: *Page = &new_node.data; - - // Re-add the style, if the page somehow doesn't have enough - // memory to add it, we emit a warning and gracefully degrade - // to the default style for the cursor. - if (self.cursor.style_id != 0) { - self.cursor.style_id = new_page.styles.add( - new_page.memory, - self.cursor.style, - ) catch |err| id: { - // TODO: Should we increase the capacity further in this case? - log.warn( - "(Screen.adjustCapacity) Failed to add cursor style back to page, err={}", - .{err}, - ); - - // Reset the cursor style. - self.cursor.style = .{}; - break :id style.default_id; - }; - } - - // Re-add the hyperlink, if the page somehow doesn't have enough - // memory to add it, we emit a warning and gracefully degrade to - // no hyperlink. - if (self.cursor.hyperlink) |link| { - // So we don't attempt to free any memory in the replaced page. - self.cursor.hyperlink_id = 0; - self.cursor.hyperlink = null; - - // Re-add - self.startHyperlinkOnce(link.*) catch |err| { - // TODO: Should we increase the capacity further in this case? - log.warn( - "(Screen.adjustCapacity) Failed to add cursor hyperlink back to page, err={}", - .{err}, - ); - }; - - // Remove our old link - link.deinit(self.alloc); - self.alloc.destroy(link); - } - - // Reload the cursor information because the pin changed. - // So our page row/cell and so on are all off. - self.cursorReload(); - - return new_node; -} - pub fn increaseCapacity( self: *Screen, node: *PageList.List.Node, @@ -2265,7 +2194,7 @@ pub fn cursorSetHyperlink(self: *Screen) PageList.IncreaseCapacityError!void { error.HyperlinkMapOutOfMemory => { // Attempt to allocate the space that would be required to // insert a new copy of the cursor hyperlink uri in to the - // string alloc, since right now adjustCapacity always just + // string alloc, since right now increaseCapacity always just // adds an extra copy even if one already exists in the page. // If this alloc fails then we know we also need to grow our // string bytes. @@ -9005,214 +8934,6 @@ test "Screen: cursorSetHyperlink OOM + URI too large for string alloc" { try testing.expect(base_string_bytes < s.cursor.page_pin.node.data.capacity.string_bytes); } -test "Screen: adjustCapacity cursor style ref count" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, .{ .cols = 5, .rows = 5, .max_scrollback = 0 }); - defer s.deinit(); - - try s.setAttribute(.{ .bold = {} }); - try s.testWriteString("1ABCD"); - - { - const page = &s.pages.pages.last.?.data; - try testing.expectEqual( - 6, // All chars + cursor - page.styles.refCount(page.memory, s.cursor.style_id), - ); - } - - // This forces the page to change. - _ = try s.adjustCapacity( - s.cursor.page_pin.node, - .{ .grapheme_bytes = s.cursor.page_pin.node.data.capacity.grapheme_bytes * 2 }, - ); - - // Our ref counts should still be the same - { - const page = &s.pages.pages.last.?.data; - try testing.expectEqual( - 6, // All chars + cursor - page.styles.refCount(page.memory, s.cursor.style_id), - ); - } -} - -test "Screen: adjustCapacity cursor hyperlink exceeds string alloc size" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 0 }); - defer s.deinit(); - - // Start a hyperlink with a URI that just barely fits in the string alloc. - // This will ensure that the redundant copy added in `adjustCapacity` won't - // fit in the available string alloc space. - const uri = "a" ** (pagepkg.std_capacity.string_bytes - 8); - try s.startHyperlink(uri, null); - - // Write some characters with this so that the URI - // is copied to the new page when adjusting capacity. - try s.testWriteString("Hello"); - - // Adjust the capacity, right now this will cause a redundant copy of - // the URI to be added to the string alloc, but since there isn't room - // for this this will clear the cursor hyperlink. - _ = try s.adjustCapacity(s.cursor.page_pin.node, .{}); - - // The cursor hyperlink should have been cleared by the `adjustCapacity` - // call, because there isn't enough room to add the redundant URI string. - // - // This behavior will change, causing this test to fail, if any of these - // changes are made: - // - // - The string alloc is changed to intern strings. - // - // - The adjustCapacity function is changed to ensure the new - // capacity will fit the redundant copy of the hyperlink uri. - // - // - The cursor managed memory handling is reworked so that it - // doesn't reside in the pages anymore and doesn't need this - // accounting. - // - // In such a case, adjust this test accordingly. - try testing.expectEqual(null, s.cursor.hyperlink); - try testing.expectEqual(0, s.cursor.hyperlink_id); -} - -test "Screen: adjustCapacity cursor style exceeds style set capacity" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, .{ .cols = 80, .rows = 24, .max_scrollback = 1000 }); - defer s.deinit(); - - const page = &s.cursor.page_pin.node.data; - - // We add unique styles to the page until no more will fit. - fill: for (0..255) |bg| { - for (0..255) |fg| { - const st: style.Style = .{ - .bg_color = .{ .palette = @intCast(bg) }, - .fg_color = .{ .palette = @intCast(fg) }, - }; - - s.cursor.style = st; - - // Try to insert the new style, if it doesn't fit then - // we succeeded in filling the style set, so we break. - s.cursor.style_id = page.styles.add( - page.memory, - s.cursor.style, - ) catch break :fill; - - try s.testWriteString("a"); - } - } - - // Adjust the capacity, this should cause the style set to reach the - // same state it was in to begin with, since it will clone the page - // in the same order as the styles were added to begin with, meaning - // the cursor style will not be able to be added to the set, which - // should, right now, result in the cursor style being cleared. - _ = try s.adjustCapacity(s.cursor.page_pin.node, .{}); - - // The cursor style should have been cleared by the `adjustCapacity`. - // - // This behavior will change, causing this test to fail, if either - // of these changes are made: - // - // - The adjustCapacity function is changed to ensure the - // new capacity will definitely fit the cursor style. - // - // - The cursor managed memory handling is reworked so that it - // doesn't reside in the pages anymore and doesn't need this - // accounting. - // - // In such a case, adjust this test accordingly. - try testing.expect(s.cursor.style.default()); - try testing.expectEqual(style.default_id, s.cursor.style_id); -} - -test "Screen: cursorDown to page with insufficient capacity" { - // Regression test for https://github.com/ghostty-org/ghostty/issues/10282 - // - // This test exposes a use-after-realloc bug in cursorDown (and similar - // cursor movement functions). The bug pattern: - // - // 1. cursorDown creates a by-value copy of the pin via page_pin.down(n) - // 2. cursorChangePin is called, which may trigger adjustCapacity - // if the target page's style map is full - // 3. adjustCapacity frees the old page and creates a new one - // 4. The local pin copy still points to the freed page - // 5. rowAndCell() on the stale pin accesses freed memory - - const testing = std.testing; - const alloc = testing.allocator; - - // Small screen to make page boundary crossing easy to set up - var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 }); - defer s.deinit(); - - // Scroll down enough to create a second page - const start_page = &s.pages.pages.last.?.data; - const rem = start_page.capacity.rows; - start_page.pauseIntegrityChecks(true); - for (0..rem) |_| try s.cursorDownOrScroll(); - start_page.pauseIntegrityChecks(false); - - // Cursor should now be on a new page - const new_page = &s.cursor.page_pin.node.data; - try testing.expect(start_page != new_page); - - // Fill new_page's style map to capacity. When we move INTO this page - // with a style set, adjustCapacity will be triggered. - { - new_page.pauseIntegrityChecks(true); - defer new_page.pauseIntegrityChecks(false); - defer new_page.assertIntegrity(); - - var n: u24 = 1; - while (new_page.styles.add( - new_page.memory, - .{ .bg_color = .{ .rgb = @bitCast(n) } }, - )) |_| n += 1 else |_| {} - } - - // Move cursor to start of active area and set a style - s.cursorAbsolute(0, 0); - try s.setAttribute(.bold); - try testing.expect(s.cursor.style.flags.bold); - try testing.expect(s.cursor.style_id != style.default_id); - - // Find the row just before the page boundary - for (0..s.pages.rows - 1) |row| { - s.cursorAbsolute(0, @intCast(row)); - const cur_node = s.cursor.page_pin.node; - if (s.cursor.page_pin.down(1)) |next_pin| { - if (next_pin.node != cur_node) { - // Cursor is at 'row', moving down crosses to new_page - try testing.expect(&next_pin.node.data == new_page); - - // This cursorDown triggers the bug: the local page_pin copy - // becomes stale after adjustCapacity, causing rowAndCell() - // to access freed memory. - s.cursorDown(1); - - // If the fix is applied, verify correct state - try testing.expect(s.cursor.y == row + 1); - try testing.expect(s.cursor.style.flags.bold); - - break; - } - } - } else { - // Didn't find boundary - try testing.expect(false); - } -} - test "Screen: increaseCapacity cursor style ref count preserved" { const testing = std.testing; const alloc = testing.allocator; @@ -9448,3 +9169,81 @@ test "Screen: increaseCapacity non-cursor page returns early" { try testing.expect(s.cursor.hyperlink != null); try testing.expectEqualStrings("https://example.com/", s.cursor.hyperlink.?.uri); } + +test "Screen: cursorDown to page with insufficient capacity" { + // Regression test for https://github.com/ghostty-org/ghostty/issues/10282 + // + // This test exposes a use-after-realloc bug in cursorDown (and similar + // cursor movement functions). The bug pattern: + // + // 1. cursorDown creates a by-value copy of the pin via page_pin.down(n) + // 2. cursorChangePin is called, which may trigger adjustCapacity + // if the target page's style map is full + // 3. adjustCapacity frees the old page and creates a new one + // 4. The local pin copy still points to the freed page + // 5. rowAndCell() on the stale pin accesses freed memory + + const testing = std.testing; + const alloc = testing.allocator; + + // Small screen to make page boundary crossing easy to set up + var s = try init(alloc, .{ .cols = 10, .rows = 3, .max_scrollback = 1 }); + defer s.deinit(); + + // Scroll down enough to create a second page + const start_page = &s.pages.pages.last.?.data; + const rem = start_page.capacity.rows; + start_page.pauseIntegrityChecks(true); + for (0..rem) |_| try s.cursorDownOrScroll(); + start_page.pauseIntegrityChecks(false); + + // Cursor should now be on a new page + const new_page = &s.cursor.page_pin.node.data; + try testing.expect(start_page != new_page); + + // Fill new_page's style map to capacity. When we move INTO this page + // with a style set, adjustCapacity will be triggered. + { + new_page.pauseIntegrityChecks(true); + defer new_page.pauseIntegrityChecks(false); + defer new_page.assertIntegrity(); + + var n: u24 = 1; + while (new_page.styles.add( + new_page.memory, + .{ .bg_color = .{ .rgb = @bitCast(n) } }, + )) |_| n += 1 else |_| {} + } + + // Move cursor to start of active area and set a style + s.cursorAbsolute(0, 0); + try s.setAttribute(.bold); + try testing.expect(s.cursor.style.flags.bold); + try testing.expect(s.cursor.style_id != style.default_id); + + // Find the row just before the page boundary + for (0..s.pages.rows - 1) |row| { + s.cursorAbsolute(0, @intCast(row)); + const cur_node = s.cursor.page_pin.node; + if (s.cursor.page_pin.down(1)) |next_pin| { + if (next_pin.node != cur_node) { + // Cursor is at 'row', moving down crosses to new_page + try testing.expect(&next_pin.node.data == new_page); + + // This cursorDown triggers the bug: the local page_pin copy + // becomes stale after adjustCapacity, causing rowAndCell() + // to access freed memory. + s.cursorDown(1); + + // If the fix is applied, verify correct state + try testing.expect(s.cursor.y == row + 1); + try testing.expect(s.cursor.style.flags.bold); + + break; + } + } + } else { + // Didn't find boundary + try testing.expect(false); + } +} diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 310adaf29..3740397d3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1669,7 +1669,7 @@ pub fn insertLines(self: *Terminal, count: usize) void { error.OutOfMemory, => @panic("increaseCapacity system allocator OOM"), - // The page can't accomodate the managed memory required + // The page can't accommodate the managed memory required // for this operation. We previously just corrupted // memory here so a crash is better. The right long // term solution is to allocate a new page here -- 2.51.2 From e70452588790f32864400709e38d6eed83966cb1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 13:08:13 -0800 Subject: [PATCH 278/349] terminal: PageList remove adjustCapacity --- src/terminal/PageList.zig | 396 ++++++-------------------------------- 1 file changed, 63 insertions(+), 333 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 9c63fd578..bf2071497 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1300,31 +1300,30 @@ const ReflowCursor = struct { // If our page can't support an additional cell // with graphemes then we increase capacity. if (self.page.graphemeCount() >= self.page.graphemeCapacity()) { - try self.adjustCapacity(list, .{ - .grapheme_bytes = cap.grapheme_bytes * 2, - }); + try self.increaseCapacity( + list, + .grapheme_bytes, + ); } // Attempt to allocate the space that would be required // for these graphemes, and if it's not available, then - // increase capacity. - if (self.page.grapheme_alloc.alloc( - u21, - self.page.memory, - cps.len, - )) |slice| { - self.page.grapheme_alloc.free(self.page.memory, slice); - } else |_| { - // Grow our capacity until we can - // definitely fit the extra bytes. - const required = cps.len * @sizeOf(u21); - var new_grapheme_capacity: size.GraphemeBytesInt = cap.grapheme_bytes; - while (new_grapheme_capacity - cap.grapheme_bytes < required) { - new_grapheme_capacity *= 2; + // increase capacity. Keep trying until we succeed. + while (true) { + if (self.page.grapheme_alloc.alloc( + u21, + self.page.memory, + cps.len, + )) |slice| { + self.page.grapheme_alloc.free( + self.page.memory, + slice, + ); + break; + } else |_| { + // Grow our capacity until we can fit the extra bytes. + try self.increaseCapacity(list, .grapheme_bytes); } - try self.adjustCapacity(list, .{ - .grapheme_bytes = new_grapheme_capacity, - }); } // This shouldn't fail since we made sure we have space above. @@ -1339,9 +1338,7 @@ const ReflowCursor = struct { // If our page can't support an additional cell // with a hyperlink then we increase capacity. if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) { - try self.adjustCapacity(list, .{ - .hyperlink_bytes = cap.hyperlink_bytes * 2, - }); + try self.increaseCapacity(list, .hyperlink_bytes); } // Ensure that the string alloc has sufficient capacity @@ -1352,23 +1349,26 @@ const ReflowCursor = struct { .explicit => |v| v.len, .implicit => 0, }; - if (self.page.string_alloc.alloc( - u8, - self.page.memory, - additional_required_string_capacity, - )) |slice| { - // We have enough capacity, free the test alloc. - self.page.string_alloc.free(self.page.memory, slice); - } else |_| { - // Grow our capacity until we can - // definitely fit the extra bytes. - var new_string_capacity: size.StringBytesInt = cap.string_bytes; - while (new_string_capacity - cap.string_bytes < additional_required_string_capacity) { - new_string_capacity *= 2; + // Keep trying until we have enough capacity. + while (true) { + if (self.page.string_alloc.alloc( + u8, + self.page.memory, + additional_required_string_capacity, + )) |slice| { + // We have enough capacity, free the test alloc. + self.page.string_alloc.free( + self.page.memory, + slice, + ); + break; + } else |_| { + // Grow our capacity until we can fit the extra bytes. + try self.increaseCapacity( + list, + .string_bytes, + ); } - try self.adjustCapacity(list, .{ - .string_bytes = new_string_capacity, - }); } const dst_id = self.page.hyperlink_set.addWithIdContext( @@ -1380,18 +1380,16 @@ const ReflowCursor = struct { ) catch |err| id: { // If the add failed then either the set needs to grow // or it needs to be rehashed. Either one of those can - // be accomplished by adjusting capacity, either with + // be accomplished by increasing capacity, either with // no actual change or with an increased hyperlink cap. - try self.adjustCapacity(list, switch (err) { - error.OutOfMemory => .{ - .hyperlink_bytes = cap.hyperlink_bytes * 2, - }, - error.NeedsRehash => .{}, + try self.increaseCapacity(list, switch (err) { + error.OutOfMemory => .hyperlink_bytes, + error.NeedsRehash => null, }); // We assume this one will succeed. We dupe the link // again, and don't have to worry about the other one - // because adjusting the capacity naturally clears up + // because increasing the capacity naturally clears up // any managed memory not associated with a cell yet. break :id try self.page.hyperlink_set.addWithIdContext( self.page.memory, @@ -1424,13 +1422,11 @@ const ReflowCursor = struct { ) catch |err| id: { // If the add failed then either the set needs to grow // or it needs to be rehashed. Either one of those can - // be accomplished by adjusting capacity, either with + // be accomplished by increasing capacity, either with // no actual change or with an increased style cap. - try self.adjustCapacity(list, switch (err) { - error.OutOfMemory => .{ - .styles = cap.styles * 2, - }, - error.NeedsRehash => .{}, + try self.increaseCapacity(list, switch (err) { + error.OutOfMemory => .styles, + error.NeedsRehash => null, }); // We assume this one will succeed. @@ -1507,11 +1503,11 @@ const ReflowCursor = struct { } } - /// Adjust the capacity of the current page. - fn adjustCapacity( + /// Increase the capacity of the current page. + fn increaseCapacity( self: *ReflowCursor, list: *PageList, - adjustment: AdjustCapacity, + adjustment: ?IncreaseCapacity, ) !void { const old_x = self.x; const old_y = self.y; @@ -1522,7 +1518,7 @@ const ReflowCursor = struct { // be correct during a reflow. list.pauseIntegrityChecks(true); defer list.pauseIntegrityChecks(false); - break :node try list.adjustCapacity( + break :node try list.increaseCapacity( self.node, adjustment, ); @@ -2642,100 +2638,6 @@ pub fn grow(self: *PageList) !?*List.Node { return next_node; } -/// Adjust the capacity of the given page in the list. -pub const AdjustCapacity = struct { - /// Adjust the number of styles in the page. This may be - /// rounded up if necessary to fit alignment requirements, - /// but it will never be rounded down. - styles: ?size.StyleCountInt = null, - - /// Adjust the number of available grapheme bytes in the page. - grapheme_bytes: ?size.GraphemeBytesInt = null, - - /// Adjust the number of available hyperlink bytes in the page. - hyperlink_bytes: ?size.HyperlinkCountInt = null, - - /// Adjust the number of available string bytes in the page. - string_bytes: ?size.StringBytesInt = null, -}; - -pub const AdjustCapacityError = Allocator.Error || Page.CloneFromError; - -/// 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. -/// -/// Adjustment works by increasing the capacity of the desired -/// dimension to a certain amount and increases the memory allocation -/// requirement for the backing memory of the page. We currently -/// never split pages or anything like that. Because increased allocation -/// has to happen outside our memory pool, its generally much slower -/// so pages should be sized to be large enough to handle all but -/// exceptional cases. -/// -/// This can currently only INCREASE capacity size. It cannot -/// decrease capacity size. This limitation is only because we haven't -/// yet needed that use case. If we ever do, this can be added. Currently -/// any requests to decrease will be ignored. -pub fn adjustCapacity( - self: *PageList, - 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 - // ensures we never shrink from what we need. - var cap = page.capacity; - - // All ceilPowerOfTwo is unreachable because we're always same or less - // bit width so maxInt is always possible. - if (adjustment.styles) |v| { - const aligned = std.math.ceilPowerOfTwo(size.StyleCountInt, v) catch unreachable; - cap.styles = @max(cap.styles, aligned); - } - if (adjustment.grapheme_bytes) |v| { - const aligned = std.math.ceilPowerOfTwo(size.GraphemeBytesInt, v) catch unreachable; - cap.grapheme_bytes = @max(cap.grapheme_bytes, aligned); - } - if (adjustment.hyperlink_bytes) |v| { - const aligned = std.math.ceilPowerOfTwo(size.HyperlinkCountInt, v) catch unreachable; - cap.hyperlink_bytes = @max(cap.hyperlink_bytes, aligned); - } - if (adjustment.string_bytes) |v| { - const aligned = std.math.ceilPowerOfTwo(size.StringBytesInt, v) catch unreachable; - cap.string_bytes = @max(cap.string_bytes, aligned); - } - - log.info("adjusting page capacity={}", .{cap}); - - // Create our new page and clone the old page into it. - const new_node = try self.createPage(cap); - errdefer self.destroyNode(new_node); - const new_page: *Page = &new_node.data; - assert(new_page.capacity.rows >= page.capacity.rows); - assert(new_page.capacity.cols >= page.capacity.cols); - new_page.size.rows = page.size.rows; - new_page.size.cols = page.size.cols; - try new_page.cloneFrom(page, 0, page.size.rows); - - // Fix up all our tracked pins to point to the new page. - const pin_keys = self.tracked_pins.keys(); - for (pin_keys) |p| { - if (p.node != node) continue; - p.node = new_node; - } - - // Insert this page and destroy the old page - self.pages.insertBefore(node, new_node); - self.pages.remove(node); - self.destroyNode(node); - - new_page.assertIntegrity(); - return new_node; -} - /// Possible dimensions to increase capacity for. pub const IncreaseCapacity = enum { styles, @@ -4991,21 +4893,14 @@ test "PageList grow prune required with a single page" { // behavior during a refactor. This is setting up a scenario that is // possible to trigger a bug (#2280). { - // Adjust our capacity until our page is larger than the standard size. + // Increase our capacity until our page is larger than the standard size. // This is important because it triggers a scenario where our calculated // minSize() which is supposed to accommodate 2 pages is no longer true. - var cap = std_capacity; while (true) { - cap.grapheme_bytes *= 2; - const layout = Page.layout(cap); + const layout = Page.layout(s.pages.first.?.data.capacity); if (layout.total_size > std_size) break; + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); } - - // Adjust to that capacity. After we should still have one page. - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .grapheme_bytes = cap.grapheme_bytes }, - ); try testing.expect(s.pages.first != null); try testing.expect(s.pages.first == s.pages.last); } @@ -6424,168 +6319,6 @@ test "PageList eraseRowBounded exhausts pages invalidates viewport offset cache" }, s.scrollbar()); } -test "PageList adjustCapacity to increase styles" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 2, 0); - defer s.deinit(); - { - try testing.expect(s.pages.first == s.pages.last); - const page = &s.pages.first.?.data; - - // Write all our data so we can assert its the same after - for (0..s.rows) |y| { - for (0..s.cols) |x| { - const rac = page.getRowAndCell(x, y); - rac.cell.* = .{ - .content_tag = .codepoint, - .content = .{ .codepoint = @intCast(x) }, - }; - } - } - } - - // Increase our styles - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .styles = std_capacity.styles * 2 }, - ); - - { - try testing.expect(s.pages.first == s.pages.last); - const page = &s.pages.first.?.data; - for (0..s.rows) |y| { - for (0..s.cols) |x| { - const rac = page.getRowAndCell(x, y); - try testing.expectEqual( - @as(u21, @intCast(x)), - rac.cell.content.codepoint, - ); - } - } - } -} - -test "PageList adjustCapacity to increase graphemes" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 2, 0); - defer s.deinit(); - { - try testing.expect(s.pages.first == s.pages.last); - const page = &s.pages.first.?.data; - - // Write all our data so we can assert its the same after - for (0..s.rows) |y| { - for (0..s.cols) |x| { - const rac = page.getRowAndCell(x, y); - rac.cell.* = .{ - .content_tag = .codepoint, - .content = .{ .codepoint = @intCast(x) }, - }; - } - } - } - - // Increase our graphemes - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .grapheme_bytes = std_capacity.grapheme_bytes * 2 }, - ); - - { - try testing.expect(s.pages.first == s.pages.last); - const page = &s.pages.first.?.data; - for (0..s.rows) |y| { - for (0..s.cols) |x| { - const rac = page.getRowAndCell(x, y); - try testing.expectEqual( - @as(u21, @intCast(x)), - rac.cell.content.codepoint, - ); - } - } - } -} - -test "PageList adjustCapacity to increase hyperlinks" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 2, 2, 0); - defer s.deinit(); - { - try testing.expect(s.pages.first == s.pages.last); - const page = &s.pages.first.?.data; - - // Write all our data so we can assert its the same after - for (0..s.rows) |y| { - for (0..s.cols) |x| { - const rac = page.getRowAndCell(x, y); - rac.cell.* = .{ - .content_tag = .codepoint, - .content = .{ .codepoint = @intCast(x) }, - }; - } - } - } - - // Increase our graphemes - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .hyperlink_bytes = @max(std_capacity.hyperlink_bytes * 2, 2048) }, - ); - - { - try testing.expect(s.pages.first == s.pages.last); - const page = &s.pages.first.?.data; - for (0..s.rows) |y| { - for (0..s.cols) |x| { - const rac = page.getRowAndCell(x, y); - try testing.expectEqual( - @as(u21, @intCast(x)), - rac.cell.content.codepoint, - ); - } - } - } -} - -test "PageList adjustCapacity after col shrink" { - const testing = std.testing; - const alloc = testing.allocator; - - var s = try init(alloc, 10, 2, 0); - defer s.deinit(); - - // Shrink columns - this updates size.cols but not capacity.cols - try s.resize(.{ .cols = 5, .reflow = false }); - try testing.expectEqual(5, s.cols); - - { - const page = &s.pages.first.?.data; - // capacity.cols is still 10, but size.cols should be 5 - try testing.expectEqual(5, page.size.cols); - try testing.expect(page.capacity.cols >= 10); - } - - // Now adjust capacity (e.g., to increase styles) - // This should preserve the current size.cols, not revert to capacity.cols - _ = try s.adjustCapacity( - s.pages.first.?, - .{ .styles = std_capacity.styles * 2 }, - ); - - { - const page = &s.pages.first.?.data; - // After adjustCapacity, size.cols should still be 5, not 10 - try testing.expectEqual(5, page.size.cols); - try testing.expectEqual(5, s.cols); - } -} - test "PageList increaseCapacity to increase styles" { const testing = std.testing; const alloc = testing.allocator; @@ -6799,13 +6532,12 @@ test "PageList increaseCapacity returns OutOfSpace at max capacity" { var s = try init(alloc, 2, 2, 0); defer s.deinit(); - // Set styles capacity to a value that will overflow when doubled - // We need to create a page with capacity at more than half of max + // Keep increasing styles capacity until we're at more than half of max const max_styles = std.math.maxInt(size.StyleCountInt); const half_max = max_styles / 2 + 1; - - // Adjust capacity to near-max first - _ = try s.adjustCapacity(s.pages.first.?, .{ .styles = half_max }); + while (s.pages.first.?.data.capacity.styles < half_max) { + _ = try s.increaseCapacity(s.pages.first.?, .styles); + } // Now increaseCapacity should fail with OutOfSpace try testing.expectError( @@ -11485,12 +11217,10 @@ test "PageList grow reuses non-standard page without leak" { var s = try init(alloc, 80, 24, 3 * std_size); defer s.deinit(); - // Save the first page node before adjustment - const first_before = s.pages.first.?; - - // Adjust the first page to have non-standard capacity. We use a small - // increase that makes it just slightly larger than std_size. - _ = try s.adjustCapacity(first_before, .{ .grapheme_bytes = std_size + 1 }); + // Increase the first page capacity to make it non-standard (larger than std_size). + while (s.pages.first.?.data.memory.len <= std_size) { + _ = try s.increaseCapacity(s.pages.first.?, .grapheme_bytes); + } // The first page should now have non-standard memory size. try testing.expect(s.pages.first.?.data.memory.len > std_size); -- 2.51.2 From 6b2455828e1aba8898d3c11fc5b9e06f78a1b91c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 15:26:12 -0800 Subject: [PATCH 279/349] terminal: resizeWithoutReflowGrowCols can only fail for OOM --- src/terminal/PageList.zig | 89 +++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index bf2071497..043a8f191 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1652,7 +1652,7 @@ const ReflowCursor = struct { } }; -fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { +fn resizeWithoutReflow(self: *PageList, opts: Resize) Allocator.Error!void { // We only set the new min_max_size if we're not reflowing. If we are // reflowing, then resize handles this for us. const old_min_max_size = self.min_max_size; @@ -1812,26 +1812,44 @@ fn resizeWithoutReflowGrowCols( self: *PageList, cols: size.CellCountInt, chunk: PageIterator.Chunk, -) !void { +) Allocator.Error!void { assert(cols > self.cols); const page = &chunk.node.data; - const cap = try page.capacity.adjust(.{ .cols = cols }); // Update our col count const old_cols = self.cols; - self.cols = cap.cols; + self.cols = cols; errdefer self.cols = old_cols; // Unlikely fast path: we have capacity in the page. This // is only true if we resized to less cols earlier. - if (page.capacity.cols >= cap.cols) { - page.size.cols = cap.cols; + if (page.capacity.cols >= cols) { + page.size.cols = cols; return; } // Likely slow path: we don't have capacity, so we need // to allocate a page, and copy the old data into it. + // Try to fit our new column size into our existing page capacity. + // If that doesn't work then use a non-standard page with the + // given columns. + const cap = page.capacity.adjust( + .{ .cols = cols }, + ) catch |err| err: { + comptime assert(@TypeOf(err) == error{OutOfMemory}); + + // We verify all maxed out page layouts work. + var cap = page.capacity; + cap.cols = cols; + + // We're growing columns so we can only get less rows so use + // the lesser of our capacity and size so we minimize wasted + // rows. + cap.rows = @min(page.size.rows, cap.rows); + break :err cap; + }; + // On error, we need to undo all the pages we've added. const prev = chunk.node.prev; errdefer { @@ -1895,6 +1913,31 @@ fn resizeWithoutReflowGrowCols( assert(prev_page.size.rows <= prev_page.capacity.rows); } + // If we have an error, we clear the rows we just added to our prev page. + const prev_copied = copied; + errdefer if (prev_copied > 0) { + const prev_page = &prev.?.data; + const prev_size = prev_page.size.rows - prev_copied; + const prev_rows = prev_page.rows.ptr(prev_page.memory)[prev_size..prev_page.size.rows]; + for (prev_rows) |*row| prev_page.clearCells( + row, + 0, + prev_page.size.cols, + ); + prev_page.size.rows = prev_size; + }; + + // We delete any of the nodes we added. + errdefer { + var it = chunk.node.prev; + while (it) |node| { + if (node == prev) break; + it = node.prev; + self.pages.remove(node); + self.destroyNode(node); + } + } + // We need to loop because our col growth may force us // to split pages. while (copied < page.size.rows) { @@ -1907,19 +1950,33 @@ fn resizeWithoutReflowGrowCols( // Perform the copy const y_start = copied; - const y_end = copied + len; - const src_rows = page.rows.ptr(page.memory)[y_start..y_end]; + const src_rows = page.rows.ptr(page.memory)[y_start .. copied + len]; const dst_rows = new_node.data.rows.ptr(new_node.data.memory)[0..len]; for (dst_rows, src_rows) |*dst_row, *src_row| { new_node.data.size.rows += 1; - errdefer new_node.data.size.rows -= 1; - try new_node.data.cloneRowFrom( + if (new_node.data.cloneRowFrom( page, dst_row, src_row, - ); + )) |_| { + copied += 1; + } else |err| { + // I don't THINK this should be possible, because while our + // row count may diminish due to the adjustment, our + // prior capacity should have been sufficient to hold all the + // managed memory. + log.warn( + "unexpected cloneRowFrom failure during resizeWithoutReflowGrowCols: {}", + .{err}, + ); + + // We can actually safely handle this though by exiting + // this loop early and cutting our copy short. + new_node.data.size.rows -= 1; + break; + } } - copied = y_end; + const y_end = copied; // Insert our new page self.pages.insertBefore(chunk.node, new_node); @@ -1936,6 +1993,10 @@ fn resizeWithoutReflowGrowCols( } assert(copied == page.size.rows); + // Our prior errdeferes are invalid after this point so ensure + // we don't have any more errors. + errdefer comptime unreachable; + // Remove the old page. // Deallocate the old page. self.pages.remove(chunk.node); @@ -2514,7 +2575,7 @@ inline fn growRequiredForActive(self: *const PageList) bool { /// adhere to max_size. /// /// This returns the newly allocated page node if there is one. -pub fn grow(self: *PageList) !?*List.Node { +pub fn grow(self: *PageList) Allocator.Error!?*List.Node { defer self.assertIntegrity(); const last = self.pages.last.?; @@ -2533,7 +2594,7 @@ pub fn grow(self: *PageList) !?*List.Node { // Get the layout first so our failable work is done early. // We'll need this for both paths. - const cap = try std_capacity.adjust(.{ .cols = self.cols }); + const cap = initialCapacity(self.cols); // If allocation would exceed our max size, we prune the first page. // We don't need to reallocate because we can simply reuse that first -- 2.51.2 From d626984418cf91910543683f64ba95d24815f4ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Jan 2026 20:31:22 -0800 Subject: [PATCH 280/349] terminal: reflowCursor improve error handling on assumed cases --- src/terminal/PageList.zig | 233 +++++++++++++++++++++++++++++++------- 1 file changed, 190 insertions(+), 43 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 043a8f191..755e0a393 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -992,32 +992,71 @@ fn resizeCols( } else null; defer if (preserved_cursor) |c| self.untrackPin(c.tracked_pin); - const first = self.pages.first.?; - var it = self.rowIterator(.right_down, .{ .screen = .{} }, null); + // Create the first node that contains our reflow. + const first_rewritten_node = node: { + const page = &self.pages.first.?.data; + const cap = page.capacity.adjust( + .{ .cols = cols }, + ) catch |err| err: { + comptime assert(@TypeOf(err) == error{OutOfMemory}); + + // We verify all maxed out page layouts work. + var cap = page.capacity; + cap.cols = cols; + + // We're growing columns so we can only get less rows so use + // the lesser of our capacity and size so we minimize wasted + // rows. + cap.rows = @min(page.size.rows, cap.rows); + break :err cap; + }; + + const node = try self.createPage(cap); + node.data.size.rows = 1; + break :node node; + }; - const dst_node = try self.createPage(try first.data.capacity.adjust(.{ .cols = cols })); - dst_node.data.size.rows = 1; + // We need to grab our rowIterator now before we rewrite our + // linked list below. + var it = self.rowIterator( + .right_down, + .{ .screen = .{} }, + null, + ); + errdefer { + // If an error occurs, we're in a pretty disastrous broken state, + // but we should still try to clean up our leaked memory. Free + // any of the remaining orphaned pages from before. If we reflowed + // successfully this will be null. + var node_: ?*Node = if (it.chunk) |chunk| chunk.node else null; + while (node_) |node| { + node_ = node.next; + self.destroyNode(node); + } + } // Set our new page as the only page. This orphans the existing pages // in the list, but that's fine since we're gonna delete them anyway. - self.pages.first = dst_node; - self.pages.last = dst_node; + self.pages.first = first_rewritten_node; + self.pages.last = first_rewritten_node; // Reflow all our rows. { - var dst_cursor = ReflowCursor.init(dst_node); + var reflow_cursor: ReflowCursor = .init(first_rewritten_node); while (it.next()) |row| { - try dst_cursor.reflowRow(self, row); + try reflow_cursor.reflowRow(self, row); - // Once we're done reflowing a page, destroy it. + // Once we're done reflowing a page, destroy it immediately. + // This frees memory and makes it more likely in memory + // constrained environments that the next reflow will work. 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; + // log.warn("total old={} new={}", .{ self.total_rows, reflow_cursor.total_rows }); + self.total_rows = reflow_cursor.total_rows; } // If our total rows is less than our active rows, we need to grow. @@ -1114,16 +1153,11 @@ const ReflowCursor = struct { const src_page: *Page = &row.node.data; const src_row = row.rowAndCell().row; const src_y = row.y; - - // Inherit increased styles or grapheme bytes from - // the src page we're reflowing from for new pages. - const cap = try src_page.capacity.adjust(.{ .cols = self.page.size.cols }); - const cells = src_row.cells.ptr(src_page.memory)[0..src_page.size.cols]; + // Calculate the columns in this row. First up we trim non-semantic + // rightmost blanks. var cols_len = src_page.size.cols; - - // If the row is wrapped, all empty cells are meaningful. if (!src_row.wrap) { while (cols_len > 0) { if (!cells[cols_len - 1].isEmpty()) break; @@ -1145,9 +1179,10 @@ const ReflowCursor = struct { // If this pin is in the blanks on the right and past the end // of the dst col width then we move it to the end of the dst // col width instead. - if (p.x >= cols_len) { - p.x = @min(p.x, cap.cols - 1 - self.x); - } + if (p.x >= cols_len) p.x = @min( + p.x, + self.page.size.cols - 1 - self.x, + ); // We increase our col len to at least include this pin. // This ensures that blank rows with pins are processed, @@ -1162,16 +1197,29 @@ const ReflowCursor = struct { // If this blank row was a wrap continuation somehow // then we won't need to write it since it should be // a part of the previously written row. - if (!src_row.wrap_continuation) { - self.new_rows += 1; - } + if (!src_row.wrap_continuation) self.new_rows += 1; return; } + // Inherit increased styles or grapheme bytes from the src page + // we're reflowing from for new pages. + const cap = src_page.capacity.adjust( + .{ .cols = self.page.size.cols }, + ) catch |err| err: { + comptime assert(@TypeOf(err) == error{OutOfMemory}); + + var cap = src_page.capacity; + cap.cols = self.page.size.cols; + // We're already a non-standard page. We don't want to + // inherit a massive set of rows, so cap it at our std size. + cap.rows = @min(src_page.size.rows, std_capacity.rows); + break :err cap; + }; + // Our row isn't blank, write any new rows we deferred. while (self.new_rows > 0) { - self.new_rows -= 1; try self.cursorScrollOrNewPage(list, cap); + self.new_rows -= 1; } self.copyRowMetadata(src_row); @@ -1326,12 +1374,30 @@ const ReflowCursor = struct { } } - // This shouldn't fail since we made sure we have space above. - try self.page.setGraphemes(self.page_row, self.page_cell, cps); + self.page.setGraphemes( + self.page_row, + self.page_cell, + cps, + ) catch |err| { + // This shouldn't fail since we made sure we have space + // above. There is no reasonable behavior we can take here + // so we have a warn level log. This is ALMOST non-recoverable, + // though we choose to recover by corrupting the cell + // to a non-grapheme codepoint. + log.err("setGraphemes failed after capacity increase err={}", .{err}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + // Unsafe builds we throw away grapheme data! + cell.content_tag = .codepoint; + cell.content = .{ .codepoint = 0xFFFD }; + }; } // Copy hyperlink data. - if (cell.hyperlink) { + if (cell.hyperlink) hyperlink: { const src_id = src_page.lookupHyperlink(cell).?; const src_link = src_page.hyperlink_set.get(src_page.memory, src_id); @@ -1371,10 +1437,25 @@ const ReflowCursor = struct { } } + const dst_link = src_link.dupe( + src_page, + self.page, + ) catch |err| { + // This shouldn't fail since we did a capacity + // check above. + log.err("link dupe failed with capacity check err={}", .{err}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + cell.hyperlink = false; + break :hyperlink; + }; + const dst_id = self.page.hyperlink_set.addWithIdContext( self.page.memory, - // We made sure there was enough capacity for this above. - try src_link.dupe(src_page, self.page), + dst_link, src_id, .{ .page = self.page }, ) catch |err| id: { @@ -1387,29 +1468,80 @@ const ReflowCursor = struct { error.NeedsRehash => null, }); + // We dupe the link again, and don't have to worry about + // freeing the other one because increasing the capacity + // destroyed the prior page. + const dst_link2 = src_link.dupe( + src_page, + self.page, + ) catch |err2| { + // This shouldn't fail since we did a capacity + // check above. + log.err("link dupe failed with capacity check err={}", .{err2}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + cell.hyperlink = false; + break :hyperlink; + }; + // We assume this one will succeed. We dupe the link // again, and don't have to worry about the other one // because increasing the capacity naturally clears up // any managed memory not associated with a cell yet. - break :id try self.page.hyperlink_set.addWithIdContext( + break :id self.page.hyperlink_set.addWithIdContext( self.page.memory, - try src_link.dupe(src_page, self.page), + dst_link2, src_id, .{ .page = self.page }, - ); + ) catch |err2| { + // This shouldn't happen since we increased capacity + // above so we handle it like the other similar + // cases and log it, crash in safe builds, and + // remove the hyperlink in unsafe builds. + log.err( + "addWithIdContext failed after capacity increase err={}", + .{err2}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + dst_link2.free(self.page); + cell.hyperlink = false; + break :hyperlink; + }; } orelse src_id; - // We expect this to succeed due to the - // hyperlinkCapacity check we did before. - try self.page.setHyperlink( + // We expect this to succeed due to the hyperlinkCapacity + // check we did before. If it doesn't succeed let's + // log it, crash (in safe builds), and clear our state. + self.page.setHyperlink( self.page_row, self.page_cell, dst_id, - ); + ) catch |err| { + log.err( + "setHyperlink failed after capacity increase err={}", + .{err}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + // Unsafe builds we throw away hyperlink data! + self.page.hyperlink_set.release(self.page.memory, dst_id); + cell.hyperlink = false; + break :hyperlink; + }; } // Copy style data. - if (cell.hasStyling()) { + if (cell.hasStyling()) style: { const style = src_page.styles.get( src_page.memory, cell.style_id, @@ -1430,15 +1562,29 @@ const ReflowCursor = struct { }); // We assume this one will succeed. - break :id try self.page.styles.addWithId( + break :id self.page.styles.addWithId( self.page.memory, style, cell.style_id, - ); + ) catch |err2| { + // Should not fail since we just modified capacity + // above. Log it, crash in safe builds, clear style + // in unsafe builds. + log.err( + "addWithId failed after capacity increase err={}", + .{err2}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } + + cell.style_id = stylepkg.default_id; + break :style; + }; } orelse cell.style_id; self.page_row.styled = true; - self.page_cell.style_id = id; } @@ -1574,12 +1720,13 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, cap: Capacity, - ) !void { + ) Allocator.Error!void { // Remember our new row count so we can restore it // after reinitializing our cursor on the new page. const new_rows = self.new_rows; const node = try list.createPage(cap); + errdefer comptime unreachable; node.data.size.rows = 1; list.pages.insertAfter(self.node, node); @@ -1594,7 +1741,7 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, cap: Capacity, - ) !void { + ) Allocator.Error!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. -- 2.51.2 From 25643ec806e5be828b236b8ead5814a0fcabaff8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 06:22:05 -0800 Subject: [PATCH 281/349] terminal: reflowRow extract writeCell --- src/terminal/PageList.zig | 633 ++++++++++++++++++++------------------ 1 file changed, 334 insertions(+), 299 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 755e0a393..0ca00f45c 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1247,203 +1247,284 @@ const ReflowCursor = struct { } } - const cell = &cells[x]; - x += 1; - - // Copy cell contents. - switch (cell.content_tag) { - .codepoint, - .codepoint_grapheme, - => switch (cell.wide) { - .narrow => self.page_cell.* = cell.*, - - .wide => if (self.page.size.cols > 1) { - if (self.x == self.page.size.cols - 1) { - // If there's a wide character in the last column of - // the reflowed page then we need to insert a spacer - // head and wrap before handling it. - self.page_cell.* = .{ - .content_tag = .codepoint, - .content = .{ .codepoint = 0 }, - .wide = .spacer_head, - }; - - // Decrement the source position so that when we - // loop we'll process this source cell again, - // since we can't copy it into a spacer head. - x -= 1; - - // Move to the next row (this sets pending wrap - // which will cause us to wrap on the next - // iteration). - self.cursorForward(); - continue; - } else { - self.page_cell.* = cell.*; - } - } else { - // Edge case, when resizing to 1 column, wide - // characters are just destroyed and replaced - // with empty narrow cells. - self.page_cell.content.codepoint = 0; - self.page_cell.wide = .narrow; + switch (try self.writeCell( + list, + &cells[x], + src_page, + )) { + // Wrote the cell, move to the next. + .success => x += 1, + // Wrote the cell but request to skip the next so skip it. + // This is used for things like spacers. + .skip_next => x += 2, + // Didn't write the cell, repeat writing this same cell. + .repeat => {}, + } + } + + // If the source row isn't wrapped then we should scroll afterwards. + if (!src_row.wrap) { + self.new_rows += 1; + } + } + + /// Write a cell. On error, this will not unwrite the cell but + /// the cell may be incomplete (but valid). For example, if the source + /// cell is styled and we failed to allocate space for styles, the + /// written cell may not be styled but it is valid. + /// + /// The key failure to recognize for callers is when we can't increase + /// capacity in our destination page. In this case, the caller may want + /// to split the page at this row, rewrite the row into a new page + /// and continue from there. + /// + /// But this function guarantees the terminal/page will be in a + /// coherent state even on error. + fn writeCell( + self: *ReflowCursor, + list: *PageList, + cell: *const pagepkg.Cell, + src_page: *const Page, + ) IncreaseCapacityError!enum { + success, + repeat, + skip_next, + } { + // Copy cell contents. + switch (cell.content_tag) { + .codepoint, + .codepoint_grapheme, + => switch (cell.wide) { + .narrow => self.page_cell.* = cell.*, + + .wide => if (self.page.size.cols > 1) { + if (self.x == self.page.size.cols - 1) { + // If there's a wide character in the last column of + // the reflowed page then we need to insert a spacer + // head and wrap before handling it. + self.page_cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0 }, + .wide = .spacer_head, + }; + + // Move to the next row (this sets pending wrap + // which will cause us to wrap on the next + // iteration). self.cursorForward(); - // Skip spacer tail so it doesn't cause a wrap. - x += 1; - continue; - }, - .spacer_tail => if (self.page.size.cols > 1) { - self.page_cell.* = cell.*; + // Decrement the source position so that when we + // loop we'll process this source cell again, + // since we can't copy it into a spacer head. + return .repeat; } else { - // Edge case, when resizing to 1 column, wide - // characters are just destroyed and replaced - // with empty narrow cells, so we should just - // discard any spacer tails. - continue; - }, + self.page_cell.* = cell.*; + } + } else { + // Edge case, when resizing to 1 column, wide + // characters are just destroyed and replaced + // with empty narrow cells. + self.page_cell.content.codepoint = 0; + self.page_cell.wide = .narrow; + self.cursorForward(); - .spacer_head => { - // Spacer heads should be ignored. If we need a - // spacer head in our reflowed page, it is added - // when processing the wide cell it belongs to. - continue; - }, + // Skip spacer tail so it doesn't cause a wrap. + return .skip_next; }, - .bg_color_palette, - .bg_color_rgb, - => { - // These are guaranteed to have no style or grapheme - // data associated with them so we can fast path them. + .spacer_tail => if (self.page.size.cols > 1) { self.page_cell.* = cell.*; - self.cursorForward(); - continue; + } else { + // Edge case, when resizing to 1 column, wide + // characters are just destroyed and replaced + // with empty narrow cells, so we should just + // discard any spacer tails. + return .success; }, + + .spacer_head => { + // Spacer heads should be ignored. If we need a + // spacer head in our reflowed page, it is added + // when processing the wide cell it belongs to. + return .success; + }, + }, + + .bg_color_palette, + .bg_color_rgb, + => { + // These are guaranteed to have no style or grapheme + // data associated with them so we can fast path them. + self.page_cell.* = cell.*; + self.cursorForward(); + return .success; + }, + } + + // These will create issues by trying to clone managed memory that + // isn't set if the current dst row needs to be moved to a new page. + // They'll be fixed once we do properly copy the relevant memory. + self.page_cell.content_tag = .codepoint; + self.page_cell.hyperlink = false; + self.page_cell.style_id = stylepkg.default_id; + + // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{ + // src_y, + // x, + // self.y, + // self.x, + // self.page.size.cols, + // cell.content.codepoint, + // cell.wide, + // self.page_cell.wide, + // }); + + if (comptime build_options.kitty_graphics) { + // Copy Kitty virtual placeholder status + if (cell.codepoint() == kitty.graphics.unicode.placeholder) { + self.page_row.kitty_virtual_placeholder = true; } + } - // These will create issues by trying to clone managed memory that - // isn't set if the current dst row needs to be moved to a new page. - // They'll be fixed once we do properly copy the relevant memory. - self.page_cell.content_tag = .codepoint; - self.page_cell.hyperlink = false; - self.page_cell.style_id = stylepkg.default_id; - - // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{ - // src_y, - // x, - // self.y, - // self.x, - // self.page.size.cols, - // cell.content.codepoint, - // cell.wide, - // self.page_cell.wide, - // }); - - // Copy grapheme data. - if (cell.content_tag == .codepoint_grapheme) { - // Copy the graphemes - const cps = src_page.lookupGrapheme(cell).?; - - // If our page can't support an additional cell - // with graphemes then we increase capacity. - if (self.page.graphemeCount() >= self.page.graphemeCapacity()) { - try self.increaseCapacity( - list, - .grapheme_bytes, + // Copy grapheme data. + if (cell.content_tag == .codepoint_grapheme) { + // Copy the graphemes + const cps = src_page.lookupGrapheme(cell).?; + + // If our page can't support an additional cell + // with graphemes then we increase capacity. + if (self.page.graphemeCount() >= self.page.graphemeCapacity()) { + try self.increaseCapacity( + list, + .grapheme_bytes, + ); + } + + // Attempt to allocate the space that would be required + // for these graphemes, and if it's not available, then + // increase capacity. Keep trying until we succeed. + while (true) { + if (self.page.grapheme_alloc.alloc( + u21, + self.page.memory, + cps.len, + )) |slice| { + self.page.grapheme_alloc.free( + self.page.memory, + slice, ); + break; + } else |_| { + // Grow our capacity until we can fit the extra bytes. + try self.increaseCapacity(list, .grapheme_bytes); } + } - // Attempt to allocate the space that would be required - // for these graphemes, and if it's not available, then - // increase capacity. Keep trying until we succeed. - while (true) { - if (self.page.grapheme_alloc.alloc( - u21, - self.page.memory, - cps.len, - )) |slice| { - self.page.grapheme_alloc.free( - self.page.memory, - slice, - ); - break; - } else |_| { - // Grow our capacity until we can fit the extra bytes. - try self.increaseCapacity(list, .grapheme_bytes); - } + self.page.setGraphemes( + self.page_row, + self.page_cell, + cps, + ) catch |err| { + // This shouldn't fail since we made sure we have space + // above. There is no reasonable behavior we can take here + // so we have a warn level log. This is ALMOST non-recoverable, + // though we choose to recover by corrupting the cell + // to a non-grapheme codepoint. + log.err("setGraphemes failed after capacity increase err={}", .{err}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; } - self.page.setGraphemes( - self.page_row, - self.page_cell, - cps, - ) catch |err| { - // This shouldn't fail since we made sure we have space - // above. There is no reasonable behavior we can take here - // so we have a warn level log. This is ALMOST non-recoverable, - // though we choose to recover by corrupting the cell - // to a non-grapheme codepoint. - log.err("setGraphemes failed after capacity increase err={}", .{err}); - if (comptime std.debug.runtime_safety) { - // Force a crash with safe builds. - unreachable; - } + // Unsafe builds we throw away grapheme data! + cell.content_tag = .codepoint; + cell.content = .{ .codepoint = 0xFFFD }; + }; + } - // Unsafe builds we throw away grapheme data! - cell.content_tag = .codepoint; - cell.content = .{ .codepoint = 0xFFFD }; - }; - } + // Copy hyperlink data. + if (cell.hyperlink) hyperlink: { + const src_id = src_page.lookupHyperlink(cell).?; + const src_link = src_page.hyperlink_set.get(src_page.memory, src_id); - // Copy hyperlink data. - if (cell.hyperlink) hyperlink: { - const src_id = src_page.lookupHyperlink(cell).?; - const src_link = src_page.hyperlink_set.get(src_page.memory, src_id); + // If our page can't support an additional cell + // with a hyperlink then we increase capacity. + if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) { + try self.increaseCapacity(list, .hyperlink_bytes); + } - // If our page can't support an additional cell - // with a hyperlink then we increase capacity. - if (self.page.hyperlinkCount() >= self.page.hyperlinkCapacity()) { - try self.increaseCapacity(list, .hyperlink_bytes); + // Ensure that the string alloc has sufficient capacity + // to dupe the link (and the ID if it's not implicit). + const additional_required_string_capacity = + src_link.uri.len + + switch (src_link.id) { + .explicit => |v| v.len, + .implicit => 0, + }; + // Keep trying until we have enough capacity. + while (true) { + if (self.page.string_alloc.alloc( + u8, + self.page.memory, + additional_required_string_capacity, + )) |slice| { + // We have enough capacity, free the test alloc. + self.page.string_alloc.free( + self.page.memory, + slice, + ); + break; + } else |_| { + // Grow our capacity until we can fit the extra bytes. + try self.increaseCapacity( + list, + .string_bytes, + ); } + } - // Ensure that the string alloc has sufficient capacity - // to dupe the link (and the ID if it's not implicit). - const additional_required_string_capacity = - src_link.uri.len + - switch (src_link.id) { - .explicit => |v| v.len, - .implicit => 0, - }; - // Keep trying until we have enough capacity. - while (true) { - if (self.page.string_alloc.alloc( - u8, - self.page.memory, - additional_required_string_capacity, - )) |slice| { - // We have enough capacity, free the test alloc. - self.page.string_alloc.free( - self.page.memory, - slice, - ); - break; - } else |_| { - // Grow our capacity until we can fit the extra bytes. - try self.increaseCapacity( - list, - .string_bytes, - ); - } + const dst_link = src_link.dupe( + src_page, + self.page, + ) catch |err| { + // This shouldn't fail since we did a capacity + // check above. + log.err("link dupe failed with capacity check err={}", .{err}); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; } - const dst_link = src_link.dupe( + break :hyperlink; + }; + + const dst_id = self.page.hyperlink_set.addWithIdContext( + self.page.memory, + dst_link, + src_id, + .{ .page = self.page }, + ) catch |err| id: { + // Always free our original link in case the increaseCap + // call fails so we aren't leaking memory. + dst_link.free(self.page); + + // If the add failed then either the set needs to grow + // or it needs to be rehashed. Either one of those can + // be accomplished by increasing capacity, either with + // no actual change or with an increased hyperlink cap. + try self.increaseCapacity(list, switch (err) { + error.OutOfMemory => .hyperlink_bytes, + error.NeedsRehash => null, + }); + + // We need to recreate the link into the new page. + const dst_link2 = src_link.dupe( src_page, self.page, - ) catch |err| { + ) catch |err2| { // This shouldn't fail since we did a capacity // check above. - log.err("link dupe failed with capacity check err={}", .{err}); + log.err("link dupe failed with capacity check err={}", .{err2}); if (comptime std.debug.runtime_safety) { // Force a crash with safe builds. unreachable; @@ -1453,155 +1534,109 @@ const ReflowCursor = struct { break :hyperlink; }; - const dst_id = self.page.hyperlink_set.addWithIdContext( + // We assume this one will succeed. We dupe the link + // again, and don't have to worry about the other one + // because increasing the capacity naturally clears up + // any managed memory not associated with a cell yet. + break :id self.page.hyperlink_set.addWithIdContext( self.page.memory, - dst_link, + dst_link2, src_id, .{ .page = self.page }, - ) catch |err| id: { - // If the add failed then either the set needs to grow - // or it needs to be rehashed. Either one of those can - // be accomplished by increasing capacity, either with - // no actual change or with an increased hyperlink cap. - try self.increaseCapacity(list, switch (err) { - error.OutOfMemory => .hyperlink_bytes, - error.NeedsRehash => null, - }); - - // We dupe the link again, and don't have to worry about - // freeing the other one because increasing the capacity - // destroyed the prior page. - const dst_link2 = src_link.dupe( - src_page, - self.page, - ) catch |err2| { - // This shouldn't fail since we did a capacity - // check above. - log.err("link dupe failed with capacity check err={}", .{err2}); - if (comptime std.debug.runtime_safety) { - // Force a crash with safe builds. - unreachable; - } - - cell.hyperlink = false; - break :hyperlink; - }; - - // We assume this one will succeed. We dupe the link - // again, and don't have to worry about the other one - // because increasing the capacity naturally clears up - // any managed memory not associated with a cell yet. - break :id self.page.hyperlink_set.addWithIdContext( - self.page.memory, - dst_link2, - src_id, - .{ .page = self.page }, - ) catch |err2| { - // This shouldn't happen since we increased capacity - // above so we handle it like the other similar - // cases and log it, crash in safe builds, and - // remove the hyperlink in unsafe builds. - log.err( - "addWithIdContext failed after capacity increase err={}", - .{err2}, - ); - if (comptime std.debug.runtime_safety) { - // Force a crash with safe builds. - unreachable; - } - - dst_link2.free(self.page); - cell.hyperlink = false; - break :hyperlink; - }; - } orelse src_id; - - // We expect this to succeed due to the hyperlinkCapacity - // check we did before. If it doesn't succeed let's - // log it, crash (in safe builds), and clear our state. - self.page.setHyperlink( - self.page_row, - self.page_cell, - dst_id, - ) catch |err| { + ) catch |err2| { + // This shouldn't happen since we increased capacity + // above so we handle it like the other similar + // cases and log it, crash in safe builds, and + // remove the hyperlink in unsafe builds. log.err( - "setHyperlink failed after capacity increase err={}", - .{err}, + "addWithIdContext failed after capacity increase err={}", + .{err2}, ); if (comptime std.debug.runtime_safety) { // Force a crash with safe builds. unreachable; } - // Unsafe builds we throw away hyperlink data! - self.page.hyperlink_set.release(self.page.memory, dst_id); + dst_link2.free(self.page); cell.hyperlink = false; break :hyperlink; }; - } + } orelse src_id; + + // We expect this to succeed due to the hyperlinkCapacity + // check we did before. If it doesn't succeed let's + // log it, crash (in safe builds), and clear our state. + self.page.setHyperlink( + self.page_row, + self.page_cell, + dst_id, + ) catch |err| { + log.err( + "setHyperlink failed after capacity increase err={}", + .{err}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } - // Copy style data. - if (cell.hasStyling()) style: { - const style = src_page.styles.get( - src_page.memory, - cell.style_id, - ).*; + // Unsafe builds we throw away hyperlink data! + self.page.hyperlink_set.release(self.page.memory, dst_id); + cell.hyperlink = false; + break :hyperlink; + }; + } - const id = self.page.styles.addWithId( + // Copy style data. + if (cell.hasStyling()) style: { + const style = src_page.styles.get( + src_page.memory, + cell.style_id, + ).*; + + const id = self.page.styles.addWithId( + self.page.memory, + style, + cell.style_id, + ) catch |err| id: { + // If the add failed then either the set needs to grow + // or it needs to be rehashed. Either one of those can + // be accomplished by increasing capacity, either with + // no actual change or with an increased style cap. + try self.increaseCapacity(list, switch (err) { + error.OutOfMemory => .styles, + error.NeedsRehash => null, + }); + + // We assume this one will succeed. + break :id self.page.styles.addWithId( self.page.memory, style, cell.style_id, - ) catch |err| id: { - // If the add failed then either the set needs to grow - // or it needs to be rehashed. Either one of those can - // be accomplished by increasing capacity, either with - // no actual change or with an increased style cap. - try self.increaseCapacity(list, switch (err) { - error.OutOfMemory => .styles, - error.NeedsRehash => null, - }); - - // We assume this one will succeed. - break :id self.page.styles.addWithId( - self.page.memory, - style, - cell.style_id, - ) catch |err2| { - // Should not fail since we just modified capacity - // above. Log it, crash in safe builds, clear style - // in unsafe builds. - log.err( - "addWithId failed after capacity increase err={}", - .{err2}, - ); - if (comptime std.debug.runtime_safety) { - // Force a crash with safe builds. - unreachable; - } - - cell.style_id = stylepkg.default_id; - break :style; - }; - } orelse cell.style_id; - - self.page_row.styled = true; - self.page_cell.style_id = id; - } + ) catch |err2| { + // Should not fail since we just modified capacity + // above. Log it, crash in safe builds, clear style + // in unsafe builds. + log.err( + "addWithId failed after capacity increase err={}", + .{err2}, + ); + if (comptime std.debug.runtime_safety) { + // Force a crash with safe builds. + unreachable; + } - if (comptime build_options.kitty_graphics) { - // Copy Kitty virtual placeholder status - if (cell.codepoint() == kitty.graphics.unicode.placeholder) { - self.page_row.kitty_virtual_placeholder = true; - } - } + cell.style_id = stylepkg.default_id; + break :style; + }; + } orelse cell.style_id; - self.cursorForward(); + self.page_row.styled = true; + self.page_cell.style_id = id; } - // If the source row isn't wrapped then we should scroll afterwards. - if (!src_row.wrap) { - self.new_rows += 1; - } + self.cursorForward(); + return .success; } /// Create a new page in the provided list with the provided @@ -1654,7 +1689,7 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, adjustment: ?IncreaseCapacity, - ) !void { + ) IncreaseCapacityError!void { const old_x = self.x; const old_y = self.y; const old_total_rows = self.total_rows; -- 2.51.2 From 97621ece935228efe82bfb451d3be75051e12cf1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 07:21:17 -0800 Subject: [PATCH 282/349] terminal: handle reflowRow OutOfSpace by no-op --- src/terminal/PageList.zig | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 0ca00f45c..2045ccc01 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -874,7 +874,7 @@ pub const Resize = struct { /// Resize /// TODO: docs -pub fn resize(self: *PageList, opts: Resize) !void { +pub fn resize(self: *PageList, opts: Resize) Allocator.Error!void { defer self.assertIntegrity(); if (comptime std.debug.runtime_safety) { @@ -944,7 +944,7 @@ fn resizeCols( self: *PageList, cols: size.CellCountInt, cursor: ?Resize.Cursor, -) !void { +) Allocator.Error!void { assert(cols != self.cols); // Update our cols. We have to do this early because grow() that we @@ -1149,7 +1149,7 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, row: Pin, - ) !void { + ) Allocator.Error!void { const src_page: *Page = &row.node.data; const src_row = row.rowAndCell().row; const src_y = row.y; @@ -1247,11 +1247,11 @@ const ReflowCursor = struct { } } - switch (try self.writeCell( + if (self.writeCell( list, &cells[x], src_page, - )) { + )) |result| switch (result) { // Wrote the cell, move to the next. .success => x += 1, // Wrote the cell but request to skip the next so skip it. @@ -1259,6 +1259,24 @@ const ReflowCursor = struct { .skip_next => x += 2, // Didn't write the cell, repeat writing this same cell. .repeat => {}, + } else |err| switch (err) { + // System out of memory, we can't fix this. + error.OutOfMemory => return error.OutOfMemory, + + // We reached the capacity of a single page and can't + // add any more of some type of managed memory. + error.OutOfSpace => { + log.warn("OutOfSpace during reflow at src_y={} src_x={} dst_y={} dst_x={} cp={X}", .{ + src_y, + x, + self.y, + self.x, + cells[x].content.codepoint, + }); + + // TODO: Split the page + x += 1; + }, } } -- 2.51.2 From 42321cc7d59f3c3f02c14e225c0d374c8a002339 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 07:24:37 -0800 Subject: [PATCH 283/349] terminal: write to the proper cell --- src/terminal/PageList.zig | 166 ++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 78 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 2045ccc01..84518e8b7 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1308,84 +1308,98 @@ const ReflowCursor = struct { repeat, skip_next, } { - // Copy cell contents. - switch (cell.content_tag) { - .codepoint, - .codepoint_grapheme, - => switch (cell.wide) { - .narrow => self.page_cell.* = cell.*, - - .wide => if (self.page.size.cols > 1) { - if (self.x == self.page.size.cols - 1) { - // If there's a wide character in the last column of - // the reflowed page then we need to insert a spacer - // head and wrap before handling it. - self.page_cell.* = .{ - .content_tag = .codepoint, - .content = .{ .codepoint = 0 }, - .wide = .spacer_head, - }; - - // Move to the next row (this sets pending wrap - // which will cause us to wrap on the next - // iteration). + // Initialize self.page_cell with basic, unmanaged memory contents. + { + // This must not fail because we want to make sure we atomically + // setup our page cell to be valid. + errdefer comptime unreachable; + + // Copy cell contents. + switch (cell.content_tag) { + .codepoint, + .codepoint_grapheme, + => switch (cell.wide) { + .narrow => self.page_cell.* = cell.*, + + .wide => if (self.page.size.cols > 1) { + if (self.x == self.page.size.cols - 1) { + // If there's a wide character in the last column of + // the reflowed page then we need to insert a spacer + // head and wrap before handling it. + self.page_cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0 }, + .wide = .spacer_head, + }; + + // Move to the next row (this sets pending wrap + // which will cause us to wrap on the next + // iteration). + self.cursorForward(); + + // Decrement the source position so that when we + // loop we'll process this source cell again, + // since we can't copy it into a spacer head. + return .repeat; + } else { + self.page_cell.* = cell.*; + } + } else { + // Edge case, when resizing to 1 column, wide + // characters are just destroyed and replaced + // with empty narrow cells. + self.page_cell.content.codepoint = 0; + self.page_cell.wide = .narrow; self.cursorForward(); - // Decrement the source position so that when we - // loop we'll process this source cell again, - // since we can't copy it into a spacer head. - return .repeat; - } else { + // Skip spacer tail so it doesn't cause a wrap. + return .skip_next; + }, + + .spacer_tail => if (self.page.size.cols > 1) { self.page_cell.* = cell.*; - } - } else { - // Edge case, when resizing to 1 column, wide - // characters are just destroyed and replaced - // with empty narrow cells. - self.page_cell.content.codepoint = 0; - self.page_cell.wide = .narrow; - self.cursorForward(); + } else { + // Edge case, when resizing to 1 column, wide + // characters are just destroyed and replaced + // with empty narrow cells, so we should just + // discard any spacer tails. + return .success; + }, - // Skip spacer tail so it doesn't cause a wrap. - return .skip_next; + .spacer_head => { + // Spacer heads should be ignored. If we need a + // spacer head in our reflowed page, it is added + // when processing the wide cell it belongs to. + return .success; + }, }, - .spacer_tail => if (self.page.size.cols > 1) { + .bg_color_palette, + .bg_color_rgb, + => { + // These are guaranteed to have no style or grapheme + // data associated with them so we can fast path them. self.page_cell.* = cell.*; - } else { - // Edge case, when resizing to 1 column, wide - // characters are just destroyed and replaced - // with empty narrow cells, so we should just - // discard any spacer tails. - return .success; - }, - - .spacer_head => { - // Spacer heads should be ignored. If we need a - // spacer head in our reflowed page, it is added - // when processing the wide cell it belongs to. + self.cursorForward(); return .success; }, - }, + } - .bg_color_palette, - .bg_color_rgb, - => { - // These are guaranteed to have no style or grapheme - // data associated with them so we can fast path them. - self.page_cell.* = cell.*; - self.cursorForward(); - return .success; - }, + // These will create issues by trying to clone managed memory that + // isn't set if the current dst row needs to be moved to a new page. + // They'll be fixed once we do properly copy the relevant memory. + self.page_cell.content_tag = .codepoint; + self.page_cell.hyperlink = false; + self.page_cell.style_id = stylepkg.default_id; + + if (comptime build_options.kitty_graphics) { + // Copy Kitty virtual placeholder status + if (cell.codepoint() == kitty.graphics.unicode.placeholder) { + self.page_row.kitty_virtual_placeholder = true; + } + } } - // These will create issues by trying to clone managed memory that - // isn't set if the current dst row needs to be moved to a new page. - // They'll be fixed once we do properly copy the relevant memory. - self.page_cell.content_tag = .codepoint; - self.page_cell.hyperlink = false; - self.page_cell.style_id = stylepkg.default_id; - // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{ // src_y, // x, @@ -1397,12 +1411,10 @@ const ReflowCursor = struct { // self.page_cell.wide, // }); - if (comptime build_options.kitty_graphics) { - // Copy Kitty virtual placeholder status - if (cell.codepoint() == kitty.graphics.unicode.placeholder) { - self.page_row.kitty_virtual_placeholder = true; - } - } + // From this point on we're moving on to failable, managed memory. + // If we reach an error, we do the minimal cleanup necessary to + // not leave dangling memory but otherwise we gracefully degrade + // into some functional but not strictly correct cell. // Copy grapheme data. if (cell.content_tag == .codepoint_grapheme) { @@ -1455,8 +1467,8 @@ const ReflowCursor = struct { } // Unsafe builds we throw away grapheme data! - cell.content_tag = .codepoint; - cell.content = .{ .codepoint = 0xFFFD }; + self.page_cell.content_tag = .codepoint; + self.page_cell.content = .{ .codepoint = 0xFFFD }; }; } @@ -1548,7 +1560,6 @@ const ReflowCursor = struct { unreachable; } - cell.hyperlink = false; break :hyperlink; }; @@ -1576,7 +1587,6 @@ const ReflowCursor = struct { } dst_link2.free(self.page); - cell.hyperlink = false; break :hyperlink; }; } orelse src_id; @@ -1600,7 +1610,7 @@ const ReflowCursor = struct { // Unsafe builds we throw away hyperlink data! self.page.hyperlink_set.release(self.page.memory, dst_id); - cell.hyperlink = false; + self.page_cell.hyperlink = false; break :hyperlink; }; } @@ -1644,7 +1654,7 @@ const ReflowCursor = struct { unreachable; } - cell.style_id = stylepkg.default_id; + self.page_cell.style_id = stylepkg.default_id; break :style; }; } orelse cell.style_id; -- 2.51.2 From c9d15949d85564aa47763de2da341ae6654f6e90 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 08:22:53 -0800 Subject: [PATCH 284/349] terminal: on reflow OutOfSpace, move last row to new page and try again --- src/terminal/PageList.zig | 60 +++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 84518e8b7..710c0aaa7 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1264,18 +1264,22 @@ const ReflowCursor = struct { error.OutOfMemory => return error.OutOfMemory, // We reached the capacity of a single page and can't - // add any more of some type of managed memory. - error.OutOfSpace => { - log.warn("OutOfSpace during reflow at src_y={} src_x={} dst_y={} dst_x={} cp={X}", .{ - src_y, - x, - self.y, - self.x, - cells[x].content.codepoint, - }); - - // TODO: Split the page + // add any more of some type of managed memory. When this + // happens we split out the current row we're working on + // into a new page and continue from there. + error.OutOfSpace => if (self.y == 0) { + // If we're already on the first-row, we can't split + // any further, so we just ignore bad cells and take + // corrupted (but valid) cell contents. + log.warn("reflowRow OutOfSpace on first row, discarding cell managed memory", .{}); x += 1; + self.cursorForward(); + } else { + // Move our last row to a new page. + try self.moveLastRowToNewPage(list, cap); + + // Do NOT increment x so that we retry writing + // the same existing cell. }, } } @@ -1683,7 +1687,7 @@ const ReflowCursor = struct { self: *ReflowCursor, list: *PageList, cap: Capacity, - ) !void { + ) Allocator.Error!void { assert(self.y == self.page.size.rows - 1); assert(!self.pending_wrap); @@ -1693,15 +1697,41 @@ const ReflowCursor = struct { const old_x = self.x; try self.cursorNewPage(list, cap); + assert(self.node != old_node); + assert(self.y == 0); // Restore the x position of the cursor. self.cursorAbsolute(old_x, 0); - // We expect to have enough capacity to clone the row. - try self.page.cloneRowFrom(old_page, self.page_row, old_row); + // Copy our old data. This should NOT fail because we have the + // capacity of the old page which already fits the data we requested. + self.page.cloneRowFrom( + old_page, + self.page_row, + old_row, + ) catch |err| { + log.err( + "error cloning single row for moveLastRowToNewPage err={}", + .{err}, + ); + @panic("unexpected copy row failure"); + }; + + // Move any tracked pins from that last row into this new node. + { + const pin_keys = list.tracked_pins.keys(); + for (pin_keys) |p| { + if (&p.node.data != old_page or + p.y != old_page.size.rows - 1) continue; + + p.node = self.node; + p.y = self.y; + // p.x remains the same since we're copying the row as-is + } + } // Clear the row from the old page and truncate it. - old_page.clearCells(old_row, 0, self.page.size.cols); + old_page.clearCells(old_row, 0, old_page.size.cols); old_page.size.rows -= 1; // If that was the last row in that page -- 2.51.2 From ec0a1500984403eefffbbf7528cf22230b08cfb1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 09:12:41 -0800 Subject: [PATCH 285/349] terminal: moveLastRowToNewPage needs to fix up total_rows --- src/terminal/PageList.zig | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 710c0aaa7..69edcafb1 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1696,10 +1696,18 @@ const ReflowCursor = struct { const old_row = self.page_row; const old_x = self.x; + // Our total row count never changes, because we're removing one + // row from the last page and moving it into a new page. + const old_total_rows = self.total_rows; + defer self.total_rows = old_total_rows; + try self.cursorNewPage(list, cap); assert(self.node != old_node); assert(self.y == 0); + // We have no cleanup for our old state from here on out. No failures! + errdefer comptime unreachable; + // Restore the x position of the cursor. self.cursorAbsolute(old_x, 0); @@ -1752,7 +1760,7 @@ const ReflowCursor = struct { const old_y = self.y; const old_total_rows = self.total_rows; - self.* = .init(node: { + const node = node: { // Pause integrity checks because the total row count won't // be correct during a reflow. list.pauseIntegrityChecks(true); @@ -1761,8 +1769,12 @@ const ReflowCursor = struct { self.node, adjustment, ); - }); + }; + // We must not fail after this, we've modified our self.node + // and we need to fix it up. + errdefer comptime unreachable; + self.* = .init(node); self.cursorAbsolute(old_x, old_y); self.total_rows = old_total_rows; } @@ -1824,7 +1836,6 @@ const ReflowCursor = struct { list.pages.insertAfter(self.node, node); self.* = .init(node); - self.new_rows = new_rows; } -- 2.51.2 From f89b6433c2470f363609ffbf3fbc4fbd2dd99c58 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 16 Jan 2026 16:26:54 -0500 Subject: [PATCH 286/349] osc: add failing test for osc 133 parsing trailing ; This actually causes a crash lol, bad indexing of a slice with `1..0` because it's `key.len + 1 ..` and the length is `0`. --- src/terminal/osc/parsers/semantic_prompt.zig | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 510fe3447..811b6d055 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -348,6 +348,18 @@ test "OSC 133: prompt_start with special_key empty" { try testing.expect(cmd.prompt_start.special_key == false); } +test "OSC 133: prompt_start with trailing ;" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?.*; + try testing.expect(cmd == .prompt_start); +} + test "OSC 133: prompt_start with click_events true" { const testing = std.testing; -- 2.51.2 From 4e5c1dcdc17d67757d32faeb2bffe0e27376db00 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 16 Jan 2026 16:29:39 -0500 Subject: [PATCH 287/349] osc: fix bad indexing for empty kv in semantic prompt --- src/terminal/osc/parsers/semantic_prompt.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index 811b6d055..ac7298267 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -186,6 +186,15 @@ const SemanticPromptKVIterator = struct { break :kv kv; }; + // If we have an empty item, we return an empty key and value. + // + // This allows for trailing semicolons, but also lets us parse + // (or rather, ignore) empty fields; for example `a=b;;e=f`. + if (kv.len < 1) return .{ + .key = kv, + .value = kv, + }; + const key = key: { const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; kv[index] = 0; -- 2.51.2 From 85a3d623b2f3452a49eb634c10e46ba6746d0cd6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 13:42:34 -0800 Subject: [PATCH 288/349] terminal: increaseCapacity should reach maxInt before overflow --- src/terminal/PageList.zig | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 69edcafb1..b027f54d4 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3003,8 +3003,16 @@ pub fn increaseCapacity( // overflow it means we're out of space in this dimension, // since pages can take up to their maxInt capacity in any // category. - const new = std.math.mul(Int, old, 2) catch |err| { + const new = std.math.mul( + Int, + old, + 2, + ) catch |err| overflow: { comptime assert(@TypeOf(err) == error{Overflow}); + // Our final doubling would overflow since maxInt is + // 2^N - 1 for an unsignged int of N bits. So, if we overflow + // and we haven't used all the bits, use all the bits. + if (old < std.math.maxInt(Int)) break :overflow std.math.maxInt(Int); return error.OutOfSpace; }; @field(cap, field_name) = new; @@ -6844,18 +6852,19 @@ test "PageList increaseCapacity returns OutOfSpace at max capacity" { var s = try init(alloc, 2, 2, 0); defer s.deinit(); - // Keep increasing styles capacity until we're at more than half of max + // Keep increasing styles capacity until we get OutOfSpace const max_styles = std.math.maxInt(size.StyleCountInt); - const half_max = max_styles / 2 + 1; - while (s.pages.first.?.data.capacity.styles < half_max) { - _ = try s.increaseCapacity(s.pages.first.?, .styles); + while (true) { + _ = s.increaseCapacity( + s.pages.first.?, + .styles, + ) catch |err| { + // Before OutOfSpace, we should have reached maxInt + try testing.expectEqual(error.OutOfSpace, err); + try testing.expectEqual(max_styles, s.pages.first.?.data.capacity.styles); + break; + }; } - - // Now increaseCapacity should fail with OutOfSpace - try testing.expectError( - error.OutOfSpace, - s.increaseCapacity(s.pages.first.?, .styles), - ); } test "PageList increaseCapacity after col shrink" { -- 2.51.2 From 69066200ef24985f4f5214f0adc38ce6c40cb975 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 16 Jan 2026 16:58:58 -0500 Subject: [PATCH 289/349] fix: handle double tmux control mode exit command --- src/termio/stream_handler.zig | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index c647e3ba2..082a0fa10 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -398,11 +398,16 @@ pub const StreamHandler = struct { break :tmux; }, - .exit => if (self.tmux_viewer) |viewer| { - // Free our viewer state - viewer.deinit(); - self.alloc.destroy(viewer); - self.tmux_viewer = null; + .exit => { + // Free our viewer state if we have one + if (self.tmux_viewer) |viewer| { + viewer.deinit(); + self.alloc.destroy(viewer); + self.tmux_viewer = null; + } + + // And always break since we assert below + // that we're not handling an exit command. break :tmux; }, -- 2.51.2 From 442a395850f30a2b6fc455414359368dfbefb9fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 14:24:02 -0800 Subject: [PATCH 290/349] terminal: ensure our std_capacity fits within max page size --- src/terminal/PageList.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index b027f54d4..d8354916b 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -292,7 +292,8 @@ fn initialCapacity(cols: size.CellCountInt) Capacity { comptime { var cap = std_capacity; cap.cols = std.math.maxInt(size.CellCountInt); - _ = Page.layout(cap); + const layout = Page.layout(cap); + assert(layout.total_size <= size.max_page_size); } if (std_capacity.adjust( -- 2.51.2 From df35363b15ef1c1284ab0ad3615e43fec4777316 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 14:35:14 -0800 Subject: [PATCH 291/349] terminal: more robust handling of max_page_size not 100% --- src/terminal/PageList.zig | 19 ++++++++++++++++++- src/terminal/page.zig | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index d8354916b..3e6a39a3f 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -414,6 +414,10 @@ fn initPages( const pooled = layout.total_size <= std_size; const page_alloc = pool.pages.arena.child_allocator; + // Guaranteed by comptime checks in initialCapacity but + // redundant here for safety. + assert(layout.total_size <= size.max_page_size); + var rem = rows; while (rem > 0) { const node = try pool.nodes.create(); @@ -2091,7 +2095,7 @@ fn resizeWithoutReflowGrowCols( ) catch |err| err: { comptime assert(@TypeOf(err) == error{OutOfMemory}); - // We verify all maxed out page layouts work. + // We verify all maxed out page layouts don't overflow, var cap = page.capacity; cap.cols = cols; @@ -3017,6 +3021,14 @@ pub fn increaseCapacity( return error.OutOfSpace; }; @field(cap, field_name) = new; + + // If our capacity exceeds the maximum page size, treat it + // as an OutOfSpace because things like page splitting will + // help. + const layout = Page.layout(cap); + if (layout.total_size > size.max_page_size) { + return error.OutOfSpace; + } }, }; @@ -3091,6 +3103,11 @@ inline fn createPageExt( const pooled = layout.total_size <= std_size; const page_alloc = pool.pages.arena.child_allocator; + // It would be better to encode this into the Zig error handling + // system but that is a big undertaking and we only have a few + // centralized call sites so it is handled on its own currently. + assert(layout.total_size <= size.max_page_size); + // Our page buffer comes from our standard memory pool if it // is within our standard size since this is what the pool // dispenses. Otherwise, we use the heap allocator to allocate. diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 1150027a4..075f03b57 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -2046,6 +2046,8 @@ test "Page.layout can take a maxed capacity" { @field(cap, field.name) = std.math.maxInt(field.type); } + // Note that a max capacity will exceed our max_page_size so we + // can't init a page with it, but it should layout. _ = Page.layout(cap); } -- 2.51.2 From 464c31328e6ced1c8bbcafe42aac67097d7c505e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Jan 2026 20:43:42 -0800 Subject: [PATCH 292/349] terminal: grow prune check should not prune if required for active Fixes #10352 The bug was that non-standard pages would mix the old `growRequiredForActive` check and make our active area insufficient in the PageList. But, since scrollbars now require we have a cached `total_rows` that our safety checks always verify, we can remove the old linked list traversal and switch to some simple math in general across all page sizes. --- src/terminal/PageList.zig | 104 ++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 3e6a39a3f..c5897b7e7 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2812,18 +2812,6 @@ pub fn maxSize(self: *const PageList) usize { return @max(self.explicit_max_size, self.min_max_size); } -/// Returns true if we need to grow into our active area. -inline fn growRequiredForActive(self: *const PageList) bool { - var rows: usize = 0; - var page = self.pages.last; - while (page) |p| : (page = p.prev) { - rows += p.data.size.rows; - if (rows >= self.rows) return false; - } - - return true; -} - /// Grow the active area by exactly one row. /// /// This may allocate, but also may not if our current page has more @@ -2864,16 +2852,22 @@ pub fn grow(self: *PageList) Allocator.Error!?*List.Node { self.pages.first != self.pages.last and self.page_size + PagePool.item_size > self.maxSize()) prune: { - // If we need to add more memory to ensure our active area is - // satisfied then we do not prune. - if (self.growRequiredForActive()) break :prune; - const first = self.pages.popFirst().?; assert(first != last); // Decrease our total row count from the pruned page self.total_rows -= first.data.size.rows; + // If our total row count is now less than our required + // rows then we can't prune. The "+ 1" is because we'll add one + // more row below. + if (self.total_rows + 1 < self.rows) { + self.pages.prepend(first); + assert(self.pages.first == first); + self.total_rows += first.data.size.rows; + break :prune; + } + // 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| { @@ -2902,12 +2896,8 @@ pub fn grow(self: *PageList) Allocator.Error!?*List.Node { } self.viewport_pin.garbage = false; - // If our first node has non-standard memory size, we can't reuse - // it. This is because our initBuf below would change the underlying - // memory length which would break our memory free outside the pool. - // It is easiest in this case to prune the node. + // Non-standard pages can't be reused, just destroy them. if (first.data.memory.len > std_size) { - // Node is already removed so we can just destroy it. self.destroyNode(first); break :prune; } @@ -11585,7 +11575,7 @@ test "PageList grow reuses non-standard page without leak" { try testing.expect(s.pages.first.?.data.memory.len > std_size); // Verify we have enough rows for active area (so prune path isn't skipped) - try testing.expect(!s.growRequiredForActive()); + try testing.expect(s.totalRows() >= s.rows); // Verify last page is full (so grow will need to allocate/reuse) try testing.expect(s.pages.last.?.data.size.rows == s.pages.last.?.data.capacity.rows); @@ -11619,3 +11609,73 @@ test "PageList grow reuses non-standard page without leak" { try testing.expectEqual(0, tracked_pin.y); try testing.expect(tracked_pin.garbage); } + +test "PageList grow non-standard page prune protection" { + const testing = std.testing; + const alloc = testing.allocator; + + // This test specifically verifies the fix for the bug where pruning a + // non-standard page would cause totalRows() < self.rows. + // + // Bug trigger conditions (all must be true simultaneously): + // 1. first page is non-standard (memory.len > std_size) + // 2. page_size + PagePool.item_size > maxSize() (triggers prune consideration) + // 3. pages.first != pages.last (have multiple pages) + // 4. total_rows >= self.rows (have enough rows for active area) + // 5. total_rows - first.size.rows + 1 < self.rows (prune would lose too many) + + // This is kind of magic and likely depends on std_size. + const rows_count = 600; + var s = try init(alloc, 80, rows_count, std_size); + defer s.deinit(); + + // Make the first page non-standard + while (s.pages.first.?.data.memory.len <= std_size) { + _ = try s.increaseCapacity( + s.pages.first.?, + .grapheme_bytes, + ); + } + try testing.expect(s.pages.first.?.data.memory.len > std_size); + + const first_page_node = s.pages.first.?; + const first_page_cap = first_page_node.data.capacity.rows; + + // Fill first page to capacity + while (first_page_node.data.size.rows < first_page_cap) _ = try s.grow(); + + // Grow until we have a second page (first page fills up first) + var second_node: ?*List.Node = null; + while (s.pages.first == s.pages.last) second_node = try s.grow(); + try testing.expect(s.pages.first != s.pages.last); + + // Fill the second page to capacity so that the next grow() triggers prune + const last_node = s.pages.last.?; + const second_cap = last_node.data.capacity.rows; + while (last_node.data.size.rows < second_cap) _ = try s.grow(); + + // Now the last page is full. The next grow must either: + // 1. Prune the first page and reuse it, OR + // 2. Allocate a new page + const total = s.totalRows(); + const would_remain = total - first_page_cap + 1; + + // Verify the bug condition is present: pruning first page would leave < rows + try testing.expect(would_remain < s.rows); + + // Verify prune path conditions are met + try testing.expect(s.pages.first != s.pages.last); + try testing.expect(s.page_size + PagePool.item_size > s.maxSize()); + try testing.expect(s.totalRows() >= s.rows); + + // Verify last page is at capacity (so grow must prune or allocate new) + try testing.expectEqual(second_cap, last_node.data.size.rows); + + // The next grow should trigger prune consideration. + // Without the fix, this would destroy the non-standard first page, + // leaving only second_cap + 1 rows, which is < self.rows. + _ = try s.grow(); + + // Verify the invariant holds - the fix prevents the destructive prune + try testing.expect(s.totalRows() >= s.rows); +} -- 2.51.2 From 73a8d64b8a0410f3f57225bae52eeb8c7f0f0bbb Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 18 Jan 2026 00:16:59 +0000 Subject: [PATCH 293/349] 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 c3e2de9f8..d5c06259a 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/ghostty-themes-release-20251229-150532-f279991.tgz", - .hash = "N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV", + .url = "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz", + .hash = "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 022a7401e..b12216bd9 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz", "hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs=" }, - "N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV": { + "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7": { "name": "iterm2_themes", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz", - "hash": "sha256-bWKQxRggz/ZLr6w0Zt/hTnnAAb13VQWV70ScCsNFIZk=" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz", + "hash": "sha256-NIqF12KqXhIrP+LyBtg6WtkHxNUdWOyziAdq8S45RrU=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index d845ea10e..430619e74 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -171,11 +171,11 @@ in }; } { - name = "N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV"; + name = "N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz"; - hash = "sha256-bWKQxRggz/ZLr6w0Zt/hTnnAAb13VQWV70ScCsNFIZk="; + url = "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz"; + hash = "sha256-NIqF12KqXhIrP+LyBtg6WtkHxNUdWOyziAdq8S45RrU="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 0eeb7c5f1..72597a650 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -6,7 +6,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918 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/ghostty-themes-release-20251229-150532-f279991.tgz +https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 7749eb67e..3e2b1e26d 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://deps.files.ghostty.org/ghostty-themes-release-20251229-150532-f279991.tgz", - "dest": "vendor/p/N-V-__8AAIdIAwAO4ro1DOaG7QTFq3ewrTQIViIKJ3lKY6lV", - "sha256": "6d6290c51820cff64bafac3466dfe14e79c001bd77550595ef449c0ac3452199" + "url": "https://deps.files.ghostty.org/ghostty-themes-release-20260112-150707-28c8f5b.tgz", + "dest": "vendor/p/N-V-__8AAIdIAwDt5PxH-cwCxEcTfw4jBV8sR6fZ_XLh-cR7", + "sha256": "348a85d762aa5e122b3fe2f206d83a5ad907c4d51d58ecb388076af12e3946b5" }, { "type": "archive", -- 2.51.2 From 5423d64c6aba7c5e160a076c808cb72b39a092e3 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sat, 17 Jan 2026 20:13:05 -0500 Subject: [PATCH 294/349] ssh-cache: use AtomicFile to write the cache file We previously wrote our new cache file into a temporary directory and the (atomically) renamed it to the canonical cache file path. This rename operation unfortunately only works when both files are on the same file system, and that's not always the case (e.g. when $TMPDIR is on its own file system). Instead, we can use Zig's AtomicFile to safely perform this operation inside of the cache directory. There's a new risk of a crash leaving the temporary file around in this directory (and not getting cleaned up like $TMPDIR-based files), but the probability is low and those files will only be readable by the creating user (mode 0o600). There's a new test cash that verifies the expected AtomicFile clean up behavior. I also switched the file-oriented tests to use testing.tmpDir rather than using our application-level TempDir type. --- src/cli/ssh-cache/DiskCache.zig | 75 +++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index 62620ecb0..6214d0429 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -9,7 +9,6 @@ const assert = @import("../../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; 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 @@ -125,7 +124,7 @@ pub fn add( break :update .updated; }; - try self.writeCacheFile(alloc, entries, null); + try self.writeCacheFile(entries, null); return result; } @@ -166,7 +165,7 @@ pub fn remove( alloc.free(kv.value.terminfo_version); } - try self.writeCacheFile(alloc, entries, null); + try self.writeCacheFile(entries, null); } /// Check if a hostname exists in the cache. @@ -209,32 +208,30 @@ fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.Chm fn writeCacheFile( self: DiskCache, - alloc: Allocator, entries: std.StringHashMap(Entry), expire_days: ?u32, ) !void { - var td: TempDir = try .init(); - defer td.deinit(); + const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath; + const cache_basename = std.fs.path.basename(self.path); - const tmp_file = try td.dir.createFile("ssh-cache", .{ .mode = 0o600 }); - defer tmp_file.close(); - const tmp_path = try td.dir.realpathAlloc(alloc, "ssh-cache"); - defer alloc.free(tmp_path); + var dir = try std.fs.cwd().openDir(cache_dir, .{}); + defer dir.close(); var buf: [1024]u8 = undefined; - var writer = tmp_file.writer(&buf); + var atomic_file = try dir.atomicFile(cache_basename, .{ + .mode = 0o600, + .write_buffer = &buf, + }); + defer atomic_file.deinit(); + 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.interface); + try kv.value_ptr.format(&atomic_file.file_writer.interface); } - // Don't forget to flush!! - try writer.interface.flush(); - - // Atomic replace - try std.fs.renameAbsolute(tmp_path, self.path); + try atomic_file.finish(); } /// List all entries in the cache. @@ -382,16 +379,16 @@ test "disk cache clear" { const alloc = testing.allocator; // Create our path - var td: TempDir = try .init(); - defer td.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try td.dir.createFile("cache", .{}); + var file = try tmp.dir.createFile("cache", .{}); defer file.close(); var file_writer = file.writer(&buf); try file_writer.interface.writeAll("HELLO!"); } - const path = try td.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); // Setup our cache @@ -401,7 +398,7 @@ test "disk cache clear" { // Verify the file is gone try testing.expectError( error.FileNotFound, - td.dir.openFile("cache", .{}), + tmp.dir.openFile("cache", .{}), ); } @@ -410,18 +407,18 @@ test "disk cache operations" { const alloc = testing.allocator; // Create our path - var td: TempDir = try .init(); - defer td.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try td.dir.createFile("cache", .{}); + var file = try tmp.dir.createFile("cache", .{}); defer file.close(); 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"); + const path = try tmp.dir.realpathAlloc(alloc, "cache"); defer alloc.free(path); // Setup our cache @@ -453,6 +450,32 @@ test "disk cache operations" { ); } +test "disk cache cleans up temp files" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp = testing.tmpDir(.{ .iterate = true }); + defer tmp.cleanup(); + + const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + defer alloc.free(tmp_path); + const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); + defer alloc.free(cache_path); + + const cache: DiskCache = .{ .path = cache_path }; + try testing.expectEqual(AddResult.added, try cache.add(alloc, "example.com")); + try testing.expectEqual(AddResult.added, try cache.add(alloc, "example.org")); + + // Verify only the cache file exists and no temp files left behind + var count: usize = 0; + var iter = tmp.dir.iterate(); + while (try iter.next()) |entry| { + count += 1; + try testing.expectEqualStrings("cache", entry.name); + } + try testing.expectEqual(1, count); +} + test isValidHost { const testing = std.testing; -- 2.51.2 From b0c868811d8233d33f8efb48dd4f2f558951a043 Mon Sep 17 00:00:00 2001 From: shivaduke28 Date: Sun, 18 Jan 2026 16:22:11 +0900 Subject: [PATCH 295/349] use underline instead of inverting colors for preedit text --- src/renderer/generic.zig | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 5fa3432a6..4181badc3 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3370,10 +3370,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { screen_bg: terminal.color.RGB, screen_fg: terminal.color.RGB, ) !void { - // Preedit is rendered inverted - const bg = screen_fg; - const fg = screen_bg; - // Render the glyph for our preedit text const render_ = self.font_grid.renderCodepoint( self.alloc, @@ -3392,11 +3388,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Add our opaque background cell self.cells.bgCell(coord.y, coord.x).* = .{ - bg.r, bg.g, bg.b, 255, + screen_bg.r, screen_bg.g, screen_bg.b, 255, }; if (cp.wide and coord.x < self.cells.size.columns - 1) { self.cells.bgCell(coord.y, coord.x + 1).* = .{ - bg.r, bg.g, bg.b, 255, + screen_bg.r, screen_bg.g, screen_bg.b, 255, }; } @@ -3404,7 +3400,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { try self.cells.add(self.alloc, .text, .{ .atlas = .grayscale, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .color = .{ fg.r, fg.g, fg.b, 255 }, + .color = .{ screen_fg.r, screen_fg.g, screen_fg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @@ -3412,6 +3408,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { @intCast(render.glyph.offset_y), }, }); + + // Add underline + try self.addUnderline(@intCast(coord.x), @intCast(coord.y), .single, screen_fg, 255); + if (cp.wide and coord.x < self.cells.size.columns - 1) { + try self.addUnderline(@intCast(coord.x + 1), @intCast(coord.y), .single, screen_fg, 255); + } } /// Sync the atlas data to the given texture. This copies the bytes -- 2.51.2 From c8f56ddaf8cd9b909de471f88d5721ee7a5ee83a Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Sun, 18 Jan 2026 10:08:51 -0800 Subject: [PATCH 296/349] feat(macos): if the search box is empty directly close the box --- macos/Sources/Ghostty/Surface View/SurfaceView.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 524cb1298..a0e735715 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -431,7 +431,12 @@ extension Ghostty { } #if canImport(AppKit) .onExitCommand { - Ghostty.moveFocus(to: surfaceView) + if searchState.needle.isEmpty { + Ghostty.moveFocus(to: surfaceView) + onClose() + } else { + Ghostty.moveFocus(to: surfaceView) + } } #endif .backport.onKeyPress(.return) { modifiers in -- 2.51.2 From 3ee30058ab8eeeefa9b6503693abd0f3e1328ae7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Jan 2026 14:43:19 -0800 Subject: [PATCH 297/349] terminal: increaseCapacity should preserve dirty flag This never caused any known issues, but it's a bug! `increaseCapacity` should produce a node with identical contents, just more capacity. We were forgetting to copy over the dirty flag. I looked back at `adjustCapacity` and it also didn't preserve the dirty flag so presumably downstream consumers have been handling this case manually. But, I think semantically it makes sense for `increaseCapacity` to preserve the dirty flag. This bug was found by AI (while I was doing another task). I fixed it and wrote the test by hand though. --- src/terminal/PageList.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index c5897b7e7..ea6168caa 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3045,6 +3045,9 @@ pub fn increaseCapacity( @panic("unexpected clone failure"); }; + // Preserve page-level dirty flag (cloneFrom only copies row data) + new_page.dirty = page.dirty; + // Must not fail after this because the operations we do after this // can't be recovered. errdefer comptime unreachable; @@ -6942,6 +6945,37 @@ test "PageList increaseCapacity multi-page" { ); } +test "PageList increaseCapacity preserves dirty flag" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 2, 4, 0); + defer s.deinit(); + + // Set page dirty flag and mark some rows as dirty + const page = &s.pages.first.?.data; + page.dirty = true; + + const rows = page.rows.ptr(page.memory); + rows[0].dirty = true; + rows[1].dirty = false; + rows[2].dirty = true; + rows[3].dirty = false; + + // Increase capacity + const new_node = try s.increaseCapacity(s.pages.first.?, .styles); + + // The page dirty flag should be preserved + try testing.expect(new_node.data.dirty); + + // Row dirty flags should be preserved + const new_rows = new_node.data.rows.ptr(new_node.data.memory); + try testing.expect(new_rows[0].dirty); + try testing.expect(!new_rows[1].dirty); + try testing.expect(new_rows[2].dirty); + try testing.expect(!new_rows[3].dirty); +} + test "PageList pageIterator single page" { const testing = std.testing; const alloc = testing.allocator; -- 2.51.2 From 836d794b9e686cf17ec419c04dd2ad94bef16037 Mon Sep 17 00:00:00 2001 From: Tobias Kohlbau Date: Tue, 25 Nov 2025 22:19:57 +0100 Subject: [PATCH 298/349] termio: report color scheme synchronously The reporting of color scheme was handled asynchronously by queuing a handler in the surface. This could lead to race conditions where the DSR is reported after subsequent VT sequences. Fixes #5922 --- src/Surface.zig | 24 +----------------------- src/apprt/surface.zig | 5 ----- src/termio/Termio.zig | 21 +++++++++++++++++++++ src/termio/Thread.zig | 1 + src/termio/message.zig | 6 ++++++ src/termio/stream_handler.zig | 6 +++--- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 4103b91fb..5ca75d865 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1073,8 +1073,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), - .report_color_scheme => |force| self.reportColorScheme(force), - .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), @@ -1386,26 +1384,6 @@ fn passwordInput(self: *Surface, v: bool) !void { try self.queueRender(); } -/// Sends a DSR response for the current color scheme to the pty. If -/// force is false then we only send the response if the terminal mode -/// 2031 is enabled. -fn reportColorScheme(self: *Surface, force: bool) void { - if (!force) { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - if (!self.renderer_state.terminal.modes.get(.report_color_scheme)) { - return; - } - } - - const output = switch (self.config_conditional_state.theme) { - .light => "\x1B[?997;2n", - .dark => "\x1B[?997;1n", - }; - - self.queueIo(.{ .write_stable = output }, .unlocked); -} - fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void { // IMPORTANT: This function is run on the SEARCH THREAD! It is NOT SAFE // to access anything other than values that never change on the surface. @@ -5039,7 +5017,7 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void { self.notifyConfigConditionalState(); // If mode 2031 is on, then we report the change live. - self.reportColorScheme(false); + self.queueIo(.{ .color_scheme_report = .{ .force = false } }, .unlocked); } pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate { diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index be2f59149..5c25281c8 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -63,11 +63,6 @@ pub const Message = union(enum) { /// Health status change for the renderer. renderer_health: renderer.Health, - /// Report the color scheme. The bool parameter is whether to force or not. - /// If force is true, the color scheme should be reported even if mode - /// 2031 is not set. - report_color_scheme: bool, - /// Tell the surface to present itself to the user. This may require raising /// a window and switching tabs. present_surface: void, diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 7263418a7..a1bcea6d3 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -165,6 +165,7 @@ pub const DerivedConfig = struct { osc_color_report_format: configpkg.Config.OSCColorReportFormat, clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, + conditional_state: configpkg.ConditionalState, pub fn init( alloc_gpa: Allocator, @@ -185,6 +186,7 @@ pub const DerivedConfig = struct { .osc_color_report_format = config.@"osc-color-report-format", .clipboard_write = config.@"clipboard-write", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), + .conditional_state = config._conditional_state, // This has to be last so that we copy AFTER the arena allocations // above happen (Zig assigns in order). @@ -712,6 +714,25 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void { } } +/// Sends a DSR response for the current color scheme to the pty. +pub fn colorSchemeReport(self: *Termio, td: *ThreadData, force: bool) !void { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + + try self.colorSchemeReportLocked(td, force); +} + +pub fn colorSchemeReportLocked(self: *Termio, td: *ThreadData, force: bool) !void { + if (!force and !self.renderer_state.terminal.modes.get(.report_color_scheme)) { + return; + } + const output = switch (self.config.conditional_state.theme) { + .light => "\x1B[?997;2n", + .dark => "\x1B[?997;1n", + }; + try self.queueWrite(td, output, false); +} + /// ThreadData is the data created and stored in the termio thread /// when the thread is started and destroyed when the thread is /// stopped. diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index b111d5a52..6aa5e1c26 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -311,6 +311,7 @@ fn drainMailbox( log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { + .color_scheme_report => |v| try io.colorSchemeReport(data, v.force), .crash => @panic("crash request, crashing intentionally"), .change_config => |config| { defer config.alloc.destroy(config.ptr); diff --git a/src/termio/message.zig b/src/termio/message.zig index f78da2058..d7a59bf5e 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -16,6 +16,12 @@ pub const Message = union(enum) { /// in the future. pub const WriteReq = MessageData(u8, 38); + /// Request a color scheme report is sent to the pty. + color_scheme_report: struct { + /// Force write the current color scheme + force: bool, + }, + /// Purposely crash the renderer. This is used for testing and debugging. /// See the "crash" binding action. crash: void, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 082a0fa10..75f6da57e 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -119,7 +119,7 @@ pub const StreamHandler = struct { }; // The config could have changed any of our colors so update mode 2031 - self.surfaceMessageWriter(.{ .report_color_scheme = false }); + self.messageWriter(.{ .color_scheme_report = .{ .force = false } }); } inline fn surfaceMessageWriter( @@ -871,7 +871,7 @@ pub const StreamHandler = struct { self.messageWriter(msg); }, - .color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = true }), + .color_scheme => self.messageWriter(.{ .color_scheme_report = .{ .force = true } }), } } @@ -956,7 +956,7 @@ pub const StreamHandler = struct { try self.setMouseShape(.text); // Reset resets our palette so we report it for mode 2031. - self.surfaceMessageWriter(.{ .report_color_scheme = false }); + self.messageWriter(.{ .color_scheme_report = .{ .force = false } }); // Clear the progress bar self.progressReport(.{ .state = .remove }); -- 2.51.2 From 1daba40247432a20f14de3fe99ede98bad199b66 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 19 Jan 2026 10:53:54 -0600 Subject: [PATCH 299/349] osc 133: handle bare keys with no '=' Fixes #10379 --- src/terminal/osc/parsers/semantic_prompt.zig | 114 ++++++++++++++----- 1 file changed, 85 insertions(+), 29 deletions(-) diff --git a/src/terminal/osc/parsers/semantic_prompt.zig b/src/terminal/osc/parsers/semantic_prompt.zig index ac7298267..652fe34da 100644 --- a/src/terminal/osc/parsers/semantic_prompt.zig +++ b/src/terminal/osc/parsers/semantic_prompt.zig @@ -33,64 +33,69 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { return null; }; while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "aid")) { + const key = kv.key orelse continue; + if (std.mem.eql(u8, key, "aid")) { parser.command.prompt_start.aid = kv.value; - } else if (std.mem.eql(u8, kv.key, "redraw")) redraw: { + } else if (std.mem.eql(u8, key, "redraw")) redraw: { // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers // 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. parser.command.prompt_start.redraw = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { + const value = kv.value orelse break :value null; + if (value.len != 1) break :value null; + switch (value[0]) { '0' => break :value false, '1' => break :value true, else => break :value null, } }) orelse { - log.info("OSC 133 A: invalid redraw value: {s}", .{kv.value}); + log.info("OSC 133 A: invalid redraw value: {?s}", .{kv.value}); break :redraw; }; - } else if (std.mem.eql(u8, kv.key, "special_key")) redraw: { + } else if (std.mem.eql(u8, key, "special_key")) redraw: { // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers parser.command.prompt_start.special_key = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { + const value = kv.value orelse break :value null; + if (value.len != 1) break :value null; + switch (value[0]) { '0' => break :value false, '1' => break :value true, else => break :value null, } }) orelse { - log.info("OSC 133 A invalid special_key value: {s}", .{kv.value}); + log.info("OSC 133 A invalid special_key value: {?s}", .{kv.value}); break :redraw; }; - } else if (std.mem.eql(u8, kv.key, "click_events")) redraw: { + } else if (std.mem.eql(u8, key, "click_events")) redraw: { // https://sw.kovidgoyal.net/kitty/shell-integration/#notes-for-shell-developers parser.command.prompt_start.click_events = (value: { - if (kv.value.len != 1) break :value null; - switch (kv.value[0]) { + const value = kv.value orelse break :value null; + if (value.len != 1) break :value null; + switch (value[0]) { '0' => break :value false, '1' => break :value true, else => break :value null, } }) orelse { - log.info("OSC 133 A invalid click_events value: {s}", .{kv.value}); + log.info("OSC 133 A invalid click_events value: {?s}", .{kv.value}); break :redraw; }; - } else if (std.mem.eql(u8, kv.key, "k")) k: { + } else if (std.mem.eql(u8, key, "k")) k: { // 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. - if (kv.value.len != 1) break :k; - parser.command.prompt_start.kind = switch (kv.value[0]) { + const value = kv.value orelse break :k; + if (value.len != 1) break :k; + parser.command.prompt_start.kind = switch (value[0]) { 'c' => .continuation, 's' => .secondary, 'r' => .right, 'i' => .primary, else => .primary, }; - } else log.info("OSC 133 A: unknown semantic prompt option: {s}", .{kv.key}); + } else log.info("OSC 133 A: unknown semantic prompt option: {?s}", .{kv.key}); } }, 'B' => prompt_end: { @@ -105,7 +110,7 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { return null; }; while (it.next()) |kv| { - log.info("OSC 133 B: unknown semantic prompt option: {s}", .{kv.key}); + log.info("OSC 133 B: unknown semantic prompt option: {?s}", .{kv.key}); } }, 'C' => end_of_input: { @@ -122,12 +127,13 @@ pub fn parse(parser: *Parser, _: ?u8) ?*Command { return null; }; while (it.next()) |kv| { - if (std.mem.eql(u8, kv.key, "cmdline")) { - parser.command.end_of_input.cmdline = string_encoding.printfQDecode(kv.value) catch null; - } else if (std.mem.eql(u8, kv.key, "cmdline_url")) { - parser.command.end_of_input.cmdline = string_encoding.urlPercentDecode(kv.value) catch null; + const key = kv.key orelse continue; + if (std.mem.eql(u8, key, "cmdline")) { + parser.command.end_of_input.cmdline = if (kv.value) |value| string_encoding.printfQDecode(value) catch null else null; + } else if (std.mem.eql(u8, key, "cmdline_url")) { + parser.command.end_of_input.cmdline = if (kv.value) |value| string_encoding.urlPercentDecode(value) catch null else null; } else { - log.info("OSC 133 C: unknown semantic prompt option: {s}", .{kv.key}); + log.info("OSC 133 C: unknown semantic prompt option: {s}", .{key}); } } }, @@ -159,8 +165,8 @@ const SemanticPromptKVIterator = struct { string: []u8, pub const SemanticPromptKV = struct { - key: [:0]u8, - value: [:0]u8, + key: ?[:0]u8, + value: ?[:0]u8, }; pub fn init(writer: *std.Io.Writer) std.Io.Writer.Error!SemanticPromptKVIterator { @@ -186,17 +192,24 @@ const SemanticPromptKVIterator = struct { break :kv kv; }; - // If we have an empty item, we return an empty key and value. + // If we have an empty item, we return a null key and value. // // This allows for trailing semicolons, but also lets us parse // (or rather, ignore) empty fields; for example `a=b;;e=f`. if (kv.len < 1) return .{ - .key = kv, - .value = kv, + .key = null, + .value = null, }; const key = key: { - const index = std.mem.indexOfScalar(u8, kv, '=') orelse break :key kv; + const index = std.mem.indexOfScalar(u8, kv, '=') orelse { + // If there is no '=' return entire `kv` string as the key and + // a null value. + return .{ + .key = kv, + .value = null, + }; + }; kv[index] = 0; const key = kv[0..index :0]; break :key key; @@ -408,6 +421,36 @@ test "OSC 133: prompt_start with click_events empty" { try testing.expect(cmd.prompt_start.click_events == false); } +test "OSC 133: prompt_start with click_events bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + 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: prompt_start with invalid bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;A;barekey"; + 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.expectEqual(.primary, cmd.prompt_start.kind); + try testing.expect(cmd.prompt_start.redraw == true); + try testing.expect(cmd.prompt_start.special_key == false); + try testing.expect(cmd.prompt_start.click_events == false); +} + test "OSC 133: end_of_command no exit code" { const testing = std.testing; @@ -713,3 +756,16 @@ test "OSC 133: end_of_input with cmdline_url 9" { try testing.expect(cmd == .end_of_input); try testing.expect(cmd.end_of_input.cmdline == null); } + +test "OSC 133: end_of_input with bare key" { + const testing = std.testing; + + var p: Parser = .init(null); + + const input = "133;C;cmdline_url"; + 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); +} -- 2.51.2 From d67dd08a21bdacb1cca0fcb73cdff392083ca8b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 Jan 2026 07:09:02 -0800 Subject: [PATCH 300/349] terminal: remap tracked pins in backfill path during resize Fixes #10369 When `resizeWithoutReflowGrowCols` copies rows to a previous page with spare capacity, tracked pins pointing to those rows were not being remapped. This left pins pointing to the original page which was subsequently destroyed. The fix adds pin remapping for rows copied to the previous page, matching the existing remapping logic for rows copied to new pages. I also added new integrity checks to verify that our tracked pins are always valid at points where internal operations complete. Thanks to @grishy for finding this! --- src/terminal/PageList.zig | 106 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index ea6168caa..b4f51a7c9 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -485,10 +485,11 @@ pub inline fn pauseIntegrityChecks(self: *PageList, pause: bool) void { } const IntegrityError = error{ + PageSerialInvalid, TotalRowsMismatch, + TrackedPinInvalid, ViewportPinOffsetMismatch, ViewportPinInsufficientRows, - PageSerialInvalid, }; /// Verify the integrity of the PageList. This is expensive and should @@ -529,6 +530,11 @@ fn verifyIntegrity(self: *const PageList) IntegrityError!void { return IntegrityError.TotalRowsMismatch; } + // Verify that all our tracked pins point to valid pages. + for (self.tracked_pins.keys()) |p| { + if (!self.pinIsValid(p.*)) return error.TrackedPinInvalid; + } + if (self.viewport == .pin) { // Verify that our viewport pin row offset is correct. const actual_offset: usize = offset: { @@ -750,8 +756,8 @@ pub fn clone( ); errdefer pool.deinit(); - // Our viewport pin is always undefined since our viewport in a clones - // goes back to the top + // Create our viewport. In a clone, the viewport always goes + // to the top. const viewport_pin = try pool.pins.create(); var tracked_pins: PinSet = .{}; errdefer tracked_pins.deinit(pool.alloc); @@ -817,6 +823,10 @@ pub fn clone( } } + // Initialize our viewport pin to point to the first cloned page + // so it points to valid memory. + viewport_pin.* = .{ .node = page_list.first.? }; + var result: PageList = .{ .pool = pool, .pages = page_list, @@ -1259,9 +1269,26 @@ const ReflowCursor = struct { )) |result| switch (result) { // Wrote the cell, move to the next. .success => x += 1, + // Wrote the cell but request to skip the next so skip it. // This is used for things like spacers. - .skip_next => x += 2, + .skip_next => { + // Remap any tracked pins at the skipped position (x+1) + // since we won't process that cell in the loop. + const pin_keys = list.tracked_pins.keys(); + for (pin_keys) |p| { + if (&p.node.data != src_page or + p.y != src_y or + p.x != x + 1) continue; + + p.node = self.node; + p.x = self.x; + p.y = self.y; + } + + x += 2; + }, + // Didn't write the cell, repeat writing this same cell. .repeat => {}, } else |err| switch (err) { @@ -2167,6 +2194,14 @@ fn resizeWithoutReflowGrowCols( assert(copied == len); assert(prev_page.size.rows <= prev_page.capacity.rows); + + // Remap any tracked pins that pointed to rows we just copied to prev. + const pin_keys = self.tracked_pins.keys(); + for (pin_keys) |p| { + if (p.node != chunk.node or p.y >= len) continue; + p.node = prev_node; + p.y += prev_page.size.rows - len; + } } // If we have an error, we clear the rows we just added to our prev page. @@ -11713,3 +11748,66 @@ test "PageList grow non-standard page prune protection" { // Verify the invariant holds - the fix prevents the destructive prune try testing.expect(s.totalRows() >= s.rows); } + +test "PageList resize (no reflow) more cols remaps pins in backfill path" { + // Regression test: when resizeWithoutReflowGrowCols copies rows to a previous + // page with spare capacity, tracked pins in those rows must be remapped. + // Without the fix, pins become dangling pointers when the original page is destroyed. + const testing = std.testing; + const alloc = testing.allocator; + + const cols: size.CellCountInt = 5; + const cap = try std_capacity.adjust(.{ .cols = cols }); + var s = try init(alloc, cols, cap.rows, null); + defer s.deinit(); + + // Grow until we have two pages. + while (s.pages.first == s.pages.last) { + _ = try s.grow(); + } + const first_page = s.pages.first.?; + const second_page = s.pages.last.?; + try testing.expect(first_page != second_page); + + // Trim a history row so the first page has spare capacity. + // This triggers the backfill path in resizeWithoutReflowGrowCols. + s.eraseRows(.{ .history = .{} }, .{ .history = .{ .y = 0 } }); + try testing.expect(first_page.data.size.rows < first_page.data.capacity.rows); + + // Ensure the resize takes the slow path (new capacity > current capacity). + const new_cols: size.CellCountInt = cols + 1; + const adjusted = try second_page.data.capacity.adjust(.{ .cols = new_cols }); + try testing.expect(second_page.data.capacity.cols < adjusted.cols); + + // Track a pin in row 0 of the second page. This row will be copied + // to the first page during backfill and the pin must be remapped. + const tracked = try s.trackPin(.{ .node = second_page, .x = 0, .y = 0 }); + defer s.untrackPin(tracked); + + // Write a marker character to the tracked cell so we can verify + // the pin points to the correct cell after resize. + const marker: u21 = 'X'; + tracked.rowAndCell().cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = marker }, + }; + + try s.resize(.{ .cols = new_cols, .reflow = false }); + + // Verify the pin points to a valid node still in the page list. + var found = false; + var it = s.pages.first; + while (it) |node| : (it = node.next) { + if (node == tracked.node) { + found = true; + break; + } + } + try testing.expect(found); + try testing.expect(tracked.y < tracked.node.data.size.rows); + + // Verify the pin still points to the cell with our marker content. + const cell = tracked.rowAndCell().cell; + try testing.expectEqual(.codepoint, cell.content_tag); + try testing.expectEqual(marker, cell.content.codepoint); +} -- 2.51.2 From 93436217c8315d660214ef105b7eb59e92095342 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Jan 2026 07:21:41 -0800 Subject: [PATCH 301/349] terminal: page.exactRowCapacity --- src/terminal/bitmap_allocator.zig | 50 +++ src/terminal/page.zig | 628 +++++++++++++++++++++++++++++- src/terminal/ref_counted_set.zig | 17 +- 3 files changed, 688 insertions(+), 7 deletions(-) diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index 258d73071..23a5048e1 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -63,6 +63,14 @@ pub fn BitmapAllocator(comptime chunk_size: comptime_int) type { }; } + /// Returns the number of bytes required to allocate n elements of + /// type T. This accounts for the chunk size alignment used by the + /// bitmap allocator. + pub fn bytesRequired(comptime T: type, n: usize) usize { + const byte_count = @sizeOf(T) * n; + return alignForward(usize, byte_count, chunk_size); + } + /// Allocate n elements of type T. This will return error.OutOfMemory /// if there isn't enough space in the backing buffer. /// @@ -955,3 +963,45 @@ test "BitmapAllocator alloc and free two 1.5 bitmaps offset 0.75" { bm.bitmap.ptr(buf)[0..4], ); } + +test "BitmapAllocator bytesRequired" { + const testing = std.testing; + + // Chunk size of 16 bytes (like grapheme_chunk in page.zig) + { + const Alloc = BitmapAllocator(16); + + // Single byte rounds up to chunk size + try testing.expectEqual(16, Alloc.bytesRequired(u8, 1)); + try testing.expectEqual(16, Alloc.bytesRequired(u8, 16)); + try testing.expectEqual(32, Alloc.bytesRequired(u8, 17)); + + // u21 (4 bytes each) + try testing.expectEqual(16, Alloc.bytesRequired(u21, 1)); // 4 bytes -> 16 + try testing.expectEqual(16, Alloc.bytesRequired(u21, 4)); // 16 bytes -> 16 + try testing.expectEqual(32, Alloc.bytesRequired(u21, 5)); // 20 bytes -> 32 + try testing.expectEqual(32, Alloc.bytesRequired(u21, 6)); // 24 bytes -> 32 + } + + // Chunk size of 4 bytes + { + const Alloc = BitmapAllocator(4); + + try testing.expectEqual(4, Alloc.bytesRequired(u8, 1)); + try testing.expectEqual(4, Alloc.bytesRequired(u8, 4)); + try testing.expectEqual(8, Alloc.bytesRequired(u8, 5)); + + // u32 (4 bytes each) - exactly one chunk per element + try testing.expectEqual(4, Alloc.bytesRequired(u32, 1)); + try testing.expectEqual(8, Alloc.bytesRequired(u32, 2)); + } + + // Chunk size of 32 bytes (like string_chunk in page.zig) + { + const Alloc = BitmapAllocator(32); + + try testing.expectEqual(32, Alloc.bytesRequired(u8, 1)); + try testing.expectEqual(32, Alloc.bytesRequired(u8, 32)); + try testing.expectEqual(64, Alloc.bytesRequired(u8, 33)); + } +} diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 075f03b57..6a5958681 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -633,6 +633,114 @@ pub const Page = struct { HyperlinkError || GraphemeError; + /// Compute the exact capacity required to store a range of rows from + /// this page. + /// + /// The returned capacity will have the same number of columns as this + /// page and the number of rows equal to the range given. The returned + /// capacity is by definition strictly less than or equal to this + /// page's capacity, so the layout is guaranteed to succeed. + /// + /// Preconditions: + /// - Range must be at least 1 row + /// - Start and end must be valid for this page + pub fn exactRowCapacity( + self: *const Page, + y_start: usize, + y_end: usize, + ) Capacity { + assert(y_start < y_end); + assert(y_end <= self.size.rows); + + // Track unique IDs using a bitset. Both style IDs and hyperlink IDs + // are CellCountInt (u16), so we reuse this set for both to save + // stack memory (~8KB instead of ~16KB). + const CellCountSet = std.StaticBitSet(std.math.maxInt(size.CellCountInt) + 1); + comptime assert(size.StyleCountInt == size.CellCountInt); + comptime assert(size.HyperlinkCountInt == size.CellCountInt); + + // Accumulators + var id_set: CellCountSet = .initEmpty(); + var grapheme_bytes: usize = 0; + var string_bytes: usize = 0; + + // First pass: count styles and grapheme bytes + const rows = self.rows.ptr(self.memory)[y_start..y_end]; + for (rows) |*row| { + const cells = row.cells.ptr(self.memory)[0..self.size.cols]; + for (cells) |*cell| { + if (cell.style_id != stylepkg.default_id) { + id_set.set(cell.style_id); + } + + if (cell.hasGrapheme()) { + if (self.lookupGrapheme(cell)) |cps| { + grapheme_bytes += GraphemeAlloc.bytesRequired(u21, cps.len); + } + } + } + } + const styles_cap = StyleSet.capacityForCount(id_set.count()); + + // Second pass: count hyperlinks and string bytes + // We count both unique hyperlinks (for hyperlink_set) and total + // hyperlink cells (for hyperlink_map capacity). + id_set = .initEmpty(); + var hyperlink_cells: usize = 0; + for (rows) |*row| { + const cells = row.cells.ptr(self.memory)[0..self.size.cols]; + for (cells) |*cell| { + if (cell.hyperlink) { + hyperlink_cells += 1; + if (self.lookupHyperlink(cell)) |id| { + // Only count each unique hyperlink once for set sizing + if (!id_set.isSet(id)) { + id_set.set(id); + + // Get the hyperlink entry to compute string bytes + const entry = self.hyperlink_set.get(self.memory, id); + string_bytes += StringAlloc.bytesRequired(u8, entry.uri.len); + + switch (entry.id) { + .implicit => {}, + .explicit => |slice| { + string_bytes += StringAlloc.bytesRequired(u8, slice.len); + }, + } + } + } + } + } + } + + // The hyperlink_map capacity in layout() is computed as: + // hyperlink_count * hyperlink_cell_multiplier (rounded to power of 2) + // We need enough hyperlink_bytes so that when layout() computes + // the map capacity, it can accommodate all hyperlink cells. This + // is unit tested. + const hyperlink_cap = cap: { + const hyperlink_count = id_set.count(); + const hyperlink_set_cap = hyperlink.Set.capacityForCount(hyperlink_count); + const hyperlink_map_min = std.math.divCeil( + usize, + hyperlink_cells, + hyperlink_cell_multiplier, + ) catch 0; + break :cap @max(hyperlink_set_cap, hyperlink_map_min); + }; + + // All the intCasts below are safe because we should have a + // capacity strictly less than or equal to this page's capacity. + return .{ + .cols = self.size.cols, + .rows = @intCast(y_end - y_start), + .styles = @intCast(styles_cap), + .grapheme_bytes = @intCast(grapheme_bytes), + .hyperlink_bytes = @intCast(hyperlink_cap * @sizeOf(hyperlink.Set.Item)), + .string_bytes = @intCast(string_bytes), + }; + } + /// Clone the contents of another page into this page. The capacities /// can be different, but the size of the other page must fit into /// this page. @@ -1569,10 +1677,13 @@ pub const Page = struct { 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 = std.math.ceilPowerOfTwo( - usize, - @divFloor(cap.grapheme_bytes, grapheme_chunk), - ) catch unreachable; + const grapheme_count: usize = count: { + if (cap.grapheme_bytes == 0) break :count 0; + // Use divCeil to match GraphemeAlloc.layout() which uses alignForward, + // ensuring grapheme_map has capacity when grapheme_alloc has chunks. + const base = std.math.divCeil(usize, cap.grapheme_bytes, grapheme_chunk) catch unreachable; + break :count std.math.ceilPowerOfTwo(usize, base) catch unreachable; + }; const grapheme_map_layout = GraphemeMap.layout(@intCast(grapheme_count)); 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; @@ -3217,3 +3328,512 @@ test "Page verifyIntegrity zero cols" { page.verifyIntegrity(testing.allocator), ); } + +test "Page exactRowCapacity empty rows" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // Empty page: all capacity fields should be 0 (except cols/rows) + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(10, cap.cols); + try testing.expectEqual(5, cap.rows); + try testing.expectEqual(0, cap.styles); + try testing.expectEqual(0, cap.grapheme_bytes); + try testing.expectEqual(0, cap.hyperlink_bytes); + try testing.expectEqual(0, cap.string_bytes); +} + +test "Page exactRowCapacity styles" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // No styles: capacity should be 0 + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(0, cap.styles); + } + + // Add one style to a cell + const style1_id = try page.styles.add(page.memory, .{ .flags = .{ .bold = true } }); + { + const rac = page.getRowAndCell(0, 0); + rac.row.styled = true; + rac.cell.style_id = style1_id; + } + + // One unique style - capacity accounts for load factor + const cap_one_style = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(StyleSet.capacityForCount(1), cap_one_style.styles); + } + + // Add same style to another cell (duplicate) - capacity unchanged + { + const rac = page.getRowAndCell(1, 0); + rac.cell.style_id = style1_id; + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_one_style.styles, cap.styles); + } + + // Add a different style + const style2_id = try page.styles.add(page.memory, .{ .flags = .{ .italic = true } }); + { + const rac = page.getRowAndCell(2, 0); + rac.cell.style_id = style2_id; + } + + // Two unique styles - capacity accounts for load factor + const cap_two_styles = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(StyleSet.capacityForCount(2), cap_two_styles.styles); + try testing.expect(cap_two_styles.styles > cap_one_style.styles); + } + + // Style outside the row range should not be counted + { + const rac = page.getRowAndCell(0, 7); + rac.row.styled = true; + rac.cell.style_id = try page.styles.add(page.memory, .{ .flags = .{ .underline = .single } }); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_two_styles.styles, cap.styles); + } + + // Full range includes the new style + { + const cap = page.exactRowCapacity(0, 10); + try testing.expectEqual(StyleSet.capacityForCount(3), cap.styles); + } + + // Verify clone works with exact capacity and produces same result + { + const cap = page.exactRowCapacity(0, 5); + var cloned = try Page.init(cap); + defer cloned.deinit(); + for (0..5) |y| { + const src_row = &page.rows.ptr(page.memory)[y]; + const dst_row = &cloned.rows.ptr(cloned.memory)[y]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + } + const cloned_cap = cloned.exactRowCapacity(0, 5); + try testing.expectEqual(cap, cloned_cap); + } +} + +test "Page exactRowCapacity single style clone" { + // Regression test: verify a single style can be cloned with exact capacity. + // This tests that capacityForCount properly accounts for ID 0 being reserved. + var page = try Page.init(.{ + .cols = 10, + .rows = 2, + .styles = 8, + }); + defer page.deinit(); + + // Add exactly one style to row 0 + const style_id = try page.styles.add(page.memory, .{ .flags = .{ .bold = true } }); + { + const rac = page.getRowAndCell(0, 0); + rac.row.styled = true; + rac.cell.style_id = style_id; + } + + // exactRowCapacity for just row 0 should give capacity for 1 style + const cap = page.exactRowCapacity(0, 1); + try testing.expectEqual(StyleSet.capacityForCount(1), cap.styles); + + // Create a new page with exact capacity and clone + var cloned = try Page.init(cap); + defer cloned.deinit(); + + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + + // This must not fail with StyleSetOutOfMemory + try cloned.cloneRowFrom(&page, dst_row, src_row); + + // Verify the style was cloned correctly + const cloned_cell = &cloned.rows.ptr(cloned.memory)[0].cells.ptr(cloned.memory)[0]; + try testing.expect(cloned_cell.style_id != stylepkg.default_id); +} + +test "Page exactRowCapacity styles max single row" { + var page = try Page.init(.{ + .cols = std.math.maxInt(size.CellCountInt), + .rows = 1, + .styles = std.math.maxInt(size.StyleCountInt), + }); + defer page.deinit(); + + // Style our first row + const row = &page.rows.ptr(page.memory)[0]; + row.styled = true; + + // Fill cells with styles until we get OOM, but limit to a reasonable count + // to avoid overflow when computing capacityForCount near maxInt + const cells = row.cells.ptr(page.memory)[0..page.size.cols]; + var count: usize = 0; + const max_count: usize = 1000; // Limit to avoid overflow in capacity calculation + for (cells, 0..) |*cell, i| { + if (count >= max_count) break; + const style_id = page.styles.add(page.memory, .{ + .fg_color = .{ .rgb = .{ + .r = @intCast(i & 0xFF), + .g = @intCast((i >> 8) & 0xFF), + .b = 0, + } }, + }) catch break; + cell.style_id = style_id; + count += 1; + } + + // Verify we added a meaningful number of styles + try testing.expect(count > 0); + + // Capacity should be at least count (adjusted for load factor) + const cap = page.exactRowCapacity(0, 1); + try testing.expectEqual(StyleSet.capacityForCount(count), cap.styles); +} + +test "Page exactRowCapacity grapheme_bytes" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // No graphemes: capacity should be 0 + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(0, cap.grapheme_bytes); + } + + // Add one grapheme (1 codepoint) to a cell - rounds up to grapheme_chunk + { + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .init('a'); + try page.appendGrapheme(rac.row, rac.cell, 0x0301); // combining acute accent + } + { + const cap = page.exactRowCapacity(0, 5); + // 1 codepoint = 4 bytes, rounds up to grapheme_chunk (16) + try testing.expectEqual(grapheme_chunk, cap.grapheme_bytes); + } + + // Add another grapheme to a different cell - should sum + { + const rac = page.getRowAndCell(1, 0); + rac.cell.* = .init('e'); + try page.appendGrapheme(rac.row, rac.cell, 0x0300); // combining grave accent + } + { + const cap = page.exactRowCapacity(0, 5); + // 2 graphemes, each 1 codepoint = 2 * grapheme_chunk + try testing.expectEqual(grapheme_chunk * 2, cap.grapheme_bytes); + } + + // Add a larger grapheme (multiple codepoints) that fits in one chunk + { + const rac = page.getRowAndCell(2, 0); + rac.cell.* = .init('o'); + try page.appendGrapheme(rac.row, rac.cell, 0x0301); + try page.appendGrapheme(rac.row, rac.cell, 0x0302); + try page.appendGrapheme(rac.row, rac.cell, 0x0303); + } + { + const cap = page.exactRowCapacity(0, 5); + // First two cells: 2 * grapheme_chunk + // Third cell: 3 codepoints = 12 bytes, rounds up to grapheme_chunk + try testing.expectEqual(grapheme_chunk * 3, cap.grapheme_bytes); + } + + // Grapheme outside the row range should not be counted + { + const rac = page.getRowAndCell(0, 7); + rac.cell.* = .init('x'); + try page.appendGrapheme(rac.row, rac.cell, 0x0304); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(grapheme_chunk * 3, cap.grapheme_bytes); + } + + // Full range includes the new grapheme + { + const cap = page.exactRowCapacity(0, 10); + try testing.expectEqual(grapheme_chunk * 4, cap.grapheme_bytes); + } + + // Verify clone works with exact capacity and produces same result + { + const cap = page.exactRowCapacity(0, 5); + var cloned = try Page.init(cap); + defer cloned.deinit(); + for (0..5) |y| { + const src_row = &page.rows.ptr(page.memory)[y]; + const dst_row = &cloned.rows.ptr(cloned.memory)[y]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + } + const cloned_cap = cloned.exactRowCapacity(0, 5); + try testing.expectEqual(cap, cloned_cap); + } +} + +test "Page exactRowCapacity grapheme_bytes larger than chunk" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + }); + defer page.deinit(); + + // Add a grapheme larger than one chunk (grapheme_chunk_len = 4 codepoints) + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .init('a'); + + // Add 6 codepoints - requires 2 chunks (6 * 4 = 24 bytes, rounds up to 32) + for (0..6) |i| { + try page.appendGrapheme(rac.row, rac.cell, @intCast(0x0300 + i)); + } + + const cap = page.exactRowCapacity(0, 1); + // 6 codepoints = 24 bytes, alignForward(24, 16) = 32 + try testing.expectEqual(32, cap.grapheme_bytes); + + // Verify clone works with exact capacity and produces same result + var cloned = try Page.init(cap); + defer cloned.deinit(); + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + const cloned_cap = cloned.exactRowCapacity(0, 1); + try testing.expectEqual(cap, cloned_cap); +} + +test "Page exactRowCapacity hyperlinks" { + var page = try Page.init(.{ + .cols = 10, + .rows = 10, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // No hyperlinks: capacity should be 0 + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(0, cap.hyperlink_bytes); + try testing.expectEqual(0, cap.string_bytes); + } + + // Add one hyperlink with implicit ID + const uri1 = "https://example.com"; + const id1 = blk: { + const rac = page.getRowAndCell(0, 0); + + // Create and add hyperlink entry + const id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = uri1, + }); + try page.setHyperlink(rac.row, rac.cell, id); + break :blk id; + }; + // 1 hyperlink - capacity accounts for load factor + const cap_one_link = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(hyperlink.Set.capacityForCount(1) * @sizeOf(hyperlink.Set.Item), cap_one_link.hyperlink_bytes); + // URI "https://example.com" = 19 bytes, rounds up to string_chunk (32) + try testing.expectEqual(string_chunk, cap_one_link.string_bytes); + } + + // Add same hyperlink to another cell (duplicate ID) - capacity unchanged + { + const rac = page.getRowAndCell(1, 0); + + // Use the same hyperlink ID for another cell + page.hyperlink_set.use(page.memory, id1); + try page.setHyperlink(rac.row, rac.cell, id1); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_one_link.hyperlink_bytes, cap.hyperlink_bytes); + try testing.expectEqual(cap_one_link.string_bytes, cap.string_bytes); + } + + // Add a different hyperlink with explicit ID + const uri2 = "https://other.example.org/path"; + const explicit_id = "my-link-id"; + { + const rac = page.getRowAndCell(2, 0); + + const id = try page.insertHyperlink(.{ + .id = .{ .explicit = explicit_id }, + .uri = uri2, + }); + try page.setHyperlink(rac.row, rac.cell, id); + } + // 2 hyperlinks - capacity accounts for load factor + const cap_two_links = page.exactRowCapacity(0, 5); + { + try testing.expectEqual(hyperlink.Set.capacityForCount(2) * @sizeOf(hyperlink.Set.Item), cap_two_links.hyperlink_bytes); + // First URI: 19 bytes -> 32, Second URI: 30 bytes -> 32, Explicit ID: 10 bytes -> 32 + try testing.expectEqual(string_chunk * 3, cap_two_links.string_bytes); + } + + // Hyperlink outside the row range should not be counted + { + const rac = page.getRowAndCell(0, 7); // row 7 is outside range [0, 5) + + const id = try page.insertHyperlink(.{ + .id = .{ .implicit = 99 }, + .uri = "https://outside.example.com", + }); + try page.setHyperlink(rac.row, rac.cell, id); + } + { + const cap = page.exactRowCapacity(0, 5); + try testing.expectEqual(cap_two_links.hyperlink_bytes, cap.hyperlink_bytes); + try testing.expectEqual(cap_two_links.string_bytes, cap.string_bytes); + } + + // Full range includes the new hyperlink + { + const cap = page.exactRowCapacity(0, 10); + try testing.expectEqual(hyperlink.Set.capacityForCount(3) * @sizeOf(hyperlink.Set.Item), cap.hyperlink_bytes); + // Third URI: 27 bytes -> 32 + try testing.expectEqual(string_chunk * 4, cap.string_bytes); + } + + // Verify clone works with exact capacity and produces same result + { + const cap = page.exactRowCapacity(0, 5); + var cloned = try Page.init(cap); + defer cloned.deinit(); + for (0..5) |y| { + const src_row = &page.rows.ptr(page.memory)[y]; + const dst_row = &cloned.rows.ptr(cloned.memory)[y]; + try cloned.cloneRowFrom(&page, dst_row, src_row); + } + const cloned_cap = cloned.exactRowCapacity(0, 5); + try testing.expectEqual(cap, cloned_cap); + } +} + +test "Page exactRowCapacity single hyperlink clone" { + // Regression test: verify a single hyperlink can be cloned with exact capacity. + // This tests that capacityForCount properly accounts for ID 0 being reserved. + var page = try Page.init(.{ + .cols = 10, + .rows = 2, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // Add exactly one hyperlink to row 0 + const uri = "https://example.com"; + const id = blk: { + const rac = page.getRowAndCell(0, 0); + const link_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = uri, + }); + try page.setHyperlink(rac.row, rac.cell, link_id); + break :blk link_id; + }; + _ = id; + + // exactRowCapacity for just row 0 should give capacity for 1 hyperlink + const cap = page.exactRowCapacity(0, 1); + try testing.expectEqual(hyperlink.Set.capacityForCount(1) * @sizeOf(hyperlink.Set.Item), cap.hyperlink_bytes); + + // Create a new page with exact capacity and clone + var cloned = try Page.init(cap); + defer cloned.deinit(); + + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + + // This must not fail with HyperlinkSetOutOfMemory + try cloned.cloneRowFrom(&page, dst_row, src_row); + + // Verify the hyperlink was cloned correctly + const cloned_cell = &cloned.rows.ptr(cloned.memory)[0].cells.ptr(cloned.memory)[0]; + try testing.expect(cloned_cell.hyperlink); +} + +test "Page exactRowCapacity hyperlink map capacity for many cells" { + // A single hyperlink spanning many cells requires hyperlink_map capacity + // based on cell count, not unique hyperlink count. + const cols = 50; + var page = try Page.init(.{ + .cols = cols, + .rows = 2, + .styles = 8, + .hyperlink_bytes = 32 * @sizeOf(hyperlink.Set.Item), + .string_bytes = 512, + }); + defer page.deinit(); + + // Add one hyperlink spanning all 50 columns in row 0 + const uri = "https://example.com"; + const id = blk: { + const rac = page.getRowAndCell(0, 0); + const link_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 1 }, + .uri = uri, + }); + try page.setHyperlink(rac.row, rac.cell, link_id); + break :blk link_id; + }; + + // Apply same hyperlink to remaining cells in row 0 + for (1..cols) |x| { + const rac = page.getRowAndCell(@intCast(x), 0); + page.hyperlink_set.use(page.memory, id); + try page.setHyperlink(rac.row, rac.cell, id); + } + + // exactRowCapacity must account for 50 hyperlink cells, not just 1 unique hyperlink + const cap = page.exactRowCapacity(0, 1); + + // The hyperlink_bytes must be large enough that layout() computes sufficient + // hyperlink_map capacity. With hyperlink_cell_multiplier=16, we need at least + // ceil(50/16) = 4 hyperlink entries worth of bytes for the map. + const min_for_map = std.math.divCeil(usize, cols, hyperlink_cell_multiplier) catch 0; + const min_hyperlink_bytes = min_for_map * @sizeOf(hyperlink.Set.Item); + try testing.expect(cap.hyperlink_bytes >= min_hyperlink_bytes); + + // Create a new page with exact capacity and clone - must not fail + var cloned = try Page.init(cap); + defer cloned.deinit(); + + const src_row = &page.rows.ptr(page.memory)[0]; + const dst_row = &cloned.rows.ptr(cloned.memory)[0]; + + // This must not fail with HyperlinkMapOutOfMemory + try cloned.cloneRowFrom(&page, dst_row, src_row); + + // Verify all hyperlinks were cloned correctly + for (0..cols) |x| { + const cloned_cell = &cloned.rows.ptr(cloned.memory)[0].cells.ptr(cloned.memory)[x]; + try testing.expect(cloned_cell.hyperlink); + } +} diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index 883dd2f0d..8040039ae 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -64,6 +64,20 @@ pub fn RefCountedSet( @alignOf(Id), )); + /// This is the max load until the set returns OutOfMemory and + /// requires more capacity. + /// + /// Experimentally, this load factor works quite well. + pub const load_factor = 0.8125; + + /// Returns the minimum capacity needed to store `n` items, + /// accounting for the load factor and the reserved ID 0. + pub fn capacityForCount(n: usize) usize { + if (n == 0) return 0; + // +1 because ID 0 is reserved, so we need at least n+1 slots. + return @intFromFloat(@ceil(@as(f64, @floatFromInt(n + 1)) / load_factor)); + } + /// Set item pub const Item = struct { /// The value this item represents. @@ -154,9 +168,6 @@ pub fn RefCountedSet( /// The returned layout `cap` property will be 1 more than the number /// of items that the set can actually store, since ID 0 is reserved. pub fn init(cap: usize) Layout { - // Experimentally, this load factor works quite well. - const load_factor = 0.8125; - assert(cap <= @as(usize, @intCast(std.math.maxInt(Id))) + 1); // Zero-cap set is valid, return special case -- 2.51.2 From f9699eceb03c2be01f40e9b90e906540e7185192 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Jan 2026 14:25:45 -0800 Subject: [PATCH 302/349] terminal: PageList.compact --- src/terminal/PageList.zig | 202 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index b4f51a7c9..90bd3b12f 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2685,6 +2685,74 @@ pub fn scrollClear(self: *PageList) !void { for (0..non_empty) |_| _ = try self.grow(); } +/// Compact a page to use the minimum required memory for the contents +/// it stores. Returns the new node pointer if compaction occurred, or null +/// if the page was already compact or compaction would not provide meaningful +/// savings. +/// +/// The current design of PageList at the time of writing this doesn't +/// allow for smaller than `std_size` nodes so if the current node's backing +/// page is standard size or smaller, no compaction will occur. In the +/// future we should fix this up. +/// +/// If this returns OOM, the PageList is left unchanged and no dangling +/// memory references exist. It is safe to ignore the error and continue using +/// the uncompacted page. +pub fn compact(self: *PageList, node: *List.Node) Allocator.Error!?*List.Node { + defer self.assertIntegrity(); + const page: *Page = &node.data; + + // We should never have empty rows in our pagelist anyways... + assert(page.size.rows > 0); + + // We never compact standard size or smaller pages because changing + // the capacity to something smaller won't save memory. + if (page.memory.len <= std_size) return null; + + // Compute the minimum capacity required for this page's content + const req_cap = page.exactRowCapacity(0, page.size.rows); + const new_size = Page.layout(req_cap).total_size; + const old_size = page.memory.len; + if (new_size >= old_size) return null; + + // Create the new smaller page + const new_node = try self.createPage(req_cap); + errdefer self.destroyNode(new_node); + const new_page: *Page = &new_node.data; + new_page.size = page.size; + new_page.dirty = page.dirty; + new_page.cloneFrom( + page, + 0, + page.size.rows, + ) catch |err| { + // cloneFrom should not fail when compacting since req_cap is + // computed to exactly fit the source content and our expectation + // of exactRowCapacity ensures it can fit all the requested + // data. + log.err("compact clone failed err={}", .{err}); + + // In this case, let's gracefully degrade by pretending we + // didn't need to compact. + self.destroyNode(new_node); + return null; + }; + + // Fix up all tracked pins to point to the new page + const pin_keys = self.tracked_pins.keys(); + for (pin_keys) |p| { + if (p.node != node) continue; + p.node = new_node; + } + + // Insert the new page and destroy the old one + self.pages.insertBefore(node, new_node); + self.pages.remove(node); + self.destroyNode(node); + + new_page.assertIntegrity(); + return new_node; +} /// 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 { @@ -11811,3 +11879,137 @@ test "PageList resize (no reflow) more cols remaps pins in backfill path" { try testing.expectEqual(.codepoint, cell.content_tag); try testing.expectEqual(marker, cell.content.codepoint); } + +test "PageList compact std_size page returns null" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + // A freshly created page should be at std_size + const node = s.pages.first.?; + try testing.expect(node.data.memory.len <= std_size); + + // compact should return null since there's nothing to compact + const result = try s.compact(node); + try testing.expectEqual(null, result); + + // Page should still be the same + try testing.expectEqual(node, s.pages.first.?); +} + +test "PageList compact oversized page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + + // Grow until we have multiple pages + const page1_node = s.pages.first.?; + page1_node.data.pauseIntegrityChecks(true); + for (0..page1_node.data.capacity.rows - page1_node.data.size.rows) |_| { + _ = try s.grow(); + } + page1_node.data.pauseIntegrityChecks(false); + _ = try s.grow(); + try testing.expect(s.pages.first != s.pages.last); + + var node = s.pages.first.?; + + // Write content to verify it's preserved + { + const page = &node.data; + for (0..page.size.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(x + y * s.cols) }, + }; + } + } + } + + // Create a tracked pin on this page + const tracked = try s.trackPin(.{ .node = node, .x = 5, .y = 10 }); + defer s.untrackPin(tracked); + + // Make the page oversized + while (node.data.memory.len <= std_size) { + node = try s.increaseCapacity(node, .grapheme_bytes); + } + try testing.expect(node.data.memory.len > std_size); + const oversized_len = node.data.memory.len; + const original_size = node.data.size; + const second_node = node.next.?; + + // Set dirty flag after increaseCapacity + node.data.dirty = true; + + // Compact the page + const new_node = try s.compact(node); + try testing.expect(new_node != null); + + // Verify memory is smaller + try testing.expect(new_node.?.data.memory.len < oversized_len); + + // Verify size preserved + try testing.expectEqual(original_size.rows, new_node.?.data.size.rows); + try testing.expectEqual(original_size.cols, new_node.?.data.size.cols); + + // Verify dirty flag preserved + try testing.expect(new_node.?.data.dirty); + + // Verify linked list integrity + try testing.expectEqual(new_node.?, s.pages.first.?); + try testing.expectEqual(null, new_node.?.prev); + try testing.expectEqual(second_node, new_node.?.next); + try testing.expectEqual(new_node.?, second_node.prev); + + // Verify pin updated correctly + try testing.expectEqual(new_node.?, tracked.node); + try testing.expectEqual(@as(size.CellCountInt, 5), tracked.x); + try testing.expectEqual(@as(size.CellCountInt, 10), tracked.y); + + // Verify content preserved + const page = &new_node.?.data; + for (0..page.size.rows) |y| { + for (0..s.cols) |x| { + const rac = page.getRowAndCell(x, y); + try testing.expectEqual( + @as(u21, @intCast(x + y * s.cols)), + rac.cell.content.codepoint, + ); + } + } +} + +test "PageList compact insufficient savings returns null" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, 0); + defer s.deinit(); + + var node = s.pages.first.?; + + // Make the page slightly oversized (just one increase) + // This might not provide enough savings to justify compaction + node = try s.increaseCapacity(node, .grapheme_bytes); + + // If the page is still at or below std_size, compact returns null + if (node.data.memory.len <= std_size) { + const result = try s.compact(node); + try testing.expectEqual(null, result); + } else { + // If it did grow beyond std_size, verify that compaction + // works or returns null based on savings calculation + const result = try s.compact(node); + // Either it compacted or determined insufficient savings + if (result) |new_node| { + try testing.expect(new_node.data.memory.len < node.data.memory.len); + } + } +} -- 2.51.2 From 5ee56409c7bc06a7f881da1fdc18c91799a2b6d4 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Mon, 19 Jan 2026 11:58:58 -0600 Subject: [PATCH 303/349] macos: support mouse buttons 8/9 (back/forward) Add support for mouse buttons 4-11 in the macOS app. Previously only left, right, and middle buttons were handled. Now otherMouseDown/Up events properly map NSEvent.buttonNumber to the corresponding Ghostty mouse button, enabling back/forward button support. Fixes: https://github.com/ghostty-org/ghostty/issues/2425 Amp-Thread-ID: https://ampcode.com/threads/T-019bd74e-6b2b-731d-b43a-ac73b3460c32 Co-authored-by: Amp --- include/ghostty.h | 8 +++++ macos/Sources/Ghostty/Ghostty.Input.swift | 35 +++++++++++++++++++ .../Surface View/SurfaceView_AppKit.swift | 8 ++--- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index b884ebc08..0133fac73 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -66,6 +66,14 @@ typedef enum { GHOSTTY_MOUSE_LEFT, GHOSTTY_MOUSE_RIGHT, GHOSTTY_MOUSE_MIDDLE, + GHOSTTY_MOUSE_FOUR, + GHOSTTY_MOUSE_FIVE, + GHOSTTY_MOUSE_SIX, + GHOSTTY_MOUSE_SEVEN, + GHOSTTY_MOUSE_EIGHT, + GHOSTTY_MOUSE_NINE, + GHOSTTY_MOUSE_TEN, + GHOSTTY_MOUSE_ELEVEN, } ghostty_input_mouse_button_e; typedef enum { diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 4b3fb9937..7b2905abb 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -370,6 +370,14 @@ extension Ghostty.Input { case left case right case middle + case four + case five + case six + case seven + case eight + case nine + case ten + case eleven var cMouseButton: ghostty_input_mouse_button_e { switch self { @@ -377,6 +385,33 @@ extension Ghostty.Input { case .left: GHOSTTY_MOUSE_LEFT case .right: GHOSTTY_MOUSE_RIGHT case .middle: GHOSTTY_MOUSE_MIDDLE + case .four: GHOSTTY_MOUSE_FOUR + case .five: GHOSTTY_MOUSE_FIVE + case .six: GHOSTTY_MOUSE_SIX + case .seven: GHOSTTY_MOUSE_SEVEN + case .eight: GHOSTTY_MOUSE_EIGHT + case .nine: GHOSTTY_MOUSE_NINE + case .ten: GHOSTTY_MOUSE_TEN + case .eleven: GHOSTTY_MOUSE_ELEVEN + } + } + + /// Initialize from NSEvent.buttonNumber + /// NSEvent buttonNumber: 0=left, 1=right, 2=middle, 3=back (button 8), 4=forward (button 9), etc. + init(fromNSEventButtonNumber buttonNumber: Int) { + switch buttonNumber { + case 0: self = .left + case 1: self = .right + case 2: self = .middle + case 3: self = .eight // Back button + case 4: self = .nine // Forward button + case 5: self = .six + case 6: self = .seven + case 7: self = .four + case 8: self = .five + case 9: self = .ten + case 10: self = .eleven + default: self = .unknown } } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 80de2e823..0ddfe57b8 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -860,16 +860,16 @@ extension Ghostty { override func otherMouseDown(with event: NSEvent) { guard let surface = self.surface else { return } - guard event.buttonNumber == 2 else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_MIDDLE, mods) + let button = Ghostty.Input.MouseButton(fromNSEventButtonNumber: event.buttonNumber) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, button.cMouseButton, mods) } override func otherMouseUp(with event: NSEvent) { guard let surface = self.surface else { return } - guard event.buttonNumber == 2 else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_MIDDLE, mods) + let button = Ghostty.Input.MouseButton(fromNSEventButtonNumber: event.buttonNumber) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, button.cMouseButton, mods) } -- 2.51.2 From 93a070c6de47cac5d02d525e33846361b423b382 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 18 Jan 2026 06:48:49 -0800 Subject: [PATCH 304/349] terminal: PageList split operation --- src/terminal/PageList.zig | 685 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 685 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 90bd3b12f..c639f15cd 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2753,6 +2753,98 @@ pub fn compact(self: *PageList, node: *List.Node) Allocator.Error!?*List.Node { new_page.assertIntegrity(); return new_node; } + +pub const SplitError = error{ + // Allocator OOM + OutOfMemory, + // Page can't be split further because it is already a single row. + OutOfSpace, +}; + +/// Split the given node in the PageList at the given pin. +/// +/// The row at the pin and after will be moved into a new page with +/// the same capacity as the original page. Alternatively, you can "split +/// above" by splitting the row following the desired split row. +/// +/// Since the split happens below the pin, the pin remains valid. +pub fn split( + self: *PageList, + p: Pin, +) SplitError!void { + if (build_options.slow_runtime_safety) assert(self.pinIsValid(p)); + + // Ran into a bug that I can only explain via aliasing. If a tracked + // pin is passed in, its possible Zig will alias the memory and then + // when we modify it later it updates our p here. Coyping the node + // fixes this. + const original_node = p.node; + const page: *Page = &original_node.data; + + // A page that is already 1 row can't be split. In the future we can + // theoretically maybe split by soft-wrapping multiple pages but that + // seems crazy and the rest of our PageList can't handle heterogeneously + // sized pages today. + if (page.size.rows <= 1) return error.OutOfSpace; + + // Splitting at row 0 is a no-op since there's nothing before the split point. + if (p.y == 0) return; + + // At this point we're doing actual modification so make sure + // on the return that we're good. + defer self.assertIntegrity(); + + // Create a new node with the same capacity of managed memory. + const target = try self.createPage(page.capacity); + errdefer self.destroyNode(target); + + // Determine how many rows we're copying + const y_start = p.y; + const y_end = page.size.rows; + target.data.size.rows = y_end - y_start; + assert(target.data.size.rows <= target.data.capacity.rows); + + // Copy our old data. This should NOT fail because we have the + // capacity of the old page which already fits the data we requested. + target.data.cloneFrom(page, y_start, y_end) catch |err| { + log.err( + "error cloning rows for split err={}", + .{err}, + ); + + // Rather than crash, we return an OutOfSpace to show that + // we couldn't split and let our callers gracefully handle it. + // Realistically though... this should not happen. + return error.OutOfSpace; + }; + + // From this point forward there is no going back. We have no + // error handling. It is possible but we haven't written it. + errdefer comptime unreachable; + + // Move any tracked pins from the copied rows + for (self.tracked_pins.keys()) |tracked| { + if (&tracked.node.data != page or + tracked.y < p.y) continue; + + tracked.node = target; + tracked.y -= p.y; + // p.x remains the same since we're copying the row as-is + } + + // Clear our rows + for (page.rows.ptr(page.memory)[y_start..y_end]) |*row| { + page.clearCells( + row, + 0, + page.size.cols, + ); + } + page.size.rows -= y_end - y_start; + + self.pages.insertAfter(original_node, target); +} + /// 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 { @@ -12013,3 +12105,596 @@ test "PageList compact insufficient savings returns null" { } } } + +test "PageList split at middle row" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Write content to rows: row 0 gets codepoint 0, row 1 gets 1, etc. + for (0..page.size.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Split at row 5 (middle) + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + // Verify two pages exist + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.next != null); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have rows 0-4 (5 rows) + try testing.expectEqual(@as(usize, 5), first_page.size.rows); + // Second page should have rows 5-9 (5 rows) + try testing.expectEqual(@as(usize, 5), second_page.size.rows); + + // Verify content in first page is preserved (rows 0-4 have codepoints 0-4) + for (0..5) |y| { + const rac = first_page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint); + } + + // Verify content in second page (original rows 5-9, now at y=0-4) + for (0..5) |y| { + const rac = second_page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, @intCast(y + 5)), rac.cell.content.codepoint); + } +} + +test "PageList split at row 0 is no-op" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Write content to all rows + for (0..page.size.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Split at row 0 should be a no-op + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 0, .x = 0 }; + try s.split(split_pin); + + // Verify only one page exists (no split occurred) + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.next == null); + + // Verify all content is still in the original page + try testing.expectEqual(@as(usize, 10), page.size.rows); + for (0..10) |y| { + const rac = page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, @intCast(y)), rac.cell.content.codepoint); + } +} + +test "PageList split at last row" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Write content to all rows + for (0..page.size.rows) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = @intCast(y) }, + }; + } + + // Split at last row (row 9) + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 9, .x = 0 }; + try s.split(split_pin); + + // Verify two pages exist + try testing.expect(s.pages.first != null); + try testing.expect(s.pages.first.?.next != null); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have 9 rows + try testing.expectEqual(@as(usize, 9), first_page.size.rows); + // Second page should have 1 row + try testing.expectEqual(@as(usize, 1), second_page.size.rows); + + // Verify content in second page (original row 9, now at y=0) + const rac = second_page.getRowAndCell(0, 0); + try testing.expectEqual(@as(u21, 9), rac.cell.content.codepoint); +} + +test "PageList split single row page returns OutOfSpace" { + const testing = std.testing; + const alloc = testing.allocator; + + // Initialize with 1 row + var s = try init(alloc, 10, 1, 0); + defer s.deinit(); + + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 0, .x = 0 }; + const result = s.split(split_pin); + + try testing.expectError(error.OutOfSpace, result); +} + +test "PageList split moves tracked pins" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + // Track a pin at row 7 + const tracked = try s.trackPin(.{ .node = s.pages.first.?, .y = 7, .x = 3 }); + defer s.untrackPin(tracked); + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + // The tracked pin should now be in the second page + try testing.expect(tracked.node == s.pages.first.?.next.?); + // y should be adjusted: was 7, split at 5, so new y = 7 - 5 = 2 + try testing.expectEqual(@as(usize, 2), tracked.y); + // x should remain unchanged + try testing.expectEqual(@as(usize, 3), tracked.x); +} + +test "PageList split tracked pin before split point unchanged" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Track a pin at row 2 (before the split point) + const tracked = try s.trackPin(.{ .node = original_node, .y = 2, .x = 5 }); + defer s.untrackPin(tracked); + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + // The tracked pin should remain in the original page + try testing.expect(tracked.node == s.pages.first.?); + // y and x should be unchanged + try testing.expectEqual(@as(usize, 2), tracked.y); + try testing.expectEqual(@as(usize, 5), tracked.x); +} + +test "PageList split tracked pin at split point moves to new page" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Track a pin at the exact split point (row 5) + const tracked = try s.trackPin(.{ .node = original_node, .y = 5, .x = 4 }); + defer s.untrackPin(tracked); + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + // The tracked pin should be in the new page + try testing.expect(tracked.node == s.pages.first.?.next.?); + // y should be 0 since it was at the split point: 5 - 5 = 0 + try testing.expectEqual(@as(usize, 0), tracked.y); + // x should remain unchanged + try testing.expectEqual(@as(usize, 4), tracked.x); +} + +test "PageList split multiple tracked pins across regions" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Track multiple pins in different regions + const pin_before = try s.trackPin(.{ .node = original_node, .y = 1, .x = 0 }); + defer s.untrackPin(pin_before); + const pin_at_split = try s.trackPin(.{ .node = original_node, .y = 5, .x = 2 }); + defer s.untrackPin(pin_at_split); + const pin_after1 = try s.trackPin(.{ .node = original_node, .y = 7, .x = 3 }); + defer s.untrackPin(pin_after1); + const pin_after2 = try s.trackPin(.{ .node = original_node, .y = 9, .x = 8 }); + defer s.untrackPin(pin_after2); + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = s.pages.first.?; + const second_page = first_page.next.?; + + // Pin before split point stays in original page + try testing.expect(pin_before.node == first_page); + try testing.expectEqual(@as(usize, 1), pin_before.y); + try testing.expectEqual(@as(usize, 0), pin_before.x); + + // Pin at split point moves to new page with y=0 + try testing.expect(pin_at_split.node == second_page); + try testing.expectEqual(@as(usize, 0), pin_at_split.y); + try testing.expectEqual(@as(usize, 2), pin_at_split.x); + + // Pins after split point move to new page with adjusted y + try testing.expect(pin_after1.node == second_page); + try testing.expectEqual(@as(usize, 2), pin_after1.y); // 7 - 5 = 2 + try testing.expectEqual(@as(usize, 3), pin_after1.x); + + try testing.expect(pin_after2.node == second_page); + try testing.expectEqual(@as(usize, 4), pin_after2.y); // 9 - 5 = 4 + try testing.expectEqual(@as(usize, 8), pin_after2.x); +} + +test "PageList split tracked viewport_pin in split region moves correctly" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_node = s.pages.first.?; + + // Set viewport_pin to row 7 (after split point) + s.viewport_pin.node = original_node; + s.viewport_pin.y = 7; + s.viewport_pin.x = 6; + + // Split at row 5 + const split_pin: Pin = .{ .node = original_node, .y = 5, .x = 0 }; + try s.split(split_pin); + + // viewport_pin should be in the new page + try testing.expect(s.viewport_pin.node == s.pages.first.?.next.?); + // y should be adjusted: 7 - 5 = 2 + try testing.expectEqual(@as(usize, 2), s.viewport_pin.y); + // x should remain unchanged + try testing.expectEqual(@as(usize, 6), s.viewport_pin.x); +} + +test "PageList split middle page preserves linked list order" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create a single page with 12 rows + var s = try init(alloc, 10, 12, 0); + defer s.deinit(); + + // Split at row 4 to create: page1 (rows 0-3), page2 (rows 4-11) + const first_node = s.pages.first.?; + const split_pin1: Pin = .{ .node = first_node, .y = 4, .x = 0 }; + try s.split(split_pin1); + + // Now we have 2 pages + const page1 = s.pages.first.?; + const page2 = s.pages.first.?.next.?; + try testing.expectEqual(@as(usize, 4), page1.data.size.rows); + try testing.expectEqual(@as(usize, 8), page2.data.size.rows); + + // Split page2 at row 4 to create: page1 -> page2 (rows 0-3) -> page3 (rows 4-7) + const split_pin2: Pin = .{ .node = page2, .y = 4, .x = 0 }; + try s.split(split_pin2); + + // Now we have 3 pages + const first = s.pages.first.?; + const middle = first.next.?; + const last = middle.next.?; + + // Verify linked list order: first -> middle -> last + try testing.expectEqual(page1, first); + try testing.expectEqual(page2, middle); + try testing.expectEqual(s.pages.last.?, last); + + // Verify prev pointers + try testing.expect(first.prev == null); + try testing.expectEqual(first, middle.prev.?); + try testing.expectEqual(middle, last.prev.?); + + // Verify next pointers + try testing.expectEqual(middle, first.next.?); + try testing.expectEqual(last, middle.next.?); + try testing.expect(last.next == null); + + // Verify row counts + try testing.expectEqual(@as(usize, 4), first.data.size.rows); + try testing.expectEqual(@as(usize, 4), middle.data.size.rows); + try testing.expectEqual(@as(usize, 4), last.data.size.rows); +} + +test "PageList split last page makes new page the last" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create a single page with 10 rows + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + // Split to create 2 pages first + const first_node = s.pages.first.?; + const split_pin1: Pin = .{ .node = first_node, .y = 5, .x = 0 }; + try s.split(split_pin1); + + // Now split the last page + const last_before_split = s.pages.last.?; + try testing.expectEqual(@as(usize, 5), last_before_split.data.size.rows); + + const split_pin2: Pin = .{ .node = last_before_split, .y = 2, .x = 0 }; + try s.split(split_pin2); + + // The new page should be the new last + const new_last = s.pages.last.?; + try testing.expect(new_last != last_before_split); + try testing.expectEqual(last_before_split, new_last.prev.?); + try testing.expect(new_last.next == null); + + // Verify row counts: original last has 2 rows, new last has 3 rows + try testing.expectEqual(@as(usize, 2), last_before_split.data.size.rows); + try testing.expectEqual(@as(usize, 3), new_last.data.size.rows); +} + +test "PageList split first page keeps original as first" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create 2 pages by splitting + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const original_first = s.pages.first.?; + const split_pin1: Pin = .{ .node = original_first, .y = 5, .x = 0 }; + try s.split(split_pin1); + + // Get second page (created by first split) + const second_page = s.pages.first.?.next.?; + + // Now split the first page again + const split_pin2: Pin = .{ .node = s.pages.first.?, .y = 2, .x = 0 }; + try s.split(split_pin2); + + // Original first should still be first + try testing.expectEqual(original_first, s.pages.first.?); + try testing.expect(s.pages.first.?.prev == null); + + // New page should be inserted between first and second + const inserted = s.pages.first.?.next.?; + try testing.expect(inserted != second_page); + try testing.expectEqual(second_page, inserted.next.?); + + // Verify row counts: first has 2, inserted has 3, second has 5 + try testing.expectEqual(@as(usize, 2), s.pages.first.?.data.size.rows); + try testing.expectEqual(@as(usize, 3), inserted.data.size.rows); + try testing.expectEqual(@as(usize, 5), second_page.data.size.rows); +} + +test "PageList split preserves wrap flags" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Set wrap flags on rows that will be in the second page after split + // Row 5: wrap = true (this is the start of a wrapped line) + // Row 6: wrap_continuation = true (this continues the wrap) + // Row 7: wrap = true, wrap_continuation = true (wrapped and continues) + { + const rac5 = page.getRowAndCell(0, 5); + rac5.row.wrap = true; + + const rac6 = page.getRowAndCell(0, 6); + rac6.row.wrap_continuation = true; + + const rac7 = page.getRowAndCell(0, 7); + rac7.row.wrap = true; + rac7.row.wrap_continuation = true; + } + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const second_page = &s.pages.first.?.next.?.data; + + // Verify wrap flags are preserved in new page + // Original row 5 is now row 0 in second page + { + const rac0 = second_page.getRowAndCell(0, 0); + try testing.expect(rac0.row.wrap); + try testing.expect(!rac0.row.wrap_continuation); + } + + // Original row 6 is now row 1 in second page + { + const rac1 = second_page.getRowAndCell(0, 1); + try testing.expect(!rac1.row.wrap); + try testing.expect(rac1.row.wrap_continuation); + } + + // Original row 7 is now row 2 in second page + { + const rac2 = second_page.getRowAndCell(0, 2); + try testing.expect(rac2.row.wrap); + try testing.expect(rac2.row.wrap_continuation); + } +} + +test "PageList split preserves styled cells" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Create a style and apply it to cells in rows 5-7 (which will be in the second page) + const style: stylepkg.Style = .{ .flags = .{ .bold = true } }; + const style_id = try page.styles.add(page.memory, style); + + for (5..8) |y| { + const rac = page.getRowAndCell(0, y); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'S' }, + .style_id = style_id, + }; + rac.row.styled = true; + page.styles.use(page.memory, style_id); + } + // Release the extra ref from add + page.styles.release(page.memory, style_id); + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have no styles (all styled rows moved to second page) + try testing.expectEqual(@as(usize, 0), first_page.styles.count()); + + // Second page should have exactly 1 style (the bold style, used by 3 cells) + try testing.expectEqual(@as(usize, 1), second_page.styles.count()); + + // Verify styled cells are preserved in new page + for (0..3) |y| { + const rac = second_page.getRowAndCell(0, y); + try testing.expectEqual(@as(u21, 'S'), rac.cell.content.codepoint); + try testing.expect(rac.cell.style_id != 0); + + const got_style = second_page.styles.get(second_page.memory, rac.cell.style_id); + try testing.expect(got_style.flags.bold); + try testing.expect(rac.row.styled); + } +} + +test "PageList split preserves grapheme clusters" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Add a grapheme cluster to row 6 (will be row 1 in second page after split at 5) + { + const rac = page.getRowAndCell(0, 6); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0x1F468 }, // Man emoji + }; + try page.setGraphemes(rac.row, rac.cell, &.{ + 0x200D, // ZWJ + 0x1F469, // Woman emoji + }); + } + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have no graphemes (the grapheme row moved to second page) + try testing.expectEqual(@as(usize, 0), first_page.graphemeCount()); + + // Second page should have exactly 1 grapheme + try testing.expectEqual(@as(usize, 1), second_page.graphemeCount()); + + // Verify grapheme is preserved in new page (original row 6 is now row 1) + { + const rac = second_page.getRowAndCell(0, 1); + try testing.expectEqual(@as(u21, 0x1F468), rac.cell.content.codepoint); + try testing.expect(rac.row.grapheme); + + const cps = second_page.lookupGrapheme(rac.cell).?; + try testing.expectEqual(@as(usize, 2), cps.len); + try testing.expectEqual(@as(u21, 0x200D), cps[0]); + try testing.expectEqual(@as(u21, 0x1F469), cps[1]); + } +} + +test "PageList split preserves hyperlinks" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 10, 0); + defer s.deinit(); + + const page = &s.pages.first.?.data; + + // Add a hyperlink to row 7 (will be row 2 in second page after split at 5) + const hyperlink_id = try page.insertHyperlink(.{ + .id = .{ .implicit = 0 }, + .uri = "https://example.com", + }); + { + const rac = page.getRowAndCell(0, 7); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 'L' }, + }; + try page.setHyperlink(rac.row, rac.cell, hyperlink_id); + } + + // Split at row 5 + const split_pin: Pin = .{ .node = s.pages.first.?, .y = 5, .x = 0 }; + try s.split(split_pin); + + const first_page = &s.pages.first.?.data; + const second_page = &s.pages.first.?.next.?.data; + + // First page should have no hyperlinks (the hyperlink row moved to second page) + try testing.expectEqual(@as(usize, 0), first_page.hyperlink_set.count()); + + // Second page should have exactly 1 hyperlink + try testing.expectEqual(@as(usize, 1), second_page.hyperlink_set.count()); + + // Verify hyperlink is preserved in new page (original row 7 is now row 2) + { + const rac = second_page.getRowAndCell(0, 2); + try testing.expectEqual(@as(u21, 'L'), rac.cell.content.codepoint); + try testing.expect(rac.cell.hyperlink); + + const link_id = second_page.lookupHyperlink(rac.cell).?; + const link = second_page.hyperlink_set.get(second_page.memory, link_id); + try testing.expectEqualStrings("https://example.com", link.uri.slice(second_page.memory)); + } +} -- 2.51.2 From 4e60a850995ae7cd89faf456208b6df38eab051a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 Jan 2026 10:27:43 -0800 Subject: [PATCH 305/349] terminal: setAttribute failure reverts back to prior style --- src/terminal/Screen.zig | 66 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 1538da5da..d09477eda 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1789,9 +1789,22 @@ fn resizeInternal( /// Set a style attribute for the current cursor. /// -/// This can cause a page split if the current page cannot fit this style. -/// This is the only scenario an error return is possible. +/// If the style can't be set due to any internal errors (memory-related), +/// then this will revert back to the existing style and return an error. pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { + // If we fail to set our style for any reason, we should revert + // back to the old style. If we fail to do that, we revert back to + // the default style. + const old_style = self.cursor.style; + errdefer { + self.cursor.style = old_style; + self.manualStyleUpdate() catch |err| { + log.warn("setAttribute error restoring old style after failure err={}", .{err}); + self.cursor.style = .{}; + self.cursor.style_id = style.default_id; + }; + } + switch (attr) { .unset => { self.cursor.style = .{}; @@ -1935,6 +1948,9 @@ pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { /// Call this whenever you manually change the cursor style. /// +/// If this returns an error, the style change did not take effect and +/// the cursor style is reverted back to the default. +/// /// Note that this can return any PageList capacity error, because it /// is possible for the internal pagelist to not accommodate the new style /// at all. This WILL attempt to resize our internal pages to fit the style @@ -9247,3 +9263,49 @@ test "Screen: cursorDown to page with insufficient capacity" { try testing.expect(false); } } + +test "Screen setAttribute returns OutOfSpace at max styles" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 }); + defer s.deinit(); + + // Increase the page's style capacity to max by repeatedly calling increaseCapacity + const max_styles = std.math.maxInt(size.CellCountInt); + while (s.cursor.page_pin.node.data.capacity.styles < max_styles) { + _ = s.pages.increaseCapacity( + s.cursor.page_pin.node, + .styles, + ) catch break; + } + + // Get the page reference after increaseCapacity + const page = &s.cursor.page_pin.node.data; + try testing.expectEqual(max_styles, page.capacity.styles); + + // Fill up all style slots + page.pauseIntegrityChecks(true); + var n: u24 = 1; + while (page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(n) } }, + )) |_| n += 1 else |_| {} + page.pauseIntegrityChecks(false); + + // Set a style that we can track as "prior" + // Use a style that's already in the page by reusing one we added + s.cursor.style = .{ .bg_color = .{ .rgb = @bitCast(@as(u24, 1)) } }; + const prior_id = page.styles.add(page.memory, s.cursor.style) catch unreachable; + s.cursor.style_id = prior_id; + + // Now try to set a new unique attribute that would require a new style slot + // This should fail with OutOfSpace since we're at max capacity + const result = s.setAttribute(.bold); + + // Should return OutOfSpace error + try testing.expectError(error.OutOfSpace, result); + + // The cursor style_id should remain the prior one + try testing.expectEqual(prior_id, s.cursor.style_id); +} -- 2.51.2 From c412b30cb5ffc1a208d4bb8b39c967cb00a5e6b4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 Jan 2026 10:08:51 -0800 Subject: [PATCH 306/349] terminal: splitForCapacity, manualStyleUpdate uses this --- src/terminal/PageList.zig | 2 +- src/terminal/Screen.zig | 189 ++++++++++++++++++++++++++++++++------ 2 files changed, 163 insertions(+), 28 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index c639f15cd..41f8d6533 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2776,7 +2776,7 @@ pub fn split( // Ran into a bug that I can only explain via aliasing. If a tracked // pin is passed in, its possible Zig will alias the memory and then - // when we modify it later it updates our p here. Coyping the node + // when we modify it later it updates our p here. Copying the node // fixes this. const original_node = p.node; const page: *Page = &original_node.data; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index d09477eda..5ea338b70 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1791,7 +1791,10 @@ fn resizeInternal( /// /// If the style can't be set due to any internal errors (memory-related), /// then this will revert back to the existing style and return an error. -pub fn setAttribute(self: *Screen, attr: sgr.Attribute) !void { +pub fn setAttribute( + self: *Screen, + attr: sgr.Attribute, +) PageList.IncreaseCapacityError!void { // If we fail to set our style for any reason, we should revert // back to the old style. If we fail to do that, we revert back to // the default style. @@ -1995,13 +1998,22 @@ pub fn manualStyleUpdate(self: *Screen) PageList.IncreaseCapacityError!void { ) catch |err| id: { // Our style map is full or needs to be rehashed, so we need to // increase style capacity (or rehash). - const node = try self.increaseCapacity( + const node = self.increaseCapacity( self.cursor.page_pin.node, switch (err) { error.OutOfMemory => .styles, error.NeedsRehash => null, }, - ); + ) catch |increase_err| switch (increase_err) { + error.OutOfMemory => return error.OutOfMemory, + error.OutOfSpace => space: { + // Out of space, we need to split the page. Split wherever + // is using less capacity and hope that works. If it doesn't + // work, we tried. + try self.splitForCapacity(self.cursor.page_pin.*); + break :space self.cursor.page_pin.node; + }, + }; page = &node.data; break :id page.styles.add( @@ -2031,6 +2043,62 @@ pub fn manualStyleUpdate(self: *Screen) PageList.IncreaseCapacityError!void { self.cursor.style_id = id; } +/// Split at the given pin so that the pinned row moves to the page +/// with less used capacity after the split. +/// +/// The primary use case for this is to handle IncreaseCapacityError +/// OutOfSpace conditions where we need to split the page in order +/// to make room for more managed memory. +/// +/// If the caller cares about where the pin moves to, they should +/// setup a tracked pin before calling this and then check that. +/// In many calling cases, the input pin is tracked (e.g. the cursor +/// pin). +/// +/// If this returns OOM then its a system OOM. If this returns OutOfSpace +/// then it means the page can't be split further. +fn splitForCapacity( + self: *Screen, + pin: Pin, +) PageList.SplitError!void { + // Get our capacities. We include our target row because its + // capacity will be preserved. + const bytes_above = Page.layout(pin.node.data.exactRowCapacity( + 0, + pin.y + 1, + )).total_size; + const bytes_below = Page.layout(pin.node.data.exactRowCapacity( + pin.y, + pin.node.data.size.rows, + )).total_size; + + // We need to track the old cursor pin because if our split + // moves the cursor pin we need to update our accounting. + const old_cursor = self.cursor.page_pin.*; + + // If our bytes above are less than bytes below, we move the pin + // to split down one since splitting includes the pinned row in + // the new node. + try self.pages.split(if (bytes_above < bytes_below) + pin.down(1) orelse pin + else + pin); + + // Cursor didn't change nodes, we're done. + if (self.cursor.page_pin.node == old_cursor.node) return; + + // Cursor changed, we need to restore the old pin then use + // cursorChangePin to move to the new pin. The old node is guaranteed + // to still exist, just not the row. + // + // Note that page_row and all that will be invalid, it points to the + // new node, but at the time of writing this we don't need any of that + // to be right in cursorChangePin. + const new_cursor = self.cursor.page_pin.*; + self.cursor.page_pin.* = old_cursor; + self.cursorChangePin(new_cursor); +} + /// Append a grapheme to the given cell within the current cursor row. pub fn appendGrapheme( self: *Screen, @@ -9264,48 +9332,115 @@ test "Screen: cursorDown to page with insufficient capacity" { } } -test "Screen setAttribute returns OutOfSpace at max styles" { +test "Screen setAttribute increases capacity when style map is full" { + // Tests that setAttribute succeeds when the style map is full by + // increasing page capacity. When capacity is at max and increaseCapacity + // returns OutOfSpace, manualStyleUpdate will split the page instead. const testing = std.testing; const alloc = testing.allocator; - var s = try init(alloc, .{ .cols = 10, .rows = 10, .max_scrollback = 0 }); + // Use a small screen with multiple rows + var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 10 }); + defer s.deinit(); + + // Write content to multiple rows + try s.testWriteString("line1\nline2\nline3\nline4\nline5"); + + // Get the page and fill its style map to capacity + const page = &s.cursor.page_pin.node.data; + const original_styles_capacity = page.capacity.styles; + + // Fill the style map to capacity + { + page.pauseIntegrityChecks(true); + defer page.pauseIntegrityChecks(false); + defer page.assertIntegrity(); + + var n: u24 = 1; + while (page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(n) } }, + )) |_| n += 1 else |_| {} + } + + // Now try to set a new unique attribute that would require a new style slot + // This should succeed by increasing capacity (or splitting if at max capacity) + try s.setAttribute(.bold); + + // The style should have been applied (bold flag set) + try testing.expect(s.cursor.style.flags.bold); + + // The cursor should have a valid non-default style_id + try testing.expect(s.cursor.style_id != style.default_id); + + // Either the capacity increased or the page was split/changed + const current_page = &s.cursor.page_pin.node.data; + const capacity_increased = current_page.capacity.styles > original_styles_capacity; + const page_changed = current_page != page; + try testing.expect(capacity_increased or page_changed); +} + +test "Screen setAttribute splits page on OutOfSpace at max styles" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, .{ + .cols = 10, + .rows = 10, + .max_scrollback = 0, + }); defer s.deinit(); + // Write content to multiple rows so we have something to split + try s.testWriteString("line1\nline2\nline3\nline4\nline5"); + + // Remember the original node + const original_node = s.cursor.page_pin.node; + // Increase the page's style capacity to max by repeatedly calling increaseCapacity + // Use Screen.increaseCapacity to properly maintain cursor state const max_styles = std.math.maxInt(size.CellCountInt); while (s.cursor.page_pin.node.data.capacity.styles < max_styles) { - _ = s.pages.increaseCapacity( + _ = s.increaseCapacity( s.cursor.page_pin.node, .styles, ) catch break; } - // Get the page reference after increaseCapacity - const page = &s.cursor.page_pin.node.data; + // Get the page reference after increaseCapacity - cursor may have moved + var page = &s.cursor.page_pin.node.data; try testing.expectEqual(max_styles, page.capacity.styles); - // Fill up all style slots - page.pauseIntegrityChecks(true); - var n: u24 = 1; - while (page.styles.add( - page.memory, - .{ .bg_color = .{ .rgb = @bitCast(n) } }, - )) |_| n += 1 else |_| {} - page.pauseIntegrityChecks(false); + // Fill the style map to capacity + { + page.pauseIntegrityChecks(true); + defer page.pauseIntegrityChecks(false); + defer page.assertIntegrity(); + + var n: u24 = 1; + while (page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(n) } }, + )) |_| n += 1 else |_| {} + } - // Set a style that we can track as "prior" - // Use a style that's already in the page by reusing one we added - s.cursor.style = .{ .bg_color = .{ .rgb = @bitCast(@as(u24, 1)) } }; - const prior_id = page.styles.add(page.memory, s.cursor.style) catch unreachable; - s.cursor.style_id = prior_id; + // Track the node before setAttribute + const node_before_set = s.cursor.page_pin.node; // Now try to set a new unique attribute that would require a new style slot - // This should fail with OutOfSpace since we're at max capacity - const result = s.setAttribute(.bold); + // At max capacity, increaseCapacity will return OutOfSpace, triggering page split + try s.setAttribute(.bold); + + // The style should have been applied (bold flag set) + try testing.expect(s.cursor.style.flags.bold); - // Should return OutOfSpace error - try testing.expectError(error.OutOfSpace, result); + // The cursor should have a valid non-default style_id + try testing.expect(s.cursor.style_id != style.default_id); - // The cursor style_id should remain the prior one - try testing.expectEqual(prior_id, s.cursor.style_id); + // The page should have been split + const page_was_split = s.cursor.page_pin.node != node_before_set or + node_before_set.next != null or + node_before_set.prev != null or + s.cursor.page_pin.node != original_node; + try testing.expect(page_was_split); } -- 2.51.2 From a8b31ceb8489d8e26a9a62155752672dcb4dcd0a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 Jan 2026 12:04:34 -0800 Subject: [PATCH 307/349] terminal: restoreCursor is now longer fallible We need to have sane behavior in error handling because the running program that sends the restore cursor command has no way to realize it failed. So if our style fails to add (our only fail case) then we revert to no style. https://ampcode.com/threads/T-019bd7dc-cf0b-7439-ad2f-218b3406277a --- src/terminal/Screen.zig | 32 +++++++---- src/terminal/Terminal.zig | 99 +++++++++++++++++++++++++------- src/terminal/stream_readonly.zig | 4 +- src/termio/stream_handler.zig | 4 +- 4 files changed, 103 insertions(+), 36 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 5ea338b70..00bf4078d 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -9350,17 +9350,21 @@ test "Screen setAttribute increases capacity when style map is full" { const page = &s.cursor.page_pin.node.data; const original_styles_capacity = page.capacity.styles; - // Fill the style map to capacity + // Fill the style map to capacity using the StyleSet's layout capacity + // which accounts for the load factor { page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false); defer page.assertIntegrity(); - var n: u24 = 1; - while (page.styles.add( - page.memory, - .{ .bg_color = .{ .rgb = @bitCast(n) } }, - )) |_| n += 1 else |_| {} + const max_items = page.styles.layout.cap; + var n: usize = 1; + while (n < max_items) : (n += 1) { + _ = page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, + ) catch break; + } } // Now try to set a new unique attribute that would require a new style slot @@ -9411,17 +9415,21 @@ test "Screen setAttribute splits page on OutOfSpace at max styles" { var page = &s.cursor.page_pin.node.data; try testing.expectEqual(max_styles, page.capacity.styles); - // Fill the style map to capacity + // Fill the style map to capacity using the StyleSet's layout capacity + // which accounts for the load factor { page.pauseIntegrityChecks(true); defer page.pauseIntegrityChecks(false); defer page.assertIntegrity(); - var n: u24 = 1; - while (page.styles.add( - page.memory, - .{ .bg_color = .{ .rgb = @bitCast(n) } }, - )) |_| n += 1 else |_| {} + const max_items = page.styles.layout.cap; + var n: usize = 1; + while (n < max_items) : (n += 1) { + _ = page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, + ) catch break; + } } // Track the node before setAttribute diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 3740397d3..73571874e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -996,7 +996,7 @@ pub fn saveCursor(self: *Terminal) void { /// /// The primary and alternate screen have distinct save state. /// If no save was done before values are reset to their initial values. -pub fn restoreCursor(self: *Terminal) !void { +pub fn restoreCursor(self: *Terminal) void { const saved: Screen.SavedCursor = self.screens.active.saved_cursor orelse .{ .x = 0, .y = 0, @@ -1008,10 +1008,17 @@ pub fn restoreCursor(self: *Terminal) !void { }; // Set the style first because it can fail - const old_style = self.screens.active.cursor.style; self.screens.active.cursor.style = saved.style; - errdefer self.screens.active.cursor.style = old_style; - try self.screens.active.manualStyleUpdate(); + self.screens.active.manualStyleUpdate() catch |err| { + // Regardless of the error here, we revert back to an unstyled + // cursor. It is more important that the restore succeeds in + // other attributes because terminals have no way to communicate + // failure back. + log.warn("restoreCursor error updating style err={}", .{err}); + const screen: *Screen = self.screens.active; + screen.cursor.style = .{}; + screen.cursor.style_id = style.default_id; + }; self.screens.active.charset = saved.charset; self.modes.set(.origin, saved.origin); @@ -2747,12 +2754,7 @@ pub fn switchScreenMode( } } else { assert(self.screens.active_key == .primary); - self.restoreCursor() catch |err| { - log.warn( - "restore cursor on switch screen failed to={} err={}", - .{ to, err }, - ); - }; + self.restoreCursor(); }, } } @@ -4807,7 +4809,7 @@ test "Terminal: horizontal tab back with cursor before left margin" { t.saveCursor(); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(5, 0); - try t.restoreCursor(); + t.restoreCursor(); try t.horizontalTabBack(); try t.print('X'); @@ -9873,7 +9875,7 @@ test "Terminal: saveCursor" { t.screens.active.charset.gr = .G0; try t.setAttribute(.{ .unset = {} }); t.modes.set(.origin, false); - try t.restoreCursor(); + t.restoreCursor(); try testing.expect(t.screens.active.cursor.style.flags.bold); try testing.expect(t.screens.active.charset.gr == .G3); try testing.expect(t.modes.get(.origin)); @@ -9889,7 +9891,7 @@ test "Terminal: saveCursor position" { t.saveCursor(); t.setCursorPos(1, 1); try t.print('B'); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -9909,7 +9911,7 @@ test "Terminal: saveCursor pending wrap state" { t.saveCursor(); t.setCursorPos(1, 1); try t.print('B'); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -9929,7 +9931,7 @@ test "Terminal: saveCursor origin mode" { t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setTopAndBottomMargin(2, 4); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -9947,7 +9949,7 @@ test "Terminal: saveCursor resize" { t.setCursorPos(1, 10); t.saveCursor(); try t.resize(alloc, 5, 5); - try t.restoreCursor(); + t.restoreCursor(); try t.print('X'); { @@ -9968,7 +9970,7 @@ test "Terminal: saveCursor protected pen" { t.saveCursor(); t.setProtectedMode(.off); try testing.expect(!t.screens.active.cursor.protected); - try t.restoreCursor(); + t.restoreCursor(); try testing.expect(t.screens.active.cursor.protected); } @@ -9981,10 +9983,67 @@ test "Terminal: saveCursor doesn't modify hyperlink state" { const id = t.screens.active.cursor.hyperlink_id; t.saveCursor(); try testing.expectEqual(id, t.screens.active.cursor.hyperlink_id); - try t.restoreCursor(); + t.restoreCursor(); try testing.expectEqual(id, t.screens.active.cursor.hyperlink_id); } +test "Terminal: restoreCursor uses default style on OutOfSpace" { + // Tests that restoreCursor falls back to default style when + // manualStyleUpdate fails with OutOfSpace (can't split a 1-row page + // and styles are at max capacity). + const alloc = testing.allocator; + + // Use a single row so the page can't be split + var t = try init(alloc, .{ .cols = 10, .rows = 1 }); + defer t.deinit(alloc); + + // Set a style and save the cursor + try t.setAttribute(.{ .bold = {} }); + t.saveCursor(); + + // Clear the style + try t.setAttribute(.{ .unset = {} }); + try testing.expect(!t.screens.active.cursor.style.flags.bold); + + // Fill the style map to max capacity + const max_styles = std.math.maxInt(size.CellCountInt); + while (t.screens.active.cursor.page_pin.node.data.capacity.styles < max_styles) { + _ = t.screens.active.increaseCapacity( + t.screens.active.cursor.page_pin.node, + .styles, + ) catch break; + } + + const page = &t.screens.active.cursor.page_pin.node.data; + try testing.expectEqual(max_styles, page.capacity.styles); + + // Fill all style slots using the StyleSet's layout capacity which accounts + // for the load factor. The capacity in the layout is the actual max number + // of items that can be stored. + { + page.pauseIntegrityChecks(true); + defer page.pauseIntegrityChecks(false); + defer page.assertIntegrity(); + + const max_items = page.styles.layout.cap; + var n: usize = 1; + while (n < max_items) : (n += 1) { + _ = page.styles.add( + page.memory, + .{ .bg_color = .{ .rgb = @bitCast(@as(u24, @intCast(n))) } }, + ) catch break; + } + } + + // Restore cursor - should fall back to default style since page + // can't be split (1 row) and styles are at max capacity + t.restoreCursor(); + + // The style should be reset to default because OutOfSpace occurred + try testing.expect(!t.screens.active.cursor.style.flags.bold); + try testing.expectEqual(style.default_id, t.screens.active.cursor.style_id); +} + test "Terminal: setProtectedMode" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 3, .rows = 3 }); @@ -11376,7 +11435,7 @@ test "Terminal: resize with reflow and saved cursor" { t.saveCursor(); try t.resize(alloc, 5, 3); - try t.restoreCursor(); + t.restoreCursor(); { const str = try t.plainString(testing.allocator); @@ -11417,7 +11476,7 @@ test "Terminal: resize with reflow and saved cursor pending wrap" { t.saveCursor(); try t.resize(alloc, 5, 3); - try t.restoreCursor(); + t.restoreCursor(); { const str = try t.plainString(testing.allocator); diff --git a/src/terminal/stream_readonly.zig b/src/terminal/stream_readonly.zig index 1ee4f3f08..9b4999116 100644 --- a/src/terminal/stream_readonly.zig +++ b/src/terminal/stream_readonly.zig @@ -125,7 +125,7 @@ pub const Handler = struct { } }, .save_cursor => self.terminal.saveCursor(), - .restore_cursor => try self.terminal.restoreCursor(), + .restore_cursor => self.terminal.restoreCursor(), .invoke_charset => self.terminal.invokeCharset(value.bank, value.charset, value.locking), .configure_charset => self.terminal.configureCharset(value.slot, value.charset), .set_attribute => switch (value) { @@ -240,7 +240,7 @@ pub const Handler = struct { .save_cursor => if (enabled) { self.terminal.saveCursor(); } else { - try self.terminal.restoreCursor(); + self.terminal.restoreCursor(); }, .enable_mode_3 => {}, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 082a0fa10..2b92c19e3 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -721,7 +721,7 @@ pub const StreamHandler = struct { if (enabled) { self.terminal.saveCursor(); } else { - try self.terminal.restoreCursor(); + self.terminal.restoreCursor(); } }, @@ -933,7 +933,7 @@ pub const StreamHandler = struct { } pub inline fn restoreCursor(self: *StreamHandler) !void { - try self.terminal.restoreCursor(); + self.terminal.restoreCursor(); } pub fn enquiry(self: *StreamHandler) !void { -- 2.51.2 From ae8d2c7a3e135b96ad7d3827dc1b8f92b49c8cd4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 Jan 2026 12:17:07 -0800 Subject: [PATCH 308/349] terminal: fix up some of the manual handling, comments --- src/terminal/Screen.zig | 21 +++++++++++---------- src/terminal/Terminal.zig | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 00bf4078d..d36cdac2a 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1804,7 +1804,7 @@ pub fn setAttribute( self.manualStyleUpdate() catch |err| { log.warn("setAttribute error restoring old style after failure err={}", .{err}); self.cursor.style = .{}; - self.cursor.style_id = style.default_id; + self.manualStyleUpdate() catch unreachable; }; } @@ -1951,17 +1951,18 @@ pub fn setAttribute( /// Call this whenever you manually change the cursor style. /// -/// If this returns an error, the style change did not take effect and -/// the cursor style is reverted back to the default. +/// This function can NOT fail if the cursor style is changing to the +/// default style. /// -/// Note that this can return any PageList capacity error, because it -/// is possible for the internal pagelist to not accommodate the new style -/// at all. This WILL attempt to resize our internal pages to fit the style -/// but it is possible that it cannot be done, in which case upstream callers -/// need to split the page or do something else. +/// If this returns an error, the style change did not take effect and +/// the cursor style is reverted back to the default. The only scenario +/// this returns an error is if there is a physical memory allocation failure +/// or if there is no possible way to increase style capacity to store +/// the style. /// -/// NOTE(mitchellh): I think in the future we'll do page splitting -/// automatically here and remove this failure scenario. +/// This function WILL split pages as necessary to accommodate the new style. +/// So if OutOfSpace is returned, it means that even after splitting the page +/// there was still no room for the new style. pub fn manualStyleUpdate(self: *Screen) PageList.IncreaseCapacityError!void { defer self.assertIntegrity(); var page: *Page = &self.cursor.page_pin.node.data; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 73571874e..7e02e3a24 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1017,7 +1017,7 @@ pub fn restoreCursor(self: *Terminal) void { log.warn("restoreCursor error updating style err={}", .{err}); const screen: *Screen = self.screens.active; screen.cursor.style = .{}; - screen.cursor.style_id = style.default_id; + self.screens.active.manualStyleUpdate() catch unreachable; }; self.screens.active.charset = saved.charset; -- 2.51.2 From bff21222d438a7454b8d55c1e0afc72d85be599a Mon Sep 17 00:00:00 2001 From: evertonstz Date: Tue, 20 Jan 2026 12:12:04 -0300 Subject: [PATCH 309/349] Refactor gsettings keys to use string literals for GTK settings and update related tests --- src/apprt/gtk/class/surface.zig | 4 +-- src/apprt/gtk/gsettings.zig | 64 +++++++++++++++------------------ 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index ea5ca203f..56c39a86f 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1516,7 +1516,7 @@ pub const Surface = extern struct { const xft_dpi_scale = xft_scale: { // gtk-xft-dpi is font DPI multiplied by 1024. See // https://docs.gtk.org/gtk4/property.Settings.gtk-xft-dpi.html - const gtk_xft_dpi = gsettings.get(.gtk_xft_dpi) orelse { + const gtk_xft_dpi = gsettings.get(.@"gtk-xft-dpi") orelse { log.warn("gtk-xft-dpi was not set, using default value", .{}); break :xft_scale 1.0; }; @@ -1772,7 +1772,7 @@ pub const Surface = extern struct { // Read GNOME desktop interface settings for primary paste (middle-click) // This is only relevant on Linux systems with GNOME settings available - priv.gtk_enable_primary_paste = gsettings.get(.gtk_enable_primary_paste) orelse blk: { + priv.gtk_enable_primary_paste = gsettings.get(.@"gtk-enable-primary-paste") orelse blk: { log.warn("gtk-enable-primary-paste was not set, using default value", .{}); break :blk false; }; diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig index f96e9f01c..3fc3d01de 100644 --- a/src/apprt/gtk/gsettings.zig +++ b/src/apprt/gtk/gsettings.zig @@ -4,42 +4,34 @@ const gobject = @import("gobject"); /// GTK Settings keys with well-defined types. pub const Key = enum { - gtk_enable_primary_paste, - gtk_xft_dpi, - gtk_font_name, + @"gtk-enable-primary-paste", + @"gtk-xft-dpi", + @"gtk-font-name", fn Type(comptime self: Key) type { return switch (self) { - .gtk_enable_primary_paste => bool, - .gtk_xft_dpi => c_int, - .gtk_font_name => []const u8, + .@"gtk-enable-primary-paste" => bool, + .@"gtk-xft-dpi" => c_int, + .@"gtk-font-name" => []const u8, }; } fn GValueType(comptime self: Key) type { return switch (self) { // Booleans are stored as integers in GTK's internal representation - .gtk_enable_primary_paste, + .@"gtk-enable-primary-paste", => c_int, // Integer types - .gtk_xft_dpi, + .@"gtk-xft-dpi", => c_int, // String types (returned as null-terminated C strings from GTK) - .gtk_font_name, + .@"gtk-font-name", => ?[*:0]const u8, }; } - fn propertyName(comptime self: Key) [*:0]const u8 { - return switch (self) { - .gtk_enable_primary_paste => "gtk-enable-primary-paste", - .gtk_xft_dpi => "gtk-xft-dpi", - .gtk_font_name => "gtk-font-name", - }; - } - /// Returns true if this setting type requires memory allocation. /// this is defensive: types that do not need allocation need to be /// explicitly marked here @@ -58,8 +50,8 @@ pub const Key = enum { /// No allocator is required or used. Returns null if the setting is not available or cannot be read. /// /// Example usage: -/// const enabled = get(.gtk_enable_primary_paste); -/// const dpi = get(.gtk_xft_dpi); +/// const enabled = get(.@"gtk-enable-primary-paste"); +/// const dpi = get(.@"gtk-xft-dpi"); pub fn get(comptime key: Key) ?key.Type() { if (comptime key.requiresAllocation()) { @compileError("Allocating types require an allocator; use getAlloc() instead"); @@ -92,20 +84,20 @@ fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: var value = gobject.ext.Value.new(GValType); defer value.unset(); - settings.as(gobject.Object).getProperty(key.propertyName(), &value); + settings.as(gobject.Object).getProperty(@tagName(key).ptr, &value); return switch (key) { // Booleans are stored as integers in GTK, convert to bool - .gtk_enable_primary_paste, + .@"gtk-enable-primary-paste", => value.getInt() != 0, // Integer types are returned directly - .gtk_xft_dpi, + .@"gtk-xft-dpi", => value.getInt(), // Strings: GTK owns the GValue's pointer, so we must duplicate it // before the GValue is destroyed by defer value.unset() - .gtk_font_name, + .@"gtk-font-name", => blk: { // This is defensive: we have already checked at compile-time that // an allocator is provided for allocating types @@ -118,25 +110,25 @@ fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: } test "Key.Type returns correct types" { - try std.testing.expectEqual(bool, Key.gtk_enable_primary_paste.Type()); - try std.testing.expectEqual(c_int, Key.gtk_xft_dpi.Type()); - try std.testing.expectEqual([]const u8, Key.gtk_font_name.Type()); + try std.testing.expectEqual(bool, Key.@"gtk-enable-primary-paste".Type()); + try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".Type()); + try std.testing.expectEqual([]const u8, Key.@"gtk-font-name".Type()); } test "Key.requiresAllocation identifies allocating types" { - try std.testing.expectEqual(false, Key.gtk_enable_primary_paste.requiresAllocation()); - try std.testing.expectEqual(false, Key.gtk_xft_dpi.requiresAllocation()); - try std.testing.expectEqual(true, Key.gtk_font_name.requiresAllocation()); + try std.testing.expectEqual(false, Key.@"gtk-enable-primary-paste".requiresAllocation()); + try std.testing.expectEqual(false, Key.@"gtk-xft-dpi".requiresAllocation()); + try std.testing.expectEqual(true, Key.@"gtk-font-name".requiresAllocation()); } test "Key.GValueType returns correct GObject types" { - try std.testing.expectEqual(c_int, Key.gtk_enable_primary_paste.GValueType()); - try std.testing.expectEqual(c_int, Key.gtk_xft_dpi.GValueType()); - try std.testing.expectEqual(?[*:0]const u8, Key.gtk_font_name.GValueType()); + try std.testing.expectEqual(c_int, Key.@"gtk-enable-primary-paste".GValueType()); + try std.testing.expectEqual(c_int, Key.@"gtk-xft-dpi".GValueType()); + try std.testing.expectEqual(?[*:0]const u8, Key.@"gtk-font-name".GValueType()); } -test "Key.propertyName returns correct GTK property names" { - try std.testing.expectEqualSlices(u8, "gtk-enable-primary-paste", std.mem.span(Key.gtk_enable_primary_paste.propertyName())); - try std.testing.expectEqualSlices(u8, "gtk-xft-dpi", std.mem.span(Key.gtk_xft_dpi.propertyName())); - try std.testing.expectEqualSlices(u8, "gtk-font-name", std.mem.span(Key.gtk_font_name.propertyName())); +test "@tagName returns correct GTK property names" { + try std.testing.expectEqualStrings("gtk-enable-primary-paste", @tagName(Key.@"gtk-enable-primary-paste")); + try std.testing.expectEqualStrings("gtk-xft-dpi", @tagName(Key.@"gtk-xft-dpi")); + try std.testing.expectEqualStrings("gtk-font-name", @tagName(Key.@"gtk-font-name")); } -- 2.51.2 From f2b5a9192a85c78b94fff03869fce147856a9d41 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Tue, 20 Jan 2026 17:05:14 +0100 Subject: [PATCH 310/349] gtk: Clean up title sorting --- src/apprt/gtk/class/command_palette.zig | 27 ++++++++----------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index af1ef8e5e..ea214bf6d 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -250,12 +250,10 @@ pub const CommandPalette = extern struct { const b_title = b.propGetTitle() orelse return true; // Compare case-insensitively with colon normalization - var i: usize = 0; - var j: usize = 0; - while (i < a_title.len and j < b_title.len) { + for (0..@min(a_title.len, b_title.len)) |i| { // Get characters, replacing ':' with '\t' const a_char = if (a_title[i] == ':') '\t' else a_title[i]; - const b_char = if (b_title[j] == ':') '\t' else b_title[j]; + const b_char = if (b_title[i] == ':') '\t' else b_title[i]; const a_lower = std.ascii.toLower(a_char); const b_lower = std.ascii.toLower(b_char); @@ -263,9 +261,6 @@ pub const CommandPalette = extern struct { if (a_lower != b_lower) { return a_lower < b_lower; } - - i += 1; - j += 1; } // If one title is a prefix of the other, shorter one comes first @@ -273,23 +268,17 @@ pub const CommandPalette = extern struct { return a_title.len < b_title.len; } - // Titles are equal - use sort_key as tie-breaker if both have one - const a_priv = a.private(); - const b_priv = b.private(); - - const a_sort_key = switch (a_priv.data) { - .regular => 0, + // Titles are equal - use sort_key as tie-breaker if both are jump commands + const a_sort_key = switch (a.private().data) { + .regular => return false, .jump => |*ja| ja.sort_key, }; - const b_sort_key = switch (b_priv.data) { - .regular => 0, + const b_sort_key = switch (b.private().data) { + .regular => return false, .jump => |*jb| jb.sort_key, }; - if (a_sort_key != 0 and b_sort_key != 0) { - return a_sort_key < b_sort_key; - } - return false; + return a_sort_key < b_sort_key; } fn close(self: *CommandPalette) void { -- 2.51.2 From 0c8b51c7ab95633a0c5c5f6e6397136736960d18 Mon Sep 17 00:00:00 2001 From: Tommy Brunn Date: Tue, 20 Jan 2026 17:09:15 +0100 Subject: [PATCH 311/349] gtk: Add todo for replacing jump sort key with surface id Using a pointer for this is a bit icky. Once Ghostty adds unique ids to surfaces, we can sort by that id instead. This can potentially also be used to navigate to the surface instead of having the command palette reference the surfaces directly. --- src/apprt/gtk/class/command_palette.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/class/command_palette.zig b/src/apprt/gtk/class/command_palette.zig index ea214bf6d..0d91c43b2 100644 --- a/src/apprt/gtk/class/command_palette.zig +++ b/src/apprt/gtk/class/command_palette.zig @@ -582,6 +582,7 @@ const Command = extern struct { const priv = self.private(); priv.data = .{ .jump = .{ + // TODO: Replace with surface id whenever Ghostty adds one .sort_key = @intFromPtr(surface), }, }; -- 2.51.2 From 9c5d4a5511c81fb3fe1e8b6b420af438e58685f0 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:27:14 +0200 Subject: [PATCH 312/349] ghosttyKit: add `ghostty_config_load_file` --- include/ghostty.h | 1 + src/config/CApi.zig | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index 0133fac73..3d3973084 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -1024,6 +1024,7 @@ ghostty_config_t ghostty_config_new(); void ghostty_config_free(ghostty_config_t); ghostty_config_t ghostty_config_clone(ghostty_config_t); void ghostty_config_load_cli_args(ghostty_config_t); +void ghostty_config_load_file(ghostty_config_t, const char*); void ghostty_config_load_default_files(ghostty_config_t); void ghostty_config_load_recursive_files(ghostty_config_t); void ghostty_config_finalize(ghostty_config_t); diff --git a/src/config/CApi.zig b/src/config/CApi.zig index a970a8d33..4ea9ea63f 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -65,6 +65,15 @@ export fn ghostty_config_load_default_files(self: *Config) void { }; } +/// Load the configuration from a specific file path. +/// The path must be null-terminated. +export fn ghostty_config_load_file(self: *Config, path: [*:0]const u8) void { + const path_slice = std.mem.span(path); + self.loadFile(state.alloc, path_slice) catch |err| { + log.err("error loading config from file path={s} err={}", .{ path_slice, err }); + }; +} + /// Load the configuration from the user-specified configuration /// file locations in the previously loaded configuration. This will /// recursively continue to load up to a built-in limit. -- 2.51.2 From 4f667520faf7a45d7bdd37575195d4108ca4841e Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:27:49 +0200 Subject: [PATCH 313/349] macOS: GhosttyUITests --- macos/Ghostty.xcodeproj/project.pbxproj | 144 +++++++++++++++++- .../xcshareddata/xcschemes/Ghostty.xcscheme | 11 ++ .../GhosttyUITests/GhosttyTitleUITests.swift | 39 +++++ macos/Sources/App/macOS/AppDelegate.swift | 7 +- macos/Sources/Ghostty/Ghostty.App.swift | 37 ++++- macos/Sources/Ghostty/Ghostty.Config.swift | 14 +- 6 files changed, 239 insertions(+), 13 deletions(-) create mode 100644 macos/GhosttyUITests/GhosttyTitleUITests.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 0b3432362..d903c8815 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -28,6 +28,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5B30529299BEAAA0047F10C /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5B30530299BEAAA0047F10C; + remoteInfo = Ghostty; + }; A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A5B30529299BEAAA0047F10C /* Project object */; @@ -42,6 +49,7 @@ 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; 55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = ""; }; 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; + 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = ""; }; A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = ""; }; @@ -198,11 +206,19 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 810ACCA02E9D3302004F8F92 /* GhosttyUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyUITests; sourceTree = ""; }; 81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = ""; }; A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 810ACC9C2E9D3301004F8F92 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F02E1F047A0046BD5C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -259,6 +275,7 @@ 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */, 81F82BC72E82815D001EDFA7 /* Sources */, A54F45F42E1F047A0046BD5C /* Tests */, + 810ACCA02E9D3302004F8F92 /* GhosttyUITests */, A5D495A3299BECBA00DD1313 /* Frameworks */, A5A1F8862A489D7400D1E8BC /* Resources */, A5B30532299BEAAA0047F10C /* Products */, @@ -271,6 +288,7 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */, A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */, + 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */, ); name = Products; sourceTree = ""; @@ -287,6 +305,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 810ACC9E2E9D3301004F8F92 /* GhosttyUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 810ACCA72E9D3302004F8F92 /* Build configuration list for PBXNativeTarget "GhosttyUITests" */; + buildPhases = ( + 810ACC9B2E9D3301004F8F92 /* Sources */, + 810ACC9C2E9D3301004F8F92 /* Frameworks */, + 810ACC9D2E9D3301004F8F92 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 810ACCA62E9D3302004F8F92 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 810ACCA02E9D3302004F8F92 /* GhosttyUITests */, + ); + name = GhosttyUITests; + packageProductDependencies = ( + ); + productName = GhosttyUITests; + productReference = 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; A54F45F22E1F047A0046BD5C /* GhosttyTests */ = { isa = PBXNativeTarget; buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */; @@ -360,9 +401,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2600; + LastSwiftUpdateCheck = 2610; LastUpgradeCheck = 1610; TargetAttributes = { + 810ACC9E2E9D3301004F8F92 = { + CreatedOnToolsVersion = 26.1; + TestTargetID = A5B30530299BEAAA0047F10C; + }; A54F45F22E1F047A0046BD5C = { CreatedOnToolsVersion = 26.0; TestTargetID = A5B30530299BEAAA0047F10C; @@ -395,11 +440,19 @@ A5B30530299BEAAA0047F10C /* Ghostty */, A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, A54F45F22E1F047A0046BD5C /* GhosttyTests */, + 810ACC9E2E9D3301004F8F92 /* GhosttyUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 810ACC9D2E9D3301004F8F92 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45F12E1F047A0046BD5C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -438,6 +491,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 810ACC9B2E9D3301004F8F92 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A54F45EF2E1F047A0046BD5C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -462,6 +522,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 810ACCA62E9D3302004F8F92 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5B30530299BEAAA0047F10C /* Ghostty */; + targetProxy = 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */; + }; A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A5B30530299BEAAA0047F10C /* Ghostty */; @@ -579,6 +644,73 @@ }; name = ReleaseLocal; }; + 810ACCA82E9D3302004F8F92 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Ghostty; + }; + name = Debug; + }; + 810ACCA92E9D3302004F8F92 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Ghostty; + }; + name = Release; + }; + 810ACCAA2E9D3302004F8F92 /* ReleaseLocal */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = Ghostty; + }; + name = ReleaseLocal; + }; A54F45F92E1F047A0046BD5C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -995,6 +1127,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 810ACCA72E9D3302004F8F92 /* Build configuration list for PBXNativeTarget "GhosttyUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 810ACCA82E9D3302004F8F92 /* Debug */, + 810ACCA92E9D3302004F8F92 /* Release */, + 810ACCAA2E9D3302004F8F92 /* ReleaseLocal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseLocal; + }; A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme index 0d8761c9e..2b4f815ea 100644 --- a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme +++ b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme @@ -40,6 +40,17 @@ ReferencedContainer = "container:Ghostty.xcodeproj"> + + + + Ghostty.Config? { + guard + let cfg = ghostty_config_new() + else { + return nil + } + if FileManager.default.fileExists(atPath: path) { + ghostty_config_load_file(cfg, path) + } + if !isRunningInXcode() { + ghostty_config_load_cli_args(cfg) + } + ghostty_config_load_recursive_files(cfg) + if finalize { + // Finalize will make our defaults available, + // and also will combine all the keys into one file, + // we might not need this in the future + ghostty_config_finalize(cfg) + } + return Ghostty.Config(config: cfg) + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index b3a8700e9..fb435c014 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -33,14 +33,16 @@ extension Ghostty { return diags } - init() { - if let cfg = Self.loadConfig() { - self.config = cfg - } + init(config: ghostty_config_t?) { + self.config = config + } + + convenience init() { + self.init(config: Self.loadConfig()) } - init(clone config: ghostty_config_t) { - self.config = ghostty_config_clone(config) + convenience init(clone config: ghostty_config_t) { + self.init(config: ghostty_config_clone(config)) } deinit { -- 2.51.2 From a94a6e4b36b7702a397b9dc47b31bc3bf212937b Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:15:23 +0200 Subject: [PATCH 314/349] build: fix Ghostty-iOS compiling --- macos/Sources/Ghostty/Ghostty.App.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 06606fae4..8e32b82af 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -2088,9 +2088,11 @@ extension Ghostty.App { if FileManager.default.fileExists(atPath: path) { ghostty_config_load_file(cfg, path) } +#if os(macOS) if !isRunningInXcode() { ghostty_config_load_cli_args(cfg) } +#endif ghostty_config_load_recursive_files(cfg) if finalize { // Finalize will make our defaults available, -- 2.51.2 From bcf42dde6c6642eb5f9d53aa2676f642bb9ccb3a Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:18:18 +0200 Subject: [PATCH 315/349] build: change deployment target --- macos/Ghostty.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index d903c8815..adcc107e1 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -653,7 +653,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.1; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -676,7 +676,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.1; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -698,7 +698,7 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 26.1; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests; PRODUCT_NAME = "$(TARGET_NAME)"; -- 2.51.2 From b1290dc47b0521fd7c9085e4b7c7043048216903 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:07:05 +0200 Subject: [PATCH 316/349] macOS: add titlebar tabs tests --- .../GhosttyCustomConfigCase.swift | 43 +++++++ .../GhosttyUITests/GhosttyTitleUITests.swift | 24 +--- .../GhosttyTitlebarTabsUITests.swift | 121 ++++++++++++++++++ 3 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 macos/GhosttyUITests/GhosttyCustomConfigCase.swift create mode 100644 macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift new file mode 100644 index 000000000..a944e9f52 --- /dev/null +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -0,0 +1,43 @@ +// +// GhosttyCustomConfigCase.swift +// Ghostty +// +// Created by luca on 16.10.2025. +// + +import XCTest + +class GhosttyCustomConfigCase: XCTestCase { + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + var configFile: URL? + override func setUpWithError() throws { + continueAfterFailure = false + guard let customGhosttyConfig else { + return + } + let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ghostty") + try customGhosttyConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) + configFile = temporaryConfig + } + + override func tearDown() async throws { + if let configFile { + try FileManager.default.removeItem(at: configFile) + } + } + + var customGhosttyConfig: String? { + nil + } + + func ghosttyApplication() -> XCUIApplication { + let app = XCUIApplication() + app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile?.path + app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"]) + return app + } +} diff --git a/macos/GhosttyUITests/GhosttyTitleUITests.swift b/macos/GhosttyUITests/GhosttyTitleUITests.swift index 2b212212f..5e9ade1c9 100644 --- a/macos/GhosttyUITests/GhosttyTitleUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitleUITests.swift @@ -7,31 +7,15 @@ import XCTest -final class GhosttyTitleUITests: XCTestCase { +final class GhosttyTitleUITests: GhosttyCustomConfigCase { - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - var configFile: URL? - override func setUpWithError() throws { - continueAfterFailure = false - let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - .appendingPathExtension("ghostty") - try #"title = "GhosttyUITestsLaunchTests""#.write(to: temporaryConfig, atomically: true, encoding: .utf8) - configFile = temporaryConfig - } - - override func tearDown() async throws { - if let configFile { - try FileManager.default.removeItem(at: configFile) - } + override var customGhosttyConfig: String? { + #"title = "GhosttyUITestsLaunchTests""# } @MainActor func testTitle() throws { - let app = XCUIApplication() - app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile?.path + let app = ghosttyApplication() app.launch() XCTAssert(app.windows.firstMatch.title == "GhosttyUITestsLaunchTests", "Oops, `title=` doesn't work!") diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift new file mode 100644 index 000000000..13319c07e --- /dev/null +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -0,0 +1,121 @@ +// +// GhosttyTitlebarTabsUITests.swift +// Ghostty +// +// Created by luca on 16.10.2025. +// + +import XCTest + +final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { + override var customGhosttyConfig: String? { + """ + macos-titlebar-style = tabs + title = "GhosttyTitlebarTabsUITests" + """ + } + + @MainActor + func testCustomTitlebar() throws { + let app = ghosttyApplication() + app.launch() + // create a split + app.groups["Terminal pane"].typeKey("d", modifierFlags: .command) + app.typeKey("\n", modifierFlags: [.command, .shift]) + let resetZoomButton = app.groups.buttons["ResetZoom"] + let windowTitle = app.windows.firstMatch.title + let titleView = app.staticTexts.element(matching: NSPredicate(format: "value == '\(windowTitle)'")) + + XCTAssertEqual(titleView.frame.midY, resetZoomButton.frame.midY, accuracy: 1, "Window title should be vertically centered with reset zoom button: \(titleView.frame.midY) != \(resetZoomButton.frame.midY)") + } + + @MainActor + func testTabsGeometryInNormalWindow() throws { + let app = ghosttyApplication() + app.launch() + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + XCTAssert(app.tabs.count == 2, "There should be 2 tabs") + checkTabsGeometry(app.windows.element(boundBy: 0)) + } + + @MainActor + func testTabsGeometryInFullscreen() throws { + let app = ghosttyApplication() + app.launch() + app.typeKey("f", modifierFlags: [.command, .control]) + // using app to type โŒ˜+t might not be able to create tabs + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + XCTAssert(app.tabs.count == 2, "There should be 2 tabs") + checkTabsGeometry(app.windows.element(boundBy: 0)) + } + + @MainActor + func testTabsGeometryAfterMovingTabs() throws { + let app = ghosttyApplication() + app.launch() + // create 3 tabs + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) + + // move to the left + app.menuItems["_zoomLeft:"].firstMatch.click() + + // create another window with 2 tabs + app.windows.element(boundBy: 0).groups["Terminal pane"].typeKey("n", modifierFlags: .command) + XCTAssert(app.windows.count == 2, "There should be 2 windows") + + // move to the right + app.menuItems["_zoomRight:"].firstMatch.click() + + // now second window is the first/main one in the list + app.windows.element(boundBy: 0).groups["Terminal pane"].typeKey("t", modifierFlags: .command) + + app.windows.element(boundBy: 1).tabs.element(boundBy: 0).click() // focus first window + + // now the first window is the main one + let firstTabInFirstWindow = app.windows.element(boundBy: 0).tabs.element(boundBy: 0) + let firstTabInSecondWindow = app.windows.element(boundBy: 1).tabs.element(boundBy: 0) + + // drag a tab from one window to another + firstTabInFirstWindow.press(forDuration: 1, thenDragTo: firstTabInSecondWindow) + + // check tabs in the first + checkTabsGeometry(app.windows.firstMatch) + // focus another window + app.windows.element(boundBy: 1).click() + checkTabsGeometry(app.windows.firstMatch) + } + + func checkTabsGeometry(_ window: XCUIElement) { + let closeTabButtons = window.buttons.matching(identifier: "_closeButton") + + XCTAssert(closeTabButtons.count == window.tabs.count, "Close tab buttons count should match tabs count") + + var previousTabHeight: CGFloat? + for idx in 0 ..< window.tabs.count { + let currentTab = window.tabs.element(boundBy: idx) + // focus + currentTab.click() + // switch to the tab + window.typeKey("\(idx + 1)", modifierFlags: .command) + // add a split + window.typeKey("d", modifierFlags: .command) + // zoom this split + // haven't found a way to locate our reset zoom button yet.. + window.typeKey("\n", modifierFlags: [.command, .shift]) + window.typeKey("\n", modifierFlags: [.command, .shift]) + + if let previousHeight = previousTabHeight { + XCTAssertEqual(currentTab.frame.height, previousHeight, accuracy: 1, "The tab's height should stay the same") + } + previousTabHeight = currentTab.frame.height + + let titleFrame = currentTab.frame + let shortcutLabelFrame = window.staticTexts.element(matching: NSPredicate(format: "value CONTAINS[c] 'โŒ˜\(idx + 1)'")).firstMatch.frame + let closeButtonFrame = closeTabButtons.element(boundBy: idx).frame + + XCTAssertEqual(titleFrame.midY, shortcutLabelFrame.midY, accuracy: 1, "Tab title should be vertically centered with its shortcut label: \(titleFrame.midY) != \(shortcutLabelFrame.midY)") + XCTAssertEqual(titleFrame.midY, closeButtonFrame.midY, accuracy: 1, "Tab title should be vertically centered with its close button: \(titleFrame.midY) != \(closeButtonFrame.midY)") + } + } +} -- 2.51.2 From f7608d0b95360a546b1b25d33f8bfb05e0eb6e04 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:11:51 +0200 Subject: [PATCH 317/349] macOS: reduce press duration --- macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index 13319c07e..f6d4563cc 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -77,7 +77,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { let firstTabInSecondWindow = app.windows.element(boundBy: 1).tabs.element(boundBy: 0) // drag a tab from one window to another - firstTabInFirstWindow.press(forDuration: 1, thenDragTo: firstTabInSecondWindow) + firstTabInFirstWindow.press(forDuration: 0.2, thenDragTo: firstTabInSecondWindow) // check tabs in the first checkTabsGeometry(app.windows.firstMatch) -- 2.51.2 From f088ce38d9f736130ff3b8f1a11aa0d353a27f1c Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 17 Oct 2025 00:09:25 +0200 Subject: [PATCH 318/349] test: read config before launching XCUIApplication --- .../GhosttyCustomConfigCase.swift | 18 +++++++++--------- macos/GhosttyUITests/GhosttyTitleUITests.swift | 2 +- .../GhosttyTitlebarTabsUITests.swift | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index a944e9f52..112dc1da4 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -15,13 +15,6 @@ class GhosttyCustomConfigCase: XCTestCase { var configFile: URL? override func setUpWithError() throws { continueAfterFailure = false - guard let customGhosttyConfig else { - return - } - let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - .appendingPathExtension("ghostty") - try customGhosttyConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) - configFile = temporaryConfig } override func tearDown() async throws { @@ -34,10 +27,17 @@ class GhosttyCustomConfigCase: XCTestCase { nil } - func ghosttyApplication() -> XCUIApplication { + func ghosttyApplication() throws -> XCUIApplication { let app = XCUIApplication() - app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile?.path app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"]) + guard let customGhosttyConfig else { + return app + } + let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ghostty") + try customGhosttyConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) + configFile = temporaryConfig + app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile?.path return app } } diff --git a/macos/GhosttyUITests/GhosttyTitleUITests.swift b/macos/GhosttyUITests/GhosttyTitleUITests.swift index 5e9ade1c9..904f54220 100644 --- a/macos/GhosttyUITests/GhosttyTitleUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitleUITests.swift @@ -15,7 +15,7 @@ final class GhosttyTitleUITests: GhosttyCustomConfigCase { @MainActor func testTitle() throws { - let app = ghosttyApplication() + let app = try ghosttyApplication() app.launch() XCTAssert(app.windows.firstMatch.title == "GhosttyUITestsLaunchTests", "Oops, `title=` doesn't work!") diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index f6d4563cc..949abe7e1 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -17,7 +17,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { @MainActor func testCustomTitlebar() throws { - let app = ghosttyApplication() + let app = try ghosttyApplication() app.launch() // create a split app.groups["Terminal pane"].typeKey("d", modifierFlags: .command) @@ -31,7 +31,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { @MainActor func testTabsGeometryInNormalWindow() throws { - let app = ghosttyApplication() + let app = try ghosttyApplication() app.launch() app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) XCTAssert(app.tabs.count == 2, "There should be 2 tabs") @@ -40,7 +40,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { @MainActor func testTabsGeometryInFullscreen() throws { - let app = ghosttyApplication() + let app = try ghosttyApplication() app.launch() app.typeKey("f", modifierFlags: [.command, .control]) // using app to type โŒ˜+t might not be able to create tabs @@ -51,7 +51,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { @MainActor func testTabsGeometryAfterMovingTabs() throws { - let app = ghosttyApplication() + let app = try ghosttyApplication() app.launch() // create 3 tabs app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) -- 2.51.2 From f52013787e9197334e9b1b2de4edb6094a7990c7 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Fri, 17 Oct 2025 00:27:18 +0200 Subject: [PATCH 319/349] test: using XCTAssertEqual to get more information --- .../GhosttyCustomConfigCase.swift | 3 +++ .../GhosttyUITests/GhosttyTitleUITests.swift | 2 +- .../GhosttyTitlebarTabsUITests.swift | 24 ++++++++++--------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index 112dc1da4..d4e2e5b89 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -33,6 +33,9 @@ class GhosttyCustomConfigCase: XCTestCase { guard let customGhosttyConfig else { return app } + if let configFile { + try FileManager.default.removeItem(at: configFile) + } let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) .appendingPathExtension("ghostty") try customGhosttyConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) diff --git a/macos/GhosttyUITests/GhosttyTitleUITests.swift b/macos/GhosttyUITests/GhosttyTitleUITests.swift index 904f54220..e6f6d67e6 100644 --- a/macos/GhosttyUITests/GhosttyTitleUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitleUITests.swift @@ -18,6 +18,6 @@ final class GhosttyTitleUITests: GhosttyCustomConfigCase { let app = try ghosttyApplication() app.launch() - XCTAssert(app.windows.firstMatch.title == "GhosttyUITestsLaunchTests", "Oops, `title=` doesn't work!") + XCTAssertEqual(app.windows.firstMatch.title, "GhosttyUITestsLaunchTests", "Oops, `title=` doesn't work!") } } diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index 949abe7e1..7e7b5edec 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -34,8 +34,8 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { let app = try ghosttyApplication() app.launch() app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) - XCTAssert(app.tabs.count == 2, "There should be 2 tabs") - checkTabsGeometry(app.windows.element(boundBy: 0)) + XCTAssertEqual(app.tabs.count, 2, "There should be 2 tabs") + checkTabsGeometry(app.windows.firstMatch) } @MainActor @@ -45,14 +45,15 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { app.typeKey("f", modifierFlags: [.command, .control]) // using app to type โŒ˜+t might not be able to create tabs app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) - XCTAssert(app.tabs.count == 2, "There should be 2 tabs") - checkTabsGeometry(app.windows.element(boundBy: 0)) + XCTAssertEqual(app.tabs.count, 2, "There should be 2 tabs") + checkTabsGeometry(app.windows.firstMatch) } @MainActor func testTabsGeometryAfterMovingTabs() throws { let app = try ghosttyApplication() app.launch() + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist") // create 3 tabs app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) @@ -61,20 +62,20 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { app.menuItems["_zoomLeft:"].firstMatch.click() // create another window with 2 tabs - app.windows.element(boundBy: 0).groups["Terminal pane"].typeKey("n", modifierFlags: .command) - XCTAssert(app.windows.count == 2, "There should be 2 windows") + app.windows.firstMatch.groups["Terminal pane"].typeKey("n", modifierFlags: .command) + XCTAssertEqual(app.windows.count, 2, "There should be 2 windows") // move to the right app.menuItems["_zoomRight:"].firstMatch.click() // now second window is the first/main one in the list - app.windows.element(boundBy: 0).groups["Terminal pane"].typeKey("t", modifierFlags: .command) + app.windows.firstMatch.groups["Terminal pane"].typeKey("t", modifierFlags: .command) - app.windows.element(boundBy: 1).tabs.element(boundBy: 0).click() // focus first window + app.windows.element(boundBy: 1).tabs.firstMatch.click() // focus first window // now the first window is the main one - let firstTabInFirstWindow = app.windows.element(boundBy: 0).tabs.element(boundBy: 0) - let firstTabInSecondWindow = app.windows.element(boundBy: 1).tabs.element(boundBy: 0) + let firstTabInFirstWindow = app.windows.firstMatch.tabs.firstMatch + let firstTabInSecondWindow = app.windows.element(boundBy: 1).tabs.firstMatch // drag a tab from one window to another firstTabInFirstWindow.press(forDuration: 0.2, thenDragTo: firstTabInSecondWindow) @@ -89,11 +90,12 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { func checkTabsGeometry(_ window: XCUIElement) { let closeTabButtons = window.buttons.matching(identifier: "_closeButton") - XCTAssert(closeTabButtons.count == window.tabs.count, "Close tab buttons count should match tabs count") + XCTAssertEqual(closeTabButtons.count, window.tabs.count, "Close tab buttons count should match tabs count") var previousTabHeight: CGFloat? for idx in 0 ..< window.tabs.count { let currentTab = window.tabs.element(boundBy: idx) + XCTAssertTrue(currentTab.waitForExistence(timeout: 1)) // focus currentTab.click() // switch to the tab -- 2.51.2 From 6ada9c784438c654d82f9557ad619731286cbe18 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Sat, 18 Oct 2025 22:14:35 +0200 Subject: [PATCH 320/349] test: add test for mergeAllWindows --- .../GhosttyTitlebarTabsUITests.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index 7e7b5edec..29694a96b 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -54,7 +54,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { let app = try ghosttyApplication() app.launch() XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist") - // create 3 tabs + // create another 2 tabs app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) app.groups["Terminal pane"].typeKey("t", modifierFlags: .command) @@ -87,6 +87,23 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { checkTabsGeometry(app.windows.firstMatch) } + @MainActor + func testTabsGeometryAfterMergingAllWindows() throws { + let app = try ghosttyApplication() + app.launch() + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 1), "Main window should exist") + + // create another 2 windows + app.typeKey("n", modifierFlags: .command) + app.typeKey("n", modifierFlags: .command) + + // merge into one window, resulting 3 tabs + app.menuItems["mergeAllWindows:"].firstMatch.click() + + XCTAssertTrue(app.wait(for: \.tabs.count, toEqual: 3, timeout: 1), "There should be 3 tabs") + checkTabsGeometry(app.windows.firstMatch) + } + func checkTabsGeometry(_ window: XCUIElement) { let closeTabButtons = window.buttons.matching(identifier: "_closeButton") @@ -95,7 +112,6 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { var previousTabHeight: CGFloat? for idx in 0 ..< window.tabs.count { let currentTab = window.tabs.element(boundBy: idx) - XCTAssertTrue(currentTab.waitForExistence(timeout: 1)) // focus currentTab.click() // switch to the tab -- 2.51.2 From d9ed325818fabcac2450f1a856a797d33cdd835f Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:43:59 +0100 Subject: [PATCH 321/349] update config explicitly --- .../GhosttyCustomConfigCase.swift | 21 +++++++++---------- .../GhosttyUITests/GhosttyTitleUITests.swift | 6 +++--- .../GhosttyTitlebarTabsUITests.swift | 15 +++++++------ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index d4e2e5b89..709533bcc 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -23,24 +23,23 @@ class GhosttyCustomConfigCase: XCTestCase { } } - var customGhosttyConfig: String? { - nil + func updateConfig(_ newConfig: String) throws { + if let configFile { + try FileManager.default.removeItem(at: configFile) + } + let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ghostty") + try newConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) + configFile = temporaryConfig } func ghosttyApplication() throws -> XCUIApplication { let app = XCUIApplication() app.launchArguments.append(contentsOf: ["-ApplePersistenceIgnoreState", "YES"]) - guard let customGhosttyConfig else { + guard let configFile else { return app } - if let configFile { - try FileManager.default.removeItem(at: configFile) - } - let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - .appendingPathExtension("ghostty") - try customGhosttyConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) - configFile = temporaryConfig - app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile?.path + app.launchEnvironment["GHOSTTY_CONFIG_PATH"] = configFile.path return app } } diff --git a/macos/GhosttyUITests/GhosttyTitleUITests.swift b/macos/GhosttyUITests/GhosttyTitleUITests.swift index e6f6d67e6..01bc64023 100644 --- a/macos/GhosttyUITests/GhosttyTitleUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitleUITests.swift @@ -8,9 +8,9 @@ import XCTest final class GhosttyTitleUITests: GhosttyCustomConfigCase { - - override var customGhosttyConfig: String? { - #"title = "GhosttyUITestsLaunchTests""# + override func setUp() async throws { + try await super.setUp() + try updateConfig(#"title = "GhosttyUITestsLaunchTests""#) } @MainActor diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index 29694a96b..7f92779e4 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -8,13 +8,16 @@ import XCTest final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { - override var customGhosttyConfig: String? { - """ - macos-titlebar-style = tabs - title = "GhosttyTitlebarTabsUITests" - """ + override func setUp() async throws { + try await super.setUp() + + try updateConfig( + """ + macos-titlebar-style = tabs + title = "GhosttyTitlebarTabsUITests" + """ + ) } - @MainActor func testCustomTitlebar() throws { let app = try ghosttyApplication() -- 2.51.2 From 4aabd947160b2786b9bb5921364a5589274d1823 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:33:23 +0100 Subject: [PATCH 322/349] add theme tests --- macos/GhosttyUITests/AppKitExtensions.swift | 34 +++++++++++++++ macos/GhosttyUITests/GhosttyThemeTests.swift | 46 ++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 macos/GhosttyUITests/AppKitExtensions.swift create mode 100644 macos/GhosttyUITests/GhosttyThemeTests.swift diff --git a/macos/GhosttyUITests/AppKitExtensions.swift b/macos/GhosttyUITests/AppKitExtensions.swift new file mode 100644 index 000000000..6bb0601a8 --- /dev/null +++ b/macos/GhosttyUITests/AppKitExtensions.swift @@ -0,0 +1,34 @@ +// +// AppKitExtensions.swift +// Ghostty +// +// Created by luca on 27.10.2025. +// + +import AppKit + +extension NSColor { + var isLightColor: Bool { + return self.luminance > 0.5 + } + + var luminance: Double { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + guard let rgb = self.usingColorSpace(.sRGB) else { return 0 } + rgb.getRed(&r, green: &g, blue: &b, alpha: &a) + return (0.299 * r) + (0.587 * g) + (0.114 * b) + } +} + +extension NSImage { + func colorAt(x: Int, y: Int) -> NSColor? { + guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + return NSBitmapImageRep(cgImage: cgImage).colorAt(x: x, y: y) + } +} diff --git a/macos/GhosttyUITests/GhosttyThemeTests.swift b/macos/GhosttyUITests/GhosttyThemeTests.swift new file mode 100644 index 000000000..fce4b061b --- /dev/null +++ b/macos/GhosttyUITests/GhosttyThemeTests.swift @@ -0,0 +1,46 @@ +// +// GhosttyThemeTests.swift +// Ghostty +// +// Created by luca on 27.10.2025. +// + +import XCTest +import AppKit + +final class GhosttyThemeTests: GhosttyCustomConfigCase { + + /// https://github.com/ghostty-org/ghostty/issues/8282 + func testIssue8282() throws { + try updateConfig("theme=light:3024 Day,dark:3024 Night\ntitle=GhosttyThemeTests") + XCUIDevice.shared.appearance = .dark + + let app = try ghosttyApplication() + app.launch() + let windowTitle = app.windows.firstMatch.title + let titleView = app.windows.firstMatch.staticTexts.element(matching: NSPredicate(format: "value == '\(windowTitle)'")) + + let image = titleView.screenshot().image + guard let imageColor = image.colorAt(x: 0, y: 0) else { + return + } + XCTAssertLessThanOrEqual(imageColor.luminance, 0.5, "Expected dark appearance for this test") + // create a split + app.groups["Terminal pane"].typeKey("d", modifierFlags: .command) + // reload config + app.typeKey(",", modifierFlags: [.command, .shift]) + // create a new window + app.typeKey("n", modifierFlags: [.command]) + + for i in 0.. Date: Mon, 27 Oct 2025 21:25:40 +0100 Subject: [PATCH 323/349] Add more theme test cases from #9360 --- .../GhosttyCustomConfigCase.swift | 11 +- macos/GhosttyUITests/GhosttyThemeTests.swift | 150 +++++++++++++++--- 2 files changed, 136 insertions(+), 25 deletions(-) diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index 709533bcc..86e66fa75 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -24,13 +24,12 @@ class GhosttyCustomConfigCase: XCTestCase { } func updateConfig(_ newConfig: String) throws { - if let configFile { - try FileManager.default.removeItem(at: configFile) + if configFile == nil { + let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + .appendingPathExtension("ghostty") + configFile = temporaryConfig } - let temporaryConfig = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - .appendingPathExtension("ghostty") - try newConfig.write(to: temporaryConfig, atomically: true, encoding: .utf8) - configFile = temporaryConfig + try newConfig.write(to: configFile!, atomically: true, encoding: .utf8) } func ghosttyApplication() throws -> XCUIApplication { diff --git a/macos/GhosttyUITests/GhosttyThemeTests.swift b/macos/GhosttyUITests/GhosttyThemeTests.swift index fce4b061b..eabd2e535 100644 --- a/macos/GhosttyUITests/GhosttyThemeTests.swift +++ b/macos/GhosttyUITests/GhosttyThemeTests.swift @@ -5,42 +5,154 @@ // Created by luca on 27.10.2025. // -import XCTest import AppKit +import XCTest final class GhosttyThemeTests: GhosttyCustomConfigCase { + let windowTitle = "GhosttyThemeTests" + private func assertTitlebarAppearance( + _ appearance: XCUIDevice.Appearance, + for app: XCUIApplication, + title: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + for i in 0 ..< app.windows.count { + let titleView = app.windows.element(boundBy: i).staticTexts.element(matching: NSPredicate(format: "value == '\(title ?? windowTitle)'")) + + let image = titleView.screenshot().image + guard let imageColor = image.colorAt(x: 0, y: 0) else { + throw XCTSkip("failed to get pixel color", file: file, line: line) + } + + switch appearance { + case .dark: + XCTAssertLessThanOrEqual(imageColor.luminance, 0.5, "Expected dark appearance for this test", file: file, line: line) + default: + XCTAssertGreaterThanOrEqual(imageColor.luminance, 0.5, "Expected light appearance for this test", file: file, line: line) + } + } + } /// https://github.com/ghostty-org/ghostty/issues/8282 - func testIssue8282() throws { - try updateConfig("theme=light:3024 Day,dark:3024 Night\ntitle=GhosttyThemeTests") + @MainActor + func testIssue8282() async throws { + try updateConfig("title=\(windowTitle) \n theme=light:3024 Day,dark:3024 Night") XCUIDevice.shared.appearance = .dark let app = try ghosttyApplication() app.launch() - let windowTitle = app.windows.firstMatch.title - let titleView = app.windows.firstMatch.staticTexts.element(matching: NSPredicate(format: "value == '\(windowTitle)'")) - - let image = titleView.screenshot().image - guard let imageColor = image.colorAt(x: 0, y: 0) else { - return - } - XCTAssertLessThanOrEqual(imageColor.luminance, 0.5, "Expected dark appearance for this test") + try assertTitlebarAppearance(.dark, for: app) // create a split app.groups["Terminal pane"].typeKey("d", modifierFlags: .command) // reload config app.typeKey(",", modifierFlags: [.command, .shift]) + try await Task.sleep(for: .seconds(0.5)) // create a new window app.typeKey("n", modifierFlags: [.command]) + try assertTitlebarAppearance(.dark, for: app) + } + + @MainActor + func testLightTransparentWindowThemeWithDarkTerminal() async throws { + try updateConfig("title=\(windowTitle) \n window-theme=light") + let app = try ghosttyApplication() + app.launch() + try await Task.sleep(for: .seconds(0.5)) + try assertTitlebarAppearance(.dark, for: app) + } - for i in 0.. Date: Tue, 28 Oct 2025 09:29:38 +0100 Subject: [PATCH 324/349] only run ui tests manually with xcode --- .../GhosttyUITests/GhosttyCustomConfigCase.swift | 15 +++++++++++++++ macos/GhosttyUITests/GhosttyThemeTests.swift | 2 +- .../GhosttyTitlebarTabsUITests.swift | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift index 86e66fa75..41993247a 100644 --- a/macos/GhosttyUITests/GhosttyCustomConfigCase.swift +++ b/macos/GhosttyUITests/GhosttyCustomConfigCase.swift @@ -8,6 +8,21 @@ import XCTest class GhosttyCustomConfigCase: XCTestCase { + /// We only want run these UI tests + /// when testing manually with Xcode IDE + /// + /// So that we don't have to wait for each ci check + /// to run these tedious tests + override class var defaultTestSuite: XCTestSuite { + // https://lldb.llvm.org/cpp_reference/PlatformDarwin_8cpp_source.html#:~:text==%20%22-,IDE_DISABLED_OS_ACTIVITY_DT_MODE + + if ProcessInfo.processInfo.environment["IDE_DISABLED_OS_ACTIVITY_DT_MODE"] != nil { + return XCTestSuite(forTestCaseClass: Self.self) + } else { + return XCTestSuite(name: "Skipping \(className())") + } + } + override class var runsForEachTargetApplicationUIConfiguration: Bool { true } diff --git a/macos/GhosttyUITests/GhosttyThemeTests.swift b/macos/GhosttyUITests/GhosttyThemeTests.swift index eabd2e535..a667d751d 100644 --- a/macos/GhosttyUITests/GhosttyThemeTests.swift +++ b/macos/GhosttyUITests/GhosttyThemeTests.swift @@ -146,7 +146,7 @@ final class GhosttyThemeTests: GhosttyCustomConfigCase { app.launch() // close default window app.typeKey("w", modifierFlags: [.command]) - // open quick termial + // open quick termnial app.menuBarItems["View"].firstMatch.click() app.menuItems["Quick Terminal"].firstMatch.click() let title = "Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development." diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index 7f92779e4..6c32875eb 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -18,6 +18,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { """ ) } + @MainActor func testCustomTitlebar() throws { let app = try ghosttyApplication() -- 2.51.2 From 4154a730ee8b4512c436929bd881ba9be3bb9b5a Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:13:56 +0100 Subject: [PATCH 325/349] Fix some edge cases --- macos/GhosttyUITests/GhosttyThemeTests.swift | 7 ++++--- macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/macos/GhosttyUITests/GhosttyThemeTests.swift b/macos/GhosttyUITests/GhosttyThemeTests.swift index a667d751d..d6f73d7f4 100644 --- a/macos/GhosttyUITests/GhosttyThemeTests.swift +++ b/macos/GhosttyUITests/GhosttyThemeTests.swift @@ -14,6 +14,7 @@ final class GhosttyThemeTests: GhosttyCustomConfigCase { _ appearance: XCUIDevice.Appearance, for app: XCUIApplication, title: String? = nil, + colorLocation: CGPoint? = nil, file: StaticString = #filePath, line: UInt = #line ) throws { @@ -21,7 +22,7 @@ final class GhosttyThemeTests: GhosttyCustomConfigCase { let titleView = app.windows.element(boundBy: i).staticTexts.element(matching: NSPredicate(format: "value == '\(title ?? windowTitle)'")) let image = titleView.screenshot().image - guard let imageColor = image.colorAt(x: 0, y: 0) else { + guard let imageColor = image.colorAt(x: Int(colorLocation?.x ?? 1), y: Int(colorLocation?.y ?? 1)) else { throw XCTSkip("failed to get pixel color", file: file, line: line) } @@ -150,9 +151,9 @@ final class GhosttyThemeTests: GhosttyCustomConfigCase { app.menuBarItems["View"].firstMatch.click() app.menuItems["Quick Terminal"].firstMatch.click() let title = "Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development." - try assertTitlebarAppearance(.light, for: app, title: title) + try assertTitlebarAppearance(.light, for: app, title: title, colorLocation: CGPoint(x: 5, y: 5)) // to avoid dark edge XCUIDevice.shared.appearance = .dark try await Task.sleep(for: .seconds(0.5)) - try assertTitlebarAppearance(.dark, for: app, title: title) + try assertTitlebarAppearance(.dark, for: app, title: title, colorLocation: CGPoint(x: 5, y: 5)) } } diff --git a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift index 6c32875eb..bf8b6124e 100644 --- a/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift +++ b/macos/GhosttyUITests/GhosttyTitlebarTabsUITests.swift @@ -87,7 +87,7 @@ final class GhosttyTitlebarTabsUITests: GhosttyCustomConfigCase { // check tabs in the first checkTabsGeometry(app.windows.firstMatch) // focus another window - app.windows.element(boundBy: 1).click() + app.windows.element(boundBy: 1).tabs.firstMatch.click() checkTabsGeometry(app.windows.firstMatch) } -- 2.51.2 From 35b2e820ce2698224a08c035cf702949c943adc5 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:35:07 +0100 Subject: [PATCH 326/349] typo fix --- macos/GhosttyUITests/GhosttyThemeTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/GhosttyUITests/GhosttyThemeTests.swift b/macos/GhosttyUITests/GhosttyThemeTests.swift index d6f73d7f4..f8f5286fb 100644 --- a/macos/GhosttyUITests/GhosttyThemeTests.swift +++ b/macos/GhosttyUITests/GhosttyThemeTests.swift @@ -147,7 +147,7 @@ final class GhosttyThemeTests: GhosttyCustomConfigCase { app.launch() // close default window app.typeKey("w", modifierFlags: [.command]) - // open quick termnial + // open quick terminal app.menuBarItems["View"].firstMatch.click() app.menuItems["Quick Terminal"].firstMatch.click() let title = "Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development." -- 2.51.2 From 32562e0c983cdbde986f0c6aef487d61c1253ae4 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:46:09 +0100 Subject: [PATCH 327/349] Update macos/Sources/Ghostty/Ghostty.App.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 8e32b82af..854427342 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -27,7 +27,7 @@ extension Ghostty { /// The global app configuration. This defines the app level configuration plus any behavior /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some /// configuration (i.e. font size) from the previously focused window. This would override this. - @Published fileprivate(set) var config: Config + @Published private(set) var config: Config /// Preferred config file than the default ones private var configPath: String? -- 2.51.2 From bfe5a4be8fa3ceba20310199b3df397809669c26 Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:37:36 +0100 Subject: [PATCH 328/349] move config loading to Config --- macos/Sources/Ghostty/Ghostty.App.swift | 32 ++-------------------- macos/Sources/Ghostty/Ghostty.Config.swift | 22 ++++++++++----- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 854427342..5ec42158f 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -49,7 +49,7 @@ extension Ghostty { init(configPath: String? = nil) { self.configPath = configPath // Initialize the global configuration. - self.config = configPath.flatMap({ Self.readConfig(at: $0, finalize: true) }) ?? Config() + self.config = Config(at: configPath) if self.config.config == nil { readiness = .error return @@ -146,7 +146,7 @@ extension Ghostty { } // Hard or full updates have to reload the full configuration - let newConfig = configPath.flatMap({ Self.readConfig(at: $0, finalize: true) }) ?? Config() + let newConfig = Config(at: configPath) guard newConfig.loaded else { Ghostty.logger.warning("failed to reload configuration") return @@ -166,7 +166,7 @@ extension Ghostty { // Hard or full updates have to reload the full configuration. // NOTE: We never set this on self.config because this is a surface-only // config. We free it after the call. - let newConfig = configPath.flatMap({ Self.readConfig(at: $0, finalize: true) }) ?? Config() + let newConfig = Config(at: configPath) guard newConfig.loaded else { Ghostty.logger.warning("failed to reload configuration") return @@ -2077,29 +2077,3 @@ extension Ghostty { #endif } } - -extension Ghostty.App { - static func readConfig(at path: String, finalize: Bool = true) -> Ghostty.Config? { - guard - let cfg = ghostty_config_new() - else { - return nil - } - if FileManager.default.fileExists(atPath: path) { - ghostty_config_load_file(cfg, path) - } -#if os(macOS) - if !isRunningInXcode() { - ghostty_config_load_cli_args(cfg) - } -#endif - ghostty_config_load_recursive_files(cfg) - if finalize { - // Finalize will make our defaults available, - // and also will combine all the keys into one file, - // we might not need this in the future - ghostty_config_finalize(cfg) - } - return Ghostty.Config(config: cfg) - } -} diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index fb435c014..86b0ecdb9 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -37,8 +37,8 @@ extension Ghostty { self.config = config } - convenience init() { - self.init(config: Self.loadConfig()) + convenience init(at path: String? = nil, finalize: Bool = true) { + self.init(config: Self.loadConfig(at: path, finalize: finalize)) } convenience init(clone config: ghostty_config_t) { @@ -50,7 +50,10 @@ extension Ghostty { } /// Initializes a new configuration and loads all the values. - static private func loadConfig() -> ghostty_config_t? { + /// - Parameters: + /// - path: An optional preferred config file path. Pass `nil` to load the default configuration files. + /// - finalize: Whether to finalize the configuration to populate default values. + static private func loadConfig(at path: String?, finalize: Bool) -> ghostty_config_t? { // Initialize the global configuration. guard let cfg = ghostty_config_new() else { logger.critical("ghostty_config_new failed") @@ -61,7 +64,11 @@ extension Ghostty { // We only do this on macOS because other Apple platforms do not have the // same filesystem concept. #if os(macOS) - ghostty_config_load_default_files(cfg); + if let path, FileManager.default.fileExists(atPath: path) { + ghostty_config_load_file(cfg, path) + } else { + ghostty_config_load_default_files(cfg) + } // We only load CLI args when not running in Xcode because in Xcode we // pass some special parameters to control the debugger. @@ -76,9 +83,10 @@ extension Ghostty { // have to do this synchronously. When we support config updating we can do // this async and update later. - // Finalize will make our defaults available. - ghostty_config_finalize(cfg) - + if finalize { + // Finalize will make our defaults available. + ghostty_config_finalize(cfg) + } // Log any configuration errors. These will be automatically shown in a // pop-up window too. let diagsCount = ghostty_config_diagnostics_count(cfg) -- 2.51.2 From 3fda31a66aeb0617558e549f12bacb912e0497af Mon Sep 17 00:00:00 2001 From: Lars <134181853+bo2themax@users.noreply.github.com> Date: Sun, 2 Nov 2025 16:03:54 +0100 Subject: [PATCH 329/349] skip checking config file --- macos/Sources/Ghostty/Ghostty.Config.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 86b0ecdb9..c64646e25 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -64,7 +64,7 @@ extension Ghostty { // We only do this on macOS because other Apple platforms do not have the // same filesystem concept. #if os(macOS) - if let path, FileManager.default.fileExists(atPath: path) { + if let path { ghostty_config_load_file(cfg, path) } else { ghostty_config_load_default_files(cfg) -- 2.51.2 From 7b6147aa28280d4c761db42fe69fcca0a0ab2169 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Tue, 20 Jan 2026 14:23:10 -0300 Subject: [PATCH 330/349] Refactor GValueType and getImpl functions to use type-based switches for improved clarity and maintainability --- src/apprt/gtk/gsettings.zig | 37 ++++++++++++------------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig index 3fc3d01de..43a1467c6 100644 --- a/src/apprt/gtk/gsettings.zig +++ b/src/apprt/gtk/gsettings.zig @@ -17,18 +17,11 @@ pub const Key = enum { } fn GValueType(comptime self: Key) type { - return switch (self) { - // Booleans are stored as integers in GTK's internal representation - .@"gtk-enable-primary-paste", - => c_int, - - // Integer types - .@"gtk-xft-dpi", - => c_int, - - // String types (returned as null-terminated C strings from GTK) - .@"gtk-font-name", - => ?[*:0]const u8, + return switch (self.Type()) { + bool => c_int, // Booleans are stored as integers in GTK's internal representation + c_int => c_int, + []const u8 => ?[*:0]const u8, // Strings (returned as null-terminated C strings from GTK) + else => @compileError("Unsupported type for GTK settings"), }; } @@ -86,19 +79,12 @@ fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: settings.as(gobject.Object).getProperty(@tagName(key).ptr, &value); - return switch (key) { - // Booleans are stored as integers in GTK, convert to bool - .@"gtk-enable-primary-paste", - => value.getInt() != 0, - - // Integer types are returned directly - .@"gtk-xft-dpi", - => value.getInt(), - - // Strings: GTK owns the GValue's pointer, so we must duplicate it - // before the GValue is destroyed by defer value.unset() - .@"gtk-font-name", - => blk: { + return switch (key.Type()) { + bool => value.getInt() != 0, // Booleans are stored as integers in GTK, convert to bool + c_int => value.getInt(), // Integer types are returned directly + []const u8 => blk: { + // Strings: GTK owns the GValue's pointer, so we must duplicate it + // before the GValue is destroyed by defer value.unset() // This is defensive: we have already checked at compile-time that // an allocator is provided for allocating types const alloc = allocator.?; @@ -106,6 +92,7 @@ fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: const str = std.mem.span(ptr); break :blk try alloc.dupe(u8, str); }, + else => @compileError("Unsupported type for GTK settings"), }; } -- 2.51.2 From bc067fc782ed32b5c20a25bbee061debf0ad1612 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Tue, 20 Jan 2026 14:23:22 -0300 Subject: [PATCH 331/349] Refactor gtk_enable_primary_paste to remove optional type and simplify condition checks --- src/apprt/gtk/class/surface.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 56c39a86f..c439a10c3 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -677,7 +677,7 @@ pub const Surface = extern struct { /// Whether primary paste (middle-click paste) is enabled via GNOME settings. /// If true, middle-click paste is enabled. If false, it's disabled. - gtk_enable_primary_paste: ?bool = true, + gtk_enable_primary_paste: bool = true, pub var offset: c_int = 0; }; @@ -2698,7 +2698,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and !(priv.gtk_enable_primary_paste)) { + if (button == .middle and !priv.gtk_enable_primary_paste) { return; } @@ -2755,7 +2755,7 @@ pub const Surface = extern struct { // Check if middle button paste should be disabled based on GNOME settings // If gtk_enable_primary_paste is explicitly false, skip processing middle button - if (button == .middle and !(priv.gtk_enable_primary_paste)) { + if (button == .middle and !priv.gtk_enable_primary_paste) { return; } -- 2.51.2 From 8c9891b5de515dfe8f9717441437123f205a2b63 Mon Sep 17 00:00:00 2001 From: evertonstz Date: Tue, 20 Jan 2026 14:32:47 -0300 Subject: [PATCH 332/349] Refactor primary paste settings handling and documentation for clarity --- src/apprt/gtk/class/surface.zig | 15 +++-------- src/apprt/gtk/gsettings.zig | 44 +++++++++------------------------ 2 files changed, 15 insertions(+), 44 deletions(-) diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index c439a10c3..5c3bf18b6 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -675,8 +675,7 @@ pub const Surface = extern struct { /// The context for this surface (window, tab, or split) context: apprt.surface.NewSurfaceContext = .window, - /// Whether primary paste (middle-click paste) is enabled via GNOME settings. - /// If true, middle-click paste is enabled. If false, it's disabled. + /// Whether primary paste (middle-click paste) is enabled. gtk_enable_primary_paste: bool = true, pub var offset: c_int = 0; @@ -1770,12 +1769,8 @@ pub const Surface = extern struct { priv.im_composing = false; priv.im_len = 0; - // Read GNOME desktop interface settings for primary paste (middle-click) - // This is only relevant on Linux systems with GNOME settings available - priv.gtk_enable_primary_paste = gsettings.get(.@"gtk-enable-primary-paste") orelse blk: { - log.warn("gtk-enable-primary-paste was not set, using default value", .{}); - break :blk false; - }; + // Read GTK primary paste setting + priv.gtk_enable_primary_paste = gsettings.get(.@"gtk-enable-primary-paste") orelse true; // Set up to handle items being dropped on our surface. Files can be dropped // from Nautilus and strings can be dropped from many programs. The order @@ -2696,8 +2691,6 @@ pub const Surface = extern struct { // Report the event const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); - // Check if middle button paste should be disabled based on GNOME settings - // If gtk_enable_primary_paste is explicitly false, skip processing middle button if (button == .middle and !priv.gtk_enable_primary_paste) { return; } @@ -2753,8 +2746,6 @@ pub const Surface = extern struct { const gtk_mods = event.getModifierState(); const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); - // Check if middle button paste should be disabled based on GNOME settings - // If gtk_enable_primary_paste is explicitly false, skip processing middle button if (button == .middle and !priv.gtk_enable_primary_paste) { return; } diff --git a/src/apprt/gtk/gsettings.zig b/src/apprt/gtk/gsettings.zig index 43a1467c6..8cf7f12d2 100644 --- a/src/apprt/gtk/gsettings.zig +++ b/src/apprt/gtk/gsettings.zig @@ -18,16 +18,15 @@ pub const Key = enum { fn GValueType(comptime self: Key) type { return switch (self.Type()) { - bool => c_int, // Booleans are stored as integers in GTK's internal representation + bool => c_int, c_int => c_int, - []const u8 => ?[*:0]const u8, // Strings (returned as null-terminated C strings from GTK) + []const u8 => ?[*:0]const u8, else => @compileError("Unsupported type for GTK settings"), }; } /// Returns true if this setting type requires memory allocation. - /// this is defensive: types that do not need allocation need to be - /// explicitly marked here + /// Types that do not need allocation must be explicitly marked. fn requiresAllocation(comptime self: Key) bool { const T = self.Type(); return switch (T) { @@ -37,14 +36,9 @@ pub const Key = enum { } }; -/// Reads a GTK setting using the GTK Settings API for non-allocating types. -/// This automatically uses XDG Desktop Portal in Flatpak environments. -/// -/// No allocator is required or used. Returns null if the setting is not available or cannot be read. -/// -/// Example usage: -/// const enabled = get(.@"gtk-enable-primary-paste"); -/// const dpi = get(.@"gtk-xft-dpi"); +/// Reads a GTK setting for non-allocating types. +/// Automatically uses XDG Desktop Portal in Flatpak environments. +/// Returns null if the setting is unavailable. pub fn get(comptime key: Key) ?key.Type() { if (comptime key.requiresAllocation()) { @compileError("Allocating types require an allocator; use getAlloc() instead"); @@ -53,25 +47,15 @@ pub fn get(comptime key: Key) ?key.Type() { return getImpl(settings, null, key) catch unreachable; } -/// Reads a GTK setting using the GTK Settings API, allocating if necessary. -/// This automatically uses XDG Desktop Portal in Flatpak environments. -/// -/// The caller must free any returned allocated memory with the provided allocator. -/// Returns null if the setting is not available or cannot be read. -/// May return an allocation error if memory allocation fails. -/// -/// Example usage: -/// const theme = try getAlloc(allocator, .gtk_theme_name); -/// defer if (theme) |t| allocator.free(t); +/// Reads a GTK setting, allocating memory if necessary. +/// Automatically uses XDG Desktop Portal in Flatpak environments. +/// Caller must free returned memory with the provided allocator. +/// Returns null if the setting is unavailable. pub fn getAlloc(allocator: std.mem.Allocator, comptime key: Key) !?key.Type() { const settings = gtk.Settings.getDefault() orelse return null; return getImpl(settings, allocator, key); } -/// Shared implementation for reading GTK settings. -/// If allocator is null, only non-allocating types can be used. -/// Note: When adding a new type, research if it requires allocation (strings and boxed types do) -/// if allocation is NOT needed, list it inside the switch statement in the function requiresAllocation() fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: Key) !?key.Type() { const GValType = key.GValueType(); var value = gobject.ext.Value.new(GValType); @@ -80,13 +64,9 @@ fn getImpl(settings: *gtk.Settings, allocator: ?std.mem.Allocator, comptime key: settings.as(gobject.Object).getProperty(@tagName(key).ptr, &value); return switch (key.Type()) { - bool => value.getInt() != 0, // Booleans are stored as integers in GTK, convert to bool - c_int => value.getInt(), // Integer types are returned directly + bool => value.getInt() != 0, + c_int => value.getInt(), []const u8 => blk: { - // Strings: GTK owns the GValue's pointer, so we must duplicate it - // before the GValue is destroyed by defer value.unset() - // This is defensive: we have already checked at compile-time that - // an allocator is provided for allocating types const alloc = allocator.?; const ptr = value.getString() orelse break :blk null; const str = std.mem.span(ptr); -- 2.51.2 From 811e3594eb7fd42c67157cccc2bf68e308b29337 Mon Sep 17 00:00:00 2001 From: mauroporras <167700+mauroporras@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:37:21 -0500 Subject: [PATCH 333/349] feat: add configurable word boundary characters for text selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new `selection-word-chars` config option to customize which characters mark word boundaries during text selection operations (double-click, word selection, etc.). Similar to zsh's WORDCHARS environment variable, but specifies boundary characters rather than word characters. Default boundaries: ` \t'"โ”‚`|:;,()[]{}<>$` Users can now customize word selection behavior, such as treating semicolons as part of words or excluding periods from boundaries: selection-word-chars = " \t'\"โ”‚`|:,()[]{}<>$" Changes: - Add selection-word-chars config field with comprehensive documentation - Modify selectWord() and selectWordBetween() to accept boundary_chars parameter - Parse UTF-8 boundary string to u32 codepoints at runtime - Update all call sites in Surface.zig and embedded.zig - Update all test cases to pass boundary characters --- src/Surface.zig | 18 +++++-- src/apprt/embedded.zig | 5 +- src/config/Config.zig | 25 ++++++++++ src/terminal/Screen.zig | 108 ++++++++++++++++++++++------------------ 4 files changed, 103 insertions(+), 53 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 1f50cb681..65cb40091 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -316,6 +316,7 @@ const DerivedConfig = struct { macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, selection_clear_on_typing: bool, + selection_word_chars: []const u8, vt_kam_allowed: bool, wait_after_command: bool, window_padding_top: u32, @@ -392,6 +393,7 @@ const DerivedConfig = struct { .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_copy = config.@"selection-clear-on-copy", .selection_clear_on_typing = config.@"selection-clear-on-typing", + .selection_word_chars = config.@"selection-word-chars", .vt_kam_allowed = config.@"vt-kam-allowed", .wait_after_command = config.@"wait-after-command", .window_padding_top = config.@"window-padding-y".top_left, @@ -4180,7 +4182,7 @@ pub fn mouseButtonCallback( // Ignore any errors, likely regex errors. } - break :sel self.io.terminal.screens.active.selectWord(pin.*); + break :sel self.io.terminal.screens.active.selectWord(pin.*, self.config.selection_word_chars); }; if (sel_) |sel| { try self.io.terminal.screens.active.select(sel); @@ -4262,7 +4264,7 @@ pub fn mouseButtonCallback( if (try self.linkAtPos(pos)) |link| { try self.setSelection(link.selection); } else { - const sel = screen.selectWord(pin) orelse break :sel; + const sel = screen.selectWord(pin, self.config.selection_word_chars) orelse break :sel; try self.setSelection(sel); } try self.queueRender(); @@ -4583,7 +4585,10 @@ pub fn mousePressureCallback( // This should always be set in this state but we don't want // to handle state inconsistency here. const pin = self.mouse.left_click_pin orelse break :select; - const sel = self.io.terminal.screens.active.selectWord(pin.*) orelse break :select; + const sel = self.io.terminal.screens.active.selectWord( + pin.*, + self.config.selection_word_chars, + ) orelse break :select; try self.io.terminal.screens.active.select(sel); try self.queueRender(); } @@ -4806,7 +4811,11 @@ fn dragLeftClickDouble( const click_pin = self.mouse.left_click_pin.?.*; // Get the word closest to our starting click. - const word_start = screen.selectWordBetween(click_pin, drag_pin) orelse { + const word_start = screen.selectWordBetween( + click_pin, + drag_pin, + self.config.selection_word_chars, + ) orelse { try self.setSelection(null); return; }; @@ -4815,6 +4824,7 @@ fn dragLeftClickDouble( const word_current = screen.selectWordBetween( drag_pin, click_pin, + self.config.selection_word_chars, ) orelse { try self.setSelection(null); return; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 364a1bec1..b4ad7f885 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -2165,7 +2165,10 @@ pub const CAPI = struct { if (comptime std.debug.runtime_safety) unreachable; return false; }; - break :sel surface.io.terminal.screens.active.selectWord(pin) orelse return false; + break :sel surface.io.terminal.screens.active.selectWord( + pin, + surface.config.selection_word_chars, + ) orelse return false; }; // Read the selection diff --git a/src/config/Config.zig b/src/config/Config.zig index 7689899de..6d941d493 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -712,6 +712,31 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// on the same selection. @"selection-clear-on-copy": bool = false, +/// Characters that mark word boundaries during text selection operations such +/// as double-clicking. When selecting a word, the selection will stop at any +/// of these characters. +/// +/// This is similar to the `WORDCHARS` environment variable in zsh, except this +/// specifies the boundary characters rather than the word characters. The +/// default includes common delimiters and punctuation that typically separate +/// words in code and prose. +/// +/// Each character in this string becomes a word boundary. Multi-byte UTF-8 +/// characters are supported. +/// +/// The null character (U+0000) is always treated as a boundary and does not +/// need to be included in this configuration. +/// +/// Default: ` \t'"โ”‚`|:;,()[]{}<>$` +/// +/// To add or remove specific characters, you can set this to a custom value. +/// For example, to treat semicolons as part of words: +/// +/// selection-word-chars = " \t'\"โ”‚`|:,()[]{}<>$" +/// +/// Available since: 1.2.0 +@"selection-word-chars": []const u8 = " \t'\"โ”‚`|:;,()[]{}<>$", + /// The minimum contrast ratio between the foreground and background colors. /// The contrast ratio is a value between 1 and 21. A value of 1 allows for no /// contrast (e.g. black on black). This value is the contrast ratio as defined diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index d36cdac2a..3f7a48a65 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2617,11 +2617,15 @@ pub fn selectAll(self: *Screen) ?Selection { /// end_pt (inclusive). Because it selects "nearest" to start point, start /// point can be before or after end point. /// +/// The boundary_chars parameter should be a UTF-8 string of characters that +/// mark word boundaries, passed through to selectWord. +/// /// TODO: test this pub fn selectWordBetween( self: *Screen, start: Pin, end: Pin, + boundary_chars: []const u8, ) ?Selection { const dir: PageList.Direction = if (start.before(end)) .right_down else .left_up; var it = start.cellIterator(dir, end); @@ -2633,7 +2637,7 @@ pub fn selectWordBetween( } // If we found a word, then return it - if (self.selectWord(pin)) |sel| return sel; + if (self.selectWord(pin, boundary_chars)) |sel| return sel; } return null; @@ -2645,32 +2649,37 @@ pub fn selectWordBetween( /// /// This will return null if a selection is impossible. The only scenario /// this happens is if the point pt is outside of the written screen space. -pub fn selectWord(self: *Screen, pin: Pin) ?Selection { +/// +/// The boundary_chars parameter should be a UTF-8 string of characters that +/// mark word boundaries. The null character (U+0000) is always included as +/// a boundary automatically. +pub fn selectWord( + self: *Screen, + pin: Pin, + boundary_chars: []const u8, +) ?Selection { _ = self; - // Boundary characters for selection purposes - const boundary = &[_]u32{ - 0, - ' ', - '\t', - '\'', - '"', - 'โ”‚', - '`', - '|', - ':', - ';', - ',', - '(', - ')', - '[', - ']', - '{', - '}', - '<', - '>', - '$', - }; + // Parse boundary characters from UTF-8 string to u32 codepoints. + // We allocate a fixed-size array on the stack (64 boundary chars should be plenty). + var boundary_buf: [64]u32 = undefined; + var boundary_len: usize = 1; + boundary_buf[0] = 0; // Always include null character as a boundary + + // Parse the UTF-8 boundary string into codepoints + if (std.unicode.Utf8View.init(boundary_chars)) |utf8_view| { + var utf8_it = utf8_view.iterator(); + while (utf8_it.nextCodepoint()) |codepoint| { + if (boundary_len >= boundary_buf.len) break; // Safety limit + boundary_buf[boundary_len] = codepoint; + boundary_len += 1; + } + } else |_| { + // If invalid UTF-8, use just the null boundary + // This is a graceful fallback that still allows selection to work + } + + const boundary = boundary_buf[0..boundary_len]; // If our cell is empty we can't select a word, because we can't select // areas where the screen is not yet written. @@ -7699,6 +7708,9 @@ test "Screen: selectWord" { defer s.deinit(); try s.testWriteString("ABC DEF\n 123\n456"); + // Default boundary characters for word selection + const boundary_chars = " \t'\"โ”‚`|:;,()[]{}<>$"; + // Outside of active area // try testing.expect(s.selectWord(.{ .x = 9, .y = 0 }) == null); // try testing.expect(s.selectWord(.{ .x = 0, .y = 5 }) == null); @@ -7708,7 +7720,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7725,7 +7737,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7742,7 +7754,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7759,7 +7771,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, @@ -7776,7 +7788,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 1, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7793,7 +7805,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7825,7 +7837,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7842,7 +7854,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7859,7 +7871,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7885,7 +7897,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7902,7 +7914,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7919,7 +7931,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7966,7 +7978,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -7983,7 +7995,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 4, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -8000,7 +8012,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -8019,7 +8031,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8065,7 +8077,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8081,7 +8093,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 3, .y = 7, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8097,7 +8109,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 10, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8168,7 +8180,7 @@ test "Screen: selectPrompt basics" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 6, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8185,7 +8197,7 @@ test "Screen: selectPrompt basics" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 3, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8229,7 +8241,7 @@ test "Screen: selectPrompt prompt at start" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8273,7 +8285,7 @@ test "Screen: selectPrompt prompt at end" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, - } }).?).?; + } }).?, boundary_chars).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, -- 2.51.2 From 27602bb4b42f88c576d9385947c139cd67a73730 Mon Sep 17 00:00:00 2001 From: mauroporras <167700+mauroporras@users.noreply.github.com> Date: Sun, 26 Oct 2025 06:23:38 -0500 Subject: [PATCH 334/349] refactor: optimize selection-word-chars with pre-parsed codepoints Refactor the selection-word-chars implementation to parse UTF-8 boundary characters once during config initialization instead of on every selection operation. Changes: - Add SelectionWordChars type that stores pre-parsed []const u32 codepoints - Parse UTF-8 to codepoints in parseCLI() during config load - Remove UTF-8 parsing logic from selectWord() hot path (27 lines removed) - Remove arbitrary 64-character buffer limit - Update selectWord() and selectWordBetween() to accept []const u32 - Update DerivedConfig to store codepoints directly - Update all tests to use codepoint arrays Benefits: - No runtime UTF-8 parsing overhead on every selection - No arbitrary character limit (uses allocator instead) - Cleaner separation of concerns (config handles parsing, selection uses data) - Better performance in selection hot path --- src/Surface.zig | 4 +- src/config/Config.zig | 107 ++++++++++++++++++++++++++++++- src/terminal/Screen.zig | 135 +++++++++++++++++++++++----------------- 3 files changed, 187 insertions(+), 59 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 65cb40091..13cef9e3d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -316,7 +316,7 @@ const DerivedConfig = struct { macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, selection_clear_on_typing: bool, - selection_word_chars: []const u8, + selection_word_chars: []const u32, vt_kam_allowed: bool, wait_after_command: bool, window_padding_top: u32, @@ -393,7 +393,7 @@ const DerivedConfig = struct { .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_copy = config.@"selection-clear-on-copy", .selection_clear_on_typing = config.@"selection-clear-on-typing", - .selection_word_chars = config.@"selection-word-chars", + .selection_word_chars = config.@"selection-word-chars".codepoints, .vt_kam_allowed = config.@"vt-kam-allowed", .wait_after_command = config.@"wait-after-command", .window_padding_top = config.@"window-padding-y".top_left, diff --git a/src/config/Config.zig b/src/config/Config.zig index 6d941d493..b959dedba 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -735,7 +735,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// selection-word-chars = " \t'\"โ”‚`|:,()[]{}<>$" /// /// Available since: 1.2.0 -@"selection-word-chars": []const u8 = " \t'\"โ”‚`|:;,()[]{}<>$", +@"selection-word-chars": SelectionWordChars = .{}, /// The minimum contrast ratio between the foreground and background colors. /// The contrast ratio is a value between 1 and 21. A value of 1 allows for no @@ -5788,6 +5788,111 @@ pub const RepeatableString = struct { } }; +/// SelectionWordChars stores the parsed codepoints for word boundary +/// characters used during text selection. The string is parsed once +/// during configuration and stored as u32 codepoints for efficient +/// lookup during selection operations. +pub const SelectionWordChars = struct { + const Self = @This(); + + /// Default boundary characters: ` \t'"โ”‚`|:;,()[]{}<>$` + const default_codepoints = [_]u32{ + 0, // null + ' ', // space + '\t', // tab + '\'', // single quote + '"', // double quote + 'โ”‚', // U+2502 box drawing + '`', // backtick + '|', // pipe + ':', // colon + ';', // semicolon + ',', // comma + '(', // left paren + ')', // right paren + '[', // left bracket + ']', // right bracket + '{', // left brace + '}', // right brace + '<', // less than + '>', // greater than + '$', // dollar + }; + + /// The parsed codepoints. Always includes null (U+0000) at index 0. + codepoints: []const u32 = &default_codepoints, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + const value = input orelse return error.ValueRequired; + + // Parse UTF-8 string into codepoints + var list = std.ArrayList(u32).init(alloc); + defer list.deinit(); + + // Always include null as first boundary + try list.append(0); + + // Parse the UTF-8 string + const utf8_view = std.unicode.Utf8View.init(value) catch { + // Invalid UTF-8, just use null boundary + self.codepoints = try list.toOwnedSlice(); + return; + }; + + var utf8_it = utf8_view.iterator(); + while (utf8_it.nextCodepoint()) |codepoint| { + try list.append(codepoint); + } + + self.codepoints = try list.toOwnedSlice(); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { + const copy = try alloc.dupe(u32, self.codepoints); + return .{ .codepoints = copy }; + } + + /// Compare if two values are equal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return std.mem.eql(u32, self.codepoints, other.codepoints); + } + + /// Used by Formatter + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { + // Convert codepoints back to UTF-8 string for display + var buf = std.ArrayList(u8).init(formatter.alloc); + defer buf.deinit(); + + // Skip the null character at index 0 + for (self.codepoints[1..]) |codepoint| { + var utf8_buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(@intCast(codepoint), &utf8_buf) catch continue; + try buf.appendSlice(utf8_buf[0..len]); + } + + try formatter.formatEntry([]const u8, buf.items); + } + + test "parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var chars: Self = .{}; + try chars.parseCLI(alloc, " \t;,"); + + // Should have null + 4 characters + try testing.expectEqual(@as(usize, 5), chars.codepoints.len); + try testing.expectEqual(@as(u32, 0), chars.codepoints[0]); + try testing.expectEqual(@as(u32, ' '), chars.codepoints[1]); + try testing.expectEqual(@as(u32, '\t'), chars.codepoints[2]); + try testing.expectEqual(@as(u32, ';'), chars.codepoints[3]); + try testing.expectEqual(@as(u32, ','), chars.codepoints[4]); + } +}; + /// FontVariation is a repeatable configuration value that sets a single /// font variation value. Font variations are configurations for what /// are often called "variable fonts." The font files usually end in diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 3f7a48a65..46e0f4fdb 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2617,7 +2617,7 @@ pub fn selectAll(self: *Screen) ?Selection { /// end_pt (inclusive). Because it selects "nearest" to start point, start /// point can be before or after end point. /// -/// The boundary_chars parameter should be a UTF-8 string of characters that +/// The boundary_codepoints parameter should be a slice of u32 codepoints that /// mark word boundaries, passed through to selectWord. /// /// TODO: test this @@ -2625,7 +2625,7 @@ pub fn selectWordBetween( self: *Screen, start: Pin, end: Pin, - boundary_chars: []const u8, + boundary_codepoints: []const u32, ) ?Selection { const dir: PageList.Direction = if (start.before(end)) .right_down else .left_up; var it = start.cellIterator(dir, end); @@ -2637,7 +2637,7 @@ pub fn selectWordBetween( } // If we found a word, then return it - if (self.selectWord(pin, boundary_chars)) |sel| return sel; + if (self.selectWord(pin, boundary_codepoints)) |sel| return sel; } return null; @@ -2650,37 +2650,15 @@ pub fn selectWordBetween( /// This will return null if a selection is impossible. The only scenario /// this happens is if the point pt is outside of the written screen space. /// -/// The boundary_chars parameter should be a UTF-8 string of characters that -/// mark word boundaries. The null character (U+0000) is always included as -/// a boundary automatically. +/// The boundary_codepoints parameter should be a slice of u32 codepoints that +/// mark word boundaries. This is expected to be pre-parsed from the config. pub fn selectWord( self: *Screen, pin: Pin, - boundary_chars: []const u8, + boundary_codepoints: []const u32, ) ?Selection { _ = self; - // Parse boundary characters from UTF-8 string to u32 codepoints. - // We allocate a fixed-size array on the stack (64 boundary chars should be plenty). - var boundary_buf: [64]u32 = undefined; - var boundary_len: usize = 1; - boundary_buf[0] = 0; // Always include null character as a boundary - - // Parse the UTF-8 boundary string into codepoints - if (std.unicode.Utf8View.init(boundary_chars)) |utf8_view| { - var utf8_it = utf8_view.iterator(); - while (utf8_it.nextCodepoint()) |codepoint| { - if (boundary_len >= boundary_buf.len) break; // Safety limit - boundary_buf[boundary_len] = codepoint; - boundary_len += 1; - } - } else |_| { - // If invalid UTF-8, use just the null boundary - // This is a graceful fallback that still allows selection to work - } - - const boundary = boundary_buf[0..boundary_len]; - // If our cell is empty we can't select a word, because we can't select // areas where the screen is not yet written. const start_cell = pin.rowAndCell().cell; @@ -2689,7 +2667,7 @@ pub fn selectWord( // Determine if we are a boundary or not to determine what our boundary is. const expect_boundary = std.mem.indexOfAny( u32, - boundary, + boundary_codepoints, &[_]u32{start_cell.content.codepoint}, ) != null; @@ -2707,7 +2685,7 @@ pub fn selectWord( // If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( u32, - boundary, + boundary_codepoints, &[_]u32{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :end prev; @@ -2744,7 +2722,7 @@ pub fn selectWord( // If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( u32, - boundary, + boundary_codepoints, &[_]u32{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :start prev; @@ -7708,8 +7686,11 @@ test "Screen: selectWord" { defer s.deinit(); try s.testWriteString("ABC DEF\n 123\n456"); - // Default boundary characters for word selection - const boundary_chars = " \t'\"โ”‚`|:;,()[]{}<>$"; + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; // Outside of active area // try testing.expect(s.selectWord(.{ .x = 9, .y = 0 }) == null); @@ -7720,7 +7701,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7737,7 +7718,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7754,7 +7735,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7771,7 +7752,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 3, @@ -7788,7 +7769,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 0, .y = 1, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7805,7 +7786,7 @@ test "Screen: selectWord" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -7826,6 +7807,12 @@ test "Screen: selectWord across soft-wrap" { defer s.deinit(); try s.testWriteString(" 1234012\n 123"); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); @@ -7837,7 +7824,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7854,7 +7841,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7871,7 +7858,7 @@ test "Screen: selectWord across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7892,12 +7879,18 @@ test "Screen: selectWord whitespace across soft-wrap" { defer s.deinit(); try s.testWriteString("1 1\n 123"); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // Going forward { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7914,7 +7907,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7931,7 +7924,7 @@ test "Screen: selectWord whitespace across soft-wrap" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 1, @@ -7948,6 +7941,12 @@ test "Screen: selectWord with character boundary" { const testing = std.testing; const alloc = testing.allocator; + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + const cases = [_][]const u8{ " 'abc' \n123", " \"abc\" \n123", @@ -7978,7 +7977,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -7995,7 +7994,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 4, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -8012,7 +8011,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 3, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 2, @@ -8031,7 +8030,7 @@ test "Screen: selectWord with character boundary" { var sel = s.selectWord(s.pages.pin(.{ .active = .{ .x = 1, .y = 0, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8052,6 +8051,12 @@ test "Screen: selectOutput" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // zig fmt: off { // line number: @@ -8077,7 +8082,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8093,7 +8098,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 3, .y = 7, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8109,7 +8114,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 10, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8142,6 +8147,12 @@ test "Screen: selectPrompt basics" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // zig fmt: off { // line number: @@ -8180,7 +8191,7 @@ test "Screen: selectPrompt basics" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 6, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8197,7 +8208,7 @@ test "Screen: selectPrompt basics" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 3, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8217,6 +8228,12 @@ test "Screen: selectPrompt prompt at start" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // zig fmt: off { // line number: @@ -8241,7 +8258,7 @@ test "Screen: selectPrompt prompt at start" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8261,6 +8278,12 @@ test "Screen: selectPrompt prompt at end" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); + // Default boundary codepoints for word selection + const boundary_codepoints = &[_]u32{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + }; + // zig fmt: off { // line number: @@ -8285,7 +8308,7 @@ test "Screen: selectPrompt prompt at end" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, - } }).?, boundary_chars).?; + } }).?, boundary_codepoints).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, -- 2.51.2 From 6f662d70bcd1b25a1971b5a14dd0430906482024 Mon Sep 17 00:00:00 2001 From: mauroporras <167700+mauroporras@users.noreply.github.com> Date: Sun, 26 Oct 2025 06:43:26 -0500 Subject: [PATCH 335/349] refactor: clean up selection-word-chars documentation and formatting --- src/config/Config.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b959dedba..931bc9db8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -733,8 +733,6 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// For example, to treat semicolons as part of words: /// /// selection-word-chars = " \t'\"โ”‚`|:,()[]{}<>$" -/// -/// Available since: 1.2.0 @"selection-word-chars": SelectionWordChars = .{}, /// The minimum contrast ratio between the foreground and background colors. @@ -5797,7 +5795,7 @@ pub const SelectionWordChars = struct { /// Default boundary characters: ` \t'"โ”‚`|:;,()[]{}<>$` const default_codepoints = [_]u32{ - 0, // null + 0, // null ' ', // space '\t', // tab '\'', // single quote -- 2.51.2 From 9b7c20f500dcc2ef3015a78a90026b36dc06c642 Mon Sep 17 00:00:00 2001 From: mauroporras <167700+mauroporras@users.noreply.github.com> Date: Sun, 26 Oct 2025 11:02:31 -0500 Subject: [PATCH 336/349] refactor: use u21 for Unicode codepoints and Zig 0.15 ArrayList - Change all codepoint types from u32 to u21 to align with Zig stdlib - Update ArrayList to use Zig 0.15 unmanaged pattern (.empty) - Remove unnecessary @intCast when encoding UTF-8 - Fix formatEntry to use stack-allocated buffer --- src/Surface.zig | 2 +- src/config/Config.zig | 44 +++++++++++----------- src/terminal/Screen.zig | 82 +++++++++++++++-------------------------- 3 files changed, 53 insertions(+), 75 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 13cef9e3d..91758a21a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -316,7 +316,7 @@ const DerivedConfig = struct { macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, selection_clear_on_typing: bool, - selection_word_chars: []const u32, + selection_word_chars: []const u21, vt_kam_allowed: bool, wait_after_command: bool, window_padding_top: u32, diff --git a/src/config/Config.zig b/src/config/Config.zig index 931bc9db8..9bbcb342b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5788,13 +5788,13 @@ pub const RepeatableString = struct { /// SelectionWordChars stores the parsed codepoints for word boundary /// characters used during text selection. The string is parsed once -/// during configuration and stored as u32 codepoints for efficient +/// during configuration and stored as u21 codepoints for efficient /// lookup during selection operations. pub const SelectionWordChars = struct { const Self = @This(); /// Default boundary characters: ` \t'"โ”‚`|:;,()[]{}<>$` - const default_codepoints = [_]u32{ + const default_codepoints = [_]u21{ 0, // null ' ', // space '\t', // tab @@ -5818,58 +5818,60 @@ pub const SelectionWordChars = struct { }; /// The parsed codepoints. Always includes null (U+0000) at index 0. - codepoints: []const u32 = &default_codepoints, + codepoints: []const u21 = &default_codepoints, pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired; // Parse UTF-8 string into codepoints - var list = std.ArrayList(u32).init(alloc); - defer list.deinit(); + var list: std.ArrayList(u21) = .empty; + defer list.deinit(alloc); // Always include null as first boundary - try list.append(0); + try list.append(alloc, 0); // Parse the UTF-8 string const utf8_view = std.unicode.Utf8View.init(value) catch { // Invalid UTF-8, just use null boundary - self.codepoints = try list.toOwnedSlice(); + self.codepoints = try list.toOwnedSlice(alloc); return; }; var utf8_it = utf8_view.iterator(); while (utf8_it.nextCodepoint()) |codepoint| { - try list.append(codepoint); + try list.append(alloc, codepoint); } - self.codepoints = try list.toOwnedSlice(); + self.codepoints = try list.toOwnedSlice(alloc); } /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self { - const copy = try alloc.dupe(u32, self.codepoints); + const copy = try alloc.dupe(u21, self.codepoints); return .{ .codepoints = copy }; } /// Compare if two values are equal. Required by Config. pub fn equal(self: Self, other: Self) bool { - return std.mem.eql(u32, self.codepoints, other.codepoints); + return std.mem.eql(u21, self.codepoints, other.codepoints); } /// Used by Formatter pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { // Convert codepoints back to UTF-8 string for display - var buf = std.ArrayList(u8).init(formatter.alloc); - defer buf.deinit(); + var buf: [4096]u8 = undefined; + var pos: usize = 0; // Skip the null character at index 0 for (self.codepoints[1..]) |codepoint| { var utf8_buf: [4]u8 = undefined; - const len = std.unicode.utf8Encode(@intCast(codepoint), &utf8_buf) catch continue; - try buf.appendSlice(utf8_buf[0..len]); + const len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch continue; + if (pos + len > buf.len) break; + @memcpy(buf[pos..][0..len], utf8_buf[0..len]); + pos += len; } - try formatter.formatEntry([]const u8, buf.items); + try formatter.formatEntry([]const u8, buf[0..pos]); } test "parseCLI" { @@ -5883,11 +5885,11 @@ pub const SelectionWordChars = struct { // Should have null + 4 characters try testing.expectEqual(@as(usize, 5), chars.codepoints.len); - try testing.expectEqual(@as(u32, 0), chars.codepoints[0]); - try testing.expectEqual(@as(u32, ' '), chars.codepoints[1]); - try testing.expectEqual(@as(u32, '\t'), chars.codepoints[2]); - try testing.expectEqual(@as(u32, ';'), chars.codepoints[3]); - try testing.expectEqual(@as(u32, ','), chars.codepoints[4]); + try testing.expectEqual(@as(u21, 0), chars.codepoints[0]); + try testing.expectEqual(@as(u21, ' '), chars.codepoints[1]); + try testing.expectEqual(@as(u21, '\t'), chars.codepoints[2]); + try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]); + try testing.expectEqual(@as(u21, ','), chars.codepoints[4]); } }; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 46e0f4fdb..c8db19903 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -2617,7 +2617,7 @@ pub fn selectAll(self: *Screen) ?Selection { /// end_pt (inclusive). Because it selects "nearest" to start point, start /// point can be before or after end point. /// -/// The boundary_codepoints parameter should be a slice of u32 codepoints that +/// The boundary_codepoints parameter should be a slice of u21 codepoints that /// mark word boundaries, passed through to selectWord. /// /// TODO: test this @@ -2625,7 +2625,7 @@ pub fn selectWordBetween( self: *Screen, start: Pin, end: Pin, - boundary_codepoints: []const u32, + boundary_codepoints: []const u21, ) ?Selection { const dir: PageList.Direction = if (start.before(end)) .right_down else .left_up; var it = start.cellIterator(dir, end); @@ -2650,12 +2650,12 @@ pub fn selectWordBetween( /// This will return null if a selection is impossible. The only scenario /// this happens is if the point pt is outside of the written screen space. /// -/// The boundary_codepoints parameter should be a slice of u32 codepoints that +/// The boundary_codepoints parameter should be a slice of u21 codepoints that /// mark word boundaries. This is expected to be pre-parsed from the config. pub fn selectWord( self: *Screen, pin: Pin, - boundary_codepoints: []const u32, + boundary_codepoints: []const u21, ) ?Selection { _ = self; @@ -2666,9 +2666,9 @@ pub fn selectWord( // Determine if we are a boundary or not to determine what our boundary is. const expect_boundary = std.mem.indexOfAny( - u32, + u21, boundary_codepoints, - &[_]u32{start_cell.content.codepoint}, + &[_]u21{start_cell.content.codepoint}, ) != null; // Go forwards to find our end boundary @@ -2684,9 +2684,9 @@ pub fn selectWord( // If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( - u32, + u21, boundary_codepoints, - &[_]u32{cell.content.codepoint}, + &[_]u21{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :end prev; @@ -2721,9 +2721,9 @@ pub fn selectWord( // If we do not match our expected set, we hit a boundary const this_boundary = std.mem.indexOfAny( - u32, + u21, boundary_codepoints, - &[_]u32{cell.content.codepoint}, + &[_]u21{cell.content.codepoint}, ) != null; if (this_boundary != expect_boundary) break :start prev; @@ -7687,9 +7687,9 @@ test "Screen: selectWord" { try s.testWriteString("ABC DEF\n 123\n456"); // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', }; // Outside of active area @@ -7808,9 +7808,9 @@ test "Screen: selectWord across soft-wrap" { try s.testWriteString(" 1234012\n 123"); // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', }; { @@ -7880,9 +7880,9 @@ test "Screen: selectWord whitespace across soft-wrap" { try s.testWriteString("1 1\n 123"); // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', }; // Going forward @@ -7942,9 +7942,9 @@ test "Screen: selectWord with character boundary" { const alloc = testing.allocator; // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', + const boundary_codepoints = &[_]u21{ + 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', + ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', }; const cases = [_][]const u8{ @@ -8051,12 +8051,6 @@ test "Screen: selectOutput" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); - // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', - }; - // zig fmt: off { // line number: @@ -8082,7 +8076,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8098,7 +8092,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 3, .y = 7, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8114,7 +8108,7 @@ test "Screen: selectOutput" { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 10, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .active = .{ .x = 0, @@ -8147,12 +8141,6 @@ test "Screen: selectPrompt basics" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); - // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', - }; - // zig fmt: off { // line number: @@ -8191,7 +8179,7 @@ test "Screen: selectPrompt basics" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 6, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8208,7 +8196,7 @@ test "Screen: selectPrompt basics" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 3, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8228,12 +8216,6 @@ test "Screen: selectPrompt prompt at start" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); - // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', - }; - // zig fmt: off { // line number: @@ -8258,7 +8240,7 @@ test "Screen: selectPrompt prompt at start" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 1, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, @@ -8278,12 +8260,6 @@ test "Screen: selectPrompt prompt at end" { var s = try init(alloc, .{ .cols = 10, .rows = 15, .max_scrollback = 0 }); defer s.deinit(); - // Default boundary codepoints for word selection - const boundary_codepoints = &[_]u32{ - 0, ' ', '\t', '\'', '"', 'โ”‚', '`', '|', ':', ';', - ',', '(', ')', '[', ']', '{', '}', '<', '>', '$', - }; - // zig fmt: off { // line number: @@ -8308,7 +8284,7 @@ test "Screen: selectPrompt prompt at end" { var sel = s.selectPrompt(s.pages.pin(.{ .active = .{ .x = 1, .y = 2, - } }).?, boundary_codepoints).?; + } }).?).?; defer sel.deinit(&s); try testing.expectEqual(point.Point{ .screen = .{ .x = 0, -- 2.51.2 From 2e0141fcdfe63cc99441161c65a15a8547594756 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 09:33:59 -0800 Subject: [PATCH 337/349] config: clarify selection-word-boundary docs --- src/Surface.zig | 7 +++++-- src/config/Config.zig | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 91758a21a..8d8a14f14 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -393,7 +393,7 @@ const DerivedConfig = struct { .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_copy = config.@"selection-clear-on-copy", .selection_clear_on_typing = config.@"selection-clear-on-typing", - .selection_word_chars = config.@"selection-word-chars".codepoints, + .selection_word_chars = try alloc.dupe(u21, config.@"selection-word-chars".codepoints), .vt_kam_allowed = config.@"vt-kam-allowed", .wait_after_command = config.@"wait-after-command", .window_padding_top = config.@"window-padding-y".top_left, @@ -4264,7 +4264,10 @@ pub fn mouseButtonCallback( if (try self.linkAtPos(pos)) |link| { try self.setSelection(link.selection); } else { - const sel = screen.selectWord(pin, self.config.selection_word_chars) orelse break :sel; + const sel = screen.selectWord( + pin, + self.config.selection_word_chars, + ) orelse break :sel; try self.setSelection(sel); } try self.queueRender(); diff --git a/src/config/Config.zig b/src/config/Config.zig index 9bbcb342b..efe35604d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -722,7 +722,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// words in code and prose. /// /// Each character in this string becomes a word boundary. Multi-byte UTF-8 -/// characters are supported. +/// characters are supported, but only single codepoints can be specified. +/// Multi-codepoint sequences (e.g. emoji) are not supported. /// /// The null character (U+0000) is always treated as a boundary and does not /// need to be included in this configuration. @@ -733,6 +734,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// For example, to treat semicolons as part of words: /// /// selection-word-chars = " \t'\"โ”‚`|:,()[]{}<>$" +/// +/// Available since: 1.3.0 @"selection-word-chars": SelectionWordChars = .{}, /// The minimum contrast ratio between the foreground and background colors. -- 2.51.2 From c2deda3231fa38e1bc1d1b06e8b50fc41164578c Mon Sep 17 00:00:00 2001 From: Martin Emde Date: Mon, 3 Nov 2025 20:00:57 -0800 Subject: [PATCH 338/349] config: switch certain physical keybinds to unicode Switches several default keybindings from physical key codes `.physical = .equal // or .bracket_left or .bracket_right` to unicode characters `.unicode = '=' // or '[' or ']'` to support alternative keyboard layouts like Dvorak and keyboards with dedicated plus keys (like German layouts). I found in testing that all of these must be fixed at once otherwise the bracket physical keys overshadew the correct (for dvorak) plus key. With this fix, tab and pane navigation (cmd+[], cmd+shift+[]), as well as cmd+shift+equals and cmd+equals work as expected on dvoark layout on MacOS. --- src/config/Config.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index efe35604d..6a846b17f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6107,7 +6107,7 @@ pub const Keybinds = struct { // set the expected keybind for the menu. try self.set.put( alloc, - .{ .key = .{ .physical = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = '=' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( @@ -6275,13 +6275,13 @@ pub const Keybinds = struct { ); try self.set.putFlags( alloc, - .{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .unicode = '[' }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .unicode = ']' }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, .{ .performable = true }, ); @@ -6607,12 +6607,12 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_left }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = '[' }, .mods = .{ .super = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_right }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = ']' }, .mods = .{ .super = true, .shift = true } }, .{ .next_tab = {} }, ); try self.set.put( @@ -6627,12 +6627,12 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_left }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = '[' }, .mods = .{ .super = true } }, .{ .goto_split = .previous }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .bracket_right }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = ']' }, .mods = .{ .super = true } }, .{ .goto_split = .next }, ); try self.set.put( @@ -6677,7 +6677,7 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .equal }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .unicode = '=' }, .mods = .{ .super = true, .ctrl = true } }, .{ .equalize_splits = {} }, ); -- 2.51.2 From cb25c0a8aeb40e940ca4ef87b24ee49db3a963a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 10:17:24 -0800 Subject: [PATCH 339/349] build: libghostty-vt now depends on uucode directly This doesn't add any real weight to it, we only need it for a type definition. This fixes our example builds. --- src/build/GhosttyZig.zig | 3 +++ src/build/SharedDeps.zig | 26 ++++++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/build/GhosttyZig.zig b/src/build/GhosttyZig.zig index a8d2726bc..e63120e74 100644 --- a/src/build/GhosttyZig.zig +++ b/src/build/GhosttyZig.zig @@ -73,6 +73,9 @@ fn initVt( // We always need unicode tables deps.unicode_tables.addModuleImport(vt); + // We need uucode for grapheme break support + deps.addUucode(b, vt, cfg.target, cfg.optimize); + // If SIMD is enabled, add all our SIMD dependencies. if (cfg.simd) { try SharedDeps.addSimd(b, vt, null); diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index b1c084002..0ca43e78d 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -412,14 +412,7 @@ pub fn add( })) |dep| { step.root_module.addImport("z2d", dep.module("z2d")); } - if (b.lazyDependency("uucode", .{ - .target = target, - .optimize = optimize, - .tables_path = self.uucode_tables, - .build_config_path = b.path("src/build/uucode_config.zig"), - })) |dep| { - step.root_module.addImport("uucode", dep.module("uucode")); - } + self.addUucode(b, step.root_module, target, optimize); if (b.lazyDependency("zf", .{ .target = target, .optimize = optimize, @@ -878,6 +871,23 @@ pub fn gtkNgDistResources( }; } +pub fn addUucode( + self: *const SharedDeps, + b: *std.Build, + module: *std.Build.Module, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) void { + if (b.lazyDependency("uucode", .{ + .target = target, + .optimize = optimize, + .tables_path = self.uucode_tables, + .build_config_path = b.path("src/build/uucode_config.zig"), + })) |dep| { + module.addImport("uucode", dep.module("uucode")); + } +} + // For dynamic linking, we prefer dynamic linking and to search by // mode first. Mode first will search all paths for a dynamic library // before falling back to static. -- 2.51.2 From 8d2eb280dbad06790a207edf5ca8f5305aafe678 Mon Sep 17 00:00:00 2001 From: ClearAspect Date: Thu, 20 Nov 2025 15:47:56 -0500 Subject: [PATCH 340/349] custom shaders: add colorscheme information to shader uniforms Adds palette and color scheme uniforms to custom shaders, allowing custom shaders to access terminal color information: - iPalette[256]: Full 256-color terminal palette (RGB) - iBackgroundColor, iForegroundColor: Terminal colors (RGB) - iCursorColor, iCursorText: Cursor colors (RGB) - iSelectionBackgroundColor, iSelectionForegroundColor: Selection colors (RGB) Colors are normalized to [0.0, 1.0] range and update when the palette changes via OSC sequences or configuration changes. The palette_dirty flag tracks when colors need to be refreshed, initialized to true to ensure correct colors on new surfaces. --- src/config/Config.zig | 18 ++++ src/renderer/generic.zig | 100 ++++++++++++++++++++- src/renderer/shaders/shadertoy_prefix.glsl | 7 ++ src/renderer/shadertoy.zig | 7 ++ 4 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6a846b17f..8ca64efe9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2883,6 +2883,24 @@ keybind: Keybinds = .{}, /// (e.g., modifier key presses, link hover events in unfocused split panes). /// Check `iFocus > 0` to determine if the surface is currently focused. /// +/// * `vec3 iPalette[256]` - The 256-color terminal palette. +/// +/// RGB values for all 256 colors in the terminal palette, normalized +/// to [0.0, 1.0]. Index 0-15 are the ANSI colors, 16-231 are the 6x6x6 +/// color cube, and 232-255 are the grayscale colors. +/// +/// * `vec3 iBackgroundColor` - Terminal background color (RGB). +/// +/// * `vec3 iForegroundColor` - Terminal foreground color (RGB). +/// +/// * `vec3 iCursorColor` - Terminal cursor color (RGB). +/// +/// * `vec3 iCursorText` - Terminal cursor text color (RGB). +/// +/// * `vec3 iSelectionBackgroundColor` - Selection background color (RGB). +/// +/// * `vec3 iSelectionForegroundColor` - Selection foreground color (RGB). +/// /// If the shader fails to compile, the shader will be ignored. Any errors /// related to shader compilation will not show up as configuration errors /// and only show up in the log, since shader compilation happens after diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 4181badc3..b70dca364 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -227,11 +227,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// a large screen. terminal_state_frame_count: usize = 0, + /// Captured dirty for reference after updateFrame() clears the flag + /// To be used for shader uniforms. + /// + /// Initialized to true because we need to get the correct palette even on + /// a new Surface. Otherwise we end up with it initialized to 0's + palette_dirty: bool = true, + const HighlightTag = enum(u8) { search_match, search_match_selected, }; - /// Swap chain which maintains multiple copies of the state needed to /// render a frame, so that we can start building the next frame while /// the previous frame is still being processed on the GPU. @@ -752,6 +758,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .cursor_change_time = 0, .time_focus = 0, .focus = 1, // assume focused initially + .palette = @splat(@splat(0)), + .background_color = @splat(0), + .foreground_color = @splat(0), + .cursor_color = @splat(0), + .cursor_text = @splat(0), + .selection_background_color = @splat(0), + .selection_foreground_color = @splat(0), }, .bg_image_buffer = undefined, @@ -1209,6 +1222,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; }; + self.palette_dirty |= state.terminal.flags.dirty.palette; + break :critical .{ .links = links, .mouse = state.mouse, @@ -2300,6 +2315,89 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; + // Renderer state required for getting the colors + const state: *renderer.State = &self.surface_mailbox.surface.renderer_state; + + // (Updates on OSC sequence changes and configuration changes) + if (self.palette_dirty) { + + // 256-color palette + for (state.terminal.colors.palette.current, 0..) |color, i| { + self.custom_shader_uniforms.palette[i] = .{ + @as(f32, @floatFromInt(color.r)) / 255.0, + @as(f32, @floatFromInt(color.g)) / 255.0, + @as(f32, @floatFromInt(color.b)) / 255.0, + 1.0, + }; + } + + // Background color + if (state.terminal.colors.background.get()) |bg| { + self.custom_shader_uniforms.background_color = .{ + @as(f32, @floatFromInt(bg.r)) / 255.0, + @as(f32, @floatFromInt(bg.g)) / 255.0, + @as(f32, @floatFromInt(bg.b)) / 255.0, + 1.0, + }; + } + + // Foreground color + if (state.terminal.colors.foreground.get()) |fg| { + self.custom_shader_uniforms.foreground_color = .{ + @as(f32, @floatFromInt(fg.r)) / 255.0, + @as(f32, @floatFromInt(fg.g)) / 255.0, + @as(f32, @floatFromInt(fg.b)) / 255.0, + 1.0, + }; + } + + // Cursor color + if (state.terminal.colors.cursor.get()) |cursor_color| { + self.custom_shader_uniforms.cursor_color = .{ + @as(f32, @floatFromInt(cursor_color.r)) / 255.0, + @as(f32, @floatFromInt(cursor_color.g)) / 255.0, + @as(f32, @floatFromInt(cursor_color.b)) / 255.0, + 1.0, + }; + } + + // NOTE: the following could be optimized to follow a change in + // config for a slight optimization however this is only 12 bytes + // each being updated and likely isn't a cause for concern + + // Cursor text color + if (self.config.cursor_text) |cursor_text| { + self.custom_shader_uniforms.cursor_text = .{ + @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0, + @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0, + @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0, + 1.0, + }; + } + + // Selection background color + if (self.config.selection_background) |seletion_bg| { + self.custom_shader_uniforms.selection_background_color = .{ + @as(f32, @floatFromInt(seletion_bg.color.r)) / 255.0, + @as(f32, @floatFromInt(seletion_bg.color.g)) / 255.0, + @as(f32, @floatFromInt(seletion_bg.color.b)) / 255.0, + 1.0, + }; + } + + // Selection foreground color + if (self.config.selection_foreground) |selection_fg| { + self.custom_shader_uniforms.selection_foreground_color = .{ + @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0, + @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0, + @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0, + 1.0, + }; + } + + self.palette_dirty = false; + } + // Update custom cursor uniforms, if we have a cursor. if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index c984334f4..661bd233d 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -18,6 +18,13 @@ layout(binding = 1, std140) uniform Globals { uniform float iTimeCursorChange; uniform float iTimeFocus; uniform int iFocus; + uniform vec3 iPalette[256]; + uniform vec3 iBackgroundColor; + uniform vec3 iForegroundColor; + uniform vec3 iCursorColor; + uniform vec3 iCursorText; + uniform vec3 iSelectionForegroundColor; + uniform vec3 iSelectionBackgroundColor; }; layout(binding = 0) uniform sampler2D iChannel0; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index f71200610..7d0ad4b0a 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -27,6 +27,13 @@ pub const Uniforms = extern struct { cursor_change_time: f32 align(4), time_focus: f32 align(4), focus: i32 align(4), + palette: [256][4]f32 align(16), + background_color: [4]f32 align(16), + foreground_color: [4]f32 align(16), + cursor_color: [4]f32 align(16), + cursor_text: [4]f32 align(16), + selection_background_color: [4]f32 align(16), + selection_foreground_color: [4]f32 align(16), }; /// The target to load shaders for. -- 2.51.2 From 87867459690de41063ac4f065a600d1b800442f5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:24:25 -0800 Subject: [PATCH 341/349] renderer: don't access shared state for custom shader color palettes --- src/renderer/generic.zig | 44 +++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index b70dca364..6f5a3d2ca 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2315,14 +2315,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; - // Renderer state required for getting the colors - const state: *renderer.State = &self.surface_mailbox.surface.renderer_state; - // (Updates on OSC sequence changes and configuration changes) if (self.palette_dirty) { + const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors; // 256-color palette - for (state.terminal.colors.palette.current, 0..) |color, i| { + for (colors.palette, 0..) |color, i| { self.custom_shader_uniforms.palette[i] = .{ @as(f32, @floatFromInt(color.r)) / 255.0, @as(f32, @floatFromInt(color.g)) / 255.0, @@ -2332,27 +2330,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Background color - if (state.terminal.colors.background.get()) |bg| { - self.custom_shader_uniforms.background_color = .{ - @as(f32, @floatFromInt(bg.r)) / 255.0, - @as(f32, @floatFromInt(bg.g)) / 255.0, - @as(f32, @floatFromInt(bg.b)) / 255.0, - 1.0, - }; - } + self.custom_shader_uniforms.background_color = .{ + @as(f32, @floatFromInt(colors.background.r)) / 255.0, + @as(f32, @floatFromInt(colors.background.g)) / 255.0, + @as(f32, @floatFromInt(colors.background.b)) / 255.0, + 1.0, + }; // Foreground color - if (state.terminal.colors.foreground.get()) |fg| { - self.custom_shader_uniforms.foreground_color = .{ - @as(f32, @floatFromInt(fg.r)) / 255.0, - @as(f32, @floatFromInt(fg.g)) / 255.0, - @as(f32, @floatFromInt(fg.b)) / 255.0, - 1.0, - }; - } + self.custom_shader_uniforms.foreground_color = .{ + @as(f32, @floatFromInt(colors.foreground.r)) / 255.0, + @as(f32, @floatFromInt(colors.foreground.g)) / 255.0, + @as(f32, @floatFromInt(colors.foreground.b)) / 255.0, + 1.0, + }; // Cursor color - if (state.terminal.colors.cursor.get()) |cursor_color| { + if (colors.cursor) |cursor_color| { self.custom_shader_uniforms.cursor_color = .{ @as(f32, @floatFromInt(cursor_color.r)) / 255.0, @as(f32, @floatFromInt(cursor_color.g)) / 255.0, @@ -2376,11 +2370,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Selection background color - if (self.config.selection_background) |seletion_bg| { + if (self.config.selection_background) |selection_bg| { self.custom_shader_uniforms.selection_background_color = .{ - @as(f32, @floatFromInt(seletion_bg.color.r)) / 255.0, - @as(f32, @floatFromInt(seletion_bg.color.g)) / 255.0, - @as(f32, @floatFromInt(seletion_bg.color.b)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0, 1.0, }; } -- 2.51.2 From 7204f7ef9e70d0a87aef95903399f60d8071579a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:31:43 -0800 Subject: [PATCH 342/349] renderer: remove palette_dirty, rely on terminal state --- src/renderer/generic.zig | 221 ++++++++++++++++++++------------------- 1 file changed, 112 insertions(+), 109 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 6f5a3d2ca..f3c0556f5 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -227,13 +227,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// a large screen. terminal_state_frame_count: usize = 0, - /// Captured dirty for reference after updateFrame() clears the flag - /// To be used for shader uniforms. - /// - /// Initialized to true because we need to get the correct palette even on - /// a new Surface. Otherwise we end up with it initialized to 0's - palette_dirty: bool = true, - const HighlightTag = enum(u8) { search_match, search_match_selected, @@ -1222,8 +1215,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; }; - self.palette_dirty |= state.terminal.flags.dirty.palette; - break :critical .{ .links = links, .mouse = state.mouse, @@ -1291,26 +1282,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } - // Build our GPU cells - try self.rebuildCells( - critical.preedit, - renderer.cursorStyle(&self.terminal_state, .{ - .preedit = critical.preedit != null, - .focused = self.focused, - .blink_visible = cursor_blink_visible, - }), - &critical.links, - ); - - // Notify our shaper we're done for the frame. For some shapers, - // such as CoreText, this triggers off-thread cleanup logic. - self.font_shaper.endFrame(); + // Reset our dirty state after updating. + defer self.terminal_state.dirty = .false; - // Acquire the draw mutex because we're modifying state here. + // Acquire the draw mutex for all remaining state updates. { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); + // Build our GPU cells + try self.rebuildCells( + critical.preedit, + renderer.cursorStyle(&self.terminal_state, .{ + .preedit = critical.preedit != null, + .focused = self.focused, + .blink_visible = cursor_blink_visible, + }), + &critical.links, + ); + // The scrollbar is only emitted during draws so we also // check the scrollbar cache here and update if needed. // This is pretty fast. @@ -1337,7 +1327,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { else => {}, }; + + // Update custom shader uniforms that depend on terminal state. + self.updateCustomShaderUniformsFromState(); } + + // Notify our shaper we're done for the frame. For some shapers, + // such as CoreText, this triggers off-thread cleanup logic. + self.font_shaper.endFrame(); } /// Draw the frame to the screen. @@ -1459,8 +1456,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Upload the background image to the GPU as necessary. try self.uploadBackgroundImage(); - // Update custom shader uniforms if necessary. - try self.updateCustomShaderUniforms(); + // Update per-frame custom shader uniforms. + try self.updateCustomShaderUniformsForFrame(); // Setup our frame data try frame.uniforms.sync(&.{self.uniforms}); @@ -2274,10 +2271,93 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.bg_image_buffer_modified +%= 1; } - /// Update uniforms for the custom shaders, if necessary. + /// Update custom shader uniforms that depend on terminal state. + /// + /// This should be called in `updateFrame` when terminal state changes. + fn updateCustomShaderUniformsFromState(self: *Self) void { + // We only need to do this if we have custom shaders. + if (!self.has_custom_shaders) return; + + // Only update when terminal state is dirty. + if (self.terminal_state.dirty == .false) return; + + const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors; + + // 256-color palette + for (colors.palette, 0..) |color, i| { + self.custom_shader_uniforms.palette[i] = .{ + @as(f32, @floatFromInt(color.r)) / 255.0, + @as(f32, @floatFromInt(color.g)) / 255.0, + @as(f32, @floatFromInt(color.b)) / 255.0, + 1.0, + }; + } + + // Background color + self.custom_shader_uniforms.background_color = .{ + @as(f32, @floatFromInt(colors.background.r)) / 255.0, + @as(f32, @floatFromInt(colors.background.g)) / 255.0, + @as(f32, @floatFromInt(colors.background.b)) / 255.0, + 1.0, + }; + + // Foreground color + self.custom_shader_uniforms.foreground_color = .{ + @as(f32, @floatFromInt(colors.foreground.r)) / 255.0, + @as(f32, @floatFromInt(colors.foreground.g)) / 255.0, + @as(f32, @floatFromInt(colors.foreground.b)) / 255.0, + 1.0, + }; + + // Cursor color + if (colors.cursor) |cursor_color| { + self.custom_shader_uniforms.cursor_color = .{ + @as(f32, @floatFromInt(cursor_color.r)) / 255.0, + @as(f32, @floatFromInt(cursor_color.g)) / 255.0, + @as(f32, @floatFromInt(cursor_color.b)) / 255.0, + 1.0, + }; + } + + // NOTE: the following could be optimized to follow a change in + // config for a slight optimization however this is only 12 bytes + // each being updated and likely isn't a cause for concern + + // Cursor text color + if (self.config.cursor_text) |cursor_text| { + self.custom_shader_uniforms.cursor_text = .{ + @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0, + @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0, + @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0, + 1.0, + }; + } + + // Selection background color + if (self.config.selection_background) |selection_bg| { + self.custom_shader_uniforms.selection_background_color = .{ + @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0, + @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0, + 1.0, + }; + } + + // Selection foreground color + if (self.config.selection_foreground) |selection_fg| { + self.custom_shader_uniforms.selection_foreground_color = .{ + @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0, + @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0, + @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0, + 1.0, + }; + } + } + + /// Update per-frame custom shader uniforms. /// /// This should be called exactly once per frame, inside `drawFrame`. - fn updateCustomShaderUniforms(self: *Self) !void { + fn updateCustomShaderUniformsForFrame(self: *Self) !void { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return; @@ -2315,83 +2395,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { 0, }; - // (Updates on OSC sequence changes and configuration changes) - if (self.palette_dirty) { - const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors; - - // 256-color palette - for (colors.palette, 0..) |color, i| { - self.custom_shader_uniforms.palette[i] = .{ - @as(f32, @floatFromInt(color.r)) / 255.0, - @as(f32, @floatFromInt(color.g)) / 255.0, - @as(f32, @floatFromInt(color.b)) / 255.0, - 1.0, - }; - } - - // Background color - self.custom_shader_uniforms.background_color = .{ - @as(f32, @floatFromInt(colors.background.r)) / 255.0, - @as(f32, @floatFromInt(colors.background.g)) / 255.0, - @as(f32, @floatFromInt(colors.background.b)) / 255.0, - 1.0, - }; - - // Foreground color - self.custom_shader_uniforms.foreground_color = .{ - @as(f32, @floatFromInt(colors.foreground.r)) / 255.0, - @as(f32, @floatFromInt(colors.foreground.g)) / 255.0, - @as(f32, @floatFromInt(colors.foreground.b)) / 255.0, - 1.0, - }; - - // Cursor color - if (colors.cursor) |cursor_color| { - self.custom_shader_uniforms.cursor_color = .{ - @as(f32, @floatFromInt(cursor_color.r)) / 255.0, - @as(f32, @floatFromInt(cursor_color.g)) / 255.0, - @as(f32, @floatFromInt(cursor_color.b)) / 255.0, - 1.0, - }; - } - - // NOTE: the following could be optimized to follow a change in - // config for a slight optimization however this is only 12 bytes - // each being updated and likely isn't a cause for concern - - // Cursor text color - if (self.config.cursor_text) |cursor_text| { - self.custom_shader_uniforms.cursor_text = .{ - @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0, - @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0, - @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0, - 1.0, - }; - } - - // Selection background color - if (self.config.selection_background) |selection_bg| { - self.custom_shader_uniforms.selection_background_color = .{ - @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0, - @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0, - @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0, - 1.0, - }; - } - - // Selection foreground color - if (self.config.selection_foreground) |selection_fg| { - self.custom_shader_uniforms.selection_foreground_color = .{ - @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0, - @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0, - @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0, - 1.0, - }; - } - - self.palette_dirty = false; - } - // Update custom cursor uniforms, if we have a cursor. if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); @@ -2480,6 +2483,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Convert the terminal state to GPU cells stored in CPU memory. These /// are then synced to the GPU in the next frame. This only updates CPU /// memory and doesn't touch the GPU. + /// + /// This requires the draw mutex. + /// + /// Dirty state on terminal state won't be reset by this. fn rebuildCells( self: *Self, preedit: ?renderer.State.Preedit, @@ -2487,10 +2494,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { links: *const terminal.RenderState.CellSet, ) !void { const state: *terminal.RenderState = &self.terminal_state; - defer state.dirty = .false; - - self.draw_mutex.lock(); - defer self.draw_mutex.unlock(); // const start = try std.time.Instant.now(); // const start_micro = std.time.microTimestamp(); -- 2.51.2 From e3c39ed502c62a907dff935089fe9e2747356da9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:46:24 -0800 Subject: [PATCH 343/349] renderer: cell.Contents.resize errdefer handling is now safe --- src/renderer/cell.zig | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 9e5802ea5..5ea5b7ab0 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -90,7 +90,6 @@ pub const Contents = struct { const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count); errdefer alloc.free(bg_cells); - @memset(bg_cells, .{ 0, 0, 0, 0 }); // The foreground lists can hold 3 types of items: @@ -106,32 +105,28 @@ pub const Contents = struct { // We have size.rows + 2 lists because indexes 0 and size.rows - 1 are // used for special lists containing the cursor cell which need to // be first and last in the buffer, respectively. - var fg_rows = try ArrayListCollection(shaderpkg.CellText).init( + var fg_rows: ArrayListCollection(shaderpkg.CellText) = try .init( alloc, size.rows + 2, size.columns * 3, ); errdefer fg_rows.deinit(alloc); - alloc.free(self.bg_cells); - self.fg_rows.deinit(alloc); - - self.bg_cells = bg_cells; - self.fg_rows = fg_rows; - // We don't need 3*cols worth of cells for the cursor lists, so we can // replace them with smaller lists. This is technically a tiny bit of // extra work but resize is not a hot function so it's worth it to not // waste the memory. - self.fg_rows.lists[0].deinit(alloc); - self.fg_rows.lists[0] = try std.ArrayListUnmanaged( - shaderpkg.CellText, - ).initCapacity(alloc, 1); - - self.fg_rows.lists[size.rows + 1].deinit(alloc); - self.fg_rows.lists[size.rows + 1] = try std.ArrayListUnmanaged( - shaderpkg.CellText, - ).initCapacity(alloc, 1); + fg_rows.lists[0].deinit(alloc); + fg_rows.lists[0] = try .initCapacity(alloc, 1); + fg_rows.lists[size.rows + 1].deinit(alloc); + fg_rows.lists[size.rows + 1] = try .initCapacity(alloc, 1); + + // Perform the swap, no going back from here. + errdefer comptime unreachable; + alloc.free(self.bg_cells); + self.fg_rows.deinit(alloc); + self.bg_cells = bg_cells; + self.fg_rows = fg_rows; } /// Reset the cell contents to an empty state without resizing. -- 2.51.2 From e875b453b7342767f7b8315ba1628853e6fb4cb6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:50:58 -0800 Subject: [PATCH 344/349] font/shaper: hook functions can't fail --- src/font/shaper/coretext.zig | 8 ++++---- src/font/shaper/harfbuzz.zig | 4 ++-- src/font/shaper/run.zig | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index cc05022c4..ab3c6aaab 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -98,7 +98,7 @@ pub const Shaper = struct { self.unichars.deinit(alloc); } - fn reset(self: *RunState) !void { + fn reset(self: *RunState) void { self.codepoints.clearRetainingCapacity(); self.unichars.clearRetainingCapacity(); } @@ -644,8 +644,8 @@ pub const Shaper = struct { pub const RunIteratorHook = struct { shaper: *Shaper, - pub fn prepare(self: *RunIteratorHook) !void { - try self.shaper.run_state.reset(); + pub fn prepare(self: *RunIteratorHook) void { + self.shaper.run_state.reset(); // log.warn("----------- run reset -------------", .{}); } @@ -681,7 +681,7 @@ pub const Shaper = struct { }); } - pub fn finalize(self: RunIteratorHook) !void { + pub fn finalize(self: RunIteratorHook) void { _ = self; } }; diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index e4a9301e8..6dcc0f94e 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -175,7 +175,7 @@ pub const Shaper = struct { pub const RunIteratorHook = struct { shaper: *Shaper, - pub fn prepare(self: RunIteratorHook) !void { + pub fn prepare(self: RunIteratorHook) void { // Reset the buffer for our current run self.shaper.hb_buf.reset(); self.shaper.hb_buf.setContentType(.unicode); @@ -191,7 +191,7 @@ pub const Shaper = struct { self.shaper.hb_buf.add(cp, cluster); } - pub fn finalize(self: RunIteratorHook) !void { + pub fn finalize(self: RunIteratorHook) void { self.shaper.hb_buf.guessSegmentProperties(); } }; diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 85c5c410b..45c5d38ca 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -73,7 +73,7 @@ pub const RunIterator = struct { var current_font: font.Collection.Index = .{}; // Allow the hook to prepare - try self.hooks.prepare(); + self.hooks.prepare(); // Initialize our hash for this run. var hasher = Hasher.init(0); @@ -283,7 +283,7 @@ pub const RunIterator = struct { } // Finalize our buffer - try self.hooks.finalize(); + self.hooks.finalize(); // Add our length to the hash as an additional mechanism to avoid collisions autoHash(&hasher, j - self.i); -- 2.51.2 From 112363c4e174eb9335e8905f052c02c78dacc882 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 11:57:18 -0800 Subject: [PATCH 345/349] font: Collection.getEntry explicit error set --- src/font/Collection.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 412098f10..5d7bfa519 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -196,8 +196,18 @@ pub fn getFace(self: *Collection, index: Index) !*Face { return try self.getFaceFromEntry(try self.getEntry(index)); } +pub const EntryError = error{ + /// Index represents a special font (built-in) and these don't + /// have an associated face. This should be caught upstream and use + /// alternate logic. + SpecialHasNoFace, + + /// Invalid index. + IndexOutOfBounds, +}; + /// Get the unaliased entry from an index -pub fn getEntry(self: *Collection, index: Index) !*Entry { +pub fn getEntry(self: *Collection, index: Index) EntryError!*Entry { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); if (index.idx >= list.len) return error.IndexOutOfBounds; -- 2.51.2 From db092ac3cec33998a4e3741dbe47775ae49b6a58 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 12:05:35 -0800 Subject: [PATCH 346/349] renderer: extract rebuildRow to its own function --- src/renderer/generic.zig | 905 ++++++++++++++++++++------------------- 1 file changed, 465 insertions(+), 440 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index f3c0556f5..bc81fa98e 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2480,6 +2480,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + const PreeditRange = struct { + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, + cp_offset: usize, + }; + /// Convert the terminal state to GPU cells stored in CPU memory. These /// are then synced to the GPU in the next frame. This only updates CPU /// memory and doesn't touch the GPU. @@ -2505,11 +2511,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { + const preedit_range: ?PreeditRange = if (preedit) |preedit_v| preedit: { // We base the preedit on the position of the cursor in the // viewport. If the cursor isn't visible in the viewport we // don't show it. @@ -2587,7 +2589,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { row_dirty[0..row_len], row_selection[0..row_len], row_highlights[0..row_len], - ) |y_usize, row, *cells, *dirty, selection, highlights| { + ) |y_usize, row, *cells, *dirty, selection, *highlights| { const y: terminal.size.CellCountInt = @intCast(y_usize); if (!rebuild) { @@ -2601,440 +2603,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Unmark the dirty state in our render state. dirty.* = false; - // If our viewport is wider than our cell contents buffer, - // we still only process cells up to the width of the buffer. - const cells_slice = cells.slice(); - const cells_len = @min(cells_slice.len, self.cells.size.columns); - const cells_raw = cells_slice.items(.raw); - const cells_style = cells_slice.items(.style); - - // On primary screen, we still apply vertical padding - // extension under certain conditions we feel are safe. - // - // This helps make some scenarios look better while - // avoiding scenarios we know do NOT look good. - switch (self.config.padding_color) { - // These already have the correct values set above. - .background, .@"extend-always" => {}, - - // Apply heuristics for padding extension. - .extend => if (y == 0) { - self.uniforms.padding_extend.up = !rowNeverExtendBg( - row, - cells_raw, - cells_style, - &state.colors.palette, - state.colors.background, - ); - } else if (y == self.cells.size.rows - 1) { - self.uniforms.padding_extend.down = !rowNeverExtendBg( - row, - cells_raw, - cells_style, - &state.colors.palette, - state.colors.background, - ); - }, - } - - // Iterator of runs for shaping. - var run_iter_opts: font.shape.RunOptions = .{ - .grid = self.font_grid, - .cells = cells_slice, - .selection = if (selection) |s| s else null, - - // We want to do font shaping as long as the cursor is - // visible on this viewport. - .cursor_x = cursor_x: { - const vp = state.cursor.viewport orelse break :cursor_x null; - if (vp.y != y) break :cursor_x null; - break :cursor_x vp.x; - }, - }; - run_iter_opts.applyBreakConfig(self.config.font_shaping_break); - var run_iter = self.font_shaper.runIterator(run_iter_opts); - var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); - var shaper_cells: ?[]const font.shape.Cell = null; - var shaper_cells_i: usize = 0; - - for ( - 0.., - cells_raw[0..cells_len], - cells_style[0..cells_len], - ) |x, *cell, *managed_style| { - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| preedit: { - // We're not on the preedit line, no actions necessary. - if (range.y != y) break :preedit; - // We're before the preedit range, no actions necessary. - if (x < range.x[0]) break :preedit; - // We're in the preedit range, skip this cell. - if (x <= range.x[1]) continue; - // After exiting the preedit range we need to catch - // the run position up because of the missed cells. - // In all other cases, no action is necessary. - if (x != range.x[1] + 1) break :preedit; - - // Step the run iterator until we find a run that ends - // after the current cell, which will be the soonest run - // that might contain glyphs for our cell. - while (shaper_run) |run| { - if (run.offset + run.cells > x) break; - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - const run = shaper_run orelse break :preedit; - - // If we haven't shaped this run, do so now. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const new_cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - new_cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache new_cells; - }; - - // Advance our index until we reach or pass - // our current x position in the shaper cells. - const shaper_cells_unwrapped = shaper_cells.?; - while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) { - shaper_cells_i += 1; - } - } - - const wide = cell.wide; - const style: terminal.Style = if (cell.hasStyling()) - managed_style.* - else - .{}; - - // True if this cell is selected - const selected: enum { - false, - selection, - search, - search_selected, - } = selected: { - // Order below matters for precedence. - - // Selection should take the highest precedence. - const x_compare = if (wide == .spacer_tail) - x -| 1 - else - x; - if (selection) |sel| { - if (x_compare >= sel[0] and - x_compare <= sel[1]) break :selected .selection; - } - - // If we're highlighted, then we're selected. In the - // future we want to use a different style for this - // but this to get started. - for (highlights.items) |hl| { - if (x_compare >= hl.range[0] and - x_compare <= hl.range[1]) - { - const tag: HighlightTag = @enumFromInt(hl.tag); - break :selected switch (tag) { - .search_match => .search, - .search_match_selected => .search_selected, - }; - } - } - - break :selected .false; - }; - - // The `_style` suffixed values are the colors based on - // the cell style (SGR), before applying any additional - // configuration, inversions, selections, etc. - const bg_style = style.bg( - cell, - &state.colors.palette, - ); - const fg_style = style.fg(.{ - .default = state.colors.foreground, - .palette = &state.colors.palette, - .bold = self.config.bold_color, - }); - - // The final background color for the cell. - const bg = switch (selected) { - // If we have an explicit selection background color - // specified in the config, use that. - // - // If no configuration, then our selection background - // is our foreground color. - .selection => if (self.config.selection_background) |v| switch (v) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - } else state.colors.foreground, - - .search => switch (self.config.search_background) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - }, - - .search_selected => switch (self.config.search_selected_background) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - }, - - // Not selected - .false => if (style.flags.inverse != isCovering(cell.codepoint())) - // Two cases cause us to invert (use the fg color as the bg) - // - The "inverse" style flag. - // - A "covering" glyph; we use fg for bg in that - // case to help make sure that padding extension - // works correctly. - // - // If one of these is true (but not the other) - // then we use the fg style color for the bg. - fg_style - else - // Otherwise they cancel out. - bg_style, - }; - - const fg = fg: { - // Our happy-path non-selection background color - // is our style or our configured defaults. - const final_bg = bg_style orelse state.colors.background; - - // Whether we need to use the bg color as our fg color: - // - Cell is selected, inverted, and set to cell-foreground - // - Cell is selected, not inverted, and set to cell-background - // - Cell is inverted and not selected - break :fg switch (selected) { - .selection => if (self.config.selection_foreground) |v| switch (v) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - } else state.colors.background, - - .search => switch (self.config.search_foreground) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - }, - - .search_selected => switch (self.config.search_selected_foreground) { - .color => |color| color.toTerminalRGB(), - .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, - .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - }, - - .false => if (style.flags.inverse) - final_bg - else - fg_style, - }; - }; - - // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255; - - // Set the cell's background color. - { - const rgb = bg orelse state.colors.background; - - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - // Cells that are selected should be fully opaque. - if (selected != .false) break :bg_alpha default; - - // Cells that are reversed should be fully opaque. - if (style.flags.inverse) break :bg_alpha default; - - // If the user requested to have opacity on all cells, apply it. - if (self.config.background_opacity_cells and bg_style != null) { - var opacity: f64 = @floatFromInt(default); - opacity *= self.config.background_opacity; - break :bg_alpha @intFromFloat(opacity); - } - - // Cells that have an explicit bg color should be fully opaque. - if (bg_style != null) break :bg_alpha default; - - // Otherwise, we won't draw the bg for this cell, - // we'll let the already-drawn background color - // show through. - break :bg_alpha 0; - }; - - self.cells.bgCell(y, x).* = .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } - - // If the invisible flag is set on this cell then we - // don't need to render any foreground elements, so - // we just skip all glyphs with this x coordinate. - // - // NOTE: This behavior matches xterm. Some other terminal - // emulators, e.g. Alacritty, still render text decorations - // and only make the text itself invisible. The decision - // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { - continue; - } - - // Give links a single underline, unless they already have - // an underline, in which case use a double underline to - // distinguish them. - const underline: terminal.Attribute.Underline = underline: { - if (links.contains(.{ - .x = @intCast(x), - .y = @intCast(y), - })) { - break :underline if (style.flags.underline == .single) - .double - else - .single; - } - break :underline style.flags.underline; - }; - - // We draw underlines first so that they layer underneath text. - // This improves readability when a colored underline is used - // which intersects parts of the text (descenders). - if (underline != .none) self.addUnderline( - @intCast(x), - @intCast(y), - underline, - style.underlineColor(&state.colors.palette) orelse fg, - alpha, - ) catch |err| { - log.warn( - "error adding underline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { - log.warn( - "error adding overline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - // If we're at or past the end of our shaper run then - // we need to get the next run from the run iterator. - if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - if (shaper_run) |run| glyphs: { - // If we haven't shaped this run yet, do so. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const new_cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - new_cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache new_cells; - }; - - const shaped_cells = shaper_cells orelse break :glyphs; - - // If there are no shaper cells for this run, ignore it. - // This can occur for runs of empty cells, and is fine. - if (shaped_cells.len == 0) break :glyphs; - - // If we encounter a shaper cell to the left of the current - // cell then we have some problems. This logic relies on x - // position monotonically increasing. - assert(run.offset + shaped_cells[shaper_cells_i].x >= x); - - // NOTE: An assumption is made here that a single cell will never - // be present in more than one shaper run. If that assumption is - // violated, this logic breaks. - - while (shaper_cells_i < shaped_cells.len and - run.offset + shaped_cells[shaper_cells_i].x == x) : ({ - shaper_cells_i += 1; - }) { - self.addGlyph( - @intCast(x), - @intCast(y), - state.cols, - cells_raw, - shaped_cells[shaper_cells_i], - shaper_run.?, - fg, - alpha, - ) catch |err| { - log.warn( - "error adding glyph to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Finally, draw a strikethrough if necessary. - if (style.flags.strikethrough) self.addStrikethrough( - @intCast(x), - @intCast(y), - fg, - alpha, - ) catch |err| { - log.warn( - "error adding strikethrough to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } + try self.rebuildRow( + y, + row, + cells, + preedit_range, + selection, + highlights, + links, + ); } // Setup our cursor rendering information. @@ -3203,6 +2780,454 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // }); } + fn rebuildRow( + self: *Self, + y: terminal.size.CellCountInt, + row: terminal.page.Row, + cells: *std.MultiArrayList(terminal.RenderState.Cell), + preedit_range: ?PreeditRange, + selection: ?[2]terminal.size.CellCountInt, + highlights: *const std.ArrayList(terminal.RenderState.Highlight), + links: *const terminal.RenderState.CellSet, + ) !void { + const state = &self.terminal_state; + + // If our viewport is wider than our cell contents buffer, + // we still only process cells up to the width of the buffer. + const cells_slice = cells.slice(); + const cells_len = @min(cells_slice.len, self.cells.size.columns); + const cells_raw = cells_slice.items(.raw); + const cells_style = cells_slice.items(.style); + + // On primary screen, we still apply vertical padding + // extension under certain conditions we feel are safe. + // + // This helps make some scenarios look better while + // avoiding scenarios we know do NOT look good. + switch (self.config.padding_color) { + // These already have the correct values set above. + .background, .@"extend-always" => {}, + + // Apply heuristics for padding extension. + .extend => if (y == 0) { + self.uniforms.padding_extend.up = !rowNeverExtendBg( + row, + cells_raw, + cells_style, + &state.colors.palette, + state.colors.background, + ); + } else if (y == self.cells.size.rows - 1) { + self.uniforms.padding_extend.down = !rowNeverExtendBg( + row, + cells_raw, + cells_style, + &state.colors.palette, + state.colors.background, + ); + }, + } + + // Iterator of runs for shaping. + var run_iter_opts: font.shape.RunOptions = .{ + .grid = self.font_grid, + .cells = cells_slice, + .selection = if (selection) |s| s else null, + + // We want to do font shaping as long as the cursor is + // visible on this viewport. + .cursor_x = cursor_x: { + const vp = state.cursor.viewport orelse break :cursor_x null; + if (vp.y != y) break :cursor_x null; + break :cursor_x vp.x; + }, + }; + run_iter_opts.applyBreakConfig(self.config.font_shaping_break); + var run_iter = self.font_shaper.runIterator(run_iter_opts); + var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); + var shaper_cells: ?[]const font.shape.Cell = null; + var shaper_cells_i: usize = 0; + + for ( + 0.., + cells_raw[0..cells_len], + cells_style[0..cells_len], + ) |x, *cell, *managed_style| { + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. + if (preedit_range) |range| preedit: { + // We're not on the preedit line, no actions necessary. + if (range.y != y) break :preedit; + // We're before the preedit range, no actions necessary. + if (x < range.x[0]) break :preedit; + // We're in the preedit range, skip this cell. + if (x <= range.x[1]) continue; + // After exiting the preedit range we need to catch + // the run position up because of the missed cells. + // In all other cases, no action is necessary. + if (x != range.x[1] + 1) break :preedit; + + // Step the run iterator until we find a run that ends + // after the current cell, which will be the soonest run + // that might contain glyphs for our cell. + while (shaper_run) |run| { + if (run.offset + run.cells > x) break; + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + const run = shaper_run orelse break :preedit; + + // If we haven't shaped this run, do so now. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const new_cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + new_cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache new_cells; + }; + + // Advance our index until we reach or pass + // our current x position in the shaper cells. + const shaper_cells_unwrapped = shaper_cells.?; + while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) { + shaper_cells_i += 1; + } + } + + const wide = cell.wide; + const style: terminal.Style = if (cell.hasStyling()) + managed_style.* + else + .{}; + + // True if this cell is selected + const selected: enum { + false, + selection, + search, + search_selected, + } = selected: { + // Order below matters for precedence. + + // Selection should take the highest precedence. + const x_compare = if (wide == .spacer_tail) + x -| 1 + else + x; + if (selection) |sel| { + if (x_compare >= sel[0] and + x_compare <= sel[1]) break :selected .selection; + } + + // If we're highlighted, then we're selected. In the + // future we want to use a different style for this + // but this to get started. + for (highlights.items) |hl| { + if (x_compare >= hl.range[0] and + x_compare <= hl.range[1]) + { + const tag: HighlightTag = @enumFromInt(hl.tag); + break :selected switch (tag) { + .search_match => .search, + .search_match_selected => .search_selected, + }; + } + } + + break :selected .false; + }; + + // The `_style` suffixed values are the colors based on + // the cell style (SGR), before applying any additional + // configuration, inversions, selections, etc. + const bg_style = style.bg( + cell, + &state.colors.palette, + ); + const fg_style = style.fg(.{ + .default = state.colors.foreground, + .palette = &state.colors.palette, + .bold = self.config.bold_color, + }); + + // The final background color for the cell. + const bg = switch (selected) { + // If we have an explicit selection background color + // specified in the config, use that. + // + // If no configuration, then our selection background + // is our foreground color. + .selection => if (self.config.selection_background) |v| switch (v) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + } else state.colors.foreground, + + .search => switch (self.config.search_background) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + }, + + .search_selected => switch (self.config.search_selected_background) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + }, + + // Not selected + .false => if (style.flags.inverse != isCovering(cell.codepoint())) + // Two cases cause us to invert (use the fg color as the bg) + // - The "inverse" style flag. + // - A "covering" glyph; we use fg for bg in that + // case to help make sure that padding extension + // works correctly. + // + // If one of these is true (but not the other) + // then we use the fg style color for the bg. + fg_style + else + // Otherwise they cancel out. + bg_style, + }; + + const fg = fg: { + // Our happy-path non-selection background color + // is our style or our configured defaults. + const final_bg = bg_style orelse state.colors.background; + + // Whether we need to use the bg color as our fg color: + // - Cell is selected, inverted, and set to cell-foreground + // - Cell is selected, not inverted, and set to cell-background + // - Cell is inverted and not selected + break :fg switch (selected) { + .selection => if (self.config.selection_foreground) |v| switch (v) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + } else state.colors.background, + + .search => switch (self.config.search_foreground) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + }, + + .search_selected => switch (self.config.search_selected_foreground) { + .color => |color| color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + }, + + .false => if (style.flags.inverse) + final_bg + else + fg_style, + }; + }; + + // Foreground alpha for this cell. + const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255; + + // Set the cell's background color. + { + const rgb = bg orelse state.colors.background; + + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + // Cells that are selected should be fully opaque. + if (selected != .false) break :bg_alpha default; + + // Cells that are reversed should be fully opaque. + if (style.flags.inverse) break :bg_alpha default; + + // If the user requested to have opacity on all cells, apply it. + if (self.config.background_opacity_cells and bg_style != null) { + var opacity: f64 = @floatFromInt(default); + opacity *= self.config.background_opacity; + break :bg_alpha @intFromFloat(opacity); + } + + // Cells that have an explicit bg color should be fully opaque. + if (bg_style != null) break :bg_alpha default; + + // Otherwise, we won't draw the bg for this cell, + // we'll let the already-drawn background color + // show through. + break :bg_alpha 0; + }; + + self.cells.bgCell(y, x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } + + // If the invisible flag is set on this cell then we + // don't need to render any foreground elements, so + // we just skip all glyphs with this x coordinate. + // + // NOTE: This behavior matches xterm. Some other terminal + // emulators, e.g. Alacritty, still render text decorations + // and only make the text itself invisible. The decision + // has been made here to match xterm's behavior for this. + if (style.flags.invisible) { + continue; + } + + // Give links a single underline, unless they already have + // an underline, in which case use a double underline to + // distinguish them. + const underline: terminal.Attribute.Underline = underline: { + if (links.contains(.{ + .x = @intCast(x), + .y = @intCast(y), + })) { + break :underline if (style.flags.underline == .single) + .double + else + .single; + } + break :underline style.flags.underline; + }; + + // We draw underlines first so that they layer underneath text. + // This improves readability when a colored underline is used + // which intersects parts of the text (descenders). + if (underline != .none) self.addUnderline( + @intCast(x), + @intCast(y), + underline, + style.underlineColor(&state.colors.palette) orelse fg, + alpha, + ) catch |err| { + log.warn( + "error adding underline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { + log.warn( + "error adding overline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + // If we're at or past the end of our shaper run then + // we need to get the next run from the run iterator. + if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + if (shaper_run) |run| glyphs: { + // If we haven't shaped this run yet, do so. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const new_cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + new_cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache new_cells; + }; + + const shaped_cells = shaper_cells orelse break :glyphs; + + // If there are no shaper cells for this run, ignore it. + // This can occur for runs of empty cells, and is fine. + if (shaped_cells.len == 0) break :glyphs; + + // If we encounter a shaper cell to the left of the current + // cell then we have some problems. This logic relies on x + // position monotonically increasing. + assert(run.offset + shaped_cells[shaper_cells_i].x >= x); + + // NOTE: An assumption is made here that a single cell will never + // be present in more than one shaper run. If that assumption is + // violated, this logic breaks. + + while (shaper_cells_i < shaped_cells.len and + run.offset + shaped_cells[shaper_cells_i].x == x) : ({ + shaper_cells_i += 1; + }) { + self.addGlyph( + @intCast(x), + @intCast(y), + state.cols, + cells_raw, + shaped_cells[shaper_cells_i], + shaper_run.?, + fg, + alpha, + ) catch |err| { + log.warn( + "error adding glyph to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Finally, draw a strikethrough if necessary. + if (style.flags.strikethrough) self.addStrikethrough( + @intCast(x), + @intCast(y), + fg, + alpha, + ) catch |err| { + log.warn( + "error adding strikethrough to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + /// Add an underline decoration to the specified cell fn addUnderline( self: *Self, -- 2.51.2 From d235c490e9e68b6d90bb0016750b47c5e09b3be6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 12:10:05 -0800 Subject: [PATCH 347/349] renderer: handle rebuildCells failures gracefully --- src/renderer/generic.zig | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index bc81fa98e..17ca3f3cd 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1282,6 +1282,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + // From this point forward no more errors. + errdefer comptime unreachable; + // Reset our dirty state after updating. defer self.terminal_state.dirty = .false; @@ -1291,7 +1294,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { defer self.draw_mutex.unlock(); // Build our GPU cells - try self.rebuildCells( + self.rebuildCells( critical.preedit, renderer.cursorStyle(&self.terminal_state, .{ .preedit = critical.preedit != null, @@ -1299,7 +1302,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .blink_visible = cursor_blink_visible, }), &critical.links, - ); + ) catch |err| { + // This means we weren't able to allocate our buffer + // to update the cells. In this case, we continue with + // our old buffer (frozen contents) and log it. + comptime assert(@TypeOf(err) == error{OutOfMemory}); + log.warn("error rebuilding GPU cells err={}", .{err}); + }; // The scrollbar is only emitted during draws so we also // check the scrollbar cache here and update if needed. @@ -2498,7 +2507,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, links: *const terminal.RenderState.CellSet, - ) !void { + ) Allocator.Error!void { const state: *terminal.RenderState = &self.terminal_state; // const start = try std.time.Instant.now(); @@ -2566,6 +2575,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + // From this point on we never fail. We produce some kind of + // working terminal state, even if incorrect. + errdefer comptime unreachable; + // Get our row data from our state const row_data = state.row_data.slice(); const row_raws = row_data.items(.raw); @@ -2603,7 +2616,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Unmark the dirty state in our render state. dirty.* = false; - try self.rebuildRow( + self.rebuildRow( y, row, cells, @@ -2611,7 +2624,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { selection, highlights, links, - ); + ) catch |err| { + // This should never happen except under exceptional + // scenarios. In this case, we don't want to corrupt + // our render state so just clear this row and keep + // trying to finish it out. + log.warn("error building row y={} err={}", .{ y, err }); + self.cells.clear(y); + }; } // Setup our cursor rendering information. -- 2.51.2 From 6ec2bfe2887ab9f81175b9faf0b05e19379896ef Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 12:27:28 -0800 Subject: [PATCH 348/349] renderer: kitty graphics prep can't fail (skip failed conversions) --- src/renderer/generic.zig | 100 +++++++++++++++++++++++++++------------ src/renderer/image.zig | 5 +- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 17ca3f3cd..e75171721 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1117,7 +1117,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self: *Self, state: *renderer.State, cursor_blink_visible: bool, - ) !void { + ) Allocator.Error!void { // We fully deinit and reset the terminal state every so often // so that a particularly large terminal state doesn't cause // the renderer to hold on to retained memory. @@ -1193,7 +1193,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { if (state.terminal.screens.active.kitty_images.dirty or self.image_virtual) { - try self.prepKittyGraphics(state.terminal); + self.prepKittyGraphics(state.terminal); } // Get our OSC8 links we're hovering if we have a mouse. @@ -1712,7 +1712,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { fn prepKittyGraphics( self: *Self, t: *terminal.Terminal, - ) !void { + ) void { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); @@ -1776,16 +1776,32 @@ pub fn Renderer(comptime GraphicsAPI: type) type { continue; }; - try self.prepKittyPlacement(t, top_y, bot_y, &image, p); + self.prepKittyPlacement( + t, + top_y, + bot_y, + &image, + p, + ) catch |err| { + // For errors we log and continue. We try to place + // other placements even if one fails. + log.warn("error preparing kitty placement err={}", .{err}); + }; } // If we have virtual placements then we need to scan for placeholders. if (self.image_virtual) { var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( - t, - &virtual_p, - ); + while (v_it.next()) |virtual_p| { + self.prepKittyVirtualPlacement( + t, + &virtual_p, + ) catch |err| { + // For errors we log and continue. We try to place + // other placements even if one fails. + log.warn("error preparing kitty placement err={}", .{err}); + }; + } } // Sort the placements by their Z value. @@ -1833,7 +1849,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self: *Self, t: *terminal.Terminal, p: *const terminal.kitty.graphics.unicode.Placement, - ) !void { + ) PrepKittyImageError!void { const storage = &t.screens.active.kitty_images; const image = storage.imageById(p.image_id) orelse { log.warn( @@ -1894,7 +1910,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { bot_y: u32, image: *const terminal.kitty.graphics.Image, p: *const terminal.kitty.graphics.ImageStorage.Placement, - ) !void { + ) PrepKittyImageError!void { // Get the rect for the placement. If this placement doesn't have // a rect then its virtual or something so skip it. const rect = p.rect(image.*, t) orelse return; @@ -1950,12 +1966,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + const PrepKittyImageError = error{ + OutOfMemory, + ImageConversionError, + }; + /// Prepare the provided image for upload to the GPU by copying its /// data with our allocator and setting it to the pending state. fn prepKittyImage( self: *Self, image: *const terminal.kitty.graphics.Image, - ) !void { + ) PrepKittyImageError!void { // If this image exists and its transmit time is the same we assume // it is the identical image so we don't need to send it to the GPU. const gop = try self.images.getOrPut(self.alloc, image.id); @@ -1966,39 +1987,60 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); + const data = if (self.alloc.dupe( + u8, + image.data, + )) |v| v else |_| { + if (!gop.found_existing) { + // If this is a new entry we can just remove it since it + // was never sent to the GPU. + _ = self.images.remove(image.id); + } else { + // If this was an existing entry, it is invalid and + // we must unload it. + gop.value_ptr.image.markForUnload(); + } + + return error.OutOfMemory; + }; + // Note: we don't need to errdefer free the data because it is + // put into the map immediately below and our errdefer to + // handle our map state will fix this up. // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .pixel_format = switch (image.format) { - .gray => .gray, - .gray_alpha => .gray_alpha, - .rgb => .rgb, - .rgba => .rgba, - .png => unreachable, // should be decoded by now + const new_image: Image = .{ + .pending = .{ + .width = image.width, + .height = image.height, + .pixel_format = switch (image.format) { + .gray => .gray, + .gray_alpha => .gray_alpha, + .rgb => .rgb, + .rgba => .rgba, + .png => unreachable, // should be decoded by now + }, + .data = data.ptr, }, - .data = data.ptr, }; - - const new_image: Image = .{ .pending = pending }; - if (!gop.found_existing) { gop.value_ptr.* = .{ .image = new_image, .transmit_time = undefined, }; } else { - try gop.value_ptr.image.markForReplace( + gop.value_ptr.image.markForReplace( self.alloc, new_image, ); } - try gop.value_ptr.image.prepForUpload(self.alloc); + // If any error happens, we unload the image and it is invalid. + errdefer gop.value_ptr.image.markForUnload(); + gop.value_ptr.image.prepForUpload(self.alloc) catch |err| { + log.warn("error preparing kitty image for upload err={}", .{err}); + return error.ImageConversionError; + }; gop.value_ptr.transmit_time = image.transmit_time; } @@ -2090,7 +2132,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If we have an existing background image, replace it. // Otherwise, set this as our background image directly. if (self.bg_image) |*img| { - try img.markForReplace(self.alloc, image); + img.markForReplace(self.alloc, image); } else { self.bg_image = image; } diff --git a/src/renderer/image.zig b/src/renderer/image.zig index 7089f5a8b..bf0f7b736 100644 --- a/src/renderer/image.zig +++ b/src/renderer/image.zig @@ -146,7 +146,7 @@ pub const Image = union(enum) { /// Mark the current image to be replaced with a pending one. This will /// attempt to update the existing texture if we have one, otherwise it /// will act like a new upload. - pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void { + pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) void { assert(img.isPending()); // If we have pending data right now, free it. @@ -216,9 +216,8 @@ pub const Image = union(enum) { /// Prepare the pending image data for upload to the GPU. /// This doesn't need GPU access so is safe to call any time. - pub fn prepForUpload(self: *Image, alloc: Allocator) !void { + pub fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void { assert(self.isPending()); - try self.convert(alloc); } -- 2.51.2 From 8ef0842b01aaafb7493fbd74a37f66d137e0c6ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Jan 2026 13:07:06 -0800 Subject: [PATCH 349/349] ci: authenticate pinact to avoid GH rate limits When we're running a lot of CI we're hitting low unauthenticated rate limits. --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbac22a47..734b8d224 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -844,6 +844,8 @@ jobs: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 + permissions: + contents: read env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache @@ -866,6 +868,8 @@ jobs: useDaemon: false # sometimes fails on short jobs - name: pinact check run: nix develop -c pinact run --check + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} prettier: if: github.repository == 'ghostty-org/ghostty' -- 2.51.2