mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
Merge branch 'ghostty-org:main' into hu_HU_localization
This commit is contained in:
8
.github/workflows/release-tip.yml
vendored
8
.github/workflows/release-tip.yml
vendored
@ -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
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Mitchell Hashimoto
|
||||
Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -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 */,
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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"/>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
44
macos/Sources/Helpers/AppInfo.swift
Normal file
44
macos/Sources/Helpers/AppInfo.swift
Normal 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
|
||||
}
|
@ -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 {
|
||||
|
5
macos/Sources/Helpers/Extensions/Double+Extension.swift
Normal file
5
macos/Sources/Helpers/Extensions/Double+Extension.swift
Normal file
@ -0,0 +1,5 @@
|
||||
extension Double {
|
||||
func clamped(to range: ClosedRange<Double>) -> Double {
|
||||
return Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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.
|
||||
#
|
||||
|
@ -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",
|
||||
"-",
|
||||
});
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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" {
|
||||
|
@ -190,6 +190,11 @@ const Kind = enum {
|
||||
// ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟
|
||||
0x2580...0x259F,
|
||||
|
||||
// "Geometric Shapes" block
|
||||
0x25e2...0x25e5, // ◢◣◤◥
|
||||
0x25f8...0x25fa, // ◸◹◺
|
||||
0x25ff, // ◿
|
||||
|
||||
// "Braille" block
|
||||
0x2800...0x28FF,
|
||||
|
||||
|
BIN
src/font/sprite/testdata/Box.ppm
vendored
BIN
src/font/sprite/testdata/Box.ppm
vendored
Binary file not shown.
Reference in New Issue
Block a user