Ok, firstly let's get some of the buzz words out of the way:
As a MacOS user I actually use Alfred every day rather than the default MacOS Spotlight. In fact so embedded it is into my daily muscle memory, that if it disappeared tomorrow I be stuck hopelessly randomly hitting key combos with an expectation that something would happen. I use it to replace repetitive tasks (search and opening files, browsing websites, playing music, finding contact phone numbers) with "workflows", to boost my productivity. Some of these "workflows" are built in to the app, but some are downloaded and installed having been created by others.
As a developer, one of these "downloaded workflows" was something I used almost every day. It allowed me to navigate around Github.com (open a repo, create a new PR or track an issue) and being able to do it with minimal keystrokes - which became fewer over time due to learning my habits - was invaluable. The problem was the "workflow" I was using was written in PHP. I don't write PHP so if I wanted to extend it or fix bugs it required learning a new language I'm not interested in.
As an aside, I was also looking for a project where I could use Deno. We use TypeScript a lot here at Kyan but we haven't yet found a client project where Deno would be a good fit yet. I think its only a matter of time. Until then though, I wanted to build something a bit more meaty than "hello world", something I could interate on.
So this post is about how I recreated the Github workflow in TypeScript using Deno and some of the problems I overcame along the way. The post doesn't dig too deep into code, but gives an overview of how I solved various problems along the way. If you're into the code, then navigating your way around the source would be your best move.
So in Alfred a workflow works via keystrokes. The user types a trigger which starts the workflow. Anything you type after than trigger gets passed to the workflow. The workflow then decides what items to show based on the keys pressed. Alfred has a drag and drop interface that actually allows you to build quite sofisticated workflows without writting any code. But in this case we want full control so will will have a mix.
For every keystroke we will be passing the keys pressed to our deno app which will return the items we want to show. Here's what the Script Filter
above looks like:
So it's passing both filter
and $argv[1]
to the Deno app. The first thing the Deno app does is parse the values passed in:
You can see we are first doing some initialization. The workflow actually uses a SQLite database. It uses this to store configuration information as well as caching requests made to github for speedup. SQLite is an amazing bit of software. A database as a file. You're probably using it without knowing as it's used in mobile phones and tvs as an easy was to manage local data. It supports complex SQL and is pretty dam fast.
So we need to create this and connect to it before we do anything else. Only then do we progress to actually deciding what we need to show. Once the DB is available to use we pass the connection around rather then keeping opening and closing the connection.
Here's a glimpse of what the data looks like once stored:
The first interesting challenge before we start showing any Github data is actually getting permission to fetch it. We will be using the Github Rest API in the workflow, but this requires a token. The process of getting this token requires fetching an initial auth code and then passing that code to a service that is able to validate it and convert it into a token which is then passed back to the workflow. Here's an overview:
The biggest challenge here was to create a running webserver that is only needed for one request! You can see the source code here https://github.com/whomwah/alfred-github-workflow/blob/main/src/server.ts where I've used timeouts to shutdown the server once its finshed with. It works really well and stores the auth token in the db. The tiny auth service that is needed to convert the access code into a token is running on https://deno.com/deploy. Super super simple to setup and run.
Now we have this auth token when the user searches for something we can first fetch and cache the results so subsiquent searches come from the DB. We use a recursive data fetch from the Github API using the header Link
information:
You can see this recursivly makes API requests if there are more pages and then caches the result in the DB. Fetching and caching the data like this makes for really fast responses to the user even though we need to parse the data first.
Alfred workflows are installed. A workflow is effectively a ZIP file with a different .workflow
extension. One useful bit of functionality is checking for a new version of the workflow and showing it in the results so the user can easily install it. This is done by periodically checking and comparing installed vs latest version. Once a latest version is determined and the user choose to install, we can then trigger a bash script that does the magic:
This works really well giving the user the choice of installing or ignoring the update.
So I've gone through a few of the problems that needed solving in building a workflow for Alfred in Deno and TypeScript. You could be thinking, but you could of just written this in JavaScript or just TypeScript without Deno. Correct, but Deno made the process of writing the app so much nicer and developer experience is super important.
Deno is a single executable with all the tooling built in. So for example I could write and run tests with deno test
. I could make sure all the code was consistently formatted with deno fmt
or type checked with deno check
. No extra build systems required. I also was able to compile the app down to a single JS file with deno bundle
and then run it with deno run
, simple.
If you use Alfred, then please so grab the latest workflow and try it yourself. Or if you just want to see the code visit https://github.com/whomwah/alfred-github-workflow.
Here's a little demo of the workflow in action:
One thing I never went through in this post is how the workflow is built and released. This is something that will addressed in a future post explaining how it uses semantic-release
and Github actions to make building and releasing a new version as simple as a commit with a message of: feat: Create a new release with feature X
I hope you find this post useful, thanks for reading.