Initial release of Pommedoro

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leopere 2026-02-06 16:14:17 -05:00
parent cc23058b1d
commit b170202033
Signed by: colin
SSH Key Fingerprint: SHA256:nRPCQTeMFLdGytxRQmPVK9VXY3/ePKQ5lGRyJhT5DY8
11 changed files with 383 additions and 17 deletions

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
# Swift / SPM build
.build/ .build/
# macOS
.DS_Store .DS_Store
*.o
Package.resolved

View File

@ -83,6 +83,8 @@ dmg: bundle
@mkdir -p $(BUILD_DIR)/dmg-staging @mkdir -p $(BUILD_DIR)/dmg-staging
@cp -R $(APP_BUNDLE) $(BUILD_DIR)/dmg-staging/ @cp -R $(APP_BUNDLE) $(BUILD_DIR)/dmg-staging/
@ln -s /Applications $(BUILD_DIR)/dmg-staging/Applications @ln -s /Applications $(BUILD_DIR)/dmg-staging/Applications
@cp Resources/$(APP_NAME).icns $(BUILD_DIR)/dmg-staging/.VolumeIcon.icns
@SetFile -a C $(BUILD_DIR)/dmg-staging
@hdiutil create -volname "$(APP_NAME)" \ @hdiutil create -volname "$(APP_NAME)" \
-srcfolder $(BUILD_DIR)/dmg-staging \ -srcfolder $(BUILD_DIR)/dmg-staging \
-ov -format UDZO \ -ov -format UDZO \

14
Package.resolved Normal file
View File

@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "sentry-cocoa",
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa.git",
"state" : {
"revision" : "16cd512711375fa73f25ae5e373f596bdf4251ae",
"version" : "8.58.0"
}
}
],
"version" : 2
}

View File

@ -13,6 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var statusMenuItem: NSMenuItem! var statusMenuItem: NSMenuItem!
var debugMenuItem: NSMenuItem! var debugMenuItem: NSMenuItem!
var launchAtLoginMenuItem: NSMenuItem! var launchAtLoginMenuItem: NSMenuItem!
var settingsMenuBuilder: SettingsMenuBuilder!
// Overlay windows // Overlay windows
var overlayWindows: [NSWindow] = [] var overlayWindows: [NSWindow] = []
@ -38,6 +39,23 @@ class AppDelegate: NSObject, NSApplicationDelegate {
startCountdown() startCountdown()
NotificationManager.shared.requestPermission() NotificationManager.shared.requestPermission()
NotificationManager.shared.postCycleStarted(durationMinutes: workTimerDuration / 60) NotificationManager.shared.postCycleStarted(durationMinutes: workTimerDuration / 60)
NotificationCenter.default.addObserver(
self, selector: #selector(settingsDidChange),
name: Settings.didChangeNotification, object: nil
)
}
@objc private func settingsDidChange() {
let color = Settings.shared.gradientColor
for label in countdownLabels {
label.textColor = color
}
for window in overlayWindows {
guard let view = window.contentView as? EdgeGradientView else { continue }
view.color = color
view.needsDisplay = true
}
} }
func startCountdown() { func startCountdown() {
@ -53,7 +71,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let s = self.remainingSeconds let s = self.remainingSeconds
if s > 60 { if s > 60 {
// 5:00 to 3:00 -> every 60 seconds, 3:00 to 1:00 -> every 30 seconds
let shouldBlink: Bool let shouldBlink: Bool
if s > 180 { if s > 180 {
shouldBlink = (s % 60 == 0) shouldBlink = (s % 60 == 0)
@ -62,7 +79,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
if shouldBlink { self.triggerBlink() } if shouldBlink { self.triggerBlink() }
} else if s > 5 { } else if s > 5 {
// Last minute: same as debug mode escalation
let shouldBlink: Bool let shouldBlink: Bool
if s > 30 { if s > 30 {
shouldBlink = (s % 10 == 0) shouldBlink = (s % 10 == 0)

View File

@ -45,11 +45,15 @@ extension AppDelegate {
} }
} }
private var breakScreenColor: NSColor {
Settings.shared.gradientColor.withAlphaComponent(0.85)
}
func buildSuggestionScreen(window: NSWindow, isPrimary: Bool) { func buildSuggestionScreen(window: NSWindow, isPrimary: Bool) {
let size = window.frame.size let size = window.frame.size
let view = NSView(frame: NSRect(origin: .zero, size: size)) let view = NSView(frame: NSRect(origin: .zero, size: size))
view.wantsLayer = true view.wantsLayer = true
view.layer?.backgroundColor = NSColor.systemTeal.withAlphaComponent(0.85).cgColor view.layer?.backgroundColor = breakScreenColor.cgColor
let countdownLabel = NSTextField(labelWithString: "05:00") let countdownLabel = NSTextField(labelWithString: "05:00")
countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 72, weight: .bold) countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 72, weight: .bold)
@ -128,11 +132,13 @@ extension AppDelegate {
@objc func didSuccess() { @objc func didSuccess() {
breakCountdownLabels.removeAll() breakCountdownLabels.removeAll()
let bgColor = breakScreenColor
for (i, window) in overlayWindows.enumerated() { for (i, window) in overlayWindows.enumerated() {
let size = window.frame.size let size = window.frame.size
let view = NSView(frame: NSRect(origin: .zero, size: size)) let view = NSView(frame: NSRect(origin: .zero, size: size))
view.wantsLayer = true view.wantsLayer = true
view.layer?.backgroundColor = NSColor.systemTeal.withAlphaComponent(0.85).cgColor view.layer?.backgroundColor = bgColor.cgColor
let countdownLabel = NSTextField(labelWithString: String(format: "%02d:%02d", breakRemainingSeconds / 60, breakRemainingSeconds % 60)) let countdownLabel = NSTextField(labelWithString: String(format: "%02d:%02d", breakRemainingSeconds / 60, breakRemainingSeconds % 60))
countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold) countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold)
@ -215,11 +221,13 @@ extension AppDelegate {
@objc func reflectionDone() { @objc func reflectionDone() {
breakCountdownLabels.removeAll() breakCountdownLabels.removeAll()
let bgColor = breakScreenColor
for (i, window) in overlayWindows.enumerated() { for (i, window) in overlayWindows.enumerated() {
let size = window.frame.size let size = window.frame.size
let view = NSView(frame: NSRect(origin: .zero, size: size)) let view = NSView(frame: NSRect(origin: .zero, size: size))
view.wantsLayer = true view.wantsLayer = true
view.layer?.backgroundColor = NSColor.systemTeal.withAlphaComponent(0.85).cgColor view.layer?.backgroundColor = bgColor.cgColor
let countdownLabel = NSTextField(labelWithString: String(format: "%02d:%02d", breakRemainingSeconds / 60, breakRemainingSeconds % 60)) let countdownLabel = NSTextField(labelWithString: String(format: "%02d:%02d", breakRemainingSeconds / 60, breakRemainingSeconds % 60))
countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold) countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold)

View File

@ -2,11 +2,11 @@ import AppKit
class EdgeGradientView: NSView { class EdgeGradientView: NSView {
var intensity: CGFloat = 0.4 var intensity: CGFloat = 0.4
var color: NSColor = Settings.shared.gradientColor
override func draw(_ dirtyRect: NSRect) { override func draw(_ dirtyRect: NSRect) {
guard let ctx = NSGraphicsContext.current?.cgContext else { return } guard let ctx = NSGraphicsContext.current?.cgContext else { return }
let w = bounds.width, h = bounds.height, edge: CGFloat = 220 let w = bounds.width, h = bounds.height, edge: CGFloat = 220
let color = NSColor.systemTeal
let colors = [color.withAlphaComponent(intensity).cgColor, color.withAlphaComponent(0.0).cgColor] as CFArray let colors = [color.withAlphaComponent(intensity).cgColor, color.withAlphaComponent(0.0).cgColor] as CFArray
guard let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: [0, 1]) else { return } guard let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors, locations: [0, 1]) else { return }

View File

@ -52,7 +52,7 @@ extension AppDelegate {
let label = NSTextField(labelWithString: "00:10") let label = NSTextField(labelWithString: "00:10")
label.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold) label.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold)
label.textColor = .systemTeal label.textColor = Settings.shared.gradientColor
label.alignment = .center label.alignment = .center
label.frame = NSRect(x: 0, y: 14, width: pillW, height: 50) label.frame = NSRect(x: 0, y: 14, width: pillW, height: 50)
pillView.addSubview(label) pillView.addSubview(label)
@ -88,30 +88,37 @@ extension AppDelegate {
} }
func triggerBlink() { func triggerBlink() {
let settings = Settings.shared
let scale = CGFloat(settings.intensityScale)
let durationScale = settings.blinkDurationScale
// Intensity scales across the effective countdown window (capped to work timer) // Intensity scales across the effective countdown window (capped to work timer)
let effectiveCountdown = min(countdownDuration, workTimerDuration) let effectiveCountdown = min(countdownDuration, workTimerDuration)
let countdownElapsed = Double(effectiveCountdown - remainingSeconds) let countdownElapsed = Double(effectiveCountdown - remainingSeconds)
let countdownTotal = Double(effectiveCountdown) let countdownTotal = Double(effectiveCountdown)
let progress = min(max(countdownElapsed / countdownTotal, 0.0), 1.0) let progress = min(max(countdownElapsed / countdownTotal, 0.0), 1.0)
let intensity = 0.15 + (0.60 * progress) // 0.15 at start, up to 0.75 at 0:00 let baseIntensity = 0.15 + (0.60 * progress) // 0.15 at start, up to 0.75 at 0:00
let intensity = min(baseIntensity * Double(scale), 1.0)
let fadeIn: Double let fadeIn: Double
let hold: Double let hold: Double
let fadeOut: Double let fadeOut: Double
if remainingSeconds > 60 { if remainingSeconds > 60 {
// 5:00 to 1:00: gentle, slow flashes fadeIn = 1.0 * durationScale; hold = 0.8 * durationScale; fadeOut = 1.0 * durationScale
fadeIn = 1.0; hold = 0.8; fadeOut = 1.0
} else if remainingSeconds > 30 { } else if remainingSeconds > 30 {
fadeIn = 0.8; hold = 0.6; fadeOut = 0.8 fadeIn = 0.8 * durationScale; hold = 0.6 * durationScale; fadeOut = 0.8 * durationScale
} else if remainingSeconds > 10 { } else if remainingSeconds > 10 {
fadeIn = 0.5; hold = 0.4; fadeOut = 0.6 fadeIn = 0.5 * durationScale; hold = 0.4 * durationScale; fadeOut = 0.6 * durationScale
} else { } else {
fadeIn = 0.3; hold = 0.3; fadeOut = 0.4 fadeIn = 0.3 * durationScale; hold = 0.3 * durationScale; fadeOut = 0.4 * durationScale
} }
let color = settings.gradientColor
for window in overlayWindows { for window in overlayWindows {
guard let view = window.contentView as? EdgeGradientView else { continue } guard let view = window.contentView as? EdgeGradientView else { continue }
view.intensity = CGFloat(intensity) view.intensity = CGFloat(intensity)
view.color = color
view.needsDisplay = true view.needsDisplay = true
NSAnimationContext.runAnimationGroup { ctx in NSAnimationContext.runAnimationGroup { ctx in
@ -130,9 +137,14 @@ extension AppDelegate {
func makeSolid() { func makeSolid() {
isSolid = true isSolid = true
let settings = Settings.shared
let maxIntensity = min(0.75 * CGFloat(settings.intensityScale), 1.0)
let color = settings.gradientColor
for window in overlayWindows { for window in overlayWindows {
guard let view = window.contentView as? EdgeGradientView else { continue } guard let view = window.contentView as? EdgeGradientView else { continue }
view.intensity = 0.75 view.intensity = maxIntensity
view.color = color
view.needsDisplay = true view.needsDisplay = true
NSAnimationContext.runAnimationGroup { ctx in NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.3 ctx.duration = 0.3

View File

@ -0,0 +1,97 @@
import AppKit
// MARK: - Settings Keys
private enum SettingsKey {
static let intensityScale = "pommedoro.intensityScale"
static let blinkDurationScale = "pommedoro.blinkDurationScale"
static let gradientColorRed = "pommedoro.gradientColor.red"
static let gradientColorGreen = "pommedoro.gradientColor.green"
static let gradientColorBlue = "pommedoro.gradientColor.blue"
}
// MARK: - Settings Manager
class Settings {
static let shared = Settings()
/// Notification posted when any setting changes
static let didChangeNotification = Notification.Name("PommedoroSettingsDidChange")
private let defaults = UserDefaults.standard
private init() {
registerDefaults()
}
private func registerDefaults() {
// Extract default teal components for consistent defaults
let teal = NSColor.systemTeal.usingColorSpace(.sRGB) ?? NSColor.systemTeal
defaults.register(defaults: [
SettingsKey.intensityScale: 1.0,
SettingsKey.blinkDurationScale: 1.0,
SettingsKey.gradientColorRed: Double(teal.redComponent),
SettingsKey.gradientColorGreen: Double(teal.greenComponent),
SettingsKey.gradientColorBlue: Double(teal.blueComponent),
])
}
/// Intensity multiplier (0.25 to 2.0, default 1.0)
var intensityScale: Double {
get { defaults.double(forKey: SettingsKey.intensityScale) }
set {
defaults.set(newValue.clamped(to: 0.25...2.0), forKey: SettingsKey.intensityScale)
notifyChange()
}
}
/// Blink duration multiplier (0.25 to 3.0, default 1.0)
var blinkDurationScale: Double {
get { defaults.double(forKey: SettingsKey.blinkDurationScale) }
set {
defaults.set(newValue.clamped(to: 0.25...3.0), forKey: SettingsKey.blinkDurationScale)
notifyChange()
}
}
/// Gradient color as NSColor
var gradientColor: NSColor {
get {
NSColor(
srgbRed: CGFloat(defaults.double(forKey: SettingsKey.gradientColorRed)),
green: CGFloat(defaults.double(forKey: SettingsKey.gradientColorGreen)),
blue: CGFloat(defaults.double(forKey: SettingsKey.gradientColorBlue)),
alpha: 1.0
)
}
set {
let c = newValue.usingColorSpace(.sRGB) ?? newValue
defaults.set(Double(c.redComponent), forKey: SettingsKey.gradientColorRed)
defaults.set(Double(c.greenComponent), forKey: SettingsKey.gradientColorGreen)
defaults.set(Double(c.blueComponent), forKey: SettingsKey.gradientColorBlue)
notifyChange()
}
}
func resetToDefaults() {
for key in [
SettingsKey.intensityScale,
SettingsKey.blinkDurationScale,
SettingsKey.gradientColorRed,
SettingsKey.gradientColorGreen,
SettingsKey.gradientColorBlue,
] {
defaults.removeObject(forKey: key)
}
notifyChange()
}
private func notifyChange() {
NotificationCenter.default.post(name: Settings.didChangeNotification, object: nil)
}
}
// MARK: - Comparable Clamping
private extension Comparable {
func clamped(to range: ClosedRange<Self>) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}

View File

@ -0,0 +1,208 @@
import AppKit
// MARK: - Slider Menu Item View
class SliderMenuItemView: NSView {
let slider: NSSlider
let valueLabel: NSTextField
private let titleLabel: NSTextField
var onChanged: ((Double) -> Void)?
init(title: String, value: Double, minValue: Double, maxValue: Double, width: CGFloat = 280) {
titleLabel = NSTextField(labelWithString: title)
slider = NSSlider(value: value, minValue: minValue, maxValue: maxValue,
target: nil, action: nil)
valueLabel = NSTextField(labelWithString: String(format: "%.2fx", value))
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 50))
titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
titleLabel.textColor = .labelColor
titleLabel.frame = NSRect(x: 16, y: 28, width: width - 32, height: 16)
addSubview(titleLabel)
let sliderW = width - 80
slider.frame = NSRect(x: 16, y: 4, width: sliderW, height: 20)
slider.target = self
slider.action = #selector(sliderMoved)
slider.isContinuous = true
addSubview(slider)
valueLabel.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
valueLabel.textColor = .secondaryLabelColor
valueLabel.alignment = .right
valueLabel.frame = NSRect(x: sliderW + 20, y: 4, width: 44, height: 20)
addSubview(valueLabel)
}
required init?(coder: NSCoder) { fatalError() }
@objc private func sliderMoved() {
let v = slider.doubleValue
valueLabel.stringValue = String(format: "%.2fx", v)
onChanged?(v)
}
func update(value: Double) {
slider.doubleValue = value
valueLabel.stringValue = String(format: "%.2fx", value)
}
}
// MARK: - Color Swatch Menu Item View
class ColorSwatchMenuItemView: NSView {
private let titleLabel: NSTextField
private var swatchButtons: [NSButton] = []
private var selectedIndex: Int = -1
var onChanged: ((NSColor) -> Void)?
static let presetColors: [(String, NSColor)] = [
("Teal", .systemTeal),
("Blue", .systemBlue),
("Purple", .systemPurple),
("Pink", .systemPink),
("Red", .systemRed),
("Orange", .systemOrange),
("Yellow", .systemYellow),
("Green", .systemGreen),
("Indigo", .systemIndigo),
]
init(title: String, currentColor: NSColor, width: CGFloat = 280) {
titleLabel = NSTextField(labelWithString: title)
super.init(frame: NSRect(x: 0, y: 0, width: width, height: 56))
titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
titleLabel.textColor = .labelColor
titleLabel.frame = NSRect(x: 16, y: 34, width: width - 32, height: 16)
addSubview(titleLabel)
let swatchSize: CGFloat = 22
let gap: CGFloat = 4
let count = Self.presetColors.count
let totalW = CGFloat(count) * swatchSize + CGFloat(count - 1) * gap
var x = (width - totalW) / 2
for (i, (_, color)) in Self.presetColors.enumerated() {
let btn = NSButton(frame: NSRect(x: x, y: 6, width: swatchSize, height: swatchSize))
btn.wantsLayer = true
btn.isBordered = false
btn.title = ""
btn.layer?.backgroundColor = color.cgColor
btn.layer?.cornerRadius = swatchSize / 2
btn.layer?.borderWidth = 2
btn.layer?.borderColor = NSColor.clear.cgColor
btn.tag = i
btn.target = self
btn.action = #selector(swatchClicked(_:))
addSubview(btn)
swatchButtons.append(btn)
x += swatchSize + gap
}
highlightClosest(to: currentColor)
}
required init?(coder: NSCoder) { fatalError() }
@objc private func swatchClicked(_ sender: NSButton) {
let idx = sender.tag
guard idx >= 0 && idx < Self.presetColors.count else { return }
setSelected(idx)
onChanged?(Self.presetColors[idx].1)
}
private func setSelected(_ index: Int) {
// Clear previous
for btn in swatchButtons {
btn.layer?.borderColor = NSColor.clear.cgColor
}
// Highlight new
if index >= 0 && index < swatchButtons.count {
swatchButtons[index].layer?.borderColor = NSColor.white.cgColor
selectedIndex = index
}
}
func highlightClosest(to target: NSColor) {
let t = target.usingColorSpace(.sRGB) ?? target
var bestIdx = 0
var bestDist: CGFloat = .greatestFiniteMagnitude
for (i, (_, color)) in Self.presetColors.enumerated() {
let c = color.usingColorSpace(.sRGB) ?? color
let dr = t.redComponent - c.redComponent
let dg = t.greenComponent - c.greenComponent
let db = t.blueComponent - c.blueComponent
let dist = dr*dr + dg*dg + db*db
if dist < bestDist { bestDist = dist; bestIdx = i }
}
setSelected(bestIdx)
}
func update(color: NSColor) {
highlightClosest(to: color)
}
}
// MARK: - Settings Menu Builder
class SettingsMenuBuilder {
private var intensityView: SliderMenuItemView?
private var durationView: SliderMenuItemView?
private var colorView: ColorSwatchMenuItemView?
func buildSettingsSubmenu() -> NSMenuItem {
let submenu = NSMenu(title: "Settings")
// Color swatches
let cv = ColorSwatchMenuItemView(title: "Gradient Color:", currentColor: Settings.shared.gradientColor)
cv.onChanged = { color in Settings.shared.gradientColor = color }
colorView = cv
let colorItem = NSMenuItem()
colorItem.view = cv
submenu.addItem(colorItem)
submenu.addItem(NSMenuItem.separator())
// Intensity
let iv = SliderMenuItemView(title: "Intensity", value: Settings.shared.intensityScale,
minValue: 0.25, maxValue: 2.0)
iv.onChanged = { val in Settings.shared.intensityScale = val }
intensityView = iv
let intensityItem = NSMenuItem()
intensityItem.view = iv
submenu.addItem(intensityItem)
submenu.addItem(NSMenuItem.separator())
// Blink Duration
let dv = SliderMenuItemView(title: "Blink Duration", value: Settings.shared.blinkDurationScale,
minValue: 0.25, maxValue: 3.0)
dv.onChanged = { val in Settings.shared.blinkDurationScale = val }
durationView = dv
let durationItem = NSMenuItem()
durationItem.view = dv
submenu.addItem(durationItem)
submenu.addItem(NSMenuItem.separator())
// Reset
let resetItem = NSMenuItem(title: "Reset to Defaults", action: #selector(resetDefaults), keyEquivalent: "")
resetItem.target = self
submenu.addItem(resetItem)
let parentItem = NSMenuItem(title: "Settings", action: nil, keyEquivalent: ",")
parentItem.submenu = submenu
return parentItem
}
func refreshValues() {
let s = Settings.shared
intensityView?.update(value: s.intensityScale)
durationView?.update(value: s.blinkDurationScale)
colorView?.update(color: s.gradientColor)
}
@objc private func resetDefaults() {
Settings.shared.resetToDefaults()
refreshValues()
}
}

View File

@ -17,6 +17,12 @@ extension AppDelegate {
menu.addItem(statusMenuItem) menu.addItem(statusMenuItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Pause", action: #selector(togglePause), keyEquivalent: "p")) menu.addItem(NSMenuItem(title: "Pause", action: #selector(togglePause), keyEquivalent: "p"))
menu.addItem(NSMenuItem.separator())
// Settings submenu with inline sliders and color picker
settingsMenuBuilder = SettingsMenuBuilder()
menu.addItem(settingsMenuBuilder.buildSettingsSubmenu())
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
debugMenuItem = NSMenuItem(title: "Debug Mode", action: #selector(toggleDebugMode), keyEquivalent: "d") debugMenuItem = NSMenuItem(title: "Debug Mode", action: #selector(toggleDebugMode), keyEquivalent: "d")
debugMenuItem.state = isDebugMode ? .on : .off debugMenuItem.state = isDebugMode ? .on : .off
@ -29,8 +35,10 @@ extension AppDelegate {
menu.addItem(NSMenuItem(title: "Quit Pommedoro", action: #selector(closePommedoro), keyEquivalent: "q")) menu.addItem(NSMenuItem(title: "Quit Pommedoro", action: #selector(closePommedoro), keyEquivalent: "q"))
for item in menu.items { for item in menu.items {
if item.target == nil && item.action != nil {
item.target = self item.target = self
} }
}
statusItem.menu = menu statusItem.menu = menu
} }

Binary file not shown.