chore(swabble): apply swiftformat

main
Peter Steinberger 2025-12-09 15:36:41 +01:00
parent 336c9d6caa
commit 318457cb2c
13 changed files with 74 additions and 75 deletions

View File

@ -33,5 +33,4 @@ let package = Package(
.product(name: "Testing", package: "swift-testing"), .product(name: "Testing", package: "swift-testing"),
]), ]),
], ],
swiftLanguageModes: [.v6] swiftLanguageModes: [.v6])
)

View File

@ -17,11 +17,11 @@ public actor HookRunner {
public init(config: SwabbleConfig) { public init(config: SwabbleConfig) {
self.config = config self.config = config
self.hostname = Host.current().localizedName ?? "host" hostname = Host.current().localizedName ?? "host"
} }
public func shouldRun() -> Bool { public func shouldRun() -> Bool {
guard self.config.hook.cooldownSeconds > 0 else { return true } guard config.hook.cooldownSeconds > 0 else { return true }
if let lastRun, Date().timeIntervalSince(lastRun) < config.hook.cooldownSeconds { if let lastRun, Date().timeIntervalSince(lastRun) < config.hook.cooldownSeconds {
return false return false
} }
@ -29,23 +29,23 @@ public actor HookRunner {
} }
public func run(job: HookJob) async throws { public func run(job: HookJob) async throws {
guard self.shouldRun() else { return } guard shouldRun() else { return }
guard !self.config.hook.command.isEmpty else { throw NSError( guard !config.hook.command.isEmpty else { throw NSError(
domain: "Hook", domain: "Hook",
code: 1, code: 1,
userInfo: [NSLocalizedDescriptionKey: "hook command not set"]) } userInfo: [NSLocalizedDescriptionKey: "hook command not set"]) }
let prefix = self.config.hook.prefix.replacingOccurrences(of: "${hostname}", with: self.hostname) let prefix = config.hook.prefix.replacingOccurrences(of: "${hostname}", with: hostname)
let payload = prefix + job.text let payload = prefix + job.text
let process = Process() let process = Process()
process.executableURL = URL(fileURLWithPath: self.config.hook.command) process.executableURL = URL(fileURLWithPath: config.hook.command)
process.arguments = self.config.hook.args + [payload] process.arguments = config.hook.args + [payload]
var env = ProcessInfo.processInfo.environment var env = ProcessInfo.processInfo.environment
env["SWABBLE_TEXT"] = job.text env["SWABBLE_TEXT"] = job.text
env["SWABBLE_PREFIX"] = prefix env["SWABBLE_PREFIX"] = prefix
for (k, v) in self.config.hook.env { for (k, v) in config.hook.env {
env[k] = v env[k] = v
} }
process.environment = env process.environment = env
@ -70,6 +70,6 @@ public actor HookRunner {
try await group.next() try await group.next()
group.cancelAll() group.cancelAll()
} }
self.lastRun = Date() lastRun = Date()
} }
} }

View File

@ -35,18 +35,18 @@ public actor SpeechPipeline {
transcriptionOptions: etiquette ? [.etiquetteReplacements] : [], transcriptionOptions: etiquette ? [.etiquetteReplacements] : [],
reportingOptions: [.volatileResults], reportingOptions: [.volatileResults],
attributeOptions: []) attributeOptions: [])
self.transcriber = transcriberModule transcriber = transcriberModule
guard let analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriberModule]) guard let analyzerFormat = await SpeechAnalyzer.bestAvailableAudioFormat(compatibleWith: [transcriberModule])
else { else {
throw SpeechPipelineError.analyzerFormatUnavailable throw SpeechPipelineError.analyzerFormatUnavailable
} }
self.analyzer = SpeechAnalyzer(modules: [transcriberModule]) analyzer = SpeechAnalyzer(modules: [transcriberModule])
let (stream, continuation) = AsyncStream<AnalyzerInput>.makeStream() let (stream, continuation) = AsyncStream<AnalyzerInput>.makeStream()
self.inputContinuation = continuation inputContinuation = continuation
let inputNode = self.engine.inputNode let inputNode = engine.inputNode
let inputFormat = inputNode.outputFormat(forBus: 0) let inputFormat = inputNode.outputFormat(forBus: 0)
inputNode.removeTap(onBus: 0) inputNode.removeTap(onBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 2048, format: inputFormat) { [weak self] buffer, _ in inputNode.installTap(onBus: 0, bufferSize: 2048, format: inputFormat) { [weak self] buffer, _ in
@ -55,11 +55,11 @@ public actor SpeechPipeline {
Task { await self.handleBuffer(boxed.buffer, targetFormat: analyzerFormat) } Task { await self.handleBuffer(boxed.buffer, targetFormat: analyzerFormat) }
} }
self.engine.prepare() engine.prepare()
try self.engine.start() try engine.start()
try await self.analyzer?.start(inputSequence: stream) try await analyzer?.start(inputSequence: stream)
guard let transcriberForStream = self.transcriber else { guard let transcriberForStream = transcriber else {
throw SpeechPipelineError.transcriberUnavailable throw SpeechPipelineError.transcriberUnavailable
} }
@ -82,18 +82,18 @@ public actor SpeechPipeline {
} }
public func stop() async { public func stop() async {
self.resultTask?.cancel() resultTask?.cancel()
self.inputContinuation?.finish() inputContinuation?.finish()
self.engine.inputNode.removeTap(onBus: 0) engine.inputNode.removeTap(onBus: 0)
self.engine.stop() engine.stop()
try? await self.analyzer?.finalizeAndFinishThroughEndOfInput() try? await analyzer?.finalizeAndFinishThroughEndOfInput()
} }
private func handleBuffer(_ buffer: AVAudioPCMBuffer, targetFormat: AVAudioFormat) async { private func handleBuffer(_ buffer: AVAudioPCMBuffer, targetFormat: AVAudioFormat) async {
do { do {
let converted = try converter.convert(buffer, to: targetFormat) let converted = try converter.convert(buffer, to: targetFormat)
let input = AnalyzerInput(buffer: converted) let input = AnalyzerInput(buffer: converted)
self.inputContinuation?.yield(input) inputContinuation?.yield(input)
} catch { } catch {
// drop on conversion failure // drop on conversion failure
} }

View File

@ -27,11 +27,11 @@ public struct Logger: Sendable {
print("[\(level.rawValue.uppercased())] \(ts) | \(message)") print("[\(level.rawValue.uppercased())] \(ts) | \(message)")
} }
public func trace(_ msg: String) { self.log(.trace, msg) } public func trace(_ msg: String) { log(.trace, msg) }
public func debug(_ msg: String) { self.log(.debug, msg) } public func debug(_ msg: String) { log(.debug, msg) }
public func info(_ msg: String) { self.log(.info, msg) } public func info(_ msg: String) { log(.info, msg) }
public func warn(_ msg: String) { self.log(.warn, msg) } public func warn(_ msg: String) { log(.warn, msg) }
public func error(_ msg: String) { self.log(.error, msg) } public func error(_ msg: String) { log(.error, msg) }
} }
extension LogLevel { extension LogLevel {

View File

@ -11,24 +11,24 @@ public actor TranscriptsStore {
let dir = FileManager.default.homeDirectoryForCurrentUser let dir = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Application Support/swabble", isDirectory: true) .appendingPathComponent("Library/Application Support/swabble", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
self.fileURL = dir.appendingPathComponent("transcripts.log") fileURL = dir.appendingPathComponent("transcripts.log")
if let data = try? Data(contentsOf: fileURL), if let data = try? Data(contentsOf: fileURL),
let text = String(data: data, encoding: .utf8) let text = String(data: data, encoding: .utf8)
{ {
self.entries = text.split(separator: "\n").map(String.init).suffix(self.limit) entries = text.split(separator: "\n").map(String.init).suffix(limit)
} }
} }
public func append(text: String) { public func append(text: String) {
self.entries.append(text) entries.append(text)
if self.entries.count > self.limit { if entries.count > limit {
self.entries.removeFirst(self.entries.count - self.limit) entries.removeFirst(entries.count - limit)
} }
let body = self.entries.joined(separator: "\n") let body = entries.joined(separator: "\n")
try? body.write(to: self.fileURL, atomically: false, encoding: .utf8) try? body.write(to: fileURL, atomically: false, encoding: .utf8)
} }
public func latest() -> [String] { self.entries } public func latest() -> [String] { entries }
} }
extension String { extension String {

View File

@ -14,14 +14,14 @@ struct DoctorCommand: ParsableCommand {
init() {} init() {}
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if let cfg = parsed.options["config"]?.last { self.configPath = cfg } if let cfg = parsed.options["config"]?.last { configPath = cfg }
} }
mutating func run() async throws { mutating func run() async throws {
let auth = await SFSpeechRecognizer.authorizationStatus() let auth = await SFSpeechRecognizer.authorizationStatus()
print("Speech auth: \(auth)") print("Speech auth: \(auth)")
do { do {
_ = try ConfigLoader.load(at: self.configURL) _ = try ConfigLoader.load(at: configURL)
print("Config: OK") print("Config: OK")
} catch { } catch {
print("Config missing or invalid; run setup") print("Config missing or invalid; run setup")
@ -33,5 +33,5 @@ struct DoctorCommand: ParsableCommand {
print("Mics found: \(session.devices.count)") print("Mics found: \(session.devices.count)")
} }
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } } private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
} }

View File

@ -47,16 +47,16 @@ struct MicSet: ParsableCommand {
init() {} init() {}
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if let value = parsed.positional.first, let intVal = Int(value) { self.index = intVal } if let value = parsed.positional.first, let intVal = Int(value) { index = intVal }
if let cfg = parsed.options["config"]?.last { self.configPath = cfg } if let cfg = parsed.options["config"]?.last { configPath = cfg }
} }
mutating func run() async throws { mutating func run() async throws {
var cfg = try ConfigLoader.load(at: self.configURL) var cfg = try ConfigLoader.load(at: configURL)
cfg.audio.deviceIndex = self.index cfg.audio.deviceIndex = index
try ConfigLoader.save(cfg, at: self.configURL) try ConfigLoader.save(cfg, at: configURL)
print("saved device index \(self.index)") print("saved device index \(index)")
} }
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } } private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
} }

View File

@ -17,19 +17,19 @@ struct ServeCommand: ParsableCommand {
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if parsed.flags.contains("noWake") { self.noWake = true } if parsed.flags.contains("noWake") { noWake = true }
if let cfg = parsed.options["config"]?.last { self.configPath = cfg } if let cfg = parsed.options["config"]?.last { configPath = cfg }
} }
mutating func run() async throws { mutating func run() async throws {
var cfg: SwabbleConfig var cfg: SwabbleConfig
do { do {
cfg = try ConfigLoader.load(at: self.configURL) cfg = try ConfigLoader.load(at: configURL)
} catch { } catch {
cfg = SwabbleConfig() cfg = SwabbleConfig()
try ConfigLoader.save(cfg, at: self.configURL) try ConfigLoader.save(cfg, at: configURL)
} }
if self.noWake { if noWake {
cfg.wake.enabled = false cfg.wake.enabled = false
} }
@ -64,7 +64,7 @@ struct ServeCommand: ParsableCommand {
} }
private var configURL: URL? { private var configURL: URL? {
self.configPath.map { URL(fileURLWithPath: $0) } configPath.map { URL(fileURLWithPath: $0) }
} }
private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool { private static func matchesWake(text: String, cfg: SwabbleConfig) -> Bool {

View File

@ -28,11 +28,11 @@ private enum LaunchdHelper {
"KeepAlive": true, "KeepAlive": true,
] ]
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try data.write(to: self.plistURL) try data.write(to: plistURL)
} }
static func removePlist() throws { static func removePlist() throws {
try? FileManager.default.removeItem(at: self.plistURL) try? FileManager.default.removeItem(at: plistURL)
} }
} }

View File

@ -13,14 +13,14 @@ struct SetupCommand: ParsableCommand {
init() {} init() {}
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if let cfg = parsed.options["config"]?.last { self.configPath = cfg } if let cfg = parsed.options["config"]?.last { configPath = cfg }
} }
mutating func run() async throws { mutating func run() async throws {
let cfg = SwabbleConfig() let cfg = SwabbleConfig()
try ConfigLoader.save(cfg, at: self.configURL) try ConfigLoader.save(cfg, at: configURL)
print("wrote config to \(self.configURL?.path ?? SwabbleConfig.defaultPath.path)") print("wrote config to \(configURL?.path ?? SwabbleConfig.defaultPath.path)")
} }
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } } private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
} }

View File

@ -13,11 +13,11 @@ struct StatusCommand: ParsableCommand {
init() {} init() {}
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if let cfg = parsed.options["config"]?.last { self.configPath = cfg } if let cfg = parsed.options["config"]?.last { configPath = cfg }
} }
mutating func run() async throws { mutating func run() async throws {
let cfg = try? ConfigLoader.load(at: self.configURL) let cfg = try? ConfigLoader.load(at: configURL)
let wake = cfg?.wake.word ?? "clawd" let wake = cfg?.wake.word ?? "clawd"
let wakeEnabled = cfg?.wake.enabled ?? false let wakeEnabled = cfg?.wake.enabled ?? false
let latest = await TranscriptsStore.shared.latest().suffix(3) let latest = await TranscriptsStore.shared.latest().suffix(3)
@ -30,5 +30,5 @@ struct StatusCommand: ParsableCommand {
} }
} }
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } } private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
} }

View File

@ -15,16 +15,16 @@ struct TestHookCommand: ParsableCommand {
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if let positional = parsed.positional.first { self.text = positional } if let positional = parsed.positional.first { text = positional }
if let cfg = parsed.options["config"]?.last { self.configPath = cfg } if let cfg = parsed.options["config"]?.last { configPath = cfg }
} }
mutating func run() async throws { mutating func run() async throws {
let cfg = try ConfigLoader.load(at: self.configURL) let cfg = try ConfigLoader.load(at: configURL)
let runner = HookRunner(config: cfg) let runner = HookRunner(config: cfg)
try await runner.run(job: HookJob(text: self.text, timestamp: Date())) try await runner.run(job: HookJob(text: text, timestamp: Date()))
print("hook invoked") print("hook invoked")
} }
private var configURL: URL? { self.configPath.map { URL(fileURLWithPath: $0) } } private var configURL: URL? { configPath.map { URL(fileURLWithPath: $0) } }
} }

View File

@ -24,12 +24,12 @@ struct TranscribeCommand: ParsableCommand {
init(parsed: ParsedValues) { init(parsed: ParsedValues) {
self.init() self.init()
if let positional = parsed.positional.first { self.inputFile = positional } if let positional = parsed.positional.first { inputFile = positional }
if let loc = parsed.options["locale"]?.last { self.locale = loc } if let loc = parsed.options["locale"]?.last { locale = loc }
if parsed.flags.contains("censor") { self.censor = true } if parsed.flags.contains("censor") { censor = true }
if let out = parsed.options["output"]?.last { self.outputFile = out } if let out = parsed.options["output"]?.last { outputFile = out }
if let fmt = parsed.options["format"]?.last { self.format = fmt } if let fmt = parsed.options["format"]?.last { format = fmt }
if let len = parsed.options["maxLength"]?.last, let intVal = Int(len) { self.maxLength = intVal } if let len = parsed.options["maxLength"]?.last, let intVal = Int(len) { maxLength = intVal }
} }
mutating func run() async throws { mutating func run() async throws {
@ -51,7 +51,7 @@ struct TranscribeCommand: ParsableCommand {
transcript += result.text transcript += result.text
} }
let output = outputFormat.text(for: transcript, maxLength: self.maxLength) let output = outputFormat.text(for: transcript, maxLength: maxLength)
if let path = outputFile { if let path = outputFile {
try output.write(to: URL(fileURLWithPath: path), atomically: false, encoding: .utf8) try output.write(to: URL(fileURLWithPath: path), atomically: false, encoding: .utf8)
} else { } else {