Xbar and SwiftBar plugin

The Meta Package Manager project is actively maintaining a plugin that is both compatible with Xbar and SwiftBar.

The plugin is written in Python and is a small wrapper around the mpm CLI.

For Linux desktops, the GNOME Shell extension provides the same menu from the top bar.

Each outdated package has its version diff colored with the same convention as mpm outdated: unchanged prefix in gray, installed-version suffix in red, latest-version suffix in green. SwiftBar renders these colors natively (package lines carry the ansi=true parameter); Xbar strips the color codes and shows plain text. On the translucent menus of recent macOS releases the suffixes adapt to the menu appearance, since the default system colors lose contrast against the material: a light menu darkens both suffixes, and a dark menu brightens the green.

Hint

I recommend SwiftBar, because Xbar has 2 outstanding issues:

Important

SwiftBar 2.1.0 or newer is required: older releases mangle the variable defaults carrying an = sign (swiftbar/SwiftBar#445). The plugin detects the host version and renders an error instead of its menu below that threshold. Xbar exposes no version to its plugins, so it is not checked.

Configuration

The plugin is configurable with these environment variables:

Variable name

Description

Type

Defaults

SwiftBar support

Xbar support

VAR_SUBMENU_LAYOUT

Group packages into a sub-menu for each manager.

Boolean

False

VAR_TABLE_RENDERING

Aligns package names and versions in a table for easier visual parsing.

Boolean

True

VAR_DEFAULT_FONT

Font parameters for regular text.

String

Empty

❌*

VAR_MONOSPACE_FONT

Font parameters for monospace text. Used for table rendering and error messages.

String

font=Menlo size=12

❌*

These variables only drive the menu layout. Everything else comes from mpm’s own configuration file: the plugin passes no option beyond the ones it decides itself, so the file found at its default location on the system applies to every run it triggers. See Configuration for the search paths and the full schema.

Screenshots

SwiftBar

VAR_SUBMENU_LAYOUT = False
VAR_TABLE_RENDERING = False
assets/swiftbar-flatmenu-standard-rendering.png
VAR_SUBMENU_LAYOUT = False
VAR_TABLE_RENDERING = True
(default)
assets/swiftbar-flatmenu-table-rendering.png
VAR_SUBMENU_LAYOUT = True
VAR_TABLE_RENDERING = True
assets/swiftbar-submenu-table-rendering.png
VAR_SUBMENU_LAYOUT = True
VAR_TABLE_RENDERING = False
assets/swiftbar-submenu-strandard-rendering.png

Xbar

VAR_SUBMENU_LAYOUT = False
VAR_TABLE_RENDERING = False
assets/xbar-flatmenu-standard-rendering.png
VAR_SUBMENU_LAYOUT = False
VAR_TABLE_RENDERING = True
(default)
assets/xbar-flatmenu-table-rendering.png
VAR_SUBMENU_LAYOUT = True
VAR_TABLE_RENDERING = True
assets/xbar-submenu-table-rendering.png
VAR_SUBMENU_LAYOUT = True
VAR_TABLE_RENDERING = False
assets/xbar-submenu-strandard-rendering.png

Location

A copy of the latest stable version of the plugin is available on Xbar website and plugin repository.

Once mpm is installed on your system, it can dynamically be located with the dedicated --bar-plugin-path option:

$ mpm --bar-plugin-path
~/Library/Python/3.11/lib/python/site-packages/meta_package_manager/bar_plugin.py

This option is handy for deployment and initial configuration of Xbar/SwiftBar. I use this in my dotfiles to symlink the plugin to its latest version:

$ ln -sf "$(mpm --bar-plugin-path)" "${HOME}/Library/Application Support/xbar/plugins/mpm.7h.py"

Python >= 3.9 required

The plugin requires Python 3.9 or newer. Which is the version that ships with the latest macOS releases:

macOS version

Python version[1]

16.x - TBA

3.9.6

15.x - Sequoia

3.9.6

14.x - Sonoma

3.9.6

13.x - Ventura

3.8.9

That way, the plugin is compatible with the latest macOS releases out of the box, and can be run as-is without any extra dependency.

Caution

It looks like since Monterey (macOS), there is no default Python version installed anymore, and the python CLI is a stub that points to the App Store to install Xcode:

$ python3 --version
xcode-select: note: no developer tools were found at '/Applications/Xcode.app', requesting install. Choose an option in the dialog to download the command line developer tools.

Development workflow

Active development of the plugin is happening here, as a side-project of mpm itself.

Releases of the plugin are synchronized with the package. Both share the exact same version to simplify management. This explains why the plugin can appear to jump ahead a couple of major/minor versions while providing tiny or no changes at all.

A release is ready when both the package and the plugin reach a stable state.

If the plugin has been changed between releases, a copy of the plugin is pushed under the name meta_package_manager.7h.py, to the official Xbar plugin repository.

Release process

  1. Fork the official Xbar plugin repository.

  2. Fetch a local copy of the fork:

    $ git clone https://github.com/kdeldycke/xbar-plugins
    $ cd xbar-plugins
    
  3. Create a new branch and switch to it:

    $ git branch "meta-package-manager-v7.6.0"
    $ git checkout "meta-package-manager-v7.6.0"
    
  4. Replace existing copy of the plugin with the latest tagged version:

    $ wget https://raw.githubusercontent.com/kdeldycke/meta-package-manager/v7.6.0/meta_package_manager/bar_plugin.py
    $ mv ./bar_plugin.py ./Dev/meta_package_manager.7h.py
    $ chmod 755 ./Dev/meta_package_manager.7h.py
    
  5. Commit the new plugin:

    $ git add ./Dev/meta_package_manager.7h.py
    $ git commit -m "Upgrade to Meta Package Manager plugin v7.6.0"
    
  6. Push new branch:

    $ git push --set-upstream origin "meta-package-manager-v7.6.0"
    
  7. Create a pull-request in the original repository.

meta_package_manager.bar_plugin API

Xbar and SwiftBar plugin for Meta Package Manager (the mpm CLI).

Default update cycle should be set to several hours so we have a chance to get user’s attention once a day. Higher frequency might ruin the system as all checks are quite resource intensive, and Homebrew might hit GitHub’s API calls quota.

meta_package_manager.bar_plugin.SWIFTBAR_MIN_VERSION = (2, 1, 0)

SwiftBar 2.1.0 fixes an issue with multiple parameters in the font strings.

The fix was first handed out as a 2.1.2-labelled test build, a number that never reached a release: the public train renumbered it down to 2.1.0. Requiring the build we validated on would lock the plugin out of every released SwiftBar.

See swiftbar/SwiftBar#445.

meta_package_manager.bar_plugin.XBAR_MIN_VERSION = (2, 1, 7)

Xbar v2.1.7-beta is the latest version available on Homebrew.

meta_package_manager.bar_plugin.MPM_MIN_VERSION = (5, 0, 0)

Mpm v5.0.0 was the first version taking care of the complete layout rendering.

meta_package_manager.bar_plugin.MPM_TIMEOUT = 60

Maximum duration in seconds the plugin lets any single mpm call run.

Passed as --timeout to every mpm invocation so the plugin is never at the mercy of mpm’s own per-operation defaults, which are tuned for interactive CLI use and far too long for a background menubar refresh (120s for read-only queries, 500s for state-changing operations like sync). A wedged package manager then fails the whole refresh in a minute instead of freezing the menubar for several.

class meta_package_manager.bar_plugin.MPMPlugin[source]

Bases: object

Implements the minimal code necessary to locate and call the mpm CLI on the system.

Once mpm is located, we can rely on it to produce the main output of the plugin.

The output must supports both Xbar dialect and SwiftBar dialect.

static getenv_str(var, default=None)[source]

Utility to get environment variables.

Note that all environment variables are strings. Always returns a lowered-case string.

Return type:

str | None

static getenv_bool(var, default=False)[source]

Utility to normalize boolean environment variables.

Relies on configparser.RawConfigParser.BOOLEAN_STATES to translate strings into boolean.

Return type:

bool

static normalize_params(font_string, valid_ids=None)[source]

Parse a multi-parameters string and return a normalized string.

The string is expected to be a space-separated list of parameters, each parameter being a key/value pair separated by an equal sign.

Only keeps the parameters that are in the valid_ids set and ignores the rest. By default, only color, font and size are kept.

Multiple values for the same parameter will be deduplicated, and the last one will be kept.

Available parameters are documented by both hosts:

Return type:

str

static str_to_version(version_string)[source]

Transforms a string into a tuple of integers representing a version.

Return type:

tuple[int, ...]

static version_to_str(version_tuple)[source]

Transforms a tuple of integers representing a version into a string.

Return type:

str

property table_rendering: bool[source]

Aligns package names and versions, like a table, for easier visual parsing.

If True, will aligns all items using a fixed-width font.

property default_font: str[source]

Make it easier to change font, sizes and colors of the output.

property monospace_font: str[source]

Make it easier to change font, sizes and colors of the output.

property error_font: str[source]

Error font is based on monospace font.

property is_swiftbar: bool[source]

SwiftBar is kind enough to tell us about its presence.

static search_venv(folder)[source]

Search for signs of a virtual env in the provided folder.

Returns CLI arguments that can be used to run mpm from the virtualenv context, or None if the folder is not a venv.

Inspired by autoswitch_virtualenv.plugin.zsh and uv’s get_interpreter_info.py.

Return type:

tuple[str, ...] | None

search_mpm()[source]

Iterate over possible CLI commands to execute mpm.

Should be able to produce the full spectrum of alternative commands we can use to invoke mpm over different context.

The order in which the candidates are returned by this method is conserved by the ranked_mpm() method below.

We prioritize venv-based findings first, as they’re more likely to have all dependencies installed and sorted out. They’re also our prime candidates in unittests.

Then we search for system-wide installation. And finally Python modules.

Return type:

Generator[tuple[str, ...], None, None]

check_mpm(mpm_cli_args)[source]

Test-run mpm execution and extract its version.

Return type:

tuple[bool, bool, tuple[int, ...] | None, str | Exception | None]

property ranked_mpm: list[tuple[tuple[str, ...], bool, bool, tuple[int, ...] | None, str | Exception | None]][source]

Rank the mpm candidates we found on the system.

Sort them by: - runnability - up-to-date status - version number - error

On tie, the order from search_mpm is respected.

property best_mpm: tuple[tuple[str, ...], bool, bool, tuple[int, ...] | None, str | Exception | None][source]
static pp(label, *args)[source]

Print one menu-line with the Xbar/SwiftBar dialect.

First argument is the menu-line label, separated by a pipe to all other non- empty parameters, themselves separated by a space.

Skip printing of the line if label is empty. A None parameter renders nothing, so a package without an upgrade CLI still gets its label-only menu line.

Return type:

None

static print_error_header()[source]

Generic header for blocking error.

Return type:

None

print_error(message, submenu='')[source]

Print a formatted error message line by line.

A red, fixed-width font is used to preserve traceback and exception layout. For compactness, the block message is dedented and empty lines are skipped.

Message is always casted to a string as we allow passing of exception objects and have them rendered.

Return type:

None

print_menu()[source]

Print the main menu.

Return type:

None

meta_package_manager.bar_plugin_renderer API

mpm-side renderer that builds Xbar/SwiftBar plugin output.

Lives in its own module rather than in meta_package_manager.bar_plugin because that module is intentionally stdlib-only: the meta_package_manager.bar_plugin.MPMPlugin class is the script that gets installed as the user’s actual bar plugin and must stay light on dependencies.

This module is the heavier mpm-side companion that augments the shippable plugin code with click_extra, boltons, the manager pool, and the theme system to produce the final rendered output from mpm outdated --plugin-output.

meta_package_manager.bar_plugin_renderer.VERSION_PREFIX_COLOR = 245

Xterm-256 palette index coloring the unchanged version prefix in menu lines.

The CLI table keeps meta_package_manager.version.diff_versions()’s default bright_black (SGR 90), which terminals remap to their own theme. SwiftBar instead hard-maps SGR 90 to a fixed NSColor.darkGray, near-invisible on a dark-mode menu, while its 256-color support renders palette index 245 as a theme-neutral mid-gray (#8a8a8a), legible on both appearances. Xbar strips the ANSI codes it does not render, so the choice is inert there.

meta_package_manager.bar_plugin_renderer.LIGHT_MENU_OLD_COLOR = 124

Palette index for the old-version (red) suffix on a light-appearance menu.

#af0000, a 6.5:1 contrast ratio on the cream material. See BarPluginRenderer.menu_diff_colors() for why the override exists.

meta_package_manager.bar_plugin_renderer.LIGHT_MENU_NEW_COLOR = 23

Palette index for the new-version (green) suffix on a light-appearance menu.

#006600, a 6.3:1 contrast ratio on the cream material. See BarPluginRenderer.menu_diff_colors().

meta_package_manager.bar_plugin_renderer.DARK_MENU_NEW_COLOR = 46

Palette index for the new-version (green) suffix on a dark-appearance menu.

#00ff00, lifting the worst-case contrast from 4.0:1 (the adaptive NSColor.systemGreen) to 5.8:1 over a bright wallpaper showing through the translucent menu. The old-version (red) suffix keeps systemRed: it is already the most readable recognizable red the xterm-256 palette can express (a pure #ff0000 scores lower, and brighter options read as orange). See BarPluginRenderer.menu_diff_colors().

class meta_package_manager.bar_plugin_renderer.BarPluginRenderer[source]

Bases: MPMPlugin

All utilities used to render output compatible with both Xbar and SwiftBar plugin dialect.

The minimal code to locate mpm, then call it and print its output resides in the plugin itself at meta_package_manager.bar_plugin.MPMPlugin.best_mpm().

All other stuff, especially the rendering code, is managed here, to allow for more complex layouts relying on external Python dependencies. This also limits the number of required updates on the plugin itself.

property submenu_layout: bool[source]

Group packages into manager sub-menus.

If True, will replace the default flat layout with an alternative structure where actions are grouped into submenus, one for each manager.

Value is sourced from the VAR_SUBMENU_LAYOUT environment variable.

property menu_diff_colors: dict[str, int][source]

Appearance-adaptive version-diff suffix colors for the menu.

SwiftBar maps meta_package_manager.version.diff_versions()’s default SGR 31/32 suffixes to the adaptive NSColor.systemRed/systemGreen, and exports the menu appearance in the OS_APPEARANCE environment variable (which propagates to the mpm outdated –plugin-output subprocess). On the translucent “Liquid Glass” menus of recent macOS releases these system colors lose contrast against the material, so override them per appearance:

  • A light menu washes out both suffixes (the green measured 1.9:1), so darken them to LIGHT_MENU_OLD_COLOR and LIGHT_MENU_NEW_COLOR.

  • A dark menu over a bright wallpaper dims the green to 4.0:1, so brighten it to DARK_MENU_NEW_COLOR; the red keeps systemRed, already the most readable red the palette allows.

The result is returned as diff_versions keyword arguments. When the variable is absent (a consumer like Xbar, which strips these codes anyway) return an empty mapping, keeping the system-color defaults.

property mpm_cli: tuple[str, ...][source]

Absolute mpm invocation the menu actions are routed through.

Re-enters the very interpreter rendering the menu, so a click runs the mpm the plugin called and resolves the same configuration file. Derived from sys.executable rather than sys.argv[0]: the former is always an absolute path to a runnable entry point, while the latter degrades to a console script, a __main__.py or a bare -c depending on how mpm was started. A Nuitka-compiled mpm is its own interpreter, so it is invoked directly instead of through the module.

Note

The candidates meta_package_manager.bar_plugin.MPMPlugin.search_mpm() produces are deliberately not reused here. The venv ones lead with a bare uv / pipenv / poetry command name, while a bar app spawns a menu action with the bare launchd PATH, where such a name does not resolve.

static render_cli(cmd_args)[source]

Return a formatted CLI compatible with Xbar and SwiftBar plugin format.

I.e. a string with this schema:

shell=cmd_args[0] param1=cmd_args[1] param2=cmd_args[2] ...
Return type:

str

print_cli_item(*args)[source]

Print two CLI entries:

  • one that opens a visible terminal so the user can follow the execution

  • a second one, reachable by holding the Option key, that runs silently

Return type:

None

print_upgrade_all_item(manager, submenu='')[source]

Print the menu entry to upgrade all outdated package of a manager.

Return type:

None

render(outdated_data)[source]

Wraps the _render() method above to capture its <stdout> output.

Every producer down the _render path (the inherited pp and print_error included) writes through bare print calls, so redirecting <stdout> captures the whole rendering.

Return type:

str

add_upgrade_cli(outdated_data)[source]

Augment the outdated data from mpm outdated subcommand with upgrade CLI fields for bar plugin consumption.

Every menu action is an mpm_cli invocation restricted to the manager owning the section (mpm --brew upgrade wget), never that manager’s own native command. Going back through mpm is what subjects a click to the same policy as the run that rendered the menu: the configuration file found on the system, and with it the release-age cooldown, the manager selection, the sudo policy and the per-manager overrides. A native command escapes all of them, silently upgrading a package mpm itself would have held back.

Only the manager selector and the operation are passed, so every other setting is resolved from the user’s configuration at click time.

A manager is offered the action only when it implements() it, which is the same predicate mpm uses to route the subcommand: a manager it would skip gets a None CLI and renders as a label-only menu line.

print(outdated_data)[source]

Print the final plugin rendering to <stdout>.

Capturing the output of the plugin and re-printing it will introduce an extra line return, hence the extra call to rstrip().

Colors are forced on echo’s auto-detection: the bar plugin captures mpm outdated --plugin-output through a pipe, where echo would strip every ANSI code and the version-diff colors would never reach SwiftBar or Xbar. TTY detection is meaningless for this dialect, which flags ANSI rendering per line with the ansi=true/ansi=false parameters. An explicit opt-out (--color=never, NO_COLOR) is still honored: only the automatic (None) state is overridden.

Return type:

None