.. _afwf-Introduction: afwf Introduction ============================================================================== ``afwf`` is a Python framework for building Alfred Workflows. It covers three things: - **A typed API for Script Filter output** — build drop-down items and script filter responses as Python objects, not raw dicts. - **A uvx-based deployment model** — ship your workflow as a PyPI package, invoke it from Alfred with a single ``uvx`` line. No virtualenv to babysit. - **Optional workflow utilities** — fuzzy ranking, disk caching, and other primitives that come up in nearly every workflow. Quick Example ------------------------------------------------------------------------------ A Script Filter that fuzzy-ranks bookmarks against the user's query: .. code-block:: python from afwf.api import ScriptFilter from afwf.opt.fuzzy_item.api import Item, FuzzyItemMatcher BOOKMARKS = [ ("Alfred App", "https://www.alfredapp.com/"), ("Python", "https://www.python.org/"), ("GitHub", "https://github.com/"), ("Stack Overflow", "https://stackoverflow.com/"), ] def search_bookmarks(query: str = "") -> ScriptFilter: items = [ Item(title=title, subtitle=url, arg=url).set_fuzzy_match_name(title) for title, url in BOOKMARKS ] if query.strip(): items = FuzzyItemMatcher.from_items(items).match(query, threshold=0) or items sf = ScriptFilter() sf.items.extend(items) return sf The Alfred Script field: .. code-block:: bash ~/.local/bin/uvx your-workflow@1.0.0 search-bookmarks --query '{query}' That's it. No ``main.py``, no hardcoded interpreter path, no per-machine setup. The Typed Script Filter API ------------------------------------------------------------------------------ Alfred's Script Filter speaks JSON. Writing that JSON by hand is fragile — a typo in a key name produces no error, just a silently broken workflow. ``afwf`` gives you a proper Python API instead. **Item** .. code-block:: python from afwf.api import Item item = ( Item( title="Alfred Documentation", subtitle="Open in browser", arg="https://www.alfredapp.com/help/", ) .open_url("https://www.alfredapp.com/help/") # Enter → open URL .add_modifier(mod="cmd", subtitle="Copy URL", arg="https://www.alfredapp.com/help/") ) ``Item`` covers the full `Alfred Script Filter JSON spec `_ — title, subtitle, arg, uid, icon, modifiers, variables, and more. Methods are chainable. **ScriptFilter** .. code-block:: python from afwf.api import ScriptFilter sf = ScriptFilter() sf.items.append(item) sf.to_script_filter() # → dict, useful in unit tests sf.send_feedback() # → writes JSON to stdout, picked up by Alfred The uvx Deployment Model ------------------------------------------------------------------------------ The old workflow deployment story is painful: - A virtualenv lives inside the workflow directory, bloating it with tens of MB of packages. - The Python interpreter path is hardcoded in Alfred's Script field. - That path breaks whenever the venv moves, Python upgrades, or you switch machines. - Every new machine needs the same manual setup. The ``uvx`` model cuts all of this out. Publish your workflow logic to PyPI, then point Alfred at it: .. code-block:: bash ~/.local/bin/uvx your-package@1.0.0 subcommand --query '{query}' ``uvx`` resolves, downloads, and caches the exact version on first run. Nothing to install. Nothing to configure. Upgrading is a one-character change in Alfred's Script field. .. list-table:: :header-rows: 1 :widths: 45 55 * - Before - With uvx * - Hardcoded ``.venv/bin/python`` in Alfred - ``uvx pkg@version subcommand`` * - Breaks on Python upgrade or venv move - Self-contained, always works * - Manual setup on every machine - Zero setup — cached on first run * - Version drift across machines - Version pinned in the Script field See :doc:`../12-uvx-Deployment-Model/index` for the full walkthrough. Optional Utilities ------------------------------------------------------------------------------ **Fuzzy ranking** (``afwf.opt.fuzzy_item``) Sort a list of Items by relevance to a query string. Built on `rapidfuzz `_. .. code-block:: python from afwf.opt.fuzzy_item.api import Item, FuzzyItemMatcher items = [Item(title=t).set_fuzzy_match_name(t) for t in ["Alfred", "Python", "GitHub"]] results = FuzzyItemMatcher.from_items(items).match("pyth", threshold=0) # → [Item("Python"), ...] **Disk cache** (``afwf.opt.cache``) Persist expensive results — API responses, filesystem scans — between Alfred invocations. Built on `diskcache `_. Programmatic Access to Alfred and Your Project ------------------------------------------------------------------------------ ``afwf`` also exposes the Alfred runtime environment and your project layout as first-class Python objects, so scripts, build tools, and tests can navigate paths without hardcoding anything. **AlfredPreferences** — the Alfred preferences folder .. code-block:: python from afwf.api import AlfredPreferences prefs = AlfredPreferences() prefs.dir_alfred_preferences # Path to Alfred.alfredpreferences prefs.dir_workflows # …/workflows/ # iterate every installed workflow for wf in prefs.list_workflows(): print(wf.name, wf.bundle_id, wf.version) # look up one workflow by its UUID wf = prefs.get_workflow("76458317-5B0A-40E7-A328-DC6C900EC1B9") ``AlfredPreferences`` reads ``~/Library/Application Support/Alfred/prefs.json`` to locate the active preferences folder (which may live on a sync drive), so it works regardless of where Alfred is configured to store its data. **AlfredWorkflow** — a single workflow directory .. code-block:: python from afwf.api import AlfredWorkflow from pathlib import Path wf = AlfredWorkflow(dir_workflow=Path("…/workflows/user.workflow.")) wf.workflow_id # UUID string extracted from the folder name wf.name # human-readable name from info.plist wf.bundle_id # reverse-DNS bundle id, e.g. "MacHu-GWU.my-workflow" wf.version # version string, e.g. "1.0.0" wf.description # short description wf.created_by # author wf.web_address # homepage / repo URL wf.disabled # bool — is the workflow currently disabled? All attributes are lazy: ``info.plist`` is only parsed on first access. **AfwfProject** — binds your source repo to its deployed workflow folder .. code-block:: python from afwf.api import AlfredPreferences, AfwfProject from pathlib import Path prefs = AlfredPreferences() wf = prefs.get_workflow("76458317-5B0A-40E7-A328-DC6C900EC1B9") proj = AfwfProject( dir_project_root=Path("~/my-workflow"), alfred_workflow=wf, ) # source-side paths proj.package_name # from [project] name in pyproject.toml proj.dir_package # // proj.path_project_info_plist # info.plist checked into VCS proj.path_project_icon_png # icon.png checked into VCS # workflow-side paths proj.alfred_workflow.dir_workflow # the live Alfred folder proj.alfred_workflow.path_info_plist ``AfwfProject`` is useful in build scripts that sync ``info.plist`` / ``icon.png`` between the Alfred folder and the git repo, or that inspect the deployed version before publishing to PyPI.