├── KeyPhantom ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32 1.png │ │ ├── 32.png │ │ ├── 512.png │ │ ├── 64.png │ │ ├── 256 1.png │ │ ├── 512 1.png │ │ ├── appstore.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── custom.keyboard.slash.symbolset │ │ ├── Contents.json │ │ └── custom.keyboard.slash.svg │ └── custom.keyboard.macwindow.slash.symbolset │ │ ├── Contents.json │ │ └── custom.keyboard.macwindow.slash.svg ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── KeyPhantom.xcdatamodeld │ ├── .xccurrentversion │ └── KeyPhantom.xcdatamodel │ │ └── contents ├── Info.plist ├── KeyPhantom.entitlements ├── Library │ ├── AccessibilityManager.swift │ ├── KeyboardEventSender.swift │ ├── AppListManager.swift │ ├── KeyString.swift │ ├── KeyBindingManager.swift │ └── KeyBinding.swift ├── View │ ├── CheckForUpdatesView.swift │ ├── SelectedAppView.swift │ ├── AboutView.swift │ ├── UpdaterSettingsView.swift │ ├── KeyCodeView.swift │ ├── AppListView.swift │ ├── KeyBindingView.swift │ ├── KeyBindingCreateView.swift │ └── KeyBindingRowView.swift ├── Persistence.swift └── KeyPhantomApp.swift ├── KeyPhantom.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── situ.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── situ.xcuserdatad │ │ ├── xcschemes │ │ └── xcschememanagement.plist │ │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist └── project.pbxproj ├── KeyPhantomTests └── KeyPhantomTests.swift ├── KeyPhantomUITests ├── KeyPhantomUITestsLaunchTests.swift └── KeyPhantomUITests.swift ├── LICENSE ├── README.md └── icon.svg /KeyPhantom/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /KeyPhantom/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/32 1.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/256 1.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/512 1.png -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom/Assets.xcassets/AppIcon.appiconset/appstore.png -------------------------------------------------------------------------------- /KeyPhantom.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /KeyPhantom.xcodeproj/project.xcworkspace/xcuserdata/situ.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/situ2001/KeyPhantom/HEAD/KeyPhantom.xcodeproj/project.xcworkspace/xcuserdata/situ.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/custom.keyboard.slash.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "custom.keyboard.slash.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/custom.keyboard.macwindow.slash.symbolset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "symbols" : [ 7 | { 8 | "filename" : "custom.keyboard.macwindow.slash.svg", 9 | "idiom" : "universal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /KeyPhantom/KeyPhantom.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | KeyPhantom.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /KeyPhantomTests/KeyPhantomTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPhantomTests.swift 3 | // KeyPhantomTests 4 | // 5 | // Created by Situ Yongcong on 20/2/2025. 6 | // 7 | 8 | import Testing 9 | @testable import KeyPhantom 10 | 11 | struct KeyPhantomTests { 12 | 13 | @Test func example() async throws { 14 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /KeyPhantom.xcodeproj/xcuserdata/situ.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | KeyPhantom.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /KeyPhantom/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SUFeedURL 6 | https://situ2001.github.io/keyphantom/appcast.xml 7 | SUPublicEDKey 8 | 3XSWVezwMdlxp8o6WaCdCbCSnptUrDF7LK5kW1cF1+Q= 9 | SUEnableAutomaticChecks 10 | 11 | LSUIElement 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /KeyPhantom/KeyPhantom.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.aps-environment 6 | development 7 | com.apple.developer.icloud-container-identifiers 8 | 9 | com.apple.developer.icloud-services 10 | 11 | CloudKit 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /KeyPhantom/Library/AccessibilityManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityManager.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 8/3/2025. 6 | // 7 | 8 | import Accessibility 9 | import SwiftUI 10 | 11 | class AccessibilityManager { 12 | var accessibilityEnabled = false 13 | 14 | static let shared = AccessibilityManager() 15 | 16 | private init() { 17 | checkAccessibility() 18 | } 19 | 20 | func checkAccessibility() { 21 | let checkOptPrompt = 22 | kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString 23 | 24 | let options: NSDictionary = [checkOptPrompt: true] 25 | 26 | let accessibilityEnabled = AXIsProcessTrustedWithOptions(options) 27 | 28 | if !accessibilityEnabled { 29 | print("Accessibility is not enabled") 30 | } 31 | 32 | self.accessibilityEnabled = accessibilityEnabled 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /KeyPhantomUITests/KeyPhantomUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPhantomUITestsLaunchTests.swift 3 | // KeyPhantomUITests 4 | // 5 | // Created by Situ Yongcong on 20/2/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class KeyPhantomUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | @MainActor 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /KeyPhantom/KeyPhantom.xcdatamodeld/KeyPhantom.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 situ2001 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /KeyPhantom.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "e9a7cebdda72ecf24310883e69407c0d70b95d6c170c22850db6a9fca9156e58", 3 | "pins" : [ 4 | { 5 | "identity" : "keyboardshortcuts", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts", 8 | "state" : { 9 | "revision" : "7ecc38bb6edf7d087d30e737057b8d8a9b7f51eb", 10 | "version" : "2.2.4" 11 | } 12 | }, 13 | { 14 | "identity" : "launchatlogin-modern", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/sindresorhus/LaunchAtLogin-Modern", 17 | "state" : { 18 | "revision" : "a04ec1c363be3627734f6dad757d82f5d4fa8fcc", 19 | "version" : "1.1.0" 20 | } 21 | }, 22 | { 23 | "identity" : "sparkle", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/sparkle-project/Sparkle", 26 | "state" : { 27 | "revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99", 28 | "version" : "2.7.0" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /KeyPhantom/View/CheckForUpdatesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckForUpdatesView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 8/3/2025. 6 | // 7 | 8 | import Sparkle 9 | import SwiftUI 10 | 11 | // This view model class publishes when new updates can be checked by the user 12 | final class CheckForUpdatesViewModel: ObservableObject { 13 | @Published var canCheckForUpdates = false 14 | 15 | init(updater: SPUUpdater) { 16 | updater.publisher(for: \.canCheckForUpdates) 17 | .assign(to: &$canCheckForUpdates) 18 | } 19 | } 20 | 21 | // This is the view for the Check for Updates menu item 22 | // Note this intermediate view is necessary for the disabled state on the menu item to work properly before Monterey. 23 | // See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more info 24 | struct CheckForUpdatesView: View { 25 | @ObservedObject private var checkForUpdatesViewModel: 26 | CheckForUpdatesViewModel 27 | private let updater: SPUUpdater 28 | 29 | init(updater: SPUUpdater) { 30 | self.updater = updater 31 | 32 | // Create our view model for our CheckForUpdatesView 33 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel( 34 | updater: updater) 35 | } 36 | 37 | var body: some View { 38 | Button("Check for Updates…", action: updater.checkForUpdates) 39 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256 1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "appstore.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /KeyPhantomUITests/KeyPhantomUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPhantomUITests.swift 3 | // KeyPhantomUITests 4 | // 5 | // Created by Situ Yongcong on 20/2/2025. 6 | // 7 | 8 | import XCTest 9 | 10 | final class KeyPhantomUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | @MainActor 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | @MainActor 35 | func testLaunchPerformance() throws { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTApplicationLaunchMetric()]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /KeyPhantom/View/SelectedAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectedAppView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 2/3/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SelectedAppView: View { 11 | var appItem: AppItem? 12 | var onSelected: (AppItem) -> Void 13 | 14 | @State private var isAppListViewPresented = false 15 | @State private var isHovering = false 16 | 17 | var body: some View { 18 | HStack { 19 | if let appItem = appItem { 20 | Image( 21 | nsImage: appItem.icon 22 | ) 23 | .resizable() 24 | .frame(width: 24, height: 24) 25 | Text(appItem.name) 26 | } else { 27 | Text("Select an application") 28 | .foregroundColor(.gray) 29 | .onTapGesture { 30 | isAppListViewPresented = true 31 | } 32 | } 33 | 34 | Spacer() 35 | 36 | if isHovering { 37 | Image( 38 | systemName: "pencil.circle.fill" 39 | ) 40 | .foregroundStyle(.blue) 41 | } 42 | 43 | } 44 | // WHY? 45 | .contentShape(Rectangle()) 46 | .onHover { 47 | isHovering = $0 48 | } 49 | .onTapGesture { 50 | isAppListViewPresented = true 51 | } 52 | .sheet(isPresented: $isAppListViewPresented) { 53 | AppListView { appItem in 54 | self.onSelected(appItem) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /KeyPhantom/View/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 1/3/2025. 6 | // 7 | 8 | import AppKit 9 | import SwiftUI 10 | 11 | struct AboutView: View { 12 | private let copyright = 13 | "© \(Calendar.current.component(.year, from: Date())) situ2001. Made with ♥" 14 | 15 | var body: some View { 16 | VStack(spacing: 20) { 17 | Image(nsImage: NSApp.applicationIconImage) 18 | .resizable() 19 | .scaledToFit() 20 | .frame(width: 128, height: 128) 21 | .clipShape(RoundedRectangle(cornerRadius: 12)) 22 | 23 | Text("KeyPhantom") 24 | .font(.title2) 25 | .fontWeight(.bold) 26 | 27 | Text(getAppVersion()) 28 | .font(.subheadline) 29 | .foregroundColor(.gray) 30 | 31 | Text(copyright) 32 | .font(.caption) 33 | .foregroundColor(.gray) 34 | } 35 | .padding() 36 | } 37 | 38 | private func getAppVersion() -> String { 39 | let bundle = Bundle.main 40 | let version = 41 | bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") 42 | as? String ?? "1.0" 43 | let build = 44 | bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String 45 | ?? "1" 46 | return "Version \(version) (\(build))" 47 | } 48 | } 49 | 50 | struct AboutView_Previews: PreviewProvider { 51 | static var previews: some View { 52 | AboutView() 53 | .frame(width: 300, height: 300) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /KeyPhantom/View/UpdaterSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdaterSettingsView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 2/3/2025. 6 | // 7 | 8 | import Sparkle 9 | import SwiftUI 10 | 11 | // This is the view for our updater settings 12 | // It manages local state for checking for updates and automatically downloading updates 13 | // Upon user changes to these, the updater's properties are set. These are backed by NSUserDefaults. 14 | // Note the updater properties should *only* be set when the user changes the state. 15 | struct UpdaterSettingsView: View { 16 | private let updater: SPUUpdater 17 | 18 | @ObservedObject private var checkForUpdatesViewModel: 19 | CheckForUpdatesViewModel 20 | 21 | @State private var automaticallyChecksForUpdates: Bool 22 | @State private var automaticallyDownloadsUpdates: Bool 23 | 24 | init(updater: SPUUpdater) { 25 | self.updater = updater 26 | 27 | self.automaticallyChecksForUpdates = 28 | updater.automaticallyChecksForUpdates 29 | self.automaticallyDownloadsUpdates = 30 | updater.automaticallyDownloadsUpdates 31 | 32 | // Create our view model for our CheckForUpdatesView 33 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel( 34 | updater: updater) 35 | } 36 | 37 | var body: some View { 38 | VStack { 39 | Button("Check for Updates…", action: updater.checkForUpdates) 40 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 41 | 42 | Toggle( 43 | "Automatically check for updates", 44 | isOn: $automaticallyChecksForUpdates 45 | ) 46 | .onChange(of: automaticallyChecksForUpdates) { newValue in 47 | updater.automaticallyChecksForUpdates = newValue 48 | } 49 | 50 | Toggle( 51 | "Automatically download updates", 52 | isOn: $automaticallyDownloadsUpdates 53 | ) 54 | .disabled(!automaticallyChecksForUpdates) 55 | .onChange(of: automaticallyDownloadsUpdates) { newValue in 56 | updater.automaticallyDownloadsUpdates = newValue 57 | } 58 | }.padding() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /KeyPhantom/View/KeyCodeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyCodeView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 2/3/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // TODO: refactor it to extensible(e.g compatible for more event types) 11 | struct KeyCodeView: View { 12 | var valueForEvent: ValueForEvent? = nil 13 | var showBackground: Bool = false 14 | 15 | @State private var isEditing = false 16 | @State private var showPopover = false 17 | @State private var newKeyCode: Int? 18 | 19 | @State private var isHovering = false 20 | 21 | @State private var eventMonitor: Any? 22 | 23 | var onChange: (Int) -> Void 24 | 25 | var body: some View { 26 | HStack { 27 | if valueForEvent == nil { 28 | Text("Click to record") 29 | } else { 30 | switch self.valueForEvent { 31 | case .keyDown(let k): 32 | Text(k.description) 33 | default: 34 | Text("Unknown") 35 | } 36 | } 37 | 38 | Spacer() 39 | 40 | if isHovering { 41 | Image( 42 | systemName: "pencil.circle.fill" 43 | ) 44 | .foregroundStyle(.blue) 45 | 46 | } 47 | } 48 | .padding() 49 | .background( 50 | isEditing 51 | ? Color.yellow.opacity(0.2) 52 | : (showBackground ? Color.gray.opacity(0.15) : Color.clear) 53 | ) 54 | .cornerRadius(5) 55 | .onHover { hovering in 56 | isHovering = hovering 57 | } 58 | // WHY? 59 | .contentShape(Rectangle()) 60 | .onTapGesture { 61 | showPopover = true 62 | } 63 | .popover( 64 | isPresented: $showPopover, 65 | arrowEdge: .top 66 | ) { 67 | VStack { 68 | Text("Press a key") 69 | .onAppear { 70 | self.eventMonitor = NSEvent.addLocalMonitorForEvents( 71 | matching: .keyDown 72 | ) { event in 73 | // TODO: preserve the modifier flags 74 | // let isShiftPressed = event.modifierFlags.contains(.shift) 75 | 76 | newKeyCode = Int(event.keyCode) 77 | 78 | self.onChange(newKeyCode!) 79 | showPopover = false 80 | return event 81 | } 82 | } 83 | .onDisappear { 84 | if let eventMonitor = self.eventMonitor { 85 | NSEvent.removeMonitor(eventMonitor) 86 | self.eventMonitor = nil 87 | } 88 | } 89 | } 90 | .padding() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /KeyPhantom/Library/KeyboardEventSender.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardEventSender.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 23/2/2025. 6 | // 7 | 8 | import AppKit 9 | import CoreFoundation 10 | import CoreGraphics 11 | import Foundation 12 | 13 | /// A class to send keyboard events to specific applications. 14 | class KeyboardEventSender { 15 | static let shared = KeyboardEventSender() 16 | 17 | // !! Should use privaite state to avoid confilction of the hidSystem one. 18 | private let eventSource = CGEventSource(stateID: .privateState) 19 | 20 | private init() {} 21 | 22 | public func send(key keyBoardKey: KeyboardKey, to appURL: URL) { 23 | let appListMgr = AppListManager.shared 24 | 25 | let appItem = appListMgr.getAppItem(from: appURL) 26 | 27 | if appItem != nil { 28 | self.send(key: keyBoardKey, to: appItem!) 29 | } 30 | } 31 | 32 | public func send(key keyBoardKey: KeyboardKey, to app: AppItem) { 33 | // Check accessibility permission 34 | AccessibilityManager.shared.checkAccessibility() 35 | 36 | let code = keyBoardKey.keyCode 37 | 38 | // construct a key down and key up event, to simulate a key press 39 | let keyDown = CGEvent( 40 | keyboardEventSource: eventSource, virtualKey: CGKeyCode(code), 41 | keyDown: true 42 | ) 43 | 44 | // TODO: preserve the modifier flags 45 | // var eventFlags: CGEventFlags = [] 46 | // if keyBoardKey.modifierFlags.contains(.shift) { 47 | // eventFlags.insert(.maskShift) 48 | // } 49 | // keyDown?.flags = eventFlags 50 | 51 | let keyUp = CGEvent( 52 | keyboardEventSource: eventSource, virtualKey: CGKeyCode(code), 53 | keyDown: false) 54 | 55 | // set the target app, try to get 56 | let pid = getPidFromRunningApplicationBy( 57 | bundleIdentifier: app.bundleIdentifier) 58 | 59 | #if DEBUG 60 | print("pid: \(String(describing: pid))") 61 | #endif 62 | 63 | if pid == nil { 64 | // TODO: throw an error 65 | } else { 66 | #if DEBUG 67 | print("sending key event to \(app.bundleIdentifier)") 68 | #endif 69 | 70 | keyDown?.postToPid(pid!) 71 | keyUp?.postToPid(pid!) 72 | 73 | #if DEBUG 74 | print("key event sent") 75 | #endif 76 | } 77 | } 78 | 79 | private func getPidFromRunningApplicationBy(bundleIdentifier: String) 80 | -> pid_t? 81 | { 82 | let workspace = NSWorkspace.shared 83 | let apps = workspace.runningApplications 84 | 85 | let targetApp = apps.first { appItem in 86 | appItem.bundleIdentifier == bundleIdentifier 87 | } 88 | 89 | if targetApp == nil { 90 | return nil 91 | } 92 | 93 | return targetApp!.processIdentifier 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /KeyPhantom/View/AppListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppListView.swift 3 | // KeyPhantom 4 | // 5 | // Created by situ on 20/2/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppListView: View { 11 | @StateObject var appListManager: AppListManager = AppListManager.shared 12 | 13 | // fn for view dimiss 14 | @Environment(\.dismiss) var dismiss 15 | 16 | // on appSelected callback function 17 | var onAppSelected: ((AppItem) -> Void)? 18 | 19 | @State var selectedRow: AppItem? 20 | 21 | @State var searchText = "" 22 | 23 | private var filteredAppList: [AppItem] { 24 | if searchText.isEmpty { 25 | return appListManager.appList 26 | } else { 27 | return appListManager.appList.filter { app in 28 | app.name.localizedCaseInsensitiveContains(searchText) 29 | } 30 | } 31 | } 32 | 33 | // TODO: should locate to the selected row when appear? 34 | var body: some View { 35 | VStack { 36 | // Search bar 37 | TextField("Search", text: $searchText) 38 | .textFieldStyle(RoundedBorderTextFieldStyle()) 39 | .padding() 40 | 41 | List(selection: $selectedRow) { 42 | ForEach(filteredAppList, id: \.name) { app in 43 | HStack { 44 | Image( 45 | nsImage: app.icon 46 | ) 47 | .resizable() 48 | .frame(width: 32, height: 32) 49 | Text(app.name) 50 | } 51 | .tag(app) 52 | // double click to select 53 | // TODO: how to expand the double-clickable area to the whole row? 54 | .onTapGesture(count: 2) { 55 | selectedRow = app 56 | onAppSelected?(app) 57 | dismiss() 58 | } 59 | } 60 | } 61 | .onAppear { 62 | appListManager.updateAppList() 63 | } 64 | // FIXME: if i use this api, system will stuck 65 | // macOS 15.3 66 | // with error, for example: Detected potentially harmful notification post rate of 281.216 notifications per second 67 | // .searchable(text: $searchText) 68 | 69 | // add cancel or done button 70 | HStack { 71 | Button("Cancel") { 72 | dismiss() 73 | } 74 | .keyboardShortcut(.cancelAction) 75 | 76 | Button("Select") { 77 | onAppSelected?(selectedRow!) 78 | dismiss() 79 | } 80 | .disabled(selectedRow == nil) 81 | .buttonStyle(BorderedButtonStyle()) 82 | .keyboardShortcut(.defaultAction) 83 | } 84 | .padding() 85 | } 86 | .frame(width: 300, height: 400) 87 | } 88 | 89 | } 90 | 91 | #Preview { 92 | AppListView() 93 | } 94 | -------------------------------------------------------------------------------- /KeyPhantom/View/KeyBindingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBindingView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 22/2/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct KeyBindingView: View { 11 | @EnvironmentObject private var keyBindingManager: KeyBindingManager 12 | 13 | @State private var isCreating = false 14 | 15 | var body: some View { 16 | VStack(spacing: 0) { 17 | HStack { 18 | // Toggle for enabling/disabling all key bindings 19 | Toggle( 20 | "Enable KeyPhantom", 21 | isOn: Binding( 22 | get: { keyBindingManager.isHandlerEnabledForView }, 23 | set: { 24 | $0 25 | ? keyBindingManager.enableHandler() 26 | : keyBindingManager.disableHandler() 27 | } 28 | )) 29 | .toggleStyle(.switch) 30 | 31 | Spacer() 32 | 33 | Button { 34 | isCreating = true 35 | } label: { 36 | Label("Add Key Binding", systemImage: "plus") 37 | } 38 | .sheet(isPresented: $isCreating) { 39 | KeyBindingCreateView { keyBinding in 40 | keyBindingManager.addKeyBinding(keyBinding) 41 | isCreating = false 42 | } 43 | } 44 | } 45 | .padding(8) 46 | 47 | // List Header 48 | HStack(spacing: 0) { 49 | Text("Shortcuts") 50 | .frame(width: 150, alignment: .center) 51 | 52 | Divider().frame(width: 1) 53 | 54 | Text("Key Sent") 55 | .frame(width: 150, alignment: .center) 56 | 57 | Divider().frame(width: 1) 58 | 59 | Text("To application") 60 | .frame(width: 200, alignment: .center) 61 | 62 | Divider().frame(width: 1) 63 | 64 | Text("Enabled") 65 | .frame(width: 100, alignment: .center) 66 | 67 | Divider().frame(width: 1) 68 | 69 | Text("Action") 70 | .frame(width: 100, alignment: .center) 71 | } 72 | .padding(.horizontal) 73 | .background(Color.gray.opacity(0.15)) 74 | .frame(height: 25) 75 | 76 | HStack(spacing: 0) { 77 | List { 78 | ForEach($keyBindingManager.keyBindings, id: \.id) { 79 | keyBinding in 80 | KeyBindingRowView( 81 | keyBinding: keyBinding, 82 | keyBindingManager: keyBindingManager 83 | ) 84 | } 85 | } 86 | .scrollContentBackground(.hidden) 87 | } 88 | } 89 | 90 | } 91 | } 92 | 93 | #Preview { 94 | KeyBindingView() 95 | .environment( 96 | \.managedObjectContext, 97 | PersistenceController.preview.container.viewContext 98 | ) 99 | .environmentObject( 100 | KeyBindingManager( 101 | context: PersistenceController.preview.container.viewContext)) 102 | } 103 | -------------------------------------------------------------------------------- /KeyPhantom/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 20/2/2025. 6 | // 7 | 8 | import CoreData 9 | import Carbon.HIToolbox 10 | 11 | struct PersistenceController { 12 | static let shared = PersistenceController() 13 | 14 | @MainActor 15 | static let preview: PersistenceController = { 16 | let result = PersistenceController(inMemory: true) 17 | let viewContext = result.container.viewContext 18 | // for _ in 0..<10 { 19 | // let newItem = Item(context: viewContext) 20 | // newItem.timestamp = Date() 21 | // } 22 | 23 | // Add two test data 24 | let keyBinding1 = KeyBinding( 25 | id: UUID(), shortcutKeyName: "testKeyBinding1", 26 | valueForEvent: .keyDown(.init(keyCode: kVK_LeftArrow)), 27 | targetApplication: AppListManager.shared.getURLForTest(), 28 | enabled: true 29 | ) 30 | let keyBinding2 = KeyBinding( 31 | id: UUID(), shortcutKeyName: "testKeyBinding2", 32 | valueForEvent: .keyDown(.init(keyCode: kVK_RightArrow)), 33 | targetApplication: AppListManager.shared.getURLForTest(), 34 | enabled: true 35 | ) 36 | 37 | let _ = CoreDataKeyBinding.fromModel(keyBinding1, viewContext) 38 | let _ = CoreDataKeyBinding.fromModel(keyBinding2, viewContext) 39 | 40 | do { 41 | try viewContext.save() 42 | } catch { 43 | // Replace this implementation with code to handle the error appropriately. 44 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 45 | let nsError = error as NSError 46 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 47 | } 48 | return result 49 | }() 50 | 51 | let container: NSPersistentCloudKitContainer 52 | 53 | init(inMemory: Bool = false) { 54 | container = NSPersistentCloudKitContainer(name: "KeyPhantom") 55 | if inMemory { 56 | container.persistentStoreDescriptions.first!.url = URL( 57 | fileURLWithPath: "/dev/null") 58 | } 59 | container.loadPersistentStores(completionHandler: { 60 | (storeDescription, error) in 61 | if let error = error as NSError? { 62 | // Replace this implementation with code to handle the error appropriately. 63 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 64 | 65 | /* 66 | Typical reasons for an error here include: 67 | * The parent directory does not exist, cannot be created, or disallows writing. 68 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 69 | * The device is out of space. 70 | * The store could not be migrated to the current model version. 71 | Check the error message to determine what the actual problem was. 72 | */ 73 | fatalError("Unresolved error \(error), \(error.userInfo)") 74 | } 75 | }) 76 | container.viewContext.automaticallyMergesChangesFromParent = true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeyPhantom 2 | 3 |

4 | 5 |

6 | 7 |

8 | Send keyboard events silently to background applications 9 |

10 | 11 | ## Overview 12 | 13 | KeyPhantom is a macOS utility that lets you create keyboard shortcuts that send specific keystrokes to background applications - like a phantom operating behind the scenes. Perfect for power users who need to control multiple applications simultaneously without switching contexts. 14 | 15 | ## Key Features 16 | 17 | - **Custom Keyboard Shortcuts**: Create global shortcuts that trigger specific key presses in target applications 18 | - **Application Targeting**: Send keystrokes to specific applications without bringing them to the foreground 19 | - **Menu Bar Control**: Quick access to enable/disable functionality from the status menu 20 | - **Easy-to-Use Interface**: Simple settings panel for managing your phantom key bindings 21 | - **Launch at Login**: Option to start automatically when you log in 22 | - **Automatic Updates**: Stay current with the latest features and improvements 23 | 24 | ## Why I Built KeyPhantom 25 | 26 | I created KeyPhantom to solve a personal frustration. As a Minecraft player, I often found myself wanting to read e-books while playing. However, Minecraft relies heavily on mouse control, making it impossible to switch to other apps like WeChat Reading or other e-book readers to flip pages without disrupting gameplay. 27 | 28 | KeyPhantom lets me assign some global shortcuts that send "page turn" keystrokes to my e-book reading app in the background while I remain focused on Minecraft. This way, I can continue gaming with full mouse control while still flipping my e-book, without switching apps and making my game lose focus and pause. 29 | 30 | ## Requirements 31 | 32 | - macOS 13.5 or later 33 | - Accessibility permissions (required to send keystrokes to applications) 34 | 35 | ## Installation 36 | 37 | 1. Download the latest release from the [Releases](https://github.com/situ2001/keyphantom/releases) page 38 | 2. Move KeyPhantom to your Applications folder 39 | 3. Launch KeyPhantom and follow the onscreen instructions to grant Accessibility permissions 40 | 41 | ## Usage 42 | 43 | After setting up KeyPhantom, you can create phantom key bindings to send keystrokes to background applications: 44 | 45 | 1. Open KeyPhantom from your Applications folder 46 | 2. Click the keyboard icon in your menu bar to access KeyPhantom 47 | 3. Open Settings to configure your phantom key bindings 48 | 4. Create a new binding by: 49 | - Setting a global shortcut. For example, `Control + D` 50 | - Recording the key to be sent. For example, `Right Arrow` 51 | - Selecting the target application 52 | 5. Enable KeyPhantom using the toggle in menu or in the settings panel 53 | 54 | Then, whenever you press your global shortcut (For example, `Control + D`), KeyPhantom will send the recorded key (For example, `Right Arrow`) to the target application, no matter which app is currently in focus. 55 | 56 | ## Planned Features 57 | 58 | KeyPhantom currently supports sending single keyboard events to background applications, but more features are planned for future releases: 59 | 60 | - **Modifier Key Support**: Send complex key combinations with modifier keys 61 | - **Scroll Wheel Events**: Control scrolling in background applications 62 | 63 | ## Privacy 64 | 65 | KeyPhantom requires accessibility permissions to function but does not record or transmit your keystrokes. All operations happen locally on your Mac. 66 | 67 | ## Support 68 | 69 | If you encounter any issues or have questions, please file an issue in the [GitHub repository](https://github.com/situ2001/keyphantom/issues). 70 | 71 | ## License 72 | 73 | Copyright © 2025 situ2001. All rights reserved. -------------------------------------------------------------------------------- /KeyPhantom/View/KeyBindingCreateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBindingCreateView.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 2/3/2025. 6 | // 7 | 8 | import KeyboardShortcuts 9 | import SwiftUI 10 | 11 | struct KeyBindingCreateView: View { 12 | var onSave: (KeyBinding) -> Void 13 | 14 | @Environment(\.dismiss) private var dismiss 15 | 16 | // keyBinding to be created 17 | @State private var keyBinding: KeyBinding = KeyBinding.defaultValue() 18 | 19 | // use nil for did not pick any value 20 | @State private var valueForEvent: ValueForEvent? = nil 21 | 22 | // appItem for keyBinding 23 | private var appItem: AppItem? { 24 | AppListManager.shared.getAppItem(from: keyBinding.targetApplication) 25 | } 26 | 27 | @State private var isAppListViewPresented: Bool = false 28 | 29 | private var canBeSaved: Bool { 30 | self.valueForEvent != nil 31 | && self.appItem != nil 32 | } 33 | 34 | var body: some View { 35 | VStack(alignment: .center) { 36 | Form { 37 | // 1. record shortcuts 38 | KeyboardShortcuts.Recorder( 39 | for: KeyboardShortcuts.Name( 40 | keyBinding.shortcutKeyName) 41 | ) 42 | .padding() 43 | .frame(width: 200, alignment: .center) 44 | .background(Color.gray.opacity(0.15)) 45 | .cornerRadius(5) 46 | 47 | HStack { 48 | Image(systemName: "arrow.down") 49 | .font(.title) 50 | .padding(.vertical, 10) 51 | .foregroundColor(.gray) 52 | 53 | Text("will trigger the keycode") 54 | .foregroundColor(.gray) 55 | } 56 | 57 | // 2. record key sent 58 | KeyCodeView( 59 | valueForEvent: self.valueForEvent, 60 | showBackground: true, 61 | onChange: { 62 | self.valueForEvent = ValueForEvent.keyDown( 63 | .init(keyCode: $0)) 64 | self.keyBinding.valueForEvent = self.valueForEvent! 65 | } 66 | ) 67 | .frame(width: 200) 68 | 69 | HStack { 70 | Image(systemName: "arrow.down") 71 | .font(.title) 72 | .padding(.vertical, 10) 73 | .foregroundColor(.gray) 74 | 75 | Text("that will be sent to") 76 | .foregroundColor(.gray) 77 | } 78 | 79 | // 3. pick to Application 80 | SelectedAppView( 81 | appItem: self.appItem, 82 | onSelected: { appItem in 83 | keyBinding.targetApplication = appItem.url 84 | isAppListViewPresented = false 85 | } 86 | ) 87 | .padding() 88 | .frame(width: 200) 89 | .background(Color.gray.opacity(0.15)) 90 | .cornerRadius(5) 91 | 92 | } 93 | .padding() 94 | 95 | HStack { 96 | Button("Cancel", role: .cancel) { 97 | dismiss() 98 | } 99 | Button("Save") { 100 | onSave(keyBinding) 101 | } 102 | .buttonStyle(BorderedButtonStyle()) 103 | .keyboardShortcut(.defaultAction) 104 | .disabled(!canBeSaved) 105 | } 106 | } 107 | .padding() 108 | .frame(width: 400) 109 | } 110 | } 111 | 112 | #Preview { 113 | KeyBindingCreateView( 114 | onSave: { _ in } 115 | ) 116 | .environment( 117 | \.managedObjectContext, 118 | PersistenceController.preview.container.viewContext 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /KeyPhantom/Library/AppListManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppListManager.swift 3 | // KeyPhantom 4 | // 5 | // Created by situ on 20/2/2025. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | 11 | struct AppItem: Identifiable, Hashable { 12 | var id = UUID() 13 | 14 | var url: URL 15 | 16 | var name: String 17 | var bundleIdentifier: String 18 | 19 | // TODO: can it be lazily init? 20 | var icon: NSImage 21 | 22 | init( 23 | url: URL, name: String, bundleIdentifier: String, 24 | icon: NSImage 25 | ) { 26 | self.url = url 27 | self.name = name 28 | self.bundleIdentifier = bundleIdentifier 29 | self.icon = icon 30 | } 31 | } 32 | 33 | class AppListManager: ObservableObject { 34 | /// Singleton instance of the AppListManager. 35 | static let shared = AppListManager() 36 | 37 | /// Allowed path 38 | private let allowedPaths = [ 39 | "/Applications", 40 | "/System/Applications", 41 | ] 42 | 43 | // array of app names and their corresponding icons 44 | @Published var appList: [AppItem] = [] 45 | 46 | // update appList 47 | func updateAppList() { 48 | self.appList = getAllAppItems() 49 | } 50 | 51 | private init() { 52 | self.updateAppList() 53 | } 54 | 55 | /// Returns whether the app with the given name is currently running. 56 | func isAppRunning(appName: String) -> Bool { 57 | let workspace = NSWorkspace.shared 58 | let runningApps = workspace.runningApplications 59 | return runningApps.contains { $0.localizedName == appName } 60 | } 61 | 62 | /// Returns all app bundle URLs (.app directories) found in the given directory. 63 | private func getApplications(in directory: String) -> [URL] { 64 | var appURLs: [URL] = [] 65 | let fileManager = FileManager.default 66 | let url = URL(fileURLWithPath: directory, isDirectory: true) 67 | 68 | if let enumerator = fileManager.enumerator( 69 | at: url, 70 | includingPropertiesForKeys: [.isDirectoryKey], 71 | options: [.skipsHiddenFiles]) 72 | { 73 | for case let fileURL as URL in enumerator { 74 | // Check if the URL represents an app bundle. 75 | if fileURL.pathExtension == "app" { 76 | appURLs.append(fileURL) 77 | // Skip further enumeration inside the app bundle. 78 | enumerator.skipDescendants() 79 | } 80 | } 81 | } 82 | return appURLs 83 | } 84 | 85 | private func isUrlStartingWithAllowedPaths(url: URL) -> Bool { 86 | return self.allowedPaths.contains { url.path.starts(with: $0) } 87 | } 88 | 89 | /// Prints the app’s name, bundle identifier, and icon size. 90 | func getAppItem(from appURL: URL) -> AppItem? { 91 | if !isUrlStartingWithAllowedPaths(url: appURL) { 92 | return nil 93 | } 94 | 95 | guard let bundle = Bundle(url: appURL) else { 96 | return nil 97 | } 98 | 99 | let appName = appURL.deletingPathExtension().lastPathComponent 100 | let bundleIdentifier = bundle.bundleIdentifier ?? "Unknown" 101 | 102 | // Get the app icon using NSWorkspace. 103 | let icon = NSWorkspace.shared.icon(forFile: appURL.path) 104 | 105 | // #if DEBUG 106 | // print("App Name: \(appName)") 107 | // print("Bundle Identifier: \(bundleIdentifier)") 108 | // print( 109 | // "Icon: \(icon)" 110 | // ) 111 | // print(String(repeating: "-", count: 40)) 112 | // #endif 113 | 114 | return AppItem( 115 | url: appURL, name: appName, bundleIdentifier: bundleIdentifier, 116 | icon: icon) 117 | } 118 | 119 | func getAllAppItems() -> [AppItem] { 120 | let appURLs = self.allowedPaths.flatMap { getApplications(in: $0) } 121 | return appURLs.compactMap { getAppItem(from: $0) } 122 | } 123 | 124 | func getURLForTest() -> URL { 125 | URL.init( 126 | string: 127 | "file:///Applications/%E5%BE%AE%E4%BF%A1%E8%AF%BB%E4%B9%A6.app/" 128 | )! 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /KeyPhantom/Library/KeyString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyString.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 23/2/2025. 6 | // 7 | 8 | import Carbon.HIToolbox 9 | import Foundation 10 | 11 | /// I have exchanged the mapping of key and value based on the file `Key.swift` in repo https://github.com/sindresorhus/KeyboardShortcuts 12 | extension KeyboardKey { 13 | private static let keyCodeToDescription: [Int: String] = [ 14 | // Letters 15 | kVK_ANSI_A: "a", 16 | kVK_ANSI_B: "b", 17 | kVK_ANSI_C: "c", 18 | kVK_ANSI_D: "d", 19 | kVK_ANSI_E: "e", 20 | kVK_ANSI_F: "f", 21 | kVK_ANSI_G: "g", 22 | kVK_ANSI_H: "h", 23 | kVK_ANSI_I: "i", 24 | kVK_ANSI_J: "j", 25 | kVK_ANSI_K: "k", 26 | kVK_ANSI_L: "l", 27 | kVK_ANSI_M: "m", 28 | kVK_ANSI_N: "n", 29 | kVK_ANSI_O: "o", 30 | kVK_ANSI_P: "p", 31 | kVK_ANSI_Q: "q", 32 | kVK_ANSI_R: "r", 33 | kVK_ANSI_S: "s", 34 | kVK_ANSI_T: "t", 35 | kVK_ANSI_U: "u", 36 | kVK_ANSI_V: "v", 37 | kVK_ANSI_W: "w", 38 | kVK_ANSI_X: "x", 39 | kVK_ANSI_Y: "y", 40 | kVK_ANSI_Z: "z", 41 | 42 | // Numbers 43 | kVK_ANSI_0: "zero", 44 | kVK_ANSI_1: "one", 45 | kVK_ANSI_2: "two", 46 | kVK_ANSI_3: "three", 47 | kVK_ANSI_4: "four", 48 | kVK_ANSI_5: "five", 49 | kVK_ANSI_6: "six", 50 | kVK_ANSI_7: "seven", 51 | kVK_ANSI_8: "eight", 52 | kVK_ANSI_9: "nine", 53 | 54 | // Modifiers 55 | kVK_CapsLock: "capsLock", 56 | kVK_Shift: "shift", 57 | kVK_Function: "function", 58 | kVK_Control: "control", 59 | kVK_Option: "option", 60 | kVK_Command: "command", 61 | kVK_RightCommand: "rightCommand", 62 | kVK_RightOption: "rightOption", 63 | kVK_RightControl: "rightControl", 64 | kVK_RightShift: "rightShift", 65 | 66 | // Miscellaneous 67 | kVK_Return: "return", 68 | kVK_ANSI_Backslash: "backslash", 69 | kVK_ANSI_Grave: "backtick", 70 | kVK_ANSI_Comma: "comma", 71 | kVK_ANSI_Equal: "equal", 72 | kVK_ANSI_Minus: "minus", 73 | kVK_ANSI_Period: "period", 74 | kVK_ANSI_Quote: "quote", 75 | kVK_ANSI_Semicolon: "semicolon", 76 | kVK_ANSI_Slash: "slash", 77 | kVK_Space: "space", 78 | kVK_Tab: "tab", 79 | kVK_ANSI_LeftBracket: "leftBracket", 80 | kVK_ANSI_RightBracket: "rightBracket", 81 | kVK_PageUp: "pageUp", 82 | kVK_PageDown: "pageDown", 83 | kVK_Home: "home", 84 | kVK_End: "end", 85 | kVK_UpArrow: "upArrow", 86 | kVK_RightArrow: "rightArrow", 87 | kVK_DownArrow: "downArrow", 88 | kVK_LeftArrow: "leftArrow", 89 | kVK_Escape: "escape", 90 | kVK_Delete: "delete", 91 | kVK_ForwardDelete: "deleteForward", 92 | kVK_Help: "help", 93 | kVK_Mute: "mute", 94 | kVK_VolumeUp: "volumeUp", 95 | kVK_VolumeDown: "volumeDown", 96 | 97 | // Function Keys 98 | kVK_F1: "f1", 99 | kVK_F2: "f2", 100 | kVK_F3: "f3", 101 | kVK_F4: "f4", 102 | kVK_F5: "f5", 103 | kVK_F6: "f6", 104 | kVK_F7: "f7", 105 | kVK_F8: "f8", 106 | kVK_F9: "f9", 107 | kVK_F10: "f10", 108 | kVK_F11: "f11", 109 | kVK_F12: "f12", 110 | kVK_F13: "f13", 111 | kVK_F14: "f14", 112 | kVK_F15: "f15", 113 | kVK_F16: "f16", 114 | kVK_F17: "f17", 115 | kVK_F18: "f18", 116 | kVK_F19: "f19", 117 | kVK_F20: "f20", 118 | 119 | // Keypad 120 | kVK_ANSI_Keypad0: "keypad0", 121 | kVK_ANSI_Keypad1: "keypad1", 122 | kVK_ANSI_Keypad2: "keypad2", 123 | kVK_ANSI_Keypad3: "keypad3", 124 | kVK_ANSI_Keypad4: "keypad4", 125 | kVK_ANSI_Keypad5: "keypad5", 126 | kVK_ANSI_Keypad6: "keypad6", 127 | kVK_ANSI_Keypad7: "keypad7", 128 | kVK_ANSI_Keypad8: "keypad8", 129 | kVK_ANSI_Keypad9: "keypad9", 130 | kVK_ANSI_KeypadClear: "keypadClear", 131 | kVK_ANSI_KeypadDecimal: "keypadDecimal", 132 | kVK_ANSI_KeypadDivide: "keypadDivide", 133 | kVK_ANSI_KeypadEnter: "keypadEnter", 134 | kVK_ANSI_KeypadEquals: "keypadEquals", 135 | kVK_ANSI_KeypadMinus: "keypadMinus", 136 | kVK_ANSI_KeypadMultiply: "keypadMultiply", 137 | kVK_ANSI_KeypadPlus: "keypadPlus", 138 | ] 139 | 140 | // description of keycode 141 | var description: String { 142 | return Self.keyCodeToDescription[keyCode] ?? "unknown" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /KeyPhantom/View/KeyBindingRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBindingRowView.swift 3 | // KeyPhantom 4 | // 5 | // Created by situ on 21/2/2025. 6 | // 7 | 8 | import AppKit 9 | import KeyboardShortcuts 10 | import SwiftUI 11 | 12 | struct KeyBindingRowView: View { 13 | @Binding var keyBinding: KeyBinding 14 | 15 | @State private var isAppListViewPresented = false 16 | @State private var isDeleteAlertPresented = false 17 | 18 | // if nil, the key binding will not be updated 19 | // else, it will call the related method to update the key binding 20 | var keyBindingManager: KeyBindingManager? = nil 21 | 22 | var appItem: AppItem? { 23 | AppListManager.shared.getAppItem(from: keyBinding.targetApplication) 24 | } 25 | 26 | var body: some View { 27 | VStack { 28 | 29 | if keyBindingManager == nil { 30 | // List Header 31 | HStack(spacing: 0) { 32 | Text("Shortcuts") 33 | .frame(width: 150, alignment: .center) 34 | 35 | Divider().frame(width: 1) 36 | 37 | Text("Key Sent") 38 | .frame(width: 150, alignment: .center) 39 | 40 | Divider().frame(width: 1) 41 | 42 | Text("To application") 43 | .frame(width: 200, alignment: .center) 44 | 45 | Divider().frame(width: 1) 46 | 47 | Text("Enabled") 48 | .frame(width: 100, alignment: .center) 49 | } 50 | .padding(.horizontal) 51 | .background(Color.gray.opacity(0.15)) 52 | .frame(height: 25) 53 | } 54 | 55 | HStack(spacing: 0) { 56 | // Display the shortcut key with a recorder 57 | KeyboardShortcuts.Recorder( 58 | for: KeyboardShortcuts.Name( 59 | keyBinding.shortcutKeyName) 60 | ) 61 | .frame(width: 150, alignment: .center) 62 | 63 | Spacer().frame(width: 1) 64 | 65 | // Right arrow 66 | // Image(systemName: "arrow.right") 67 | // .foregroundColor(.gray) 68 | 69 | switch keyBinding.valueForEvent { 70 | case .keyDown(_): 71 | KeyCodeView( 72 | valueForEvent: keyBinding.valueForEvent, 73 | onChange: { newValue in 74 | keyBinding.valueForEvent = .keyDown( 75 | KeyboardKey(keyCode: newValue)) 76 | 77 | // if keyBindingManager is not nil, update the key binding 78 | self.keyBindingManager?.updateKeyBinding(keyBinding) 79 | } 80 | ) 81 | .frame(width: 150, alignment: .center) 82 | default: 83 | Text("Unknown") 84 | .frame(width: 150, alignment: .center) 85 | } 86 | 87 | Spacer().frame(width: 1) 88 | 89 | SelectedAppView( 90 | appItem: self.appItem, 91 | onSelected: { appItem in 92 | // Actually update the target application 93 | keyBinding.targetApplication = appItem.url 94 | 95 | // delete if the key binding manager is not nil 96 | self.keyBindingManager?.updateKeyBinding(keyBinding) 97 | 98 | isAppListViewPresented = false 99 | } 100 | ) 101 | .frame(width: 200) 102 | 103 | Spacer().frame(width: 1) 104 | 105 | // Checkbox for enable/disable 106 | Toggle( 107 | "", 108 | isOn: Binding( 109 | get: { 110 | keyBinding.enabled 111 | }, 112 | set: { newValue in 113 | keyBinding.enabled = newValue 114 | // if keyBindingManager is not nil, update the key binding 115 | self.keyBindingManager?.updateKeyBinding(keyBinding) 116 | } 117 | ) 118 | ) 119 | .frame(width: 100, alignment: .center) 120 | 121 | Spacer().frame(width: 1) 122 | 123 | // Delete 124 | if let keyBindingManager = keyBindingManager { 125 | Button { 126 | isDeleteAlertPresented = true 127 | } label: { 128 | Image(systemName: "trash") 129 | .foregroundColor(.red) 130 | } 131 | .alert( 132 | "Delete this key binding?", 133 | isPresented: $isDeleteAlertPresented 134 | ) { 135 | Button("Delete", role: .destructive) { 136 | keyBindingManager.deleteKeyBinding(keyBinding) 137 | isDeleteAlertPresented = false 138 | } 139 | } 140 | .frame(width: 100, alignment: .center) 141 | } 142 | } 143 | .frame(height: 25) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /KeyPhantom/KeyPhantomApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPhantomApp.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 20/2/2025. 6 | // 7 | 8 | import LaunchAtLogin 9 | import Sparkle 10 | import SwiftUI 11 | 12 | @main 13 | struct KeyPhantomApp: App { 14 | private let updaterController: SPUStandardUpdaterController 15 | 16 | let persistenceController = PersistenceController.shared 17 | 18 | @StateObject private var keyBindingManager = KeyBindingManager( 19 | context: PersistenceController.shared.container.viewContext) 20 | 21 | private var accessibilityManager = AccessibilityManager.shared 22 | 23 | let appListManager = AppListManager.shared 24 | 25 | init() { 26 | // If you want to start the updater manually, pass false to startingUpdater and call .startUpdater() later 27 | // This is where you can also pass an updater delegate if you need one 28 | updaterController = SPUStandardUpdaterController( 29 | startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil 30 | ) 31 | 32 | // check Accessibility 33 | self.accessibilityManager.checkAccessibility() 34 | } 35 | 36 | @State private var selectedTab = 0 37 | 38 | var body: some Scene { 39 | Settings { 40 | TabView(selection: $selectedTab) { 41 | KeyBindingView() 42 | .tabItem { 43 | Label("Key Binding", systemImage: "keyboard") 44 | } 45 | .tag(0) 46 | 47 | UpdaterSettingsView(updater: self.updaterController.updater) 48 | .tabItem { 49 | Label( 50 | "Updater", 51 | systemImage: "arrow.triangle.2.circlepath") 52 | } 53 | .tag(1) 54 | 55 | AboutView() 56 | .tabItem { 57 | Label("About", systemImage: "info.circle") 58 | } 59 | .tag(2) 60 | } 61 | // .onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { 62 | // _ in 63 | // self.accessibilityManager.checkAccessibility() 64 | // } 65 | .frame(maxWidth: .infinity, minHeight: 480) 66 | .environment( 67 | \.managedObjectContext, 68 | persistenceController.container.viewContext 69 | ) 70 | .environmentObject(keyBindingManager) 71 | .background( 72 | VisualEffectView( 73 | material: NSVisualEffectView.Material.fullScreenUI, 74 | blendingMode: NSVisualEffectView.BlendingMode 75 | .withinWindow)) 76 | 77 | } 78 | .defaultSize(width: 700, height: 400) 79 | 80 | MenuBarExtra { 81 | // Status: active/inactive 82 | Text( 83 | "Status: \(keyBindingManager.isHandlerEnabledForView ? "Active" : "Inactive")" 84 | ) 85 | 86 | Toggle("Enable KeyPhantom", isOn: Binding( 87 | get: { keyBindingManager.isHandlerEnabledForView }, 88 | set: { $0 ? keyBindingManager.enableHandler() : keyBindingManager.disableHandler() } 89 | )) 90 | .keyboardShortcut("k", modifiers: [.command]) 91 | 92 | Divider() 93 | 94 | LaunchAtLogin.Toggle() 95 | 96 | CheckForUpdatesView(updater: updaterController.updater) 97 | 98 | Divider() 99 | 100 | // Credit: https://stackoverflow.com/a/77265223 101 | if #available(macOS 14.0, *) { 102 | SettingsLink { 103 | Text("Settings") 104 | } 105 | .keyboardShortcut(",", modifiers: [.command]) 106 | } else { 107 | Button( 108 | action: { 109 | if #available(macOS 13.0, *) { 110 | NSApp.sendAction( 111 | Selector(("showSettingsWindow:")), to: nil, 112 | from: nil) 113 | } else { 114 | NSApp.sendAction( 115 | Selector(("showPreferencesWindow:")), to: nil, 116 | from: nil) 117 | } 118 | }, 119 | label: { 120 | Text("Settings") 121 | } 122 | ) 123 | .keyboardShortcut(",", modifiers: [.command]) 124 | } 125 | 126 | // quit 127 | Button("Quit KeyPhantom") { 128 | NSApplication.shared.terminate(self) 129 | } 130 | .keyboardShortcut("q", modifiers: [.command]) 131 | 132 | } label: { 133 | keyBindingManager.isHandlerEnabledForView 134 | ? Image(systemName: "keyboard.macwindow") 135 | : Image("custom.keyboard.macwindow.slash") 136 | } 137 | .menuBarExtraStyle(.menu) 138 | 139 | } 140 | } 141 | 142 | // Credit: https://stackoverflow.com/a/61458115 143 | struct VisualEffectView: NSViewRepresentable { 144 | let material: NSVisualEffectView.Material 145 | let blendingMode: NSVisualEffectView.BlendingMode 146 | 147 | func makeNSView(context: Context) -> NSVisualEffectView { 148 | let visualEffectView = NSVisualEffectView() 149 | visualEffectView.material = material 150 | visualEffectView.blendingMode = blendingMode 151 | visualEffectView.state = NSVisualEffectView.State.active 152 | return visualEffectView 153 | } 154 | 155 | func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) 156 | { 157 | visualEffectView.material = material 158 | visualEffectView.blendingMode = blendingMode 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /KeyPhantom/Library/KeyBindingManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBindingManager.swift 3 | // KeyPhantom 4 | // 5 | // Created by Situ Yongcong on 23/2/2025. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | import KeyboardShortcuts 11 | import SwiftUI 12 | 13 | class KeyBindingManager: ObservableObject { 14 | @Published var keyBindings: [KeyBinding] = [] { 15 | didSet { 16 | print("[KeyBindingManager] keyBindings didSet") 17 | // TODO: brute-force, should refactor it. 18 | self.listenOnAllKeyBindingShortcuts() 19 | } 20 | } 21 | 22 | private var viewContext: NSManagedObjectContext 23 | 24 | init(context: NSManagedObjectContext) { 25 | self.viewContext = context 26 | self.fetchKeyBindings() 27 | 28 | self.listenOnAllKeyBindingShortcuts() 29 | 30 | #if DEBUG 31 | // self.keyBindings.append( 32 | // KeyBinding( 33 | // valueForEvent: .keyDown(KeyboardKey(keyCode: 0x7C)), 34 | // targetApplication: AppListManager.shared.getURLForTest()) 35 | // ) 36 | #endif 37 | } 38 | 39 | // MARK: - CRUD methods 40 | 41 | private func fetchKeyBindings() { 42 | let request: NSFetchRequest = 43 | CoreDataKeyBinding.fetchRequest() 44 | 45 | request.sortDescriptors = [ 46 | NSSortDescriptor( 47 | keyPath: \CoreDataKeyBinding.createdAt, ascending: true) 48 | ] 49 | 50 | do { 51 | let coreDataKeyBindings = try viewContext.fetch(request) 52 | keyBindings = try coreDataKeyBindings.map { coreDataKeyBinding in 53 | try coreDataKeyBinding.toModel() 54 | } 55 | } catch { 56 | // TODO: how to handle this error? 57 | print("Failed to fetch key bindings: \(error)") 58 | } 59 | } 60 | 61 | func addKeyBinding(_ keyBinding: KeyBinding) { 62 | let _ = CoreDataKeyBinding.fromModel( 63 | keyBinding, self.viewContext) 64 | 65 | self.saveContext() 66 | self.fetchKeyBindings() 67 | } 68 | 69 | func updateKeyBinding(_ keyBinding: KeyBinding) { 70 | let request: NSFetchRequest = 71 | CoreDataKeyBinding.fetchRequest() 72 | request.predicate = NSPredicate( 73 | format: "id == %@", keyBinding.id as CVarArg) 74 | do { 75 | let coreDataKeyBindings = try viewContext.fetch(request) 76 | if let coreDataKeyBinding = coreDataKeyBindings.first { 77 | // update all properties 78 | coreDataKeyBinding.targetApplication = 79 | keyBinding.targetApplication 80 | coreDataKeyBinding.valueForEventRef = keyBinding.valueForEvent 81 | coreDataKeyBinding.enabledBoolRef = keyBinding.enabled 82 | 83 | saveContext() 84 | fetchKeyBindings() 85 | } 86 | } catch { 87 | print("Failed to update key binding: \(error)") 88 | } 89 | } 90 | 91 | func deleteKeyBinding(_ keyBinding: KeyBinding) { 92 | let request: NSFetchRequest = 93 | CoreDataKeyBinding.fetchRequest() 94 | request.predicate = NSPredicate( 95 | format: "id == %@", keyBinding.id as CVarArg) 96 | do { 97 | let coreDataKeyBindings = try viewContext.fetch(request) 98 | if let coreDataKeyBinding = coreDataKeyBindings.first { 99 | viewContext.delete(coreDataKeyBinding) 100 | 101 | saveContext() 102 | fetchKeyBindings() 103 | } 104 | } catch { 105 | print("Failed to delete key binding: \(error)") 106 | } 107 | } 108 | 109 | private func saveContext() { 110 | do { 111 | try viewContext.save() 112 | } catch { 113 | print("Failed to save context: \(error)") 114 | } 115 | } 116 | 117 | // MARK: - Keyboard shortcuts listener 118 | 119 | @AppStorage("handlerEnabled") 120 | private var handlerEnabled = true 121 | 122 | var isHandlerEnabledForView: Bool { 123 | self.handlerEnabled 124 | } 125 | 126 | public func enableHandler() { 127 | AccessibilityManager.shared.checkAccessibility() 128 | if AccessibilityManager.shared.accessibilityEnabled { 129 | print("Accessibility is enabled") 130 | handlerEnabled = true 131 | } else { 132 | print("Accessibility is disabled. Please enable it.") 133 | handlerEnabled = false 134 | } 135 | } 136 | 137 | public func disableHandler() { 138 | handlerEnabled = false 139 | } 140 | 141 | private func listenOnAllKeyBindingShortcuts() { 142 | // remove 143 | KeyboardShortcuts.removeAllHandlers() 144 | 145 | keyBindings.forEach { binding in 146 | KeyboardShortcuts.onKeyDown(for: binding.getKeyBoardShortcutsName()) 147 | { 148 | [self] in 149 | if binding.enabled { 150 | onShortcutsKeyDown(for: binding) 151 | } 152 | } 153 | } 154 | } 155 | 156 | private func onShortcutsKeyDown(for keyBinding: KeyBinding) { 157 | if !handlerEnabled { 158 | print("Handler is disabled, ignore the event") 159 | return 160 | } 161 | 162 | print("Shortcut triggered for keyBinding: \(keyBinding)") 163 | 164 | let sender = KeyboardEventSender.shared 165 | 166 | switch keyBinding.valueForEvent { 167 | case .keyDown(let targetKey): 168 | do { 169 | print( 170 | "Sending keyDown event for key: \(targetKey) to application: \(keyBinding.targetApplication)" 171 | ) 172 | 173 | sender.send(key: targetKey, to: keyBinding.targetApplication) 174 | } 175 | default: 176 | do { 177 | print("TODO") 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /KeyPhantom/Library/KeyBinding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyBinding.swift 3 | // KeyPhantom 4 | // 5 | // Created by situ on 20/2/2025. 6 | // 7 | 8 | import AppKit 9 | import CoreData 10 | import Foundation 11 | import KeyboardShortcuts 12 | 13 | enum ValueForEvent: Codable { 14 | case keyDown(KeyboardKey) 15 | 16 | // TODO: leave it Int now. 17 | case scrollWheel(Int) 18 | 19 | // TODO: leave it Int now. 20 | case mouseClick(Int) 21 | } 22 | 23 | /// The core model of the app, representing a key binding. 24 | /// Relationship: User trigger a `shortcutKeyName` to trigger a `targetKey` for a `targetApplication`. 25 | /// We call the `targetKey` the Phantom Key. 26 | struct KeyBinding: Codable, Identifiable { 27 | var id: UUID = UUID() 28 | 29 | var enabled: Bool 30 | 31 | /// Store the name of the shortcut 32 | var shortcutKeyName: String 33 | 34 | /// Store the key code of the target key 35 | var valueForEvent: ValueForEvent 36 | 37 | /// Store the target application, to which the key binding is triggered. 38 | /// This url will be read by `AppListManager` 39 | var targetApplication: URL 40 | 41 | /// Initialize a new key binding from Core Data Model. 42 | init( 43 | valueForEvent: ValueForEvent, 44 | targetApplication: URL, 45 | enabled: Bool 46 | ) { 47 | self.shortcutKeyName = self.id.uuidString 48 | self.valueForEvent = valueForEvent 49 | self.targetApplication = targetApplication 50 | self.enabled = enabled 51 | } 52 | 53 | init( 54 | id: UUID, 55 | shortcutKeyName: String, 56 | valueForEvent: ValueForEvent, 57 | targetApplication: URL, 58 | enabled: Bool 59 | ) { 60 | self.id = id 61 | self.shortcutKeyName = shortcutKeyName 62 | self.valueForEvent = valueForEvent 63 | self.targetApplication = targetApplication 64 | self.enabled = enabled 65 | } 66 | 67 | func getKeyBoardShortcutsName() -> KeyboardShortcuts.Name { 68 | return KeyboardShortcuts.Name(self.shortcutKeyName) 69 | } 70 | 71 | static func defaultValue() -> KeyBinding { 72 | return KeyBinding( 73 | valueForEvent: .keyDown(KeyboardKey(keyCode: 0)), 74 | targetApplication: URL( 75 | fileURLWithPath: ""), 76 | enabled: true 77 | ) 78 | } 79 | } 80 | 81 | struct KeyboardKey: Codable { 82 | /// The key code of the keyboard key. 83 | var keyCode: Int 84 | 85 | // TODO: preserve the modifier flags 86 | /// The modifier flags of the keyboard key. 87 | var modifierFlags: NSEvent.ModifierFlags = [] 88 | 89 | var character: String { 90 | return String(UnicodeScalar(keyCode)!) 91 | } 92 | 93 | init(keyCode: Int) { 94 | self.keyCode = keyCode 95 | } 96 | 97 | init(keyCode: Int, modifierFlags: NSEvent.ModifierFlags) { 98 | self.keyCode = keyCode 99 | self.modifierFlags = modifierFlags 100 | } 101 | } 102 | 103 | extension NSEvent.ModifierFlags: Codable { 104 | public init(from decoder: Decoder) throws { 105 | let container = try decoder.singleValueContainer() 106 | let rawValue = try container.decode(UInt.self) 107 | self.init(rawValue: rawValue) 108 | } 109 | 110 | public func encode(to encoder: Encoder) throws { 111 | var container = encoder.singleValueContainer() 112 | try container.encode(rawValue) 113 | } 114 | } 115 | 116 | extension KeyboardKey { 117 | enum CodingKeys: String, CodingKey { 118 | case keyCode 119 | case modifierFlags 120 | } 121 | 122 | init(from decoder: any Decoder) throws { 123 | let container = try decoder.container(keyedBy: CodingKeys.self) 124 | self.keyCode = try container.decode(Int.self, forKey: .keyCode) 125 | 126 | // migiate from old version 127 | self.modifierFlags = 128 | try container.decodeIfPresent( 129 | NSEvent.ModifierFlags.self, forKey: .modifierFlags) ?? [] 130 | } 131 | } 132 | 133 | // MARK: - CoreData Model for KeyBinding 134 | 135 | extension CoreDataKeyBinding { 136 | var valueForEventRef: ValueForEvent { 137 | get { 138 | do { 139 | return try JSONDecoder().decode( 140 | ValueForEvent.self, from: valueForEventData!) 141 | } catch { 142 | print("Error decoding ValueForEvent: \(error)") 143 | 144 | // Return a default value 145 | return .keyDown(KeyboardKey(keyCode: 0)) 146 | } 147 | } 148 | set { 149 | do { 150 | valueForEventData = try JSONEncoder().encode(newValue) 151 | } catch { 152 | print("Error encoding ValueForEvent: \(error)") 153 | } 154 | } 155 | } 156 | 157 | var enabledBoolRef: Bool { 158 | get { 159 | if self.enabled == nil || self.enabled == 1 { 160 | return true 161 | } else if self.enabled == 0 { 162 | return false 163 | } else { 164 | fatalError("Invalid value for enabled: \(self.enabled!)") 165 | } 166 | } 167 | set { 168 | enabled = newValue ? 1 : 0 169 | } 170 | } 171 | 172 | static func fromModel( 173 | _ keyBinding: KeyBinding, _ context: NSManagedObjectContext 174 | ) -> CoreDataKeyBinding { 175 | let obj = CoreDataKeyBinding(context: context) 176 | obj.id = keyBinding.id 177 | obj.shortcutKeyName = keyBinding.shortcutKeyName 178 | obj.valueForEventRef = keyBinding.valueForEvent 179 | obj.targetApplication = keyBinding.targetApplication 180 | obj.enabled = keyBinding.enabled ? 1 : 0 181 | 182 | return obj 183 | } 184 | 185 | func toModel() throws -> KeyBinding { 186 | return KeyBinding( 187 | id: id!, 188 | shortcutKeyName: shortcutKeyName!, 189 | valueForEvent: valueForEventRef, 190 | targetApplication: targetApplication!, 191 | enabled: enabledBoolRef 192 | ) 193 | } 194 | 195 | override public func awakeFromInsert() { 196 | super.awakeFromInsert() 197 | self.createdAt = Date() 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /KeyPhantom.xcodeproj/xcuserdata/situ.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 25 | 37 | 38 | 39 | 41 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 73 | 85 | 86 | 87 | 89 | 101 | 102 | 103 | 105 | 117 | 118 | 119 | 121 | 133 | 134 | 135 | 137 | 149 | 150 | 151 | 153 | 165 | 166 | 167 | 169 | 181 | 182 | 183 | 185 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /KeyPhantom.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5504FAF22D7BF22C00681638 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 5504FAF12D7BF22C00681638 /* LaunchAtLogin */; }; 11 | 558DADB52D7489FB00603BB3 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 558DADB42D7489FB00603BB3 /* Sparkle */; }; 12 | 55BE19552D66D1AB00266107 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 55BE19542D66D1AB00266107 /* KeyboardShortcuts */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXContainerItemProxy section */ 16 | 552B57452D6639B200BA0B49 /* PBXContainerItemProxy */ = { 17 | isa = PBXContainerItemProxy; 18 | containerPortal = 552B57262D6639B000BA0B49 /* Project object */; 19 | proxyType = 1; 20 | remoteGlobalIDString = 552B572D2D6639B000BA0B49; 21 | remoteInfo = KeyPhantom; 22 | }; 23 | 552B574F2D6639B200BA0B49 /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 552B57262D6639B000BA0B49 /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 552B572D2D6639B000BA0B49; 28 | remoteInfo = KeyPhantom; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 552B572E2D6639B000BA0B49 /* KeyPhantom.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeyPhantom.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 552B57442D6639B200BA0B49 /* KeyPhantomTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeyPhantomTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 552B574E2D6639B200BA0B49 /* KeyPhantomUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KeyPhantomUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | /* End PBXFileReference section */ 37 | 38 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 39 | 55529EDE2D66D95000E5FE77 /* Exceptions for "KeyPhantom" folder in "KeyPhantom" target */ = { 40 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 41 | membershipExceptions = ( 42 | Info.plist, 43 | ); 44 | target = 552B572D2D6639B000BA0B49 /* KeyPhantom */; 45 | }; 46 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 47 | 48 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 49 | 552B57302D6639B000BA0B49 /* KeyPhantom */ = { 50 | isa = PBXFileSystemSynchronizedRootGroup; 51 | exceptions = ( 52 | 55529EDE2D66D95000E5FE77 /* Exceptions for "KeyPhantom" folder in "KeyPhantom" target */, 53 | ); 54 | path = KeyPhantom; 55 | sourceTree = ""; 56 | }; 57 | 552B57472D6639B200BA0B49 /* KeyPhantomTests */ = { 58 | isa = PBXFileSystemSynchronizedRootGroup; 59 | path = KeyPhantomTests; 60 | sourceTree = ""; 61 | }; 62 | 552B57512D6639B200BA0B49 /* KeyPhantomUITests */ = { 63 | isa = PBXFileSystemSynchronizedRootGroup; 64 | path = KeyPhantomUITests; 65 | sourceTree = ""; 66 | }; 67 | /* End PBXFileSystemSynchronizedRootGroup section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 552B572B2D6639B000BA0B49 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | 5504FAF22D7BF22C00681638 /* LaunchAtLogin in Frameworks */, 75 | 558DADB52D7489FB00603BB3 /* Sparkle in Frameworks */, 76 | 55BE19552D66D1AB00266107 /* KeyboardShortcuts in Frameworks */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | 552B57412D6639B200BA0B49 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | 552B574B2D6639B200BA0B49 /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | /* End PBXFrameworksBuildPhase section */ 95 | 96 | /* Begin PBXGroup section */ 97 | 552B57252D6639B000BA0B49 = { 98 | isa = PBXGroup; 99 | children = ( 100 | 552B57302D6639B000BA0B49 /* KeyPhantom */, 101 | 552B57472D6639B200BA0B49 /* KeyPhantomTests */, 102 | 552B57512D6639B200BA0B49 /* KeyPhantomUITests */, 103 | 552B572F2D6639B000BA0B49 /* Products */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | 552B572F2D6639B000BA0B49 /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 552B572E2D6639B000BA0B49 /* KeyPhantom.app */, 111 | 552B57442D6639B200BA0B49 /* KeyPhantomTests.xctest */, 112 | 552B574E2D6639B200BA0B49 /* KeyPhantomUITests.xctest */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | /* End PBXGroup section */ 118 | 119 | /* Begin PBXNativeTarget section */ 120 | 552B572D2D6639B000BA0B49 /* KeyPhantom */ = { 121 | isa = PBXNativeTarget; 122 | buildConfigurationList = 552B57582D6639B200BA0B49 /* Build configuration list for PBXNativeTarget "KeyPhantom" */; 123 | buildPhases = ( 124 | 552B572A2D6639B000BA0B49 /* Sources */, 125 | 552B572B2D6639B000BA0B49 /* Frameworks */, 126 | 552B572C2D6639B000BA0B49 /* Resources */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | ); 132 | fileSystemSynchronizedGroups = ( 133 | 552B57302D6639B000BA0B49 /* KeyPhantom */, 134 | ); 135 | name = KeyPhantom; 136 | packageProductDependencies = ( 137 | 55BE19542D66D1AB00266107 /* KeyboardShortcuts */, 138 | 558DADB42D7489FB00603BB3 /* Sparkle */, 139 | 5504FAF12D7BF22C00681638 /* LaunchAtLogin */, 140 | ); 141 | productName = KeyPhantom; 142 | productReference = 552B572E2D6639B000BA0B49 /* KeyPhantom.app */; 143 | productType = "com.apple.product-type.application"; 144 | }; 145 | 552B57432D6639B200BA0B49 /* KeyPhantomTests */ = { 146 | isa = PBXNativeTarget; 147 | buildConfigurationList = 552B575B2D6639B200BA0B49 /* Build configuration list for PBXNativeTarget "KeyPhantomTests" */; 148 | buildPhases = ( 149 | 552B57402D6639B200BA0B49 /* Sources */, 150 | 552B57412D6639B200BA0B49 /* Frameworks */, 151 | 552B57422D6639B200BA0B49 /* Resources */, 152 | ); 153 | buildRules = ( 154 | ); 155 | dependencies = ( 156 | 552B57462D6639B200BA0B49 /* PBXTargetDependency */, 157 | ); 158 | fileSystemSynchronizedGroups = ( 159 | 552B57472D6639B200BA0B49 /* KeyPhantomTests */, 160 | ); 161 | name = KeyPhantomTests; 162 | packageProductDependencies = ( 163 | ); 164 | productName = KeyPhantomTests; 165 | productReference = 552B57442D6639B200BA0B49 /* KeyPhantomTests.xctest */; 166 | productType = "com.apple.product-type.bundle.unit-test"; 167 | }; 168 | 552B574D2D6639B200BA0B49 /* KeyPhantomUITests */ = { 169 | isa = PBXNativeTarget; 170 | buildConfigurationList = 552B575E2D6639B200BA0B49 /* Build configuration list for PBXNativeTarget "KeyPhantomUITests" */; 171 | buildPhases = ( 172 | 552B574A2D6639B200BA0B49 /* Sources */, 173 | 552B574B2D6639B200BA0B49 /* Frameworks */, 174 | 552B574C2D6639B200BA0B49 /* Resources */, 175 | ); 176 | buildRules = ( 177 | ); 178 | dependencies = ( 179 | 552B57502D6639B200BA0B49 /* PBXTargetDependency */, 180 | ); 181 | fileSystemSynchronizedGroups = ( 182 | 552B57512D6639B200BA0B49 /* KeyPhantomUITests */, 183 | ); 184 | name = KeyPhantomUITests; 185 | packageProductDependencies = ( 186 | ); 187 | productName = KeyPhantomUITests; 188 | productReference = 552B574E2D6639B200BA0B49 /* KeyPhantomUITests.xctest */; 189 | productType = "com.apple.product-type.bundle.ui-testing"; 190 | }; 191 | /* End PBXNativeTarget section */ 192 | 193 | /* Begin PBXProject section */ 194 | 552B57262D6639B000BA0B49 /* Project object */ = { 195 | isa = PBXProject; 196 | attributes = { 197 | BuildIndependentTargetsInParallel = 1; 198 | LastSwiftUpdateCheck = 1620; 199 | LastUpgradeCheck = 1620; 200 | TargetAttributes = { 201 | 552B572D2D6639B000BA0B49 = { 202 | CreatedOnToolsVersion = 16.2; 203 | }; 204 | 552B57432D6639B200BA0B49 = { 205 | CreatedOnToolsVersion = 16.2; 206 | TestTargetID = 552B572D2D6639B000BA0B49; 207 | }; 208 | 552B574D2D6639B200BA0B49 = { 209 | CreatedOnToolsVersion = 16.2; 210 | TestTargetID = 552B572D2D6639B000BA0B49; 211 | }; 212 | }; 213 | }; 214 | buildConfigurationList = 552B57292D6639B000BA0B49 /* Build configuration list for PBXProject "KeyPhantom" */; 215 | developmentRegion = en; 216 | hasScannedForEncodings = 0; 217 | knownRegions = ( 218 | en, 219 | Base, 220 | ); 221 | mainGroup = 552B57252D6639B000BA0B49; 222 | minimizedProjectReferenceProxies = 1; 223 | packageReferences = ( 224 | 55BE19532D66D17000266107 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 225 | 558DADB32D7489FB00603BB3 /* XCRemoteSwiftPackageReference "Sparkle" */, 226 | 5504FAF02D7BF22C00681638 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, 227 | ); 228 | preferredProjectObjectVersion = 77; 229 | productRefGroup = 552B572F2D6639B000BA0B49 /* Products */; 230 | projectDirPath = ""; 231 | projectRoot = ""; 232 | targets = ( 233 | 552B572D2D6639B000BA0B49 /* KeyPhantom */, 234 | 552B57432D6639B200BA0B49 /* KeyPhantomTests */, 235 | 552B574D2D6639B200BA0B49 /* KeyPhantomUITests */, 236 | ); 237 | }; 238 | /* End PBXProject section */ 239 | 240 | /* Begin PBXResourcesBuildPhase section */ 241 | 552B572C2D6639B000BA0B49 /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | runOnlyForDeploymentPostprocessing = 0; 247 | }; 248 | 552B57422D6639B200BA0B49 /* Resources */ = { 249 | isa = PBXResourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | 552B574C2D6639B200BA0B49 /* Resources */ = { 256 | isa = PBXResourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | /* End PBXResourcesBuildPhase section */ 263 | 264 | /* Begin PBXSourcesBuildPhase section */ 265 | 552B572A2D6639B000BA0B49 /* Sources */ = { 266 | isa = PBXSourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | 552B57402D6639B200BA0B49 /* Sources */ = { 273 | isa = PBXSourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | 552B574A2D6639B200BA0B49 /* Sources */ = { 280 | isa = PBXSourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | /* End PBXSourcesBuildPhase section */ 287 | 288 | /* Begin PBXTargetDependency section */ 289 | 552B57462D6639B200BA0B49 /* PBXTargetDependency */ = { 290 | isa = PBXTargetDependency; 291 | target = 552B572D2D6639B000BA0B49 /* KeyPhantom */; 292 | targetProxy = 552B57452D6639B200BA0B49 /* PBXContainerItemProxy */; 293 | }; 294 | 552B57502D6639B200BA0B49 /* PBXTargetDependency */ = { 295 | isa = PBXTargetDependency; 296 | target = 552B572D2D6639B000BA0B49 /* KeyPhantom */; 297 | targetProxy = 552B574F2D6639B200BA0B49 /* PBXContainerItemProxy */; 298 | }; 299 | /* End PBXTargetDependency section */ 300 | 301 | /* Begin XCBuildConfiguration section */ 302 | 552B57562D6639B200BA0B49 /* Debug */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 307 | CLANG_ANALYZER_NONNULL = YES; 308 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_ENABLE_OBJC_WEAK = YES; 313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 314 | CLANG_WARN_BOOL_CONVERSION = YES; 315 | CLANG_WARN_COMMA = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 329 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 330 | CLANG_WARN_STRICT_PROTOTYPES = YES; 331 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 332 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | COPY_PHASE_STRIP = NO; 336 | DEBUG_INFORMATION_FORMAT = dwarf; 337 | ENABLE_STRICT_OBJC_MSGSEND = YES; 338 | ENABLE_TESTABILITY = YES; 339 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 340 | GCC_C_LANGUAGE_STANDARD = gnu17; 341 | GCC_DYNAMIC_NO_PIC = NO; 342 | GCC_NO_COMMON_BLOCKS = YES; 343 | GCC_OPTIMIZATION_LEVEL = 0; 344 | GCC_PREPROCESSOR_DEFINITIONS = ( 345 | "DEBUG=1", 346 | "$(inherited)", 347 | ); 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 355 | MACOSX_DEPLOYMENT_TARGET = 15.2; 356 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 357 | MTL_FAST_MATH = YES; 358 | ONLY_ACTIVE_ARCH = YES; 359 | SDKROOT = macosx; 360 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 361 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 362 | }; 363 | name = Debug; 364 | }; 365 | 552B57572D6639B200BA0B49 /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ALWAYS_SEARCH_USER_PATHS = NO; 369 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 370 | CLANG_ANALYZER_NONNULL = YES; 371 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 372 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_ENABLE_OBJC_WEAK = YES; 376 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 377 | CLANG_WARN_BOOL_CONVERSION = YES; 378 | CLANG_WARN_COMMA = YES; 379 | CLANG_WARN_CONSTANT_CONVERSION = YES; 380 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 381 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 382 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 383 | CLANG_WARN_EMPTY_BODY = YES; 384 | CLANG_WARN_ENUM_CONVERSION = YES; 385 | CLANG_WARN_INFINITE_RECURSION = YES; 386 | CLANG_WARN_INT_CONVERSION = YES; 387 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 388 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 389 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 390 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 391 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 392 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 393 | CLANG_WARN_STRICT_PROTOTYPES = YES; 394 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 395 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 396 | CLANG_WARN_UNREACHABLE_CODE = YES; 397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 398 | COPY_PHASE_STRIP = NO; 399 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 400 | ENABLE_NS_ASSERTIONS = NO; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 403 | GCC_C_LANGUAGE_STANDARD = gnu17; 404 | GCC_NO_COMMON_BLOCKS = YES; 405 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 406 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 407 | GCC_WARN_UNDECLARED_SELECTOR = YES; 408 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 409 | GCC_WARN_UNUSED_FUNCTION = YES; 410 | GCC_WARN_UNUSED_VARIABLE = YES; 411 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 412 | MACOSX_DEPLOYMENT_TARGET = 15.2; 413 | MTL_ENABLE_DEBUG_INFO = NO; 414 | MTL_FAST_MATH = YES; 415 | SDKROOT = macosx; 416 | SWIFT_COMPILATION_MODE = wholemodule; 417 | }; 418 | name = Release; 419 | }; 420 | 552B57592D6639B200BA0B49 /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 424 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 425 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 426 | CODE_SIGN_ENTITLEMENTS = KeyPhantom/KeyPhantom.entitlements; 427 | CODE_SIGN_IDENTITY = "Apple Development"; 428 | CODE_SIGN_STYLE = Automatic; 429 | COMBINE_HIDPI_IMAGES = YES; 430 | CURRENT_PROJECT_VERSION = 1; 431 | DEVELOPMENT_ASSET_PATHS = "\"KeyPhantom/Preview Content\""; 432 | DEVELOPMENT_TEAM = 9MUAVKPBZ9; 433 | ENABLE_HARDENED_RUNTIME = YES; 434 | ENABLE_PREVIEWS = YES; 435 | GENERATE_INFOPLIST_FILE = YES; 436 | INFOPLIST_FILE = KeyPhantom/Info.plist; 437 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 438 | LD_RUNPATH_SEARCH_PATHS = ( 439 | "$(inherited)", 440 | "@executable_path/../Frameworks", 441 | ); 442 | MACOSX_DEPLOYMENT_TARGET = 13.5; 443 | MARKETING_VERSION = 1.0; 444 | ONLY_ACTIVE_ARCH = YES; 445 | PRODUCT_BUNDLE_IDENTIFIER = com.situ2001.KeyPhantom; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | PROVISIONING_PROFILE_SPECIFIER = ""; 448 | SWIFT_EMIT_LOC_STRINGS = YES; 449 | SWIFT_VERSION = 5.0; 450 | }; 451 | name = Debug; 452 | }; 453 | 552B575A2D6639B200BA0B49 /* Release */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 457 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 458 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 459 | CODE_SIGN_ENTITLEMENTS = KeyPhantom/KeyPhantom.entitlements; 460 | CODE_SIGN_IDENTITY = "Apple Development"; 461 | CODE_SIGN_STYLE = Automatic; 462 | COMBINE_HIDPI_IMAGES = YES; 463 | CURRENT_PROJECT_VERSION = 1; 464 | DEVELOPMENT_ASSET_PATHS = "\"KeyPhantom/Preview Content\""; 465 | DEVELOPMENT_TEAM = 9MUAVKPBZ9; 466 | ENABLE_HARDENED_RUNTIME = YES; 467 | ENABLE_PREVIEWS = YES; 468 | GENERATE_INFOPLIST_FILE = YES; 469 | INFOPLIST_FILE = KeyPhantom/Info.plist; 470 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 471 | LD_RUNPATH_SEARCH_PATHS = ( 472 | "$(inherited)", 473 | "@executable_path/../Frameworks", 474 | ); 475 | MACOSX_DEPLOYMENT_TARGET = 13.5; 476 | MARKETING_VERSION = 1.0; 477 | ONLY_ACTIVE_ARCH = NO; 478 | PRODUCT_BUNDLE_IDENTIFIER = com.situ2001.KeyPhantom; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | PROVISIONING_PROFILE_SPECIFIER = ""; 481 | SWIFT_EMIT_LOC_STRINGS = YES; 482 | SWIFT_VERSION = 5.0; 483 | }; 484 | name = Release; 485 | }; 486 | 552B575C2D6639B200BA0B49 /* Debug */ = { 487 | isa = XCBuildConfiguration; 488 | buildSettings = { 489 | BUNDLE_LOADER = "$(TEST_HOST)"; 490 | CODE_SIGN_STYLE = Automatic; 491 | CURRENT_PROJECT_VERSION = 1; 492 | DEVELOPMENT_TEAM = 9MUAVKPBZ9; 493 | GENERATE_INFOPLIST_FILE = YES; 494 | MACOSX_DEPLOYMENT_TARGET = 15.2; 495 | MARKETING_VERSION = 1.0; 496 | PRODUCT_BUNDLE_IDENTIFIER = com.situ2001.KeyPhantomTests; 497 | PRODUCT_NAME = "$(TARGET_NAME)"; 498 | SWIFT_EMIT_LOC_STRINGS = NO; 499 | SWIFT_VERSION = 5.0; 500 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KeyPhantom.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KeyPhantom"; 501 | }; 502 | name = Debug; 503 | }; 504 | 552B575D2D6639B200BA0B49 /* Release */ = { 505 | isa = XCBuildConfiguration; 506 | buildSettings = { 507 | BUNDLE_LOADER = "$(TEST_HOST)"; 508 | CODE_SIGN_STYLE = Automatic; 509 | CURRENT_PROJECT_VERSION = 1; 510 | DEVELOPMENT_TEAM = 9MUAVKPBZ9; 511 | GENERATE_INFOPLIST_FILE = YES; 512 | MACOSX_DEPLOYMENT_TARGET = 15.2; 513 | MARKETING_VERSION = 1.0; 514 | PRODUCT_BUNDLE_IDENTIFIER = com.situ2001.KeyPhantomTests; 515 | PRODUCT_NAME = "$(TARGET_NAME)"; 516 | SWIFT_EMIT_LOC_STRINGS = NO; 517 | SWIFT_VERSION = 5.0; 518 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KeyPhantom.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/KeyPhantom"; 519 | }; 520 | name = Release; 521 | }; 522 | 552B575F2D6639B200BA0B49 /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | buildSettings = { 525 | CODE_SIGN_STYLE = Automatic; 526 | CURRENT_PROJECT_VERSION = 1; 527 | DEVELOPMENT_TEAM = 9MUAVKPBZ9; 528 | GENERATE_INFOPLIST_FILE = YES; 529 | MARKETING_VERSION = 1.0; 530 | PRODUCT_BUNDLE_IDENTIFIER = com.situ2001.KeyPhantomUITests; 531 | PRODUCT_NAME = "$(TARGET_NAME)"; 532 | SWIFT_EMIT_LOC_STRINGS = NO; 533 | SWIFT_VERSION = 5.0; 534 | TEST_TARGET_NAME = KeyPhantom; 535 | }; 536 | name = Debug; 537 | }; 538 | 552B57602D6639B200BA0B49 /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | CODE_SIGN_STYLE = Automatic; 542 | CURRENT_PROJECT_VERSION = 1; 543 | DEVELOPMENT_TEAM = 9MUAVKPBZ9; 544 | GENERATE_INFOPLIST_FILE = YES; 545 | MARKETING_VERSION = 1.0; 546 | PRODUCT_BUNDLE_IDENTIFIER = com.situ2001.KeyPhantomUITests; 547 | PRODUCT_NAME = "$(TARGET_NAME)"; 548 | SWIFT_EMIT_LOC_STRINGS = NO; 549 | SWIFT_VERSION = 5.0; 550 | TEST_TARGET_NAME = KeyPhantom; 551 | }; 552 | name = Release; 553 | }; 554 | /* End XCBuildConfiguration section */ 555 | 556 | /* Begin XCConfigurationList section */ 557 | 552B57292D6639B000BA0B49 /* Build configuration list for PBXProject "KeyPhantom" */ = { 558 | isa = XCConfigurationList; 559 | buildConfigurations = ( 560 | 552B57562D6639B200BA0B49 /* Debug */, 561 | 552B57572D6639B200BA0B49 /* Release */, 562 | ); 563 | defaultConfigurationIsVisible = 0; 564 | defaultConfigurationName = Release; 565 | }; 566 | 552B57582D6639B200BA0B49 /* Build configuration list for PBXNativeTarget "KeyPhantom" */ = { 567 | isa = XCConfigurationList; 568 | buildConfigurations = ( 569 | 552B57592D6639B200BA0B49 /* Debug */, 570 | 552B575A2D6639B200BA0B49 /* Release */, 571 | ); 572 | defaultConfigurationIsVisible = 0; 573 | defaultConfigurationName = Release; 574 | }; 575 | 552B575B2D6639B200BA0B49 /* Build configuration list for PBXNativeTarget "KeyPhantomTests" */ = { 576 | isa = XCConfigurationList; 577 | buildConfigurations = ( 578 | 552B575C2D6639B200BA0B49 /* Debug */, 579 | 552B575D2D6639B200BA0B49 /* Release */, 580 | ); 581 | defaultConfigurationIsVisible = 0; 582 | defaultConfigurationName = Release; 583 | }; 584 | 552B575E2D6639B200BA0B49 /* Build configuration list for PBXNativeTarget "KeyPhantomUITests" */ = { 585 | isa = XCConfigurationList; 586 | buildConfigurations = ( 587 | 552B575F2D6639B200BA0B49 /* Debug */, 588 | 552B57602D6639B200BA0B49 /* Release */, 589 | ); 590 | defaultConfigurationIsVisible = 0; 591 | defaultConfigurationName = Release; 592 | }; 593 | /* End XCConfigurationList section */ 594 | 595 | /* Begin XCRemoteSwiftPackageReference section */ 596 | 5504FAF02D7BF22C00681638 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { 597 | isa = XCRemoteSwiftPackageReference; 598 | repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; 599 | requirement = { 600 | kind = upToNextMajorVersion; 601 | minimumVersion = 1.0.0; 602 | }; 603 | }; 604 | 558DADB32D7489FB00603BB3 /* XCRemoteSwiftPackageReference "Sparkle" */ = { 605 | isa = XCRemoteSwiftPackageReference; 606 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 607 | requirement = { 608 | kind = upToNextMajorVersion; 609 | minimumVersion = 2.7.0; 610 | }; 611 | }; 612 | 55BE19532D66D17000266107 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 613 | isa = XCRemoteSwiftPackageReference; 614 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 615 | requirement = { 616 | kind = upToNextMajorVersion; 617 | minimumVersion = 2.2.4; 618 | }; 619 | }; 620 | /* End XCRemoteSwiftPackageReference section */ 621 | 622 | /* Begin XCSwiftPackageProductDependency section */ 623 | 5504FAF12D7BF22C00681638 /* LaunchAtLogin */ = { 624 | isa = XCSwiftPackageProductDependency; 625 | package = 5504FAF02D7BF22C00681638 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; 626 | productName = LaunchAtLogin; 627 | }; 628 | 558DADB42D7489FB00603BB3 /* Sparkle */ = { 629 | isa = XCSwiftPackageProductDependency; 630 | package = 558DADB32D7489FB00603BB3 /* XCRemoteSwiftPackageReference "Sparkle" */; 631 | productName = Sparkle; 632 | }; 633 | 55BE19542D66D1AB00266107 /* KeyboardShortcuts */ = { 634 | isa = XCSwiftPackageProductDependency; 635 | package = 55BE19532D66D17000266107 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 636 | productName = KeyboardShortcuts; 637 | }; 638 | /* End XCSwiftPackageProductDependency section */ 639 | }; 640 | rootObject = 552B57262D6639B000BA0B49 /* Project object */; 641 | } 642 | -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/custom.keyboard.slash.symbolset/custom.keyboard.slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 22 | 23 | 24 | 25 | Weight/Scale Variations 26 | Ultralight 27 | Thin 28 | Light 29 | Regular 30 | Medium 31 | Semibold 32 | Bold 33 | Heavy 34 | Black 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Design Variations 46 | Symbols are supported in up to nine weights and three scales. 47 | For optimal layout with text and other symbols, vertically align 48 | symbols with the adjacent text. 49 | 50 | 51 | 52 | 53 | 54 | Margins 55 | Leading and trailing margins on the left and right side of each symbol 56 | can be adjusted by modifying the x-location of the margin guidelines. 57 | Modifications are automatically applied proportionally to all 58 | scales and weights. 59 | 60 | 61 | 62 | Exporting 63 | Symbols should be outlined when exporting to ensure the 64 | design is preserved when submitting to Xcode. 65 | Template v.6.0 66 | Requires Xcode 16 or greater 67 | Generated from 68 | Typeset at 100.0 points 69 | Small 70 | Medium 71 | Large 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /KeyPhantom/Assets.xcassets/custom.keyboard.macwindow.slash.symbolset/custom.keyboard.macwindow.slash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 22 | 23 | 24 | 25 | Weight/Scale Variations 26 | Ultralight 27 | Thin 28 | Light 29 | Regular 30 | Medium 31 | Semibold 32 | Bold 33 | Heavy 34 | Black 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Design Variations 46 | Symbols are supported in up to nine weights and three scales. 47 | For optimal layout with text and other symbols, vertically align 48 | symbols with the adjacent text. 49 | 50 | 51 | 52 | 53 | 54 | Margins 55 | Leading and trailing margins on the left and right side of each symbol 56 | can be adjusted by modifying the x-location of the margin guidelines. 57 | Modifications are automatically applied proportionally to all 58 | scales and weights. 59 | 60 | 61 | 62 | Exporting 63 | Symbols should be outlined when exporting to ensure the 64 | design is preserved when submitting to Xcode. 65 | Template v.6.0 66 | Requires Xcode 16 or greater 67 | Generated from 68 | Typeset at 100.0 points 69 | Small 70 | Medium 71 | Large 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | --------------------------------------------------------------------------------