From 1781a167df3d4d8727ede252f6bfd0a72701e210 Mon Sep 17 00:00:00 2001 From: neo Date: Thu, 21 May 2026 12:40:22 +0700 Subject: [PATCH 1/9] Fix UI not show on Tahoe 26.4.1 --- Sources/OpenKey/macOS/ModernKey/AppDelegate.m | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m index 011708e2..c783b0c0 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m @@ -62,6 +62,13 @@ @interface AppDelegate () @end +static void okdbg(NSString *fmt, ...) { + va_list ap; va_start(ap, fmt); + NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:ap]; + va_end(ap); + FILE *f = fopen("/tmp/openkey-debug.log", "a"); + if (f) { fprintf(f, "%s\n", msg.UTF8String); fclose(f); } +} @implementation AppDelegate { NSWindowController *_mainWC; @@ -110,25 +117,31 @@ -(void)askPermission { } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + okdbg(@"applicationDidFinishLaunching START"); appDelegate = self; - + [self registerSupportedNotification]; - + //set quick tooltip [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithInt: 50] forKey: @"NSInitialToolTipDelay"]; - + //check whether this app has been launched before that or not NSArray* runningApp = [[NSWorkspace sharedWorkspace] runningApplications]; if ([runningApp containsObject:OPENKEY_BUNDLE]) { //if already running -> exit + okdbg(@"already running, terminating"); [NSApp terminate:nil]; return; } - + okdbg(@"past dup check"); + // check if user granted Accessabilty permission - if (!MJAccessibilityIsEnabled()) { - [self askPermission]; - return; + BOOL axOk = MJAccessibilityIsEnabled(); + okdbg(@"accessibility=%d", axOk); + if (!axOk) { + // Don't early-return — let the UI still open so the user can interact. + // The event tap (typing) will be disabled until accessibility is granted. + MJAccessibilityOpenPanel(); } vShowIconOnDock = (int)[[NSUserDefaults standardUserDefaults] integerForKey:@"vShowIconOnDock"]; @@ -142,10 +155,12 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { //init dispatch_async(dispatch_get_main_queue(), ^{ - if (![OpenKeyManager initEventTap]) { + BOOL tapOk = [OpenKeyManager initEventTap]; + NSInteger showui = [[NSUserDefaults standardUserDefaults] integerForKey:@"ShowUIOnStartup"]; + okdbg(@"didFinishLaunching dispatch: tapOk=%d showui=%ld", tapOk, (long)showui); + if (!tapOk) { [self onControlPanelSelected]; } else { - NSInteger showui = [[NSUserDefaults standardUserDefaults] integerForKey:@"ShowUIOnStartup"]; if (showui == 1) { [self onControlPanelSelected]; } @@ -472,15 +487,35 @@ -(void)onQuickConvert { } -(void) onControlPanelSelected { + okdbg(@"onControlPanelSelected called, _mainWC=%@", _mainWC); if (_mainWC == nil) { _mainWC = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"OpenKey"]; + okdbg(@"instantiated _mainWC=%@ window=%@", _mainWC, _mainWC.window); } - //[OpenKeyManager showDockIcon:YES]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; if ([_mainWC.window isVisible]) { + [NSApp activateIgnoringOtherApps:YES]; + okdbg(@"already visible, frame=%@", NSStringFromRect(_mainWC.window.frame)); return; } + [NSApp activateIgnoringOtherApps:YES]; + [_mainWC showWindow:nil]; [_mainWC.window makeKeyAndOrderFront:nil]; - [_mainWC.window setLevel:NSFloatingWindowLevel]; + [_mainWC.window setLevel:NSNormalWindowLevel]; + + // Center on the screen with the menu bar (primary built-in display). + // Saved frames + multi-display setups can place the window off-screen + // or on a display the user isn't currently looking at. + [_mainWC.window setFrameAutosaveName:@""]; + NSScreen *primary = [NSScreen screens].firstObject ?: [NSScreen mainScreen]; + NSRect visible = primary.visibleFrame; + NSRect frame = _mainWC.window.frame; + NSPoint origin = NSMakePoint( + visible.origin.x + (visible.size.width - frame.size.width) / 2, + visible.origin.y + (visible.size.height - frame.size.height) / 2); + [_mainWC.window setFrameOrigin:origin]; + okdbg(@"primary visibleFrame=%@ moved to=%@", NSStringFromRect(visible), NSStringFromRect(_mainWC.window.frame)); + okdbg(@"after show: visible=%d frame=%@ screen=%@", [_mainWC.window isVisible], NSStringFromRect(_mainWC.window.frame), _mainWC.window.screen); } -(void) onMacroSelected { @@ -488,9 +523,11 @@ -(void) onMacroSelected { _macroWC = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"MacroWindow"]; } //[OpenKeyManager showDockIcon:YES]; - if ([_macroWC.window isVisible]) + if ([_macroWC.window isVisible]) { + [NSApp activateIgnoringOtherApps:YES]; return; - + } + [NSApp activateIgnoringOtherApps:YES]; [_macroWC.window makeKeyAndOrderFront:nil]; [_macroWC.window setLevel:NSFloatingWindowLevel]; } @@ -500,9 +537,11 @@ -(void) onAboutSelected { _aboutWC = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"AboutWindow"]; } //[OpenKeyManager showDockIcon:YES]; - if ([_aboutWC.window isVisible]) + if ([_aboutWC.window isVisible]) { + [NSApp activateIgnoringOtherApps:YES]; return; - + } + [NSApp activateIgnoringOtherApps:YES]; [_aboutWC.window makeKeyAndOrderFront:nil]; [_aboutWC.window setLevel:NSFloatingWindowLevel]; } From 8d763b5ff8439c20eb919b6ce35c248f90dfee92 Mon Sep 17 00:00:00 2001 From: neo Date: Thu, 21 May 2026 13:23:22 +0700 Subject: [PATCH 2/9] Support switch E <-> V using Fn (Globe) key on new Mac keyboard --- Sources/OpenKey/engine/Engine.h | 2 + Sources/OpenKey/macOS/ModernKey/AppDelegate.m | 45 +++++------------- .../ModernKey/Base.lproj/Main.storyboard | 47 ++++++++++++++----- Sources/OpenKey/macOS/ModernKey/OpenKey.mm | 2 + .../OpenKey/macOS/ModernKey/OpenKeyManager.m | 9 ++-- .../OpenKey/macOS/ModernKey/ViewController.m | 9 ++++ 6 files changed, 62 insertions(+), 52 deletions(-) diff --git a/Sources/OpenKey/engine/Engine.h b/Sources/OpenKey/engine/Engine.h index f288f8c5..f1e54905 100644 --- a/Sources/OpenKey/engine/Engine.h +++ b/Sources/OpenKey/engine/Engine.h @@ -32,12 +32,14 @@ #define HAS_OPTION(data) ((data & 0x200) ? 1 : 0) #define HAS_COMMAND(data) ((data & 0x400) ? 1 : 0) #define HAS_SHIFT(data) ((data & 0x800) ? 1 : 0) +#define HAS_FN(data) ((data & 0x1000) ? 1 : 0) #define GET_BOOL(data) (data ? 1 : 0) #define HAS_BEEP(data) (data & 0x8000) #define SET_SWITCH_KEY(data, key) data = (data & 0xFF) | key #define SET_CONTROL_KEY(data, val) data|=val<<8; #define SET_OPTION_KEY(data, val) data|=val<<9; #define SET_COMMAND_KEY(data, val) data|=val<<10; +#define SET_FN_KEY(data, val) data|=val<<12; //define these variable in your application //API diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m index c783b0c0..3216104d 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m @@ -62,13 +62,6 @@ @interface AppDelegate () @end -static void okdbg(NSString *fmt, ...) { - va_list ap; va_start(ap, fmt); - NSString *msg = [[NSString alloc] initWithFormat:fmt arguments:ap]; - va_end(ap); - FILE *f = fopen("/tmp/openkey-debug.log", "a"); - if (f) { fprintf(f, "%s\n", msg.UTF8String); fclose(f); } -} @implementation AppDelegate { NSWindowController *_mainWC; @@ -117,7 +110,6 @@ -(void)askPermission { } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - okdbg(@"applicationDidFinishLaunching START"); appDelegate = self; [self registerSupportedNotification]; @@ -129,38 +121,31 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { //check whether this app has been launched before that or not NSArray* runningApp = [[NSWorkspace sharedWorkspace] runningApplications]; if ([runningApp containsObject:OPENKEY_BUNDLE]) { //if already running -> exit - okdbg(@"already running, terminating"); [NSApp terminate:nil]; return; } - okdbg(@"past dup check"); // check if user granted Accessabilty permission - BOOL axOk = MJAccessibilityIsEnabled(); - okdbg(@"accessibility=%d", axOk); - if (!axOk) { - // Don't early-return — let the UI still open so the user can interact. - // The event tap (typing) will be disabled until accessibility is granted. - MJAccessibilityOpenPanel(); + if (!MJAccessibilityIsEnabled()) { + [self askPermission]; + return; } - + vShowIconOnDock = (int)[[NSUserDefaults standardUserDefaults] integerForKey:@"vShowIconOnDock"]; if (vShowIconOnDock) [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; - + if (vSwitchKeyStatus & 0x8000) NSBeep(); [self createStatusBarMenu]; - + //init dispatch_async(dispatch_get_main_queue(), ^{ - BOOL tapOk = [OpenKeyManager initEventTap]; - NSInteger showui = [[NSUserDefaults standardUserDefaults] integerForKey:@"ShowUIOnStartup"]; - okdbg(@"didFinishLaunching dispatch: tapOk=%d showui=%ld", tapOk, (long)showui); - if (!tapOk) { + if (![OpenKeyManager initEventTap]) { [self onControlPanelSelected]; } else { + NSInteger showui = [[NSUserDefaults standardUserDefaults] integerForKey:@"ShowUIOnStartup"]; if (showui == 1) { [self onControlPanelSelected]; } @@ -487,25 +472,19 @@ -(void)onQuickConvert { } -(void) onControlPanelSelected { - okdbg(@"onControlPanelSelected called, _mainWC=%@", _mainWC); if (_mainWC == nil) { _mainWC = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"OpenKey"]; - okdbg(@"instantiated _mainWC=%@ window=%@", _mainWC, _mainWC.window); } - [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; if ([_mainWC.window isVisible]) { [NSApp activateIgnoringOtherApps:YES]; - okdbg(@"already visible, frame=%@", NSStringFromRect(_mainWC.window.frame)); return; } [NSApp activateIgnoringOtherApps:YES]; - [_mainWC showWindow:nil]; [_mainWC.window makeKeyAndOrderFront:nil]; - [_mainWC.window setLevel:NSNormalWindowLevel]; + [_mainWC.window setLevel:NSFloatingWindowLevel]; - // Center on the screen with the menu bar (primary built-in display). - // Saved frames + multi-display setups can place the window off-screen - // or on a display the user isn't currently looking at. + // Center on the primary screen — saved frames from prior multi-display + // setups can land the window off-screen. [_mainWC.window setFrameAutosaveName:@""]; NSScreen *primary = [NSScreen screens].firstObject ?: [NSScreen mainScreen]; NSRect visible = primary.visibleFrame; @@ -514,8 +493,6 @@ -(void) onControlPanelSelected { visible.origin.x + (visible.size.width - frame.size.width) / 2, visible.origin.y + (visible.size.height - frame.size.height) / 2); [_mainWC.window setFrameOrigin:origin]; - okdbg(@"primary visibleFrame=%@ moved to=%@", NSStringFromRect(visible), NSStringFromRect(_mainWC.window.frame)); - okdbg(@"after show: visible=%d frame=%@ screen=%@", [_mainWC.window isVisible], NSStringFromRect(_mainWC.window.frame), _mainWC.window.screen); } -(void) onMacroSelected { diff --git a/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard b/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard index 1ad0aedf..848672a3 100644 --- a/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard +++ b/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard @@ -889,10 +889,10 @@ - + - + @@ -900,7 +900,7 @@ + + + + + + + + + + - + @@ -967,7 +987,7 @@ - + @@ -976,7 +996,7 @@ - + @@ -985,7 +1005,7 @@ - + @@ -994,7 +1014,7 @@ - + @@ -1012,7 +1032,7 @@ - + @@ -1692,6 +1712,7 @@ + diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm index 4292db2d..18e75a1d 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm +++ b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm @@ -500,6 +500,8 @@ bool checkHotKey(int hotKeyData, bool checkKeyCode=true) { return false; if (HAS_SHIFT(hotKeyData) ^ GET_BOOL(_lastFlag & kCGEventFlagMaskShift)) return false; + if (HAS_FN(hotKeyData) ^ GET_BOOL(_lastFlag & kCGEventFlagMaskSecondaryFn)) + return false; if (checkKeyCode) { if (GET_SWITCH_KEY(hotKeyData) != _keycode) return false; diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m index 2d1b665b..fbb24b04 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m +++ b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m @@ -71,12 +71,11 @@ +(BOOL)initEventTap { // Add to the current run loop. CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); - // Enable the event tap. + // Enable the event tap. The source is on the main run loop, which + // NSApplication is already running — calling CFRunLoopRun() here would + // start a nested loop that blocks AppKit from processing UI events. CGEventTapEnable(eventTap, true); - - // Set it all running. - CFRunLoopRun(); - + return YES; } diff --git a/Sources/OpenKey/macOS/ModernKey/ViewController.m b/Sources/OpenKey/macOS/ModernKey/ViewController.m index d4c5b73c..0f2a00d5 100644 --- a/Sources/OpenKey/macOS/ModernKey/ViewController.m +++ b/Sources/OpenKey/macOS/ModernKey/ViewController.m @@ -44,6 +44,7 @@ @implementation ViewController { __weak IBOutlet NSButton *CustomSwitchOption; __weak IBOutlet NSButton *CustomSwitchControl; __weak IBOutlet NSButton *CustomSwitchShift; + __weak IBOutlet NSButton *CustomSwitchFn; __weak IBOutlet MyTextField *CustomSwitchKey; __weak IBOutlet NSButton *CustomBeepSound; NSArray* tabviews, *tabbuttons; @@ -250,6 +251,13 @@ - (IBAction)onShiftSwitchKey:(NSButton *)sender { [[NSUserDefaults standardUserDefaults] setInteger:vSwitchKeyStatus forKey:@"SwitchKeyStatus"]; } +- (IBAction)onFnSwitchKey:(NSButton *)sender { + NSInteger val = [self setCustomValue:sender keyToSet:nil]; + vSwitchKeyStatus &= (~0x1000); + vSwitchKeyStatus |= val << 12; + [[NSUserDefaults standardUserDefaults] setInteger:vSwitchKeyStatus forKey:@"SwitchKeyStatus"]; +} + -(void)onMyTextFieldKeyChange:(unsigned short)keyCode character:(unsigned short)character { vSwitchKeyStatus &= 0xFFFFFF00; vSwitchKeyStatus |= keyCode; @@ -467,6 +475,7 @@ -(void)fillData { CustomSwitchOption.state = (vSwitchKeyStatus & 0x200) ? NSControlStateValueOn : NSControlStateValueOff; CustomSwitchCommand.state = (vSwitchKeyStatus & 0x400) ? NSControlStateValueOn : NSControlStateValueOff; CustomSwitchShift.state = (vSwitchKeyStatus & 0x800) ? NSControlStateValueOn : NSControlStateValueOff; + CustomSwitchFn.state = (vSwitchKeyStatus & 0x1000) ? NSControlStateValueOn : NSControlStateValueOff; CustomBeepSound.state = (vSwitchKeyStatus & 0x8000) ? NSControlStateValueOn : NSControlStateValueOff; [CustomSwitchKey setTextByChar:((vSwitchKeyStatus>>24) & 0xFF)]; From b702fa712f8b68cba7eb92bab521f0af085fcb3b Mon Sep 17 00:00:00 2001 From: Viet Quoc Date: Sat, 18 Jul 2026 17:24:18 +0700 Subject: [PATCH 3/9] Fix duplicated characters when typing after Spotlight is dismissed isSpotlightVisible() previously returned true whenever a window owned by "Spotlight" existed anywhere in the on-screen window list. A dismissed or fading Spotlight window can linger in that list, so OpenKey wrongly kept using the selection-replacement path (Shift+Left + overwrite) while the user was already typing in another app. On apps that don't cooperate with that path, the overwrite is appended instead of replacing, producing duplicated characters. Tighten the check to only count Spotlight as active when its window is actually being drawn (alpha > 0) and floats above normal app windows (window layer > 0), which excludes stale/fading leftover windows. Reproduced and verified fixed on macOS (Apple Silicon) with Telex + Unicode and "Fix browser/Excel suggestions" enabled: open Spotlight, type Vietnamese, dismiss, then type in another app. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012Ro8o88gTWKFHDPaXwCvzR --- Sources/OpenKey/macOS/ModernKey/OpenKey.mm | 25 +++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm index 1e6b6c14..a8e34126 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm +++ b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm @@ -176,12 +176,31 @@ BOOL containUnicodeCompoundApp(NSString* topApp) { } BOOL isSpotlightVisible() { + // Only treat Spotlight as active when its window is ACTUALLY visible and on top, + // not merely present on screen. A closed/fading Spotlight window can linger in the + // window list with alpha 0; using it there makes OpenKey wrongly switch to the + // selection-replacement path in other apps, which duplicates characters. We require: + // - alpha > 0 (window is really being drawn, not mid fade-out / stale) + // - window layer > 0 (Spotlight's panel floats above normal app windows) NSArray *windows = CFBridgingRelease(CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID)); for (NSDictionary *window in windows) { - if ([[window objectForKey:(__bridge NSString *)kCGWindowOwnerName] isEqualToString:@"Spotlight"]) { - return true; - } + if (![[window objectForKey:(__bridge NSString *)kCGWindowOwnerName] isEqualToString:@"Spotlight"]) + continue; + + // Skip windows that are not actually being drawn (alpha == 0 => stale/fading). + NSNumber* alpha = [window objectForKey:(__bridge NSString *)kCGWindowAlpha]; + if (alpha != nil && [alpha doubleValue] <= 0.0) + continue; + + // Spotlight's search field floats above normal app windows (layer > 0). + // Leftover / background Spotlight layers sit at the normal window level (0) + // or below; only the actual floating search panel should count. + NSNumber* layer = [window objectForKey:(__bridge NSString *)kCGWindowLayer]; + if (layer != nil && [layer intValue] <= 0) + continue; + + return true; } return false; } From 21b98e3660f6acdb3c31d2a805aa87d872b2123f Mon Sep 17 00:00:00 2001 From: Duy Huynh Date: Sat, 29 Aug 2026 00:12:18 +0700 Subject: [PATCH 4/9] fix(macOS): re-enable event tap after macOS disables it initEventTap creates an active event tap, so macOS puts a deadline on OpenKeyCallback. When the callback misses that deadline (sustained CPU load) or on kCGEventTapDisabledByUserInput, the system switches the tap off and reports it back through the callback as a pseudo event type. OpenKeyCallback never checked for those two types, and CGEventTapEnable was only ever called once during setup. Once macOS turned the tap off, nothing turned it back on: the menu bar still showed the current mode and the switch key still worked, but no keystroke was converted in any app until OpenKey was relaunched. Handle both notifications in the callback and re-enable the tap. The normal event path is unchanged. Refs #258 --- Sources/OpenKey/macOS/ModernKey/OpenKey.mm | 11 +++++++++++ Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm index 1e6b6c14..16cc9ea1 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm +++ b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm @@ -68,6 +68,9 @@ CGKeyCode _keycode; CGEventFlags _flag, _lastFlag = 0, _privateFlag; CGEventTapProxy _proxy; + + //defined in OpenKeyManager.m + extern void OpenKeyReEnableEventTap(void); Uint16 _newCharString[MAX_UNICODE_STRING]; Uint16 _newCharSize; @@ -599,6 +602,14 @@ CGKeyCode ConvertEventToKeyboadLayoutCompatKeyCode(CGEventRef keyEvent, CGKeyCod * MAIN Callback. */ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { + //macOS turns the tap off if this callback misses its deadline (heavy system load) + //or after certain user input, and tells us through these two pseudo event types. + //Without re-enabling here, OpenKey silently stops converting until it is relaunched. + if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) { + OpenKeyReEnableEventTap(); + return event; + } + //dont handle my event if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == CGEventSourceGetSourceStateID(myEventSource)) { return event; diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m index 97883f1a..4ac82862 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m +++ b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m @@ -77,6 +77,14 @@ +(BOOL)initEventTap { return YES; } +// Called from OpenKeyCallback when macOS reports that it turned our tap off. +// Only this file owns `eventTap`, so re-enabling has to happen here. +void OpenKeyReEnableEventTap(void) { + if (eventTap) { + CGEventTapEnable(eventTap, true); + } +} + +(BOOL)stopEventTap { if (_isInited) { //release all object CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); From c4c16a0df26afc2e86aed9bb55ac52ae42fe3a71 Mon Sep 17 00:00:00 2001 From: Nghia Pham Date: Mon, 12 Oct 2026 13:17:18 +0700 Subject: [PATCH 5/9] fix(feature/openkey-ce-modernize): fresh backspace events --- Sources/OpenKey/macOS/ModernKey/OpenKey.mm | 30 ++++++++++++---------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm index a56cb904..c538c3b8 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm +++ b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm @@ -61,8 +61,6 @@ CGEventSourceRef myEventSource = NULL; vKeyHookState* pData; - CGEventRef eventBackSpaceDown; - CGEventRef eventBackSpaceUp; UniChar _newChar, _newCharHi; CGEventRef _newEventDown, _newEventUp; CGKeyCode _keycode; @@ -120,8 +118,6 @@ void OpenKeyInit() { myEventSource = CGEventSourceCreate(kCGEventSourceStatePrivate); pData = (vKeyHookState*)vKeyInit(); - eventBackSpaceDown = CGEventCreateKeyboardEvent (myEventSource, 51, true); - eventBackSpaceUp = CGEventCreateKeyboardEvent (myEventSource, 51, false); //init and load macro data NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; @@ -388,15 +384,22 @@ void SendVirtualKey(const Byte& vKey) { CFRelease(eventVkeyUp); } + void PostBackspaceEvent() { + CGEventRef bsDown = CGEventCreateKeyboardEvent(myEventSource, 51, true); + CGEventRef bsUp = CGEventCreateKeyboardEvent(myEventSource, 51, false); + CGEventTapPostEvent(_proxy, bsDown); + CGEventTapPostEvent(_proxy, bsUp); + CFRelease(bsDown); + CFRelease(bsUp); + } + void SendBackspace() { - CGEventTapPostEvent(_proxy, eventBackSpaceDown); - CGEventTapPostEvent(_proxy, eventBackSpaceUp); - - if (IS_DOUBLE_CODE(vCodeTable)) { //VNI or Unicode Compound + PostBackspaceEvent(); + + if (IS_DOUBLE_CODE(vCodeTable) && !_syncKey.empty()) { //VNI or Unicode Compound if (_syncKey.back() > 1) { if (!(vCodeTable == 3 && containUnicodeCompoundApp(FRONT_APP))) { - CGEventTapPostEvent(_proxy, eventBackSpaceDown); - CGEventTapPostEvent(_proxy, eventBackSpaceUp); + PostBackspaceEvent(); } } _syncKey.pop_back(); @@ -413,8 +416,8 @@ void SendShiftAndLeftArrow() { CGEventTapPostEvent(_proxy, eventVkeyDown); CGEventTapPostEvent(_proxy, eventVkeyUp); - - if (IS_DOUBLE_CODE(vCodeTable)) { //VNI or Unicode Compound + + if (IS_DOUBLE_CODE(vCodeTable) && !_syncKey.empty()) { //VNI or Unicode Compound if (_syncKey.back() > 1) { if (!(vCodeTable == 3 && containUnicodeCompoundApp(FRONT_APP))) { CGEventTapPostEvent(_proxy, eventVkeyDown); @@ -757,8 +760,7 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e if (_syncKey.size() > 0) { if (_syncKey.back() > 1 && (vCodeTable == 2 || !containUnicodeCompoundApp(FRONT_APP))) { //send one more backspace - CGEventTapPostEvent(_proxy, eventBackSpaceDown); - CGEventTapPostEvent(_proxy, eventBackSpaceUp); + PostBackspaceEvent(); } _syncKey.pop_back(); } From 61ad14f6a4104ab04baa4caa6b1eb77d97ab9065 Mon Sep 17 00:00:00 2001 From: Nghia Pham Date: Mon, 12 Oct 2026 13:19:45 +0700 Subject: [PATCH 6/9] feat(feature/openkey-ce-modernize): OpenKey CE 2.1.0 Target macOS 13, hardened runtime, bundle ID com.nghia-pham.openkey-ce, SMAppService launch at login, legacy settings import, update check against the fork, release script that signs and notarizes. --- .gitignore | 2 + README.md | 24 ++++++++++++ .../macOS/ModernKey/AboutViewController.m | 2 +- Sources/OpenKey/macOS/ModernKey/AppDelegate.h | 2 +- Sources/OpenKey/macOS/ModernKey/AppDelegate.m | 13 ++++++- .../ModernKey/Base.lproj/Main.storyboard | 2 +- Sources/OpenKey/macOS/ModernKey/Info.plist | 4 +- .../macOS/ModernKey/MacroViewController.mm | 4 +- Sources/OpenKey/macOS/ModernKey/MyTextField.m | 2 +- .../OpenKey/macOS/ModernKey/OpenKeyManager.m | 28 ++------------ Sources/OpenKey/macOS/ModernKey/main.m | 20 ++++++++++ .../macOS/OpenKey.xcodeproj/project.pbxproj | 38 ++++++++++--------- Sources/OpenKey/macOS/release.sh | 33 ++++++++++++++++ version.json | 6 ++- 14 files changed, 126 insertions(+), 54 deletions(-) create mode 100755 Sources/OpenKey/macOS/release.sh diff --git a/.gitignore b/.gitignore index 7fd14bb2..6d1e1f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ /Sources/OpenKey/win32/OpenKey/OpenKey/x64/Debug /Sources/OpenKey/win32/OpenKey/OpenKey/x64/Release /Sources/OpenKey/win32/OpenKey/OpenKeyUpdate/x64/Release + +/Sources/OpenKey/macOS/build/ diff --git a/README.md b/README.md index fe15ed8c..628ba431 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ +# OpenKey CE (Community Edition) for macOS + +A community-maintained macOS build of [OpenKey](https://github.com/tuyenvm/OpenKey) by Mai Vũ Tuyên. The original project has not shipped a macOS release since 2.0.5 (2022); this fork rebuilds it for current macOS, signed with a Developer ID and notarized by Apple, so it opens without the "unidentified developer" warning. + +All credit for OpenKey goes to its author, Mai Vũ Tuyên. OpenKey CE stays GPL-3.0, like the original, and its full source is this repository. + +[Download OpenKey CE](https://github.com/llawliet11/OpenKey/releases/latest) + +## Changes from OpenKey 2.0.5 + +- Includes every fix merged upstream since 2.0.5: Spotlight composition and macOS UI (#325), vowel fixes for "á" and "huỵch" (#306), one instance per macOS user (#314). +- Includes open upstream PRs: re-enable the event tap after macOS disables it under load, which fixes typing dying until relaunch (#332); no duplicated characters after Spotlight closes (#329); Fn/Globe key as the language switch (#324). +- Creates a fresh backspace event per keystroke instead of reusing one, which fixes stuck typing in Apple Mail and WebKit apps (from #333). +- Launch at login uses `SMAppService`; the old helper app is gone. +- Requires macOS 13 Ventura or later; universal binary for Apple silicon and Intel. +- Bundle ID `com.nghia-pham.openkey-ce`, so it installs next to the original OpenKey. On first launch it imports settings and macros from OpenKey 2.0.5. Quit the original OpenKey before using CE: two input apps at once conflict. +- Update checks read this fork's `version.json` and open the Releases page; the closed-source in-app updater is removed. + +## Build from source + +Requires Xcode 15 or later. `Sources/OpenKey/macOS/release.sh` builds a universal app, signs it with the Developer ID identity in your keychain, notarizes and staples the app and the DMG. Set `NOTARY_PROFILE` to a `notarytool store-credentials` profile first. To build without signing, open `Sources/OpenKey/macOS/OpenKey.xcodeproj` and build the `OpenKey` scheme. + +--- + # [OpenKey](http://open-key.org) ### [Download bản mới nhất](https://github.com/tuyenvm/OpenKey/releases) diff --git a/Sources/OpenKey/macOS/ModernKey/AboutViewController.m b/Sources/OpenKey/macOS/ModernKey/AboutViewController.m index aaa721c4..3909712a 100644 --- a/Sources/OpenKey/macOS/ModernKey/AboutViewController.m +++ b/Sources/OpenKey/macOS/ModernKey/AboutViewController.m @@ -37,7 +37,7 @@ - (IBAction)onFanPage:(id)sender { } - (IBAction)onLatestReleaseVersion:(id)sender { - [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://github.com/tuyenvm/OpenKey/releases"]]; + [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://github.com/llawliet11/OpenKey/releases"]]; } - (IBAction)onCheckUpdateOnStartup:(NSButton *)sender { diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.h b/Sources/OpenKey/macOS/ModernKey/AppDelegate.h index 706dccd0..e4b2b744 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.h +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.h @@ -9,7 +9,7 @@ #import #import "ViewController.h" -#define OPENKEY_BUNDLE @"com.tuyenmai.openkey" +#define OPENKEY_BUNDLE [[NSBundle mainBundle] bundleIdentifier] @interface AppDelegate : NSObject diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m index 979f421f..22265d0f 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m @@ -320,8 +320,17 @@ -(void)loadDefaultConfig { } -(void)setRunOnStartup:(BOOL)val { - CFStringRef appId = (__bridge CFStringRef)@"com.tuyenmai.OpenKeyHelper"; - SMLoginItemSetEnabled(appId, val); + SMAppService *service = [SMAppService mainAppService]; + NSError *error = nil; + if (val && service.status != SMAppServiceStatusEnabled) { + if (![service registerAndReturnError:&error]) { + NSLog(@"[OpenKey] SMAppService register failed: %@", error); + } + } else if (!val && service.status == SMAppServiceStatusEnabled) { + if (![service unregisterAndReturnError:&error]) { + NSLog(@"[OpenKey] SMAppService unregister failed: %@", error); + } + } } -(void)setGrayIcon:(BOOL)val { diff --git a/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard b/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard index 848672a3..c7ca4439 100644 --- a/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard +++ b/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard @@ -777,7 +777,7 @@ - + diff --git a/Sources/OpenKey/macOS/ModernKey/Info.plist b/Sources/OpenKey/macOS/ModernKey/Info.plist index d2a8d1ef..6ce2115d 100644 --- a/Sources/OpenKey/macOS/ModernKey/Info.plist +++ b/Sources/OpenKey/macOS/ModernKey/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - OpenKey + OpenKey CE CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile @@ -33,7 +33,7 @@ NSAppleEventsUsageDescription Used for keyboard NSHumanReadableCopyright - Copyright © 2019 Tuyen Mai. All rights reserved. + Copyright © 2019 Tuyen Mai. OpenKey CE modifications © 2026 Nia D Pham. GPL-3.0. NSMainStoryboardFile Main NSPrincipalClass diff --git a/Sources/OpenKey/macOS/ModernKey/MacroViewController.mm b/Sources/OpenKey/macOS/ModernKey/MacroViewController.mm index 54e5998b..ffd20308 100644 --- a/Sources/OpenKey/macOS/ModernKey/MacroViewController.mm +++ b/Sources/OpenKey/macOS/ModernKey/MacroViewController.mm @@ -95,7 +95,7 @@ - (IBAction)onLoadFromFile:(id)sender { [alert addButtonWithTitle:@"Có"]; [alert addButtonWithTitle:@"Không"]; [alert setMessageText:@"Dữ liệu gõ tắt"]; - [alert setAlertStyle:NSCriticalAlertStyle]; + [alert setAlertStyle:NSAlertStyleCritical]; [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) { readFromFile(openPanel.URL.path.UTF8String, returnCode == 1000); [self saveAndReload]; @@ -121,7 +121,7 @@ - (void)showMessage:(NSString*)msg { [alert setInformativeText:msg]; [alert addButtonWithTitle:@"OK"]; [alert setMessageText:@"Gõ tắt"]; - [alert setAlertStyle:NSCriticalAlertStyle]; + [alert setAlertStyle:NSAlertStyleCritical]; [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) { }]; diff --git a/Sources/OpenKey/macOS/ModernKey/MyTextField.m b/Sources/OpenKey/macOS/ModernKey/MyTextField.m index 8bfe37f2..c216890c 100644 --- a/Sources/OpenKey/macOS/ModernKey/MyTextField.m +++ b/Sources/OpenKey/macOS/ModernKey/MyTextField.m @@ -25,7 +25,7 @@ - (BOOL)becomeFirstResponder { [self setKeyboardFocusRingNeedsDisplayInRect: [self bounds]]; if (!eventMonitor) { - eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^(NSEvent *event) { + eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown handler:^(NSEvent *event) { self.LastKeyCode = event.keyCode; self.LastKeyChar = event.characters.UTF8String[0]; return event; diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m index 4412ebd7..32400b71 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m +++ b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m @@ -159,7 +159,7 @@ +(void)showMessage:(NSWindow*)window message:(NSString*)msg subMsg:(NSString*)su +(void)checkNewVersion:(NSWindow*)parent callbackFunc:(CheckNewVersionCallback) callback { //load new version config NSURLSession *aSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; - [[aSession dataTaskWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/tuyenvm/OpenKey/master/version.json"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + [[aSession dataTaskWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/llawliet11/OpenKey/master/version.json"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (((NSHTTPURLResponse *)response).statusCode == 200) { if (data) { if(NSClassFromString(@"NSJSONSerialization")) { @@ -172,7 +172,7 @@ +(void)checkNewVersion:(NSWindow*)parent callbackFunc:(CheckNewVersionCallback) if(error) { } if([object isKindOfClass:[NSDictionary class]]) { NSDictionary *results = object; - NSDictionary *ver = [results valueForKey:@"latestVersion"]; + NSDictionary *ver = [results valueForKey:@"latestMacCEVersion"]; NSString* versionCodeString = [ver valueForKey:@"versionCode"]; int versionCode = (int)[versionCodeString integerValue]; int currentVersionCode = (int)[((NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleVersion"]) integerValue]; @@ -226,25 +226,7 @@ +(void)showUpdateMessage:(NSWindow*)parent needUpdating:(BOOL)needUpdating newVe } +(void)launchUpdateHelper { - //check update app has exist or not - NSError *copyError = nil; - NSString* target = [NSString stringWithFormat:@"%@/OpenKeyUpdate.app", [self getApplicationSupportFolder]]; - [[NSFileManager defaultManager] removeItemAtPath:target error:©Error]; - if (![[NSFileManager defaultManager] fileExistsAtPath:target]) { - [[NSFileManager defaultManager] createDirectoryAtPath:[self getApplicationSupportFolder] withIntermediateDirectories:YES attributes:nil error:nil]; - - if (![[NSFileManager defaultManager] copyItemAtPath:[self getUpdateBundlePath] toPath:target error:©Error]) { - NSLog(@"Error on copy"); - } - } - - NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; - NSURL *url = [NSURL fileURLWithPath:[workspace fullPathForApplication:target]]; - NSError *error = nil; - NSArray *arguments = [NSArray arrayWithObjects: @"yeah", nil]; - [workspace launchApplicationAtURL:url options:0 configuration:[NSDictionary dictionaryWithObject:arguments forKey:NSWorkspaceLaunchConfigurationArguments] error:&error]; - - [NSApp terminate:0]; //exit main app + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/llawliet11/OpenKey/releases/latest"]]; } +(NSString*)getApplicationSupportFolder { @@ -253,8 +235,4 @@ +(NSString*)getApplicationSupportFolder { return [NSString stringWithFormat:@"%@/OpenKey", applicationSupportDirectory]; } -+(NSString*)getUpdateBundlePath { - NSString *currentpath = [[NSBundle mainBundle] bundlePath]; - return [NSString stringWithFormat:@"%@/Contents/Library/LoginItems/OpenKeyUpdate.app", currentpath]; -} @end diff --git a/Sources/OpenKey/macOS/ModernKey/main.m b/Sources/OpenKey/macOS/ModernKey/main.m index 8a1be08f..7b1c1d82 100644 --- a/Sources/OpenKey/macOS/ModernKey/main.m +++ b/Sources/OpenKey/macOS/ModernKey/main.m @@ -9,6 +9,26 @@ #import #import "ViewController.h" +static NSString* const kLegacyOpenKeyDomain = @"com.tuyenmai.openkey"; + +static void importLegacyOpenKeySettings(void) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + if ([defaults boolForKey:@"NonFirstTime"] || [defaults boolForKey:@"ImportedLegacySettings"]) { + return; + } + NSDictionary *legacy = [defaults persistentDomainForName:kLegacyOpenKeyDomain]; + if (legacy.count > 0) { + NSString *ownDomain = [[NSBundle mainBundle] bundleIdentifier]; + NSMutableDictionary *merged = [legacy mutableCopy]; + [merged addEntriesFromDictionary:[defaults persistentDomainForName:ownDomain] ?: @{}]; + [defaults setPersistentDomain:merged forName:ownDomain]; + } + [defaults setBool:YES forKey:@"ImportedLegacySettings"]; +} + int main(int argc, const char * argv[]) { + @autoreleasepool { + importLegacyOpenKeySettings(); + } return NSApplicationMain(argc, argv); } diff --git a/Sources/OpenKey/macOS/OpenKey.xcodeproj/project.pbxproj b/Sources/OpenKey/macOS/OpenKey.xcodeproj/project.pbxproj index 8434abed..b26935da 100644 --- a/Sources/OpenKey/macOS/OpenKey.xcodeproj/project.pbxproj +++ b/Sources/OpenKey/macOS/OpenKey.xcodeproj/project.pbxproj @@ -504,7 +504,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -558,7 +558,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -569,24 +569,25 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = NO; - CODE_SIGN_IDENTITY = "Mac Developer"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 48; - DEVELOPMENT_TEAM = 8S7348QV8Q; + CURRENT_PROJECT_VERSION = 50; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; HEADER_SEARCH_PATHS = ""; INFOPLIST_FILE = ModernKey/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.10; - MARKETING_VERSION = 2.0.4; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "-lc++\n-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = com.tuyenmai.openkey; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.nghia-pham.openkey-ce"; + PRODUCT_NAME = "OpenKey CE"; PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Debug; @@ -595,24 +596,25 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = NO; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 48; - DEVELOPMENT_TEAM = 8S7348QV8Q; + CURRENT_PROJECT_VERSION = 50; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; HEADER_SEARCH_PATHS = ""; INFOPLIST_FILE = ModernKey/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.10; - MARKETING_VERSION = 2.0.4; + MACOSX_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "-lc++\n-lc++", ); - PRODUCT_BUNDLE_IDENTIFIER = com.tuyenmai.openkey; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.nghia-pham.openkey-ce"; + PRODUCT_NAME = "OpenKey CE"; PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Release; diff --git a/Sources/OpenKey/macOS/release.sh b/Sources/OpenKey/macOS/release.sh new file mode 100755 index 00000000..3c091f84 --- /dev/null +++ b/Sources/OpenKey/macOS/release.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" +OUT="${OUT:-$PWD/build}" +APP_NAME="OpenKey CE" +: "${NOTARY_PROFILE:?set NOTARY_PROFILE to a notarytool keychain profile}" +IDENTITY="${DEVELOPER_ID_IDENTITY:-$(security find-identity -v -p codesigning | sed -n 's/.*"\(Developer ID Application: .*\)"/\1/p' | head -1)}" +[[ -n "$IDENTITY" ]] || { echo "no Developer ID Application identity found" >&2; exit 1; } + +rm -rf "$OUT" +xcodebuild -project OpenKey.xcodeproj -scheme OpenKey -configuration Release \ + -derivedDataPath "$OUT/dd" ARCHS="arm64 x86_64" ONLY_ACTIVE_ARCH=NO build -quiet +APP="$OUT/$APP_NAME.app" +cp -R "$OUT/dd/Build/Products/Release/$APP_NAME.app" "$APP" +VERSION="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$APP/Contents/Info.plist")" + +codesign --force --options runtime --timestamp --entitlements OpenKey.entitlements --sign "$IDENTITY" "$APP" +codesign --verify --deep --strict --verbose=2 "$APP" +ditto -c -k --keepParent "$APP" "$OUT/notarize.zip" +xcrun notarytool submit "$OUT/notarize.zip" --keychain-profile "$NOTARY_PROFILE" --wait +xcrun stapler staple "$APP" +spctl -a -vvv -t exec "$APP" + +ZIP="$OUT/OpenKey-CE-$VERSION.zip" +DMG="$OUT/OpenKey-CE-$VERSION.dmg" +ditto -c -k --keepParent "$APP" "$ZIP" +STAGE="$OUT/dmg"; mkdir -p "$STAGE"; cp -R "$APP" "$STAGE/"; ln -s /Applications "$STAGE/Applications" +hdiutil create -volname "$APP_NAME $VERSION" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null +codesign --force --timestamp --sign "$IDENTITY" "$DMG" +xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait +xcrun stapler staple "$DMG" +shasum -a 256 "$ZIP" "$DMG" diff --git a/version.json b/version.json index c4ce23e1..c7e746dc 100644 --- a/version.json +++ b/version.json @@ -6,5 +6,9 @@ "latestWinVersion": { "versionName": "2.0.2", "versionCode": 131074 + }, + "latestMacCEVersion": { + "versionName": "2.1.0", + "versionCode": 50 } -} \ No newline at end of file +} From 810a47a6a720ae163334a460ac78400215e3a1ff Mon Sep 17 00:00:00 2001 From: Nghia Pham Date: Mon, 12 Oct 2026 14:22:52 +0700 Subject: [PATCH 7/9] fix(feature/openkey-ce-modernize): review fixes Stop over-releasing the input source language string, finish the update check callback on every path, warn when legacy OpenKey runs, pin the release output directory. --- Sources/OpenKey/macOS/ModernKey/AppDelegate.m | 20 ++++++ Sources/OpenKey/macOS/ModernKey/OpenKey.mm | 14 ++-- .../OpenKey/macOS/ModernKey/OpenKeyManager.m | 68 ++++++++++--------- Sources/OpenKey/macOS/release.sh | 2 +- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m index 22265d0f..996b6760 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m @@ -111,6 +111,24 @@ -(void)askPermission { [NSApp terminate:0]; } +-(void)offerToQuitLegacyOpenKey { + NSArray* legacyApps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.tuyenmai.openkey"]; + if (legacyApps.count == 0) { + return; + } + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"OpenKey bản cũ đang chạy"]; + [alert setInformativeText:@"Hai bộ gõ chạy cùng lúc sẽ làm sai chữ khi gõ. OpenKey CE có thể thoát bản cũ ngay bây giờ.\n\nĐể bản cũ không tự chạy lại khi đăng nhập, hãy tắt \"Khởi động cùng máy\" trong OpenKey cũ hoặc xoá OpenKey.app khỏi Applications."]; + [alert addButtonWithTitle:@"Thoát OpenKey cũ"]; + [alert addButtonWithTitle:@"Để sau"]; + [alert.window setLevel:NSStatusWindowLevel]; + if ([alert runModal] == NSAlertFirstButtonReturn) { + for (NSRunningApplication *app in legacyApps) { + [app terminate]; + } + } +} + - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { appDelegate = self; @@ -145,6 +163,8 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { return; } + [self offerToQuitLegacyOpenKey]; + // check if user granted Accessabilty permission if (!MJAccessibilityIsEnabled()) { [self askPermission]; diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm index c538c3b8..6cfdf92c 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm +++ b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm @@ -731,15 +731,15 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e if ( isource != NULL ) { CFArrayRef languages = (CFArrayRef) TISGetInputSourceProperty(isource, kTISPropertyInputSourceLanguages); - - if (CFArrayGetCount(languages) > 0) { + BOOL isOtherLanguage = NO; + if (languages != NULL && CFArrayGetCount(languages) > 0) { CFStringRef langRef = (CFStringRef)CFArrayGetValueAtIndex(languages, 0); NSString *currentLanguage = (__bridge NSString *)langRef; - if(![currentLanguage isLike:@"en"]){ - return event; - } - CFRelease(langRef); - CFRelease(isource); + isOtherLanguage = ![currentLanguage isLike:@"en"]; + } + CFRelease(isource); + if (isOtherLanguage) { + return event; } } } diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m index 32400b71..8f40476d 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m +++ b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m @@ -160,44 +160,48 @@ +(void)checkNewVersion:(NSWindow*)parent callbackFunc:(CheckNewVersionCallback) //load new version config NSURLSession *aSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; [[aSession dataTaskWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/llawliet11/OpenKey/master/version.json"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (((NSHTTPURLResponse *)response).statusCode == 200) { - if (data) { - if(NSClassFromString(@"NSJSONSerialization")) { - NSError *error = nil; - id object = [NSJSONSerialization - JSONObjectWithData:data - options:0 - error:&error]; - - if(error) { } - if([object isKindOfClass:[NSDictionary class]]) { - NSDictionary *results = object; - NSDictionary *ver = [results valueForKey:@"latestMacCEVersion"]; - NSString* versionCodeString = [ver valueForKey:@"versionCode"]; - int versionCode = (int)[versionCodeString integerValue]; - int currentVersionCode = (int)[((NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleVersion"]) integerValue]; - - dispatch_async(dispatch_get_main_queue(), ^{ - if (callback != nil) { - callback(); - } - if (versionCode > currentVersionCode || callback != nil) { - [self showUpdateMessage:parent needUpdating:versionCode > currentVersionCode newVersion:[ver valueForKey:@"versionName"]]; - } - }); - } - else { - //oh my god - } - } - else { - //can not parse json + NSDictionary *ver = nil; + if (((NSHTTPURLResponse *)response).statusCode == 200 && data) { + id object = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if ([object isKindOfClass:[NSDictionary class]]) { + id entry = [object valueForKey:@"latestMacCEVersion"]; + if ([entry isKindOfClass:[NSDictionary class]]) { + ver = entry; } } } + dispatch_async(dispatch_get_main_queue(), ^{ + if (callback != nil) { + callback(); + } + if (ver == nil) { + if (callback != nil) { + [self showUpdateCheckFailed:parent]; + } + return; + } + int versionCode = (int)[[ver valueForKey:@"versionCode"] integerValue]; + int currentVersionCode = (int)[((NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey: @"CFBundleVersion"]) integerValue]; + if (versionCode > currentVersionCode || callback != nil) { + [self showUpdateMessage:parent needUpdating:versionCode > currentVersionCode newVersion:[ver valueForKey:@"versionName"]]; + } + }); }] resume]; } ++(void)showUpdateCheckFailed:(NSWindow*)parent { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Không kiểm tra được phiên bản mới"]; + [alert setInformativeText:@"Vui lòng kiểm tra kết nối mạng, hoặc xem trang Releases trên GitHub."]; + [alert addButtonWithTitle:@"OK"]; + if (parent == nil) { + [alert runModal]; + } else { + [alert beginSheetModalForWindow:parent completionHandler:nil]; + } +} + + +(void)showUpdateMessage:(NSWindow*)parent needUpdating:(BOOL)needUpdating newVersion:(NSString*)versionString { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:(needUpdating ? [NSString stringWithFormat:@"OpenKey Có phiên bản mới (%@), bạn có muốn cập nhật không?", versionString] : @"Bạn đang dùng phiên bản mới nhất!")]; diff --git a/Sources/OpenKey/macOS/release.sh b/Sources/OpenKey/macOS/release.sh index 3c091f84..9ea4b938 100755 --- a/Sources/OpenKey/macOS/release.sh +++ b/Sources/OpenKey/macOS/release.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "$0")" -OUT="${OUT:-$PWD/build}" +OUT="$PWD/build" APP_NAME="OpenKey CE" : "${NOTARY_PROFILE:?set NOTARY_PROFILE to a notarytool keychain profile}" IDENTITY="${DEVELOPER_ID_IDENTITY:-$(security find-identity -v -p codesigning | sed -n 's/.*"\(Developer ID Application: .*\)"/\1/p' | head -1)}" From a848e9230a7db91c5e6cff43bb9d0da23371e3fe Mon Sep 17 00:00:00 2001 From: Nghia Pham Date: Mon, 12 Oct 2026 14:40:48 +0700 Subject: [PATCH 8/9] feat(feature/openkey-ce-modernize): permission, login, DMG Name the macOS 27 permission, show real SMAppService status with a Login Items prompt, ship LICENSE and a source pointer inside the DMG. --- README.md | 6 +++- Sources/OpenKey/macOS/ModernKey/AppDelegate.h | 2 ++ Sources/OpenKey/macOS/ModernKey/AppDelegate.m | 34 ++++++++++++++++--- .../OpenKey/macOS/ModernKey/ViewController.m | 9 ++--- Sources/OpenKey/macOS/release.sh | 2 ++ 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 628ba431..e8d2d7bc 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,13 @@ All credit for OpenKey goes to its author, Mai Vũ Tuyên. OpenKey CE stays GPL- - Creates a fresh backspace event per keystroke instead of reusing one, which fixes stuck typing in Apple Mail and WebKit apps (from #333). - Launch at login uses `SMAppService`; the old helper app is gone. - Requires macOS 13 Ventura or later; universal binary for Apple silicon and Intel. -- Bundle ID `com.nghia-pham.openkey-ce`, so it installs next to the original OpenKey. On first launch it imports settings and macros from OpenKey 2.0.5. Quit the original OpenKey before using CE: two input apps at once conflict. +- Bundle ID `com.nghia-pham.openkey-ce`, so it installs next to the original OpenKey. On first launch it imports settings and macros from OpenKey 2.0.5. Quit the original OpenKey before using CE: two input apps at once conflict. CE detects a running OpenKey 2.0.5 and offers to quit it. - Update checks read this fork's `version.json` and open the Releases page; the closed-source in-app updater is removed. +## Permission + +OpenKey CE needs the permission that lets it read and send keystrokes: System Settings > Privacy & Security > Accessibility on macOS 13 to 26, renamed Device Control and Data Access on macOS 27. Turn on "OpenKey CE", then open the app again. + ## Build from source Requires Xcode 15 or later. `Sources/OpenKey/macOS/release.sh` builds a universal app, signs it with the Developer ID identity in your keychain, notarizes and staples the app and the DMG. Set `NOTARY_PROFILE` to a `notarytool store-credentials` profile first. To build without signing, open `Sources/OpenKey/macOS/OpenKey.xcodeproj` and build the `OpenKey` scheme. diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.h b/Sources/OpenKey/macOS/ModernKey/AppDelegate.h index e4b2b744..28c0bc60 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.h +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.h @@ -22,6 +22,8 @@ -(void)onCodeTableChanged:(int)index; -(void)setRunOnStartup:(BOOL)val; +-(void)setRunOnStartup:(BOOL)val interactive:(BOOL)interactive; +-(BOOL)isRunOnStartupActive; -(void)loadDefaultConfig; -(void)setGrayIcon:(BOOL)val; diff --git a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m index 996b6760..c97f682f 100644 --- a/Sources/OpenKey/macOS/ModernKey/AppDelegate.m +++ b/Sources/OpenKey/macOS/ModernKey/AppDelegate.m @@ -93,8 +93,11 @@ @implementation AppDelegate { -(void)askPermission { NSAlert *alert = [[NSAlert alloc] init]; - [alert setMessageText: [NSString stringWithFormat:@"OpenKey cần bạn cấp quyền để có thể hoạt động!"]]; - [alert setInformativeText:@"Vui lòng chạy lại ứng dụng sau khi cấp quyền."]; + NSString *permissionName = [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){27, 0, 0}] + ? @"Device Control and Data Access" + : @"Accessibility (Trợ năng)"; + [alert setMessageText:@"OpenKey CE cần bạn cấp quyền để có thể hoạt động!"]; + [alert setInformativeText:[NSString stringWithFormat:@"Vào System Settings > Privacy & Security > %@, bật \"OpenKey CE\", rồi mở lại ứng dụng.", permissionName]]; [alert addButtonWithTitle:@"Không"]; [alert addButtonWithTitle:@"Cấp quyền"]; @@ -340,17 +343,40 @@ -(void)loadDefaultConfig { } -(void)setRunOnStartup:(BOOL)val { + [self setRunOnStartup:val interactive:NO]; +} + +-(void)setRunOnStartup:(BOOL)val interactive:(BOOL)interactive { SMAppService *service = [SMAppService mainAppService]; NSError *error = nil; - if (val && service.status != SMAppServiceStatusEnabled) { + if (val && service.status != SMAppServiceStatusEnabled && (interactive || service.status != SMAppServiceStatusRequiresApproval)) { if (![service registerAndReturnError:&error]) { NSLog(@"[OpenKey] SMAppService register failed: %@", error); } - } else if (!val && service.status == SMAppServiceStatusEnabled) { + } else if (!val && service.status != SMAppServiceStatusNotRegistered) { if (![service unregisterAndReturnError:&error]) { NSLog(@"[OpenKey] SMAppService unregister failed: %@", error); } } + if (interactive && val && service.status != SMAppServiceStatusEnabled) { + NSAlert *alert = [[NSAlert alloc] init]; + if (service.status == SMAppServiceStatusRequiresApproval) { + [alert setMessageText:@"macOS cần bạn cho phép OpenKey CE khởi động cùng máy"]; + [alert setInformativeText:@"Vào System Settings > General > Login Items và bật \"OpenKey CE\"."]; + } else { + [alert setMessageText:@"Không bật được khởi động cùng máy"]; + [alert setInformativeText:[NSString stringWithFormat:@"Hãy đặt OpenKey CE trong thư mục Applications rồi thử lại.%@", error ? [NSString stringWithFormat:@"\n\n%@", error.localizedDescription] : @""]]; + } + [alert addButtonWithTitle:@"Mở Login Items"]; + [alert addButtonWithTitle:@"Để sau"]; + if ([alert runModal] == NSAlertFirstButtonReturn) { + [SMAppService openSystemSettingsLoginItems]; + } + } +} + +-(BOOL)isRunOnStartupActive { + return [SMAppService mainAppService].status == SMAppServiceStatusEnabled; } -(void)setGrayIcon:(BOOL)val { diff --git a/Sources/OpenKey/macOS/ModernKey/ViewController.m b/Sources/OpenKey/macOS/ModernKey/ViewController.m index 898082de..a78091d2 100644 --- a/Sources/OpenKey/macOS/ModernKey/ViewController.m +++ b/Sources/OpenKey/macOS/ModernKey/ViewController.m @@ -205,7 +205,8 @@ - (IBAction)onShowUIOnStartup:(NSButton *)sender { - (IBAction)onRunOnStartup:(NSButton *)sender { NSInteger val = [self setCustomValue:sender keyToSet:@"RunOnStartup"]; - [appDelegate setRunOnStartup:val]; + [appDelegate setRunOnStartup:val interactive:YES]; + sender.state = [appDelegate isRunOnStartupActive] ? NSControlStateValueOn : NSControlStateValueOff; } - (IBAction)onGrayIcon:(id)sender { @@ -417,8 +418,7 @@ -(void)fillData { NSInteger spelling = [[NSUserDefaults standardUserDefaults] integerForKey:@"Spelling"]; self.CheckSpellingButton.state = spelling ? NSControlStateValueOn : NSControlStateValueOff; - NSInteger runOnStartup = [[NSUserDefaults standardUserDefaults] integerForKey:@"RunOnStartup"]; - self.RunOnStartupButton.state = runOnStartup ? NSControlStateValueOn : NSControlStateValueOff; + self.RunOnStartupButton.state = [appDelegate isRunOnStartupActive] ? NSControlStateValueOn : NSControlStateValueOff; NSInteger useGrayIcon = [[NSUserDefaults standardUserDefaults] integerForKey:@"GrayIcon"]; self.UseGrayIcon.state = useGrayIcon ? NSControlStateValueOn : NSControlStateValueOff; @@ -513,7 +513,8 @@ - (IBAction)onDefaultConfig:(id)sender { self.ShowUIButton.state = NSControlStateValueOff; [[NSUserDefaults standardUserDefaults] setInteger:1 forKey:@"RunOnStartup"]; - self.RunOnStartupButton.state = NSControlStateValueOn; + [appDelegate setRunOnStartup:YES]; + self.RunOnStartupButton.state = [appDelegate isRunOnStartupActive] ? NSControlStateValueOn : NSControlStateValueOff; } }]; } diff --git a/Sources/OpenKey/macOS/release.sh b/Sources/OpenKey/macOS/release.sh index 9ea4b938..e23dd927 100755 --- a/Sources/OpenKey/macOS/release.sh +++ b/Sources/OpenKey/macOS/release.sh @@ -26,6 +26,8 @@ ZIP="$OUT/OpenKey-CE-$VERSION.zip" DMG="$OUT/OpenKey-CE-$VERSION.dmg" ditto -c -k --keepParent "$APP" "$ZIP" STAGE="$OUT/dmg"; mkdir -p "$STAGE"; cp -R "$APP" "$STAGE/"; ln -s /Applications "$STAGE/Applications" +cp ../../../LICENSE "$STAGE/LICENSE.txt" +printf "OpenKey CE %s\n\nCommunity build of OpenKey by Mai Vu Tuyen, GPL-3.0.\nSource for this build: https://github.com/llawliet11/OpenKey/tree/v%s-ce\nOriginal project: https://github.com/tuyenvm/OpenKey\n" "$VERSION" "$VERSION" > "$STAGE/README.txt" hdiutil create -volname "$APP_NAME $VERSION" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null codesign --force --timestamp --sign "$IDENTITY" "$DMG" xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait From 303e427ad8e6da5225f7e460f881e72d61b5aa4c Mon Sep 17 00:00:00 2001 From: Nghia Pham Date: Mon, 12 Oct 2026 15:00:31 +0700 Subject: [PATCH 9/9] chore(feature/openkey-ce-modernize): AI tools gitignore --- .gitignore | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/.gitignore b/.gitignore index 6d1e1f4b..257c873f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,198 @@ /Sources/OpenKey/win32/OpenKey/OpenKeyUpdate/x64/Release /Sources/OpenKey/macOS/build/ + +.envrc + +# AI Coding Tools — shared core, applied to every project +# Credentials, cross-tool conventions and the rules that govern the per-tool +# blocks. Always applied. The credential lines cover tools this project may +# not use: a stray inert line costs nothing, a committed token costs a +# rotation, and that asymmetry is the whole reason they sit together here. +# Added by /nf-ignore + +# Agent credentials — the only files in this asset whose accidental commit +# cannot be undone without rotating a token. One line each, always worth it. +.claude/.credentials.json +.codex/auth.json +.codex/.env +.opencode/auth.json + +# Local conventions +.local/ + +# MCP server output — runtime artifacts, never team-shared +# Playwright MCP: screenshots, page snapshots (.yml), scraped page content +.playwright-mcp/ + +# Skill-sync CLI manifest (openskills / agent-skills-cli). Ignored by DEFAULT +# only because it can carry private registry URLs; it holds sync decisions and +# custom_overrides, NOT raw tokens. That cuts both ways: custom_overrides is +# what stops the next `sync` overwriting locally customized skills, so a team +# that actually runs the CLI together should UN-IGNORE this and commit it — +# it is also the only file identifying which skill dirs are generated. +.skillsrc +!.skillsrc.example + +# Local agent-tool output directory. Source unconfirmed: observed only on an +# Antigravity user's checkout, but Antigravity's documented artifact path is +# ~/.gemini/antigravity/brain//, not the project root, and no +# common test/build tool defaults to a bare artifacts/. Ignored because it is +# generated locally and has never belonged to a repo's history. +# Leading slash on purpose: a bare artifacts/ would also swallow a real +# packages/*/artifacts/ in a workspace repo. +/artifacts/ + +# Personal-prefix obligation — which directories carry the set, and why. +# A directory this asset keeps TRACKED needs the user-/private-/hidden-/ +# personal-/me-/*.local* set, or a personal file in it gets committed. The set +# is emitted for .claude/, .agents/, .agent/, .codex/, .gemini/ and +# .commandcode/ — the six whose authored contents (rules, skills, agents, hooks, commands) were +# measured or vendor-documented. +# It is NOT emitted for .cursor/, .roo/, .kilocode/, .windsurf/, .continue/, +# .qwen/, .opencode/ or .antigravity/. Not because they are exempt — the same +# obligation applies the moment a team authors inside one — but because this +# asset only ignores a connection-config file in each, and none of them was +# investigated. Adding six globs per directory on assumption is the exact +# habit that put fifteen inert patterns in here in the first place. Measure +# first, then add. + +# NOT ignored, deliberately — do not "tidy up" by adding these: +# .claude/ .github/ .codex/rules/ .agents/rules/ .agents/plugins/ +# CLAUDE.md AGENTS.md AGENT.md .mcp.json .github/CODEOWNERS +# Each holds team-shared files that must stay tracked. A whole-directory entry +# for any of them overrides the targeted blocks above AND kills the negations, +# because git never descends into an ignored directory. +# .mcp.json in particular is the project-scope MCP list, MEANT to be shared. +# Ignore it only if that repo's copy hardcodes a key inline — check with +# grep -nE '(sk-|Bearer |ghp_|glpat-|AIza|xox[baprs]-)' .mcp.json +# and if it hits, the fix is moving the secret to an ${ENV_VAR}, not ignoring +# the file. Same test before ignoring any *.json/*.toml config in this asset. + +# Claude Code — project files +# Applied when Claude Code is detected or selected. +# Added by /nf-ignore + +# Claude Code — local/personal only; team-shared stay committed +# Prefix user-, private-, hidden-, personal-, me- + .local suffix (any depth) +**/.claude/**/user-* +**/.claude/**/private-* +**/.claude/**/hidden-* +**/.claude/**/personal-* +**/.claude/**/me-* +**/.claude/**/*.local* +CLAUDE.local.md +# me- = "MeOnly": personal-by-default. New project skills are named +# me-- so they stay LOCAL unless explicitly unlocked. +# To share one, add a negation BELOW this line, e.g.: +# !.claude/skills/me--/ +# Runtime state — CC-generated, machine-specific +.claude/worktrees/ +.claude/scratch*/ +.claude/scheduled_tasks.lock +.claude/evolve/ +.claude/**/*.log +.claude/snippets/.trash/ +# agent-memory/ is NOT here on purpose. Anthropic's own file reference badges +# `.claude/agent-memory/` as COMMITTED: "This directory holds project-scoped +# subagent memory, meant to be shared with your team. To keep memory out of +# version control use `memory: local`, which writes to +# .claude/agent-memory-local/". So the shared one stays tracked and only the +# -local variant is ignored. An earlier revision had this exactly backwards. +.claude/agent-memory-local/ +# plans/ is documented ONLY under ~/.claude/ (the cleanupPeriodDays table), so +# by this file's own rule it should live in config-home.gitignore. It is kept +# here because 10 of 80 measured project .claude/ folders do contain it — +# likely older CC behaviour — and plan files are disposable output, so hiding +# them costs nothing. Do not cite it as matching vendor convention. +.claude/plans/ +# Config-dir-only runtime state (sessions/, jobs/, todos/, teams/, statsig/, +# ide/, debug/, uploads/, history.jsonl, ...) is deliberately NOT here. Those +# live at the ROOT of the Claude Code config dir, never under a project's +# .claude/ — measured 0 occurrences across 80 real project .claude/ folders. +# They moved to config-home.gitignore, which /nf-ignore applies only when run +# inside the config dir itself, where the correct form is unprefixed +# (`sessions/`, not `.claude/sessions/`). Do NOT re-add them with a .claude/ +# prefix: that prefix matches nothing in either place. + +# Codex CLI — project files +# Applied when Codex CLI is detected or selected. +# Added by /nf-ignore + +# Codex CLI — ignore only the local parts; agents/, hooks/, hooks.json and +# rules/ are hand-authored and team-shared. +# This used to be a `.codex/*` blanket with a `!.codex/rules/` whitelist. That +# silently swallowed real work: a 21KB hand-written .codex/agents/*.toml in one +# repo was invisible to git under it, while ANOTHER repo commits +# .codex/hooks.json and a third commits .codex/rules/default.rules — the same +# files, tracked or hidden depending only on which .gitignore they landed next +# to. config.toml is the per-developer layer (it carries [mcp_servers] entries +# with inline env, and one measured repo has a live API key in it), so it is +# the one that stays ignored. +.codex/config.toml +.codex/sessions/ +.codex/cache/ +# Personal variants. The .claude/ and .agents/ prefix blocks are hard-scoped +# to those directories and do NOT reach .codex/, so the same set is repeated. +# This is what makes keeping .codex/ tracked safe: the directory is shared by +# default and personal items opt OUT by name, rather than the whole directory +# opting out and taking the team's work with it. +**/.codex/**/user-* +**/.codex/**/private-* +**/.codex/**/hidden-* +**/.codex/**/personal-* +**/.codex/**/me-* +**/.codex/**/*.local* +# .codex/skills/ is deliberately absent. Codex's own docs list +# $REPO_ROOT/.agents/skills as the repository skill location, never +# .codex/skills; where the latter does appear it is a third-party sync tool's +# render target. Settle it on content before ignoring it — see the .agents/ +# section below for the diff test. + +# Common baseline — language-agnostic, safe for any stack +# Added by /nf-ignore (fast path for projects with no detected stack) + +# OS junk +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini + +# Editor / IDE — local, personal +*.swp +*.swo +*~ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +.idea/ + +# Logs & temp +*.log +logs/ +*.tmp +*.pid +*.seed +tmp/ +temp/ + +# Dependency dirs (common across ecosystems; harmless if unused) +node_modules/ +.venv/ +venv/ +__pycache__/ +*.pyc + +# Env / secrets — block by default; whitelist examples +.env +.env.* +!.env.example +!.env.sample +*.local + +# Local Claude Code overrides (per-machine, not for VCS) +.claude/settings.local.json