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
109 changes: 104 additions & 5 deletions Sources/OpenKey/macOS/ModernKey/OpenKey.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// OpenKey
//
// Created by Tuyen on 1/18/19.
// Copyright © 2019 Tuyen Mai. All rights reserved.
// Copyright 2019 Tuyen Mai. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <Carbon/Carbon.h>
Expand Down Expand Up @@ -51,7 +51,13 @@
//app which must sent special empty character
NSArray* _niceSpaceApp = @[@"com.sublimetext.3",
@"com.sublimetext.2",
@"com.apple.Spotlight", // Spotlight search
];

// Array of bundle IDs for apps that should be ignored
static NSArray* const IGNORED_BUNDLES = @[
// Spotlight search - removed to enable Vietnamese input
];

//app which error with unicode Compound
NSArray* _unicodeCompoundApp = @[@"com.apple.",
Expand Down Expand Up @@ -85,6 +91,21 @@

NSString* _frontMostApp = @"UnknownApp";

static NSString *lastFocusedAppBundleId = nil;
static pid_t lastFocusedAppPid = -1;
static bool _willUpdateFocusedApp = false;

// Global AX variables
static AXUIElementRef g_systemWide = NULL;

// Cleanup function for AX variables
static void cleanupAXVariables() {
if (g_systemWide) {
CFRelease(g_systemWide);
g_systemWide = NULL;
}
}

void OpenKeyInit() {
//load saved data
vFreeMark = 0;//(int)[[NSUserDefaults standardUserDefaults] integerForKey:@"FreeMark"];
Expand Down Expand Up @@ -143,6 +164,11 @@ void OpenKeyInit() {
}
}

void OpenKeyCleanup() {
cleanupAXVariables();
// ... other cleanup code ...
}

void RequestNewSession() {
//send event signal to Engine
vKeyHandleEvent(vKeyEvent::Mouse, vKeyEventState::MouseDown, 0);
Expand Down Expand Up @@ -174,6 +200,10 @@ BOOL containUnicodeCompoundApp(NSString* topApp) {
return false;
}

BOOL isSpotlightApp(NSString* topApp) {
return topApp != nil && [topApp isEqualToString:@"com.apple.Spotlight"];
}

void saveSmartSwitchKeyData() {
getSmartSwitchKeySaveData(savedSmartSwitchKeyData);
NSData* _data = [NSData dataWithBytes:savedSmartSwitchKeyData.data() length:savedSmartSwitchKeyData.size()];
Expand All @@ -182,6 +212,7 @@ void saveSmartSwitchKeyData() {
}

void OnActiveAppChanged() { //use for smart switch key; improved on Sep 28th, 2019
_willUpdateFocusedApp = true;
queryFrontMostApp();
_languageTemp = getAppInputMethodStatus(string(_frontMostApp.UTF8String), vLanguage | (vCodeTable << 1));
if ((_languageTemp & 0x01) != vLanguage) { //for input method
Expand Down Expand Up @@ -356,6 +387,13 @@ void SendBackspace() {
}
_syncKey.pop_back();
}

// Special handling for Spotlight: ensure backspace removes character, not just completion
if (isSpotlightApp(FRONT_APP)) {
// Send an additional backspace to ensure character removal
CGEventTapPostEvent(_proxy, eventBackSpaceDown);
CGEventTapPostEvent(_proxy, eventBackSpaceUp);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SendBackspace doubles every backspace causing potential over-deletion

High Severity

SendBackspace() unconditionally sends an extra backspace for Spotlight on every invocation. When called in a loop (e.g., for backspaceCount iterations during character replacement), this doubles every programmatic backspace. Combined with the autocomplete fix that already calls SendEmptyCharacter() + backspaceCount++ to handle autocomplete dismissal, the total backspaces sent become 2*(N+1) instead of the needed N+1, likely over-deleting text.

Fix in Cursor Fix in Web

}

void SendShiftAndLeftArrow() {
Expand Down Expand Up @@ -571,10 +609,45 @@ CGKeyCode ConvertEventToKeyboadLayoutCompatKeyCode(CGEventRef keyEvent, CGKeyCod
fallbackKeyCode);
}

void updateFocusedAppBundleId() {
if (!g_systemWide) {
g_systemWide = AXUIElementCreateSystemWide();
}

AXUIElementRef focusedApp = NULL;
AXError result = AXUIElementCopyAttributeValue(g_systemWide, kAXFocusedApplicationAttribute, (CFTypeRef*)&focusedApp);

if (result == kAXErrorSuccess && focusedApp) {
pid_t pid = 0;
AXUIElementGetPid(focusedApp, &pid);

// Check if the focused app has changed
if (pid != lastFocusedAppPid) {
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
lastFocusedAppBundleId = app.bundleIdentifier;
lastFocusedAppPid = pid;
}

CFRelease(focusedApp);
return;
} else {
// Fallback to NSWorkspace when AX API fails
NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (frontApp && frontApp.processIdentifier != lastFocusedAppPid) {
lastFocusedAppBundleId = frontApp.bundleIdentifier;
lastFocusedAppPid = frontApp.processIdentifier;
}
}

if (focusedApp) {
CFRelease(focusedApp);
}
}

/**
* MAIN HOOK entry, very important function.
* MAIN Callback.
*/
*/
CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
//dont handle my event
if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == CGEventSourceGetSourceStateID(myEventSource)) {
Expand Down Expand Up @@ -663,6 +736,7 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e

//handle mouse
if (type == kCGEventLeftMouseDown || type == kCGEventRightMouseDown || type == kCGEventLeftMouseDragged || type == kCGEventRightMouseDragged) {
_willUpdateFocusedApp = true;
RequestNewSession();
return event;
}
Expand All @@ -677,7 +751,7 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e
if (CFArrayGetCount(languages) > 0) {
CFStringRef langRef = (CFStringRef)CFArrayGetValueAtIndex(languages, 0);
NSString *currentLanguage = (__bridge NSString *)langRef;
if(![currentLanguage isLike:@"en"]){
if(![currentLanguage isLike:@"en"] && ![currentLanguage isLike:@""]){ // empty for "unicode hex input"
return event;
}
CFRelease(langRef);
Expand All @@ -688,6 +762,21 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e

//handle keyboard
if (type == kCGEventKeyDown) {

//update focused app
if (_willUpdateFocusedApp) {
updateFocusedAppBundleId();
_willUpdateFocusedApp = false;
}
if (OTHER_CONTROL_KEY) {
_willUpdateFocusedApp = true;
}

//ignore some apps
if (lastFocusedAppBundleId && [IGNORED_BUNDLES containsObject:lastFocusedAppBundleId]) {
return event;
}

//send event signal to Engine
vKeyHandleEvent(vKeyEvent::Keyboard,
vKeyEventState::KeyDown,
Expand All @@ -707,7 +796,13 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e
}
_syncKey.pop_back();
}


// Special handling for Spotlight: ensure backspace removes character, not just completion
if (isSpotlightApp(FRONT_APP)) {
// Send an additional backspace to ensure character removal
CGEventTapPostEvent(_proxy, eventBackSpaceDown);
CGEventTapPostEvent(_proxy, eventBackSpaceUp);
}
} else if (pData->extCode == 3) { //normal key
InsertKeyLength(1);
}
Expand All @@ -723,6 +818,10 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e
if (pData->backspaceCount == 1)
pData->backspaceCount--;
}
} else if (isSpotlightApp(FRONT_APP)) {
// Special handling for Spotlight: send empty character to clear autocomplete
SendEmptyCharacter();
pData->backspaceCount++;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant Spotlight branch duplicates default autocomplete logic

Low Severity

The isSpotlightApp(FRONT_APP) branch (lines 821–824) executes SendEmptyCharacter() and pData->backspaceCount++, which is identical to the default else branch (lines 825–828). Since SendEmptyCharacter() already handles Spotlight-specific behavior internally via the _niceSpaceApp check, this separate branch adds no differentiation and is purely redundant.

Fix in Cursor Fix in Web

} else {
SendEmptyCharacter();
pData->backspaceCount++;
Expand All @@ -732,7 +831,7 @@ CGEventRef OpenKeyCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef e

//send backspace
if (pData->backspaceCount > 0 && pData->backspaceCount < MAX_BUFF) {
for (_i = 0; _i < pData->backspaceCount; _i++) {
for (int i = 0; i < pData->backspaceCount; i++) {
SendBackspace();
}
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/OpenKey/macOS/OpenKey.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = NO;
CODE_SIGN_IDENTITY = "Mac Developer";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 48;
Expand All @@ -580,7 +581,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 11.5;
MARKETING_VERSION = 2.0.4;
OTHER_LDFLAGS = (
"-lc++\n-lc++",
Expand All @@ -596,6 +597,7 @@
buildSettings = {
CLANG_ENABLE_MODULES = NO;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 48;
Expand All @@ -606,7 +608,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 11.5;
MARKETING_VERSION = 2.0.4;
OTHER_LDFLAGS = (
"-lc++\n-lc++",
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<VariablesViewState
version = "1.0">
<ContextStates>
<ContextState
contextName = "OpenKeyCallback:OpenKey.mm">
<PersistentStrings>
<PersistentString
value = "OTHER_CONTROL_KEY">
</PersistentString>
</PersistentStrings>
</ContextState>
</ContextStates>
</VariablesViewState>
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "61B1757B-E811-4F1B-B754-05506CF5FAEB"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "17321050-D561-4BD5-9564-680AA8B7FCBD"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/AppDelegate.m"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "574"
endingLineNumber = "574">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "CD09A5D5-531F-415F-A2CB-575D4193119E"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/AppDelegate.m"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "558"
endingLineNumber = "558">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "7EAB6205-9350-449E-BCCC-1D17D1B59860"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/OpenKey.mm"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "631"
endingLineNumber = "631"
landmarkName = "updateFocusedAppBundleId()"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "D329BD0D-9CC1-465A-8C8C-147E775037A7"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/OpenKey.mm"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "630"
endingLineNumber = "630"
landmarkName = "updateFocusedAppBundleId()"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "98C60ED8-3FF8-4D83-8FD5-D27159B4CC3B"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/OpenKey.mm"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "632"
endingLineNumber = "632"
landmarkName = "updateFocusedAppBundleId()"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "78E6070A-CF8B-4165-88C0-A47852247949"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/OpenKey.mm"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "740"
endingLineNumber = "740"
landmarkName = "OpenKeyCallback(proxy, type, event, refcon)"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "2C15CF3E-A762-42DA-A5C4-6E9C8319A555"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/OpenKey.mm"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "742"
endingLineNumber = "742"
landmarkName = "OpenKeyCallback(proxy, type, event, refcon)"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "460DC62D-F44F-468B-9B4E-C4162DDDC658"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "ModernKey/OpenKey.mm"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "225"
endingLineNumber = "225"
landmarkName = "OnActiveAppChanged()"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Committed developer-specific Xcode debug files

Low Severity

Developer-specific Xcode debug files were committed, including breakpoints (some with shouldBeEnabled = "Yes") and debug watch expressions. These xcuserdatad files are personal IDE state and can interfere with other developers' debugging experience.

Additional Locations (1)

Fix in Cursor Fix in Web

Loading