diff --git a/.gitignore b/.gitignore index 7fd14bb2..257c873f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,200 @@ /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/ + +.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 diff --git a/README.md b/README.md index fe15ed8c..e8d2d7bc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ +# 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. 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. + +--- + # [OpenKey](http://open-key.org) ### [Download bản mới nhất](https://github.com/tuyenvm/OpenKey/releases) 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/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..28c0bc60 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 @@ -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 94f84eac..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"]; @@ -111,15 +114,33 @@ -(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; - + [self registerSupportedNotification]; - + //set quick tooltip [[NSUserDefaults standardUserDefaults] setObject: [NSNumber numberWithInt: 50] forKey: @"NSInitialToolTipDelay"]; - + //check whether this app has been launched before that or not //Only check instances owned by current user (for multi-user/Fast User Switching support) uid_t currentUID = getuid(); @@ -144,22 +165,24 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [NSApp terminate:nil]; return; } - + + [self offerToQuitLegacyOpenKey]; + // check if user granted Accessabilty permission 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(), ^{ if (![OpenKeyManager initEventTap]) { @@ -320,8 +343,40 @@ -(void)loadDefaultConfig { } -(void)setRunOnStartup:(BOOL)val { - CFStringRef appId = (__bridge CFStringRef)@"com.tuyenmai.OpenKeyHelper"; - SMLoginItemSetEnabled(appId, 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 && (interactive || service.status != SMAppServiceStatusRequiresApproval)) { + if (![service registerAndReturnError:&error]) { + NSLog(@"[OpenKey] SMAppService register failed: %@", error); + } + } 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 { @@ -495,12 +550,24 @@ -(void) onControlPanelSelected { if (_mainWC == nil) { _mainWC = [[NSStoryboard storyboardWithName:@"Main" bundle:nil] instantiateControllerWithIdentifier:@"OpenKey"]; } - //[OpenKeyManager showDockIcon:YES]; if ([_mainWC.window isVisible]) { + [NSApp activateIgnoringOtherApps:YES]; return; } + [NSApp activateIgnoringOtherApps:YES]; [_mainWC.window makeKeyAndOrderFront:nil]; [_mainWC.window setLevel:NSFloatingWindowLevel]; + + // 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; + 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]; } -(void) onMacroSelected { @@ -508,9 +575,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]; } @@ -520,9 +589,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]; } diff --git a/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard b/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard index 1ad0aedf..c7ca4439 100644 --- a/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard +++ b/Sources/OpenKey/macOS/ModernKey/Base.lproj/Main.storyboard @@ -777,7 +777,7 @@ - + @@ -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/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/OpenKey.mm b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm index 1e6b6c14..6cfdf92c 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKey.mm +++ b/Sources/OpenKey/macOS/ModernKey/OpenKey.mm @@ -61,13 +61,14 @@ CGEventSourceRef myEventSource = NULL; vKeyHookState* pData; - CGEventRef eventBackSpaceDown; - CGEventRef eventBackSpaceUp; UniChar _newChar, _newCharHi; CGEventRef _newEventDown, _newEventUp; CGKeyCode _keycode; CGEventFlags _flag, _lastFlag = 0, _privateFlag; CGEventTapProxy _proxy; + + //defined in OpenKeyManager.m + extern void OpenKeyReEnableEventTap(void); Uint16 _newCharString[MAX_UNICODE_STRING]; Uint16 _newCharSize; @@ -117,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]; @@ -176,12 +175,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; } @@ -366,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(); @@ -391,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); @@ -523,6 +548,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; @@ -599,6 +626,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; @@ -696,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; } } } @@ -725,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(); } diff --git a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m index 97883f1a..8f40476d 100644 --- a/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m +++ b/Sources/OpenKey/macOS/ModernKey/OpenKeyManager.m @@ -71,12 +71,22 @@ +(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); - + 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); @@ -149,45 +159,49 @@ +(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) { - 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:@"latestVersion"]; - 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 + [[aSession dataTaskWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/llawliet11/OpenKey/master/version.json"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + 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!")]; @@ -216,25 +230,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 { @@ -243,8 +239,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/ViewController.m b/Sources/OpenKey/macOS/ModernKey/ViewController.m index 386d3872..a78091d2 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; @@ -204,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 { @@ -266,6 +268,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; @@ -409,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; @@ -483,6 +491,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)]; @@ -504,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/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..e23dd927 --- /dev/null +++ b/Sources/OpenKey/macOS/release.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" +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" +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 +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 +}