Compare commits

...

3 commits

Author SHA1 Message Date
maxsoch 4ab3cc1e70 add unit test target with BrewParser extraction
Extract brew output parsing into BrewParser.swift (pure Swift, no AppKit)
and add BrewBarTests target with 7 passing tests covering parseOutdated
and parseVersion edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 21:17:26 +02:00
maxsoch 6f255380d0 add dev tooling from /init: swiftlint, skills, gitignore; apply swiftformat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 21:07:17 +02:00
Maxence Socheleau 5def46f2cd n/a 2026-07-03 15:41:00 +02:00
9 changed files with 341 additions and 57 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.

3
.gitignore vendored
View file

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

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

@ -6,8 +6,19 @@
objectVersion = 77; objectVersion = 77;
objects = { objects = {
/* Begin PBXContainerItemProxy section */
87F2AA852FEE797F0014F9D6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 87F2AA5A2FEE797F0014F9D6 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 87F2AA612FEE797F0014F9D6;
remoteInfo = BrewBar;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
87F2AA622FEE797F0014F9D6 /* BrewBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrewBar.app; sourceTree = BUILT_PRODUCTS_DIR; }; 87F2AA622FEE797F0014F9D6 /* BrewBar.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrewBar.app; sourceTree = BUILT_PRODUCTS_DIR; };
87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BrewBarTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@ -29,6 +40,11 @@
path = BrewBar; path = BrewBar;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
87F2AA812FEE797F0014F9D6 /* BrewBarTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = BrewBarTests;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -39,6 +55,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
87F2AA822FEE797F0014F9D6 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@ -46,6 +69,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
87F2AA642FEE797F0014F9D6 /* BrewBar */, 87F2AA642FEE797F0014F9D6 /* BrewBar */,
87F2AA812FEE797F0014F9D6 /* BrewBarTests */,
87F2AA632FEE797F0014F9D6 /* Products */, 87F2AA632FEE797F0014F9D6 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@ -54,6 +78,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
87F2AA622FEE797F0014F9D6 /* BrewBar.app */, 87F2AA622FEE797F0014F9D6 /* BrewBar.app */,
87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -83,6 +108,28 @@
productReference = 87F2AA622FEE797F0014F9D6 /* BrewBar.app */; productReference = 87F2AA622FEE797F0014F9D6 /* BrewBar.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
87F2AA842FEE797F0014F9D6 /* BrewBarTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 87F2AA892FEE797F0014F9D6 /* Build configuration list for PBXNativeTarget "BrewBarTests" */;
buildPhases = (
87F2AA832FEE797F0014F9D6 /* Sources */,
87F2AA822FEE797F0014F9D6 /* Frameworks */,
);
buildRules = (
);
dependencies = (
87F2AA862FEE797F0014F9D6 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
87F2AA812FEE797F0014F9D6 /* BrewBarTests */,
);
name = BrewBarTests;
packageProductDependencies = (
);
productName = BrewBarTests;
productReference = 87F2AA802FEE797F0014F9D6 /* BrewBarTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@ -96,6 +143,10 @@
87F2AA612FEE797F0014F9D6 = { 87F2AA612FEE797F0014F9D6 = {
CreatedOnToolsVersion = 26.3; CreatedOnToolsVersion = 26.3;
}; };
87F2AA842FEE797F0014F9D6 = {
CreatedOnToolsVersion = 26.3;
TestTargetID = 87F2AA612FEE797F0014F9D6;
};
}; };
}; };
buildConfigurationList = 87F2AA5D2FEE797F0014F9D6 /* Build configuration list for PBXProject "BrewBar" */; buildConfigurationList = 87F2AA5D2FEE797F0014F9D6 /* Build configuration list for PBXProject "BrewBar" */;
@ -113,6 +164,7 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
87F2AA612FEE797F0014F9D6 /* BrewBar */, 87F2AA612FEE797F0014F9D6 /* BrewBar */,
87F2AA842FEE797F0014F9D6 /* BrewBarTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -135,8 +187,23 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
87F2AA832FEE797F0014F9D6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
87F2AA862FEE797F0014F9D6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 87F2AA612FEE797F0014F9D6 /* BrewBar */;
targetProxy = 87F2AA852FEE797F0014F9D6 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
87F2AA6B2FEE79800014F9D6 /* Debug */ = { 87F2AA6B2FEE79800014F9D6 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
@ -261,11 +328,20 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GLVUYYHFD9;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BrewBar/Info.plist; INFOPLIST_FILE = BrewBar/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = BrewBar; INFOPLIST_KEY_CFBundleDisplayName = BrewBar;
@ -280,6 +356,12 @@
PRODUCT_BUNDLE_IDENTIFIER = com.MaxSoch.BrewBar; PRODUCT_BUNDLE_IDENTIFIER = com.MaxSoch.BrewBar;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@ -294,11 +376,20 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GLVUYYHFD9;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
ENABLE_RESOURCE_ACCESS_LOCATION = NO;
ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = BrewBar/Info.plist; INFOPLIST_FILE = BrewBar/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = BrewBar; INFOPLIST_KEY_CFBundleDisplayName = BrewBar;
@ -313,6 +404,12 @@
PRODUCT_BUNDLE_IDENTIFIER = com.MaxSoch.BrewBar; PRODUCT_BUNDLE_IDENTIFIER = com.MaxSoch.BrewBar;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
RUNTIME_EXCEPTION_ALLOW_JIT = NO;
RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO;
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
@ -322,6 +419,48 @@
}; };
name = Release; name = Release;
}; };
87F2AA872FEE797F0014F9D6 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GLVUYYHFD9;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.MaxSoch.BrewBarTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BrewBar.app/Contents/MacOS/BrewBar";
};
name = Debug;
};
87F2AA882FEE797F0014F9D6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GLVUYYHFD9;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.MaxSoch.BrewBarTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BrewBar.app/Contents/MacOS/BrewBar";
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@ -343,6 +482,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
87F2AA892FEE797F0014F9D6 /* Build configuration list for PBXNativeTarget "BrewBarTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
87F2AA872FEE797F0014F9D6 /* Debug */,
87F2AA882FEE797F0014F9D6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 87F2AA5A2FEE797F0014F9D6 /* Project object */; rootObject = 87F2AA5A2FEE797F0014F9D6 /* Project object */;

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()
@ -24,7 +23,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
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")
@ -42,8 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
// 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")
@ -65,8 +63,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
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()
@ -102,9 +98,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
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)
@ -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,7 +235,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
fetchOutdated() fetchOutdated()
} }
// FORCE refresh (menu button) /// FORCE refresh (menu button)
@objc func forceRefreshOutdated() { @objc func forceRefreshOutdated() {
fetchOutdated() fetchOutdated()
} }
@ -254,8 +244,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
setMenuBarIcon("brewbar-updating") setMenuBarIcon("brewbar-updating")
runBrew("outdated") { output in runBrew("outdated") { output in
let lines = BrewParser.parseOutdated(output)
let lines = output.split(separator: "\n").map { String($0) }
self.cachedOutdated = lines self.cachedOutdated = lines
self.lastOutdatedFetch = Date() self.lastOutdatedFetch = Date()
@ -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,10 +301,10 @@ 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 = BrewParser.parseVersion(output)
DispatchQueue.main.async { DispatchQueue.main.async {
self.versionItem.title = "🏷 \(firstLine)" self.versionItem.title = "🏷 \(firstLine)"

13
BrewBar/BrewParser.swift Normal file
View file

@ -0,0 +1,13 @@
import Foundation
enum BrewParser {
static func parseOutdated(_ output: String) -> [String] {
output.split(separator: "\n")
.map { String($0) }
.filter { !$0.isEmpty }
}
static func parseVersion(_ output: String) -> String {
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)
} }
} }

View file

@ -0,0 +1,39 @@
@testable import BrewBar
import XCTest
final class BrewParserTests: XCTestCase {
// MARK: - parseOutdated
func testParseOutdated_multiplePackages() {
let output = "wget\nffmpeg\ngit\n"
XCTAssertEqual(BrewParser.parseOutdated(output), ["wget", "ffmpeg", "git"])
}
func testParseOutdated_emptyOutput() {
XCTAssertEqual(BrewParser.parseOutdated(""), [])
}
func testParseOutdated_singlePackage() {
XCTAssertEqual(BrewParser.parseOutdated("wget\n"), ["wget"])
}
func testParseOutdated_trailingNewlinesIgnored() {
let output = "wget\n\n\n"
XCTAssertEqual(BrewParser.parseOutdated(output), ["wget"])
}
// MARK: - parseVersion
func testParseVersion_picksFirstLine() {
let output = "Homebrew 4.3.0\nHomebrew/homebrew-core (git revision abc123)\n"
XCTAssertEqual(BrewParser.parseVersion(output), "Homebrew 4.3.0")
}
func testParseVersion_emptyOutput() {
XCTAssertEqual(BrewParser.parseVersion(""), "Unknown")
}
func testParseVersion_singleLine() {
XCTAssertEqual(BrewParser.parseVersion("Homebrew 4.3.0"), "Homebrew 4.3.0")
}
}