diff --git a/Sources/MenubucketApp/ScreenshotMode.swift b/Sources/MenubucketApp/ScreenshotMode.swift index 82991e2..9a4ef2a 100644 --- a/Sources/MenubucketApp/ScreenshotMode.swift +++ b/Sources/MenubucketApp/ScreenshotMode.swift @@ -36,6 +36,17 @@ enum ScreenshotMode { ("tile-developer-inbox", "Developer Inbox", "tray.full", ShotData.developerInboxNode, "blue"), ("tile-system", "System", "cpu.fill", ShotData.systemNode, "purple"), ("tile-sensors", "Sensors", "thermometer.medium", ShotData.sensorsNode, "orange"), + ("tile-battery-meter", "Battery", "battery.100percent", ShotData.batteryMeterRenderNode, "green"), + ("tile-clock", "Clock", "clock.fill", ShotData.clockRenderNode, "blue"), + ("tile-calendar", "Calendar", "calendar", ShotData.calendarRenderNode, "red"), + ("tile-codex-reset", "Codex Reset", "cloud.sun.fill", ShotData.codexResetRenderNode, "orange"), + ("tile-downloads-new", "Downloads", "arrow.down.circle.fill", ShotData.downloadsNewRenderNode, "green"), + ("tile-exchange", "Exchange", "dollarsign.circle.fill", ShotData.exchangeRenderNode, "green"), + ("tile-github-status", "GitHub Status", "checkmark.seal.fill", ShotData.githubStatusRenderNode, "green"), + ("tile-network", "Network", "arrow.up.arrow.down.circle.fill", ShotData.networkRenderNode, "blue"), + ("tile-now-playing", "Now Playing", "music.note", ShotData.nowPlayingRenderNode, "pink"), + ("tile-reminders", "Reminders", "checklist", ShotData.remindersRenderNode, "orange"), + ("tile-stock", "Stock", "chart.line.uptrend.xyaxis", ShotData.stockRenderNode, "green"), ] for tile in tiles { ok = render( @@ -405,6 +416,105 @@ private enum ShotData { (try? JSONDecoder().decode(UINode.self, from: Data(json.utf8))) ?? UINode(type: "spacer") } + /// Real `Battery` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against the exec source on a MacBook at 82%, discharging. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var batteryMeterRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"x-apple.systempreferences:com.apple.Battery-Settings.extension"},"children":[{"children":[{"type":"spacer"},{"foreground":"secondary","lineLimit":1,"role":"caption","text":"discharging","type":"text"}],"spacing":6,"type":"hstack"},{"monospacedDigit":true,"role":"title","size":40,"text":"82%","type":"text"},{"style":"linear","tint":"good","type":"progress","value":0.82}],"spacing":6,"type":"vstack"} + """#) + } + + /// Real `Clock` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against this Mac's own `/bin/date` output. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var clockRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"x-apple.systempreferences:com.apple.Date-Time-Settings.extension"},"children":[{"children":[{"monospacedDigit":true,"role":"title","size":44,"text":"22:05","type":"text"},{"foreground":"secondary","monospacedDigit":true,"role":"caption","size":17,"text":"38","type":"text"}],"spacing":3,"type":"hstack"},{"foreground":"secondary","role":"caption","text":"Fri Sep 25","type":"text"}],"spacing":2,"type":"vstack"} + """#) + } + + /// Real `Calendar` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against this Mac's own month-grid command output. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var calendarRenderNode: UINode { + decode(#""" + {"action":{"type":"openApp","value":"com.apple.iCal"},"children":[{"children":[{"lineLimit":1,"role":"title","text":"September","type":"text"}],"spacing":6,"type":"hstack"},{"children":[{"foreground":"tertiary","role":"caption","text":"Su","type":"text"},{"foreground":"tertiary","role":"caption","text":"Mo","type":"text"},{"foreground":"tertiary","role":"caption","text":"Tu","type":"text"},{"foreground":"tertiary","role":"caption","text":"We","type":"text"},{"foreground":"tertiary","role":"caption","text":"Th","type":"text"},{"foreground":"tertiary","role":"caption","text":"Fr","type":"text"},{"foreground":"tertiary","role":"caption","text":"Sa","type":"text"}],"columns":7,"spacing":2,"type":"grid"},{"columns":7,"items":[{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"1","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"2","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"3","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"4","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"5","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"6","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"7","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"8","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"9","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"10","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"11","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"12","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"13","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"14","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"15","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"16","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"17","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"18","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"19","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"20","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"21","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"22","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"23","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"24","type":"text"},{"fill":"accent","foreground":"primary","monospacedDigit":true,"role":"title","text":"25","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"26","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"27","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"28","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"29","type":"text"},{"fill":"","foreground":"primary","monospacedDigit":true,"role":"body","text":"30","type":"text"}],"spacing":2,"type":"grid"}],"spacing":4,"type":"vstack"} + """#) + } + + /// Real `Codex Reset` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against a live response from willcodexquotareset.com/api/forecast. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var codexResetRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"https://www.willcodexquotareset.com/"},"children":[{"children":[{"foreground":"secondary","lineLimit":1,"role":"caption","text":"Will Codex reset?","type":"text"},{"type":"spacer"},{"foreground":"secondary","role":"caption","text":"48H","type":"text"}],"spacing":6,"type":"hstack"},{"foreground":"secondary","monospacedDigit":true,"role":"title","size":32,"text":"29%","type":"text"},{"style":"linear","tint":"secondary","type":"progress","value":0.29},{"foreground":"secondary","lineLimit":2,"role":"caption","text":"Last reset 610.4h ago · alert at 70%","type":"text"}],"padding":10,"spacing":6,"type":"vstack"} + """#) + } + + /// Real `Downloads` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against 42 files with 39 at the last check. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var downloadsNewRenderNode: UINode { + decode(#""" + {"action":{"type":"revealFile","value":"~/Downloads"},"children":[{"children":[{"type":"spacer"},{"foreground":"secondary","monospacedDigit":true,"role":"caption","text":"42 files","type":"text"}],"spacing":7,"type":"hstack"},{"foreground":"good","monospacedDigit":true,"role":"body","text":"+3 since last check","type":"text"}],"spacing":6,"tone":"accent","type":"card"} + """#) + } + + /// Real `Exchange` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against a live response from open.er-api.com. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var exchangeRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"https://www.xe.com/currencyconverter/"},"children":[{"children":[{"foreground":"secondary","role":"caption","text":"USD → KRW","type":"text"}],"spacing":6,"type":"hstack"},{"monospacedDigit":true,"role":"title","size":32,"text":"₩1369","type":"text"},{"foreground":"secondary","role":"caption","text":"per $1","type":"text"}],"spacing":2,"type":"vstack"} + """#) + } + + /// Real `GitHub Status` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against a live response from githubstatus.com. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var githubStatusRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"https://www.githubstatus.com"},"children":[{"children":[{"lineLimit":1,"role":"title","text":"GitHub","type":"text"},{"type":"spacer"},{"text":"OK","tint":"good","type":"badge"}],"spacing":8,"type":"hstack"},{"lineLimit":2,"role":"body","text":"All Systems Operational","type":"text"},{"foreground":"secondary","lineLimit":2,"role":"caption","text":"Refreshes on open, every 15 minutes, and when your Mac wakes.","type":"text"}],"padding":10,"spacing":8,"type":"vstack"} + """#) + } + + /// Real `Network` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against the system network source with sample rates and a private LAN address. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var networkRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"x-apple.systempreferences:com.apple.Network-Settings.extension"},"children":[{"children":[{"type":"spacer"},{"foreground":"secondary","monospacedDigit":true,"role":"caption","text":"All interfaces","type":"text"}],"spacing":6,"type":"hstack"},{"lineLimit":1,"monospacedDigit":true,"role":"title","size":18,"text":"192.168.0.8","type":"text"},{"children":[{"children":[{"size":11,"source":{"kind":"sfSymbol","name":"arrow.down"},"tint":"accent","type":"image"},{"monospacedDigit":true,"role":"caption","text":"1.3 MB/s","type":"text"}],"spacing":4,"type":"hstack"},{"children":[{"size":11,"source":{"kind":"sfSymbol","name":"arrow.up"},"tint":"warning","type":"image"},{"monospacedDigit":true,"role":"caption","text":"86 KB/s","type":"text"}],"spacing":4,"type":"hstack"}],"spacing":12,"type":"hstack"}],"spacing":8,"type":"vstack"} + """#) + } + + /// Real `Now Playing` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against a sample track (the real source reads Music or Spotify). + /// The inner header's glyph and title are dropped: the tile header carries them. + static var nowPlayingRenderNode: UINode { + decode(#""" + {"action":{"type":"openApp","value":"com.apple.Music"},"children":[{"lineLimit":2,"role":"title","text":"Clair de Lune - Claude Debussy","type":"text"}],"spacing":4,"type":"vstack"} + """#) + } + + /// Real `Reminders` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against a sample count of 3 (the real source reads Reminders). + /// The inner header's glyph and title are dropped: the tile header carries them. + static var remindersRenderNode: UINode { + decode(#""" + {"action":{"type":"openApp","value":"com.apple.reminders"},"children":[{"monospacedDigit":true,"role":"title","size":40,"text":"3","type":"text"},{"foreground":"secondary","role":"caption","text":"open","type":"text"}],"spacing":2,"type":"vstack"} + """#) + } + + /// Real `Stock` render: the widget's own `workflow.json` evaluated + /// by `WorkflowEngine` against a live Yahoo Finance quote for the default AAPL symbol. + /// The inner header's glyph and title are dropped: the tile header carries them. + static var stockRenderNode: UINode { + decode(#""" + {"action":{"type":"openURL","url":"https://finance.yahoo.com/quote/AAPL"},"children":[{"children":[{"foreground":"secondary","role":"caption","text":"AAPL","type":"text"}],"spacing":6,"type":"hstack"},{"monospacedDigit":true,"role":"title","size":32,"text":"335.92","type":"text"},{"foreground":"danger","monospacedDigit":true,"role":"caption","text":"▼ -1.1","type":"text"}],"spacing":2,"type":"vstack"} + """#) + } + static var todayNode: UINode { decode(""" {"type":"hstack","spacing":10,"children":[ diff --git a/Tests/MenubucketCoreTests/GitHubStatusWidgetTests.swift b/Tests/MenubucketCoreTests/GitHubStatusWidgetTests.swift new file mode 100644 index 0000000..89fd946 --- /dev/null +++ b/Tests/MenubucketCoreTests/GitHubStatusWidgetTests.swift @@ -0,0 +1,61 @@ +import XCTest +@testable import MenubucketCore + +/// The status page API reports its indicator as `none`, `minor`, `major` or +/// `critical`. The widget used to print that raw value, so an all-clear read +/// "none" in both the badge and the menu bar. +final class GitHubStatusWidgetTests: XCTestCase { + private func definition() throws -> WorkflowDefinition { + let root = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() + return try WorkflowDefinition.decode( + from: try Data(contentsOf: root.appendingPathComponent("widgets/github-status/workflow.json")) + ) + } + + private func render(indicator: JSONValue) throws -> WorkflowEngine.Output { + try WorkflowEngine.evaluate( + try definition(), + sources: ["github": .object([ + "page": .object(["name": .string("GitHub")]), + "status": .object([ + "indicator": indicator, + "description": .string("All Systems Operational"), + ]), + ])], + settings: .object([:]) + ) + } + + private func badge(_ node: UINode) -> UINode? { + if node.type == "badge" { return node } + return (node.children ?? []).lazy.compactMap(badge).first + } + + func testAllClearReadsOKInGreen() throws { + let output = try render(indicator: .string("none")) + let badge = try XCTUnwrap(badge(output.viewTree)) + XCTAssertEqual(badge.text, "OK") + XCTAssertEqual(badge.tint, "good") + XCTAssertEqual(output.statusLabel, "GitHub OK") + } + + func testIncidentLevelsAreNamedAndColored() throws { + let cases: [(String, String, String)] = [ + ("minor", "Minor", "warning"), + ("major", "Major", "danger"), + ("critical", "Critical", "danger"), + ] + for (indicator, label, tint) in cases { + let badge = try XCTUnwrap(badge(try render(indicator: .string(indicator)).viewTree)) + XCTAssertEqual(badge.text, label, indicator) + XCTAssertEqual(badge.tint, tint, indicator) + } + } + + func testMissingIndicatorSaysUnknown() throws { + let badge = try XCTUnwrap(badge(try render(indicator: .null).viewTree)) + XCTAssertEqual(badge.text, "Unknown") + XCTAssertEqual(badge.tint, "secondary") + } +} diff --git a/assets/widget-previews/tile-battery-meter.png b/assets/widget-previews/tile-battery-meter.png new file mode 100644 index 0000000..afe93a5 Binary files /dev/null and b/assets/widget-previews/tile-battery-meter.png differ diff --git a/assets/widget-previews/tile-calendar.png b/assets/widget-previews/tile-calendar.png new file mode 100644 index 0000000..7a38668 Binary files /dev/null and b/assets/widget-previews/tile-calendar.png differ diff --git a/assets/widget-previews/tile-clock.png b/assets/widget-previews/tile-clock.png new file mode 100644 index 0000000..dcfc30b Binary files /dev/null and b/assets/widget-previews/tile-clock.png differ diff --git a/assets/widget-previews/tile-codex-reset.png b/assets/widget-previews/tile-codex-reset.png new file mode 100644 index 0000000..6dfe0ac Binary files /dev/null and b/assets/widget-previews/tile-codex-reset.png differ diff --git a/assets/widget-previews/tile-downloads-new.png b/assets/widget-previews/tile-downloads-new.png new file mode 100644 index 0000000..174b531 Binary files /dev/null and b/assets/widget-previews/tile-downloads-new.png differ diff --git a/assets/widget-previews/tile-exchange.png b/assets/widget-previews/tile-exchange.png new file mode 100644 index 0000000..5483f75 Binary files /dev/null and b/assets/widget-previews/tile-exchange.png differ diff --git a/assets/widget-previews/tile-github-status.png b/assets/widget-previews/tile-github-status.png new file mode 100644 index 0000000..d70d993 Binary files /dev/null and b/assets/widget-previews/tile-github-status.png differ diff --git a/assets/widget-previews/tile-network.png b/assets/widget-previews/tile-network.png new file mode 100644 index 0000000..d8dd92b Binary files /dev/null and b/assets/widget-previews/tile-network.png differ diff --git a/assets/widget-previews/tile-now-playing.png b/assets/widget-previews/tile-now-playing.png new file mode 100644 index 0000000..52f1a78 Binary files /dev/null and b/assets/widget-previews/tile-now-playing.png differ diff --git a/assets/widget-previews/tile-reminders.png b/assets/widget-previews/tile-reminders.png new file mode 100644 index 0000000..790b037 Binary files /dev/null and b/assets/widget-previews/tile-reminders.png differ diff --git a/assets/widget-previews/tile-stock.png b/assets/widget-previews/tile-stock.png new file mode 100644 index 0000000..f4a79dd Binary files /dev/null and b/assets/widget-previews/tile-stock.png differ diff --git a/registry/index.json b/registry/index.json index 58ebbd3..62d228f 100644 --- a/registry/index.json +++ b/registry/index.json @@ -2,7 +2,7 @@ "_comment": "Sample index served from this repository. install.bundled lets the packaged gallery install the starter widgets without network access. The `requires` field is a display-only gallery badge for widgets that need an external CLI/runtime. `category` is an optional curated grouping surfaced as gallery filter chips (falls back to `tags`). `screenshot` renders a preview thumbnail and `readme` links a rendered Markdown details page.", "schemaVersion": 1, "name": "barshelf official registry", - "updatedAt": "2026-09-22T12:10:00Z", + "updatedAt": "2026-09-25T13:30:00Z", "widgets": [ { "id": "dev.barshelf.today", @@ -30,6 +30,7 @@ "kind": "workflow", "tags": ["files", "workflow", "grid", "list", "stashbar"], "category": "Files", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/recent-files-grid/README.md", "install": { "url": "https://github.com/jiunbae/stashbar", "bundled": "recent-files-grid" }, "permissions": { "exec": [], "readPaths": ["~/Downloads"], "keychain": false, "notifications": false }, "homepage": "https://github.com/jiunbae/stashbar" @@ -61,6 +62,8 @@ "kind": "workflow", "tags": ["system", "battery", "meter", "native", "workflow"], "category": "System", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-battery-meter.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/battery-meter/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/battery-meter", "bundled": "battery-meter" }, "permissions": { "exec": ["/bin/sh"], "keychain": false, "notifications": false }, "homepage": "https://github.com/Open330/barshelf" @@ -130,6 +133,8 @@ "kind": "workflow", "tags": ["developer", "ai", "codex", "quota", "workflow", "http"], "category": "Developer", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-codex-reset.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/codex-reset/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/codex-reset", "bundled": "codex-reset" }, "permissions": { "exec": [], "keychain": false, "notifications": false, "network": ["www.willcodexquotareset.com"] }, "homepage": "https://www.willcodexquotareset.com/" @@ -139,12 +144,14 @@ "accent": "green", "name": "Downloads", "description": "Counts files in a folder and shows how many were added or removed since your last check, persisting the count across refreshes with the workflow storage layer.", - "version": "0.1.0", + "version": "0.1.1", "author": "barshelf", - "icon": "arrow.down.circle.badge.clock", + "icon": "arrow.down.circle.fill", "kind": "workflow", "tags": ["files", "workflow", "storage", "persistence", "diff"], "category": "Files", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-downloads-new.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/downloads-new/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/downloads-new", "bundled": "downloads-new" }, "permissions": { "exec": [], "readPaths": ["~/Downloads"], "keychain": false, "notifications": false }, "homepage": "https://github.com/Open330/barshelf" @@ -154,12 +161,14 @@ "accent": "green", "name": "GitHub Status", "description": "Fetches GitHub's public status JSON over HTTPS and refreshes when your Mac wakes — an HTTP-source example.", - "version": "0.1.0", + "version": "0.1.1", "author": "barshelf", "icon": "checkmark.seal.fill", "kind": "workflow", "tags": ["developer", "status", "workflow", "http"], "category": "Developer", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-github-status.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/github-status/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/github-status", "bundled": "github-status" }, "permissions": { "exec": [], "keychain": false, "notifications": false, "network": ["www.githubstatus.com"] }, "homepage": "https://www.githubstatus.com/" @@ -175,6 +184,8 @@ "kind": "workflow", "tags": ["clock", "time", "native", "workflow"], "category": "Demo", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-clock.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/clock/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/clock", "bundled": "clock" }, "permissions": { "exec": ["/bin/date"], "keychain": false, "notifications": false }, "homepage": "https://github.com/Open330/barshelf" @@ -225,6 +236,7 @@ "kind": "workflow", "tags": ["weather", "http", "native", "workflow"], "category": "Demo", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/weather/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/weather", "bundled": "weather" }, "permissions": { "exec": [], "keychain": false, "notifications": false, "network": ["api.open-meteo.com"] }, "homepage": "https://open-meteo.com/" @@ -240,6 +252,8 @@ "kind": "workflow", "tags": ["calendar", "date", "grid", "native", "workflow"], "category": "Demo", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-calendar.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/calendar/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/calendar", "bundled": "calendar" }, "permissions": { "exec": ["/bin/sh"], "keychain": false, "notifications": false }, "homepage": "https://github.com/Open330/barshelf" @@ -255,6 +269,8 @@ "kind": "workflow", "tags": ["finance", "exchange", "http", "native"], "category": "Demo", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-exchange.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/exchange/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/exchange", "bundled": "exchange" }, "permissions": { "exec": [], "keychain": false, "notifications": false, "network": ["open.er-api.com"] }, "homepage": "https://www.exchangerate-api.com/" @@ -270,6 +286,8 @@ "kind": "workflow", "tags": ["system", "network", "ip", "activity", "native"], "category": "System", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-network.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/network/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/network", "bundled": "network" }, "permissions": { "exec": [], "keychain": false, "notifications": false, "system": ["network"] }, "homepage": "https://github.com/Open330/barshelf" @@ -285,6 +303,8 @@ "kind": "workflow", "tags": ["finance", "stock", "http", "headers", "native"], "category": "Demo", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-stock.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/stock/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/stock", "bundled": "stock" }, "permissions": { "exec": [], "keychain": false, "notifications": false, "network": ["query1.finance.yahoo.com"] }, "homepage": "https://finance.yahoo.com/" @@ -300,6 +320,8 @@ "kind": "workflow", "tags": ["media", "music", "spotify", "native"], "category": "Media", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-now-playing.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/now-playing/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/now-playing", "bundled": "now-playing" }, "permissions": { "exec": ["/bin/sh"], "keychain": false, "notifications": false }, "homepage": "https://github.com/Open330/barshelf" @@ -315,6 +337,8 @@ "kind": "workflow", "tags": ["reminders", "tasks", "native"], "category": "Media", + "screenshot": "https://raw.githubusercontent.com/Open330/barshelf/main/assets/widget-previews/tile-reminders.png", + "readme": "https://github.com/Open330/barshelf/blob/main/widgets/reminders/README.md", "install": { "url": "https://github.com/Open330/barshelf/tree/main/widgets/reminders", "bundled": "reminders" }, "permissions": { "exec": ["/bin/sh"], "keychain": false, "notifications": false }, "homepage": "https://github.com/Open330/barshelf" diff --git a/scripts/build-widget-pages.py b/scripts/build-widget-pages.py index d96f23e..d9268f7 100644 --- a/scripts/build-widget-pages.py +++ b/scripts/build-widget-pages.py @@ -46,6 +46,14 @@ "sensors": "Mac temperatures, fans and power in the menu bar", "system": "CPU, memory and disk in the macOS menu bar", "muxa-watch": "Watch coding agents from the macOS menu bar", + "codex-reset": "Will Codex reset? A quota reset forecast in the macOS menu bar", + "github-status": "GitHub status in the macOS menu bar", + "network": "Network speed and local IP in the macOS menu bar", + "exchange": "USD to KRW exchange rate in the macOS menu bar", + "stock": "Stock prices in the macOS menu bar", + "battery-meter": "Battery percentage in the macOS menu bar", + "now-playing": "Now playing from Music or Spotify in the macOS menu bar", + "weather": "Weather in the macOS menu bar", } # Widgets that live in another repository have no README here. Say what the diff --git a/scripts/check-release-versions.py b/scripts/check-release-versions.py index fd7e596..b2fc73d 100755 --- a/scripts/check-release-versions.py +++ b/scripts/check-release-versions.py @@ -225,10 +225,13 @@ def check_widget_versions(released: str) -> list[str]: ["git", "diff", "--name-only", tag, "--", "widgets/"], cwd=ROOT, capture_output=True, text=True, check=True, ).stdout.split() + # A README is documentation, not behaviour: the app never reads it, and + # bumping a version for one would reinstall every copy for no change. names = sorted({ pathlib.PurePosixPath(path).parts[1] for path in changed if len(pathlib.PurePosixPath(path).parts) > 2 + and pathlib.PurePosixPath(path).name != "README.md" }) if not names: print("ok: no widget changed since the last release") diff --git a/widgets/battery-meter/README.md b/widgets/battery-meter/README.md new file mode 100644 index 0000000..f7cbb23 --- /dev/null +++ b/widgets/battery-meter/README.md @@ -0,0 +1,21 @@ +# Battery + +A native battery card for BarShelf: a large percentage, a level-colored meter, +and the charging state, read from `pmset`. It is a pure workflow widget, so it +needs no Deno and no extra tools. + +## What it shows + +- The charge as a big percentage and a meter. +- The state `pmset` reports: charging, discharging, or charged. +- A battery glyph and color that follow the level: green above 25%, orange + above 10%, red below that. + +On a Mac without a battery, `pmset` reports no percentage, so the card shows +0% and the state "unknown". + +## Permissions and refresh + +The widget runs one fixed `/bin/sh -c` pipeline over `/usr/bin/pmset -g batt`; +it cannot run anything else. It refreshes when the popover opens and every +minute while it is visible. Clicking the card opens Battery settings. diff --git a/widgets/calendar/README.md b/widgets/calendar/README.md new file mode 100644 index 0000000..7ef541b --- /dev/null +++ b/widgets/calendar/README.md @@ -0,0 +1,21 @@ +# Calendar + +A native month calendar for BarShelf: the month name over a seven-column grid, +with today highlighted. + +## What it shows + +- The current month, starting the week on Sunday. +- Every day of the month in a grid, with blank cells before the first day so + dates line up under the right weekday. +- Today as a filled red circle. + +The grid is built by a fixed shell command that reads the date and the month +length from `/bin/date` and `/usr/bin/cal`. It does not read your calendar +events; for those, use the Next Meeting widget. + +## Permissions and refresh + +The widget runs that one `/bin/sh -c` command and nothing else. It rebuilds the +grid when the popover opens if the last one is more than an hour old, so it +rolls over to a new day or month on its own. Clicking it opens Calendar. diff --git a/widgets/clock/README.md b/widgets/clock/README.md new file mode 100644 index 0000000..c8f14c5 --- /dev/null +++ b/widgets/clock/README.md @@ -0,0 +1,19 @@ +# Clock + +A native digital clock for BarShelf: a large 24-hour `HH:MM`, a live seconds +counter beside it, and the date underneath. + +## What it shows + +- Hours and minutes in 24-hour format, with tabular digits so the layout does + not jump as numbers change. +- Seconds as a smaller counter. +- The weekday, month, and day, for example "Fri Sep 25". + +## Permissions and refresh + +The widget runs a single fixed `/bin/date` format string and nothing else, so +it has no external dependency and makes no network request. It refreshes every +second while the popover is visible and pauses when it is closed. Clicking the +card opens Date & Time settings, where you can change the system clock or time +zone the widget reads. diff --git a/widgets/codex-reset/README.md b/widgets/codex-reset/README.md new file mode 100644 index 0000000..db738e5 --- /dev/null +++ b/widgets/codex-reset/README.md @@ -0,0 +1,22 @@ +# Codex Reset + +An unofficial forecast of whether OpenAI Codex usage limits will reset in the +next 48 hours, from the public feed at +[willcodexquotareset.com](https://www.willcodexquotareset.com/). It is not +affiliated with OpenAI; treat the number as a community estimate, not a promise. + +## What it shows + +- A 0–100 score for a reset within 48 hours, as a large number and a meter. +- A tint that changes at the 70 and 40 marks, so a likely reset stands out. +- How long ago the last reset was, and the alert threshold. + +The score is mirrored into the widget's status item, so you can keep it in the +menu bar. + +## Permissions and refresh + +The only permission is HTTPS access to `www.willcodexquotareset.com`. The feed +re-derives its forecast about every 30 minutes, so the widget refreshes on the +same cadence, plus when the popover opens and when your Mac wakes. Clicking it +opens the site. diff --git a/widgets/downloads-new/README.md b/widgets/downloads-new/README.md new file mode 100644 index 0000000..6a29c2e --- /dev/null +++ b/widgets/downloads-new/README.md @@ -0,0 +1,25 @@ +# Downloads + +Counts the files in a folder and tells you how many arrived or disappeared +since the last time you looked. The count is kept in the widget's own storage, +so the difference survives between refreshes. + +## What it shows + +- The folder name and how many files it holds. +- The change since the previous check, for example "+3 since last check", in + green when files were added and orange when some were removed. + +Hidden files are skipped, and the newest 500 files are counted. + +## Settings + +| Setting | Default | Effect | +|---|---|---| +| Folder | `~/Downloads` | Which folder to count. | + +## Permissions and refresh + +The widget asks for read access to the chosen folder and for widget storage; +it only reads file names and dates, never file contents. It refreshes when the +popover opens. Clicking the card reveals the folder in Finder. diff --git a/widgets/downloads-new/widget.json b/widgets/downloads-new/widget.json index efee659..3cde5b4 100644 --- a/widgets/downloads-new/widget.json +++ b/widgets/downloads-new/widget.json @@ -3,8 +3,8 @@ "schemaVersion": 1, "id": "dev.barshelf.downloads-new", "name": "Downloads", - "version": "0.1.0", - "icon": "arrow.down.circle.badge.clock", + "version": "0.1.1", + "icon": "arrow.down.circle.fill", "bucket": { "group": "Files", "order": 35, "size": "S" }, "entry": { "kind": "workflow", "main": "workflow.json" }, diff --git a/widgets/exchange/README.md b/widgets/exchange/README.md new file mode 100644 index 0000000..33f48f6 --- /dev/null +++ b/widgets/exchange/README.md @@ -0,0 +1,21 @@ +# Exchange + +The US dollar to Korean won exchange rate as one large figure, from the free +[open.er-api.com](https://open.er-api.com) API. No API key is needed. + +## What it shows + +- The pair, "USD → KRW". +- The rate as a large won amount per dollar. + +The widget is also a compact example of BarShelf's HTTP source: one request, +one value pulled out of the JSON, and a native card, with no code. To follow a +different pair, copy the widget and change the currency in the workflow's URL +and expression. + +## Permissions and refresh + +The only permission is HTTPS access to `open.er-api.com`. The widget refreshes +when the popover opens if the last rate is more than an hour old, and every +hour while it is visible. Clicking it opens a currency converter in your +browser. diff --git a/widgets/github-status/README.md b/widgets/github-status/README.md new file mode 100644 index 0000000..9427221 --- /dev/null +++ b/widgets/github-status/README.md @@ -0,0 +1,24 @@ +# GitHub Status + +GitHub's own service status, from the public status page API, so you can tell +at a glance whether a failing push or a slow Actions run is on your side or +GitHub's. + +## What it shows + +- The status page name. +- The overall level as a badge: OK in green, Minor in orange, Major or + Critical in red. The menu bar label uses the same word, for example + "GitHub OK". +- The status description, for example "All Systems Operational". + +## Permissions and refresh + +The only permission is HTTPS access to `www.githubstatus.com`; no GitHub +account or token is involved. The widget refreshes when the popover opens, +every 15 minutes while it is visible, and when your Mac wakes, so a status +change that happened while the lid was closed shows up right away. Clicking it +opens githubstatus.com. + +It is also a small example of an HTTP source with a wake trigger, useful as a +starting point for any status-page widget. diff --git a/widgets/github-status/widget.json b/widgets/github-status/widget.json index 2310ead..5286101 100644 --- a/widgets/github-status/widget.json +++ b/widgets/github-status/widget.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "dev.barshelf.github-status", "name": "GitHub Status", - "version": "0.1.0", + "version": "0.1.1", "icon": "checkmark.seal.fill", "bucket": { "group": "Developer", "order": 40, "size": "S" }, diff --git a/widgets/github-status/workflow.json b/widgets/github-status/workflow.json index aff266b..068872c 100644 --- a/widgets/github-status/workflow.json +++ b/widgets/github-status/workflow.json @@ -13,7 +13,9 @@ "transforms": { "status": { "use": "assign", "from": "$.sources.github.status" }, - "page": { "use": "assign", "from": "$.sources.github.page" } + "page": { "use": "assign", "from": "$.sources.github.page" }, + "label": { "use": "assign", "from": "if(eq(transforms.status.indicator,'none'),'OK',if(eq(transforms.status.indicator,'minor'),'Minor',if(eq(transforms.status.indicator,'major'),'Major',if(eq(transforms.status.indicator,'critical'),'Critical',coalesce(transforms.status.indicator,'Unknown')))))" }, + "tone": { "use": "assign", "from": "if(eq(transforms.status.indicator,'none'),'good',if(eq(transforms.status.indicator,'minor'),'warning',if(or(eq(transforms.status.indicator,'major'),eq(transforms.status.indicator,'critical')),'danger','secondary')))" } }, "view": { @@ -26,10 +28,10 @@ "type": "hstack", "spacing": 8, "children": [ - { "type": "image", "source": { "kind": "sfSymbol", "name": "checkmark.seal.fill" }, "size": 16, "tint": "accent" }, + { "type": "image", "source": { "kind": "sfSymbol", "name": "${if(eq(transforms.status.indicator,'none'),'checkmark.seal.fill','exclamationmark.triangle.fill')}" }, "size": 16, "tint": "${transforms.tone}" }, { "type": "text", "text": "${transforms.page.name}", "role": "title", "lineLimit": 1 }, { "type": "spacer" }, - { "type": "badge", "text": "${transforms.status.indicator}", "tint": "accent" } + { "type": "badge", "text": "${transforms.label}", "tint": "${transforms.tone}" } ] }, { @@ -49,7 +51,7 @@ }, "status": { - "label": "GitHub ${transforms.status.indicator}", + "label": "GitHub ${transforms.label}", "tooltip": "GitHub status: ${transforms.status.description}" } } diff --git a/widgets/network/README.md b/widgets/network/README.md new file mode 100644 index 0000000..d2aa3ec --- /dev/null +++ b/widgets/network/README.md @@ -0,0 +1,26 @@ +# Network + +Native upload and download activity for BarShelf, with your local address and +an optional live speed readout in the menu bar. It reads the system's network +counters directly: no subprocess and no external tools. + +## What it shows + +- The interface being measured, or "All interfaces". +- Your local IP address. +- Current download and upload rates, scaled to B/s, KB/s, MB/s, or GB/s. + +The first sample has nothing to compare against, so rates show "—" until the +second refresh instead of a misleading zero. + +## Settings + +| Setting | Default | Effect | +|---|---|---| +| Menu bar shows | Activity | Two activity dots, or both speeds as text. | +| Network interface | `all` | Measure every interface, or one such as `en0`. | + +## Permissions and refresh + +The widget uses the built-in `network` system source. It refreshes every two +seconds while visible. Clicking it opens Network settings. diff --git a/widgets/now-playing/README.md b/widgets/now-playing/README.md new file mode 100644 index 0000000..59e02b5 --- /dev/null +++ b/widgets/now-playing/README.md @@ -0,0 +1,23 @@ +# Now Playing + +The track that is playing right now, from Music or Spotify, as one line in the +BarShelf popover. + +## What it shows + +- The current track as "Title - Artist". +- "Nothing playing" when neither app is playing. + +The widget asks Music first and falls back to Spotify, so whichever app is +playing is the one you see. + +## Permissions and refresh + +The widget runs one fixed `/bin/sh -c` command that asks Music and Spotify +through AppleScript. The first time it runs, macOS asks whether BarShelf may +control those apps; approve it once in the Automation prompt. You can change +that later in System Settings › Privacy & Security › Automation. + +It refreshes every five seconds while the popover is visible and stops when it +is closed. Clicking the card opens Music. The widget only reads the track name +and artist; it does not control playback. diff --git a/widgets/recent-files-grid/README.md b/widgets/recent-files-grid/README.md new file mode 100644 index 0000000..c8be19c --- /dev/null +++ b/widgets/recent-files-grid/README.md @@ -0,0 +1,27 @@ +# Recent Files + +The newest files in a folder as QuickLook thumbnails, in a grid or a list. +Drag a file straight out to Finder or another app, or click it to open it. +Inspired by [Stashbar](https://github.com/jiunbae/stashbar). + +## What it shows + +- The folder name and how many files are listed. +- The newest files first, with real QuickLook thumbnails. +- Grid tiles or list rows with names and dates, whichever you pick. + +The widget watches the folder, so a new download appears without waiting for +the next refresh. + +## Settings + +| Setting | Default | Effect | +|---|---|---| +| Folder | `~/Downloads` | Which folder to show. | +| Maximum files | 24 | How many of the newest files to list. | +| View | Grid | Grid of thumbnails, or a list. | +| Thumbnail size | 52 | Thumbnail size in points. | + +## Permissions + +The widget asks for read access to the chosen folder. Hidden files are skipped. diff --git a/widgets/reminders/README.md b/widgets/reminders/README.md new file mode 100644 index 0000000..9ef1107 --- /dev/null +++ b/widgets/reminders/README.md @@ -0,0 +1,22 @@ +# Reminders + +How many reminders you still have open, as one large number, so you can see +whether the list is growing without opening the app. + +## What it shows + +- The count of incomplete reminders across all your lists. +- The word "open" underneath, so the number reads on its own. + +It shows 0 when Reminders cannot be read, for example before you approve the +permission prompt. + +## Permissions and refresh + +The widget runs one fixed `/bin/sh -c` command that asks Reminders for the +count through AppleScript. The first time it runs, macOS asks whether BarShelf +may control Reminders; approve it once in the Automation prompt. You can change +that later in System Settings › Privacy & Security › Automation. + +The widget reads only the count, never reminder titles or notes. It refreshes +every minute while the popover is visible. Clicking it opens Reminders. diff --git a/widgets/stock/README.md b/widgets/stock/README.md new file mode 100644 index 0000000..6a4cb22 --- /dev/null +++ b/widgets/stock/README.md @@ -0,0 +1,24 @@ +# Stock + +A live quote for any ticker from the Yahoo Finance chart API: a large price +and the change since the previous close. + +## What it shows + +- The ticker symbol. +- The latest market price. +- The change against the previous close, with ▲ in green or ▼ in red. + +## Settings + +| Setting | Default | Effect | +|---|---|---| +| Ticker | `AAPL` | Any symbol Yahoo Finance knows, such as `MSFT` or `005930.KS`. | + +## Permissions and refresh + +The only permission is HTTPS access to `query1.finance.yahoo.com`. The request +sends a browser User-Agent header, which the API requires; the widget is also +BarShelf's example of an HTTP source with custom headers. It refreshes every +five minutes while the popover is visible. Clicking it opens the quote page on +Yahoo Finance. Prices can be delayed; do not trade on them. diff --git a/widgets/weather/README.md b/widgets/weather/README.md new file mode 100644 index 0000000..89c8adb --- /dev/null +++ b/widgets/weather/README.md @@ -0,0 +1,26 @@ +# Weather + +The current temperature and conditions for any place, from the free +[Open-Meteo](https://open-meteo.com) API. No API key or account is needed. + +## What it shows + +- The place name you set. +- The temperature in °C, rounded to a whole degree. +- A condition and matching icon: Clear, Cloudy, Rain, or Storm, derived from + the reported weather code. + +## Settings + +| Setting | Default | Effect | +|---|---|---| +| Place name | Seoul | The label on the card. | +| Latitude | 37.57 | Where to read the weather. | +| Longitude | 126.98 | Where to read the weather. | + +The place name is only a label; the latitude and longitude decide the forecast. + +## Permissions and refresh + +The only permission is HTTPS access to `api.open-meteo.com`. The widget +refreshes every 15 minutes while visible. Clicking it opens the Weather app.