Creating new modules - Safturento/stbar GitHub Wiki
Modules have many different components that can be implemented by adding small pieces of code. Lets look at the Lock module as an example and build upon it as we go.
from stbar.modules.module import Module
DEFAULT_CONFIG = {
'Lock': {
}
}
class Lock(Module):
def __init__(self, stbar, parent_bar):
Module.__init__(self, 'Lock', stbar, parent_bar, DEFAULT_CONFIG)
self.setText('')
def init(stbar, parent_bar): return Lock(stbar, parent_bar)
There are 3 main components to this file:
- DEFAULT_CONFIG is a JSON style dictionary that will be merged with user config on module initialization
- The module itself is a class that extends the modules.module.Module which handles most of the initialization automatically. The second parameter in Module.init is the only one that should change, and it contains the name of your module.
- The init function at the bottom is a wrapper that allows the initialization file to handle loading all of the modules. Simply return an object of your class and you're good to go.
Adding the module
To use your newly made module you simple add your python file to ~/.config/stbar/modules
, and add it to one of the bars in your user config file at ~/.config/stbar/config
. More information on the config file found here.
Adding a left-click action
Adding a left click action is very simple, as most of the work is already done in the back end. simple add an on_click function to your class like so:
from stbar.modules.module import Module
DEFAULT_CONFIG = {
'Lock': {
'exec': '~/.config/i3/scripts/i3lock.sh',
}
}
class Lock(Module):
def __init__(self, stbar, parent_bar):
Module.__init__(self, 'Lock', stbar, parent_bar, DEFAULT_CONFIG)
self.setText('')
def on_click(self):
self.exec(self.config[self.name]['exec'])
def init(stbar, parent_bar): return Lock(stbar, parent_bar)
Here you can also see an example of how to access the config file. the command to execute on_click is stored under 'exec', that way a user can override it in the user config without having to touch the actual module.
Adding a right-click menu
We can also create a right click menu and bind functions to items in the menu. To do this begin by calling self.init_menu()
in the __init__
function. We can then call self.add_menu_action(text,self.func)
for every item we want.
from stbar.modules.module import Module
from PySide2.QtWidgets import QMessageBox
DEFAULT_CONFIG = {
'Lock': {
'exec': '~/.config/i3/scripts/i3lock.sh',
}
}
class Lock(Module):
def __init__(self, stbar, parent_bar):
Module.__init__(self, 'Lock', stbar, parent_bar, DEFAULT_CONFIG)
self.setText('')
self.init_menu()
self.add_menu_action('Lock', self.on_click)
self.add_menu_action('Shutdown', self.shutdown)
self.add_menu_action('Restart', self.restart)
self.add_menu_action('Logout', self.logout)
self.dialog = QMessageBox()
self.dialog.setDefaultButton(QMessageBox.No)
self.dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
def on_click(self):
self.exec(self.config[self.name]['exec'])
def shutdown(self):
self.dialog.setText("Shutdown computer?")
if self.dialog.exec() == QMessageBox.Yes:
self.exec('shutdown -h now')
def restart(self):
self.dialog.setText("Restart computer?")
if self.dialog.exec() == QMessageBox.Yes:
self.exec('shutdown -r now')
def logout(self):
self.dialog.setText("Logout?")
if self.dialog.exec() == QMessageBox.Yes:
session = self.exec("loginctl session-status | head -n 1 | awk '{print $1}'")
self.exec('loginctl terminate-session ' + session)
def init(stbar, parent_bar): return Lock(stbar, parent_bar)
Update on interval
If we want the content of the module to update on a given interval, we can also do that very easily. Since Module extends QThread we can have our modules all update asynchronously without stalling. Simple add a run()
function to your class and implement your update code in there. Here we update the clock every second.
from stbar.modules.module import Module
from time import strftime
DEFAULT_CONFIG = {
'Clock': {
'format': '%H:%M:%S',
'interval': 1
}
}
class Clock(Module):
def __init__(self, stbar, parent_bar):
Module.__init__(self, 'Clock', stbar, parent_bar, DEFAULT_CONFIG)
def run(self):
while True:
self.setText(strftime(self.config[self.name]['format']))
self.sleep(self.config[self.name]['interval'])
def init(stbar, parent_bar): return Clock(stbar, parent_bar)
Adding new QWidgets to your Module
Since the modules are QThreads, adding widgets becomes a bit tricky due to the parent (stbar) being on a separate thread. To do this we can use Qt's Signals & Slots to tell the main thread to call our update function. Lets look at how the i3 module handles this.
from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout
from PySide2.QtCore import QThread, Signal
import i3ipc
DEFAULT_CONFIG = {
'I3': {
...
}
}
class I3(QWidget, QThread):
update_signal = Signal(QWidget)
class Workspace(QPushButton):
def __init__(self, i3, ws_info):
QPushButton.__init__(self, i3)
...
...
def __init__(self, stbar, parent_bar):
QWidget.__init__(self, parent_bar)
QThread.__init__(self)
...
self.start()
def run(self):
self.ipc = i3ipc.Connection()
self.ipc.on('workspace::focus', self.on_workspace_focus)
self.ipc.main()
def switch_to_workspace(self, workspace):
...
def update(self):
visible = []
workspaces = self.ipc.get_workspaces()
# Make sure all active workspaces have a button created
for ws_info in self.ipc.get_workspaces():
if ws_info['num'] not in self.buttons:
self.buttons[ws_info['num']] = self.Workspace(self, ws_info)
...
...
def on_workspace_focus(self, i3ipc, event):
self.update_signal.emit(self)
def init(stbar, parent_bar): return I3(stbar, parent_bar)
I've removed most of the code and only left what's relevant to the signals. The first thing to note is that I'm not importing Module, as the main widget of this module is not a button. This means that I have to manually add the Signal using update_signal = Signal(QWidget)
at the top of my class definition, but if your module is importing Module
then this is already done for you. The important components to look at are update function, and the on_workspace_focus
function. on_workspace_focus
is called whenever you change i3 workspaces, which in turn calls Signal.emit
on the update_signal
we created earlier. (remember, if you're extending Module you don't have to create this signal, simple call self.update_signal.emit(self))
whenever you want to update. This signal will then trigger the main thread to call this thread's update function, which will then properly create your new QObjects on the proper thread.
Passing parameters to your update function
If you wish to have a more complex update function and want to pass parameters to it that different based on when or where you emit, you can do so without much of a change. To begin we can override the update_signal that Module
creates and specify the parameters we want to pass. You may find it useful to add a second parameter as a list, as you can then pass anything you want through that list. To do this you would write your module like so:
from PySide2.QtWidgets import QWidget, QPushButton
from PySide2.QtCore import Signal
from stbar.modules.module import Module
class NewModule(Module):
update_signal = Signal(QWidget, list)
def __init__(self, stbar, parent_bar):
Module.__init__(self, 'NewModule', stbar, parent_bar, DEFAULT_CONFIG)
def update(self, args)
def init(stbar, parent_bar): return NewModule(stbar, parent_bar)