mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 08:14:29 +00:00
125 lines
4.5 KiB
Swift
125 lines
4.5 KiB
Swift
import AppKit
|
|
|
|
final class StatusView: NSView {
|
|
private let light1 = TrafficLight()
|
|
private let light2 = TrafficLight()
|
|
private let light3 = TrafficLight()
|
|
private let timestampLabel: NSTextField = {
|
|
let tf = NSTextField(labelWithString: "--:--:--")
|
|
tf.textColor = .secondaryLabelColor
|
|
tf.alignment = .left
|
|
tf.font = NSFont.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize(for: .small), weight: .regular)
|
|
tf.setContentHuggingPriority(.required, for: .horizontal)
|
|
tf.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
return tf
|
|
}()
|
|
|
|
private var checker: ServiceChecker?
|
|
private let timeFormatter: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.locale = .current
|
|
df.timeZone = .current
|
|
df.dateFormat = "HH:mm:ss"
|
|
return df
|
|
}()
|
|
private let fullFormatter: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.locale = .current
|
|
df.timeZone = .current
|
|
df.dateStyle = .medium
|
|
df.timeStyle = .medium
|
|
return df
|
|
}()
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
wantsLayer = true
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
let stack = NSStackView(views: [light1, light2, light3, timestampLabel])
|
|
stack.orientation = .horizontal
|
|
stack.alignment = .centerY
|
|
stack.distribution = .equalSpacing
|
|
stack.spacing = 12
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
addSubview(stack)
|
|
NSLayoutConstraint.activate([
|
|
stack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
stack.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
])
|
|
|
|
// Tracking for hover tooltips
|
|
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeAlways, .inVisibleRect], owner: self, userInfo: nil))
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
// Pass all clicks through to apps beneath
|
|
return nil
|
|
}
|
|
|
|
func bindTo(serviceChecker: ServiceChecker) {
|
|
self.checker = serviceChecker
|
|
NotificationCenter.default.addObserver(self, selector: #selector(updateLights), name: ServiceChecker.statusDidChangeNotification, object: serviceChecker)
|
|
updateLights()
|
|
}
|
|
|
|
@objc private func updateLights() {
|
|
guard let c = checker else { return }
|
|
// Light 1: svc.prole.org:443
|
|
light1.state = c.svcReachable ? .green : .red
|
|
light1.toolTip = c.tooltipForSvc()
|
|
|
|
// Light 2: aggregate raspberry + pi
|
|
let count = (c.raspberryReachable ? 1 : 0) + (c.piReachable ? 1 : 0)
|
|
light2.state = count == 2 ? .green : (count == 1 ? .yellow : .red)
|
|
light2.toolTip = c.tooltipForAggregateK3s()
|
|
|
|
// Light 3: localhost k3d
|
|
light3.state = c.localReachable ? .green : .red
|
|
light3.toolTip = c.tooltipForLocal()
|
|
|
|
// Timestamp label
|
|
let now = Date()
|
|
timestampLabel.stringValue = timeFormatter.string(from: now)
|
|
timestampLabel.toolTip = "Last updated: " + fullFormatter.string(from: now)
|
|
needsDisplay = true
|
|
}
|
|
|
|
func aggregateStatusSummary() -> String {
|
|
guard let c = checker else { return "No status" }
|
|
let svc = c.svcReachable ? "svc.prole.org:443 ✓" : "svc.prole.org:443 ✗"
|
|
let agg = "k3s: raspberry \(c.raspberryReachable ? "✓" : "✗"), pi \(c.piReachable ? "✓" : "✗")"
|
|
let loc = c.localReachable ? "k3d localhost:6443 ✓" : "k3d localhost:6443 ✗"
|
|
return [svc, agg, loc].joined(separator: " • ")
|
|
}
|
|
}
|
|
|
|
final class TrafficLight: NSView {
|
|
enum State { case green, yellow, red }
|
|
var state: State = .red { didSet { needsDisplay = true } }
|
|
|
|
override var intrinsicContentSize: NSSize { NSSize(width: 14, height: 14) }
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
super.draw(dirtyRect)
|
|
let rect = bounds.insetBy(dx: 1, dy: 1)
|
|
let path = NSBezierPath(ovalIn: rect)
|
|
let color: NSColor
|
|
switch state {
|
|
case .green: color = NSColor.systemGreen
|
|
case .yellow: color = NSColor.systemYellow
|
|
case .red: color = NSColor.systemRed
|
|
}
|
|
color.setFill()
|
|
path.fill()
|
|
|
|
// subtle ring for better contrast against menu material
|
|
NSColor.black.withAlphaComponent(0.12).setStroke()
|
|
path.lineWidth = 1
|
|
path.stroke()
|
|
}
|
|
}
|