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
35 changes: 34 additions & 1 deletion Sources/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class AppState: ObservableObject, @unchecked Sendable {
private let customContextPromptStorageKey = "custom_context_prompt"
private let customSystemPromptLastModifiedStorageKey = "custom_system_prompt_last_modified"
private let customContextPromptLastModifiedStorageKey = "custom_context_prompt_last_modified"
private let hotkeyHoldDelaySecondsStorageKey = "hotkey_hold_delay_seconds"
private let transcribingIndicatorDelay: TimeInterval = 1.0
let maxPipelineHistoryCount = 20

Expand Down Expand Up @@ -73,6 +74,14 @@ final class AppState: ObservableObject, @unchecked Sendable {
}
}

@Published var hotkeyHoldDelaySeconds: Int {
didSet {
let clamped = min(5, max(0, hotkeyHoldDelaySeconds))
if clamped != hotkeyHoldDelaySeconds { hotkeyHoldDelaySeconds = clamped; return }
UserDefaults.standard.set(hotkeyHoldDelaySeconds, forKey: hotkeyHoldDelaySecondsStorageKey)
}
}

@Published var customVocabulary: String {
didSet {
UserDefaults.standard.set(customVocabulary, forKey: customVocabularyStorageKey)
Expand Down Expand Up @@ -146,6 +155,7 @@ final class AppState: ObservableObject, @unchecked Sendable {
private var hasShownScreenshotPermissionAlert = false
private var audioDeviceListenerBlock: AudioObjectPropertyListenerBlock?
private let pipelineHistoryStore = PipelineHistoryStore()
private var holdToRecordWorkItem: DispatchWorkItem?

init() {
let hasCompletedSetup = UserDefaults.standard.bool(forKey: "hasCompletedSetup")
Expand All @@ -172,11 +182,20 @@ final class AppState: ObservableObject, @unchecked Sendable {

let selectedMicrophoneID = UserDefaults.standard.string(forKey: selectedMicrophoneStorageKey) ?? "default"

let hotkeyHoldDelaySeconds: Int
if UserDefaults.standard.object(forKey: "hotkey_hold_delay_seconds") == nil {
hotkeyHoldDelaySeconds = 1
} else {
let raw = UserDefaults.standard.integer(forKey: "hotkey_hold_delay_seconds")
hotkeyHoldDelaySeconds = min(5, max(0, raw))
}

self.contextService = AppContextService(apiKey: apiKey, baseURL: apiBaseURL, customContextPrompt: customContextPrompt)
self.hasCompletedSetup = hasCompletedSetup
self.apiKey = apiKey
self.apiBaseURL = apiBaseURL
self.selectedHotkey = selectedHotkey
self.hotkeyHoldDelaySeconds = hotkeyHoldDelaySeconds
self.customVocabulary = customVocabulary
self.customSystemPrompt = customSystemPrompt
self.customContextPrompt = customContextPrompt
Expand Down Expand Up @@ -410,10 +429,24 @@ final class AppState: ObservableObject, @unchecked Sendable {
private func handleHotkeyDown() {
os_log(.info, log: recordingLog, "handleHotkeyDown() fired, isRecording=%{public}d, isTranscribing=%{public}d", isRecording, isTranscribing)
guard !isRecording && !isTranscribing else { return }
startRecording()
holdToRecordWorkItem?.cancel()
holdToRecordWorkItem = nil
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
guard !self.isRecording && !self.isTranscribing else { return }
self.holdToRecordWorkItem = nil
self.startRecording()
}
holdToRecordWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(hotkeyHoldDelaySeconds), execute: workItem)
}

private func handleHotkeyUp() {
if holdToRecordWorkItem != nil {
holdToRecordWorkItem?.cancel()
holdToRecordWorkItem = nil
return
}
guard isRecording else { return }
stopAndTranscribe()
}
Expand Down
22 changes: 22 additions & 0 deletions Sources/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ struct GeneralSettingsView: View {
SettingsCard("Push-to-Talk Key", icon: "keyboard.fill") {
hotkeySection
}
SettingsCard("Hold delay", icon: "timer") {
holdDelaySection
}
SettingsCard("Microphone", icon: "mic.fill") {
microphoneSection
}
Expand Down Expand Up @@ -506,6 +509,25 @@ struct GeneralSettingsView: View {
}
}

// MARK: Hold delay

private var holdDelaySection: some View {
VStack(alignment: .leading, spacing: 10) {
Text("How long to hold the key before recording starts. 0 = no delay.")
.font(.caption)
.foregroundStyle(.secondary)

Picker("Hold delay", selection: $appState.hotkeyHoldDelaySeconds) {
ForEach(0...5, id: \.self) { seconds in
Text(seconds == 0 ? "No delay" : (seconds == 1 ? "1 second" : "\(seconds) seconds"))
.tag(seconds)
}
}
.pickerStyle(.menu)
.frame(maxWidth: 200)
}
}

// MARK: Microphone

private var microphoneSection: some View {
Expand Down