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

View file

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