Git Product home page Git Product logo

keyboardcowboy's Introduction

I'm a fun-loving and nerdy Swed living in Oslo, Norway. After getting hooked on creating open source components and can't seem to stop.

I've built and open sourced the following things:

  • ⌨️ Keyboard Cowboy, The missing keyboard shortcut utility for macOS
  • πŸŒ“ Gray, a macOS application for tailoring your Mojave experience on an app-per-app basis
  • 🍫 Syncalicious, on macOS, preferences are everything; they are what separate your mac from the rest, make it stand out, and give it personality. Setting up a new installation of macOS can be fun and liberating, but it can also be a chore. This is where Syncalicious comes in.

When I'm not doing open-sourcing, I find myself writing about it on Medium. You can find my medium profile here: https://medium.com/@zenangst So, if you like what I do... why not support me and see how far we can take this open-source party? :)

zenangst's github stats

keyboardcowboy's People

Contributors

olemikkel avatar pkrll avatar stoffen avatar vadymmarkov avatar zenangst avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

keyboardcowboy's Issues

Add a permissions controller

We should encapsulate permission handling into a controller which can monitor the application's permissions and ask/present dialog for requesting initial or more if required. Not doing it directly in AppDelegate gives us the opportunity to do it JIT which leads to better user experience than just showing the permission dialog directly when the application launches.

Add a proper responder chain

We need a way to control the responder chain in order to deliver a proper user experience.

Example:

Adding new content

  1. User creates a new workflow or group
  2. The input field for the name of the workflow should be focused

Editing

  1. A user tries to edit an entity
  2. The correct fields should gain focus
  3. Buttons for saving and cancelling should be properly highlighted and bound to the correct keys

Own shortcuts should be disabled when setting keyboard shortcuts in workflow

When the shortcut input box has focus, Keyboard Cowboy's own keyboard shortcuts should preferably not fire:

Screenshot 2020-12-14 at 16 54 01

Obviously, setting ⌘K (or any of these, really) as a shortcut is a terrible idea, but it took me a while to figure out why it resulted in an empty keyboard shortcut at the bottom of the list simultaneously πŸ˜‚

Use of dependencies

How do you feel about using some swift packages when it makes things easier? Or do you prefer to build everything from scratch? For example R.swift or a package with SFSymbol constants, which are more tools than some UI or architecture dependencies?

Workflows

Let's try and define what a Workflow is. It might be easier it we make this issues based on some Swift-pseudo code.

struct Workflow {
  var name: String
  var combinations: [Combination]
  var commands: [Command]
}

.name: String

The idea with a workflow is that you can name them to describe what they are doing, mainly because they can contain a collection of commands. When it comes to UI, we could try and predict what kind of workflow name should be appropriate for the user.
So if a user adds a workflow with one application command, we could provide a default the suggestion that reads something like this:

Open "Finder" application

But if it contains multiple commands, it might be more appropriate to either suggest or at least give the user the opportunity to rename it. Here is an example for a workflow that open "office" applications.

Example command:

Workflow(commands: [
	ApplicationCommand(bundleIdentifier: "com.apple.Mail"),
    ApplicationCommand(bundleIdentifier: "com.apple.Calendar"),
    ApplicationCommand(bundleIdentifier: "com.apple.Contacts")
])

Example name:

Open work applications

.combinations: [Combination]

The idea here is that you might want to bundle keyboard shortcuts invocations with the same "igniting"-key and have multiple other commands exposed when the first invocation has been fired.

Example commands that are scoped under the same combination

Workflow(
  name: "Open Mail",
  combinations: [
    Combination(source: "βŒƒβŒ₯A"), Combination(source: "βŒƒβŒ₯M")
  ]
)

Workflow(
  name: "Open Calendar",
  combinations: [
    Combination(source: "βŒƒβŒ₯A"), Combination(source: "βŒƒβŒ₯C")
  ]
)

So when βŒƒβŒ₯A is invoked, we filter on the first combination and then move on to the next, that way we can bundle and funnel the invocation to the appropriate command at the end. Hope that makes sense.

So…

The keyboard shortcut: βŒƒβŒ₯A followed by βŒƒβŒ₯M would open Mail.
The keyboard shortcut: βŒƒβŒ₯A followed by βŒƒβŒ₯C would open Contacts.

It would mean that βŒƒβŒ₯A is the "igniting"-key for both commands.

The motivation behind going in this direction is the amount of bindings that a user can create. It is kinda of similar with how emacs bindings work.

.commands: [Command]

A collection of commands that will executed in linear order. For more information about commands, there is more in-depth details here #4

I have this idea of input and output for certain commands so that you can create a workflow that has chained commands.

Example:

  • Run a shell script
  • Pass the output and use it as input for the next command
  • Perhaps announce the final result using a specialized NotificationCommand.

The chaining might come with some additional complexity, so Let's hold of from that idea for the time being but we should have it in the back of our heads that this is something that we might want to do in the future.

Improve disabling hotkeys

Currently, all hotkeys are disabled when the main window is open, this is odd and confusing. A better user experience would be to disable it when the user tries to bind a new key.

Controller domains

I've been thinking a little bit about the logic layer. I think it would be a good idea to try and keep the controller layer neat and tidy in terms of responsibility.

I've been tinkering a bit with the following cascading hierarchy.

  • Core controller #60
  • Group controller
    Implemented in #28
  • Rule controller
  • Workflow controller #32
  • Command controller #34

Core Controller

This controller is responsible for setting up the core functionality, mainly mapping keys to invocations.

Group Controller

  • This controller is responsible for filtering out non-active groups based on the rules. Evaluating rules is handled in the next layer which is the RuleController.

Rule controller

Evaluate and filter groups based on their rules.

Workflow controller

Execute the underlying commands if all previous criteria are met. Potentially also moving output between commands if that is something that we decide to implement.

Command controller

Switch on Command and run the appropriate command with a corresponding controller.

Workflow selection is reset when re-ordering commands

How to reproduce:

  • Select the group Bundles
  • Select the second workflow "Awesome Workflow"
  • Drag one of the commands to re-order them.

Expected result:

  • After re-ordering, the "Awesome workflow" should still be selected.

Actual result:

  • After re-ordering, the selection is reset and the first workflow is instead selected.

Loading icons

We need to make a controller that can load and cache images based on installed apps.

Command controllers

We should implement controllers for each individual command so that we can easily scale and have tailor domain behavior for each individual command.

A command controller should be concerned with things like error handling (preferably by throwing when trying to run the command).

The core command controllers are:

  • ApplicationCommandController
  • AppleScriptCommandController
  • KeyboardCommandController
  • OpenCommandController
  • ShellScriptCommandController

These controller should also have their own corresponding protocols so that they are easily mocked in tests and to make the public API super clear.

Example:

protocol ApplicationCommandControlling {}
class ApplicationCommandController: ApplicationCommandControlling {}

For the time being, I think the CommandController and own these controllers. They might be moved into a different place more suited if requirements change or if we decide to change the current architecture.
But worth nothing is that the owner of the command controllers should not own the concrete implementation but should hold reference to a protocol, that way we can always mock behavior when needed and ApplicationCommandController can be hidden from the application implementation annotated as internal, which is a huge win in terms of not leaking implementation details.

Handle ~ and symlinks in paths

There is an issue with paths that icons and path locations aren't properly resolved because they are using the tilde character (to represent the home directory) or are symlinked (such as core applications).

File-format and storage

The thing about the Command data type is that it needs to be polymorphic.
Perhaps it should be a protocol of some kind rather than a concrete type.
I think that could improve the underlying data that we need for each individual command.

With that said, there is also another thing to consider, which is the data format when saving the data tree to disk.

The early prototypes used a JSON format as its storage, I kind of liked that option as it gives you the ability to store your configuration in a GitHub repo and is really easy to share across devices and with friends.

However, I just thought we should consider that. We need to take a larger discussion on the file storage format when we get to that point. Will create a different issue to discuss that feature.

Add preference window

The application should most definitely have a preference window where the user should be able to configure the following:

  • Open main window on launch
  • Run in Dock/Menubar or both
  • Timeout delay between hotkey invocations
  • Location of the configuration file

There are probably more things that we want to add here

Minimum deployment target

Do you want to support Catalina or we can set minimum deployment target to 11.0 in order to use new SwiftUI features?

Add Combine to the command controllers

As suggested here. It would be a great improvement to streamline command controller execution by adding Combine and treat all controllers as if they were asynchronous.
It would probably also help improving our error handling model.

Add linter

We should add SwiftLint to both ensure that we have a unified code-style and to highlight TODO's as warnings.

Refactor ShellScriptController to be able to mock Process

Right now we cannot test the shell script implementation because it uses Process directly to execute shell scripts.

We should consider adding a protocol that Process conforms to so that we can mock the outcome in a test. Right now ShellScriptController is untested.

Commands

Here is a quick list of commands that we should try and support

Update

  • Application command
    Launch and/or activate applications (on double invocation, bring all windows to front)

  • Open command
    Open a file/folder or URL with an optional bundle identifier to force which application should be used to before the command

  • Keyboard command
    Perform a keyboard shortcut when a workflow keyboard shortcut is invoked

  • Script command
    Execute an Apple- or Shell-script, either referring to a file on the file-system or an inline script.

––––––––––––––––––––––––––––––––––––––

  • File command
    Open a specific file with either the default application or with a specified application
  • Rebinding command
    Rebind a hotkey to a different shortcut.
    Example: Option-W would be equal to Arrow up
  • Apple Script command
    Run an Apple script, either by file or an inline Apple Script blob
  • Shell command
    Run a shell script, either by file or an inline Shell script
  • URL command
    Open a url with either the default application or by specifying a targeted application.

Improve application parsing

On some machines and setup, there can be a long delay before the application is ready because too goes through the hard drive looking for applications. We should improve this search pattern to be scope to only the regular paths and the option to add extra paths if the user has applications in other locations.

Snapshot testing

I've grown rather fond of snapshot tests lately and we might want to consider including http://github.com/pointfreeco/swift-snapshot-testing on the unit test target so that we can rely on their dump-based asserts when writing our tests.

And it goes without saying that we should use it for the ViewKit target to get snapshots of views and windows. I think that could be achieved by simply loading the _previews and creating a snapshot from a NSHostingController.

Activation improvement for application command

The application command currently uses this logic.

  1. Launch application if the application isn't already open
  2. If the application is opened, activate it to bring its front-most window to front
  3. If the application is opened and front-most, then bring all the applications windows to front.
  4. If the application has no open windows, try and launch the application again to force the application to open a window

I would suggest adding one small change to this behavior that would increase the usability of the command.

  1. Launch application if the application isn't already open - remains the same
  2. If the application is opened, activate it to bring its front-most window to front - remains the same
  3. If the application is opened and front-most, then bring all the applications windows to front.
    Instead of bringing all applications to front, start cycling through the applications active windows, that way to you can quickly jump between all windows for an open and active application.
  4. If the application has no open windows, try and launch the application again to force the application to open a window - remains the same

Ability to support third-party commands

It would be neat if developers could supply their own third-party commands by conforming to a protocol.

I think it might be limited to some degree but it would be a neat idea to consider for future versions. Would be very interesting what people come up with.

Bundle name versus application name in drop-down

Unsure whether this is a big deal, but I could initially not find Visual Studio Code in the list as it shows bundle names as opposed to application names.

Screenshot 2020-12-14 at 16 49 03

For what it's worth, Spotlight shows it as follows:

Screenshot 2020-12-14 at 17 00 49

XcodeGen, CocoaPods, Carthage, Swift Package Manager

Right now, the project uses XcodeGen too generate a project for us.
However, I think we should at least consider other options because it might have implications for CI and the choice of dependency manager.

I think XcodeGen could potentially support any type of dependency manager but it is an extra step for anyone how wants to get involve in the project.

So what I would suggest or rather want, is to just use SPM for everything.
I'm a bit unsure if that would work with the current state of SPM. One thing to also consider here is that third-party dependencies might not support SPM but they most likely will support CocoaPods as that is more popular and mainstream.

The reasoning behind favoring SPM is mainly easy of use, just opening the Package.swift and get going has a form of elegance to it.

What are your thoughts on this @vadymmarkov ?

Add drop support for adding commands

We should have drop support for adding commands. So when the user takes an application, file, or folder, they should be able to drop it into the list of commands and the application needs to be smart enough to determine which kind of command that should be added based on the file type.

Run tests on CI

We really should get the code running on a CI sooner rather than later.
Don't really know which CI to go for but we need one that keeps updating their stack so that we can run the latest Xcode toolchains.

Make CommandController thread-safe

CommandController uses Combine to stream values as commands are executed.
Because this is an integral part of the app and that workflows should execute sequentially, there is a requirement for CommandController to be thread-safe.

Implement disk-write throttling and toggling listening of file system events

We need to implement a proper way to throttle writing changes to disk, we should not save the storage to disk each time the user enters a character in the detail for or reorders objects.
This needs to be throttled in order to ensure that we don't do more work than needed.

Additionally, we should figure out how to enable and disable file-system event listeners so that we can tailor the experience to our current context by separating internal and external changes.

Early UI prototype brainstorm

I just thought I'd share a previous iteration of the application (that never got out of the alpha stage). It was written in AppKit but now since we are doing a complete rewrite in SwiftUI, I think we can rethink some things.

image

I was also thinking that this screenshot might clarify the different models.

So if you look at the screenshot and then check this table, it might give an overview on what we want to achieve.

Group(s) Workflows Detail view for Workflow
Group model Workflow model Other models

Add support for running the application in the system menubar

I think it would be a good idea to add a small icon for the application in the system menu bar to open the application window, check for new versions, quit the application, etc.

Would be neat if the users could configure what type of application mode the app should run in:

  • In the Dock
  • In the menubar
  • Face-less (meaning that the app is hidden from the app switcher, dock, and menubar but will open the main window when the user activates the app)

Add support for contextual/environment arguments for script commands

I think it would increase usability if arguments could be passed to shell scripts using environmental or contextual variables.

Example:

If the user selects file(s) in Finder, the file(s) paths can be passed as arguments to an inline or path based script.

If we add support for passing of arguments, you could take it one step further and have one command produce output for the succeeding command(s). That could open up for command chains and ability to produce unique command type(s) that can take output.

Example:

Output from a script command is sent to an AnnounceCommand which has different types, either by using say command or notification center to render the result of the script execution result.

Add check for updates mechanism

We should implement a way for checking if a new version is available.
In the PR #123, I added a menu bar item called "Check for updates…" but that just opens the GitHub release page.

  • Switch updating behavior to make a call using Sparkle rather than opening the GitHub release page.

Encapsulate sign posting and only run in debug

Signposting should only be used when developing the application. I think we should add it as a launch argument. The signposting code should also be encapsulated into its own class that can be toggled or just opt-out when the launch argument isn't enabled.

Make a prototype of the UI in SwiftUI

I'm thinking the first version is basic lists that are tied together using @State and other SwiftUI operators.

Mainly just to get the ball rolling, we can look at design and making it unique at a later stage.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.