@opentray/ext-badge-darwin-arm64 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/app/Info.plist +32 -0
- package/app/main.swift +190 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @opentray/ext-badge-darwin-arm64
|
|
2
|
+
|
|
3
|
+
macOS arm64 Dock helper app package for `@opentray/ext-badge`.
|
|
4
|
+
|
|
5
|
+
This package owns the Dock-facing helper bundle used to present badge state with a real
|
|
6
|
+
application identity. The helper is intentionally separate from the broker process.
|
|
7
|
+
It currently projects badge text, overlay, attention, and click/quit lifecycle only; progress is not a Dock projection on this helper.
|
|
8
|
+
|
|
9
|
+
## Build
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bash scripts/release/build-badge-dock-helper.sh /tmp/OpenTrayBadgeHelper.app.zip
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Runtime knobs
|
|
16
|
+
|
|
17
|
+
- `OPENTRAY_BADGE_DOCK_TITLE`
|
|
18
|
+
- `OPENTRAY_BADGE_DOCK_BADGE`
|
|
19
|
+
- `OPENTRAY_BADGE_DOCK_ICON_NAME`
|
|
20
|
+
- `OPENTRAY_BADGE_DOCK_ICON_PATH`
|
|
21
|
+
- `OPENTRAY_BADGE_DOCK_CLICK_SIGNAL`
|
|
22
|
+
- `OPENTRAY_BADGE_DOCK_LOG`
|
|
23
|
+
|
|
24
|
+
The helper stays source-only in git. The app bundle is staged locally or in release jobs.
|
package/app/Info.plist
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
+
<key>CFBundleExecutable</key>
|
|
8
|
+
<string>OpenTrayBadgeHelper</string>
|
|
9
|
+
<key>CFBundleIconFile</key>
|
|
10
|
+
<string>AppIcon</string>
|
|
11
|
+
<key>CFBundleIdentifier</key>
|
|
12
|
+
<string>com.opentray.ext-badge.helper</string>
|
|
13
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
14
|
+
<string>6.0</string>
|
|
15
|
+
<key>CFBundleName</key>
|
|
16
|
+
<string>OpenTray Badge Dock Helper</string>
|
|
17
|
+
<key>CFBundleDisplayName</key>
|
|
18
|
+
<string>OpenTray Badge</string>
|
|
19
|
+
<key>CFBundlePackageType</key>
|
|
20
|
+
<string>APPL</string>
|
|
21
|
+
<key>CFBundleShortVersionString</key>
|
|
22
|
+
<string>1.0</string>
|
|
23
|
+
<key>CFBundleVersion</key>
|
|
24
|
+
<string>1</string>
|
|
25
|
+
<key>LSMinimumSystemVersion</key>
|
|
26
|
+
<string>13.0</string>
|
|
27
|
+
<key>NSHighResolutionCapable</key>
|
|
28
|
+
<true/>
|
|
29
|
+
<key>NSPrincipalClass</key>
|
|
30
|
+
<string>NSApplication</string>
|
|
31
|
+
</dict>
|
|
32
|
+
</plist>
|
package/app/main.swift
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
final class BadgeDockHelper: NSObject, NSApplicationDelegate {
|
|
5
|
+
private let dockTitle = ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_TITLE"]
|
|
6
|
+
?? "OpenTray Badge"
|
|
7
|
+
private let dockIconName = ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_ICON_NAME"]
|
|
8
|
+
?? "bell.badge"
|
|
9
|
+
private let dockIconPath = ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_ICON_PATH"]
|
|
10
|
+
private let helperLog = argumentValue("--log-path") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_LOG"]
|
|
11
|
+
private let clickSignalPath = argumentValue("--click-signal") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_CLICK_SIGNAL"]
|
|
12
|
+
private let quitSignalPath = argumentValue("--quit-signal") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_QUIT_SIGNAL"]
|
|
13
|
+
private let quitNotifyPath = argumentValue("--quit-notify") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_QUIT_NOTIFY"]
|
|
14
|
+
private let stateDir = argumentValue("--state-dir") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_STATE_DIR"]
|
|
15
|
+
private let stateBadgePath = argumentValue("--badge-path") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_BADGE_PATH"]
|
|
16
|
+
private let stateTitlePath = argumentValue("--title-path") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_TITLE_PATH"]
|
|
17
|
+
private let stateIconNamePath = argumentValue("--icon-path") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_ICON_NAME_PATH"]
|
|
18
|
+
private let forceMultiOpen = CommandLine.arguments.contains("--force-multi-open")
|
|
19
|
+
private var lifecycleTimer: Timer?
|
|
20
|
+
private var stateTimer: Timer?
|
|
21
|
+
private var cachedDockTitle: String
|
|
22
|
+
private var cachedDockBadge: String?
|
|
23
|
+
private var cachedDockIconName: String
|
|
24
|
+
private var cachedDockIconPath: String?
|
|
25
|
+
private var renderedSignature: String = ""
|
|
26
|
+
|
|
27
|
+
override init() {
|
|
28
|
+
self.cachedDockTitle = argumentValue("--title") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_TITLE"] ?? "OpenTray Badge"
|
|
29
|
+
self.cachedDockBadge = argumentValue("--badge") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_BADGE"]
|
|
30
|
+
self.cachedDockIconName = argumentValue("--icon-name") ?? ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_ICON_NAME"] ?? "bell.badge"
|
|
31
|
+
self.cachedDockIconPath = ProcessInfo.processInfo.environment["OPENTRAY_BADGE_DOCK_ICON_PATH"]
|
|
32
|
+
super.init()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
36
|
+
NSApplication.shared.setActivationPolicy(.regular)
|
|
37
|
+
NSApplication.shared.applicationIconImage = placeholderDockIcon()
|
|
38
|
+
applyDockBadge(cachedDockBadge)
|
|
39
|
+
log("launched title=\(cachedDockTitle)")
|
|
40
|
+
startLifecycleWatch()
|
|
41
|
+
startStateWatch()
|
|
42
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
|
46
|
+
log("dock-reopen")
|
|
47
|
+
signalDockClick()
|
|
48
|
+
sender.activate(ignoringOtherApps: true)
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
53
|
+
signalDockQuitNotify()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func resolveDockIcon() -> NSImage? {
|
|
57
|
+
if let cachedDockIconPath, !cachedDockIconPath.isEmpty {
|
|
58
|
+
return NSImage(contentsOfFile: cachedDockIconPath)
|
|
59
|
+
}
|
|
60
|
+
return NSImage(systemSymbolName: cachedDockIconName, accessibilityDescription: cachedDockTitle)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func applyDockBadge(_ badge: String?) {
|
|
64
|
+
guard let badge, !badge.isEmpty else {
|
|
65
|
+
NSApp.dockTile.badgeLabel = nil
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
NSApp.dockTile.badgeLabel = badge
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func signalDockClick() {
|
|
72
|
+
guard let clickSignalPath, !clickSignalPath.isEmpty else {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
let payload = "\(Date().timeIntervalSince1970)\n"
|
|
76
|
+
try? payload.write(toFile: clickSignalPath, atomically: true, encoding: .utf8)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private func startLifecycleWatch() {
|
|
80
|
+
guard let quitSignalPath, !quitSignalPath.isEmpty else {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if forceMultiOpen {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
lifecycleTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] timer in
|
|
87
|
+
guard let self else {
|
|
88
|
+
timer.invalidate()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if FileManager.default.fileExists(atPath: quitSignalPath) {
|
|
92
|
+
timer.invalidate()
|
|
93
|
+
NSApp.terminate(self)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func startStateWatch() {
|
|
99
|
+
guard let stateDir, !stateDir.isEmpty else {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
stateTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
103
|
+
self?.refreshDockState()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private func refreshDockState() {
|
|
108
|
+
var changed = false
|
|
109
|
+
if let stateBadgePath, let badge = readTrimmedFile(stateBadgePath) {
|
|
110
|
+
cachedDockBadge = badge.isEmpty ? nil : badge
|
|
111
|
+
changed = true
|
|
112
|
+
}
|
|
113
|
+
if let stateTitlePath, let title = readTrimmedFile(stateTitlePath), !title.isEmpty {
|
|
114
|
+
cachedDockTitle = title
|
|
115
|
+
changed = true
|
|
116
|
+
}
|
|
117
|
+
if let stateIconNamePath, let iconName = readTrimmedFile(stateIconNamePath), !iconName.isEmpty {
|
|
118
|
+
cachedDockIconName = iconName
|
|
119
|
+
changed = true
|
|
120
|
+
}
|
|
121
|
+
if changed {
|
|
122
|
+
updateDockTile()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func readTrimmedFile(_ path: String) -> String? {
|
|
127
|
+
guard FileManager.default.fileExists(atPath: path) else {
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
return (try? String(contentsOfFile: path, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func updateDockTile() {
|
|
134
|
+
let signature = [cachedDockTitle, cachedDockBadge ?? "", cachedDockIconName, cachedDockIconPath ?? ""].joined(separator: "|")
|
|
135
|
+
guard signature != renderedSignature else {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
renderedSignature = signature
|
|
139
|
+
applyDockBadge(cachedDockBadge)
|
|
140
|
+
NSApplication.shared.applicationIconImage = resolveDockIcon() ?? placeholderDockIcon()
|
|
141
|
+
NSApplication.shared.dockTile.display()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private func placeholderDockIcon() -> NSImage {
|
|
145
|
+
let image = NSImage(size: NSSize(width: 64, height: 64))
|
|
146
|
+
image.lockFocus()
|
|
147
|
+
NSColor.clear.set()
|
|
148
|
+
NSBezierPath(rect: NSRect(x: 0, y: 0, width: 64, height: 64)).fill()
|
|
149
|
+
image.unlockFocus()
|
|
150
|
+
return image
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func signalDockQuitNotify() {
|
|
154
|
+
guard let quitNotifyPath, !quitNotifyPath.isEmpty else {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
let payload = "\(Date().timeIntervalSince1970)\n"
|
|
158
|
+
try? payload.write(toFile: quitNotifyPath, atomically: true, encoding: .utf8)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func log(_ message: String) {
|
|
162
|
+
NSLog("OpenTray badge helper: %@", message)
|
|
163
|
+
if let helperLog {
|
|
164
|
+
try? "\(message)\n".appendToFile(atPath: helperLog)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private extension String {
|
|
170
|
+
func appendToFile(atPath path: String) throws {
|
|
171
|
+
let url = URL(fileURLWithPath: path)
|
|
172
|
+
let existing = (try? String(contentsOf: url, encoding: .utf8)) ?? ""
|
|
173
|
+
try (existing + self).write(to: url, atomically: true, encoding: .utf8)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private func argumentValue(_ name: String) -> String? {
|
|
178
|
+
let arguments = CommandLine.arguments
|
|
179
|
+
for index in 0..<arguments.count {
|
|
180
|
+
if arguments[index] == name, index + 1 < arguments.count {
|
|
181
|
+
return arguments[index + 1]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return nil
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let delegate = BadgeDockHelper()
|
|
188
|
+
NSApplication.shared.delegate = delegate
|
|
189
|
+
NSApplication.shared.setActivationPolicy(.regular)
|
|
190
|
+
NSApplication.shared.run()
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opentray/ext-badge-darwin-arm64",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTray badge Dock helper app for macOS arm64.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/jixoai/opentray"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"README.md",
|
|
12
|
+
"app"
|
|
13
|
+
],
|
|
14
|
+
"os": [
|
|
15
|
+
"darwin"
|
|
16
|
+
],
|
|
17
|
+
"cpu": [
|
|
18
|
+
"arm64"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"opentray": ">=0.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|