From f7994e641251a7f93567c60c17809ad0b3cc53a0 Mon Sep 17 00:00:00 2001 From: ClearAspect Date: Fri, 1 Aug 2025 21:06:46 -0400 Subject: [PATCH 01/75] fix: correct the cursor Y position value exposed to shader uniforms Fix for discussion #8113 The cursor Y position value exposed to the shader uniforms was incorrectly calculated. As per the doc in cell_text.v.glsl: In order to get the top left of the glyph, we compute an offset based on the bearings. The Y bearing is the distance from the bottom of the cell to the top of the glyph, so we subtract it from the cell height to get the y offset. This calculation was mistakenly left out of the original code. This will ensure that the custom shaders using iCurrentCursor/iPreviousCursor get the correct Y coordinate representing the top-left corner of the cursor rectangle, matching the documented uniform behavior --- src/renderer/generic.zig | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d975f0f96..578ab1779 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2233,15 +2233,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { ); pixel_x += @floatFromInt(cursor.bearings[0]); - pixel_y += @floatFromInt(cursor.bearings[1]); + // Convert the Y coordinate from bottom-to-top to top-to-bottom. + // Otherwise we end up with glyphs like underline at the top of the cell. + pixel_y += @floatFromInt(@as(i32, @intCast(cell.height)) - cursor.bearings[1]); // If +Y is up in our shaders, we need to flip the coordinate. if (!GraphicsAPI.custom_shader_y_is_down) { pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y; - // We need to add the cursor height because we need the +Y - // edge for the Y coordinate, and flipping means that it's - // the -Y edge now. - pixel_y += cursor_height; } const new_cursor: [4]f32 = .{ From 35102ddb5a211dbbf5a01a39b109ac7562c80000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Fri, 22 Aug 2025 14:53:56 +0100 Subject: [PATCH 02/75] Updating Irish translation for Ghostty 1.2 --- po/ga_IE.UTF-8.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 0771ecbcd..ee5a53522 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ 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-06-29 21:15+0100\n" +"PO-Revision-Date: 2025-08-22 14:52+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" -"X-Generator: Poedit 3.4.4\n" +"X-Generator: Poedit 3.4.2\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -209,12 +209,12 @@ msgstr "Ceadaigh" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Cuimhnigh ar an rogha don scoilt seo" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -280,15 +280,15 @@ msgstr "Cóipeáilte chuig an ghearrthaisce" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Glanadh an ghearrthóg" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "D'éirigh leis an ordú" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Theip ar an ordú" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From 652f6f1deb00d83d7db3c09cbcd932e362cbbe2a Mon Sep 17 00:00:00 2001 From: Cheru Berhanu Date: Thu, 14 Aug 2025 17:51:45 -0700 Subject: [PATCH 03/75] terminal: fix use-after-free in exec This was only an issue on Linux, as MacOS' command is reallocated and rewritten. We hit this using embedded Ghostty w/o a login shell :p --- src/termio/Exec.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 15b6b8cd4..ec332afa1 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1513,7 +1513,8 @@ fn execCommand( } return switch (command) { - .direct => |v| v, + // We need to clone the command since there's no guarantee the config remains valid. + .direct => |_| (try command.clone(alloc)).direct, .shell => |v| shell: { var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); From d854ecd37403a0781a5e9b72066a3282f5bc6f0b Mon Sep 17 00:00:00 2001 From: Cheru Berhanu Date: Fri, 22 Aug 2025 15:55:08 -0700 Subject: [PATCH 04/75] terminal: test execCommand w/ freed config --- src/termio/Exec.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index ec332afa1..5a15392b4 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1689,3 +1689,35 @@ test "execCommand: direct command, error passwd" { try testing.expectEqualStrings(result[0], "foo"); try testing.expectEqualStrings(result[1], "bar baz"); } + +test "execCommand: direct command, config freed" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var command_arena = ArenaAllocator.init(alloc); + const command_alloc = command_arena.allocator(); + const command = try (configpkg.Command{ + .direct = &.{ + "foo", + "bar baz", + }, + }).clone(command_alloc); + + const result = try execCommand(alloc, command, struct { + fn get(_: Allocator) !PasswdEntry { + // Failed passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return error.Fail; + } + }); + + command_arena.deinit(); + + try testing.expectEqual(2, result.len); + try testing.expectEqualStrings(result[0], "foo"); + try testing.expectEqualStrings(result[1], "bar baz"); +} From c181fc4fbf4f3aee4382097e278acaa2b622bd99 Mon Sep 17 00:00:00 2001 From: Gal <68018569+CraziestOwl@users.noreply.github.com> Date: Sat, 23 Aug 2025 08:03:26 +0300 Subject: [PATCH 05/75] i18n: Update Hebrew translation issue: #8344 --- po/he_IL.UTF-8.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po index 08ffb1b36..4ddeb9584 100644 --- a/po/he_IL.UTF-8.po +++ b/po/he_IL.UTF-8.po @@ -2,15 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. # Sl (Shahaf Levi), Sl's Repository Ltd , 2025. +# CraziestOwl , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-13 00:00+0000\n" -"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" +"PO-Revision-Date: 2025-08-23 08:00+0300\n" +"Last-Translator: CraziestOwl \n" "Language-Team: Hebrew \n" "Language: he\n" "MIME-Version: 1.0\n" @@ -276,15 +276,15 @@ msgstr "הועתק ללוח ההעתקה" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "לוח ההעתקה רוקן" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "הפקודה הצליחה" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "הפקודה נכשלה" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From f6f2a852562971e01976deff05091bee9460ff7a Mon Sep 17 00:00:00 2001 From: Emir SARI Date: Fri, 22 Aug 2025 17:05:56 +0300 Subject: [PATCH 06/75] i18n: Update Turkish translations Signed-off-by: Emir SARI --- po/tr_TR.UTF-8.po | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index ab6c0554f..9e85484f5 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -8,7 +8,7 @@ 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-03-24 22:01+0300\n" +"PO-Revision-Date: 2025-08-23 17:30+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" "Language: tr\n" @@ -88,7 +88,7 @@ msgstr "Sağa Doğru Böl" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Bir komut çalıştır…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -161,7 +161,7 @@ msgstr "Yapılandırmayı Aç" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Komut Paleti" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -209,12 +209,12 @@ msgstr "İzin Ver" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Bu bölme için tercihi anımsa" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Bu istemi tekrar göstermek için yapılandırmayı yeniden yükle" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -279,15 +279,15 @@ msgstr "Panoya kopyalandı" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Pano temizlendi" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Komut başarılı oldu" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Komut başarısız oldu" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From 4f4c06967a609c3d299f94d64b653922c64e2726 Mon Sep 17 00:00:00 2001 From: hanna Date: Sat, 23 Aug 2025 12:43:02 -0400 Subject: [PATCH 07/75] i18n: update norwegian translations --- po/nb_NO.UTF-8.po | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 67c436e46..1a30d6e7f 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -1,7 +1,7 @@ # Norwegian Bokmal translations for com.mitchellh.ghostty package. # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. -# Hanna Rose , 2025. +# Hanna Rose , 2025. # Uzair Aftab , 2025. # Christoffer Tønnessen , 2025. # cryptocode , 2025. @@ -90,7 +90,7 @@ msgstr "Del til høyre" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Utfør en kommando..." #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -163,7 +163,7 @@ msgstr "Åpne konfigurasjon" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Kommandopalett" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -211,12 +211,12 @@ msgstr "Tillat" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Husk valget for dette vinduet?" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Last inn konfigurasjonen på nytt for å vise denne meldingen igjen" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -281,15 +281,15 @@ msgstr "Kopiert til utklippstavlen" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Utklippstavle tømt" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Kommando lyktes" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Kommando mislyktes" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From 78f05ec96c3a171bf9b8148fc4b3e3050c4bb1e7 Mon Sep 17 00:00:00 2001 From: hanna Date: Sat, 23 Aug 2025 12:48:32 -0400 Subject: [PATCH 08/75] i18n: adjust wording in translation --- po/nb_NO.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 1a30d6e7f..626d86788 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -90,7 +90,7 @@ msgstr "Del til høyre" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "Utfør en kommando..." +msgstr "Kjør en kommando..." #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 From bd4e9b96bf882596dbeea71d32019b3c690cfc34 Mon Sep 17 00:00:00 2001 From: hanna Date: Sat, 23 Aug 2025 12:53:05 -0400 Subject: [PATCH 09/75] i18n: update translation metadata --- po/nb_NO.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 626d86788..7cf1219f7 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -11,8 +11,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-04-14 16:25+0200\n" -"Last-Translator: cryptocode \n" +"PO-Revision-Date: 2025-08-23 12:52+0000\n" +"Last-Translator: Hanna Rose \n" "Language-Team: Norwegian Bokmal \n" "Language: nb\n" "MIME-Version: 1.0\n" From 1aa59cf63d1cbf92dc8aa08fb9f87f129c3afe37 Mon Sep 17 00:00:00 2001 From: hanna Date: Sat, 23 Aug 2025 13:19:13 -0400 Subject: [PATCH 10/75] i18n: update choice selection prompt text --- po/nb_NO.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 7cf1219f7..047736470 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -211,7 +211,7 @@ msgstr "Tillat" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "Husk valget for dette vinduet?" +msgstr "Husk valget for dette delte vinduet?" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 From a3be474d28c80977b953e26c4785d93e03a6dc5e Mon Sep 17 00:00:00 2001 From: Kirwiisp <59315476+Kirwiisp@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:58:23 +0200 Subject: [PATCH 11/75] add new translations --- po/fr_FR.UTF-8.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 59959bd49..4c520dd7c 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -8,7 +8,7 @@ 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-03-22 09:31+0100\n" +"PO-Revision-Date: 2025-08-23 21:01+0200\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" "Language: fr\n" @@ -88,7 +88,7 @@ msgstr "Panneau à droite" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Exécuter une commande…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -161,7 +161,7 @@ msgstr "Ouvrir la configuration" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Palette de commandes" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -209,12 +209,12 @@ msgstr "Autoriser" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Se rappeler du choix pour ce panneau" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recharger la configuration pour afficher à nouveau ce message" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -279,15 +279,15 @@ msgstr "Copié dans le presse-papiers" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Presse-papiers vidé" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Commande réussie" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "La commande a échoué" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -299,7 +299,7 @@ msgstr "Voir les onglets ouverts" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Nouveau panneau" #: src/apprt/gtk/Window.zig:329 msgid "" From 43e010bf47abb96ed406f12f88d543afa6f916bd Mon Sep 17 00:00:00 2001 From: dy0gu Date: Mon, 28 Jul 2025 12:46:40 +0100 Subject: [PATCH 12/75] feat: add option to disable the "Reloaded the configuration" notification --- src/apprt/gtk-ng/class/window.zig | 11 ++++++++++- src/apprt/gtk/Window.zig | 4 +++- src/config/Config.zig | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 91e65731b..e085529ca 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -961,7 +961,16 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { - self.addToast(i18n._("Reloaded the configuration")); + // We only toast if configured to + const priv = self.private(); + const config_obj = priv.config orelse { + self.syncAppearance(); + return; + }; + const config = config_obj.get(); + if (config.@"app-notifications".@"config-reload") { + self.addToast(i18n._("Reloaded the configuration")); + } self.syncAppearance(); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2f026e33c..d3408e867 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -772,7 +772,9 @@ pub fn focusCurrentTab(self: *Window) void { } pub fn onConfigReloaded(self: *Window) void { - self.sendToast(i18n._("Reloaded the configuration")); + if (self.app.config.@"app-notifications".@"config-reload") { + self.sendToast(i18n._("Reloaded the configuration")); + } } pub fn sendToast(self: *Window, title: [*:0]const u8) void { diff --git a/src/config/Config.zig b/src/config/Config.zig index d8fcfa1d7..637cfee49 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2499,6 +2499,8 @@ keybind: Keybinds = .{}, /// /// - `clipboard-copy` (default: true) - Show a notification when text is copied /// to the clipboard. +/// - `config-reload` (default: true) - Show a notification when +/// the configuration is reloaded. /// /// To specify a notification to enable, specify the name of the notification. /// To specify a notification to disable, prefix the name with `no-`. For @@ -7058,6 +7060,7 @@ pub const GtkTitlebarStyle = enum(c_int) { /// See app-notifications pub const AppNotifications = packed struct { @"clipboard-copy": bool = true, + @"config-reload": bool = true, }; /// See bell-features From e1d4c37996a94b8843951d45c29c0733ecd1c434 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 23 Aug 2025 12:51:50 -0700 Subject: [PATCH 13/75] apprt/gtk-ng: some style changes for toast --- src/apprt/gtk-ng/class/window.zig | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index e085529ca..30fa43dae 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -961,16 +961,14 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { - // We only toast if configured to const priv = self.private(); - const config_obj = priv.config orelse { - self.syncAppearance(); - return; - }; - const config = config_obj.get(); - if (config.@"app-notifications".@"config-reload") { - self.addToast(i18n._("Reloaded the configuration")); + if (priv.config) |config_obj| { + const config = config_obj.get(); + if (config.@"app-notifications".@"config-reload") { + self.addToast(i18n._("Reloaded the configuration")); + } } + self.syncAppearance(); } From 00e4a906992d9cf09d88bc74e25f5496efd34b90 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 24 Aug 2025 00:14:50 +0000 Subject: [PATCH 14/75] 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 7f29e0419..66ae7804c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -112,8 +112,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz", - .hash = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz", + .hash = "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index a8fa38469..fa17d4599 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-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls": { + "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz", - "hash": "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz", + "hash": "sha256-LQIa9siNICX5zzajvrJNKBmgDqAlBDY7QEmHihs65d0=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 6450b8df9..e0453ccae 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -162,11 +162,11 @@ in }; } { - name = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls"; + name = "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz"; - hash = "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz"; + hash = "sha256-LQIa9siNICX5zzajvrJNKBmgDqAlBDY7QEmHihs65d0="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 29191f741..e4dad6f5a 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 2d48f9617..1f1f31e27 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz", - "dest": "vendor/p/N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls", - "sha256": "3f249617ff4800ae0364291de4546989a225e9eb76426eadf082824f387f5dad" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz", + "dest": "vendor/p/N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri", + "sha256": "2d021af6c88d2025f9cf36a3beb24d2819a00ea02504363b4049878a1b3ae5dd" }, { "type": "archive", From a18332828abca22f10c8e0f817941bfc4c808993 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 23 Aug 2025 21:25:19 -0500 Subject: [PATCH 15/75] nix: update zon2nix - Builds with Zig 0.15 now (but still works just fine with Zig 0.14 projects). - Fixes a double-free if nix-prefetch-git can't be found or errors out - Adds support for generating Flatpak package metadata natively. --- .github/workflows/test.yml | 30 +----- .github/workflows/update-colorschemes.yml | 2 - build.zig.zon.nix | 1 + flake.lock | 25 +++-- flake.nix | 7 +- flatpak/build-support/check-zig-cache.sh | 108 ---------------------- nix/build-support/check-zig-cache.sh | 39 +++++--- 7 files changed, 53 insertions(+), 159 deletions(-) delete mode 100755 flatpak/build-support/check-zig-cache.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8910d8c07..a5a2d1ef0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: - build-macos - build-macos-matrix - build-windows - - flatpak-check-zig-cache - test - test-gtk - test-gtk-ng @@ -954,33 +953,6 @@ jobs: build-args: | DISTRO_VERSION=13 - flatpak-check-zig-cache: - if: github.repository == 'ghostty-org/ghostty' - runs-on: namespace-profile-ghostty-xsm - env: - ZIG_LOCAL_CACHE_DIR: /zig/local-cache - ZIG_GLOBAL_CACHE_DIR: /zig/global-cache - steps: - - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 - with: - path: | - /nix - /zig - - name: Setup Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - useDaemon: false # sometimes fails on short jobs - - name: Check Flatpak Zig Dependencies - run: nix develop -c ./flatpak/build-support/check-zig-cache.sh - flatpak: if: github.repository == 'ghostty-org/ghostty' name: "Flatpak" @@ -996,7 +968,7 @@ jobs: - arch: aarch64 runner: namespace-profile-ghostty-md-arm64 runs-on: ${{ matrix.variant.runner }} - needs: [flatpak-check-zig-cache, test] + needs: test steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index e1ee92168..3848a7ad4 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -50,8 +50,6 @@ jobs: if ! git diff --exit-code build.zig.zon; then nix develop -c ./nix/build-support/check-zig-cache.sh --update nix develop -c ./nix/build-support/check-zig-cache.sh - nix develop -c ./flatpak/build-support/check-zig-cache.sh --update - nix develop -c ./flatpak/build-support/check-zig-cache.sh fi # Verify the build still works. We choose an arbitrary build type diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 6450b8df9..d3b41b58a 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -49,6 +49,7 @@ inherit name rev hash; url = url_without_query; deepClone = false; + fetchSubmodules = false; }; fetchZigArtifact = { diff --git a/flake.lock b/flake.lock index 0374b3e5a..ba1adb08a 100644 --- a/flake.lock +++ b/flake.lock @@ -47,6 +47,19 @@ "url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1755972213, + "narHash": "sha256-VYK7aDAv8H1enXn1ECRHmGbeY6RqLnNwUJkOwloIsko=", + "rev": "73e96df7cff5783f45e21342a75a1540c4eddce4", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable-small/nixos-25.11pre850642.73e96df7cff5/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable-small/nixexprs.tar.xz" + } + }, "root": { "inputs": { "flake-compat": "flake-compat", @@ -102,22 +115,20 @@ "flake-utils": [ "flake-utils" ], - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1742104771, - "narHash": "sha256-LhidlyEA9MP8jGe1rEnyjGFCzLLgCdDpYeWggibayr0=", + "lastModified": 1756000480, + "narHash": "sha256-fR5pdcjO0II5MNdCzqvyokyuFkmff7/FyBAjUS6sMfA=", "owner": "jcollie", "repo": "zon2nix", - "rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613", + "rev": "d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60", "type": "github" }, "original": { "owner": "jcollie", "repo": "zon2nix", - "rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613", + "rev": "d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60", "type": "github" } } diff --git a/flake.nix b/flake.nix index 7cf58b27c..99f7fcb7c 100644 --- a/flake.nix +++ b/flake.nix @@ -24,9 +24,12 @@ }; zon2nix = { - url = "github:jcollie/zon2nix?rev=56c159be489cc6c0e73c3930bd908ddc6fe89613"; + url = "github:jcollie/zon2nix?rev=d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60"; inputs = { - nixpkgs.follows = "nixpkgs"; + # Don't override nixpkgs until Zig 0.15 is available in the Nix branch + # we are using for "normal" builds. + # + # nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; }; }; diff --git a/flatpak/build-support/check-zig-cache.sh b/flatpak/build-support/check-zig-cache.sh deleted file mode 100755 index bea718640..000000000 --- a/flatpak/build-support/check-zig-cache.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash -# -# This script checks if the flatpak/zig-packages.json file is up-to-date. -# If the `--update` flag is passed, it will update all necessary -# files to be up to date. -# -# The files owned by this are: -# -# - flatpak/zig-packages.json -# -# All of these are auto-generated and should not be edited manually. - -# Nothing in this script should fail. -set -eu -set -o pipefail - -WORK_DIR=$(mktemp -d) - -if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then - echo "could not create temp dir" - exit 1 -fi - -function cleanup { - rm -rf "$WORK_DIR" -} - -trap cleanup EXIT - -help() { - echo "" - echo "To fix, please (manually) re-run the script from the repository root," - echo "commit, and submit a PR with the update:" - echo "" - echo " ./flatpak/build-support/check-zig-cache.sh --update" - echo " git add flatpak/zig-packages.json" - echo " git commit -m \"flatpak: update zig-packages.json\"" - echo "" -} - -# Turn Nix's base64 hashes into regular hexadecimal form -decode_hash() { - input=$1 - input=${input#sha256-} - echo "$input" | base64 -d | od -vAn -t x1 | tr -d ' \n' -} - -ROOT="$(realpath "$(dirname "$0")/../../")" -ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json" -BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json" - -if [ ! -f "${BUILD_ZIG_ZON_JSON}" ]; then - echo -e "\nERROR: build.zig.zon2json-lock missing." - help - exit 1 -fi - -if [ -f "${ZIG_PACKAGES_JSON}" ]; then - OLD_HASH=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}') -fi - -while read -r url sha256 dest; do - src_type=archive - sha256=$(decode_hash "$sha256") - git_commit= - if [[ "$url" =~ ^git\+* ]]; then - src_type=git - sha256= - url=${url#git+} - git_commit=${url##*#} - url=${url%%/\?ref*} - url=${url%%#*} - fi - - jq \ - -nec \ - --arg type "$src_type" \ - --arg url "$url" \ - --arg git_commit "$git_commit" \ - --arg dest "$dest" \ - --arg sha256 "$sha256" \ - '{ - type: $type, - url: $url, - commit: $git_commit, - dest: $dest, - sha256: $sha256, - } | with_entries(select(.value != ""))' -done < <(jq -rc 'to_entries[] | [.value.url, .value.hash, "vendor/p/\(.key)"] | @tsv' "$BUILD_ZIG_ZON_JSON") | - jq -s '.' >"$WORK_DIR/zig-packages.json" - -NEW_HASH=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}') - -if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then - echo -e "\nOK: flatpak/zig-packages.json unchanged." - exit 0 -elif [ "${1:-}" != "--update" ]; then - echo -e "\nERROR: flatpak/zig-packages.json needs to be updated." - echo "" - echo " * Old hash: ${OLD_HASH}" - echo " * New hash: ${NEW_HASH}" - help - exit 1 -else - mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON" - echo -e "\nOK: flatpak/zig-packages.json updated." - exit 0 -fi diff --git a/nix/build-support/check-zig-cache.sh b/nix/build-support/check-zig-cache.sh index 49997ac1a..33e57e790 100755 --- a/nix/build-support/check-zig-cache.sh +++ b/nix/build-support/check-zig-cache.sh @@ -9,6 +9,7 @@ # - build.zig.zon.nix # - build.zig.zon.txt # - build.zig.zon.json +# - flatpak/zig-packages.json # # All of these are auto-generated and should not be edited manually. @@ -34,8 +35,8 @@ help() { echo "commit, and submit a PR with the update:" echo "" echo " ./nix/build-support/check-zig-cache-hash.sh --update" - echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json" - echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json\"" + echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json" + echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json\"" echo "" } @@ -44,6 +45,7 @@ BUILD_ZIG_ZON="$ROOT/build.zig.zon" BUILD_ZIG_ZON_NIX="$ROOT/build.zig.zon.nix" BUILD_ZIG_ZON_TXT="$ROOT/build.zig.zon.txt" BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json" +ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json" if [ -f "${BUILD_ZIG_ZON_NIX}" ]; then OLD_HASH_NIX=$(sha512sum "${BUILD_ZIG_ZON_NIX}" | awk '{print $1}') @@ -69,27 +71,40 @@ elif [ "$1" != "--update" ]; then exit 1 fi -zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" +if [ -f "${ZIG_PACKAGES_JSON}" ]; then + OLD_HASH_FLATPAK=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}') +elif [ "$1" != "--update" ]; then + echo -e "\nERROR: flatpak/zig-packages.json missing." + help + exit 1 +fi + +zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json" alejandra --quiet "$WORK_DIR/build.zig.zon.nix" -prettier --write "$WORK_DIR/build.zig.zon.json" +prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json" +prettier --log-level warn --write "$WORK_DIR/zig-packages.json" NEW_HASH_NIX=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}') NEW_HASH_TXT=$(sha512sum "$WORK_DIR/build.zig.zon.txt" | awk '{print $1}') NEW_HASH_JSON=$(sha512sum "$WORK_DIR/build.zig.zon.json" | awk '{print $1}') +NEW_HASH_FLATPAK=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}') -if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ]; then +if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ] && [ "${OLD_HASH_FLATPAK}" == "${NEW_HASH_FLATPAK}" ]; then echo -e "\nOK: build.zig.zon.nix unchanged." echo -e "OK: build.zig.zon.txt unchanged." echo -e "OK: build.zig.zon.json unchanged." + echo -e "OK: flatpak/zig-packages.json unchanged." exit 0 elif [ "$1" != "--update" ]; then echo -e "\nERROR: build.zig.zon.nix, build.zig.zon.txt, or build.zig.zon.json needs to be updated.\n" - echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}" - echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}" - echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}" - echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}" - echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}" - echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}" + echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}" + echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}" + echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}" + echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}" + echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}" + echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}" + echo " * Old flatpak/zig-packages.json hash: ${OLD_HASH_FLATPAK}" + echo " * New flatpak/zig-packages.json hash: ${NEW_HASH_FLATPAK}" help exit 1 else @@ -99,6 +114,8 @@ else echo -e "OK: build.zig.zon.txt updated." mv "$WORK_DIR/build.zig.zon.json" "$BUILD_ZIG_ZON_JSON" echo -e "OK: build.zig.zon.json updated." + mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON" + echo -e "OK: flatpak/zig-packages.json updated." exit 0 fi From 95bc181c986e91ea14db18eb9f300984e1f6ebdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:10:21 +0200 Subject: [PATCH 16/75] Add hu_HU for Hungarian locale (#7560) ## Description of changes Added Hungarian locale files, and corresponding translation For the translation I mainly relied on my native skills, double checked my work using LLMs. Copilot generated summary: This pull request introduces Hungarian language support to the application by adding translations and updating the locale configurations. The most important changes include the addition of Hungarian translations in the `.po` file and registering the new locale in the application's supported locales. ### Hungarian Language Support: * Added Hungarian translations for various UI elements and messages in the `po/hu_HU.UTF-8.po` file. This includes translations for prompts, dialogs, menus, and other interface components. * Updated the supported locales list in `src/os/i18n.zig` to include `hu_HU.UTF-8`, enabling Hungarian as an available language option. ## Picture(s) of the translation ![image](https://github.com/user-attachments/assets/60f47f11-d55e-4408-889b-5b44ecaffc23) --- CODEOWNERS | 1 + po/hu_HU.UTF-8.po | 320 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 3 files changed, 322 insertions(+) create mode 100644 po/hu_HU.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 0fb60758e..770c08860 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ /po/es_BO.UTF-8.po @ghostty-org/es_BO /po/es_AR.UTF-8.po @ghostty-org/es_AR /po/fr_FR.UTF-8.po @ghostty-org/fr_FR +/po/hu_HU.UTF-8.po @ghostty-org/hu_HU /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP /po/mk_MK.UTF-8.po @ghostty-org/mk_MK diff --git a/po/hu_HU.UTF-8.po b/po/hu_HU.UTF-8.po new file mode 100644 index 000000000..2ad48eadb --- /dev/null +++ b/po/hu_HU.UTF-8.po @@ -0,0 +1,320 @@ +# Hungarian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Balázs Szücs , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-07-22 17:18+0000\n" +"PO-Revision-Date: 2025-08-23 17:14+0200\n" +"Last-Translator: Balázs Szücs \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Terminál címének módosítása" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Hagyja üresen az alapértelmezett cím visszaállításához." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Mégse" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Rendben" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Konfigurációs hibák" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Egy vagy több konfigurációs hiba található. Kérjük, ellenőrizze az alábbi " +"hibákat, és frissítse a konfigurációt, vagy hagyja figyelmen kívül ezeket a " +"hibákat." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Figyelmen kívül hagyás" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "Konfiguráció frissítése" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Felosztás felfelé" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Felosztás lefelé" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Felosztás balra" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Felosztás jobbra" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Parancs végrehajtása…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Másolás" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "Beillesztés" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Törlés" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Visszaállítás" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Felosztás" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Cím módosítása…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Fül" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:265 +msgid "New Tab" +msgstr "Új fül" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Fül bezárása" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Ablak" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Új ablak" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Ablak bezárása" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Konfiguráció" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Konfiguráció megnyitása" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Parancspaletta" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Terminálvizsgáló" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 +msgid "About Ghostty" +msgstr "A Ghostty névjegye" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Kilépés" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Vágólap-hozzáférés engedélyezése" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Egy alkalmazás megpróbál olvasni a vágólapról. A vágólap jelenlegi tartalma " +"lent látható." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Elutasítás" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Engedélyezés" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "Választás megjegyzése erre a felosztásra" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "Konfiguráció frissítése a kérdés újbóli megjelenítéséhez" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Egy alkalmazás megpróbál írni a vágólapra. A vágólap jelenlegi tartalma lent " +"látható." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Figyelem: potenciálisan veszélyes beillesztés" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Ennek a szövegnek a terminálba való beillesztése veszélyes lehet, mivel " +"néhány parancs végrehajtásra kerülhet." + +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 +msgid "Close" +msgstr "Bezárás" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Kilép a Ghostty-ból?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Ablak bezárása?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Fül bezárása?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Felosztás bezárása?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Minden terminál munkamenet lezárul." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Ebben az ablakban minden terminál munkamenet lezárul." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Ezen a fülön minden terminál munkamenet lezárul." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Ebben a felosztásban a jelenleg futó folyamat lezárul." + +#: src/apprt/gtk/Surface.zig:1266 +msgid "Copied to clipboard" +msgstr "Vágólapra másolva" + +#: src/apprt/gtk/Surface.zig:1268 +msgid "Cleared clipboard" +msgstr "Vágólap törölve" + +#: src/apprt/gtk/Surface.zig:2525 +msgid "Command succeeded" +msgstr "Parancs sikeres" + +#: src/apprt/gtk/Surface.zig:2527 +msgid "Command failed" +msgstr "Parancs sikertelen" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Főmenü" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Megnyitott fülek megtekintése" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Új felosztás" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ A Ghostty hibakereső verzióját futtatja! A teljesítmény csökkenni fog." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Konfiguráció frissítve" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty fejlesztők" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Terminálvizsgáló" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 29f7f6bc3..d39976811 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{ "ca_ES.UTF-8", "bg_BG.UTF-8", "ga_IE.UTF-8", + "hu_HU.UTF-8", "he_IL.UTF-8", }; From 42b1ff70d1d51f06286a2cd436e5f221797c83a9 Mon Sep 17 00:00:00 2001 From: MiguelElGallo <60221874+MiguelElGallo@users.noreply.github.com> Date: Sun, 24 Aug 2025 15:52:13 +0300 Subject: [PATCH 17/75] fix: update Spanish translations and revision date in es_BO.UTF-8.po --- po/es_BO.UTF-8.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index a1b8f75cd..0ca45545c 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -8,7 +8,7 @@ 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-03-28 17:46+0200\n" +"PO-Revision-Date: 2025-08-23 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" "Language: es_BO\n" @@ -87,7 +87,7 @@ msgstr "Dividir a la derecha" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Ejecutar comando..." #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +160,7 @@ msgstr "Abrir configuración" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Paleta de comandos" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +208,12 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Recordar su elección para esta división" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recargar configuración para mostrar este aviso nuevamente" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +278,15 @@ msgstr "Copiado al portapapeles" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "El portapapeles está limpio" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comando ejecutado con éxito" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Comando fallido" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -298,7 +298,7 @@ msgstr "Ver pestañas abiertas" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Nueva división" #: src/apprt/gtk/Window.zig:329 msgid "" From c57a84a6def97dcef1a24ba73fb6aa7fbe53c83c Mon Sep 17 00:00:00 2001 From: MiguelElGallo <60221874+MiguelElGallo@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:26:41 +0300 Subject: [PATCH 18/75] fix: update Spanish translations for window split terminology --- po/es_BO.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index 0ca45545c..50fcb14e2 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -208,7 +208,7 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "Recordar su elección para esta división" +msgstr "Recordar su elección para esta división de ventana" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 @@ -298,7 +298,7 @@ msgstr "Ver pestañas abiertas" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "Nueva división" +msgstr "Nueva ventana dividida" #: src/apprt/gtk/Window.zig:329 msgid "" From c26323d69708dc3d2dcad959b5a38b307ad26196 Mon Sep 17 00:00:00 2001 From: jamylak Date: Sat, 23 Aug 2025 14:41:01 +1000 Subject: [PATCH 19/75] Close other tabs feature on Mac. Supporting command line, file menu and keybindings. Default mac shortcut of `super + alt + o` (other) Not able to test on Linux so excluding `close_other_tabs` from `gtk` for now make a default short cut for close other tabs --- include/ghostty.h | 1 + macos/Sources/App/macOS/MainMenu.xib | 4 +- .../Terminal/TerminalController.swift | 109 ++++++++++++++++-- macos/Sources/Ghostty/Ghostty.App.swift | 26 ++++- macos/Sources/Ghostty/Package.swift | 3 + src/Surface.zig | 6 + src/apprt/action.zig | 5 + src/apprt/gtk-ng/class/application.zig | 1 + src/apprt/gtk-ng/class/command_palette.zig | 1 + src/apprt/gtk/App.zig | 1 + src/apprt/gtk/CommandPalette.zig | 1 + src/input/Binding.zig | 7 ++ src/input/command.zig | 6 + 13 files changed, 160 insertions(+), 11 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 082711836..37c844472 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -709,6 +709,7 @@ typedef enum { GHOSTTY_ACTION_NEW_WINDOW, GHOSTTY_ACTION_NEW_TAB, GHOSTTY_ACTION_CLOSE_TAB, + GHOSTTY_ACTION_CLOSE_OTHER_TABS, GHOSTTY_ACTION_NEW_SPLIT, GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, GHOSTTY_ACTION_TOGGLE_MAXIMIZE, diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 5cd6d9bec..c97ed7c61 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 644a0c8ac..414f38d81 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -95,6 +95,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr selector: #selector(onCloseTab), name: .ghosttyCloseTab, object: nil) + center.addObserver( + self, + selector: #selector(onCloseOtherTabs), + name: .ghosttyCloseOtherTabs, + object: nil) center.addObserver( self, selector: #selector(onResetWindowSize), @@ -559,7 +564,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeWindow(nil) } - private func closeTabImmediately() { + private func closeTabImmediately(registerRedo: Bool = true) { guard let window = window else { return } guard let tabGroup = window.tabGroup, tabGroup.windows.count > 1 else { @@ -576,19 +581,69 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr expiresAfter: undoExpiration ) { ghostty in let newController = TerminalController(ghostty, with: undoState) - - // Register redo action - undoManager.registerUndo( - withTarget: newController, - expiresAfter: newController.undoExpiration - ) { target in - target.closeTabImmediately() + + if registerRedo { + undoManager.registerUndo( + withTarget: newController, + expiresAfter: newController.undoExpiration + ) { target in + target.closeTabImmediately() + } } } } window.close() } + + private func closeOtherTabsImmediately() { + guard let window = window else { return } + guard let tabGroup = window.tabGroup else { return } + guard tabGroup.windows.count > 1 else { return } + + // Start an undo grouping + if let undoManager { + undoManager.beginUndoGrouping() + } + defer { + undoManager?.endUndoGrouping() + } + + // Iterate through all tabs except the current one. + for window in tabGroup.windows where window != self.window { + // We ignore any non-terminal tabs. They don't currently exist and we can't + // properly undo them anyways so I'd rather ignore them and get a bug report + // later if and when we introduce non-terminal tabs. + if let controller = window.windowController as? TerminalController { + // We must not register a redo, because it messes with our own redo + // that we register later. + controller.closeTabImmediately(registerRedo: false) + } + } + + if let undoManager { + undoManager.setActionName("Close Other Tabs") + + // We need to register an undo that refocuses this window. Otherwise, the + // undo operation above for each tab will steal focus. + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration + ) { target in + DispatchQueue.main.async { + target.window?.makeKeyAndOrderFront(nil) + } + + // Register redo action + undoManager.registerUndo( + withTarget: target, + expiresAfter: target.undoExpiration + ) { target in + target.closeOtherTabsImmediately() + } + } + } + } /// Closes the current window (including any other tabs) immediately and without /// confirmation. This will setup proper undo state so the action can be undone. @@ -1023,6 +1078,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } + @IBAction func closeOtherTabs(_ sender: Any?) { + guard let window = window else { return } + guard let tabGroup = window.tabGroup else { return } + + // If we only have one window then we have no other tabs to close + guard tabGroup.windows.count > 1 else { return } + + // Check if we have to confirm close. + guard tabGroup.windows.contains(where: { window in + // Ignore ourself + if window == self.window { return false } + + // Ignore non-terminals + guard let controller = window.windowController as? TerminalController else { + return false + } + + // Check if any surfaces require confirmation + return controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) + }) else { + self.closeOtherTabsImmediately() + return + } + + confirmClose( + messageText: "Close Other Tabs?", + informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed." + ) { + self.closeOtherTabsImmediately() + } + } + @IBAction func returnToDefaultSize(_ sender: Any?) { guard let defaultSize else { return } window?.setFrame(defaultSize, display: true) @@ -1206,6 +1293,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeTab(self) } + @objc private func onCloseOtherTabs(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree.contains(target) else { return } + closeOtherTabs(self) + } + @objc private func onCloseWindow(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard surfaceTree.contains(target) else { return } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index c94f40291..e64ed30ee 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -457,6 +457,9 @@ extension Ghostty { case GHOSTTY_ACTION_CLOSE_TAB: closeTab(app, target: target) + case GHOSTTY_ACTION_CLOSE_OTHER_TABS: + closeOtherTabs(app, target: target) + case GHOSTTY_ACTION_CLOSE_WINDOW: closeWindow(app, target: target) @@ -781,7 +784,7 @@ extension Ghostty { private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) { switch (target.tag) { case GHOSTTY_TARGET_APP: - Ghostty.logger.warning("close tab does nothing with an app target") + Ghostty.logger.warning("close tabs does nothing with an app target") return case GHOSTTY_TARGET_SURFACE: @@ -799,6 +802,27 @@ extension Ghostty { } } + private static func closeOtherTabs(_ app: ghostty_app_t, target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("close other tabs 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 } + + NotificationCenter.default.post( + name: .ghosttyCloseOtherTabs, + object: surfaceView + ) + + + default: + assertionFailure() + } + } + private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) { switch (target.tag) { case GHOSTTY_TARGET_APP: diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 73487f1bd..85040d390 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -329,6 +329,9 @@ extension Notification.Name { /// Close tab static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") + /// Close other tabs + static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs") + /// Close window static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow") diff --git a/src/Surface.zig b/src/Surface.zig index 770c2daef..686d214cd 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4840,6 +4840,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .close_other_tabs => return try self.rt_app.performAction( + .{ .surface = self }, + .close_other_tabs, + {}, + ), + .select_all => { const sel = self.io.terminal.screen.selectAll(); if (sel) |s| { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index d2d444c3a..1a7a2a345 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -86,6 +86,10 @@ pub const Action = union(Key) { /// Closes the tab belonging to the currently focused split. close_tab, + /// Closes all tabs in the current window other than the currently + /// focused tab. + close_other_tabs, + /// Create a new split. The value determines the location of the split /// relative to the target. new_split: SplitDirection, @@ -300,6 +304,7 @@ pub const Action = union(Key) { new_window, new_tab, close_tab, + close_other_tabs, new_split, close_all_windows, toggle_maximize, diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 29a124798..8cf61c4ba 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -625,6 +625,7 @@ pub const Application = extern struct { // Unimplemented .secure_input, .close_all_windows, + .close_other_tabs, .float_window, .toggle_visibility, .cell_size, diff --git a/src/apprt/gtk-ng/class/command_palette.zig b/src/apprt/gtk-ng/class/command_palette.zig index 8b7bb328c..c2a4ec215 100644 --- a/src/apprt/gtk-ng/class/command_palette.zig +++ b/src/apprt/gtk-ng/class/command_palette.zig @@ -156,6 +156,7 @@ pub const CommandPalette = extern struct { // for GTK. switch (command.action) { .close_all_windows, + .close_other_tabs, .toggle_secure_input, .check_for_updates, .redo, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 0f75a2d97..70c03e098 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -528,6 +528,7 @@ pub fn performAction( // Unimplemented .close_all_windows, + .close_other_tabs, .float_window, .toggle_visibility, .cell_size, diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index 076459dbd..e0ff8c177 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -108,6 +108,7 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi // or don't make sense for GTK switch (command.action) { .close_all_windows, + .close_other_tabs, .toggle_secure_input, .check_for_updates, .redo, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2ed6c2636..6db0decc2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -558,6 +558,12 @@ pub const Action = union(enum) { /// of the `confirm-close-surface` configuration setting. close_tab, + /// Close all tabs other than the currently focused one within the same + /// window. + /// + /// Only available on macOS currently. + close_other_tabs, + /// Close the current window and all tabs and splits therein. /// /// This might trigger a close confirmation popup, depending on the value @@ -1052,6 +1058,7 @@ pub const Action = union(enum) { .write_selection_file, .close_surface, .close_tab, + .close_other_tabs, .close_window, .toggle_maximize, .toggle_fullscreen, diff --git a/src/input/command.zig b/src/input/command.zig index 615ffb713..68652cce3 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -375,6 +375,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Show the on-screen keyboard if present.", }}, + .close_other_tabs => comptime &.{.{ + .action = .close_other_tabs, + .title = "Close Other Tabs", + .description = "Close all tabs in this window except the current one.", + }}, + .open_config => comptime &.{.{ .action = .open_config, .title = "Open Config", From c9199f2ba27b0cc29cef23d93bcedf54ce6fef47 Mon Sep 17 00:00:00 2001 From: KristoferSoler <31729650+KristoferSoler@users.noreply.github.com> Date: Sun, 24 Aug 2025 10:11:57 +0200 Subject: [PATCH 20/75] i18n: Add missing ca_ES translations Part of #8344 --- po/ca_ES.UTF-8.po | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index c2c969a66..a776284ac 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Francesc Arpi , 2025. +# Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>, 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-20 08:07+0100\n" -"Last-Translator: Francesc Arpi \n" +"PO-Revision-Date: 2025-08-24 19:22+0200\n" +"Last-Translator: Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>\n" "Language-Team: \n" "Language: ca\n" "MIME-Version: 1.0\n" @@ -87,7 +88,7 @@ msgstr "Divideix a la dreta" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Executa una ordre…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +161,7 @@ msgstr "Obre la configuració" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Paleta de comandes" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +209,12 @@ msgstr "Permet" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Recorda l’opció per a aquest panell dividit" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recarrega la configuració per tornar a mostrar aquest missatge" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +279,15 @@ msgstr "Copiat al porta-retalls" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Porta-retalls netejat" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comanda completada amb èxit" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Comanda fallida" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -298,7 +299,7 @@ msgstr "Mostra les pestanyes obertes" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Nova divisió" #: src/apprt/gtk/Window.zig:329 msgid "" From 5c03ff8165390029297b3809ff0d1b68a5842197 Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:58:10 +0200 Subject: [PATCH 21/75] i18n: update Ukrainian translation --- po/uk_UA.UTF-8.po | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index c5fe465b1..e0f735678 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Danylo Zalizchuk , 2025. +# Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>, 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-16 20:16+0200\n" -"Last-Translator: Danylo Zalizchuk \n" +"PO-Revision-Date: 2025-08-24 22:57+0100\n" +"Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n" "Language-Team: Ukrainian \n" "Language: uk\n" "MIME-Version: 1.0\n" @@ -20,17 +21,17 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Змінити назву терміналу" +msgstr "Змінити заголовок терміналу" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." -msgstr "Залиште порожнім, щоб відновити назву за замовчуванням." +msgstr "Залиште порожнім, щоб відновити заголовок за замовчуванням." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 #: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" -msgstr "Відмінити" +msgstr "Скасувати" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 msgid "OK" @@ -47,7 +48,7 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Виявлено одну або декілька помилок у конфігурації. Будь ласка, перегляньте " +"Виявлено одну або декілька помилок конфігурації. Будь ласка, перегляньте " "наведені нижче помилки і або перезавантажте конфігурацію, або проігноруйте " "ці помилки." @@ -67,13 +68,13 @@ msgstr "Перезавантажити конфігурацію" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 msgid "Split Up" -msgstr "Розділити панель вгору" +msgstr "Розділити панель догори" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 msgid "Split Down" -msgstr "Розділити панель вниз" +msgstr "Розділити панель донизу" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 @@ -89,7 +90,7 @@ msgstr "Розділити панель праворуч" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Виконати команду…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -162,7 +163,7 @@ msgstr "Відкрити конфігурацію" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Палітра команд" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -198,7 +199,7 @@ msgstr "" #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" -msgstr "Відхилити" +msgstr "Заборонити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 @@ -210,12 +211,12 @@ msgstr "Дозволити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Запамʼятати вибір для цієї розділеної панелі" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Перезавантажте конфігурацію, щоб показати це повідомлення знову" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -272,24 +273,23 @@ msgstr "Всі сесії терміналу в цій вкладці будут #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "" -"Поточний процес, що виконується в цій розділеній панелі, буде завершено." +msgstr "Процес, що виконується в цій розділеній панелі, буде завершено." #: src/apprt/gtk/Surface.zig:1266 msgid "Copied to clipboard" -msgstr "Скопійовано в буфер обміну" +msgstr "Скопійовано до буферa обміну" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Очищено буфер обміну" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Команда завершилась успішно" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Команда завершилась з помилкою" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -301,7 +301,7 @@ msgstr "Переглянути відкриті вкладки" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Нова розділена панель" #: src/apprt/gtk/Window.zig:329 msgid "" From 8a14f213252fca606e2ceaef4d5598fb40c91db2 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 24 Aug 2025 12:00:20 -0500 Subject: [PATCH 22/75] gtk-ng: fix setting/unsetting of urgency - Don't set urgency on windows that are the topmost window. - Turn off urgency on windows that become the topmost window. Fixes #8373 --- src/apprt/gtk-ng/class/window.zig | 24 ++++++++++++++++++++++-- src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 30fa43dae..117fac540 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -987,6 +987,22 @@ pub const Window = extern struct { }; } + fn propIsActive( + _: *gtk.Window, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + // Don't change urgency if we're not the active window. + if (self.as(gtk.Window).isActive() == 0) return; + + self.winproto().setUrgent(false) catch |err| { + log.warn( + "winproto failed to reset urgency={}", + .{err}, + ); + }; + } + fn propGdkSurfaceWidth( _: *gdk.Surface, _: *gobject.ParamSpec, @@ -1765,10 +1781,13 @@ pub const Window = extern struct { native.beep(); } - if (config.@"bell-features".attention) { + if (config.@"bell-features".attention) attention: { + // Dont set urgency if the window is already active. + if (self.as(gtk.Window).isActive() != 0) break :attention; + // Request user attention self.winproto().setUrgent(true) catch |err| { - log.warn("failed to request user attention={}", .{err}); + log.warn("winproto failed to set urgency={}", .{err}); }; } } @@ -1912,6 +1931,7 @@ pub const Window = extern struct { class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_fullscreened", &propFullscreened); + class.bindTemplateCallback("notify_is_active", &propIsActive); class.bindTemplateCallback("notify_maximized", &propMaximized); class.bindTemplateCallback("notify_menu_active", &propMenuActive); class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index b09c0d9b3..a63da6792 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -10,6 +10,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { realize => $realize(); notify::config => $notify_config(); notify::fullscreened => $notify_fullscreened(); + notify::is-active => $notify_is_active(); notify::maximized => $notify_maximized(); notify::quick-terminal => $notify_quick_terminal(); notify::scale-factor => $notify_scale_factor(); From 52a25e9c696a85c36d2e9bbf65e467e6deb8b28b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 25 Aug 2025 11:00:26 -0500 Subject: [PATCH 23/75] parameterize close_tab - Add mode (`this`/`other`) parameter to `close_tab` keybind/apprt action. - Keybinds will default to `this` if not specified, eliminating backward compatibility issues (`keybind=x=close_tab` === `keybind=x=close_tab:this`). - Remove `close_other_tabs` keybind and apprt action. --- include/ghostty.h | 8 +++- macos/Sources/Ghostty/Ghostty.App.swift | 46 +++++++++------------- src/Surface.zig | 13 +++--- src/apprt/action.zig | 18 +++++---- src/apprt/gtk-ng/class/application.zig | 14 ++++--- src/apprt/gtk-ng/class/command_palette.zig | 1 - src/apprt/gtk-ng/class/tab.zig | 39 ++++++++++++++++-- src/apprt/gtk-ng/class/window.zig | 30 ++++++++++++-- src/apprt/gtk-ng/ui/1.2/surface.blp | 3 +- src/apprt/gtk-ng/ui/1.5/window.blp | 1 + src/apprt/gtk/App.zig | 19 ++++++--- src/apprt/gtk/CommandPalette.zig | 1 - src/apprt/gtk/Window.zig | 2 +- src/config/Config.zig | 4 +- src/input/Binding.zig | 24 ++++++----- src/input/command.zig | 32 +++++++++------ 16 files changed, 168 insertions(+), 87 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 37c844472..88da70b8b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -680,6 +680,12 @@ typedef struct { uintptr_t len; } ghostty_action_open_url_s; +// apprt.action.CloseTabMode +typedef enum { + GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, + GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, +} ghostty_action_close_tab_mode_e; + // apprt.surface.Message.ChildExited typedef struct { uint32_t exit_code; @@ -709,7 +715,6 @@ typedef enum { GHOSTTY_ACTION_NEW_WINDOW, GHOSTTY_ACTION_NEW_TAB, GHOSTTY_ACTION_CLOSE_TAB, - GHOSTTY_ACTION_CLOSE_OTHER_TABS, GHOSTTY_ACTION_NEW_SPLIT, GHOSTTY_ACTION_CLOSE_ALL_WINDOWS, GHOSTTY_ACTION_TOGGLE_MAXIMIZE, @@ -787,6 +792,7 @@ typedef union { ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; ghostty_action_open_url_s open_url; + ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; ghostty_terminal_osc_command_progressreport_s progress_report; } ghostty_action_u; diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e64ed30ee..eaccb43cf 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -455,10 +455,7 @@ extension Ghostty { newSplit(app, target: target, direction: action.action.new_split) case GHOSTTY_ACTION_CLOSE_TAB: - closeTab(app, target: target) - - case GHOSTTY_ACTION_CLOSE_OTHER_TABS: - closeOtherTabs(app, target: target) + closeTab(app, target: target, mode: action.action.close_tab_mode) case GHOSTTY_ACTION_CLOSE_WINDOW: closeWindow(app, target: target) @@ -781,7 +778,7 @@ extension Ghostty { } } - private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) { + private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { switch (target.tag) { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close tabs does nothing with an app target") @@ -791,31 +788,24 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - NotificationCenter.default.post( - name: .ghosttyCloseTab, - object: surfaceView - ) + switch (mode) { + case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS: + NotificationCenter.default.post( + name: .ghosttyCloseTab, + object: surfaceView + ) + return + case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER: + NotificationCenter.default.post( + name: .ghosttyCloseOtherTabs, + object: surfaceView + ) + return - default: - assertionFailure() - } - } - - private static func closeOtherTabs(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { - case GHOSTTY_TARGET_APP: - Ghostty.logger.warning("close other tabs 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 } - - NotificationCenter.default.post( - name: .ghosttyCloseOtherTabs, - object: surfaceView - ) + default: + assertionFailure() + } default: diff --git a/src/Surface.zig b/src/Surface.zig index 686d214cd..e72322e31 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4701,10 +4701,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), - .close_tab => return try self.rt_app.performAction( + .close_tab => |v| return try self.rt_app.performAction( .{ .surface = self }, .close_tab, - {}, + switch (v) { + .this => .this, + .other => .other, + }, ), inline .previous_tab, @@ -4840,12 +4843,6 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), - .close_other_tabs => return try self.rt_app.performAction( - .{ .surface = self }, - .close_other_tabs, - {}, - ), - .select_all => { const sel = self.io.terminal.screen.selectAll(); if (sel) |s| { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1a7a2a345..a41a4627f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -83,12 +83,9 @@ pub const Action = union(Key) { /// the tab should be opened in a new window. new_tab, - /// Closes the tab belonging to the currently focused split. - close_tab, - - /// Closes all tabs in the current window other than the currently - /// focused tab. - close_other_tabs, + /// Closes the tab belonging to the currently focused split, or all other + /// tabs, depending on the mode. + close_tab: CloseTabMode, /// Create a new split. The value determines the location of the split /// relative to the target. @@ -304,7 +301,6 @@ pub const Action = union(Key) { new_window, new_tab, close_tab, - close_other_tabs, new_split, close_all_windows, toggle_maximize, @@ -706,3 +702,11 @@ pub const OpenUrl = struct { }; } }; + +/// sync with ghostty_action_close_tab_mode_e in ghostty.h +pub const CloseTabMode = enum(c_int) { + /// Close the current tab. + this, + /// Close all other tabs. + other, +}; diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 8cf61c4ba..984eda15e 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -542,7 +542,7 @@ pub const Application = extern struct { value: apprt.Action.Value(action), ) !bool { switch (action) { - .close_tab => return Action.closeTab(target), + .close_tab => return Action.closeTab(target, value), .close_window => return Action.closeWindow(target), .config_change => try Action.configChange( @@ -625,7 +625,6 @@ pub const Application = extern struct { // Unimplemented .secure_input, .close_all_windows, - .close_other_tabs, .float_window, .toggle_visibility, .cell_size, @@ -873,7 +872,8 @@ pub const Application = extern struct { self.syncActionAccelerator("win.close", .{ .close_window = {} }); self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); - self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this }); + self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this }); self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); @@ -1577,12 +1577,16 @@ pub const Application = extern struct { /// All apprt action handlers const Action = struct { - pub fn closeTab(target: apprt.Target) bool { + pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool { switch (target) { .app => return false, .surface => |core| { const surface = core.rt_surface.surface; - return surface.as(gtk.Widget).activateAction("tab.close", null) != 0; + return surface.as(gtk.Widget).activateAction( + "tab.close", + glib.ext.VariantType.stringFor([:0]const u8), + @as([*:0]const u8, @tagName(value)), + ) != 0; }, } } diff --git a/src/apprt/gtk-ng/class/command_palette.zig b/src/apprt/gtk-ng/class/command_palette.zig index c2a4ec215..8b7bb328c 100644 --- a/src/apprt/gtk-ng/class/command_palette.zig +++ b/src/apprt/gtk-ng/class/command_palette.zig @@ -156,7 +156,6 @@ pub const CommandPalette = extern struct { // for GTK. switch (command.action) { .close_all_windows, - .close_other_tabs, .toggle_secure_input, .check_for_updates, .redo, diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 5f1cf50de..0899d06ee 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -199,8 +199,11 @@ pub const Tab = extern struct { } fn initActionMap(self: *Self) void { + const s_param_type = glib.ext.VariantType.newFor([:0]const u8); + defer s_param_type.free(); + const actions = [_]ext.actions.Action(Self){ - .init("close", actionClose, null), + .init("close", actionClose, s_param_type), .init("ring-bell", actionRingBell, null), }; @@ -314,18 +317,48 @@ pub const Tab = extern struct { fn actionClose( _: *gio.SimpleAction, - _: ?*glib.Variant, + param_: ?*glib.Variant, self: *Self, ) callconv(.c) void { + const param = param_ orelse { + log.warn("tab.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + const tab_view = ext.getAncestor( adw.TabView, self.as(gtk.Widget), ) orelse return; + const page = tab_view.getPage(self.as(gtk.Widget)); + const mode = std.meta.stringToEnum( + apprt.action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to tab.close-tab", .{}); + return; + }, + ), + ) orelse { + // Need to be defensive here since actions can be triggered externally. + log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?}); + return; + }; + // Delegate to our parent to handle this, since this will emit // a close-page signal that the parent can intercept. - tab_view.closePage(page); + switch (mode) { + .this => { + tab_view.closePage(page); + }, + .other => { + log.warn("close-tab:other is not implemented", .{}); + }, + } } fn actionRingBell( diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 117fac540..862455fc8 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -320,10 +320,13 @@ pub const Window = extern struct { /// Setup our action map. fn initActionMap(self: *Self) void { + const s_variant_type = glib.ext.VariantType.newFor([:0]const u8); + defer s_variant_type.free(); + const actions = [_]ext.actions.Action(Self){ .init("about", actionAbout, null), .init("close", actionClose, null), - .init("close-tab", actionCloseTab, null), + .init("close-tab", actionCloseTab, s_variant_type), .init("new-tab", actionNewTab, null), .init("new-window", actionNewWindow, null), .init("ring-bell", actionRingBell, null), @@ -1679,10 +1682,31 @@ pub const Window = extern struct { fn actionCloseTab( _: *gio.SimpleAction, - _: ?*glib.Variant, + param_: ?*glib.Variant, self: *Window, ) callconv(.c) void { - self.performBindingAction(.close_tab); + const param = param_ orelse { + log.warn("win.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + + const mode = std.meta.stringToEnum( + input.Binding.Action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to win.close-tab", .{}); + return; + }, + ), + ) orelse { + log.warn("invalid mode provided to win.close-tab: {s}", .{str.?}); + return; + }; + + self.performBindingAction(.{ .close_tab = mode }); } fn actionNewWindow( diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 6c027e735..9989e9c10 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -228,7 +228,8 @@ menu context_menu_model { item { label: _("Close Tab"); - action: "win.close-tab"; + action: "tab.close"; + target: "this"; } } diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index a63da6792..8c0a7bedb 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -226,6 +226,7 @@ menu main_menu { item { label: _("Close Tab"); action: "win.close-tab"; + target: "this"; } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 70c03e098..ee5f3eb96 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -491,7 +491,7 @@ pub fn performAction( .toggle_maximize => self.toggleMaximize(target), .toggle_fullscreen => self.toggleFullscreen(target, value), .new_tab => try self.newTab(target), - .close_tab => return try self.closeTab(target), + .close_tab => return try self.closeTab(target, value), .goto_tab => return self.gotoTab(target, value), .move_tab => self.moveTab(target, value), .new_split => try self.newSplit(target, value), @@ -528,7 +528,6 @@ pub fn performAction( // Unimplemented .close_all_windows, - .close_other_tabs, .float_window, .toggle_visibility, .cell_size, @@ -586,7 +585,7 @@ fn newTab(_: *App, target: apprt.Target) !void { } } -fn closeTab(_: *App, target: apprt.Target) !bool { +fn closeTab(_: *App, target: apprt.Target, value: apprt.Action.Value(.close_tab)) !bool { switch (target) { .app => return false, .surface => |v| { @@ -598,8 +597,16 @@ fn closeTab(_: *App, target: apprt.Target) !bool { return false; }; - tab.closeWithConfirmation(); - return true; + switch (value) { + .this => { + tab.closeWithConfirmation(); + return true; + }, + .other => { + log.warn("close-tab:other is not implemented", .{}); + return false; + }, + } }, } } @@ -1146,7 +1153,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("win.close", .{ .close_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); - try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + try self.syncActionAccelerator("win.close-tab", .{ .close_tab = .this }); try self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); try self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); try self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index e0ff8c177..076459dbd 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -108,7 +108,6 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi // or don't make sense for GTK switch (command.action) { .close_all_windows, - .close_other_tabs, .toggle_secure_input, .check_for_updates, .redo, diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d3408e867..8c02396a6 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -1076,7 +1076,7 @@ fn gtkActionCloseTab( _: ?*glib.Variant, self: *Window, ) callconv(.c) void { - self.performBindingAction(.{ .close_tab = {} }); + self.performBindingAction(.{ .close_tab = .this }); } fn gtkActionSplitRight( diff --git a/src/config/Config.zig b/src/config/Config.zig index 637cfee49..b82824a93 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5598,7 +5598,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, - .{ .close_tab = {} }, + .{ .close_tab = .this }, ); try self.set.putFlags( alloc, @@ -5904,7 +5904,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } }, - .{ .close_tab = {} }, + .{ .close_tab = .this }, ); try self.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6db0decc2..6d1050859 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -552,17 +552,17 @@ pub const Action = union(enum) { /// of the `confirm-close-surface` configuration setting. close_surface, - /// Close the current tab and all splits therein. + /// Close the current tab and all splits therein _or_ close all tabs and + /// splits thein of tabs _other_ than the current tab, depending on the + /// mode. + /// + /// If the mode is not specified, defaults to closing the current tab. + /// + /// close-tab:other is only available on macOS. /// /// This might trigger a close confirmation popup, depending on the value /// of the `confirm-close-surface` configuration setting. - close_tab, - - /// Close all tabs other than the currently focused one within the same - /// window. - /// - /// Only available on macOS currently. - close_other_tabs, + close_tab: CloseTabMode, /// Close the current window and all tabs and splits therein. /// @@ -864,6 +864,13 @@ pub const Action = union(enum) { hide, }; + pub const CloseTabMode = enum { + this, + other, + + pub const default: CloseTabMode = .this; + }; + fn parseEnum(comptime T: type, value: []const u8) !T { return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat; } @@ -1058,7 +1065,6 @@ pub const Action = union(enum) { .write_selection_file, .close_surface, .close_tab, - .close_other_tabs, .close_window, .toggle_maximize, .toggle_fullscreen, diff --git a/src/input/command.zig b/src/input/command.zig index 68652cce3..f6c29040a 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -375,12 +375,6 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Show the on-screen keyboard if present.", }}, - .close_other_tabs => comptime &.{.{ - .action = .close_other_tabs, - .title = "Close Other Tabs", - .description = "Close all tabs in this window except the current one.", - }}, - .open_config => comptime &.{.{ .action = .open_config, .title = "Open Config", @@ -399,11 +393,27 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Close the current terminal.", }}, - .close_tab => comptime &.{.{ - .action = .close_tab, - .title = "Close Tab", - .description = "Close the current tab.", - }}, + .close_tab => comptime if (builtin.target.os.tag.isDarwin()) + &.{ + .{ + .action = .{ .close_tab = .this }, + .title = "Close Tab", + .description = "Close the current tab.", + }, + .{ + .action = .{ .close_tab = .other }, + .title = "Close Other Tabs", + .description = "Close all tabs in this window except the current one.", + }, + } + else + &.{ + .{ + .action = .{ .close_tab = .this }, + .title = "Close Tab", + .description = "Close the current tab.", + }, + }, .close_window => comptime &.{.{ .action = .close_window, From e98e868265faffc1e5dd3ae994ec175c73e590d7 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 25 Aug 2025 11:56:17 -0500 Subject: [PATCH 24/75] close-tab: style-fixes --- src/apprt/gtk-ng/class/tab.zig | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 0899d06ee..8dbe21c13 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -352,12 +352,8 @@ pub const Tab = extern struct { // Delegate to our parent to handle this, since this will emit // a close-page signal that the parent can intercept. switch (mode) { - .this => { - tab_view.closePage(page); - }, - .other => { - log.warn("close-tab:other is not implemented", .{}); - }, + .this => tab_view.closePage(page), + .other => log.warn("close-tab:other is not implemented", .{}), } } From c396c25898e062ff696d83ddd88e1ce7dc205ae5 Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:22:11 +0200 Subject: [PATCH 25/75] i18n: shorten Ukrainian translation for "split" --- po/uk_UA.UTF-8.po | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index e0f735678..5865002c2 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -9,7 +9,7 @@ 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-24 22:57+0100\n" +"PO-Revision-Date: 2025-08-25 19:23+0100\n" "Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n" "Language-Team: Ukrainian \n" "Language: uk\n" @@ -68,25 +68,25 @@ msgstr "Перезавантажити конфігурацію" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 msgid "Split Up" -msgstr "Розділити панель догори" +msgstr "Нова панель зверху" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 msgid "Split Down" -msgstr "Розділити панель донизу" +msgstr "Нова панель знизу" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 msgid "Split Left" -msgstr "Розділити панель ліворуч" +msgstr "Нова панель ліворуч" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 msgid "Split Right" -msgstr "Розділити панель праворуч" +msgstr "Нова панель праворуч" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" @@ -116,7 +116,7 @@ msgstr "Скинути" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 msgid "Split" -msgstr "Розділена панель" +msgstr "Панель" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 @@ -211,7 +211,7 @@ msgstr "Дозволити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "Запамʼятати вибір для цієї розділеної панелі" +msgstr "Запамʼятати вибір для цієї панелі" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 @@ -257,7 +257,7 @@ msgstr "Закрити вкладку?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Закрити розділену панель?" +msgstr "Закрити панель?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -273,7 +273,7 @@ msgstr "Всі сесії терміналу в цій вкладці будут #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "Процес, що виконується в цій розділеній панелі, буде завершено." +msgstr "Процес, що виконується в цій панелі, буде завершено." #: src/apprt/gtk/Surface.zig:1266 msgid "Copied to clipboard" @@ -301,7 +301,7 @@ msgstr "Переглянути відкриті вкладки" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "Нова розділена панель" +msgstr "Нова панель" #: src/apprt/gtk/Window.zig:329 msgid "" From f8d69e5baf0b3c7374cfc895f3398fcc7870d39d Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:33:19 +0200 Subject: [PATCH 26/75] i18n: use native Ukrainian word for "config" --- po/uk_UA.UTF-8.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 5865002c2..817ce7951 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -9,7 +9,7 @@ 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:23+0100\n" +"PO-Revision-Date: 2025-08-25 19:33+0100\n" "Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n" "Language-Team: Ukrainian \n" "Language: uk\n" @@ -40,7 +40,7 @@ msgstr "ОК" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Помилки конфігурації" +msgstr "Помилки налаштування" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 @@ -48,8 +48,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Виявлено одну або декілька помилок конфігурації. Будь ласка, перегляньте " -"наведені нижче помилки і або перезавантажте конфігурацію, або проігноруйте " +"Виявлено одну або декілька помилок налаштування. Будь ласка, перегляньте " +"наведені нижче помилки і або перезавантажте налаштування, або проігноруйте " "ці помилки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 @@ -62,7 +62,7 @@ msgstr "Ігнорувати" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" -msgstr "Перезавантажити конфігурацію" +msgstr "Перезавантажити налаштування" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 @@ -154,12 +154,12 @@ msgstr "Закрити вікно" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" -msgstr "Конфігурація" +msgstr "Налаштування" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" -msgstr "Відкрити конфігурацію" +msgstr "Відкрити налаштування" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" @@ -216,7 +216,7 @@ msgstr "Запамʼятати вибір для цієї панелі" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "Перезавантажте конфігурацію, щоб показати це повідомлення знову" +msgstr "Перезавантажте налаштування, щоб показати це повідомлення знову" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -311,7 +311,7 @@ msgstr "" #: src/apprt/gtk/Window.zig:775 msgid "Reloaded the configuration" -msgstr "Конфігурацію перезавантажено" +msgstr "Налаштування перезавантажено" #: src/apprt/gtk/Window.zig:1019 msgid "Ghostty Developers" From c629ea674c05e0cc6431ef1ac04eb15e75679f62 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 Aug 2025 16:19:06 +0200 Subject: [PATCH 27/75] i18n: add missing de_DE translations --- po/de_DE.UTF-8.po | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 153c298c0..c7fc6643f 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -9,7 +9,7 @@ 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-03-06 14:57+0100\n" +"PO-Revision-Date: 2025-08-25 19:38+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" "Language: de\n" @@ -39,7 +39,7 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "" +msgstr "Konfigurationsfehler" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 @@ -47,11 +47,14 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" +"Ein oder mehrere Konfigurationsfehler wurden gefunden. Bitte überprüfe " +"die untenstehenden Fehler und lade entweder deine Konfiguration erneut oder " +"ignoriere die Fehler." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" -msgstr "" +msgstr "Ignorieren" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 @@ -86,7 +89,7 @@ msgstr "Fenster nach rechts teilen" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Einen Befehl ausführen…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -159,7 +162,7 @@ msgstr "Konfiguration öffnen" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Befehlspalette" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -207,12 +210,13 @@ msgstr "Erlauben" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Auswahl für dieses geteilte Fenster beibehalten" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" msgstr "" +"Lade die Konfiguration erneut, um diese Eingabeaufforderung erneut anzuzeigen" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -277,15 +281,15 @@ msgstr "In die Zwischenablage kopiert" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Zwischenablage geleert" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Befehl erfolgreich" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Befehl fehlgeschlagen" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -297,7 +301,7 @@ msgstr "Offene Tabs einblenden" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Neues geteiltes Fenster" #: src/apprt/gtk/Window.zig:329 msgid "" From 11d845ce17126af93d00e9fd842c66434a3246b6 Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:57:08 +0200 Subject: [PATCH 28/75] i18n: shorten Ukrainian translations --- po/uk_UA.UTF-8.po | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 817ce7951..2433b6fa6 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -9,7 +9,7 @@ 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:33+0100\n" +"PO-Revision-Date: 2025-08-25 19:57+0100\n" "Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n" "Language-Team: Ukrainian \n" "Language: uk\n" @@ -49,8 +49,7 @@ msgid "" "and either reload your configuration or ignore these errors." msgstr "" "Виявлено одну або декілька помилок налаштування. Будь ласка, перегляньте " -"наведені нижче помилки і або перезавантажте налаштування, або проігноруйте " -"ці помилки." +"помилки нижче і або перезавантажте налаштування, або проігноруйте ці помилки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 @@ -183,7 +182,7 @@ msgstr "Завершити" #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" -msgstr "Дозволити доступ до буфера обміну" +msgstr "Надати доступ до буфера обміну" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 @@ -191,8 +190,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Програма намагається прочитати дані з буфера обміну. Нижче показано поточний " -"вміст буфера обміну." +"Програма намагається прочитати дані з буфера обміну. Нижче наведено вміст " +"буфера обміну." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -211,7 +210,7 @@ msgstr "Дозволити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "Запамʼятати вибір для цієї панелі" +msgstr "Запамʼятати для цієї панелі" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 @@ -224,8 +223,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Програма намагається записати дані до буфера обміну. Нижче показано поточний " -"вміст буфера обміну." +"Програма намагається записати дані до буфера обміну. Нижче наведено вміст " +"буфера обміну." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" @@ -236,8 +235,8 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає " -"так, ніби деякі команди можуть бути виконані." +"Вставка цього тексту в термінал може бути небезпечною, бо схоже що деякі " +"команди можуть бути виконані." #: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 msgid "Close" @@ -281,7 +280,7 @@ msgstr "Скопійовано до буферa обміну" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "Очищено буфер обміну" +msgstr "Буфер обміну очищено" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" From 754bb4011ae8f3cdc9c802ffb2fe909fcb88554f Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:59:46 +0200 Subject: [PATCH 29/75] i18n: add missing coma --- po/uk_UA.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 2433b6fa6..9d9c58b6e 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -9,7 +9,7 @@ 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:57+0100\n" +"PO-Revision-Date: 2025-08-25 19:59+0100\n" "Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n" "Language-Team: Ukrainian \n" "Language: uk\n" @@ -235,7 +235,7 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Вставка цього тексту в термінал може бути небезпечною, бо схоже що деякі " +"Вставка цього тексту в термінал може бути небезпечною, бо схоже, що деякі " "команди можуть бути виконані." #: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 From ca06b95f657a89ed31fe1b979243cf82f9694f90 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 25 Aug 2025 15:00:32 -0500 Subject: [PATCH 30/75] cli: show colors in +list-colors if possible Fixes #8386 This is a fairly simple implementaion, there's no interactivity or searching. It will adapt the number of columns to the available width of the display though. Will fallback to a plain text dump if there's no tty or the `--plain` argument is specified on the CLI. --- src/cli/list_colors.zig | 180 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 170 insertions(+), 10 deletions(-) diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig index e43a43c86..63945de99 100644 --- a/src/cli/list_colors.zig +++ b/src/cli/list_colors.zig @@ -1,13 +1,20 @@ const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const x11_color = @import("../terminal/main.zig").x11_color; +const vaxis = @import("vaxis"); +const tui = @import("tui.zig"); pub const Options = struct { pub fn deinit(self: Options) void { _ = self; } + /// If `true`, print without formatting even if printing to a tty + plain: bool = false, + /// Enables "-h" and "--help" to work. pub fn help(self: Options) !void { _ = self; @@ -17,7 +24,12 @@ pub const Options = struct { /// The `list-colors` command is used to list all the named RGB colors in /// Ghostty. -pub fn run(alloc: std.mem.Allocator) !u8 { +/// +/// Flags: +/// +/// * `--plain`: will disable formatting and make the output more +/// friendly for Unix tooling. This is default when not printing to a tty. +pub fn run(alloc: Allocator) !u8 { var opts: Options = .{}; defer opts.deinit(); @@ -27,7 +39,7 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + const stdout = std.io.getStdOut(); var keys = std.ArrayList([]const u8).init(alloc); defer keys.deinit(); @@ -39,15 +51,163 @@ pub fn run(alloc: std.mem.Allocator) !u8 { } }.lessThan); - for (keys.items) |name| { - const rgb = x11_color.map.get(name).?; - try stdout.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ - name, - rgb.r, - rgb.g, - rgb.b, - }); + // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 + if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { + var arena = std.heap.ArenaAllocator.init(alloc); + defer arena.deinit(); + return prettyPrint(arena.allocator(), keys.items); + } else { + const writer = stdout.writer(); + for (keys.items) |name| { + const rgb = x11_color.map.get(name).?; + try writer.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ + name, + rgb.r, + rgb.g, + rgb.b, + }); + } } return 0; } + +fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { + // Set up vaxis + var tty = try vaxis.Tty.init(); + defer tty.deinit(); + var vx = try vaxis.init(alloc, .{}); + defer vx.deinit(alloc, tty.anyWriter()); + + // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an + // event loop to auto-enable it. + vx.caps.unicode = .unicode; + try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set); + defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; + + var buf_writer = tty.bufferedWriter(); + const writer = buf_writer.writer().any(); + + const winsize: vaxis.Winsize = switch (builtin.os.tag) { + // We use some default, it doesn't really matter for what + // we're doing because we don't do any wrapping. + .windows => .{ + .rows = 24, + .cols = 120, + .x_pixel = 1024, + .y_pixel = 768, + }, + + else => try vaxis.Tty.getWinsize(tty.fd), + }; + try vx.resize(alloc, tty.anyWriter(), winsize); + + const win = vx.window(); + + var max_name_len: usize = 0; + for (keys) |name| { + if (name.len > max_name_len) max_name_len = name.len; + } + + // max name length plus " = #RRGGBB XX" plus " " gutter between columns + const column_size = max_name_len + 15; + // add two to take into account lack of gutter after last column + const columns: usize = @divFloor(win.width + 2, column_size); + + var i: usize = 0; + const step = @divFloor(keys.len, columns) + 1; + while (i < step) : (i += 1) { + win.clear(); + + var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; + + for (0..columns) |j| { + const k = i + (step * j); + if (k >= keys.len) continue; + + const name = keys[k]; + const rgb = x11_color.map.get(name).?; + + const style1: vaxis.Style = .{ + .fg = .{ + .rgb = .{ rgb.r, rgb.g, rgb.b }, + }, + }; + const style2: vaxis.Style = .{ + .fg = .{ + .rgb = .{ rgb.r, rgb.g, rgb.b }, + }, + .bg = .{ + .rgb = .{ rgb.r, rgb.g, rgb.b }, + }, + }; + + // name of the color + result = win.printSegment( + .{ .text = name }, + .{ .col_offset = result.col }, + ); + // push the color data to the end of the column + for (0..max_name_len - name.len) |_| { + result = win.printSegment( + .{ .text = " " }, + .{ .col_offset = result.col }, + ); + } + result = win.printSegment( + .{ .text = " = " }, + .{ .col_offset = result.col }, + ); + // rgb triple + result = win.printSegment(.{ + .text = try std.fmt.allocPrint( + alloc, + "#{x:0>2}{x:0>2}{x:0>2}", + .{ + rgb.r, rgb.g, rgb.b, + }, + ), + .style = style1, + }, .{ .col_offset = result.col }); + result = win.printSegment( + .{ .text = " " }, + .{ .col_offset = result.col }, + ); + // colored block + result = win.printSegment( + .{ + .text = " ", + .style = style2, + }, + .{ .col_offset = result.col }, + ); + // add the gutter if needed + if (j + 1 < columns) { + result = win.printSegment( + .{ + .text = " ", + }, + .{ .col_offset = result.col }, + ); + } + } + + // clear the rest of the line + while (result.col != 0) { + result = win.printSegment( + .{ + .text = " ", + }, + .{ .col_offset = result.col }, + ); + } + + // output the data + try vx.prettyPrint(writer); + } + + // be sure to flush! + try buf_writer.flush(); + + return 0; +} From a471bac7822d59f4f068f6ad88311406ba23daf0 Mon Sep 17 00:00:00 2001 From: Marija Gjorgjieva Gjondeva <82838042+marijagjorgjieva@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:03:43 +0200 Subject: [PATCH 31/75] Update macedonian translation strings (#8392) Updated new translation strings for version 1.2. #8344 --- po/mk_MK.UTF-8.po | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index d07837ee0..0bb4aea40 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Andrej Daskalov , 2025. +# Marija Gjorgjieva Gjondeva , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-23 14:17+0100\n" -"Last-Translator: Andrej Daskalov \n" +"PO-Revision-Date: 2025-08-25 22:17+0200\n" +"Last-Translator: Marija Gjorgjieva Gjondeva \n" "Language-Team: Macedonian\n" "Language: mk\n" "MIME-Version: 1.0\n" @@ -87,7 +88,7 @@ msgstr "Подели надесно" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Изврши команда…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +161,7 @@ msgstr "Отвори конфигурација" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Командна палета" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +209,12 @@ msgstr "Дозволи" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Запомни го изборот за оваа поделба" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Одново вчитај конфигурација за да се повторно прикаже пораката" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +279,15 @@ msgstr "Копирано во привремена меморија" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Исчистена привремена меморија" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Командата успеа" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Командата не успеа" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -298,7 +299,7 @@ msgstr "Прегледај отворени јазичиња" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Нова поделба" #: src/apprt/gtk/Window.zig:329 msgid "" From 3320a081b4b461b9b58d55920532ac6e3895c952 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 25 Aug 2025 19:23:52 -0500 Subject: [PATCH 32/75] osc 9: allow single character notifications --- src/terminal/osc.zig | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 6090166da..808cfc08f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1212,6 +1212,11 @@ pub const Parser = struct { 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; } fn prepAllocableString(self: *Parser) void { @@ -2836,8 +2841,22 @@ test "OSC: show desktop notification" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings(cmd.show_desktop_notification.title, ""); - try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world"); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); +} + +test "OSC: show single character desktop notification" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;H"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); } test "OSC: show desktop notification with title" { From 9c725187e17a08d1746e19c037bc11a4244c630d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Tue, 26 Aug 2025 15:47:16 +0100 Subject: [PATCH 33/75] Updating two strings --- po/ga_IE.UTF-8.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index ee5a53522..9395d2dec 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ 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-22 14:52+0100\n" +"PO-Revision-Date: 2025-08-26 15:46+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -209,7 +209,7 @@ msgstr "Ceadaigh" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "Cuimhnigh ar an rogha don scoilt seo" +msgstr "Sábháil an rogha don scoilt seo" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 @@ -280,7 +280,7 @@ msgstr "Cóipeáilte chuig an ghearrthaisce" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "Glanadh an ghearrthóg" +msgstr "Gearrthaisce glanta" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" From 63cd424678cf75ceb6a85501a38fb06f4bf7f118 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Wed, 11 Jun 2025 14:41:35 -0400 Subject: [PATCH 34/75] macOS: Add support for quick terminal sizing configuration Added C bindings for the already existing quick-terminal-size configuration. Created a new QuickTerminalSize struct to hold these values in Swift. Updated the QuickTerminal implementation to use the user's configuration if supplied. Retains defaults. Also adds support to customize the width of the quick terminal (height if quick terminal is set to right or left). --- include/ghostty.h | 8 ++ macos/Ghostty.xcodeproj/project.pbxproj | 5 ++ .../QuickTerminalController.swift | 13 +-- .../QuickTerminal/QuickTerminalPosition.swift | 72 +++++++---------- .../QuickTerminal/QuickTerminalSize.swift | 80 +++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Config.swift | 8 ++ src/config/Config.zig | 29 +++++++ 7 files changed, 167 insertions(+), 48 deletions(-) create mode 100644 macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift diff --git a/include/ghostty.h b/include/ghostty.h index 88da70b8b..780f49b6c 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -450,6 +450,14 @@ typedef struct { ghostty_config_color_s colors[256]; } ghostty_config_palette_s; +// config.QuickTerminalSize +typedef struct { + uint8_t primary_type; // 0 = none, 1 = percentage, 2 = pixels + float primary_value; + uint8_t secondary_type; // 0 = none, 1 = percentage, 2 = pixels + float secondary_value; +} ghostty_config_quick_terminal_size_s; + // apprt.Target.Key typedef enum { GHOSTTY_TARGET_APP, diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 0c54ba693..9e2110941 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -104,6 +104,7 @@ A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; }; + A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; @@ -252,6 +253,7 @@ A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; + A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = ""; }; A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; }; @@ -638,6 +640,7 @@ A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */, A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */, A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */, + A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */, ); path = QuickTerminal; sourceTree = ""; @@ -939,6 +942,8 @@ A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, + A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 1f608f767..40b06e036 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -109,7 +109,7 @@ class QuickTerminalController: BaseTerminalController { syncAppearance() // Setup our initial size based on our configured position - position.setLoaded(window) + position.setLoaded(window, size: derivedConfig.quickTerminalSize) // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, @@ -213,7 +213,7 @@ class QuickTerminalController: BaseTerminalController { // We use the actual screen the window is on for this, since it should // be on the proper screen. guard let screen = window?.screen ?? NSScreen.main else { return frameSize } - return position.restrictFrameSize(frameSize, on: screen) + return position.restrictFrameSize(frameSize, on: screen, terminalSize: derivedConfig.quickTerminalSize) } // MARK: Base Controller Overrides @@ -341,7 +341,7 @@ class QuickTerminalController: BaseTerminalController { } // Move our window off screen to the top - position.setInitial(in: window, on: screen) + position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize) // We need to set our window level to a high value. In testing, only // popUpMenu and above do what we want. This gets it above the menu bar @@ -372,7 +372,7 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setFinal(in: window.animator(), on: screen) + position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize) }, completionHandler: { // There is a very minor delay here so waiting at least an event loop tick // keeps us safe from the view not being on the window. @@ -496,7 +496,7 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setInitial(in: window.animator(), on: screen) + position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize) }, completionHandler: { // This causes the window to be removed from the screen list and macOS // handles what should be focused next. @@ -627,6 +627,7 @@ class QuickTerminalController: BaseTerminalController { let quickTerminalAnimationDuration: Double let quickTerminalAutoHide: Bool let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior + let quickTerminalSize: QuickTerminalSize let backgroundOpacity: Double init() { @@ -634,6 +635,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalAnimationDuration = 0.2 self.quickTerminalAutoHide = true self.quickTerminalSpaceBehavior = .move + self.quickTerminalSize = QuickTerminalSize() self.backgroundOpacity = 1.0 } @@ -642,6 +644,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration self.quickTerminalAutoHide = config.quickTerminalAutoHide self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior + self.quickTerminalSize = config.quickTerminalSize self.backgroundOpacity = config.backgroundOpacity } } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index 7ba124a30..a1da81758 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -8,72 +8,58 @@ enum QuickTerminalPosition : String { case center /// Set the loaded state for a window. - func setLoaded(_ window: NSWindow) { + func setLoaded(_ window: NSWindow, size: QuickTerminalSize) { guard let screen = window.screen ?? NSScreen.main else { return } - switch (self) { - case .top, .bottom: - window.setFrame(.init( - origin: window.frame.origin, - size: .init( - width: screen.frame.width, - height: screen.frame.height / 4) - ), display: false) - - case .left, .right: - window.setFrame(.init( - origin: window.frame.origin, - size: .init( - width: screen.frame.width / 4, - height: screen.frame.height) - ), display: false) - - case .center: - window.setFrame(.init( - origin: window.frame.origin, - size: .init( - width: screen.frame.width / 2, - height: screen.frame.height / 3) - ), display: false) - } + let dimensions = size.calculate(position: self, screenDimensions: screen.frame.size) + window.setFrame(.init( + origin: window.frame.origin, + size: .init( + width: dimensions.width, + height: dimensions.height) + ), display: false) } /// Set the initial state for a window for animating out of this position. - func setInitial(in window: NSWindow, on screen: NSScreen) { + func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) { // We always start invisible window.alphaValue = 0 // Position depends window.setFrame(.init( origin: initialOrigin(for: window, on: screen), - size: restrictFrameSize(window.frame.size, on: screen) + size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize) ), display: false) } /// Set the final state for a window in this position. - func setFinal(in window: NSWindow, on screen: NSScreen) { + func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) { // We always end visible window.alphaValue = 1 // Position depends window.setFrame(.init( origin: finalOrigin(for: window, on: screen), - size: restrictFrameSize(window.frame.size, on: screen) + size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize) ), display: true) } /// Restrict the frame size during resizing. - func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize { + func restrictFrameSize(_ size: NSSize, on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize { var finalSize = size + let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size) + switch (self) { case .top, .bottom: - finalSize.width = screen.frame.width + finalSize.width = dimensions.width + finalSize.height = dimensions.height case .left, .right: - finalSize.height = screen.visibleFrame.height + finalSize.width = dimensions.width + finalSize.height = dimensions.height case .center: - finalSize.width = screen.frame.width / 2 - finalSize.height = screen.frame.height / 3 + finalSize.width = dimensions.width + finalSize.height = dimensions.height } return finalSize @@ -83,16 +69,16 @@ enum QuickTerminalPosition : String { func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: screen.frame.minX, y: screen.frame.maxY) + return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.frame.maxY) case .bottom: - return .init(x: screen.frame.minX, y: -window.frame.height) + return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: -window.frame.height) case .left: - return .init(x: screen.frame.minX-window.frame.width, y: 0) + return .init(x: screen.frame.minX-window.frame.width, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) case .right: - return .init(x: screen.frame.maxX, y: 0) + return .init(x: screen.frame.maxX, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) case .center: return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width) @@ -103,16 +89,16 @@ enum QuickTerminalPosition : String { func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height) + return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.height) case .bottom: - return .init(x: screen.frame.minX, y: screen.frame.minY) + return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.frame.minY) case .left: - return .init(x: screen.frame.minX, y: window.frame.origin.y) + return .init(x: screen.frame.minX, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) case .right: - return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y) + return .init(x: screen.visibleFrame.maxX - window.frame.width, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) case .center: return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift new file mode 100644 index 000000000..c86f15b84 --- /dev/null +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -0,0 +1,80 @@ +import Cocoa +import GhosttyKit + +struct QuickTerminalSize { + let primary: Size? + let secondary: Size? + + init(primary: Size? = nil, secondary: Size? = nil) { + self.primary = primary + self.secondary = secondary + } + + init(from cStruct: ghostty_config_quick_terminal_size_s) { + self.primary = cStruct.primary_type == 0 ? nil : Size(type: cStruct.primary_type, value: cStruct.primary_value) + self.secondary = cStruct.secondary_type == 0 ? nil : Size(type: cStruct.secondary_type, value: cStruct.secondary_value) + } + + enum Size { + case percentage(Float) + case pixels(UInt32) + + init?(type: UInt8, value: Float) { + switch type { + case 1: + self = .percentage(value) + case 2: + self = .pixels(UInt32(value)) + default: + return nil + } + } + + func toPixels(parentDimension: CGFloat) -> CGFloat { + switch self { + case .percentage(let value): + return parentDimension * CGFloat(value) / 100.0 + case .pixels(let value): + return CGFloat(value) + } + } + } + + struct Dimensions { + let width: CGFloat + let height: CGFloat + } + + func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> Dimensions { + let dims = Dimensions(width: screenDimensions.width, height: screenDimensions.height) + + switch position { + case .left, .right: + return Dimensions( + width: primary?.toPixels(parentDimension: dims.width) ?? 400, + height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height + ) + + case .top, .bottom: + return Dimensions( + width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width, + height: primary?.toPixels(parentDimension: dims.height) ?? 400 + ) + + case .center: + if dims.width >= dims.height { + // Landscape + return Dimensions( + width: primary?.toPixels(parentDimension: dims.width) ?? 800, + height: secondary?.toPixels(parentDimension: dims.height) ?? 400 + ) + } else { + // Portrait + return Dimensions( + width: secondary?.toPixels(parentDimension: dims.width) ?? 400, + height: primary?.toPixels(parentDimension: dims.height) ?? 800 + ) + } + } + } +} \ No newline at end of file diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 6992f59f6..b106082bb 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -504,6 +504,14 @@ extension Ghostty { let str = String(cString: ptr) return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move } + + var quickTerminalSize: QuickTerminalSize { + guard let config = self.config else { return QuickTerminalSize() } + var v = ghostty_config_quick_terminal_size_s() + let key = "quick-terminal-size" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return QuickTerminalSize() } + return QuickTerminalSize(from: v) + } #endif var resizeOverlay: ResizeOverlay { diff --git a/src/config/Config.zig b/src/config/Config.zig index b82824a93..6d7337dfd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7198,6 +7198,35 @@ pub const QuickTerminalSize = struct { height: u32, }; + /// C API structure for QuickTerminalSize + pub const C = extern struct { + primary_type: u8, // 0 = none, 1 = percentage, 2 = pixels + primary_value: f32, + secondary_type: u8, // 0 = none, 1 = percentage, 2 = pixels + secondary_value: f32, + }; + + pub fn cval(self: QuickTerminalSize) C { + return .{ + .primary_type = if (self.primary) |p| switch (p) { + .percentage => 1, + .pixels => 2, + } else 0, + .primary_value = if (self.primary) |p| switch (p) { + .percentage => |v| v, + .pixels => |v| @floatFromInt(v), + } else 0, + .secondary_type = if (self.secondary) |s| switch (s) { + .percentage => 1, + .pixels => 2, + } else 0, + .secondary_value = if (self.secondary) |s| switch (s) { + .percentage => |v| v, + .pixels => |v| @floatFromInt(v), + } else 0, + }; + } + pub fn calculate( self: QuickTerminalSize, position: QuickTerminalPosition, From 17f7f204e1478a3ae00be2644bc04865539defd6 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Thu, 12 Jun 2025 12:47:03 -0400 Subject: [PATCH 35/75] macOS: update zig and c structs for quick terminal size Applying the feedback given by @pluiedev to use an enum to specify the type of quick terminal size configuration given (pixels or percentage). Updated the Swift code to work with the enum as well. --- include/ghostty.h | 17 +++++-- .../QuickTerminal/QuickTerminalSize.swift | 44 +++++++++-------- src/config/Config.zig | 49 +++++++++++-------- 3 files changed, 66 insertions(+), 44 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 780f49b6c..300aaec35 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -451,11 +451,20 @@ typedef struct { } ghostty_config_palette_s; // config.QuickTerminalSize +typedef enum { + GHOSTTY_QUICK_TERMINAL_SIZE_NONE, + GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE, + GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS, +} ghostty_quick_terminal_size_e; + typedef struct { - uint8_t primary_type; // 0 = none, 1 = percentage, 2 = pixels - float primary_value; - uint8_t secondary_type; // 0 = none, 1 = percentage, 2 = pixels - float secondary_value; + ghostty_quick_terminal_size_e type; + uint32_t value; +} ghostty_quick_terminal_size_u; + +typedef struct { + ghostty_quick_terminal_size_u primary; + ghostty_quick_terminal_size_u secondary; } ghostty_config_quick_terminal_size_s; // apprt.Target.Key diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift index c86f15b84..b2d39e8eb 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -1,35 +1,39 @@ -import Cocoa import GhosttyKit struct QuickTerminalSize { let primary: Size? let secondary: Size? - + init(primary: Size? = nil, secondary: Size? = nil) { self.primary = primary self.secondary = secondary } - + init(from cStruct: ghostty_config_quick_terminal_size_s) { - self.primary = cStruct.primary_type == 0 ? nil : Size(type: cStruct.primary_type, value: cStruct.primary_value) - self.secondary = cStruct.secondary_type == 0 ? nil : Size(type: cStruct.secondary_type, value: cStruct.secondary_value) + self.primary = Size(from: cStruct.primary) + self.secondary = Size(from: cStruct.secondary) } - + enum Size { case percentage(Float) case pixels(UInt32) - - init?(type: UInt8, value: Float) { - switch type { - case 1: - self = .percentage(value) - case 2: - self = .pixels(UInt32(value)) + + init?(from cStruct: ghostty_quick_terminal_size_u) { + switch cStruct.type { + case GHOSTTY_QUICK_TERMINAL_SIZE_NONE: + return nil + case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE: + let floatValue = withUnsafePointer(to: cStruct.value) { ptr in + ptr.withMemoryRebound(to: Float.self, capacity: 1) { $0.pointee } + } + self = .percentage(floatValue) + case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS: + self = .pixels(cStruct.value) default: return nil } } - + func toPixels(parentDimension: CGFloat) -> CGFloat { switch self { case .percentage(let value): @@ -39,28 +43,28 @@ struct QuickTerminalSize { } } } - + struct Dimensions { let width: CGFloat let height: CGFloat } - + func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> Dimensions { let dims = Dimensions(width: screenDimensions.width, height: screenDimensions.height) - + switch position { case .left, .right: return Dimensions( width: primary?.toPixels(parentDimension: dims.width) ?? 400, height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height ) - + case .top, .bottom: return Dimensions( width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width, height: primary?.toPixels(parentDimension: dims.height) ?? 400 ) - + case .center: if dims.width >= dims.height { // Landscape @@ -77,4 +81,4 @@ struct QuickTerminalSize { } } } -} \ No newline at end of file +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 6d7337dfd..c611570c4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7200,30 +7200,39 @@ pub const QuickTerminalSize = struct { /// C API structure for QuickTerminalSize pub const C = extern struct { - primary_type: u8, // 0 = none, 1 = percentage, 2 = pixels - primary_value: f32, - secondary_type: u8, // 0 = none, 1 = percentage, 2 = pixels - secondary_value: f32, + primary: CSize, + secondary: CSize, + }; + + pub const CSize = extern struct { + type: Type, + value: u32, + + pub const Type = enum(u8) { none, percentage, pixels }; + + fn none() CSize { + return .{ .type = .none, .value = 0 }; + } + + fn percentage(v: f32) CSize { + return .{ .type = .percentage, .value = @bitCast(v) }; + } + + fn pixels(v: u32) CSize { + return .{ .type = .pixels, .value = v }; + } }; pub fn cval(self: QuickTerminalSize) C { return .{ - .primary_type = if (self.primary) |p| switch (p) { - .percentage => 1, - .pixels => 2, - } else 0, - .primary_value = if (self.primary) |p| switch (p) { - .percentage => |v| v, - .pixels => |v| @floatFromInt(v), - } else 0, - .secondary_type = if (self.secondary) |s| switch (s) { - .percentage => 1, - .pixels => 2, - } else 0, - .secondary_value = if (self.secondary) |s| switch (s) { - .percentage => |v| v, - .pixels => |v| @floatFromInt(v), - } else 0, + .primary = if (self.primary) |p| switch (p) { + .percentage => |v| CSize.percentage(v), + .pixels => |v| CSize.pixels(v), + } else CSize.none(), + .secondary = if (self.secondary) |s| switch (s) { + .percentage => |v| CSize.percentage(v), + .pixels => |v| CSize.pixels(v), + } else CSize.none(), }; } From 7cc0728fe5b97b35021f8675786416a748f21789 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Sat, 21 Jun 2025 10:46:30 -0400 Subject: [PATCH 36/75] macOS: enable quick terminal manual resizing You can now resize the quick terminal both vertically and horizontally. To incorporate adjusting the custom secondary size on the quick terminal we needed to have the ability to resize the width (if from top, bottom, or center), and height (if from right, left, or center). The quick terminal will retain the user's manually adjusted size while the app is open. A new feature with this is that when the secondary size is adjusted (or primary if the quick terminal is center), the size will increase or decrease on both sides of the terminal. --- .../QuickTerminalController.swift | 61 ++++++++++++-- .../QuickTerminal/QuickTerminalPosition.swift | 83 ++++++++++++++----- 2 files changed, 113 insertions(+), 31 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 40b06e036..605b18581 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController { private var previousActiveSpace: CGSSpace? = nil /// The window frame saved when the quick terminal's surface tree becomes empty. - /// + /// /// This preserves the user's window size and position when all terminal surfaces /// are closed (e.g., via the `exit` command). When a new surface is created, /// the window will be restored to this frame, preventing SwiftUI from resetting @@ -34,6 +34,9 @@ class QuickTerminalController: BaseTerminalController { /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + + /// Tracks if we're currently handling a manual resize to prevent recursion + private var isHandlingResize: Bool = false init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, @@ -76,6 +79,11 @@ class QuickTerminalController: BaseTerminalController { selector: #selector(onNewTab), name: Ghostty.Notification.ghosttyNewTab, object: nil) + center.addObserver( + self, + selector: #selector(windowDidResize(_:)), + name: NSWindow.didResizeNotification, + object: nil) } required init?(coder: NSCoder) { @@ -210,10 +218,45 @@ class QuickTerminalController: BaseTerminalController { } func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize { - // We use the actual screen the window is on for this, since it should - // be on the proper screen. - guard let screen = window?.screen ?? NSScreen.main else { return frameSize } - return position.restrictFrameSize(frameSize, on: screen, terminalSize: derivedConfig.quickTerminalSize) + // Allow unrestricted resizing - users have full control + return frameSize + } + + override func windowDidResize(_ notification: Notification) { + guard let window = notification.object as? NSWindow, + window == self.window, + visible, + !isHandlingResize else { return } + + // For centered positions (top, bottom, center), we need to recenter the window + // when it's manually resized to maintain proper positioning + switch position { + case .top, .bottom, .center: + recenterWindow(window) + case .left, .right: + // For side positions, we may need to adjust vertical centering + recenterWindowVertically(window) + } + } + + private func recenterWindow(_ window: NSWindow) { + guard let screen = window.screen ?? NSScreen.main else { return } + + isHandlingResize = true + defer { isHandlingResize = false } + + let newOrigin = position.centeredOrigin(for: window, on: screen) + window.setFrameOrigin(newOrigin) + } + + private func recenterWindowVertically(_ window: NSWindow) { + guard let screen = window.screen ?? NSScreen.main else { return } + + isHandlingResize = true + defer { isHandlingResize = false } + + let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen) + window.setFrameOrigin(newOrigin) } // MARK: Base Controller Overrides @@ -335,13 +378,15 @@ class QuickTerminalController: BaseTerminalController { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } // Restore our previous frame if we have one + var preserveSize: NSSize? = nil if let lastClosedFrame { window.setFrame(lastClosedFrame, display: false) + preserveSize = lastClosedFrame.size self.lastClosedFrame = nil } // Move our window off screen to the top - position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize) + position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize) // We need to set our window level to a high value. In testing, only // popUpMenu and above do what we want. This gets it above the menu bar @@ -372,7 +417,7 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize) + position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize) }, completionHandler: { // There is a very minor delay here so waiting at least an event loop tick // keeps us safe from the view not being on the window. @@ -496,7 +541,7 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize) + position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: window.frame.size) }, completionHandler: { // This causes the window to be removed from the screen list and macOS // handles what should be focused next. diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index a1da81758..bf7ed4b08 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -20,49 +20,38 @@ enum QuickTerminalPosition : String { } /// Set the initial state for a window for animating out of this position. - func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) { + func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) { // We always start invisible window.alphaValue = 0 // Position depends window.setFrame(.init( origin: initialOrigin(for: window, on: screen), - size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize) + size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize) ), display: false) } /// Set the final state for a window in this position. - func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) { + func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) { // We always end visible window.alphaValue = 1 // Position depends window.setFrame(.init( origin: finalOrigin(for: window, on: screen), - size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize) + size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize) ), display: true) } - /// Restrict the frame size during resizing. - func restrictFrameSize(_ size: NSSize, on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize { - var finalSize = size - let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size) - - switch (self) { - case .top, .bottom: - finalSize.width = dimensions.width - finalSize.height = dimensions.height - - case .left, .right: - finalSize.width = dimensions.width - finalSize.height = dimensions.height - - case .center: - finalSize.width = dimensions.width - finalSize.height = dimensions.height + /// Get the configured frame size for initial positioning and animations. + func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize, preserveExisting: NSSize? = nil) -> NSSize { + // If we have existing dimensions from manual resizing, preserve them + if let existing = preserveExisting, existing.width > 0 && existing.height > 0 { + return existing } - - return finalSize + + let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size) + return NSSize(width: dimensions.width, height: dimensions.height) } /// The initial point origin for this position. @@ -122,4 +111,52 @@ enum QuickTerminalPosition : String { case .right: self == .top || self == .bottom } } + + /// Calculate the centered origin for a window, keeping it properly positioned after manual resizing + func centeredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { + switch self { + case .top: + return CGPoint( + x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, + y: window.frame.origin.y // Keep the same Y position + ) + + case .bottom: + return CGPoint( + x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, + y: window.frame.origin.y // Keep the same Y position + ) + + case .center: + return CGPoint( + x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, + y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2 + ) + + case .left, .right: + // For left/right positions, only adjust horizontal centering if needed + return window.frame.origin + } + } + + /// Calculate the vertically centered origin for side-positioned windows + func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { + switch self { + case .left: + return CGPoint( + x: window.frame.origin.x, // Keep the same X position + y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2 + ) + + case .right: + return CGPoint( + x: window.frame.origin.x, // Keep the same X position + y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2 + ) + + case .top, .bottom, .center: + // These positions don't need vertical recentering during resize + return window.frame.origin + } + } } From 0afadeea5f3fb17d1083f3a1691bb56c7f4677d8 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Thu, 26 Jun 2025 10:30:30 -0400 Subject: [PATCH 37/75] use decl literals as suggested Applied from the code review Co-authored-by: Leah Amelia Chen --- src/config/Config.zig | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index c611570c4..498f59bfb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7210,9 +7210,7 @@ pub const QuickTerminalSize = struct { pub const Type = enum(u8) { none, percentage, pixels }; - fn none() CSize { - return .{ .type = .none, .value = 0 }; - } + pub const none: CSize = .{ .type = .none, .value = 0 }; fn percentage(v: f32) CSize { return .{ .type = .percentage, .value = @bitCast(v) }; @@ -7226,13 +7224,13 @@ pub const QuickTerminalSize = struct { pub fn cval(self: QuickTerminalSize) C { return .{ .primary = if (self.primary) |p| switch (p) { - .percentage => |v| CSize.percentage(v), - .pixels => |v| CSize.pixels(v), - } else CSize.none(), + .percentage => |v| .percentage(v), + .pixels => |v| .pixels(v), + } else .none, .secondary = if (self.secondary) |s| switch (s) { - .percentage => |v| CSize.percentage(v), - .pixels => |v| CSize.pixels(v), - } else CSize.none(), + .percentage => |v| .percentage(v), + .pixels => |v| .pixels(v), + } else .none, }; } From 58e7400ea562d715a51f720ca63a5b83acaf9b65 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Thu, 26 Jun 2025 10:51:36 -0400 Subject: [PATCH 38/75] macOS: Round quick terminal window position coordinates This resolves an issue where the right side of the quick terminal would not resize equally to the left side if adjusting the width from the left side. --- .../QuickTerminal/QuickTerminalPosition.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index bf7ed4b08..418d8da94 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -58,19 +58,19 @@ enum QuickTerminalPosition : String { func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.frame.maxY) + return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.frame.maxY) case .bottom: - return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: -window.frame.height) + return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: -window.frame.height) case .left: - return .init(x: screen.frame.minX-window.frame.width, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) + return .init(x: screen.frame.minX-window.frame.width, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) case .right: - return .init(x: screen.frame.maxX, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) + return .init(x: screen.frame.maxX, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) case .center: - return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) } } @@ -78,19 +78,19 @@ enum QuickTerminalPosition : String { func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.height) + return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.visibleFrame.maxY - window.frame.height) case .bottom: - return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.frame.minY) + return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.frame.minY) case .left: - return .init(x: screen.frame.minX, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) + return .init(x: screen.frame.minX, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) case .right: - return .init(x: screen.visibleFrame.maxX - window.frame.width, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) + return .init(x: screen.visibleFrame.maxX - window.frame.width, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) case .center: - return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) } } @@ -117,20 +117,20 @@ enum QuickTerminalPosition : String { switch self { case .top: return CGPoint( - x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, + x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) case .bottom: return CGPoint( - x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, + x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) case .center: return CGPoint( - x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, - y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2 + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) case .left, .right: @@ -145,13 +145,13 @@ enum QuickTerminalPosition : String { case .left: return CGPoint( x: window.frame.origin.x, // Keep the same X position - y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2 + y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) ) case .right: return CGPoint( x: window.frame.origin.x, // Keep the same X position - y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2 + y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) ) case .top, .bottom, .center: From 466fdfffe6038128c4b4c8e76283eea0acbe2356 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Mon, 30 Jun 2025 16:35:31 -0400 Subject: [PATCH 39/75] macOS: rename c struct, relocate QuickTerminalSize file Renamed the ghostty_quick_terminal_size_u to ghostty_quick_terminal_size_s and moved the QuickTerminalSize file to the Ghostty folder as requested. --- include/ghostty.h | 6 +++--- macos/Ghostty.xcodeproj/project.pbxproj | 10 +++++----- .../QuickTerminal => Ghostty}/QuickTerminalSize.swift | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename macos/Sources/{Features/QuickTerminal => Ghostty}/QuickTerminalSize.swift (97%) diff --git a/include/ghostty.h b/include/ghostty.h index 300aaec35..7758423fe 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -460,11 +460,11 @@ typedef enum { typedef struct { ghostty_quick_terminal_size_e type; uint32_t value; -} ghostty_quick_terminal_size_u; +} ghostty_quick_terminal_size_s; typedef struct { - ghostty_quick_terminal_size_u primary; - ghostty_quick_terminal_size_u secondary; + ghostty_quick_terminal_size_s primary; + ghostty_quick_terminal_size_s secondary; } ghostty_config_quick_terminal_size_s; // apprt.Target.Key diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b896c7692..d9fa44f08 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -103,8 +103,8 @@ A5A6F72A2CC41B8900B232A5 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* AppInfo.swift */; }; A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; }; A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */; }; - A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; @@ -252,8 +252,8 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = ""; }; - A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; }; @@ -490,6 +490,7 @@ A55B7BB429B6F4410055DE60 /* Ghostty */ = { isa = PBXGroup; children = ( + A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */, A55B7BB729B6F53A0055DE60 /* Package.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */, @@ -640,7 +641,6 @@ A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */, A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */, A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */, - A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */, ); path = QuickTerminal; sourceTree = ""; @@ -942,9 +942,9 @@ A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, - A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */, - A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Ghostty/QuickTerminalSize.swift similarity index 97% rename from macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift rename to macos/Sources/Ghostty/QuickTerminalSize.swift index b2d39e8eb..194407014 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift +++ b/macos/Sources/Ghostty/QuickTerminalSize.swift @@ -18,7 +18,7 @@ struct QuickTerminalSize { case percentage(Float) case pixels(UInt32) - init?(from cStruct: ghostty_quick_terminal_size_u) { + init?(from cStruct: ghostty_quick_terminal_size_s) { switch cStruct.type { case GHOSTTY_QUICK_TERMINAL_SIZE_NONE: return nil From a90bf58080fdcc671e666e92d741ad90e8868be8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Aug 2025 09:47:03 -0700 Subject: [PATCH 40/75] config: change quick terminal size C layout to tagged union --- include/ghostty.h | 11 ++++-- macos/Ghostty.xcodeproj/project.pbxproj | 2 +- .../QuickTerminal}/QuickTerminalSize.swift | 9 ++--- src/config/Config.zig | 39 ++++++++++++------- 4 files changed, 37 insertions(+), 24 deletions(-) rename macos/Sources/{Ghostty => Features/QuickTerminal}/QuickTerminalSize.swift (88%) diff --git a/include/ghostty.h b/include/ghostty.h index 7758423fe..6fd218e9e 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -455,11 +455,16 @@ typedef enum { GHOSTTY_QUICK_TERMINAL_SIZE_NONE, GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE, GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS, -} ghostty_quick_terminal_size_e; +} ghostty_quick_terminal_size_tag_e; + +typedef union { + float percentage; + uint32_t pixels; +} ghostty_quick_terminal_size_value_u; typedef struct { - ghostty_quick_terminal_size_e type; - uint32_t value; + ghostty_quick_terminal_size_tag_e tag; + ghostty_quick_terminal_size_value_u value; } ghostty_quick_terminal_size_s; typedef struct { diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index d9fa44f08..e3fb92fa4 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -490,7 +490,6 @@ A55B7BB429B6F4410055DE60 /* Ghostty */ = { isa = PBXGroup; children = ( - A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */, A55B7BB729B6F53A0055DE60 /* Package.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */, @@ -640,6 +639,7 @@ CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */, A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */, A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */, + A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */, A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */, ); path = QuickTerminal; diff --git a/macos/Sources/Ghostty/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift similarity index 88% rename from macos/Sources/Ghostty/QuickTerminalSize.swift rename to macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift index 194407014..cb3cc6fd6 100644 --- a/macos/Sources/Ghostty/QuickTerminalSize.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -19,16 +19,13 @@ struct QuickTerminalSize { case pixels(UInt32) init?(from cStruct: ghostty_quick_terminal_size_s) { - switch cStruct.type { + switch cStruct.tag { case GHOSTTY_QUICK_TERMINAL_SIZE_NONE: return nil case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE: - let floatValue = withUnsafePointer(to: cStruct.value) { ptr in - ptr.withMemoryRebound(to: Float.self, capacity: 1) { $0.pointee } - } - self = .percentage(floatValue) + self = .percentage(cStruct.value.percentage) case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS: - self = .pixels(cStruct.value) + self = .pixels(cStruct.value.pixels) default: return nil } diff --git a/src/config/Config.zig b/src/config/Config.zig index 498f59bfb..1156c5fef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7200,25 +7200,36 @@ pub const QuickTerminalSize = struct { /// C API structure for QuickTerminalSize pub const C = extern struct { - primary: CSize, - secondary: CSize, - }; + primary: C.Size, + secondary: C.Size, - pub const CSize = extern struct { - type: Type, - value: u32, + pub const Size = extern struct { + tag: Tag, + value: Value, - pub const Type = enum(u8) { none, percentage, pixels }; + pub const Tag = enum(u8) { none, percentage, pixels }; - pub const none: CSize = .{ .type = .none, .value = 0 }; + pub const Value = extern union { + percentage: f32, + pixels: u32, + }; - fn percentage(v: f32) CSize { - return .{ .type = .percentage, .value = @bitCast(v) }; - } + pub const none: C.Size = .{ .tag = .none, .value = undefined }; - fn pixels(v: u32) CSize { - return .{ .type = .pixels, .value = v }; - } + pub fn percentage(v: f32) C.Size { + return .{ + .tag = .percentage, + .value = .{ .percentage = v }, + }; + } + + pub fn pixels(v: u32) C.Size { + return .{ + .tag = .pixels, + .value = .{ .pixels = v }, + }; + } + }; }; pub fn cval(self: QuickTerminalSize) C { From ae48f323d7df1dc04bc1860627ab27a22151b040 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Aug 2025 10:20:12 -0700 Subject: [PATCH 41/75] macos: style changes for quick terminal sizing --- .../QuickTerminalController.swift | 76 +++++++++---------- .../QuickTerminal/QuickTerminalPosition.swift | 42 +++++----- .../QuickTerminal/QuickTerminalSize.swift | 23 +++--- src/config/Config.zig | 1 + 4 files changed, 73 insertions(+), 69 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 605b18581..76b835358 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -217,48 +217,30 @@ class QuickTerminalController: BaseTerminalController { } } - func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize { - // Allow unrestricted resizing - users have full control - return frameSize - } - override func windowDidResize(_ notification: Notification) { guard let window = notification.object as? NSWindow, window == self.window, visible, !isHandlingResize else { return } + guard let screen = window.screen ?? NSScreen.main else { return } + + // Prevent recursive loops + isHandlingResize = true + defer { isHandlingResize = false } - // For centered positions (top, bottom, center), we need to recenter the window - // when it's manually resized to maintain proper positioning switch position { case .top, .bottom, .center: - recenterWindow(window) + // For centered positions (top, bottom, center), we need to recenter the window + // when it's manually resized to maintain proper positioning + let newOrigin = position.centeredOrigin(for: window, on: screen) + window.setFrameOrigin(newOrigin) case .left, .right: // For side positions, we may need to adjust vertical centering - recenterWindowVertically(window) + let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen) + window.setFrameOrigin(newOrigin) } } - private func recenterWindow(_ window: NSWindow) { - guard let screen = window.screen ?? NSScreen.main else { return } - - isHandlingResize = true - defer { isHandlingResize = false } - - let newOrigin = position.centeredOrigin(for: window, on: screen) - window.setFrameOrigin(newOrigin) - } - - private func recenterWindowVertically(_ window: NSWindow) { - guard let screen = window.screen ?? NSScreen.main else { return } - - isHandlingResize = true - defer { isHandlingResize = false } - - let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen) - window.setFrameOrigin(newOrigin) - } - // MARK: Base Controller Overrides override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { @@ -376,17 +358,17 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } + + // Grab our last closed frame to use, and clear our state since we're animating in. + let lastClosedFrame = self.lastClosedFrame + self.lastClosedFrame = nil - // Restore our previous frame if we have one - var preserveSize: NSSize? = nil - if let lastClosedFrame { - window.setFrame(lastClosedFrame, display: false) - preserveSize = lastClosedFrame.size - self.lastClosedFrame = nil - } - - // Move our window off screen to the top - position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize) + // Move our window off screen to the initial animation position. + position.setInitial( + in: window, + on: screen, + terminalSize: derivedConfig.quickTerminalSize, + closedFrame: lastClosedFrame) // We need to set our window level to a high value. In testing, only // popUpMenu and above do what we want. This gets it above the menu bar @@ -417,7 +399,11 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize) + position.setFinal( + in: window.animator(), + on: screen, + terminalSize: derivedConfig.quickTerminalSize, + closedFrame: lastClosedFrame) }, completionHandler: { // There is a very minor delay here so waiting at least an event loop tick // keeps us safe from the view not being on the window. @@ -499,7 +485,9 @@ class QuickTerminalController: BaseTerminalController { // the user's preferred window size and position for when the quick // terminal is reactivated with a new surface. Without this, SwiftUI // would reset the window to its minimum content size. - lastClosedFrame = window.frame + if window.frame.width > 0 && window.frame.height > 0 { + lastClosedFrame = window.frame + } // If we hid the dock then we unhide it. hiddenDock = nil @@ -541,7 +529,11 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: window.frame.size) + position.setInitial( + in: window.animator(), + on: screen, + terminalSize: derivedConfig.quickTerminalSize, + closedFrame: window.frame) }, completionHandler: { // This causes the window to be removed from the screen list and macOS // handles what should be focused next. diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index 418d8da94..55e183fa8 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -7,49 +7,57 @@ enum QuickTerminalPosition : String { case right case center - /// Set the loaded state for a window. + /// Set the loaded state for a window. This should only be called when the window is first loaded, + /// usually in `windowDidLoad` or in a similar callback. This is the initial state. func setLoaded(_ window: NSWindow, size: QuickTerminalSize) { guard let screen = window.screen ?? NSScreen.main else { return } - let dimensions = size.calculate(position: self, screenDimensions: screen.frame.size) window.setFrame(.init( origin: window.frame.origin, - size: .init( - width: dimensions.width, - height: dimensions.height) + size: size.calculate(position: self, screenDimensions: screen.frame.size) ), display: false) } - /// Set the initial state for a window for animating out of this position. - func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) { - // We always start invisible + /// Set the initial state for a window NOT yet into position (either before animating in or + /// after animating out). + func setInitial( + in window: NSWindow, + on screen: NSScreen, + terminalSize: QuickTerminalSize, + closedFrame: NSRect? = nil + ) { + // Invisible window.alphaValue = 0 // Position depends window.setFrame(.init( origin: initialOrigin(for: window, on: screen), - size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize) + size: closedFrame?.size ?? configuredFrameSize( + on: screen, + terminalSize: terminalSize) ), display: false) } /// Set the final state for a window in this position. - func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) { + func setFinal( + in window: NSWindow, + on screen: NSScreen, + terminalSize: QuickTerminalSize, + closedFrame: NSRect? = nil + ) { // We always end visible window.alphaValue = 1 // Position depends window.setFrame(.init( origin: finalOrigin(for: window, on: screen), - size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize) + size: closedFrame?.size ?? configuredFrameSize( + on: screen, + terminalSize: terminalSize) ), display: true) } /// Get the configured frame size for initial positioning and animations. - func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize, preserveExisting: NSSize? = nil) -> NSSize { - // If we have existing dimensions from manual resizing, preserve them - if let existing = preserveExisting, existing.width > 0 && existing.height > 0 { - return existing - } - + func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize { let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size) return NSSize(width: dimensions.width, height: dimensions.height) } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift index cb3cc6fd6..9f86a7c2b 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -1,5 +1,11 @@ import GhosttyKit +/// Represents the Ghostty `quick-terminal-size` configuration. See the documentation for +/// that for more details on exactly how it works. Some of those docs will be reproduced in various comments +/// in this file but that is the best source of truth for it. +/// +/// The size determines the size of the quick terminal along the primary and secondary axis. The primary and +/// secondary axis is defined by the `quick-terminal-position`. struct QuickTerminalSize { let primary: Size? let secondary: Size? @@ -41,23 +47,20 @@ struct QuickTerminalSize { } } - struct Dimensions { - let width: CGFloat - let height: CGFloat - } - func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> Dimensions { - let dims = Dimensions(width: screenDimensions.width, height: screenDimensions.height) + /// This is an almost direct port of th Zig function QuickTerminalSize.calculate + func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize { + let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height) switch position { case .left, .right: - return Dimensions( + return CGSize( width: primary?.toPixels(parentDimension: dims.width) ?? 400, height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height ) case .top, .bottom: - return Dimensions( + return CGSize( width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width, height: primary?.toPixels(parentDimension: dims.height) ?? 400 ) @@ -65,13 +68,13 @@ struct QuickTerminalSize { case .center: if dims.width >= dims.height { // Landscape - return Dimensions( + return CGSize( width: primary?.toPixels(parentDimension: dims.width) ?? 800, height: secondary?.toPixels(parentDimension: dims.height) ?? 400 ) } else { // Portrait - return Dimensions( + return CGSize( width: secondary?.toPixels(parentDimension: dims.width) ?? 400, height: primary?.toPixels(parentDimension: dims.height) ?? 800 ) diff --git a/src/config/Config.zig b/src/config/Config.zig index 1156c5fef..23e84ebaa 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7318,6 +7318,7 @@ pub const QuickTerminalSize = struct { try formatter.formatEntry([]const u8, fbs.getWritten()); } + test "parse QuickTerminalSize" { const testing = std.testing; var v: QuickTerminalSize = undefined; From 6f630a27be58dd4673a2bb7351b8bc7171293369 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 26 Aug 2025 12:22:45 -0500 Subject: [PATCH 42/75] gtk-ng: implement close_tab:other keybind --- src/apprt/gtk-ng/class/tab.zig | 2 +- src/input/Binding.zig | 2 -- src/input/command.zig | 31 +++++++++++-------------------- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 8dbe21c13..20672abe4 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -353,7 +353,7 @@ pub const Tab = extern struct { // a close-page signal that the parent can intercept. switch (mode) { .this => tab_view.closePage(page), - .other => log.warn("close-tab:other is not implemented", .{}), + .other => tab_view.closeOtherPages(page), } } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6d1050859..d475db539 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -558,8 +558,6 @@ pub const Action = union(enum) { /// /// If the mode is not specified, defaults to closing the current tab. /// - /// close-tab:other is only available on macOS. - /// /// This might trigger a close confirmation popup, depending on the value /// of the `confirm-close-surface` configuration setting. close_tab: CloseTabMode, diff --git a/src/input/command.zig b/src/input/command.zig index f6c29040a..63feb2edf 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -393,27 +393,18 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Close the current terminal.", }}, - .close_tab => comptime if (builtin.target.os.tag.isDarwin()) - &.{ - .{ - .action = .{ .close_tab = .this }, - .title = "Close Tab", - .description = "Close the current tab.", - }, - .{ - .action = .{ .close_tab = .other }, - .title = "Close Other Tabs", - .description = "Close all tabs in this window except the current one.", - }, - } - else - &.{ - .{ - .action = .{ .close_tab = .this }, - .title = "Close Tab", - .description = "Close the current tab.", - }, + .close_tab => comptime &.{ + .{ + .action = .{ .close_tab = .this }, + .title = "Close Tab", + .description = "Close the current tab.", }, + .{ + .action = .{ .close_tab = .other }, + .title = "Close Other Tabs", + .description = "Close all tabs in this window except the current one.", + }, + }, .close_window => comptime &.{.{ .action = .close_window, From e676eae640a07d586859fb549db193e85577cd79 Mon Sep 17 00:00:00 2001 From: Alexander Lais Date: Mon, 28 Jul 2025 23:05:05 +0200 Subject: [PATCH 43/75] macos: fix quick terminal fullscreen Fullscreen on quick terminal was failing with a crash, when it tried to save the state of a non-existent toolbar and its accessory view controllers. --- macos/Sources/Helpers/Fullscreen.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index f3940a9aa..6c70e8cf7 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -407,8 +407,14 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.styleMask = window.styleMask self.toolbar = window.toolbar self.toolbarStyle = window.toolbarStyle - self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers self.dock = window.screen?.hasDock ?? false + + self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { + // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. + window.titlebarAccessoryViewControllers + } else { + [] + } if let cgWindowId = window.cgWindowId { // We hide the menu only if this window is not on any fullscreen From 520eaec61c1f6da1607f7c3a674e29ab9469cf8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 26 Aug 2025 10:40:39 -0700 Subject: [PATCH 44/75] macos: fix quick terminal issue where closing while fullscreen --- .../QuickTerminal/QuickTerminalController.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 76b835358..68b9ba337 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -223,7 +223,7 @@ class QuickTerminalController: BaseTerminalController { visible, !isHandlingResize else { return } guard let screen = window.screen ?? NSScreen.main else { return } - + // Prevent recursive loops isHandlingResize = true defer { isHandlingResize = false } @@ -481,6 +481,12 @@ class QuickTerminalController: BaseTerminalController { } private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) { + // If we are in fullscreen, then we exit fullscreen. We do this immediately so + // we have th correct window.frame for the save state below. + if let fullscreenStyle, fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + } + // Save the current window frame before animating out. This preserves // the user's preferred window size and position for when the quick // terminal is reactivated with a new surface. Without this, SwiftUI @@ -503,11 +509,6 @@ class QuickTerminalController: BaseTerminalController { // We always animate out to whatever screen the window is actually on. guard let screen = window.screen ?? NSScreen.main else { return } - // If we are in fullscreen, then we exit fullscreen. - if let fullscreenStyle, fullscreenStyle.isFullscreen { - fullscreenStyle.exit() - } - // If we have a previously active application, restore focus to it. We // do this BEFORE the animation below because when the animation completes // macOS will bring forward another window. From 6cf636b1ad3a6b147a7b3d5c5dec840664d0cad0 Mon Sep 17 00:00:00 2001 From: John Drouhard Date: Thu, 24 Apr 2025 14:00:23 -0500 Subject: [PATCH 45/75] scroll: round up fractional mouse scroll ticks Scrolling with a mouse on macos doesn't work very well when doing small, single tick scrolls. macos attempts to mimic precision scrolling by changing the magnitude of the scroll deltas based on scrolling speed. Slow scrolls only send deltas with a magnitude of 0.1, which isn't enough to send a single scroll event with the default scroll multiplier of 3. Changing the scroll multiplier to 10 as a workaround (so even single small scroll ticks are enough to register a scroll event) cause scrolling to be way too fast if the scroll speed is ramped up. This commit causes the yoffset delta to be rounded out to at least a magnitude of 1 in the appropriate direction. For small single scroll ticks, it's enough to register a scroll event, but as scroll speed is ramped up, the true delta reported to the surface is used again. Setting a scroll multiplier of 1 with the changes here makes mouse scrolling feel just as good as trackpad precision scrolling. --- src/Surface.zig | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index e72322e31..c17585aa1 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2766,8 +2766,21 @@ pub fn scrollCallback( // that a wheel tick of 1 results in single scroll event. const yoff_adjusted: f64 = if (scroll_mods.precision) yoff - else - yoff * cell_size * self.config.mouse_scroll_multiplier; + else yoff_adjusted: { + // Round out the yoff to an absolute minimum of 1. macos tries to + // simulate precision scrolling with non precision events by + // ramping up the magnitude of the offsets as it detects faster + // scrolling. Single click (very slow) scrolls are reported with a + // magnitude of 0.1 which would normally require a few clicks + // before we register an actual scroll event (depending on cell + // height and the mouse_scroll_multiplier setting). + const yoff_max: f64 = if (yoff > 0) + @max(yoff, 1) + else + @min(yoff, -1); + + break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier; + }; // Add our previously saved pending amount to the offset to get the // new offset value. The signs of the pending and yoff should match From 4d6269a8596f72d86aa609cedf3ffff89bb41be1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Aug 2025 09:38:44 -0700 Subject: [PATCH 46/75] apprt/gtk-ng: show error widget if GLArea fails to initialize If GTK can't acquire an OpenGL context, this shows a message. Previously, we would only log a warning which was difficult to find. The GUI previously was the default GTK view which showed "Failed to acquire EGL display" which was equally confusing. --- src/apprt/gtk-ng/class/application.zig | 20 +- src/apprt/gtk-ng/class/surface.zig | 101 +++++++-- src/apprt/gtk-ng/ui/1.2/surface.blp | 276 ++++++++++++++----------- 3 files changed, 256 insertions(+), 141 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 984eda15e..05b24f064 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -116,6 +116,11 @@ pub const Application = extern struct { /// and initialization was successful. transient_cgroup_base: ?[]const u8 = null, + /// This is set to true so long as we request a window exactly + /// once. This prevents quitting the app before we've shown one + /// window. + requested_window: bool = false, + /// This is set to false internally when the event loop /// should exit and the application should quit. This must /// only be set by the main loop thread. @@ -461,7 +466,13 @@ pub const Application = extern struct { // If the quit timer has expired, quit. if (priv.quit_timer == .expired) break :q true; - // There's no quit timer running, or it hasn't expired, don't quit. + // If we have no windows attached to our app, also quit. + if (priv.requested_window and @as( + ?*glib.List, + self.as(gtk.Application).getWindows(), + ) == null) break :q true; + + // No quit conditions met break :q false; }; @@ -1858,6 +1869,13 @@ const Action = struct { self: *Application, parent: ?*CoreSurface, ) !void { + // Note that we've requested a window at least once. This is used + // to trigger quit on no windows. Note I'm not sure if this is REALLY + // necessary, but I don't want to risk a bug where on a slow machine + // or something we quit immediately after starting up because there + // was a delay in the event loop before we created a Window. + self.private().requested_window = true; + const win = Window.new(self); initAndShowWindow(self, win, parent); } diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 2debff93b..25ee1f94f 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -105,6 +105,24 @@ pub const Surface = extern struct { ); }; + pub const @"error" = struct { + pub const name = "error"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "error", + ), + }, + ); + }; + pub const @"font-size-request" = struct { pub const name = "font-size-request"; const impl = gobject.ext.defineProperty( @@ -472,6 +490,12 @@ pub const Surface = extern struct { // false by a parent widget. bell_ringing: bool = false, + /// True if this surface is in an error state. This is currently + /// a simple boolean with no additional information on WHAT the + /// error state is, because we don't yet need it or use it. For now, + /// if this is true, then it means the terminal is non-functional. + @"error": bool = false, + /// A weak reference to an inspector window. inspector: ?*InspectorWindow = null, @@ -571,6 +595,17 @@ pub const Surface = extern struct { return @intFromBool(config.@"bell-features".border); } + fn closureStackChildName( + _: *Self, + error_: c_int, + ) callconv(.c) ?[*:0]const u8 { + const err = error_ != 0; + return if (err) + glib.ext.dupeZ(u8, "error") + else + glib.ext.dupeZ(u8, "terminal"); + } + pub fn toggleFullscreen(self: *Self) void { signals.@"toggle-fullscreen".impl.emit( self, @@ -1540,6 +1575,12 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec); } + pub fn setError(self: *Self, v: bool) void { + const priv = self.private(); + priv.@"error" = v; + self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec); + } + fn propConfig( self: *Self, _: *gobject.ParamSpec, @@ -1592,6 +1633,28 @@ pub const Surface = extern struct { } } + fn propError( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + if (priv.@"error") { + // Ensure we have an opaque background. The window will NOT set + // this if we have transparency set and we need an opaque + // background for the error message to be readable. + self.as(gtk.Widget).addCssClass("background"); + } else { + // Regardless of transparency setting, we remove the background + // CSS class from this widget. Parent widgets will set it + // appropriately (see window.zig for example). + self.as(gtk.Widget).removeCssClass("background"); + } + + // Note above: in both cases setting our error view is handled by + // a Gtk.Stack visible-child-name binding. + } + fn propMouseHoverUrl( self: *Self, _: *gobject.ParamSpec, @@ -1942,8 +2005,11 @@ pub const Surface = extern struct { // Bell stops ringing if any mouse button is pressed. self.setBellRinging(false); - // If we don't have focus, grab it. + // Get our surface. If we don't have one, ignore this. const priv = self.private(); + const core_surface = priv.core_surface orelse return; + + // If we don't have focus, grab it. const gl_area_widget = priv.gl_area.as(gtk.Widget); if (gl_area_widget.hasFocus() == 0) { _ = gl_area_widget.grabFocus(); @@ -1951,10 +2017,10 @@ pub const Surface = extern struct { // Report the event const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); - const consumed = if (priv.core_surface) |surface| consumed: { + const consumed = consumed: { const gtk_mods = event.getModifierState(); const mods = gtk_key.translateMods(gtk_mods); - break :consumed surface.mouseButtonCallback( + break :consumed core_surface.mouseButtonCallback( .press, button, mods, @@ -1962,7 +2028,7 @@ pub const Surface = extern struct { log.warn("error in key callback err={}", .{err}); break :err false; }; - } else false; + }; // If a right click isn't consumed, mouseButtonCallback selects the hovered // word and returns false. We can use this to handle the context menu @@ -2303,21 +2369,23 @@ pub const Surface = extern struct { ) callconv(.c) void { log.debug("realize", .{}); + // Make the GL area current so we can detect any OpenGL errors. If + // we have errors here we can't render and we switch to the error + // state. + const priv = self.private(); + priv.gl_area.makeCurrent(); + if (priv.gl_area.getError()) |err| { + log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"}); + log.warn("this error is almost always due to a library, driver, or GTK issue", .{}); + log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{}); + self.setError(true); + return; + } + // If we already have an initialized surface then we notify it. // If we don't, we'll initialize it on the first resize so we have // our proper initial dimensions. - const priv = self.private(); if (priv.core_surface) |v| realize: { - // We need to make the context current so we can call GL functions. - // This is required for all surface operations. - priv.gl_area.makeCurrent(); - if (priv.gl_area.getError()) |err| { - log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"}); - log.warn("this error is usually due to a driver or gtk bug", .{}); - log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{}); - break :realize; - } - v.renderer.displayRealized() catch |err| { log.warn("core displayRealized failed err={}", .{err}); break :realize; @@ -2662,11 +2730,13 @@ pub const Surface = extern struct { class.bindTemplateCallback("child_exited_close", &childExitedClose); class.bindTemplateCallback("context_menu_closed", &contextMenuClosed); class.bindTemplateCallback("notify_config", &propConfig); + class.bindTemplateCallback("notify_error", &propError); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); + class.bindTemplateCallback("stack_child_name", &closureStackChildName); // Properties gobject.ext.registerProperties(class, &.{ @@ -2674,6 +2744,7 @@ pub const Surface = extern struct { properties.config.impl, properties.@"child-exited".impl, properties.@"default-size".impl, + properties.@"error".impl, properties.@"font-size-request".impl, properties.focused.impl, properties.@"min-size".impl, diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 9989e9c10..39c88ff33 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -8,146 +8,172 @@ template $GhosttySurface: Adw.Bin { notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); + notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hidden => $notify_mouse_hidden(); notify::mouse-shape => $notify_mouse_shape(); - Overlay { - focusable: false; - focus-on-click: false; + Stack { + StackPage { + name: "terminal"; - child: Box { - hexpand: true; - vexpand: true; + child: Overlay { + focusable: false; + focus-on-click: false; - GLArea gl_area { - realize => $gl_realize(); - unrealize => $gl_unrealize(); - render => $gl_render(); - resize => $gl_resize(); - hexpand: true; - vexpand: true; - focusable: true; - focus-on-click: true; - has-stencil-buffer: false; - has-depth-buffer: false; - allowed-apis: gl; - } + child: Box { + hexpand: true; + vexpand: true; - PopoverMenu context_menu { - closed => $context_menu_closed(); - menu-model: context_menu_model; - flags: nested; - halign: start; - has-arrow: false; - } - }; + GLArea gl_area { + realize => $gl_realize(); + unrealize => $gl_unrealize(); + render => $gl_render(); + resize => $gl_resize(); + hexpand: true; + vexpand: true; + focusable: true; + focus-on-click: true; + has-stencil-buffer: false; + has-depth-buffer: false; + allowed-apis: gl; + } - [overlay] - ProgressBar progress_bar_overlay { - styles [ - "osd", - ] + PopoverMenu context_menu { + closed => $context_menu_closed(); + menu-model: context_menu_model; + flags: nested; + halign: start; + has-arrow: false; + } + }; - visible: false; - halign: fill; - valign: start; + [overlay] + ProgressBar progress_bar_overlay { + styles [ + "osd", + ] + + visible: false; + halign: fill; + valign: start; + } + + [overlay] + // The "border" bell feature is implemented here as an overlay rather than + // just adding a border to the GLArea or other widget for two reasons. + // First, adding a border to an existing widget causes a resize of the + // widget which undesirable side effects. Second, we can make it reactive + // here in the blueprint with relatively little code. + Revealer { + reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as ; + transition-type: crossfade; + transition-duration: 500; + + Box bell_overlay { + styles [ + "bell-overlay", + ] + + halign: fill; + valign: fill; + } + } + + [overlay] + $GhosttySurfaceChildExited child_exited_overlay { + visible: bind template.child-exited; + close-request => $child_exited_close(); + } + + [overlay] + $GhosttyResizeOverlay resize_overlay {} + + [overlay] + Label url_left { + styles [ + "background", + "url-overlay", + ] + + visible: false; + halign: start; + valign: end; + label: bind template.mouse-hover-url; + + EventControllerMotion url_ec_motion { + enter => $url_mouse_enter(); + leave => $url_mouse_leave(); + } + } + + [overlay] + Label url_right { + styles [ + "background", + "url-overlay", + ] + + visible: false; + halign: end; + valign: end; + label: bind template.mouse-hover-url; + } + + // Event controllers for interactivity + EventControllerFocus { + enter => $focus_enter(); + leave => $focus_leave(); + } + + EventControllerKey { + key-pressed => $key_pressed(); + key-released => $key_released(); + } + + EventControllerMotion { + motion => $mouse_motion(); + leave => $mouse_leave(); + } + + EventControllerScroll { + scroll => $scroll(); + scroll-begin => $scroll_begin(); + scroll-end => $scroll_end(); + flags: both_axes; + } + + GestureClick { + pressed => $mouse_down(); + released => $mouse_up(); + button: 0; + } + + DropTarget drop_target { + drop => $drop(); + actions: copy; + } + }; } - [overlay] - // The "border" bell feature is implemented here as an overlay rather than - // just adding a border to the GLArea or other widget for two reasons. - // First, adding a border to an existing widget causes a resize of the - // widget which undesirable side effects. Second, we can make it reactive - // here in the blueprint with relatively little code. - Revealer { - reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as ; - transition-type: crossfade; - transition-duration: 500; + StackPage { + name: "error"; - Box bell_overlay { - styles [ - "bell-overlay", - ] + child: Adw.StatusPage { + icon-name: "computer-fail-symbolic"; + title: _("Oh, no."); + description: _("Unable to acquire an OpenGL context for rendering."); - halign: fill; - valign: fill; - } + child: LinkButton { + label: "https://ghostty.org/docs/help/gtk-opengl-context"; + uri: "https://ghostty.org/docs/help/gtk-opengl-context"; + }; + }; } - [overlay] - $GhosttySurfaceChildExited child_exited_overlay { - visible: bind template.child-exited; - close-request => $child_exited_close(); - } - - [overlay] - $GhosttyResizeOverlay resize_overlay {} - - [overlay] - Label url_left { - styles [ - "background", - "url-overlay", - ] - - visible: false; - halign: start; - valign: end; - label: bind template.mouse-hover-url; - - EventControllerMotion url_ec_motion { - enter => $url_mouse_enter(); - leave => $url_mouse_leave(); - } - } - - [overlay] - Label url_right { - styles [ - "background", - "url-overlay", - ] - - visible: false; - halign: end; - valign: end; - label: bind template.mouse-hover-url; - } - } - - // Event controllers for interactivity - EventControllerFocus { - enter => $focus_enter(); - leave => $focus_leave(); - } - - EventControllerKey { - key-pressed => $key_pressed(); - key-released => $key_released(); - } - - EventControllerMotion { - motion => $mouse_motion(); - leave => $mouse_leave(); - } - - EventControllerScroll { - scroll => $scroll(); - scroll-begin => $scroll_begin(); - scroll-end => $scroll_end(); - flags: both_axes; - } - - GestureClick { - pressed => $mouse_down(); - released => $mouse_up(); - button: 0; - } - - DropTarget drop_target { - drop => $drop(); - actions: copy; + // The order matters here: we can only set this after the stack + // pages above have been created. + visible-child-name: bind $stack_child_name(template.error) as ; } } From f047db6a3be5d0bbc5a9154ea8fa0d77743222e8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 26 Aug 2025 16:28:25 -0500 Subject: [PATCH 47/75] osc: parse OSC 9;6 gui macros --- src/terminal/osc.zig | 72 +++++++++++++++++++++++++++++++++++++++++ src/terminal/stream.zig | 7 +++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 808cfc08f..6cf76e92e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -167,6 +167,9 @@ pub const Command = union(enum) { /// Wait input (OSC 9;5) wait_input: void, + /// ConEmu GUI macro (OSC 9;6) + conemu_guimacro: []const u8, + pub const ColorOperation = union(enum) { pub const Source = enum(u16) { // these numbers are based on the OSC operation code @@ -431,6 +434,7 @@ pub const Parser = struct { conemu_progress_state, conemu_progress_prevalue, conemu_progress_value, + conemu_guimacro, }; pub fn init() Parser { @@ -972,6 +976,13 @@ pub const Parser = struct { self.command = .{ .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 @@ -1104,6 +1115,19 @@ pub const Parser = struct { }, }, + .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; @@ -1600,6 +1624,12 @@ pub const Parser = struct { .hyperlink_uri => self.endHyperlink(), .string => self.endString(), .conemu_sleep_value => self.endConEmuSleepValue(), + .conemu_guimacro => { + // We received OSC 9;6 ST, but nothing else, finish off as a + // desktop notification with "6" as the body. + self.showDesktopNotification(); + self.endString(); + }, .allocable_string => self.endAllocableString(), .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), @@ -3434,3 +3464,45 @@ test "OSC: kitty color protocol no key" { try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); } + +test "OSC: 9;6: ConEmu guimacro 1" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6;a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("a", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 2" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("ab", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); +} diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index f40fc4c94..cac1da2f5 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1605,7 +1605,12 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .sleep, .show_message_box, .change_conemu_tab_title, .wait_input => { + .sleep, + .show_message_box, + .change_conemu_tab_title, + .wait_input, + .conemu_guimacro, + => { log.warn("unimplemented OSC callback: {}", .{cmd}); }, From 64d849283626fb4a90152dbc61b36fdb12f50a13 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 26 Aug 2025 21:28:50 -0500 Subject: [PATCH 48/75] osc: conemu cleanup - Add more comments, and make existing ones more consistent. - Rename commands so they consitently have a `conemu_` prefix. - Ensure that OSC 9 desktop notifications can be sent in the maximum number of circumstances. There are still many notifications that can't be sent because of our support for the ConEmu OSCs but that's the tradeoff we have chosen. We recommend that you switch to OSC 777 to ensure desktop notifications can be sent in all circumstances. - Make sure that the tests that exercise the ConEmu OSCs have a consistent naming structure. That will make them easier to find through searching as well as make it easier to filter only the ConEmu OSC tests. - Add more tests to make sure that desktop notifications are sent properly. --- src/terminal/osc.zig | 461 ++++++++++++++++++++++++++++------------ src/terminal/stream.zig | 10 +- 2 files changed, 329 insertions(+), 142 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 6cf76e92e..786a1772c 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -147,25 +147,25 @@ pub const Command = union(enum) { /// End a hyperlink (OSC 8) hyperlink_end: void, - /// Sleep (OSC 9;1) - sleep: struct { + /// ConEmu sleep (OSC 9;1) + conemu_sleep: struct { duration_ms: u16, }, - /// Show GUI message Box (OSC 9;2) - show_message_box: []const u8, + /// ConEmu show GUI message box (OSC 9;2) + conemu_show_message_box: []const u8, - /// Change ConEmu tab (OSC 9;3) - change_conemu_tab_title: union(enum) { - reset: void, + /// ConEmu change tab title (OSC 9;3) + conemu_change_tab_title: union(enum) { + reset, value: []const u8, }, - /// Set progress state (OSC 9;4) - progress_report: ProgressReport, + /// ConEmu progress report (OSC 9;4) + conemu_progress_report: ProgressReport, - /// Wait input (OSC 9;5) - wait_input: void, + /// ConEmu wait input (OSC 9;5) + conemu_wait_input, /// ConEmu GUI macro (OSC 9;6) conemu_guimacro: []const u8, @@ -961,19 +961,37 @@ pub const Parser = struct { .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 = .{ .wait_input = {} }; + self.command = .conemu_wait_input; self.complete = true; }, '6' => { @@ -984,91 +1002,106 @@ pub const Parser = struct { 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. + // 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 = .{ .sleep = .{ .duration_ms = 100 } }; + self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } }; self.buf_start = self.buf_idx; self.complete = true; self.state = .conemu_sleep_value; }, - else => self.state = .invalid, - }, - .conemu_message_box => switch (c) { - ';' => { - self.command = .{ .show_message_box = undefined }; - self.temp_state = .{ .str = &self.command.show_message_box }; - self.buf_start = self.buf_idx; - self.complete = true; - self.prepAllocableString(); - }, - else => self.state = .invalid, + // 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 = .{ .change_conemu_tab_title = .{ .reset = {} } }; + self.command = .{ .conemu_change_tab_title = .reset }; self.buf_start = self.buf_idx; self.complete = true; }, - else => self.state = .invalid, + + // OSC 9;3 is a desktop + // notification. + else => self.showDesktopNotification(), }, .conemu_tab_txt => { - self.command = .{ .change_conemu_tab_title = .{ .value = undefined } }; - self.temp_state = .{ .str = &self.command.change_conemu_tab_title.value }; + 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 = .{ .progress_report = .{ + 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.progress_report.state = .remove; + self.command.conemu_progress_report.state = .remove; self.state = .swallow; self.complete = true; }, '1' => { - self.command.progress_report.state = .set; - self.command.progress_report.progress = 0; + self.command.conemu_progress_report.state = .set; + self.command.conemu_progress_report.progress = 0; self.state = .conemu_progress_prevalue; }, '2' => { - self.command.progress_report.state = .@"error"; + self.command.conemu_progress_report.state = .@"error"; self.complete = true; self.state = .conemu_progress_prevalue; }, '3' => { - self.command.progress_report.state = .indeterminate; + self.command.conemu_progress_report.state = .indeterminate; self.complete = true; self.state = .swallow; }, '4' => { - self.command.progress_report.state = .pause; + 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(), }, @@ -1077,6 +1110,8 @@ pub const Parser = struct { self.state = .conemu_progress_value; }, + // OSC 9;4;<0-4> is a desktop + // notification. else => self.showDesktopNotification(), }, @@ -1088,8 +1123,16 @@ pub const Parser = struct { // If we aren't a set substate, then we don't care // about the value. - const p = &self.command.progress_report; - if (p.state != .set and p.state != .@"error" and p.state != .pause) break :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) @@ -1123,6 +1166,7 @@ pub const Parser = struct { self.state = .string; self.complete = true; }, + // OSC 9;6 is a desktop // notification. else => self.showDesktopNotification(), @@ -1361,7 +1405,7 @@ pub const Parser = struct { fn endConEmuSleepValue(self: *Parser) void { switch (self.command) { - .sleep => |*v| v.duration_ms = value: { + .conemu_sleep => |*v| v.duration_ms = value: { const str = self.buf[self.buf_start..self.buf_idx]; if (str.len == 0) break :value 100; @@ -1624,12 +1668,26 @@ pub const Parser = struct { .hyperlink_uri => self.endHyperlink(), .string => self.endString(), .conemu_sleep_value => self.endConEmuSleepValue(), - .conemu_guimacro => { - // We received OSC 9;6 ST, but nothing else, finish off as a - // desktop notification with "6" as the body. + + // 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(); }, + + // A ConEmu progress report that has reached these states is + // complete, don't do anything to them. + .conemu_progress_prevalue, + .conemu_progress_value, + => {}, + .allocable_string => self.endAllocableString(), .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), @@ -2805,7 +2863,7 @@ test "OSC: OSC104: empty palette index" { try std.testing.expect(it.next() == null); } -test "OSC: conemu sleep" { +test "OSC: OSC 9;1 ConEmu sleep" { const testing = std.testing; var p: Parser = .init(); @@ -2815,11 +2873,11 @@ test "OSC: conemu sleep" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(420, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); } -test "OSC: conemu sleep with no value default to 100ms" { +test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" { const testing = std.testing; var p: Parser = .init(); @@ -2829,11 +2887,11 @@ test "OSC: conemu sleep with no value default to 100ms" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(100, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); } -test "OSC: conemu sleep cannot exceed 10000ms" { +test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" { const testing = std.testing; var p: Parser = .init(); @@ -2843,11 +2901,11 @@ test "OSC: conemu sleep cannot exceed 10000ms" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(10000, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); } -test "OSC: conemu sleep invalid input" { +test "OSC: OSC 9;1 conemu sleep invalid input" { const testing = std.testing; var p: Parser = .init(); @@ -2857,11 +2915,39 @@ test "OSC: conemu sleep invalid input" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(100, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); } -test "OSC: show desktop notification" { +test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9 show desktop notification" { const testing = std.testing; var p: Parser = .init(); @@ -2875,7 +2961,7 @@ test "OSC: show desktop notification" { try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); } -test "OSC: show single character desktop notification" { +test "OSC: OSC 9 show single character desktop notification" { const testing = std.testing; var p: Parser = .init(); @@ -2889,7 +2975,7 @@ test "OSC: show single character desktop notification" { try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); } -test "OSC: show desktop notification with title" { +test "OSC: OSC 777 show desktop notification with title" { const testing = std.testing; var p: Parser = .init(); @@ -2903,7 +2989,7 @@ test "OSC: show desktop notification with title" { try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); } -test "OSC: conemu message box" { +test "OSC: OSC 9;2 ConEmu message box" { const testing = std.testing; var p: Parser = .init(); @@ -2912,11 +2998,11 @@ test "OSC: conemu message box" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_message_box); - try testing.expectEqualStrings("hello world", cmd.show_message_box); + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); } -test "OSC: conemu message box invalid input" { +test "OSC: 9;2 ConEmu message box invalid input" { const testing = std.testing; var p: Parser = .init(); @@ -2924,11 +3010,12 @@ test "OSC: conemu message box invalid input" { const input = "9;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); } -test "OSC: conemu message box empty message" { +test "OSC: 9;2 ConEmu message box empty message" { const testing = std.testing; var p: Parser = .init(); @@ -2937,11 +3024,11 @@ test "OSC: conemu message box empty message" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_message_box); - try testing.expectEqualStrings("", cmd.show_message_box); + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("", cmd.conemu_show_message_box); } -test "OSC: conemu message box spaces only message" { +test "OSC: 9;2 ConEmu message box spaces only message" { const testing = std.testing; var p: Parser = .init(); @@ -2950,11 +3037,39 @@ test "OSC: conemu message box spaces only message" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_message_box); - try testing.expectEqualStrings(" ", cmd.show_message_box); + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); } -test "OSC: conemu change tab title" { +test "OSC: OSC 9;2 message box -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;2 message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); +} + +test "OSC: 9;3 ConEmu change tab title" { const testing = std.testing; var p: Parser = .init(); @@ -2963,11 +3078,11 @@ test "OSC: conemu change tab title" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .change_conemu_tab_title); - try testing.expectEqualStrings("foo bar", cmd.change_conemu_tab_title.value); + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); } -test "OSC: conemu change tab reset title" { +test "OSC: 9;3 ConEmu change tab title reset" { const testing = std.testing; var p: Parser = .init(); @@ -2977,11 +3092,11 @@ test "OSC: conemu change tab reset title" { const cmd = p.end('\x1b').?; - const expected_command: Command = .{ .change_conemu_tab_title = .{ .reset = {} } }; + const expected_command: Command = .{ .conemu_change_tab_title = .reset }; try testing.expectEqual(expected_command, cmd); } -test "OSC: conemu change tab spaces only title" { +test "OSC: 9;3 ConEmu change tab title spaces only" { const testing = std.testing; var p: Parser = .init(); @@ -2991,11 +3106,11 @@ test "OSC: conemu change tab spaces only title" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .change_conemu_tab_title); - try testing.expectEqualStrings(" ", cmd.change_conemu_tab_title.value); + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); } -test "OSC: conemu change tab invalid input" { +test "OSC: OSC 9;3 change tab title -> desktop notification 1" { const testing = std.testing; var p: Parser = .init(); @@ -3003,11 +3118,27 @@ test "OSC: conemu change tab invalid input" { const input = "9;3"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); } -test "OSC: OSC9 progress set" { +test "OSC: OSC 9;3 message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 ConEmu progress set" { const testing = std.testing; var p: Parser = .init(); @@ -3016,12 +3147,12 @@ test "OSC: OSC9 progress set" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 100); + 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: OSC9 progress set overflow" { +test "OSC: OSC 9;4 ConEmu progress set overflow" { const testing = std.testing; var p: Parser = .init(); @@ -3030,12 +3161,12 @@ test "OSC: OSC9 progress set overflow" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 100); + 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: OSC9 progress set single digit" { +test "OSC: OSC 9;4 ConEmu progress set single digit" { const testing = std.testing; var p: Parser = .init(); @@ -3044,12 +3175,12 @@ test "OSC: OSC9 progress set single digit" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 9); + 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: OSC9 progress set double digit" { +test "OSC: OSC 9;4 ConEmu progress set double digit" { const testing = std.testing; var p: Parser = .init(); @@ -3058,12 +3189,12 @@ test "OSC: OSC9 progress set double digit" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 94); + 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: OSC9 progress set extra semicolon ignored" { +test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" { const testing = std.testing; var p: Parser = .init(); @@ -3072,12 +3203,12 @@ test "OSC: OSC9 progress set extra semicolon ignored" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 100); + 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: OSC9 progress remove with no progress" { +test "OSC: OSC 9;4 ConEmu progress remove with no progress" { const testing = std.testing; var p: Parser = .init(); @@ -3086,12 +3217,12 @@ test "OSC: OSC9 progress remove with no progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); - try testing.expect(cmd.progress_report.progress == null); + 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: OSC9 progress remove with double semicolon" { +test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" { const testing = std.testing; var p: Parser = .init(); @@ -3100,12 +3231,12 @@ test "OSC: OSC9 progress remove with double semicolon" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); - try testing.expect(cmd.progress_report.progress == null); + 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: OSC9 progress remove ignores progress" { +test "OSC: OSC 9;4 ConEmu progress remove ignores progress" { const testing = std.testing; var p: Parser = .init(); @@ -3114,12 +3245,12 @@ test "OSC: OSC9 progress remove ignores progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); - try testing.expect(cmd.progress_report.progress == null); + 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: OSC9 progress remove extra semicolon" { +test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" { const testing = std.testing; var p: Parser = .init(); @@ -3128,11 +3259,11 @@ test "OSC: OSC9 progress remove extra semicolon" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); } -test "OSC: OSC9 progress error" { +test "OSC: OSC 9;4 ConEmu progress error" { const testing = std.testing; var p: Parser = .init(); @@ -3141,12 +3272,12 @@ test "OSC: OSC9 progress error" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .@"error"); - try testing.expect(cmd.progress_report.progress == null); + 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: OSC9 progress error with progress" { +test "OSC: OSC 9;4 ConEmu progress error with progress" { const testing = std.testing; var p: Parser = .init(); @@ -3155,12 +3286,12 @@ test "OSC: OSC9 progress error with progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .@"error"); - try testing.expect(cmd.progress_report.progress == 100); + 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: OSC9 progress pause" { +test "OSC: OSC 9;4 progress pause" { const testing = std.testing; var p: Parser = .init(); @@ -3169,12 +3300,12 @@ test "OSC: OSC9 progress pause" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .pause); - try testing.expect(cmd.progress_report.progress == null); + 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: OSC9 progress pause with progress" { +test "OSC: OSC 9;4 ConEmu progress pause with progress" { const testing = std.testing; var p: Parser = .init(); @@ -3183,12 +3314,68 @@ test "OSC: OSC9 progress pause with progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .pause); - try testing.expect(cmd.progress_report.progress == 100); + 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: OSC9 conemu wait input" { +test "OSC: OSC 9;4 progress -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 progress -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 progress -> desktop notification 3" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;5"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 progress -> desktop notification 4" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;5a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;5 ConEmu wait input" { const testing = std.testing; var p: Parser = .init(); @@ -3197,10 +3384,10 @@ test "OSC: OSC9 conemu wait input" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .wait_input); + try testing.expect(cmd == .conemu_wait_input); } -test "OSC: OSC9 conemu wait ignores trailing characters" { +test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" { const testing = std.testing; var p: Parser = .init(); @@ -3209,7 +3396,7 @@ test "OSC: OSC9 conemu wait ignores trailing characters" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .wait_input); + try testing.expect(cmd == .conemu_wait_input); } test "OSC: empty param" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index cac1da2f5..ce09cbda2 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1598,17 +1598,17 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .progress_report => |v| { + .conemu_progress_report => |v| { if (@hasDecl(T, "handleProgressReport")) { try self.handler.handleProgressReport(v); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .sleep, - .show_message_box, - .change_conemu_tab_title, - .wait_input, + .conemu_sleep, + .conemu_show_message_box, + .conemu_change_tab_title, + .conemu_wait_input, .conemu_guimacro, => { log.warn("unimplemented OSC callback: {}", .{cmd}); From 31c96d906afc6af560708b24e84d86f294d251b1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 26 Aug 2025 18:42:24 -0500 Subject: [PATCH 49/75] config: add entry for scroll-to-bottom Fixes #8408 --- src/config/Config.zig | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 23e84ebaa..ecaf87ef4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -767,6 +767,22 @@ palette: Palette = .{}, /// the mouse is shown again when a new window, tab, or split is created. @"mouse-hide-while-typing": bool = false, +/// When to scroll the surface to the bottom. The format of this is a list of +/// options to enable separated by commas. If you prefix an option with `no-` +/// then it is disabled. If you omit an option, its default value is used. +/// +/// Available options: +/// +/// - `keystroke` If set, scroll the surface to the bottom when the user +/// presses a key that results in data being sent to the PTY (basically +/// anything but modifiers or keybinds that are processed by Ghostty). +/// +/// - `output` If set, scroll the surface to the bottom if there is new data +/// to display. (Currently unimplemented.) +/// +/// The default is `keystroke, no-output`. +@"scroll-to-bottom": ScrollToBottom = .default, + /// Determines whether running programs can detect the shift key pressed with a /// mouse click. Typically, the shift key is used to extend mouse selection. /// @@ -8031,6 +8047,14 @@ pub const WindowPadding = struct { } }; +/// See scroll-to-bottom +pub const ScrollToBottom = packed struct { + keystroke: bool = true, + output: bool = false, + + pub const default: ScrollToBottom = .{}; +}; + test "parse duration" { inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; From 2490171304808803be9a88d48033b86cd3c8b93f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 26 Aug 2025 18:45:15 -0500 Subject: [PATCH 50/75] surface: implement scroll-to-bottom=keystroke --- src/Surface.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index c17585aa1..d3267f7f4 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -272,6 +272,7 @@ const DerivedConfig = struct { title_report: bool, links: []Link, link_previews: configpkg.LinkPreviews, + scroll_to_bottom_on_keystroke: bool, const Link = struct { regex: oni.Regex, @@ -340,6 +341,7 @@ const DerivedConfig = struct { .title_report = config.@"title-report", .links = links, .link_previews = config.@"link-previews", + .scroll_to_bottom_on_keystroke = config.@"scroll-to-bottom".keystroke, // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -2280,7 +2282,8 @@ pub fn keyCallback( try self.setSelection(null); } - try self.io.terminal.scrollViewport(.{ .bottom = {} }); + if (self.config.scroll_to_bottom_on_keystroke) try self.io.terminal.scrollViewport(.bottom); + try self.queueRender(); } From 87056a260094d7a545f4e7396f1b4d461ef9bf11 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 26 Aug 2025 22:04:23 -0500 Subject: [PATCH 51/75] surface: store entire scroll-to-bottom struct --- src/Surface.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index d3267f7f4..330d25102 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -272,7 +272,7 @@ const DerivedConfig = struct { title_report: bool, links: []Link, link_previews: configpkg.LinkPreviews, - scroll_to_bottom_on_keystroke: bool, + scroll_to_bottom: configpkg.Config.ScrollToBottom, const Link = struct { regex: oni.Regex, @@ -341,7 +341,7 @@ const DerivedConfig = struct { .title_report = config.@"title-report", .links = links, .link_previews = config.@"link-previews", - .scroll_to_bottom_on_keystroke = config.@"scroll-to-bottom".keystroke, + .scroll_to_bottom = config.@"scroll-to-bottom", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -2282,7 +2282,7 @@ pub fn keyCallback( try self.setSelection(null); } - if (self.config.scroll_to_bottom_on_keystroke) try self.io.terminal.scrollViewport(.bottom); + if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom); try self.queueRender(); } From 8fa065512fc26289d1d40cc3f90e29ad3024a1c6 Mon Sep 17 00:00:00 2001 From: reo101 Date: Fri, 22 Aug 2025 14:47:09 +0300 Subject: [PATCH 52/75] i18n: update bg_BG translations --- po/bg_BG.UTF-8.po | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 68aa63595..47954124d 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. # Damyan Bogoev , 2025. +# reo101 , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-05-19 11:34+0300\n" -"Last-Translator: Damyan Bogoev \n" +"PO-Revision-Date: 2025-08-22 14:52+0300\n" +"Last-Translator: reo101 \n" "Language-Team: Bulgarian \n" "Language: bg\n" "MIME-Version: 1.0\n" @@ -208,12 +209,12 @@ msgstr "Позволи" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Запомни избора за това разделяне" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "За да покажеш това съобщение отново, презареди конфигурацията" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +279,15 @@ msgstr "Копирано в клипборда" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Клипбордът е изчистен" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Командата завърши успешно" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Командата завърши неуспешно" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From 19a27383f85dc6e1ade714f7f3841127c2275317 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Aug 2025 06:57:43 -0700 Subject: [PATCH 53/75] macos: use visible frame for quick terminal sizing calculation Fixes #8418 This fixes issues where left/right positions would be cut off from the menu bar. And makes it so that size 100%,100% doesn't overflow into the non-visible space of the edge of the screen. --- .../QuickTerminal/QuickTerminalPosition.swift | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index 55e183fa8..d7660f77a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -13,7 +13,7 @@ enum QuickTerminalPosition : String { guard let screen = window.screen ?? NSScreen.main else { return } window.setFrame(.init( origin: window.frame.origin, - size: size.calculate(position: self, screenDimensions: screen.frame.size) + size: size.calculate(position: self, screenDimensions: screen.visibleFrame.size) ), display: false) } @@ -58,7 +58,7 @@ enum QuickTerminalPosition : String { /// Get the configured frame size for initial positioning and animations. func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize { - let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size) + let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.visibleFrame.size) return NSSize(width: dimensions.width, height: dimensions.height) } @@ -66,16 +66,24 @@ enum QuickTerminalPosition : String { func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.frame.maxY) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: screen.visibleFrame.maxY) case .bottom: - return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: -window.frame.height) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: -window.frame.height) case .left: - return .init(x: screen.frame.minX-window.frame.width, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) + return .init( + x: screen.visibleFrame.minX-window.frame.width, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .right: - return .init(x: screen.frame.maxX, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) + return .init( + x: screen.visibleFrame.maxX, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) @@ -86,16 +94,24 @@ enum QuickTerminalPosition : String { func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.visibleFrame.maxY - window.frame.height) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: screen.visibleFrame.maxY - window.frame.height) case .bottom: - return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.frame.minY) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: screen.visibleFrame.minY) case .left: - return .init(x: screen.frame.minX, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) + return .init( + x: screen.visibleFrame.minX, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .right: - return .init(x: screen.visibleFrame.maxX - window.frame.width, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)) + return .init( + x: screen.visibleFrame.maxX - window.frame.width, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) @@ -125,13 +141,13 @@ enum QuickTerminalPosition : String { switch self { case .top: return CGPoint( - x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) case .bottom: return CGPoint( - x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) @@ -153,13 +169,13 @@ enum QuickTerminalPosition : String { case .left: return CGPoint( x: window.frame.origin.x, // Keep the same X position - y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) case .right: return CGPoint( x: window.frame.origin.x, // Keep the same X position - y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2) + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) case .top, .bottom, .center: From 56d3fd872e90d24c7ba75ef40a08a27852bd5570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Wed, 27 Aug 2025 12:18:03 +0900 Subject: [PATCH 54/75] fix(terminal): improve CSI parameter parsing Make `MAX_PARAMS` public and increase CSI parameter limit from 16 to 24. Fix potential out-of-bounds read in SGR partial sequence extraction. --- src/terminal/Parser.zig | 2 +- src/terminal/sgr.zig | 2 +- src/terminal/stream.zig | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 0223545e5..92f405a23 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -206,7 +206,7 @@ const MAX_INTERMEDIATE = 4; /// number. I implore TUI authors to not use more than this number of CSI /// params, but I suspect we'll introduce a slow path with heap allocation /// one day. -const MAX_PARAMS = 24; +pub const MAX_PARAMS = 24; /// Current state of the state machine state: State, diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index e4b85fbdd..d589172ad 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -134,7 +134,7 @@ pub const Parser = struct { self.idx += 1; return .{ .unknown = .{ .full = self.params, - .partial = slice[0 .. self.idx - start + 1], + .partial = slice[0..@min(self.idx - start + 1, slice.len)], } }; }, }; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index ce09cbda2..c9bb50158 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -249,7 +249,7 @@ pub fn Stream(comptime Handler: type) type { // the parser state to ground. 0x18, 0x1A => self.parser.state = .ground, // A parameter digit: - '0'...'9' => if (self.parser.params_idx < 16) { + '0'...'9' => if (self.parser.params_idx < Parser.MAX_PARAMS) { self.parser.param_acc *|= 10; self.parser.param_acc +|= c - '0'; // The parser's CSI param action uses param_acc_idx @@ -259,7 +259,7 @@ pub fn Stream(comptime Handler: type) type { self.parser.param_acc_idx |= 1; }, // A parameter separator: - ':', ';' => if (self.parser.params_idx < 16) { + ':', ';' => if (self.parser.params_idx < Parser.MAX_PARAMS) { self.parser.params[self.parser.params_idx] = self.parser.param_acc; if (c == ':') self.parser.params_sep.set(self.parser.params_idx); self.parser.params_idx += 1; From a3f4997fbc75a487dd73dc57c2ba43cb2e324597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Wed, 27 Aug 2025 14:16:37 +0900 Subject: [PATCH 55/75] fix(terminal): handle CSI/SGR with many parameters Adds tests to ensure CSI and SGR sequences with 17 or more parameters are correctly parsed, fixing a bug where later parameters were previously dropped. --- src/terminal/Parser.zig | 25 +++++++++++++++++++++++++ src/terminal/stream.zig | 19 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 92f405a23..5fbf214a6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -949,6 +949,31 @@ test "csi: too many params" { } } +test "csi: 17 parameters" { + // Test with exactly 17 parameters (the Kakoune case) + var p = init(); + _ = p.next(0x1B); + _ = p.next('['); + // Build 17 parameters separated by semicolons + for (0..16) |_| { + _ = p.next('1'); + _ = p.next(';'); + } + _ = p.next('2'); // 17th parameter + + { + const a = p.next('H'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[2] == null); + + const csi = a[1].?.csi_dispatch; + try testing.expectEqual(@as(usize, 17), csi.params.len); + try testing.expectEqual(@as(u16, 2), csi.params[16]); + } +} + test "dcs: XTGETTCAP" { var p = init(); _ = p.next(0x1B); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index c9bb50158..3009935ec 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -2601,3 +2601,22 @@ test "stream CSI ? W reset tab stops" { try s.nextSlice("\x1b[?1;2;3W"); try testing.expect(s.handler.reset); } + +test "stream: SGR with 17+ parameters for underline color" { + const H = struct { + attrs: ?sgr.Attribute = null, + called: bool = false, + + pub fn setAttribute(self: *@This(), attr: sgr.Attribute) !void { + self.attrs = attr; + self.called = true; + } + }; + + var s: Stream(H) = .init(.{}); + + // Kakoune-style SGR with underline color as 17th parameter + // This tests the fix where param 17 was being dropped + try s.nextSlice("\x1b[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136;0m"); + try testing.expect(s.handler.called); +} From adfc93047c58102914e5be211469b7ac77514764 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Aug 2025 07:15:39 -0700 Subject: [PATCH 56/75] terminal: fix up some tests to be more robust --- src/terminal/Parser.zig | 46 +++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 5fbf214a6..428274878 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -193,7 +193,7 @@ pub const Action = union(enum) { /// Maximum number of intermediate characters during parsing. This is /// 4 because we also use the intermediates array for UTF8 decoding which /// can be at most 4 bytes. -const MAX_INTERMEDIATE = 4; +pub const MAX_INTERMEDIATE = 4; /// Maximum number of CSI parameters. This is arbitrary. Practically, the /// only CSI command that uses more than 3 parameters is the SGR command @@ -949,28 +949,52 @@ test "csi: too many params" { } } -test "csi: 17 parameters" { - // Test with exactly 17 parameters (the Kakoune case) +test "csi: sgr with up to our max parameters" { + for (1..MAX_PARAMS + 1) |max| { + var p = init(); + _ = p.next(0x1B); + _ = p.next('['); + + for (0..max - 1) |_| { + _ = p.next('1'); + _ = p.next(';'); + } + _ = p.next('2'); + + { + const a = p.next('H'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[2] == null); + + const csi = a[1].?.csi_dispatch; + try testing.expectEqual(@as(usize, max), csi.params.len); + try testing.expectEqual(@as(u16, 2), csi.params[max - 1]); + } + } +} + +test "csi: sgr beyond our max drops it" { + // Has to be +2 for the loops below + const max = MAX_PARAMS + 2; + var p = init(); _ = p.next(0x1B); _ = p.next('['); - // Build 17 parameters separated by semicolons - for (0..16) |_| { + + for (0..max - 1) |_| { _ = p.next('1'); _ = p.next(';'); } - _ = p.next('2'); // 17th parameter + _ = p.next('2'); { const a = p.next('H'); try testing.expect(p.state == .ground); try testing.expect(a[0] == null); - try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[1] == null); try testing.expect(a[2] == null); - - const csi = a[1].?.csi_dispatch; - try testing.expectEqual(@as(usize, 17), csi.params.len); - try testing.expectEqual(@as(u16, 2), csi.params[16]); } } From f54f2dc7f344a4ded754441c2d3e1e91d5b46824 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 Aug 2025 09:42:29 -0700 Subject: [PATCH 57/75] apprt/gtk-ng: "cancel" should be default close response for dialog Fixes #8424 This is the response that will be sent when "escape" is pressed. This also fixes a null ptr deref that was possible when this fix wasn't in. --- src/apprt/gtk-ng/class/application.zig | 10 +++++++++- src/apprt/gtk-ng/class/close_confirmation_dialog.zig | 2 +- src/apprt/gtk-ng/class/tab.zig | 1 - src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp | 2 ++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 05b24f064..6b866cecb 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -499,7 +499,15 @@ pub const Application = extern struct { const parent: ?*gtk.Widget = parent: { const list = gtk.Window.listToplevels(); defer list.free(); - const focused = list.findCustom(null, findActiveWindow); + const focused = @as(?*glib.List, list.findCustom( + null, + findActiveWindow, + )) orelse { + // If we have an active surface then we should have + // a window available but in the rare case we don't we + // should exit so we don't crash. + break :parent null; + }; break :parent @ptrCast(@alignCast(focused.f_data)); }; diff --git a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig index 3debafbb5..e806eb354 100644 --- a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig +++ b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig @@ -10,7 +10,7 @@ const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; const Dialog = @import("dialog.zig").Dialog; -const log = std.log.scoped(.gtk_ghostty_config_errors_dialog); +const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog); pub const CloseConfirmationDialog = extern struct { const Self = @This(); diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 20672abe4..d8f9b97f8 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -18,7 +18,6 @@ const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; const Application = @import("application.zig").Application; -const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const SplitTree = @import("split_tree.zig").SplitTree; const Surface = @import("surface.zig").Surface; diff --git a/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp b/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp index c2dcbadbd..f58ef523c 100644 --- a/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp @@ -7,4 +7,6 @@ template $GhosttyCloseConfirmationDialog: $GhosttyDialog { cancel: _("Cancel"), close: _("Close") destructive, ] + + close-response: "cancel"; } From f1ea30dcf15d41f6ed1e0277ee1e9ff5bcf68b44 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 28 Aug 2025 11:26:26 -0700 Subject: [PATCH 58/75] macos: when executing a script directly, always wait after command --- include/ghostty.h | 1 + macos/Sources/App/macOS/AppDelegate.swift | 5 +++++ macos/Sources/Ghostty/SurfaceView.swift | 6 ++++++ src/apprt/embedded.zig | 8 ++++++++ 4 files changed, 20 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index 6fd218e9e..574cc6278 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -419,6 +419,7 @@ typedef struct { ghostty_env_var_s* env_vars; size_t env_var_count; const char* initial_input; + bool wait_after_command; } ghostty_surface_config_s; typedef struct { diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c00025bf5..3f0a15b6a 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -412,6 +412,11 @@ class AppDelegate: NSObject, // due to things like Homebrew. Instead, we set the command to // `; exit` which is what Terminal and iTerm2 do. config.initialInput = "\(filename); exit\n" + + // For commands executed directly, we want to ensure we wait after exit + // because in most cases scripts don't block on exit and we don't want + // the window to just flash closed once complete. + config.waitAfterCommand = true // Set the parent directory to our working directory so that relative // paths in scripts work. diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index aa4de5178..039245429 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -424,6 +424,9 @@ extension Ghostty { /// Extra input to send as stdin var initialInput: String? = nil + + /// Wait after the command + var waitAfterCommand: Bool = false init() {} @@ -475,6 +478,9 @@ extension Ghostty { // Zero is our default value that means to inherit the font size. config.font_size = fontSize ?? 0 + + // Set wait after command + config.wait_after_command = waitAfterCommand // 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 bd1ffd460..c4f0fe18c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -447,6 +447,9 @@ pub const Surface = struct { /// Input to send to the command after it is started. initial_input: ?[*:0]const u8 = null, + + // Wait after the command exits + wait_after_command: bool = false, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -540,6 +543,11 @@ pub const Surface = struct { ); } + // Wait after command + if (opts.wait_after_command) { + config.@"wait-after-command" = true; + } + // Initialize our surface right away. We're given a view that is // ready to use. try self.core_surface.init( From 04956f3dc1e721f4b6efee9b2cb69f7e87524de7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 28 Aug 2025 12:33:28 -0700 Subject: [PATCH 59/75] macos: require confirmation to run any script --- macos/Sources/App/macOS/AppDelegate.swift | 39 ++++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 3f0a15b6a..310a46d6c 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -394,18 +394,29 @@ class AppDelegate: NSObject, // Ghostty will validate as well but we can avoid creating an entirely new // surface by doing our own validation here. We can also show a useful error // this way. - + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } - + + // Set to true if confirmation is required before starting up the + // new terminal. + var requiresConfirm: Bool = false + // Initialize the surface config which will be used to create the tab or window for the opened file. var config = Ghostty.SurfaceConfiguration() - + if (isDirectory.boolValue) { // When opening a directory, check the configuration to decide // whether to open in a new tab or new window. config.workingDirectory = filename } else { + // Unconditionally require confirmation in the file execution case. + // In the future I have ideas about making this more fine-grained if + // we can not inherit of unsandboxed state. For now, we need to confirm + // because there is a sandbox escape possible if a sandboxed application + // somehow is tricked into `open`-ing a non-sandboxed application. + requiresConfirm = true + // When opening a file, we want to execute the file. To do this, we // don't override the command directly, because it won't load the // profile/rc files for the shell, which is super important on macOS @@ -417,17 +428,35 @@ class AppDelegate: NSObject, // because in most cases scripts don't block on exit and we don't want // the window to just flash closed once complete. config.waitAfterCommand = true - + // Set the parent directory to our working directory so that relative // paths in scripts work. config.workingDirectory = (filename as NSString).deletingLastPathComponent } + if requiresConfirm { + // Confirmation required. We use an app-wide NSAlert for now. In the future we + // may want to show this as a sheet on the focused window (especially if we're + // opening a tab). I'm not sure. + let alert = NSAlert() + alert.messageText = "Allow Ghostty to execute \"\(filename)\"?" + alert.addButton(withTitle: "Allow") + alert.addButton(withTitle: "Cancel") + alert.alertStyle = .warning + switch (alert.runModal()) { + case .alertFirstButtonReturn: + break + + default: + return false + } + } + switch ghostty.config.macosDockDropBehavior { case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } - + return true } From 9962e523a8497fd0b2ee41532bd844a98cf9a363 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 28 Aug 2025 12:58:00 -0700 Subject: [PATCH 60/75] some typos --- src/apprt/embedded.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c4f0fe18c..e4961ac49 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -448,7 +448,7 @@ pub const Surface = struct { /// Input to send to the command after it is started. initial_input: ?[*:0]const u8 = null, - // Wait after the command exits + /// Wait after the command exits wait_after_command: bool = false, }; From 2701932475d2b4e0b44482c6603359b5f44a49aa Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 29 Aug 2025 03:28:03 +0800 Subject: [PATCH 61/75] docs: separate out HACKING.md from README.md Also did some copy-editing and removed some things that haven't been present in the codebase for a while (I thought we nuked conformance stuff in 1.1 already...?) --- HACKING.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 116 +----------------------------------------------- 2 files changed, 128 insertions(+), 114 deletions(-) create mode 100644 HACKING.md diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 000000000..ed356edd1 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,126 @@ +# Developing Ghostty + +To start development on Ghostty, you need to build Ghostty from a Git checkout, +which is very similar in process to [building Ghostty from a source tarball](http://ghostty.org/docs/install/build). One key difference is that obviously +you need to clone the Git repository instead of unpacking the source tarball: + +```shell +git clone https://github.com/ghostty-org/ghostty +cd ghostty +``` + +> [!NOTE] +> +> Ghostty may require [extra dependencies](#extra-dependencies) +> when building from a Git checkout compared to a source tarball. +> Tip versions may also require a different version of Zig or other toolchains +> (e.g. the Xcode SDK on macOS) compared to stable versions — make sure to +> follow the steps closely! + +When you're developing Ghostty, it's very likely that you will want to build a +_debug_ build to diagnose issues more easily. This is already the default for +Zig builds, so simply run `zig build` **without any `-Doptimize` flags**. + +There are many more build steps than just `zig build`, some of which are listed +here: + +| Command | Description | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `zig build run` | Runs Ghostty | +| `zig build run-valgrind` | Runs Ghostty under Valgrind to detect memory misuses | +| `zig build test` | Runs unit tests (accepts `-Dtest-filter=` to only run tests whose name matches the filter) | +| `zig build update-translations` | Updates Ghostty's translation strings (see the [Contributor's Guide on Localizing Ghostty](po/README_CONTRIBUTORS.md)) | +| `zig build dist` | Builds a source tarball | +| `zig build distcheck` | Installs and validates a source tarball | + +## Extra Dependencies + +Building Ghostty from a Git checkout on Linux requires some additional +dependencies: + +- `blueprint-compiler` (version 0.16.0 or newer) + +macOS users don't require any additional dependencies. + +## Xcode Version and SDKs + +Building the Ghostty macOS app requires that Xcode, the macOS SDK, +and the iOS SDK are all installed. + +A common issue is that the incorrect version of Xcode is either +installed or selected. Use the `xcode-select` command to +ensure that the correct version of Xcode is selected: + +```shell-session +sudo xcode-select --switch /Applications/Xcode-beta.app +``` + +> [!IMPORTANT] +> +> Main branch development of Ghostty is preparing for the next major +> macOS release, Tahoe (macOS 26). Therefore, the main branch requires +> **Xcode 26 and the macOS 26 SDK**. +> +> You do not need to be running on macOS 26 to build Ghostty, you can +> still use Xcode 26 beta on macOS 15 stable. + +## Linting + +### Prettier + +Ghostty's docs and resources (not including Zig code) are linted using +[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI +check will fail builds with improper formatting. Therefore, if you are +modifying anything Prettier will lint, you may want to install it locally and +run this from the repo root before you commit: + +``` +prettier --write . +``` + +Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). + +Nix users can use the following command to format with Prettier: + +``` +nix develop -c prettier --write . +``` + +### Alejandra + +Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check +will fail builds with improper formatting. + +Nix users can use the following command to format with Alejandra: + +``` +nix develop -c alejandra . +``` + +Non-Nix users should install Alejandra and use the following command to format with Alejandra: + +``` +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). + +### Updating the Zig Cache Fixed-Output Derivation Hash + +The Nix package depends on a [fixed-output +derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash) +that manages the Zig package cache. This allows the package to be built in the +Nix sandbox. + +Occasionally (usually when `build.zig.zon` is updated), the hash that +identifies the cache will need to be updated. There are jobs that monitor the +hash in CI, and builds will fail if it drifts. + +To update it, you can run the following in the repository root: + +``` +./nix/build-support/check-zig-cache-hash.sh --update +``` + +This will write out the `nix/zigCacheHash.nix` file with the updated hash +that can then be committed and pushed to fix the builds. diff --git a/README.md b/README.md index a761e25ce..d932b4586 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ · Documentation · - Developing + Developing

@@ -187,116 +187,4 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us. ## Developing Ghostty -See the documentation on the Ghostty website for -[building Ghostty from a source tarball](http://ghostty.org/docs/install/build). -Building Ghostty from a Git checkout is very similar, except you want to -omit the `-Doptimize` flag to build a debug build, and you may require -additional dependencies since the source tarball includes some processed -files that are not in the Git repository. - -Other useful commands: - -- `zig build test` for running unit tests. -- `zig build test -Dtest-filter=` for running a specific subset of those unit tests -- `zig build run -Dconformance=` runs a conformance test case from - the `conformance` directory. The `name` is the name of the file. This runs - in the current running terminal emulator so if you want to check the - behavior of this project, you must run this command in Ghostty. - -### Extra Dependencies - -Building Ghostty from a Git checkout on Linux requires some additional -dependencies: - -- `blueprint-compiler` - -macOS users don't require any additional dependencies. - -> [!NOTE] -> This only applies to building from a _Git checkout_. This section does -> not apply if you're building from a released _source tarball_. For -> source tarballs, see the -> [website](http://ghostty.org/docs/install/build). - -### Xcode Version and SDKs - -Building the Ghostty macOS app requires that Xcode, the macOS SDK, -and the iOS SDK are all installed. - -A common issue is that the incorrect version of Xcode is either -installed or selected. Use the `xcode-select` command to -ensure that the correct version of Xcode is selected: - -```shell-session -sudo xcode-select --switch /Applications/Xcode-beta.app -``` - -> [!IMPORTANT] -> -> Main branch development of Ghostty is preparing for the next major -> macOS release, Tahoe (macOS 26). Therefore, the main branch requires -> **Xcode 26 and the macOS 26 SDK**. -> -> You do not need to be running on macOS 26 to build Ghostty, you can -> still use Xcode 26 beta on macOS 15 stable. - -### Linting - -#### Prettier - -Ghostty's docs and resources (not including Zig code) are linted using -[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI -check will fail builds with improper formatting. Therefore, if you are -modifying anything Prettier will lint, you may want to install it locally and -run this from the repo root before you commit: - -``` -prettier --write . -``` - -Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). - -Nix users can use the following command to format with Prettier: - -``` -nix develop -c prettier --write . -``` - -#### Alejandra - -Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check -will fail builds with improper formatting. - -Nix users can use the following command to format with Alejandra: - -``` -nix develop -c alejandra . -``` - -Non-Nix users should install Alejandra and use the following command to format with Alejandra: - -``` -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). - -#### Updating the Zig Cache Fixed-Output Derivation Hash - -The Nix package depends on a [fixed-output -derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash) -that manages the Zig package cache. This allows the package to be built in the -Nix sandbox. - -Occasionally (usually when `build.zig.zon` is updated), the hash that -identifies the cache will need to be updated. There are jobs that monitor the -hash in CI, and builds will fail if it drifts. - -To update it, you can run the following in the repository root: - -``` -./nix/build-support/check-zig-cache-hash.sh --update -``` - -This will write out the `nix/zigCacheHash.nix` file with the updated hash -that can then be committed and pushed to fix the builds. +Please check out our [HACKING.md](HACKING.md) for more details. From f802d336525750a81066676a3d20f135deb5e522 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 29 Aug 2025 05:20:52 +0800 Subject: [PATCH 62/75] docs: divide content more evenly between CONTRIBUTING and HACKING CONTRIBUTING should now solely be about the contribution *process* while HACKING goes into the technical details --- CONTRIBUTING.md | 264 +++++++----------------------------------------- HACKING.md | 205 ++++++++++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 227 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e988704b..1f6d8193d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -# Ghostty Development Process +# Contributing to Ghostty -This document describes the development process for Ghostty. It is intended for -anyone considering opening an **issue** or **pull request**. If in doubt, -please open a [discussion](https://github.com/ghostty-org/ghostty/discussions); -we can always convert that to an issue later. +This document describes the process of contributing to Ghostty. It is intended +for anyone considering opening an **issue**, **discussion** or **pull request**. +For people who are interested in developing Ghostty and technical details behind +it, please check out our ["Developing Ghostty"](HACKING.md) document as well. > [!NOTE] > @@ -49,13 +49,16 @@ Please be respectful to maintainers and disclose AI assistance. ## Quick Guide -**I'd like to contribute!** +### I'd like to contribute! -All issues are actionable. Pick one and start working on it. Thank you. -If you need help or guidance, comment on the issue. Issues that are extra -friendly to new contributors are tagged with "contributor friendly". +[All issues are actionable](#issues-are-actionable). Pick one and start +working on it. Thank you. If you need help or guidance, comment on the issue. +Issues that are extra friendly to new contributors are tagged with +["contributor friendly"]. -**I'd like to translate Ghostty to my language!** +["contributor friendly"]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22 + +### I'd like to translate Ghostty to my language! We have written a [Translator's Guide](po/README_TRANSLATORS.md) for everyone interested in contributing translations to Ghostty. @@ -64,25 +67,39 @@ and you can submit pull requests directly, although please make sure that our [Style Guide](po/README_TRANSLATORS.md#style-guide) is followed before submission. -**I have a bug!** +### I have a bug! / Something isn't working! -1. Search the issue tracker and discussions for similar issues. -2. If you don't have steps to reproduce, open a discussion. -3. If you have steps to reproduce, open an issue. +1. Search the issue tracker and discussions for similar issues. Tip: also + search for [closed issues] and [discussions] — your issue might have already + been fixed! +2. If your issue hasn't been reported already, open an ["Issue Triage" discussion] + and make sure to fill in the template **completely**. They are vital for + maintainers to figure out important details about your setup. Because of + this, please make sure that you _only_ use the "Issue Triage" category for + reporting bugs — thank you! -**I have an idea for a feature!** +[closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed +[discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed +["Issue Triage" discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage -1. Open a discussion. +### I have an idea for a feature! -**I've implemented a feature!** +Open a discussion in the ["Feature Requests, Ideas" category](https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas). -1. If there is an issue for the feature, open a pull request. +### I've implemented a feature! + +1. If there is an issue for the feature, open a pull request straight away. 2. If there is no issue, open a discussion and link to your branch. -3. If you want to live dangerously, open a pull request and hope for the best. +3. If you want to live dangerously, open a pull request and + [hope for the best](#pull-requests-implement-an-issue). -**I have a question!** +### I have a question! -1. Open a discussion or use Discord. +Open an [Q&A discussion], or join our [Discord Server] and ask away in the +`#help` channel. + +[Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a +[Discord Server]: https://discord.gg/ghostty ## General Patterns @@ -121,208 +138,3 @@ pull request will be accepted with a high degree of certainty. > not open a WIP pull request to discuss a feature. Instead, use a discussion > and link to your branch. -# Developer Guide - -> [!NOTE] -> -> **The remainder of this file is dedicated to developers actively -> working on Ghostty.** If you're a user reporting an issue, you can -> ignore the rest of this document. - -## Including and Updating Translations - -See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details. - -## Checking for Memory Leaks - -While Zig does an amazing job of finding and preventing memory leaks, -Ghostty uses many third-party libraries that are written in C. Improper usage -of those libraries or bugs in those libraries can cause memory leaks that -Zig cannot detect by itself. - -### On Linux - -On Linux the recommended tool to check for memory leaks is Valgrind. The -recommended way to run Valgrind is via `zig build`: - -```sh -zig build run-valgrind -``` - -This builds a Ghostty executable with Valgrind support and runs Valgrind -with the proper flags to ensure we're suppressing known false positives. - -You can combine the same build args with `run-valgrind` that you can with -`run`, such as specifying additional configurations after a trailing `--`. - -## Input Stack Testing - -The input stack is the part of the codebase that starts with a -key event and ends with text encoding being sent to the pty (it -does not include _rendering_ the text, which is part of the -font or rendering stack). - -If you modify any part of the input stack, you must manually verify -all the following input cases work properly. We unfortunately do -not automate this in any way, but if we can do that one day that'd -save a LOT of grief and time. - -Note: this list may not be exhaustive, I'm still working on it. - -### Linux IME - -IME (Input Method Editors) are a common source of bugs in the input stack, -especially on Linux since there are multiple different IME systems -interacting with different windowing systems and application frameworks -all written by different organizations. - -The following matrix should be tested to ensure that all IME input works -properly: - -1. Wayland, X11 -2. ibus, fcitx, none -3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex -4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors) - -> [!NOTE] -> -> This is a **work in progress**. I'm still working on this list and it -> is not complete. As I find more test cases, I will add them here. - -#### Dead Key Input - -Set your keyboard layout to "Spanish" (or another layout that uses dead keys). - -1. Launch Ghostty -2. Press `'` -3. Press `a` -4. Verify that `á` is displayed - -Note that the dead key may or may not show a preedit state visually. -For ibus and fcitx it does but for the "none" case it does not. Importantly, -the text should be correct when it is sent to the pty. - -We should also test canceling dead key input: - -1. Launch Ghostty -2. Press `'` -3. Press escape -4. Press `a` -5. Verify that `a` is displayed (no diacritic) - -#### CJK Input - -Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The -exact layout doesn't matter. - -1. Launch Ghostty -2. Press `Ctrl+Shift` to switch to "Hiragana" -3. On a US physical layout, type: `konn`, you should see `こん` in preedit. -4. Press `Enter` -5. Verify that `こん` is displayed in the terminal. - -We should also test switching input methods while preedit is active, which -should commit the text: - -1. Launch Ghostty -2. Press `Ctrl+Shift` to switch to "Hiragana" -3. On a US physical layout, type: `konn`, you should see `こん` in preedit. -4. Press `Ctrl+Shift` to switch to another layout (any) -5. Verify that `こん` is displayed in the terminal as committed text. - -## Nix Virtual Machines - -Several Nix virtual machine definitions are provided by the project for testing -and developing Ghostty against multiple different Linux desktop environments. - -Running these requires a working Nix installation, either Nix on your -favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further -requirements for macOS are detailed below. - -VMs should only be run on your local desktop and then powered off when not in -use, which will discard any changes to the VM. - -The VM definitions provide minimal software "out of the box" but additional -software can be installed by using standard Nix mechanisms like `nix run nixpkgs#`. - -### Linux - -1. Check out the Ghostty source and change to the directory. -2. Run `nix run .#`. `` can be any of the VMs defined in the - `nix/vm` directory (without the `.nix` suffix) excluding any file prefixed - with `common` or `create`. -3. The VM will build and then launch. Depending on the speed of your system, this - can take a while, but eventually you should get a new VM window. -4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending - on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be - writable by the VM user, so be careful! - -### macOS - -1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin` - config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your - configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/) - blog post for more information about the Linux builder and how to tune the performance. -2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions - above to launch a VM. - -### Custom VMs - -To easily create a custom VM without modifying the Ghostty source, create a new -directory, then create a file called `flake.nix` with the following text in the -new directory. - -``` -{ - inputs = { - nixpkgs.url = "nixpkgs/nixpkgs-unstable"; - ghostty.url = "github:ghostty-org/ghostty"; - }; - outputs = { - nixpkgs, - ghostty, - ... - }: { - nixosConfigurations.custom-vm = ghostty.create-gnome-vm { - nixpkgs = nixpkgs; - system = "x86_64-linux"; - overlay = ghostty.overlays.releasefast; - # module = ./configuration.nix # also works - module = {pkgs, ...}: { - environment.systemPackages = [ - pkgs.btop - ]; - }; - }; - }; -} -``` - -The custom VM can then be run with a command like this: - -``` -nix run .#nixosConfigurations.custom-vm.config.system.build.vm -``` - -A file named `ghostty.qcow2` will be created that is used to persist any changes -made in the VM. To "reset" the VM to default delete the file and it will be -recreated the next time you run the VM. - -### Contributing new VM definitions - -#### VM Acceptance Criteria - -We welcome the contribution of new VM definitions, as long as they meet the following criteria: - -1. They should be different enough from existing VM definitions that they represent a distinct - user (and developer) experience. -2. There's a significant Ghostty user population that uses a similar environment. -3. The VMs can be built using only packages from the current stable NixOS release. - -#### VM Definition Criteria - -1. VMs should be as minimal as possible so that they build and launch quickly. - Additional software can be added at runtime with a command like `nix run nixpkgs#`. -2. VMs should not expose any services to the network, or run any remote access - software like SSH daemons, VNC or RDP. -3. VMs should auto-login using the "ghostty" user. diff --git a/HACKING.md b/HACKING.md index ed356edd1..d79d15a4a 100644 --- a/HACKING.md +++ b/HACKING.md @@ -1,5 +1,10 @@ # Developing Ghostty +This document describes the technical details behind Ghostty's development. +If you'd like to open any pull requests or would like to implement new features +into Ghostty, please make sure to read our ["Contributing to Ghostty"](CONTRIBUTING.md) +document first. + To start development on Ghostty, you need to build Ghostty from a Git checkout, which is very similar in process to [building Ghostty from a source tarball](http://ghostty.org/docs/install/build). One key difference is that obviously you need to clone the Git repository instead of unpacking the source tarball: @@ -27,7 +32,7 @@ here: | Command | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `zig build run` | Runs Ghostty | -| `zig build run-valgrind` | Runs Ghostty under Valgrind to detect memory misuses | +| `zig build run-valgrind` | Runs Ghostty under Valgrind to [check for memory leaks](#checking-for-memory-leaks) | | `zig build test` | Runs unit tests (accepts `-Dtest-filter=` to only run tests whose name matches the filter) | | `zig build update-translations` | Updates Ghostty's translation strings (see the [Contributor's Guide on Localizing Ghostty](po/README_CONTRIBUTORS.md)) | | `zig build dist` | Builds a source tarball | @@ -124,3 +129,201 @@ To update it, you can run the following in the repository root: This will write out the `nix/zigCacheHash.nix` file with the updated hash that can then be committed and pushed to fix the builds. + +## Including and Updating Translations + +See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details. + +## Checking for Memory Leaks + +While Zig does an amazing job of finding and preventing memory leaks, +Ghostty uses many third-party libraries that are written in C. Improper usage +of those libraries or bugs in those libraries can cause memory leaks that +Zig cannot detect by itself. + +### On Linux + +On Linux the recommended tool to check for memory leaks is Valgrind. The +recommended way to run Valgrind is via `zig build`: + +```sh +zig build run-valgrind +``` + +This builds a Ghostty executable with Valgrind support and runs Valgrind +with the proper flags to ensure we're suppressing known false positives. + +You can combine the same build args with `run-valgrind` that you can with +`run`, such as specifying additional configurations after a trailing `--`. + +## Input Stack Testing + +The input stack is the part of the codebase that starts with a +key event and ends with text encoding being sent to the pty (it +does not include _rendering_ the text, which is part of the +font or rendering stack). + +If you modify any part of the input stack, you must manually verify +all the following input cases work properly. We unfortunately do +not automate this in any way, but if we can do that one day that'd +save a LOT of grief and time. + +Note: this list may not be exhaustive, I'm still working on it. + +### Linux IME + +IME (Input Method Editors) are a common source of bugs in the input stack, +especially on Linux since there are multiple different IME systems +interacting with different windowing systems and application frameworks +all written by different organizations. + +The following matrix should be tested to ensure that all IME input works +properly: + +1. Wayland, X11 +2. ibus, fcitx, none +3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex +4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors) + +> [!NOTE] +> +> This is a **work in progress**. I'm still working on this list and it +> is not complete. As I find more test cases, I will add them here. + +#### Dead Key Input + +Set your keyboard layout to "Spanish" (or another layout that uses dead keys). + +1. Launch Ghostty +2. Press `'` +3. Press `a` +4. Verify that `á` is displayed + +Note that the dead key may or may not show a preedit state visually. +For ibus and fcitx it does but for the "none" case it does not. Importantly, +the text should be correct when it is sent to the pty. + +We should also test canceling dead key input: + +1. Launch Ghostty +2. Press `'` +3. Press escape +4. Press `a` +5. Verify that `a` is displayed (no diacritic) + +#### CJK Input + +Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The +exact layout doesn't matter. + +1. Launch Ghostty +2. Press `Ctrl+Shift` to switch to "Hiragana" +3. On a US physical layout, type: `konn`, you should see `こん` in preedit. +4. Press `Enter` +5. Verify that `こん` is displayed in the terminal. + +We should also test switching input methods while preedit is active, which +should commit the text: + +1. Launch Ghostty +2. Press `Ctrl+Shift` to switch to "Hiragana" +3. On a US physical layout, type: `konn`, you should see `こん` in preedit. +4. Press `Ctrl+Shift` to switch to another layout (any) +5. Verify that `こん` is displayed in the terminal as committed text. + +## Nix Virtual Machines + +Several Nix virtual machine definitions are provided by the project for testing +and developing Ghostty against multiple different Linux desktop environments. + +Running these requires a working Nix installation, either Nix on your +favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further +requirements for macOS are detailed below. + +VMs should only be run on your local desktop and then powered off when not in +use, which will discard any changes to the VM. + +The VM definitions provide minimal software "out of the box" but additional +software can be installed by using standard Nix mechanisms like `nix run nixpkgs#`. + +### Linux + +1. Check out the Ghostty source and change to the directory. +2. Run `nix run .#`. `` can be any of the VMs defined in the + `nix/vm` directory (without the `.nix` suffix) excluding any file prefixed + with `common` or `create`. +3. The VM will build and then launch. Depending on the speed of your system, this + can take a while, but eventually you should get a new VM window. +4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending + on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be + writable by the VM user, so be careful! + +### macOS + +1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin` + config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your + configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/) + blog post for more information about the Linux builder and how to tune the performance. +2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions + above to launch a VM. + +### Custom VMs + +To easily create a custom VM without modifying the Ghostty source, create a new +directory, then create a file called `flake.nix` with the following text in the +new directory. + +``` +{ + inputs = { + nixpkgs.url = "nixpkgs/nixpkgs-unstable"; + ghostty.url = "github:ghostty-org/ghostty"; + }; + outputs = { + nixpkgs, + ghostty, + ... + }: { + nixosConfigurations.custom-vm = ghostty.create-gnome-vm { + nixpkgs = nixpkgs; + system = "x86_64-linux"; + overlay = ghostty.overlays.releasefast; + # module = ./configuration.nix # also works + module = {pkgs, ...}: { + environment.systemPackages = [ + pkgs.btop + ]; + }; + }; + }; +} +``` + +The custom VM can then be run with a command like this: + +``` +nix run .#nixosConfigurations.custom-vm.config.system.build.vm +``` + +A file named `ghostty.qcow2` will be created that is used to persist any changes +made in the VM. To "reset" the VM to default delete the file and it will be +recreated the next time you run the VM. + +### Contributing new VM definitions + +#### VM Acceptance Criteria + +We welcome the contribution of new VM definitions, as long as they meet the following criteria: + +1. They should be different enough from existing VM definitions that they represent a distinct + user (and developer) experience. +2. There's a significant Ghostty user population that uses a similar environment. +3. The VMs can be built using only packages from the current stable NixOS release. + +#### VM Definition Criteria + +1. VMs should be as minimal as possible so that they build and launch quickly. + Additional software can be added at runtime with a command like `nix run nixpkgs#`. +2. VMs should not expose any services to the network, or run any remote access + software like SSH daemons, VNC or RDP. +3. VMs should auto-login using the "ghostty" user. From f91e6f1764bb8dea3c9e9c77ee39642d85db22b3 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 29 Aug 2025 05:48:15 +0800 Subject: [PATCH 63/75] docs: better integrate CONTRIBUTING into the README --- CONTRIBUTING.md | 1 - README.md | 14 ++++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f6d8193d..777771145 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,4 +137,3 @@ pull request will be accepted with a high degree of certainty. > **Pull requests are NOT a place to discuss feature design.** Please do > not open a WIP pull request to discuss a feature. Instead, use a discussion > and link to your branch. - diff --git a/README.md b/README.md index d932b4586..df86f7830 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ · Documentation · + Contributing + · Developing

@@ -49,6 +51,14 @@ See the [download page](https://ghostty.org/download) on the Ghostty website. See the [documentation](https://ghostty.org/docs) on the Ghostty website. +## Contributing and Developing + +If you have any ideas, issues, etc. regarding Ghostty, or would like to +contribute to Ghostty through pull requests, please check out our +["Contributing to Ghostty"](CONTRIBUTING.md) document. Those who would like +to get involved with Ghostty's development as well should also read the +["Developing Ghostty"](HACKING.md) document for more technical details. + ## Roadmap and Status The high-level ambitious plan for the project, in order: @@ -184,7 +194,3 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us. > stack memory of each thread at the time of the crash. This information > is used to rebuild the stack trace but can also contain sensitive data > depending when the crash occurred. - -## Developing Ghostty - -Please check out our [HACKING.md](HACKING.md) for more details. From bed350f0be5f516805fb80920f260dfda32c3df3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:07:51 +0000 Subject: [PATCH 64/75] build(deps): bump namespacelabs/nscloud-cache-action Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.16 to 1.2.17. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/305bfa7ea980a858d511af4899414a84847c7991...a289cf5d2fcd6874376aa92f0ef7f99dc923592a) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.2.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/test.yml | 42 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 522847c88..336ad32ef 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index f7fb72f65..7f1525d75 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index bae096054..7a574179f 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5a2d1ef0..777b351a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -100,7 +100,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -136,7 +136,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -165,7 +165,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -198,7 +198,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -242,7 +242,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -377,7 +377,7 @@ jobs: mkdir dist tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -473,7 +473,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -518,7 +518,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -567,7 +567,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -615,7 +615,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -673,7 +673,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -701,7 +701,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -728,7 +728,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -755,7 +755,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -782,7 +782,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -809,7 +809,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -843,7 +843,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -870,7 +870,7 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -905,7 +905,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -992,7 +992,7 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 3848a7ad4..8975fa065 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix From 85e642097a574028ba2e1872cbc9aec0052d8be9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:08:00 +0000 Subject: [PATCH 65/75] build(deps): bump cachix/install-nix-action from 31.5.2 to 31.6.0 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.5.2 to 31.6.0. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md) - [Commits](https://github.com/cachix/install-nix-action/compare/fc6e360bedc9ee72d75e701397f0bb30dce77568...56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-version: 31.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/test.yml | 40 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 522847c88..a511fc1e5 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index f7fb72f65..eb11c69d5 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -89,7 +89,7 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index bae096054..0365cf495 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -112,7 +112,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5a2d1ef0..2ee7f70c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -76,7 +76,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -107,7 +107,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -143,7 +143,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -172,7 +172,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -205,7 +205,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -249,7 +249,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -480,7 +480,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -525,7 +525,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -574,7 +574,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -622,7 +622,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -678,7 +678,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -706,7 +706,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -733,7 +733,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -760,7 +760,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -787,7 +787,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -814,7 +814,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -848,7 +848,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -875,7 +875,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -912,7 +912,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -999,7 +999,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 3848a7ad4..11ce7ef5c 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -29,7 +29,7 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 From 5761f66f353e751c971cd45890fcb9ba5db9e642 Mon Sep 17 00:00:00 2001 From: Guilherme Nandi Tiscoski Date: Fri, 29 Aug 2025 08:26:37 -0500 Subject: [PATCH 66/75] i18n: update pt_BR translations (#8391) relative to https://github.com/ghostty-org/ghostty/issues/8344 --- po/pt_BR.UTF-8.po | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index d49ab9df7..1b7162f90 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -3,14 +3,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Gustavo Peres , 2025. +# Guilherme Tiscoski , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-06-20 10:19-0300\n" -"Last-Translator: Mário Victor Ribeiro Silva \n" +"PO-Revision-Date: 2025-08-25 11:46-0500\n" +"Last-Translator: Guilherme Tiscoski \n" "Language-Team: Brazilian Portuguese \n" "Language: pt_BR\n" @@ -89,7 +90,7 @@ msgstr "Dividir à direita" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Executar um comando…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -162,7 +163,7 @@ msgstr "Abrir configuração" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Paleta de comandos" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -210,12 +211,12 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Lembrar escolha para esta divisão" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recarregue a configuração para mostrar este aviso novamente" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -280,15 +281,15 @@ msgstr "Copiado para a área de transferência" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Área de transferência limpa" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comando executado com sucesso" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Comando falhou" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From d1e01ec5c3030e21e1bb3a6147aca0be0423997f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 29 Aug 2025 14:36:40 -0500 Subject: [PATCH 67/75] gtk-ng: ensure CSS works on both 4.14 and 4.16+ Ghostty 1.2 needs to support GTK 4.14 because that's the version that ships with Ubuntu 24.04. This PR ensures that any GTK 4.16 CSS features are not used in any static CSS and that the runtime CSS loading handles both 4.14 and 4.16+ appropriately. --- src/apprt/gtk-ng/class/application.zig | 143 ++++++++++++++++++++++--- src/apprt/gtk-ng/css/style.css | 16 +-- 2 files changed, 136 insertions(+), 23 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 6b866cecb..f0fda2680 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -732,27 +732,24 @@ pub const Application = extern struct { } } - fn loadRuntimeCss( - self: *Self, - ) Allocator.Error!void { + fn loadRuntimeCss(self: *Self) Allocator.Error!void { const alloc = self.allocator(); - var buf: std.ArrayListUnmanaged(u8) = .empty; + const config = self.private().config.get(); + + var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048); defer buf.deinit(alloc); const writer = buf.writer(alloc); - const config = self.private().config.get(); - const window_theme = config.@"window-theme"; const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; - const headerbar_background = config.@"window-titlebar-background" orelse config.background; - const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; try writer.print( \\widget.unfocused-split {{ \\ opacity: {d:.2}; \\ background-color: rgb({d},{d},{d}); \\}} + \\ , .{ 1.0 - config.@"unfocused-split-opacity", unfocused_fill.r, @@ -766,6 +763,7 @@ pub const Application = extern struct { \\ color: rgb({[r]d},{[g]d},{[b]d}); \\ background: rgb({[r]d},{[g]d},{[b]d}); \\}} + \\ , .{ .r = color.r, .g = color.g, @@ -778,9 +776,129 @@ pub const Application = extern struct { \\.window headerbar {{ \\ font-family: "{[font_family]s}"; \\}} + \\ , .{ .font_family = font_family }); } + try loadRuntimeCss414(config, &writer); + try loadRuntimeCss416(config, &writer); + + // ensure that we have a sentinel + try writer.writeByte(0); + + const data = buf.items[0 .. buf.items.len - 1 :0]; + + log.debug("runtime CSS is {d} bytes", .{data.len + 1}); + + // Clears any previously loaded CSS from this provider + loadCssProviderFromData( + self.private().css_provider, + data, + ); + } + + /// Load runtime CSS for older than GTK 4.16 + fn loadRuntimeCss414( + config: *const CoreConfig, + writer: *const std.ArrayListUnmanaged(u8).Writer, + ) Allocator.Error!void { + if (gtk_version.runtimeAtLeast(4, 16, 0)) return; + + const window_theme = config.@"window-theme"; + const headerbar_background = config.@"window-titlebar-background" orelse config.background; + const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; + + switch (window_theme) { + .ghostty => try writer.print( + \\windowhandle {{ + \\ background-color: rgb({d},{d},{d}); + \\ color: rgb({d},{d},{d}); + \\}} + \\windowhandle:backdrop {{ + \\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha); + \\}} + \\ + , .{ + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + }), + else => {}, + } + } + + /// Load runtime for GTK 4.16 and newer + fn loadRuntimeCss416( + config: *const CoreConfig, + writer: *const std.ArrayListUnmanaged(u8).Writer, + ) Allocator.Error!void { + if (gtk_version.runtimeUntil(4, 16, 0)) return; + + const window_theme = config.@"window-theme"; + const headerbar_background = config.@"window-titlebar-background" orelse config.background; + const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; + + try writer.writeAll( + \\/* + \\ * Child Exited Overlay + \\ */ + \\ + \\.child-exited.normal revealer widget { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--success-bg-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\.child-exited.abnormal revealer widget { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--error-bg-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\/* + \\ * Surface + \\ */ + \\ + \\.surface progressbar.error trough progress { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--error-bg-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\.surface .bell-overlay { + \\ border-color: color-mix( + \\ in srgb, + \\ var(--accent-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\/* + \\ * Splits + \\ */ + \\ + \\.window .split paned > separator { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--window-bg-color), + \\ transparent 0% + \\ ); + \\} + \\ + ); + switch (window_theme) { .ghostty => try writer.print( \\:root {{ @@ -813,15 +931,6 @@ pub const Application = extern struct { }), else => {}, } - - const data = try alloc.dupeZ(u8, buf.items); - defer alloc.free(data); - - // Clears any previously loaded CSS from this provider - loadCssProviderFromData( - self.private().css_provider, - data, - ); } fn loadCustomCss(self: *Self) !void { diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css index 5901d1d7e..5620c9ca4 100644 --- a/src/apprt/gtk-ng/css/style.css +++ b/src/apprt/gtk-ng/css/style.css @@ -12,7 +12,7 @@ window.ssd.no-border-radius { border-radius: 0 0; } -/* +/* * GhosttySurface URL overlay */ label.url-overlay { @@ -83,13 +83,13 @@ label.resize-overlay { */ .child-exited.normal revealer widget { background-color: rgba(38, 162, 105, 0.5); - /* after GTK 4.16 is a requirement, switch to the following: + /* after GTK 4.16 is a requirement, switch to the following: */ /* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */ } .child-exited.abnormal revealer widget { background-color: rgba(192, 28, 40, 0.5); - /* after GTK 4.16 is a requirement, switch to the following: + /* after GTK 4.16 is a requirement, switch to the following: */ /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */ } @@ -97,13 +97,15 @@ label.resize-overlay { * Surface */ .surface progressbar.error trough progress { - background-color: rgb(192, 28, 40); + background-color: rgba(192, 28, 40, 0.5); /* after GTK 4.16 is a requirement, switch to the following: */ - /* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */ + /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */ } .surface .bell-overlay { - border-color: color-mix(in srgb, var(--accent-color), transparent 50%); + border-color: rgba(58, 148, 74, 0.5); + /* after GTK 4.16 is a requirement, switch to the following: */ + /* background-color: color-mix(in srgb, var(--accent-color), transparent 50%); */ border-width: 3px; border-style: solid; } @@ -127,6 +129,8 @@ label.resize-overlay { .window .split paned > separator { background-color: rgba(250, 250, 250, 1); + /* after GTK 4.16 is a requirement, switch to the following: */ + /* background-color: color-mix(in srgb, var(--window-bg-color), transparent 0%); */ background-clip: content-box; /* This works around the oversized drag area for the right side of GtkPaned. From 0bc90b2a206202b1d7ccb47cec1fe22b6ca91cb8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 30 Aug 2025 13:58:25 -0500 Subject: [PATCH 68/75] ci: build on freebsd --- .github/workflows/test.yml | 55 +++++++++++++++++++++++++++++++++++++ src/apprt.zig | 16 ++++++----- src/benchmark/Benchmark.zig | 7 +++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28efc095b..8666d01d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: - build-bench - build-dist - build-flatpak + - build-freebsd - build-linux - build-linux-libghostty - build-nix @@ -1015,3 +1016,57 @@ jobs: - name: valgrind run: | nix develop -c zig build test-valgrind + + build-freebsd: + name: Build on FreeBSD + needs: test + runs-on: namespace-profile-mitchellh-sm-systemd + strategy: + matrix: + release: + - "14.3" + # - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108 + steps: + - name: Checkout Ghostty + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Start SSH + run: | + sudo systemctl start ssh + + - name: Set up FreeBSD VM + uses: vmactions/freebsd-vm@05856381fab64eeee9b038a0818f6cec649ca17a # v1.2.3 + with: + release: ${{ matrix.release }} + copyback: false + usesh: true + prepare: | + pkg install -y \ + devel/blueprint-compiler \ + devel/gettext \ + devel/git \ + devel/pkgconf \ + graphics/wayland \ + lang/zig \ + security/ca_root_nss \ + textproc/hs-pandoc \ + x11-fonts/jetbrains-mono \ + x11-toolkits/libadwaita \ + x11-toolkits/gtk40 \ + x11-toolkits/gtk4-layer-shell + + run: | + zig env + + - name: Run tests + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE + zig build test + + - name: Build GTK-NG app runtime + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE + zig build + ./zig-out/bin/ghostty +version diff --git a/src/apprt.zig b/src/apprt.zig index 6c1f040ea..2e3a722a6 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -70,13 +70,15 @@ pub const Runtime = enum { gtk, pub fn default(target: std.Target) Runtime { - // The Linux default is GTK because it is a full featured application. - if (target.os.tag == .linux) return .@"gtk-ng"; - - // Otherwise, we do NONE so we don't create an exe and we - // create libghostty. On macOS, Xcode is used to build the app - // that links to libghostty. - return .none; + return switch (target.os.tag) { + // The Linux and FreeBSD default is GTK because it is a full + // featured application. + .linux, .freebsd => .@"gtk-ng", + // Otherwise, we do NONE so we don't create an exe and we create + // libghostty. On macOS, Xcode is used to build the app that links + // to libghostty. + else => .none, + }; } }; diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig index 4128a7adc..0ca154414 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -131,6 +131,13 @@ pub const VTable = struct { }; test Benchmark { + // This test fails on FreeBSD so skip: + // + // /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test) + // try testing.expect(result.duration > 0); + // ^ + if (builtin.os.tag == .freebsd) return error.SkipZigTest; + const testing = std.testing; const Simple = struct { const Self = @This(); From c94805f0aa0b4ea0162b93003cad5c336c25a766 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 31 Aug 2025 00:16:30 +0000 Subject: [PATCH 69/75] 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 66ae7804c..fb75252c8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -112,8 +112,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz", - .hash = "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", + .hash = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index fa17d4599..1059338c2 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-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri": { + "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz", - "hash": "sha256-LQIa9siNICX5zzajvrJNKBmgDqAlBDY7QEmHihs65d0=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", + "hash": "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 974ad3efb..1c3578da4 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri"; + name = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz"; - hash = "sha256-LQIa9siNICX5zzajvrJNKBmgDqAlBDY7QEmHihs65d0="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz"; + hash = "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index e4dad6f5a..b6123dd2a 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 1f1f31e27..ef7099102 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz", - "dest": "vendor/p/N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri", - "sha256": "2d021af6c88d2025f9cf36a3beb24d2819a00ea02504363b4049878a1b3ae5dd" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", + "dest": "vendor/p/N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME", + "sha256": "3655177013a6680f16fbb457b97727f532219bdeb87573b63a9e10090c610f27" }, { "type": "archive", From fc6266133fadfaa057e811f8529b9ebcd7da9e06 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Sat, 21 Jun 2025 10:04:19 +0200 Subject: [PATCH 70/75] feat: added faint-opacity option --- src/config/Config.zig | 7 +++++++ src/renderer/generic.zig | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ecaf87ef4..0d67d254e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3035,6 +3035,13 @@ else /// Available since Ghostty 1.2.0. @"bold-color": ?BoldColor = null, +/// The opacity level (opposite of transparency) of the faint text. A value of +/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 +/// or greater than 1 will be clamped to the nearest valid value. +/// +/// Available since Ghostty 1.2.0. +@"faint-opacity": f64 = 0.6, + /// This will be used to set the `TERM` environment variable. /// HACK: We set this with an `xterm` prefix because vim uses that to enable key /// protocols (specifically this will enable `modifyOtherKeys`), among other diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d975f0f96..a6d642d1f 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -522,6 +522,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { selection_background: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor, bold_color: ?configpkg.BoldColor, + faint_opacity: u8, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, custom_shaders: configpkg.RepeatablePath, @@ -584,6 +585,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), .bold_color = config.@"bold-color", + .faint_opacity = @intFromFloat(@ceil(std.math.clamp(config.@"faint-opacity", 0.0, 1.0) * 255)), .min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color", @@ -2612,7 +2614,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) 175 else 255; + const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255; // Set the cell's background color. { From 6319464cfba32666c119dd09bbc1bef80ff92461 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Sun, 31 Aug 2025 17:19:51 +0200 Subject: [PATCH 71/75] refactor: move faint-opacity clamping to config finalization --- src/config/Config.zig | 2 ++ src/renderer/generic.zig | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 0d67d254e..b33cf059e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4024,6 +4024,8 @@ pub fn finalize(self: *Config) !void { if (self.@"auto-update-channel" == null) { self.@"auto-update-channel" = build_config.release_channel; } + + self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0); } /// Callback for src/cli/args.zig to allow us to handle special cases diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index a6d642d1f..dc69b781c 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -585,7 +585,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), .bold_color = config.@"bold-color", - .faint_opacity = @intFromFloat(@ceil(std.math.clamp(config.@"faint-opacity", 0.0, 1.0) * 255)), + .faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)), .min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color", From 650095e7e9ff0ae8c802b22a07cdebe2b155adf9 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Sun, 31 Aug 2025 17:21:00 +0200 Subject: [PATCH 72/75] fix: changed default faint-opacity value to 0.5 --- 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 b33cf059e..5ffd01871 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3040,7 +3040,7 @@ else /// or greater than 1 will be clamped to the nearest valid value. /// /// Available since Ghostty 1.2.0. -@"faint-opacity": f64 = 0.6, +@"faint-opacity": f64 = 0.5, /// This will be used to set the `TERM` environment variable. /// HACK: We set this with an `xterm` prefix because vim uses that to enable key From 0d30f859bdcb47c84f2381591beae18df94a80af Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 31 Aug 2025 11:43:10 -0600 Subject: [PATCH 73/75] renderer: clarify and correct custom shader cursor position math This math was incorrect from the start, the previous fix helped OpenGL but broke positioning under Metal; this commit fixes the math to be correct under both backends and adds comments explaining exactly what's going on. --- src/renderer/generic.zig | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 578ab1779..476aa31ec 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2225,23 +2225,46 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]); + // Left edge of the cell the cursor is in. var pixel_x: f32 = @floatFromInt( cursor.grid_pos[0] * cell.width + padding.left, ); + // Top edge, relative to the top of the + // screen, of the cell the cursor is in. var pixel_y: f32 = @floatFromInt( cursor.grid_pos[1] * cell.height + padding.top, ); - pixel_x += @floatFromInt(cursor.bearings[0]); - // Convert the Y coordinate from bottom-to-top to top-to-bottom. - // Otherwise we end up with glyphs like underline at the top of the cell. - pixel_y += @floatFromInt(@as(i32, @intCast(cell.height)) - cursor.bearings[1]); - - // If +Y is up in our shaders, we need to flip the coordinate. + // If +Y is up in our shaders, we need to flip the coordinate + // so that it's instead the top edge of the cell relative to + // the *bottom* of the screen. if (!GraphicsAPI.custom_shader_y_is_down) { pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y; } + // Add the X bearing to get the -X (left) edge of the cursor. + pixel_x += @floatFromInt(cursor.bearings[0]); + + // How we deal with the Y bearing depends on which direction + // is "up", since we want our final `pixel_y` value to be the + // +Y edge of the cursor. + if (GraphicsAPI.custom_shader_y_is_down) { + // As a reminder, the Y bearing is the distance from the + // bottom of the cell to the top of the glyph, so to get + // the +Y edge we need to add the cell height, subtract + // the Y bearing, and add the glyph height to get the +Y + // (bottom) edge of the cursor. + pixel_y += @floatFromInt(cell.height); + pixel_y -= @floatFromInt(cursor.bearings[1]); + pixel_y += @floatFromInt(cursor.glyph_size[1]); + } else { + // If the Y direction is reversed though, we instead want + // the *top* edge of the cursor, which means we just need + // to subtract the cell height and add the Y bearing. + pixel_y -= @floatFromInt(cell.height); + pixel_y += @floatFromInt(cursor.bearings[1]); + } + const new_cursor: [4]f32 = .{ pixel_x, pixel_y, From c535d0a664f56a36514eb6fc3196bb6a38686c43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 00:47:33 +0000 Subject: [PATCH 74/75] build(deps): bump actions/checkout from 4.3.0 to 5.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.0 to 5.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.3.0...08c6903cd8c0fde910a37f88322edcfb5dd907a8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... 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 8666d01d9..419e83235 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1028,7 +1028,7 @@ jobs: # - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108 steps: - name: Checkout Ghostty - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Start SSH run: | From 0b588308826bb47dd34169253c61a02b7178290d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 31 Aug 2025 19:54:26 -0700 Subject: [PATCH 75/75] macOS: Progress bar for OSC9 progress reports --- include/ghostty.h | 8 ++-- macos/Ghostty.xcodeproj/project.pbxproj | 2 + macos/Sources/Ghostty/Ghostty.Action.swift | 35 ++++++++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 30 ++++++++++++ macos/Sources/Ghostty/SurfaceView.swift | 48 +++++++++++++++++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 23 +++++++++ macos/Sources/Ghostty/SurfaceView_UIKit.swift | 3 ++ src/terminal/osc.zig | 9 ++-- 8 files changed, 151 insertions(+), 7 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 574cc6278..c871dd593 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -722,15 +722,15 @@ typedef enum { GHOSTTY_PROGRESS_STATE_ERROR, GHOSTTY_PROGRESS_STATE_INDETERMINATE, GHOSTTY_PROGRESS_STATE_PAUSE, -} ghostty_terminal_osc_command_progressreport_state_e; +} ghostty_action_progress_report_state_e; // terminal.osc.Command.ProgressReport.C typedef struct { - ghostty_terminal_osc_command_progressreport_state_e state; + ghostty_action_progress_report_state_e state; // -1 if no progress was reported, otherwise 0-100 indicating percent // completeness. int8_t progress; -} ghostty_terminal_osc_command_progressreport_s; +} ghostty_action_progress_report_s; // apprt.Action.Key typedef enum { @@ -817,7 +817,7 @@ typedef union { ghostty_action_open_url_s open_url; ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; - ghostty_terminal_osc_command_progressreport_s progress_report; + ghostty_action_progress_report_s progress_report; } ghostty_action_u; typedef struct { diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index e3fb92fa4..6a6adb494 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; }; A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; }; A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; }; + A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; }; A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; }; A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; }; A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; @@ -987,6 +988,7 @@ A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */, A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */, A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, + A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */, A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */, diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index a6559600d..ff265189b 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -70,4 +70,39 @@ extension Ghostty.Action { } } } + + struct ProgressReport { + enum State { + case remove + case set + case error + case indeterminate + case pause + + init(_ c: ghostty_action_progress_report_state_e) { + switch c { + case GHOSTTY_PROGRESS_STATE_REMOVE: + self = .remove + case GHOSTTY_PROGRESS_STATE_SET: + self = .set + case GHOSTTY_PROGRESS_STATE_ERROR: + self = .error + case GHOSTTY_PROGRESS_STATE_INDETERMINATE: + self = .indeterminate + case GHOSTTY_PROGRESS_STATE_PAUSE: + self = .pause + default: + self = .remove + } + } + } + + let state: State + let progress: UInt8? + + init(c: ghostty_action_progress_report_s) { + self.state = State(c.state) + self.progress = c.progress >= 0 ? UInt8(c.progress) : nil + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index eaccb43cf..65979bd40 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -543,6 +543,9 @@ extension Ghostty { case GHOSTTY_ACTION_KEY_SEQUENCE: keySequence(app, target: target, v: action.action.key_sequence) + + case GHOSTTY_ACTION_PROGRESS_REPORT: + progressReport(app, target: target, v: action.action.progress_report) case GHOSTTY_ACTION_CONFIG_CHANGE: configChange(app, target: target, v: action.action.config_change) @@ -1523,6 +1526,33 @@ extension Ghostty { assertionFailure() } } + + private static func progressReport( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_progress_report_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("progress report 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 progressReport = Ghostty.Action.ProgressReport(c: v) + DispatchQueue.main.async { + if progressReport.state == .remove { + surfaceView.progressReport = nil + } else { + surfaceView.progressReport = progressReport + } + } + + default: + assertionFailure() + } + } private static func configReload( _ app: ghostty_app_t, diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 039245429..38efef646 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -113,6 +113,11 @@ extension Ghostty { } } .ghosttySurfaceView(surfaceView) + + // Progress report overlay + if let progressReport = surfaceView.progressReport { + ProgressReportOverlay(report: progressReport) + } #if canImport(AppKit) // If we are in the middle of a key sequence, then we show a visual element. We only @@ -267,6 +272,49 @@ extension Ghostty { } } + // Progress report overlay that shows a progress bar at the top of the terminal + struct ProgressReportOverlay: View { + let report: Action.ProgressReport + + @ViewBuilder + private var progressBar: some View { + if let progress = report.progress { + // Determinate progress bar + ProgressView(value: Double(progress), total: 100) + .progressViewStyle(.linear) + .tint(report.state == .error ? .red : report.state == .pause ? .orange : nil) + .animation(.easeInOut(duration: 0.2), value: progress) + } else { + // Indeterminate states + switch report.state { + case .indeterminate: + ProgressView() + .progressViewStyle(.linear) + case .error: + ProgressView() + .progressViewStyle(.linear) + .tint(.red) + case .pause: + Rectangle().fill(Color.orange) + default: + EmptyView() + } + } + } + + var body: some View { + VStack(spacing: 0) { + progressBar + .scaleEffect(x: 1, y: 0.5, anchor: .center) + .frame(height: 2) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .allowsHitTesting(false) + } + } + // This is the resize overlay that shows on top of a surface to show the current // size during a resize operation. struct SurfaceResizeOverlay: View { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 97637e737..2d83a8a6b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -41,6 +41,23 @@ 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 + self?.progressReport = nil + self?.progressReportTimer = nil + } + } + } + } // The currently active key sequence. The sequence is not active if this is empty. @Published var keySequence: [KeyboardShortcut] = [] @@ -142,6 +159,9 @@ 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? // This is the title from the terminal. This is nil if we're currently using // the terminal title as the main title property. If the title is set manually @@ -348,6 +368,9 @@ 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() } func focusDidChange(_ focused: Bool) { diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift index e88ec82e2..29364d4a5 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift @@ -30,6 +30,9 @@ extension Ghostty { // The hovered URL @Published var hoverUrl: String? = nil + + // The progress report (if any) + @Published var progressReport: Action.ProgressReport? = nil // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 786a1772c..7f4f32597 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -211,7 +211,6 @@ pub const Command = union(enum) { }; pub const ProgressReport = struct { - // sync with ghostty_terminal_osc_command_progressreport_state_e in include/ghostty.h pub const State = enum(c_int) { remove, set, @@ -223,7 +222,7 @@ pub const Command = union(enum) { state: State, progress: ?u8 = null, - // sync with ghostty_terminal_osc_command_progressreport_s in include/ghostty.h + // sync with ghostty_action_progress_report_s pub const C = extern struct { state: c_int, progress: i8, @@ -232,7 +231,11 @@ pub const Command = union(enum) { pub fn cval(self: ProgressReport) C { return .{ .state = @intFromEnum(self.state), - .progress = if (self.progress) |progress| @intCast(std.math.clamp(progress, 0, 100)) else -1, + .progress = if (self.progress) |progress| @intCast(std.math.clamp( + progress, + 0, + 100, + )) else -1, }; } };