From a479c9b2af7f85e0a06576e9424bad03155c1776 Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Fri, 22 Aug 2025 10:59:04 -0300 Subject: [PATCH 01/16] i18n: add missing es_AR translations (#8347) Part of #8344 --- po/es_AR.UTF-8.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index 16f955355..496e0ebbf 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.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-05-19 20:17-0300\n" +"PO-Revision-Date: 2025-08-22 09:35-0300\n" "Last-Translator: Alan Moyano \n" "Language-Team: Argentinian \n" "Language: es_AR\n" @@ -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 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 la configuración para volver a mostrar este mensaje" #: 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 "Portapapeles limpiado" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comando ejecutado correctamente" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "El comando ha fallado" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" From f4009721a172a8007d18d8ef7c5ec0a4bde4c10b Mon Sep 17 00:00:00 2001 From: Moeeze Hassan Date: Fri, 22 Aug 2025 13:11:05 +0200 Subject: [PATCH 02/16] macOS: order out alert sheets to avoid Stage Manager focus loss Signed-off-by: Moeeze Hassan --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 4 ++++ macos/Sources/Features/Terminal/TerminalController.swift | 3 +++ 2 files changed, 7 insertions(+) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index c93a9450d..ffe7997a6 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -290,8 +290,12 @@ class BaseTerminalController: NSWindowController, alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning alert.beginSheetModal(for: window) { response in + let alertWindow = alert.window self.alert = nil if response == .alertFirstButtonReturn { + // This is important so that we avoid losing focus when Stage + // Manager is used (#8336) + alertWindow.orderOut(nil) completion() } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ec56fb934..f31c43aec 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -747,6 +747,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr alert.alertStyle = .warning alert.beginSheetModal(for: alertWindow, completionHandler: { response in if (response == .alertFirstButtonReturn) { + // This is important so that we avoid losing focus when Stage + // Manager is used (#8336) + alert.window.orderOut(nil) closeAllWindowsImmediately() } }) From 93c2400bf4b2403803cc718337ff6709d08fe9e8 Mon Sep 17 00:00:00 2001 From: Alex Straight Date: Sun, 8 Jun 2025 21:53:28 -0700 Subject: [PATCH 03/16] fix: copy_url_to_clipboard copies full OSC8 URL instead of single character OSC8 links were only detected when exact platform-specific modifiers were held (Cmd on macOS, Ctrl on Linux), but copy_url_to_clipboard should work with either. Additionally, OSC8 links were using selectionString() which gets visible text instead of the actual URI. Now we use osc8URI() for OSC8 links and fall back to selectionString() for regex-detected links. Fixes #7491 --- src/Surface.zig | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 2ab265cda..82d8fc954 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3681,7 +3681,7 @@ fn linkAtPos( const mouse_mods = self.mouseModsWithCapture(self.mouse.mods); // If we have the proper modifiers set then we can check for OSC8 links. - if (mouse_mods.equal(input.ctrlOrSuper(.{}))) hyperlink: { + if (mouse_mods.ctrl or mouse_mods.super) hyperlink: { const rac = mouse_pin.rowAndCell(); const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; @@ -4494,19 +4494,32 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool const pos = try self.rt_surface.getCursorPos(); if (try self.linkAtPos(pos)) |link_info| { - // Get the URL text from selection - const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{ - .sel = link_info[1], - .trim = self.config.clipboard_trim_trailing_spaces, - })) catch |err| { - log.err("error reading url string err={}", .{err}); - return false; + const url_text = switch (link_info[0]) { + ._open_osc8 => url_text: { + // For OSC8 links, get the URI directly from hyperlink data + const uri = self.osc8URI(link_info[1].start()) orelse { + log.warn("failed to get URI for OSC8 hyperlink", .{}); + return false; + }; + break :url_text try self.alloc.dupeZ(u8, uri); + }, + .open => url_text: { + // For regex links, get the text from selection + break :url_text (self.io.terminal.screen.selectionString(self.alloc, .{ + .sel = link_info[1], + .trim = self.config.clipboard_trim_trailing_spaces, + })) catch |err| { + log.err("error reading url string err={}", .{err}); + return false; + }; + }, }; + defer self.alloc.free(url_text); self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| { log.err("error copying url to clipboard err={}", .{err}); - return true; + return false; }; return true; From 5836dc4ce6db9b84bb251a3fa17cf551478c6a5f Mon Sep 17 00:00:00 2001 From: Alex Straight Date: Sun, 8 Jun 2025 21:53:28 -0700 Subject: [PATCH 04/16] fix: copy_url_to_clipboard copies full OSC8 URL instead of single character OSC8 links were only detected when exact platform-specific modifiers were held (Cmd on macOS, Ctrl on Linux), but copy_url_to_clipboard should work with either. Additionally, OSC8 links were using selectionString() which gets visible text instead of the actual URI. Now we use osc8URI() for OSC8 links and fall back to selectionString() for regex-detected links. Fixes #7491 --- src/Surface.zig | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 2ab265cda..82d8fc954 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3681,7 +3681,7 @@ fn linkAtPos( const mouse_mods = self.mouseModsWithCapture(self.mouse.mods); // If we have the proper modifiers set then we can check for OSC8 links. - if (mouse_mods.equal(input.ctrlOrSuper(.{}))) hyperlink: { + if (mouse_mods.ctrl or mouse_mods.super) hyperlink: { const rac = mouse_pin.rowAndCell(); const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; @@ -4494,19 +4494,32 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool const pos = try self.rt_surface.getCursorPos(); if (try self.linkAtPos(pos)) |link_info| { - // Get the URL text from selection - const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{ - .sel = link_info[1], - .trim = self.config.clipboard_trim_trailing_spaces, - })) catch |err| { - log.err("error reading url string err={}", .{err}); - return false; + const url_text = switch (link_info[0]) { + ._open_osc8 => url_text: { + // For OSC8 links, get the URI directly from hyperlink data + const uri = self.osc8URI(link_info[1].start()) orelse { + log.warn("failed to get URI for OSC8 hyperlink", .{}); + return false; + }; + break :url_text try self.alloc.dupeZ(u8, uri); + }, + .open => url_text: { + // For regex links, get the text from selection + break :url_text (self.io.terminal.screen.selectionString(self.alloc, .{ + .sel = link_info[1], + .trim = self.config.clipboard_trim_trailing_spaces, + })) catch |err| { + log.err("error reading url string err={}", .{err}); + return false; + }; + }, }; + defer self.alloc.free(url_text); self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| { log.err("error copying url to clipboard err={}", .{err}); - return true; + return false; }; return true; From fc1307e9393e7868edf624bcb5bdd5a8055c3919 Mon Sep 17 00:00:00 2001 From: Alex Straight Date: Sun, 8 Jun 2025 23:08:11 -0700 Subject: [PATCH 05/16] fix: make regular URLs work with either ctrl or super modifiers --- src/Surface.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 82d8fc954..fc1b80ed3 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3712,7 +3712,17 @@ fn linkAtPos( for (self.config.links) |link| { switch (link.highlight) { .always, .hover => {}, - .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, + .always_mods, .hover_mods => |v| { + // Special case: if the expected mods are "ctrl or super" (like the default URL config), + // then we should match if the user pressed either ctrl or super, just like OSC8 links. + const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or + (v.super and !v.ctrl and !v.shift and !v.alt); + if (is_ctrl_or_super_expected) { + if (!(mouse_mods.ctrl or mouse_mods.super)) continue; + } else { + if (!v.equal(mouse_mods)) continue; + } + }, } var it = strmap.searchIterator(link.regex); From c78fb0f89554b354e1aea4a805e81b53ec1add56 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 12 Jul 2025 09:29:03 -0700 Subject: [PATCH 06/16] Removed boolean logic, reverted back to ctrlOrSuper call --- src/Surface.zig | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index fc1b80ed3..43a76eebe 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3712,17 +3712,18 @@ fn linkAtPos( for (self.config.links) |link| { switch (link.highlight) { .always, .hover => {}, - .always_mods, .hover_mods => |v| { - // Special case: if the expected mods are "ctrl or super" (like the default URL config), - // then we should match if the user pressed either ctrl or super, just like OSC8 links. - const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or - (v.super and !v.ctrl and !v.shift and !v.alt); - if (is_ctrl_or_super_expected) { - if (!(mouse_mods.ctrl or mouse_mods.super)) continue; - } else { - if (!v.equal(mouse_mods)) continue; - } - }, + .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, + // .always_mods, .hover_mods => |_| { + // // Special case: if the expected mods are "ctrl or super" (like the default URL config), + // // then we should match if the user pressed either ctrl or super, just like OSC8 links. + // // const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or + // // (v.super and !v.ctrl and !v.shift and !v.alt); + // // if (is_ctrl_or_super_expected) { + // // if (!(mouse_mods.ctrl or mouse_mods.super)) continue; + // // } else { + // // if (!v.equal(mouse_mods)) continue; + // // } + // }, } var it = strmap.searchIterator(link.regex); From 83b573aed7c88f6e0b1319e2228275b62a6005a1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 12 Jul 2025 09:30:01 -0700 Subject: [PATCH 07/16] remove commented out block --- src/Surface.zig | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 43a76eebe..82d8fc954 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3713,17 +3713,6 @@ fn linkAtPos( switch (link.highlight) { .always, .hover => {}, .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, - // .always_mods, .hover_mods => |_| { - // // Special case: if the expected mods are "ctrl or super" (like the default URL config), - // // then we should match if the user pressed either ctrl or super, just like OSC8 links. - // // const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or - // // (v.super and !v.ctrl and !v.shift and !v.alt); - // // if (is_ctrl_or_super_expected) { - // // if (!(mouse_mods.ctrl or mouse_mods.super)) continue; - // // } else { - // // if (!v.equal(mouse_mods)) continue; - // // } - // }, } var it = strmap.searchIterator(link.regex); From 14f5a879a9bac7cc82d28cb13acb43c58fb54adb Mon Sep 17 00:00:00 2001 From: Alex Straight Date: Sun, 8 Jun 2025 23:08:11 -0700 Subject: [PATCH 08/16] fix: make regular URLs work with either ctrl or super modifiers --- src/Surface.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 82d8fc954..fc1b80ed3 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3712,7 +3712,17 @@ fn linkAtPos( for (self.config.links) |link| { switch (link.highlight) { .always, .hover => {}, - .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, + .always_mods, .hover_mods => |v| { + // Special case: if the expected mods are "ctrl or super" (like the default URL config), + // then we should match if the user pressed either ctrl or super, just like OSC8 links. + const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or + (v.super and !v.ctrl and !v.shift and !v.alt); + if (is_ctrl_or_super_expected) { + if (!(mouse_mods.ctrl or mouse_mods.super)) continue; + } else { + if (!v.equal(mouse_mods)) continue; + } + }, } var it = strmap.searchIterator(link.regex); From 6708229a7e7d18bd0d525a7c7246c6069ce2a1de Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 12 Jul 2025 09:29:03 -0700 Subject: [PATCH 09/16] Removed boolean logic, reverted back to ctrlOrSuper call --- src/Surface.zig | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index fc1b80ed3..43a76eebe 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3712,17 +3712,18 @@ fn linkAtPos( for (self.config.links) |link| { switch (link.highlight) { .always, .hover => {}, - .always_mods, .hover_mods => |v| { - // Special case: if the expected mods are "ctrl or super" (like the default URL config), - // then we should match if the user pressed either ctrl or super, just like OSC8 links. - const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or - (v.super and !v.ctrl and !v.shift and !v.alt); - if (is_ctrl_or_super_expected) { - if (!(mouse_mods.ctrl or mouse_mods.super)) continue; - } else { - if (!v.equal(mouse_mods)) continue; - } - }, + .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, + // .always_mods, .hover_mods => |_| { + // // Special case: if the expected mods are "ctrl or super" (like the default URL config), + // // then we should match if the user pressed either ctrl or super, just like OSC8 links. + // // const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or + // // (v.super and !v.ctrl and !v.shift and !v.alt); + // // if (is_ctrl_or_super_expected) { + // // if (!(mouse_mods.ctrl or mouse_mods.super)) continue; + // // } else { + // // if (!v.equal(mouse_mods)) continue; + // // } + // }, } var it = strmap.searchIterator(link.regex); From 6248030426352f544e4459847362ecaaf6498844 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 12 Jul 2025 10:03:43 -0700 Subject: [PATCH 10/16] removed boolean logic, reverted back to ctrlOrSuper call --- src/Surface.zig | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 43a76eebe..b4e75c8b8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3681,7 +3681,7 @@ fn linkAtPos( const mouse_mods = self.mouseModsWithCapture(self.mouse.mods); // If we have the proper modifiers set then we can check for OSC8 links. - if (mouse_mods.ctrl or mouse_mods.super) hyperlink: { + if (mouse_mods.equal(input.ctrlOrSuper(.{}))) hyperlink: { const rac = mouse_pin.rowAndCell(); const cell = rac.cell; if (!cell.hyperlink) break :hyperlink; @@ -3713,17 +3713,6 @@ fn linkAtPos( switch (link.highlight) { .always, .hover => {}, .always_mods, .hover_mods => |v| if (!v.equal(mouse_mods)) continue, - // .always_mods, .hover_mods => |_| { - // // Special case: if the expected mods are "ctrl or super" (like the default URL config), - // // then we should match if the user pressed either ctrl or super, just like OSC8 links. - // // const is_ctrl_or_super_expected = (v.ctrl and !v.super and !v.shift and !v.alt) or - // // (v.super and !v.ctrl and !v.shift and !v.alt); - // // if (is_ctrl_or_super_expected) { - // // if (!(mouse_mods.ctrl or mouse_mods.super)) continue; - // // } else { - // // if (!v.equal(mouse_mods)) continue; - // // } - // }, } var it = strmap.searchIterator(link.regex); @@ -4506,14 +4495,6 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool const pos = try self.rt_surface.getCursorPos(); if (try self.linkAtPos(pos)) |link_info| { const url_text = switch (link_info[0]) { - ._open_osc8 => url_text: { - // For OSC8 links, get the URI directly from hyperlink data - const uri = self.osc8URI(link_info[1].start()) orelse { - log.warn("failed to get URI for OSC8 hyperlink", .{}); - return false; - }; - break :url_text try self.alloc.dupeZ(u8, uri); - }, .open => url_text: { // For regex links, get the text from selection break :url_text (self.io.terminal.screen.selectionString(self.alloc, .{ @@ -4524,8 +4505,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return false; }; }, - }; + ._open_osc8 => url_text: { + // For OSC8 links, get the URI directly from hyperlink data + const uri = self.osc8URI(link_info[1].start()) orelse { + log.warn("failed to get URI for OSC8 hyperlink", .{}); + return false; + }; + break :url_text try self.alloc.dupeZ(u8, uri); + }, + }; defer self.alloc.free(url_text); self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| { From 85cba70c2e2a0459ddea8d337d4fd0f97809d54a Mon Sep 17 00:00:00 2001 From: Tobias Pape Date: Tue, 15 Jul 2025 18:12:39 +0200 Subject: [PATCH 11/16] Work around strange SwiftUI behavior in "older" macOSen. The Quick Terminal would not appear anymore, as somewhere in the framework the Quick Terminal Window's geometry gets corrupted when the window is added to the UI. This works around by caching the windows geometry and reusing it afterwards --- .../QuickTerminal/QuickTerminalController.swift | 12 ++++++++++++ .../Features/QuickTerminal/QuickTerminalWindow.swift | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 3bd8bc18f..b9f879a30 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -111,12 +111,24 @@ class QuickTerminalController: BaseTerminalController { // Setup our initial size based on our configured position position.setLoaded(window) + // Upon first adding this Window to its host view, older SwiftUI + // seems to have a "hiccup" and corrupts the frameRect, + // sometimes setting the size to zero, sometimes corrupting it. + // We pass the actual window's frame as "inital" frame directly + // to the window, so it can use that instead of the frameworks + // "interpretation" + if let qtWindow = window as? QuickTerminalWindow { + qtWindow.initialFrame = window.frame + } // Setup our content window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self )) + if let qtWindow = window as? QuickTerminalWindow { + qtWindow.initialFrame = nil + } // Animate the window in animateIn() diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift index 005808a23..663f868f9 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift @@ -29,4 +29,14 @@ class QuickTerminalWindow: NSPanel { // We don't want to activate the owning app when quick terminal is triggered. self.styleMask.insert(.nonactivatingPanel) } + var initialFrame: NSRect? = nil + override func setFrame(_ frameRect: NSRect, display flag: Bool) { + // Upon first adding this Window to its host view, older SwiftUI + // seems to have a "hiccup" and corrupts the frameRect, + // sometimes setting the size to zero, sometimes corrupting it. + // If we find we have cached the "initial" frame, use that instead + // the propagated one throught the framework + super.setFrame(initialFrame ?? frameRect, display: flag) + } + } From 4fdf0b687e6512c3effccaa713fd0fcfa98ab7aa Mon Sep 17 00:00:00 2001 From: Tobias Pape Date: Tue, 22 Jul 2025 23:04:24 +0200 Subject: [PATCH 12/16] typo found typos, typo may keep them --- .../Features/QuickTerminal/QuickTerminalController.swift | 5 ++++- .../Features/QuickTerminal/QuickTerminalWindow.swift | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index b9f879a30..1f608f767 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -114,18 +114,21 @@ class QuickTerminalController: BaseTerminalController { // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, // sometimes setting the size to zero, sometimes corrupting it. - // We pass the actual window's frame as "inital" frame directly + // We pass the actual window's frame as "initial" frame directly // to the window, so it can use that instead of the frameworks // "interpretation" if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = window.frame } + // Setup our content window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, viewModel: self, delegate: self )) + + // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = nil } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift index 663f868f9..1a4170dbc 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift @@ -29,14 +29,19 @@ class QuickTerminalWindow: NSPanel { // We don't want to activate the owning app when quick terminal is triggered. self.styleMask.insert(.nonactivatingPanel) } + + /// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround + /// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026 var initialFrame: NSRect? = nil + override func setFrame(_ frameRect: NSRect, display flag: Bool) { // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, // sometimes setting the size to zero, sometimes corrupting it. // If we find we have cached the "initial" frame, use that instead - // the propagated one throught the framework + // the propagated one through the framework + // + // https://github.com/ghostty-org/ghostty/pull/8026 super.setFrame(initialFrame ?? frameRect, display: flag) } - } From db60e981d150c874c2c88468d074dc04cca678ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 22 Aug 2025 09:44:26 -0700 Subject: [PATCH 13/16] core: avoid possible deadlock in right-click-action paste actions Fixes #8313 The clipboard request flow can result in the apprt immediately completing the request which itself grabs a lock. For pastes, we should yield the lock during the clipboard request. GTK is always async so this worked there, but we want to be resilient to any apprt behavior here. --- src/Surface.zig | 60 ++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index b4e75c8b8..770c2daef 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3555,26 +3555,7 @@ pub fn mouseButtonCallback( }; switch (self.config.right_click_action) { - .ignore => { - // Return early to skip clearing the selection. - try self.queueRender(); - return true; - }, - .copy => { - if (self.io.terminal.screen.selection) |sel| { - self.copySelectionToClipboards(sel, &.{.standard}); - } - }, - .@"copy-or-paste" => { - if (self.io.terminal.screen.selection) |sel| { - self.copySelectionToClipboards(sel, &.{.standard}); - } else { - try self.startClipboardRequest(.standard, .paste); - } - }, - .paste => { - try self.startClipboardRequest(.standard, .paste); - }, + .ignore => {}, .@"context-menu" => { // If we already have a selection and the selection contains // where we clicked then we don't want to modify the selection. @@ -3588,12 +3569,45 @@ pub fn mouseButtonCallback( const sel = screen.selectWord(pin) orelse break :sel; try self.setSelection(sel); try self.queueRender(); + + // Don't consume so that we show the context menu in apprt. return false; }, - } + .copy => { + if (self.io.terminal.screen.selection) |sel| { + self.copySelectionToClipboards(sel, &.{.standard}); + } - try self.setSelection(null); - try self.queueRender(); + try self.setSelection(null); + try self.queueRender(); + }, + .@"copy-or-paste" => if (self.io.terminal.screen.selection) |sel| { + self.copySelectionToClipboards(sel, &.{.standard}); + try self.setSelection(null); + try self.queueRender(); + } else { + // Pasting can trigger a lock grab in complete clipboard + // request so we need to unlock. + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + try self.startClipboardRequest(.standard, .paste); + + // We don't need to clear selection because we didn't have + // one to begin with. + }, + .paste => { + // Before we yield the lock, clear our selection if we have + // one. + try self.setSelection(null); + try self.queueRender(); + + // Pasting can trigger a lock grab in complete clipboard + // request so we need to unlock. + self.renderer_state.mutex.unlock(); + defer self.renderer_state.mutex.lock(); + try self.startClipboardRequest(.standard, .paste); + }, + } // Consume the event such that the context menu is not displayed. return true; From 50fe12e85c25b0e96960d0ed1ad60506e46996a9 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Fri, 22 Aug 2025 10:30:01 -0700 Subject: [PATCH 14/16] deps: update z2d to v0.7.2 Release notes at: https://github.com/vancluever/z2d/blob/v0.7.2/CHANGELOG.md This is mostly a bugfix release for text rendering, including a fix for an invalid free flagged as: https://github.com/vancluever/z2d/security/advisories/GHSA-v7f4-f3hm-282w Note that the invalid free affects the new in-library text rendering only and as such is likely not in use in Ghostty. I'm anticipating *maybe* one more release after this ahead of Ghostty 1.2.0, depending on if I find any more bugs, but close to around the release I am planning a freeze to ensure a clean versioned release continues to be set. --- 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 950ae8acd..7f29e0419 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz", - .hash = "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5", + .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz", + .hash = "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 64286db22..a8fa38469 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -129,10 +129,10 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5": { + "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz", - "hash": "sha256-6ZqgrO/bcjgnuQcfq89VYptUUodsErM8Fz6nwBZhTJs=" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz", + "hash": "sha256-tWrLlEOU4/0ZOlzgGOXB08fW7sqfyAFf3rlv0wl9b/c=" }, "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 610372dbd..6450b8df9 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -290,11 +290,11 @@ in }; } { - name = "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5"; + name = "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz"; - hash = "sha256-6ZqgrO/bcjgnuQcfq89VYptUUodsErM8Fz6nwBZhTJs="; + url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz"; + hash = "sha256-tWrLlEOU4/0ZOlzgGOXB08fW7sqfyAFf3rlv0wl9b/c="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 65fbec22c..29191f741 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -32,4 +32,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1 https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz +https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 7a56a57e1..2d48f9617 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -157,9 +157,9 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz", - "dest": "vendor/p/z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5", - "sha256": "e99aa0acefdb723827b9071fabcf55629b5452876c12b33c173ea7c016614c9b" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz", + "dest": "vendor/p/z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l", + "sha256": "b56acb944394e3fd193a5ce018e5c1d3c7d6eeca9fc8015fdeb96fd3097d6ff7" }, { "type": "archive", From 54f8dff3086cc24806ca9daff1107e9fc7828c1b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 22 Aug 2025 14:29:54 -0700 Subject: [PATCH 15/16] macos: if parent window is fullscreen, new window is fullscreen too Fixes #8229 This was a regression. The discussion noted in #8229 requests we create a new window on the non-fullscreen desktop but that isn't how Ghostty has behaved historically. I bisected back and tried 1.1.3 as well and we always created a new fullscreen window when the parent was fullscreen. This behavior matches iTerm2. Its noteworthy that native tabbing and Apple apps such as Terminal.app and Safari do NOT do this. For both of these, new window creates a _tab_ when in fullscreen. I don't think that's particularly desirable, though. --- macos/Sources/Features/Terminal/TerminalController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f31c43aec..644a0c8ac 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -196,7 +196,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr if let parent { if parent.styleMask.contains(.fullScreen) { - parent.toggleFullScreen(nil) + // If our previous window was fullscreen then we want our new window to + // be fullscreen. This behavior actually doesn't match the native tabbing + // behavior of macOS apps where new windows create tabs when in native + // fullscreen but this is how we've always done it. This matches iTerm2 + // behavior. + c.toggleFullscreen(mode: .native) } else if ghostty.config.windowFullscreen { switch (ghostty.config.windowFullscreenMode) { case .native: From 292efec669ca35cadccf3abedd39058ec5411bdd Mon Sep 17 00:00:00 2001 From: Cheru Berhanu Date: Thu, 14 Aug 2025 18:22:28 -0700 Subject: [PATCH 16/16] terminal: fix build with -Di18n=false canonicalizeLocale should return a null-terminated string, and didn't previously. Compiler output: ``` src/os/i18n.zig:139:45: error: expected type 'error{NoSpaceLeft}![:0]const u8', found '[]const u8' if (comptime !build_config.i18n) return locale; ^~~~~~ src/os/i18n.zig:139:45: note: destination pointer requires '0' sentinel src/os/i18n.zig:138:21: note: function return type declared here ) error{NoSpaceLeft}![:0]const u8 { ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~ ``` --- src/os/i18n.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/os/i18n.zig b/src/os/i18n.zig index c6bce6fbf..29f7f6bc3 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -136,7 +136,12 @@ pub fn canonicalizeLocale( buf: []u8, locale: []const u8, ) error{NoSpaceLeft}![:0]const u8 { - if (comptime !build_config.i18n) return locale; + if (comptime !build_config.i18n) { + if (buf.len < locale.len + 1) return error.NoSpaceLeft; + @memcpy(buf[0..locale.len], locale); + buf[locale.len] = 0; + return buf[0..locale.len :0]; + } // Fix zh locales for macOS if (fixZhLocale(locale)) |fixed| {