@@ -4,6 +4,7 @@ import CoreVideo
44import AVFoundation
55import Accelerate
66import SwiftUI
7+ import HotKey
78
89final class PrompterViewModel : ObservableObject {
910
@@ -56,6 +57,9 @@ final class PrompterViewModel: ObservableObject {
5657 @Published var horizontalAlignment : PrompterHorizontalAlignment = . center
5758 @Published var textAlignment : PrompterTextAlignment = . center
5859 @Published var showProgressBar : Bool = true
60+ @Published var enableGlobalKeyboardShortcuts : Bool = false
61+ @Published var speedIncrement : Double = 2.0
62+ @Published var manualScrollAmount : Double = 50.0
5963
6064 var backScrollAmount : Double = 20.0 // pixels to scroll back
6165
@@ -64,6 +68,14 @@ final class PrompterViewModel: ObservableObject {
6468 private var lastTick : CFTimeInterval ?
6569 private var cancellables : Set < AnyCancellable > = [ ]
6670
71+ // MARK: Global keyboard shortcuts
72+ private var playPauseHotKey : HotKey ?
73+ private var showHideHotKey : HotKey ?
74+ private var increaseSpeedHotKey : HotKey ?
75+ private var decreaseSpeedHotKey : HotKey ?
76+ private var scrollUpHotKey : HotKey ?
77+ private var scrollDownHotKey : HotKey ?
78+
6779 var audioMonitor : AudioMonitor ?
6880 @Published var showMicrophoneAlert : Bool = false
6981 @Published var audioThreshold : Float = 0.01
@@ -94,13 +106,17 @@ final class PrompterViewModel: ObservableObject {
94106 static let horizontalAlignment = " HorizontalAlignment "
95107 static let textAlignment = " TextAlignment "
96108 static let showProgressBar = " ShowProgressBar "
109+ static let enableGlobalKeyboardShortcuts = " EnableGlobalKeyboardShortcuts "
110+ static let speedIncrement = " SpeedIncrement "
111+ static let manualScrollAmount = " ManualScrollAmount "
97112 }
98113
99114 // MARK: Init
100115 init ( ) {
101116 loadSettings ( )
102117 startTimer ( )
103118 observeSettingsChanges ( )
119+ setupKeyboardShortcuts ( )
104120 $voiceActivation
105121 . removeDuplicates ( )
106122 . sink { [ weak self] enabled in
@@ -112,6 +128,18 @@ final class PrompterViewModel: ObservableObject {
112128 }
113129 }
114130 . store ( in: & cancellables)
131+
132+ $enableGlobalKeyboardShortcuts
133+ . removeDuplicates ( )
134+ . sink { [ weak self] enabled in
135+ guard let self = self else { return }
136+ if enabled {
137+ self . registerGlobalKeyboardShortcuts ( )
138+ } else {
139+ self . unregisterGlobalKeyboardShortcuts ( )
140+ }
141+ }
142+ . store ( in: & cancellables)
115143 }
116144
117145 private func requestMicrophoneAccessAndStart( ) {
@@ -186,6 +214,86 @@ final class PrompterViewModel: ObservableObject {
186214 offset = max ( 0 , offset - backScrollAmount)
187215 }
188216
217+ // MARK: Manual Scroll Control
218+ func scrollUp( ) {
219+ offset = max ( 0 , offset - manualScrollAmount)
220+ }
221+
222+ func scrollDown( ) {
223+ offset += manualScrollAmount
224+ }
225+
226+ // MARK: Speed Control
227+ func increaseSpeed( ) {
228+ speed = min ( 40 , speed + speedIncrement)
229+ }
230+
231+ func decreaseSpeed( ) {
232+ speed = max ( 1 , speed - speedIncrement)
233+ }
234+
235+ // MARK: Keyboard Shortcuts Setup
236+ private func setupKeyboardShortcuts( ) {
237+ // Initial registration if enabled
238+ if enableGlobalKeyboardShortcuts {
239+ registerGlobalKeyboardShortcuts ( )
240+ }
241+ }
242+
243+ private func registerGlobalKeyboardShortcuts( ) {
244+ // Control + Option + P: Play/Pause
245+ playPauseHotKey = HotKey ( key: . p, modifiers: [ . control, . option] )
246+ playPauseHotKey? . keyDownHandler = { [ weak self] in
247+ guard let self = self else { return }
248+ if !self . voiceActivation {
249+ if self . isPlaying {
250+ self . pause ( )
251+ } else {
252+ self . play ( )
253+ }
254+ }
255+ }
256+
257+ // Control + Option + H: Show/Hide
258+ showHideHotKey = HotKey ( key: . h, modifiers: [ . control, . option] )
259+ showHideHotKey? . keyDownHandler = { [ weak self] in
260+ self ? . isPrompterVisible. toggle ( )
261+ }
262+
263+ // Control + Option + Right Arrow: Increase Speed
264+ increaseSpeedHotKey = HotKey ( key: . rightArrow, modifiers: [ . control, . option] )
265+ increaseSpeedHotKey? . keyDownHandler = { [ weak self] in
266+ self ? . increaseSpeed ( )
267+ }
268+
269+ // Control + Option + Left Arrow: Decrease Speed
270+ decreaseSpeedHotKey = HotKey ( key: . leftArrow, modifiers: [ . control, . option] )
271+ decreaseSpeedHotKey? . keyDownHandler = { [ weak self] in
272+ self ? . decreaseSpeed ( )
273+ }
274+
275+ // Control + Option + Up Arrow: Scroll Up
276+ scrollUpHotKey = HotKey ( key: . upArrow, modifiers: [ . control, . option] )
277+ scrollUpHotKey? . keyDownHandler = { [ weak self] in
278+ self ? . scrollUp ( )
279+ }
280+
281+ // Control + Option + Down Arrow: Scroll Down
282+ scrollDownHotKey = HotKey ( key: . downArrow, modifiers: [ . control, . option] )
283+ scrollDownHotKey? . keyDownHandler = { [ weak self] in
284+ self ? . scrollDown ( )
285+ }
286+ }
287+
288+ private func unregisterGlobalKeyboardShortcuts( ) {
289+ playPauseHotKey = nil
290+ showHideHotKey = nil
291+ increaseSpeedHotKey = nil
292+ decreaseSpeedHotKey = nil
293+ scrollUpHotKey = nil
294+ scrollDownHotKey = nil
295+ }
296+
189297 // MARK: Timer
190298 private func startTimer( ) {
191299 timerCancellable = CADisplayLinkPublisher ( )
@@ -231,6 +339,9 @@ final class PrompterViewModel: ObservableObject {
231339 $horizontalAlignment. sink { [ weak self] _ in self ? . saveSettings ( ) } . store ( in: & cancellables)
232340 $textAlignment. sink { [ weak self] _ in self ? . saveSettings ( ) } . store ( in: & cancellables)
233341 $showProgressBar. sink { [ weak self] _ in self ? . saveSettings ( ) } . store ( in: & cancellables)
342+ $enableGlobalKeyboardShortcuts. sink { [ weak self] _ in self ? . saveSettings ( ) } . store ( in: & cancellables)
343+ $speedIncrement. sink { [ weak self] _ in self ? . saveSettings ( ) } . store ( in: & cancellables)
344+ $manualScrollAmount. sink { [ weak self] _ in self ? . saveSettings ( ) } . store ( in: & cancellables)
234345 }
235346
236347 private func loadSettings( ) {
@@ -280,6 +391,12 @@ final class PrompterViewModel: ObservableObject {
280391 }
281392
282393 showProgressBar = defaults. object ( forKey: Keys . showProgressBar) as? Bool ?? true
394+
395+ enableGlobalKeyboardShortcuts = defaults. object ( forKey: Keys . enableGlobalKeyboardShortcuts) as? Bool ?? false
396+ speedIncrement = defaults. double ( forKey: Keys . speedIncrement)
397+ if speedIncrement == 0 { speedIncrement = 2.0 }
398+ manualScrollAmount = defaults. double ( forKey: Keys . manualScrollAmount)
399+ if manualScrollAmount == 0 { manualScrollAmount = 50.0 }
283400 }
284401
285402 private func saveSettings( ) {
@@ -305,6 +422,9 @@ final class PrompterViewModel: ObservableObject {
305422 defaults. set ( horizontalAlignment. rawValue, forKey: Keys . horizontalAlignment)
306423 defaults. set ( textAlignment. rawValue, forKey: Keys . textAlignment)
307424 defaults. set ( showProgressBar, forKey: Keys . showProgressBar)
425+ defaults. set ( enableGlobalKeyboardShortcuts, forKey: Keys . enableGlobalKeyboardShortcuts)
426+ defaults. set ( speedIncrement, forKey: Keys . speedIncrement)
427+ defaults. set ( manualScrollAmount, forKey: Keys . manualScrollAmount)
308428 }
309429
310430 // MARK: Connector for display refresh
0 commit comments