fix: address swiftlint warnings

This commit is contained in:
Peter Steinberger
2025-12-24 15:10:22 +01:00
parent c5ede3f167
commit 8d34e54dc5
2 changed files with 280 additions and 177 deletions

View File

@@ -257,54 +257,37 @@ enum CritterIconRenderer {
private struct Canvas { private struct Canvas {
let w: CGFloat let w: CGFloat
let h: CGFloat let h: CGFloat
let stepX: CGFloat
let stepY: CGFloat
let snapX: (CGFloat) -> CGFloat let snapX: (CGFloat) -> CGFloat
let snapY: (CGFloat) -> CGFloat let snapY: (CGFloat) -> CGFloat
let context: CGContext let context: CGContext
} }
static func makeIcon( private struct Geometry {
blink: CGFloat, let bodyRect: CGRect
legWiggle: CGFloat = 0, let bodyCorner: CGFloat
earWiggle: CGFloat = 0, let leftEarRect: CGRect
earScale: CGFloat = 1, let rightEarRect: CGRect
earHoles: Bool = false, let earCorner: CGFloat
eyesClosedLines: Bool = false, let earW: CGFloat
badge: Badge? = nil) -> NSImage let earH: CGFloat
{ let legW: CGFloat
// Force a 36×36px backing store (2× for the 18pt logical canvas) so the menu bar icon stays crisp on Retina. let legH: CGFloat
let pixelsWide = 36 let legSpacing: CGFloat
let pixelsHigh = 36 let legStartX: CGFloat
guard let rep = NSBitmapImageRep( let legYBase: CGFloat
bitmapDataPlanes: nil, let legLift: CGFloat
pixelsWide: pixelsWide, let legHeightScale: CGFloat
pixelsHigh: pixelsHigh, let eyeW: CGFloat
bitsPerSample: 8, let eyeY: CGFloat
samplesPerPixel: 4, let eyeOffset: CGFloat
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bitmapFormat: [],
bytesPerRow: 0,
bitsPerPixel: 0)
else {
return NSImage(size: self.size)
}
rep.size = self.size
NSGraphicsContext.saveGraphicsState() init(canvas: Canvas, legWiggle: CGFloat, earWiggle: CGFloat, earScale: CGFloat) {
if let context = NSGraphicsContext(bitmapImageRep: rep) { let w = canvas.w
NSGraphicsContext.current = context let h = canvas.h
context.imageInterpolation = .none let snapX = canvas.snapX
context.cgContext.setShouldAntialias(false) let snapY = canvas.snapY
defer { NSGraphicsContext.restoreGraphicsState() }
let stepX = self.size.width / max(CGFloat(rep.pixelsWide), 1)
let stepY = self.size.height / max(CGFloat(rep.pixelsHigh), 1)
let snapX: (CGFloat) -> CGFloat = { ($0 / stepX).rounded() * stepX }
let snapY: (CGFloat) -> CGFloat = { ($0 / stepY).rounded() * stepY }
let w = snapX(size.width)
let h = snapY(size.height)
let bodyW = snapX(w * 0.78) let bodyW = snapX(w * 0.78)
let bodyH = snapY(h * 0.58) let bodyH = snapY(h * 0.58)
@@ -333,133 +316,77 @@ enum CritterIconRenderer {
let legStartX = snapX((w - legsWidth) / 2) let legStartX = snapX((w - legsWidth) / 2)
let legLift = snapY(legH * 0.35 * legWiggle) let legLift = snapY(legH * 0.35 * legWiggle)
let legYBase = snapY(bodyY - legH + h * 0.05) let legYBase = snapY(bodyY - legH + h * 0.05)
let legHeightScale = 1 - 0.12 * legWiggle
let eyeW = snapX(bodyW * 0.2) let eyeW = snapX(bodyW * 0.2)
let eyeY = snapY(bodyY + bodyH * 0.56) let eyeY = snapY(bodyY + bodyH * 0.56)
let eyeOffset = snapX(bodyW * 0.24) let eyeOffset = snapX(bodyW * 0.24)
context.cgContext.setFillColor(NSColor.labelColor.cgColor) self.bodyRect = CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH)
self.bodyCorner = bodyCorner
self.leftEarRect = leftEarRect
self.rightEarRect = rightEarRect
self.earCorner = earCorner
self.earW = earW
self.earH = earH
self.legW = legW
self.legH = legH
self.legSpacing = legSpacing
self.legStartX = legStartX
self.legYBase = legYBase
self.legLift = legLift
self.legHeightScale = legHeightScale
self.eyeW = eyeW
self.eyeY = eyeY
self.eyeOffset = eyeOffset
}
}
context.cgContext.addPath(CGPath( private struct FaceOptions {
roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH), let blink: CGFloat
cornerWidth: bodyCorner, let earHoles: Bool
cornerHeight: bodyCorner, let earScale: CGFloat
transform: nil)) let eyesClosedLines: Bool
context.cgContext.addPath(CGPath( }
roundedRect: leftEarRect,
cornerWidth: earCorner,
cornerHeight: earCorner,
transform: nil))
context.cgContext.addPath(CGPath(
roundedRect: rightEarRect,
cornerWidth: earCorner,
cornerHeight: earCorner,
transform: nil))
for i in 0..<4 {
let x = legStartX + CGFloat(i) * (legW + legSpacing)
let lift = (i % 2 == 0 ? legLift : -legLift)
let rect = CGRect(
x: x,
y: legYBase + lift,
width: legW,
height: legH * (1 - 0.12 * legWiggle))
context.cgContext.addPath(CGPath(
roundedRect: rect,
cornerWidth: legW * 0.34,
cornerHeight: legW * 0.34,
transform: nil))
}
context.cgContext.fillPath()
context.cgContext.saveGState() static func makeIcon(
context.cgContext.setBlendMode(CGBlendMode.clear) blink: CGFloat,
legWiggle: CGFloat = 0,
let leftCenter = CGPoint(x: snapX(w / 2 - eyeOffset), y: snapY(eyeY)) earWiggle: CGFloat = 0,
let rightCenter = CGPoint(x: snapX(w / 2 + eyeOffset), y: snapY(eyeY)) earScale: CGFloat = 1,
earHoles: Bool = false,
if earHoles || earScale > 1.05 { eyesClosedLines: Bool = false,
let holeW = snapX(earW * 0.6) badge: Badge? = nil) -> NSImage
let holeH = snapY(earH * 0.46) {
let holeCorner = snapX(holeW * 0.34) guard let rep = self.makeBitmapRep() else {
let leftHoleRect = CGRect(
x: snapX(leftEarRect.midX - holeW / 2),
y: snapY(leftEarRect.midY - holeH / 2 + earH * 0.04),
width: holeW,
height: holeH)
let rightHoleRect = CGRect(
x: snapX(rightEarRect.midX - holeW / 2),
y: snapY(rightEarRect.midY - holeH / 2 + earH * 0.04),
width: holeW,
height: holeH)
context.cgContext.addPath(CGPath(
roundedRect: leftHoleRect,
cornerWidth: holeCorner,
cornerHeight: holeCorner,
transform: nil))
context.cgContext.addPath(CGPath(
roundedRect: rightHoleRect,
cornerWidth: holeCorner,
cornerHeight: holeCorner,
transform: nil))
}
if eyesClosedLines {
let lineW = snapX(eyeW * 0.95)
let lineH = snapY(max(stepY * 2, bodyH * 0.06))
let corner = snapX(lineH * 0.6)
let leftRect = CGRect(
x: snapX(leftCenter.x - lineW / 2),
y: snapY(leftCenter.y - lineH / 2),
width: lineW,
height: lineH)
let rightRect = CGRect(
x: snapX(rightCenter.x - lineW / 2),
y: snapY(rightCenter.y - lineH / 2),
width: lineW,
height: lineH)
context.cgContext.addPath(CGPath(
roundedRect: leftRect,
cornerWidth: corner,
cornerHeight: corner,
transform: nil))
context.cgContext.addPath(CGPath(
roundedRect: rightRect,
cornerWidth: corner,
cornerHeight: corner,
transform: nil))
} else {
let eyeOpen = max(0.05, 1 - blink)
let eyeH = snapY(bodyH * 0.26 * eyeOpen)
let left = CGMutablePath()
left.move(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y - eyeH)))
left.addLine(to: CGPoint(x: snapX(leftCenter.x + eyeW / 2), y: snapY(leftCenter.y)))
left.addLine(to: CGPoint(x: snapX(leftCenter.x - eyeW / 2), y: snapY(leftCenter.y + eyeH)))
left.closeSubpath()
let right = CGMutablePath()
right.move(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y - eyeH)))
right.addLine(to: CGPoint(x: snapX(rightCenter.x - eyeW / 2), y: snapY(rightCenter.y)))
right.addLine(to: CGPoint(x: snapX(rightCenter.x + eyeW / 2), y: snapY(rightCenter.y + eyeH)))
right.closeSubpath()
context.cgContext.addPath(left)
context.cgContext.addPath(right)
}
context.cgContext.fillPath()
context.cgContext.restoreGState()
if let badge {
self.drawBadge(
badge,
canvas: Canvas(w: w, h: h, snapX: snapX, snapY: snapY, context: context.cgContext))
}
} else {
NSGraphicsContext.restoreGraphicsState()
return NSImage(size: self.size) return NSImage(size: self.size)
} }
rep.size = self.size
NSGraphicsContext.saveGraphicsState()
defer { NSGraphicsContext.restoreGraphicsState() }
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
return NSImage(size: self.size)
}
NSGraphicsContext.current = context
context.imageInterpolation = .none
context.cgContext.setShouldAntialias(false)
let canvas = self.makeCanvas(for: rep, context: context)
let geometry = Geometry(canvas: canvas, legWiggle: legWiggle, earWiggle: earWiggle, earScale: earScale)
self.drawBody(in: canvas, geometry: geometry)
let face = FaceOptions(
blink: blink,
earHoles: earHoles,
earScale: earScale,
eyesClosedLines: eyesClosedLines)
self.drawFace(in: canvas, geometry: geometry, options: face)
if let badge {
self.drawBadge(badge, canvas: canvas)
}
let image = NSImage(size: size) let image = NSImage(size: size)
image.addRepresentation(rep) image.addRepresentation(rep)
@@ -467,6 +394,181 @@ enum CritterIconRenderer {
return image return image
} }
private static func makeBitmapRep() -> NSBitmapImageRep? {
// Force a 36×36px backing store (2× for the 18pt logical canvas) so the menu bar icon stays crisp on Retina.
let pixelsWide = 36
let pixelsHigh = 36
return NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: pixelsWide,
pixelsHigh: pixelsHigh,
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bitmapFormat: [],
bytesPerRow: 0,
bitsPerPixel: 0)
}
private static func makeCanvas(for rep: NSBitmapImageRep, context: NSGraphicsContext) -> Canvas {
let stepX = self.size.width / max(CGFloat(rep.pixelsWide), 1)
let stepY = self.size.height / max(CGFloat(rep.pixelsHigh), 1)
let snapX: (CGFloat) -> CGFloat = { ($0 / stepX).rounded() * stepX }
let snapY: (CGFloat) -> CGFloat = { ($0 / stepY).rounded() * stepY }
let w = snapX(size.width)
let h = snapY(size.height)
return Canvas(
w: w,
h: h,
stepX: stepX,
stepY: stepY,
snapX: snapX,
snapY: snapY,
context: context.cgContext)
}
private static func drawBody(in canvas: Canvas, geometry: Geometry) {
canvas.context.setFillColor(NSColor.labelColor.cgColor)
canvas.context.addPath(CGPath(
roundedRect: geometry.bodyRect,
cornerWidth: geometry.bodyCorner,
cornerHeight: geometry.bodyCorner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: geometry.leftEarRect,
cornerWidth: geometry.earCorner,
cornerHeight: geometry.earCorner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: geometry.rightEarRect,
cornerWidth: geometry.earCorner,
cornerHeight: geometry.earCorner,
transform: nil))
for i in 0..<4 {
let x = geometry.legStartX + CGFloat(i) * (geometry.legW + geometry.legSpacing)
let lift = i % 2 == 0 ? geometry.legLift : -geometry.legLift
let rect = CGRect(
x: x,
y: geometry.legYBase + lift,
width: geometry.legW,
height: geometry.legH * geometry.legHeightScale)
canvas.context.addPath(CGPath(
roundedRect: rect,
cornerWidth: geometry.legW * 0.34,
cornerHeight: geometry.legW * 0.34,
transform: nil))
}
canvas.context.fillPath()
}
private static func drawFace(
in canvas: Canvas,
geometry: Geometry,
options: FaceOptions)
{
canvas.context.saveGState()
canvas.context.setBlendMode(.clear)
let leftCenter = CGPoint(
x: canvas.snapX(canvas.w / 2 - geometry.eyeOffset),
y: canvas.snapY(geometry.eyeY))
let rightCenter = CGPoint(
x: canvas.snapX(canvas.w / 2 + geometry.eyeOffset),
y: canvas.snapY(geometry.eyeY))
if options.earHoles || options.earScale > 1.05 {
let holeW = canvas.snapX(geometry.earW * 0.6)
let holeH = canvas.snapY(geometry.earH * 0.46)
let holeCorner = canvas.snapX(holeW * 0.34)
let leftHoleRect = CGRect(
x: canvas.snapX(geometry.leftEarRect.midX - holeW / 2),
y: canvas.snapY(geometry.leftEarRect.midY - holeH / 2 + geometry.earH * 0.04),
width: holeW,
height: holeH)
let rightHoleRect = CGRect(
x: canvas.snapX(geometry.rightEarRect.midX - holeW / 2),
y: canvas.snapY(geometry.rightEarRect.midY - holeH / 2 + geometry.earH * 0.04),
width: holeW,
height: holeH)
canvas.context.addPath(CGPath(
roundedRect: leftHoleRect,
cornerWidth: holeCorner,
cornerHeight: holeCorner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: rightHoleRect,
cornerWidth: holeCorner,
cornerHeight: holeCorner,
transform: nil))
}
if options.eyesClosedLines {
let lineW = canvas.snapX(geometry.eyeW * 0.95)
let lineH = canvas.snapY(max(canvas.stepY * 2, geometry.bodyRect.height * 0.06))
let corner = canvas.snapX(lineH * 0.6)
let leftRect = CGRect(
x: canvas.snapX(leftCenter.x - lineW / 2),
y: canvas.snapY(leftCenter.y - lineH / 2),
width: lineW,
height: lineH)
let rightRect = CGRect(
x: canvas.snapX(rightCenter.x - lineW / 2),
y: canvas.snapY(rightCenter.y - lineH / 2),
width: lineW,
height: lineH)
canvas.context.addPath(CGPath(
roundedRect: leftRect,
cornerWidth: corner,
cornerHeight: corner,
transform: nil))
canvas.context.addPath(CGPath(
roundedRect: rightRect,
cornerWidth: corner,
cornerHeight: corner,
transform: nil))
} else {
let eyeOpen = max(0.05, 1 - options.blink)
let eyeH = canvas.snapY(geometry.bodyRect.height * 0.26 * eyeOpen)
let left = CGMutablePath()
left.move(to: CGPoint(
x: canvas.snapX(leftCenter.x - geometry.eyeW / 2),
y: canvas.snapY(leftCenter.y - eyeH)))
left.addLine(to: CGPoint(
x: canvas.snapX(leftCenter.x + geometry.eyeW / 2),
y: canvas.snapY(leftCenter.y)))
left.addLine(to: CGPoint(
x: canvas.snapX(leftCenter.x - geometry.eyeW / 2),
y: canvas.snapY(leftCenter.y + eyeH)))
left.closeSubpath()
let right = CGMutablePath()
right.move(to: CGPoint(
x: canvas.snapX(rightCenter.x + geometry.eyeW / 2),
y: canvas.snapY(rightCenter.y - eyeH)))
right.addLine(to: CGPoint(
x: canvas.snapX(rightCenter.x - geometry.eyeW / 2),
y: canvas.snapY(rightCenter.y)))
right.addLine(to: CGPoint(
x: canvas.snapX(rightCenter.x + geometry.eyeW / 2),
y: canvas.snapY(rightCenter.y + eyeH)))
right.closeSubpath()
canvas.context.addPath(left)
canvas.context.addPath(right)
}
canvas.context.fillPath()
canvas.context.restoreGState()
}
private static func drawBadge(_ badge: Badge, canvas: Canvas) { private static func drawBadge(_ badge: Badge, canvas: Canvas) {
let strength: CGFloat = switch badge.prominence { let strength: CGFloat = switch badge.prominence {
case .primary: 1.0 case .primary: 1.0

View File

@@ -72,6 +72,14 @@ actor VoiceWakeRuntime {
let sendChime: VoiceWakeChime let sendChime: VoiceWakeChime
} }
private struct RecognitionUpdate {
let transcript: String?
let segments: [WakeWordSegment]
let isFinal: Bool
let error: Error?
let generation: Int
}
func refresh(state: AppState) async { func refresh(state: AppState) async {
let snapshot = await MainActor.run { () -> (Bool, RuntimeConfig) in let snapshot = await MainActor.run { () -> (Bool, RuntimeConfig) in
let enabled = state.swabbleEnabled let enabled = state.swabbleEnabled
@@ -154,13 +162,13 @@ actor VoiceWakeRuntime {
.map { WakeWordSpeechSegments.from(transcription: result.bestTranscription, transcript: $0) } .map { WakeWordSpeechSegments.from(transcription: result.bestTranscription, transcript: $0) }
} ?? [] } ?? []
let isFinal = result?.isFinal ?? false let isFinal = result?.isFinal ?? false
Task { await self.handleRecognition( let update = RecognitionUpdate(
transcript: transcript, transcript: transcript,
segments: segments, segments: segments,
isFinal: isFinal, isFinal: isFinal,
error: error, error: error,
config: config, generation: generation)
generation: generation) } Task { await self.handleRecognition(update, config: config) }
} }
self.logger.info("voicewake runtime started") self.logger.info("voicewake runtime started")
@@ -212,22 +220,15 @@ actor VoiceWakeRuntime {
self.recognizer = SFSpeechRecognizer(locale: locale) self.recognizer = SFSpeechRecognizer(locale: locale)
} }
private func handleRecognition( private func handleRecognition(_ update: RecognitionUpdate, config: RuntimeConfig) async {
transcript: String?, if update.generation != self.recognitionGeneration {
segments: [WakeWordSegment],
isFinal: Bool,
error: Error?,
config: RuntimeConfig,
generation: Int) async
{
if generation != self.recognitionGeneration {
return // stale callback from a superseded recognizer session return // stale callback from a superseded recognizer session
} }
if let error { if let error = update.error {
self.logger.debug("voicewake recognition error: \(error.localizedDescription, privacy: .public)") self.logger.debug("voicewake recognition error: \(error.localizedDescription, privacy: .public)")
} }
guard let transcript else { return } guard let transcript = update.transcript else { return }
let now = Date() let now = Date()
if !transcript.isEmpty { if !transcript.isEmpty {
@@ -240,7 +241,7 @@ actor VoiceWakeRuntime {
triggers: config.triggers) triggers: config.triggers)
self.capturedTranscript = trimmed self.capturedTranscript = trimmed
self.updateHeardBeyondTrigger(withTrimmed: trimmed) self.updateHeardBeyondTrigger(withTrimmed: trimmed)
if isFinal { if update.isFinal {
self.committedTranscript = trimmed self.committedTranscript = trimmed
self.volatileTranscript = "" self.volatileTranscript = ""
} else { } else {
@@ -250,7 +251,7 @@ actor VoiceWakeRuntime {
let attributed = Self.makeAttributed( let attributed = Self.makeAttributed(
committed: self.committedTranscript, committed: self.committedTranscript,
volatile: self.volatileTranscript, volatile: self.volatileTranscript,
isFinal: isFinal) isFinal: update.isFinal)
let snapshot = self.committedTranscript + self.volatileTranscript let snapshot = self.committedTranscript + self.volatileTranscript
if let token = self.overlayToken { if let token = self.overlayToken {
await MainActor.run { await MainActor.run {
@@ -266,7 +267,7 @@ actor VoiceWakeRuntime {
if self.isCapturing { return } if self.isCapturing { return }
let gateConfig = WakeWordGateConfig(triggers: config.triggers) let gateConfig = WakeWordGateConfig(triggers: config.triggers)
if let match = WakeWordGate.match(transcript: transcript, segments: segments, config: gateConfig) { if let match = WakeWordGate.match(transcript: transcript, segments: update.segments, config: gateConfig) {
if let cooldown = cooldownUntil, now < cooldown { if let cooldown = cooldownUntil, now < cooldown {
return return
} }