├── 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 | Logo 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 | 1440x900bb@2x screenshot_2 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 | --------------------------------------------------------------------------------