Widget¶
Basic blocks for building interactive elements.
This is a low-level module upon which yuio.io builds
its higher-level abstraction.
Widget basics¶
All widgets are are derived from the Widget class, where they implement
event handlers, layout and rendering routines. Specifically,
Widget.layout() and Widget.draw() are required to implement
a widget.
- class yuio.widget.Widget¶
Base class for all interactive console elements.
Widgets are displayed with their
run()method. They always go through the same event loop:flowchart TD Start([Start]) --> Layout["`layout()`"] Layout --> Draw["`draw()`"] Draw -->|Wait for keyboard event| Event["`Event()`"] Event --> Result{{Returned result?}} Result -->|no| Layout Result -->|yes| Finish([Finish])Widgets run indefinitely until they stop themselves and return a value. For example,
Inputwill return when user presses Enter. When widget needs to stop, it can return theResult()class from its event handler.For typing purposes,
Widgetis generic. That is,Widget[T]returnsTfrom itsrun()method. So,Input, for example, isWidget[str].Some widgets are
Widget[Never](seetyping.Never), indicating that they don’t ever stop. Others areWidget[None], indicating that they stop, but don’t return a value.- event(
- e: KeyboardEvent,
- /,
Handle incoming keyboard event.
By default, this function dispatches event to handlers registered via
bind(). If no handler is found, it callsdefault_event_handler().
- default_event_handler(
- e: KeyboardEvent,
- /,
Process any event that wasn’t caught by other event handlers.
- abstractmethod layout(rc: RenderContext, /) tuple[int, int]¶
Prepare widget for drawing, and recalculate its dimensions according to new frame dimensions.
Yuio’s widgets always take all available width. They should return their minimum height that they will definitely take, and their maximum height that they can potentially take.
- abstractmethod draw(rc: RenderContext, /)¶
Draw the widget.
Render context’s drawing frame dimensions are guaranteed to be between the minimum and the maximum height returned from the last call to
layout().
- property help_data: WidgetHelp¶
Data for displaying help messages.
See
help()for more info.
- class yuio.widget.Result(value: T_co)¶
Result of a widget run.
We have to wrap the return value of event processors into this class. Otherwise we won’t be able to distinguish between returning None as result of a
Widget[None], and not returning anything.- value: T_co¶
Result of a widget run.
- yuio.widget.bind(
- key: Key | str,
- *,
- ctrl: bool = False,
- alt: bool = False,
- shift: bool = False,
- show_in_inline_help: bool = False,
- show_in_detailed_help: bool = True,
Register an event handler for a widget.
Widget’s methods can be registered as handlers for keyboard events. When a new event comes in, it is checked to match arguments of this decorator. If there is a match, the decorated method is called instead of the
Widget.default_event_handler().Note
Ctrl+L and F1 are always reserved by the widget itself.
If show_in_help is
True, this binding will be shown in the widget’s inline help. If show_in_detailed_help isTrue, this binding will be shown in the widget’s help menu.Example:
class MyWidget(Widget): @bind(Key.ENTER) def enter(self): # all `ENTER` events go here. ... def default_event_handler(self, e: KeyboardEvent): # all non-`ENTER` events go here (including `ALT+ENTER`). ...
- class yuio.widget.Key(*values)¶
Non-character keys.
- ENTER = 1¶
Enter key.
- ESCAPE = 2¶
Escape key.
- INSERT = 3¶
Insert key.
- DELETE = 4¶
Delete key.
- BACKSPACE = 5¶
Backspace key.
- TAB = 6¶
Tab key.
- HOME = 7¶
Home key.
- END = 8¶
End key.
- PAGE_UP = 9¶
PageUp key.
- PAGE_DOWN = 10¶
PageDown key.
- ARROW_UP = 11¶
ArrowUp key.
- ARROW_DOWN = 12¶
ArrowDown key.
- ARROW_LEFT = 13¶
ArrowLeft key.
- ARROW_RIGHT = 14¶
ArrowRight key.
- F1 = 15¶
F1 key.
- F2 = 16¶
F2 key.
- F3 = 17¶
F3 key.
- F4 = 18¶
F4 key.
- F5 = 19¶
F5 key.
- F6 = 20¶
F6 key.
- F7 = 21¶
F7 key.
- F8 = 22¶
F8 key.
- F9 = 23¶
F9 key.
- F10 = 24¶
F10 key.
- F11 = 25¶
F11 key.
- F12 = 26¶
F12 key.
- PASTE = 27¶
Triggered when a text is pasted into a terminal.
- class yuio.widget.KeyboardEvent(
- key: Key | str,
- ctrl: bool = False,
- alt: bool = False,
- shift: bool = False,
- *,
- paste_str: str | None = None,
A single keyboard event.
Warning
Protocol for interacting with terminals is quite old, and not all terminals support all keystroke combinations.
Use
python -m yuio.scripts.showkeyto check how your terminal reports keystrokes, and how Yuio interprets them.- ctrl: bool¶
Whether a Ctrl modifier was pressed with keystroke.
For letter keys modified with control, the letter is always lowercase; if terminal supports reporting Shift being pressed, the
shiftattribute will be set. This does not affect punctuation keys, though:# `Ctrl+X` was pressed. KeyboardEvent("x", ctrl=True) # `Ctrl+Shift+X` was pressed. Not all terminals are able # to report this correctly, though. KeyboardEvent("x", ctrl=True, shift=True) # This can't happen. KeyboardEvent("X", ctrl=True) # `Ctrl+_` was pressed. On most keyboards, the actual keystroke # is `Ctrl+Shift+-`, but most terminals can't properly report this. KeyboardEvent("_", ctrl=True)
- shift: bool¶
Whether a Shift modifier was pressed with keystroke.
Note that, when letters are typed with shift, they will not have this flag. Instead, their upper case version will be set as
key:KeyboardEvent("x") # `X` was pressed. KeyboardEvent("X") # `Shift+X` was pressed.
Warning
Only Shift+Tab can be reliably reported by all terminals.
Drawing and rendering widgets¶
Widgets are rendered through RenderContext. It provides simple facilities
to print characters on screen and manipulate screen cursor.
- final class yuio.widget.RenderContext(term: Term, theme: Theme, /)¶
A canvas onto which widgets render themselves.
This class represents a canvas with size equal to the available space on the terminal. Like a real terminal, it has a character grid and a virtual cursor that can be moved around freely.
Before each render, context’s canvas is cleared, and then widgets print themselves onto it. When render ends, context compares new canvas with what’s been rendered previously, and then updates those parts of the real terminal’s grid that changed between renders.
This approach allows simplifying widgets (they don’t have to track changes and do conditional screen updates themselves), while still minimizing the amount of data that’s sent between the program and the terminal. It is especially helpful with rendering larger widgets over ssh.
- property spinner_state: int¶
A timer that ticks once every
Theme.spinner_update_rate_ms.
- frame( )¶
Override drawing frame.
Widgets are always drawn in the frame’s top-left corner, and they can take the entire frame size.
The idea is that, if you want to draw a widget at specific coordinates, you make a frame and draw the widget inside said frame.
When new frame is created, cursor’s position and color are reset. When frame is dropped, they are restored. Therefore, drawing widgets in a frame will not affect current drawing state.
Example:
>>> rc = RenderContext(term, theme) >>> rc.prepare() >>> # By default, our frame is located at (0, 0)... >>> rc.write("+") >>> # ...and spans the entire canvas. >>> print(rc.width, rc.height) 20 5 >>> # Let's write something at (4, 0). >>> rc.set_pos(4, 0) >>> rc.write("Hello, world!") >>> # Now we set our drawing frame to be at (2, 2). >>> with rc.frame(2, 2): ... # Out current pos was reset to the frame's top-left corner, ... # which is now (2, 2). ... rc.write("+") ... ... # Frame dimensions were automatically reduced. ... print(rc.width, rc.height) ... ... # Set pos and all other functions work relative ... # to the current frame, so writing at (4, 0) ... # in the current frame will result in text at (6, 2). ... rc.set_pos(4, 0) ... rc.write("Hello, world!") 18 3 >>> rc.render() + Hello, world! + Hello, world!
Usually you don’t have to think about frames. If you want to stack multiple widgets one on top of another, simply use
VerticalLayout. In cases where it’s not enough though, you’ll have to calllayout()for each of the nested widgets, and then manually create frames and executedraw()methods:class MyWidget(Widget): # Let's say we want to print a text indented by four spaces, # and limit its with by 15. And we also want to print a small # un-indented heading before it. def __init__(self): # This is the text we'll print. self._nested_widget = Text( "very long paragraph which potentially can span multiple lines" ) def layout(self, rc: RenderContext) -> tuple[int, int]: # The text will be placed at (4, 1), and we'll also limit # its width. So we'll reflect those constrains # by arranging a drawing frame. with rc.frame(4, 1, width=min(rc.width - 4, 15)): min_h, max_h = self._nested_widget.layout(rc) # Our own widget will take as much space as the nested text, # plus one line for our heading. return min_h + 1, max_h + 1 def draw(self, rc: RenderContext): # Print a small heading. rc.set_color_path("bold") rc.write("Small heading") # And draw our nested widget, controlling its position # via a frame. with rc.frame(4, 1, width=min(rc.width - 4, 15)): self._nested_widget.draw(rc)
- new_line()¶
Move cursor to new line within the current frame.
- set_final_pos(x: int, y: int, /)¶
Set position where the cursor should end up after everything has been rendered.
By default, cursor will end up at the beginning of the last line. Components such as
Inputcan modify this behavior and move the cursor into the correct position.
- reset_color()¶
Set current color to the default color of the terminal.
- write( )¶
Write string at the current position using the current color. Move cursor while printing.
While the displayed text will not be clipped at frame’s borders, its width can be limited by passing max_width. Note that
rc.write(text, max_width)is not the same asrc.write(text[:max_width]), because the later case doesn’t account for double-width characters.All whitespace characters in the text, including tabs and newlines, will be treated as single spaces. If you need to print multiline text, use
yuio.string.ColorizedString.wrap()andwrite_text().Example:
>>> rc = RenderContext(term, theme) >>> rc.prepare() >>> rc.write("Hello, world!") >>> rc.new_line() >>> rc.write("Hello,\nworld!") >>> rc.new_line() >>> rc.write( ... "Hello, 🌍!<this text will be clipped>", ... max_width=10 ... ) >>> rc.new_line() >>> rc.write( ... "Hello, 🌍!<this text will be clipped>"[:10] ... ) >>> rc.new_line() >>> rc.render() Hello, world! Hello, world! Hello, 🌍! Hello, 🌍!<
Notice that
"\n"on the second line was replaced with a space. Notice also that the last line wasn’t properly clipped.
- write_text( )¶
Write multiple lines.
Each line is printed using
write(), so newline characters and tabs within each line are replaced with spaces. Useyuio.string.ColorizedString.wrap()to properly handle them.After each line, the cursor is moved one line down, and back to its original horizontal position.
Example:
>>> rc = RenderContext(term, theme) >>> rc.prepare() >>> # Cursor is at (0, 0). >>> rc.write("+ > ") >>> # First line is printed at the cursor's position. >>> # All consequent lines are horizontally aligned with first line. >>> rc.write_text(["Hello,", "world!"]) >>> # Cursor is at the last line. >>> rc.write("+") >>> rc.render() + > Hello, world!+
- bell()¶
Ring a terminal bell.
- make_repr_context(
- *,
- multiline: bool | None = None,
- highlighted: bool | None = None,
- max_depth: int | None = None,
- width: int | None = None,
Create a new
ReprContextfor rendering colorized strings inside widgets.- Parameters:
multiline – sets initial value for
ReprContext.multiline.highlighted – sets initial value for
ReprContext.highlighted.max_depth – sets initial value for
ReprContext.max_depth.width – sets initial value for
ReprContext.width. If not given, uses current frame’s width.
- Returns:
a new repr context suitable for rendering colorized strings.
- prepare( )¶
Reset output canvas and prepare context for a new round of widget formatting.
- clear_screen()¶
Clear screen and prepare for a full redraw.
- render()¶
Render current canvas onto the terminal.
- finalize()¶
Erase any rendered widget and move cursor to the initial position.
Stacking widgets together¶
To get help with drawing multiple widgets and setting their own frames,
you can use the VerticalLayout class:
- class yuio.widget.VerticalLayout(*widgets: Widget[object])¶
Helper class for stacking widgets together.
You can stack your widgets together, then calculate their layout and draw them all at once.
You can use this class as a helper component inside your own widgets, or you can use it as a standalone widget. See
VerticalLayoutBuilderfor an example.- event(
- e: KeyboardEvent,
Dispatch event to the widget that was added with
receive_events=True.See
VerticalLayoutBuilderfor details.
- layout(rc: RenderContext, /) tuple[int, int]¶
Calculate layout of the entire stack.
- draw(rc: RenderContext, /)¶
Draw the stack according to the calculated layout and available height.
- final class yuio.widget.VerticalLayoutBuilder¶
Builder for
VerticalLayoutthat allows for precise control of keyboard events.By default,
VerticalLayoutdoes not handle incoming keyboard events. However, you can createVerticalLayoutthat forwards all keyboard events to a particular widget within the stack:widget = VerticalLayout.builder() \ .add(Line("Enter something:")) \ .add(Input(), receive_events=True) \ .build() result = widget.run(term, theme)
- add( ) Any¶
Add a new widget to the bottom of the layout.
If receive_events is True, all incoming events will be forwarded to the added widget. Only the latest widget added with
receive_events=Truewill receive events.This method does not mutate the builder, but instead returns a new one. Use it with method chaining.
Widget help¶
Widgets automatically generate help: the help menu is available via the F1 key, and there’s also inline help that is displayed under the widget.
By default, help items are generated from event handler docstrings: all event handlers that have them will be displayed in the help menu.
You can control which keybindings appear in the help menu and inline help
by supplying show_in_inline_help and show_in_detailed_help arguments
to the bind() function.
For even more detailed customization you can decorate an event handler with
the help() decorator:
- yuio.widget.help(
- *,
- group: str = 'Actions',
- inline_msg: str | None = None,
- long_msg: str | None = None,
- msg: str | None = None,
Set options for how this callback should be displayed.
This decorator controls automatic generation of help messages for a widget.
- Parameters:
group – title of a group that this action will appear in when the user opens a help menu. Groups appear in order of declaration of their first element.
inline_msg – this parameter overrides a message in the inline help. By default, it will be taken from a docstring.
long_msg – this parameter overrides a message in the help menu. By default, it will be taken from a docstring.
msg – a shortcut parameter for setting both inline_msg and long_msg at the same time.
Example:
class MyWidget(Widget): NAVIGATE = "Navigate" @bind(Key.TAB) @help(group=NAVIGATE) def tab(self): """next item""" ... @bind(Key.TAB, shift=True) @help(group=NAVIGATE) def shift_tab(self): """previous item""" ...
Lastly, you can override Widget.help_data and generate
the WidgetHelp yourself:
- class yuio.widget.WidgetHelp( )¶
Data for automatic help generation.
Warning
Do not modify contents of this class in-place. This might break layout caching in the widget rendering routine, which will cause displaying outdated help messages.
Use the provided helpers to modify contents of this class.
- with_action(
- *bindings: _Binding | ActionKey,
- group: str = 'Actions',
- msg: str | None = None,
- inline_msg: str | None = None,
- long_msg: str | None = None,
- prepend: bool = False,
- prepend_group: bool = False,
Return a new
WidgetHelpthat has an extra action.- Parameters:
bindings – keys that trigger an action.
group – title of a group that this action will appear in when the user opens a help menu. Groups appear in order of declaration of their first element.
inline_msg – this parameter overrides a message in the inline help. By default, it will be taken from a docstring.
long_msg – this parameter overrides a message in the help menu. By default, it will be taken from a docstring.
msg – a shortcut parameter for setting both inline_msg and long_msg at the same time.
prepend – if
True, action will be added to the beginning of its group.prepend_group – if
True, group will be added to the beginning of the help menu.
- merge(other: WidgetHelp, /) WidgetHelp¶
Merge this help data with another one and return a new instance of
WidgetHelp.- Parameters:
other – other
WidgetHelpfor merging.
- without_group(title: str, /) WidgetHelp¶
Return a new
WidgetHelpthat has a group with the given title removed.- Parameters:
title – title to remove.
- rename_group( ) WidgetHelp¶
Return a new
WidgetHelpthat has a group with the given title renamed.- Parameters:
title – title to replace.
new_title – new title.
- class yuio.widget.ActionKey¶
A single key associated with an action. Can be either a hotkey or a string with an arbitrary description.
- class yuio.widget.ActionKeys¶
A list of keys associated with an action.
- class yuio.widget.Action¶
An action itself, i.e. a set of hotkeys and a description for them.
Pre-defined widgets¶
- class yuio.widget.Input(
- *,
- text: str = '',
- pos: int | None = None,
- placeholder: str = '',
- decoration_path: str = 'menu/input/decoration',
- allow_multiline: bool = False,
- allow_special_characters: bool = False,
An input box.
Note
Inputis not optimized to handle long texts or long editing sessions. It’s best used to get relatively short answers from users withyuio.io.ask(). If you need to edit large text, especially multiline, consider usingyuio.io.edit()instead.- Parameters:
text – initial text.
pos – initial cursor position, calculated as an offset from beginning of the text. Should be
0 <= pos <= len(text).placeholder – placeholder text, shown when input is empty.
decoration_path – path that will be used to look up decoration printed before the input box.
allow_multiline – if True, Enter key makes a new line, otherwise it accepts input. In this mode, newlines in pasted text are also preserved.
allow_special_characters – If True, special characters like tabs or escape symbols are preserved and not replaced with whitespaces.
- class yuio.widget.SecretInput(
- *,
- text: str = '',
- pos: int | None = None,
- placeholder: str = '',
- decoration_path: str = 'menu/input/decoration',
An input box that shows stars instead of entered symbols.
- Parameters:
text – initial text.
pos – initial cursor position, calculated as an offset from beginning of the text. Should be
0 <= pos <= len(text).placeholder – placeholder text, shown when input is empty.
decoration – decoration printed before the input box.
- class yuio.widget.Grid(
- options: list[Option[T]],
- /,
- *,
- active_item_decoration_path: str = 'menu/choice/decoration/active_item',
- selected_item_decoration_path: str = '',
- deselected_item_decoration_path: str = '',
- default_index: int | None = 0,
- min_rows: int | None = 5,
A helper widget that shows up in
ChoiceandInputWithCompletion.Note
On its own,
Griddoesn’t return when you press Enter or Ctrl+D. It’s meant to be used as part of another widget.- Parameters:
options – list of options displayed in the grid.
decoration – decoration printed before the selected option.
default_index – index of the initially selected option.
min_rows – minimum number of rows that the grid should occupy before it starts splitting options into columns. This option is ignored if there isn’t enough space on the screen.
- class yuio.widget.Option(
- value: T_co,
- display_text: str,
- *,
- display_text_prefix: str = '',
- display_text_suffix: str = '',
- comment: str | None = None,
- color_tag: str | None = None,
- selected: bool = False,
An option for the
GridandChoicewidgets.- value: T_co¶
Option’s value that will be returned from widget.
- display_text_prefix: str¶
Prefix that will be displayed before
display_text.
- display_text_suffix: str¶
Suffix that will be displayed after
display_text.
- class yuio.widget.Choice(options: list[~yuio.widget.Option[~yuio.widget.T]], /, *, mapper: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T]], str] = <function Choice.<lambda>>, filter: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T], str], bool] | None = None, default_index: int = 0, search_bar_decoration_path: str = 'menu/input/decoration_search', active_item_decoration_path: str = 'menu/choice/decoration/active_item')¶
Allows choosing from pre-defined options.
- Parameters:
options – list of choice options.
mapper – maps option to a text that will be used for filtering. By default, uses
Option.display_text. This argument is ignored if a custom filter is given.filter – customizes behavior of list filtering. The default filter extracts text from an option using the mapper, and checks if it starts with the search query.
default_index – index of the initially selected option.
- class yuio.widget.Multiselect(options: list[~yuio.widget.Option[~yuio.widget.T]], /, *, mapper: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T]], str] = <function Multiselect.<lambda>>, filter: ~typing.Callable[[~yuio.widget.Option[~yuio.widget.T], str], bool] | None = None, search_bar_decoration_path: str = 'menu/input/decoration_search', active_item_decoration_path: str = 'menu/choice/decoration/active_item', selected_item_decoration_path: str = 'menu/choice/decoration/selected_item', deselected_item_decoration_path: str = 'menu/choice/decoration/deselected_item')¶
Like
Choice, but allows selecting multiple items.- Parameters:
options – list of choice options.
mapper – maps option to a text that will be used for filtering. By default, uses
Option.display_text. This argument is ignored if a custom filter is given.filter – customizes behavior of list filtering. The default filter extracts text from an option using the mapper, and checks if it starts with the search query.
default_index – index of the initially selected option.
- class yuio.widget.InputWithCompletion(
- completer: Completer,
- /,
- *,
- placeholder: str = '',
- decoration_path: str = 'menu/input/decoration',
- active_item_decoration_path: str = 'menu/choice/decoration/active_item',
An input box with tab completion.
- class yuio.widget.Map( )¶
A wrapper that maps result of the given widget using the given function.
Example:
>>> # Run `Input` widget, then parse user input as `int`. >>> int_input = Map(Input(), int) >>> int_input.run(term, theme) 10
- class yuio.widget.Apply( )¶
A wrapper that applies the given function to the result of a wrapped widget.
Example:
>>> # Run `Input` widget, then print its output before returning >>> print_output = Apply(Input(), print) >>> result = print_output.run(term, theme) foobar! >>> result 'foobar!'
- class yuio.widget.Task(msg: str, /, *args, comment: str | None = None)¶
Widget that’s used to render
Tasks.- class Status(*values)¶
Task status.
- DONE = 'done'¶
Task has finished successfully.
- ERROR = 'error'¶
Task has finished with an error.
- RUNNING = 'running'¶
Task is running.
- PENDING = 'pending'¶
Task is waiting to start.
- progress_size( )¶
See
progress_size().
- progress_scale( )¶
See
progress_scale().
- _format_task(ctx: ReprContext) ColorizedString¶
Format this task for printing to the log.
- _format_task_msg(
- ctx: ReprContext,
Format task’s message.
- _format_task_comment(
- rc: RenderContext,
Format task’s comment.
- _draw_task(rc: RenderContext)¶
Draw task.
- _draw_task_progress(rc: RenderContext)¶
Draw number that indicates task’s progress.
- _draw_task_progressbar(rc: RenderContext)¶
Draw task’s progressbar.