terminal-widgets

3. Adding new widgets

3.1 Define Configuration (.yaml)

Create the configuration file at ~/.config/twidgets/widgets/custom.yaml

Naming schemes are described here.
You can create an infinite amount of widgets, the file names custom.yaml and custom_widget.py are just examples.

Configure name, title, enabled, interval, height, width, y and x. For simple widgets, set interval = 0 (see Configuration Guide)

3.2 Write the Widget Logic (.py)

Create the widget’s Python file at ~/.config/twidgets/py_widgets/custom_widget.py

Naming schemes are described here.
You can create an infinite amount of widgets, the file names custom.yaml and custom_widget.py are just examples.

Note that the built-in widgets are located in your python installation, at (python_installation_path)/lib/(python-version)/site-packages/twidgets/widgets/*_widget.py

3.2.1 Imports

Import:

from twidgets.core.base import Widget, draw_widget, add_widget_content, Config, UIState, BaseConfig, CursesWindowType

3.2.2 Simple widgets

Then define a draw function:

def draw(widget: Widget, ui_state: UIState, base_config: BaseConfig) -> None:

Start the function with:

draw_widget(widget, ui_state, base_config)

which will initialize the widget title and make it loadable and highlightable.

3.2.3 Add widget content

Add content with:

content: list[str] = ['line1', 'line2', 'line3', 'line4', 'line5']
add_widget_content(widget, content)

Advanced: For precise text positioning or colors in a terminal widget use safe_addstr

from twidgets.core.base import (
    safe_addstr,
    convert_color_number_to_curses_pair,
    CursesBold
)

row: int = 3
col: int = 2
text: str = 'Example text'

safe_addstr(
    widget, row, col, text,
    convert_color_number_to_curses_pair(base_config.PRIMARY_PAIR_NUMBER) | CursesBold)

3.2.4 Widgets with heavy loading

If your widget requires heavy loading, API calls or the data doesn’t need to be reloaded every frame, move the update logic into its own function:

from twidgets.core.base import ConfigLoader
import typing

def update(_widget: Widget, _config_loader: ConfigLoader) -> typing.Any:

Note that widget and config_loader will always be passed to your update function, so make sure to keep those arguments.

And modify the draw function to accept info. (info will be passed automatically from the update function by the scheduler):

def draw(widget: Widget, ui_state: UIState, base_config: BaseConfig, info: typing.Any) -> None:

Example:

def draw(widget: Widget, ui_state: UIState, base_config: BaseConfig, info: list[str]) -> None:
    draw_widget(widget, ui_state, base_config)
    add_widget_content(widget, info)

You can adapt the time, when the update function will be called again (reloading the data) by changing interval in ~/.config/twidgets/widgets/custom.yaml

To integrate this, see building widget.

3.2.5 Custom mouse, keyboard, initialize & help functions

3.2.5.1 Mouse actions

Example:

def mouse_click_action(widget: Widget, _mx: int, _my: int, _b_state: int, ui_state: UIState) -> None:
    # Click relative to widget border
    local_y: int = _my - widget.dimensions.y - 1  # -1 for top border

This function will get called whenever a mouse click happens (in your widget), so you can use it to for example make clickable buttons.

Note that the widget border color will automatically be updated on every mouse click, without utilising your mouse_click_action function.

3.2.5.2 Keyboard actions

Example:

from twidgets.core.base import prompt_user_input, CursesKeys

def keyboard_press_action(widget: Widget, key: typing.Any, ui_state: UIState, base_config: BaseConfig) -> None:
    if key in (CursesKeys.ENTER, 10, 13):  # Enter key + enter key codes
        confirm = prompt_user_input(widget, 'Confirm deletion (y): ')
        if confirm.lower().strip() in ['y']:
            some_func(widget, ...)

This function will get called whenever a key is pressed while your widget is highlighted.

3.2.5.3 Initialize functions

Example:

def init(widget: Widget, _ui_state: UIState, _base_config: BaseConfig) -> None:
    load_todos(widget)

This function will get called initially when twidgets is starting, or when the user manually reloads.

3.2.5.4 Help functions

Example:

def draw_help(widget: Widget, ui_state: UIState, base_config: BaseConfig) -> None:
    draw_widget(widget, ui_state, base_config)

    add_widget_content(
        widget,
        [
            f'Help page ({widget.name} widget)',
            '',
            'Displays information about something.'
        ]
    )

This function will get called whenever the help key (Default: h) is getting pressed on your widget.

3.2.5.5 Integrating custom functions

To integrate any custom function, see building widget.

3.2.6 Using secrets

Import:

from twidgets.core.base import ConfigLoader  # Loading secrets (secrets.env)
import typing

Inside your update function:

def update(_widget: Widget, _config_loader: ConfigLoader) -> typing.Any:

You can now use:

data: typing.Any = _config_loader.get_secret(key)

to get secrets.

Example:

def update(_widget: Widget, _config_loader: ConfigLoader) -> typing.Any:
    api_key: str = _config_loader.get_secret('WEATHER_API_KEY')

Note that this can only be used in the update function, so secrets don’t get reloaded every frame.

3.2.7 Adding custom data to your widget configuration

Example:

Python:

custom_attribute: typing.Any = widget.config.custom_attribute

YAML:

custom_attribute: 'this is a custom attribute!'

Note that this will not be checked by the ConfigScanner. It only checks base.yaml for integrity, as well as name, title, enabled, interval, height, width, y and x for every widget.

To detect if these attributes are missing, see the next section.

3.2.7.1 Config specific Errors

Example:

from twidgets.core.base import (
    ConfigSpecificException,
    LogMessages,
    LogMessage,
    LogLevels
)
def draw(widget: Widget, ui_state: UIState, base_config: BaseConfig) -> None:
    if not widget.config.some_value:  # Will be None if no attribute is found
        raise ConfigSpecificException(LogMessages([LogMessage(
            f'Configuration for some_value is missing / incorrect ("{widget.name}" widget)',
            LogLevels.ERROR.key)]))

With this you can add custom error messages to your widget, for example if certain attributes are missing.

3.2.8 Building widget

If your widget has an update, mouse_click_action, keyboard_press_action, init or a draw_help function, specify them here. (See the comments for examples)

def build(stdscr: CursesWindowType, config: Config) -> Widget:
    return Widget(
        config.name, config.title, config, draw, config.interval, config.dimensions, stdscr,  # exactly this order!
        update_func=None,  # update_func=update
        mouse_click_func=None,  # mouse_click_func=mouse_click_action
        keyboard_func=None,  # keyboard_func=keyboard_press_action
        init_func=None,  # init_func=init
        help_func=None  # help_func=draw_help
    )

3.3 Adding widgets to your layout

While integration is automatic, your files must still follow a specific naming convention for the system to recognize them as a valid widget:

Note: Make sure to name the .yaml and .py files the same way (excluding suffixes)