Sal's

Filtering Bear.app's recent files in Alfred

One feature I like to have in any notes editor is a way to quickly get back to my most recently edited files. Obsidian has a community plugin for this, but I don’t love relying on community plugins. That’s part of why I moved to Bear.app.

In Bear, you can sort any list of notes by modification date, but it takes a couple clicks to accomplish. It’s also got forward/back shortcuts, which are useful for hopping around. And there’s the Today view, which is handy. But when I’m busy and trying to get back to that one note — what was it called? — with minimal cognitive flailing and without resorting to the mouse, I want something even easier. Or at least, something that my synapses are already wired for.

I just built that thing in Alfred, my preferred launcher. It was pretty easy and fun! Here’s how to do it, in case it’s helpful.

Note: If you’re considering this specific use case, definitely check out the real-deal Alfred plugin, drgrib/alfred-bear. It doesn’t work for me because it compiles its Go code to binaries, which get flagged and blocked on my work laptop. Otherwise I would have started there.

Creating the workflow

First, I created a new Alfred workflow and added a Script Filter Input. That input lets me provide Alfred with a list of arbitrary things to present and filter on.

Next, I figured out how to query Alfred’s SQLite database. Helpfully, Alfred documents how to find it. I copied the database file to a backup that I could use to safely experiment on. I love how easy that is with SQLite.

Next, I opened the backup database with DBeaver to make it easy to explore the schema. I saw a table that looked promising, poked around its columns, and came up with this simple query to pull the most recent notes’ titles and unique identifiers:

SELECT ZUNIQUEIDENTIFIER, ZTITLE
FROM ZSFNOTE
ORDER BY ZMODIFICATIONDATE DESC
LIMIT 20

I then wrapped that in a Python script to generate the JSON that Alfred wants for its script filter input (code modified for brevity):

import json
from pathlib import Path
import sqlite3
import sys

db_path = (
    Path.home()
    / "Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear/Application Data/database.sqlite"
)
if not db_path.exists():
    raise ValueError(f"No db found at path: {db_path}")

# Open in read-only mode (https://stackoverflow.com/a/21794758)
con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)

cur = con.cursor()
items = []
for row in cur.execute(QUERY):
    uid, title = row
    items.append({"title": title, "arg": uid})

filter_input = {"items": items}
sys.stdout.write(json.dumps(filter_input))

Then, back in the Alfred workflow UI, I hooked the script filter input up to an Open URL Action with the following URL template, which I generated with Bear’s super helpful URL builder:

bear://x-callback-url/open-note?id={query}&show_window=yes&open_note=yes

And that’s it!

Here’s what the workflow looks like in the UI:

Screenshot 2025-01-12 at 10

And here’s the result1:

Screenshot 2025-01-12 at 10

Followup notes

The LIMIT

I’ve bumped up the LIMIT parameter to 500, and I’ll keep tweaking it. Bear can filter tens of thousands of list items at the speed of thought. I might get rid of the limit altogether.

To make tweaking that limit easier, I made it so I can optionally set the limit number in an environment variable. Bear makes it easy to access the environment config in the workflow UI. For those newer to Python, here’s a way to read a variable in a relatively safe way:

QUERY_LIMIT = int(os.getenv("BEAR_RECENT_QUERY_LIMIT", "100"))
>>> int("drop table")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'drop table'

Script Input Filter config

If you want Alfred’s filtering to be super fast, take a look at the Alfred filters results checkbox in the input’s config. By default, Alfred will assume your script wants to handle the filtering, so it will re-run your script each time your filter input changes, passing your input as a parameter to the script. If your script does anything computationally expensive, this could be slow.

If you check that checkbox, Alfred will run your script just once, and then keep all the results in memory and handle the filtering. So far, that’s what I’ve wanted in all my workflows. Docs here.

  1. Why Steve Ballmer, you might be wondering? 😉 I stumbled across Dan Luu’s post, Steve Ballmer was an underrated CEO, and wanted to capture it for posterity. I was poking around Dan’s site looking for discussion of his reluctance to use any max-width in his blog’s CSS. See this HN thread.