From 3d692e46f435ec5488e9570d1fc65b8778437480 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 10 Jun 2025 10:20:26 -0600 Subject: [PATCH 01/37] license: update copyright notices to include contributors Updates all copyright notices to include "Ghostty contributors" to reflect the fact that Mitchell is not the sole copyright owner. Also adds "Ghostty contributors" to the author section in the manpages, linking https://github.com/ghostty-org/ghostty/graphs/contributors for proper credit. --- LICENSE | 2 +- pkg/README.md | 2 +- pkg/glfw/LICENSE | 2 +- po/ca_ES.UTF-8.po | 2 +- po/com.mitchellh.ghostty.pot | 2 +- po/de_DE.UTF-8.po | 2 +- po/es_BO.UTF-8.po | 2 +- po/fr_FR.UTF-8.po | 2 +- po/id_ID.UTF-8.po | 2 +- po/ja_JP.UTF-8.po | 2 +- po/mk_MK.UTF-8.po | 2 +- po/nb_NO.UTF-8.po | 2 +- po/nl_NL.UTF-8.po | 2 +- po/pl_PL.UTF-8.po | 2 +- po/pt_BR.UTF-8.po | 2 +- po/ru_RU.UTF-8.po | 2 +- po/tr_TR.UTF-8.po | 2 +- po/uk_UA.UTF-8.po | 2 +- po/zh_CN.UTF-8.po | 2 +- src/build/GhosttyI18n.zig | 2 +- src/build/mdgen/ghostty_1_footer.md | 1 + src/build/mdgen/ghostty_5_footer.md | 1 + 22 files changed, 22 insertions(+), 20 deletions(-) diff --git a/LICENSE b/LICENSE index 14e132f55..0a07a66cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Mitchell Hashimoto +Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pkg/README.md b/pkg/README.md index 1d6f9f6eb..fddc4b3db 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -12,7 +12,7 @@ paste them into your project. the Ghostty project. This license does not apply to the rest of the Ghostty project.** -Copyright © 2024 Mitchell Hashimoto +Copyright © 2024 Mitchell Hashimoto, Ghostty contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in diff --git a/pkg/glfw/LICENSE b/pkg/glfw/LICENSE index eeeb852fe..8c422bd23 100644 --- a/pkg/glfw/LICENSE +++ b/pkg/glfw/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2021 Hexops Contributors (given via the Git commit history). -Copyright (c) 2025 Mitchell Hashimoto +Copyright (c) 2025 Mitchell Hashimoto, Ghostty contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 712f0d5af..653439fa2 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -1,5 +1,5 @@ # Catalan translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Francesc Arpi , 2025. # diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index d6a99d01d..da0efbbee 100644 --- a/po/com.mitchellh.ghostty.pot +++ b/po/com.mitchellh.ghostty.pot @@ -1,5 +1,5 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR Mitchell Hashimoto +# Copyright (C) YEAR Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # FIRST AUTHOR , YEAR. # diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 44f3bae39..2d3b96d81 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -1,6 +1,6 @@ # German translations for com.mitchellh.ghostty package # German translation for com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Robin Pfäffle , 2025. # diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index f3a62748a..077b7dfa1 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -1,5 +1,5 @@ # Spanish translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Miguel Peredo , 2025. # diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 4db72a23e..aef0d96ac 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -1,5 +1,5 @@ # French translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Kirwiisp , 2025. # diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index d5204d420..f82ec6197 100644 --- a/po/id_ID.UTF-8.po +++ b/po/id_ID.UTF-8.po @@ -1,5 +1,5 @@ # Indonesian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Satrio Bayu Aji , 2025. # diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index e6e015f8a..73ddd9f5a 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -1,6 +1,6 @@ # Japanese translations for com.mitchellh.ghostty package # com.mitchellh.ghostty パッケージに対する和訳. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Lon Sagisawa , 2025. # diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 39bb72b91..20a43572e 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -1,5 +1,5 @@ # Macedonian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Andrej Daskalov , 2025. # diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 2685d67bb..045d47a80 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -1,5 +1,5 @@ # Norwegian Bokmal translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Hanna Rose , 2025. # Uzair Aftab , 2025. diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 466116352..355bc4a57 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -1,5 +1,5 @@ # Dutch translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Nico Geesink , 2025. # diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index 22d2cd975..a68d56818 100644 --- a/po/pl_PL.UTF-8.po +++ b/po/pl_PL.UTF-8.po @@ -1,6 +1,6 @@ # Polish translations for com.mitchellh.ghostty package # Polskie tłumaczenia dla pakietu com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Bartosz Sokorski , 2025. # diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index f6d2f26a2..d2ba0e693 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -1,6 +1,6 @@ # Portuguese translations for com.mitchellh.ghostty package # Traduções em português brasileiro para o pacote com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Gustavo Peres , 2025. # diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 9e9cf8077..0cb533de7 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -1,6 +1,6 @@ # Russian translations for com.mitchellh.ghostty package # Русские переводы для пакета com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # blackzeshi , 2025. # diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index 3de70d61c..5d761f6a4 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -1,5 +1,5 @@ # Turkish translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Emir SARI , 2025. # diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 5a264b537..bde975fc4 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -1,5 +1,5 @@ # Ukrainian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Danylo Zalizchuk , 2025. # diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index ee2c51362..77be8a351 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -1,6 +1,6 @@ # Chinese translations for com.mitchellh.ghostty package # com.mitchellh.ghostty 软件包的简体中文翻译. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Leah , 2025. # diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig index daf523938..a1852bb96 100644 --- a/src/build/GhosttyI18n.zig +++ b/src/build/GhosttyI18n.zig @@ -54,7 +54,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step { "--keyword=C_:1c,2", "--package-name=" ++ domain, "--msgid-bugs-address=m@mitchellh.com", - "--copyright-holder=Mitchell Hashimoto", + "--copyright-holder=\"Mitchell Hashimoto, Ghostty contributors\"", "-o", "-", }); diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index 7ace64cd8..f8e502b45 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -44,6 +44,7 @@ See GitHub issues: # AUTHOR Mitchell Hashimoto +Ghostty contributors # SEE ALSO diff --git a/src/build/mdgen/ghostty_5_footer.md b/src/build/mdgen/ghostty_5_footer.md index c5077ab97..380d83a53 100644 --- a/src/build/mdgen/ghostty_5_footer.md +++ b/src/build/mdgen/ghostty_5_footer.md @@ -36,6 +36,7 @@ See GitHub issues: # AUTHOR Mitchell Hashimoto +Ghostty contributors # SEE ALSO From 12ad0fa4b68c0748fbbbbf3ba89ceecad3567d0c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 10 Jun 2025 12:11:59 -0600 Subject: [PATCH 02/37] font/sprite: add corner pieces from Geometric Shapes block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ◢ ◣ ◤ ◥ ◸ ◹ ◺ ◿ --- src/font/sprite/Box.zig | 123 +++++++++++++++++++++++++++++++ src/font/sprite/Face.zig | 5 ++ src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 1048593 bytes 3 files changed, 128 insertions(+) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index dd02f701b..f5140091d 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -581,6 +581,120 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void // '▟' 0x259f => self.draw_quadrant(canvas, .{ .tr = true, .bl = true, .br = true }), + // '◢' + 0x25e2 => self.draw_corner_triangle_shade(canvas, .br, .on), + // '◣' + 0x25e3 => self.draw_corner_triangle_shade(canvas, .bl, .on), + // '◤' + 0x25e4 => self.draw_corner_triangle_shade(canvas, .tl, .on), + // '◥' + 0x25e5 => self.draw_corner_triangle_shade(canvas, .tr, .on), + + // '◸' + 0x25f8 => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // top edge + self.rect( + canvas, + 0, + 0, + self.metrics.cell_width, + thickness_px, + ); + // left edge + self.rect( + canvas, + 0, + 0, + thickness_px, + self.metrics.cell_height -| 1, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .lower_left, + .upper_right, + ); + }, + // '◹' + 0x25f9 => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // top edge + self.rect( + canvas, + 0, + 0, + self.metrics.cell_width, + thickness_px, + ); + // right edge + self.rect( + canvas, + self.metrics.cell_width -| thickness_px, + 0, + self.metrics.cell_width, + self.metrics.cell_height -| 1, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .upper_left, + .lower_right, + ); + }, + // '◺' + 0x25fa => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // bottom edge + self.rect( + canvas, + 0, + self.metrics.cell_height -| thickness_px, + self.metrics.cell_width, + self.metrics.cell_height, + ); + // left edge + self.rect( + canvas, + 0, + 1, + thickness_px, + self.metrics.cell_height, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .upper_left, + .lower_right, + ); + }, + // '◿' + 0x25ff => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // bottom edge + self.rect( + canvas, + 0, + self.metrics.cell_height -| thickness_px, + self.metrics.cell_width, + self.metrics.cell_height, + ); + // right edge + self.rect( + canvas, + self.metrics.cell_width -| thickness_px, + 1, + self.metrics.cell_width, + self.metrics.cell_height, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .lower_left, + .upper_right, + ); + }, + 0x2800...0x28ff => self.draw_braille(canvas, cp), 0x1fb00...0x1fb3b => self.draw_sextant(canvas, cp), @@ -3197,6 +3311,15 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { else => {}, } } + + // Geometric Shapes: filled and outlined corners + for ([_]u21{ '◢', '◣', '◤', '◥', '◸', '◹', '◺', '◿' }) |char| { + _ = try self.renderGlyph( + alloc, + atlas, + char, + ); + } } test "render all sprites" { diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index f15423ada..af0c0af6a 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -190,6 +190,11 @@ const Kind = enum { // ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ 0x2580...0x259F, + // "Geometric Shapes" block + 0x25e2...0x25e5, // ◢◣◤◥ + 0x25f8...0x25fa, // ◸◹◺ + 0x25ff, // ◿ + // "Braille" block 0x2800...0x28FF, diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm index d5a6cc72906318b235e33bc3ea53be8f88b67193..6082475af7d3e2264cf04061e9f63d7c6e6fdc9e 100644 GIT binary patch delta 12462 zcmeHNdw3L8mhYQ%Rdv@rmCmCRG>~*95f~&uf#h}|%Z@=j23f`By7^#LIuDBvSe z;w;W0rfnw22QV2w-y)$AEl`{UaRFVGpo=CR8R?+AF0>xl*ZgR64h*%XcLDY&IO+Pj+L= zD&`D%$2eFWhO6YXhmPdqwI9lTvFv7$&^w%^SC8i^Rzh;+ zD`s&B6(5IAkAnKd>hYXo*+;65hhI@SR%G!}beDluAE}(~)??W$M_o{NqRyPtLUlz> zteNKUpuZ<~Nj6&@DI5@|5CbO^313<#yB<0+02j`eQ&C@_4E%}o`68rmaJxPQi(4fL zU#yZ-@U=xs3jW|{=?Ret9{6XeH@+T_v+##;9DV{<)(NRYJ4kIKsq11=M_W>NlhhJ9 z8zbX5ZpE|#A`XWNZ@OBL{xl3VnxOtJ{!L~6hBlN=83jX^NDFyxS$jSKqQ{ckD z32=1`a8MuC`N5>~(^h9{8__{qo1`$GrEP+x?SmJkl%Sr3HJc?n4%rKR+8y*iGcA(P zFUj@;Ei*Kghi6HrtdA3IM;=f(tHUjel%Du~3A-pJ`U#ecshK>x zbC_ds&!EL*l30#{R`9)Bc{(0$Qgi5|3TIB`DOf*H%EZHyxzng{JM$P08pSjWWXXyY zi9{lG_{l1EQOG;Sjxbl@_l_rsl6F29kUvM1(35iTH5wZ7h@AqT!}h zibi5IJQ-jY$2_y1<>Kzgl}?^HgWQX*z~|&NaMW2kapZ8tjpes7AfIVCeYotz9kkY+ zoEsqA{ z+?aipEElgUQ+kK>zpzt)jgYz15*Mk%Sq;oh^3q6N6!FeD#0m>BsAq~D$h(#6=xs7f zoE}*JimGDqR-R*2w5j76mPJ&+`mxSM!irGE7w^-O(~;mn}bU8YRIO5j+VIA0|Di z3>F-#<2aUH0FFA71J!`SarDiQO%Zcgep?eqR(9TGa-uI1Wut9l^b+t;loX4tDvoUd z%f;5g{5+y0C)P#ToMhX`p^ArWi?XeYW4n>%hVK6ya7Hiag;npX_hC&Eq#G6Kd^|&~ z+uW0wFBC4yq&3PxYr9UC$d)A!$6?Pkv5kdmwL*qcCf|elc&_2$;ZkXbY?fFZAv)@; zM8>)yget{Q*T1U-NdI;ipc`kntlh?^T zjQwlX(F|{yCp%5PEwUTmp*n{u9>MoS9AC+W;QIw*zEEvZHdSRZ?%gCO;T2>dm2Ot- zo{8hU_5yL%nVgI`pOKSrKUqojsKvP{jB7Hy7zB4l8?~kX`5>DZ9m#IPfCv z?`A1(@lRm6p^razV4o&c|Iu`sh=sYlDi*#`jMbSmbbQ-eLo~mX-C~}9vP8-dr;*r_ zIOZH#Mlx~S3cXg8O7_@s?|L~I{gY)ktzLBQV$Gq&kEbCW#qb!D?+)t^F>yM_z(cG& z6#3$9+*&24nh33g@Gv1vk=?Y$(jk-0G!gu#MxZ0#Lr2{B#yZ&@)M@qK^|;ap>j|&O z?#RH>MpX%UC%DYR!lYb2T}-~I{do>fji|dZ@EDV@hK~L-r_xJ+Rw+6w=ksf@VV^w> ze;COx!LlT$@HFke#i=a=GCFwnE(*dWQ2kT}8cI8`5R;b^I?3H|*Tqvm z3RG|wKr4A$;?ub+xm|o1xhwg%WUFFSJit#f-0~}KH)d|&c1KwMGtDSFe)TK0XkI{d&wQ5W zpITZ>Y`aF`v@}!LA>P8m`VmEZ)9d*!7{R|vn*sUDj?=dBEa4zy&^A7ZN6pD(mYePH zsxh;bpK#I)=H{AUF0I!lc6s#o6v9H|!2^6NLoEYb-E}QntO1Qwuq<2KgUJK5e1ctp zfiIu{{qL(17B{N0(cpr?32_u>I1U?JFub#`KPF!%uah0PWwy5OM>$6%pm&7gzCayG zO;MNVwa1Eg%+PYOHwViu)3#v!Vr@mJ_;@nvl6D1dDF7842Ep90ep0Na=iMvMbdxd7 z4t9pR9sYw-t^b8naJPtF4hAM@ZP-vq($DQt0Ul20Nm!q)tr71(gL*bT7?fQ2yO$*i zkM)8woG}?*6~9CTbu=xAvUWT8w^9;*uET9OV~RB{tREF_6)7%qF(bxxeKl}gy;VxW z7w&;6K|PJt;atGo`NSYhdKCJqneF(EQSr}yj22o(-O7b9n0WiqiBA&C{Fx( z9`wQTy^4k>{4mc#Sq2-h|7fKH%3?yf8Mcc+NVq?pXP`HvmE#MeVGB+f4L>KntLbf| zjunlnldQ?ah9rlRe(!J_3rgX!PJ=6O^BAbb>l)P|ILs>pZnz8Xh)1{yGL51oT}bc2 z+6l1P92dL?{vm$IJQ{Lamm&A!mSWh7(;C%5xP7ezxZysN%5CT?;qy+yR$JIV8>S+zqsuCMM8PMmDnVh413O{u;hwMtT6oFqCG23m5zrwqU~& za8P#%*G>T$Z=4JTLA@Vdp9M)q@HeoW8R>IrMDb>j5S9VQ(%*xEOCNz}!k$)!4R0x` z;kgm6V5qEsWGsCWWW03)Jd2Ty&@-$bigsB*UF2nuLIAZv4@b`*z-jcZhCfBSl!-1Y zsLPX3i{2HCqvv(%^k{phrPN7U2C2B{S=fv<25_U-MmQD)SuWaFQ2Pt88C^R8jI77u zXte!KYM&8?UDb~PH?p2;Z>bP1FTpN!y(n`d>$&!(N@}`)2V~%_JK%YY>|`7ll!9vX z-31*)3@T}2uD}2omTn?v^y$1XYf3piRaOp01KIq{__RAt9RWOd> z%zuJ<@)UlC8&!FYyi-h>Ri?M--&x_kZrUos>8VbSmoHM|4Q2{##@*Wp{LqY|*! zMsOH?_rP%$*3|@j@DWKd`o0CnX?N#TKa^6vRN8S{^KRu6samt@m+wA=eDqC`ov2lV zV)T9YEQfy#`RJdkq=)q{&x*FYakt>Z4X_7W8lV(gwm^UU?lTh|@QxV2$p?>Cs?*-3#t zzLPrbq)y*-?zEj5Y4sGu3xCRoFEc=mqxb6Ts0wMaxBR9`qOA zZfR3FB|%O!CB}91aX#K~0P;x}$AKpygFZM8JOtb*Ite>jSAdBGS0dmV=JEo{r+joh z#Y^Ze1NS?%u|XaFb`fQ59}DX&p?eEX*Sb?|*P>+p1S4O-4OB9ZGV7u;7i3dOJDEyk z2c5tHg$nppf2tkEP-# zpUmLsU!rlUbxX`jE>_)GuL`m>nYwJGR{_K56xOF(_3}?UD#xfFtBqr(&g6O89I8Nb!un^Xx-)OBe?XfN zTSrPet|QYeuh2{~lPZgHVu{w9@O>8hSs-+CMwuny9^}bNhp6LHe0BFr`FGco{$ifa&J|(saQBLk=s24z?*pcYSl!Ac*SdT5aP-s+* z)&9&-{~lJKUvsGDuswEkpJ$PhV;vQgNjdo@A2~a58k||<+ zX@@JI-(V&BnnI_2N2bqaD~kPJ^^c&S8_SeQ z-M8SwR%$U4^=XK_pY_qkymM+ubkw`2I z;_oYQxRnZT|oBOgs?Q7bgBUQ{9$h delta 10213 zcmeHNYj9Lm60VbE9=Y8o84{e32g6N3C2JP5g1lE^cnJp0it?6>;DUk`25@a zDvyjKv_zEDs>lL{2_R<`7XqvZDC{Vd%7U1!S`h?=#Z^EQw))P^%p?$=5sNDSOij+| z{<{0??sNOxiO-wo^X7R=MQKcFY-wDnrPNw#E47!#uhOQxMMoCJYE(7~G;;6OG=J`w zK!xStq~O0aIP))`eh1Z*!!;CiK|c!i0x$$AcuLD8?;33Z>07Wp1zgacbQd^z4XErv zOeW8R_H}GcQrWS%fz<6St<$f@qiFZ(i+)~eeR@Z7w}2#CH^MsX=QVZ3N~kS)M*xs_ zIK&a;1Jo9=UET7Lwq20tE^Or=ce|Bj3P`G+B`x%#6K)`$O||`4+a<`G2CtFFB^~qb7FL#vSGha2o}zBlVY>gkMoTbT#Rc7|;)F%x zHY)rab@J}!zS*snQQ(xOaW6TIzTwU@dMyi^Q^i4Bi;x*_>-d&}Zp;^Hp-kR+tLPQ_ z_A?qO+Mrz#BaxbHT#~Mc|5qdvbsD!3NKy;i#k?6)zrN1C>Q5}Ra3{n^PJIg90_sl&YIp?UcJiv(Vm$p+D(c;*x29cW9-O3*o<_F4P689T!#lFhTC# zFoyA(l6xX*Cg>nIE|jAT8rmNgrCnrdPs3|W?Q`$m561-cTdUb9w;X`<1&~V%Uqu^x zat#&r250!gL34(Ko!o<=GZij?VhYX@b`I7;g(uKPEs7w+WW@tFh5QI~mUZ8P zEDX$h8B#d4(ZY@$aMaFGwdNXdQrYWJM%AASpuF8$3o3gWJBDH|1Ucv|fd@DvahwJ_ z`*lW^f{V>8rL*&$3{!)YUd=vv$3s?qrNZ@)d3H}`VK0VQtg%0XD0dO$tHSlL#u2X8 zs2Wv;3t(M+X1H+MGRB?pCN)_YqGke@a}qD+)RGjb|0q~S*jA-~XKRoXr5}eQXD`^` zA;=Qy$it8>s`M|cMi|V*3=s-Aw=+iAO!s?=7>5rF5FGsZalV$lu^kcryhGWZ#fxa2jU$^?Ty@)Xlgc*r9MOy}b=J+A$ob z(&-oR!$t(&G6JVl_w~>wRKmgIFze#cx%BvQD2>2s1MX%9JQ;In>+RsA1EX*xeYOQ{ zbns!=+Sq0$VjEgo1WoC{7#vBv9zqPYs!wlYnmU2klcR~8%B<&5%^*3Iwr5}(KeUYJ zU_HkjpCQtoaoCcU6+to`D8`Z0vNNuTG^kH6y&UMVcr8^HK@&PKfj#y#hR2dtO~M!G z-D#LprDvOmK?9Me(7J(;O}S|>o)$cUfs56eKrcOp%at`~Umar(E+ujXz5E1L)?0j< z7Cwn9=nwsIHD8o6`Kg?&db@Frpi}8!_gB=!g`N2mRXl?$DD`>lYNAfVW_0>VOrXsJ zv0DUy#(iyxSB3ZE4pF7wY{Y7%)Uvv2PoSjthGJesP4#{N_k=ZUlr|H)gbWovE>q`~ zAkd~Fyfs?kA>1O!yUC`hgn4+2pnd%@k%o@MWnrNt)ysohYlX^L=*{uioN`Msji!&r zQkp#(ogBBF#!SXk)%RiCR4e{+MBK~bV_AG+SbQR;(t@9~M4z5OC4OtX@=U?!!o*1h zf5BS?O?ltel-#2+o^l^$kZ}w$E(BszxsT#U(JG5r<*!BHqA?TkC7L}pEaj z18@kxUV{M3_lS>d- zc)4m8z(c~Pr-tS!y&5uk$jkBR`Qhm%hfY*7_UkOZ>H@@%FTn&Fz7#7}v%g)4cqNN3 zXYqHU#RD#I(Np{IT`E|K>*>dZ2&z>z)(8sb$!5y&6?PR=pm^&Q)KtrV;J*cRd=ry> zxZ$Sm`<3^e@-(pi)pW&6rRn~_y5C-sZm^3)TKhfDrZ@IrGoNmbSH|=gyis=G962~|R<(Vm6oMQ( zwI;kRqsGnf&{%Io2~I<2)a>H!iFR6Xl$||^nVe`)F1y@+`Jnn?20h)lpeu}Y#>$30 z4N&!c*qV;Ck@wQdeOS(IsGD&$mH&$#Z^~|yDm-Cl`t;FU?D^c3PVUhhpKkjF6kYo{ z1-jfXeEQ+2Suxm6cCXTXvFBatLWM2l8Y&zqH-s*H?u^j*RIk^;7{Yz}M|BY|PQ6O+ zWKvp@ft|vqM-$_MS?B5(b^54kkIWb3IDqAb50?#6=lb@+1$dF0i%_>&%%-T*WDu^7 z(EY2nmFnNi&Rmafu|R>$Hh}!Dd#rqI{nf!M$>8sdlagNkMC(f4$K>z%2h4oszD-sN zzCUE1H^=oVqqk4LDbg{@F<07!+Fl^@MEL3 z#cY{vJoju3o}0?;k&lR|nZq!3!HVWgWxP7@x}?Tq*y8BpYJ@aRT)1SEGZh&qqtWid z`bV&5iOeR)ZLpG#EEL)EZV^d!URq5xX=>p8)A`ohg^oG*K^s+HFCD+SV!Xw>L{>sS z3YEysb#rp(89h5omisIE*!eJXh7X=I53S`PoZ1>O{#~PX1gOos`MN98%adwfvs{hH z#(Sz!VM;Q6y5Ho|jPqGO;R4FNKz0*#O4kw|5T;%|>a=zs{Dxo6MTK0Y_lyl!(zHk{ R7rV|sIb7THYWhq{{6F2@nBD*Y From 2b9a6a482017b7bd3a1856e976ff8c2a3b13cecf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 12:11:17 -0700 Subject: [PATCH 03/37] macos: unsplit window shouldn't allow split zooming This was always the case, and is a recent regression from the SplitTree rework. This brings it back to the previous behavior. --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 594a58056..e91199358 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -556,12 +556,15 @@ class BaseTerminalController: NSWindowController, // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Toggle the zoomed state if surfaceTree.zoomed == targetNode { // Already zoomed, unzoom it surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil) } else { + // We require that the split tree have splits + guard surfaceTree.isSplit else { return } + // Not zoomed or different node zoomed, zoom this node surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode) } From 8b5cceed3ecb916f6a5be4d384dcd964402abc3c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 12:30:15 -0700 Subject: [PATCH 04/37] ci: pin gh-action-release to 2.2.2 to workaround issue https://github.com/softprops/action-gh-release/issues/628 --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 42626288c..b6a6c5f6c 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -132,7 +132,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -299,7 +299,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -507,7 +507,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -682,7 +682,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From 1f340b4b2dc8e2f9752bee8702a555e8bd367b51 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 12:39:09 -0700 Subject: [PATCH 05/37] macos: for windowShouldClose, only close the tab if we have multiple Fixes a regression from our undo/redo rework. We were accidentally closing the entire window when the "X" button in the tab bar was clicked. --- 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 c9f8ef216..a984952f9 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1046,7 +1046,12 @@ class TerminalController: BaseTerminalController { //MARK: - NSWindowDelegate override func windowShouldClose(_ sender: NSWindow) -> Bool { - closeWindow(sender) + // If we have tabs, then this should only close the tab. + if window?.tabGroup?.windows.count ?? 0 > 1 { + closeTab(sender) + } else { + closeWindow(sender) + } // We will always explicitly close the window using the above return false From 3db5b3da752b07d3877ab9682f660c76320e82a6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 14:31:41 -0700 Subject: [PATCH 06/37] macos: hidden titlebar windows should cascade on new tab Windows with `macos-titlebar-style = hidden` create new windows when the new tab binding is pressed. This behavior has existed for a long time. However, these windows did not cascade, meaning they'd appear overlapped directly on top of the previous window, which is kind of nasty. This commit changes it so that new windows created via new tab from a hidden titlebar window will cascade. --- .../Terminal/TerminalController.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a984952f9..5916c5921 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -278,11 +278,6 @@ class TerminalController: BaseTerminalController { tg.removeWindow(window) } - // Our windows start out invisible. We need to make it visible. If we - // don't do this then various features such as window blur won't work because - // the macOS APIs only work on a visible window. - controller.showWindow(self) - // If we have the "hidden" titlebar style we want to create new // tabs as windows instead, so just skip adding it to the parent. if (ghostty.config.macosTitlebarStyle != "hidden") { @@ -303,7 +298,19 @@ class TerminalController: BaseTerminalController { } } - window.makeKeyAndOrderFront(self) + // We're dispatching this async because otherwise the lastCascadePoint doesn't + // take effect. Our best theory is there is some next-event-loop-tick logic + // that Cocoa is doing that we need to be after. + DispatchQueue.main.async { + // Only cascade if we aren't fullscreen and are alone in the tab group. + if !window.styleMask.contains(.fullScreen) && + window.tabGroup?.windows.count ?? 1 == 1 { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } + + controller.showWindow(self) + window.makeKeyAndOrderFront(self) + } // It takes an event loop cycle until the macOS tabGroup state becomes // consistent which causes our tab labeling to be off when the "+" button From 990b6a6b0808f3eef1549bdeaf6b5413384141db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:31:07 +0000 Subject: [PATCH 07/37] build(deps): bump softprops/action-gh-release from 2.2.2 to 2.3.2 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.2.2 to 2.3.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2.2.2...v2.3.2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index b6a6c5f6c..73a1ddeeb 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -132,7 +132,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -299,7 +299,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -507,7 +507,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -682,7 +682,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From 4d33a73fc4640b19cb6160942c5bdcd2a8102fb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 13:03:30 -0700 Subject: [PATCH 08/37] wip: redo terminal window styling --- macos/Ghostty.xcodeproj/project.pbxproj | 42 ++++- .../Terminal/TerminalController.swift | 170 +++++------------- .../HiddenTitlebarTerminalWindow.swift | 78 ++++++++ .../LegacyTerminalWindow.swift} | 44 +---- .../Terminal/{ => Window Styles}/Terminal.xib | 8 +- .../Window Styles/TerminalHiddenTitlebar.xib | 31 ++++ .../Terminal/Window Styles/TerminalLegacy.xib | 31 ++++ .../TerminalTransparentTitlebar.xib | 31 ++++ .../Window Styles/TerminalWindow.swift | 75 ++++++++ .../TransparentTitlebarTerminalWindow.swift | 72 ++++++++ .../Helpers/Extensions/NSView+Extension.swift | 27 +++ macos/Sources/Helpers/Fullscreen.swift | 11 +- 12 files changed, 448 insertions(+), 172 deletions(-) create mode 100644 macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift rename macos/Sources/Features/Terminal/{TerminalWindow.swift => Window Styles/LegacyTerminalWindow.swift} (95%) rename macos/Sources/Features/Terminal/{ => Window Styles}/Terminal.xib (86%) create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift create mode 100644 macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 9686dcbd1..594579744 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; - A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; }; + A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC212B2FB6B400E92F16 /* AboutView.swift */; }; @@ -51,6 +51,12 @@ A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; + A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; }; + A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; }; + A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; }; + A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */; }; + A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */; }; + A5593FE92DF927DF00B47B10 /* TerminalTransparentTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; }; A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; }; @@ -129,7 +135,7 @@ 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; - A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; + A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; A51BFC212B2FB6B400E92F16 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; @@ -159,6 +165,12 @@ A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = ""; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; + A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; + A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; }; + A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = ""; }; + A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalLegacy.xib; sourceTree = ""; }; + A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentTitlebarTerminalWindow.swift; sourceTree = ""; }; + A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTransparentTitlebar.xib; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = ""; }; A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; @@ -384,6 +396,21 @@ path = Sources; sourceTree = ""; }; + A5593FDD2DF8D56000B47B10 /* Window Styles */ = { + isa = PBXGroup; + children = ( + A59630992AEE1C6400D64628 /* Terminal.xib */, + A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, + A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, + A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, + A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, + A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, + A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, + A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, + ); + path = "Window Styles"; + sourceTree = ""; + }; A55B7BB429B6F4410055DE60 /* Ghostty */ = { isa = PBXGroup; children = ( @@ -467,11 +494,10 @@ A59630982AEE1C4400D64628 /* Terminal */ = { isa = PBXGroup; children = ( - A59630992AEE1C6400D64628 /* Terminal.xib */, + A5593FDD2DF8D56000B47B10 /* Window Styles */, A596309B2AEE1C9E00D64628 /* TerminalController.swift */, A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, - A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */, AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */, @@ -647,9 +673,11 @@ buildActionMask = 2147483647; files = ( FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */, + A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */, 29C15B1D2CDC3B2900520DD4 /* bat in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */, + A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */, A546F1142D7B68D7003B11A0 /* locale in Resources */, A5985CE62C33060F00C57AD3 /* man in Resources */, 9351BE8E3D22937F003B3499 /* nvim in Resources */, @@ -658,6 +686,7 @@ FC5218FA2D10FFCE004C93E0 /* zsh in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */, + A5593FE92DF927DF00B47B10 /* TerminalTransparentTitlebar.xib in Resources */, A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */, A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, @@ -702,6 +731,7 @@ A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, + A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */, A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, @@ -709,6 +739,7 @@ A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, + A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */, A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */, A59630972AEE163600D64628 /* HostingWindow.swift in Sources */, A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */, @@ -734,9 +765,10 @@ A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, - A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, + A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 5916c5921..7fb8f9a07 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -6,7 +6,22 @@ import GhosttyKit /// A classic, tabbed terminal experience. class TerminalController: BaseTerminalController { - override var windowNibName: NSNib.Name? { "Terminal" } + override var windowNibName: NSNib.Name? { + //NOTE(mitchellh): switch to this when we've transitioned all legacy logic out + //let defaultValue = "Terminal" + let defaultValue = "TerminalLegacy" + + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } + let config = appDelegate.ghostty.config + let nib = switch config.macosTitlebarStyle { + case "tabs": defaultValue + case "hidden": "TerminalHiddenTitlebar" + case "transparent": "TerminalTransparentTitlebar" + default: defaultValue + } + + return nib + } /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail @@ -114,7 +129,7 @@ class TerminalController: BaseTerminalController { invalidateRestorableState() // Update our zoom state - if let window = window as? TerminalWindow { + if let window = window as? LegacyTerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } @@ -129,11 +144,6 @@ class TerminalController: BaseTerminalController { // When our fullscreen state changes, we resync our appearance because some // properties change when fullscreen or not. guard let focusedSurface else { return } - if (!(fullscreenStyle?.isFullscreen ?? false) && - ghostty.config.macosTitlebarStyle == "hidden") - { - applyHiddenTitlebarStyle() - } syncAppearance(focusedSurface.derivedConfig) } @@ -278,9 +288,8 @@ class TerminalController: BaseTerminalController { tg.removeWindow(window) } - // If we have the "hidden" titlebar style we want to create new - // tabs as windows instead, so just skip adding it to the parent. - if (ghostty.config.macosTitlebarStyle != "hidden") { + // If we don't allow tabs then we create a new window instead. + if (window.tabbingMode != .disallowed) { // Add the window to the tab group and show it. switch ghostty.config.windowNewTabPosition { case "end": @@ -389,7 +398,7 @@ class TerminalController: BaseTerminalController { // Reset this to false. It'll be set back to true later. tabListenForFrame = false - guard let windows = self.window?.tabbedWindows as? [TerminalWindow] else { return } + guard let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] else { return } // We only listen for frame changes if we have more than 1 window, // otherwise the accessory view doesn't matter. @@ -440,7 +449,11 @@ class TerminalController: BaseTerminalController { } private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { - guard let window = self.window as? TerminalWindow else { return } + if let window = window as? TerminalWindow { + window.syncAppearance(surfaceConfig) + } + + guard let window = self.window as? LegacyTerminalWindow else { return } // Set our explicit appearance if we need to based on the configuration. window.appearance = surfaceConfig.windowAppearance @@ -523,31 +536,6 @@ class TerminalController: BaseTerminalController { } } - private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { - guard let window else { return } - - // If we don't have an X/Y then we try to use the previously saved window pos. - guard let x, let y else { - if (!LastWindowPosition.shared.restore(window)) { - window.center() - } - - return - } - - // Prefer the screen our window is being placed on otherwise our primary screen. - guard let screen = window.screen ?? NSScreen.screens.first else { - window.center() - return - } - - // Orient based on the top left of the primary monitor - let frame = screen.visibleFrame - window.setFrameOrigin(.init( - x: frame.minX + CGFloat(x), - y: frame.maxY - (CGFloat(y) + window.frame.height))) - } - /// Returns the default size of the window. This is contextual based on the focused surface because /// the focused surface may specify a different default size than others. private var defaultSize: NSRect? { @@ -889,52 +877,9 @@ class TerminalController: BaseTerminalController { shouldCascadeWindows = false } - fileprivate func hideWindowButtons() { - guard let window else { return } - - window.standardWindowButton(.closeButton)?.isHidden = true - window.standardWindowButton(.miniaturizeButton)?.isHidden = true - window.standardWindowButton(.zoomButton)?.isHidden = true - } - - fileprivate func applyHiddenTitlebarStyle() { - guard let window else { return } - - window.styleMask = [ - // We need `titled` in the mask to get the normal window frame - .titled, - - // Full size content view so we can extend - // content in to the hidden titlebar's area - .fullSizeContentView, - - .resizable, - .closable, - .miniaturizable, - ] - - // Hide the title - window.titleVisibility = .hidden - window.titlebarAppearsTransparent = true - - // Hide the traffic lights (window control buttons) - hideWindowButtons() - - // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. - window.tabbingMode = .disallowed - - // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are - // some operations that appear to bring back the titlebar visibility so this ensures - // it is gone forever. - if let themeFrame = window.contentView?.superview, - let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") { - titleBarContainer.isHidden = true - } - } - override func windowDidLoad() { super.windowDidLoad() - guard let window = window as? TerminalWindow else { return } + guard let window else { return } // Store our initial frame so we can know our default later. initialFrame = window.frame @@ -952,9 +897,6 @@ class TerminalController: BaseTerminalController { window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } - // If window decorations are disabled, remove our title - if (!config.windowDecorations) { window.styleMask.remove(.titled) } - // If we have only a single surface (no splits) and there is a default size then // we should resize to that default size. if case let .leaf(view) = surfaceTree.root { @@ -967,42 +909,29 @@ class TerminalController: BaseTerminalController { } } - // Set our window positioning to coordinates if config value exists, otherwise - // fallback to original centering behavior - setInitialWindowPosition( - x: config.windowPositionX, - y: config.windowPositionY, - windowDecorations: config.windowDecorations) - - if config.macosWindowButtons == .hidden { - hideWindowButtons() - } - - // Make sure our theme is set on the window so styling is correct. - if let windowTheme = config.windowTheme { - window.windowTheme = .init(rawValue: windowTheme) - } - - // Handle titlebar tabs config option. Something about what we do while setting up the - // titlebar tabs interferes with the window restore process unless window.tabbingMode - // is set to .preferred, so we set it, and switch back to automatic as soon as we can. - if (config.macosTitlebarStyle == "tabs") { - window.tabbingMode = .preferred - window.titlebarTabs = true - DispatchQueue.main.async { - window.tabbingMode = .automatic + // TODO: remove + if let window = window as? LegacyTerminalWindow { + // Handle titlebar tabs config option. Something about what we do while setting up the + // titlebar tabs interferes with the window restore process unless window.tabbingMode + // is set to .preferred, so we set it, and switch back to automatic as soon as we can. + if (config.macosTitlebarStyle == "tabs") { + window.tabbingMode = .preferred + window.titlebarTabs = true + DispatchQueue.main.async { + window.tabbingMode = .automatic + } + } else if (config.macosTitlebarStyle == "transparent") { + window.transparentTabs = true } - } else if (config.macosTitlebarStyle == "transparent") { - window.transparentTabs = true - } - if window.hasStyledTabs { - // Set the background color of the window - let backgroundColor = NSColor(config.backgroundColor) - window.backgroundColor = backgroundColor + if window.hasStyledTabs { + // Set the background color of the window + let backgroundColor = NSColor(config.backgroundColor) + window.backgroundColor = backgroundColor - // This makes sure our titlebar renders correctly when there is a transparent background - window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + // This makes sure our titlebar renders correctly when there is a transparent background + window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + } } // Initialize our content view to the SwiftUI root @@ -1012,11 +941,6 @@ class TerminalController: BaseTerminalController { delegate: self )) - // If our titlebar style is "hidden" we adjust the style appropriately - if (config.macosTitlebarStyle == "hidden") { - applyHiddenTitlebarStyle() - } - // In various situations, macOS automatically tabs new windows. Ghostty handles // its own tabbing so we DONT want this behavior. This detects this scenario and undoes // it. @@ -1218,7 +1142,7 @@ class TerminalController: BaseTerminalController { override func titleDidChange(to: String) { super.titleDidChange(to: to) - guard let window = window as? TerminalWindow else { return } + guard let window = window as? LegacyTerminalWindow else { return } // Custom toolbar-based title used when titlebar tabs are enabled. if let toolbar = window.toolbar as? TerminalToolbar { diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift new file mode 100644 index 000000000..f2d3b9b85 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -0,0 +1,78 @@ +import AppKit + +class HiddenTitlebarTerminalWindow: TerminalWindow { + override func awakeFromNib() { + super.awakeFromNib() + + // Setup our initial style + reapplyHiddenStyle() + + // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(fullscreenDidExit(_:)), + name: .fullscreenDidExit, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // We override this so that with the hidden titlebar style the titlebar + // area is not draggable. + override var contentLayoutRect: CGRect { + var rect = super.contentLayoutRect + rect.origin.y = 0 + rect.size.height = self.frame.height + return rect + } + + /// Apply the hidden titlebar style. + private func reapplyHiddenStyle() { + styleMask = [ + // We need `titled` in the mask to get the normal window frame + .titled, + + // Full size content view so we can extend + // content in to the hidden titlebar's area + .fullSizeContentView, + + .resizable, + .closable, + .miniaturizable, + ] + + // Hide the title + titleVisibility = .hidden + titlebarAppearsTransparent = true + + // Hide the traffic lights (window control buttons) + standardWindowButton(.closeButton)?.isHidden = true + standardWindowButton(.miniaturizeButton)?.isHidden = true + standardWindowButton(.zoomButton)?.isHidden = true + + // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. + tabbingMode = .disallowed + + // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are + // some operations that appear to bring back the titlebar visibility so this ensures + // it is gone forever. + if let themeFrame = contentView?.superview, + let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") { + titleBarContainer.isHidden = true + } + } + + // MARK: Notifications + + @objc private func fullscreenDidExit(_ notification: Notification) { + // Make sure they're talking about our window + guard let fullscreen = notification.object as? FullscreenBase else { return } + guard fullscreen.window == self else { return } + + // On exit we need to reapply the style because macOS breaks it usually. + // This is safe to call repeatedly so if its not broken its still safe. + reapplyHiddenStyle() + } +} diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift similarity index 95% rename from macos/Sources/Features/Terminal/TerminalWindow.swift rename to macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift index 0b43582f3..208e86343 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift @@ -1,9 +1,8 @@ import Cocoa -class TerminalWindow: NSWindow { - /// This is the key in UserDefaults to use for the default `level` value. - static let defaultLevelKey: String = "TerminalDefaultLevel" - +/// The terminal window that we originally had in Ghostty for a long time. Kind of a soupy mess +/// of styling. +class LegacyTerminalWindow: TerminalWindow { @objc dynamic var keyEquivalent: String = "" /// This is used to determine if certain elements should be drawn light or dark and should @@ -56,11 +55,6 @@ class TerminalWindow: NSWindow { } } - // Both of these must be true for windows without decorations to be able to - // still become key/main and receive events. - override var canBecomeKey: Bool { return true } - override var canBecomeMain: Bool { return true } - // MARK: - Lifecycle override func awakeFromNib() { @@ -77,8 +71,6 @@ class TerminalWindow: NSWindow { if titlebarTabs { generateToolbar() } - - level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } deinit { @@ -135,25 +127,6 @@ class TerminalWindow: NSWindow { } } - // We override this so that with the hidden titlebar style the titlebar - // area is not draggable. - override var contentLayoutRect: CGRect { - var rect = super.contentLayoutRect - - // If we are using a hidden titlebar style, the content layout is the - // full frame making it so that it is not draggable. - if let controller = windowController as? TerminalController, - controller.derivedConfig.macosTitlebarStyle == "hidden" { - rect.origin.y = 0 - rect.size.height = self.frame.height - } - return rect - } - - // The window theme configuration from Ghostty. This is used to control some - // behaviors that don't look quite right in certain situations. - var windowTheme: TerminalWindowTheme? - // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to // remove the effect view if the default tab bar is being used since the effect created in @@ -703,7 +676,7 @@ fileprivate class WindowDragView: NSView { fileprivate class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. - private weak var terminalWindow: TerminalWindow? + private weak var terminalWindow: LegacyTerminalWindow? private let isLightTheme: Bool private let overlayLayer = VibrantLayer() @@ -731,7 +704,7 @@ fileprivate class WindowButtonsBackdropView: NSView { fatalError("init(coder:) has not been implemented") } - init(window: TerminalWindow) { + init(window: LegacyTerminalWindow) { self.terminalWindow = window self.isLightTheme = window.isLightTheme @@ -746,10 +719,3 @@ fileprivate class WindowButtonsBackdropView: NSView { layer?.addSublayer(overlayLayer) } } - -enum TerminalWindowTheme: String { - case auto - case system - case light - case dark -} diff --git a/macos/Sources/Features/Terminal/Terminal.xib b/macos/Sources/Features/Terminal/Window Styles/Terminal.xib similarity index 86% rename from macos/Sources/Features/Terminal/Terminal.xib rename to macos/Sources/Features/Terminal/Window Styles/Terminal.xib index 65b03b6eb..cfbb2221c 100644 --- a/macos/Sources/Features/Terminal/Terminal.xib +++ b/macos/Sources/Features/Terminal/Window Styles/Terminal.xib @@ -1,8 +1,8 @@ - + - + @@ -17,10 +17,10 @@ - + - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib new file mode 100644 index 000000000..eb4675657 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib new file mode 100644 index 000000000..61ed6f782 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib new file mode 100644 index 000000000..ada6959b3 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift new file mode 100644 index 000000000..74744a962 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -0,0 +1,75 @@ +import AppKit + +/// The base class for all standalone, "normal" terminal windows. This sets the basic +/// style and configuration of the window based on the app configuration. +class TerminalWindow: NSWindow { + /// This is the key in UserDefaults to use for the default `level` value. This is + /// used by the manual float on top menu item feature. + static let defaultLevelKey: String = "TerminalDefaultLevel" + + // MARK: NSWindow Overrides + + override func awakeFromNib() { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return } + + // All new windows are based on the app config at the time of creation. + let config = appDelegate.ghostty.config + + // If window decorations are disabled, remove our title + if (!config.windowDecorations) { styleMask.remove(.titled) } + + // Set our window positioning to coordinates if config value exists, otherwise + // fallback to original centering behavior + setInitialWindowPosition( + x: config.windowPositionX, + y: config.windowPositionY, + windowDecorations: config.windowDecorations) + + // If our traffic buttons should be hidden, then hide them + if config.macosWindowButtons == .hidden { + hideWindowButtons() + } + + // Get our saved level + level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal + } + + // Both of these must be true for windows without decorations to be able to + // still become key/main and receive events. + override var canBecomeKey: Bool { return true } + override var canBecomeMain: Bool { return true } + + // MARK: Positioning And Styling + + /// This is called by the controller when there is a need to reset the window apperance. + func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {} + + private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { + // If we don't have an X/Y then we try to use the previously saved window pos. + guard let x, let y else { + if (!LastWindowPosition.shared.restore(self)) { + center() + } + + return + } + + // Prefer the screen our window is being placed on otherwise our primary screen. + guard let screen = screen ?? NSScreen.screens.first else { + center() + return + } + + // Orient based on the top left of the primary monitor + let frame = screen.visibleFrame + setFrameOrigin(.init( + x: frame.minX + CGFloat(x), + y: frame.maxY - (CGFloat(y) + frame.height))) + } + + private func hideWindowButtons() { + standardWindowButton(.closeButton)?.isHidden = true + standardWindowButton(.miniaturizeButton)?.isHidden = true + standardWindowButton(.zoomButton)?.isHidden = true + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift new file mode 100644 index 000000000..4b3336874 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -0,0 +1,72 @@ +import AppKit + +class TransparentTitlebarTerminalWindow: TerminalWindow { + private var reapplyTimer: Timer? + + override func awakeFromNib() { + super.awakeFromNib() + } + + deinit { + reapplyTimer?.invalidate() + } + + override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + if #available(macOS 26.0, *) { + syncAppearanceTahoe(surfaceConfig) + } else { + syncAppearanceVentura(surfaceConfig) + } + } + + @available(macOS 26.0, *) + private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + guard let titlebarBackgroundView else { return } + titlebarBackgroundView.isHidden = true + backgroundColor = NSColor(surfaceConfig.backgroundColor) + } + + @available(macOS 13.0, *) + private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + guard let titlebarContainer else { return } + + let configBgColor = NSColor(surfaceConfig.backgroundColor) + + // Set our window background color so it shows up + backgroundColor = configBgColor + + // Set the background color of our titlebar to match + titlebarContainer.wantsLayer = true + titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor + } + + private var titlebarBackgroundView: NSView? { + titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView") + } + + private var titlebarContainer: NSView? { + // If we aren't fullscreen then the titlebar container is part of our window. + if !styleMask.contains(.fullScreen) { + return titlebarContainerView + } + + // If we are fullscreen, the titlebar container view is part of a separate + // "fullscreen window", we need to find the window and then get the view. + for window in NSApplication.shared.windows { + // This is the private window class that contains the toolbar + guard window.className == "NSToolbarFullScreenWindow" else { continue } + + // The parent will match our window. This is used to filter the correct + // fullscreen window if we have multiple. + guard window.parent == self else { continue } + + return titlebarContainerView + } + + return nil + } + + private var titlebarContainerView: NSView? { + contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") + } +} diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 48284df74..121d9a62a 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -13,6 +13,19 @@ extension NSView { return false } +} + +// MARK: View Traversal and Search + +extension NSView { + /// Returns the absolute root view by walking up the superview chain. + var rootView: NSView { + var root: NSView = self + while let superview = root.superview { + root = superview + } + return root + } /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { @@ -54,4 +67,18 @@ extension NSView { return nil } + + /// Finds and returns the first view with the given class name starting from the absolute root of the view hierarchy. + /// This includes private views like title bar views. + func firstViewFromRoot(withClassName name: String) -> NSView? { + let root = rootView + + // Check if the root view itself matches + if String(describing: type(of: root)) == name { + return root + } + + // Otherwise search descendants + return root.firstDescendant(withClassName: name) + } } diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 6b10ceb40..3200608d0 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -78,10 +78,12 @@ class FullscreenBase { } @objc private func didEnterFullScreenNotification(_ notification: Notification) { + NotificationCenter.default.post(name: .fullscreenDidEnter, object: self) delegate?.fullscreenDidChange() } @objc private func didExitFullScreenNotification(_ notification: Notification) { + NotificationCenter.default.post(name: .fullscreenDidExit, object: self) delegate?.fullscreenDidChange() } } @@ -238,6 +240,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.window.makeFirstResponder(firstResponder) } + NotificationCenter.default.post(name: .fullscreenDidEnter, object: self) self.delegate?.fullscreenDidChange() } } @@ -268,7 +271,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. - if let window = window as? TerminalWindow, + if let window = window as? LegacyTerminalWindow, window.titlebarTabs { window.titlebarTabs = true } @@ -303,6 +306,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { window.makeKeyAndOrderFront(nil) // Notify the delegate + NotificationCenter.default.post(name: .fullscreenDidExit, object: self) self.delegate?.fullscreenDidChange() } @@ -422,3 +426,8 @@ class NonNativeFullscreenVisibleMenu: NonNativeFullscreen { class NonNativeFullscreenPaddedNotch: NonNativeFullscreen { override var properties: Properties { Properties(paddedNotch: true) } } + +extension Notification.Name { + static let fullscreenDidEnter = Notification.Name("com.mitchellh.fullscreenDidEnter") + static let fullscreenDidExit = Notification.Name("com.mitchellh.fullscreenDidExit") +} From 7d02977482a3e8f6c1565c24ee26986038e0bf16 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 07:00:40 -0700 Subject: [PATCH 09/37] macos: add NSView hierarchy debugging code --- .../TransparentTitlebarTerminalWindow.swift | 11 ++++- .../Helpers/Extensions/NSView+Extension.swift | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 4b3336874..d9d42365a 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -1,14 +1,21 @@ import AppKit class TransparentTitlebarTerminalWindow: TerminalWindow { - private var reapplyTimer: Timer? + private var debugTimer: Timer? override func awakeFromNib() { super.awakeFromNib() + + // Debug timer to print view hierarchy every second + debugTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + print("=== TransparentTitlebarTerminalWindow Debug ===") + self?.contentView?.rootView.printViewHierarchy() + print("===============================================\n") + } } deinit { - reapplyTimer?.invalidate() + debugTimer?.invalidate() } override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 121d9a62a..14c07f6c9 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -82,3 +82,51 @@ extension NSView { return root.firstDescendant(withClassName: name) } } + +// MARK: Debug + +extension NSView { + /// Prints the view hierarchy from the root in a tree-like ASCII format. + /// + /// I need this because the "Capture View Hiearchy" was broken under some scenarios in + /// Xcode 26 (FB17912569). But, I kept it around because it might be useful to print out + /// the view hierarchy without halting the program. + func printViewHierarchy() { + let root = rootView + print("View Hierarchy from Root:") + print(root.viewHierarchyDescription()) + } + + /// Returns a string representation of the view hierarchy in a tree-like format. + private func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { + var result = "" + + // Add the tree branch characters + result += indent + if !indent.isEmpty { + result += isLast ? "└── " : "├── " + } + + // Add the class name and optional identifier + let className = String(describing: type(of: self)) + result += className + + // Add identifier if present + if let identifier = self.identifier { + result += " (id: \(identifier.rawValue))" + } + + // Add frame info + result += " [frame: \(frame)]" + result += "\n" + + // Process subviews + for (index, subview) in subviews.enumerated() { + let isLastSubview = index == subviews.count - 1 + let newIndent = indent + (isLast ? " " : "│ ") + result += subview.viewHierarchyDescription(indent: newIndent, isLast: isLastSubview) + } + + return result + } +} From 6ce7f612a66ea6e50884b426a4b0c3cfd6dd68be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 07:29:44 -0700 Subject: [PATCH 10/37] macos: transparent titlebar needs to be rehidden when tabs change --- .../TransparentTitlebarTerminalWindow.swift | 64 ++++++++++++++++--- .../Helpers/Extensions/NSView+Extension.swift | 30 +++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index d9d42365a..9f7b5e62f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -1,24 +1,39 @@ import AppKit class TransparentTitlebarTerminalWindow: TerminalWindow { - private var debugTimer: Timer? + // We need to restore our last synced appearance so that we can reapply + // the appearance in certain scenarios. + private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig? + + // KVO observations + private var tabGroupWindowsObservation: NSKeyValueObservation? override func awakeFromNib() { super.awakeFromNib() - - // Debug timer to print view hierarchy every second - debugTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in - print("=== TransparentTitlebarTerminalWindow Debug ===") - self?.contentView?.rootView.printViewHierarchy() - print("===============================================\n") - } + + // We need to observe the tab group because we need to redraw on + // tabbed window changes and there is no notification for that. + setupTabGroupObservation() } deinit { - debugTimer?.invalidate() + tabGroupWindowsObservation?.invalidate() } + override func becomeMain() { + // On macOS Tahoe, the tab bar redraws and restores non-transparency when + // switching tabs. To overcome this, we resync the appearance whenever this + // window becomes main (focused). + if #available(macOS 26.0, *), + let lastSurfaceConfig { + syncAppearance(lastSurfaceConfig) + } + } + + // MARK: Appearance + override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + lastSurfaceConfig = surfaceConfig if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) } else { @@ -47,6 +62,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor } + // MARK: View Finders + private var titlebarBackgroundView: NSView? { titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView") } @@ -76,4 +93,33 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { private var titlebarContainerView: NSView? { contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") } + + // MARK: Tab Group Observation + + private func setupTabGroupObservation() { + // Remove existing observation if any + tabGroupWindowsObservation?.invalidate() + tabGroupWindowsObservation = nil + + // Check if tabGroup is available + guard let tabGroup else { return } + + // Set up KVO observation for the windows array. Whenever it changes + // we resync the appearance because it can cause macOS to redraw the + // tab bar. + tabGroupWindowsObservation = tabGroup.observe( + \.windows, + options: [.new] + ) { [weak self] _, _ in + // NOTE: At one point, I guarded this on only if we went from 0 to N + // or N to 0 under the assumption that the tab bar would only get + // replaced on those cases. This turned out to be false (Tahoe). + // It's cheap enough to always redraw this so we should just do it + // unconditionally. + + guard let self else { return } + guard let lastSurfaceConfig else { return } + self.syncAppearance(lastSurfaceConfig) + } + } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 14c07f6c9..0cf71138d 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -118,6 +118,36 @@ extension NSView { // Add frame info result += " [frame: \(frame)]" + + // Add visual properties + var properties: [String] = [] + + // Hidden status + if isHidden { + properties.append("hidden") + } + + // Opaque status + properties.append(isOpaque ? "opaque" : "transparent") + + // Layer backing + if wantsLayer { + properties.append("layer-backed") + if let bgColor = layer?.backgroundColor { + let color = NSColor(cgColor: bgColor) + if let rgb = color?.usingColorSpace(.deviceRGB) { + properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", + rgb.redComponent * 255, + rgb.greenComponent * 255, + rgb.blueComponent * 255, + rgb.alphaComponent)) + } else { + properties.append("bg:\(bgColor)") + } + } + } + + result += " [\(properties.joined(separator: ", "))]" result += "\n" // Process subviews From 3595b2a8476ff16dae2ba266ef672d4fa295a777 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 12:37:15 -0700 Subject: [PATCH 11/37] macos: transparent titlebar handles transparent background --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Terminal/TerminalController.swift | 53 +++-------- .../Window Styles/TerminalWindow.swift | 95 ++++++++++++++++++- .../TransparentTitlebarTerminalWindow.swift | 22 ++++- .../Helpers/Extensions/Double+Extension.swift | 5 + 5 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/Double+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 594579744..c00d6119a 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; + A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; @@ -134,6 +135,7 @@ 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; + A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; @@ -464,6 +466,7 @@ isa = PBXGroup; children = ( A586366A2DF0A98900E04A10 /* Array+Extension.swift */, + A50297342DFA0F3300B4E924 /* Double+Extension.swift */, A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, @@ -737,6 +740,7 @@ A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, + A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7fb8f9a07..5adef8ded 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -449,57 +449,34 @@ class TerminalController: BaseTerminalController { } private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + // Let our window handle its own appearance if let window = window as? TerminalWindow { window.syncAppearance(surfaceConfig) } - guard let window = self.window as? LegacyTerminalWindow else { return } + guard let window else { return } - // Set our explicit appearance if we need to based on the configuration. - window.appearance = surfaceConfig.windowAppearance + if let window = window as? LegacyTerminalWindow { + // Update our window light/darkness based on our updated background color + window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor - // Update our window light/darkness based on our updated background color - window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor + // Sync our zoom state for splits + window.surfaceIsZoomed = surfaceTree.zoomed != nil - // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree.zoomed != nil + // Set the font for the window and tab titles. + if let titleFontName = surfaceConfig.windowTitleFontFamily { + window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) + } else { + window.titlebarFont = nil + } + } // If our window is not visible, then we do nothing. Some things such as blurring // have no effect if the window is not visible. Ultimately, we'll have this called // at some point when a surface becomes focused. guard window.isVisible else { return } - // Set the font for the window and tab titles. - if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) - } else { - window.titlebarFont = nil - } - - // If we have window transparency then set it transparent. Otherwise set it opaque. - - // Window transparency only takes effect if our window is not native fullscreen. - // In native fullscreen we disable transparency/opacity because the background - // becomes gray and widgets show through. - if (!window.styleMask.contains(.fullScreen) && - surfaceConfig.backgroundOpacity < 1 - ) { - window.isOpaque = false - - // This is weird, but we don't use ".clear" because this creates a look that - // matches Terminal.app much more closer. This lets users transition from - // Terminal.app more easily. - window.backgroundColor = .white.withAlphaComponent(0.001) - - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) - } else { - window.isOpaque = true - window.backgroundColor = .windowBackgroundColor - } - - window.hasShadow = surfaceConfig.macosWindowShadow - - guard window.hasStyledTabs else { return } + guard let window = window as? LegacyTerminalWindow, window.hasStyledTabs else { return } // Our background color depends on if our focused surface borders the top or not. // If it does, we match the focused surface. If it doesn't, we use the app diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 74744a962..daf5b4554 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -1,4 +1,5 @@ import AppKit +import GhosttyKit /// The base class for all standalone, "normal" terminal windows. This sets the basic /// style and configuration of the window based on the app configuration. @@ -7,6 +8,14 @@ class TerminalWindow: NSWindow { /// used by the manual float on top menu item feature. static let defaultLevelKey: String = "TerminalDefaultLevel" + /// The configuration derived from the Ghostty config so we don't need to rely on references. + private var derivedConfig: DerivedConfig? + + /// Gets the terminal controller from the window controller. + var terminalController: TerminalController? { + windowController as? TerminalController + } + // MARK: NSWindow Overrides override func awakeFromNib() { @@ -15,6 +24,9 @@ class TerminalWindow: NSWindow { // All new windows are based on the app config at the time of creation. let config = appDelegate.ghostty.config + // Setup our initial config + derivedConfig = .init(config) + // If window decorations are disabled, remove our title if (!config.windowDecorations) { styleMask.remove(.titled) } @@ -42,7 +54,71 @@ class TerminalWindow: NSWindow { // MARK: Positioning And Styling /// This is called by the controller when there is a need to reset the window apperance. - func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {} + func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + // If our window is not visible, then we do nothing. Some things such as blurring + // have no effect if the window is not visible. Ultimately, we'll have this called + // at some point when a surface becomes focused. + guard isVisible else { return } + + // Basic properties + appearance = surfaceConfig.windowAppearance + hasShadow = surfaceConfig.macosWindowShadow + + // Window transparency only takes effect if our window is not native fullscreen. + // In native fullscreen we disable transparency/opacity because the background + // becomes gray and widgets show through. + if !styleMask.contains(.fullScreen) && + surfaceConfig.backgroundOpacity < 1 + { + isOpaque = false + + // This is weird, but we don't use ".clear" because this creates a look that + // matches Terminal.app much more closer. This lets users transition from + // Terminal.app more easily. + backgroundColor = .white.withAlphaComponent(0.001) + + if let appDelegate = NSApp.delegate as? AppDelegate { + ghostty_set_window_background_blur( + appDelegate.ghostty.app, + Unmanaged.passUnretained(self).toOpaque()) + } + } else { + isOpaque = true + + let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor) + self.backgroundColor = backgroundColor.withAlphaComponent(1) + } + } + + /// The preferred window background color. The current window background color may not be set + /// to this, since this is dynamic based on the state of the surface tree. + /// + /// This background color will include alpha transparency if set. If the caller doesn't want that, + /// change the alpha channel again manually. + var preferredBackgroundColor: NSColor? { + if let terminalController, !terminalController.surfaceTree.isEmpty { + // If our focused surface borders the top then we prefer its background color + if let focusedSurface = terminalController.focusedSurface, + let treeRoot = terminalController.surfaceTree.root, + let focusedNode = treeRoot.node(view: focusedSurface), + treeRoot.spatial().doesBorder(side: .up, from: focusedNode), + let backgroundcolor = focusedSurface.backgroundColor { + let alpha = focusedSurface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) + return NSColor(backgroundcolor).withAlphaComponent(alpha) + } + + // Doesn't border the top or we don't have a focused surface, so + // we try to match the top-left surface. + let topLeftSurface = terminalController.surfaceTree.root?.leftmostLeaf() + if let topLeftBgColor = topLeftSurface?.backgroundColor { + let alpha = topLeftSurface?.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) ?? 1 + return NSColor(topLeftBgColor).withAlphaComponent(alpha) + } + } + + let alpha = derivedConfig?.backgroundOpacity.clamped(to: 0.001...1) ?? 1 + return derivedConfig?.backgroundColor.withAlphaComponent(alpha) + } private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { // If we don't have an X/Y then we try to use the previously saved window pos. @@ -72,4 +148,21 @@ class TerminalWindow: NSWindow { standardWindowButton(.miniaturizeButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true } + + // MARK: Config + + struct DerivedConfig { + let backgroundColor: NSColor + let backgroundOpacity: Double + + init() { + self.backgroundColor = NSColor.windowBackgroundColor + self.backgroundOpacity = 1 + } + + init(_ config: Ghostty.Config) { + self.backgroundColor = NSColor(config.backgroundColor) + self.backgroundOpacity = config.backgroundOpacity + } + } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 9f7b5e62f..dfe2d35a1 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -33,6 +33,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // MARK: Appearance override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + super.syncAppearance(surfaceConfig) + lastSurfaceConfig = surfaceConfig if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) @@ -43,9 +45,23 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 26.0, *) private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { - guard let titlebarBackgroundView else { return } - titlebarBackgroundView.isHidden = true - backgroundColor = NSColor(surfaceConfig.backgroundColor) + // When we have transparency, we need to set the titlebar background to match the + // window background but with opacity. The window background is set using the + // "preferred background color" property. + // + // As an inverse, if we don't have transparency, we don't bother with this because + // the window background will be set to the correct color so we can just hide the + // titlebar completely and we're good to go. + if !isOpaque { + if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") { + titlebarView.wantsLayer = true + titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor + } + } + + // In all cases, we have to hide the background view since this has multiple subviews + // that force a background color. + titlebarBackgroundView?.isHidden = true } @available(macOS 13.0, *) diff --git a/macos/Sources/Helpers/Extensions/Double+Extension.swift b/macos/Sources/Helpers/Extensions/Double+Extension.swift new file mode 100644 index 000000000..8d1151bac --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Double+Extension.swift @@ -0,0 +1,5 @@ +extension Double { + func clamped(to range: ClosedRange) -> Double { + return Swift.min(Swift.max(self, range.lowerBound), range.upperBound) + } +} From dfa7a114def0f4a9617ef3433dd67c3bb288b924 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 13:12:25 -0700 Subject: [PATCH 12/37] macos: make transparent titlebars robust against show/hide tabs --- .../TransparentTitlebarTerminalWindow.swift | 81 +++++++++++++------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index dfe2d35a1..98dd9f834 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -1,32 +1,41 @@ import AppKit +/// A terminal window style that provides a transparent titlebar effect. With this effect, the titlebar +/// matches the background color of the window. class TransparentTitlebarTerminalWindow: TerminalWindow { - // We need to restore our last synced appearance so that we can reapply - // the appearance in certain scenarios. + /// Stores the last surface configuration to reapply appearance when needed. + /// This is necessary because various macOS operations (tab switching, tab bar + /// visibility changes) can reset the titlebar appearance. private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig? - // KVO observations + /// KVO observation for tab group window changes. private var tabGroupWindowsObservation: NSKeyValueObservation? + private var tabBarVisibleObservation: NSKeyValueObservation? override func awakeFromNib() { super.awakeFromNib() - // We need to observe the tab group because we need to redraw on - // tabbed window changes and there is no notification for that. - setupTabGroupObservation() + // Setup all the KVO we will use, see the docs for the respective functions + // to learn why we need KVO. + setupKVO() } deinit { tabGroupWindowsObservation?.invalidate() + tabBarVisibleObservation?.invalidate() } override func becomeMain() { - // On macOS Tahoe, the tab bar redraws and restores non-transparency when - // switching tabs. To overcome this, we resync the appearance whenever this - // window becomes main (focused). - if #available(macOS 26.0, *), - let lastSurfaceConfig { - syncAppearance(lastSurfaceConfig) + guard let lastSurfaceConfig else { return } + syncAppearance(lastSurfaceConfig) + + // This is a nasty edge case. If we're going from 2 to 1 tab and the tab bar + // automatically disappears, then we need to resync our appearance because + // at some point macOS replaces the tab views. + if tabGroup?.windows.count ?? 0 == 2 { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in + self?.syncAppearance(self?.lastSurfaceConfig ?? lastSurfaceConfig) + } } } @@ -34,8 +43,14 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { super.syncAppearance(surfaceConfig) - + + // Save our config in case we need to reapply lastSurfaceConfig = surfaceConfig + + // Everytime we change appearance, set KVO up again in case any of our + // references changed (e.g. tabGroup is new). + setupKVO() + if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) } else { @@ -67,15 +82,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 13.0, *) private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { guard let titlebarContainer else { return } - - let configBgColor = NSColor(surfaceConfig.backgroundColor) - - // Set our window background color so it shows up - backgroundColor = configBgColor - - // Set the background color of our titlebar to match titlebarContainer.wantsLayer = true - titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor + titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor } // MARK: View Finders @@ -111,7 +119,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } // MARK: Tab Group Observation - + + private func setupKVO() { + // See the docs for the respective setup functions for why. + setupTabGroupObservation() + setupTabBarVisibleObservation() + } + + /// Monitors the tabGroup windows value for any changes and resyncs the appearance on change. + /// This is necessary because when the windows change, the tab bar and titlebar are recreated + /// which breaks our changes. private func setupTabGroupObservation() { // Remove existing observation if any tabGroupWindowsObservation?.invalidate() @@ -126,7 +143,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabGroupWindowsObservation = tabGroup.observe( \.windows, options: [.new] - ) { [weak self] _, _ in + ) { [weak self] _, change in // NOTE: At one point, I guarded this on only if we went from 0 to N // or N to 0 under the assumption that the tab bar would only get // replaced on those cases. This turned out to be false (Tahoe). @@ -138,4 +155,22 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { self.syncAppearance(lastSurfaceConfig) } } + + /// Monitors the tab bar for visibility. This lets the "Show/Hide Tab Bar" manual menu item + /// to not break our appearance. + private func setupTabBarVisibleObservation() { + // Remove existing observation if any + tabBarVisibleObservation?.invalidate() + tabBarVisibleObservation = nil + + // Set up KVO observation for isTabBarVisible + tabBarVisibleObservation = tabGroup?.observe( + \.isTabBarVisible, + options: [.new] + ) { [weak self] _, change in + guard let self else { return } + guard let lastSurfaceConfig else { return } + self.syncAppearance(lastSurfaceConfig) + } + } } From a804dab28845675271afa13a80e23fc524386c78 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 14:35:49 -0700 Subject: [PATCH 13/37] macos: native terminal style works with new subclasses --- macos/Ghostty.xcodeproj/project.pbxproj | 6 +- .../Terminal/BaseTerminalController.swift | 6 +- .../Terminal/TerminalController.swift | 54 ++++++++---- .../TerminalTransparentTitlebar.xib | 2 +- .../Window Styles/TerminalWindow.swift | 88 +++++++++++++++++++ .../TransparentTitlebarTerminalWindow.swift | 2 + .../Helpers/Extensions/NSView+Extension.swift | 15 ++++ 7 files changed, 151 insertions(+), 22 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index c00d6119a..5f5b3013c 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -402,12 +402,12 @@ isa = PBXGroup; children = ( A59630992AEE1C6400D64628 /* Terminal.xib */, - A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, - A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, - A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, + A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, + A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, + A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, ); path = "Window Styles"; diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e91199358..849f13b34 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -568,7 +568,11 @@ class BaseTerminalController: NSWindowController, // Not zoomed or different node zoomed, zoom this node surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode) } - + + // Move focus to our window. Importantly this ensures that if we click the + // reset zoom button in a tab bar of an unfocused tab that we become focused. + window?.makeKeyAndOrderFront(nil) + // Ensure focus stays on the target surface. We lose focus when we do // this so we need to grab it again. DispatchQueue.main.async { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 5adef8ded..082a3c806 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -14,6 +14,7 @@ class TerminalController: BaseTerminalController { guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { + case "native": "Terminal" case "tabs": defaultValue case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" @@ -132,6 +133,9 @@ class TerminalController: BaseTerminalController { if let window = window as? LegacyTerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } + if let window = window as? TerminalWindow { + window.surfaceIsZoomed2 = to.zoomed != nil + } // If our surface tree is now nil then we close our window. if (to.isEmpty) { @@ -395,28 +399,44 @@ class TerminalController: BaseTerminalController { /// changes, when a window is closed, and when tabs are reordered /// with the mouse. func relabelTabs() { - // Reset this to false. It'll be set back to true later. - tabListenForFrame = false - - guard let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] else { return } - // We only listen for frame changes if we have more than 1 window, // otherwise the accessory view doesn't matter. - tabListenForFrame = windows.count > 1 + tabListenForFrame = window?.tabbedWindows?.count ?? 0 > 1 - for (tab, window) in zip(1..., windows) { - // We need to clear any windows beyond this because they have had - // a keyEquivalent set previously. - guard tab <= 9 else { - window.keyEquivalent = "" - continue + if let windows = window?.tabbedWindows as? [TerminalWindow] { + for (tab, window) in zip(1..., windows) { + // We need to clear any windows beyond this because they have had + // a keyEquivalent set previously. + guard tab <= 9 else { + window.keyEquivalent2 = "" + continue + } + + let action = "goto_tab:\(tab)" + if let equiv = ghostty.config.keyboardShortcut(for: action) { + window.keyEquivalent2 = "\(equiv)" + } else { + window.keyEquivalent2 = "" + } } + } - let action = "goto_tab:\(tab)" - if let equiv = ghostty.config.keyboardShortcut(for: action) { - window.keyEquivalent = "\(equiv)" - } else { - window.keyEquivalent = "" + // Legacy + if let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] { + for (tab, window) in zip(1..., windows) { + // We need to clear any windows beyond this because they have had + // a keyEquivalent set previously. + guard tab <= 9 else { + window.keyEquivalent = "" + continue + } + + let action = "goto_tab:\(tab)" + if let equiv = ghostty.config.keyboardShortcut(for: action) { + window.keyEquivalent = "\(equiv)" + } else { + window.keyEquivalent = "" + } } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib index ada6959b3..25922e2f3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index daf5b4554..9fac08c4b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -1,4 +1,5 @@ import AppKit +import SwiftUI import GhosttyKit /// The base class for all standalone, "normal" terminal windows. This sets the basic @@ -42,6 +43,14 @@ class TerminalWindow: NSWindow { hideWindowButtons() } + // Setup the accessory view for tabs that shows our keyboard shortcuts, + // zoomed state, etc. Note I tried to use SwiftUI here but ran into issues + // where buttons were not clickable. + let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) + stackView.setHuggingPriority(.defaultHigh, for: .horizontal) + stackView.spacing = 3 + tab.accessoryView = stackView + // Get our saved level level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } @@ -51,6 +60,85 @@ class TerminalWindow: NSWindow { override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } + override func becomeKey() { + super.becomeKey() + resetZoomTabButton.contentTintColor = .controlAccentColor + } + + override func resignKey() { + super.resignKey() + resetZoomTabButton.contentTintColor = .secondaryLabelColor + } + + override func mergeAllWindows(_ sender: Any?) { + super.mergeAllWindows(sender) + + // It takes an event loop cycle to merge all the windows so we set a + // short timer to relabel the tabs (issue #1902) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.terminalController?.relabelTabs() + } + } + + // MARK: Tab Key Equivalents + + // TODO: rename once Legacy window removes + var keyEquivalent2: String? = nil { + didSet { + // When our key equivalent is set, we must update the tab label. + guard let keyEquivalent2 else { + keyEquivalentLabel.attributedStringValue = NSAttributedString() + return + } + + keyEquivalentLabel.attributedStringValue = NSAttributedString( + string: "\(keyEquivalent2) ", + attributes: [ + .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), + .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ]) + } + } + + /// The label that has the key equivalent for tab views. + private lazy var keyEquivalentLabel: NSTextField = { + let label = NSTextField(labelWithAttributedString: NSAttributedString()) + label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) + label.postsFrameChangedNotifications = true + return label + }() + + // MARK: Surface Zoom + + /// Set to true if a surface is currently zoomed to show the reset zoom button. + var surfaceIsZoomed2: Bool = false { + didSet { + // Show/hide our reset zoom button depending on if we're zoomed. + // We want to show it if we are zoomed. + resetZoomTabButton.isHidden = !surfaceIsZoomed2 + } + } + + private lazy var resetZoomTabButton: NSButton = generateResetZoomButton() + + private func generateResetZoomButton() -> NSButton { + let button = NSButton() + button.isHidden = true + button.target = terminalController + button.action = #selector(TerminalController.splitZoom(_:)) + button.isBordered = false + button.allowsExpansionToolTips = true + button.toolTip = "Reset Zoom" + button.contentTintColor = .controlAccentColor + button.state = .on + button.image = NSImage(named:"ResetZoom") + button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) + button.translatesAutoresizingMaskIntoConstraints = false + button.widthAnchor.constraint(equalToConstant: 20).isActive = true + button.heightAnchor.constraint(equalToConstant: 20).isActive = true + return button + } + // MARK: Positioning And Styling /// This is called by the controller when there is a need to reset the window apperance. diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 98dd9f834..ada84ff12 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -26,6 +26,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } override func becomeMain() { + super.becomeMain() + guard let lastSurfaceConfig else { return } syncAppearance(lastSurfaceConfig) diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 0cf71138d..aa56fe32e 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -27,6 +27,21 @@ extension NSView { return root } + /// Checks if a view contains another view in its hierarchy. + func contains(_ view: NSView) -> Bool { + if self == view { + return true + } + + for subview in subviews { + if subview.contains(view) { + return true + } + } + + return false + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { From 63e56d0402a04696fbaff2eb7e5dbbf8f6257740 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 15:08:12 -0700 Subject: [PATCH 14/37] macos: titlebar fonts work with new terminal window --- .../Terminal/TerminalController.swift | 11 ++++ .../Window Styles/LegacyTerminalWindow.swift | 42 ------------- .../Window Styles/TerminalWindow.swift | 62 ++++++++++++++++++- .../TransparentTitlebarTerminalWindow.swift | 26 -------- 4 files changed, 72 insertions(+), 69 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 082a3c806..cf771d556 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -471,6 +471,17 @@ class TerminalController: BaseTerminalController { private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // Let our window handle its own appearance if let window = window as? TerminalWindow { + // Sync our zoom state for splits + window.surfaceIsZoomed2 = surfaceTree.zoomed != nil + + // Set the font for the window and tab titles. + if let titleFontName = surfaceConfig.windowTitleFontFamily { + window.titlebarFont2 = NSFont(name: titleFontName, size: NSFont.systemFontSize) + } else { + window.titlebarFont2 = nil + } + + // Call this last in case it uses any of the properties above. window.syncAppearance(surfaceConfig) } diff --git a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift index 208e86343..e63681463 100644 --- a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift @@ -77,48 +77,6 @@ class LegacyTerminalWindow: TerminalWindow { bindings.forEach() { $0.invalidate() } } - // MARK: Titlebar Helpers - // These helpers are generic to what we're trying to achieve (i.e. titlebar - // style tabs, titlebar styling, etc.). They're just here to make it easier. - - private var titlebarContainer: NSView? { - // If we aren't fullscreen then the titlebar container is part of our window. - if !styleMask.contains(.fullScreen) { - guard let view = contentView?.superview ?? contentView else { return nil } - return titlebarContainerView(in: view) - } - - // If we are fullscreen, the titlebar container view is part of a separate - // "fullscreen window", we need to find the window and then get the view. - for window in NSApplication.shared.windows { - // This is the private window class that contains the toolbar - guard window.className == "NSToolbarFullScreenWindow" else { continue } - - // The parent will match our window. This is used to filter the correct - // fullscreen window if we have multiple. - guard window.parent == self else { continue } - - guard let view = window.contentView else { continue } - return titlebarContainerView(in: view) - } - - return nil - } - - private func titlebarContainerView(in view: NSView) -> NSView? { - if view.className == "NSTitlebarContainerView" { - return view - } - - for subview in view.subviews { - if let found = titlebarContainerView(in: subview) { - return found - } - } - - return nil - } - // MARK: - NSWindow override var title: String { diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 9fac08c4b..1e9ca1b01 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -72,7 +72,7 @@ class TerminalWindow: NSWindow { override func mergeAllWindows(_ sender: Any?) { super.mergeAllWindows(sender) - + // It takes an event loop cycle to merge all the windows so we set a // short timer to relabel the tabs (issue #1902) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in @@ -139,6 +139,66 @@ class TerminalWindow: NSWindow { return button } + // MARK: Title Text + + override var title: String { + didSet { + // Whenever we change the window title we must also update our + // tab title if we're using custom fonts. + tab.attributedTitle = attributedTitle + } + } + + // Used to set the titlebar font. + var titlebarFont2: NSFont? { + didSet { + let font = titlebarFont2 ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) + + titlebarTextField?.font = font + tab.attributedTitle = attributedTitle + } + } + + // Find the NSTextField responsible for displaying the titlebar's title. + private var titlebarTextField: NSTextField? { + titlebarContainer? + .firstDescendant(withClassName: "NSTitlebarView")? + .firstDescendant(withClassName: "NSTextField") as? NSTextField + } + + // Return a styled representation of our title property. + private var attributedTitle: NSAttributedString? { + guard let titlebarFont = titlebarFont2 else { return nil } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: titlebarFont, + .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ] + return NSAttributedString(string: title, attributes: attributes) + } + + var titlebarContainer: NSView? { + // If we aren't fullscreen then the titlebar container is part of our window. + if !styleMask.contains(.fullScreen) { + return contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") + } + + // If we are fullscreen, the titlebar container view is part of a separate + // "fullscreen window", we need to find the window and then get the view. + for window in NSApplication.shared.windows { + // This is the private window class that contains the toolbar + guard window.className == "NSToolbarFullScreenWindow" else { continue } + + // The parent will match our window. This is used to filter the correct + // fullscreen window if we have multiple. + guard window.parent == self else { continue } + + return window.contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") + } + + return nil + } + // MARK: Positioning And Styling /// This is called by the controller when there is a need to reset the window apperance. diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index ada84ff12..f949b6094 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -94,32 +94,6 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView") } - private var titlebarContainer: NSView? { - // If we aren't fullscreen then the titlebar container is part of our window. - if !styleMask.contains(.fullScreen) { - return titlebarContainerView - } - - // If we are fullscreen, the titlebar container view is part of a separate - // "fullscreen window", we need to find the window and then get the view. - for window in NSApplication.shared.windows { - // This is the private window class that contains the toolbar - guard window.className == "NSToolbarFullScreenWindow" else { continue } - - // The parent will match our window. This is used to filter the correct - // fullscreen window if we have multiple. - guard window.parent == self else { continue } - - return titlebarContainerView - } - - return nil - } - - private var titlebarContainerView: NSView? { - contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") - } - // MARK: Tab Group Observation private func setupKVO() { From e5cb33e9114a7b0818043c0f210bc2039b10cc00 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 15:09:42 -0700 Subject: [PATCH 15/37] typos --- .../Features/Terminal/Window Styles/TerminalWindow.swift | 2 +- macos/Sources/Helpers/Extensions/NSView+Extension.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 1e9ca1b01..eb4b4a6da 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -201,7 +201,7 @@ class TerminalWindow: NSWindow { // MARK: Positioning And Styling - /// This is called by the controller when there is a need to reset the window apperance. + /// This is called by the controller when there is a need to reset the window appearance. func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // If our window is not visible, then we do nothing. Some things such as blurring // have no effect if the window is not visible. Ultimately, we'll have this called diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index aa56fe32e..b958130f1 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -103,7 +103,7 @@ extension NSView { extension NSView { /// Prints the view hierarchy from the root in a tree-like ASCII format. /// - /// I need this because the "Capture View Hiearchy" was broken under some scenarios in + /// I need this because the "Capture View Hierarchy" was broken under some scenarios in /// Xcode 26 (FB17912569). But, I kept it around because it might be useful to print out /// the view hierarchy without halting the program. func printViewHierarchy() { From ccfd33022f2402b294aafdc9ce8224b15069a5f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 15:15:06 -0700 Subject: [PATCH 16/37] macos: only titlebar tabs uses legacy styling now --- macos/Sources/Features/Terminal/TerminalController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cf771d556..86b47b9bd 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -7,15 +7,13 @@ import GhosttyKit /// A classic, tabbed terminal experience. class TerminalController: BaseTerminalController { override var windowNibName: NSNib.Name? { - //NOTE(mitchellh): switch to this when we've transitioned all legacy logic out - //let defaultValue = "Terminal" - let defaultValue = "TerminalLegacy" + let defaultValue = "Terminal" guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { case "native": "Terminal" - case "tabs": defaultValue + case "tabs": "TerminalLegacy" case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" default: defaultValue From fd785f98bb5d794645079918df2828c4e0e09e1f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 11:36:38 -0700 Subject: [PATCH 17/37] macos: titlebar tabs uses legacy window for now --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++ .../Terminal/TerminalController.swift | 56 +++-------- .../Window Styles/LegacyTerminalWindow.swift | 93 ++----------------- .../TabsTitlebarTerminalWindow.swift | 58 ++++++++++++ .../Window Styles/TerminalHiddenTitlebar.xib | 2 +- .../Window Styles/TerminalTabsTitlebar.xib | 31 +++++++ .../Window Styles/TerminalWindow.swift | 12 +-- 7 files changed, 124 insertions(+), 136 deletions(-) create mode 100644 macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 5f5b3013c..3f1cddf44 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; + A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */; }; + A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */; }; A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; @@ -137,6 +139,8 @@ 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; + A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTitlebarTerminalWindow.swift; sourceTree = ""; }; + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebar.xib; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; @@ -404,10 +408,12 @@ A59630992AEE1C6400D64628 /* Terminal.xib */, A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */, A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, + A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */, A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, ); path = "Window Styles"; @@ -694,6 +700,7 @@ A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, + A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */, A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -761,6 +768,7 @@ A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, + A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 86b47b9bd..d59f71619 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -13,6 +13,7 @@ class TerminalController: BaseTerminalController { let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { case "native": "Terminal" + //case "tabs": "TerminalTabsTitlebar" case "tabs": "TerminalLegacy" case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" @@ -128,11 +129,8 @@ class TerminalController: BaseTerminalController { invalidateRestorableState() // Update our zoom state - if let window = window as? LegacyTerminalWindow { - window.surfaceIsZoomed = to.zoomed != nil - } if let window = window as? TerminalWindow { - window.surfaceIsZoomed2 = to.zoomed != nil + window.surfaceIsZoomed = to.zoomed != nil } // If our surface tree is now nil then we close our window. @@ -418,25 +416,6 @@ class TerminalController: BaseTerminalController { } } } - - // Legacy - if let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] { - for (tab, window) in zip(1..., windows) { - // We need to clear any windows beyond this because they have had - // a keyEquivalent set previously. - guard tab <= 9 else { - window.keyEquivalent = "" - continue - } - - let action = "goto_tab:\(tab)" - if let equiv = ghostty.config.keyboardShortcut(for: action) { - window.keyEquivalent = "\(equiv)" - } else { - window.keyEquivalent = "" - } - } - } } private func fixTabBar() { @@ -470,13 +449,13 @@ class TerminalController: BaseTerminalController { // Let our window handle its own appearance if let window = window as? TerminalWindow { // Sync our zoom state for splits - window.surfaceIsZoomed2 = surfaceTree.zoomed != nil + window.surfaceIsZoomed = surfaceTree.zoomed != nil // Set the font for the window and tab titles. if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont2 = NSFont(name: titleFontName, size: NSFont.systemFontSize) + window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) } else { - window.titlebarFont2 = nil + window.titlebarFont = nil } // Call this last in case it uses any of the properties above. @@ -488,16 +467,6 @@ class TerminalController: BaseTerminalController { if let window = window as? LegacyTerminalWindow { // Update our window light/darkness based on our updated background color window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor - - // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree.zoomed != nil - - // Set the font for the window and tab titles. - if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) - } else { - window.titlebarFont = nil - } } // If our window is not visible, then we do nothing. Some things such as blurring @@ -916,18 +885,15 @@ class TerminalController: BaseTerminalController { } // TODO: remove - if let window = window as? LegacyTerminalWindow { + if let window = window as? LegacyTerminalWindow, + config.macosTitlebarStyle == "tabs" { // Handle titlebar tabs config option. Something about what we do while setting up the // titlebar tabs interferes with the window restore process unless window.tabbingMode // is set to .preferred, so we set it, and switch back to automatic as soon as we can. - if (config.macosTitlebarStyle == "tabs") { - window.tabbingMode = .preferred - window.titlebarTabs = true - DispatchQueue.main.async { - window.tabbingMode = .automatic - } - } else if (config.macosTitlebarStyle == "transparent") { - window.transparentTabs = true + window.tabbingMode = .preferred + window.titlebarTabs = true + DispatchQueue.main.async { + window.tabbingMode = .automatic } if window.hasStyledTabs { diff --git a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift index e63681463..89afbf72f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift @@ -3,12 +3,16 @@ import Cocoa /// The terminal window that we originally had in Ghostty for a long time. Kind of a soupy mess /// of styling. class LegacyTerminalWindow: TerminalWindow { - @objc dynamic var keyEquivalent: String = "" - /// This is used to determine if certain elements should be drawn light or dark and should /// be updated whenever the window background color or surrounding elements changes. var isLightTheme: Bool = false + override var surfaceIsZoomed: Bool { + didSet { + updateResetZoomTitlebarButtonVisibility() + } + } + lazy var titlebarColor: NSColor = backgroundColor { didSet { guard let titlebarContainer else { return } @@ -17,33 +21,6 @@ class LegacyTerminalWindow: TerminalWindow { } } - private lazy var keyEquivalentLabel: NSTextField = { - let label = NSTextField(labelWithAttributedString: NSAttributedString()) - label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) - label.postsFrameChangedNotifications = true - - return label - }() - - private lazy var bindings = [ - observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in - guard let tabGroup = self?.tabGroup else { return } - - self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed - self?.updateResetZoomTitlebarButtonVisibility() - }, - - observe(\.keyEquivalent, options: [.initial, .new]) { [weak self] window, _ in - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), - .foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - let attributedString = NSAttributedString(string: " \(window.keyEquivalent) ", attributes: attributes) - - self?.keyEquivalentLabel.attributedStringValue = attributedString - }, - ] - // false if all three traffic lights are missing/hidden, otherwise true private var hasWindowButtons: Bool { get { @@ -60,31 +37,13 @@ class LegacyTerminalWindow: TerminalWindow { override func awakeFromNib() { super.awakeFromNib() - _ = bindings - - // Create the tab accessory view that houses the key-equivalent label and optional un-zoom button - let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) - stackView.setHuggingPriority(.defaultHigh, for: .horizontal) - stackView.spacing = 3 - tab.accessoryView = stackView - if titlebarTabs { generateToolbar() } } - deinit { - bindings.forEach() { $0.invalidate() } - } - // MARK: - NSWindow - override var title: String { - didSet { - tab.attributedTitle = attributedTitle - } - } - // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to // remove the effect view if the default tab bar is being used since the effect created in @@ -101,7 +60,6 @@ class LegacyTerminalWindow: TerminalWindow { super.becomeKey() updateNewTabButtonOpacity() - resetZoomTabButton.contentTintColor = .controlAccentColor resetZoomToolbarButton.contentTintColor = .controlAccentColor tab.attributedTitle = attributedTitle } @@ -110,7 +68,6 @@ class LegacyTerminalWindow: TerminalWindow { super.resignKey() updateNewTabButtonOpacity() - resetZoomTabButton.contentTintColor = .secondaryLabelColor resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor tab.attributedTitle = attributedTitle } @@ -284,16 +241,8 @@ class LegacyTerminalWindow: TerminalWindow { // MARK: - Split Zoom Button - @objc dynamic var surfaceIsZoomed: Bool = false - private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() - private lazy var resetZoomTabButton: NSButton = { - let button = generateResetZoomButton() - button.action = #selector(selectTabAndZoom(_:)) - return button - }() - private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { guard let titlebarContainer else { return nil } let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) @@ -356,37 +305,13 @@ class LegacyTerminalWindow: TerminalWindow { // MARK: - Titlebar Font // Used to set the titlebar font. - var titlebarFont: NSFont? { + override var titlebarFont: NSFont? { didSet { - let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) - - titlebarTextField?.font = font - tab.attributedTitle = attributedTitle - - if let toolbar = toolbar as? TerminalToolbar { - toolbar.titleFont = font - } + guard let toolbar = toolbar as? TerminalToolbar else { return } + toolbar.titleFont = titlebarFont ?? .titleBarFont(ofSize: NSFont.systemFontSize) } } - // Find the NSTextField responsible for displaying the titlebar's title. - private var titlebarTextField: NSTextField? { - guard let titlebarView = titlebarContainer?.subviews - .first(where: { $0.className == "NSTitlebarView" }) else { return nil } - return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField - } - - // Return a styled representation of our title property. - private var attributedTitle: NSAttributedString? { - guard let titlebarFont else { return nil } - - let attributes: [NSAttributedString.Key: Any] = [ - .font: titlebarFont, - .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - return NSAttributedString(string: title, attributes: attributes) - } - // MARK: - Titlebar Tabs private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil diff --git a/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift new file mode 100644 index 000000000..858b54829 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift @@ -0,0 +1,58 @@ +import AppKit +import SwiftUI + +class TabsTitlebarTerminalWindow: TerminalWindow, NSToolbarDelegate { + override func awakeFromNib() { + super.awakeFromNib() + + // We must hide the title since we're going to be moving tabs into + // the titlebar which have their own title. + titleVisibility = .hidden + + // Create a toolbar + let toolbar = NSToolbar(identifier: "TerminalToolbar") + toolbar.delegate = self + toolbar.centeredItemIdentifiers.insert(.title) + self.toolbar = toolbar + //toolbarStyle = .unifiedCompact + } + + // MARK: NSToolbarDelegate + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.title, .flexibleSpace, .space] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.flexibleSpace, .title, .flexibleSpace] + } + + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + switch itemIdentifier { + case .title: + let item = NSToolbarItem(itemIdentifier: .title) + item.view = NSHostingView(rootView: TitleItem()) + item.visibilityPriority = .user + item.isEnabled = true + return item + default: + return NSToolbarItem(itemIdentifier: itemIdentifier) + } + } + +} + +extension NSToolbarItem.Identifier { + /// Displays the title of the window + static let title = NSToolbarItem.Identifier("Title") +} + +extension TabsTitlebarTerminalWindow { + struct TitleItem: View { + var body: some View { + Text("HELLO THIS IS A PRETTY LONG TITLE") + } + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib index eb4675657..1a2a6c192 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib new file mode 100644 index 000000000..779b6e094 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index eb4b4a6da..a0e18d283 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -111,11 +111,11 @@ class TerminalWindow: NSWindow { // MARK: Surface Zoom /// Set to true if a surface is currently zoomed to show the reset zoom button. - var surfaceIsZoomed2: Bool = false { + var surfaceIsZoomed: Bool = false { didSet { // Show/hide our reset zoom button depending on if we're zoomed. // We want to show it if we are zoomed. - resetZoomTabButton.isHidden = !surfaceIsZoomed2 + resetZoomTabButton.isHidden = !surfaceIsZoomed } } @@ -150,9 +150,9 @@ class TerminalWindow: NSWindow { } // Used to set the titlebar font. - var titlebarFont2: NSFont? { + var titlebarFont: NSFont? { didSet { - let font = titlebarFont2 ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) + let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) titlebarTextField?.font = font tab.attributedTitle = attributedTitle @@ -167,8 +167,8 @@ class TerminalWindow: NSWindow { } // Return a styled representation of our title property. - private var attributedTitle: NSAttributedString? { - guard let titlebarFont = titlebarFont2 else { return nil } + var attributedTitle: NSAttributedString? { + guard let titlebarFont = titlebarFont else { return nil } let attributes: [NSAttributedString.Key: Any] = [ .font: titlebarFont, From 5877913ab8104d9887efb6dbacee8c2bc7b39200 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 12:02:31 -0700 Subject: [PATCH 18/37] macoS: Split out terminal tabs for ventura vs tahoe --- macos/Ghostty.xcodeproj/project.pbxproj | 32 ++--- .../Terminal/TerminalController.swift | 114 +++--------------- .../HiddenTitlebarTerminalWindow.swift | 29 +++-- .../Window Styles/TerminalHiddenTitlebar.xib | 2 +- ...gacy.xib => TerminalTabsTitlebarTahoe.xib} | 2 +- ...ar.xib => TerminalTabsTitlebarVentura.xib} | 4 +- .../Window Styles/TerminalWindow.swift | 2 +- ... => TitlebarTabsTahoeTerminalWindow.swift} | 5 +- ...> TitlebarTabsVenturaTerminalWindow.swift} | 72 +++++++++-- macos/Sources/Helpers/Fullscreen.swift | 3 +- 10 files changed, 120 insertions(+), 145 deletions(-) rename macos/Sources/Features/Terminal/Window Styles/{TerminalLegacy.xib => TerminalTabsTitlebarTahoe.xib} (94%) rename macos/Sources/Features/Terminal/Window Styles/{TerminalTabsTitlebar.xib => TerminalTabsTitlebarVentura.xib} (91%) rename macos/Sources/Features/Terminal/Window Styles/{TabsTitlebarTerminalWindow.swift => TitlebarTabsTahoeTerminalWindow.swift} (90%) rename macos/Sources/Features/Terminal/Window Styles/{LegacyTerminalWindow.swift => TitlebarTabsVenturaTerminalWindow.swift} (91%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 3f1cddf44..cd0c17f9b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -16,9 +16,9 @@ A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; - A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */; }; - A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */; }; - A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */; }; + A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */; }; + A51545002DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */; }; + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC212B2FB6B400E92F16 /* AboutView.swift */; }; @@ -57,7 +57,7 @@ A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; }; A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; }; A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; }; - A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */; }; + A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */; }; A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */; }; A5593FE92DF927DF00B47B10 /* TerminalTransparentTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; @@ -139,9 +139,9 @@ 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; - A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTitlebarTerminalWindow.swift; sourceTree = ""; }; - A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebar.xib; sourceTree = ""; }; - A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; + A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; }; + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; }; + A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsVenturaTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; A51BFC212B2FB6B400E92F16 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; @@ -174,7 +174,7 @@ A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; }; A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = ""; }; - A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalLegacy.xib; sourceTree = ""; }; + A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarVentura.xib; sourceTree = ""; }; A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentTitlebarTerminalWindow.swift; sourceTree = ""; }; A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTransparentTitlebar.xib; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -407,13 +407,13 @@ children = ( A59630992AEE1C6400D64628 /* Terminal.xib */, A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, - A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, - A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */, + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */, + A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */, A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, - A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, - A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */, + A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */, + A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */, A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, ); path = "Window Styles"; @@ -682,7 +682,7 @@ buildActionMask = 2147483647; files = ( FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */, - A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */, + A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib in Resources */, 29C15B1D2CDC3B2900520DD4 /* bat in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */, @@ -700,7 +700,7 @@ A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, - A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */, + A51545002DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib in Resources */, A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -768,7 +768,7 @@ A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, - A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */, + A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, @@ -777,7 +777,7 @@ A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, - A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d59f71619..977a064e0 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -13,10 +13,15 @@ class TerminalController: BaseTerminalController { let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { case "native": "Terminal" - //case "tabs": "TerminalTabsTitlebar" - case "tabs": "TerminalLegacy" case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" + case "tabs": + if #available(macOS 26.0, *) { + // TODO: Switch to Tahoe when ready + "TerminalTabsTitlebarVentura" + } else { + "TerminalTabsTitlebarVentura" + } default: defaultValue } @@ -447,68 +452,20 @@ class TerminalController: BaseTerminalController { private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // Let our window handle its own appearance - if let window = window as? TerminalWindow { - // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree.zoomed != nil + guard let window = window as? TerminalWindow else { return } - // Set the font for the window and tab titles. - if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) - } else { - window.titlebarFont = nil - } + // Sync our zoom state for splits + window.surfaceIsZoomed = surfaceTree.zoomed != nil - // Call this last in case it uses any of the properties above. - window.syncAppearance(surfaceConfig) - } - - guard let window else { return } - - if let window = window as? LegacyTerminalWindow { - // Update our window light/darkness based on our updated background color - window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor - } - - // If our window is not visible, then we do nothing. Some things such as blurring - // have no effect if the window is not visible. Ultimately, we'll have this called - // at some point when a surface becomes focused. - guard window.isVisible else { return } - - guard let window = window as? LegacyTerminalWindow, window.hasStyledTabs else { return } - - // Our background color depends on if our focused surface borders the top or not. - // If it does, we match the focused surface. If it doesn't, we use the app - // configuration. - let backgroundColor: OSColor - if !surfaceTree.isEmpty { - if let focusedSurface = focusedSurface, - let treeRoot = surfaceTree.root, - let focusedNode = treeRoot.node(view: focusedSurface), - treeRoot.spatial().doesBorder(side: .up, from: focusedNode) { - // Similar to above, an alpha component of "0" causes compositor issues, so - // we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308 - backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001) - } else { - // We don't have a focused surface or our surface doesn't border the - // top. We choose to match the color of the top-left most surface. - let topLeftSurface = surfaceTree.root?.leftmostLeaf() - backgroundColor = OSColor(topLeftSurface?.backgroundColor ?? derivedConfig.backgroundColor) - } + // Set the font for the window and tab titles. + if let titleFontName = surfaceConfig.windowTitleFontFamily { + window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) } else { - backgroundColor = OSColor(self.derivedConfig.backgroundColor) + window.titlebarFont = nil } - window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity) - if (window.isOpaque) { - // Bg color is only synced if we have no transparency. This is because - // the transparency is handled at the surface level (window.backgroundColor - // ignores alpha components) - window.backgroundColor = backgroundColor - - // If there is transparency, calling this will make the titlebar opaque - // so we only call this if we are opaque. - window.updateTabBar() - } + // Call this last in case it uses any of the properties above. + window.syncAppearance(surfaceConfig) } /// Returns the default size of the window. This is contextual based on the focused surface because @@ -884,28 +841,6 @@ class TerminalController: BaseTerminalController { } } - // TODO: remove - if let window = window as? LegacyTerminalWindow, - config.macosTitlebarStyle == "tabs" { - // Handle titlebar tabs config option. Something about what we do while setting up the - // titlebar tabs interferes with the window restore process unless window.tabbingMode - // is set to .preferred, so we set it, and switch back to automatic as soon as we can. - window.tabbingMode = .preferred - window.titlebarTabs = true - DispatchQueue.main.async { - window.tabbingMode = .automatic - } - - if window.hasStyledTabs { - // Set the background color of the window - let backgroundColor = NSColor(config.backgroundColor) - window.backgroundColor = backgroundColor - - // This makes sure our titlebar renders correctly when there is a transparent background - window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) - } - } - // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, @@ -1110,23 +1045,6 @@ class TerminalController: BaseTerminalController { } //MARK: - TerminalViewDelegate - - override func titleDidChange(to: String) { - super.titleDidChange(to: to) - - guard let window = window as? LegacyTerminalWindow else { return } - - // Custom toolbar-based title used when titlebar tabs are enabled. - if let toolbar = window.toolbar as? TerminalToolbar { - if (window.titlebarTabs || derivedConfig.macosTitlebarStyle == "hidden") { - // Updating the title text as above automatically reveals the - // native title view in macOS 15.0 and above. Since we're using - // a custom view instead, we need to re-hide it. - window.titleVisibility = .hidden - } - toolbar.titleText = to - } - } override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index f2d3b9b85..5f4d6b177 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -19,15 +19,6 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { NotificationCenter.default.removeObserver(self) } - // We override this so that with the hidden titlebar style the titlebar - // area is not draggable. - override var contentLayoutRect: CGRect { - var rect = super.contentLayoutRect - rect.origin.y = 0 - rect.size.height = self.frame.height - return rect - } - /// Apply the hidden titlebar style. private func reapplyHiddenStyle() { styleMask = [ @@ -64,6 +55,26 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { } } + // MARK: NSWindow + + override var title: String { + didSet { + // Updating the title text as above automatically reveals the + // native title view in macOS 15.0 and above. Since we're using + // a custom view instead, we need to re-hide it. + reapplyHiddenStyle() + } + } + + // We override this so that with the hidden titlebar style the titlebar + // area is not draggable. + override var contentLayoutRect: CGRect { + var rect = super.contentLayoutRect + rect.origin.y = 0 + rect.size.height = self.frame.height + return rect + } + // MARK: Notifications @objc private func fullscreenDidExit(_ notification: Notification) { diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib index 1a2a6c192..eb4675657 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib similarity index 94% rename from macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib rename to macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib index 61ed6f782..deaeded9f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib @@ -13,7 +13,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarVentura.xib similarity index 91% rename from macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib rename to macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarVentura.xib index 779b6e094..bf53a4510 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarVentura.xib @@ -13,11 +13,11 @@ - + - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index a0e18d283..f6b53c289 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -10,7 +10,7 @@ class TerminalWindow: NSWindow { static let defaultLevelKey: String = "TerminalDefaultLevel" /// The configuration derived from the Ghostty config so we don't need to rely on references. - private var derivedConfig: DerivedConfig? + private(set) var derivedConfig: DerivedConfig? /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { diff --git a/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift similarity index 90% rename from macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift rename to macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 858b54829..c45e93d79 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -1,7 +1,8 @@ import AppKit import SwiftUI -class TabsTitlebarTerminalWindow: TerminalWindow, NSToolbarDelegate { +/// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later. +class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { override func awakeFromNib() { super.awakeFromNib() @@ -49,7 +50,7 @@ extension NSToolbarItem.Identifier { static let title = NSToolbarItem.Identifier("Title") } -extension TabsTitlebarTerminalWindow { +extension TitlebarTabsTahoeTerminalWindow { struct TitleItem: View { var body: some View { Text("HELLO THIS IS A PRETTY LONG TITLE") diff --git a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift similarity index 91% rename from macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift rename to macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 89afbf72f..2f8eb5840 100644 --- a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -1,11 +1,10 @@ import Cocoa -/// The terminal window that we originally had in Ghostty for a long time. Kind of a soupy mess -/// of styling. -class LegacyTerminalWindow: TerminalWindow { +/// Titlebar tabs for macOS 13 to 15. +class TitlebarTabsVenturaTerminalWindow: TerminalWindow { /// This is used to determine if certain elements should be drawn light or dark and should /// be updated whenever the window background color or surrounding elements changes. - var isLightTheme: Bool = false + fileprivate var isLightTheme: Bool = false override var surfaceIsZoomed: Bool { didSet { @@ -32,17 +31,30 @@ class LegacyTerminalWindow: TerminalWindow { } } - // MARK: - Lifecycle + // MARK: NSWindow override func awakeFromNib() { super.awakeFromNib() - if titlebarTabs { - generateToolbar() - } - } + // Handle titlebar tabs config option. Something about what we do while setting up the + // titlebar tabs interferes with the window restore process unless window.tabbingMode + // is set to .preferred, so we set it, and switch back to automatic as soon as we can. + tabbingMode = .preferred + DispatchQueue.main.async { + self.tabbingMode = .automatic + } - // MARK: - NSWindow + titlebarTabs = true + + // This should always be true since our super sets this up. + if let derivedConfig { + // Set the background color of the window + backgroundColor = derivedConfig.backgroundColor + + // This makes sure our titlebar renders correctly when there is a transparent background + titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) + } + } // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to @@ -145,7 +157,29 @@ class LegacyTerminalWindow: TerminalWindow { } } - // MARK: - Tab Bar Styling + // MARK: Appearance + + override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + super.syncAppearance(surfaceConfig) + + // Update our window light/darkness based on our updated background color + isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor + + // Update our titlebar color + if let preferredBackgroundColor { + titlebarColor = preferredBackgroundColor + } else if let derivedConfig { + titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) + } + + if (isOpaque) { + // If there is transparency, calling this will make the titlebar opaque + // so we only call this if we are opaque. + updateTabBar() + } + } + + // MARK: Tab Bar Styling // This is true if we should apply styles to the titlebar or tab bar. var hasStyledTabs: Bool { @@ -333,6 +367,18 @@ class LegacyTerminalWindow: TerminalWindow { } } + override var title: String { + didSet { + // Updating the title text as above automatically reveals the + // native title view in macOS 15.0 and above. Since we're using + // a custom view instead, we need to re-hide it. + titleVisibility = .hidden + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleText = title + } + } + } + // We have to regenerate a toolbar when the titlebar tabs setting changes since our // custom toolbar conditionally generates the items based on this setting. I tried to // invalidate the toolbar items and force a refresh, but as far as I can tell that @@ -559,7 +605,7 @@ fileprivate class WindowDragView: NSView { fileprivate class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. - private weak var terminalWindow: LegacyTerminalWindow? + private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? private let isLightTheme: Bool private let overlayLayer = VibrantLayer() @@ -587,7 +633,7 @@ fileprivate class WindowButtonsBackdropView: NSView { fatalError("init(coder:) has not been implemented") } - init(window: LegacyTerminalWindow) { + init(window: TitlebarTabsVenturaTerminalWindow) { self.terminalWindow = window self.isLightTheme = window.isLightTheme diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 3200608d0..d1dac49a3 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -271,8 +271,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. - if let window = window as? LegacyTerminalWindow, - window.titlebarTabs { + if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { window.titlebarTabs = true } From 70029bf82ae13cc5697b7961db08fa2178740327 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 13:39:17 -0700 Subject: [PATCH 19/37] macos: tahoe terminal tabs shows title --- .../Terminal/TerminalController.swift | 8 ++-- .../Window Styles/TerminalWindow.swift | 7 ++-- .../TitlebarTabsTahoeTerminalWindow.swift | 42 +++++++++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 977a064e0..848617a53 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -18,7 +18,7 @@ class TerminalController: BaseTerminalController { case "tabs": if #available(macOS 26.0, *) { // TODO: Switch to Tahoe when ready - "TerminalTabsTitlebarVentura" + "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } @@ -409,15 +409,15 @@ class TerminalController: BaseTerminalController { // We need to clear any windows beyond this because they have had // a keyEquivalent set previously. guard tab <= 9 else { - window.keyEquivalent2 = "" + window.keyEquivalent = "" continue } let action = "goto_tab:\(tab)" if let equiv = ghostty.config.keyboardShortcut(for: action) { - window.keyEquivalent2 = "\(equiv)" + window.keyEquivalent = "\(equiv)" } else { - window.keyEquivalent2 = "" + window.keyEquivalent = "" } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index f6b53c289..907e0b250 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -82,17 +82,16 @@ class TerminalWindow: NSWindow { // MARK: Tab Key Equivalents - // TODO: rename once Legacy window removes - var keyEquivalent2: String? = nil { + var keyEquivalent: String? = nil { didSet { // When our key equivalent is set, we must update the tab label. - guard let keyEquivalent2 else { + guard let keyEquivalent else { keyEquivalentLabel.attributedStringValue = NSAttributedString() return } keyEquivalentLabel.attributedStringValue = NSAttributedString( - string: "\(keyEquivalent2) ", + string: "\(keyEquivalent) ", attributes: [ .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index c45e93d79..a139b1b62 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -3,6 +3,9 @@ import SwiftUI /// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later. class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { + /// The view model for SwiftUI views + private var viewModel = ViewModel() + override func awakeFromNib() { super.awakeFromNib() @@ -15,7 +18,23 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { toolbar.delegate = self toolbar.centeredItemIdentifiers.insert(.title) self.toolbar = toolbar - //toolbarStyle = .unifiedCompact + toolbarStyle = .unifiedCompact + } + + // MARK: NSWindow + + override var title: String { + didSet { + viewModel.title = title + } + } + + override func update() { + super.update() + + if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { + glass.isHidden = true + } } // MARK: NSToolbarDelegate @@ -34,7 +53,7 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { switch itemIdentifier { case .title: let item = NSToolbarItem(itemIdentifier: .title) - item.view = NSHostingView(rootView: TitleItem()) + item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel)) item.visibilityPriority = .user item.isEnabled = true return item @@ -43,6 +62,11 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { } } + // MARK: SwiftUI + + class ViewModel: ObservableObject { + @Published var title: String = "👻 Ghostty" + } } extension NSToolbarItem.Identifier { @@ -51,9 +75,21 @@ extension NSToolbarItem.Identifier { } extension TitlebarTabsTahoeTerminalWindow { + /// Displays the window title struct TitleItem: View { + @ObservedObject var viewModel: ViewModel + + var title: String { + // An empty title makes this view zero-sized and NSToolbar on macOS + // tahoe just deletes the item when that happens. So we use a space + // instead to ensure there's always some size. + viewModel.title.isEmpty ? " " : viewModel.title + } + var body: some View { - Text("HELLO THIS IS A PRETTY LONG TITLE") + Text(title) + .lineLimit(1) + .truncationMode(.tail) } } } From 658ec2eb6f13a7896720a3e95db87d2b808309d6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 14:33:18 -0700 Subject: [PATCH 20/37] macos: add reset zoom to all window titles --- .../Terminal/BaseTerminalController.swift | 2 + .../Terminal/TerminalController.swift | 4 +- .../Window Styles/TerminalWindow.swift | 62 +++++++++++++++++++ macos/Sources/Helpers/Fullscreen.swift | 12 ++-- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 849f13b34..bc91b920e 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -758,6 +758,8 @@ class BaseTerminalController: NSWindowController, } } + func fullscreenDidChange() {} + // MARK: Clipboard Confirmation @objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 848617a53..cff230249 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -145,7 +145,9 @@ class TerminalController: BaseTerminalController { } - func fullscreenDidChange() { + override func fullscreenDidChange() { + super.fullscreenDidChange() + // When our fullscreen state changes, we resync our appearance because some // properties change when fullscreen or not. guard let focusedSurface else { return } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 907e0b250..4221d9ba4 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -9,6 +9,9 @@ class TerminalWindow: NSWindow { /// used by the manual float on top menu item feature. static let defaultLevelKey: String = "TerminalDefaultLevel" + /// The view model for SwiftUI views + private var viewModel = ViewModel() + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig? @@ -19,6 +22,15 @@ class TerminalWindow: NSWindow { // MARK: NSWindow Overrides + override var toolbar: NSToolbar? { + didSet { + DispatchQueue.main.async { + // When we have a toolbar, our SwiftUI view needs to know for layout + self.viewModel.hasToolbar = self.toolbar != nil + } + } + } + override func awakeFromNib() { guard let appDelegate = NSApp.delegate as? AppDelegate else { return } @@ -43,6 +55,18 @@ class TerminalWindow: NSWindow { hideWindowButtons() } + // Create our reset zoom titlebar accessory. + let resetZoomAccessory = NSTitlebarAccessoryViewController() + resetZoomAccessory.layoutAttribute = .right + resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView( + viewModel: viewModel, + action: { [weak self] in + guard let self else { return } + self.terminalController?.splitZoom(self) + })) + addTitlebarAccessoryViewController(resetZoomAccessory) + resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false + // Setup the accessory view for tabs that shows our keyboard shortcuts, // zoomed state, etc. Note I tried to use SwiftUI here but ran into issues // where buttons were not clickable. @@ -115,6 +139,10 @@ class TerminalWindow: NSWindow { // Show/hide our reset zoom button depending on if we're zoomed. // We want to show it if we are zoomed. resetZoomTabButton.isHidden = !surfaceIsZoomed + + DispatchQueue.main.async { + self.viewModel.isSurfaceZoomed = self.surfaceIsZoomed + } } } @@ -313,3 +341,37 @@ class TerminalWindow: NSWindow { } } } + +// MARK: SwiftUI View + +extension TerminalWindow { + class ViewModel: ObservableObject { + @Published var isSurfaceZoomed: Bool = false + @Published var hasToolbar: Bool = false + } + + struct ResetZoomAccessoryView: View { + @ObservedObject var viewModel: ViewModel + let action: () -> Void + + var body: some View { + if viewModel.isSurfaceZoomed { + VStack { + Button(action: action) { + Image("ResetZoom") + .foregroundColor(.accentColor) + } + .buttonStyle(.plain) + .help("Reset Split Zoom") + .frame(width: 20, height: 20) + Spacer() + } + // With a toolbar, the window title is taller, so we need more padding + // to properly align. + .padding(.top, viewModel.hasToolbar ? 10 : 5) + // We always need space at the end of the titlebar + .padding(.trailing, 10) + } + } + } +} diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index d1dac49a3..49cab0756 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -45,10 +45,6 @@ protocol FullscreenDelegate: AnyObject { func fullscreenDidChange() } -extension FullscreenDelegate { - func fullscreenDidChange() {} -} - /// The base class for fullscreen implementations, cannot be used as a FullscreenStyle on its own. class FullscreenBase { let window: NSWindow @@ -269,6 +265,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { window.styleMask = savedState.styleMask window.setFrame(window.frameRect(forContentRect: savedState.contentFrame), display: true) + // Removing the "titled" style also derefs all our accessory view controllers + // so we need to restore those. + for c in savedState.titlebarAccessoryViewControllers { + window.addTitlebarAccessoryViewController(c) + } + // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { @@ -383,6 +385,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { let tabGroupIndex: Int? let contentFrame: NSRect let styleMask: NSWindow.StyleMask + let titlebarAccessoryViewControllers: [NSTitlebarAccessoryViewController] let dock: Bool let menu: Bool @@ -394,6 +397,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.tabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) self.contentFrame = window.convertToScreen(contentView.frame) self.styleMask = window.styleMask + self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers self.dock = window.screen?.hasDock ?? false if let cgWindowId = window.cgWindowId { From de40e7ce028b57dc993fafc3c18be863d52437c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 14:36:33 -0700 Subject: [PATCH 21/37] macos: non-native fullscreen should restore toolbars --- macos/Sources/Helpers/Fullscreen.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 49cab0756..a2294a0af 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -271,6 +271,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { window.addTitlebarAccessoryViewController(c) } + // Removing "titled" also clears our toolbar + window.toolbar = savedState.toolbar + window.toolbarStyle = savedState.toolbarStyle + // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { @@ -385,6 +389,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { let tabGroupIndex: Int? let contentFrame: NSRect let styleMask: NSWindow.StyleMask + let toolbar: NSToolbar? + let toolbarStyle: NSWindow.ToolbarStyle let titlebarAccessoryViewControllers: [NSTitlebarAccessoryViewController] let dock: Bool let menu: Bool @@ -397,6 +403,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.tabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) self.contentFrame = window.convertToScreen(contentView.frame) self.styleMask = window.styleMask + self.toolbar = window.toolbar + self.toolbarStyle = window.toolbarStyle self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers self.dock = window.screen?.hasDock ?? false From 5c8f1948cecf1c4dc63fb53fae0ef6203dd0d691 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 14:42:08 -0700 Subject: [PATCH 22/37] macos: remove the duplicated reset zoom accessory view from legacy --- .../Terminal/TerminalController.swift | 3 +- .../TitlebarTabsVenturaTerminalWindow.swift | 36 ------------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cff230249..9fbf154bc 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -18,7 +18,8 @@ class TerminalController: BaseTerminalController { case "tabs": if #available(macOS 26.0, *) { // TODO: Switch to Tahoe when ready - "TerminalTabsTitlebarTahoe" + "TerminalTabsTitlebarVentura" + //"TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 2f8eb5840..bc6a66a87 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -6,12 +6,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { /// be updated whenever the window background color or surrounding elements changes. fileprivate var isLightTheme: Bool = false - override var surfaceIsZoomed: Bool { - didSet { - updateResetZoomTitlebarButtonVisibility() - } - } - lazy var titlebarColor: NSColor = backgroundColor { didSet { guard let titlebarContainer else { return } @@ -108,8 +102,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - updateResetZoomTitlebarButtonVisibility() - // The remainder of this function only applies to styled tabs. guard hasStyledTabs else { return } @@ -277,33 +269,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() - private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { - guard let titlebarContainer else { return nil } - let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) - let view = NSView(frame: NSRect(origin: .zero, size: size)) - - let button = generateResetZoomButton() - button.frame.origin.x = size.width/2 - button.bounds.width/2 - button.frame.origin.y = size.height/2 - button.bounds.height/2 - view.addSubview(button) - - let titlebarAccessoryViewController = NSTitlebarAccessoryViewController() - titlebarAccessoryViewController.view = view - titlebarAccessoryViewController.layoutAttribute = .right - - return titlebarAccessoryViewController - }() - - private func updateResetZoomTitlebarButtonVisibility() { - guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } - - if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) { - addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController) - } - - resetZoomTitlebarAccessoryViewController.view.isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed - } - private func generateResetZoomButton() -> NSButton { let button = NSButton() button.target = nil @@ -394,7 +359,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { resetZoomItem.view!.widthAnchor.constraint(equalToConstant: 22).isActive = true resetZoomItem.view!.heightAnchor.constraint(equalToConstant: 20).isActive = true } - updateResetZoomTitlebarButtonVisibility() } // For titlebar tabs, we want to hide the separator view so that we get rid From 6ae8bd737ae63bb15ba796512319632054248e21 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 15:11:35 -0700 Subject: [PATCH 23/37] macos: hide the reset zoom titlebar accessory when tab bar is shown --- .../Window Styles/TerminalWindow.swift | 23 ++++++++++++++++++- .../Helpers/Extensions/Array+Extension.swift | 4 ++++ .../Helpers/Extensions/NSView+Extension.swift | 17 +++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 4221d9ba4..fe7293d5b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -104,6 +104,26 @@ class TerminalWindow: NSWindow { } } + override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { + super.addTitlebarAccessoryViewController(childViewController) + + // Tab bar is attached as a titlebar accessory view controller (layout bottom). We + // can detect when it is shown or hidden by overriding add/remove and searching for + // it. This has been verified to work on macOS 12 to 26 + if childViewController.view.contains(className: "NSTabBar") { + viewModel.hasTabBar = true + } + } + + override func removeTitlebarAccessoryViewController(at index: Int) { + if let childViewController = titlebarAccessoryViewControllers[safe: index], + childViewController.view.contains(className: "NSTabBar") { + viewModel.hasTabBar = false + } + + super.removeTitlebarAccessoryViewController(at: index) + } + // MARK: Tab Key Equivalents var keyEquivalent: String? = nil { @@ -348,6 +368,7 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false + @Published var hasTabBar: Bool = false } struct ResetZoomAccessoryView: View { @@ -355,7 +376,7 @@ extension TerminalWindow { let action: () -> Void var body: some View { - if viewModel.isSurfaceZoomed { + if viewModel.isSurfaceZoomed && !viewModel.hasTabBar { VStack { Button(action: action) { Image("ResetZoom") diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index 6f005a349..12f2de43d 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -1,4 +1,8 @@ extension Array { + subscript(safe index: Int) -> Element? { + return indices.contains(index) ? self[index] : nil + } + /// Returns the index before i, with wraparound. Assumes i is a valid index. func indexWrapping(before i: Int) -> Int { if i == 0 { diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index b958130f1..0da84abda 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -42,6 +42,21 @@ extension NSView { return false } + /// Checks if the view contains the given class in its hierarchy. + func contains(className name: String) -> Bool { + if String(describing: type(of: self)) == name { + return true + } + + for subview in subviews { + if subview.contains(className: name) { + return true + } + } + + return false + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { @@ -113,7 +128,7 @@ extension NSView { } /// Returns a string representation of the view hierarchy in a tree-like format. - private func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { + func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { var result = "" // Add the tree branch characters From 5f9967024744827314535026221565763fd4791d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 16:37:26 -0700 Subject: [PATCH 24/37] macos: tahoe titlebar tabs taking shape --- .../Terminal/TerminalController.swift | 4 +- .../Window Styles/TerminalWindow.swift | 36 +++++- .../TitlebarTabsTahoeTerminalWindow.swift | 106 ++++++++++++++++-- .../Helpers/Extensions/NSView+Extension.swift | 10 ++ 4 files changed, 142 insertions(+), 14 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 9fbf154bc..0b0b264d3 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -18,8 +18,8 @@ class TerminalController: BaseTerminalController { case "tabs": if #available(macOS 26.0, *) { // TODO: Switch to Tahoe when ready - "TerminalTabsTitlebarVentura" - //"TerminalTabsTitlebarTahoe" + //"TerminalTabsTitlebarVentura" + "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index fe7293d5b..032886802 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -110,20 +110,50 @@ class TerminalWindow: NSWindow { // Tab bar is attached as a titlebar accessory view controller (layout bottom). We // can detect when it is shown or hidden by overriding add/remove and searching for // it. This has been verified to work on macOS 12 to 26 - if childViewController.view.contains(className: "NSTabBar") { + if isTabBar(childViewController) { + childViewController.identifier = Self.tabBarIdentifier viewModel.hasTabBar = true } } override func removeTitlebarAccessoryViewController(at index: Int) { - if let childViewController = titlebarAccessoryViewControllers[safe: index], - childViewController.view.contains(className: "NSTabBar") { + if let childViewController = titlebarAccessoryViewControllers[safe: index], isTabBar(childViewController) { viewModel.hasTabBar = false } super.removeTitlebarAccessoryViewController(at: index) } + // MARK: Tab Bar + + /// This identifier is attached to the tab bar view controller when we detect it being + /// added. + private static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") + + func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { + if childViewController.identifier == nil { + // The good case + if childViewController.view.contains(className: "NSTabBar") { + return true + } + + // When a new window is attached to an existing tab group, AppKit adds + // an empty NSView as an accessory view and adds the tab bar later. If + // we're at the bottom and are a single NSView we assume its a tab bar. + if childViewController.layoutAttribute == .bottom && + childViewController.view.className == "NSView" && + childViewController.view.subviews.isEmpty { + return true + } + + return false + } + + // View controllers should be tagged with this as soon as possible to + // increase our accuracy. We do this manually. + return childViewController.identifier == Self.tabBarIdentifier + } + // MARK: Tab Key Equivalents var keyEquivalent: String? = nil { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index a139b1b62..42bcabee7 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -20,7 +20,6 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { self.toolbar = toolbar toolbarStyle = .unifiedCompact } - // MARK: NSWindow override var title: String { @@ -29,11 +28,92 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { } } - override func update() { - super.update() + override var toolbar: NSToolbar? { + didSet{ + guard toolbar != nil else { return } - if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { - glass.isHidden = true + // When a toolbar is added, remove the Liquid Glass look because we're + // abusing the toolbar as a tab bar. + if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { + glass.isHidden = true + } + } + } + + override func becomeMain() { + super.becomeMain() + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { + self.contentView?.printViewHierarchy() + } + } + + // This is called by macOS for native tabbing in order to add the tab bar. We hook into + // this, detect the tab bar being added, and override its behavior. + override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { + // If this is the tab bar then we need to set it up for the titlebar + guard isTabBar(childViewController) else { + super.addTitlebarAccessoryViewController(childViewController) + return + } + + // Some setup needs to happen BEFORE it is added, such as layout. If + // we don't do this before the call below, we'll trigger an AppKit + // assertion. + childViewController.layoutAttribute = .right + + super.addTitlebarAccessoryViewController(childViewController) + + // View model updates must happen on their own ticks + DispatchQueue.main.async { + self.viewModel.hasTabBar = true + } + + // Setup the tab bar to go into the titlebar. + DispatchQueue.main.async { + // HACK: wait a tick before doing anything, to avoid edge cases during startup... :/ + // If we don't do this then on launch windows with restored state with tabs will end + // up with messed up tab bars that don't show all tabs. + let accessoryView = childViewController.view + guard let clipView = accessoryView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } + guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } + guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } + + // The container is the view that we'll constrain our tab bar within. + let container = toolbarView + + // Constrain the accessory clip view (the parent of the accessory view + // usually that clips the children) to the container view. + clipView.translatesAutoresizingMaskIntoConstraints = false + clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 78).isActive = true + clipView.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true + clipView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + clipView.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true + clipView.needsLayout = true + + // Constrain the actual accessory view (the tab bar) to the clip view + // so it takes up the full space. + accessoryView.translatesAutoresizingMaskIntoConstraints = false + accessoryView.leftAnchor.constraint(equalTo: clipView.leftAnchor).isActive = true + accessoryView.rightAnchor.constraint(equalTo: clipView.rightAnchor).isActive = true + accessoryView.topAnchor.constraint(equalTo: clipView.topAnchor).isActive = true + accessoryView.heightAnchor.constraint(equalTo: clipView.heightAnchor).isActive = true + accessoryView.needsLayout = true + } + } + + override func removeTitlebarAccessoryViewController(at index: Int) { + guard let childViewController = titlebarAccessoryViewControllers[safe: index], + isTabBar(childViewController) else { + super.removeTitlebarAccessoryViewController(at: index) + return + } + + super.removeTitlebarAccessoryViewController(at: index) + + // View model needs to be updated on another tick because it + // triggers view updates. + DispatchQueue.main.async { + self.viewModel.hasTabBar = false } } @@ -66,6 +146,7 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { class ViewModel: ObservableObject { @Published var title: String = "👻 Ghostty" + @Published var hasTabBar: Bool = false } } @@ -83,13 +164,20 @@ extension TitlebarTabsTahoeTerminalWindow { // An empty title makes this view zero-sized and NSToolbar on macOS // tahoe just deletes the item when that happens. So we use a space // instead to ensure there's always some size. - viewModel.title.isEmpty ? " " : viewModel.title + return viewModel.title.isEmpty ? " " : viewModel.title } var body: some View { - Text(title) - .lineLimit(1) - .truncationMode(.tail) + if !viewModel.hasTabBar { + Text(title) + .lineLimit(1) + .truncationMode(.tail) + } else { + // 1x1.gif strikes again! For real: if we render a zero-sized + // view here then the toolbar just disappears our view. I don't + // know. + Color.clear.frame(width: 1, height: 1) + } } } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 0da84abda..b3628d406 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -57,6 +57,16 @@ extension NSView { return false } + /// Finds the superview with the given class name. + func firstSuperview(withClassName name: String) -> NSView? { + guard let superview else { return nil } + if String(describing: type(of: superview)) == name { + return superview + } + + return superview.firstSuperview(withClassName: name) + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { From d84c30ce71546efc36e671beac1febf5b2a92875 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 18:10:25 -0700 Subject: [PATCH 25/37] macos: titlebar tabs should be transparent --- .../Window Styles/TitlebarTabsTahoeTerminalWindow.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 42bcabee7..ca88322ed 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -2,7 +2,10 @@ import AppKit import SwiftUI /// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later. -class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { +/// +/// This inherits from transparent styling so that the titlebar matches the background color +/// of the window. +class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate { /// The view model for SwiftUI views private var viewModel = ViewModel() From 9d9c451b0a893c35c29ba453722cfb337d73817a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 20:03:19 -0700 Subject: [PATCH 26/37] macos: titlebar tabs handle hidden traffic buttons --- .../Terminal/Window Styles/TerminalWindow.swift | 9 ++++++--- .../TitlebarTabsTahoeTerminalWindow.swift | 11 +++++++++-- .../TitlebarTabsVenturaTerminalWindow.swift | 13 +++++-------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 032886802..d9b98695e 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -13,7 +13,7 @@ class TerminalWindow: NSWindow { private var viewModel = ViewModel() /// The configuration derived from the Ghostty config so we don't need to rely on references. - private(set) var derivedConfig: DerivedConfig? + private(set) var derivedConfig: DerivedConfig = .init() /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { @@ -341,8 +341,8 @@ class TerminalWindow: NSWindow { } } - let alpha = derivedConfig?.backgroundOpacity.clamped(to: 0.001...1) ?? 1 - return derivedConfig?.backgroundColor.withAlphaComponent(alpha) + let alpha = derivedConfig.backgroundOpacity.clamped(to: 0.001...1) + return derivedConfig.backgroundColor.withAlphaComponent(alpha) } private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { @@ -379,15 +379,18 @@ class TerminalWindow: NSWindow { struct DerivedConfig { let backgroundColor: NSColor let backgroundOpacity: Double + let macosWindowButtons: Ghostty.MacOSWindowButtons init() { self.backgroundColor = NSColor.windowBackgroundColor self.backgroundOpacity = 1 + self.macosWindowButtons = .visible } init(_ config: Ghostty.Config) { self.backgroundColor = NSColor(config.backgroundColor) self.backgroundOpacity = config.backgroundOpacity + self.macosWindowButtons = config.macosWindowButtons } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index ca88322ed..3dc505088 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -84,12 +84,19 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // The container is the view that we'll constrain our tab bar within. let container = toolbarView + // The padding for the tab bar. If we're showing window buttons then + // we need to offset the window buttons. + let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + case .hidden: 0 + case .visible: 70 + } + // Constrain the accessory clip view (the parent of the accessory view // usually that clips the children) to the container view. clipView.translatesAutoresizingMaskIntoConstraints = false - clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 78).isActive = true + clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: leftPadding).isActive = true clipView.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true - clipView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + clipView.topAnchor.constraint(equalTo: container.topAnchor, constant: 2).isActive = true clipView.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true clipView.needsLayout = true diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index bc6a66a87..6e19d144d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -40,14 +40,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarTabs = true - // This should always be true since our super sets this up. - if let derivedConfig { - // Set the background color of the window - backgroundColor = derivedConfig.backgroundColor + // Set the background color of the window + backgroundColor = derivedConfig.backgroundColor - // This makes sure our titlebar renders correctly when there is a transparent background - titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) - } + // This makes sure our titlebar renders correctly when there is a transparent background + titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } // We only need to set this once, but need to do it after the window has been created in order @@ -160,7 +157,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // Update our titlebar color if let preferredBackgroundColor { titlebarColor = preferredBackgroundColor - } else if let derivedConfig { + } else { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } From 17ad77b5b0b7b7ead22bcfda11c9250d064d408d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 21:33:40 -0700 Subject: [PATCH 27/37] macos: fix background color of terminal window to match surface --- .../Window Styles/TerminalWindow.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index d9b98695e..a1bb1d86d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -322,22 +322,23 @@ class TerminalWindow: NSWindow { /// change the alpha channel again manually. var preferredBackgroundColor: NSColor? { if let terminalController, !terminalController.surfaceTree.isEmpty { + let surface: Ghostty.SurfaceView? + // If our focused surface borders the top then we prefer its background color if let focusedSurface = terminalController.focusedSurface, let treeRoot = terminalController.surfaceTree.root, let focusedNode = treeRoot.node(view: focusedSurface), - treeRoot.spatial().doesBorder(side: .up, from: focusedNode), - let backgroundcolor = focusedSurface.backgroundColor { - let alpha = focusedSurface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) - return NSColor(backgroundcolor).withAlphaComponent(alpha) + treeRoot.spatial().doesBorder(side: .up, from: focusedNode) { + surface = focusedSurface + } else { + // If it doesn't border the top, we use the top-left leaf + surface = terminalController.surfaceTree.root?.leftmostLeaf() } - // Doesn't border the top or we don't have a focused surface, so - // we try to match the top-left surface. - let topLeftSurface = terminalController.surfaceTree.root?.leftmostLeaf() - if let topLeftBgColor = topLeftSurface?.backgroundColor { - let alpha = topLeftSurface?.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) ?? 1 - return NSColor(topLeftBgColor).withAlphaComponent(alpha) + if let surface { + let backgroundColor = surface.backgroundColor ?? surface.derivedConfig.backgroundColor + let alpha = surface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) + return NSColor(backgroundColor).withAlphaComponent(alpha) } } From 00d41239dad768d903790b86d56bd8a3bb1de82b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 11:11:00 -0700 Subject: [PATCH 28/37] macOS: prep the tab bar when system appearance changes --- .../TitlebarTabsTahoeTerminalWindow.swift | 182 +++++++++++++----- 1 file changed, 129 insertions(+), 53 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 3dc505088..ac4fae12a 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -9,20 +9,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool /// The view model for SwiftUI views private var viewModel = ViewModel() - override func awakeFromNib() { - super.awakeFromNib() - - // We must hide the title since we're going to be moving tabs into - // the titlebar which have their own title. - titleVisibility = .hidden - - // Create a toolbar - let toolbar = NSToolbar(identifier: "TerminalToolbar") - toolbar.delegate = self - toolbar.centeredItemIdentifiers.insert(.title) - self.toolbar = toolbar - toolbarStyle = .unifiedCompact + deinit { + tabBarObserver = nil } + // MARK: NSWindow override var title: String { @@ -43,11 +33,27 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool } } + override func awakeFromNib() { + super.awakeFromNib() + + // We must hide the title since we're going to be moving tabs into + // the titlebar which have their own title. + titleVisibility = .hidden + + // Create a toolbar + let toolbar = NSToolbar(identifier: "TerminalToolbar") + toolbar.delegate = self + toolbar.centeredItemIdentifiers.insert(.title) + self.toolbar = toolbar + toolbarStyle = .unifiedCompact + } + override func becomeMain() { super.becomeMain() - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { - self.contentView?.printViewHierarchy() - } + + // Check if we have a tab bar and set it up if we have to. See the comment + // on this function to learn why we need to check this here. + setupTabBar() } // This is called by macOS for native tabbing in order to add the tab bar. We hook into @@ -66,48 +72,12 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.addTitlebarAccessoryViewController(childViewController) - // View model updates must happen on their own ticks - DispatchQueue.main.async { - self.viewModel.hasTabBar = true - } - // Setup the tab bar to go into the titlebar. DispatchQueue.main.async { // HACK: wait a tick before doing anything, to avoid edge cases during startup... :/ // If we don't do this then on launch windows with restored state with tabs will end // up with messed up tab bars that don't show all tabs. - let accessoryView = childViewController.view - guard let clipView = accessoryView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } - guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } - guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } - - // The container is the view that we'll constrain our tab bar within. - let container = toolbarView - - // The padding for the tab bar. If we're showing window buttons then - // we need to offset the window buttons. - let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { - case .hidden: 0 - case .visible: 70 - } - - // Constrain the accessory clip view (the parent of the accessory view - // usually that clips the children) to the container view. - clipView.translatesAutoresizingMaskIntoConstraints = false - clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: leftPadding).isActive = true - clipView.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true - clipView.topAnchor.constraint(equalTo: container.topAnchor, constant: 2).isActive = true - clipView.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true - clipView.needsLayout = true - - // Constrain the actual accessory view (the tab bar) to the clip view - // so it takes up the full space. - accessoryView.translatesAutoresizingMaskIntoConstraints = false - accessoryView.leftAnchor.constraint(equalTo: clipView.leftAnchor).isActive = true - accessoryView.rightAnchor.constraint(equalTo: clipView.rightAnchor).isActive = true - accessoryView.topAnchor.constraint(equalTo: clipView.topAnchor).isActive = true - accessoryView.heightAnchor.constraint(equalTo: clipView.heightAnchor).isActive = true - accessoryView.needsLayout = true + self.setupTabBar() } } @@ -120,11 +90,117 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.removeTitlebarAccessoryViewController(at: index) + removeTabBar() + } + + // MARK: Tab Bar Setup + + private var tabBarObserver: NSObjectProtocol? { + didSet { + // When we change this we want to clear our old observer + guard let oldValue else { return } + NotificationCenter.default.removeObserver(oldValue) + } + } + + /// Take the NSTabBar that is on the window and convert it into titlebar tabs. + /// + /// Let me explain more background on what is happening here. When a tab bar is created, only the + /// main window actually has an NSTabBar. When an NSWindow in the tab group gains main, AppKit + /// creates/moves (unsure which) the NSTabBar for it and shows it. When it loses main, the tab bar + /// is removed from the view hierarchy. + /// + /// We can't detect this via `addTitlebarAccessoryViewController` because AppKit + /// _always_ creates an accessory view controller for every window in the tab group, but puts a + /// zero-sized NSView into it (that the tab bar is then attached to later). + /// + /// The best way I've found to detect this is to search for and setup the tab bar anytime the + /// window gains focus. There are probably edge cases to check but to resolve all this I made + /// this function which is idempotent to call. + /// + /// There are more scenarios to look out for and they're documented within the method. + func setupTabBar() { + // We only want to setup the observer once + guard tabBarObserver == nil else { return } + + // Find our tab bar. If it doesn't exist we don't do anything. + guard let tabBar = contentView?.rootView.firstDescendant(withClassName: "NSTabBar") else { return } + + // View model updates must happen on their own ticks. + DispatchQueue.main.async { + self.viewModel.hasTabBar = true + } + + // Find our clip view + guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } + guard let accessoryView = clipView.subviews[safe: 0] else { return } + guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } + guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } + + // The container is the view that we'll constrain our tab bar within. + let container = toolbarView + + // The padding for the tab bar. If we're showing window buttons then + // we need to offset the window buttons. + let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + case .hidden: 0 + case .visible: 70 + } + + // Constrain the accessory clip view (the parent of the accessory view + // usually that clips the children) to the container view. + clipView.translatesAutoresizingMaskIntoConstraints = false + accessoryView.translatesAutoresizingMaskIntoConstraints = false + + // Setup all our constraints + NSLayoutConstraint.activate([ + clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: leftPadding), + clipView.rightAnchor.constraint(equalTo: container.rightAnchor), + clipView.topAnchor.constraint(equalTo: container.topAnchor, constant: 2), + clipView.heightAnchor.constraint(equalTo: container.heightAnchor), + accessoryView.leftAnchor.constraint(equalTo: clipView.leftAnchor), + accessoryView.rightAnchor.constraint(equalTo: clipView.rightAnchor), + accessoryView.topAnchor.constraint(equalTo: clipView.topAnchor), + accessoryView.heightAnchor.constraint(equalTo: clipView.heightAnchor), + ]) + + clipView.needsLayout = true + accessoryView.needsLayout = true + + // We need to setup an observer for the NSTabBar frame. When we change system + // appearance, the tab bar temporarily becomes width/height 0 and breaks all our + // constraints and AppKit responds by nuking the whole tab bar cause it doesn't + // know what to do with it. We need to detect this before bad things happen. + tabBar.postsFrameChangedNotifications = true + tabBarObserver = NotificationCenter.default.addObserver( + forName: NSView.frameDidChangeNotification, + object: tabBar, + queue: .main + ) { [weak self] _ in + guard let self else { return } + + // Check if either width or height is zero + guard tabBar.frame.size.width == 0 || tabBar.frame.size.height == 0 else { return } + + // Remove the observer so we can call setup again. + self.tabBarObserver = nil + + // Wait a tick to let the new tab bars appear and then set them up. + DispatchQueue.main.async { + self.setupTabBar() + } + } + } + + func removeTabBar() { // View model needs to be updated on another tick because it // triggers view updates. DispatchQueue.main.async { self.viewModel.hasTabBar = false } + + // Clear our observations + self.tabBarObserver = nil } // MARK: NSToolbarDelegate From b1b74d3421693fa9d7042fd9167603ade3619aa3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 12:25:21 -0700 Subject: [PATCH 29/37] comments --- .../TitlebarTabsTahoeTerminalWindow.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index ac4fae12a..145c37c59 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -25,8 +25,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool didSet{ guard toolbar != nil else { return } - // When a toolbar is added, remove the Liquid Glass look because we're - // abusing the toolbar as a tab bar. + // When a toolbar is added, remove the Liquid Glass look to have a cleaner + // appearance for our custom titlebar tabs. if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { glass.isHidden = true } @@ -110,9 +110,9 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool /// creates/moves (unsure which) the NSTabBar for it and shows it. When it loses main, the tab bar /// is removed from the view hierarchy. /// - /// We can't detect this via `addTitlebarAccessoryViewController` because AppKit - /// _always_ creates an accessory view controller for every window in the tab group, but puts a - /// zero-sized NSView into it (that the tab bar is then attached to later). + /// We can't reliably detect this via `addTitlebarAccessoryViewController` because AppKit + /// creates an accessory view controller for every window in the tab group, but only attaches + /// the actual NSTabBar to the main window's accessory view. /// /// The best way I've found to detect this is to search for and setup the tab bar anytime the /// window gains focus. There are probably edge cases to check but to resolve all this I made @@ -167,10 +167,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool clipView.needsLayout = true accessoryView.needsLayout = true - // We need to setup an observer for the NSTabBar frame. When we change system - // appearance, the tab bar temporarily becomes width/height 0 and breaks all our - // constraints and AppKit responds by nuking the whole tab bar cause it doesn't - // know what to do with it. We need to detect this before bad things happen. + // Setup an observer for the NSTabBar frame. When system appearance changes or + // other events occur, the tab bar can temporarily become zero-sized. When this + // happens, we need to remove our custom constraints and re-apply them once the + // tab bar has proper dimensions again to avoid constraint conflicts. tabBar.postsFrameChangedNotifications = true tabBarObserver = NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, From 59812c3b02ec6fffe0d9febf3b234faf27bd4dbc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 12:27:44 -0700 Subject: [PATCH 30/37] macos: remove TODO --- macos/Sources/Features/Terminal/TerminalController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 0b0b264d3..03a4e548e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -17,8 +17,6 @@ class TerminalController: BaseTerminalController { case "transparent": "TerminalTransparentTitlebar" case "tabs": if #available(macOS 26.0, *) { - // TODO: Switch to Tahoe when ready - //"TerminalTabsTitlebarVentura" "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" From f7f0514b9ff8065e7e0b75cf0e05e75d1ef00642 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 13:14:14 -0700 Subject: [PATCH 31/37] macos: move old toolbar into ventura file --- macos/Ghostty.xcodeproj/project.pbxproj | 4 - .../Features/Terminal/TerminalToolbar.swift | 130 ----------------- .../TitlebarTabsVenturaTerminalWindow.swift | 131 ++++++++++++++++++ 3 files changed, 131 insertions(+), 134 deletions(-) delete mode 100644 macos/Sources/Features/Terminal/TerminalToolbar.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cd0c17f9b..e9c02ef41 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -120,7 +120,6 @@ A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; - AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; }; @@ -239,7 +238,6 @@ A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; - AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalToolbar.swift; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; C1F26EA62B738B9900404083 /* NSView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView+Extension.swift"; sourceTree = ""; }; C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = ""; }; @@ -507,7 +505,6 @@ A596309B2AEE1C9E00D64628 /* TerminalController.swift */, A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, - AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */, ); @@ -799,7 +796,6 @@ A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */, A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */, - AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */, C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift deleted file mode 100644 index 9da14562c..000000000 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Cocoa - -// Custom NSToolbar subclass that displays a centered window title, -// in order to accommodate the titlebar tabs feature. -class TerminalToolbar: NSToolbar, NSToolbarDelegate { - private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") - - var titleText: String { - get { - titleTextField.stringValue - } - - set { - titleTextField.stringValue = newValue - } - } - - var titleFont: NSFont? { - get { - titleTextField.font - } - - set { - titleTextField.font = newValue - } - } - - var titleIsHidden: Bool { - get { - titleTextField.isHidden - } - - set { - titleTextField.isHidden = newValue - } - } - - override init(identifier: NSToolbar.Identifier) { - super.init(identifier: identifier) - - delegate = self - centeredItemIdentifiers.insert(.titleText) - } - - func toolbar(_ toolbar: NSToolbar, - itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, - willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - var item: NSToolbarItem - - switch itemIdentifier { - case .titleText: - item = NSToolbarItem(itemIdentifier: .titleText) - item.view = self.titleTextField - item.visibilityPriority = .user - - // This ensures the title text field doesn't disappear when shrinking the view - self.titleTextField.translatesAutoresizingMaskIntoConstraints = false - self.titleTextField.setContentHuggingPriority(.defaultLow, for: .horizontal) - self.titleTextField.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - // Add constraints to the toolbar item's view - NSLayoutConstraint.activate([ - // Set the height constraint to match the toolbar's height - self.titleTextField.heightAnchor.constraint(equalToConstant: 22), // Adjust as needed - ]) - - item.isEnabled = true - case .resetZoom: - item = NSToolbarItem(itemIdentifier: .resetZoom) - default: - item = NSToolbarItem(itemIdentifier: itemIdentifier) - } - - return item - } - - func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.titleText, .flexibleSpace, .space, .resetZoom] - } - - func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - // These space items are here to ensure that the title remains centered when it starts - // getting smaller than the max size so starts clipping. Lucky for us, two of the - // built-in spacers plus the un-zoom button item seems to exactly match the space - // on the left that's reserved for the window buttons. - return [.flexibleSpace, .titleText, .flexibleSpace] - } -} - -/// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. -fileprivate class CenteredDynamicLabel: NSTextField { - override func viewDidMoveToSuperview() { - // Configure the text field - isEditable = false - isBordered = false - drawsBackground = false - alignment = .center - lineBreakMode = .byTruncatingTail - cell?.truncatesLastVisibleLine = true - - // Use Auto Layout - translatesAutoresizingMaskIntoConstraints = false - - // Set content hugging and compression resistance priorities - setContentHuggingPriority(.defaultLow, for: .horizontal) - setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - } - - // Vertically center the text - override func draw(_ dirtyRect: NSRect) { - guard let attributedString = self.attributedStringValue.mutableCopy() as? NSMutableAttributedString else { - super.draw(dirtyRect) - return - } - - let textSize = attributedString.size() - - let yOffset = (self.bounds.height - textSize.height) / 2 - 1 // -1 to center it better - - let centeredRect = NSRect(x: self.bounds.origin.x, y: self.bounds.origin.y + yOffset, - width: self.bounds.width, height: textSize.height) - - attributedString.draw(in: centeredRect) - } -} - -extension NSToolbarItem.Identifier { - static let resetZoom = NSToolbarItem.Identifier("ResetZoom") - static let titleText = NSToolbarItem.Identifier("TitleText") -} diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 6e19d144d..20280b982 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -609,3 +609,134 @@ fileprivate class WindowButtonsBackdropView: NSView { layer?.addSublayer(overlayLayer) } } + +// MARK: Toolbar + +// Custom NSToolbar subclass that displays a centered window title, +// in order to accommodate the titlebar tabs feature. +fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { + private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") + + var titleText: String { + get { + titleTextField.stringValue + } + + set { + titleTextField.stringValue = newValue + } + } + + var titleFont: NSFont? { + get { + titleTextField.font + } + + set { + titleTextField.font = newValue + } + } + + var titleIsHidden: Bool { + get { + titleTextField.isHidden + } + + set { + titleTextField.isHidden = newValue + } + } + + override init(identifier: NSToolbar.Identifier) { + super.init(identifier: identifier) + + delegate = self + centeredItemIdentifiers.insert(.titleText) + } + + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + var item: NSToolbarItem + + switch itemIdentifier { + case .titleText: + item = NSToolbarItem(itemIdentifier: .titleText) + item.view = self.titleTextField + item.visibilityPriority = .user + + // This ensures the title text field doesn't disappear when shrinking the view + self.titleTextField.translatesAutoresizingMaskIntoConstraints = false + self.titleTextField.setContentHuggingPriority(.defaultLow, for: .horizontal) + self.titleTextField.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + // Add constraints to the toolbar item's view + NSLayoutConstraint.activate([ + // Set the height constraint to match the toolbar's height + self.titleTextField.heightAnchor.constraint(equalToConstant: 22), // Adjust as needed + ]) + + item.isEnabled = true + case .resetZoom: + item = NSToolbarItem(itemIdentifier: .resetZoom) + default: + item = NSToolbarItem(itemIdentifier: itemIdentifier) + } + + return item + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.titleText, .flexibleSpace, .space, .resetZoom] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + // These space items are here to ensure that the title remains centered when it starts + // getting smaller than the max size so starts clipping. Lucky for us, two of the + // built-in spacers plus the un-zoom button item seems to exactly match the space + // on the left that's reserved for the window buttons. + return [.flexibleSpace, .titleText, .flexibleSpace] + } +} + +/// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. +fileprivate class CenteredDynamicLabel: NSTextField { + override func viewDidMoveToSuperview() { + // Configure the text field + isEditable = false + isBordered = false + drawsBackground = false + alignment = .center + lineBreakMode = .byTruncatingTail + cell?.truncatesLastVisibleLine = true + + // Use Auto Layout + translatesAutoresizingMaskIntoConstraints = false + + // Set content hugging and compression resistance priorities + setContentHuggingPriority(.defaultLow, for: .horizontal) + setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + } + + // Vertically center the text + override func draw(_ dirtyRect: NSRect) { + guard let attributedString = self.attributedStringValue.mutableCopy() as? NSMutableAttributedString else { + super.draw(dirtyRect) + return + } + + let textSize = attributedString.size() + + let yOffset = (self.bounds.height - textSize.height) / 2 - 1 // -1 to center it better + + let centeredRect = NSRect(x: self.bounds.origin.x, y: self.bounds.origin.y + yOffset, + width: self.bounds.width, height: textSize.height) + + attributedString.draw(in: centeredRect) + } +} + +extension NSToolbarItem.Identifier { + static let resetZoom = NSToolbarItem.Identifier("ResetZoom") + static let titleText = NSToolbarItem.Identifier("TitleText") +} From a7df90ee5529b2cccac5651c57661dac1f350b99 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 13:36:03 -0700 Subject: [PATCH 32/37] macos: remove split zoom accessory when tabs appear --- .../Window Styles/TerminalWindow.swift | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index a1bb1d86d..d588a5944 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -12,6 +12,9 @@ class TerminalWindow: NSWindow { /// The view model for SwiftUI views private var viewModel = ViewModel() + /// Reset split zoom button in titlebar + private let resetZoomAccessory = NSTitlebarAccessoryViewController() + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() @@ -56,7 +59,6 @@ class TerminalWindow: NSWindow { } // Create our reset zoom titlebar accessory. - let resetZoomAccessory = NSTitlebarAccessoryViewController() resetZoomAccessory.layoutAttribute = .right resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView( viewModel: viewModel, @@ -94,6 +96,18 @@ class TerminalWindow: NSWindow { resetZoomTabButton.contentTintColor = .secondaryLabelColor } + override func becomeMain() { + super.becomeMain() + + // Its possible we miss the accessory titlebar call so we check again + // whenever the window becomes main. Both of these are idempotent. + if hasTabBar { + tabBarDidAppear() + } else { + tabBarDidDisappear() + } + } + override func mergeAllWindows(_ sender: Any?) { super.mergeAllWindows(sender) @@ -112,13 +126,13 @@ class TerminalWindow: NSWindow { // it. This has been verified to work on macOS 12 to 26 if isTabBar(childViewController) { childViewController.identifier = Self.tabBarIdentifier - viewModel.hasTabBar = true + tabBarDidAppear() } } override func removeTitlebarAccessoryViewController(at index: Int) { if let childViewController = titlebarAccessoryViewControllers[safe: index], isTabBar(childViewController) { - viewModel.hasTabBar = false + tabBarDidDisappear() } super.removeTitlebarAccessoryViewController(at: index) @@ -130,6 +144,11 @@ class TerminalWindow: NSWindow { /// added. private static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") + /// Returns true if there is a tab bar visible on this window. + var hasTabBar: Bool { + contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil + } + func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { if childViewController.identifier == nil { // The good case @@ -154,6 +173,28 @@ class TerminalWindow: NSWindow { return childViewController.identifier == Self.tabBarIdentifier } + /// Ensures we only run didAppear/didDisappear once per state. + private var tabBarDidAppearRan = false + + private func tabBarDidAppear() { + guard !tabBarDidAppearRan else { return } + tabBarDidAppearRan = true + + // Remove our reset zoom accessory. For some reason having a SwiftUI + // titlebar accessory causes our content view scaling to be wrong. + // Removing it fixes it, we just need to remember to add it again later. + if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) { + removeTitlebarAccessoryViewController(at: idx) + } + } + + private func tabBarDidDisappear() { + guard tabBarDidAppearRan else { return } + tabBarDidAppearRan = false + + addTitlebarAccessoryViewController(resetZoomAccessory) + } + // MARK: Tab Key Equivalents var keyEquivalent: String? = nil { @@ -402,7 +443,6 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false - @Published var hasTabBar: Bool = false } struct ResetZoomAccessoryView: View { @@ -410,7 +450,7 @@ extension TerminalWindow { let action: () -> Void var body: some View { - if viewModel.isSurfaceZoomed && !viewModel.hasTabBar { + if viewModel.isSurfaceZoomed { VStack { Button(action: action) { Image("ResetZoom") From 8cfc904c0c86a8e87cb6f5234aa6fe55e4bd2d53 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 14:38:07 -0700 Subject: [PATCH 33/37] macos: fix up some sequoia regressions --- .../Window Styles/TerminalWindow.swift | 27 +++++++++++-------- macos/Sources/Helpers/Fullscreen.swift | 12 +++------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index d588a5944..74181089d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -173,13 +173,7 @@ class TerminalWindow: NSWindow { return childViewController.identifier == Self.tabBarIdentifier } - /// Ensures we only run didAppear/didDisappear once per state. - private var tabBarDidAppearRan = false - private func tabBarDidAppear() { - guard !tabBarDidAppearRan else { return } - tabBarDidAppearRan = true - // Remove our reset zoom accessory. For some reason having a SwiftUI // titlebar accessory causes our content view scaling to be wrong. // Removing it fixes it, we just need to remember to add it again later. @@ -189,10 +183,11 @@ class TerminalWindow: NSWindow { } private func tabBarDidDisappear() { - guard tabBarDidAppearRan else { return } - tabBarDidAppearRan = false - - addTitlebarAccessoryViewController(resetZoomAccessory) + if styleMask.contains(.titled) { + if titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) == nil { + addTitlebarAccessoryViewController(resetZoomAccessory) + } + } } // MARK: Tab Key Equivalents @@ -448,6 +443,16 @@ extension TerminalWindow { struct ResetZoomAccessoryView: View { @ObservedObject var viewModel: ViewModel let action: () -> Void + + // The padding from the top that the view appears. This was all just manually + // measured based on the OS. + var topPadding: CGFloat { + if #available(macOS 26.0, *) { + return viewModel.hasToolbar ? 10 : 5 + } else { + return viewModel.hasToolbar ? 9 : 4 + } + } var body: some View { if viewModel.isSurfaceZoomed { @@ -463,7 +468,7 @@ extension TerminalWindow { } // With a toolbar, the window title is taller, so we need more padding // to properly align. - .padding(.top, viewModel.hasToolbar ? 10 : 5) + .padding(.top, topPadding) // We always need space at the end of the titlebar .padding(.trailing, 10) } diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index a2294a0af..d78775a1d 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -268,19 +268,15 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Removing the "titled" style also derefs all our accessory view controllers // so we need to restore those. for c in savedState.titlebarAccessoryViewControllers { - window.addTitlebarAccessoryViewController(c) + if window.titlebarAccessoryViewControllers.firstIndex(of: c) == nil { + window.addTitlebarAccessoryViewController(c) + } } // Removing "titled" also clears our toolbar window.toolbar = savedState.toolbar window.toolbarStyle = savedState.toolbarStyle - - // This is a hack that I want to remove from this but for now, we need to - // fix up the titlebar tabs here before we do everything below. - if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { - window.titlebarTabs = true - } - + // If the window was previously in a tab group that isn't empty now, // we re-add it. We have to do this because our process of doing non-native // fullscreen removes the window from the tab group. From 1388c277d5978094994b3b928b9d79a950974989 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 14:43:01 -0700 Subject: [PATCH 34/37] macos: sequoia should use same tab bar identifier as TerminalWindow --- .../Terminal/Window Styles/TerminalWindow.swift | 2 +- .../TitlebarTabsVenturaTerminalWindow.swift | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 74181089d..f9dfb9591 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -142,7 +142,7 @@ class TerminalWindow: NSWindow { /// This identifier is attached to the tab bar view controller when we detect it being /// added. - private static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") + static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") /// Returns true if there is a tab bar visible on this window. var hasTabBar: Bool { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 20280b982..a236df107 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -196,8 +196,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // We can only update titlebar tabs if there is a titlebar. Without the // styleMask check the app will crash (issue #1876) if titlebarTabs && styleMask.contains(.titled) { - guard let tabBarAccessoryViewController = titlebarAccessoryViewControllers.first(where: { $0.identifier == Self.TabBarController}) else { return } - + guard let tabBarAccessoryViewController = titlebarAccessoryViewControllers.first(where: { $0.identifier == Self.tabBarIdentifier}) else { return } tabBarAccessoryViewController.layoutAttribute = .right pushTabsToTitlebar(tabBarAccessoryViewController) } @@ -314,9 +313,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private var windowDragHandle: WindowDragView? = nil - // The tab bar controller ID from macOS - static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") - // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { didSet { @@ -384,10 +380,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // This is called by macOS for native tabbing in order to add the tab bar. We hook into // this, detect the tab bar being added, and override its behavior. override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { - let isTabBar = self.titlebarTabs && ( - childViewController.layoutAttribute == .bottom || - childViewController.identifier == Self.TabBarController - ) + let isTabBar = self.titlebarTabs && isTabBar(childViewController) if (isTabBar) { // Ensure it has the right layoutAttribute to force it next to our titlebar @@ -399,7 +392,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // Mark the controller for future reference so we can easily find it. Otherwise // the tab bar has no ID by default. - childViewController.identifier = Self.TabBarController + childViewController.identifier = Self.tabBarIdentifier } super.addTitlebarAccessoryViewController(childViewController) @@ -410,7 +403,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } override func removeTitlebarAccessoryViewController(at index: Int) { - let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.TabBarController + let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.tabBarIdentifier super.removeTitlebarAccessoryViewController(at: index) if (isTabBar) { resetCustomTabBarViews() From ac4b0dcac0b5b864a212ddf2c08280b72c5a8016 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 14:57:49 -0700 Subject: [PATCH 35/37] macos: fix transparent tabs on sequoia --- .../TitlebarTabsVenturaTerminalWindow.swift | 16 ------- .../TransparentTitlebarTerminalWindow.swift | 47 +++++++++++++++++-- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index a236df107..99111b55b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -99,9 +99,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - // The remainder of this function only applies to styled tabs. - guard hasStyledTabs else { return } - titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none if titlebarTabs { hideToolbarOverflowButton() @@ -170,19 +167,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // MARK: Tab Bar Styling - // This is true if we should apply styles to the titlebar or tab bar. - var hasStyledTabs: Bool { - // If we have titlebar tabs then we always style. - guard !titlebarTabs else { return true } - - // We style the tabs if they're transparent - return transparentTabs - } - - // Set to true if the background color should bleed through the titlebar/tab bar. - // This only applies to non-titlebar tabs. - var transparentTabs: Bool = false - var hasVeryDarkBackground: Bool { backgroundColor.luminance < 0.05 } diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index f949b6094..94e938326 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -11,6 +11,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { /// KVO observation for tab group window changes. private var tabGroupWindowsObservation: NSKeyValueObservation? private var tabBarVisibleObservation: NSKeyValueObservation? + + deinit { + tabGroupWindowsObservation?.invalidate() + tabBarVisibleObservation?.invalidate() + } + + // MARK: NSWindow override func awakeFromNib() { super.awakeFromNib() @@ -19,11 +26,6 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // to learn why we need KVO. setupKVO() } - - deinit { - tabGroupWindowsObservation?.invalidate() - tabBarVisibleObservation?.invalidate() - } override func becomeMain() { super.becomeMain() @@ -40,6 +42,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } } } + + override func update() { + super.update() + + // On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our + // titlebar to be truly transparent. + if #unavailable(macOS 26.0) { + hideEffectView() + } + } // MARK: Appearance @@ -86,6 +98,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { guard let titlebarContainer else { return } titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor + effectViewIsHidden = false } // MARK: View Finders @@ -149,4 +162,28 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { self.syncAppearance(lastSurfaceConfig) } } + + // MARK: macOS 13 to 15 + + // We only need to set this once, but need to do it after the window has been created in order + // to determine if the theme is using a very dark background, in which case we don't want to + // remove the effect view if the default tab bar is being used since the effect created in + // `updateTabsForVeryDarkBackgrounds` creates a confusing visual design. + private var effectViewIsHidden = false + + private func hideEffectView() { + guard !effectViewIsHidden else { return } + + // By hiding the visual effect view, we allow the window's (or titlebar's in this case) + // background color to show through. If we were to set `titlebarAppearsTransparent` to true + // the selected tab would look fine, but the unselected ones and new tab button backgrounds + // would be an opaque color. When the titlebar isn't transparent, however, the system applies + // a compositing effect to the unselected tab backgrounds, which makes them blend with the + // titlebar's/window's background. + if let effectView = titlebarContainer?.descendants(withClassName: "NSVisualEffectView").first { + effectView.isHidden = true + } + + effectViewIsHidden = true + } } From 1b6142b271b72048f82648920f6de0b2b48960f5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 15:02:05 -0700 Subject: [PATCH 36/37] macos: don't restore tab bar with non-native fs --- macos/Sources/Helpers/Fullscreen.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index d78775a1d..f3940a9aa 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -268,6 +268,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Removing the "titled" style also derefs all our accessory view controllers // so we need to restore those. for c in savedState.titlebarAccessoryViewControllers { + // Restoring the tab bar causes all sorts of problems. Its best to just ignore it, + // even though this is kind of a hack. + if let window = window as? TerminalWindow, window.isTabBar(c) { + continue + } + if window.titlebarAccessoryViewControllers.firstIndex(of: c) == nil { window.addTitlebarAccessoryViewController(c) } From 928603c23e1e59717e1c042e3d39487066d9f12f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 20:20:49 -0700 Subject: [PATCH 37/37] macos: use a runtime liquid glass check for our Tahoe styling --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++-- .../Terminal/TerminalController.swift | 2 +- .../Window Styles/TerminalWindow.swift | 2 +- .../TransparentTitlebarTerminalWindow.swift | 4 +- macos/Sources/Helpers/AppInfo.swift | 44 +++++++++++++++++++ macos/Sources/Helpers/Xcode.swift | 10 ----- 6 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 macos/Sources/Helpers/AppInfo.swift delete mode 100644 macos/Sources/Helpers/Xcode.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index e9c02ef41..4943f2f4d 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -90,7 +90,7 @@ A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; }; A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; }; - A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; }; + 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 */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; @@ -205,7 +205,7 @@ A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; }; A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; }; - A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; }; + A5A6F7292CC41B8700B232A5 /* AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfo.swift; sourceTree = ""; }; A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = ""; }; 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 = ""; }; @@ -312,8 +312,8 @@ children = ( A58636692DF0A98100E04A10 /* Extensions */, A5874D9B2DAD781100E83852 /* Private */, + A5A6F7292CC41B8700B232A5 /* AppInfo.swift */, A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, - A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, @@ -756,7 +756,7 @@ A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, - A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */, + A5A6F72A2CC41B8900B232A5 /* AppInfo.swift in Sources */, A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CA378E2D31D6C300931030 /* Weak.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03a4e548e..49b3fea34 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -16,7 +16,7 @@ class TerminalController: BaseTerminalController { case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" case "tabs": - if #available(macOS 26.0, *) { + if #available(macOS 26.0, *), hasLiquidGlass() { "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index f9dfb9591..e24323113 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -447,7 +447,7 @@ extension TerminalWindow { // The padding from the top that the view appears. This was all just manually // measured based on the OS. var topPadding: CGFloat { - if #available(macOS 26.0, *) { + if #available(macOS 26.0, *), hasLiquidGlass() { return viewModel.hasToolbar ? 10 : 5 } else { return viewModel.hasToolbar ? 9 : 4 diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 94e938326..1a92fa024 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -48,7 +48,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our // titlebar to be truly transparent. - if #unavailable(macOS 26.0) { + if !effectViewIsHidden && !hasLiquidGlass() { hideEffectView() } } @@ -65,7 +65,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // references changed (e.g. tabGroup is new). setupKVO() - if #available(macOS 26.0, *) { + if #available(macOS 26.0, *), hasLiquidGlass() { syncAppearanceTahoe(surfaceConfig) } else { syncAppearanceVentura(surfaceConfig) diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift new file mode 100644 index 000000000..cf66e332d --- /dev/null +++ b/macos/Sources/Helpers/AppInfo.swift @@ -0,0 +1,44 @@ +import Foundation + +/// True if we appear to be running in Xcode. +func isRunningInXcode() -> Bool { + if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { + return true + } + + return false +} + +/// True if we have liquid glass available. +func hasLiquidGlass() -> Bool { + // Can't have liquid glass unless we're in macOS 26+ + if #unavailable(macOS 26.0) { + return false + } + + // If we aren't running SDK 26.0 or later then we definitely + // do not have liquid glass. + guard let sdkName = Bundle.main.infoDictionary?["DTSDKName"] as? String else { + // If we don't have this, we assume we're built against the latest + // since we're on macOS 26+ + return true + } + + // If the SDK doesn't start with macosx then we just assume we + // have it because we already verified we're on macOS above. + guard sdkName.hasPrefix("macosx") else { + return true + } + + // The SDK version must be at least 26 + let versionString = String(sdkName.dropFirst("macosx".count)) + guard let major = if let dotIndex = versionString.firstIndex(of: ".") { + Int(String(versionString[..= 26 +} diff --git a/macos/Sources/Helpers/Xcode.swift b/macos/Sources/Helpers/Xcode.swift deleted file mode 100644 index 281bad18b..000000000 --- a/macos/Sources/Helpers/Xcode.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// True if we appear to be running in Xcode. -func isRunningInXcode() -> Bool { - if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { - return true - } - - return false -}