Adding a new module to bumblebee-status
is straight-forward:
- Add a new Python module in
bumblebee_status/modules/contrib/
. The name of the module will be the name that the user needs to specify when invokingbumblebee-status
(i.e. a module calledbumblebee_status/modules/contrib/test.py
will be loaded usingbumblebee-status -m test
) - Alternatively, you can put your module in
~/.config/bumblebee-status/modules/
- The module name must follow the Python Naming Conventions
- See below for how to actually write the module
- Test (run
bumblebee-status
in the CLI) - Make sure your changes don’t break anything:
./coverage.sh
- If you want to do me a favour, run your module through
black -t py34
before submitting
The project gladly accepts PRs for bugfixes, new functionality, new modules, etc. When you feel comfortable with what you’ve developed, please just open a PR. Somebody will look at it eventually :) Thanks!
I’m pretty open to whatever style you use, but if it’s all the same to
you (and yes, I know that the current codebase is only slowly adapting
to this): - Please favour double quotes for strings.
For private methods/variables,
please use a leading __
(e.g. __output
rather than _output
)
For anything else, please run your code through black.
This example will show “hello world” in the status bar:
"""Short description in RST format
please have a look at other modules, this will go into the
documentation verbatim (list of modules)
"""
import core.module
import core.widget
class Module(core.module.Module):
def __init__(self, config):
super().__init__(config, core.widget.Widget(self.full_text))
def full_text(self, widgets):
return 'hello world'
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
There are two important concepts for module writers: - A module is something that offers a single set of coherent functionality - A module has 1 to n “widgets”, which translates to individual blocks in the i3bar.
Very often, this is a 1:1 relationship, and a single module has a single widget. If that’s the case for you, you can stop reading now :)
Otherwise, you have a number of ways to handle widgets: - During the
super().init__(...)
inside the module’s constructor, you can specify
a list of widgets, and those will comprise the widgets (in ordered
fashion) - During runtime, you can set a new list of widgets by using
the self.add_widget()
method of the module to add new widgets and
self.clear_widgets()
method to remove all widgets.
If you want to add widgets during runtime, please use the
add_widget()
method of the module:
def do_something(self): self.add_widget(full_text="my sample text", name="<optional name>")
TODO: expand on this
bumblebee-status
modules have two different ways to update their
data: 1. Each interval, the callback registered when the widget was
created is called. You can do arbitrarily complex things there 2. Each
interval, before the widget’s callback is invoked, a generic
update(self, widgets)
method is called on the module
Largely, where you want to put your update code is up to you. My
observations: - If you want to change the widgets a module has, you
have to stick with update()
- For simple modules, doing the data
update in the widget callback is simplest (see kernel
, for example)
Each widget inside a module can have a list of states (for example, two
predefined states are warning
and critical
). States define how
a widget is rendered (i.e. which fields in the theme file are selected to
draw it.
Somewhat paradoxically, to give a widget a state, a method called
def state(self, widget)
has to be defined on the module. The
reason for this is that the module typically contains all of the statefulness,
so assumedly, it's easier to determine the state of a widget from the
module, rather than from the widget itself.
The state()
method simply returns a list of strings, which make up
the state this particular widget has.
The themeing code then iterates these states and selects the matching theme information from the theme file. This, it does by performing a "best match" search through the theme, like this:
- Is there a theme definition for the module that in turn contains a JSON object
for the state? If so, use that (for example:
"cpu": { "critical": { "fg": "#ff0000" } }
) - If not, is there a theme definition inside the
defaults
orcycle
theme entries?
For more details on that, please refer to How to write a theme
If multiple states match on the "same level", the last state in the state list is used.
For example, if a module returns [ "critical", "warning" ]
as state, typically, the
widget will be drawn as warning
.
One important helper method is def threshold_state(value, warning, critical)
, which each
module possesses. Using that, it is very easy to define warning and critical states when the
widget represents a simple numeric value.
Sounds confusing? An example will clarify: Let's say your widget returns a percentage (disk usage, or CPU usage). The widget should be marked as "warning" when the percentage is above 50, and as "critical", if it is above 90. This, you would do like this:
def state(self, widget):
return self.threshold_state(self.__value, 50, 90)
The core.input
module can be used to execute callbacks during mouse
events:
import core.module
import core.widget
import core.input
class Module(core.module.Module):
@core.decorators.every(minutes=60, seconds=20)
def __init__(self, config):
super().__init__(config=config, widgets=<widgets>)
core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=<cmd>)
The command can be either a CLI tool that will be directly executed
(e.g. cmd='shutdown -h now'
) or a method that will be executed. The
method’s signature needs to be: def <name>(self, event)
, where
“event” is the event data provided by i3wm.
The full list of possible bindings: - LEFT_MOUSE - RIGHT_MOUSE - MIDDLE_MOUSE - WHEEL_UP - WHEEL_UP
To change the default update interval, you can use a simple decorator:
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
@core.decorators.every(minutes=60, seconds=20)
def __init__(self, config):
super().__init__(config=config, widgets=<widgets>)
NOTE: This makes the update interval of the module independent of
what the user configures via -i <interval>
! It is still possible to
override the module’s interval using -p <module>.interval=<value>
,
however.
Sometimes, it is desirable to redraw a widget dynamically, even outside its update
interva. This can be useful if the value to be displayed is calculated in a separate
thread. In such a scenario, the update()
method would simply trigger of a thread
and the actual value would be available later (but presumably before the next
update call).
If that is the case, it is possible to fire off an event in the thread to cause the affected widget to be redrawn, like this:
import core.event
# later
core.event.trigger("update", [<list of module IDs>], redraw_only=True)
A concrete example of this can be found in the module redshift
, and a couple of others.
If a widgets produces a large amount of content, it might be desirable to limit the amount of space the widget can occupy and scroll the content, if necessary.
This behaviour can be achieved using the scrollable
decorator like this:
import core.module
import core.widget
import core.decorators
class Module(core.module.Module):
def __init__(self, config, theme):
super().__init__(config, theme, core.widget.Widget(self.description))
@core.decorators.scrollable
def description(self, widget):
pass # TODO: implement
There are a couple of parameters that can be set on the affected module, either in the
module using self.set()
or via the CLI using the --parameter
flag:
scrolling.width
: Integer, defaults to 30, determines the minimum width of the widgets, ifmakewide
is specifiedscrolling.makewide
: Boolean, defaults to true, determines whether the widgets should be expanded to their minwidthscrolling.bounce
: Boolean, defaults to true, determines whether the content should change directions when a scroll is completed, or just marquee through