Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 58 additions & 9 deletions Sources/App/AppDelegate+Hotkeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ internal extension AppDelegate {
return
}

// Store the current frontmost app for paste functionality in silent mode
let silentExpressMode = UserDefaults.standard.bool(forKey: "silentExpressMode")
if silentExpressMode {
storePreviousAppForPaste()
}

if recorder.startRecording() {
isHoldRecordingActive = true
updateMenuBarIcon(isRecording: true)
Expand All @@ -73,7 +79,9 @@ internal extension AppDelegate {
}

private func stopRecordingFromPressAndHold() {
guard isHoldRecordingActive else { return }
guard isHoldRecordingActive else {
return
}
guard let recorder = audioRecorder, recorder.isRecording else {
isHoldRecordingActive = false
return
Expand All @@ -82,9 +90,23 @@ internal extension AppDelegate {
isHoldRecordingActive = false
updateMenuBarIcon(isRecording: false)

showRecordingWindowForProcessing {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
NotificationCenter.default.post(name: .spaceKeyPressed, object: nil)
let silentExpressMode = UserDefaults.standard.bool(forKey: "silentExpressMode")

if silentExpressMode {
// Silent mode: use the direct transcription service without UI
let targetApp = WindowController.storedTargetApp
Task {
await SilentTranscriptionService.shared.performSilentTranscription(
audioRecorder: recorder,
targetApp: targetApp
)
}
} else {
// Normal mode: show the window for visual feedback
showRecordingWindowForProcessing {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
NotificationCenter.default.post(name: .spaceKeyPressed, object: nil)
}
}
}
}
Expand All @@ -95,6 +117,7 @@ internal extension AppDelegate {
}

let immediateRecording = UserDefaults.standard.bool(forKey: "immediateRecording")
let silentExpressMode = UserDefaults.standard.bool(forKey: "silentExpressMode")

if immediateRecording {
guard let recorder = audioRecorder else {
Expand All @@ -105,19 +128,37 @@ internal extension AppDelegate {

if recorder.isRecording {
updateMenuBarIcon(isRecording: false)
if recordingWindow == nil || recordingWindow?.isVisible == false {
toggleRecordWindow()
}

DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
NotificationCenter.default.post(name: .spaceKeyPressed, object: nil)
if silentExpressMode {
// Silent mode: use the direct transcription service without UI
let targetApp = WindowController.storedTargetApp
Task {
await SilentTranscriptionService.shared.performSilentTranscription(
audioRecorder: recorder,
targetApp: targetApp
)
}
} else {
// Normal mode: show the window for visual feedback
if recordingWindow == nil || recordingWindow?.isVisible == false {
toggleRecordWindow()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
NotificationCenter.default.post(name: .spaceKeyPressed, object: nil)
}
}
} else {
if !recorder.hasPermission {
toggleRecordWindow()
return
}

// Store the current frontmost app before starting recording
// This is needed for paste functionality in silent mode
if silentExpressMode {
storePreviousAppForPaste()
}

if recorder.startRecording() {
updateMenuBarIcon(isRecording: true)
SoundManager().playRecordingStartSound()
Expand All @@ -134,6 +175,14 @@ internal extension AppDelegate {
}
}

private func storePreviousAppForPaste() {
if let frontmostApp = NSWorkspace.shared.frontmostApplication,
frontmostApp.bundleIdentifier != Bundle.main.bundleIdentifier {
WindowController.storedTargetApp = frontmostApp
NotificationCenter.default.post(name: .targetAppStored, object: frontmostApp)
}
}

private func updateMenuBarIcon(isRecording: Bool) {
guard let button = statusItem?.button else { return }

Expand Down
6 changes: 4 additions & 2 deletions Sources/App/AppDelegate+Lifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ internal extension AppDelegate {

audioRecorder = AudioRecorder()

statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem?.button {
button.image = AppSetupHelper.createMenuBarIcon()
let icon = AppSetupHelper.createMenuBarIcon()
button.image = icon
button.imagePosition = .imageOnly
button.action = #selector(toggleRecordWindow)
button.target = self
}
Expand Down
7 changes: 7 additions & 0 deletions Sources/App/AppDelegate+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import UniformTypeIdentifiers
internal extension AppDelegate {
func makeStatusMenu() -> NSMenu {
let menu = NSMenu()

// Version indicator at top for debugging
let versionItem = NSMenuItem(title: "AudioWhisper \(VersionInfo.displayVersion)", action: nil, keyEquivalent: "")
versionItem.isEnabled = false
menu.addItem(versionItem)
menu.addItem(NSMenuItem.separator())

menu.addItem(NSMenuItem(title: LocalizedStrings.Menu.record, action: #selector(toggleRecordWindow), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Transcribe Audio File...", action: #selector(transcribeAudioFile), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
Expand Down
42 changes: 39 additions & 3 deletions Sources/App/AppSetupHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,45 @@ internal class AppSetupHelper {
static func createMenuBarIcon() -> NSImage {
let iconSize = getAdaptiveMenuBarIconSize()
let config = NSImage.SymbolConfiguration(pointSize: iconSize, weight: .medium)
let image = NSImage(systemSymbolName: "microphone.circle", accessibilityDescription: LocalizedStrings.Accessibility.microphoneIcon)?.withSymbolConfiguration(config)
image?.isTemplate = true // This makes it adapt to menu bar appearance
return image ?? NSImage()

// Try SF Symbol first
if let image = NSImage(systemSymbolName: "microphone.circle", accessibilityDescription: LocalizedStrings.Accessibility.microphoneIcon)?.withSymbolConfiguration(config) {
image.isTemplate = true
return image
}

// Fallback: draw a simple microphone icon
let fallbackSize = NSSize(width: iconSize, height: iconSize)
let fallbackImage = NSImage(size: fallbackSize, flipped: false) { rect in
NSColor.black.setFill()

// Draw a simple microphone shape
let micWidth = rect.width * 0.4
let micHeight = rect.height * 0.5
let micX = (rect.width - micWidth) / 2
let micY = rect.height * 0.35

// Microphone body (rounded rect)
let micRect = NSRect(x: micX, y: micY, width: micWidth, height: micHeight)
let micPath = NSBezierPath(roundedRect: micRect, xRadius: micWidth / 2, yRadius: micWidth / 2)
micPath.fill()

// Stand
let standWidth: CGFloat = 2
let standX = (rect.width - standWidth) / 2
let standPath = NSBezierPath(rect: NSRect(x: standX, y: rect.height * 0.15, width: standWidth, height: rect.height * 0.2))
standPath.fill()

// Base
let baseWidth = rect.width * 0.4
let baseX = (rect.width - baseWidth) / 2
let basePath = NSBezierPath(rect: NSRect(x: baseX, y: rect.height * 0.1, width: baseWidth, height: 2))
basePath.fill()

return true
}
fallbackImage.isTemplate = true
return fallbackImage
}

// MARK: - Menu Bar Icon Constants
Expand Down
Loading