← XeFM English | 日本語 · crftwr/xefm on GitHub · craftware

Extension Packages

English | 日本語

An extension package is a folder that adds actions, event hooks, themes, sort orders, filters, picture formats, locations or syntax highlighting to XeFM. These are the same kinds of things config.py can add, with no lines in config.py. A package can be installed, shared, disabled and removed as one unit.

Extensions run against the same API as config.py customization, version 1 (see Customization). Most of it is stable; the parts marked Preview may still change, and xefm ext check lists every Preview part a package uses. In the manifest, [contributes.actions], sort_keys, filters, path_schemes and key_suggestions are stable; menus, event_hooks, image_decoders, lexers, themes, [settings], [requires] and updates are Preview.

Finding extensions

The wiki’s Extensions page lists what is available, both the official packages and the ones written by others.

The XeFM project keeps its own extensions in crftwr/xefm-extensions. They are written against the same public API as anyone else’s, so they also serve as examples to copy. Each has a README with its usage, and each release is a tag such as compute-hash-v0.1.0. An install source without @<tag> installs the newest release; add @<tag> to pin one.

Installing and managing

From a shell — this works the same for pip/pipx/uv and Homebrew installs, the macOS app and the Windows app:

xefm ext install ~/Downloads/hello-xefm.zip   # a .zip
xefm ext install ./hello-xefm                 # a folder
xefm ext install github:someone/hello-xefm    # a GitHub repository: its newest release, or @ref
xefm ext install github:crftwr/xefm-extensions/compute-hash   # one package of several in a repository
xefm ext install https://example.com/hello-xefm.zip

xefm ext list                 # what is installed, and whether it will load
xefm ext disable hello-xefm   # keep it, but don't load it
xefm ext enable hello-xefm
xefm ext uninstall hello-xefm # asks whether to delete its data too
xefm ext sync                 # reinstall anything listed but missing
xefm ext settings hello-xefm  # its settings class, ready to paste into config.py

xefm ext outdated             # which packages have a newer version
xefm ext upgrade              # install the newer versions (asks first)
xefm ext upgrade compute-hash # just this one

Inside XeFM:

Newer versions

A package says where its newer versions are published (its updates field; see Publishing). It does not matter how you installed it: a package installed from a downloaded zip still knows where to look. Compute Hash does this.

XeFM never looks on its own. Checking reaches GitHub, so it happens only when you run xefm ext outdated, xefm ext upgrade or Check for Extension Updates…. An upgrade goes through the same check as an install. A version that needs a newer XeFM is refused and says so, and the version you have stays installed. A package you disabled stays disabled.

A package that does not say where it is published cannot be checked; the command says so. Checking needs no GitHub account or token, and packages published in the same repository are checked together.

At startup XeFM warns, without going online, about a package nothing could reinstall: one installed from a file that is now gone, with no updates. It keeps working. Only xefm ext sync, on another machine or after extensions/ is lost, could not bring it back.

Before installing, XeFM checks the package and shows everything it adds, including which code each item runs. Extensions run inside XeFM with your permissions, and there is no sandbox. Install only packages you trust.

After installing, upgrading or enabling a package from a shell, reload the configuration (Tools ▸ Reload Configuration) or restart XeFM. Inside XeFM the configuration reloads by itself.

Your config.py always wins

If a package and your config.py both define the same action, theme, filter, location or other entry, your entry is used. The package’s entry is skipped, with a note in the log.

Packages only suggest keys. A suggested key is used only when you haven’t bound that action yourself and the key isn’t already in use. Otherwise bind it in KEY_BINDINGS. Any action is also reachable from the command palette.

Settings

A package with settings reads them from a class in your config.py. Write it the way Config is written: a plain class with no base class and no import.

class CommandPaletteConfig:
    MAX_ITEMS = 30

class Config:
    ...

The class name comes from the package id in PascalCase plus Config, so command-palette becomes CommandPaletteConfig. A package can choose a different name. xefm ext list shows each package’s class name.

xefm ext settings <id> prints a ready-to-paste class with every setting and its default. Settings you leave out keep their defaults.

XeFM warns in the log in two cases:

If you uninstall the package, its class stays in config.py unused, and the config still loads.

Where things live

What Where
Packages ~/.xefm/extensions/<id>/, beside your own modules
What is installed, from where, enabled or not ~/.xefm/extensions.toml
A package’s own files ~/.xefm/extension-data/<id>/
A package’s remembered state XeFM’s state database

extensions.toml is the part worth keeping. Copy it to another machine and run xefm ext sync, and the packages are fetched again. A reinstall never touches extension-data/.

Writing a package

The quickest start is the scaffold. It creates a package that already passes the check, with one action, settings and a test:

xefm ext new hello-xefm

With an AI assistant

XeFM provides a skill for AI agents, xefm, that walks through writing, checking and installing a package (it also covers editing your config.py). It covers the manifest, the API your code may use, settings, dependencies and testing, and it has the agent run xefm ext check until the package is clean. It works with any agent that reads Agent Skills.

You can let the agent install the skill itself. Give it the page The XeFM Skill for AI Agents (https://crftwr.github.io/xefm/doc/AGENT_SKILL_FEATURE.html) and ask it to install the skill. You can also download xefm-X.Y.Z-skill.zip from the releases page and unzip it into your agent’s skills folder yourself; that page explains both.

Then ask for the feature (“make an XeFM extension that…”). If the agent follows the skill, the result meets the requirements below. xefm ext check verifies every one of them, so a clean check confirms it.

Publishing, and where newer versions come from

Push the package to GitHub; nothing has to be uploaded. github:owner/repo@ref installs any tag, branch or commit, github:owner/repo alone installs the newest release (or the default branch when there is none), and a version is a tag:

updates in [extension] says where newer versions are published, as github:owner/repo or github:owner/repo/path/to/package, with no @ref. It belongs to the package, not to how it was installed, so a copy installed from a downloaded zip still knows where its next version is. Set it from the first release: a version that lacks it cannot point its users to the next one.

A release can also carry the package as an asset named after its tag (compute-hash-v0.2.0.zip, holding the package folder). XeFM downloads that instead of the whole repository when it is there, and it gives anyone who wants the package as a file something to download. Without it, XeFM downloads the repository at that tag and keeps only the package.

To have it listed on the wiki’s Extensions page, post it in the Show and tell discussion category with a link to the repository, a one-line description and its install source.

The layout

A package is a folder containing a manifest and, if it runs code, a Python package:

hello-xefm/
  xefm-extension.toml
  hello_xefm/
    __init__.py
    actions.py
    settings.py
[extension]
id = "hello-xefm"
version = "0.1.0"
api_version = 1                       # the XeFM API it was written for
# optional:
name = "Hello"
description = "Says hello"
min_xefm = "1.8.0"
backends = ["desktop", "tui"]         # default: both
platforms = ["macos", "windows", "linux"]   # default: all
updates = "github:someone/hello-xefm" # where newer versions are published

[contributes.actions]
"hello.greet" = { entry = "hello_xefm.actions:greet", description = "Say hello" }

[contributes.key_suggestions]
"hello.greet" = ["Ctrl-Shift-G"]

[settings]
defaults = "hello_xefm.settings:Settings"
# config_class = "HelloConfig"        # default: derived from the id

[requires]
programs = ["ffmpeg"]                 # missing → its actions say so when run
host_packages = ["PIL"]               # must already be in this XeFM build
python_packages = ["PyYAML>=6"]       # installed from PyPI by the sync
# hello_xefm/actions.py
from xefm.user_api import extension


def greet(ctx):
    """Say hello in the log."""
    me = extension(__name__)
    times = me.state.get("times", 0) + 1
    me.state.set("times", times)
    ctx.message(f"{me.settings.GREETING} (#{times})")
# hello_xefm/settings.py
class Settings:
    GREETING = "Hello"

Code is only referenced, as module:name. XeFM imports it only when the package loads, never when it is checked.

What a package can contribute

Each [contributes.<kind>] table feeds the config.py table of the same name — see Customization for what each entry does.

Kind Feeds Entry
actions ACTIONS "mod:func", or { entry = "mod:func", description = "..." }
event_hooks EVENT_HOOKS "mod:func", or a list of them
sort_keys SORT_KEYS "mod:name"
filters FILTERS "mod:name", or a table such as { globs = ["*.png"] }
image_decoders IMAGE_DECODERS "mod:name"
path_schemes PATH_SCHEMES "mod:Class"
lexers LEXERS a Pygments alias, or { entry = "mod:LexerClass" }
themes THEMES a table of colors, as in config.py
menus MENU_ITEMS { menus = ["file", "context"], label = "...", when = "entry" }, for the package’s own actions
key_suggestions KEY_BINDINGS a list of keys, for the package’s own actions

A package with only themes, glob filters, lexer aliases, menus and key suggestions runs no code at all.

Name actions <something>.<name>, such as hello.greet. A name without a dot gets a warning, and a built-in action’s name is refused.

What your code is given

xefm.user_api.extension(__name__) returns your package’s handle:

The package is found from the module’s own name, so it works anywhere in your code and at any time — in an action, in the callback of a prompt that answers after the action has returned, in a background task, in a sort key. Outside a loaded package (in config.py) it returns None.

(Earlier previews had ctx.extension, set only while an action ran, so a prompt’s callback found None. It is gone, and xefm ext check names the replacement.)

Checking a package

xefm ext check ./hello-xefm
xefm ext check ~/Downloads/hello-xefm.zip
xefm ext check github:crftwr/xefm-extensions/compute-hash

The check takes anything xefm ext install takes, so a package can be checked straight from a download or a link, before it is installed. It first lists what the package adds — the same list an install shows before it asks:

Hello 0.1.0 (hello-xefm) adds:
  action: hello.greet  (runs hello_xefm.actions:greet)
  installs Python package from PyPI: PyYAML>=6

The check validates the manifest without running any of the package’s code. Each problem it reports names the manifest field it concerns:

error: xefm-extension.toml: contributes.actions."hello.greet".entry: module hello_xefm.actions has no top-level 'greet'

The check also reads the package’s Python files, again without running them:

Files under a _vendor/ folder are skipped.

Last, it lists every part of the API the package uses that is still Preview — a manifest section, a name it imports, a ctx method:

preview: xefm-extension.toml: contributes.menus: Preview, may change in any release
preview: hello_xefm/actions.py:42: ctx.run_task — Preview, may change in any release
2 Preview API(s) in use; everything else this package uses is stable.

These are not problems — using Preview is fine, and a package with only these lines installs and loads. They tell you which parts of your package may need a change after an XeFM release. To keep a package on the stable part only, check it with --stable-only, which reports them as errors:

xefm ext check --stable-only ./hello-xefm

The same shows at run time: the first time a package calls a Preview function, XeFM writes one line to the log pane naming the package and the function, and the startup line that lists your extensions names the Preview manifest sections each one uses.

The exit status is 0 when the package would install. An install runs the same check and installs nothing if it finds an error.

Trying a package

xefm try --ext ./hello-xefm

This runs XeFM in the terminal with your package and nothing else, so you can see its menus, dialogs and tasks before you install it. It runs in a temporary home folder, so your own ~/.xefm, its config and your other extensions are never touched. Quit XeFM as usual and the temporary home is deleted. An AI assistant uses the same command with a script of keys. See Trying XeFM in a Throwaway Home.

Dependencies

python_packages entries are plain requirements: a project name, optional [extras], version specifiers and a ; marker. Pip options (--index-url), URLs and paths are refused by the check, so no extension can redirect where pip downloads from.

What the user sees:

Things to know