Pattern: Read-Only Script Filters¶
A read-only Script Filter returns items for the user to inspect or act on,
but the action is performed by Alfred’s built-in widgets (open a URL, open a
file) rather than by running additional Python code. The main() function
only builds the response; it never writes to disk or makes network calls at
action time.
The example workflow contains four read-only Script Filters, each illustrating a distinct sub-pattern.
search-bookmarks: Python-Side Fuzzy Filtering¶
Keyword: afwf-search-bookmarks
Script: afwf-examples search-bookmarks --query '{query}'
What it does: Maintains a static bookmark list. On every keystroke Alfred
passes the current query to Python, which narrows the list with
FuzzyItemMatcher and returns the filtered
items. Selecting an item opens the URL in the default browser.
# -*- coding: utf-8 -*-
"""
Example: Search Bookmarks
=========================
**What it demonstrates**
Shows how to build a fuzzy-search Script Filter using
:mod:`afwf.opt.fuzzy_item`. A static list of bookmarks is turned into
:class:`afwf.opt.fuzzy_item.Item` objects; when the user types a query the
list is narrowed with :class:`afwf.opt.fuzzy_item.FuzzyItemMatcher`. If no
fuzzy match is found the full list is returned so the user always sees
results. Selecting an item opens the URL in the default browser via the
``open_url`` variable pair. Type ``error`` as the query to trigger a
simulated error and see how :func:`afwf.log_error` writes a traceback to a
log file.
"""
import afwf.api as afwf
import afwf.opt.fuzzy_item.api as fuzzy_item
BOOKMARKS = [
("Alfred App", "https://www.alfredapp.com/"),
("Python", "https://www.python.org/"),
("GitHub", "https://github.com/"),
("Stack Overflow", "https://stackoverflow.com/"),
("MDN Web Docs", "https://developer.mozilla.org/"),
("PyPI", "https://pypi.org/"),
("Read the Docs", "https://readthedocs.org/"),
("Hacker News", "https://news.ycombinator.com/"),
("Wikipedia", "https://www.wikipedia.org/"),
("Google", "https://www.google.com/"),
("YouTube", "https://www.youtube.com/"),
("Twitter / X", "https://twitter.com/"),
("Reddit", "https://www.reddit.com/"),
("AWS Console", "https://console.aws.amazon.com/"),
("Docker Hub", "https://hub.docker.com/"),
("Homebrew", "https://brew.sh/"),
("VS Code Docs", "https://code.visualstudio.com/docs"),
("Real Python", "https://realpython.com/"),
("Anthropic Claude", "https://claude.ai/"),
("OpenAI", "https://openai.com/"),
]
@afwf.log_error(
log_file=afwf.path_enum.dir_home.joinpath(
".alfred-afwf/search_bookmarks.log"
), # or just @log_error()
)
def main(query: str) -> afwf.ScriptFilter:
if query.strip() == "error":
raise ValueError("This is a simulated Python error triggered by query='error'")
items = []
for title, url in BOOKMARKS:
item = fuzzy_item.Item(title=title, subtitle=url, arg=url)
item.set_fuzzy_match_name(title)
item.open_url(url)
items.append(item)
if query.strip():
matcher = fuzzy_item.FuzzyItemMatcher.from_items(items)
matched = matcher.match(query, threshold=0)
result_items = matched if matched else items
else:
result_items = items
sf = afwf.ScriptFilter()
sf.items.extend(result_items)
return sf
Key points:
Each item calls
item.open_url(url)to set theopen_url/open_url_argvariable pair (see Item Variables and Alfred’s Conditional Widget).argumenttype: 1(required) ininfo.plist— Alfred always passes the query, even when empty.When no fuzzy match is found, the full list is returned so the pane is never empty.
Typing
errorraises a deliberate exception to demonstratelog_error().
Downstream widgets (from info.plist):
Script Filter ──► Conditional (open_url=y) ──► Open URL {var:open_url_arg}
└──► Conditional (_open_log_file=y) ──► Open File {var:_open_log_file_path}
open-file: Alfred-Side Filtering via the match Field¶
Keyword: afwf-open-file
Script: afwf-examples open-file (no query argument)
What it does: Lists every .py file in afwf/examples/ and lets
Alfred filter the list interactively as the user types. Selecting an item
opens the file with the system default application.
# -*- coding: utf-8 -*-
"""
Example: Open File
==================
**What it demonstrates**
Shows how to build a file-picker Script Filter that lets Alfred's built-in
result filtering do the work. The handler lists every ``.py`` file in the
``examples/`` directory and attaches ``open_file`` / ``open_file_path``
variables to each item so a downstream *Open File* action can open the
selected file with the system default application.
Because **Alfred filters results** is enabled, the ``main`` function always
returns the full list and Alfred narrows it in real time as the user types —
no Python-side filtering needed.
**Downstream widgets**
1. *Utilities → Conditional* — condition: ``{var:open_file}`` is equal to ``y``
2. *Actions → Open File* — File: ``{var:open_file_path}``
"""
from pathlib import Path
import afwf.api as afwf
@afwf.log_error(log_file=afwf.path_enum.dir_afwf / "open_file.log")
def main() -> afwf.ScriptFilter:
sf = afwf.ScriptFilter()
dir_here = Path(__file__).parent
for p in sorted(dir_here.iterdir(), key=lambda x: x.name):
if p.suffix.lower() == ".py":
item = afwf.Item(
title=p.name,
subtitle=f"Open {p}",
autocomplete=p.name,
match=p.name,
arg=str(p),
)
item.open_file(path=str(p))
sf.items.append(item)
return sf
Key points:
argumenttype: 2(no argument) ininfo.plist— the script receives no query string. It always returns the full file list.Each item sets
item.match = p.basenameanditem.autocomplete = p.basename. Alfred uses thematchfield for its own client-side filtering, narrowing the displayed list as the user types without re-invoking the script.Each item calls
item.open_file(path=p.abspath)to set theopen_file/open_file_pathvariable pair.
When to choose Alfred-side filtering: Use it when the full item list is small, static, or cheap to produce, and you do not need Python-side logic to decide what to show. The script runs once; Alfred handles the rest.
Downstream widgets (from info.plist):
Script Filter ──► Conditional (open_file=y) ──► Open File {var:open_file_path}
read-file: Conditional Item Display¶
Keyword: afwf-read-file
Script: afwf-examples read-file (no query argument)
What it does: Reads ~/.alfred-afwf/file.txt and displays its content as
a subtitle. If the file does not exist, an error item with the error icon is
shown instead.
# -*- coding: utf-8 -*-
"""
Example: Read File
==================
**What it demonstrates**
Shows how to build a Script Filter that reads the content of a file and
displays it as an Alfred item. This example is designed to work alongside
``write_file.py``: use ``write_file`` to write text into ``file.txt``, then
use ``read_file`` to confirm the content was saved correctly.
If the file does not exist yet, an error item (with the error icon) is shown
instead.
"""
import afwf.api as afwf
from afwf.paths import path_enum
path_file = path_enum.dir_afwf / "file.txt"
@afwf.log_error(log_file=path_enum.dir_afwf / "read_file.log")
def main() -> afwf.ScriptFilter:
sf = afwf.ScriptFilter()
if path_file.exists():
content = path_file.read_text()
item = afwf.Item(
title=f"content of {path_file} is",
subtitle=content,
)
else:
item = afwf.Item(
title=f"{path_file} does not exist!",
)
item.set_icon(afwf.IconFileEnum.error)
sf.items.append(item)
return sf
Key points:
The handler branches on a file-existence condition and returns a different item in each case — a common pattern for Script Filters that depend on external state that may not yet exist.
IconFileEnum()provides bundled icons;IconFileEnum.errorgives a consistent visual cue for failure states.This Script Filter is intentionally read-only — it has no downstream action widget. The item is for display only; pressing Enter does nothing meaningful.
Designed to work alongside
write-file: write something withafwf-write-file, then confirm withafwf-read-file.
memoize: Disk-Cached Computation¶
Keyword: afwf-memoize
Script: afwf-examples memoize --query '{query}'
What it does: Generates a random integer for the given query key and caches it for 5 seconds. Repeated queries with the same key return the cached value without re-running the function.
# -*- coding: utf-8 -*-
"""
Example: Memoize
================
**What it demonstrates**
Shows how to use :func:`afwf.opt.cache.api.TypedCache.typed_memoize` to cache
expensive function results across Alfred invocations. The Script Filter
generates a random integer for a given query key and caches it for 5 seconds,
so repeated queries with the same key return the same value until the TTL
expires. Type ``error`` as the query to trigger a simulated error and see how
:func:`afwf.log_error` writes a traceback to a log file.
"""
import random
import afwf.api as afwf
from afwf.opt.cache.api import TypedCache
from afwf.paths import path_enum
cache = TypedCache(path_enum.dir_afwf / ".cache")
@cache.typed_memoize(tag="memoize", expire=5)
def _get_value(key: str) -> int:
return random.randint(1, 1000)
@afwf.log_error(log_file=path_enum.dir_afwf / "memoize.log")
def main(query: str) -> afwf.ScriptFilter:
query = str(query)
if query.strip() == "error":
raise ValueError("This is a simulated Python error triggered by query='error'")
value = _get_value(query)
sf = afwf.ScriptFilter()
sf.items.append(afwf.Item(title=f"value is {value}"))
return sf
Key points:
cache = TypedCache(path_enum.dir_afwf / ".cache")is a module-level singleton. It is created once when the module is imported and reused across Alfred invocations (eachuvxcall is a fresh process, so the cache persists on disk between calls).@cache.typed_memoize(tag="memoize", expire=5)decorates the inner function, notmain(). Only the expensive computation is cached; theScriptFilterassembly runs on every call.This Script Filter has no downstream action widget. It demonstrates caching behaviour, not a user-facing action.
Observing the cache: Call afwf-memoize hello twice within 5 seconds —
the integer stays the same. Wait 6 seconds — it changes.