add dev tooling from /init: swiftlint, skills, gitignore; apply swiftformat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-06 21:07:17 +02:00
parent 5def46f2cd
commit 6f255380d0
6 changed files with 138 additions and 55 deletions

View file

@ -0,0 +1,25 @@
---
name: build
description: Build BrewBar with xcodebuild in Debug configuration and report any compiler errors or warnings.
disable-model-invocation: false
---
Build the BrewBar project using xcodebuild with the Debug configuration.
Run the following command from the project root:
```
xcodebuild \
-project BrewBar.xcodeproj \
-scheme BrewBar \
-configuration Debug \
-destination "platform=macOS" \
build \
2>&1
```
After running:
1. If the build succeeds, report "Build succeeded" and note any warnings.
2. If it fails, show the relevant error lines (not the full raw output), explain what each error means, and suggest a fix if the cause is clear.
This is a zero-dependency project (no SPM, CocoaPods, or Carthage), so no package resolution step is needed.

View file

@ -0,0 +1,34 @@
---
name: run
description: Build and launch BrewBar.app for manual testing. Use after making changes to verify the app runs and the menu bar icon appears correctly.
disable-model-invocation: false
---
Build and launch BrewBar for manual testing.
Steps:
1. Build the app in Debug configuration:
```
xcodebuild \
-project BrewBar.xcodeproj \
-scheme BrewBar \
-configuration Debug \
-destination "platform=macOS" \
build \
SYMROOT=build \
2>&1
```
If the build fails, stop here and report the errors with explanations.
2. If the build succeeds, open the built app:
```
open build/Debug/BrewBar.app
```
3. Tell the user:
- The app is a menu bar app (.accessory policy) — it will NOT appear in the Dock, only in the menu bar at the top right of the screen
- Look for the BrewBar icon in the menu bar to confirm it launched
- Clicking the icon opens the menu; the app runs Homebrew commands in the background
- To quit, use the Quit option in the app's menu
Note: If a previous instance is already running, macOS may bring it forward instead of launching a new one. Quit the existing instance first if you need a fresh launch.

2
.gitignore vendored
View file

@ -1 +1,3 @@
*.xcuserdatad/ *.xcuserdatad/
CLAUDE.local.md
.claude/settings.local.json

35
.swiftlint.yml Normal file
View file

@ -0,0 +1,35 @@
disabled_rules:
- trailing_whitespace # swiftformat handles this
- todo # todos in comments are fine
opt_in_rules:
- force_unwrapping
- empty_count
- closure_spacing
- contains_over_first_not_nil
included:
- BrewBar
excluded:
- BrewBar.xcodeproj
line_length:
warning: 140
error: 200
file_length:
warning: 500
error: 800
function_body_length:
warning: 80
error: 120
type_body_length:
warning: 400
error: 600
cyclomatic_complexity:
warning: 15
error: 25

View file

@ -2,7 +2,6 @@ import Cocoa
@main @main
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
static func main() { static func main() {
let app = NSApplication.shared let app = NSApplication.shared
let delegate = AppDelegate() let delegate = AppDelegate()
@ -12,7 +11,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
// MARK: - Varaibles // MARK: - Varaibles
var statusItem: NSStatusItem! var statusItem: NSStatusItem!
var statusMenuItem: NSMenuItem! var statusMenuItem: NSMenuItem!
var outdatedSubmenu: NSMenu! var outdatedSubmenu: NSMenu!
@ -22,9 +21,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var logBuffer: String = "" var logBuffer: String = ""
var brewPath: String? var brewPath: String?
var refreshTimer: Timer? var refreshTimer: Timer?
//var refreshInterval: TimeInterval = 3600 /// var refreshInterval: TimeInterval = 3600
var refreshInterval: TimeInterval { var refreshInterval: TimeInterval {
get { get {
let saved = UserDefaults.standard.double(forKey: "refreshInterval") let saved = UserDefaults.standard.double(forKey: "refreshInterval")
@ -39,11 +38,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// cache to prevent duplicate outdated calls // cache to prevent duplicate outdated calls
var cachedOutdated: [String] = [] var cachedOutdated: [String] = []
var lastOutdatedFetch: Date? var lastOutdatedFetch: Date?
// MARK: - Menu // MARK: - Menu
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
setMenuBarIcon("brewbar-uptodate") setMenuBarIcon("brewbar-uptodate")
@ -57,16 +55,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
menu.addItem(NSMenuItem(title: "🔄 Refresh", action: #selector(refreshAction), keyEquivalent: "r")) menu.addItem(NSMenuItem(title: "🔄 Refresh", action: #selector(refreshAction), keyEquivalent: "r"))
menu.addItem(NSMenuItem(title: "📦 Upgrade All", action: #selector(upgradeAll), keyEquivalent: "u")) menu.addItem(NSMenuItem(title: "📦 Upgrade All", action: #selector(upgradeAll), keyEquivalent: "u"))
let outdatedItem = NSMenuItem(title: "📋 Outdated", action: nil, keyEquivalent: "") let outdatedItem = NSMenuItem(title: "📋 Outdated", action: nil, keyEquivalent: "")
outdatedSubmenu = NSMenu() outdatedSubmenu = NSMenu()
outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: "")) outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: ""))
outdatedItem.submenu = outdatedSubmenu outdatedItem.submenu = outdatedSubmenu
menu.addItem(outdatedItem) menu.addItem(outdatedItem)
menu.addItem(NSMenuItem(title: "📜 Logs", action: #selector(showLogs), keyEquivalent: "l")) menu.addItem(NSMenuItem(title: "📜 Logs", action: #selector(showLogs), keyEquivalent: "l"))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
let intervalItem = NSMenuItem(title: "⏱ Refresh Interval", action: nil, keyEquivalent: "") let intervalItem = NSMenuItem(title: "⏱ Refresh Interval", action: nil, keyEquivalent: "")
let intervalSubmenu = NSMenu() let intervalSubmenu = NSMenu()
@ -80,48 +76,45 @@ class AppDelegate: NSObject, NSApplicationDelegate {
item.representedObject = interval item.representedObject = interval
intervalSubmenu.addItem(item) intervalSubmenu.addItem(item)
} }
let customMenuItem = NSMenuItem()
let customView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 30))
let textField = NSTextField(frame: NSRect(x: 10, y: 4, width: 120, height: 22)) let customMenuItem = NSMenuItem()
textField.placeholderString = "Secondes..." let customView = NSView(frame: NSRect(x: 0, y: 0, width: 200, height: 30))
textField.stringValue = "\(Int(refreshInterval))"
let confirmButton = NSButton(frame: NSRect(x: 138, y: 4, width: 52, height: 22)) let textField = NSTextField(frame: NSRect(x: 10, y: 4, width: 120, height: 22))
confirmButton.title = "✓ Set" textField.placeholderString = "Secondes..."
confirmButton.bezelStyle = .rounded textField.stringValue = "\(Int(refreshInterval))"
confirmButton.target = self
confirmButton.action = #selector(applyCustomInterval(_:))
customView.addSubview(textField) let confirmButton = NSButton(frame: NSRect(x: 138, y: 4, width: 52, height: 22))
customView.addSubview(confirmButton) confirmButton.title = "✓ Set"
customMenuItem.view = customView confirmButton.bezelStyle = .rounded
intervalSubmenu.addItem(customMenuItem) confirmButton.target = self
confirmButton.action = #selector(applyCustomInterval(_:))
customView.addSubview(textField)
customView.addSubview(confirmButton)
customMenuItem.view = customView
intervalSubmenu.addItem(customMenuItem)
intervalItem.submenu = intervalSubmenu intervalItem.submenu = intervalSubmenu
menu.addItem(intervalItem) menu.addItem(intervalItem)
versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "") versionItem = NSMenuItem(title: "🏷 Brew: ...", action: nil, keyEquivalent: "")
menu.addItem(versionItem) menu.addItem(versionItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu statusItem.menu = menu
refreshAll() refreshAll()
refreshBrewVersion() refreshBrewVersion()
refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in
self.refreshAll() self.refreshAll()
} // pour arrêter : refreshTimer?.invalidate() } // pour arrêter : refreshTimer?.invalidate()
} }
// MARK: - Env func // MARK: - Env func
func setMenuBarIcon(_ name: String) { func setMenuBarIcon(_ name: String) {
@ -135,30 +128,30 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
} }
func restartTimer() { func restartTimer() {
refreshTimer?.invalidate() // stop running timer refreshTimer?.invalidate() // stop running timer
refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { _ in
self.refreshAll() self.refreshAll()
} }
} }
@objc func setRefreshInterval(_ sender: NSMenuItem) { @objc func setRefreshInterval(_ sender: NSMenuItem) {
guard let interval = sender.representedObject as? TimeInterval else { return } guard let interval = sender.representedObject as? TimeInterval else { return }
refreshInterval = interval // le set sauvegarde dans UserDefaults refreshInterval = interval // le set sauvegarde dans UserDefaults
restartTimer() restartTimer()
} }
@objc func applyCustomInterval(_ sender: NSButton) { @objc func applyCustomInterval(_ sender: NSButton) {
guard let view = sender.superview, guard let view = sender.superview,
let textField = view.subviews.first(where: { $0 is NSTextField }) as? NSTextField, let textField = view.subviews.first(where: { $0 is NSTextField }) as? NSTextField,
let seconds = Int(textField.stringValue), seconds > 0 else { return } let seconds = Int(textField.stringValue), seconds > 0 else { return }
refreshInterval = TimeInterval(seconds) refreshInterval = TimeInterval(seconds)
restartTimer() restartTimer()
statusItem.menu?.cancelTracking() statusItem.menu?.cancelTracking()
} }
func resolveBrewPath() -> String { func resolveBrewPath() -> String {
if let cached = brewPath { if let cached = brewPath {
return cached return cached
@ -166,7 +159,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let paths = [ let paths = [
"/opt/homebrew/bin/brew", "/opt/homebrew/bin/brew",
"/usr/local/bin/brew" "/usr/local/bin/brew",
] ]
for path in paths { for path in paths {
@ -176,15 +169,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
//fallback // fallback
brewPath = "brew" brewPath = "brew"
return "brew" return "brew"
} }
func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) { func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) {
DispatchQueue.global().async { DispatchQueue.global().async {
let brew = self.resolveBrewPath() let brew = self.resolveBrewPath()
let cleaned = command.replacingOccurrences(of: "brew ", with: "") let cleaned = command.replacingOccurrences(of: "brew ", with: "")
let fullCommand = "\(brew) \(cleaned)" let fullCommand = "\(brew) \(cleaned)"
@ -233,9 +224,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
// SMART refresh (uses cache) /// SMART refresh (uses cache)
@objc func refreshOutdatedList() { @objc func refreshOutdatedList() {
// if fetched in last 5s uses cache // if fetched in last 5s uses cache
if let last = lastOutdatedFetch, Date().timeIntervalSince(last) < 5 { if let last = lastOutdatedFetch, Date().timeIntervalSince(last) < 5 {
updateOutdatedMenu(with: cachedOutdated) updateOutdatedMenu(with: cachedOutdated)
@ -245,16 +235,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
fetchOutdated() fetchOutdated()
} }
// FORCE refresh (menu button) /// FORCE refresh (menu button)
@objc func forceRefreshOutdated() { @objc func forceRefreshOutdated() {
fetchOutdated() fetchOutdated()
} }
func fetchOutdated() { func fetchOutdated() {
setMenuBarIcon("brewbar-updating") setMenuBarIcon("brewbar-updating")
runBrew("outdated") { output in
runBrew("outdated") { output in
let lines = output.split(separator: "\n").map { String($0) } let lines = output.split(separator: "\n").map { String($0) }
self.cachedOutdated = lines self.cachedOutdated = lines
@ -273,7 +262,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
func updateOutdatedMenu(with lines: [String]) { func updateOutdatedMenu(with lines: [String]) {
outdatedSubmenu.removeAllItems() outdatedSubmenu.removeAllItems()
outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: "")) outdatedSubmenu.addItem(NSMenuItem(title: "Refresh List", action: #selector(forceRefreshOutdated), keyEquivalent: ""))
@ -283,7 +271,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
for formula in lines { for formula in lines {
let item = NSMenuItem(title: formula, action: #selector(self.upgradeSingle(_:)), keyEquivalent: "") let item = NSMenuItem(title: formula, action: #selector(upgradeSingle(_:)), keyEquivalent: "")
item.representedObject = formula item.representedObject = formula
outdatedSubmenu.addItem(item) outdatedSubmenu.addItem(item)
} }
@ -313,7 +301,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
} }
// Brew version (correct) /// Brew version (correct)
func refreshBrewVersion() { func refreshBrewVersion() {
runBrew("--version") { output in runBrew("--version") { output in
let firstLine = output.split(separator: "\n").first.map(String.init) ?? "Unknown" let firstLine = output.split(separator: "\n").first.map(String.init) ?? "Unknown"

View file

@ -1,11 +1,9 @@
import Cocoa import Cocoa
class LogWindowController: NSWindowController { class LogWindowController: NSWindowController {
var textView: NSTextView! var textView: NSTextView!
convenience init() { convenience init() {
let contentRect = NSRect(x: 0, y: 0, width: 600, height: 400) let contentRect = NSRect(x: 0, y: 0, width: 600, height: 400)
let styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable] let styleMask: NSWindow.StyleMask = [.titled, .closable, .resizable]
@ -31,6 +29,7 @@ class LogWindowController: NSWindowController {
func update(text: String) { func update(text: String) {
textView.string = text textView.string = text
textView.scrollToEndOfDocument(nil) textView.scrollToEndOfDocument(nil)
} }
} }