Creating a new block
Development setup
If you want to quick start the development of i3pyblocks, make sure you have Python >=3.7 installed and run:
$ git clone https://github.com/thiagokokada/i3pyblocks
$ cd i3pyblocks
$ python3 -m venv venv
$ source venv/bin/activate
$ make dev-install
To test if everything is working, try to run i3pyblocks -c example.py
in
your terminal.
See also
NixOS users can use setup a development environment by simply running
nix-shell
at the root of the repository, thanks to the included
shell.nix file.
Let’s start with a “Hello World!”
Creating a new block (“thing that display something in i3pyblocks”) is reasonable easy. Let’s start with a simple, “Hello World!” example:
import asyncio
from i3pyblocks import Runner, blocks, utils
class HelloWorldBlock(blocks.Block):
"""Block that shows a 'Hello World!' text."""
async def start(self) -> None:
self.update("Hello World!")
async def main():
runner = Runner()
await runner.register_block(HelloWorldBlock())
await runner.start()
asyncio.run(main())
It is a silly example, but it should be sufficient to illustrate. We are using
the Block
, that is the root of all blocks in
i3pyblocks.
Save the content above in a file called hello_world.py
. To test in terminal,
we can run it by using:
$ i3pyblocks -c hello_world.py
And we should saw the following being printed in terminal:
{"version": 1, "click_events": true}
[
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Hello World!"}],
^C
There is only one update since this block just update itself once. Use Ctrl+C
(you may need to press twice) to exit.
See also
Check Block
documentation for its methods
so you can see what it is possible to do with it.
A more advanced example
To do something more interesting, we need to have some kind of event that will trigger block events. Also, we need to do things inside a loop, so we can update the block more than once.
One of the easiest ways to do it is to use time, for example:
import asyncio
from i3pyblocks import Runner, blocks, utils
class CounterBlock(blocks.Block):
"""Block that's count at each second."""
def __init__(self):
super().__init__()
self.counter = 0
async def start(self) -> None:
while True:
self.update(f"Counter: {self.counter}")
self.counter += 1
await asyncio.sleep(1)
async def main():
runner = Runner()
await runner.register_block(CounterBlock())
await runner.start()
asyncio.run(main())
Running it in terminal for ~5 seconds results in:
$ i3pyblocks -c example.py
{"version": 1, "click_events": true}
[
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Counter: 0"}],
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Counter: 1"}],
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Counter: 2"}],
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Counter: 3"}],
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Counter: 4"}],
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Counter: 5"}],
^C
As we would expect. Actually, blocks that run an update at each X seconds are
so common that i3pyblocks has an abstraction for it, the
PollingBlock
1:
import asyncio
from i3pyblocks import Runner, blocks, utils
class ImprovedCounterBlock(blocks.PollingBlock):
"""Block that shows a 'Hello World!' text."""
def __init__(self):
super().__init__(sleep=1)
self.counter = 0
async def run(self) -> None:
self.update(f"Counter: {self.counter}")
self.counter += 1
async def main():
runner = Runner()
await runner.register_block(ImprovedCounterBlock())
await runner.start()
asyncio.run(main())
PollingBlock
will call
run()
at each second, exactly like
our previous example. We can increase the interval between each update by passing
super.__init__(sleep=X)
, where X
is the seconds between each update.
- 1
Since both
Block
andPollingBlock
are blocks used to construct other blocks, they’re kept in the same namespace,i3pyblocks.blocks.base
. There is also some other base blocks that will be shown later on.
Customizing output
Sometimes you want to give some emphasis in an output. For example, if the
user battery is too low you probably want to alert the user. In this case,
you can pass some keyword arguments to update()
that will alter the output of i3bar. For example:
import asyncio
from i3pyblocks import Runner, blocks, utils
class WhiteHelloWorldBlock(blocks.Block):
"""Block that shows a 'Hello World!' text."""
async def start(self) -> None:
self.update("Hello World!", background="#FFFFFF")
async def main():
runner = Runner()
await runner.register_block(HelloWorldBlock())
await runner.start()
asyncio.run(main())
Running it in terminal:
$ i3pyblocks -c hello_world.py
{"version": 1, "click_events": true}
[
[{"name": "WhiteHelloWorldBlock", "instance": "<random-id>", "full_text": "Hello World!", "background": "#FFFFFF"}],
^C
Those keyword arguments follow the i3bar’s protocol, so check its documentation for more information.
Clicks and signals
Let’s expand our HelloWorldBlock
to change the text when the user sends
a common Unix signal, SIGUSR1
, to the i3pyblocks process. To do this
we will implement signal_handler()
:
import asyncio
import signal
from i3pyblocks import Runner, blocks, utils
class HelloWorldBlock(blocks.Block):
async def signal_handler(self, *, sig: signal.Signals) -> None:
if sig == signal.SIGUSR1:
self.update("Bye!")
async def start(self) -> None:
self.update("Hello World!")
async def main():
runner = Runner()
await runner.register_block(HelloWorldBlock(), signals=(signal.SIGUSR1,))
await runner.start()
asyncio.run(main())
Now running this in one terminal and running pkill -SIGUSR1 i3pyblocks
in
another results in:
$ i3pyblocks -c example.py
{"version": 1, "click_events": true}
[
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Hello World!"}],
[{"name": "HelloWorldBlock", "instance": "<random-id>", "full_text": "Bye!"}],
^C
To handle mouse clicks, there is a similar method called
click_handler()
that you can implement in
a similar way.
When to use each base block?
Generally using either PollingBlock
(for asyncio)
or PollingSyncBlock
(for non-asyncio) 2 is
the easiest way to start. However it is not necessary the most efficient way.
For example, volume is not something that is changed frequently. You may change the volume of your system once or twice until you find a confortable volume for what you’re currently listening, and keep the same volume for hours. So, querying the system each second for the current volume seems unnecessary.
If you want to be efficient, in those cases you need to have an event loop.
An event loop waits for some kind of event (for example, increase or decrease
in volume), and after we receives this event we trigger an update. This is
exactly what PulseAudioBlock
does, waiting
for any change in the PulseAudio configuration to trigger updates.
Implementing an event loop goes out the scope of this tutorial, but keep in mind
that there is generally a Python package that does it for you, and all you need
is to add it as a dependency to i3pyblocks and integrate it inside a block.
For this, you can use Block
as we saw before,
for projects that integrates well with asyncio. Just implement
start()
with something like this:
async def start(self):
while True:
result = await wait_for_async_event()
self.update(result)
However, some projects doesn’t integrate well with asyncio (i.e.: their
methods are not async). Using them with Block
would freeze i3pyblocks completely until some update on them happened.
In those cases, you can use SyncBlock
.
It runs the code inside an Executor, that can be either a thread or a process,
so the updates inside this block doesn’t affect the rest of i3pyblocks. The
usage ends up being very similar to before, just without async/await keywords
and using start_sync()
instead:
def start_sync(self):
while True:
result = wait_for_sync_event()
self.update(result)
- 2
PollingBlock
should be your first choice even for non-asyncio dependencies if the calls are cheap, since it is more efficient.PollingSyncBlock
is only recommended if your calls are slow and synchronous (i.e.: they need a network or IPC communication).
See also
There is multiple examples of each kind of base block usage in i3pyblocks
already. For examples of PollingBlock
check i3pyblocks.blocks.ps
namespace, for example of a
SyncBlock
check
PulseAudioBlock
, for example of a
PollingSyncBlock
check
CaffeineBlock
and for examples of
event-based blocks using Block
check
i3pyblocks.blocks.inotify
namespace.
Handling dependencies
To add a new dependency to i3pyblocks, add it to setup.py
file in
extras_require
section, using the namespace of your module without
i3pyblocks
. For example, if your module depend on foo
version >=1.0
and any version of bar
and it uses the namespace i3pyblocks.blocks.spam
,
add the following to setup.py
:
extras_require={
# ...
"blocks.spam": ["foo>=1.0", "bar"],
}
Don’t forget to add your module to requirements/dev.in
file and run
make deps
to update the dev/CI dependencies.
Collaborating
i3pyblocks use Continuous Integration (CI) to ensure the quality of codebase. We use Black to automatically format the code, Read the Docs to automatically generate the documentation and multiple linters to check possible issues of the code.
Also, writting automated tests are strongly recommended for new blocks since they’re the only way to ensure that we don’t break something in case of changes.
If you want to test your modifications locally, you can use:
$ make
This will run everything that the CI run. If you want to run only tests, use:
$ make test
To run only linters, use:
$ make lint
To automatically fix code issues, run:
$ make lint-fix
But keep in mind that not all issues are fixed automatically, so running
make lint
and fixing the code manually is still necessary in some cases.