Skip to content

Commit ab261ec

Browse files
committed
Global shortcuts
1 parent e7c4564 commit ab261ec

3 files changed

Lines changed: 291 additions & 20 deletions

File tree

notch-prompter/PrompterView.swift

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -230,25 +230,34 @@ struct PrompterContentView: View {
230230
Spacer()
231231

232232
HStack(spacing: 8) {
233-
// Play/Pause button
234-
Button(action: {
235-
if viewModel.isPlaying {
236-
viewModel.pause()
237-
} else {
238-
viewModel.play()
233+
// Play
234+
if viewModel.voiceActivation {
235+
Button(action: {
236+
openSettingsWindow() // TODO: open voice settings
237+
}) {
238+
Image(systemName: "microphone.circle.fill")
239+
.font(.system(size: 22))
240+
.foregroundColor(.orange)
241+
.shadow(color: .black.opacity(0.5), radius: 4, x: 0, y: 2)
239242
}
240-
}) {
241-
Image(systemName: viewModel.isPlaying ? "pause.circle.fill" : "play.circle.fill")
242-
.font(.system(size: 22))
243-
.foregroundColor(.white)
244-
.shadow(color: .black.opacity(0.5), radius: 4, x: 0, y: 2)
243+
.disabled(true)
244+
.buttonStyle(.plain)
245+
} else {
246+
Button(action: {
247+
if viewModel.isPlaying {
248+
viewModel.pause()
249+
} else {
250+
viewModel.play()
251+
}
252+
}) {
253+
Image(systemName: viewModel.isPlaying ? "pause.circle.fill" : "play.circle.fill")
254+
.font(.system(size: 22))
255+
.foregroundColor(.white)
256+
.shadow(color: .black.opacity(0.5), radius: 4, x: 0, y: 2)
257+
}
258+
.buttonStyle(.plain)
245259
}
246-
.buttonStyle(.plain)
247-
.disabled(viewModel.voiceActivation)
248-
.opacity(viewModel.voiceActivation ? 0.5 : 1.0)
249-
.help(viewModel.voiceActivation ? "Disabled during voice activation" : (viewModel.isPlaying ? "Pause" : "Play"))
250-
251-
// Back button (scroll back)
260+
// SCroll back
252261
Button(action: {
253262
viewModel.scrollBack()
254263
}) {
@@ -260,7 +269,7 @@ struct PrompterContentView: View {
260269
.buttonStyle(.plain)
261270
.help("Scroll back a few lines")
262271

263-
// Settings button
272+
// Settings
264273
Button(action: {
265274
openSettingsWindow()
266275
}) {
@@ -296,7 +305,7 @@ struct PrompterContentView: View {
296305
private func openSettingsWindow() {
297306
// Activate the app
298307
NSApp.activate(ignoringOtherApps: true)
299-
308+
300309
// Try to find existing settings window first
301310
for window in NSApp.windows {
302311
if window.title == "NotchPrompter" {

notch-prompter/PrompterViewModel.swift

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import CoreVideo
44
import AVFoundation
55
import Accelerate
66
import SwiftUI
7+
import HotKey
78

89
final 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

Comments
 (0)