You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
571 lines
25 KiB
571 lines
25 KiB
import Cocoa
|
|
import AVFoundation
|
|
import ApplicationServices
|
|
|
|
public final class StatusBarController: NSObject, NSMenuDelegate {
|
|
private var statusItem: NSStatusItem?
|
|
public var onToggleRecording: (() -> Void)?
|
|
|
|
private let dateFormatter: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.dateFormat = "HH:mm:ss"
|
|
return df
|
|
}()
|
|
|
|
public override init() {
|
|
super.init()
|
|
setupStatusItem()
|
|
}
|
|
|
|
private func setupStatusItem() {
|
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
updateIcon(state: .idle)
|
|
buildMenu()
|
|
}
|
|
|
|
public enum State {
|
|
case idle
|
|
case recording
|
|
case transcribing
|
|
}
|
|
|
|
public func updateIcon(state: State) {
|
|
guard let button = statusItem?.button else { return }
|
|
|
|
switch state {
|
|
case .idle:
|
|
let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .regular)
|
|
if let image = NSImage(systemSymbolName: "mic", accessibilityDescription: "Soniox Voice")?.withSymbolConfiguration(config) {
|
|
image.isTemplate = true
|
|
button.image = image
|
|
}
|
|
button.contentTintColor = nil
|
|
button.toolTip = "Soniox Voice (Ready)"
|
|
|
|
case .recording:
|
|
let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
|
|
if let image = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: "Recording")?.withSymbolConfiguration(config) {
|
|
image.isTemplate = false
|
|
button.image = image
|
|
button.contentTintColor = NSColor.systemRed
|
|
}
|
|
button.toolTip = "Recording speech..."
|
|
|
|
case .transcribing:
|
|
let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
|
|
if let image = NSImage(systemSymbolName: "waveform", accessibilityDescription: "Transcribing")?.withSymbolConfiguration(config) {
|
|
image.isTemplate = false
|
|
button.image = image
|
|
button.contentTintColor = NSColor.systemOrange
|
|
}
|
|
button.toolTip = "Transcribing with Soniox..."
|
|
}
|
|
}
|
|
|
|
public func buildMenu(isRecording: Bool = false) {
|
|
let menu = NSMenu()
|
|
menu.autoenablesItems = false
|
|
menu.delegate = self
|
|
|
|
// 1. Primary Action: Start / Stop Recording
|
|
let recordTitle = isRecording ? "⏹️ Stop Recording & Insert Text" : "🎙️ Start Recording (\(HotkeyManager.shared.currentPreset.rawValue))"
|
|
let recordItem = NSMenuItem(title: recordTitle, action: #selector(toggleRecordAction), keyEquivalent: "")
|
|
recordItem.target = self
|
|
menu.addItem(recordItem)
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
// 2. Dictation Mode Submenu
|
|
let modeMenu = NSMenu()
|
|
for mode in DictationMode.allCases {
|
|
let item = NSMenuItem(title: mode.localizedTitle, action: #selector(selectModeAction(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
item.representedObject = mode
|
|
item.state = (HotkeyManager.shared.currentMode == mode) ? .on : .off
|
|
modeMenu.addItem(item)
|
|
}
|
|
let modeMenuItem = NSMenuItem(title: "⚙️ Dictation Mode", action: nil, keyEquivalent: "")
|
|
modeMenuItem.submenu = modeMenu
|
|
menu.addItem(modeMenuItem)
|
|
|
|
// 3. Hotkey Preset Submenu
|
|
let hotkeyMenu = NSMenu()
|
|
for preset in HotkeyPreset.allCases {
|
|
let item = NSMenuItem(title: preset.rawValue, action: #selector(selectHotkeyAction(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
item.representedObject = preset
|
|
item.state = (HotkeyManager.shared.currentPreset == preset) ? .on : .off
|
|
hotkeyMenu.addItem(item)
|
|
}
|
|
let hotkeyMenuItem = NSMenuItem(title: "⌨️ Shortcut Key", action: nil, keyEquivalent: "")
|
|
hotkeyMenuItem.submenu = hotkeyMenu
|
|
menu.addItem(hotkeyMenuItem)
|
|
|
|
// 4. Recognition Language Submenu
|
|
let langMenu = NSMenu()
|
|
for lang in RecognitionLanguage.allCases {
|
|
let item = NSMenuItem(title: lang.displayName, action: #selector(selectLanguageAction(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
item.representedObject = lang
|
|
item.state = (SonioxSettings.shared.language == lang) ? .on : .off
|
|
langMenu.addItem(item)
|
|
}
|
|
let langMenuItem = NSMenuItem(title: "🌐 Recognition Language", action: nil, keyEquivalent: "")
|
|
langMenuItem.submenu = langMenu
|
|
menu.addItem(langMenuItem)
|
|
|
|
// 5. Microphone Input Device Submenu
|
|
let micDeviceMenu = NSMenu()
|
|
let availableDevices = AudioRecorder.availableAudioDevices()
|
|
let preferredID = SonioxSettings.shared.preferredAudioDeviceID
|
|
|
|
// Default device item
|
|
let defaultItem = NSMenuItem(title: "System Default Microphone", action: #selector(selectAudioDeviceAction(_:)), keyEquivalent: "")
|
|
defaultItem.target = self
|
|
defaultItem.representedObject = nil as String?
|
|
defaultItem.state = (preferredID == nil) ? .on : .off
|
|
micDeviceMenu.addItem(defaultItem)
|
|
|
|
if !availableDevices.isEmpty {
|
|
micDeviceMenu.addItem(NSMenuItem.separator())
|
|
for device in availableDevices {
|
|
let item = NSMenuItem(title: device.localizedName, action: #selector(selectAudioDeviceAction(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
item.representedObject = device.uniqueID
|
|
item.state = (preferredID == device.uniqueID) ? .on : .off
|
|
micDeviceMenu.addItem(item)
|
|
}
|
|
}
|
|
let micDeviceMenuItem = NSMenuItem(title: "🎙️ Microphone Input", action: nil, keyEquivalent: "")
|
|
micDeviceMenuItem.submenu = micDeviceMenu
|
|
menu.addItem(micDeviceMenuItem)
|
|
|
|
// 6. Number Formatting Submenu
|
|
let numberMenu = NSMenu()
|
|
for option in NumberFormatOption.allCases {
|
|
let item = NSMenuItem(title: option.displayName, action: #selector(selectNumberFormatAction(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
item.representedObject = option
|
|
item.state = (SonioxSettings.shared.numberFormat == option) ? .on : .off
|
|
numberMenu.addItem(item)
|
|
}
|
|
let numberMenuItem = NSMenuItem(title: "🔢 Number Formatting", action: nil, keyEquivalent: "")
|
|
numberMenuItem.submenu = numberMenu
|
|
menu.addItem(numberMenuItem)
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
// 7. History Submenu (Last 500 entries)
|
|
let historyMenu = buildHistorySubmenu()
|
|
let historyMenuItem = NSMenuItem(title: "🕒 History (\(HistoryManager.shared.totalCount()) of 500)", action: nil, keyEquivalent: "")
|
|
historyMenuItem.submenu = historyMenu
|
|
menu.addItem(historyMenuItem)
|
|
|
|
// 8. Advanced Options Submenu
|
|
let advancedMenu = NSMenu()
|
|
|
|
let slashCmdItem = NSMenuItem(title: "⚡️ Executive Voice Slash Commands", action: #selector(toggleVoiceCommandsAction(_:)), keyEquivalent: "")
|
|
slashCmdItem.target = self
|
|
slashCmdItem.state = SonioxSettings.shared.voiceCommands ? .on : .off
|
|
advancedMenu.addItem(slashCmdItem)
|
|
|
|
let punctItem = NSMenuItem(title: "🗣️ Voice Punctuation Commands", action: #selector(togglePunctuationAction(_:)), keyEquivalent: "")
|
|
punctItem.target = self
|
|
punctItem.state = SonioxSettings.shared.voicePunctuation ? .on : .off
|
|
advancedMenu.addItem(punctItem)
|
|
|
|
let vocabItem = NSMenuItem(title: "📚 Domain Vocabulary Enhancement", action: #selector(toggleDomainVocabAction(_:)), keyEquivalent: "")
|
|
vocabItem.target = self
|
|
vocabItem.state = SonioxSettings.shared.domainVocabulary ? .on : .off
|
|
advancedMenu.addItem(vocabItem)
|
|
|
|
let trailingSpaceItem = NSMenuItem(title: "␣ Add Trailing Space", action: #selector(toggleTrailingSpaceAction(_:)), keyEquivalent: "")
|
|
trailingSpaceItem.target = self
|
|
trailingSpaceItem.state = SonioxSettings.shared.addTrailingSpace ? .on : .off
|
|
advancedMenu.addItem(trailingSpaceItem)
|
|
|
|
let normalizeItem = NSMenuItem(title: "✨ Persian Text Normalization", action: #selector(toggleNormalizeAction(_:)), keyEquivalent: "")
|
|
normalizeItem.target = self
|
|
normalizeItem.state = SonioxSettings.shared.normalizeText ? .on : .off
|
|
advancedMenu.addItem(normalizeItem)
|
|
|
|
let soundItem = NSMenuItem(title: "🔊 Sound Feedback", action: #selector(toggleSoundsAction(_:)), keyEquivalent: "")
|
|
soundItem.target = self
|
|
soundItem.state = SonioxSettings.shared.playSounds ? .on : .off
|
|
advancedMenu.addItem(soundItem)
|
|
|
|
advancedMenu.addItem(NSMenuItem.separator())
|
|
|
|
let editCmdsItem = NSMenuItem(title: "📝 Customize Voice Commands (JSON)...", action: #selector(editCustomCommandsAction), keyEquivalent: "")
|
|
editCmdsItem.target = self
|
|
advancedMenu.addItem(editCmdsItem)
|
|
|
|
let reloadCmdsItem = NSMenuItem(title: "🔄 Reload Custom Commands", action: #selector(reloadCustomCommandsAction), keyEquivalent: "")
|
|
reloadCmdsItem.target = self
|
|
advancedMenu.addItem(reloadCmdsItem)
|
|
|
|
let cheatSheetItem = NSMenuItem(title: "📖 Voice Commands Cheat Sheet...", action: #selector(openCheatSheetAction), keyEquivalent: "")
|
|
cheatSheetItem.target = self
|
|
advancedMenu.addItem(cheatSheetItem)
|
|
|
|
advancedMenu.addItem(NSMenuItem.separator())
|
|
|
|
let viewLogItem = NSMenuItem(title: "📜 View Voice Commands Log...", action: #selector(viewVoiceLogAction), keyEquivalent: "")
|
|
viewLogItem.target = self
|
|
advancedMenu.addItem(viewLogItem)
|
|
|
|
let openFolderItem = NSMenuItem(title: "📁 Open Logs Folder...", action: #selector(openLogFolderAction), keyEquivalent: "")
|
|
openFolderItem.target = self
|
|
advancedMenu.addItem(openFolderItem)
|
|
|
|
let clearLogItem = NSMenuItem(title: "🗑️ Clear Voice Commands Log", action: #selector(clearVoiceLogAction), keyEquivalent: "")
|
|
clearLogItem.target = self
|
|
advancedMenu.addItem(clearLogItem)
|
|
|
|
advancedMenu.addItem(NSMenuItem.separator())
|
|
|
|
let resetWindowMemItem = NSMenuItem(title: "🖥️ Reset Window Memory (Default Full-Screen)", action: #selector(resetWindowMemoryAction), keyEquivalent: "")
|
|
resetWindowMemItem.target = self
|
|
advancedMenu.addItem(resetWindowMemItem)
|
|
|
|
let advancedMenuItem = NSMenuItem(title: "🛠️ Advanced Options", action: nil, keyEquivalent: "")
|
|
advancedMenuItem.submenu = advancedMenu
|
|
menu.addItem(advancedMenuItem)
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
// 9. System Permissions
|
|
let micStatus = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
let micTitle = (micStatus == .authorized) ? "✅ Microphone Access: Granted" : "⚠️ Microphone Access: Click to Grant"
|
|
let micItem = NSMenuItem(title: micTitle, action: #selector(openMicrophoneSettings), keyEquivalent: "")
|
|
micItem.target = self
|
|
menu.addItem(micItem)
|
|
|
|
let isAxTrusted = AXIsProcessTrusted()
|
|
let axTitle = isAxTrusted ? "✅ Accessibility: Granted" : "⚠️ Accessibility: Click to Grant"
|
|
let axItem = NSMenuItem(title: axTitle, action: #selector(openAccessibilitySettings), keyEquivalent: "")
|
|
axItem.target = self
|
|
menu.addItem(axItem)
|
|
|
|
let launchLogin = SonioxSettings.shared.launchAtLogin
|
|
let launchItem = NSMenuItem(title: "🚀 Launch at Login", action: #selector(toggleLaunchAtLoginAction(_:)), keyEquivalent: "")
|
|
launchItem.target = self
|
|
launchItem.state = launchLogin ? .on : .off
|
|
menu.addItem(launchItem)
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
// 10. Reset to Defaults
|
|
let resetItem = NSMenuItem(title: "🔄 Reset to Recommended Defaults", action: #selector(resetDefaultsAction), keyEquivalent: "")
|
|
resetItem.target = self
|
|
menu.addItem(resetItem)
|
|
|
|
// 11. About & Quit
|
|
let aboutItem = NSMenuItem(title: "ℹ️ About Soniox Voice", action: #selector(aboutAction), keyEquivalent: "")
|
|
aboutItem.target = self
|
|
menu.addItem(aboutItem)
|
|
|
|
let quitItem = NSMenuItem(title: "❌ Quit", action: #selector(quitAction), keyEquivalent: "q")
|
|
quitItem.target = self
|
|
menu.addItem(quitItem)
|
|
|
|
statusItem?.menu = menu
|
|
}
|
|
|
|
private func buildHistorySubmenu() -> NSMenu {
|
|
let historyMenu = NSMenu()
|
|
let items = HistoryManager.shared.getRecentEntries(limit: 35)
|
|
|
|
if items.isEmpty {
|
|
let emptyItem = NSMenuItem(title: "No dictation history yet", action: nil, keyEquivalent: "")
|
|
emptyItem.isEnabled = false
|
|
historyMenu.addItem(emptyItem)
|
|
} else {
|
|
let count = HistoryManager.shared.totalCount()
|
|
let header = NSMenuItem(title: "— Recent Transcriptions (Total: \(count)) —", action: nil, keyEquivalent: "")
|
|
header.isEnabled = false
|
|
historyMenu.addItem(header)
|
|
historyMenu.addItem(NSMenuItem.separator())
|
|
|
|
for item in items {
|
|
let timeStr = dateFormatter.string(from: item.timestamp)
|
|
let preview = item.text.count > 45 ? String(item.text.prefix(45)) + "…" : item.text
|
|
let title = "\(timeStr) \(preview)"
|
|
let menuItem = NSMenuItem(title: title, action: #selector(historyItemClicked(_:)), keyEquivalent: "")
|
|
menuItem.target = self
|
|
menuItem.representedObject = item.text
|
|
menuItem.toolTip = "\(item.text)\n\n(Click to copy & paste)"
|
|
historyMenu.addItem(menuItem)
|
|
}
|
|
|
|
historyMenu.addItem(NSMenuItem.separator())
|
|
|
|
let copyAllItem = NSMenuItem(title: "📋 Copy All History to Clipboard", action: #selector(copyAllHistoryAction), keyEquivalent: "")
|
|
copyAllItem.target = self
|
|
historyMenu.addItem(copyAllItem)
|
|
|
|
let clearItem = NSMenuItem(title: "🗑️ Clear History (All \(count) Items)", action: #selector(clearHistoryAction), keyEquivalent: "")
|
|
clearItem.target = self
|
|
historyMenu.addItem(clearItem)
|
|
}
|
|
|
|
return historyMenu
|
|
}
|
|
|
|
public func menuWillOpen(_ menu: NSMenu) {
|
|
// Refresh menu dynamically on hover/open to show live history & devices
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func toggleRecordAction() {
|
|
onToggleRecording?()
|
|
}
|
|
|
|
@objc private func selectModeAction(_ sender: NSMenuItem) {
|
|
if let mode = sender.representedObject as? DictationMode {
|
|
HotkeyManager.shared.currentMode = mode
|
|
buildMenu()
|
|
}
|
|
}
|
|
|
|
@objc private func selectHotkeyAction(_ sender: NSMenuItem) {
|
|
if let preset = sender.representedObject as? HotkeyPreset {
|
|
HotkeyManager.shared.currentPreset = preset
|
|
buildMenu()
|
|
}
|
|
}
|
|
|
|
@objc private func selectLanguageAction(_ sender: NSMenuItem) {
|
|
if let lang = sender.representedObject as? RecognitionLanguage {
|
|
SonioxSettings.shared.language = lang
|
|
buildMenu()
|
|
}
|
|
}
|
|
|
|
@objc private func selectAudioDeviceAction(_ sender: NSMenuItem) {
|
|
let deviceID = sender.representedObject as? String
|
|
SonioxSettings.shared.preferredAudioDeviceID = deviceID
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func selectNumberFormatAction(_ sender: NSMenuItem) {
|
|
if let format = sender.representedObject as? NumberFormatOption {
|
|
SonioxSettings.shared.numberFormat = format
|
|
buildMenu()
|
|
}
|
|
}
|
|
|
|
@objc private func historyItemClicked(_ sender: NSMenuItem) {
|
|
guard let text = sender.representedObject as? String else { return }
|
|
let pb = NSPasteboard.general
|
|
pb.clearContents()
|
|
pb.setString(text, forType: .string)
|
|
|
|
// Instant paste
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
let source = CGEventSource(stateID: .combinedSessionState)
|
|
let vKeyCode: CGKeyCode = 0x09
|
|
if let down = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: true),
|
|
let up = CGEvent(keyboardEventSource: source, virtualKey: vKeyCode, keyDown: false) {
|
|
down.flags = .maskCommand
|
|
up.flags = .maskCommand
|
|
down.post(tap: .cghidEventTap)
|
|
up.post(tap: .cghidEventTap)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func copyAllHistoryAction() {
|
|
let all = HistoryManager.shared.getAllEntries()
|
|
guard !all.isEmpty else { return }
|
|
|
|
let formatted = all.map { "[\(self.dateFormatter.string(from: $0.timestamp))] \($0.text)" }.joined(separator: "\n")
|
|
let pb = NSPasteboard.general
|
|
pb.clearContents()
|
|
pb.setString(formatted, forType: .string)
|
|
}
|
|
|
|
@objc private func clearHistoryAction() {
|
|
HistoryManager.shared.clearAll()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func toggleVoiceCommandsAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.voiceCommands.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func togglePunctuationAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.voicePunctuation.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func toggleDomainVocabAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.domainVocabulary.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func toggleTrailingSpaceAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.addTrailingSpace.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func toggleNormalizeAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.normalizeText.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func toggleSoundsAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.playSounds.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func editCustomCommandsAction() {
|
|
CustomCommandManager.shared.openConfigFileInEditor()
|
|
}
|
|
|
|
@objc private func reloadCustomCommandsAction() {
|
|
CustomCommandManager.shared.loadCommands()
|
|
buildMenu()
|
|
let alert = NSAlert()
|
|
alert.messageText = "Custom Commands Reloaded"
|
|
alert.informativeText = "Successfully reloaded custom voice commands from custom_commands.json."
|
|
alert.alertStyle = .informational
|
|
alert.addButton(withTitle: "OK")
|
|
alert.runModal()
|
|
}
|
|
|
|
@objc private func openCheatSheetAction() {
|
|
let helpText = """
|
|
=== Soniox Voice Commands Cheat Sheet ===
|
|
|
|
[Applications & Windows]
|
|
• /settings OR /ستینگ -> Open macOS Settings
|
|
• /antigravity ide OR /آنتی گرویتی آیدی ای -> Open Antigravity IDE
|
|
• /antigravity OR /آنتی گرویتی -> Open Antigravity
|
|
• /firefox OR /فایرفاکس -> Open Firefox
|
|
• /chrome OR /کروم -> Open Google Chrome
|
|
• /telegram OR /تلگرام -> Open Telegram
|
|
• /left OR /چپ -> Tile window to Left half
|
|
• /right OR /راست -> Tile window to Right half
|
|
• /maximize OR /تمام صفحه -> Maximize window
|
|
• /center OR /وسط -> Center window on screen
|
|
• /hide OR /هاید -> Hide frontmost app
|
|
• /quit OR /ببند -> Quit frontmost app
|
|
|
|
[Browser & App Tabs / Accounts]
|
|
• /telegram 1..3 OR /تلگرام ۱..۳ -> Open Telegram & Switch to Account 1..3
|
|
• /firefox 1..9 OR /فایرفاکس ۱..۹ -> Open Firefox & Switch to Tab 1..9
|
|
• /chrome 1..9 OR /کروم ۱..۹ -> Open Chrome & Switch to Tab 1..9
|
|
• /safari 1..9 OR /سافاری ۱..۹ -> Open Safari & Switch to Tab 1..9
|
|
• /tab 1..9 OR /تب ۱..۹ -> Jump to Tab 1..9 in active app
|
|
• /new tab OR /تب جدید -> Open new tab (Cmd+T)
|
|
• /close tab OR /بستن تب -> Close active tab (Cmd+W)
|
|
• /reopen tab OR /ری اپن تب -> Reopen closed tab (Cmd+Shift+T)
|
|
• /next tab / /prev tab -> Switch tabs
|
|
• /reload OR /رفرش -> Reload web page (Cmd+R)
|
|
• /search [query] OR /سرچ [موضوع] -> Search on Google
|
|
• /youtube [query] OR /یوتیوب [موضوع] -> Search on YouTube
|
|
|
|
[System Controls]
|
|
• /volume up OR /صدا زیاد -> Increase volume (+15%)
|
|
• /volume down OR /صدا کم -> Decrease volume (-15%)
|
|
• /volume 50 OR /صدا ۵۰ -> Set volume to 50%
|
|
• /mute OR /میوت -> Toggle sound mute
|
|
• /lock OR /قفل -> Lock screen
|
|
• /screenshot OR /اسکرین شات -> Interactive screenshot
|
|
• /dark mode OR /دارک مود -> Toggle Dark / Light mode
|
|
|
|
[Editing & Clipboard]
|
|
• /copy OR /کپی -> Copy selection (Cmd+C)
|
|
• /paste OR /پیست -> Paste (Cmd+V)
|
|
• /cut OR /کات -> Cut selection (Cmd+X)
|
|
• /select all OR /انتخاب همه -> Select All (Cmd+A)
|
|
• /undo OR /آندو -> Undo (Cmd+Z)
|
|
• /redo OR /ریدو -> Redo (Cmd+Shift+Z)
|
|
• /save OR /سیو -> Save document (Cmd+S)
|
|
• /enter OR /اینتر -> Press Return key
|
|
|
|
[Custom Voice Commands]
|
|
• Customize anytime via 'custom_commands.json' under Advanced Options!
|
|
"""
|
|
|
|
let alert = NSAlert()
|
|
alert.messageText = "Soniox Voice Commands Reference"
|
|
alert.informativeText = helpText
|
|
alert.alertStyle = .informational
|
|
alert.addButton(withTitle: "Got it!")
|
|
alert.runModal()
|
|
}
|
|
|
|
@objc private func viewVoiceLogAction() {
|
|
VoiceCommandLogger.shared.openLogInViewer()
|
|
}
|
|
|
|
@objc private func openLogFolderAction() {
|
|
VoiceCommandLogger.shared.openLogFolder()
|
|
}
|
|
|
|
@objc private func clearVoiceLogAction() {
|
|
VoiceCommandLogger.shared.clearLog()
|
|
let alert = NSAlert()
|
|
alert.messageText = "Voice Log Cleared"
|
|
alert.informativeText = "The voice commands debug log has been cleared."
|
|
alert.alertStyle = .informational
|
|
alert.addButton(withTitle: "OK")
|
|
alert.runModal()
|
|
}
|
|
|
|
@objc private func resetWindowMemoryAction() {
|
|
WindowStateManager.shared.resetAllPreferences()
|
|
let alert = NSAlert()
|
|
alert.messageText = "Window Memory Reset"
|
|
alert.informativeText = "All apps will now open in Full Screen by default unless manually exited."
|
|
alert.alertStyle = .informational
|
|
alert.addButton(withTitle: "OK")
|
|
alert.runModal()
|
|
}
|
|
|
|
@objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) {
|
|
SonioxSettings.shared.launchAtLogin.toggle()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func resetDefaultsAction() {
|
|
SonioxSettings.shared.resetToDefaults()
|
|
buildMenu()
|
|
}
|
|
|
|
@objc private func openMicrophoneSettings() {
|
|
if AVCaptureDevice.authorizationStatus(for: .audio) == .notDetermined {
|
|
AVCaptureDevice.requestAccess(for: .audio) { _ in
|
|
DispatchQueue.main.async {
|
|
self.buildMenu()
|
|
}
|
|
}
|
|
} else {
|
|
let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")!
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
@objc private func openAccessibilitySettings() {
|
|
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
|
|
AXIsProcessTrustedWithOptions(options)
|
|
let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
|
|
@objc private func aboutAction() {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Soniox Voice for macOS"
|
|
alert.informativeText = "High-accuracy, real-time speech-to-text dictation powered by Soniox AI.\n\nOptimized for macOS Apple Silicon."
|
|
alert.alertStyle = .informational
|
|
alert.addButton(withTitle: "OK")
|
|
alert.runModal()
|
|
}
|
|
|
|
@objc private func quitAction() {
|
|
NSApplication.shared.terminate(nil)
|
|
}
|
|
}
|