├── Commander
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ ├── 512.png
│ │ ├── 64.png
│ │ ├── 1024.png
│ │ ├── 256 1.png
│ │ ├── 32 1.png
│ │ ├── 512 1.png
│ │ └── Contents.json
│ ├── shortcut.imageset
│ │ ├── shortcut.png
│ │ └── Contents.json
│ ├── menu template.imageset
│ │ ├── menu template.png
│ │ ├── menu template@2x.png
│ │ ├── menu template@3x.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Common
│ ├── Extension
│ │ ├── NotificationName+Extension.swift
│ │ ├── CGPoint+Extension.swift
│ │ ├── View+Extension.swift
│ │ ├── Sequence+Extension.swift
│ │ └── NSWindow+Extension.swift
│ ├── View
│ │ ├── VisualEffectView.swift
│ │ └── TrackingAreaView.swift
│ └── Util
│ │ └── UserDefault.swift
├── main.swift
├── Info.plist
├── Models
│ ├── AppGroup.swift
│ └── App.swift
├── Manager
│ ├── AppStorageKey.swift
│ ├── MenuBarItemManager.swift
│ ├── AutoLaunchManager.swift
│ ├── AppRatingManager.swift
│ ├── DockIconManager.swift
│ ├── ShortcutNotifier.swift
│ ├── DIContainer.swift
│ ├── ShortcutsAppManager.swift
│ ├── ImageProvider.swift
│ ├── BookmarksManager.swift
│ ├── AppsManager.swift
│ └── AppSearcher.swift
├── Commander.entitlements
├── View
│ ├── WheelPicker
│ │ ├── WheelPickerHoverState.swift
│ │ ├── WheelPicker.swift
│ │ └── WheelPickerSection.swift
│ ├── Preferences
│ │ ├── SettingsView.swift
│ │ ├── URLInputView.swift
│ │ ├── SettingsTutorialView.swift
│ │ ├── SettingsSearchField.swift
│ │ ├── ShortcutKeysView.swift
│ │ └── SettingsSidebarView.swift
│ ├── Commander
│ │ └── CommanderView.swift
│ └── Feedback
│ │ ├── TextArea.swift
│ │ └── SendFeedbackView.swift
├── AppMenu.swift
└── AppDelegate.swift
├── Launcher
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── NotificationName+Extension.swift
├── main.swift
├── Info.plist
├── Launcher.entitlements
└── AppDelegate.swift
├── .gitignore
├── Commander.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── Commander.xcscheme
└── project.pbxproj
├── LICENSE
├── README.md
└── commander.swiftformat
/Commander/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Launcher/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/256 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/256 1.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/32 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/32 1.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/AppIcon.appiconset/512 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/AppIcon.appiconset/512 1.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/shortcut.imageset/shortcut.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/shortcut.imageset/shortcut.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Commander.xcodeproj/xcuserdata/*
2 | Commander.xcodeproj/project.xcworkspace/xcuserdata/*
3 | Commander.xcodeproj/project.xcworkspace/xcshareddata/*
4 | build/*
5 |
--------------------------------------------------------------------------------
/Launcher/NotificationName+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Notification.Name {
4 | static let killLauncher = Notification.Name("killLauncher")
5 | }
6 |
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/menu template.imageset/menu template.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/menu template.imageset/menu template.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/menu template.imageset/menu template@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/menu template.imageset/menu template@2x.png
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/menu template.imageset/menu template@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vadim-ahmerov/Commander/HEAD/Commander/Assets.xcassets/menu template.imageset/menu template@3x.png
--------------------------------------------------------------------------------
/Commander/Common/Extension/NotificationName+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Notification.Name {
4 | static let killLauncher = Notification.Name("killLauncher")
5 | }
6 |
--------------------------------------------------------------------------------
/Commander/main.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | let app = NSApplication.shared
4 | let delegate = AppDelegate()
5 | app.delegate = delegate
6 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
7 |
--------------------------------------------------------------------------------
/Launcher/main.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | let app = NSApplication.shared
4 | let delegate = AppDelegate()
5 | app.delegate = delegate
6 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
7 |
--------------------------------------------------------------------------------
/Commander/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Commander.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Commander/Common/Extension/CGPoint+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension CGPoint {
4 | func distance(to point: CGPoint) -> CGFloat {
5 | sqrt(pow(point.x - x, 2) + pow(point.y - y, 2))
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Launcher/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 |
--------------------------------------------------------------------------------
/Commander/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 |
--------------------------------------------------------------------------------
/Commander/Common/Extension/View+Extension.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | func scaleEffect(_ scaleFactor: CGFloat) -> some View {
5 | scaleEffect(CGSize(width: scaleFactor, height: scaleFactor))
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Commander/Models/AppGroup.swift:
--------------------------------------------------------------------------------
1 | // MARK: - AppGroup
2 |
3 | struct AppGroup {
4 | let name: String
5 | let apps: [App]
6 | }
7 |
8 | extension Collection where Element == AppGroup {
9 | var apps: [App] {
10 | flatMap(\.apps)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Launcher/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LSBackgroundOnly
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Commander/Common/Extension/Sequence+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Sequence {
4 | func sorted(by keyPath: KeyPath) -> [Element] {
5 | sorted { lhs, rhs in
6 | lhs[keyPath: keyPath] < rhs[keyPath: keyPath]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Commander/Manager/AppStorageKey.swift:
--------------------------------------------------------------------------------
1 | enum AppStorageKey {
2 | static let showOnMouseMove = "show on mouse move"
3 | static let showMenuBarItem = "show menu bar item"
4 | static let showDockIcon = "show dock icon"
5 | static let launchOnLogin = "launch on login"
6 | static let hapticFeedbackEnabled = "haptic feedback enabled"
7 | }
8 |
--------------------------------------------------------------------------------
/Launcher/Launcher.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/shortcut.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "shortcut.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Commander/Commander.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.bookmarks.app-scope
8 |
9 | com.apple.security.files.user-selected.read-only
10 |
11 | com.apple.security.network.client
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Commander/Manager/MenuBarItemManager.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import SwiftUI
3 |
4 | final class MenuBarItemManager {
5 | // MARK: Lifecycle
6 |
7 | init(statusItem: NSStatusItem) {
8 | self.statusItem = statusItem
9 |
10 | statusItem.isVisible = showMenuBarItem
11 | }
12 |
13 | // MARK: Internal
14 |
15 | func set(isVisible: Bool) {
16 | statusItem.isVisible = isVisible
17 | }
18 |
19 | // MARK: Private
20 |
21 | private let statusItem: NSStatusItem
22 | @AppStorage(AppStorageKey.showMenuBarItem) var showMenuBarItem = false
23 | }
24 |
--------------------------------------------------------------------------------
/Commander/Assets.xcassets/menu template.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "menu template.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "menu template@2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "menu template@3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | },
23 | "properties" : {
24 | "template-rendering-intent" : "template"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Commander/Manager/AutoLaunchManager.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import ServiceManagement
3 |
4 | final class AutoLaunchManager {
5 | func configureAutoLaunch(enabled: Bool) {
6 | let launcherAppID = "com.va.commander.launcher"
7 | let runningApps = NSWorkspace.shared.runningApplications
8 | let isRunning = runningApps.contains {
9 | $0.bundleIdentifier == launcherAppID
10 | }
11 |
12 | SMLoginItemSetEnabled(launcherAppID as CFString, enabled)
13 |
14 | if isRunning {
15 | DistributedNotificationCenter.default()
16 | .post(name: .killLauncher, object: Bundle.main.bundleIdentifier!)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Commander/Manager/AppRatingManager.swift:
--------------------------------------------------------------------------------
1 | import StoreKit
2 |
3 | final class AppRatingManager {
4 | // MARK: Internal
5 |
6 | func openRatePage() {
7 | let appId = 1_636_862_100
8 | if let url = URL(string: "https://apps.apple.com/app/id\(appId)?action=write-review") {
9 | NSWorkspace.shared.open(url)
10 | }
11 | }
12 |
13 | func requestRateIfNeeded() {
14 | requestCount += 1
15 |
16 | guard
17 | requestCount > 2,
18 | requestCount % 3 == 0
19 | else {
20 | return
21 | }
22 | SKStoreReviewController.requestReview()
23 | }
24 |
25 | // MARK: Private
26 |
27 | @UserDefault("rate_request_count") private var requestCount = 0
28 | }
29 |
--------------------------------------------------------------------------------
/Commander/Common/View/VisualEffectView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct VisualEffectView: NSViewRepresentable {
4 | let material: NSVisualEffectView.Material
5 | let blendingMode: NSVisualEffectView.BlendingMode
6 |
7 | func makeNSView(context _: Context) -> NSVisualEffectView {
8 | let visualEffectView = NSVisualEffectView()
9 | visualEffectView.material = material
10 | visualEffectView.blendingMode = blendingMode
11 | visualEffectView.state = NSVisualEffectView.State.active
12 | return visualEffectView
13 | }
14 |
15 | func updateNSView(_ visualEffectView: NSVisualEffectView, context _: Context) {
16 | visualEffectView.material = material
17 | visualEffectView.blendingMode = blendingMode
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Commander/Common/Extension/NSWindow+Extension.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | extension NSWindow {
4 | func makeCompletelyVisibleIfNeeded() {
5 | guard let visibleFrame = screen?.visibleFrame else {
6 | return
7 | }
8 | var frame = frame
9 | if frame.minX < visibleFrame.minX {
10 | frame.origin.x = visibleFrame.minX
11 | }
12 | if frame.maxX > visibleFrame.maxX {
13 | frame.origin.x = visibleFrame.maxX - frame.width
14 | }
15 | if frame.minY < visibleFrame.minY {
16 | frame.origin.y = visibleFrame.minY
17 | }
18 | if frame.maxY > visibleFrame.maxY {
19 | frame.origin.y = visibleFrame.maxY - frame.height
20 | }
21 | setFrame(frame, display: true)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Commander/Manager/DockIconManager.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import SwiftUI
3 |
4 | final class DockIconManager {
5 | // MARK: Internal
6 |
7 | var settingsWindow: NSWindow? {
8 | didSet {
9 | updateIconVisibility()
10 | }
11 | }
12 |
13 | func updateIconVisibility() {
14 | if showDockIcon {
15 | NSApp.setActivationPolicy(.regular)
16 | } else {
17 | settingsWindow?.canHide = false
18 | NSApp.setActivationPolicy(.accessory)
19 | settingsWindow?.canHide = true
20 | NSApp.activate(ignoringOtherApps: true)
21 | settingsWindow?.makeKeyAndOrderFront(nil)
22 | }
23 | }
24 |
25 | // MARK: Private
26 |
27 | @AppStorage(AppStorageKey.showDockIcon) private var showDockIcon = true
28 | }
29 |
--------------------------------------------------------------------------------
/Commander/Manager/ShortcutNotifier.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import SwiftUI
3 |
4 | final class ShortcutNotifier: ObservableObject {
5 | // MARK: Lifecycle
6 |
7 | init() {
8 | NSEvent.addGlobalMonitorForEvents(matching: [.flagsChanged]) { [weak self] event in
9 | self?.didReceive(event: event)
10 | }
11 | NSEvent.addLocalMonitorForEvents(matching: [.flagsChanged]) { [weak self] event in
12 | self?.didReceive(event: event)
13 | return event
14 | }
15 | }
16 |
17 | // MARK: Internal
18 |
19 | @Published var shortcutTriggered = false
20 | @UserDefault("shortcut modifiers") var modifiers = NSEvent.ModifierFlags.command
21 |
22 | // MARK: Private
23 |
24 | private func didReceive(event: NSEvent) {
25 | let newPressedModifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
26 | if modifiers == newPressedModifiers {
27 | shortcutTriggered = true
28 | } else {
29 | shortcutTriggered = false
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2023 Vadim Ahmerov
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
13 | all 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
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Commander/Manager/DIContainer.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | // MARK: - DIContainer
5 |
6 | final class DIContainer {
7 | // MARK: Lifecycle
8 |
9 | init(statusItem: NSStatusItem) {
10 | appsManager = AppsManager(bookmarksManager: BookmarksManager())
11 | shortcutNotifier = ShortcutNotifier()
12 | imageProvider = ImageProvider()
13 | appRatingManager = AppRatingManager()
14 | menuBarItemManager = MenuBarItemManager(statusItem: statusItem)
15 | autoLaunchManager = AutoLaunchManager()
16 | dockIconManager = DockIconManager()
17 | }
18 |
19 | // MARK: Internal
20 |
21 | static var shared: DIContainer {
22 | guard let appDelegate = NSApp.delegate as? AppDelegate else {
23 | fatalError()
24 | }
25 | return appDelegate.diContainer
26 | }
27 |
28 | let appsManager: AppsManager
29 | let shortcutNotifier: ShortcutNotifier
30 | let imageProvider: ImageProvider
31 | let appRatingManager: AppRatingManager
32 | let menuBarItemManager: MenuBarItemManager
33 | let dockIconManager: DockIconManager
34 | let autoLaunchManager: AutoLaunchManager
35 | }
36 |
--------------------------------------------------------------------------------
/Launcher/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Launcher/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | final class AppDelegate: NSObject, NSApplicationDelegate {
4 | // MARK: Internal
5 |
6 | func applicationDidFinishLaunching(_: Notification) {
7 | let runningApps = NSWorkspace.shared.runningApplications
8 | let isRunning = runningApps.contains {
9 | $0.bundleIdentifier == Self.mainAppIdentifier
10 | }
11 |
12 | if isRunning {
13 | terminateSelf()
14 | } else {
15 | launchMainApp()
16 | }
17 | }
18 |
19 | @objc
20 | func terminateSelf() {
21 | NSApp.terminate(nil)
22 | }
23 |
24 | // MARK: Private
25 |
26 | private static let mainAppIdentifier = "com.va.commander"
27 |
28 | private func launchMainApp() {
29 | DistributedNotificationCenter.default().addObserver(
30 | self,
31 | selector: #selector(terminateSelf),
32 | name: .killLauncher,
33 | object: Self.mainAppIdentifier
34 | )
35 | NSWorkspace.shared.openApplication(
36 | at: mainAppURL(),
37 | configuration: NSWorkspace.OpenConfiguration()
38 | )
39 | }
40 |
41 | private func mainAppURL() -> URL {
42 | var path = Bundle.main.bundlePath as NSString
43 | for _ in 1 ... 4 {
44 | path = path.deletingLastPathComponent as NSString
45 | }
46 | return URL(fileURLWithPath: path as String)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Commander/View/WheelPicker/WheelPickerHoverState.swift:
--------------------------------------------------------------------------------
1 | extension WheelPicker {
2 | enum HoverState: Equatable {
3 | case disabled
4 | case enabled([Int: Bool])
5 |
6 | // MARK: Internal
7 |
8 | static var enabledEmpty = HoverState.enabled([:])
9 |
10 | var isEnabled: Bool {
11 | switch self {
12 | case .enabled:
13 | return true
14 | case .disabled:
15 | return false
16 | }
17 | }
18 |
19 | var hoveringAppIndex: Int? {
20 | switch self {
21 | case .enabled(let hoverDictionary):
22 | return hoverDictionary.first(where: { $0.value })?.key
23 | case .disabled:
24 | return nil
25 | }
26 | }
27 |
28 | func isHovering(appIndex: Int) -> Bool {
29 | switch self {
30 | case .enabled(let hoverDictionary):
31 | return hoverDictionary[appIndex] ?? false
32 | case .disabled:
33 | return false
34 | }
35 | }
36 |
37 | mutating func set(isHovering: Bool, at appIndex: Int) {
38 | switch self {
39 | case .enabled(var hoverDictionary):
40 | hoverDictionary[appIndex] = isHovering
41 | self = .enabled(hoverDictionary)
42 | case .disabled:
43 | break
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Commander/Models/App.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - App
4 |
5 | struct App: Codable, Equatable, Hashable {
6 | // MARK: Lifecycle
7 |
8 | init(url: URL, name: String) {
9 | self.url = url
10 | self.name = name
11 | }
12 |
13 | init(url: URL) {
14 | self.url = url
15 | switch url.kind {
16 | case .app:
17 | name = url.deletingPathExtension().lastPathComponent
18 | case .file, .shortcut:
19 | name = url.lastPathComponent
20 | case .link:
21 | name = url.host ?? url.absoluteString
22 | }
23 | }
24 |
25 | // MARK: Internal
26 |
27 | enum Kind: Decodable {
28 | case shortcut
29 | case app
30 | case file
31 | case link
32 | }
33 |
34 | let url: URL
35 | let name: String
36 |
37 | var kind: Kind {
38 | url.kind
39 | }
40 | }
41 |
42 | // MARK: Identifiable
43 |
44 | extension App: Identifiable {
45 | var id: URL {
46 | url
47 | }
48 | }
49 |
50 | extension URL {
51 | fileprivate var kind: App.Kind {
52 | switch scheme {
53 | case "shortcuts":
54 | return .shortcut
55 | case "http", "https":
56 | return .link
57 | default:
58 | switch lastPathComponent {
59 | case "app":
60 | return .app
61 | default:
62 | return .file
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Commander/View/Preferences/SettingsView.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import SwiftUI
3 |
4 | // MARK: - Settings
5 |
6 | struct SettingsView: View {
7 | // MARK: Internal
8 |
9 | var body: some View {
10 | HStack(spacing: 0) {
11 | SettingsSidebarView(apps: $apps)
12 | Color(nsColor: .separatorColor).frame(width: 1).ignoresSafeArea()
13 | wheelPicker
14 | }.onReceive(diContainer.appsManager.apps) { apps in
15 | self.apps = apps
16 | }.frame(minWidth: Self.width, maxWidth: Self.width).onAppear {
17 | diContainer.appRatingManager.requestRateIfNeeded()
18 | }
19 | }
20 |
21 | // MARK: Private
22 |
23 | private static let width: CGFloat = 900
24 | private static let wheelWidth: CGFloat = 600
25 | private static let height: CGFloat = 500
26 |
27 | private let diContainer = DIContainer.shared
28 |
29 | @State private var hoverState = WheelPicker.HoverState.enabledEmpty
30 | @State private var apps = [App]()
31 |
32 | private var wheelPicker: some View {
33 | VStack(spacing: 20) {
34 | SettingsTutorialView()
35 | .padding([.leading, .trailing], 32)
36 | WheelPicker(
37 | apps: diContainer.appsManager.apps,
38 | hoverState: $hoverState
39 | )
40 | ShortcutKeysView()
41 | Spacer()
42 | }.frame(minWidth: Self.wheelWidth, maxWidth: Self.wheelWidth)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Commander/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" : "1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Commander: App Launcher
4 | [Download from App Store](https://apps.apple.com/app/commander-app-launcher/id1636862100)
5 |
6 | Commander is a lightweight macOS app launcher that simplifies your daily tasks by providing speedy access to all your essential apps, files, and folders.
7 |
8 | ## System Requirements
9 | Commander App supports macOS 12.0+.
10 |
11 | ## Installation
12 | You can download Commander from the [App Store](https://apps.apple.com/app/commander-app-launcher/id1636862100), or you can choose to build it manually using the source code.
13 |
14 | ## Usage
15 | 1. Open the Commander settings by clicking it's icon in the top right system menu.
16 | 2. Customize the list of apps, files, and folders displayed in the launcher by ticking the corresponding checkboxes on the left sidebar.
17 | 3. To launch an app, hold down the shortcut keys (by default command ⌘ + option ⌥) and move your mouse slightly.
18 |
19 |
20 |
21 | ## Contributing
22 | Contributions are welcome! If you encounter a bug or have a feature request, please open an issue or submit a pull request.
23 |
24 | ## License
25 | Commander is released under the MIT License. See [LICENSE](./LICENSE) for details.
26 |
--------------------------------------------------------------------------------
/Commander/View/Preferences/URLInputView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension SettingsSidebarView {
4 | struct URLInputView: View {
5 | // MARK: Internal
6 |
7 | @Binding var isVisible: Bool
8 | @Binding var enteredURL: URL?
9 |
10 | var body: some View {
11 | VStack(alignment: .leading, spacing: 8) {
12 | HStack(alignment: .firstTextBaseline) {
13 | Image(systemName: "link")
14 | .foregroundColor(.blue)
15 | .font(.title3)
16 | Text("Add URL for quick access")
17 | .font(.title3.weight(.medium))
18 | }
19 | Text("Once you've added the URL, you can launch it like any other app using the Commander launcher.")
20 | .padding(.bottom, 8)
21 | TextField("Paste your URL here", text: $enteredText)
22 | .textFieldStyle(.roundedBorder)
23 | .padding(.bottom, 8)
24 | HStack {
25 | Spacer()
26 | Button("Cancel") {
27 | isVisible = false
28 | }.keyboardShortcut(.cancelAction)
29 | Button("Create") {
30 | isVisible = false
31 | enteredURL = URL(string: enteredText)
32 | }.disabled(urlIsInvalid).keyboardShortcut(.defaultAction)
33 | }
34 | }.onChange(of: enteredText) { newValue in
35 | urlIsInvalid = URL(string: newValue) == nil
36 | }.padding().frame(width: 400)
37 | }
38 |
39 | // MARK: Private
40 |
41 | @State private var urlIsInvalid = true
42 | @State private var enteredText = ""
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Commander/Manager/ShortcutsAppManager.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Foundation
3 |
4 | final class ShortcutsAppManager {
5 | // MARK: Internal
6 |
7 | enum ShortcutError: Error {
8 | case failedToDecodeOutput
9 | }
10 |
11 | func getShortcuts() -> [App] {
12 | do {
13 | return try run(arguments: ["list"])
14 | .get()
15 | .components(separatedBy: .newlines)
16 | .compactMap { name in
17 | guard
18 | !name.isEmpty,
19 | var components = URLComponents(string: "shortcuts://run-shortcut")
20 | else {
21 | return nil
22 | }
23 | components.queryItems = [URLQueryItem(name: "name", value: name)]
24 | guard let url = components.url else {
25 | return nil
26 | }
27 | return App(url: url, name: name)
28 | }
29 | } catch {
30 | return []
31 | }
32 | }
33 |
34 | func run(app: App) {
35 | run(arguments: ["run", app.name])
36 | }
37 |
38 | // MARK: Private
39 |
40 | @discardableResult
41 | private func run(arguments: [String]) -> Result {
42 | let process = Process()
43 | process.executableURL = URL(fileURLWithPath: "/usr/bin/shortcuts")
44 | process.arguments = arguments
45 |
46 | let outputPipe = Pipe()
47 | process.standardOutput = outputPipe
48 |
49 | do {
50 | try process.run()
51 | process.waitUntilExit()
52 | guard
53 | let data = try outputPipe.fileHandleForReading.readToEnd(),
54 | let string = String(data: data, encoding: .utf8)
55 | else {
56 | return .failure(ShortcutError.failedToDecodeOutput)
57 | }
58 | return .success(string)
59 | } catch {
60 | return .failure(error)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Commander/View/Preferences/SettingsTutorialView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - SettingsTutorialView
4 |
5 | struct SettingsTutorialView: View {
6 | var body: some View {
7 | VStack(alignment: .leading, spacing: 12) {
8 | Text("How to use the app:")
9 | .font(.title2.bold())
10 |
11 | TutorialStepView(
12 | stepNumber: "1",
13 | title: "Select apps to launch",
14 | description: "Choose the apps you want to launch by either selecting them in the left sidebar or dragging and dropping them onto the circle below. You can also add URLs, files, folders or other custom apps by clicking buttons on the top left corner of this window."
15 | )
16 |
17 | TutorialStepView(
18 | stepNumber: "2",
19 | title: "Rearrange apps",
20 | description: "If you prefer a different arrangement of your apps, just drag and drop them within the circle below."
21 | )
22 |
23 | TutorialStepView(
24 | stepNumber: "3",
25 | title: "Launch apps",
26 | description: "Access the app launcher at any time by using the specified shortcut and slightly moving your mouse. When the app picker appears, hover over the desired app and release the shortcut to launch it."
27 | )
28 | }
29 | }
30 | }
31 |
32 | // MARK: - TutorialStepView
33 |
34 | private struct TutorialStepView: View {
35 | let stepNumber: String
36 | let title: String
37 | let description: String
38 |
39 | var body: some View {
40 | HStack(alignment: .top, spacing: 12) {
41 | RoundedRectangle(cornerRadius: 6)
42 | .fill(Color(nsColor: NSColor.quaternaryLabelColor))
43 | .frame(width: 22, height: 22)
44 | .overlay(Text(stepNumber).font(.body).fontWeight(.bold))
45 | .padding(.top, 2)
46 | VStack(alignment: .leading, spacing: 4) {
47 | Text(title)
48 | .font(.title3.bold())
49 | Text(description)
50 | .fixedSize(horizontal: false, vertical: true)
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Commander/Manager/ImageProvider.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Foundation
3 | import SwiftUI
4 |
5 | final class ImageProvider {
6 | // MARK: Internal
7 |
8 | @ViewBuilder
9 | func image(for app: App, preferredSize: CGFloat) -> some View {
10 | switch app.kind {
11 | case .shortcut:
12 | ZStack {
13 | Image("shortcut")
14 | .resizable()
15 | .frame(width: preferredSize, height: preferredSize)
16 | if let firstLetter = app.name.first {
17 | Text(String(firstLetter))
18 | .font(.system(size: preferredSize * 0.8, weight: .medium, design: .rounded))
19 | .foregroundColor(.white)
20 | .opacity(0.8)
21 | }
22 | }
23 | case .app, .file:
24 | Image(nsImage: nsImage(for: app.url, preferredSize: preferredSize))
25 | .resizable()
26 | .frame(width: preferredSize, height: preferredSize)
27 | case .link:
28 | Image(systemName: "link")
29 | .renderingMode(.template)
30 | .resizable()
31 | .foregroundColor(.secondary)
32 | .frame(width: preferredSize * 0.8, height: preferredSize * 0.8)
33 | .frame(width: preferredSize, height: preferredSize)
34 | }
35 | }
36 |
37 | // MARK: Private
38 |
39 | private func nsImage(for url: URL, preferredSize: CGFloat) -> NSImage {
40 | let nsImage: NSImage
41 | let imageRepresentation = NSWorkspace.shared
42 | .icon(forFile: url.path)
43 | .bestRepresentation(
44 | for: NSRect(
45 | x: 0,
46 | y: 0,
47 | width: preferredSize,
48 | height: preferredSize
49 | ),
50 | context: nil,
51 | hints: nil
52 | )
53 | if let imageRepresentation = imageRepresentation {
54 | nsImage = NSImage(size: imageRepresentation.size)
55 | nsImage.addRepresentation(imageRepresentation)
56 | } else {
57 | nsImage = NSWorkspace.shared.icon(forFile: url.path)
58 | }
59 | return nsImage
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Commander/View/Commander/CommanderView.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Combine
3 | import SwiftUI
4 |
5 | // MARK: - CommanderView
6 |
7 | struct CommanderView: View {
8 | // MARK: Internal
9 |
10 | var body: some View {
11 | WheelPicker(
12 | apps: diContainer.appsManager.apps,
13 | hoverState: $hoverState
14 | ).onReceive(clearHoverSubject) { _ in
15 | hoverState = .enabledEmpty
16 | }.onReceive(diContainer.shortcutNotifier.$shortcutTriggered) { isTriggered in
17 | if isTriggered {
18 | shortcutTriggerDate = Date()
19 | } else {
20 | let lastHoveredAppIndex = hoverState.hoveringAppIndex
21 | let lastHoveredApp = lastHoveredAppIndex.map { diContainer.appsManager.apps.value[$0] }
22 |
23 | clearHoverSubject.send(())
24 | if
25 | let app = lastHoveredApp,
26 | let keyDownDate = shortcutTriggerDate,
27 | -keyDownDate.timeIntervalSinceNow > 0.2
28 | {
29 | diContainer.appsManager.open(app: app)
30 | shortcutTriggerDate = nil
31 | produceLaunchHapticFeedbackIfNeeded()
32 | }
33 | }
34 | }.opacity(showOnMouseMove && isHidden ? 0 : 1).overlay {
35 | if showOnMouseMove {
36 | EmptyView().trackingMouse(offset: 32) {
37 | isHidden = false
38 | }.onReceive(diContainer.shortcutNotifier.$shortcutTriggered) { _ in
39 | isHidden = true
40 | }
41 | }
42 | }
43 | }
44 |
45 | // MARK: Private
46 |
47 | @State private var shortcutTriggerDate: Date?
48 | @AppStorage(AppStorageKey.showOnMouseMove) private var showOnMouseMove = true
49 | @AppStorage(AppStorageKey.hapticFeedbackEnabled) private var hapticFeedbackEnabled = true
50 | private let diContainer = DIContainer.shared
51 | @State private var hoverState = WheelPicker.HoverState.enabledEmpty
52 | @State private var isHidden = true
53 | private let clearHoverSubject = PassthroughSubject()
54 |
55 | private func produceLaunchHapticFeedbackIfNeeded() {
56 | if hapticFeedbackEnabled {
57 | NSHapticFeedbackManager.defaultPerformer.perform(.levelChange, performanceTime: .default)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/commander.swiftformat:
--------------------------------------------------------------------------------
1 | # options
2 | --swiftversion 5.6
3 | --self remove # redundantSelf
4 | --importgrouping testable-bottom # sortedImports
5 | --commas always # trailingCommas
6 | --trimwhitespace always # trailingSpace
7 | --indent 4 #indent
8 | --ifdef no-indent #indent
9 | --indentstrings true #indent
10 | --wraparguments before-first # wrapArguments
11 | --wrapparameters before-first # wrapArguments
12 | --wrapcollections before-first # wrapArguments
13 | --wrapconditions before-first # wrapArguments
14 | --wrapreturntype preserve #wrapArguments
15 | --wraptypealiases before-first # wrapArguments
16 | --funcattributes prev-line # wrapAttributes
17 | --typeattributes prev-line # wrapAttributes
18 | --wrapternary before-operators # wrap
19 | --structthreshold 20 # organizeDeclarations
20 | --enumthreshold 20 # organizeDeclarations
21 | --organizetypes class,struct,enum,extension,actor # organizeDeclarations
22 | --extensionacl on-declarations # extensionAccessControl
23 | --patternlet inline # hoistPatternLet
24 | --redundanttype inferred # redundantType
25 | --maxwidth 130 # wrap
26 | --header ""
27 | --varattributes same-line
28 | --funcattributes prev-line
29 |
30 | # rules
31 | --rules fileHeader
32 | --rules anyObjectProtocol
33 | --rules blankLinesBetweenScopes
34 | --rules consecutiveSpaces
35 | --rules duplicateImports
36 | --rules extensionAccessControl
37 | --rules hoistPatternLet
38 | --rules indent
39 | --rules markTypes
40 | --rules organizeDeclarations
41 | --rules redundantParens
42 | --rules redundantReturn
43 | --rules redundantSelf
44 | --rules redundantType
45 | --rules redundantPattern
46 | --rules redundantGet
47 | --rules redundantFileprivate
48 | --rules redundantRawValues
49 | --rules sortedImports
50 | --rules sortDeclarations
51 | --rules strongifiedSelf
52 | --rules trailingCommas
53 | --rules trailingSpace
54 | --rules typeSugar
55 | --rules wrap
56 | --rules wrapMultilineStatementBraces
57 | --rules wrapArguments
58 | --rules wrapAttributes
59 | --rules braces
60 | --rules redundantClosure
61 | --rules redundantInit
62 | --rules redundantVoidReturnType
63 | --rules unusedArguments
64 | --rules spaceInsideBrackets
65 | --rules spaceInsideBraces
66 | --rules spaceAroundBraces
67 | --rules spaceInsideParens
68 | --rules spaceAroundParens
69 | --rules enumNamespaces
70 | --rules blockComments
71 | --rules spaceAroundComments
72 | --rules spaceInsideComments
73 | --rules blankLinesAtStartOfScope
74 | --rules blankLinesAtEndOfScope
75 | --rules emptyBraces
76 | --rules andOperator
--------------------------------------------------------------------------------
/Commander/Manager/BookmarksManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | final class BookmarksManager {
4 | // MARK: Lifecycle
5 |
6 | init() {
7 | try? restoreAllBookmarks()
8 | }
9 |
10 | // MARK: Internal
11 |
12 | /// Call this method to get persistent access for a given URL.
13 | func addToBookmarks(url: URL) throws {
14 | var bookmarks = try loadBookmarks()
15 | bookmarks[url] = try url.bookmarkData(
16 | options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess],
17 | includingResourceValuesForKeys: nil,
18 | relativeTo: nil
19 | )
20 | try save(bookmarks: bookmarks)
21 | }
22 |
23 | /// Call this method on every app launch to reacquire access for all URLs.
24 | func restoreAllBookmarks() throws {
25 | var error: Error?
26 |
27 | for (url, bookmarkData) in try loadBookmarks() {
28 | var isStale = false
29 | let restoredUrl = try URL(
30 | resolvingBookmarkData: bookmarkData,
31 | options: .withSecurityScope,
32 | relativeTo: nil,
33 | bookmarkDataIsStale: &isStale
34 | )
35 |
36 | let accessSucceeded = restoredUrl.startAccessingSecurityScopedResource()
37 | if !accessSucceeded {
38 | error = error ?? .accessFailed(url)
39 | }
40 | if isStale {
41 | error = error ?? .bookmarkDataIsStale(url)
42 | }
43 | }
44 |
45 | if let error = error {
46 | throw error
47 | }
48 | }
49 |
50 | // MARK: Private
51 |
52 | private enum Error: Swift.Error {
53 | case noDocumentsURL
54 | case bookmarkDataIsStale(URL)
55 | case accessFailed(URL)
56 | }
57 |
58 | private func save(bookmarks: [URL: Data]) throws {
59 | let bookmarksURL = try getBookmarksURL()
60 | let data = try JSONEncoder().encode(bookmarks)
61 | try data.write(to: bookmarksURL)
62 | }
63 |
64 | private func loadBookmarks() throws -> [URL: Data] {
65 | let bookmarksURL = try getBookmarksURL()
66 | let bookmarksDictionary = try? JSONDecoder().decode(
67 | [URL: Data].self,
68 | from: try Data(contentsOf: bookmarksURL)
69 | )
70 | return bookmarksDictionary ?? [:]
71 | }
72 |
73 | private func getBookmarksURL() throws -> URL {
74 | guard var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
75 | throw Error.noDocumentsURL
76 | }
77 | url = url.appendingPathComponent("bookmarks.json")
78 | return url
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Commander/AppMenu.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | // MARK: - AppMenuEditActions
4 |
5 | @objc
6 | protocol AppMenuEditActions {
7 | func redo(_ sender: AnyObject)
8 | func undo(_ sender: AnyObject)
9 | }
10 |
11 | // MARK: - AppMenu
12 |
13 | final class AppMenu: NSMenu {
14 | // MARK: Lifecycle
15 |
16 | override init(title: String) {
17 | super.init(title: title)
18 | let menu = NSMenuItem()
19 | menu.submenu = NSMenu(title: "Main")
20 | menu.submenu?.items = allItems()
21 | items = [menu, windowItem()]
22 | }
23 |
24 | @available(*, unavailable)
25 | required init(coder _: NSCoder) {
26 | fatalError()
27 | }
28 |
29 | // MARK: Internal
30 |
31 | func allItems() -> [NSMenuItem] {
32 | [
33 | NSMenuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a"),
34 | NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c"),
35 | NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x"),
36 | NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"),
37 |
38 | NSMenuItem(title: "Undo", action: #selector(AppMenuEditActions.undo(_:)), keyEquivalent: "z"),
39 | NSMenuItem(title: "Redo", action: #selector(AppMenuEditActions.redo(_:)), keyEquivalent: "Z"),
40 |
41 | NSMenuItem(title: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w"),
42 | NSMenuItem(
43 | title: "Quit \(ProcessInfo.processInfo.processName)",
44 | action: #selector(NSApplication.shared.terminate(_:)),
45 | keyEquivalent: "q"
46 | ),
47 | ]
48 | }
49 |
50 | func windowItem() -> NSMenuItem {
51 | let windowMenu = NSMenuItem()
52 | windowMenu.submenu = NSMenu(title: "Window")
53 | windowMenu.submenu?.addItem(AlwaysEnabledMenuItem(
54 | title: "Show",
55 | action: #selector(AppDelegate.openSettings),
56 | keyEquivalent: ""
57 | ))
58 | windowMenu.submenu?.addItem(NSMenuItem(
59 | title: "Minimize",
60 | action: #selector(NSWindow.miniaturize(_:)),
61 | keyEquivalent: "m"
62 | ))
63 | windowMenu.submenu?.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""))
64 | return windowMenu
65 | }
66 | }
67 |
68 | // MARK: - AlwaysEnabledMenuItem
69 |
70 | private final class AlwaysEnabledMenuItem: NSMenuItem {
71 | override var isEnabled: Bool {
72 | get {
73 | true
74 | }
75 | set {}
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Commander/Manager/AppsManager.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Combine
3 |
4 | // MARK: - AppsManagerError
5 |
6 | enum AppsManagerError: Error {
7 | case maxCountReached
8 | case alreadyAdded
9 | }
10 |
11 | // MARK: - AppsManager
12 |
13 | final class AppsManager {
14 | // MARK: Lifecycle
15 |
16 | init(bookmarksManager: BookmarksManager) {
17 | self.bookmarksManager = bookmarksManager
18 |
19 | try? addStoredAppsIfNeeded()
20 | apps.send(storedApps)
21 | apps.dropFirst().sink { [weak self] apps in
22 | self?.storedApps = apps
23 | }.store(in: &cancellables)
24 | appGroups.send(searcher.search())
25 | }
26 |
27 | // MARK: Internal
28 |
29 | static let maxAppsCount = 12
30 |
31 | let apps = CurrentValueSubject<[App], Never>([])
32 | let appGroups = CurrentValueSubject<[AppGroup], Never>([])
33 |
34 | func updateApps() {
35 | appGroups.send(searcher.search())
36 | }
37 |
38 | func open(app: App) {
39 | DispatchQueue.global(qos: .userInitiated).async { [self] in
40 | switch app.kind {
41 | case .shortcut:
42 | searcher.shortcutsAppManager.run(app: app)
43 | case .file, .app, .link:
44 | NSWorkspace.shared.open(app.url)
45 | }
46 | }
47 | }
48 |
49 | func isLaunched(app: App) -> Bool {
50 | NSWorkspace.shared.runningApplications.contains {
51 | $0.bundleURL == app.url
52 | }
53 | }
54 |
55 | func recentApps() -> [App] {
56 | searcher.getRecentApps()
57 | }
58 |
59 | func add(appURL: URL) throws {
60 | guard apps.value.count < Self.maxAppsCount else {
61 | throw AppsManagerError.maxCountReached
62 | }
63 | if apps.value.map(\.url).contains(appURL) {
64 | throw AppsManagerError.alreadyAdded
65 | }
66 | let app = App(url: appURL)
67 | switch app.kind {
68 | case .file:
69 | try bookmarksManager.addToBookmarks(url: app.url)
70 | default:
71 | break // noop
72 | }
73 | apps.value.append(app)
74 | }
75 |
76 | // MARK: Private
77 |
78 | @UserDefault("apps.json") private var storedApps: [App] = []
79 | private let searcher = AppSearcher()
80 | private let bookmarksManager: BookmarksManager
81 | private var cancellables = Set()
82 |
83 | private func addStoredAppsIfNeeded() throws {
84 | guard storedApps.isEmpty else {
85 | return
86 | }
87 | let allApps = searcher.search()
88 | // let recommendedApps = allApps.localApps.shuffled().prefix(6)
89 | // storedApps = Array(recommendedApps)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Commander/View/Feedback/TextArea.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct TextArea: NSViewRepresentable {
4 | class Coordinator: NSObject, NSTextViewDelegate {
5 | // MARK: Lifecycle
6 |
7 | init(text: Binding) {
8 | self.text = text
9 | }
10 |
11 | // MARK: Internal
12 |
13 | var text: Binding
14 |
15 | func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString text: String?) -> Bool {
16 | defer {
17 | self.text.wrappedValue = (textView.string as NSString).replacingCharacters(in: range, with: text!)
18 | }
19 | return true
20 | }
21 |
22 | func createTextViewStack(maximumNumberOfLines: Int) -> NSScrollView {
23 | let contentSize = scrollview.contentSize
24 |
25 | textContainer.containerSize = CGSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
26 | textContainer.widthTracksTextView = true
27 | textContainer.maximumNumberOfLines = maximumNumberOfLines
28 |
29 | textView.textContainerInset = NSSize(width: 2, height: 8)
30 | textView.minSize = CGSize(width: 0, height: 0)
31 | textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
32 | textView.isVerticallyResizable = true
33 | textView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
34 | textView.autoresizingMask = [.width]
35 | textView.delegate = self
36 | textView.font = NSFont.preferredFont(forTextStyle: .body)
37 |
38 | scrollview.borderType = .noBorder
39 | scrollview.hasVerticalScroller = true
40 | scrollview.documentView = textView
41 |
42 | textStorage.addLayoutManager(layoutManager)
43 | layoutManager.addTextContainer(textContainer)
44 |
45 | return scrollview
46 | }
47 |
48 | // MARK: Fileprivate
49 |
50 | fileprivate lazy var textStorage = NSTextStorage()
51 | fileprivate lazy var layoutManager = NSLayoutManager()
52 | fileprivate lazy var textContainer = NSTextContainer()
53 | fileprivate lazy var textView = NSTextView(frame: CGRect(), textContainer: textContainer)
54 | fileprivate lazy var scrollview = NSScrollView()
55 | }
56 |
57 | @Binding var text: String
58 | let maximumNumberOfLines: Int
59 |
60 | func makeNSView(context: Context) -> NSScrollView {
61 | context.coordinator.createTextViewStack(maximumNumberOfLines: maximumNumberOfLines)
62 | }
63 |
64 | func updateNSView(_ nsView: NSScrollView, context _: Context) {
65 | if let textArea = nsView.documentView as? NSTextView, textArea.string != self.text {
66 | textArea.string = text
67 | }
68 | }
69 |
70 | func makeCoordinator() -> Coordinator {
71 | Coordinator(text: $text)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Commander.xcodeproj/xcshareddata/xcschemes/Commander.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Commander/View/Preferences/SettingsSearchField.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - SettingsSearchField
4 |
5 | struct SettingsSearchField: View {
6 | @FocusState private var isFocused: Bool
7 | @Binding var searchText: String
8 |
9 | var body: some View {
10 | GroupBox {
11 | HStack(spacing: 2) {
12 | contentView
13 | }.animation(.default, value: isFocused)
14 | }.onTapGesture {
15 | isFocused = true
16 | }
17 | }
18 |
19 | @ViewBuilder private var contentView: some View {
20 | if !isFocused {
21 | searchIconView
22 | }
23 | TextField(text: $searchText)
24 | .focused($isFocused)
25 | if !isFocused {
26 | Spacer()
27 | }
28 | if !searchText.isEmpty {
29 | clearButton
30 | }
31 | }
32 |
33 | private var searchIconView: some View {
34 | Image(systemName: "magnifyingglass")
35 | .foregroundColor(.gray)
36 | .opacity(isFocused ? 0 : 1)
37 | .padding(.leading, 6)
38 | }
39 |
40 | private var clearButton: some View {
41 | Button {
42 | searchText = ""
43 | } label: {
44 | Image(systemName: "xmark.circle.fill")
45 | .padding(.horizontal, 4)
46 | .opacity(0.7)
47 | }.buttonStyle(.plain)
48 | }
49 | }
50 |
51 | // MARK: SettingsSearchField.TextField
52 |
53 | extension SettingsSearchField {
54 | struct TextField: NSViewRepresentable {
55 | final class Coordinator: NSObject, NSTextFieldDelegate {
56 | // MARK: Lifecycle
57 |
58 | override init() {
59 | textField = NSTextField()
60 | super.init()
61 | configureTextField()
62 | }
63 |
64 | // MARK: Internal
65 |
66 | let textField: NSTextField
67 | var onTextChange: ((String) -> Void)?
68 |
69 | func controlTextDidChange(_: Notification) {
70 | onTextChange?(textField.stringValue)
71 | }
72 |
73 | // MARK: Private
74 |
75 | private func configureTextField() {
76 | let font = NSFont.preferredFont(forTextStyle: .body)
77 | textField.font = font
78 | textField.placeholderAttributedString = NSAttributedString(
79 | string: "Search",
80 | attributes: [.foregroundColor: NSColor.gray, .font: font]
81 | )
82 | textField.focusRingType = .none
83 | textField.isBezeled = false
84 | textField.backgroundColor = .clear
85 | textField.delegate = self
86 | }
87 | }
88 |
89 | @Binding var text: String
90 |
91 | func makeNSView(context: Context) -> some NSView {
92 | context.coordinator.textField
93 | }
94 |
95 | func makeCoordinator() -> Coordinator {
96 | let coordinator = Coordinator()
97 | coordinator.onTextChange = {
98 | text = $0
99 | }
100 | return coordinator
101 | }
102 |
103 | func updateNSView(_ nsView: NSViewType, context _: Context) {
104 | guard let textField = nsView as? NSTextField else {
105 | return
106 | }
107 | textField.stringValue = text
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Commander/View/Preferences/ShortcutKeysView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension SettingsView {
4 | struct ShortcutKeysView: View {
5 | // MARK: Internal
6 |
7 | var body: some View {
8 | VStack(alignment: .leading, spacing: 10) {
9 | HStack {
10 | Toggle("⌘", isOn: $isCommandEnabled)
11 | Toggle("⌥", isOn: $isOptionEnabled)
12 | Toggle("⌃", isOn: $isControlEnabled)
13 | Toggle("⇧", isOn: $isShiftEnabled)
14 | Toggle("fn", isOn: $isFunctionEnabled)
15 | }
16 | Toggle("Show only after mouse move", isOn: $showOnMouseMove)
17 | Toggle("Show menu bar icon", isOn: $showMenuBarItem)
18 | Toggle("Show dock icon", isOn: $showDockIcon)
19 | Toggle("Launch on Login", isOn: $launchOnLogin)
20 | Toggle("Enable Haptic Feedback", isOn: $hapticFeedbackEnabled)
21 | }.onChange(of: isOptionEnabled) { _ in
22 | diContainer.shortcutNotifier.modifiers = enabledModifiers
23 | }.onChange(of: isControlEnabled) { _ in
24 | diContainer.shortcutNotifier.modifiers = enabledModifiers
25 | }.onChange(of: isShiftEnabled) { _ in
26 | diContainer.shortcutNotifier.modifiers = enabledModifiers
27 | }.onChange(of: isFunctionEnabled) { _ in
28 | diContainer.shortcutNotifier.modifiers = enabledModifiers
29 | }.onChange(of: isCommandEnabled) { _ in
30 | diContainer.shortcutNotifier.modifiers = enabledModifiers
31 | }.onChange(of: showMenuBarItem) { showMenuBarItem in
32 | diContainer.menuBarItemManager.set(isVisible: showMenuBarItem)
33 | }.onChange(of: launchOnLogin) { launchOnLogin in
34 | diContainer.autoLaunchManager.configureAutoLaunch(enabled: launchOnLogin)
35 | }.onChange(of: showDockIcon) { _ in
36 | diContainer.dockIconManager.updateIconVisibility()
37 | }.onAppear {
38 | isCommandEnabled = diContainer.shortcutNotifier.modifiers.contains(.command)
39 | isOptionEnabled = diContainer.shortcutNotifier.modifiers.contains(.option)
40 | isControlEnabled = diContainer.shortcutNotifier.modifiers.contains(.control)
41 | isShiftEnabled = diContainer.shortcutNotifier.modifiers.contains(.shift)
42 | isFunctionEnabled = diContainer.shortcutNotifier.modifiers.contains(.function)
43 | }
44 | }
45 |
46 | // MARK: Private
47 |
48 | private let diContainer = DIContainer.shared
49 | @AppStorage(AppStorageKey.showOnMouseMove) private var showOnMouseMove = true
50 | @AppStorage(AppStorageKey.showMenuBarItem) private var showMenuBarItem = false
51 | @AppStorage(AppStorageKey.showDockIcon) private var showDockIcon = true
52 | @AppStorage(AppStorageKey.launchOnLogin) private var launchOnLogin = false
53 | @AppStorage(AppStorageKey.hapticFeedbackEnabled) private var hapticFeedbackEnabled = true
54 | @State private var isCommandEnabled = false
55 | @State private var isOptionEnabled = false
56 | @State private var isControlEnabled = false
57 | @State private var isShiftEnabled = false
58 | @State private var isFunctionEnabled = false
59 |
60 | private var enabledModifiers: NSEvent.ModifierFlags {
61 | var flags = NSEvent.ModifierFlags()
62 | if isCommandEnabled {
63 | flags.insert(.command)
64 | }
65 | if isOptionEnabled {
66 | flags.insert(.option)
67 | }
68 | if isControlEnabled {
69 | flags.insert(.control)
70 | }
71 | if isShiftEnabled {
72 | flags.insert(.shift)
73 | }
74 | if isFunctionEnabled {
75 | flags.insert(.function)
76 | }
77 | return flags
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Commander/View/Feedback/SendFeedbackView.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 | import Foundation
4 |
5 | struct SendFeedbackView: View {
6 | // MARK: Internal
7 |
8 | enum ViewState {
9 | case entering
10 | case submitting
11 | case submitted
12 | }
13 |
14 | var fontLineHeight: CGFloat {
15 | NSLayoutManager().defaultLineHeight(for: NSFont.preferredFont(forTextStyle: .body))
16 | }
17 |
18 | var body: some View {
19 | Form {
20 | Section(header: Text("Message").font(.title3)) {
21 | TextArea(text: $feedbackText, maximumNumberOfLines: 0)
22 | .frame(height: 120)
23 | .cornerRadius(4)
24 | }
25 | Spacer().frame(height: 16)
26 | Section(header: Text("Your email or Contact Details (optional)").font(.title3)) {
27 | TextArea(text: $contactDetails, maximumNumberOfLines: 1)
28 | .frame(minHeight: fontLineHeight + 16)
29 | .cornerRadius(4)
30 | .fixedSize(horizontal: false, vertical: true)
31 | }
32 | Spacer().frame(height: 16)
33 |
34 | switch state {
35 | case .entering:
36 | Button {
37 | submitFeedback()
38 | } label: {
39 | Text("Submit")
40 | }.disabled(feedbackText.isEmpty)
41 | case .submitting:
42 | HStack {
43 | Spacer()
44 | ProgressView().progressViewStyle(.circular)
45 | Spacer()
46 | }
47 | case .submitted:
48 | Text("\(Image(systemName: "checkmark")) Your feedback has been received, thank you")
49 | .multilineTextAlignment(.center)
50 | .font(.headline)
51 | .foregroundColor(Color.accentColor)
52 | }
53 | }
54 | .animation(.default, value: state)
55 | .frame(minWidth: 400)
56 | .padding()
57 | }
58 |
59 | @MainActor
60 | func submitFeedback() {
61 | guard !feedbackText.isEmpty else {
62 | return
63 | }
64 |
65 | state = .submitting
66 | sendFeedback(message: feedbackText, contactDetails: contactDetails)
67 | .receive(on: DispatchQueue.main)
68 | .sink(
69 | receiveCompletion: { completion in
70 | switch completion {
71 | case .finished:
72 | state = .submitted
73 | case .failure:
74 | state = .entering
75 | }
76 | },
77 | receiveValue: { _ in }
78 | )
79 | .store(in: &cancellables)
80 | }
81 |
82 | // MARK: Private
83 |
84 | @State private var feedbackText = ""
85 | @State private var contactDetails = ""
86 | @State private var state = ViewState.entering
87 | @State private var cancellables = Set()
88 |
89 | private func sendFeedback(message: String, contactDetails: String) -> AnyPublisher {
90 | guard let url = URL(string: "https://ethereal-expanse.com/api/chromex/feedback") else {
91 | return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
92 | }
93 |
94 | var request = URLRequest(url: url)
95 | request.httpMethod = "POST"
96 | request.setValue("application/json", forHTTPHeaderField: "Content-Type")
97 |
98 | let params: [String: Any] = [
99 | "message": message,
100 | "email": contactDetails,
101 | "app": "Commander",
102 | "platform": "macOS",
103 | "version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown",
104 | "systemVersion": ProcessInfo.processInfo.operatingSystemVersionString
105 | ]
106 |
107 | guard let jsonData = try? JSONSerialization.data(withJSONObject: params) else {
108 | return Fail(error: URLError(.cannotParseResponse)).eraseToAnyPublisher()
109 | }
110 |
111 | request.httpBody = jsonData
112 |
113 | return URLSession.shared
114 | .dataTaskPublisher(for: request)
115 | .map { _ in () }
116 | .eraseToAnyPublisher()
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Commander/Common/View/TrackingAreaView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - MouseLocationUpdate
4 |
5 | enum MouseLocationUpdate {
6 | case moved(newLocation: NSPoint)
7 | case exited
8 |
9 | var point: NSPoint? {
10 | switch self {
11 | case .moved(let point):
12 | return point
13 | case .exited:
14 | return nil
15 | }
16 | }
17 | }
18 |
19 | extension View {
20 | func trackingMouse(
21 | onUpdate: @escaping (MouseLocationUpdate) -> Void,
22 | onWindowFrameChange: (() -> Void)? = nil
23 | ) -> some View {
24 | TrackinAreaView(onUpdate: onUpdate, onWindowFrameChange: onWindowFrameChange) { self }
25 | }
26 |
27 | func trackingMouse(offset: CGFloat, onReachOffset: @escaping () -> Void) -> some View {
28 | var firstLocation: CGPoint?
29 |
30 | return trackingMouse { update in
31 | guard let point = update.point else {
32 | return
33 | }
34 | if firstLocation == nil {
35 | firstLocation = point
36 | }
37 | if let firstLocation = firstLocation, firstLocation.distance(to: point) >= offset {
38 | onReachOffset()
39 | }
40 | } onWindowFrameChange: {
41 | firstLocation = nil
42 | }
43 | }
44 | }
45 |
46 | // MARK: - TrackinAreaView
47 |
48 | struct TrackinAreaView: View where Content: View {
49 | let onUpdate: (MouseLocationUpdate) -> Void
50 | let onWindowFrameChange: (() -> Void)?
51 | let content: () -> Content
52 |
53 | init(
54 | onUpdate: @escaping (MouseLocationUpdate) -> Void,
55 | onWindowFrameChange: (() -> Void)?,
56 | @ViewBuilder content: @escaping () -> Content
57 | ) {
58 | self.onUpdate = onUpdate
59 | self.onWindowFrameChange = onWindowFrameChange
60 | self.content = content
61 | }
62 |
63 | var body: some View {
64 | TrackingAreaRepresentable(onUpdate: onUpdate, onWindowFrameChange: onWindowFrameChange, content: content())
65 | }
66 | }
67 |
68 | // MARK: - TrackingAreaRepresentable
69 |
70 | struct TrackingAreaRepresentable: NSViewRepresentable where Content: View {
71 | let onUpdate: (MouseLocationUpdate) -> Void
72 | let onWindowFrameChange: (() -> Void)?
73 | let content: Content
74 |
75 | func makeNSView(context _: Context) -> NSHostingView {
76 | TrackingNSHostingView(onUpdate: onUpdate, onWindowFrameChange: onWindowFrameChange, rootView: content)
77 | }
78 |
79 | func updateNSView(_: NSHostingView, context _: Context) {}
80 | }
81 |
82 | // MARK: - TrackingNSHostingView
83 |
84 | final class TrackingNSHostingView: NSHostingView where Content: View {
85 | // MARK: Lifecycle
86 |
87 | init(
88 | onUpdate: @escaping (MouseLocationUpdate) -> Void,
89 | onWindowFrameChange: (() -> Void)?,
90 | rootView: Content
91 | ) {
92 | self.onUpdate = onUpdate
93 | self.onWindowFrameChange = onWindowFrameChange
94 | super.init(rootView: rootView)
95 | setupTrackingArea()
96 | }
97 |
98 | required init(rootView _: Content) {
99 | fatalError("Should never be called")
100 | }
101 |
102 | @objc
103 | @available(*, unavailable)
104 | dynamic required init?(coder _: NSCoder) {
105 | fatalError("Should never be called")
106 | }
107 |
108 | // MARK: Internal
109 |
110 | override func mouseMoved(with event: NSEvent) {
111 | if lastFrameWindow != window?.frame {
112 | onWindowFrameChange?()
113 | } else {
114 | let point = convert(event.locationInWindow, from: nil)
115 | onUpdate(.moved(newLocation: point))
116 | }
117 | lastFrameWindow = window?.frame
118 | }
119 |
120 | override func mouseExited(with event: NSEvent) {
121 | super.mouseExited(with: event)
122 | onUpdate(.exited)
123 | }
124 |
125 | // MARK: Private
126 |
127 | private let onUpdate: (MouseLocationUpdate) -> Void
128 | private let onWindowFrameChange: (() -> Void)?
129 | private var lastFrameWindow: NSRect?
130 |
131 | private func setupTrackingArea() {
132 | var options: NSTrackingArea.Options = [
133 | .mouseMoved,
134 | .activeAlways,
135 | .inVisibleRect,
136 | ]
137 |
138 | if #available(macOS 13, *) {
139 | options.insert(.mouseEnteredAndExited)
140 | }
141 |
142 | let trackingArea = NSTrackingArea(
143 | rect: .zero,
144 | options: options,
145 | owner: self,
146 | userInfo: nil
147 | )
148 | addTrackingArea(trackingArea)
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/Commander/Manager/AppSearcher.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import ApplicationServices
3 | import Foundation
4 |
5 | final class AppSearcher {
6 | // MARK: Internal
7 |
8 | enum SearchError: Error {
9 | case failedToInitializeFileEnumerator
10 | }
11 |
12 | let shortcutsAppManager = ShortcutsAppManager()
13 |
14 | func search() -> [AppGroup] {
15 | var groups = [AppGroup]()
16 |
17 | groups.append(contentsOf: readApplications(
18 | directory: .applicationDirectory,
19 | domain: .localDomainMask,
20 | parentPath: ["Local"]
21 | ))
22 |
23 | groups.append(AppGroup(
24 | name: "Shortcuts",
25 | apps: shortcutsAppManager.getShortcuts()
26 | ))
27 |
28 | if let finderURL = readFinderURL(), let finderApp = app(at: finderURL) {
29 | groups.append(AppGroup(name: "Core Services", apps: [finderApp]))
30 | }
31 |
32 | groups.append(contentsOf: readApplications(
33 | directory: .applicationDirectory,
34 | domain: .systemDomainMask,
35 | parentPath: ["System"]
36 | ))
37 | print(
38 | "Finished searching, apps count =",
39 | groups.map(\.apps).reduce([], +).count
40 | )
41 |
42 | return groups
43 | }
44 |
45 | func getRecentApps() -> [App] {
46 | let applicationDirectory: URL
47 | do {
48 | applicationDirectory = try FileManager.default.url(
49 | for: .applicationDirectory,
50 | in: .localDomainMask,
51 | appropriateFor: nil,
52 | create: false
53 | )
54 | } catch {
55 | return []
56 | }
57 | let recentApps = NSWorkspace.shared.runningApplications
58 |
59 | let now = Date()
60 | let urls = recentApps
61 | .sorted { ($0.launchDate ?? now) > ($1.launchDate ?? now) }
62 | .compactMap { $0.bundleURL }
63 | .filter { $0.absoluteString.hasPrefix(applicationDirectory.absoluteString) }
64 | let apps = urls.compactMap { app(at: $0) }
65 | return Array(
66 | apps.prefix(5)
67 | )
68 | }
69 |
70 | // MARK: Private
71 |
72 | private func app(at url: URL) -> App? {
73 | let resourceKeys = [URLResourceKey.isExecutableKey, .isApplicationKey]
74 | let resourceValues = try? url.resourceValues(forKeys: Set(resourceKeys))
75 | if resourceValues?.isApplication == true, resourceValues?.isExecutable == true {
76 | let name = url.deletingPathExtension().lastPathComponent
77 | return App(url: url, name: name)
78 | } else {
79 | return nil
80 | }
81 | }
82 |
83 | private func apps(at urls: [URL]) -> [App] {
84 | urls.compactMap { url in
85 | app(at: url)
86 | }.sorted(by: \.name)
87 | }
88 |
89 | private func readApplications(
90 | directory: FileManager.SearchPathDirectory,
91 | domain: FileManager.SearchPathDomainMask,
92 | parentPath: [String]
93 | ) -> [AppGroup] {
94 | do {
95 | let directoryURL = try FileManager.default.url(
96 | for: directory,
97 | in: domain,
98 | appropriateFor: nil,
99 | create: false
100 | )
101 | return try readApplications(at: directoryURL, parentPath: parentPath)
102 | } catch {
103 | return []
104 | }
105 | }
106 |
107 | private func readApplications(at url: URL, parentPath: [String]) throws -> [AppGroup] {
108 | let childURLs = try FileManager.default.contentsOfDirectory(
109 | at: url,
110 | includingPropertiesForKeys: []
111 | )
112 | let appURLs = childURLs.filter { url in
113 | url.lastPathComponent.hasSuffix(".app")
114 | }
115 | let directoryURLs = childURLs.filter {
116 | $0.pathExtension.isEmpty && !$0.lastPathComponent.starts(with: ".")
117 | }
118 |
119 | let newPath = parentPath + [url.lastPathComponent]
120 | let childAppGroups = directoryURLs.compactMap { url in
121 | (try? readApplications(at: url, parentPath: newPath)) ?? []
122 | }.flatMap {
123 | $0
124 | }
125 |
126 | let appGroupPath = parentPath + [url.lastPathComponent]
127 | let appGroup = AppGroup(
128 | name: appGroupPath.joined(separator: " → "),
129 | apps: apps(at: appURLs)
130 | )
131 | let appGroups = [appGroup] + childAppGroups
132 |
133 | return appGroups.filter { group in
134 | !group.apps.isEmpty
135 | }
136 | }
137 |
138 | private func readFinderURL() -> URL? {
139 | guard
140 | let coreServicesURL = try? FileManager.default.url(
141 | for: .coreServiceDirectory,
142 | in: .systemDomainMask,
143 | appropriateFor: nil,
144 | create: false
145 | ) else
146 | {
147 | return nil
148 | }
149 |
150 | let finderURL = coreServicesURL.appendingPathComponent("Finder.app")
151 | let executableExists = FileManager.default.fileExists(atPath: finderURL.path)
152 |
153 | if executableExists {
154 | return finderURL
155 | } else {
156 | return nil
157 | }
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/Commander/View/WheelPicker/WheelPicker.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 | import SwiftUI
4 | import UniformTypeIdentifiers
5 |
6 | struct WheelPicker: View {
7 | // MARK: Internal
8 |
9 | @State private var isTargetedForDrop = false
10 |
11 | struct IndexedApp: Equatable {
12 | var visualIndex: Int
13 | let app: App
14 | }
15 |
16 | enum DragState: Equatable {
17 | case inactive
18 | case active(appVisualIndex: Int, offset: CGPoint)
19 |
20 | // MARK: Internal
21 |
22 | func isDragging(appVisualIndex: Int) -> Bool {
23 | switch self {
24 | case .active(let index, _):
25 | return index == appVisualIndex
26 | case .inactive:
27 | return false
28 | }
29 | }
30 | }
31 |
32 | let apps: CurrentValueSubject<[App], Never>
33 | @Binding var hoverState: HoverState
34 |
35 | private let diContainer = DIContainer.shared
36 |
37 | private var textInTheMiddle: String? {
38 | if let hoverIndex = hoverState.hoveringAppIndex, indexedApps.indices.contains(hoverIndex) {
39 | return indexedApps[hoverIndex].app.name
40 | } else if isTargetedForDrop {
41 | if apps.value.count < AppsManager.maxAppsCount {
42 | return "Release the mouse button or trackpad to add a new item to the picker"
43 | } else {
44 | return "Maximum item limit reached. Please remove an item before adding another"
45 | }
46 | } else {
47 | return nil
48 | }
49 | }
50 |
51 | var body: some View {
52 | ZStack {
53 | VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
54 | .clipShape(Circle())
55 | .shadow(radius: 3)
56 | sections
57 |
58 | if let textInTheMiddle {
59 | Text(textInTheMiddle)
60 | .foregroundColor(Color(.secondaryLabelColor))
61 | .bold().frame(width: emptySpaceRadius)
62 | .multilineTextAlignment(.center)
63 | .animation(.default, value: hoverState)
64 | }
65 | }.onReceive(apps) { newApps in
66 | let oldApps = indexedApps.sorted(by: \.visualIndex).map(\.app)
67 | if oldApps != newApps {
68 | withAnimation(oldApps.isEmpty ? nil : .default) {
69 | indexedApps = newApps.enumerated().map(IndexedApp.init)
70 | sectionAngle = indexedApps.isEmpty ? 0 : 2 * .pi / CGFloat(indexedApps.count)
71 | }
72 | }
73 | }.onChange(of: dragState) { state in
74 | guard state == .inactive else {
75 | return
76 | }
77 | apps.send(indexedApps.sorted(by: \.visualIndex).map(\.app))
78 | }
79 | .onDrop(of: [UTType.fileURL.identifier], isTargeted: $isTargetedForDrop) { providers in
80 | providers.forEach { provider in
81 | provider.loadDataRepresentation(forTypeIdentifier: UTType.fileURL.identifier) { data, _ in
82 | if let data, let path = String(data: data, encoding: .utf8), let url = URL(string: path) {
83 | DispatchQueue.main.async {
84 | try? diContainer.appsManager.add(appURL: url)
85 | }
86 | }
87 | }
88 | }
89 | return true
90 | }
91 | .onChange(of: isTargetedForDrop) { isTargeted in
92 | if isTargeted {
93 | hoverState = .disabled
94 | } else {
95 | hoverState = .enabledEmpty
96 | }
97 | }
98 | .opacity(isTargetedForDrop ? 0.8 : 1).padding(6).frame(width: size, height: size)
99 | .onChange(of: hoverState.hoveringAppIndex) { index in
100 | if index != nil, hapticFeedbackEnabled {
101 | NSHapticFeedbackManager.defaultPerformer.perform(.generic, performanceTime: .default)
102 | }
103 | }
104 | }
105 |
106 | // MARK: Private
107 |
108 | @State private var indexedApps = [IndexedApp]()
109 | @State private var dragState = DragState.inactive
110 | @State private var sectionAngle: CGFloat = 0
111 | @AppStorage(AppStorageKey.hapticFeedbackEnabled) private var hapticFeedbackEnabled = true
112 |
113 | private var sections: some View {
114 | GeometryReader { proxy in
115 | ForEach(indexedApps.indices, id: \.self) { index in
116 | makeSection(index: index, proxy: proxy)
117 | }
118 | }
119 | }
120 |
121 | private var size: CGFloat {
122 | 260 + CGFloat(indexedApps.count) * 10
123 | }
124 |
125 | private var emptySpaceRadius: CGFloat {
126 | size * 0.35
127 | }
128 |
129 | private func makeSection(index: Int, proxy: GeometryProxy) -> WheelPicker.Section {
130 | Section(
131 | indexedApp: indexedApps[index],
132 | appsCount: indexedApps.count,
133 | emptySpaceRadius: emptySpaceRadius,
134 | proxy: proxy,
135 | hoverState: $hoverState,
136 | dragState: $dragState,
137 | sectionAngle: $sectionAngle
138 | ) { visualIndex in
139 | guard
140 | let toApp = indexedApps
141 | .enumerated()
142 | .first(where: { visualIndex == $0.element.visualIndex }),
143 | let fromApp = indexedApps
144 | .enumerated()
145 | .first(where: { indexedApps[index].visualIndex == $0.element.visualIndex })
146 | else {
147 | assertionFailure()
148 | return
149 | }
150 | withAnimation {
151 | let fromAppVisualIndex = indexedApps[fromApp.offset].visualIndex
152 | indexedApps[fromApp.offset].visualIndex = indexedApps[toApp.offset].visualIndex
153 | indexedApps[toApp.offset].visualIndex = fromAppVisualIndex
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Commander/View/Preferences/SettingsSidebarView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SettingsSidebarView: View {
4 | // MARK: Internal
5 |
6 | @Binding var apps: [App]
7 |
8 | var body: some View {
9 | ZStack(alignment: .top) {
10 | sidebarContent
11 | sidebarTopGradient
12 | }.onReceive(diContainer.appsManager.appGroups) { allApps in
13 | appGroups = allApps
14 | }.onChange(of: enteredURL) { url in
15 | guard let url = url else {
16 | return
17 | }
18 | try? diContainer.appsManager.add(appURL: url)
19 | }.sheet(isPresented: $urlSheetIsShowing) {
20 | URLInputView(isVisible: $urlSheetIsShowing, enteredURL: $enteredURL)
21 | }.alert("App Limit Reached", isPresented: $maxAppsCountSheetIsShowing, actions: {
22 | Button("Ok") {
23 | maxAppsCountSheetIsShowing = false
24 | }
25 | }, message: {
26 | Text(
27 | "You have reached the maximum of \(AppsManager.maxAppsCount) apps. Please remove an existing app before adding a new one."
28 | )
29 | }).animation(.default, value: apps)
30 | }
31 |
32 | // MARK: Private
33 |
34 | @State private var appGroups = [AppGroup]()
35 | @State private var urlSheetIsShowing = false
36 | @State private var enteredURL: URL?
37 | @State private var maxAppsCountSheetIsShowing = false
38 | @State private var searchText = ""
39 | @State private var isEditing = false
40 | private let diContainer = DIContainer.shared
41 |
42 | private var sidebarContent: some View {
43 | List {
44 | HStack {
45 | button(title: "Open file or folder", action: addNewFileOrFolder)
46 | button(title: "Open URL", action: addURL)
47 | }
48 | SettingsSearchField(searchText: $searchText)
49 |
50 | if searchText.isEmpty {
51 | makeSection(
52 | title: "Added to launcher",
53 | apps: diContainer.appsManager.apps.value
54 | ).transition(.move(edge: .trailing))
55 |
56 | ForEach(appGroups, id: \.name) { group in
57 | makeSection(title: group.name, apps: group.apps)
58 | }
59 | } else {
60 | let matchingApps = appGroups.apps.filter { app in
61 | app.name.localizedCaseInsensitiveContains(searchText)
62 | }
63 | if matchingApps.isEmpty {
64 | HStack {
65 | Spacer()
66 | Text("No Results")
67 | .font(.title3.weight(.bold))
68 | Spacer()
69 | }.padding(.top, 32)
70 | } else {
71 | makeSection(title: "Search Results", apps: matchingApps)
72 | }
73 | }
74 | }.listStyle(.sidebar)
75 | }
76 |
77 | private var sidebarTopGradient: some View {
78 | GeometryReader { reader in
79 | VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
80 | .frame(height: reader.safeAreaInsets.top * 4 / 3, alignment: .top)
81 | .mask(LinearGradient(
82 | gradient: Gradient(colors: [.black, .black, .black, .clear]),
83 | startPoint: .top,
84 | endPoint: .bottom
85 | ))
86 | .ignoresSafeArea()
87 | }
88 | }
89 |
90 | private var maxAppsCountNotReached: Bool {
91 | !maxAppsCountReached
92 | }
93 |
94 | private var maxAppsCountReached: Bool {
95 | apps.count >= AppsManager.maxAppsCount
96 | }
97 |
98 | private func button(title: String, action: @escaping () -> Void) -> some View {
99 | Button(action: action) {
100 | GroupBox {
101 | Text(title).padding(.horizontal, 2)
102 | }
103 | }.buttonStyle(.plain)
104 | }
105 |
106 | private func addNewFileOrFolder() {
107 | guard maxAppsCountNotReached else {
108 | maxAppsCountSheetIsShowing = true
109 | return
110 | }
111 | let panel = NSOpenPanel()
112 | panel.allowsMultipleSelection = false
113 | panel.canChooseFiles = true
114 | panel.canChooseDirectories = true
115 | if panel.runModal() == .OK, let url = panel.url {
116 | do {
117 | try diContainer.appsManager.add(appURL: url)
118 | } catch {
119 | // do nothing
120 | }
121 | }
122 | }
123 |
124 | private func addURL() {
125 | guard maxAppsCountNotReached else {
126 | maxAppsCountSheetIsShowing = true
127 | return
128 | }
129 | urlSheetIsShowing = true
130 | }
131 |
132 | private func makeSection(title: String, apps: [App]) -> some View {
133 | Section(title) {
134 | ForEach(apps) { app in
135 | ZStack {
136 | Toggle(isOn: Binding(get: { self.apps.contains(app) }, set: { enable in
137 | if maxAppsCountReached, enable {
138 | maxAppsCountSheetIsShowing = true
139 | } else if !self.apps.contains(app), enable {
140 | diContainer.appsManager.apps.value.append(app)
141 | } else {
142 | diContainer.appsManager.apps.value.removeAll(where: { $0 == app })
143 | }
144 | })) {
145 | Label {
146 | Text(app.name).font(.body)
147 | } icon: {
148 | ZStack {
149 | diContainer.imageProvider.image(for: app, preferredSize: 32)
150 | }
151 | }.labelStyle(.titleAndIcon).padding([.leading, .trailing], 4)
152 | }.frame(maxWidth: .infinity, alignment: .leading)
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/Commander/Common/Util/UserDefault.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | // MARK: - UserDefault
5 |
6 | @propertyWrapper
7 | public struct UserDefault {
8 | private let get: () -> Value
9 | private let set: (Value) -> Void
10 |
11 | public var wrappedValue: Value {
12 | get { get() }
13 | nonmutating set { set(newValue) }
14 | }
15 | }
16 |
17 | extension UserDefault {
18 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Bool {
19 | self.init(defaultValue: wrappedValue, key: key, store: store)
20 | }
21 |
22 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Int {
23 | self.init(defaultValue: wrappedValue, key: key, store: store)
24 | }
25 |
26 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Double {
27 | self.init(defaultValue: wrappedValue, key: key, store: store)
28 | }
29 |
30 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == String {
31 | self.init(defaultValue: wrappedValue, key: key, store: store)
32 | }
33 |
34 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == URL {
35 | self.init(defaultValue: wrappedValue, key: key, store: store)
36 | }
37 |
38 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value == Data {
39 | self.init(defaultValue: wrappedValue, key: key, store: store)
40 | }
41 |
42 | private init(defaultValue: Value, key: String, store: UserDefaults) {
43 | get = {
44 | let value = store.value(forKey: key) as? Value
45 | return value ?? defaultValue
46 | }
47 |
48 | set = { newValue in
49 | store.set(newValue, forKey: key)
50 | }
51 | }
52 | }
53 |
54 | extension UserDefault where Value: Codable {
55 | public init(wrappedValue defaultValue: Value, _ key: String, store: UserDefaults = .standard) {
56 | get = {
57 | let data = store.value(forKey: key) as? Data
58 | return data.flatMap {
59 | try? JSONDecoder().decode(Value.self, from: $0)
60 | } ?? defaultValue
61 | }
62 |
63 | set = { newValue in
64 | let data = try? JSONEncoder().encode(newValue)
65 | store.set(data, forKey: key)
66 | }
67 | }
68 | }
69 |
70 | extension UserDefault where Value: ExpressibleByNilLiteral {
71 | public init(_ key: String, store: UserDefaults = .standard) where Value == Bool? {
72 | self.init(wrappedType: Bool.self, key: key, store: store)
73 | }
74 |
75 | public init(_ key: String, store: UserDefaults = .standard) where Value == Int? {
76 | self.init(wrappedType: Int.self, key: key, store: store)
77 | }
78 |
79 | public init(_ key: String, store: UserDefaults = .standard) where Value == Double? {
80 | self.init(wrappedType: Double.self, key: key, store: store)
81 | }
82 |
83 | public init(_ key: String, store: UserDefaults = .standard) where Value == String? {
84 | self.init(wrappedType: String.self, key: key, store: store)
85 | }
86 |
87 | public init(_ key: String, store: UserDefaults = .standard) where Value == URL? {
88 | self.init(wrappedType: URL.self, key: key, store: store)
89 | }
90 |
91 | public init(_ key: String, store: UserDefaults = .standard) where Value == Data? {
92 | self.init(wrappedType: Data.self, key: key, store: store)
93 | }
94 |
95 | private init(wrappedType _: T.Type, key: String, store: UserDefaults) {
96 | get = {
97 | let value = store.value(forKey: key) as? Value
98 | return value ?? nil
99 | }
100 |
101 | set = { newValue in
102 | let newValue = newValue as? T?
103 |
104 | if let newValue = newValue {
105 | store.set(newValue, forKey: key)
106 | } else {
107 | store.removeObject(forKey: key)
108 | }
109 | }
110 | }
111 | }
112 |
113 | extension UserDefault where Value: RawRepresentable {
114 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == String {
115 | self.init(defaultValue: wrappedValue, key: key, store: store)
116 | }
117 |
118 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == Int {
119 | self.init(defaultValue: wrappedValue, key: key, store: store)
120 | }
121 |
122 | public init(wrappedValue: Value, _ key: String, store: UserDefaults = .standard) where Value.RawValue == UInt {
123 | self.init(defaultValue: wrappedValue, key: key, store: store)
124 | }
125 |
126 | private init(defaultValue: Value, key: String, store: UserDefaults) {
127 | get = {
128 | var value: Value?
129 |
130 | if let rawValue = store.value(forKey: key) as? Value.RawValue {
131 | value = Value(rawValue: rawValue)
132 | }
133 |
134 | return value ?? defaultValue
135 | }
136 |
137 | set = { newValue in
138 | let value = newValue.rawValue
139 | store.set(value, forKey: key)
140 | }
141 | }
142 | }
143 |
144 | extension UserDefault {
145 | public init(_ key: String, store: UserDefaults = .standard) where Value == R?, R: RawRepresentable, R.RawValue == Int {
146 | self.init(key: key, store: store)
147 | }
148 |
149 | public init(_ key: String, store: UserDefaults = .standard) where Value == R?, R: RawRepresentable, R.RawValue == String {
150 | self.init(key: key, store: store)
151 | }
152 |
153 | private init(key: String, store: UserDefaults) where Value == R?, R: RawRepresentable {
154 | get = {
155 | if let rawValue = store.value(forKey: key) as? R.RawValue {
156 | return R(rawValue: rawValue)
157 | } else {
158 | return nil
159 | }
160 | }
161 |
162 | set = { newValue in
163 | let newValue = newValue as R?
164 |
165 | if let newValue = newValue {
166 | store.set(newValue.rawValue, forKey: key)
167 | } else {
168 | store.removeObject(forKey: key)
169 | }
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/Commander/View/WheelPicker/WheelPickerSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension WheelPicker {
4 | struct Section: View {
5 | // MARK: Internal
6 |
7 | let indexedApp: IndexedApp
8 | let appsCount: Int
9 | let emptySpaceRadius: CGFloat
10 | let proxy: GeometryProxy
11 |
12 | @Binding var hoverState: HoverState
13 | @Binding var dragState: DragState
14 | @Binding var sectionAngle: CGFloat
15 | private let diContainer = DIContainer.shared
16 |
17 | var onIndexChange: (_ newIndex: Int) -> Void
18 |
19 | var body: some View {
20 | sectionBackground
21 | EmptyView().trackingMouse { update in
22 | switch update {
23 | case .moved(let newLocation):
24 | hoverState.set(isHovering: sectionPath.contains(newLocation), at: indexedApp.visualIndex)
25 | case .exited:
26 | hoverState.set(isHovering: false, at: indexedApp.visualIndex)
27 | }
28 | }
29 |
30 | ZStack {
31 | diContainer.imageProvider.image(for: indexedApp.app, preferredSize: 64)
32 | .scaleEffect(imageScale)
33 | .background(
34 | Color.clear.contentShape(Circle()).frame(width: 90, height: 90)
35 | )
36 | .animation(.default, value: isHovering)
37 | runningDotView
38 | }.position(
39 | x: dragOffset?.x ?? imageXOffset,
40 | y: dragOffset?.y ?? imageYOffset
41 | ).gesture(dragGesture)
42 | }
43 |
44 | // MARK: Private
45 |
46 | private var dragOffset: CGPoint? {
47 | switch dragState {
48 | case .inactive:
49 | return nil
50 | case .active(let appVisualIndex, let offset):
51 | if appVisualIndex == indexedApp.visualIndex {
52 | return offset
53 | } else {
54 | return nil
55 | }
56 | }
57 | }
58 |
59 | private var isHovering: Bool {
60 | hoverState.isHovering(appIndex: indexedApp.visualIndex)
61 | }
62 |
63 | private var imageScale: CGFloat {
64 | if isHovering {
65 | return 1.1
66 | } else if dragState.isDragging(appVisualIndex: indexedApp.visualIndex) {
67 | return 1
68 | } else {
69 | return 0.9
70 | }
71 | }
72 |
73 | private var imageXOffset: CGFloat {
74 | proxy.size.width / 2 + cos(sectionAngle * CGFloat(indexedApp.visualIndex)) * proxy.size.width * 0.35
75 | }
76 |
77 | private var imageYOffset: CGFloat {
78 | proxy.size.height / 2 - sin(sectionAngle * CGFloat(indexedApp.visualIndex)) * proxy.size.width * 0.35
79 | }
80 |
81 | private var dragGesture: some Gesture {
82 | DragGesture()
83 | .onChanged { (gesture: DragGesture.Value) in
84 | hoverState = .disabled
85 |
86 | let width = proxy.size.width
87 | let height = proxy.size.height
88 | let center = CGPoint(x: width * 0.5, y: height * 0.5)
89 | let locationX = center.x - gesture.location.x
90 | let locationY = -center.y + gesture.location.y
91 | let gestureAngle = atan2(locationY, locationX) + .pi
92 |
93 | var angleDistances = [CGFloat]()
94 | for sectionIndex in 0 ..< appsCount {
95 | let startAngle = sectionAngle * CGFloat(sectionIndex) + sectionAngle / 2 - sectionAngle
96 |
97 | angleDistances.append(
98 | abs((gestureAngle - startAngle - sectionAngle / 2).remainder(dividingBy: 2 * .pi))
99 | )
100 | }
101 | let minDistanceIndex = angleDistances.enumerated().min {
102 | $0.element < $1.element
103 | }?.offset
104 | if let minDistanceIndex = minDistanceIndex, indexedApp.visualIndex != minDistanceIndex {
105 | onIndexChange(minDistanceIndex)
106 | }
107 | withAnimation {
108 | dragState = .active(appVisualIndex: indexedApp.visualIndex, offset: gesture.location)
109 | }
110 | }.onEnded { _ in
111 | withAnimation {
112 | dragState = .inactive
113 | hoverState = .enabledEmpty
114 | }
115 | }
116 | }
117 |
118 | private var sectionPath: Path {
119 | Path { path in
120 | let width = proxy.size.width
121 | let height = proxy.size.height
122 |
123 | let center = CGPoint(x: width * 0.5, y: height * 0.5)
124 | let startAngle = Angle(radians: -sectionAngle * CGFloat(indexedApp.visualIndex) + sectionAngle / 2)
125 | let diffAngle = Angle(radians: -sectionAngle)
126 |
127 | path.addArc(
128 | center: center,
129 | radius: emptySpaceRadius / 2,
130 | startAngle: startAngle,
131 | endAngle: startAngle + diffAngle,
132 | clockwise: true
133 | )
134 |
135 | path.addArc(
136 | center: center,
137 | radius: width * 0.5,
138 | startAngle: startAngle + diffAngle,
139 | endAngle: startAngle,
140 | clockwise: false
141 | )
142 | path.closeSubpath()
143 | }
144 | }
145 |
146 | @ViewBuilder private var sectionBackground: some View {
147 | if hoverState.isEnabled, isHovering {
148 | VisualEffectView(material: .selection, blendingMode: .behindWindow)
149 | .clipShape(sectionPath)
150 | }
151 | }
152 |
153 | @ViewBuilder private var runningDotView: some View {
154 | if diContainer.appsManager.isLaunched(app: indexedApp.app), !isHovering {
155 | Circle()
156 | .fill(Color(nsColor: NSColor.tertiaryLabelColor))
157 | .frame(width: 5, height: 5)
158 | .offset(runningDotOffset)
159 | }
160 | }
161 |
162 | private var runningDotOffset: CGSize {
163 | let offset: CGFloat = proxy.size.width * 0.12
164 | return CGSize(
165 | width: cos(sectionAngle * CGFloat(indexedApp.visualIndex)) * offset,
166 | height: -sin(sectionAngle * CGFloat(indexedApp.visualIndex)) * offset
167 | )
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/Commander/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import ServiceManagement
3 | import StoreKit
4 | import SwiftUI
5 |
6 | // MARK: - AppDelegate
7 |
8 | final class AppDelegate: NSObject, NSApplicationDelegate {
9 | // MARK: Lifecycle
10 |
11 | override init() {
12 | statusItem = Self.makeStatusItem()
13 | diContainer = DIContainer(statusItem: statusItem)
14 | super.init()
15 | }
16 |
17 | // MARK: Internal
18 |
19 | let diContainer: DIContainer
20 |
21 | func applicationDidFinishLaunching(_: Notification) {
22 | window = Self.makeWindow(diContainer: diContainer)
23 | popover = Self.makePopover(diContainer: diContainer)
24 |
25 | diContainer.shortcutNotifier.$shortcutTriggered.sink { [weak self] isTriggered in
26 | self?.shortcutStateUpdated(isTriggered: isTriggered)
27 | }.store(in: &cancellables)
28 | NSApplication.shared.mainMenu = AppMenu()
29 |
30 | openSettingsIfNeeded()
31 | }
32 |
33 | func applicationDidBecomeActive(_: Notification) {
34 | print(#function)
35 | if !diContainer.shortcutNotifier.shortcutTriggered {
36 | openSettings()
37 | }
38 | }
39 |
40 | func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
41 | true
42 | }
43 |
44 | // MARK: Private
45 |
46 | @UserDefault("first launch") private var firstLaunch = true
47 |
48 | private let statusItem: NSStatusItem
49 | private var window: NSWindow?
50 | private var popover: NSPopover?
51 | private var cancellables = Set()
52 |
53 | private lazy var settingsWindow = AppDelegate.makeSettingsWindow(diContainer: diContainer)
54 |
55 | private static func makeWindow(diContainer _: DIContainer) -> NSWindow {
56 | let window = NSWindow(
57 | contentViewController: NSHostingController(
58 | rootView: CommanderView()
59 | )
60 | )
61 | window.level = .modalPanel
62 | window.isReleasedWhenClosed = false
63 | window.titleVisibility = .hidden
64 | window.titlebarAppearsTransparent = true
65 | window.styleMask = [.borderless, .fullSizeContentView]
66 | window.standardWindowButton(.closeButton)?.isHidden = true
67 | window.standardWindowButton(.miniaturizeButton)?.isHidden = true
68 | window.standardWindowButton(.zoomButton)?.isHidden = true
69 | window.standardWindowButton(.toolbarButton)?.isHidden = true
70 | window.backgroundColor = .clear
71 | window.hasShadow = false
72 | window.isOpaque = false
73 | if #available(macOS 13.0, *) {
74 | window.collectionBehavior = [
75 | .auxiliary,
76 | .moveToActiveSpace,
77 | .stationary,
78 | .fullScreenAuxiliary,
79 | .ignoresCycle,
80 | ]
81 | } else {
82 | window.collectionBehavior = .canJoinAllSpaces
83 | }
84 | return window
85 | }
86 |
87 | private static func makePopover(diContainer _: DIContainer) -> NSPopover {
88 | let popover = NSPopover()
89 | popover.behavior = .transient
90 | popover.contentViewController = NSHostingController(
91 | rootView: CommanderView()
92 | )
93 | return popover
94 | }
95 |
96 | private static func makeStatusItem() -> NSStatusItem {
97 | let statusBarItem = NSStatusBar.system
98 | .statusItem(withLength: CGFloat(NSStatusItem.variableLength))
99 | statusBarItem.menu = NSMenu(title: "Menu")
100 |
101 | let quitItem = NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q")
102 | let settingsItem = NSMenuItem(title: "Settings", action: #selector(openSettings), keyEquivalent: ",")
103 | let aboutItem = NSMenuItem(title: "About", action: #selector(openAbout), keyEquivalent: "")
104 |
105 | let rateAppItem = NSMenuItem(title: "Rate App", action: #selector(rateApp), keyEquivalent: "")
106 | let githubItem = NSMenuItem(title: "View on Github", action: #selector(viewGithub), keyEquivalent: "")
107 |
108 | let requestFeatureItem = NSMenuItem(title: "Request a Feature", action: #selector(requestFeature), keyEquivalent: "")
109 | let reportBugItem = NSMenuItem(title: "Report a Bug", action: #selector(reportBug), keyEquivalent: "")
110 | let contactUsItem = NSMenuItem(title: "Contact us", action: #selector(contactUs), keyEquivalent: "")
111 |
112 | [
113 | settingsItem,
114 | .separator(),
115 | requestFeatureItem,
116 | reportBugItem,
117 | contactUsItem,
118 | .separator(),
119 | rateAppItem,
120 | githubItem,
121 | .separator(),
122 | aboutItem,
123 | quitItem,
124 | ].forEach {
125 | statusBarItem.menu?.addItem($0)
126 | }
127 |
128 | if let button = statusBarItem.button {
129 | button.image = NSImage(named: "menu template")
130 | button.action = #selector(togglePopover)
131 | }
132 |
133 | return statusBarItem
134 | }
135 |
136 | private static func makeSettingsWindow(diContainer: DIContainer) -> NSWindow {
137 | let controller = NSHostingController(
138 | rootView: SettingsView()
139 | )
140 | let window = NSWindow(contentViewController: controller)
141 | window.title = "Settings"
142 | window.titlebarAppearsTransparent = true
143 | window.styleMask = [
144 | .unifiedTitleAndToolbar,
145 | .fullSizeContentView,
146 | .closable,
147 | .miniaturizable,
148 | .resizable,
149 | .titled,
150 | ]
151 | window.toolbar?.isVisible = false
152 | window.titleVisibility = .hidden
153 | diContainer.dockIconManager.settingsWindow = window
154 |
155 | return window
156 | }
157 |
158 | private func openSettingsIfNeeded() {
159 | guard firstLaunch || !diContainer.menuBarItemManager.showMenuBarItem else {
160 | return
161 | }
162 | firstLaunch = false
163 | openSettings()
164 | }
165 |
166 | @objc
167 | private func togglePopover() {
168 | guard let popover, let button = statusItem.button else {
169 | return
170 | }
171 | if popover.isShown {
172 | popover.performClose(nil)
173 | } else {
174 | popover.show(
175 | relativeTo: button.bounds,
176 | of: button,
177 | preferredEdge: .minY
178 | )
179 | }
180 | }
181 |
182 | private func shortcutStateUpdated(isTriggered: Bool) {
183 | guard let window, window.isVisible != isTriggered else {
184 | return
185 | }
186 |
187 | window.setIsVisible(isTriggered)
188 | if isTriggered {
189 | window.orderFrontRegardless()
190 | window.setFrameOrigin(
191 | NSEvent.mouseLocation.applying(.init(
192 | translationX: -window.frame.width / 2,
193 | y: -window.frame.height / 2
194 | ))
195 | )
196 | window.makeCompletelyVisibleIfNeeded()
197 | }
198 | }
199 | }
200 |
201 | extension AppDelegate {
202 | // MARK: Internal
203 |
204 | @objc
205 | func openSettings() {
206 | print(#function, "started")
207 | diContainer.appsManager.updateApps()
208 | settingsWindow.makeKeyAndOrderFront(nil)
209 | settingsWindow.orderFrontRegardless()
210 | NSApp.activate(ignoringOtherApps: true)
211 | print(#function, "Finished")
212 | }
213 |
214 | // MARK: Private
215 |
216 | private static func makeWindow(title: String, swiftUIView: Content, diContainer _: DIContainer) -> NSWindow {
217 | let controller = NSHostingController(
218 | rootView: swiftUIView
219 | )
220 | let window = NSWindow(contentViewController: controller)
221 | window.title = title
222 | return window
223 | }
224 |
225 | @objc
226 | private func quit() {
227 | NSApp.terminate(nil)
228 | }
229 |
230 | @objc
231 | private func openAbout() {
232 | NSApp.activate(ignoringOtherApps: true)
233 | NSApp.orderFrontStandardAboutPanel()
234 | }
235 |
236 | @objc
237 | private func rateApp() {
238 | diContainer.appRatingManager.openRatePage()
239 | }
240 |
241 | @objc
242 | private func viewGithub() {
243 | if let url = URL(string: "https://github.com/vadim-ahmerov/Commander") {
244 | NSWorkspace.shared.open(url)
245 | }
246 | }
247 |
248 | @objc
249 | private func requestFeature() {
250 | let window = Self.makeWindow(
251 | title: "Request a Feature",
252 | swiftUIView: SendFeedbackView(),
253 | diContainer: diContainer
254 | )
255 | window.makeKeyAndOrderFront(nil)
256 | window.orderFrontRegardless()
257 | NSApp.activate(ignoringOtherApps: true)
258 | }
259 |
260 | @objc
261 | private func reportBug() {
262 | let window = Self.makeWindow(
263 | title: "Report a Bug",
264 | swiftUIView: SendFeedbackView(),
265 | diContainer: diContainer
266 | )
267 | window.makeKeyAndOrderFront(nil)
268 | window.orderFrontRegardless()
269 | NSApp.activate(ignoringOtherApps: true)
270 | }
271 |
272 | @objc
273 | private func contactUs() {
274 | let window = Self.makeWindow(
275 | title: "Contact us",
276 | swiftUIView: SendFeedbackView(),
277 | diContainer: diContainer
278 | )
279 | window.makeKeyAndOrderFront(nil)
280 | window.orderFrontRegardless()
281 | NSApp.activate(ignoringOtherApps: true)
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/Commander.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 7F2329BE2A7BFE1C00378358 /* MenuBarItemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2329BD2A7BFE1C00378358 /* MenuBarItemManager.swift */; };
11 | 7F2329C02A7CD57300378358 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2329BF2A7CD57300378358 /* AppGroup.swift */; };
12 | 7F2F774029F402AC00BC5402 /* SettingsTutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2F773F29F402AC00BC5402 /* SettingsTutorialView.swift */; };
13 | 7F32BD2A2A946FE100CF4E22 /* AutoLaunchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F32BD292A946FE100CF4E22 /* AutoLaunchManager.swift */; };
14 | 7F4E6E722A9C5AE800BBAD9D /* DockIconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F4E6E712A9C5AE800BBAD9D /* DockIconManager.swift */; };
15 | 7F5A302028815BF500E3372D /* TrackingAreaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5A301F28815BF500E3372D /* TrackingAreaView.swift */; };
16 | 7F5A302228815C6300E3372D /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5A302128815C6300E3372D /* VisualEffectView.swift */; };
17 | 7F5A30242881601300E3372D /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5A30232881601300E3372D /* View+Extension.swift */; };
18 | 7F5D61DE29CAC8F600092688 /* AppRatingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5D61DD29CAC8F600092688 /* AppRatingManager.swift */; };
19 | 7F5D61E029CAE15900092688 /* AppMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5D61DF29CAE15900092688 /* AppMenu.swift */; };
20 | 7F8481AD2880A854002419E7 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8481AC2880A854002419E7 /* main.swift */; };
21 | 7F8481AF2880BE46002419E7 /* WheelPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8481AE2880BE46002419E7 /* WheelPicker.swift */; };
22 | 7F84C29929CB426D00316630 /* SettingsSidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F84C29829CB426D00316630 /* SettingsSidebarView.swift */; };
23 | 7F84C29D29CB614800316630 /* SettingsSearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F84C29C29CB614800316630 /* SettingsSearchField.swift */; };
24 | 7F8B666A2893F14200E2B108 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7F8B66692893F14200E2B108 /* Assets.xcassets */; };
25 | 7F8B66792893F27100E2B108 /* Launcher.app in Resources */ = {isa = PBXBuildFile; fileRef = 7F8B66632893F14200E2B108 /* Launcher.app */; };
26 | 7F8B66842893F34800E2B108 /* Launcher.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7F8B66632893F14200E2B108 /* Launcher.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
27 | 7F8B66852893F36100E2B108 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F8B66772893F26900E2B108 /* ServiceManagement.framework */; };
28 | 7F8B66872893F40D00E2B108 /* NotificationName+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B66862893F40C00E2B108 /* NotificationName+Extension.swift */; };
29 | 7F8B66892893F43D00E2B108 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B66882893F43D00E2B108 /* AppDelegate.swift */; };
30 | 7F8B668B2893F47100E2B108 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B668A2893F47100E2B108 /* main.swift */; };
31 | 7F8B668D2893F4E200E2B108 /* NotificationName+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B668C2893F4E200E2B108 /* NotificationName+Extension.swift */; };
32 | 7F8D9792289135CA0018AA2A /* ShortcutKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8D9791289135CA0018AA2A /* ShortcutKeysView.swift */; };
33 | 7F8D9794289165A10018AA2A /* AppStorageKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8D9793289165A10018AA2A /* AppStorageKey.swift */; };
34 | 7F8D9796289167690018AA2A /* AppSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8D9795289167690018AA2A /* AppSearcher.swift */; };
35 | 7F8D9798289169FA0018AA2A /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8D9797289169FA0018AA2A /* UserDefault.swift */; };
36 | 7F8D979A28916EBB0018AA2A /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8D979928916EBB0018AA2A /* ImageProvider.swift */; };
37 | 7F8D979C2891731E0018AA2A /* CGPoint+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8D979B2891731E0018AA2A /* CGPoint+Extension.swift */; };
38 | 7FA07D4D29A1BEF900F32F1C /* SendFeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA07D4C29A1BEF900F32F1C /* SendFeedbackView.swift */; };
39 | 7FA07D4F29A1C6D000F32F1C /* TextArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FA07D4E29A1C6D000F32F1C /* TextArea.swift */; };
40 | 7FAD55C9288956E2009A4020 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FAD55C8288956E2009A4020 /* BookmarksManager.swift */; };
41 | 7FAD55CC2889D8DC009A4020 /* URLInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FAD55CB2889D8DC009A4020 /* URLInputView.swift */; };
42 | 7FBDF3FF2880953C00173B7B /* ShortcutNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBDF3FE2880953C00173B7B /* ShortcutNotifier.swift */; };
43 | 7FC3E32129C4E9CB008EFC1B /* ShortcutsAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FC3E32029C4E9CB008EFC1B /* ShortcutsAppManager.swift */; };
44 | 7FD345AD28803DEE00DDD1D5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD345AC28803DEE00DDD1D5 /* AppDelegate.swift */; };
45 | 7FD345AF28803DEE00DDD1D5 /* CommanderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FD345AE28803DEE00DDD1D5 /* CommanderView.swift */; };
46 | 7FD345B128803DEF00DDD1D5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7FD345B028803DEF00DDD1D5 /* Assets.xcassets */; };
47 | 7FF08B082881E636000ED2AD /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B072881E636000ED2AD /* NSWindow+Extension.swift */; };
48 | 7FF08B0B2881E6DD000ED2AD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B0A2881E6DD000ED2AD /* SettingsView.swift */; };
49 | 7FF08B0E288544AC000ED2AD /* AppsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B0D288544AB000ED2AD /* AppsManager.swift */; };
50 | 7FF08B10288544E0000ED2AD /* DIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B0F288544E0000ED2AD /* DIContainer.swift */; };
51 | 7FF08B1728854FE4000ED2AD /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B1628854FE4000ED2AD /* App.swift */; };
52 | 7FF08B1B2885DAE9000ED2AD /* WheelPickerHoverState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B1A2885DAE9000ED2AD /* WheelPickerHoverState.swift */; };
53 | 7FF08B1D2885DB94000ED2AD /* WheelPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B1C2885DB94000ED2AD /* WheelPickerSection.swift */; };
54 | 7FF08B1F288618BA000ED2AD /* Sequence+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF08B1E288618BA000ED2AD /* Sequence+Extension.swift */; };
55 | /* End PBXBuildFile section */
56 |
57 | /* Begin PBXContainerItemProxy section */
58 | 7F8B667A2893F27100E2B108 /* PBXContainerItemProxy */ = {
59 | isa = PBXContainerItemProxy;
60 | containerPortal = 7FD345A128803DEE00DDD1D5 /* Project object */;
61 | proxyType = 1;
62 | remoteGlobalIDString = 7F8B66622893F14200E2B108;
63 | remoteInfo = Launcher;
64 | };
65 | 7F8B667C2893F28300E2B108 /* PBXContainerItemProxy */ = {
66 | isa = PBXContainerItemProxy;
67 | containerPortal = 7FD345A128803DEE00DDD1D5 /* Project object */;
68 | proxyType = 1;
69 | remoteGlobalIDString = 7F8B66622893F14200E2B108;
70 | remoteInfo = Launcher;
71 | };
72 | 7F8B667E2893F2A200E2B108 /* PBXContainerItemProxy */ = {
73 | isa = PBXContainerItemProxy;
74 | containerPortal = 7FD345A128803DEE00DDD1D5 /* Project object */;
75 | proxyType = 1;
76 | remoteGlobalIDString = 7F8B66622893F14200E2B108;
77 | remoteInfo = Launcher;
78 | };
79 | 7F8B66802893F2C700E2B108 /* PBXContainerItemProxy */ = {
80 | isa = PBXContainerItemProxy;
81 | containerPortal = 7FD345A128803DEE00DDD1D5 /* Project object */;
82 | proxyType = 1;
83 | remoteGlobalIDString = 7F8B66622893F14200E2B108;
84 | remoteInfo = Launcher;
85 | };
86 | 7F8B66822893F32A00E2B108 /* PBXContainerItemProxy */ = {
87 | isa = PBXContainerItemProxy;
88 | containerPortal = 7FD345A128803DEE00DDD1D5 /* Project object */;
89 | proxyType = 1;
90 | remoteGlobalIDString = 7F8B66622893F14200E2B108;
91 | remoteInfo = Launcher;
92 | };
93 | /* End PBXContainerItemProxy section */
94 |
95 | /* Begin PBXCopyFilesBuildPhase section */
96 | 7F8B66752893F23B00E2B108 /* CopyFiles */ = {
97 | isa = PBXCopyFilesBuildPhase;
98 | buildActionMask = 2147483647;
99 | dstPath = Contents/Library/LoginItems;
100 | dstSubfolderSpec = 1;
101 | files = (
102 | 7F8B66842893F34800E2B108 /* Launcher.app in CopyFiles */,
103 | );
104 | runOnlyForDeploymentPostprocessing = 0;
105 | };
106 | /* End PBXCopyFilesBuildPhase section */
107 |
108 | /* Begin PBXFileReference section */
109 | 7F2329BD2A7BFE1C00378358 /* MenuBarItemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItemManager.swift; sourceTree = ""; };
110 | 7F2329BF2A7CD57300378358 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; };
111 | 7F2F773F29F402AC00BC5402 /* SettingsTutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTutorialView.swift; sourceTree = ""; };
112 | 7F32BD292A946FE100CF4E22 /* AutoLaunchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLaunchManager.swift; sourceTree = ""; };
113 | 7F4E6E712A9C5AE800BBAD9D /* DockIconManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockIconManager.swift; sourceTree = ""; };
114 | 7F5A301F28815BF500E3372D /* TrackingAreaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingAreaView.swift; sourceTree = ""; };
115 | 7F5A302128815C6300E3372D /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; };
116 | 7F5A30232881601300E3372D /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; };
117 | 7F5D61DD29CAC8F600092688 /* AppRatingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRatingManager.swift; sourceTree = ""; };
118 | 7F5D61DF29CAE15900092688 /* AppMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMenu.swift; sourceTree = ""; };
119 | 7F8481AC2880A854002419E7 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
120 | 7F8481AE2880BE46002419E7 /* WheelPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelPicker.swift; sourceTree = ""; };
121 | 7F84C29829CB426D00316630 /* SettingsSidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebarView.swift; sourceTree = ""; };
122 | 7F84C29C29CB614800316630 /* SettingsSearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchField.swift; sourceTree = ""; };
123 | 7F8B66632893F14200E2B108 /* Launcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Launcher.app; sourceTree = BUILT_PRODUCTS_DIR; };
124 | 7F8B66692893F14200E2B108 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
125 | 7F8B666E2893F14200E2B108 /* Launcher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Launcher.entitlements; sourceTree = ""; };
126 | 7F8B66742893F21A00E2B108 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
127 | 7F8B66772893F26900E2B108 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; };
128 | 7F8B66862893F40C00E2B108 /* NotificationName+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Extension.swift"; sourceTree = ""; };
129 | 7F8B66882893F43D00E2B108 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
130 | 7F8B668A2893F47100E2B108 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
131 | 7F8B668C2893F4E200E2B108 /* NotificationName+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Extension.swift"; sourceTree = ""; };
132 | 7F8D9791289135CA0018AA2A /* ShortcutKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutKeysView.swift; sourceTree = ""; };
133 | 7F8D9793289165A10018AA2A /* AppStorageKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorageKey.swift; sourceTree = ""; };
134 | 7F8D9795289167690018AA2A /* AppSearcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSearcher.swift; sourceTree = ""; };
135 | 7F8D9797289169FA0018AA2A /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; };
136 | 7F8D979928916EBB0018AA2A /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = ""; };
137 | 7F8D979B2891731E0018AA2A /* CGPoint+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Extension.swift"; sourceTree = ""; };
138 | 7FA07D4C29A1BEF900F32F1C /* SendFeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendFeedbackView.swift; sourceTree = ""; };
139 | 7FA07D4E29A1C6D000F32F1C /* TextArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextArea.swift; sourceTree = ""; };
140 | 7FAD55C8288956E2009A4020 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = ""; };
141 | 7FAD55CB2889D8DC009A4020 /* URLInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLInputView.swift; sourceTree = ""; };
142 | 7FBDF3FE2880953C00173B7B /* ShortcutNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutNotifier.swift; sourceTree = ""; };
143 | 7FC3E32029C4E9CB008EFC1B /* ShortcutsAppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsAppManager.swift; sourceTree = ""; };
144 | 7FD345A928803DEE00DDD1D5 /* Commander.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Commander.app; sourceTree = BUILT_PRODUCTS_DIR; };
145 | 7FD345AC28803DEE00DDD1D5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
146 | 7FD345AE28803DEE00DDD1D5 /* CommanderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommanderView.swift; sourceTree = ""; };
147 | 7FD345B028803DEF00DDD1D5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
148 | 7FD345B528803DEF00DDD1D5 /* Commander.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Commander.entitlements; sourceTree = ""; };
149 | 7FD345BB28804D8F00DDD1D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
150 | 7FF08B072881E636000ED2AD /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; };
151 | 7FF08B0A2881E6DD000ED2AD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
152 | 7FF08B0D288544AB000ED2AD /* AppsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsManager.swift; sourceTree = ""; };
153 | 7FF08B0F288544E0000ED2AD /* DIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DIContainer.swift; sourceTree = ""; };
154 | 7FF08B1628854FE4000ED2AD /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
155 | 7FF08B1A2885DAE9000ED2AD /* WheelPickerHoverState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelPickerHoverState.swift; sourceTree = ""; };
156 | 7FF08B1C2885DB94000ED2AD /* WheelPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WheelPickerSection.swift; sourceTree = ""; };
157 | 7FF08B1E288618BA000ED2AD /* Sequence+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extension.swift"; sourceTree = ""; };
158 | /* End PBXFileReference section */
159 |
160 | /* Begin PBXFrameworksBuildPhase section */
161 | 7F8B66602893F14200E2B108 /* Frameworks */ = {
162 | isa = PBXFrameworksBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | );
166 | runOnlyForDeploymentPostprocessing = 0;
167 | };
168 | 7FD345A628803DEE00DDD1D5 /* Frameworks */ = {
169 | isa = PBXFrameworksBuildPhase;
170 | buildActionMask = 2147483647;
171 | files = (
172 | 7F8B66852893F36100E2B108 /* ServiceManagement.framework in Frameworks */,
173 | );
174 | runOnlyForDeploymentPostprocessing = 0;
175 | };
176 | /* End PBXFrameworksBuildPhase section */
177 |
178 | /* Begin PBXGroup section */
179 | 7F5A301E28815BE500E3372D /* Common */ = {
180 | isa = PBXGroup;
181 | children = (
182 | 7FE7B2E22897BDD6002D449E /* Extension */,
183 | 7FE7B2E12897BDBD002D449E /* Util */,
184 | 7FE7B2E02897BDB0002D449E /* View */,
185 | );
186 | path = Common;
187 | sourceTree = "";
188 | };
189 | 7F8B66642893F14200E2B108 /* Launcher */ = {
190 | isa = PBXGroup;
191 | children = (
192 | 7F8B66742893F21A00E2B108 /* Info.plist */,
193 | 7F8B66692893F14200E2B108 /* Assets.xcassets */,
194 | 7F8B666E2893F14200E2B108 /* Launcher.entitlements */,
195 | 7F8B66882893F43D00E2B108 /* AppDelegate.swift */,
196 | 7F8B668C2893F4E200E2B108 /* NotificationName+Extension.swift */,
197 | 7F8B668A2893F47100E2B108 /* main.swift */,
198 | );
199 | path = Launcher;
200 | sourceTree = "";
201 | };
202 | 7F8B66762893F26900E2B108 /* Frameworks */ = {
203 | isa = PBXGroup;
204 | children = (
205 | 7F8B66772893F26900E2B108 /* ServiceManagement.framework */,
206 | );
207 | name = Frameworks;
208 | sourceTree = "";
209 | };
210 | 7FA07D4B29A1BEED00F32F1C /* Feedback */ = {
211 | isa = PBXGroup;
212 | children = (
213 | 7FA07D4C29A1BEF900F32F1C /* SendFeedbackView.swift */,
214 | 7FA07D4E29A1C6D000F32F1C /* TextArea.swift */,
215 | );
216 | path = Feedback;
217 | sourceTree = "";
218 | };
219 | 7FD345A028803DEE00DDD1D5 = {
220 | isa = PBXGroup;
221 | children = (
222 | 7FD345AB28803DEE00DDD1D5 /* Commander */,
223 | 7F8B66642893F14200E2B108 /* Launcher */,
224 | 7FD345AA28803DEE00DDD1D5 /* Products */,
225 | 7F8B66762893F26900E2B108 /* Frameworks */,
226 | );
227 | sourceTree = "";
228 | };
229 | 7FD345AA28803DEE00DDD1D5 /* Products */ = {
230 | isa = PBXGroup;
231 | children = (
232 | 7FD345A928803DEE00DDD1D5 /* Commander.app */,
233 | 7F8B66632893F14200E2B108 /* Launcher.app */,
234 | );
235 | name = Products;
236 | sourceTree = "";
237 | };
238 | 7FD345AB28803DEE00DDD1D5 /* Commander */ = {
239 | isa = PBXGroup;
240 | children = (
241 | 7FF08B1528854FDF000ED2AD /* Models */,
242 | 7FF08B1328854BDA000ED2AD /* View */,
243 | 7FF08B0C2885449A000ED2AD /* Manager */,
244 | 7F5A301E28815BE500E3372D /* Common */,
245 | 7FD345BB28804D8F00DDD1D5 /* Info.plist */,
246 | 7FD345AC28803DEE00DDD1D5 /* AppDelegate.swift */,
247 | 7FD345B028803DEF00DDD1D5 /* Assets.xcassets */,
248 | 7FD345B528803DEF00DDD1D5 /* Commander.entitlements */,
249 | 7F8481AC2880A854002419E7 /* main.swift */,
250 | 7F5D61DF29CAE15900092688 /* AppMenu.swift */,
251 | );
252 | path = Commander;
253 | sourceTree = "";
254 | };
255 | 7FE7B2E02897BDB0002D449E /* View */ = {
256 | isa = PBXGroup;
257 | children = (
258 | 7F5A301F28815BF500E3372D /* TrackingAreaView.swift */,
259 | 7F5A302128815C6300E3372D /* VisualEffectView.swift */,
260 | );
261 | path = View;
262 | sourceTree = "";
263 | };
264 | 7FE7B2E12897BDBD002D449E /* Util */ = {
265 | isa = PBXGroup;
266 | children = (
267 | 7F8D9797289169FA0018AA2A /* UserDefault.swift */,
268 | );
269 | path = Util;
270 | sourceTree = "";
271 | };
272 | 7FE7B2E22897BDD6002D449E /* Extension */ = {
273 | isa = PBXGroup;
274 | children = (
275 | 7F5A30232881601300E3372D /* View+Extension.swift */,
276 | 7FF08B072881E636000ED2AD /* NSWindow+Extension.swift */,
277 | 7FF08B1E288618BA000ED2AD /* Sequence+Extension.swift */,
278 | 7F8D979B2891731E0018AA2A /* CGPoint+Extension.swift */,
279 | 7F8B66862893F40C00E2B108 /* NotificationName+Extension.swift */,
280 | );
281 | path = Extension;
282 | sourceTree = "";
283 | };
284 | 7FF08B0C2885449A000ED2AD /* Manager */ = {
285 | isa = PBXGroup;
286 | children = (
287 | 7FF08B0F288544E0000ED2AD /* DIContainer.swift */,
288 | 7F8D9793289165A10018AA2A /* AppStorageKey.swift */,
289 | 7FF08B0D288544AB000ED2AD /* AppsManager.swift */,
290 | 7FBDF3FE2880953C00173B7B /* ShortcutNotifier.swift */,
291 | 7FAD55C8288956E2009A4020 /* BookmarksManager.swift */,
292 | 7F8D9795289167690018AA2A /* AppSearcher.swift */,
293 | 7F8D979928916EBB0018AA2A /* ImageProvider.swift */,
294 | 7FC3E32029C4E9CB008EFC1B /* ShortcutsAppManager.swift */,
295 | 7F5D61DD29CAC8F600092688 /* AppRatingManager.swift */,
296 | 7F2329BD2A7BFE1C00378358 /* MenuBarItemManager.swift */,
297 | 7F32BD292A946FE100CF4E22 /* AutoLaunchManager.swift */,
298 | 7F4E6E712A9C5AE800BBAD9D /* DockIconManager.swift */,
299 | );
300 | path = Manager;
301 | sourceTree = "";
302 | };
303 | 7FF08B1328854BDA000ED2AD /* View */ = {
304 | isa = PBXGroup;
305 | children = (
306 | 7FA07D4B29A1BEED00F32F1C /* Feedback */,
307 | 7FF08B1828855FFB000ED2AD /* Preferences */,
308 | 7FF08B1428854BE1000ED2AD /* Commander */,
309 | 7FF08B192885DACE000ED2AD /* WheelPicker */,
310 | );
311 | path = View;
312 | sourceTree = "";
313 | };
314 | 7FF08B1428854BE1000ED2AD /* Commander */ = {
315 | isa = PBXGroup;
316 | children = (
317 | 7FD345AE28803DEE00DDD1D5 /* CommanderView.swift */,
318 | );
319 | path = Commander;
320 | sourceTree = "";
321 | };
322 | 7FF08B1528854FDF000ED2AD /* Models */ = {
323 | isa = PBXGroup;
324 | children = (
325 | 7FF08B1628854FE4000ED2AD /* App.swift */,
326 | 7F2329BF2A7CD57300378358 /* AppGroup.swift */,
327 | );
328 | path = Models;
329 | sourceTree = "";
330 | };
331 | 7FF08B1828855FFB000ED2AD /* Preferences */ = {
332 | isa = PBXGroup;
333 | children = (
334 | 7FF08B0A2881E6DD000ED2AD /* SettingsView.swift */,
335 | 7FAD55CB2889D8DC009A4020 /* URLInputView.swift */,
336 | 7F8D9791289135CA0018AA2A /* ShortcutKeysView.swift */,
337 | 7F84C29829CB426D00316630 /* SettingsSidebarView.swift */,
338 | 7F84C29C29CB614800316630 /* SettingsSearchField.swift */,
339 | 7F2F773F29F402AC00BC5402 /* SettingsTutorialView.swift */,
340 | );
341 | path = Preferences;
342 | sourceTree = "";
343 | };
344 | 7FF08B192885DACE000ED2AD /* WheelPicker */ = {
345 | isa = PBXGroup;
346 | children = (
347 | 7F8481AE2880BE46002419E7 /* WheelPicker.swift */,
348 | 7FF08B1C2885DB94000ED2AD /* WheelPickerSection.swift */,
349 | 7FF08B1A2885DAE9000ED2AD /* WheelPickerHoverState.swift */,
350 | );
351 | path = WheelPicker;
352 | sourceTree = "";
353 | };
354 | /* End PBXGroup section */
355 |
356 | /* Begin PBXNativeTarget section */
357 | 7F8B66622893F14200E2B108 /* Launcher */ = {
358 | isa = PBXNativeTarget;
359 | buildConfigurationList = 7F8B66712893F14200E2B108 /* Build configuration list for PBXNativeTarget "Launcher" */;
360 | buildPhases = (
361 | 7F8B665F2893F14200E2B108 /* Sources */,
362 | 7F8B66602893F14200E2B108 /* Frameworks */,
363 | 7F8B66612893F14200E2B108 /* Resources */,
364 | );
365 | buildRules = (
366 | );
367 | dependencies = (
368 | );
369 | name = Launcher;
370 | productName = Launcher;
371 | productReference = 7F8B66632893F14200E2B108 /* Launcher.app */;
372 | productType = "com.apple.product-type.application";
373 | };
374 | 7FD345A828803DEE00DDD1D5 /* Commander */ = {
375 | isa = PBXNativeTarget;
376 | buildConfigurationList = 7FD345B828803DEF00DDD1D5 /* Build configuration list for PBXNativeTarget "Commander" */;
377 | buildPhases = (
378 | 7FD345A528803DEE00DDD1D5 /* Sources */,
379 | 7FD345A628803DEE00DDD1D5 /* Frameworks */,
380 | 7FD345A728803DEE00DDD1D5 /* Resources */,
381 | 7F8B66752893F23B00E2B108 /* CopyFiles */,
382 | );
383 | buildRules = (
384 | );
385 | dependencies = (
386 | 7F8B667B2893F27100E2B108 /* PBXTargetDependency */,
387 | 7F8B667D2893F28300E2B108 /* PBXTargetDependency */,
388 | 7F8B667F2893F2A200E2B108 /* PBXTargetDependency */,
389 | 7F8B66812893F2C700E2B108 /* PBXTargetDependency */,
390 | 7F8B66832893F32A00E2B108 /* PBXTargetDependency */,
391 | );
392 | name = Commander;
393 | packageProductDependencies = (
394 | );
395 | productName = Commander;
396 | productReference = 7FD345A928803DEE00DDD1D5 /* Commander.app */;
397 | productType = "com.apple.product-type.application";
398 | };
399 | /* End PBXNativeTarget section */
400 |
401 | /* Begin PBXProject section */
402 | 7FD345A128803DEE00DDD1D5 /* Project object */ = {
403 | isa = PBXProject;
404 | attributes = {
405 | BuildIndependentTargetsInParallel = 1;
406 | LastSwiftUpdateCheck = 1340;
407 | LastUpgradeCheck = 1410;
408 | TargetAttributes = {
409 | 7F8B66622893F14200E2B108 = {
410 | CreatedOnToolsVersion = 13.4;
411 | };
412 | 7FD345A828803DEE00DDD1D5 = {
413 | CreatedOnToolsVersion = 13.4;
414 | };
415 | };
416 | };
417 | buildConfigurationList = 7FD345A428803DEE00DDD1D5 /* Build configuration list for PBXProject "Commander" */;
418 | compatibilityVersion = "Xcode 13.0";
419 | developmentRegion = en;
420 | hasScannedForEncodings = 0;
421 | knownRegions = (
422 | en,
423 | Base,
424 | );
425 | mainGroup = 7FD345A028803DEE00DDD1D5;
426 | packageReferences = (
427 | );
428 | productRefGroup = 7FD345AA28803DEE00DDD1D5 /* Products */;
429 | projectDirPath = "";
430 | projectRoot = "";
431 | targets = (
432 | 7FD345A828803DEE00DDD1D5 /* Commander */,
433 | 7F8B66622893F14200E2B108 /* Launcher */,
434 | );
435 | };
436 | /* End PBXProject section */
437 |
438 | /* Begin PBXResourcesBuildPhase section */
439 | 7F8B66612893F14200E2B108 /* Resources */ = {
440 | isa = PBXResourcesBuildPhase;
441 | buildActionMask = 2147483647;
442 | files = (
443 | 7F8B666A2893F14200E2B108 /* Assets.xcassets in Resources */,
444 | );
445 | runOnlyForDeploymentPostprocessing = 0;
446 | };
447 | 7FD345A728803DEE00DDD1D5 /* Resources */ = {
448 | isa = PBXResourcesBuildPhase;
449 | buildActionMask = 2147483647;
450 | files = (
451 | 7F8B66792893F27100E2B108 /* Launcher.app in Resources */,
452 | 7FD345B128803DEF00DDD1D5 /* Assets.xcassets in Resources */,
453 | );
454 | runOnlyForDeploymentPostprocessing = 0;
455 | };
456 | /* End PBXResourcesBuildPhase section */
457 |
458 | /* Begin PBXSourcesBuildPhase section */
459 | 7F8B665F2893F14200E2B108 /* Sources */ = {
460 | isa = PBXSourcesBuildPhase;
461 | buildActionMask = 2147483647;
462 | files = (
463 | 7F8B66892893F43D00E2B108 /* AppDelegate.swift in Sources */,
464 | 7F8B668B2893F47100E2B108 /* main.swift in Sources */,
465 | 7F8B668D2893F4E200E2B108 /* NotificationName+Extension.swift in Sources */,
466 | );
467 | runOnlyForDeploymentPostprocessing = 0;
468 | };
469 | 7FD345A528803DEE00DDD1D5 /* Sources */ = {
470 | isa = PBXSourcesBuildPhase;
471 | buildActionMask = 2147483647;
472 | files = (
473 | 7FD345AF28803DEE00DDD1D5 /* CommanderView.swift in Sources */,
474 | 7F5D61E029CAE15900092688 /* AppMenu.swift in Sources */,
475 | 7F8481AF2880BE46002419E7 /* WheelPicker.swift in Sources */,
476 | 7FF08B1B2885DAE9000ED2AD /* WheelPickerHoverState.swift in Sources */,
477 | 7F8481AD2880A854002419E7 /* main.swift in Sources */,
478 | 7F8B66872893F40D00E2B108 /* NotificationName+Extension.swift in Sources */,
479 | 7FF08B1F288618BA000ED2AD /* Sequence+Extension.swift in Sources */,
480 | 7FAD55C9288956E2009A4020 /* BookmarksManager.swift in Sources */,
481 | 7FBDF3FF2880953C00173B7B /* ShortcutNotifier.swift in Sources */,
482 | 7FA07D4D29A1BEF900F32F1C /* SendFeedbackView.swift in Sources */,
483 | 7F2329C02A7CD57300378358 /* AppGroup.swift in Sources */,
484 | 7F8D9798289169FA0018AA2A /* UserDefault.swift in Sources */,
485 | 7F5A302228815C6300E3372D /* VisualEffectView.swift in Sources */,
486 | 7F8D9796289167690018AA2A /* AppSearcher.swift in Sources */,
487 | 7FF08B1728854FE4000ED2AD /* App.swift in Sources */,
488 | 7F5A302028815BF500E3372D /* TrackingAreaView.swift in Sources */,
489 | 7FA07D4F29A1C6D000F32F1C /* TextArea.swift in Sources */,
490 | 7F32BD2A2A946FE100CF4E22 /* AutoLaunchManager.swift in Sources */,
491 | 7FF08B10288544E0000ED2AD /* DIContainer.swift in Sources */,
492 | 7F5A30242881601300E3372D /* View+Extension.swift in Sources */,
493 | 7F8D9792289135CA0018AA2A /* ShortcutKeysView.swift in Sources */,
494 | 7F2329BE2A7BFE1C00378358 /* MenuBarItemManager.swift in Sources */,
495 | 7F8D9794289165A10018AA2A /* AppStorageKey.swift in Sources */,
496 | 7FF08B082881E636000ED2AD /* NSWindow+Extension.swift in Sources */,
497 | 7F8D979C2891731E0018AA2A /* CGPoint+Extension.swift in Sources */,
498 | 7F2F774029F402AC00BC5402 /* SettingsTutorialView.swift in Sources */,
499 | 7FC3E32129C4E9CB008EFC1B /* ShortcutsAppManager.swift in Sources */,
500 | 7FF08B1D2885DB94000ED2AD /* WheelPickerSection.swift in Sources */,
501 | 7FD345AD28803DEE00DDD1D5 /* AppDelegate.swift in Sources */,
502 | 7F8D979A28916EBB0018AA2A /* ImageProvider.swift in Sources */,
503 | 7F84C29929CB426D00316630 /* SettingsSidebarView.swift in Sources */,
504 | 7F4E6E722A9C5AE800BBAD9D /* DockIconManager.swift in Sources */,
505 | 7FF08B0B2881E6DD000ED2AD /* SettingsView.swift in Sources */,
506 | 7F5D61DE29CAC8F600092688 /* AppRatingManager.swift in Sources */,
507 | 7FF08B0E288544AC000ED2AD /* AppsManager.swift in Sources */,
508 | 7FAD55CC2889D8DC009A4020 /* URLInputView.swift in Sources */,
509 | 7F84C29D29CB614800316630 /* SettingsSearchField.swift in Sources */,
510 | );
511 | runOnlyForDeploymentPostprocessing = 0;
512 | };
513 | /* End PBXSourcesBuildPhase section */
514 |
515 | /* Begin PBXTargetDependency section */
516 | 7F8B667B2893F27100E2B108 /* PBXTargetDependency */ = {
517 | isa = PBXTargetDependency;
518 | target = 7F8B66622893F14200E2B108 /* Launcher */;
519 | targetProxy = 7F8B667A2893F27100E2B108 /* PBXContainerItemProxy */;
520 | };
521 | 7F8B667D2893F28300E2B108 /* PBXTargetDependency */ = {
522 | isa = PBXTargetDependency;
523 | target = 7F8B66622893F14200E2B108 /* Launcher */;
524 | targetProxy = 7F8B667C2893F28300E2B108 /* PBXContainerItemProxy */;
525 | };
526 | 7F8B667F2893F2A200E2B108 /* PBXTargetDependency */ = {
527 | isa = PBXTargetDependency;
528 | target = 7F8B66622893F14200E2B108 /* Launcher */;
529 | targetProxy = 7F8B667E2893F2A200E2B108 /* PBXContainerItemProxy */;
530 | };
531 | 7F8B66812893F2C700E2B108 /* PBXTargetDependency */ = {
532 | isa = PBXTargetDependency;
533 | target = 7F8B66622893F14200E2B108 /* Launcher */;
534 | targetProxy = 7F8B66802893F2C700E2B108 /* PBXContainerItemProxy */;
535 | };
536 | 7F8B66832893F32A00E2B108 /* PBXTargetDependency */ = {
537 | isa = PBXTargetDependency;
538 | target = 7F8B66622893F14200E2B108 /* Launcher */;
539 | targetProxy = 7F8B66822893F32A00E2B108 /* PBXContainerItemProxy */;
540 | };
541 | /* End PBXTargetDependency section */
542 |
543 | /* Begin XCBuildConfiguration section */
544 | 7F8B666F2893F14200E2B108 /* Debug */ = {
545 | isa = XCBuildConfiguration;
546 | buildSettings = {
547 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
548 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
549 | CODE_SIGN_ENTITLEMENTS = Launcher/Launcher.entitlements;
550 | CODE_SIGN_IDENTITY = "Apple Development";
551 | CODE_SIGN_STYLE = Automatic;
552 | COMBINE_HIDPI_IMAGES = YES;
553 | CURRENT_PROJECT_VERSION = "$(inherited)";
554 | DEAD_CODE_STRIPPING = YES;
555 | DEVELOPMENT_TEAM = E225268FKL;
556 | ENABLE_HARDENED_RUNTIME = YES;
557 | ENABLE_PREVIEWS = YES;
558 | GENERATE_INFOPLIST_FILE = YES;
559 | INFOPLIST_FILE = Launcher/Info.plist;
560 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
561 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
562 | LD_RUNPATH_SEARCH_PATHS = (
563 | "$(inherited)",
564 | "@executable_path/../Frameworks",
565 | );
566 | MARKETING_VERSION = "$(inherited)";
567 | PRODUCT_BUNDLE_IDENTIFIER = com.va.commander.launcher;
568 | PRODUCT_NAME = "$(TARGET_NAME)";
569 | PROVISIONING_PROFILE_SPECIFIER = "";
570 | SKIP_INSTALL = YES;
571 | SWIFT_EMIT_LOC_STRINGS = YES;
572 | SWIFT_VERSION = 5.0;
573 | };
574 | name = Debug;
575 | };
576 | 7F8B66702893F14200E2B108 /* Release */ = {
577 | isa = XCBuildConfiguration;
578 | buildSettings = {
579 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
580 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
581 | CODE_SIGN_ENTITLEMENTS = Launcher/Launcher.entitlements;
582 | CODE_SIGN_IDENTITY = "Apple Development";
583 | CODE_SIGN_STYLE = Automatic;
584 | COMBINE_HIDPI_IMAGES = YES;
585 | CURRENT_PROJECT_VERSION = "$(inherited)";
586 | DEAD_CODE_STRIPPING = YES;
587 | DEVELOPMENT_TEAM = E225268FKL;
588 | ENABLE_HARDENED_RUNTIME = YES;
589 | ENABLE_PREVIEWS = YES;
590 | GENERATE_INFOPLIST_FILE = YES;
591 | INFOPLIST_FILE = Launcher/Info.plist;
592 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
593 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
594 | LD_RUNPATH_SEARCH_PATHS = (
595 | "$(inherited)",
596 | "@executable_path/../Frameworks",
597 | );
598 | MARKETING_VERSION = "$(inherited)";
599 | PRODUCT_BUNDLE_IDENTIFIER = com.va.commander.launcher;
600 | PRODUCT_NAME = "$(TARGET_NAME)";
601 | PROVISIONING_PROFILE_SPECIFIER = "";
602 | SKIP_INSTALL = YES;
603 | SWIFT_EMIT_LOC_STRINGS = YES;
604 | SWIFT_VERSION = 5.0;
605 | };
606 | name = Release;
607 | };
608 | 7FD345B628803DEF00DDD1D5 /* Debug */ = {
609 | isa = XCBuildConfiguration;
610 | buildSettings = {
611 | ALWAYS_SEARCH_USER_PATHS = NO;
612 | CLANG_ANALYZER_NONNULL = YES;
613 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
614 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
615 | CLANG_ENABLE_MODULES = YES;
616 | CLANG_ENABLE_OBJC_ARC = YES;
617 | CLANG_ENABLE_OBJC_WEAK = YES;
618 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
619 | CLANG_WARN_BOOL_CONVERSION = YES;
620 | CLANG_WARN_COMMA = YES;
621 | CLANG_WARN_CONSTANT_CONVERSION = YES;
622 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
623 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
624 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
625 | CLANG_WARN_EMPTY_BODY = YES;
626 | CLANG_WARN_ENUM_CONVERSION = YES;
627 | CLANG_WARN_INFINITE_RECURSION = YES;
628 | CLANG_WARN_INT_CONVERSION = YES;
629 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
630 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
631 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
632 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
633 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
634 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
635 | CLANG_WARN_STRICT_PROTOTYPES = YES;
636 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
637 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
638 | CLANG_WARN_UNREACHABLE_CODE = YES;
639 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
640 | COPY_PHASE_STRIP = NO;
641 | CURRENT_PROJECT_VERSION = 26;
642 | DEAD_CODE_STRIPPING = YES;
643 | DEBUG_INFORMATION_FORMAT = dwarf;
644 | ENABLE_STRICT_OBJC_MSGSEND = YES;
645 | ENABLE_TESTABILITY = YES;
646 | GCC_C_LANGUAGE_STANDARD = gnu11;
647 | GCC_DYNAMIC_NO_PIC = NO;
648 | GCC_NO_COMMON_BLOCKS = YES;
649 | GCC_OPTIMIZATION_LEVEL = 0;
650 | GCC_PREPROCESSOR_DEFINITIONS = (
651 | "DEBUG=1",
652 | "$(inherited)",
653 | );
654 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
655 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
656 | GCC_WARN_UNDECLARED_SELECTOR = YES;
657 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
658 | GCC_WARN_UNUSED_FUNCTION = YES;
659 | GCC_WARN_UNUSED_VARIABLE = YES;
660 | MACOSX_DEPLOYMENT_TARGET = 12.0;
661 | MARKETING_VERSION = 1.14;
662 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
663 | MTL_FAST_MATH = YES;
664 | ONLY_ACTIVE_ARCH = YES;
665 | SDKROOT = macosx;
666 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
667 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
668 | };
669 | name = Debug;
670 | };
671 | 7FD345B728803DEF00DDD1D5 /* Release */ = {
672 | isa = XCBuildConfiguration;
673 | buildSettings = {
674 | ALWAYS_SEARCH_USER_PATHS = NO;
675 | CLANG_ANALYZER_NONNULL = YES;
676 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
677 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
678 | CLANG_ENABLE_MODULES = YES;
679 | CLANG_ENABLE_OBJC_ARC = YES;
680 | CLANG_ENABLE_OBJC_WEAK = YES;
681 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
682 | CLANG_WARN_BOOL_CONVERSION = YES;
683 | CLANG_WARN_COMMA = YES;
684 | CLANG_WARN_CONSTANT_CONVERSION = YES;
685 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
686 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
687 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
688 | CLANG_WARN_EMPTY_BODY = YES;
689 | CLANG_WARN_ENUM_CONVERSION = YES;
690 | CLANG_WARN_INFINITE_RECURSION = YES;
691 | CLANG_WARN_INT_CONVERSION = YES;
692 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
693 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
694 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
695 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
696 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
697 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
698 | CLANG_WARN_STRICT_PROTOTYPES = YES;
699 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
700 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
701 | CLANG_WARN_UNREACHABLE_CODE = YES;
702 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
703 | COPY_PHASE_STRIP = NO;
704 | CURRENT_PROJECT_VERSION = 26;
705 | DEAD_CODE_STRIPPING = YES;
706 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
707 | ENABLE_NS_ASSERTIONS = NO;
708 | ENABLE_STRICT_OBJC_MSGSEND = YES;
709 | GCC_C_LANGUAGE_STANDARD = gnu11;
710 | GCC_NO_COMMON_BLOCKS = YES;
711 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
712 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
713 | GCC_WARN_UNDECLARED_SELECTOR = YES;
714 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
715 | GCC_WARN_UNUSED_FUNCTION = YES;
716 | GCC_WARN_UNUSED_VARIABLE = YES;
717 | MACOSX_DEPLOYMENT_TARGET = 12.0;
718 | MARKETING_VERSION = 1.14;
719 | MTL_ENABLE_DEBUG_INFO = NO;
720 | MTL_FAST_MATH = YES;
721 | SDKROOT = macosx;
722 | SWIFT_COMPILATION_MODE = wholemodule;
723 | SWIFT_OPTIMIZATION_LEVEL = "-O";
724 | };
725 | name = Release;
726 | };
727 | 7FD345B928803DEF00DDD1D5 /* Debug */ = {
728 | isa = XCBuildConfiguration;
729 | buildSettings = {
730 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
731 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
732 | CODE_SIGN_ENTITLEMENTS = Commander/Commander.entitlements;
733 | CODE_SIGN_IDENTITY = "Apple Development";
734 | CODE_SIGN_STYLE = Automatic;
735 | COMBINE_HIDPI_IMAGES = YES;
736 | CURRENT_PROJECT_VERSION = "$(inherited)";
737 | DEAD_CODE_STRIPPING = YES;
738 | DEVELOPMENT_TEAM = E225268FKL;
739 | ENABLE_HARDENED_RUNTIME = YES;
740 | ENABLE_PREVIEWS = YES;
741 | GENERATE_INFOPLIST_FILE = YES;
742 | INFOPLIST_FILE = Commander/Info.plist;
743 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
744 | INFOPLIST_KEY_LSUIElement = YES;
745 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Vadim Ahmerov. All rights reserved.";
746 | LD_RUNPATH_SEARCH_PATHS = (
747 | "$(inherited)",
748 | "@executable_path/../Frameworks",
749 | );
750 | MACOSX_DEPLOYMENT_TARGET = 12.0;
751 | MARKETING_VERSION = "$(inherited)";
752 | PRODUCT_BUNDLE_IDENTIFIER = com.va.commander;
753 | PRODUCT_NAME = "$(TARGET_NAME)";
754 | PROVISIONING_PROFILE_SPECIFIER = "";
755 | SWIFT_EMIT_LOC_STRINGS = YES;
756 | SWIFT_VERSION = 5.0;
757 | };
758 | name = Debug;
759 | };
760 | 7FD345BA28803DEF00DDD1D5 /* Release */ = {
761 | isa = XCBuildConfiguration;
762 | buildSettings = {
763 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
764 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
765 | CODE_SIGN_ENTITLEMENTS = Commander/Commander.entitlements;
766 | CODE_SIGN_IDENTITY = "Apple Development";
767 | CODE_SIGN_STYLE = Automatic;
768 | COMBINE_HIDPI_IMAGES = YES;
769 | CURRENT_PROJECT_VERSION = "$(inherited)";
770 | DEAD_CODE_STRIPPING = YES;
771 | DEVELOPMENT_TEAM = E225268FKL;
772 | ENABLE_HARDENED_RUNTIME = YES;
773 | ENABLE_PREVIEWS = YES;
774 | GENERATE_INFOPLIST_FILE = YES;
775 | INFOPLIST_FILE = Commander/Info.plist;
776 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
777 | INFOPLIST_KEY_LSUIElement = YES;
778 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Vadim Ahmerov. All rights reserved.";
779 | LD_RUNPATH_SEARCH_PATHS = (
780 | "$(inherited)",
781 | "@executable_path/../Frameworks",
782 | );
783 | MACOSX_DEPLOYMENT_TARGET = 12.0;
784 | MARKETING_VERSION = "$(inherited)";
785 | PRODUCT_BUNDLE_IDENTIFIER = com.va.commander;
786 | PRODUCT_NAME = "$(TARGET_NAME)";
787 | PROVISIONING_PROFILE_SPECIFIER = "";
788 | SWIFT_EMIT_LOC_STRINGS = YES;
789 | SWIFT_VERSION = 5.0;
790 | };
791 | name = Release;
792 | };
793 | /* End XCBuildConfiguration section */
794 |
795 | /* Begin XCConfigurationList section */
796 | 7F8B66712893F14200E2B108 /* Build configuration list for PBXNativeTarget "Launcher" */ = {
797 | isa = XCConfigurationList;
798 | buildConfigurations = (
799 | 7F8B666F2893F14200E2B108 /* Debug */,
800 | 7F8B66702893F14200E2B108 /* Release */,
801 | );
802 | defaultConfigurationIsVisible = 0;
803 | defaultConfigurationName = Release;
804 | };
805 | 7FD345A428803DEE00DDD1D5 /* Build configuration list for PBXProject "Commander" */ = {
806 | isa = XCConfigurationList;
807 | buildConfigurations = (
808 | 7FD345B628803DEF00DDD1D5 /* Debug */,
809 | 7FD345B728803DEF00DDD1D5 /* Release */,
810 | );
811 | defaultConfigurationIsVisible = 0;
812 | defaultConfigurationName = Release;
813 | };
814 | 7FD345B828803DEF00DDD1D5 /* Build configuration list for PBXNativeTarget "Commander" */ = {
815 | isa = XCConfigurationList;
816 | buildConfigurations = (
817 | 7FD345B928803DEF00DDD1D5 /* Debug */,
818 | 7FD345BA28803DEF00DDD1D5 /* Release */,
819 | );
820 | defaultConfigurationIsVisible = 0;
821 | defaultConfigurationName = Release;
822 | };
823 | /* End XCConfigurationList section */
824 | };
825 | rootObject = 7FD345A128803DEE00DDD1D5 /* Project object */;
826 | }
827 |
--------------------------------------------------------------------------------