Skip to content

Commit 5f78b4c

Browse files
authored
feat: add system-wide logging framework with rotation and export (closes #36) (#54)
1 parent 9e12332 commit 5f78b4c

10 files changed

Lines changed: 384 additions & 50 deletions

Sources/VocaMac/Models/AppState.swift

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ final class AppState: ObservableObject {
107107
@AppStorage("vocamac.soundEffectsEnabled") var soundEffectsEnabled: Bool = true
108108
@AppStorage("vocamac.showCursorIndicator") var showCursorIndicator: Bool = true
109109
@AppStorage("vocamac.translationEnabled") var translationEnabled: Bool = false
110+
@AppStorage("vocamac.logLevel") var logLevel: String = "info"
110111

111112
// MARK: - Services
112113

@@ -125,7 +126,7 @@ final class AppState: ObservableObject {
125126
// MARK: - Initialization
126127

127128
init() {
128-
NSLog("[AppState] Initializing...")
129+
VocaLogger.info(.appState, "Initializing...")
129130
setupServices()
130131
// Trigger startup automatically
131132
Task {
@@ -480,46 +481,43 @@ final class AppState: ObservableObject {
480481
// MARK: - Startup
481482

482483
func performStartup() async {
483-
NSLog("[AppState] performStartup beginning...")
484+
VocaLogger.info(.appState, "performStartup beginning...")
484485

485486
// 1. Detect hardware
486487
systemCapabilities = SystemInfo.detect()
487-
NSLog("[AppState] System: %@ | %d GB RAM | %d cores",
488-
systemCapabilities?.processorName ?? "unknown",
489-
systemCapabilities?.physicalMemoryGB ?? 0,
490-
systemCapabilities?.coreCount ?? 0)
488+
let sysInfo = systemCapabilities
489+
VocaLogger.info(.appState, "System: \(sysInfo?.processorName ?? "unknown") | \(sysInfo?.physicalMemoryGB ?? 0) GB RAM | \(sysInfo?.coreCount ?? 0) cores")
491490

492491
// 2. Check/request permissions
493492
checkPermissions()
494-
NSLog("[AppState] Mic permission: %@ | Accessibility: %@ | Input Monitoring: %@",
495-
micPermission.rawValue, accessibilityPermission.rawValue, inputMonitoringPermission.rawValue)
493+
VocaLogger.info(.appState, "Mic permission: \(micPermission.rawValue) | Accessibility: \(accessibilityPermission.rawValue) | Input Monitoring: \(inputMonitoringPermission.rawValue)")
496494

497495
// Auto-prompt for microphone permission on first launch
498496
if micPermission == .notDetermined {
499-
NSLog("[AppState] Mic permission not determined — requesting...")
497+
VocaLogger.info(.appState, "Mic permission not determined — requesting...")
500498
requestMicrophonePermission()
501499
}
502500

503501
// 3. Load model — let WhisperKit auto-select and download the best model
504-
NSLog("[AppState] Loading WhisperKit model (auto-select)...")
502+
VocaLogger.info(.appState, "Loading WhisperKit model (auto-select)...")
505503
await loadModel()
506504
NSLog("[AppState] Model loaded: %@", whisperService.loadedModelName ?? "none")
507505

508506
// 4. Always attempt to start hotkey listener
509507
// The event tap creation itself will fail if permissions aren't granted,
510508
// and we handle that gracefully in HotKeyManager.
511-
NSLog("[AppState] Attempting to start hotkey listener...")
509+
VocaLogger.info(.appState, "Attempting to start hotkey listener...")
512510
hotKeyManager.startListening(
513511
keyCode: hotKeyCode,
514512
mode: activationMode,
515513
doubleTapThreshold: doubleTapThreshold
516514
)
517515
if hotKeyManager.isListening {
518-
NSLog("[AppState] Hotkey listener active (keyCode=%d, mode=%@)", hotKeyCode, activationMode.rawValue)
516+
VocaLogger.info(.appState, "Hotkey listener active (keyCode=\(hotKeyCode), mode=\(activationMode.rawValue))")
519517
} else {
520-
NSLog("[AppState] WARNING: Hotkey listener failed to start. Check Accessibility & Input Monitoring permissions.")
518+
VocaLogger.warning(.appState, "Hotkey listener failed to start. Check Accessibility & Input Monitoring permissions.")
521519
}
522520

523-
NSLog("[AppState] Startup complete!")
521+
VocaLogger.info(.appState, "Startup complete!")
524522
}
525523
}

Sources/VocaMac/Services/AudioEngine.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ final class AudioEngine {
108108
do {
109109
try engine.start()
110110
} catch {
111-
print("[AudioEngine] Failed to start audio engine: \(error)")
111+
VocaLogger.error(.audioEngine, "Failed to start audio engine: \(error)")
112112
isCurrentlyRecording = false
113113
}
114114
}
@@ -200,7 +200,7 @@ final class AudioEngine {
200200

201201
// Create a converter
202202
guard let converter = AVAudioConverter(from: inputFormat, to: whisperFormat) else {
203-
print("[AudioEngine] Failed to create audio format converter")
203+
VocaLogger.error(.audioEngine, "Failed to create audio format converter")
204204
return nil
205205
}
206206

@@ -224,7 +224,7 @@ final class AudioEngine {
224224
converter.convert(to: outputBuffer, error: &error, withInputFrom: inputBlock)
225225

226226
if let error = error {
227-
print("[AudioEngine] Conversion error: \(error)")
227+
VocaLogger.error(.audioEngine, "Conversion error: \(error)")
228228
return nil
229229
}
230230

Sources/VocaMac/Services/CursorOverlayManager.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ final class CursorOverlayManager {
7373
}
7474

7575
viewModel.isActive = true
76-
NSLog("[CursorOverlay] Indicator shown (recording)")
76+
VocaLogger.debug(.cursorOverlay, "Indicator shown (recording)")
7777
}
7878

7979
/// Transition the indicator from recording (red) to processing (purple)
8080
/// Keeps the overlay visible so the user knows text is on its way.
8181
func transitionToProcessing() {
8282
viewModel.phase = .processing
83-
NSLog("[CursorOverlay] Transitioned to processing")
83+
VocaLogger.debug(.cursorOverlay, "Transitioned to processing")
8484
}
8585

8686
/// Hide the recording indicator
@@ -92,7 +92,7 @@ final class CursorOverlayManager {
9292
overlayPanel?.orderOut(nil)
9393
overlayPanel = nil
9494
hostingView = nil
95-
NSLog("[CursorOverlay] Indicator hidden")
95+
VocaLogger.debug(.cursorOverlay, "Indicator hidden")
9696
}
9797

9898
/// Update the audio level (kept for future use)

Sources/VocaMac/Services/HotKeyManager.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ final class HotKeyManager {
7272
doubleTapThreshold: Double = 0.4
7373
) {
7474
guard !isListening else {
75-
print("[HotKeyManager] Already listening")
75+
VocaLogger.debug(.hotKeyManager, "Already listening")
7676
return
7777
}
7878

@@ -101,7 +101,7 @@ final class HotKeyManager {
101101
callback: HotKeyManager.eventTapCallback,
102102
userInfo: userInfo
103103
) else {
104-
NSLog("[HotKeyManager] FAILED to create event tap! Check Accessibility & Input Monitoring permissions.")
104+
VocaLogger.error(.hotKeyManager, "FAILED to create event tap! Check Accessibility & Input Monitoring permissions.")
105105
return
106106
}
107107

@@ -114,7 +114,7 @@ final class HotKeyManager {
114114
CGEvent.tapEnable(tap: tap, enable: true)
115115

116116
isListening = true
117-
NSLog("[HotKeyManager] Event tap created successfully. Listening for keyCode %d in %@ mode", keyCode, mode.rawValue)
117+
VocaLogger.info(.hotKeyManager, "Event tap created successfully. Listening for keyCode \(keyCode) in \(mode.rawValue) mode")
118118
}
119119

120120
/// Stop listening for global hotkey events
@@ -135,7 +135,7 @@ final class HotKeyManager {
135135
isKeyHeld = false
136136
isToggled = false
137137

138-
print("[HotKeyManager] Stopped listening")
138+
VocaLogger.info(.hotKeyManager, "Stopped listening")
139139
}
140140

141141
/// Update the configuration while listening
@@ -180,7 +180,7 @@ final class HotKeyManager {
180180
// For regular keys, we use keyDown/keyUp events
181181
if type == .flagsChanged {
182182
if keyCode == targetKeyCode {
183-
NSLog("[HotKeyManager] flagsChanged event for target keyCode %d", keyCode)
183+
VocaLogger.debug(.hotKeyManager, "flagsChanged event for target keyCode \(keyCode)")
184184
}
185185
handleModifierKeyEvent(keyCode: keyCode, event: event)
186186
} else if type == .keyDown || type == .keyUp {
@@ -234,14 +234,14 @@ final class HotKeyManager {
234234
/// Process a key-down event for the target hotkey
235235
private func handleKeyDown() {
236236
let currentTime = CFAbsoluteTimeGetCurrent()
237-
NSLog("[HotKeyManager] Key DOWN detected (mode=%@)", mode.rawValue)
237+
VocaLogger.debug(.hotKeyManager, "Key DOWN detected (mode=\(mode.rawValue))")
238238

239239
switch mode {
240240
case .pushToTalk:
241241
// Push-to-talk: start recording on key down (if not already held)
242242
if !isKeyHeld {
243243
isKeyHeld = true
244-
NSLog("[HotKeyManager] Push-to-talk: START recording")
244+
VocaLogger.debug(.hotKeyManager, "Push-to-talk: START recording")
245245
DispatchQueue.main.async { [weak self] in
246246
self?.onRecordingStart?()
247247
}
@@ -272,14 +272,14 @@ final class HotKeyManager {
272272

273273
/// Process a key-up event for the target hotkey
274274
private func handleKeyUp() {
275-
NSLog("[HotKeyManager] Key UP detected (mode=%@)", mode.rawValue)
275+
VocaLogger.debug(.hotKeyManager, "Key UP detected (mode=\(mode.rawValue))")
276276

277277
switch mode {
278278
case .pushToTalk:
279279
// Push-to-talk: stop recording on key release
280280
if isKeyHeld {
281281
isKeyHeld = false
282-
NSLog("[HotKeyManager] Push-to-talk: STOP recording")
282+
VocaLogger.debug(.hotKeyManager, "Push-to-talk: STOP recording")
283283
DispatchQueue.main.async { [weak self] in
284284
self?.onRecordingStop?()
285285
}

0 commit comments

Comments
 (0)