From 92aa96038137ef4f88a04e5a30b9c65405d8f835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Soares?= Date: Sun, 23 Nov 2025 12:43:11 -0300 Subject: [PATCH 1/3] Add flag for quick terminal --- .../Features/QuickTerminal/QuickTerminalController.swift | 5 ++++- src/apprt/gtk/class/surface.zig | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index b3ad88666..4c2052f23 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -342,7 +342,10 @@ class QuickTerminalController: BaseTerminalController { // animate out. if surfaceTree.isEmpty, let ghostty_app = ghostty.app { - let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil) + var config = Ghostty.SurfaceConfiguration() + config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" + + let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config) surfaceTree = SplitTree(view: view) focusedSurface = view } diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 291a405ce..3f9c0d741 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1465,6 +1465,10 @@ pub const Surface = extern struct { // EnvMap is a bit annoying so I'm punting it. if (ext.getAncestor(Window, self.as(gtk.Widget))) |window| { try window.winproto().addSubprocessEnv(&env); + + if (window.isQuickTerminal()) { + try env.put("GHOSTTY_QUICK_TERMINAL", "1"); + } } return env; From 36c32958068c54879adfdd389f8146193f9b0e92 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Sun, 23 Nov 2025 20:39:35 -0500 Subject: [PATCH 2/3] unicode: don't narrow invalid text presentation (VS15) sequences --- src/build/uucode_config.zig | 3 +- src/terminal/Terminal.zig | 92 +++++++++++++++++++++++++++++------- src/unicode/props.zig | 12 ++--- src/unicode/props_uucode.zig | 6 +-- 4 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/build/uucode_config.zig b/src/build/uucode_config.zig index 0843732b1..fcad6ad6a 100644 --- a/src/build/uucode_config.zig +++ b/src/build/uucode_config.zig @@ -90,8 +90,7 @@ pub const tables = [_]config.Table{ width.field("width"), d.field("grapheme_break"), is_symbol.field("is_symbol"), - d.field("is_emoji_vs_text"), - d.field("is_emoji_vs_emoji"), + d.field("is_emoji_vs_base"), }, }, }; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 1ec5b5d47..09e3727df 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -374,20 +374,10 @@ 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) { - // This check below isn't robust enough to be correct. - // 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(); - if (!emoji) valid_check: { - // If not an emoji, check if it is a defined variation - // sequence in emoji-variation-sequences.txt - if (c == 0xFE0F and prev_props.emoji_vs_emoji) break :valid_check; - if (c == 0xFE0E and prev_props.emoji_vs_text) break :valid_check; - return; - } + // 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; switch (c) { 0xFE0F => wide: { @@ -3288,7 +3278,7 @@ test "Terminal: print invalid VS16 non-grapheme" { try t.print('x'); try t.print(0xFE0F); - // We should have 2 cells taken up. It is one character but "wide". + // We should have 1 narrow cell. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x); @@ -3601,6 +3591,40 @@ test "Terminal: VS15 on already narrow emoji" { } } +test "Terminal: print invalid VS15 in emoji ZWJ sequence" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.print('\u{1F469}'); // 👩 + try t.print(0xFE0E); // not valid with U+1F469 as base + try t.print('\u{200D}'); // ZWJ + try t.print('\u{1F466}'); // 👦 + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + 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 = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, '\u{1F469}'), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{ '\u{200D}', '\u{1F466}' }, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } +} + test "Terminal: VS15 to make narrow character with pending wrap" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 2 }); defer t.deinit(testing.allocator); @@ -3723,9 +3747,9 @@ test "Terminal: print invalid VS16 grapheme" { // https://github.com/mitchellh/ghostty/issues/1482 try t.print('x'); - try t.print(0xFE0F); + try t.print(0xFE0F); // invalid VS16 - // We should have 2 cells taken up. It is one character but "wide". + // We should have 1 cells taken up, and narrow. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x); @@ -3758,7 +3782,7 @@ test "Terminal: print invalid VS16 with second char" { try t.print(0xFE0F); try t.print('y'); - // We should have 2 cells taken up. It is one character but "wide". + // We should have 2 cells taken up, from two separate narrow characters. try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x); @@ -3781,6 +3805,40 @@ test "Terminal: print invalid VS16 with second char" { } } +test "Terminal: print invalid VS16 with second char (combining)" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + // https://github.com/mitchellh/ghostty/issues/1482 + try t.print('n'); + try t.print(0xFE0F); // invalid VS16 + try t.print(0x0303); // combining tilde + + // We should have 1 cells taken up, and narrow. + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + try testing.expectEqual(@as(usize, 1), 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 = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 'n'), cell.content.codepoint); + try testing.expect(cell.hasGrapheme()); + try testing.expectEqualSlices(u21, &.{'\u{0303}'}, list_cell.node.data.lookupGrapheme(cell).?); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expectEqual(Cell.Wide.narrow, cell.wide); + } +} + test "Terminal: overwrite grapheme should clear grapheme data" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator); diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 7099e79cd..492dad34a 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -16,15 +16,13 @@ pub const Properties = packed struct { grapheme_boundary_class: GraphemeBoundaryClass = .invalid, /// Emoji VS compatibility - emoji_vs_text: bool = false, - emoji_vs_emoji: bool = false, + emoji_vs_base: bool = false, // 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.emoji_vs_text == b.emoji_vs_text and - a.emoji_vs_emoji == b.emoji_vs_emoji; + a.emoji_vs_base == b.emoji_vs_base; } // Needed for lut.Generator @@ -36,14 +34,12 @@ pub const Properties = packed struct { \\.{{ \\ .width= {}, \\ .grapheme_boundary_class= .{s}, - \\ .emoji_vs_text= {}, - \\ .emoji_vs_emoji= {}, + \\ .emoji_vs_base= {}, \\}} , .{ self.width, @tagName(self.grapheme_boundary_class), - self.emoji_vs_text, - self.emoji_vs_emoji, + self.emoji_vs_base, }); } }; diff --git a/src/unicode/props_uucode.zig b/src/unicode/props_uucode.zig index b30c4be3a..2440d437c 100644 --- a/src/unicode/props_uucode.zig +++ b/src/unicode/props_uucode.zig @@ -48,15 +48,13 @@ pub fn get(cp: u21) Properties { if (cp > uucode.config.max_code_point) return .{ .width = 1, .grapheme_boundary_class = .invalid, - .emoji_vs_text = false, - .emoji_vs_emoji = false, + .emoji_vs_base = false, }; return .{ .width = uucode.get(.width, cp), .grapheme_boundary_class = graphemeBoundaryClass(cp), - .emoji_vs_text = uucode.get(.is_emoji_vs_text, cp), - .emoji_vs_emoji = uucode.get(.is_emoji_vs_emoji, cp), + .emoji_vs_base = uucode.get(.is_emoji_vs_base, cp), }; } From 8f033c7022ef36b9a5bed6508ee245bc38b0d072 Mon Sep 17 00:00:00 2001 From: Jacob Sandlund Date: Mon, 24 Nov 2025 09:25:39 -0500 Subject: [PATCH 3/3] Add test with just a single emoji followed by VS15 (invalid) --- src/terminal/Terminal.zig | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 09e3727df..e75fd731a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -3591,6 +3591,37 @@ test "Terminal: VS15 on already narrow emoji" { } } +test "Terminal: print invalid VS15 following emoji is wide" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + try t.print('\u{1F9E0}'); // 🧠 + try t.print(0xFE0E); // not valid with U+1F9E0 as base + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y); + 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 = 0, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, '\u{1F9E0}'), cell.content.codepoint); + try testing.expect(!cell.hasGrapheme()); + try testing.expectEqual(Cell.Wide.wide, cell.wide); + } + { + const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?; + const cell = list_cell.cell; + try testing.expectEqual(@as(u21, 0), cell.content.codepoint); + try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); + } +} + test "Terminal: print invalid VS15 in emoji ZWJ sequence" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator);