Term
Everything to do with terminal output: detecting terminal capabilities, working with colors and themes, formatting text.
This is a low-level API module, upon which :mod:yuio.io
builds
its higher-level abstraction.
Detecting terminal capabilities
Terminal capabilities are stored in a Term
object.
Usually, you don’t need to query terminal capabilities yourself,
as you gan use yuio global configuration from yuio.io
(see yuio.io.get_term()
).
However, you can get a Term
object by using get_term_from_stream()
:
- yuio.term.get_term_from_stream(stream: TextIO, /, *, query_terminal_colors: bool = True) Term [source]
Query info about a terminal attached to the given stream.
If
query_terminal_colors
isTrue
, Yuio will try to query background and foreground color of the terminal.
Term
contains all info about what kinds of things the terminal
supports. If available, it will also have info about terminal’s theme,
i.e. dark or light background, etc:
- class yuio.term.Term(stream: TextIO, color_support: ColorSupport = ColorSupport.NONE, interactive_support: InteractiveSupport = InteractiveSupport.NONE, terminal_colors: TerminalColors | None = None)[source]
Overall info about a terminal.
- color_support: ColorSupport = 0
Terminal’s capability for coloring output.
- interactive_support: InteractiveSupport = 0
Terminal’s capability for rendering interactive widgets.
- terminal_colors: TerminalColors | None = None
Terminal color settings.
- property can_query_terminal: bool
Return
True
if terminal can process queries, enter CBREAK mode, etc.This is an alias to
is_fully_interactive
.
- class yuio.term.Lightness(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
Overall color theme of a terminal.
Can help with deciding which colors to use when printing output.
- UNKNOWN = 1
We couldn’t determine terminal background, or it wasn’t dark or bright enough to fall in one category or another.
- DARK = 2
Terminal background is dark.
- LIGHT = 3
Terminal background is light.
- class yuio.term.ColorSupport(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
Terminal’s capability for coloring output.
- NONE = 0
Color codes are not supported.
- ANSI = 1
Only simple 8-bit color codes are supported.
- ANSI_256 = 2
256-encoded colors are supported.
- ANSI_TRUE = 3
True colors are supported.
- class yuio.term.InteractiveSupport(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]
Terminal’s capability for rendering interactive widgets.
- NONE = 0
Terminal can’t render anything interactive.
- MOVE_CURSOR = 1
Terminal can move cursor and erase lines.
- FULL = 2
Terminal can process queries, enter
CBREAK
mode, etc.
- class yuio.term.TerminalColors(background: ColorValue, black: ColorValue, red: ColorValue, green: ColorValue, yellow: ColorValue, blue: ColorValue, magenta: ColorValue, cyan: ColorValue, white: ColorValue, lightness: Lightness)[source]
Colors and theme of the attached terminal.
- background: ColorValue
Background color of a terminal.
- black: ColorValue
Color value for the default “black” color.
- red: ColorValue
Color value for the default “red” color.
- green: ColorValue
Color value for the default “green” color.
- yellow: ColorValue
Color value for the default “yellow” color.
- blue: ColorValue
Color value for the default “blue” color.
- magenta: ColorValue
Color value for the default “magenta” color.
- cyan: ColorValue
Color value for the default “cyan” color.
- white: ColorValue
Color value for the default “white” color.
Working with colors
Text background and foreground color, as well as its style, is defined
by the Color
class. It stores RGB components and ANSI escape codes
for every aspect of text presentation:
- class yuio.term.Color(fore: ColorValue | None = None, back: ColorValue | None = None, bold: bool | None = None, dim: bool | None = None)[source]
Data about terminal output style. Contains foreground and background color, as well as text styles.
This class only contains data about the color. It doesn’t know anything about how to apply it to a terminal, or whether a terminal supports colors at all. These decisions are thus deferred to a lower level of API (see
Color.as_code()
).When converted to an ANSI code and printed, a color completely overwrites a previous color that was used by a terminal. This behavior prevents different colors and styles bleeding one into another. So, for example, printing
Color.STYLE_BOLD
and thenColor.FORE_RED
will result in non-bold red text.Colors can be combined before printing, though:
>>> Color.STYLE_BOLD | Color.FORE_RED # Bold red Color(fore=<ColorValue 1>, back=None, bold=True, dim=None)
Yuio supports true RGB colors. They are automatically converted to 256- or 8-bit colors if needed.
- fore: ColorValue | None
Foreground color.
- back: ColorValue | None
Background color.
- classmethod fore_from_rgb(r: int, g: int, b: int) Color [source]
Create a foreground color value from rgb components.
Each component should be between 0 and 255.
Example:
>>> Color.fore_from_rgb(0xA0, 0x1E, 0x9C) Color(fore=<ColorValue #A01E9C>, back=None, bold=None, dim=None)
- classmethod fore_from_hex(h: str) Color [source]
Create a foreground color value from a hex string.
Example:
>>> Color.fore_from_hex('#A01E9C') Color(fore=<ColorValue #A01E9C>, back=None, bold=None, dim=None)
- classmethod back_from_rgb(r: int, g: int, b: int) Color [source]
Create a background color value from rgb components.
Each component should be between 0 and 255.
Example:
>>> Color.back_from_rgb(0xA0, 0x1E, 0x9C) Color(fore=None, back=<ColorValue #A01E9C>, bold=None, dim=None)
- classmethod back_from_hex(h: str) Color [source]
Create a background color value from a hex string.
Example:
>>> Color.back_from_hex('#A01E9C') Color(fore=None, back=<ColorValue #A01E9C>, bold=None, dim=None)
- darken(amount: float) Color [source]
Make this color darker by the given percentage.
Amount should be between 0 and 1.
Example:
>>> # Darken by 30%. ... Color.fore_from_hex('#A01E9C').darken(0.30) Color(fore=<ColorValue #70156D>, back=None, bold=None, dim=None)
- lighten(amount: float) Color [source]
Make this color lighter by the given percentage.
Amount should be between 0 and 1.
Example:
>>> # Lighten by 30%. ... Color.fore_from_hex('#A01E9C').lighten(0.30) Color(fore=<ColorValue #DB42D6>, back=None, bold=None, dim=None)
- static lerp(*colors: Color) Callable[[float], Color] [source]
Return a lambda that allows linear interpolation between several colors.
If either color is a single ANSI escape code, the first color is always returned from the lambda.
Example:
>>> a = Color.fore_from_hex('#A01E9C') >>> b = Color.fore_from_hex('#22C60C') >>> lerp = Color.lerp(a, b) >>> lerp(0) Color(fore=<ColorValue #A01E9C>, back=None, bold=None, dim=None) >>> lerp(0.5) Color(fore=<ColorValue #617254>, back=None, bold=None, dim=None) >>> lerp(1) Color(fore=<ColorValue #22C60C>, back=None, bold=None, dim=None)
- as_code(term: Term, /) str [source]
Convert this color into an ANSI escape code with respect to the given terminal capabilities.
- STYLE_NORMAL: ClassVar[Color] = Color(fore=None, back=None, bold=False, dim=False)
Not bold nor dim.
- FORE_NORMAL: ClassVar[Color] = Color(fore=<ColorValue 9>, back=None, bold=None, dim=None)
Normal foreground color.
- FORE_NORMAL_DIM: ClassVar[Color] = Color(fore=<ColorValue 2>, back=None, bold=None, dim=None)
Normal foreground color rendered with dim setting.
This is an alternative to bright black that works with most terminals and color schemes.
- FORE_BLACK: ClassVar[Color] = Color(fore=<ColorValue 0>, back=None, bold=None, dim=None)
Black foreground color.
Avoid using it, in most terminals it is same as background color. Instead, use
FORE_NORMAL_DIM
.
- FORE_RED: ClassVar[Color] = Color(fore=<ColorValue 1>, back=None, bold=None, dim=None)
Red foreground color.
- FORE_GREEN: ClassVar[Color] = Color(fore=<ColorValue 2>, back=None, bold=None, dim=None)
Green foreground color.
- FORE_YELLOW: ClassVar[Color] = Color(fore=<ColorValue 3>, back=None, bold=None, dim=None)
Yellow foreground color.
- FORE_BLUE: ClassVar[Color] = Color(fore=<ColorValue 4>, back=None, bold=None, dim=None)
Blue foreground color.
- FORE_MAGENTA: ClassVar[Color] = Color(fore=<ColorValue 5>, back=None, bold=None, dim=None)
Magenta foreground color.
- FORE_CYAN: ClassVar[Color] = Color(fore=<ColorValue 6>, back=None, bold=None, dim=None)
Cyan foreground color.
- FORE_WHITE: ClassVar[Color] = Color(fore=<ColorValue 7>, back=None, bold=None, dim=None)
White foreground color.
Avoid using it, in some terminals, notably in the Mac OS default terminal, it is unreadable.
- BACK_NORMAL: ClassVar[Color] = Color(fore=None, back=<ColorValue 9>, bold=None, dim=None)
Normal background color.
- BACK_BLACK: ClassVar[Color] = Color(fore=None, back=<ColorValue 0>, bold=None, dim=None)
Black background color.
- BACK_RED: ClassVar[Color] = Color(fore=None, back=<ColorValue 1>, bold=None, dim=None)
Red background color.
- BACK_GREEN: ClassVar[Color] = Color(fore=None, back=<ColorValue 2>, bold=None, dim=None)
Green background color.
- BACK_YELLOW: ClassVar[Color] = Color(fore=None, back=<ColorValue 3>, bold=None, dim=None)
Yellow background color.
- BACK_BLUE: ClassVar[Color] = Color(fore=None, back=<ColorValue 4>, bold=None, dim=None)
Blue background color.
- BACK_MAGENTA: ClassVar[Color] = Color(fore=None, back=<ColorValue 5>, bold=None, dim=None)
Magenta background color.
A single color value is stored in the ColorValue
class.
Usually you don’t need to use these directly, as you will be working
with Color
instances. Nevertheless, you can operate individual
colors should you need such thing:
- class yuio.term.ColorValue(data: int | str | Tuple[int, int, int])[source]
Data about a single color.
Can be either a single ANSI escape code, or an RGB-tuple.
Single ANSI escape code represents a standard terminal color code. The actual color value for it is controlled by the terminal’s user. Therefore, it doesn’t permit operations on colors, such as
Color.darken()
orColor.interpolate()
.An RGB-tuple represents a true color. When displaying on a terminal that doesn’t support true colors, it will be converted to a corresponding 256-color or an 8-color automatically.
- classmethod from_rgb(r: int, g: int, b: int, /) ColorValue [source]
Create a color value from rgb components.
Each component should be between 0 and 255.
Example:
>>> ColorValue.from_rgb(0xA0, 0x1E, 0x9C) <ColorValue #A01E9C>
- classmethod from_hex(h: str, /) ColorValue [source]
Create a color value from a hex string.
Example:
>>> ColorValue.from_hex('#A01E9C') <ColorValue #A01E9C>
- to_hex() str [source]
Return color in hex format with leading
#
.Example:
>>> a = ColorValue.from_hex('#A01E9C') >>> a.to_hex() '#A01E9C'
- darken(amount: float, /) ColorValue [source]
Make this color darker by the given percentage.
Amount should be between 0 and 1.
Example:
>>> # Darken by 30%. ... ColorValue.from_hex('#A01E9C').darken(0.30) <ColorValue #70156D>
- lighten(amount: float, /) ColorValue [source]
Make this color lighter by the given percentage.
Amount should be between 0 and 1.
Example:
>>> # Lighten by 30%. ... ColorValue.from_hex('#A01E9C').lighten(0.30) <ColorValue #DB42D6>
- static lerp(*colors: ColorValue) Callable[[float], ColorValue] [source]
Return a lambda that allows linear interpolation between several colors.
If either color is a single ANSI escape code, the first color is always returned from the lambda.
Example:
>>> a = ColorValue.from_hex('#A01E9C') >>> b = ColorValue.from_hex('#22C60C') >>> lerp = ColorValue.lerp(a, b) >>> lerp(0) <ColorValue #A01E9C> >>> lerp(0.5) <ColorValue #617254> >>> lerp(1) <ColorValue #22C60C>
Coloring and formatting text
The higher-level io
module uses strings with xml-like color
tags to transfer information about line formatting. Here, on the lower level,
these strings are parsed and transformed into ColorizedString
:
- class yuio.term.ColorizedString(content: str | Color | Iterable[Color | str] | ColorizedString = '', /, *, explicit_newline: str = '')[source]
A string with colors.
This class is a wrapper over a list of strings and colors. Each color applies to strings after it, right until the next color.
ColorizedString
supports some basic string operations. Most notable, it supports wide-character-aware wrapping (seeline_width()
), and%
-formatting.Unlike str instances,
ColorizedString
is mutable through the+=
operator.You can build a colorized string from raw parts, or you can use
Theme.colorize()
.- property explicit_newline: str
Explicit newline indicates that a line of a wrapped text was broken because the original text contained a new line character.
See
wrap()
for details.
- property width: int
String width when the string is displayed in a terminal.
See
line_width()
for more information.
- iter() Iterator[Color | str] [source]
Iterate over raw parts of the string, i.e. the underlying list of strings and colors.
- wrap(width: int, /, *, break_on_hyphens: bool = True, preserve_spaces: bool = False, preserve_newlines: bool = True, first_line_indent: str | ColorizedString = '', continuation_indent: str | ColorizedString = '') List[ColorizedString] [source]
Wrap a long line of text into multiple lines.
If break_on_hyphens is True (default), lines can be broken after hyphens in hyphenated words.
If preserve_spaces is True, all spaces are preserved. Otherwise, consecutive spaces are collapsed into a single space. Note that tabs are always treated as a single space.
If preserve_newlines is True (default), text is additionally wrapped on newline characters. When this happens, the newline sequence that wrapped the line will be placed into
explicit_newline
.If preserve_newlines is False, newlines are treated as whitespaces.
If first_line_indent and continuation_indent are given, they are placed in the beginning of respective lines. Passing colorized strings as indents does not break coloring of the wrapped text.
Example:
>>> ColorizedString("hello, world!\nit's a good day!").wrap(13) [<ColorizedString('hello, world!', explicit_newline='\n')>, <ColorizedString("it's a good")>, <ColorizedString('day!')>]
- percent_format(args: Any) ColorizedString [source]
Format colorized string as if with
%
-formatting (i.e. old-style formatting).Example:
>>> line = ColorizedString("Hello, %s!") >>> line % "Username" <ColorizedString('Hello, Username!')>
Utilities
- yuio.term.line_width(s: str, /) int [source]
Calculates string width when the string is displayed in a terminal.
This function makes effort to detect wide characters such as emojis. If does not, however, work correctly with extended grapheme clusters, and so it may fail for emojis with modifiers, or other complex characters.
Example where it fails is
👩🏽💻
. It consists of four code points:Unicode Character WOMAN (
U+1F469
,👩
),Unicode Character EMOJI MODIFIER FITZPATRICK TYPE-4 (
U+1F3FD
),Unicode Character ZERO WIDTH JOINER (
U+200D
),Unicode Character PERSONAL COMPUTER (
U+1F4BB
,💻
).
Since
line_width()
can’t understand that these code points are combined into a single emoji, it treats them separately, resulting in answer 6 (2 for every code point except ZERO WIDTH JOINER):>>> line_width("👩🏽💻") 6
In all fairness, detecting how much space such an emoji will take is not so straight forward, as that will depend on unicode capabilities of a specific terminal. Since a lot of terminals will not handle such emojis correctly, I’ve decided to go with this simplistic implementation for now.
If able, this function uses the
wcwidth
module, as it provides more accurate results.