From 19e63f0dcd742fe3cca23b23adbbf19df0411889 Mon Sep 17 00:00:00 2001 From: maxsoch Date: Mon, 13 Jul 2026 19:04:08 +0200 Subject: [PATCH] add a Tips window for the app's less discoverable features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lists the ⌥ cleanup variant, menu keyboard shortcuts, selective upgrades, the self-updating cask reinstall behavior and the Logs reflex. Content is a plain array in TipsWindowController — adding a tip is one entry. No pbxproj change needed: the project uses Xcode 16 synchronized groups, so new files in BrewBar/ are picked up. Co-Authored-By: Claude Fable 5 --- BrewBar/BrewBarApp.swift | 11 ++++- BrewBar/TipsWindowController.swift | 78 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 BrewBar/TipsWindowController.swift diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index 45e2a19..a8487d1 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -26,6 +26,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var logWindow: LogWindowController? var settingsWindow: SettingsWindowController? + var tipsWindow: TipsWindowController? var logBuffer: String = "" var brewPath: String? @@ -105,6 +106,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "⚙️ Settings…", action: #selector(showSettings), keyEquivalent: ",")) + menu.addItem(NSMenuItem(title: "💡 Tips", action: #selector(showTips), keyEquivalent: "")) versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "") menu.addItem(versionItem) @@ -161,6 +163,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { presentWindow(of: settingsWindow) } + @objc func showTips() { + if tipsWindow == nil { + tipsWindow = TipsWindowController() + } + presentWindow(of: tipsWindow) + } + // MARK: - Window presentation /// Accessory apps never appear in Cmd-Tab and their windows can open @@ -177,7 +186,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func windowWillClose(_: Notification) { // async so the closing window's isVisible has flipped to false DispatchQueue.main.async { - let ourWindows = [self.logWindow?.window, self.settingsWindow?.window] + let ourWindows = [self.logWindow?.window, self.settingsWindow?.window, self.tipsWindow?.window] let stillOpen = ourWindows.compactMap { $0 }.contains { $0.isVisible } if !stillOpen { NSApp.setActivationPolicy(.accessory) diff --git a/BrewBar/TipsWindowController.swift b/BrewBar/TipsWindowController.swift new file mode 100644 index 0000000..5d0a97c --- /dev/null +++ b/BrewBar/TipsWindowController.swift @@ -0,0 +1,78 @@ +import Cocoa + +/// Static "good to know" window for the features a menu can't explain +/// by itself. Content lives in the tips array — adding one there is all +/// it takes. +class TipsWindowController: NSWindowController { + let tips: [(title: String, body: String)] = [ + ("⌥ reveals hidden menu items", + "With the menu open, hold Option (⌥): Cleanup becomes Cleanup (purge cache), which also empties Homebrew's download cache (brew cleanup --prune=all). Downloads are simply re-fetched when next needed."), + + ("Keyboard shortcuts", + "While the menu is open: R refreshes, U upgrades formulae & casks, L opens the logs, comma (,) opens Settings and Q quits."), + + ("Upgrade only one kind", + "When both formulae and casks are outdated, an “Upgrade only…” submenu appears under the main upgrade item to handle just one of the two."), + + ("Self-updating apps", + "Apps that update themselves (browsers, editors…) can drift from what brew installed, and brew then refuses a normal upgrade. BrewBar detects this and automatically reinstalls the cask instead — you may see reinstall --cask --force in the logs; that is expected."), + + ("When something looks off, read the Logs", + "Every brew command BrewBar runs is in the Logs window with its full output — errors, warnings and all. It is the first place to look."), + ] + + convenience init() { + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 440, height: 300), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + window.title = "BrewBar Tips" + // the controller keeps the window alive; without this, closing the + // window would deallocate it and reopening would crash + window.isReleasedWhenClosed = false + + self.init(window: window) + + buildUI() + window.center() + } + + func buildUI() { + guard let contentView = window?.contentView else { return } + + let tipViews: [NSView] = tips.map { tip in + let title = NSTextField(labelWithString: tip.title) + title.font = .boldSystemFont(ofSize: NSFont.systemFontSize) + + let body = NSTextField(wrappingLabelWithString: tip.body) + body.font = .systemFont(ofSize: NSFont.smallSystemFontSize) + body.textColor = .secondaryLabelColor + body.preferredMaxLayoutWidth = 400 + + let tipStack = NSStackView(views: [title, body]) + tipStack.orientation = .vertical + tipStack.alignment = .leading + tipStack.spacing = 4 + return tipStack + } + + let stack = NSStackView(views: tipViews) + stack.orientation = .vertical + stack.alignment = .leading + stack.spacing = 16 + + stack.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(stack) + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + stack.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -20), + stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20), + ]) + + contentView.layoutSubtreeIfNeeded() + window?.setContentSize(contentView.fittingSize) + } +}