3 Commit-ok
v1.4.0 ... main

Szerző SHA1 Üzenet Dátum
hariel1985
c2bd30bbf5 Update README for 1.4.1
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:18:04 +02:00
hariel1985
8bbea20dcc Bump version to 1.4.1
Patch release: native SF Symbols icons instead of emoji in the menu bar,
recording HUD, download window, menus and Settings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:17:44 +02:00
hariel1985
6b6d9342a9 Replace emoji icons with SF Symbols
The menu bar, recording HUD, download window, menus and Settings used
emoji as icons. They now use SF Symbols: vector, crisp at any scale, and
template-rendered in the menu bar so they follow light, dark and tinted
menu bars.

Menu bar: mic (idle), red mic.fill (recording), hourglass (transcribing /
loading the model), arrow.down.circle with the percentage (downloading).
The icon is now driven by a state (idle/recording/busy) instead of
comparing emoji strings, so download progress can't overwrite the
recording indicator. Icons carry accessibility descriptions.

Coloured symbols are tinted by drawing the monochrome symbol, which keeps
knockouts transparent (a one-colour palette filled in the tick of
checkmark.circle.fill). Status warnings take a warning flag and show a
yellow triangle instead of a "⚠️" prefix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:17:44 +02:00
4 fájl változott, egészen pontosan 129 új sor hozzáadva és 48 régi sor törölve

Fájl megtekintése

@@ -12,6 +12,10 @@ A simple menu bar app for voice dictation using OpenAI Whisper (local, offline).
## macOS ## macOS
### What's new in 1.4.1
- 🎨 **Native icons instead of emoji** — the menu bar icon, recording overlay, download window, menus and Settings now use macOS SF Symbols: sharp at any size, matching light/dark menu bars, and the menu bar microphone turns red while recording
### What's new in 1.4.0 ### What's new in 1.4.0
- 💬 **Live preview while recording** — the recording overlay shows what you're saying as you speak, using macOS's on-device dictation model (works in Hungarian too). It's a preview: the pasted text still comes from Whisper, so it can differ slightly. Toggle it in Settings. - 💬 **Live preview while recording** — the recording overlay shows what you're saying as you speak, using macOS's on-device dictation model (works in Hungarian too). It's a preview: the pasted text still comes from Whisper, so it can differ slightly. Toggle it in Settings.
@@ -85,9 +89,9 @@ make install
### Usage ### Usage
1. Launch WhisperDictate from Applications 1. Launch WhisperDictate from Applications
2. Look for the 🎤 icon in your menu bar 2. Look for the microphone icon in your menu bar
3. Press **⌃⌥D** (Control + Option + D) to start recording 3. Press **⌃⌥D** (Control + Option + D) to start recording
4. Speak (icon changes to 🔴) 4. Speak (the menu bar microphone turns red)
5. Press **⌃⌥D** again to stop and transcribe 5. Press **⌃⌥D** again to stop and transcribe
6. Text is automatically pasted where your cursor is 6. Text is automatically pasted where your cursor is

Fájl megtekintése

@@ -2,7 +2,7 @@
APP_NAME = WhisperDictate APP_NAME = WhisperDictate
APP_BUNDLE = $(APP_NAME).app APP_BUNDLE = $(APP_NAME).app
VERSION = 1.4.0 VERSION = 1.4.1
# Directories # Directories
SRC_DIR = src SRC_DIR = src

Fájl megtekintése

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.4.0</string> <string>1.4.1</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>5</string> <string>6</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>

Fájl megtekintése

@@ -15,6 +15,35 @@ struct Defaults {
static let livePreview = "livePreview" static let livePreview = "livePreview"
} }
// MARK: - Icons
// SF Symbols: vector icons that stay sharp at any scale. Template symbols are
// tinted by macOS (menu bar, menus, buttons) and follow light/dark appearance.
enum Icons {
static func symbol(_ name: String, pointSize: CGFloat = 13, weight: NSFont.Weight = .regular, description: String? = nil) -> NSImage? {
let image = NSImage(systemSymbolName: name, accessibilityDescription: description)?
.withSymbolConfiguration(.init(pointSize: pointSize, weight: weight))
image?.isTemplate = true
return image
}
// Fixed colour, for states that need it (recording) and for custom drawing (HUD).
// Tints the monochrome symbol so knockouts stay transparent (the tick in
// checkmark.circle.fill); a one-colour palette would fill them in. Drawn
// lazily, so it stays sharp on any display scale.
static func coloredSymbol(_ name: String, pointSize: CGFloat, weight: NSFont.Weight = .regular, color: NSColor, description: String? = nil) -> NSImage? {
guard let symbol = symbol(name, pointSize: pointSize, weight: weight, description: description) else { return nil }
let image = NSImage(size: symbol.size, flipped: false) { rect in
symbol.draw(in: rect)
color.set()
rect.fill(using: .sourceAtop)
return true
}
image.accessibilityDescription = description
image.isTemplate = false
return image
}
}
// MARK: - Whisper Models // MARK: - Whisper Models
struct WhisperModels { struct WhisperModels {
struct Model { struct Model {
@@ -681,24 +710,40 @@ final class MeterView: NSView {
} }
func reset() { level = 0; silent = false; mode = .recording; notice = nil; previewText = nil; needsDisplay = true } func reset() { level = 0; silent = false; mode = .recording; notice = nil; previewText = nil; needsDisplay = true }
// Draws a symbol centred on a text line drawn at `origin`; returns the x where the text starts
private static func drawSymbol(_ name: String, color: NSColor, font: NSFont, at origin: NSPoint) -> CGFloat {
guard let image = Icons.coloredSymbol(name, pointSize: font.pointSize, weight: .medium, color: color) else { return origin.x }
let lineHeight = ceil(font.ascender - font.descender)
image.draw(in: NSRect(x: origin.x, y: origin.y + (lineHeight - image.size.height) / 2,
width: image.size.width, height: image.size.height))
return origin.x + image.size.width + 5
}
override func draw(_ dirtyRect: NSRect) { override func draw(_ dirtyRect: NSRect) {
let bg = NSBezierPath(roundedRect: bounds, xRadius: 12, yRadius: 12) let bg = NSBezierPath(roundedRect: bounds, xRadius: 12, yRadius: 12)
NSColor(calibratedWhite: 0.10, alpha: 0.93).setFill() NSColor(calibratedWhite: 0.10, alpha: 0.93).setFill()
bg.fill() bg.fill()
let warn = mode == .recording && silent let warn = mode == .recording && silent
let label = mode == .transcribing ? "⏳ Transcribing…" : warn ? "🎤 No audio input?" : "🎤 Recording…" let titleFont = NSFont.systemFont(ofSize: 12, weight: .medium)
let (symbol, symbolColor, label): (String, NSColor, String) =
mode == .transcribing ? ("hourglass", .white, "Transcribing…")
: warn ? ("mic.slash.fill", .systemRed, "No audio input?")
: ("mic.fill", .systemRed, "Recording…")
var textY = bounds.height - 26 var textY = bounds.height - 26
label.draw(at: NSPoint(x: 16, y: textY), withAttributes: [ let titleX = MeterView.drawSymbol(symbol, color: symbolColor, font: titleFont, at: NSPoint(x: 16, y: textY))
.font: NSFont.systemFont(ofSize: 12, weight: .medium), label.draw(at: NSPoint(x: titleX, y: textY), withAttributes: [
.font: titleFont,
.foregroundColor: warn ? NSColor.systemRed : NSColor.white .foregroundColor: warn ? NSColor.systemRed : NSColor.white
]) ])
switch notice { switch notice {
case .loadingModel?: case .loadingModel?:
textY -= 19 textY -= 19
"Loading model into memory…".draw(at: NSPoint(x: 16, y: textY), withAttributes: [ let font = NSFont.systemFont(ofSize: 11, weight: .semibold)
.font: NSFont.systemFont(ofSize: 11, weight: .semibold), let x = MeterView.drawSymbol("memorychip", color: .systemYellow, font: font, at: NSPoint(x: 16, y: textY))
"Loading model into memory…".draw(at: NSPoint(x: x, y: textY), withAttributes: [
.font: font,
.foregroundColor: NSColor.systemYellow .foregroundColor: NSColor.systemYellow
]) ])
textY -= 16 textY -= 16
@@ -708,8 +753,10 @@ final class MeterView: NSView {
]) ])
case .modelLoaded?: case .modelLoaded?:
textY -= 19 textY -= 19
"✓ Model loaded".draw(at: NSPoint(x: 16, y: textY), withAttributes: [ let font = NSFont.systemFont(ofSize: 11, weight: .semibold)
.font: NSFont.systemFont(ofSize: 11, weight: .semibold), let x = MeterView.drawSymbol("checkmark.circle.fill", color: .systemGreen, font: font, at: NSPoint(x: 16, y: textY))
"Model loaded".draw(at: NSPoint(x: x, y: textY), withAttributes: [
.font: font,
.foregroundColor: NSColor.systemGreen .foregroundColor: NSColor.systemGreen
]) ])
case nil: case nil:
@@ -818,6 +865,7 @@ final class DownloadProgressWindow: NSObject {
private var phase = Phase.downloading private var phase = Phase.downloading
private var window: NSWindow? private var window: NSWindow?
private let titleIcon = NSImageView()
private let titleLabel = NSTextField(labelWithString: "") private let titleLabel = NSTextField(labelWithString: "")
// Two indicators instead of toggling isIndeterminate on one: after an // Two indicators instead of toggling isIndeterminate on one: after an
// indeterminate phase, NSProgressIndicator stops drawing determinate values. // indeterminate phase, NSProgressIndicator stops drawing determinate values.
@@ -871,6 +919,7 @@ final class DownloadProgressWindow: NSObject {
bytesPerSecond = 0 bytesPerSecond = 0
lastRender = .distantPast lastRender = .distantPast
titleLabel.stringValue = "Downloading \(modelName)" titleLabel.stringValue = "Downloading \(modelName)"
setTitleIcon("arrow.down.circle.fill", color: .controlAccentColor)
setBar(indeterminate: true) setBar(indeterminate: true)
setDetail("Connecting…") setDetail("Connecting…")
hintLabel.isHidden = false hintLabel.isHidden = false
@@ -916,6 +965,7 @@ final class DownloadProgressWindow: NSObject {
func showLoading(modelName: String) { func showLoading(modelName: String) {
phase = .loading phase = .loading
titleLabel.stringValue = "Loading \(modelName)…" titleLabel.stringValue = "Loading \(modelName)…"
setTitleIcon("memorychip.fill", color: .controlAccentColor)
setBar(indeterminate: true) setBar(indeterminate: true)
setDetail("Download complete. Loading the model into memory…") setDetail("Download complete. Loading the model into memory…")
hintLabel.isHidden = true hintLabel.isHidden = true
@@ -924,7 +974,8 @@ final class DownloadProgressWindow: NSObject {
func showReady(modelName: String) { func showReady(modelName: String) {
phase = .ready phase = .ready
titleLabel.stringValue = "✓ \(modelName) is ready" titleLabel.stringValue = "\(modelName) is ready"
setTitleIcon("checkmark.circle.fill", color: .systemGreen)
setBar(indeterminate: false, value: 1) setBar(indeterminate: false, value: 1)
setDetail("It is now the active model. Press ⌃⌥D to dictate.") setDetail("It is now the active model. Press ⌃⌥D to dictate.")
hintLabel.isHidden = true hintLabel.isHidden = true
@@ -933,7 +984,8 @@ final class DownloadProgressWindow: NSObject {
func showFailed(modelName: String, message: String) { func showFailed(modelName: String, message: String) {
phase = .failed phase = .failed
titleLabel.stringValue = "⚠️ Could not download \(modelName)" titleLabel.stringValue = "Could not download \(modelName)"
setTitleIcon("exclamationmark.triangle.fill", color: .systemYellow)
busyBar.stopAnimation(nil) busyBar.stopAnimation(nil)
busyBar.isHidden = true busyBar.isHidden = true
bar.isHidden = true bar.isHidden = true
@@ -951,7 +1003,11 @@ final class DownloadProgressWindow: NSObject {
let v = NSView(frame: NSRect(x: 0, y: 0, width: 440, height: 170)) let v = NSView(frame: NSRect(x: 0, y: 0, width: 440, height: 170))
titleLabel.frame = NSRect(x: 20, y: 130, width: 400, height: 20) titleIcon.frame = NSRect(x: 20, y: 131, width: 18, height: 18)
titleIcon.symbolConfiguration = .init(pointSize: 14, weight: .semibold)
v.addSubview(titleIcon)
titleLabel.frame = NSRect(x: 44, y: 130, width: 376, height: 20)
titleLabel.font = .boldSystemFont(ofSize: 13) titleLabel.font = .boldSystemFont(ofSize: 13)
titleLabel.lineBreakMode = .byTruncatingTail titleLabel.lineBreakMode = .byTruncatingTail
v.addSubview(titleLabel) v.addSubview(titleLabel)
@@ -995,6 +1051,11 @@ final class DownloadProgressWindow: NSObject {
return w return w
} }
private func setTitleIcon(_ name: String, color: NSColor) {
titleIcon.image = NSImage(systemSymbolName: name, accessibilityDescription: nil)
titleIcon.contentTintColor = color
}
private func setBar(indeterminate: Bool, value: Double = 0) { private func setBar(indeterminate: Bool, value: Double = 0) {
bar.isHidden = indeterminate bar.isHidden = indeterminate
busyBar.isHidden = !indeterminate busyBar.isHidden = !indeterminate
@@ -1162,7 +1223,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
if alert.runModal() == .alertFirstButtonReturn { if alert.runModal() == .alertFirstButtonReturn {
downloadModel(WhisperModels.available[popup.indexOfSelectedItem]) downloadModel(WhisperModels.available[popup.indexOfSelectedItem])
} else { } else {
updateStatus("⚠️ No model selected") updateStatus("No model selected", warning: true)
} }
} }
@@ -1180,7 +1241,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
return return
} }
guard let url = URL(string: model.url) else { guard let url = URL(string: model.url) else {
updateStatus("⚠️ Invalid URL") updateStatus("Invalid URL", warning: true)
return return
} }
@@ -1224,7 +1285,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
func failDownload(_ message: String) { func failDownload(_ message: String) {
let name = currentDownloadModel?.name ?? "model" let name = currentDownloadModel?.name ?? "model"
clearDownloadState() clearDownloadState()
updateStatus("⚠️ Download failed") updateStatus("Download failed", warning: true)
if playSounds { NSSound(named: "Basso")?.play() } if playSounds { NSSound(named: "Basso")?.play() }
downloadWindow.showFailed(modelName: name, message: message) downloadWindow.showFailed(modelName: name, message: message)
downloadWindow.show() downloadWindow.show()
@@ -1237,24 +1298,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
updateDownloadIndicators() updateDownloadIndicators()
} }
// Menu bar icon when idle: download progress while a model downloads, else the mic // Menu bar icon. While idle it shows the download progress if a model is downloading.
var idleIcon: String { enum MenuBarIcon { case idle, recording, busy }
downloadTask != nil ? "⬇️ \(downloadPercent)%" : "🎤" var menuBarIcon = MenuBarIcon.idle { didSet { refreshMenuBarIcon() } }
func refreshMenuBarIcon() {
guard let button = statusItem?.button else { return }
let downloading = menuBarIcon == .idle && downloadTask != nil
switch menuBarIcon {
case .idle:
button.image = downloading
? Icons.symbol("arrow.down.circle", pointSize: 15, description: "WhisperDictate: downloading a model")
: Icons.symbol("mic", pointSize: 15, description: "WhisperDictate")
case .recording:
button.image = Icons.coloredSymbol("mic.fill", pointSize: 15, color: .systemRed, description: "WhisperDictate: recording")
case .busy:
button.image = Icons.symbol("hourglass", pointSize: 15, description: "WhisperDictate: working")
}
button.imagePosition = .imageLeading
button.title = downloading ? "\(downloadPercent)%" : ""
} }
// Sync the menu bar icon, the menu's download line and the Settings button // Sync the menu bar icon, the menu's download line and the Settings button
// with the download state, without overriding the recording/transcribing icon. // with the download state, without overriding the recording/transcribing icon.
func updateDownloadIndicators() { func updateDownloadIndicators() {
if let title = statusItem.button?.title, title == "🎤" || title.hasPrefix("⬇️") { refreshMenuBarIcon()
statusItem.button?.title = idleIcon
}
if let item = statusItem.menu?.item(withTag: 101) { if let item = statusItem.menu?.item(withTag: 101) {
item.isHidden = currentDownloadModel == nil item.isHidden = currentDownloadModel == nil
if let model = currentDownloadModel { if let model = currentDownloadModel {
item.title = "⬇️ Downloading \(model.name): \(downloadPercent)% — Show Progress…" item.title = "Downloading \(model.name): \(downloadPercent)% — Show Progress…"
} }
} }
settingsDownloadButton?.title = downloadTask != nil ? "⬇️ \(downloadPercent)%" : "Download..." if let button = settingsDownloadButton {
let downloading = downloadTask != nil
button.image = downloading ? Icons.symbol("arrow.down.circle", pointSize: 11) : nil
button.imagePosition = .imageLeading
button.title = downloading ? "\(downloadPercent)%" : "Download..."
}
} }
// MARK: - URLSessionDownloadDelegate // MARK: - URLSessionDownloadDelegate
@@ -1324,7 +1404,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
// MARK: - Status Item // MARK: - Status Item
func setupStatusItem() { func setupStatusItem() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.title = "🎤" refreshMenuBarIcon()
let menu = NSMenu() let menu = NSMenu()
@@ -1336,6 +1416,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
menu.addItem(statusMenuItem) menu.addItem(statusMenuItem)
let downloadMenuItem = NSMenuItem(title: "", action: #selector(showDownloadWindow), keyEquivalent: "") let downloadMenuItem = NSMenuItem(title: "", action: #selector(showDownloadWindow), keyEquivalent: "")
downloadMenuItem.image = Icons.symbol("arrow.down.circle")
downloadMenuItem.target = self downloadMenuItem.target = self
downloadMenuItem.tag = 101 downloadMenuItem.tag = 101
downloadMenuItem.isHidden = true downloadMenuItem.isHidden = true
@@ -1734,20 +1815,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
func checkModelExists() { func checkModelExists() {
let validation = isValidModelPath(modelPath) let validation = isValidModelPath(modelPath)
if !validation.valid { if !validation.valid {
updateStatus("⚠️ \(validation.error ?? "Invalid model")") updateStatus(validation.error ?? "Invalid model", warning: true)
} else { } else {
updateStatus("Ready") updateStatus("Ready")
} }
} }
func updateStatus(_ status: String) { func updateStatus(_ status: String, warning: Bool = false) {
if let menu = statusItem.menu { guard let item = statusItem.menu?.item(withTag: 100) else { return }
for item in menu.items { item.title = "Status: \(status)"
if item.tag == 100 { item.image = warning ? Icons.coloredSymbol("exclamationmark.triangle.fill", pointSize: 13, color: .systemYellow) : nil
item.title = "Status: \(status)"
}
}
}
} }
// MARK: - Permissions // MARK: - Permissions
@@ -1840,7 +1917,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
audioRecorder?.isMeteringEnabled = true audioRecorder?.isMeteringEnabled = true
audioRecorder?.record() audioRecorder?.record()
isRecording = true isRecording = true
statusItem.button?.title = "🔴" menuBarIcon = .recording
updateStatus("Recording...") updateStatus("Recording...")
if playSounds { NSSound(named: "Tink")?.play() } if playSounds { NSSound(named: "Tink")?.play() }
NSLog("Recording started") NSLog("Recording started")
@@ -1961,7 +2038,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
let keepHUD = explainLoading || recordingHUD.hasPreview let keepHUD = explainLoading || recordingHUD.hasPreview
stopMeter(hideHUD: !keepHUD) stopMeter(hideHUD: !keepHUD)
if keepHUD { recordingHUD.showTranscribing(notice: explainLoading ? .loadingModel : nil) } if keepHUD { recordingHUD.showTranscribing(notice: explainLoading ? .loadingModel : nil) }
statusItem.button?.title = "⏳" menuBarIcon = .busy
updateStatus("Transcribing...") updateStatus("Transcribing...")
if playSounds { NSSound(named: "Pop")?.play() } if playSounds { NSSound(named: "Pop")?.play() }
NSLog("Recording stopped, transcribing...") NSLog("Recording stopped, transcribing...")
@@ -2031,7 +2108,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
} }
if !isRecording && !isTranscribing { if !isRecording && !isTranscribing {
statusItem.button?.title = "⏳" menuBarIcon = .busy
updateStatus("Loading model…") updateStatus("Loading model…")
} }
@@ -2043,7 +2120,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
defer { completion?() } defer { completion?() }
guard let self = self, self.whisperServer === server else { return } guard let self = self, self.whisperServer === server else { return }
let busy = self.isRecording || self.isTranscribing let busy = self.isRecording || self.isTranscribing
if !busy { self.statusItem.button?.title = self.idleIcon } if !busy { self.menuBarIcon = .idle }
if ok { if ok {
if !busy { self.updateStatus("Ready") } if !busy { self.updateStatus("Ready") }
NSLog("whisper-server ready on port \(server.port)") NSLog("whisper-server ready on port \(server.port)")
@@ -2061,8 +2138,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
let modelValidation = isValidModelPath(modelPath) let modelValidation = isValidModelPath(modelPath)
guard modelValidation.valid else { guard modelValidation.valid else {
DispatchQueue.main.async { DispatchQueue.main.async {
self.statusItem.button?.title = self.idleIcon self.menuBarIcon = .idle
self.updateStatus("⚠️ \(modelValidation.error ?? "Invalid model")") self.updateStatus(modelValidation.error ?? "Invalid model", warning: true)
if self.playSounds { NSSound(named: "Basso")?.play() } if self.playSounds { NSSound(named: "Basso")?.play() }
} }
return return
@@ -2070,8 +2147,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
guard SupportedLanguages.isValid(language) else { guard SupportedLanguages.isValid(language) else {
DispatchQueue.main.async { DispatchQueue.main.async {
self.statusItem.button?.title = self.idleIcon self.menuBarIcon = .idle
self.updateStatus("⚠️ Invalid language") self.updateStatus("Invalid language", warning: true)
if self.playSounds { NSSound(named: "Basso")?.play() } if self.playSounds { NSSound(named: "Basso")?.play() }
} }
return return
@@ -2098,7 +2175,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
if let finalText = finalText, !finalText.isEmpty { if let finalText = finalText, !finalText.isEmpty {
self.pasteText(finalText) self.pasteText(finalText)
} else { } else {
self.statusItem.button?.title = self.idleIcon self.menuBarIcon = .idle
self.updateStatus("Ready") self.updateStatus("Ready")
if self.playSounds { NSSound(named: "Basso")?.play() } if self.playSounds { NSSound(named: "Basso")?.play() }
NSLog("No speech recognized") NSLog("No speech recognized")
@@ -2111,8 +2188,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
func transcribeWithCLI(prompt: String? = nil) -> String? { func transcribeWithCLI(prompt: String? = nil) -> String? {
guard let whisperPath = findWhisperCLI() else { guard let whisperPath = findWhisperCLI() else {
DispatchQueue.main.async { DispatchQueue.main.async {
self.statusItem.button?.title = self.idleIcon self.menuBarIcon = .idle
self.updateStatus("⚠️ whisper-cli not found") self.updateStatus("whisper-cli not found", warning: true)
if self.playSounds { NSSound(named: "Basso")?.play() } if self.playSounds { NSSound(named: "Basso")?.play() }
NSLog("whisper-cli not found in PATH") NSLog("whisper-cli not found in PATH")
} }
@@ -2187,7 +2264,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDownloadDelegate,
} }
} }
self.statusItem.button?.title = self.idleIcon self.menuBarIcon = .idle
self.updateStatus("Ready") self.updateStatus("Ready")
if self.playSounds { NSSound(named: "Glass")?.play() } if self.playSounds { NSSound(named: "Glass")?.play() }
} }