From e64beee8c1adf59db2a303add742973779afb461 Mon Sep 17 00:00:00 2001 From: Christoffer Winterkvist Date: Fri, 23 Oct 2020 11:14:50 +0200 Subject: [PATCH] Add documentation and make classes final - Add documentation to functions - Move ViewKit `typealias` to a separate file - Add `final` to classes used throughout the source code --- App/Sources/Application/AppDelegate.swift | 4 ++ .../AppDelegateErrorController.swift | 4 +- .../AppDelegateLaunchController.swift | 10 ++- App/Sources/Application/LaunchArgument.swift | 9 +++ .../Commands/CommandsFeatureController.swift | 10 +-- App/Sources/Configuration/Configuration.swift | 5 ++ .../FilePicker/FilePickerViewController.swift | 2 +- .../Groups/GroupsFeatureController.swift | 19 +++--- .../ApplicationsProvider.swift | 2 +- .../KeyboardShortcutsFeatureController.swift | 2 +- App/Sources/Menus/AppMenu.swift | 2 +- App/Sources/Menus/EditMenu.swift | 2 +- App/Sources/Menus/FileMenu.swift | 2 +- App/Sources/Menus/HelpMenu.swift | 2 +- App/Sources/Menus/MainMenu.swift | 2 +- App/Sources/Menus/WindowMenu.swift | 2 +- .../Permissions/PermissionsController.swift | 6 +- .../Search/SearchCommandsController.swift | 2 +- .../Search/SearchFeatureController.swift | 2 +- .../Search/SearchGroupsController.swift | 2 +- App/Sources/Search/SearchRootController.swift | 2 +- .../Search/SearchWorkflowController.swift | 2 +- .../Workflow/WorkflowFeatureController.swift | 2 +- .../ApplicationCommandController.swift | 2 +- .../Applications/ApplicationParser.swift | 18 ++++- .../Applications/WindowListProvider.swift | 6 +- .../Sources/Commands/CommandController.swift | 2 +- LogicFramework/Sources/Debug/Debug.swift | 2 +- .../Extensions/Identifiable+Extensions.swift | 65 +++++++++++++++---- .../Extensions/String+Extensions.swift | 11 ++++ .../Sources/Factories/ControllerFactory.swift | 2 +- .../FileIndex/FileIndexPatternsFactory.swift | 2 +- .../FileIndex/FileIndexerController.swift | 2 +- .../Sources/Groups/GroupsController.swift | 2 +- .../Sources/Logic/CoreController.swift | 6 +- .../Sources/Open/OpenCommandController.swift | 2 +- .../Scripting/AppleScriptController.swift | 2 +- .../Scripting/ShellScriptController.swift | 2 +- .../Workflows/WorkflowController.swift | 2 +- Shared/Keymapping/InputSourceController.swift | 2 +- Shared/Keymapping/KeyCodeMapper.swift | 2 +- ViewKit/Sources/Views/MainView/MainView.swift | 9 --- ViewKit/Sources/Views/Shared/Typealias.swift | 10 +++ 43 files changed, 178 insertions(+), 70 deletions(-) create mode 100644 ViewKit/Sources/Views/Shared/Typealias.swift diff --git a/App/Sources/Application/AppDelegate.swift b/App/Sources/Application/AppDelegate.swift index 338db80f..3a104436 100644 --- a/App/Sources/Application/AppDelegate.swift +++ b/App/Sources/Application/AppDelegate.swift @@ -25,6 +25,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, fileName: configuration.fileName) } + // MARK: Application life cycle + func applicationDidFinishLaunching(_ notification: Notification) { if launchArguments.isEnabled(.runningUnitTests) { return } if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != nil { return } @@ -50,6 +52,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, shouldOpenMainWindow = true } + // MARK: Private methods + private func createAndOpenWindow(_ coreController: CoreControlling) { let window = createMainWindow(coreController) window?.makeKeyAndOrderFront(NSApp) diff --git a/App/Sources/Application/AppDelegateErrorController.swift b/App/Sources/Application/AppDelegateErrorController.swift index 0e5de699..de4dedd0 100644 --- a/App/Sources/Application/AppDelegateErrorController.swift +++ b/App/Sources/Application/AppDelegateErrorController.swift @@ -1,6 +1,8 @@ import Cocoa -class AppDelegateErrorController { +final class AppDelegateErrorController { + /// Display an error using `NSAlert` + /// - Parameter error: The error that should be displayed static func handle(_ error: Error) { let alert = NSAlert() alert.messageText = error.localizedDescription diff --git a/App/Sources/Application/AppDelegateLaunchController.swift b/App/Sources/Application/AppDelegateLaunchController.swift index 54bcf97c..01d02739 100644 --- a/App/Sources/Application/AppDelegateLaunchController.swift +++ b/App/Sources/Application/AppDelegateLaunchController.swift @@ -2,13 +2,21 @@ import Cocoa import LaunchArguments import LogicFramework -class AppDelegateLaunchController { +final class AppDelegateLaunchController { let factory: ControllerFactory init(factory: ControllerFactory) { self.factory = factory } + /// Construct the applications `CoreController` and configure it using + /// launch arguments. + /// + /// - Parameter storageController: The storage controller that is used to load + /// information from disk. + /// - Throws: If the storage cannot be loaded properly, this method will throw + /// a `StorageControllingError` + /// - Returns: A controller that conforms to `CoreControlling` func initialLoad(storageController: StorageControlling) throws -> CoreControlling { let groups = try storageController.load() let groupsController = factory.groupsController(groups: groups) diff --git a/App/Sources/Application/LaunchArgument.swift b/App/Sources/Application/LaunchArgument.swift index 2be10d5c..6e6833f8 100644 --- a/App/Sources/Application/LaunchArgument.swift +++ b/App/Sources/Application/LaunchArgument.swift @@ -1,9 +1,18 @@ import LaunchArguments enum LaunchArgument: String, LaunchArgumentType { + // Used to avoid running the application when running unit tests case runningUnitTests = "-running-unit-tests" + // Determines if the main window should open at launch. + // Encourage during development to ease the development process + // while testing changes. case openWindowAtLaunch = "-open-window-at-launch" + // Disable setting up keyboard shortcut during development. case disableKeyboardShortcuts = "-disable-keyboard-shortcuts" + // When enabled, the application will use the bundled JSON file + // to display information. case demoMode = "-demo-mode" + // Will print information to the console while running the + // application. Great for debugging. case debug = "-debug" } diff --git a/App/Sources/Commands/CommandsFeatureController.swift b/App/Sources/Commands/CommandsFeatureController.swift index a4f29288..fc95631b 100644 --- a/App/Sources/Commands/CommandsFeatureController.swift +++ b/App/Sources/Commands/CommandsFeatureController.swift @@ -14,7 +14,7 @@ protocol CommandsFeatureControllerDelegate: AnyObject { didDeleteCommand command: Command, in workflow: Workflow) } -class CommandsFeatureController: ViewController { +final class CommandsFeatureController: ViewController { weak var delegate: CommandsFeatureControllerDelegate? @Published var state: [Command] let userSelection: UserSelection @@ -57,25 +57,25 @@ class CommandsFeatureController: ViewController { // MARK: Private methods - func createCommand(_ command: Command, in workflow: Workflow) { + private func createCommand(_ command: Command, in workflow: Workflow) { var workflow = workflow workflow.commands.append(command) delegate?.commandsFeatureController(self, didCreateCommand: command, in: workflow) } - func updateCommand(_ command: Command, in workflow: Workflow) { + private func updateCommand(_ command: Command, in workflow: Workflow) { var workflow = workflow try? workflow.commands.replace(command) delegate?.commandsFeatureController(self, didCreateCommand: command, in: workflow) } - func moveCommand(_ command: Command, to index: Int, in workflow: Workflow) { + private func moveCommand(_ command: Command, to index: Int, in workflow: Workflow) { var workflow = workflow try? workflow.commands.move(command, to: index) delegate?.commandsFeatureController(self, didUpdateCommand: command, in: workflow) } - func deleteCommand(_ command: Command, in workflow: Workflow) { + private func deleteCommand(_ command: Command, in workflow: Workflow) { var workflow = workflow try? workflow.commands.remove(command) delegate?.commandsFeatureController(self, didDeleteCommand: command, in: workflow) diff --git a/App/Sources/Configuration/Configuration.swift b/App/Sources/Configuration/Configuration.swift index 7a270b23..3281ab58 100644 --- a/App/Sources/Configuration/Configuration.swift +++ b/App/Sources/Configuration/Configuration.swift @@ -2,12 +2,17 @@ import Foundation struct Configuration { struct Storage { + /// The path to the configuration file var path: String { launchArguments.isEnabled(.demoMode) ? ProcessInfo.processInfo.environment["SOURCE_ROOT"]! : UserDefaults.standard.string(forKey: "configurationPath") ?? "~" } + /// Determines if the file name should use `.` as a prefix in order + /// to hide it in the Finder var hiddenFile: Bool = true + /// A computed variable that changes depending on `hiddenFile`. + /// The file name is either `.keyboard-cowboy.json` or `keyboard-cowboy.json` var fileName: String { (hiddenFile && !launchArguments.isEnabled(.demoMode)) ? ".keyboard-cowboy.json" diff --git a/App/Sources/FilePicker/FilePickerViewController.swift b/App/Sources/FilePicker/FilePickerViewController.swift index ac9a1364..297d934a 100644 --- a/App/Sources/FilePicker/FilePickerViewController.swift +++ b/App/Sources/FilePicker/FilePickerViewController.swift @@ -3,7 +3,7 @@ import LogicFramework import Combine import Cocoa -class OpenPanelViewController: NSObject, ViewController, NSOpenSavePanelDelegate { +final class OpenPanelViewController: NSObject, ViewController, NSOpenSavePanelDelegate { @Published var state: String = "" var fileExtension: String? diff --git a/App/Sources/Groups/GroupsFeatureController.swift b/App/Sources/Groups/GroupsFeatureController.swift index 59be635e..f3665a65 100644 --- a/App/Sources/Groups/GroupsFeatureController.swift +++ b/App/Sources/Groups/GroupsFeatureController.swift @@ -7,7 +7,7 @@ protocol GroupsFeatureControllerDelegate: AnyObject { func groupsFeatureController(_ controller: GroupsFeatureController, didReloadGroups groups: [Group]) } -class GroupsFeatureController: ViewController, WorkflowFeatureControllerDelegate { +final class GroupsFeatureController: ViewController, WorkflowFeatureControllerDelegate { weak var delegate: GroupsFeatureControllerDelegate? @Published var state = [Group]() @@ -59,9 +59,9 @@ class GroupsFeatureController: ViewController, WorkflowFeatureControllerDelegate let group = Group.empty() var groups = groupsController.groups groups.append(group) - reload(groups) { _ in - self.userSelection.group = group - self.userSelection.workflow = nil + reload(groups) { [weak self] _ in + self?.userSelection.group = group + self?.userSelection.workflow = nil } } @@ -73,9 +73,9 @@ class GroupsFeatureController: ViewController, WorkflowFeatureControllerDelegate var groups = groupsController.groups let group = Group.droppedApplication(application) groups.append(group) - reload(groups) { _ in - self.userSelection.group = group - self.userSelection.workflow = nil + reload(groups) { [weak self] _ in + self?.userSelection.group = group + self?.userSelection.workflow = nil } } @@ -97,8 +97,9 @@ class GroupsFeatureController: ViewController, WorkflowFeatureControllerDelegate private func save(_ group: ModelKit.Group) { var groups = groupsController.groups try? groups.replace(group) - reload(groups) - userSelection.group = group + reload(groups) { [weak self] _ in + self?.userSelection.group = group + } } private func delete(_ group: ModelKit.Group) { diff --git a/App/Sources/InstalledApplications/ApplicationsProvider.swift b/App/Sources/InstalledApplications/ApplicationsProvider.swift index 0a75d9ee..254ee2af 100644 --- a/App/Sources/InstalledApplications/ApplicationsProvider.swift +++ b/App/Sources/InstalledApplications/ApplicationsProvider.swift @@ -3,7 +3,7 @@ import LogicFramework import Combine import ModelKit -class ApplicationsProvider: StateController { +final class ApplicationsProvider: StateController { @Published var state: [Application] = [] init(applications: [Application]) { diff --git a/App/Sources/KeyboardShortcuts/KeyboardShortcutsFeatureController.swift b/App/Sources/KeyboardShortcuts/KeyboardShortcutsFeatureController.swift index 28c0d6d3..38c7edb2 100644 --- a/App/Sources/KeyboardShortcuts/KeyboardShortcutsFeatureController.swift +++ b/App/Sources/KeyboardShortcuts/KeyboardShortcutsFeatureController.swift @@ -18,7 +18,7 @@ protocol KeyboardShortcutsFeatureControllerDelegate: AnyObject { } -class KeyboardShortcutsFeatureController: ViewController { +final class KeyboardShortcutsFeatureController: ViewController { weak var delegate: KeyboardShortcutsFeatureControllerDelegate? @Published var state: [KeyboardShortcut] diff --git a/App/Sources/Menus/AppMenu.swift b/App/Sources/Menus/AppMenu.swift index ee0e6d3f..81eb8d87 100644 --- a/App/Sources/Menus/AppMenu.swift +++ b/App/Sources/Menus/AppMenu.swift @@ -1,6 +1,6 @@ import Cocoa -class AppMenu: NSMenu { +final class AppMenu: NSMenu { override init(title: String) { super.init(title: title) items = [ diff --git a/App/Sources/Menus/EditMenu.swift b/App/Sources/Menus/EditMenu.swift index 891ee7ea..e170900a 100644 --- a/App/Sources/Menus/EditMenu.swift +++ b/App/Sources/Menus/EditMenu.swift @@ -1,6 +1,6 @@ import Cocoa -class EditMenu: NSMenuItem { +final class EditMenu: NSMenuItem { init() { super.init(title: "", action: nil, keyEquivalent: "") submenu = NSMenu(title: "Edit") diff --git a/App/Sources/Menus/FileMenu.swift b/App/Sources/Menus/FileMenu.swift index 18585d89..e367fdfe 100644 --- a/App/Sources/Menus/FileMenu.swift +++ b/App/Sources/Menus/FileMenu.swift @@ -1,6 +1,6 @@ import Cocoa -class FileMenu: NSMenuItem { +final class FileMenu: NSMenuItem { init() { super.init(title: "", action: nil, keyEquivalent: "") submenu = NSMenu(title: "File") diff --git a/App/Sources/Menus/HelpMenu.swift b/App/Sources/Menus/HelpMenu.swift index ee5f7e42..3ad1ed05 100644 --- a/App/Sources/Menus/HelpMenu.swift +++ b/App/Sources/Menus/HelpMenu.swift @@ -1,6 +1,6 @@ import Cocoa -class HelpMenu: NSMenuItem { +final class HelpMenu: NSMenuItem { init() { super.init(title: "", action: nil, keyEquivalent: "") submenu = NSMenu(title: "Help") diff --git a/App/Sources/Menus/MainMenu.swift b/App/Sources/Menus/MainMenu.swift index b64441c1..d62d31d2 100644 --- a/App/Sources/Menus/MainMenu.swift +++ b/App/Sources/Menus/MainMenu.swift @@ -1,6 +1,6 @@ import Cocoa -class MainMenu: NSMenuItem { +final class MainMenu: NSMenuItem { private lazy var applicationName = ProcessInfo.processInfo.processName init() { diff --git a/App/Sources/Menus/WindowMenu.swift b/App/Sources/Menus/WindowMenu.swift index 41d293ca..900c92bb 100644 --- a/App/Sources/Menus/WindowMenu.swift +++ b/App/Sources/Menus/WindowMenu.swift @@ -1,6 +1,6 @@ import Cocoa -class WindowMenu: NSMenuItem { +final class WindowMenu: NSMenuItem { init() { super.init(title: "", action: nil, keyEquivalent: "") submenu = NSMenu(title: "Window") diff --git a/App/Sources/Permissions/PermissionsController.swift b/App/Sources/Permissions/PermissionsController.swift index 3fa7674c..17331092 100644 --- a/App/Sources/Permissions/PermissionsController.swift +++ b/App/Sources/Permissions/PermissionsController.swift @@ -4,7 +4,9 @@ protocol PermissionsControlling { func hasPrivileges() -> Bool } -class PermissionsController: PermissionsControlling { +final class PermissionsController: PermissionsControlling { + /// Check if the application has the permissions to use accessiblity + /// - Returns: True if the application has been granted permissions. func hasPrivileges() -> Bool { let options: [String: Bool] = [ kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true @@ -12,6 +14,8 @@ class PermissionsController: PermissionsControlling { return AXIsProcessTrustedWithOptions(options as CFDictionary) } + /// Display modal message using `NSAlert` and ask the user to provide + /// accessibility permissions for the application func displayModal() { let applicationName = ProcessInfo.processInfo.processName let alert = NSAlert() diff --git a/App/Sources/Search/SearchCommandsController.swift b/App/Sources/Search/SearchCommandsController.swift index b8fc8e93..062a7a74 100644 --- a/App/Sources/Search/SearchCommandsController.swift +++ b/App/Sources/Search/SearchCommandsController.swift @@ -4,7 +4,7 @@ import ViewKit import SwiftUI import Combine -class SearchCommandsController: StateController { +final class SearchCommandsController: StateController { @Published var state = ModelKit.SearchResult.commands([]) let searchWorkflowController: SearchWorkflowController var query: String = "" diff --git a/App/Sources/Search/SearchFeatureController.swift b/App/Sources/Search/SearchFeatureController.swift index 74a74f60..efedb09b 100644 --- a/App/Sources/Search/SearchFeatureController.swift +++ b/App/Sources/Search/SearchFeatureController.swift @@ -4,7 +4,7 @@ import ViewKit import SwiftUI import Combine -class SearchFeatureController: ViewController { +final class SearchFeatureController: ViewController { @Published var state = ModelKit.SearchResults.empty() let searchController: SearchRootController var userSelection: UserSelection diff --git a/App/Sources/Search/SearchGroupsController.swift b/App/Sources/Search/SearchGroupsController.swift index 94476765..81eca1b7 100644 --- a/App/Sources/Search/SearchGroupsController.swift +++ b/App/Sources/Search/SearchGroupsController.swift @@ -2,6 +2,6 @@ import Foundation import ModelKit import ViewKit -class SearchGroupsController: StateController { +final class SearchGroupsController: StateController { @Published var state = ModelKit.SearchResult.groups([]) } diff --git a/App/Sources/Search/SearchRootController.swift b/App/Sources/Search/SearchRootController.swift index 97a07ba6..b55a9230 100644 --- a/App/Sources/Search/SearchRootController.swift +++ b/App/Sources/Search/SearchRootController.swift @@ -5,7 +5,7 @@ import ViewKit import SwiftUI import Combine -class SearchRootController { +final class SearchRootController { @Published var state: ModelKit.SearchResults = .empty() let commandSearch: SearchCommandsController let groupSearch: SearchGroupsController diff --git a/App/Sources/Search/SearchWorkflowController.swift b/App/Sources/Search/SearchWorkflowController.swift index 043c12c8..cb612c7e 100644 --- a/App/Sources/Search/SearchWorkflowController.swift +++ b/App/Sources/Search/SearchWorkflowController.swift @@ -4,7 +4,7 @@ import ViewKit import SwiftUI import Combine -class SearchWorkflowController: StateController { +final class SearchWorkflowController: StateController { @Published var state = ModelKit.SearchResult.workflows([]) let searchGroupController: SearchGroupsController var anyCancellables = [AnyCancellable]() diff --git a/App/Sources/Workflow/WorkflowFeatureController.swift b/App/Sources/Workflow/WorkflowFeatureController.swift index ddbbba03..fb9a704d 100644 --- a/App/Sources/Workflow/WorkflowFeatureController.swift +++ b/App/Sources/Workflow/WorkflowFeatureController.swift @@ -19,7 +19,7 @@ protocol WorkflowFeatureControllerDelegate: AnyObject { in group: Group) } -class WorkflowFeatureController: ViewController, +final class WorkflowFeatureController: ViewController, CommandsFeatureControllerDelegate, KeyboardShortcutsFeatureControllerDelegate { weak var delegate: WorkflowFeatureControllerDelegate? diff --git a/LogicFramework/Sources/Applications/ApplicationCommandController.swift b/LogicFramework/Sources/Applications/ApplicationCommandController.swift index 12363a33..4d761901 100644 --- a/LogicFramework/Sources/Applications/ApplicationCommandController.swift +++ b/LogicFramework/Sources/Applications/ApplicationCommandController.swift @@ -20,7 +20,7 @@ public enum ApplicationCommandControllingError: Error { case failedToActivate(ApplicationCommand) } -class ApplicationCommandController: ApplicationCommandControlling { +final class ApplicationCommandController: ApplicationCommandControlling { let windowListProvider: WindowListProviding let workspace: WorkspaceProviding diff --git a/LogicFramework/Sources/Applications/ApplicationParser.swift b/LogicFramework/Sources/Applications/ApplicationParser.swift index 2e52e491..5b6b8838 100644 --- a/LogicFramework/Sources/Applications/ApplicationParser.swift +++ b/LogicFramework/Sources/Applications/ApplicationParser.swift @@ -2,7 +2,15 @@ import Cocoa import Foundation import ModelKit -class ApplicationParser { +final class ApplicationParser { + /// Resolve an `Application` model from an application at a certain url. + /// + /// Parsing is done by invoking `Bundle(url:)` and verifying the contents + /// of the applications property list. + /// + /// - Parameter url: The url of the application + /// - Returns: A `Application` if all the validation critieras are met, otherwise + /// if will simply return `nil` func process(_ url: URL) -> Application? { guard let bundle = Bundle(url: url), let bundleIdentifier = bundle.bundleIdentifier, @@ -27,6 +35,14 @@ class ApplicationParser { path: bundle.bundlePath) } + /// Verify existence of certain keys, only one of the keys must match + /// the dictionary that is used as the subject for validation. + /// + /// - Parameters: + /// - dictionary: The dictionary that should be validated + /// - keys: An array of keys that should be used for validation + /// - Returns: Only one of the keys must match the dictionary in order for the method + /// to return true private func checkDictionary(dictionary: [String: Any], for keys: [String]) -> Bool { let lhs = Set(dictionary.keys) let rhs = Set(keys) diff --git a/LogicFramework/Sources/Applications/WindowListProvider.swift b/LogicFramework/Sources/Applications/WindowListProvider.swift index 7507a082..4072082d 100644 --- a/LogicFramework/Sources/Applications/WindowListProvider.swift +++ b/LogicFramework/Sources/Applications/WindowListProvider.swift @@ -4,7 +4,11 @@ public protocol WindowListProviding { func windowOwners() -> [String] } -class WindowListProvider: WindowListProviding { +final class WindowListProvider: WindowListProviding { + /// Get a list of owners based on the currently open windows. + /// + /// - Returns: A collection of window names, the window names are the bundle + /// names of the window owner. func windowOwners() -> [String] { let info = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] ?? [] return info.filter { diff --git a/LogicFramework/Sources/Commands/CommandController.swift b/LogicFramework/Sources/Commands/CommandController.swift index dd0e063a..6ff3b750 100644 --- a/LogicFramework/Sources/Commands/CommandController.swift +++ b/LogicFramework/Sources/Commands/CommandController.swift @@ -25,7 +25,7 @@ public enum CommandControllerError: Error { case failedToRunCommand(Error) } -public class CommandController: CommandControlling { +public final class CommandController: CommandControlling { weak public var delegate: CommandControllingDelegate? let applicationCommandController: ApplicationCommandControlling diff --git a/LogicFramework/Sources/Debug/Debug.swift b/LogicFramework/Sources/Debug/Debug.swift index 1132f802..bcaed641 100644 --- a/LogicFramework/Sources/Debug/Debug.swift +++ b/LogicFramework/Sources/Debug/Debug.swift @@ -1,6 +1,6 @@ import Foundation -public class Debug { +public final class Debug { public static var isEnabled: Bool = false public static func print(_ statement: String, diff --git a/LogicFramework/Sources/Extensions/Identifiable+Extensions.swift b/LogicFramework/Sources/Extensions/Identifiable+Extensions.swift index 39e4af9f..060a3596 100644 --- a/LogicFramework/Sources/Extensions/Identifiable+Extensions.swift +++ b/LogicFramework/Sources/Extensions/Identifiable+Extensions.swift @@ -10,14 +10,34 @@ public extension Array where Element: Identifiable { first(where: { $0.id == element.id }) != nil ? true : false } + /// Add an `Identifiable` element to the collection. + /// The index is constrained to the collection size. + /// If the index is less than `0`, it will be prepended at index `0`. + /// And if the index is larger than the collection, the element will + /// be appended. + /// + /// - Parameters: + /// - element: The `Identifiable` element subject that should be + /// added to the collection + /// - index: The desired index where the element should be inserted. mutating func add(_ element: Element, at index: Int = 0) { if index < count { - self.insert(element, at: index) + self.insert(element, at: Swift.max(index, 0)) } else { self.append(element) } } + /// Replace an `Identifiable` element inside the collection. + /// + /// Uniqueness is determined by the `.id` of the `Identifiable` element. + /// The function will locate the index using `.id` and then replace + /// the existing element with the new one using subscripting. + /// + /// - Parameter element: The `Identifiable` element subject that should be + /// used as the replacement. + /// - Throws: The function will throw if the element does not exist inside + /// the collection. mutating func replace(_ element: Element) throws { guard let index = self.firstIndex(where: { $0.id == element.id }) else { throw IdentifiableCollectionError.unableToFindElement @@ -26,25 +46,48 @@ public extension Array where Element: Identifiable { self[index] = element } - mutating func remove(_ element: Element) throws { - guard let index = self.firstIndex(where: { $0.id == element.id }) else { + /// Remove an `Identifiable` element inside the collection. + /// + /// Uniqueness is determined by the `.id` of the `Identifiable` element. + /// The function will locate the index using `.id` and then remove it + /// by invoking `.remove(at:)`. + /// + /// - Parameter element: The `Identifiable` element subject that should be + /// be removed from the collection + /// - Parameter index: An optional index that will be used for removing the + /// element. If `nil` is passed into the function, then + /// it will try and determine the index using + /// `firstIndex(where: { $0.id == element.id })` + /// - Throws: The function will throw if the element does not exist inside + /// the collection. + mutating func remove(_ element: Element, at index: Int? = nil) throws { + var targetIndex: Int + + if let index = index { + targetIndex = index + } else if let index = self.firstIndex(where: { $0.id == element.id }) { + targetIndex = index + } else { throw IdentifiableCollectionError.unableToFindElement } - self.remove(at: index) + self.remove(at: targetIndex) } + /// Move an `Identifiable` element inside the collection to a new index. + /// + /// - Parameters: + /// - element: The `Identifiable` element subject that should be + /// moved to a new index inside the collection. + /// - to: The new index of the element + /// - Throws: The function will throw if the element does not exist inside + /// the collection. mutating func move(_ element: Element, to: Int) throws { guard let index = self.firstIndex(where: { $0.id == element.id }) else { throw IdentifiableCollectionError.unableToFindElement } - _ = self.remove(at: index) - - if to > count { - append(element) - } else { - insert(element, at: to) - } + try self.remove(element, at: index) + self.insert(element, at: to) } } diff --git a/LogicFramework/Sources/Extensions/String+Extensions.swift b/LogicFramework/Sources/Extensions/String+Extensions.swift index a7260424..e61d961f 100644 --- a/LogicFramework/Sources/Extensions/String+Extensions.swift +++ b/LogicFramework/Sources/Extensions/String+Extensions.swift @@ -7,12 +7,23 @@ extension String { self = _sanitizePath() } + /// Expand the tile character used in the path & replace any escaped spaces + /// + /// - Returns: A new string that expanded and has no escaped whitespace private func _sanitizePath() -> String { var path = (self as NSString).expandingTildeInPath path = path.replacingOccurrences(of: "", with: "\\ ") return path } + /// Check if the current string contains a subject string. + /// + /// `.lowercased` is applied to both subjects in order to ensure + /// case-insensitivity. + /// + /// - Parameter subject: The string that should be used as the argument + /// for `contains()` + /// - Returns: True if self contains the subject string. public func containsCaseSensitive(_ subject: String) -> Bool { self.lowercased().contains(subject.lowercased()) } diff --git a/LogicFramework/Sources/Factories/ControllerFactory.swift b/LogicFramework/Sources/Factories/ControllerFactory.swift index 22613378..c5ca7e43 100644 --- a/LogicFramework/Sources/Factories/ControllerFactory.swift +++ b/LogicFramework/Sources/Factories/ControllerFactory.swift @@ -1,7 +1,7 @@ import Cocoa import ModelKit -public class ControllerFactory { +public final class ControllerFactory { private let _keycodeMapper = KeyCodeMapper.shared private let _groupsController = GroupsController(groups: []) diff --git a/LogicFramework/Sources/FileIndex/FileIndexPatternsFactory.swift b/LogicFramework/Sources/FileIndex/FileIndexPatternsFactory.swift index b60055bf..72bbdaf8 100644 --- a/LogicFramework/Sources/FileIndex/FileIndexPatternsFactory.swift +++ b/LogicFramework/Sources/FileIndex/FileIndexPatternsFactory.swift @@ -1,6 +1,6 @@ import Foundation -public class FileIndexPatternsFactory { +public final class FileIndexPatternsFactory { // swiftlint:disable function_body_length public static func patterns() -> [FileIndexPattern] { [ diff --git a/LogicFramework/Sources/FileIndex/FileIndexerController.swift b/LogicFramework/Sources/FileIndex/FileIndexerController.swift index 98466f10..25bf1378 100644 --- a/LogicFramework/Sources/FileIndex/FileIndexerController.swift +++ b/LogicFramework/Sources/FileIndex/FileIndexerController.swift @@ -38,7 +38,7 @@ public enum FileIndexPattern { /// The class supports both synchronous and asynchronous indexing. /// Asynchronousity is achieved using Combine. /// -public class FileIndexController: FileIndexControlling { +public final class FileIndexController: FileIndexControlling { let baseUrl: URL public init(baseUrl: URL) { diff --git a/LogicFramework/Sources/Groups/GroupsController.swift b/LogicFramework/Sources/Groups/GroupsController.swift index 26337e67..6400a4d8 100644 --- a/LogicFramework/Sources/Groups/GroupsController.swift +++ b/LogicFramework/Sources/Groups/GroupsController.swift @@ -22,7 +22,7 @@ public protocol GroupsControllingDelegate: AnyObject { func groupsController(_ controller: GroupsControlling, didReloadGroups groups: [Group]) } -class GroupsController: GroupsControlling { +final class GroupsController: GroupsControlling { weak var delegate: GroupsControllingDelegate? var groups: [Group] diff --git a/LogicFramework/Sources/Logic/CoreController.swift b/LogicFramework/Sources/Logic/CoreController.swift index 911c563f..c2e6e7c0 100644 --- a/LogicFramework/Sources/Logic/CoreController.swift +++ b/LogicFramework/Sources/Logic/CoreController.swift @@ -12,9 +12,9 @@ public protocol CoreControlling: AnyObject { func respond(to keyboardShortcut: KeyboardShortcut) -> [Workflow] } -public class CoreController: NSObject, CoreControlling, - CommandControllingDelegate, - GroupsControllingDelegate { +public final class CoreController: NSObject, CoreControlling, + CommandControllingDelegate, + GroupsControllingDelegate { let commandController: CommandControlling public let groupsController: GroupsControlling let keycodeMapper: KeyCodeMapping diff --git a/LogicFramework/Sources/Open/OpenCommandController.swift b/LogicFramework/Sources/Open/OpenCommandController.swift index 3853957d..22c4db54 100644 --- a/LogicFramework/Sources/Open/OpenCommandController.swift +++ b/LogicFramework/Sources/Open/OpenCommandController.swift @@ -21,7 +21,7 @@ public enum OpenCommandControllingError: Error { case failedToOpenUrl } -class OpenCommandController: OpenCommandControlling { +final class OpenCommandController: OpenCommandControlling { let workspace: WorkspaceProviding init(workspace: WorkspaceProviding) { diff --git a/LogicFramework/Sources/Scripting/AppleScriptController.swift b/LogicFramework/Sources/Scripting/AppleScriptController.swift index 65d831ee..c938edec 100644 --- a/LogicFramework/Sources/Scripting/AppleScriptController.swift +++ b/LogicFramework/Sources/Scripting/AppleScriptController.swift @@ -23,7 +23,7 @@ enum AppleScriptControllingError: Error { case failedToRunAppleScript(String?) } -class AppleScriptController: AppleScriptControlling { +final class AppleScriptController: AppleScriptControlling { let queue: DispatchQueue = .init(label: "com.zenangst.Keyboard-Cowboy.AppleScriptControllerQueue", qos: .userInitiated) diff --git a/LogicFramework/Sources/Scripting/ShellScriptController.swift b/LogicFramework/Sources/Scripting/ShellScriptController.swift index 2cc8a8ed..bedc4886 100644 --- a/LogicFramework/Sources/Scripting/ShellScriptController.swift +++ b/LogicFramework/Sources/Scripting/ShellScriptController.swift @@ -22,7 +22,7 @@ public protocol ShellScriptControlling { func run(_ source: ScriptCommand.Source) -> CommandPublisher } -class ShellScriptController: ShellScriptControlling { +final class ShellScriptController: ShellScriptControlling { let shellPath: String = "/bin/bash" func run(_ source: ScriptCommand.Source) -> CommandPublisher { diff --git a/LogicFramework/Sources/Workflows/WorkflowController.swift b/LogicFramework/Sources/Workflows/WorkflowController.swift index 1a0ec440..5ec322c3 100644 --- a/LogicFramework/Sources/Workflows/WorkflowController.swift +++ b/LogicFramework/Sources/Workflows/WorkflowController.swift @@ -22,7 +22,7 @@ public protocol WorkflowControlling { func filterWorkflows(from groups: [Group], keyboardShortcuts: [KeyboardShortcut]) -> [Workflow] } -class WorkflowController: WorkflowControlling { +final class WorkflowController: WorkflowControlling { init() {} public func filterWorkflows(from groups: [Group], keyboardShortcuts: [KeyboardShortcut]) -> [Workflow] { diff --git a/Shared/Keymapping/InputSourceController.swift b/Shared/Keymapping/InputSourceController.swift index caedc811..458c4320 100644 --- a/Shared/Keymapping/InputSourceController.swift +++ b/Shared/Keymapping/InputSourceController.swift @@ -3,7 +3,7 @@ import Foundation /// Based on: https://github.com/Clipy/Sauce -class InputSourceController { +final class InputSourceController { func currentInputSource() -> InputSource { InputSource(source: TISCopyCurrentKeyboardInputSource().takeUnretainedValue()) } diff --git a/Shared/Keymapping/KeyCodeMapper.swift b/Shared/Keymapping/KeyCodeMapper.swift index 1456a26f..0fa0c33f 100644 --- a/Shared/Keymapping/KeyCodeMapper.swift +++ b/Shared/Keymapping/KeyCodeMapper.swift @@ -10,7 +10,7 @@ public enum KeyCodeMappingError: Error { case unableToMapKeyCode(Int) } -class KeyCodeMapper: KeyCodeMapping { +final class KeyCodeMapper: KeyCodeMapping { let inputSource: InputSource let inputController: InputSourceController static var inputController = InputSourceController() diff --git a/ViewKit/Sources/Views/MainView/MainView.swift b/ViewKit/Sources/Views/MainView/MainView.swift index 26dac1f2..70cdf4e9 100644 --- a/ViewKit/Sources/Views/MainView/MainView.swift +++ b/ViewKit/Sources/Views/MainView/MainView.swift @@ -1,15 +1,6 @@ import SwiftUI import ModelKit -public typealias GroupController = AnyViewController<[ModelKit.Group], GroupList.Action> -public typealias WorkflowController = AnyViewController -public typealias CommandController = AnyViewController<[Command], CommandListView.Action> -public typealias OpenPanelController = AnyViewController -public typealias KeyboardShortcutController = AnyViewController<[ModelKit.KeyboardShortcut], - KeyboardShortcutListView.Action> -public typealias ApplicationProvider = AnyStateController<[Application]> -public typealias SearchController = AnyViewController - public struct MainView: View { @ObservedObject var applicationProvider: ApplicationProvider @ObservedObject var commandController: CommandController diff --git a/ViewKit/Sources/Views/Shared/Typealias.swift b/ViewKit/Sources/Views/Shared/Typealias.swift new file mode 100644 index 00000000..667e8b93 --- /dev/null +++ b/ViewKit/Sources/Views/Shared/Typealias.swift @@ -0,0 +1,10 @@ +import ModelKit + +public typealias ApplicationProvider = AnyStateController<[Application]> +public typealias CommandController = AnyViewController<[Command], CommandListView.Action> +public typealias GroupController = AnyViewController<[ModelKit.Group], GroupList.Action> +public typealias KeyboardShortcutController = AnyViewController<[ModelKit.KeyboardShortcut], + KeyboardShortcutListView.Action> +public typealias OpenPanelController = AnyViewController +public typealias SearchController = AnyViewController +public typealias WorkflowController = AnyViewController