Create your own
While the default widgets are pretty nifty themselves, sometimes you might wanna do your own thing. Luckily for you, PyTermGUI's Widget API is ridiculously easy to build upon!
What to know
Firstly, you should know some things about the shape and function of various parts of the API.
Output & rendering
The most important method to a widget is its get_lines. This returns a list of strings, which is what is used to eventually display it in the terminal. This method is called a healthy amount, so you probably wanna make all state changes in some other place, so get_lines can request mostly static data.
Selectables
Each widget can be made selectable by either setting widget._selectables_length to a non-zero value, or overwriting the selectables_length property of said widget.
If you want to make parent widgets know that your widget can be selected, you can just define _selectables_length as 1. If you have multiple inner-widgets you need to handle selecting, you should inherit from Container and let it handle all the plumbing for you. If you're interested in how it does all that magic, see it's selectables property, and its select method.
Input handling
Both keyboard and mouse inputs are cascaded down from the WindowManager to each widget. A widget can mark some input as "handled" and stop it from cascading further by having its handler method return True.
Note
The base Widget class may go through some routines for each input, such as calling any widget bindings, or, in the case of mouse inputs, call semantic handlers.
Because of this, and to ensure future backwards-compatibility (weird phrase, I know), you should start all your input handlers by calling the parent class' handler:
class MyWidget(Widget):
    def handle_key(self, key: str) -> bool:
        if super().handle_key(key):
            return
        # Do our own handling
Keyboard input
The signature of the keyboard handler is pretty simple:
def handle_key(self, key: str) -> bool
To compare key codes to canonical names (e.g. CTRL_B to \x02), you can use the keys singleton:
from pytermgui import Widget, keys
class MyWidget(Widget):
    def handle_key(self, key: str) -> bool:
        if super().handle_key(key):
            return True
        if key == keys.CTRL_F:
            self.do_something()
            return True
        return False
Mouse input
While terminal mouse inputs are historically pretty hard to handle, PTG offers a simple interface to hide all that paperwork.
An interface symmetrical to the keyboard handlers exists, and is the base of all that comes below:
def handle_mouse(self, event: MouseEvent) -> bool:
The only argument is the event, which is an instance of MouseEvent. This is what it looks like:
Depending on your circumstances, you can test the event's position or action attributes:
from pytermgui import Widget, MouseEvent, MouseAction
class MyWidget(Widget):
    def handle_mouse(self, event: MouseEvent) -> bool:
        if super().handle_mouse(event):
            return True
        if event.action == MouseAction.LEFT_CLICK:
            self.do_something()
Since comparing against actions gets a li'l tedious over time, we have a system called semantic mouse handlers to help you. These are optional methods that are looked for when Widget is handling some mouse event, and only called when present.
They all follow the syntax on_{event_name}. events can be one of:
[
    "left_click",
    "right_click",
    "click",
    "left_drag",
    "right_drag",
    "drag",
    "scroll_up",
    "scroll_down",
    "scroll",
    "shift_scroll_up",
    "shift_scroll_down",
    "shift_scroll",
    "hover",
]
Handler methods are looked for in the order of highest specifity. For example, the following widget:
from pytermgui import MouseEvent, Widget
class MyWidget(Widget):
    label: str = "No action"
    def get_lines(self) -> list[str]:
        return [self.label]
    def on_left_click(self, event: MouseEvent) -> bool:
        self.label = "Left click"
        return True
    def on_click(self, event: MouseEvent) -> bool:
        self.label = "Generic click"
        return True 
    def handle_mouse(self, event: MouseEvent) -> bool:
        # Make sure we call the super handler
        if super().handle_mouse(event):
            return True
        self.label = "No action"
        return True
...will display Left click only on left clicks, Generic click only on right clicks (as on_right_click isn't defined) and No action on any other mouse input.
FAQ
How do I dynamically update a widget's content?
The pattern I've come to adopt for this purpose is based on a regularly updated inner state that gets displayed within get_lines. For example, let's say we have a Weather widget that regularly requests weather information and displays it. 
Here is how I would do it. Take extra notice of the highlighted lines:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69  |  | 
As you can see, I made the widget inherit Container. This is usually what you want when dealing with a widget that:
- Contains inner content representable by sub-widgets (
Labelin our case) - Has to periodically update said inner content
 
We also use a thread to do all the monitoring & updating, instead of doing it in get_lines or some other periodically called method. Since get_lines is called very regularly, and its time-to-return is critical for the rendering of an application, we need to make sure to avoid putting anything with noticable delays in there.
Don't overuse threads
If you have multiple widgets that run on a thread-based monitor, you are likely better of creating a single master thread that updates every widget periodically. A simple monitor implementation like the following should work alright:
from __future__ import annotations
from dataclasses import dataclass, field
from threading import Thread
from time import sleep, time
from typing import Callable
@dataclass
class Listener:
    callback: Callable[[], None]
    period: float
    time_till_next: float
@dataclass
class Monitor:
    update_frequency: float = 0.5
    listeners: list[Listener] = field(default_factory=list)
    def attach(self, callback: Callable[[], None], *, period: float) -> Listener:
        listener = Listener(callback, period, period)
        self.listeners.append(listener)
        return listener
    def start(self) -> Monitor:
        def _monitor() -> None:
            previous = time()
            while True:
                elapsed = time() - previous
                for listener in self.listeners:
                    listener.time_till_next -= elapsed
                    if listener.time_till_next <= 0.0:
                        listener.callback()
                        listener.time_till_next = listener.period
                previous = time()
                sleep(self.update_frequency)
        Thread(target=_monitor).start()
        return self
You can then use this in your widgets:
from .monitor import Monitor
from pytermgui import Container
monitor = Monitor().start()
class Weather(Container):
    def __init__(self, location: str, timeout: float, **attrs: Any) -> None:
        ...  # Standard init code (see above)
        monitor.attach(self._request_and_update, timeout) 
    def _request_and_update(self) -> None:
        self.data = self._request()
        self.update_content()
Let's talk about those highlighted lines, shall we?
In the first set of lines we send out a request to the (imaginary) external API, and update ourselves accordingly. This update is done in the second set of lines, where the set_widgets method is used to overwrite the current widget selection.
Why use this method instead of manually overwriting _widgets?
The reason this method was created in the first place was to simplify the process of:
- Emptying the container's widgets
 - Resetting its height
 - Going through a list, running 
autoon each item and adding it to the container 
It makes things a lot simpler, and it also accounts for any future oddities that mess with the process!