From 202020cd7d10bb6fa7b61c5d67033ddb5565b52c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 14 Jun 2025 14:21:40 -0700 Subject: [PATCH] macos: menu item symbols for Tahoe This is recommended for macOS Tahoe and all standard menu items now have associated images. This makes our app look more polished and native for macOS Tahoe. For icon choice, I tried to copy other native macOS apps as much as possible, mostly from Xcode. It looks like a lot of apps aren't updated yet. I'm absolutely open to suggestions for better icons but I think these are a good starting point. One menu change is I moved "reset font size" above "increase font size" which better matches other apps (e.g. Terminal.app). --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ macos/Sources/App/macOS/AppDelegate.swift | 40 ++++++++++++++++++- macos/Sources/App/macOS/MainMenu.xib | 16 ++++---- .../Sources/Ghostty/SurfaceView_AppKit.swift | 25 ++++++++---- .../Extensions/NSMenuItem+Extension.swift | 11 +++++ 5 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 4943f2f4d..a5663202b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ A5A6F72A2CC41B8900B232A5 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* AppInfo.swift */; }; A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; @@ -210,6 +211,7 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; }; @@ -478,6 +480,7 @@ A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */, + A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, @@ -763,6 +766,7 @@ A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, + A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7fb52a025..f460017f5 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -166,7 +166,7 @@ class AppDelegate: NSObject, // This registers the Ghostty => Services menu to exist. NSApp.servicesMenu = menuServices - + // Setup a local event monitor for app-level keyboard shortcuts. See // localEventHandler for more info why. _ = NSEvent.addLocalMonitorForEvents( @@ -242,6 +242,9 @@ class AppDelegate: NSObject, ghostty_app_set_color_scheme(app, scheme) } + + // Setup our menu + setupMenuImages() } func applicationDidBecomeActive(_ notification: Notification) { @@ -392,6 +395,41 @@ class AppDelegate: NSObject, return dockMenu } + /// Setup all the images for our menu items. + private func setupMenuImages() { + // Note: This COULD Be done all in the xib file, but I find it easier to + // modify this stuff as code. + self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus") + self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow") + self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") + self.menuSplitLeft?.setImageIfDesired(systemSymbolName: "rectangle.leadinghalf.inset.filled") + self.menuSplitUp?.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled") + self.menuSplitDown?.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled") + self.menuClose?.setImageIfDesired(systemSymbolName: "xmark") + self.menuIncreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.larger") + self.menuResetFontSize?.setImageIfDesired(systemSymbolName: "textformat.size") + self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller") + self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection") + self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal") + self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line") + self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") + self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") + self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") + self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") + self.menuPreviousSplit?.setImageIfDesired(systemSymbolName: "chevron.backward.2") + self.menuNextSplit?.setImageIfDesired(systemSymbolName: "chevron.forward.2") + self.menuEqualizeSplits?.setImageIfDesired(systemSymbolName: "inset.filled.topleft.topright.bottomleft.bottomright.rectangle") + self.menuSelectSplitLeft?.setImageIfDesired(systemSymbolName: "arrow.left") + self.menuSelectSplitRight?.setImageIfDesired(systemSymbolName: "arrow.right") + self.menuSelectSplitAbove?.setImageIfDesired(systemSymbolName: "arrow.up") + self.menuSelectSplitBelow?.setImageIfDesired(systemSymbolName: "arrow.down") + self.menuMoveSplitDividerUp?.setImageIfDesired(systemSymbolName: "arrow.up.to.line") + self.menuMoveSplitDividerDown?.setImageIfDesired(systemSymbolName: "arrow.down.to.line") + self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line") + self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line") + self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.3.layers.3d.top.filled") + } + /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. private func syncMenuShortcuts(_ config: Ghostty.Config) { guard ghostty.readiness == .ready else { return } diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 7130d544e..c9bff8b4a 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -251,18 +251,18 @@ - - - - - - + + + + + + diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index e4f6f507c..3e87176fc 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1281,6 +1281,10 @@ extension Ghostty { let menu = NSMenu() + // We just use a floating var so we can easily setup metadata on each item + // in a row without storing it all. + var item: NSMenuItem + // If we have a selection, add copy if self.selectedRange().length > 0 { menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "") @@ -1288,16 +1292,23 @@ extension Ghostty { menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "") menu.addItem(.separator()) - menu.addItem(withTitle: "Split Right", action: #selector(splitRight(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Left", action: #selector(splitLeft(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Down", action: #selector(splitDown(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Split Right", action: #selector(splitRight(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") + item = menu.addItem(withTitle: "Split Left", action: #selector(splitLeft(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.leadinghalf.inset.filled") + item = menu.addItem(withTitle: "Split Down", action: #selector(splitDown(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled") + item = menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled") menu.addItem(.separator()) - menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise") + item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "scope") menu.addItem(.separator()) - menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "pencil.line") return menu } diff --git a/macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift b/macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift new file mode 100644 index 000000000..e512904ef --- /dev/null +++ b/macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift @@ -0,0 +1,11 @@ +import AppKit + +extension NSMenuItem { + /// Sets the image property from a symbol if we want images on our menu items. + func setImageIfDesired(systemSymbolName symbol: String) { + // We only set on macOS 26 when icons on menu items became the norm. + if #available(macOS 26, *) { + image = NSImage(systemSymbolName: symbol, accessibilityDescription: title) + } + } +}