Skip to content

Commit

Permalink
Bulked up fixes to the release
Browse files Browse the repository at this point in the history
- Fix cache identifiers
- Expose `run` on `CommandEngine`
- Move the keyboard engine error definitions outside of the class
- Move the apple script plugin error definitions outside of the class
- Set notification default to false on `KeyboardCommand`
- Set defaults for `ScriptCommand`'s
- Set notification default to false on `TypeCommand`
- Add editor command to the default configuration (either Xcode or TextEdit)
- Add additional application commands (Terminal, Safari) to the default configuration
- Add example Apple Script command to show how to use Apple Script to open a specific
  note in the Notes.app
- Add additional open commands (Documents, Downloads)
- Add rebinding commands to add Vim binding navigation
- Add imdb.com url command
- Add type command that is scoped to Mail to add a mail signature
- Add support for running commands in the `DetailCommandActionReducer`
- Implement basic error handling when running commands using the `DetailCommandActionReducer`
  by forwarding the errors to `NSAlert`
- Add new UI for the "pick configuration" screen
- Pass the same namespace between loading and pick-configuration view
  for a nice transition between states
- Fix visual bugs and nit-picky design improvements in the content list
- Fix bug with using `.focusSection` in the `EditableKeyboardShortcutView`
- Remove redundant `.onAppear` implementation in the `GroupsListView`
  • Loading branch information
zenangst committed May 24, 2023
1 parent bb46a83 commit 98c4bd1
Show file tree
Hide file tree
Showing 19 changed files with 318 additions and 92 deletions.
10 changes: 5 additions & 5 deletions App/Sources/App/KeyboardCowboy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,19 @@ struct KeyboardCowboy: App {
.environmentObject(OpenPanelController())
.matchedGeometryEffect(id: "content-window", in: namespace)
case .loading:
AppLoadingView()
.frame(width: 480, height: 360)
AppLoadingView(namespace: namespace)
.frame(width: 560, height: 380)
.matchedGeometryEffect(id: "content-window", in: namespace)
case .noConfiguration:
EmptyConfigurationView {
EmptyConfigurationView(namespace) {
contentStore.handle($0)
}
.matchedGeometryEffect(id: "content-window", in: namespace)
.frame(width: 480, height: 360)
.frame(width: 560, height: 380)
.animation(.none, value: contentStore.state)
}
}
.animation(.spring(), value: contentStore.state)
.animation(.spring(response: 0.3, dampingFraction: 0.65, blendDuration: 0.2), value: contentStore.state)
}
.windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar)
Expand Down
6 changes: 3 additions & 3 deletions App/Sources/Controllers/IconCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ final class IconCache {
// MARK: Private methods

private func load(_ identifier: String) async throws -> NSImage? {
let url = try applicationCacheDirectory().appending(component: "\(identifier).tiff")
let url = try applicationCacheDirectory().appending(component: identifier)

if FileManager.default.fileExists(atPath: url.path()) {
return NSImage(contentsOf: url)
Expand All @@ -64,7 +64,7 @@ final class IconCache {
}

private func save(_ image: NSImage, identifier: String) async throws {
let url = try applicationCacheDirectory().appending(component: "\(identifier).tiff")
let url = try applicationCacheDirectory().appending(component: identifier)

guard let tiff = image.tiffRepresentation else {
throw IconCacheError.unableToObtainTiffRepresentation
Expand Down Expand Up @@ -100,5 +100,5 @@ final class IconCache {
}

private extension CGSize {
var suffix: String { "\(width)x\(height)" }
var suffix: String { "\(Int(width))x\(Int(height))" }
}
2 changes: 1 addition & 1 deletion App/Sources/Engines/CommandEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ final class CommandEngine {
}
}

private func run(_ command: Command) async throws {
func run(_ command: Command) async throws {
if command.notification {
await MainActor.run {
lastExecutedCommand = command
Expand Down
13 changes: 7 additions & 6 deletions App/Sources/Engines/KeyboardEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import MachPort
import CoreGraphics
import KeyCodes

enum KeyboardEngineError: Error {
case failedToResolveMachPortController
case failedToResolveKey(String)
case failedToCreateKeyCode(Int)
case failedToCreateEvent
}

final class KeyboardEngine {
enum KeyboardEngineError: Error {
case failedToResolveMachPortController
case failedToResolveKey(String)
case failedToCreateKeyCode(Int)
case failedToCreateEvent
}

var machPort: MachPortEventController?
let store: KeyCodesStore
Expand Down
13 changes: 7 additions & 6 deletions App/Sources/Engines/Scripting/Plugins/AppleScriptPlugin.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Combine
import Cocoa

enum AppleScriptPluginError: Error {
case failedToCreateInlineScript
case failedToCreateScriptAtURL(URL)
case compileFailed(Error)
case executionFailed(Error)
}

final class AppleScriptPlugin {
enum AppleScriptPluginError: Error {
case failedToCreateInlineScript
case failedToCreateScriptAtURL(URL)
case compileFailed(Error)
case executionFailed(Error)
}

private let bundleIdentifier = Bundle.main.bundleIdentifier!
private let queue = DispatchQueue(label: "ApplicationPlugin")
Expand Down
2 changes: 1 addition & 1 deletion App/Sources/Models/Commands/KeyboardCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct KeyboardCommand: Identifiable, Codable, Hashable, Sendable {
public init(id: String = UUID().uuidString,
name: String = "",
keyboardShortcut: KeyShortcut,
notification: Bool) {
notification: Bool = false) {
self.id = id
self.name = name
self.keyboardShortcuts = [keyboardShortcut]
Expand Down
4 changes: 2 additions & 2 deletions App/Sources/Models/Commands/ScriptCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public enum ScriptCommand: Identifiable, Codable, Hashable, Sendable {
case shellScript = "sh"
}

case appleScript(id: String, isEnabled: Bool, name: String?, source: Source)
case shell(id: String, isEnabled: Bool, name: String?, source: Source)
case appleScript(id: String = UUID().uuidString, isEnabled: Bool = true, name: String?, source: Source)
case shell(id: String = UUID().uuidString, isEnabled: Bool = true, name: String?, source: Source)

public enum CodingKeys: String, CodingKey {
case appleScript
Expand Down
2 changes: 1 addition & 1 deletion App/Sources/Models/Commands/TypeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public struct TypeCommand: Identifiable, Codable, Hashable, Sendable {
public init(id: String = UUID().uuidString,
name: String,
input: String,
notification: Bool) {
notification: Bool = false) {
self.id = id
self.name = name
self.input = input
Expand Down
126 changes: 120 additions & 6 deletions App/Sources/Models/KeyboardCowboyConfiguration.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Cocoa
import Foundation

struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable {
Expand All @@ -16,7 +17,33 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable {
}

static func `default`() -> KeyboardCowboyConfiguration {
KeyboardCowboyConfiguration(
let editorWorkflow: Workflow

if !NSWorkspace.shared.urlsForApplications(withBundleIdentifier: "com.apple.dt.Xcode").isEmpty {
editorWorkflow = Workflow(
name: "Switch to Xcode",
trigger: .keyboardShortcuts([.init(key: "E", modifiers: [.function])]),
commands: [
.application(
.init(application: .xcode())
)
]
)
} else {
editorWorkflow = Workflow(
name: "Switch to TextEdit",
trigger: .keyboardShortcuts([.init(key: "E", modifiers: [.function])]),
commands: [
.application(
.init(application: .init(bundleIdentifier: "com.apple.TextEdit",
bundleName: "TextEdit",
path: "/System/Applications/TextEdit.app"))
)
]
)
}

return KeyboardCowboyConfiguration(
name: "Default Configuration",
groups: [
WorkflowGroup(symbol: "autostartstop", name: "Automation", color: "#EB5545"),
Expand All @@ -30,6 +57,29 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable {
)
]
),
editorWorkflow,
Workflow(
name: "Switch to Terminal",
trigger: .keyboardShortcuts([.init(key: "T", modifiers: [.function])]),
commands: [
.application(
.init(application: .init(
bundleIdentifier: "com.apple.Terminal",
bundleName: "Terminal",
path: "/System/Applications/Utilities/Terminal.app"))
)
]
),

Workflow(
name: "Switch to Safari",
trigger: .keyboardShortcuts([.init(key: "S", modifiers: [.function])]),
commands: [
.application(
.init(application: .safari())
)
]
),
Workflow(
name: "Open System Settings",
trigger: .keyboardShortcuts([.init(key: ",", modifiers: [.function])]),
Expand All @@ -40,16 +90,60 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable {
]
),
]),
WorkflowGroup(symbol: "applescript", name: "AppleScripts", color: "#F9D64A"),
WorkflowGroup(symbol: "applescript", name: "AppleScripts", color: "#F9D64A",
workflows: [
Workflow(name: "Open a specific note",
commands: [
.script(.appleScript(name: "Show note", source: .inline("""
tell application "Notes"
show note "awesome note"
end tell
""")))
])
]),
WorkflowGroup(symbol: "folder", name: "Files & Folders", color: "#6BD35F",
workflows: [
Workflow(name: "Open Home folder",
trigger: .keyboardShortcuts([.init(key: "H", modifiers: [.function])]),
commands: [
.open(.init(path: "~/"))
])
.open(.init(path: ("~/" as NSString).expandingTildeInPath))
]),
Workflow(name: "Open Documents folder",
trigger: .keyboardShortcuts([]),
commands: [
.open(.init(path: ("~/Documents" as NSString).expandingTildeInPath))
]),
Workflow(name: "Open Downloads folder",
trigger: .keyboardShortcuts([]),
commands: [
.open(.init(path: ("~/Downloads" as NSString).expandingTildeInPath))
]),
]),
WorkflowGroup(symbol: "app.connected.to.app.below.fill",
name: "Rebinding",
color: "#3984F7",
workflows: [
Workflow(name: "Vim bindings H to ←",
trigger: .keyboardShortcuts([.init(key: "H", modifiers: [.option])]),
commands: [
.keyboard(.init(keyboardShortcut: .init(key: "")))
], isEnabled: false),
Workflow(name: "Vim bindings J to ↓",
trigger: .keyboardShortcuts([.init(key: "J", modifiers: [.option])]),
commands: [
.keyboard(.init(keyboardShortcut: .init(key: "")))
], isEnabled: false),
Workflow(name: "Vim bindings K to ↑",
trigger: .keyboardShortcuts([.init(key: "K", modifiers: [.option])]),
commands: [
.keyboard(.init(keyboardShortcut: .init(key: "")))
], isEnabled: false),
Workflow(name: "Vim bindings L to →",
trigger: .keyboardShortcuts([.init(key: "L", modifiers: [.option])]),
commands: [
.keyboard(.init(keyboardShortcut: .init(key: "")))
], isEnabled: false)
]),
WorkflowGroup(symbol: "app.connected.to.app.below.fill", name: "Rebinding", color: "#3984F7"),
WorkflowGroup(symbol: "flowchart", name: "Shortcuts", color: "#B263EA"),
WorkflowGroup(symbol: "terminal", name: "ShellScripts", color: "#5D5FDE"),
WorkflowGroup(symbol: "laptopcomputer", name: "System", color: "#A78F6D"),
Expand All @@ -67,8 +161,28 @@ struct KeyboardCowboyConfiguration: Identifiable, Codable, Hashable, Sendable {
.init(key: "G"),
]),
commands: [.open(.init(path: "https://www.github.com"))]),

Workflow(name: "Open imdb.com",
trigger: .keyboardShortcuts([
.init(key: "", modifiers: [.function]),
.init(key: "I"),
]),
commands: [.open(.init(path: "https://www.imdb.com"))]),
]),
WorkflowGroup(name: "Mail", color:"#3984F7",
rule: Rule.init(bundleIdentifiers: ["com.apple.mail"]),
workflows: [
Workflow(name: "Type mail signature",
trigger: .keyboardShortcuts([.init(key: "S", modifiers: [.function, .command])]),
commands: [
.type(.init(name: "Signature", input: """
Stay hungry, stay awesome!
--------------------------
xoxo
\(NSFullUserName())
"""
))
])
])
])
}
}
28 changes: 25 additions & 3 deletions App/Sources/Reducers/DetailCommandActionReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Cocoa

final class DetailCommandActionReducer {
static func reduce(_ action: CommandView.Action,
keyboardCowboyEngine: KeyboardCowboyEngine,
commandEngine: CommandEngine,
workflow: inout Workflow) {
guard var command: Command = workflow.commands.first(where: { $0.id == action.commandId }) else {
fatalError("Unable to find command.")
Expand All @@ -14,7 +14,29 @@ final class DetailCommandActionReducer {
command.isEnabled = newValue
workflow.updateOrAddCommand(command)
case .run(_, _):
break
let runCommand = command
Task {
do {
try await commandEngine.run(runCommand)
} catch let error as KeyboardEngineError {
let alert = await NSAlert(error: error)
await alert.runModal()
} catch let error as AppleScriptPluginError {
let alert: NSAlert
switch error {
case .failedToCreateInlineScript:
alert = await NSAlert(error: error)
case .failedToCreateScriptAtURL:
alert = await NSAlert(error: error)
case .compileFailed(let error):
alert = await NSAlert(error: error)
case .executionFailed(let error):
alert = await NSAlert(error: error)
}

await alert.runModal()
}
}
case .remove(_, let commandId):
workflow.commands.removeAll(where: { $0.id == commandId })
case .modify(let kind):
Expand Down Expand Up @@ -126,7 +148,7 @@ final class DetailCommandActionReducer {
let execution = workflow.execution
Task {
let path = (source as NSString).expandingTildeInPath
await keyboardCowboyEngine.run([.open(.init(path: path, notification: false))], execution: execution)
try await commandEngine.run(.open(.init(path: path)))
}
case .reveal(let path):
NSWorkspace.shared.selectFile(path, inFileViewerRootedAtPath: "")
Expand Down
5 changes: 1 addition & 4 deletions App/Sources/Reducers/DetailViewActionReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@ final class DetailViewActionReducer {
case .updateKeyboardShortcuts(_, let keyboardShortcuts):
workflow.trigger = .keyboardShortcuts(keyboardShortcuts)
case .commandView(_, let action):
DetailCommandActionReducer.reduce(
action,
keyboardCowboyEngine: keyboardCowboyEngine,
workflow: &workflow)
DetailCommandActionReducer.reduce(action, commandEngine: commandEngine, workflow: &workflow)
case .moveCommand(_, let fromOffsets, let toOffset):
workflow.commands.move(fromOffsets: fromOffsets, toOffset: toOffset)
result = .animated
Expand Down
9 changes: 8 additions & 1 deletion App/Sources/Views/AppLoadingView.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import SwiftUI

struct AppLoadingView: View {
private let namespace: Namespace.ID
@State var done: Bool = false

init(namespace: Namespace.ID) {
self.namespace = namespace
}

var body: some View {
VStack {
KeyboardCowboyAsset.applicationIcon.swiftUIImage
.resizable()
.frame(width: 64, height: 64)
.matchedGeometryEffect(id: "initial-item", in: namespace)
Text("Loading ...")
}
.padding()
Expand All @@ -17,7 +23,8 @@ struct AppLoadingView: View {
}

struct AppLoadingView_Previews: PreviewProvider {
@Namespace static var namespace
static var previews: some View {
AppLoadingView()
AppLoadingView(namespace: namespace)
}
}
1 change: 1 addition & 0 deletions App/Sources/Views/ContentImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ struct ContentImageView: View {
Text(">_")
.font(Font.system(.caption, design: .monospaced))
}
.frame(width: size, height: size)
case .path:
Image(nsImage: NSWorkspace.shared.icon(forFile: "/System/Applications/Utilities/Script Editor.app"))
.resizable()
Expand Down

0 comments on commit 98c4bd1

Please sign in to comment.