A collection of plugins for SwiftBar (also compatible with xbar).
- Clone this repository
git clone https://github.com/gdanko/xbar-plugins.git
- Symlink the desired plugins to your plugins folder for SwiftBar or
~/Library/Application Support/xbar/plugins
for xbar
ln -s /path/to/repo/plugin_name.py $PLUGINS_PATH/plugin_name.py
SwiftBar is a more modern implementation of xbar and it's implied that xbar plugins will just work. This is mostly true. I wanted to create a plugin class that would work with both applications, but I ran into a few issues.
-
There are some parameters that are unique to each application
- SwiftBar
- md
- sfcolor
- sfconfig
- sfimage
- sfsize
- shortcut
- tooltip
- webview
- webviewh
- webvieww
- xbar
- disabled
- key
- shell
I wanted to create a
typing.NamedTuple
class that would accommodate all of these, but if my plugin sent thesfimage
, xbar would complain and the plugin's output would never be rendered. To work around this, I wrote a customTypedDict
class which does a few things:- It has 3 subclasses,
Params
,ParamsXbar
, andParamsSwiftBar
. Anytime a plugin method that writes output is called, theparams
dictionary is sanitized. If the invoking application is SwiftBar, an instance ofParamsSwiftBar
is created and all of the entries from theParams
instance are inserted into the newParamsSwiftBar
object. To avoid any issues, we trapKeyError
exceptions and just pass on them. TheTypeDict
class also has two options,enforce_schema
andenforce_typing
. When you create an instance ofTypedDict
, you pass it a schema in the form of a dict, it looks something like this:
{ 'key1': int, 'key2': bool, 'key3': str, 'key4': float, }
If you create an instance of
Params
and try someting likeparams['key'] = 'asdf'
, aTypeError
exception will be thrown becausekey1
is typed as an int. If you try something likeparams['foo'] = 'bar'
, aKeyError
exception will be thrown becausefoo
is not a part of the schema. Disabling enforcement will allow these situations. - SwiftBar
-
xbar stores its plugins in
~/Library/Application Support/xbar/plugins
. It also stores the JSON vars files in there. If your plugin's name ismy-great-plugin.15m.py
then any custom variables will live in a file namedmy-great-plugin.15m.py.vars.json
. If you try something like this with SwiftBar, SwiftBar will try to execute the JSON files as plugins, and while you can add dot files to exclude these JSON files, I've come up with what I think is a more elegant cross-platform solution. If you're using SwiftBar, the plugin framework will create a directory for the JSON files in~/.config/SwiftBar
. When a thePlugin
class is instantiated, theplugin._get_config_dir()
method is called and it does the following:- Determine the parent pid and use that to set the variable
plugin.invoked_by
. - Set the
plugin.config_dir
variable based on the value ofplugin.invoked_by
. - After
plugin._get_config_dir()
has completed,plugin._create_config_dir()
is called to create the configuration directory if it doesn't exist. This should only ever happen with SwiftBar.
- Determine the parent pid and use that to set the variable
This combination of tweaks and workarounds allows both xbar and SwiftBar to execute plugins happily.
- Auto-detect whether or not you are using xbar or SwiftBar and configure the plugin path and configuration path accordingly.
- Plugins should be able to automatically generate the argparer from
plugin.defaults_dict
. - Plugins should be able to automatically generate the settings menu from
plugin.defaults_dict
. - Automatically generate a plugin configration file using default values for every plugin that can use one. Its location is based on
plugin.config_dir
andplugin.plugin_name
. For example, if you're using SwiftBar and your plugin name isfancy-plugin-DoSomething.10s.py
then your plugin configuration file's path will be~/.config/SwiftBar/fancy-plugin-DoSomething.10s.py.vars.json
. - All plugins have a
Settings
menu that can modify MOST of the settings. Obviously, things like API keys need to be manually configured by hand editing the JSON. If a specific setting displays a list of options, e.g., network interfaces, the currently selected option will have a checkmark next to it. The selected option used to be colored differently, but I opted to use a checkmark for the sake of accessibility. - All plugins have a debugging menu that can be toggled via the plugin's
Settings
menu which shows the following:- OS version, e.g.,
macOS 15.2 (Sequoia)
- Installed system memory
- Debug flag enabled
- Brew enabled (whether or not
${HOMEBREW_PREFIX}/bin
and${HOMEBREW_PREFIX}/sbin
are included in the PATH) - Python binary path
- Python version
- Plugins directory
- Plugin path
- Invoker (xbar or SwiftBar)
- Invoker (full path)
- Invoker pid
- SwiftBar version (SwiftBar only)
- Default font family
- Default font size
- Plugin configuration directory
- Plugin JSON variables file path
- Variables listed in
FOO = bar
format - Environment variables listed in
FOO = bar
format
- OS version, e.g.,
The defaults_dict is an instance of collections.OrderedDict
that does a few things
- Create a set of defaults for all of the variables used by the plugin.
- Generate the
.vars.json
file if it doesn't exist. - Find and set variables that are missing from the
.vars.json
file. - Store data used for generating the plugin's
argparse.Namespace
object. - Determine how to process changes made via the
Settings
menu. - Render the
Settings
menu.
Here are some sample defaults_dict entries
plugin.defaults_dict['VAR_WEATHER_WAPI_DEBUG_ENABLED'] = {
'default_value': False,
'valid_values': [True, False],
'type': bool,
'setting_configuration': {
'default': False,
'flag': '--debug',
'title': 'the "Debugging" menu',
},
}
plugin.defaults_dict['VAR_DISK_USAGE_MOUNTPOINT'] = {
'default_value': '/',
'valid_values': valid_mountpoints,
'type': str,
'setting_configuration': {
'default': None,
'flag': '--mountpoint',
'title': 'Mountpoint',
},
}
plugin.defaults_dict['VAR_DISK_USAGE_UNIT'] = {
'default_value': 'auto',
'valid_values': util.valid_storage_units(),
'type': str,
'setting_configuration': {
'default': None,
'flag': '--unit',
'title': 'Unit',
},
}
plugin.defaults_dict['VAR_EARTHQUAKES_RADIUS_MILES'] = {
'default_value': 100,
'minmax': namedtuple('minmax', ['min', 'max'])(50, 500),
'type': int,
'setting_configuration': {
'default': None,
'flag': '--radius',
'increment': 50,
'title': 'Radius',
},
}
Each entry is mapped to one of the xbar-style <xbar.var></xbar.var>
variable/comment entries. I'll explain each of the fields an what it does.
default_value
- This is the default value for this variable. If, when parsing the configuration file, the variable is missing or invalid, this default entry will replace the existing value and the.vars.json
file will be rewritten.valid_values
- This is a list of valid values for the given field. As you can see in theVAR_DISK_USAGE_UNIT
example, the list of valid values is returned from a function inutil
. When parsing the configuration file, if the valueVAR_DISK_USAGE_UNIT
is not included invalid_values
, it will be replaced by the value defined bydefault_value
.minmax
- This is a type of value used when you want a list of numbers with a defined increment. If the value in the configuration is less thanmin
or greater thanmax
, it will be replaced by the value defined bydefault_value
.type
- This is the type of value the setting will use. For example,VAR_WEATHER_WAPI_DEBUG_ENABLED
is abool
andVAR_EARTHQUAKES_RADIUS_MILES
is anint
.settings
- This block is used for any variable that can be used as a setting. Its fields will be explained below.default
- This is the default value for theagrparse.Namespace
object. It's used to determine if a setting has been changed via one of the argument flags.flag
- This is the flag name forargparse
action. Also, when checking to see if flags were sent at plugin invocation, it's used to invoke the plugin with the flag in order to change the setting.increment
- If the setting block usesminmax
, this is the increment for displaying the list. For example, in the above list,VAR_EARTHQUAKES_RADIUS_MILES
has amin
of 50 and amax
of 500. With anincrement
of 50, the list will be rendered as 50, 100, 150.....500. If one is not specified, the default is 10. I will eventually programatically change the logic to adjust the default increment based on the the size of the span frommin
tomax
.title
- This is an important setting. This is the title of theSettings
menu item. If the variable type isbool
, the menu item will be eitherEnable {title}
orDisable {title}
, depending on the current state of the variable. For this reason, the title, in this example, isthe "Debugging" menu
. For every other type of setting, you can just put the name of the item, e.g.,Radius
orUnit
.
Note, you should follow this format when setting the default
field in a setting_configuration
block:
bool
=False
float
=None
int
=None
str
=None
The plugin class is used by all plugins to do things like define plugin settings, render the Settings
menu, and more. In a very simple example, you can do something like this.
plugin = Plugin()
plugin.print_menu_title('Plugin Output')
plugin.render_footer()
You can define a few things when you instantiate an instance of the class:
disable_brew
excludes${HOMEBREW_PREFIX}/bin
and${HOMEBREW_PREFIX}/sbin
from the PATH when you want to use built-in versions of certain binaries.font_family
defines the default font family for the plugin's output. You can, of course, override this with thefont
parameter when using something likeplugin.print_menu_item()
.font_size
defines the default font size for the plugin's output. You can, of course, override this with thesize
parameter when using something likeplugin.print_menu_item()
.
plugin._set_path()
- Executed at instantiation, this function sets the path based on whether or not homebrew is installed. If thedisable_brew
parameter is passed, homebrew paths are excluded automatically.plugin._get_config_dir()
- Executed at instantiation, this function setsplugin.invoked_by
by examining the parent pid of the executed plugin. It then uses that value to set the location of the configuration directory.plugin._create_config_dir()
- Executed at instantiation, this function should only be whenplugin.invoked_by
isSwiftBar
, since plugin.var.json
files cannot live in the same directory as the plugins themselves.plugin.setup()
- This has to be executed after adding any settings toplugin.defaults_dict
. It executes the followin methods:plugin._read_config()
- This method is used to sanitize and populateplugin.configuation
from the.vars.json
file if it exists. If the file does not exist, one is created from the defaults. We call it here to get the values of any booleans so that if the plugin is executed with a flag like--debug
, we can now compare the existing setting with the new setting and make the change to the.vars.json
file as needed.plugin._generate_args()
- This method generates theargparse.Namespace
object fromplugin.defaults_dict
and parse the command line arguments.plugin._update_json_from_args()
- This method parsesplugin.parser
and gathers the arguments sent to the script. For each argument that was passed, it compares the passed value with the existing value stored inplugin.configuration
. If we find an instance where a change was made, we pass the variable name, e.g.,VAR_SWAP_USAGE_DEBUG_ENABLED
and the new value toplugin.update_setting()
. This function reads the.vars.json
file to a dictionary, updates the changed setting, and rewrites the file. After rewriting the file,plugin.read_config()
is called to repopulateplugin.configuration
.
plugin._write_config()
- This method rewrites the plugin's.vars.json
file any time a setting is changed.plugin._write_default_vars_file()
- This method writes the.vars.json
file from the contents ofplugin.defaults_dict
. It's used when a plugin's.vars.json
file cannot be found.plugin._rewrite_vars_file()
- This method completely rewrites the plugin's.vars.json
file from the contents ofself.configuration
.plugin.sanitize_params()
- This method takes an arbitrary list of params and returns an instance ofParamsXbar
orParamsSwiftBar
, depending on the value ofplugin.invoked_by
.plugin.print_ordered_dict()
- This method accepts acollections.OrderedDict
object and renders it cleanly. It allows you to pass the entire dict instead of passing individual lines.plugin.print_menu_item()
- This method accepts any of the parameters acceptable byself.invoked_by
and renders it.plugin.print_menu_separator()
- This method prints---
to separate menu items.plugin.print_update_time()
- This method prints the last date and time the plugin was updated. It's used byplugin.print_menu_title()
.plugin._update_setting()
- This method is invoked byplugin._update_json_from_args()
. When the user changes a setting, the plugin is invoked with a unique flag, which tellsPlugin()
that the setting needs to be updated in the.var.json
file.plugin.find_longest
- This method accepts either a list or a dictionary. It returns the length of the longest member of the list, or in the case of a dictionary, the length of the longest dictionary key. It's used to properly pad lists of strings for proper formatting.plugin._render_settings_menu()
- This method is invoked byplugin.render_footer()
and renders the contents of theSettings
menu.plugin._render_debugging_menu()
- This method is invoked byplugin.render_footer()
and renders the contents of theDebugging
menu.
gdanko-finance-StockIndexes.15m.py
- Features
- Show the last change for the Dow, Nasdaq, and S&P500 indices.
- Features
gdanko-finance-StockQuotes.15m.py
- Features
- Show lots and lots of detail about one or more stock symbols.
- Settings
- Toggle the
Company Info
sub-menu - Toggle the
Company Officers
sub-menu - Toggle the
Key Stats
sub-menu - Toggle the
Ratios and Profitability
sub-menu - Toggle the
Events
sub-menu
- Toggle the
- Features
gdanko-network-NetworkThroughput.2s.py
- Features
- Display the TX/RX rate for the specified interface.
- Display interface flags, hardware address, IPV4 address, IPV6 address, and public IP address (if applicable).
- Settings
- Toggle verbose mode, which shows information about errors and dropped packets
- Select the interface to view
- Features
gdanko-network-WifiSignal.30s.py
- Features
- Display the specified interface's connection strength to its configured SSID.
- Display device name, channel number, WiFi mode, signal, noise, and signal quality.
- Settings
- Toggle display of extended WiFi information, e.g., Mode, Signal, Noise, and so on
- Select the interface to view
- Features
-
gdanko-stystem-BrewOutdated.30m.py
- Features
- Display a list of outdated homebrew packages with an option to install one or all of them.
- Features
-
gdanko-system-CpuPercent.2s.py
- Features
- Display average user, system, and idle times for the CPU.
- Display user, system, and idle times for each individual core.
- Display top CPU consumers with an option to attempt to kill those owned by you.
- Settings
- Toggle display of extended CPU information, e.g., CPU model, CPU frequency, and so on
- Toggle "Click to Kill" functionality
- Set the kill signal to use when attempting to kill a process
- Set the maximum number of top CPU consumers to display
- Features
-
gdanko-system-DiskConsumers.5m.py
- Features
- Display the largest disk consumers for one or more paths, with the ability to open the selected item.
- Features
-
gdanko-system-DiskUsage.2s.py
- Features
- Display used/total disk space for the specified mountpoint.
- Display the mountpoint, device name, filesystem type, and mount options as shown by
mount (8)
.
- Settings
- Toggle display of extended partition information, e.g., mountpoint, device name, and so on
- Select the mountpoint to view
- Select the unit for displaying the data, e.g.,
M
orGi
- Select the output format, e.g.,
Used / Total
,% Used
, or% Free
- Features
-
gdanko-system-MemoryUsage.2s.py
- Features
- Display used/total system memory.
- Display memory manufacturer and type (if possible), total memory, available memory, used memory, free memory, active memory, inactive memory, wired memory, and speculative memory.
- Display top memory consumers with an option to attempt to kill those owned by you.
- Settings
- Toggle display of extended memory information, e.g., memory manufacter, memory type, and so on
- Toggle "Click to Kill" functionality
- Set the kill signal to use when attempting to kill a process
- Set the maximum number of top memory consumers to display
- Select the unit for displaying the data, e.g.,
M
orGi
- Features
-
gdanko-system-SwapUsage.2s.py
- Features
- Display used/total swap memory.
- Settings
- Select the unit for displaying the data, e.g.,
M
orGi
- Select the unit for displaying the data, e.g.,
- Features
-
gdanko-system-SystemUpdates.15m.py
- Features
- Display a list of available system updates and their version numbers, with an option to install them individually.
- Features
-
gdanko-system-Uptime.2s.py
- Features
- Display system uptime.
- Display last boot time.
- Features
gdanko-weather-WeatherWAPI.10m.py
- Features
- Display the current temperature for the specified location.
- Display "feels like" temperaure, pressure, visibility, condition, dew point, humidity, precipitation, wind, wind chill, heat index, UV index.
- Display up to an eight day forecast, showing low/high temperature, average temperature, average visibility, condition, average humidity, total precipitation, chance of rain, chance of snow, UV index, sunrise time, sunset time, moonrise time, moonset time, and moon phase.
- Settings
- Toggle displaying the
x Day Forecast
menu - Set the units in either
C
orF
- Toggle displaying the
- Features
gdanko-other-Earthquakes.15m.py
- Features
- Location based on gelocation of your IP address.
- Display a list of recent earthquakes based on your location.
- Display the magnitude, time of occurence, updated time, status, as well as a clickable link for the quake's details page at usgs.gov.
- Settings
- Set the limit for the number of results to display
- Set the minimum magnitude
- Set the maximum radius based on your location
- Set the unit in either
km
orm
- Features
- Each plugin first determines the path to the config directory and the name of the plugin.
- When you change a setting, the plugin is invoked with a flag, e.g.,
--max-consumers 5
. The Pythonargparse
module is used to parse these flags. If a configured flag is set, the plugin callsplugin.update_config()
, passing the config path, plugin name, variable name, and new value.plugin.update_config()
determines the path to the JSON var file and updates the value accordingly. The settings toggles are all configured to refresh the plugin so once you make the change, everything is reloaded.
First let's look at the code for a simple plugin that pulls information about system swap usage
def main() -> None:
plugin = Plugin(disable_brew=True)
plugin.defaults_dict['VAR_SWAP_USAGE_UNIT'] = {
'default_value': 'auto',
'valid_values': util.valid_storage_units(),
'type': str,
'setting_configuration': {
'default': None,
'flag': '--unit',
'title': 'Unit',
},
}
plugin.setup()
swap = get_swap_usage()
if swap:
used = util.format_number(swap.used) if plugin.configuration['VAR_SWAP_USAGE_UNIT'] == 'auto' else util.byte_converter(swap.used, plugin.configuration['VAR_SWAP_USAGE_UNIT'])
total = util.format_number(swap.total) if plugin.configuration['VAR_SWAP_USAGE_UNIT'] == 'auto' else util.byte_converter(swap.total, plugin.configuration['VAR_SWAP_USAGE_UNIT'])
plugin.print_menu_title(f'Swap: {used} / {total}')
else:
plugin.print_menu_title('Swap: Failed')
plugin.print_menu_item('Failed to gather swap information')
plugin.render_footer()
Now we'll explain what is happening.
- We create an instance of the
Plugin()
class, passing thedisable_brew
flag. This flag tells the plugin not to include${HOMEBREW_PREFIX}/bin
or${HOMEBREW_PREFIX}/sbin
in the path since we want to use the OS versions of certain binaries. - We can now add additional variables to
plugin.defaults_dict
. If you have not read the section about this dictionary, please do so now. - After adding variable definitions, we make a call to
plugin.setup()
. This is documented in thePlugin()
section. - Once all of that stuff is done, we call
get_swap_usage()
to gather the data. If the data is retrieved successfully we render the output happily, otherwise we display an error. - The last call is to
plugin.render_footer()
. This function does three things:- It renders the "Debugging" menu if debugging is enabled.
- It renders the "Settings" menu if
plugin.defaults_dict
exists. - It displays a "Refresh" menu item.