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: 35 additions & 0 deletions KoeApp/Koe/AppDelegate/SPAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ - (void)hotkeyMonitorDidDetectHoldStart {
NSLog(@"[Koe] Hold start detected");
[self stopNumberKeyMonitoring];
[self stopAnyKeyDismissMonitoring];
[self startEscapeKeyMonitoring];
[self.overlayPanel hideTemplateButtons];
self.showingError = NO;
[self cancelPendingSessionEnd];
Expand All @@ -315,6 +316,7 @@ - (void)hotkeyMonitorDidDetectHoldStart {

- (void)hotkeyMonitorDidDetectHoldEnd {
NSLog(@"[Koe] Hold end detected");
[self stopEscapeKeyMonitoring];
[self.cuePlayer playStop];

// Keep recording for 300ms after Fn release to capture trailing speech,
Expand All @@ -335,6 +337,7 @@ - (void)hotkeyMonitorDidDetectTapStart {
NSLog(@"[Koe] Tap start detected");
[self stopNumberKeyMonitoring];
[self stopAnyKeyDismissMonitoring];
[self startEscapeKeyMonitoring];
[self.overlayPanel hideTemplateButtons];
self.showingError = NO;
[self cancelPendingSessionEnd];
Expand All @@ -355,6 +358,7 @@ - (void)hotkeyMonitorDidDetectTapStart {

- (void)hotkeyMonitorDidDetectTapEnd {
NSLog(@"[Koe] Tap end detected");
[self stopEscapeKeyMonitoring];
[self.cuePlayer playStop];

// Keep recording for 300ms after tap-end to capture trailing speech,
Expand Down Expand Up @@ -628,7 +632,9 @@ - (void)statusBarDidSelectSetupWizard {
self.setupWizard.delegate = self;
self.setupWizard.rustBridge = self.rustBridge;
}
[NSApp activateIgnoringOtherApps:YES];
[self.setupWizard showWindow:nil];
[self.setupWizard.window makeKeyAndOrderFront:nil];
}

- (void)statusBarDidSelectCheckForUpdates {
Expand Down Expand Up @@ -720,4 +726,33 @@ - (void)stopAnyKeyDismissMonitoring {
self.hotkeyMonitor.anyKeyDismissHandler = nil;
}

#pragma mark - ESC Cancel Monitoring

- (void)startEscapeKeyMonitoring {
// Default enabled: only disable if explicitly set to "false" in config.
char *raw = sp_config_get("hotkey.escape_cancel_enabled");
BOOL disabled = (raw && strcmp(raw, "false") == 0);
if (raw) sp_core_free_string(raw);
if (disabled) {
return;
}
__weak typeof(self) weakSelf = self;
self.hotkeyMonitor.escapeKeyHandler = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSLog(@"[Koe] ESC pressed — cancelling session");
[strongSelf stopEscapeKeyMonitoring];
[strongSelf cancelPendingSessionEnd];
[strongSelf.audioCaptureManager stopCapture];
[strongSelf.rustBridge cancelSession];
[strongSelf.hotkeyMonitor resetToIdle];
[strongSelf.overlayPanel updateState:@"idle"];
[strongSelf.statusBarManager updateState:@"idle"];
};
}

- (void)stopEscapeKeyMonitoring {
self.hotkeyMonitor.escapeKeyHandler = nil;
}

@end
5 changes: 5 additions & 0 deletions KoeApp/Koe/Hotkey/SPHotkeyMonitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,9 @@ typedef NS_ENUM(uint8_t, SPHotkeyMatchKind) {
/// Used to dismiss the overlay when the user resumes typing after text insertion.
@property (nonatomic, copy) void (^anyKeyDismissHandler)(void);

/// Optional block called when ESC (keyCode 53) is pressed during an active recording session.
/// The key event is NOT consumed — it passes through to the target app.
/// Use this to cancel the current session without producing output.
@property (nonatomic, copy) void (^escapeKeyHandler)(void);

@end
15 changes: 15 additions & 0 deletions KoeApp/Koe/Hotkey/SPHotkeyMonitor.m
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ static CGEventRef hotkeyEventCallback(CGEventTapProxy proxy,
return monitor.canConsumeGlobalKeyEvents ? NULL : event;
}

// ESC (keyCode 53) during recording → cancel session.
if (type == kCGEventKeyDown && keyCode == 53 && monitor.escapeKeyHandler) {
void (^handler)(void) = monitor.escapeKeyHandler;
dispatch_async(dispatch_get_main_queue(), ^{
handler();
});
return monitor.canConsumeGlobalKeyEvents ? NULL : event;
}

// Any keyDown (not handled by number keys above) dismisses the overlay.
// The event is NOT consumed — it passes through to the target app.
if (type == kCGEventKeyDown && monitor.anyKeyDismissHandler) {
Expand Down Expand Up @@ -340,6 +349,12 @@ - (BOOL)handleNSEvent:(NSEvent *)event {
return YES;
}

// ESC (keyCode 53) during recording → cancel session.
if (event.type == NSEventTypeKeyDown && keyCode == 53 && self.escapeKeyHandler) {
self.escapeKeyHandler();
return YES; // consume
}

// Some macOS versions send modifier keys as keyDown/keyUp events. Keep
// a direct keyDown/keyUp fallback for modifier-only triggers like Fn.
BOOL shouldHandleTriggerKeyEvent = NO;
Expand Down
8 changes: 8 additions & 0 deletions KoeApp/Koe/Localization/SPLocalization.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ extern NSNotificationName const SPLocalizationLanguageDidChangeNotification;
/// changes.
+ (void)invalidateCache;

/// Returns every language code shipped in the app bundle (each .lproj
/// folder containing Localizable.strings). E.g. @[@"en", @"zh-Hans", ...].
+ (NSArray<NSString *> *)availableLanguages;

/// Returns a human-readable endonym for a language code (e.g. "en" →
/// "English", "zh-Hans" → "简体中文"), suitable for a language picker.
+ (NSString *)displayNameForLanguage:(NSString *)code;

@end

NS_ASSUME_NONNULL_END
30 changes: 30 additions & 0 deletions KoeApp/Koe/Localization/SPLocalization.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,34 @@ + (NSBundle *)bundleForLanguage:(NSString *)language {
return [NSBundle mainBundle];
}

+ (NSArray<NSString *> *)availableLanguages {
NSFileManager *fm = [NSFileManager defaultManager];
NSString *resourcePath = [NSBundle mainBundle].resourcePath;
if (!resourcePath) return @[];

NSArray<NSString *> *entries = [fm contentsOfDirectoryAtPath:resourcePath error:NULL] ?: @[];
NSMutableArray<NSString *> *langs = [NSMutableArray array];
for (NSString *entry in entries) {
if (![entry.pathExtension isEqualToString:@"lproj"]) continue;
NSString *lproj = [resourcePath stringByAppendingPathComponent:entry];
NSString *strings = [lproj stringByAppendingPathComponent:@"Localizable.strings"];
if (![fm fileExistsAtPath:strings]) continue;
NSString *code = [entry stringByDeletingPathExtension];
[langs addObject:code];
}
[langs sortUsingSelector:@selector(caseInsensitiveCompare:)];
return langs;
}

+ (NSString *)displayNameForLanguage:(NSString *)code {
if (!code.length) return @"";
NSLocale *targetLocale = [NSLocale localeWithLocaleIdentifier:code];
NSString *name = [targetLocale localizedStringForLocaleIdentifier:code];
if (name.length) {
NSString *first = [[name substringToIndex:1] uppercaseStringWithLocale:targetLocale];
return [name stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:first];
}
return code;
}

@end
Loading