Merge branch 'ghostty-org:main' into hu_HU_localization

This commit is contained in:
Balázs Szücs
2025-06-14 18:01:01 +02:00
committed by GitHub
46 changed files with 1847 additions and 649 deletions

View File

@ -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.3.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.3.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.3.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.3.2
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true

View File

@ -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

View File

@ -12,10 +12,13 @@
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 */; };
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.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 */; };
@ -51,6 +54,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 /* 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 */; };
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; };
A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; };
@ -81,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 */; };
@ -111,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 */; };
@ -128,8 +136,11 @@
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = "<group>"; };
A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = "<group>"; };
A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsVenturaTerminalWindow.swift; sourceTree = "<group>"; };
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
A51BFC212B2FB6B400E92F16 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@ -159,6 +170,12 @@
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = "<group>"; };
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = "<group>"; };
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = "<group>"; };
A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarVentura.xib; sourceTree = "<group>"; };
A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentTitlebarTerminalWindow.swift; sourceTree = "<group>"; };
A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTransparentTitlebar.xib; sourceTree = "<group>"; };
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
@ -188,7 +205,7 @@
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = "<group>"; };
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = "<group>"; };
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
A5A6F7292CC41B8700B232A5 /* AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfo.swift; sourceTree = "<group>"; };
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@ -221,7 +238,6 @@
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = "<group>"; };
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalToolbar.swift; sourceTree = "<group>"; };
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
C1F26EA62B738B9900404083 /* NSView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView+Extension.swift"; sourceTree = "<group>"; };
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = "<group>"; };
@ -296,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 */,
@ -384,6 +400,23 @@
path = Sources;
sourceTree = "<group>";
};
A5593FDD2DF8D56000B47B10 /* Window Styles */ = {
isa = PBXGroup;
children = (
A59630992AEE1C6400D64628 /* Terminal.xib */,
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */,
A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */,
A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */,
A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */,
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */,
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */,
A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */,
A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */,
A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */,
);
path = "Window Styles";
sourceTree = "<group>";
};
A55B7BB429B6F4410055DE60 /* Ghostty */ = {
isa = PBXGroup;
children = (
@ -437,6 +470,7 @@
isa = PBXGroup;
children = (
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
@ -467,12 +501,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 +679,11 @@
buildActionMask = 2147483647;
files = (
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */,
A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.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,10 +692,12 @@
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 */,
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
A51545002DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib in Resources */,
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -702,13 +738,16 @@
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 */,
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 */,
A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */,
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */,
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
@ -717,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 */,
@ -726,6 +765,7 @@
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
A586366B2DF0A98C00E04A10 /* Array+Extension.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 */,
@ -734,9 +774,10 @@
A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */,
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.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 */,
@ -755,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 */,

View File

@ -556,16 +556,23 @@ 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)
}
// 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 {
@ -751,6 +758,8 @@ class BaseTerminalController: NSWindowController,
}
}
func fullscreenDidChange() {}
// MARK: Clipboard Confirmation
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {

View File

@ -6,7 +6,26 @@ import GhosttyKit
/// A classic, tabbed terminal experience.
class TerminalController: BaseTerminalController {
override var windowNibName: NSNib.Name? { "Terminal" }
override var windowNibName: NSNib.Name? {
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 "hidden": "TerminalHiddenTitlebar"
case "transparent": "TerminalTransparentTitlebar"
case "tabs":
if #available(macOS 26.0, *), hasLiquidGlass() {
"TerminalTabsTitlebarTahoe"
} else {
"TerminalTabsTitlebarVentura"
}
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
@ -125,15 +144,12 @@ 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 }
if (!(fullscreenStyle?.isFullscreen ?? false) &&
ghostty.config.macosTitlebarStyle == "hidden")
{
applyHiddenTitlebarStyle()
}
syncAppearance(focusedSurface.derivedConfig)
}
@ -278,14 +294,8 @@ 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") {
// 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":
@ -303,7 +313,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
@ -379,28 +401,25 @@ 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? [TerminalWindow] 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.keyEquivalent = ""
continue
}
let action = "goto_tab:\(tab)"
if let equiv = ghostty.config.keyboardShortcut(for: action) {
window.keyEquivalent = "\(equiv)"
} else {
window.keyEquivalent = ""
let action = "goto_tab:\(tab)"
if let equiv = ghostty.config.keyboardShortcut(for: action) {
window.keyEquivalent = "\(equiv)"
} else {
window.keyEquivalent = ""
}
}
}
}
@ -433,22 +452,12 @@ class TerminalController: BaseTerminalController {
}
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
guard let window = self.window as? TerminalWindow else { return }
// Set our explicit appearance if we need to based on the configuration.
window.appearance = surfaceConfig.windowAppearance
// Update our window light/darkness based on our updated background color
window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
// Let our window handle its own appearance
guard let window = window as? TerminalWindow else { return }
// Sync our zoom state for splits
window.surfaceIsZoomed = surfaceTree.zoomed != 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)
@ -456,89 +465,8 @@ class TerminalController: BaseTerminalController {
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 }
// 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)
}
} else {
backgroundColor = OSColor(self.derivedConfig.backgroundColor)
}
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()
}
}
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)))
// 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
@ -882,52 +810,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
@ -945,9 +830,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 {
@ -960,44 +842,6 @@ 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
}
} 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
// 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,
@ -1005,11 +849,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.
@ -1046,7 +885,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
@ -1202,23 +1046,6 @@ class TerminalController: BaseTerminalController {
}
//MARK: - TerminalViewDelegate
override func titleDidChange(to: String) {
super.titleDidChange(to: to)
guard let window = window as? TerminalWindow 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)

View File

@ -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")
}

View File

@ -0,0 +1,89 @@
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)
}
/// 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: 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) {
// 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()
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -17,10 +17,10 @@
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="tG2-b7-nb8"/>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="TerminalController" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="cg9-Ep-qHg"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="👻 Ghostty" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="HiddenTitlebarTerminalWindow" customModule="Ghostty" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="tG2-b7-nb8"/>
</connections>
<point key="canvasLocation" x="132" y="-82"/>
</window>
</objects>
</document>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="TerminalController" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="cg9-Ep-qHg"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="👻 Ghostty" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="TitlebarTabsTahoeTerminalWindow" customModule="Ghostty" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="tG2-b7-nb8"/>
</connections>
<point key="canvasLocation" x="132" y="-82"/>
</window>
</objects>
</document>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="TerminalController" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="cg9-Ep-qHg"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="👻 Ghostty" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="TitlebarTabsVenturaTerminalWindow" customModule="Ghostty" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="tG2-b7-nb8"/>
</connections>
<point key="canvasLocation" x="132" y="-82"/>
</window>
</objects>
</document>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="TerminalController" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="window" destination="QvC-M9-y7g" id="cg9-Ep-qHg"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="👻 Ghostty" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="TransparentTitlebarTerminalWindow" customModule="Ghostty" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<connections>
<outlet property="delegate" destination="-2" id="tG2-b7-nb8"/>
</connections>
<point key="canvasLocation" x="132" y="-82"/>
</window>
</objects>
</document>

View File

@ -0,0 +1,477 @@
import AppKit
import SwiftUI
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.
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"
/// 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()
/// Gets the terminal controller from the window controller.
var terminalController: TerminalController? {
windowController as? TerminalController
}
// 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 }
// 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) }
// 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()
}
// Create our reset zoom titlebar accessory.
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.
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
}
// 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 }
override func becomeKey() {
super.becomeKey()
resetZoomTabButton.contentTintColor = .controlAccentColor
}
override func resignKey() {
super.resignKey()
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)
// 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()
}
}
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 isTabBar(childViewController) {
childViewController.identifier = Self.tabBarIdentifier
tabBarDidAppear()
}
}
override func removeTitlebarAccessoryViewController(at index: Int) {
if let childViewController = titlebarAccessoryViewControllers[safe: index], isTabBar(childViewController) {
tabBarDidDisappear()
}
super.removeTitlebarAccessoryViewController(at: index)
}
// MARK: Tab Bar
/// This identifier is attached to the tab bar view controller when we detect it being
/// added.
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
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
}
private func tabBarDidAppear() {
// 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() {
if styleMask.contains(.titled) {
if titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) == nil {
addTitlebarAccessoryViewController(resetZoomAccessory)
}
}
}
// MARK: Tab Key Equivalents
var keyEquivalent: String? = nil {
didSet {
// When our key equivalent is set, we must update the tab label.
guard let keyEquivalent else {
keyEquivalentLabel.attributedStringValue = NSAttributedString()
return
}
keyEquivalentLabel.attributedStringValue = NSAttributedString(
string: "\(keyEquivalent) ",
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 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 = !surfaceIsZoomed
DispatchQueue.main.async {
self.viewModel.isSurfaceZoomed = self.surfaceIsZoomed
}
}
}
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: 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 titlebarFont: NSFont? {
didSet {
let font = titlebarFont ?? 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.
var attributedTitle: NSAttributedString? {
guard let titlebarFont = titlebarFont 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 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
// 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 {
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) {
surface = focusedSurface
} else {
// If it doesn't border the top, we use the top-left leaf
surface = terminalController.surfaceTree.root?.leftmostLeaf()
}
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)
}
}
let alpha = derivedConfig.backgroundOpacity.clamped(to: 0.001...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.
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
}
// MARK: Config
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
}
}
}
// 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
// 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, *), hasLiquidGlass() {
return viewModel.hasToolbar ? 10 : 5
} else {
return viewModel.hasToolbar ? 9 : 4
}
}
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, topPadding)
// We always need space at the end of the titlebar
.padding(.trailing, 10)
}
}
}
}

View File

@ -0,0 +1,269 @@
import AppKit
import SwiftUI
/// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later.
///
/// 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()
deinit {
tabBarObserver = nil
}
// MARK: NSWindow
override var title: String {
didSet {
viewModel.title = title
}
}
override var toolbar: NSToolbar? {
didSet{
guard toolbar != nil else { return }
// 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
}
}
}
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()
// 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
// 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)
// 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.
self.setupTabBar()
}
}
override func removeTitlebarAccessoryViewController(at index: Int) {
guard let childViewController = titlebarAccessoryViewControllers[safe: index],
isTabBar(childViewController) else {
super.removeTitlebarAccessoryViewController(at: index)
return
}
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 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
/// 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
// 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,
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
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(viewModel: viewModel))
item.visibilityPriority = .user
item.isEnabled = true
return item
default:
return NSToolbarItem(itemIdentifier: itemIdentifier)
}
}
// MARK: SwiftUI
class ViewModel: ObservableObject {
@Published var title: String = "👻 Ghostty"
@Published var hasTabBar: Bool = false
}
}
extension NSToolbarItem.Identifier {
/// Displays the title of the window
static let title = NSToolbarItem.Identifier("Title")
}
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.
return viewModel.title.isEmpty ? " " : viewModel.title
}
var body: some View {
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)
}
}
}
}

View File

@ -1,14 +1,10 @@
import Cocoa
class TerminalWindow: NSWindow {
/// This is the key in UserDefaults to use for the default `level` value.
static let defaultLevelKey: String = "TerminalDefaultLevel"
@objc dynamic var keyEquivalent: String = ""
/// 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
lazy var titlebarColor: NSColor = backgroundColor {
didSet {
@ -18,33 +14,6 @@ class TerminalWindow: NSWindow {
}
}
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 {
@ -56,104 +25,28 @@ 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
// MARK: NSWindow
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()
}
level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal
}
deinit {
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)
// 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
}
// 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 }
titlebarTabs = true
// 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 }
// Set the background color of the window
backgroundColor = derivedConfig.backgroundColor
guard let view = window.contentView else { continue }
return titlebarContainerView(in: view)
}
return nil
// This makes sure our titlebar renders correctly when there is a transparent background
titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity)
}
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 {
didSet {
tab.attributedTitle = attributedTitle
}
}
// 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
@ -170,7 +63,6 @@ class TerminalWindow: NSWindow {
super.becomeKey()
updateNewTabButtonOpacity()
resetZoomTabButton.contentTintColor = .controlAccentColor
resetZoomToolbarButton.contentTintColor = .controlAccentColor
tab.attributedTitle = attributedTitle
}
@ -179,7 +71,6 @@ class TerminalWindow: NSWindow {
super.resignKey()
updateNewTabButtonOpacity()
resetZoomTabButton.contentTintColor = .secondaryLabelColor
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
tab.attributedTitle = attributedTitle
}
@ -208,11 +99,6 @@ class TerminalWindow: NSWindow {
}
}
updateResetZoomTitlebarButtonVisibility()
// The remainder of this function only applies to styled tabs.
guard hasStyledTabs else { return }
titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none
if titlebarTabs {
hideToolbarOverflowButton()
@ -257,20 +143,29 @@ class TerminalWindow: NSWindow {
}
}
// MARK: - Tab Bar Styling
// MARK: Appearance
// 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 }
override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
super.syncAppearance(surfaceConfig)
// We style the tabs if they're transparent
return transparentTabs
// 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 {
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()
}
}
// 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
// MARK: Tab Bar Styling
var hasVeryDarkBackground: Bool {
backgroundColor.luminance < 0.05
@ -285,8 +180,7 @@ class TerminalWindow: NSWindow {
// 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)
}
@ -353,43 +247,8 @@ class TerminalWindow: NSWindow {
// 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)
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
@ -425,46 +284,19 @@ class TerminalWindow: NSWindow {
// 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
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 {
@ -477,6 +309,18 @@ class TerminalWindow: 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.
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
@ -492,7 +336,6 @@ class TerminalWindow: NSWindow {
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
@ -521,10 +364,7 @@ class TerminalWindow: NSWindow {
// 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
@ -536,7 +376,7 @@ class TerminalWindow: NSWindow {
// 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)
@ -547,7 +387,7 @@ class TerminalWindow: NSWindow {
}
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()
@ -703,7 +543,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: TitlebarTabsVenturaTerminalWindow?
private let isLightTheme: Bool
private let overlayLayer = VibrantLayer()
@ -731,7 +571,7 @@ fileprivate class WindowButtonsBackdropView: NSView {
fatalError("init(coder:) has not been implemented")
}
init(window: TerminalWindow) {
init(window: TitlebarTabsVenturaTerminalWindow) {
self.terminalWindow = window
self.isLightTheme = window.isLightTheme
@ -747,9 +587,133 @@ fileprivate class WindowButtonsBackdropView: NSView {
}
}
enum TerminalWindowTheme: String {
case auto
case system
case light
case dark
// 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")
}

View File

@ -0,0 +1,189 @@
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 {
/// 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 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()
// Setup all the KVO we will use, see the docs for the respective functions
// to learn why we need KVO.
setupKVO()
}
override func becomeMain() {
super.becomeMain()
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)
}
}
}
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 !effectViewIsHidden && !hasLiquidGlass() {
hideEffectView()
}
}
// MARK: Appearance
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, *), hasLiquidGlass() {
syncAppearanceTahoe(surfaceConfig)
} else {
syncAppearanceVentura(surfaceConfig)
}
}
@available(macOS 26.0, *)
private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
// 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, *)
private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
guard let titlebarContainer else { return }
titlebarContainer.wantsLayer = true
titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor
effectViewIsHidden = false
}
// MARK: View Finders
private var titlebarBackgroundView: NSView? {
titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView")
}
// 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()
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] _, 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).
// 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)
}
}
/// 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)
}
}
// 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
}
}

View File

@ -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[..<dotIndex]))
} else {
Int(versionString)
} else { return true }
// Note: we could also check for the UIDesignRequiresCompatibility key
// but our project doesn't use it so there's no point.
return major >= 26
}

View File

@ -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 {

View File

@ -0,0 +1,5 @@
extension Double {
func clamped(to range: ClosedRange<Double>) -> Double {
return Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
}
}

View File

@ -13,6 +13,59 @@ 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
}
/// 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
}
/// 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
}
/// 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? {
@ -54,4 +107,96 @@ 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)
}
}
// MARK: Debug
extension NSView {
/// Prints the view hierarchy from the root in a tree-like ASCII format.
///
/// 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() {
let root = rootView
print("View Hierarchy from Root:")
print(root.viewHierarchyDescription())
}
/// Returns a string representation of the view hierarchy in a tree-like format.
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)]"
// 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
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
}
}

View File

@ -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
@ -78,10 +74,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 +236,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
self.window.makeFirstResponder(firstResponder)
}
NotificationCenter.default.post(name: .fullscreenDidEnter, object: self)
self.delegate?.fullscreenDidChange()
}
}
@ -266,13 +265,24 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
window.styleMask = savedState.styleMask
window.setFrame(window.frameRect(forContentRect: savedState.contentFrame), display: true)
// 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,
window.titlebarTabs {
window.titlebarTabs = true
// 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)
}
}
// Removing "titled" also clears our toolbar
window.toolbar = savedState.toolbar
window.toolbarStyle = savedState.toolbarStyle
// If the window was previously in a tab group that isn't empty now,
// we re-add it. We have to do this because our process of doing non-native
// fullscreen removes the window from the tab group.
@ -303,6 +313,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
window.makeKeyAndOrderFront(nil)
// Notify the delegate
NotificationCenter.default.post(name: .fullscreenDidExit, object: self)
self.delegate?.fullscreenDidChange()
}
@ -380,6 +391,9 @@ 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
@ -391,6 +405,9 @@ 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
if let cgWindowId = window.cgWindowId {
@ -422,3 +439,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")
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 <francesc.arpi@gmail.com>, 2025.
#

View File

@ -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 <EMAIL@ADDRESS>, YEAR.
#

View File

@ -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 <r@rpfaeffle.com>, 2025.
#

View File

@ -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 <miguelp@quientienemail.com>, 2025.
#

View File

@ -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 <swiip__@hotmail.com>, 2025.
#

View File

@ -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 <halosatrio@gmail.com>, 2025.
#

View File

@ -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 <lon@sagisawa.me>, 2025.
#

View File

@ -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 <andrej.daskalov@gmail.com>, 2025.
#

View File

@ -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 <hanna@hanna.lol>, 2025.
# Uzair Aftab <uzaaft@outlook.com>, 2025.

View File

@ -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 <geesinknico@gmail.com>, 2025.
#

View File

@ -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 <b.sokorski@gmail.com>, 2025.
#

View File

@ -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 <gsodevel@gmail.com>, 2025.
#

View File

@ -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 <sergey_zhuzhgov@mail.ru>, 2025.
#

View File

@ -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 <emir_sari@icloud.com>, 2025.
#

View File

@ -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 <danilmail0110@gmail.com>, 2025.
#

View File

@ -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 <hi@pluie.me>, 2025.
#

View File

@ -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",
"-",
});

View File

@ -44,6 +44,7 @@ See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
# AUTHOR
Mitchell Hashimoto <m@mitchellh.com>
Ghostty contributors <https://github.com/ghostty-org/ghostty/graphs/contributors>
# SEE ALSO

View File

@ -36,6 +36,7 @@ See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
# AUTHOR
Mitchell Hashimoto <m@mitchellh.com>
Ghostty contributors <https://github.com/ghostty-org/ghostty/graphs/contributors>
# SEE ALSO

View File

@ -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" {

View File

@ -190,6 +190,11 @@ const Kind = enum {
//
0x2580...0x259F,
// "Geometric Shapes" block
0x25e2...0x25e5, //
0x25f8...0x25fa, //
0x25ff, //
// "Braille" block
0x2800...0x28FF,

Binary file not shown.