Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
873c46d
refactor: use event-driven notifications instead of polling for NowPl…
bottlebrushes Jan 3, 2026
6666de0
feat: add weather widget using Open-Meteo API
bottlebrushes Jan 3, 2026
fcd0fff
feat: add event-based yabai provider with Unix socket listener
bottlebrushes Jan 3, 2026
d20a3de
feat: add weather popup with hourly forecast
bottlebrushes Jan 3, 2026
59cc625
fix: move yabai calls off main thread to prevent crash
bottlebrushes Jan 3, 2026
b3776d4
feat: add click-action config option for time widget
bottlebrushes Jan 3, 2026
c78e49e
feat: add click-action config for time widget with accessibility prompt
bottlebrushes Jan 3, 2026
0e0a782
fix: use SOCK_STREAM for yabai socket communication
bottlebrushes Jan 3, 2026
ded70fb
feat: enhanced WiFi popup with macOS-style controls
bottlebrushes Jan 4, 2026
0171ff0
feat: use keyboard shortcut simulation for Notification Center
bottlebrushes Jan 4, 2026
f9ee373
fix: correct popup Y positioning to appear directly below menu bar
bottlebrushes Jan 4, 2026
2335adb
Fix popup positioning to appear directly below widgets
bottlebrushes Jan 4, 2026
25be29b
Fix popup positioning to appear just below menu bar
bottlebrushes Jan 4, 2026
713d115
feat: replace polling with event-driven notifications
bottlebrushes Jan 16, 2026
6ba00ab
Merge pull request #1 from bettercoderthanyou/bettercoderthanyou/remo…
bottlebrushes Jan 16, 2026
54aae17
fix: rewrite popup positioning for consistent Y level across all widgets
bottlebrushes Jan 19, 2026
1904b49
feat: add calendar day view popup with today/tomorrow layout (#2)
bottlebrushes Jan 20, 2026
23c28e8
feat: enhanced calendar popup with day view, selection, and event det…
bottlebrushes Jan 21, 2026
d1bcef6
feat: click album art or song info to open music player (#4)
bottlebrushes Jan 22, 2026
85892fb
docs: update README for barik-but-better fork (#5)
bottlebrushes Jan 22, 2026
630186a
docs: add more improvements to README (#6)
bottlebrushes Jan 22, 2026
0455b69
Added support for changing the keybind of openning the notification c…
jarboer Jan 23, 2026
bc986fd
fix: restore now playing widget in default config
bottlebrushes Jan 30, 2026
3974bdd
Merge pull request #8 from bettercoderthanyou/bettercoderthanyou/fix-…
bottlebrushes Jan 30, 2026
91800ba
feat: add Claude Code usage tracking widget
bottlebrushes Feb 2, 2026
6ea8b6e
Merge pull request #9 from bettercoderthanyou/bettercoderthanyou/clau…
bottlebrushes Feb 2, 2026
03f7457
feat: drag-and-drop widget reordering in menu bar
bottlebrushes Feb 2, 2026
7f715e7
Merge pull request #10 from bettercoderthanyou/bettercoderthanyou/dra…
bottlebrushes Feb 2, 2026
521549d
Merge pull request #7 from jarboer/notification-center-keybind-config
bottlebrushes Feb 2, 2026
4a28e28
ci: auto-build on push + update README quickstart
bottlebrushes Feb 2, 2026
d7929dc
Merge pull request #11 from bettercoderthanyou/bettercoderthanyou/dra…
bottlebrushes Feb 2, 2026
2e4435c
docs: add drag-and-drop and Claude widget to improvements list
bottlebrushes Feb 2, 2026
049a410
Merge pull request #12 from bettercoderthanyou/bettercoderthanyou/dra…
bottlebrushes Feb 2, 2026
a69ef83
fix: hide claude widget when no data, count session messages accurately
bottlebrushes Feb 2, 2026
de321ce
feat: use Anthropic usage API with deferred keychain access
bottlebrushes Feb 2, 2026
639e855
Merge pull request #13 from bettercoderthanyou/bettercoderthanyou/hid…
bottlebrushes Feb 2, 2026
3e2644c
feat: add pomodoro timer widget
bottlebrushes Feb 3, 2026
040132a
fix: show pomodoro notifications while app is active
bottlebrushes Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Build and Release

on:
push:
branches: [main]

jobs:
build:
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Build Release
run: |
xcodebuild -scheme Barik -configuration Release \
-derivedDataPath build \
CODE_SIGNING_ALLOWED=NO

- name: Zip app
run: ditto -c -k --keepParent build/Build/Products/Release/Barik.app Barik.zip

- name: Get version
id: version
run: |
VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" build/Build/Products/Release/Barik.app/Contents/Info.plist)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Update latest release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release delete latest --yes 2>/dev/null || true
git push origin :refs/tags/latest 2>/dev/null || true
gh release create latest Barik.zip \
--title "Latest Build (v${{ steps.version.outputs.version }})" \
--notes "Automated build from \`main\` branch ($(date -u +%Y-%m-%d)).

Built from commit ${{ github.sha }}." \
--prerelease
2 changes: 2 additions & 0 deletions Barik/Barik.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
126 changes: 118 additions & 8 deletions Barik/Config/ConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class ConfigManager: ObservableObject {
private var fileWatchSource: DispatchSourceFileSystemObject?
private var fileDescriptor: CInt = -1
private var configFilePath: String?
private var suppressNextReload = false

private init() {
loadOrCreateConfigIfNeeded()
Expand Down Expand Up @@ -72,6 +73,8 @@ final class ConfigManager: ObservableObject {
displayed = [ # widgets on menu bar
"default.spaces",
"spacer",
"default.claude-usage",
"default.nowplaying",
"default.network",
"default.battery",
"divider",
Expand All @@ -84,6 +87,11 @@ final class ConfigManager: ObservableObject {
window.show-title = true
window.title.max-length = 50

[widgets.default.claude-usage]
plan = "pro"
five-hour-limit = 80
weekly-limit = 500

[widgets.default.battery]
show-percentage = true
warning-level = 30
Expand Down Expand Up @@ -116,6 +124,10 @@ final class ConfigManager: ObservableObject {
guard let self = self, let path = self.configFilePath else {
return
}
if self.suppressNextReload {
self.suppressNextReload = false
return
}
self.parseConfigFile(at: path)
}
fileWatchSource?.setCancelHandler { [weak self] in
Expand All @@ -134,7 +146,26 @@ final class ConfigManager: ObservableObject {
do {
let currentText = try String(contentsOfFile: path, encoding: .utf8)
let updatedText = updatedTOMLString(
original: currentText, key: key, newValue: newValue)
original: currentText, key: key, newValue: newValue, quoteValue: true)
try updatedText.write(
toFile: path, atomically: false, encoding: .utf8)
DispatchQueue.main.async {
self.parseConfigFile(at: path)
}
} catch {
print("Error updating config:", error)
}
}

func updateConfigValueRaw(key: String, newValue: String) {
guard let path = configFilePath else {
print("Config file path is not set")
return
}
do {
let currentText = try String(contentsOfFile: path, encoding: .utf8)
let updatedText = updatedTOMLString(
original: currentText, key: key, newValue: newValue, quoteValue: false)
try updatedText.write(
toFile: path, atomically: false, encoding: .utf8)
DispatchQueue.main.async {
Expand All @@ -146,8 +177,9 @@ final class ConfigManager: ObservableObject {
}

private func updatedTOMLString(
original: String, key: String, newValue: String
original: String, key: String, newValue: String, quoteValue: Bool = true
) -> String {
let formattedValue = quoteValue ? "\"\(newValue)\"" : newValue
if key.contains(".") {
let components = key.split(separator: ".").map(String.init)
guard components.count >= 2 else {
Expand All @@ -168,7 +200,7 @@ final class ConfigManager: ObservableObject {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("[") && trimmed.hasSuffix("]") {
if insideTargetTable && !updatedKey {
newLines.append("\(actualKey) = \"\(newValue)\"")
newLines.append("\(actualKey) = \(formattedValue)")
updatedKey = true
}
if trimmed == tableHeader {
Expand All @@ -185,7 +217,7 @@ final class ConfigManager: ObservableObject {
if line.range(of: pattern, options: .regularExpression)
!= nil
{
newLines.append("\(actualKey) = \"\(newValue)\"")
newLines.append("\(actualKey) = \(formattedValue)")
updatedKey = true
continue
}
Expand All @@ -195,13 +227,13 @@ final class ConfigManager: ObservableObject {
}

if foundTable && insideTargetTable && !updatedKey {
newLines.append("\(actualKey) = \"\(newValue)\"")
newLines.append("\(actualKey) = \(formattedValue)")
}

if !foundTable {
newLines.append("")
newLines.append("[\(tablePath)]")
newLines.append("\(actualKey) = \"\(newValue)\"")
newLines.append("\(actualKey) = \(formattedValue)")
}
return newLines.joined(separator: "\n")
} else {
Expand All @@ -217,20 +249,98 @@ final class ConfigManager: ObservableObject {
if line.range(of: pattern, options: .regularExpression)
!= nil
{
newLines.append("\(key) = \"\(newValue)\"")
newLines.append("\(key) = \(formattedValue)")
updatedAtLeastOnce = true
continue
}
}
newLines.append(line)
}
if !updatedAtLeastOnce {
newLines.append("\(key) = \"\(newValue)\"")
newLines.append("\(key) = \(formattedValue)")
}
return newLines.joined(separator: "\n")
}
}

func updateDisplayedWidgets(_ items: [TomlWidgetItem]) {
guard let path = configFilePath else {
print("Config file path is not set")
return
}
do {
let currentText = try String(contentsOfFile: path, encoding: .utf8)
let updatedText = replaceDisplayedArray(in: currentText, with: items)
suppressNextReload = true
try updatedText.write(toFile: path, atomically: true, encoding: .utf8)
DispatchQueue.main.async {
self.parseConfigFile(at: path)
}
} catch {
suppressNextReload = false
print("Error updating displayed widgets:", error)
}
}

private func replaceDisplayedArray(in original: String, with items: [TomlWidgetItem]) -> String {
let lines = original.components(separatedBy: "\n")
var inWidgetsSection = false
var arrayStartLine: Int?
var arrayEndLine: Int?
var bracketDepth = 0
var foundStart = false

for (lineIndex, line) in lines.enumerated() {
let trimmed = line.trimmingCharacters(in: .whitespaces)

if trimmed.hasPrefix("[") && trimmed.hasSuffix("]") {
inWidgetsSection = (trimmed == "[widgets]")
if foundStart && !inWidgetsSection {
break
}
continue
}

if inWidgetsSection && !foundStart {
if trimmed.hasPrefix("displayed") && trimmed.contains("=") {
arrayStartLine = lineIndex
foundStart = true
for char in trimmed {
if char == Character("[") { bracketDepth += 1 }
if char == Character("]") { bracketDepth -= 1 }
}
if bracketDepth == 0 {
arrayEndLine = lineIndex
break
}
}
} else if foundStart && arrayEndLine == nil {
for char in trimmed {
if char == Character("[") { bracketDepth += 1 }
if char == Character("]") { bracketDepth -= 1 }
}
if bracketDepth == 0 {
arrayEndLine = lineIndex
break
}
}
}

guard let start = arrayStartLine, let end = arrayEndLine else {
return original
}

let newArrayLines = "displayed = " + items.toTomlDisplayedArray()

var newLines = Array(lines[0..<start])
newLines.append(newArrayLines)
if end + 1 < lines.count {
newLines.append(contentsOf: lines[(end + 1)...])
}

return newLines.joined(separator: "\n")
}

func globalWidgetConfig(for widgetId: String) -> ConfigData {
config.rootToml.widgets.config(for: widgetId) ?? [:]
}
Expand Down
Loading