├── 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 |
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 |
114 |
--------------------------------------------------------------------------------
/KeyPhantom/Assets.xcassets/custom.keyboard.macwindow.slash.symbolset/custom.keyboard.macwindow.slash.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
114 |
--------------------------------------------------------------------------------