Skip to content

xnano.terminal

xnano.terminal

xnano.terminal

A canvas on your screen, and the function that opens one.

Classes:

  • Terminal –

    A canvas that draws on your screen and reads your keyboard and mouse.

Functions:

  • open –

    Put a canvas on your screen and return it as a Terminal.

Terminal

Terminal(*, width: int | str | None, height: int | str | None, fps: float, bpm: float, is_realtime: bool, deps: object, state: object, title: str | None, gates: Mapping[str, object], appearance: Mapping[str, object], effects: Sequence[RenderableEffectSpecification], enclosing: Canvas[object, object] | None = None, layout: CanvasLayout = 'overlay', edges: EdgeBehavior = 'clip')

Bases: Canvas[DepsT, StateT]

A canvas that draws on your screen and reads your keyboard and mouse.

A canvas becomes a terminal when it is opened, with open or Canvas.open: it is the same object, and isinstance(canvas, xnano.Terminal) is how to ask whether a canvas is on the screen. It has every member of Canvas. It runs by itself: opening it starts a loop on a thread of its own, and everything you write, remove or draw appears the moment it is declared, however slowly the rest of your program runs, because declaring only changes buffers and the loop draws them. A program has one screen, so only one terminal is open at a time.

Attributes:

  • is_closed (bool) –

    Whether the terminal has been closed, or its loop has ended.

  • is_running (bool) –

    Whether the loop is running and has not been asked to end.

Examples:

with xnano.open(width=20, height=2) as terminal:
    terminal.write('hello')
    assert isinstance(terminal, xnano.Terminal)
    terminal.run(quit_on='q')

Create a canvas. Use new instead of calling this.

Parameters:

  • width (int | str | None) –

    The width in cells or as a percentage of the window, or None for 80.

  • height (int | str | None) –

    The height in cells or as a percentage of the window, or None for 24.

  • fps (float) –

    The most frames drawn per second.

  • bpm (float) –

    Beats per minute.

  • is_realtime (bool) –

    Whether the canvas runs on the wall clock, on a thread of its own.

  • deps (object) –

    The dependencies, or the unset marker when none were given.

  • state (object) –

    A dataclass instance the canvas keeps and validates, or the unset marker.

  • title (str | None) –

    The window title, or None.

  • gates (Mapping[str, object]) –

    The input gates that were given, by name.

  • appearance (Mapping[str, object]) –

    The appearance keywords of the canvas itself, by name.

  • effects (Sequence[RenderableEffectSpecification]) –

    Effects to apply to the canvas, as dictionaries with a kind key.

  • enclosing (Canvas[object, object] | None, default: None ) –

    The canvas whose with block this one was created in, which its sizes are measured in and which closes it, or None.

  • layout (CanvasLayout, default: 'overlay' ) –

    How the canvas places what is written on it.

  • edges (EdgeBehavior, default: 'clip' ) –

    What content written on the canvas does at the edges of its area.

Methods:

  • __setattr__ –

    Set an attribute, or raise AttributeError for an unknown public name.

  • bold –

    Make the text bold, or undo it with False; the same as setting is_bold.

  • dim –

    Dim the text, or undo it with False; the same as setting is_dim.

  • italic –

    Make the text italic, or undo it with False; the same as setting is_italic.

  • underline –

    Underline the text, or undo it with False; the same as setting is_underlined.

  • blink –

    Make the text blink, or undo it with False; the same as setting is_blinking.

  • reverse –

    Swap the foreground and background, or undo it with False; the same as setting is_reversed.

  • hidden –

    Hide the text, or undo it with False; the same as setting is_hidden.

  • crossed_out –

    Strike the text through, or undo it with False; the same as setting is_crossed_out.

  • hook –

    Call a function when something happens on this canvas or placement.

  • __new__ –

    Refuse a direct call: a canvas is created with xnano.new.

  • open –

    Put this canvas on your screen, and return it as a Terminal.

  • close –

    Release everything the canvas holds, and give the screen back if it has one.

  • local –

    Convert a cell of the screen into a cell of this canvas.

  • move –

    Move a canvas that is written into another one by a number of cells along each axis.

  • up –

    Move a written canvas up by count rows; see move.

  • down –

    Move a written canvas down by count rows; see move.

  • left –

    Move a written canvas left by count columns; see move.

  • right –

    Move a written canvas right by count columns; see move.

  • new –

    Create a canvas inside this one and draw it here at once, at the top-left corner.

  • write –

    Put something on the canvas: text, a widget, a buffer, another canvas or an image.

  • clear –

    Erase everything written or drawn directly on the canvas's cells.

  • update –

    Change the canvas's own cells with operations, styles or effects.

  • __or__ –

    Apply an operation, customization or style with canvas | operation.

  • __ior__ –

    Apply an operation, customization or style in place with canvas |= operation.

  • inspect –

    Evaluate an expression against the canvas and return its value.

  • iter –

    Iterate over the canvas's frames, drawing between them; pass count= or seconds=,

  • step –

    Move time by delta_seconds, one frame by default, and draw exactly one frame.

  • deliver_events –

    Deliver pushed events to their hooks and draw a frame, without moving time.

  • stop –

    Freeze time: the same as Time.stop.

  • resume –

    Let time run on from where stop froze it.

  • quit –

    Ask the canvas's loop to end.

  • run –

    Wait until the canvas's loop ends.

  • wait_for_key –

    Wait until a key is pressed, and return its name.

  • prompt –

    Prompt for a line of text, and return what was typed.

  • render –

    Draw the current frame and return it, without a screen.

  • push –

    Deliver an event to the canvas as if it had been typed, clicked or reported.

  • play –

    Play a note, a sound, a symbol name, a melody or a sequence of those, and return at once.

  • state_reference –

    A core reference for one field of the canvas's state.

Source code in xnano/canvas.py
def __init__(
    self,
    *,
    width: int | str | None,
    height: int | str | None,
    fps: float,
    bpm: float,
    is_realtime: bool,
    deps: object,
    state: object,
    title: str | None,
    gates: Mapping[str, object],
    appearance: Mapping[str, object],
    effects: Sequence[RenderableEffectSpecification],
    enclosing: Canvas[object, object] | None = None,
    layout: CanvasLayout = 'overlay',
    edges: EdgeBehavior = 'clip',
) -> None:
    """
    Create a canvas. Use [`new`][xnano.new] instead of calling this.

    Args:
        width: The width in cells or as a percentage of the window, or ``None`` for 80.
        height: The height in cells or as a percentage of the window, or ``None`` for 24.
        fps: The most frames drawn per second.
        bpm: Beats per minute.
        is_realtime: Whether the canvas runs on the wall clock, on a thread of its own.
        deps: The dependencies, or the unset marker when none were given.
        state: A dataclass instance the canvas keeps and validates, or the unset marker.
        title: The window title, or ``None``.
        gates: The input gates that were given, by name.
        appearance: The appearance keywords of the canvas itself, by name.
        effects: Effects to apply to the canvas, as dictionaries with a ``kind`` key.
        enclosing: The canvas whose ``with`` block this one was created in, which its
            sizes are measured in and which closes it, or ``None``.
        layout: How the canvas places what is written on it.
        edges: What content written on the canvas does at the edges of its area.
    """
    super().__init__()
    self._canvas = self
    self._root_canvas = self
    self._lock = threading.RLock()
    self._root = _core.Buffer()
    self._buffer = self._root
    self._fps = fps
    self._bpm = bpm
    self._is_realtime = is_realtime
    self._requested_width = width
    self._requested_height = height
    self._deps_given = deps is not _UNSET
    self._deps = cast('DepsT', None if deps is _UNSET else deps)
    self._state_given = state is not _UNSET
    self._title = title
    self._gates: dict[str, object] = dict(gates)
    self._resolved_gates: dict[str, object] = {}
    self._appearance = (
        Appearance(**cast('dict[str, object]', _plain_appearance(appearance)))  # type: ignore[arg-type]
        if appearance
        else None
    )
    self._decorated: Decorated | None = None
    self._dirty_looks: dict[int, _Looked] = {}
    self._placements: list[Placement[DepsT, StateT]] = []
    self._ledger: list[_Registration] = []
    self._effects: list[
        tuple[Customization, Canvas[object, object] | Placement[object, object]]
    ] = []
    self._frame_listeners: dict[int, Callable[[FrameContext], None]] = {}
    self._frame_listener_tokens = itertools.count()
    self._frame_demand = 0
    self._frame_registration: _Registration | None = None
    self._size_hook: _core.Hook | None = None
    self._parent_placement: Placement[object, object] | None = None
    self._children: list[Canvas[object, object]] = []
    self._enclosing: weakref.ref[Canvas[object, object]] | None = None
    self._enclosing_size: tuple[int, int] | None = None
    self._nested_canvases: list[weakref.ref[Canvas[object, object]]] = []
    self._areas: dict[int, Placement[object, object]] = {}
    self._is_hidden_until_written = False
    self._layout: CanvasLayout = layout
    self._edges: EdgeBehavior = edges
    self._slots: list[
        tuple[Placement[object, object] | None, _core.Buffer, _core.SizeRule]
    ] = []
    self._is_layout_pending = False
    self._pending_layouts: dict[int, Canvas[object, object]] = {}
    if enclosing is not None:
        self._enclosing = weakref.ref(enclosing)
        self._enclosing_size = (enclosing.width, enclosing.height)
        with enclosing._lock:
            enclosing._nested_canvases.append(
                weakref.ref(cast('Canvas[object, object]', self))
            )
    self._is_closed = False
    self._is_finalized = False
    self._stop_requested = threading.Event()
    self._failure: BaseException | None = None
    self._is_failure_reported = False
    self._failure_report = ''
    self._moved = 0
    self._carried: dict[str, object] | None = None
    self._cursor_follow: tuple[_core.Buffer, int, int] | None = None
    self._state_namespace: StateNamespace | None = None
    self._state_fields: dict[str, object] = {}
    self._state_store: _core.TerminalStateStore | None = None
    if state is not _UNSET and state is not None:
        self._state_store = _prepare_state(cast('DataclassInstance', state))
        self._state_fields = _state_field_types(cast('DataclassInstance', state))
    self._state_followers: _StateFollowers | None = None
    self._state_subscription: _core.TerminalStateSubscription | None = None
    self._state_hook: _core.Hook | None = None
    self._state_marked: dict[_StateFollower, _core.TerminalStateChange] = {}
    self._internal_revisions: deque[int] = deque(maxlen=16)
    self._namespace_identifiers = itertools.count(1)
    self._active_run_by_thread = threading.local()
    self._images: dict[int, ImageSource] = {}
    # Only placements with a formula or a range need looking at before each frame.
    self._dynamic: dict[int, Placement[DepsT, StateT]] = {}
    self._time = Time(cast('Canvas[object, object]', self))
    self._cursor_view = Cursor(cast('Canvas[object, object]', self))
    self.output = Output(cast('Canvas[object, object]', self))
    self._audio: AudioDevice | None = None
    self._input: Input | None = None
    self._runtime_object: _core.Runtime | None = None
    self._clock: _core.Clock | None = None
    self._cursor: _core.Cursor | None = None
    self._input_state: _core.InputState | None = None
    self._pushed_pointer: tuple[tuple[int, int], object, int] | None = None
    self._size = _core.Size(0, 0)
    self._previous_chain = tuple(
        reference for reference in _active_canvas.get() if reference() is not None
    )
    _active_canvas.set(
        (weakref.ref(cast('Canvas[object, object]', self)), *self._previous_chain)
    )
    self._host: _Host
    columns, rows = _canvas_size(width, height, self._enclosing_size)
    interval_milliseconds = max(1, round(1000.0 / fps))

    def create_runtime() -> _core.Runtime:
        runtime = _core.Runtime.offscreen(
            width=columns,
            height=rows,
            frame_interval_milliseconds=interval_milliseconds,
            clock=_core.Clock.monotonic() if is_realtime else None,
        )
        if title is not None:
            runtime.title = title
        return runtime

    try:
        if is_realtime:
            _ThreadHost(cast('Canvas[object, object]', self), create_runtime)
        else:
            _InlineHost(cast('Canvas[object, object]', self), create_runtime)
    except BaseException:
        _active_canvas.set(self._previous_chain)
        self._finalize()
        raise
    try:
        if effects:
            apply_effects(cast('Canvas[object, object]', self), effects)
    except BaseException:
        self.close(raise_failure=False)
        raise
    self._renderable_effects = RenderableEffects(cast('Canvas[object, object]', self))
    self._seal()

region property

region: Region

The cells of the canvas as a Region: read and write canvas.region[x, y].

The default resolution is one cell. canvas.region.half_blocks addresses two pixels per cell and canvas.region.braille eight dots.

Examples:

import xnano

with xnano.new(width=4, height=2) as canvas:
    canvas.region[0, 0] = xnano.Pixel('diamond')
    canvas.region.half_blocks[1, 0] = (255, 0, 0)
    assert canvas.region[0, 0].character == '◆'
    assert canvas.region.half_blocks.height == 4

color property

color: Colors

The colors: color.fg for the text and color.bg for what is behind it.

Each is a ColorSlot. Assign a color name, an (red, green, blue) tuple, a Color or another slot; 'reset' clears the color.

Examples:

import xnano

with xnano.new(width=10, height=3) as canvas:
    label = canvas.write('hi')
    label.color.fg = 'red'
    label.color.bg = (0, 0, 80)
    label.color.bg.lighten(0.1)
    assert label.color.fg.value == xnano.Color('red')

border property writable

The border: its kind, a setting for each side, its color and its title.

Assign 'plain', 'rounded', 'double' or 'thick' to draw a border on every side, True for 'plain' and False to remove it. It works on any content, not only on text.

The border, padding and margin are drawn around the content, so the placement grows by the cells they take. A widget with no size of its own is shrunk to fit its parent, border included.

Examples:

import xnano

with xnano.new(width=12, height=5) as canvas:
    card = canvas.write('hi', border='rounded')
    card.border.top = 'double'
    card.border.left = False
    card.border.color = 'cyan'
    card.border = False
    assert not card.border

padding property writable

The space between the border and the content, edge by edge.

Assign a number of cells, a percentage such as '10%' or a dictionary such as {'vertical': 1, 'horizontal': 2}, or set one edge with padding.left = 2.

margin property writable

The transparent space outside the border, edge by edge, set like padding.

is_bold property writable

is_bold: bool

Whether the text is bold.

is_dim property writable

is_dim: bool

Whether the text is dimmed.

is_italic property writable

is_italic: bool

Whether the text is italic.

is_underlined property writable

is_underlined: bool

Whether the text is underlined.

is_blinking property writable

is_blinking: bool

Whether the text blinks.

is_reversed property writable

is_reversed: bool

Whether foreground and background are swapped.

is_hidden property writable

is_hidden: bool

Whether the text is hidden.

is_crossed_out property writable

is_crossed_out: bool

Whether the text is struck through.

output instance-attribute

output: Output = Output(cast('Canvas[object, object]', self))

The canvas's drawn frames and captured text.

is_closed property

is_closed: bool

Whether the canvas has been torn down: closed, or its loop has ended.

is_running property

is_running: bool

Whether the canvas is open and its loop has not been asked to end.

core_buffer property

core_buffer: Buffer

The core buffer the canvas draws, as large as the canvas.

Everything you write is a child of this buffer, and everything drawn directly on the canvas is stored in it. A canvas with a border, padding or margin draws them around a buffer of its own; this is the buffer inside them.

parent property

Symbolic expressions for the parent container's width, height and size.

root property

Symbolic expressions for the root canvas's width, height, size and time.

size property

size: Size

The size of the canvas in cells.

width property

width: int

The width of the canvas in cells.

height property

height: int

The height of the canvas in cells.

x property

x: int

The column of the canvas: 0, unless it was written into another canvas.

y property

y: int

The row of the canvas: 0, unless it was written into another canvas.

fps property

fps: float

The most frames drawn per second, and the rate iter aims for.

bpm property writable

bpm: float

Beats per minute: the length of a beat for beat timers, notes and melodies.

It is 120 unless new(bpm=...) said otherwise, and it can change while things are playing; the change applies from the next beat.

Raises:

  • ValueError –

    The tempo is not above 0 and at most 600000.

deps property

deps: DepsT

The dependencies given to new(deps=...), or None. Hooks read them.

title property writable

title: str | None

The window title last set on this canvas.

Assign a single line of printable text to set the title of the window a canvas on the screen draws in, or None to clear it. A canvas without a screen keeps the value and shows it when it is opened.

time property

time: Time

The canvas's Time: how long it has run, stopping, and formulas.

cursor property

cursor: Cursor

The canvas's Cursor.

A canvas has exactly one cursor. Set its position (a cell, or None to hide it), shape, is_blinking and is_visible, move it with up, down, left, right and move, or follow a placement. It can be changed from any thread, and on the screen the cursor you had before is restored when the canvas closes.

Examples:

import xnano

with xnano.new(width=10, height=2) as canvas:
    canvas.cursor.position = (3, 0)
    canvas.cursor.shape = 'bar'
    assert canvas.cursor.position.x == 3

layout property

layout: CanvasLayout

How the canvas places what is written on it, chosen when the canvas is created.

'overlay' places every piece on its own over the whole canvas. 'column' stacks the pieces from top to bottom in the order they are written, and 'row' from left to right. Each piece then takes a slot: one with a size on the stacking axis (height in a column, width in a row, in cells or as a percentage) gets exactly that, and the others share what is left equally. Inside its slot a piece is positioned as on any canvas, so y='center' and y='50%' are measured in the slot. Removing a piece gives its slot back. A canvas created inside the with block of a stacking canvas takes a slot only once it is written.

Examples:

import xnano

with xnano.new(width=20, height=10, layout='column') as terminal:
    terminal.write('top half', y='center')
    panel = terminal.new(height='50%')
    panel.write('bottom half')
    rows = terminal.render().splitlines()
    assert (rows[2], rows[5]) == ('top half', 'bottom half')

edges property writable

edges: EdgeBehavior

What content written on the canvas does at the edges of its area, unless given its own.

'clip' lets it move past the edge, where it is not drawn; 'stop' holds it inside its area when it moves; 'wrap' continues text on the next rows of its area. A piece written with edges=... keeps its own; see Placement.edges.

Raises:

  • ValueError –

    The value is not 'clip', 'stop' or 'wrap'.

audio property

audio: AudioDevice

The canvas's AudioDevice: notes, melodies, sounds and the microphone.

It needs the optional xnano-audio package, which is imported the first time this is read.

Raises:

  • ImportError –

    xnano-audio is not installed. Install it with pip install "xnano[audio]".

fx property

The visual effects of the canvas itself, one method for each effect.

canvas.fx.fade_to(background='navy') starts a fade over the whole canvas at once, and canvas.fx.clear() stops what was started here. A placement has its own fx, and the sound effects of what the canvas plays are canvas.audio.fx. See RenderableEffects.

Examples:

import xnano

with xnano.new('hello', width=8, height=1) as canvas:
    canvas.fx.dissolve(duration_milliseconds=300)
    assert len(canvas.fx.active) == 1

input property

input: Input

The keys held down right now, for programs that poll instead of listening.

Read it anywhere, from any thread. A canvas opened with keyboard='release_events' knows exactly when a key is let go.

state class-attribute instance-attribute

state = _StateAccessor()

The canvas's state: values that hooks and your code share, read and written by path.

canvas.state.get('player.score') reads a value and canvas.state.push('player.score', 3) writes one; see StateNamespace for the path rules. A canvas created with new(state=Settings()) also gives the fields of the dataclass as attributes with their declared types, so canvas.state.volume is an int for a field declared volume: int; a write is checked against the field's type. A type checker sees get and push only on a canvas without a state dataclass, but both exist on every canvas.

The state can be used from any thread. A write updates every expression that reads the field and draws a frame.

__setattr__

__setattr__(name: str, value: object) -> None

Set an attribute, or raise AttributeError for an unknown public name.

Source code in xnano/_attributes.py
def __setattr__(self, name: str, value: object) -> None:
    """Set an attribute, or raise ``AttributeError`` for an unknown public name."""
    if (
        name[:1] != '_'
        and self.__dict__.get('_is_sealed')
        and not hasattr(type(self), name)
        and name not in self.__dict__
    ):
        kind = type(self).__name__
        settable = sorted(
            key
            for key in dir(type(self))
            if key[:1] != '_' and _is_settable(type(self), key)
        )
        close = difflib.get_close_matches(name, settable, n=1)
        hint = f' Did you mean {close[0]!r}?' if close else ''
        raise AttributeError(
            f'{kind} has no attribute called {name!r}, so assigning to it would change '
            f'nothing.{hint} The attributes you can assign are: {", ".join(settable)}.'
        )
    object.__setattr__(self, name, value)

bold

bold(value: bool = True) -> Self

Make the text bold, or undo it with False; the same as setting is_bold.

Parameters:

  • value (bool, default: True ) –

    Whether the text is bold.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Examples:

import xnano

with xnano.new(width=12, height=1) as canvas:
    title = canvas.write('Ready').bold().underline()
    assert title.is_bold and title.is_underlined
    title.bold(False)
    assert not title.is_bold
Source code in xnano/appearance.py
def bold(self, value: bool = True) -> Self:
    """
    Make the text bold, or undo it with ``False``; the same as setting ``is_bold``.

    Args:
        value: Whether the text is bold.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.

    Examples:
        ```python
        import xnano

        with xnano.new(width=12, height=1) as canvas:
            title = canvas.write('Ready').bold().underline()
            assert title.is_bold and title.is_underlined
            title.bold(False)
            assert not title.is_bold
        ```
    """
    self.is_bold = value
    return self

dim

dim(value: bool = True) -> Self

Dim the text, or undo it with False; the same as setting is_dim.

Parameters:

  • value (bool, default: True ) –

    Whether the text is dimmed.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def dim(self, value: bool = True) -> Self:
    """
    Dim the text, or undo it with ``False``; the same as setting ``is_dim``.

    Args:
        value: Whether the text is dimmed.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_dim = value
    return self

italic

italic(value: bool = True) -> Self

Make the text italic, or undo it with False; the same as setting is_italic.

Parameters:

  • value (bool, default: True ) –

    Whether the text is italic.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def italic(self, value: bool = True) -> Self:
    """
    Make the text italic, or undo it with ``False``; the same as setting ``is_italic``.

    Args:
        value: Whether the text is italic.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_italic = value
    return self

underline

underline(value: bool = True) -> Self

Underline the text, or undo it with False; the same as setting is_underlined.

Parameters:

  • value (bool, default: True ) –

    Whether the text is underlined.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def underline(self, value: bool = True) -> Self:
    """
    Underline the text, or undo it with ``False``; the same as setting ``is_underlined``.

    Args:
        value: Whether the text is underlined.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_underlined = value
    return self
blink(value: bool = True) -> Self

Make the text blink, or undo it with False; the same as setting is_blinking.

Parameters:

  • value (bool, default: True ) –

    Whether the text blinks.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def blink(self, value: bool = True) -> Self:
    """
    Make the text blink, or undo it with ``False``; the same as setting ``is_blinking``.

    Args:
        value: Whether the text blinks.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_blinking = value
    return self

reverse

reverse(value: bool = True) -> Self

Swap the foreground and background, or undo it with False; the same as setting is_reversed.

Parameters:

  • value (bool, default: True ) –

    Whether foreground and background are swapped.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def reverse(self, value: bool = True) -> Self:
    """
    Swap the foreground and background, or undo it with ``False``; the same as setting ``is_reversed``.

    Args:
        value: Whether foreground and background are swapped.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_reversed = value
    return self

hidden

hidden(value: bool = True) -> Self

Hide the text, or undo it with False; the same as setting is_hidden.

Parameters:

  • value (bool, default: True ) –

    Whether the text is hidden.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def hidden(self, value: bool = True) -> Self:
    """
    Hide the text, or undo it with ``False``; the same as setting ``is_hidden``.

    Args:
        value: Whether the text is hidden.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_hidden = value
    return self

crossed_out

crossed_out(value: bool = True) -> Self

Strike the text through, or undo it with False; the same as setting is_crossed_out.

Parameters:

  • value (bool, default: True ) –

    Whether the text is struck through.

Returns:

  • Self –

    This canvas or placement, so calls can chain.

Raises:

Source code in xnano/appearance.py
def crossed_out(self, value: bool = True) -> Self:
    """
    Strike the text through, or undo it with ``False``; the same as setting ``is_crossed_out``.

    Args:
        value: Whether the text is struck through.

    Returns:
        This canvas or placement, so calls can chain.

    Raises:
        TypeError: ``value`` is not ``True`` or ``False``.
    """
    self.is_crossed_out = value
    return self

hook

hook(function: Hook[DepsT, StateT], /, *, on: HookKind | None = None, when: When[DepsT, StateT] | None = None, every: float | None = None, after: float | None = None, keys: KeyName | Sequence[KeyName] | None = None, requires_deps: bool = False) -> HookHandle[DepsT, StateT]
hook(*, on: HookKind | None = None, when: When[DepsT, StateT] | None = None, every: float | None = None, after: float | None = None, keys: KeyName | Sequence[KeyName] | None = None, requires_deps: bool = False) -> Callable[[Hook[DepsT, StateT]], HookHandle[DepsT, StateT]]
hook(function: Hook[DepsT, StateT] | None = None, /, *, on: HookKind | None = None, when: When[DepsT, StateT] | None = None, every: float | None = None, after: float | None = None, keys: KeyName | Sequence[KeyName] | None = None, requires_deps: bool = False) -> HookHandle[DepsT, StateT] | Callable[[Hook[DepsT, StateT]], HookHandle[DepsT, StateT]]

Call a function when something happens on this canvas or placement.

on says what to listen to. A hook without on hears every input event. A mouse, scroll or drag hook on a placement hears only the pointer inside the placement, and a press inside holds the pointer for the hook until the button is released. A pointer hook that matches an event stops it from reaching the parent; a hook that does not match lets it through. Keys are not consumed: while nothing has focus, every key hook hears a key.

hook(function, ...) attaches one function, and @target.hook(...) decorates the function that follows. A hook function may be a generator that receives a new context at each bare yield: write context = yield. A drag is a loop that runs while context.mouse.is_down.

every and after are seconds for on='tick' and beats for on='beat': every=2 means 2 seconds for a tick hook and 2 beats for a beat hook. On a canvas from new() a hook that is due several times in one step runs once for each interval, up to 100,000 times in one step; the intervals that remain run in the steps that follow. The function's return value is ignored, unless it is an operation, a style or an effect, which is applied to this target.

Parameters:

  • function (Hook[DepsT, StateT] | None, default: None ) –

    The function to call with a Context, or a function with no parameters, which is called without it.

  • on (HookKind | None, default: None ) –

    What to listen to: 'frame', 'tick', 'beat', 'key', 'mouse', 'scroll', 'drag', 'paste', 'focus', 'resize', 'state' or 'custom'. See HookKind.

  • when (When[DepsT, StateT] | None, default: None ) –

    A condition that must hold: an expression, a function of the context, or a string. See When. A string tells xnano which input the hook needs, so the mouse, scroll and key-release gates it reads are turned on when the canvas opens. An expression or a function does not, so a condition that reads Mouse, Scroll or key releases needs the gate set on new() or open() yourself: mouse=True, scroll=True or keyboard='release_events'. A comparison with an input the event does not carry, such as keyboard.key on a paste, does not hold; != is one of them, so keyboard.key != 'q' hears only key events. not and not in negate the whole comparison, so not keyboard.key == 'q' and keyboard.key not in ['q'] also hold for events that have no key.

  • every (float | None, default: None ) –

    Repeat every this many seconds ('tick') or beats ('beat').

  • after (float | None, default: None ) –

    Run once after this many seconds ('tick') or beats ('beat').

  • keys (KeyName | Sequence[KeyName] | None, default: None ) –

    The key or keys an on='key' hook reacts to, such as 'q' or 'ctrl+s'. A key hook with keys hears presses and repeats; omit them and test keyboard.kind to hear releases.

  • requires_deps (bool, default: False ) –

    Whether the hook needs the dependencies given to new(deps=...).

Returns:

  • HookHandle[DepsT, StateT] | Callable[[Hook[DepsT, StateT]], HookHandle[DepsT, StateT]] –

    The hook, whose remove stops it; or, without a function, the decorator.

Raises:

  • ValueError –

    The combination of arguments does not make sense, a key name is unknown, or a when string is not a condition. The message says what to write instead.

  • TypeError –

    on is not text, when is a plain bool, or the function is not callable with a context, or is an async function.

  • TerminalError –

    requires_deps is true and the canvas was created without deps, or the canvas is on a screen with an input gate this hook needs turned off.

Examples:

import xnano
from xnano_core import Event

with xnano.new(width=8, height=1) as canvas:
    heard = []

    @canvas.hook(on='key', keys=['q', 'escape'])
    def leave(context: xnano.Context) -> None:
        heard.append(context.keyboard.key)

    @canvas.hook(on='tick', every=0.5)
    def blink(context: xnano.Context) -> None:
        heard.append('tick')

    canvas.push(Event.key_press('q'))
    canvas.step(delta_seconds=1.0)
    assert heard == ['q', 'tick', 'tick']
Source code in xnano/_hooks.py
def hook(
    self,
    function: Hook[DepsT, StateT] | None = None,
    /,
    *,
    on: HookKind | None = None,
    when: When[DepsT, StateT] | None = None,
    every: float | None = None,
    after: float | None = None,
    keys: KeyName | Sequence[KeyName] | None = None,
    requires_deps: bool = False,
) -> (
    HookHandle[DepsT, StateT]
    | Callable[[Hook[DepsT, StateT]], HookHandle[DepsT, StateT]]
):
    """
    Call a function when something happens on this canvas or placement.

    ``on`` says what to listen to. A hook without ``on`` hears every input event. A mouse,
    scroll or drag hook on a placement hears only the pointer inside the placement, and a
    press inside holds the pointer for the hook until the button is released. A pointer hook
    that matches an event stops it from reaching the parent; a hook that does not match lets
    it through. Keys are not consumed: while nothing has focus, every key hook hears a key.

    ``hook(function, ...)`` attaches one function, and ``@target.hook(...)`` decorates the
    function that follows. A hook function may be a generator that receives a new context
    at each bare ``yield``: write ``context = yield``. A drag is a loop that runs while
    ``context.mouse.is_down``.

    ``every`` and ``after`` are seconds for ``on='tick'`` and beats for ``on='beat'``:
    ``every=2`` means 2 seconds for a tick hook and 2 beats for a beat hook. On a canvas
    from ``new()`` a hook that is due several times in one ``step`` runs once for each
    interval, up to 100,000 times in one step; the intervals that remain run in the steps
    that follow. The function's return value is ignored, unless it is an operation, a
    style or an effect, which is applied to this target.

    Args:
        function: The function to call with a [`Context`][xnano.context.Context], or a
            function with no parameters, which is called without it.
        on: What to listen to: ``'frame'``, ``'tick'``, ``'beat'``, ``'key'``, ``'mouse'``,
            ``'scroll'``, ``'drag'``, ``'paste'``, ``'focus'``, ``'resize'``, ``'state'``
            or ``'custom'``. See [`HookKind`][xnano.types.HookKind].
        when: A condition that must hold: an expression, a function of the context, or a
            string. See [`When`][xnano.types.When]. A string tells xnano which input
            the hook needs, so the mouse, scroll and key-release gates it reads are
            turned on when the canvas opens. An expression or a function does not, so a
            condition that reads ``Mouse``, ``Scroll`` or key releases needs the gate set
            on ``new()`` or ``open()`` yourself: ``mouse=True``, ``scroll=True`` or
            ``keyboard='release_events'``. A comparison with an input the event does not
            carry, such as ``keyboard.key`` on a paste, does not hold; ``!=`` is one of
            them, so ``keyboard.key != 'q'`` hears only key events. ``not`` and ``not in``
            negate the whole comparison, so ``not keyboard.key == 'q'`` and
            ``keyboard.key not in ['q']`` also hold for events that have no key.
        every: Repeat every this many seconds (``'tick'``) or beats (``'beat'``).
        after: Run once after this many seconds (``'tick'``) or beats (``'beat'``).
        keys: The key or keys an ``on='key'`` hook reacts to, such as ``'q'`` or
            ``'ctrl+s'``. A key hook with keys hears presses and repeats; omit them and
            test ``keyboard.kind`` to hear releases.
        requires_deps: Whether the hook needs the dependencies given to
            ``new(deps=...)``.

    Returns:
        The hook, whose ``remove`` stops it; or, without a function, the decorator.

    Raises:
        ValueError: The combination of arguments does not make sense, a key name is
            unknown, or a ``when`` string is not a condition. The message says what to
            write instead.
        TypeError: ``on`` is not text, ``when`` is a plain ``bool``, or the function is not callable with a
            context, or is an ``async`` function.
        TerminalError: ``requires_deps`` is true and the canvas was created without
            ``deps``, or the canvas is on a screen with an input gate this hook needs
            turned off.

    Examples:
        ```python
        import xnano
        from xnano_core import Event

        with xnano.new(width=8, height=1) as canvas:
            heard = []

            @canvas.hook(on='key', keys=['q', 'escape'])
            def leave(context: xnano.Context) -> None:
                heard.append(context.keyboard.key)

            @canvas.hook(on='tick', every=0.5)
            def blink(context: xnano.Context) -> None:
                heard.append('tick')

            canvas.push(Event.key_press('q'))
            canvas.step(delta_seconds=1.0)
            assert heard == ['q', 'tick', 'tick']
        ```
    """
    canvas = self._canvas
    self._require_attached()
    owner = cast('_Hooked[object, object]', self)
    normalized_keys = _check_hook_arguments(owner, on, every, after, keys)
    compiled = compile_when(when)
    if on is None and not compiled.is_opaque and compiled.reads:
        if not compiled.reads & {'keyboard', 'mouse', 'scroll'}:
            raise ValueError(
                'when reads time, the size of the parent or deps, but a hook with no on= is '
                'only checked when an input event arrives. Add on="tick" or '
                'on="frame" so it is checked as time passes, or read an input in the '
                'condition.'
            )
    if requires_deps and not canvas._deps_given:
        raise TerminalError(
            'This hook needs dependencies (requires_deps=True), but the canvas has none '
            'yet. Give them when you create the canvas: xnano.new(deps=your_dependencies) '
            'or xnano.open(deps=your_dependencies), or open the canvas with '
            'canvas.open(deps=your_dependencies) before you register this hook.'
        )

    def attach(
        callback: Hook[DepsT, StateT],
    ) -> HookHandle[DepsT, StateT]:
        return self._attach_hook(
            cast(
                'Callable[[Context[DepsT, StateT]], object]',
                _check_hook_function(callback),
            ),
            on=on,
            when=compiled,
            every=every,
            after=after,
            keys=normalized_keys,
        )

    if function is None:
        return attach
    return attach(function)

__new__

__new__(*arguments: object, **keywords: object) -> Self

Refuse a direct call: a canvas is created with xnano.new.

Source code in xnano/canvas.py
def __new__(cls, *arguments: object, **keywords: object) -> Self:
    """Refuse a direct call: a canvas is created with ``xnano.new``."""
    if arguments or 'is_realtime' not in keywords:
        raise TypeError(
            f'{cls.__name__} is not created by calling it. Create a canvas with '
            f'xnano.new(...), or put one on the screen with xnano.open(...).'
        )
    return super().__new__(cls)

open

open(*, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, **settings: Unpack[OpenSettings]) -> Terminal[DepsT, StateT]

Put this canvas on your screen, and return it as a Terminal.

The same object is returned: after this, isinstance(canvas, xnano.Terminal) is true. From then on it runs by itself on a thread of its own and on the wall clock, which continues from the time the canvas had: elapsed time, the frame count, every and after timers and a stopped clock carry over, and so do the cursor, the title, everything written and every hook. Effects that were running start again from their beginning, and keyboard focus and a captured pointer are released.

width and height are the size of the canvas: the ones given here, else the ones given to new(), draw in a region of that size below the cursor; a canvas created without a size, or with '100%' for both, fills the whole window. A percentage such as '50%' is a share of the window when the canvas is opened. Any other keyword of new() except realtime may be given and applies before the canvas is shown; a gate given here wins over the one given to new(). title is the window title.

Parameters:

  • title (str | None, default: None ) –

    The window title; None keeps the canvas's title.

  • width (int | Percentage | str | None, default: None ) –

    The width of the region to draw in: cells, or a percentage of the window.

  • height (int | Percentage | str | None, default: None ) –

    The height of the region to draw in: cells, or a percentage of the window.

  • settings (Unpack[OpenSettings], default: {} ) –

    Gates, appearance keywords, fps, bpm, effects, and deps and state for a canvas that was created without them; see OpenSettings.

Returns:

  • Terminal[DepsT, StateT] –

    This canvas, now attached to the screen.

Raises:

  • TerminalAlreadyOpenError –

    Another canvas is already on the screen.

  • TerminalError –

    This canvas is already on the screen, was written into another canvas, has a hook that needs a gate given as False, or there is no terminal to draw on; the canvas stays as it was.

  • TypeError –

    realtime was given, deps or state that the canvas already has, or an effect that is not a visual effect dictionary.

  • ValueError –

    A width was given without a height, or a value is out of range.

Examples:

with xnano.new(width=40, height=5) as canvas:
    canvas.write('hello', border='rounded')
    canvas.step()
    canvas.open(mouse=True)        # now a xnano.Terminal, 40 by 5 below the prompt
    canvas.run(quit_on='q')
Source code in xnano/canvas.py
def open(
    self,
    *,
    title: str | None = None,
    width: int | Percentage | str | None = None,
    height: int | Percentage | str | None = None,
    **settings: Unpack[OpenSettings],
) -> Terminal[DepsT, StateT]:
    """
    Put this canvas on your screen, and return it as a [`Terminal`][xnano.Terminal].

    The same object is returned: after this, ``isinstance(canvas, xnano.Terminal)`` is
    true. From then on it runs by itself on a thread of its own and on the wall clock, which
    continues from the time the canvas had: elapsed time, the frame count, ``every`` and
    ``after`` timers and a stopped clock carry over, and so do the cursor, the title,
    everything written and every hook. Effects that were running start again from their
    beginning, and keyboard focus and a captured pointer are released.

    ``width`` and ``height`` are the size of the canvas: the ones given here, else the
    ones given to ``new()``, draw in a region of that size below the cursor; a canvas
    created without a size, or with ``'100%'`` for both, fills the whole window. A
    percentage such as ``'50%'`` is a share of the window when the canvas is opened. Any other keyword of ``new()`` except
    ``realtime`` may be given and applies before the canvas is shown; a gate given here
    wins over the one given to ``new()``. ``title`` is the window title.

    Args:
        title: The window title; ``None`` keeps the canvas's title.
        width: The width of the region to draw in: cells, or a percentage of the window.
        height: The height of the region to draw in: cells, or a percentage of the window.
        settings: Gates, appearance keywords, ``fps``, ``bpm``, ``effects``, and ``deps``
            and ``state`` for a canvas that was created without them; see
            [`OpenSettings`][xnano.types.OpenSettings].

    Returns:
        This canvas, now attached to the screen.

    Raises:
        TerminalAlreadyOpenError: Another canvas is already on the screen.
        TerminalError: This canvas is already on the screen, was written into another
            canvas, has a hook that needs a gate given as ``False``, or there is no
            terminal to draw on; the canvas stays as it was.
        TypeError: ``realtime`` was given, ``deps`` or ``state`` that the canvas already
            has, or an effect that is not a visual effect dictionary.
        ValueError: A ``width`` was given without a ``height``, or a value is out of
            range.

    Examples:
        ```text
        with xnano.new(width=40, height=5) as canvas:
            canvas.write('hello', border='rounded')
            canvas.step()
            canvas.open(mouse=True)        # now a xnano.Terminal, 40 by 5 below the prompt
            canvas.run(quit_on='q')
        ```
    """
    self._attach_screen(title=title, width=width, height=height, settings=settings)
    from xnano.terminal import Terminal

    assert isinstance(self, Terminal)
    return self

close

close(*, raise_failure: bool = True) -> None

Release everything the canvas holds, and give the screen back if it has one.

Closing twice is harmless. Leaving a with block closes the canvas, unless it is part of another canvas or was created inside another canvas's with block: then it stays usable and closes with that canvas. Closing a canvas closes the canvases created inside its block that were not written anywhere. Closing a canvas that was written into another one takes it off and leaves that one running.

Parameters:

  • raise_failure (bool, default: True ) –

    Whether to raise the error a callback raised on the loop, which stopped the canvas. A with block passes False while another error is already propagating.

Raises:

  • TerminalError –

    A callback raised on the canvas's loop, or the loop would not stop.

Source code in xnano/canvas.py
def close(self, *, raise_failure: bool = True) -> None:
    """
    Release everything the canvas holds, and give the screen back if it has one.

    Closing twice is harmless. Leaving a ``with`` block closes the canvas, unless it is
    part of another canvas or was created inside another canvas's ``with`` block: then it
    stays usable and closes with that canvas. Closing a canvas closes the canvases created
    inside its block that were not written anywhere. Closing a canvas that was written into
    another one takes it off and leaves that one running.

    Args:
        raise_failure: Whether to raise the error a callback raised on the loop, which
            stopped the canvas. A ``with`` block passes ``False`` while another error is
            already propagating.

    Raises:
        TerminalError: A callback raised on the canvas's loop, or the loop would not stop.
    """
    self._close_nested_canvases()
    if self._canvas is not self:
        if not self._is_closed:
            self._is_closed = True
            root = self._canvas
            closing: list[Canvas[object, object]] = [
                cast('Canvas[object, object]', self)
            ]
            with root._lock:
                # A canvas written into this one is part of it, so it ends with it.
                closing.extend(
                    inner
                    for inner in list(root._children)
                    if inner is not self
                    and inner._is_inside(cast('Canvas[object, object]', self))
                )
                for canvas in closing:
                    canvas._is_closed = True
                    if canvas in root._children:
                        root._children.remove(canvas)
            placement = self._parent_placement
            if placement is not None and not root.is_closed:
                for canvas in closing:
                    root._host.post(canvas._cancel_hooks)
                placement.remove()
        if _current_canvas() is self:
            _active_canvas.set(self._previous_chain)
        return
    if not self._stop_requested.is_set() or not self._is_finalized:
        host = getattr(self, '_host', None)
        if host is not None:
            host.require_closable()
        self._stop_requested.set()
        try:
            if host is not None:
                host.shutdown()
        finally:
            self._finalize()
    if _current_canvas() is self:
        _active_canvas.set(self._previous_chain)
    failure = self._failure
    if failure is not None and raise_failure and not self._is_failure_reported:
        self._failure = None
        raise TerminalError(self._failure_text(failure)) from failure

local

local(point: Point) -> Point

Convert a cell of the screen into a cell of this canvas.

Parameters:

  • point (Point) –

    A cell in screen coordinates, such as context.mouse.position.

Examples:

import xnano
from xnano_core import Point

with xnano.new(width=10, height=3) as canvas:
    assert canvas.local(Point(4, 1)) == Point(4, 1)
Source code in xnano/canvas.py
def local(self, point: _core.Point) -> _core.Point:
    """
    Convert a cell of the screen into a cell of this canvas.

    Args:
        point: A cell in screen coordinates, such as ``context.mouse.position``.

    Examples:
        ```python
        import xnano
        from xnano_core import Point

        with xnano.new(width=10, height=3) as canvas:
            assert canvas.local(Point(4, 1)) == Point(4, 1)
        ```
    """
    point = check_point(point)
    if self._canvas is self:
        return _core.Point(point.x, point.y)
    bounds = self._root._absolute_bounds
    return _core.Point(point.x - bounds.x, point.y - bounds.y)

move

move(x: int = 0, y: int = 0) -> Self

Move a canvas that is written into another one by a number of cells along each axis.

This moves the placement the canvas is drawn in; see Placement.move.

Parameters:

  • x (int, default: 0 ) –

    Columns to move right; a negative number moves left.

  • y (int, default: 0 ) –

    Rows to move down; a negative number moves up.

Returns:

  • Self –

    The canvas, so calls can chain.

Raises:

  • TerminalError –

    The canvas is not written into another canvas, so it sits at the origin and cannot move.

  • TypeError –

    An amount is not a whole number.

Examples:

import xnano

with xnano.new(width=20, height=3) as outer:
    inner = outer.new('box', width=4, height=1)
    inner.move(x=2, y=1)
    assert (inner.x, inner.y) == (2, 1)
    assert outer.render().splitlines()[1] == '  box'
Source code in xnano/canvas.py
def move(self, x: int = 0, y: int = 0) -> Self:
    """
    Move a canvas that is written into another one by a number of cells along each axis.

    This moves the placement the canvas is drawn in; see
    [`Placement.move`][xnano.placement.Placement.move].

    Args:
        x: Columns to move right; a negative number moves left.
        y: Rows to move down; a negative number moves up.

    Returns:
        The canvas, so calls can chain.

    Raises:
        TerminalError: The canvas is not written into another canvas, so it sits at the
            origin and cannot move.
        TypeError: An amount is not a whole number.

    Examples:
        ```python
        import xnano

        with xnano.new(width=20, height=3) as outer:
            inner = outer.new('box', width=4, height=1)
            inner.move(x=2, y=1)
            assert (inner.x, inner.y) == (2, 1)
            assert outer.render().splitlines()[1] == '  box'
        ```
    """
    self._written_placement().move(x=x, y=y)
    return self

up

up(count: int = 1) -> Self

Move a written canvas up by count rows; see move.

Parameters:

  • count (int, default: 1 ) –

    How many rows to move.

Raises:

Source code in xnano/canvas.py
def up(self, count: int = 1) -> Self:
    """
    Move a written canvas up by ``count`` rows; see [`move`][xnano.canvas.Canvas.move].

    Args:
        count: How many rows to move.

    Raises:
        TerminalError: The canvas is not written into another canvas.
        TypeError: ``count`` is not a whole number.
        ValueError: ``count`` is negative.
    """
    self._written_placement().up(count)
    return self

down

down(count: int = 1) -> Self

Move a written canvas down by count rows; see move.

Parameters:

  • count (int, default: 1 ) –

    How many rows to move.

Raises:

Source code in xnano/canvas.py
def down(self, count: int = 1) -> Self:
    """
    Move a written canvas down by ``count`` rows; see [`move`][xnano.canvas.Canvas.move].

    Args:
        count: How many rows to move.

    Raises:
        TerminalError: The canvas is not written into another canvas.
        TypeError: ``count`` is not a whole number.
        ValueError: ``count`` is negative.
    """
    self._written_placement().down(count)
    return self

left

left(count: int = 1) -> Self

Move a written canvas left by count columns; see move.

Parameters:

  • count (int, default: 1 ) –

    How many columns to move.

Raises:

Source code in xnano/canvas.py
def left(self, count: int = 1) -> Self:
    """
    Move a written canvas left by ``count`` columns; see [`move`][xnano.canvas.Canvas.move].

    Args:
        count: How many columns to move.

    Raises:
        TerminalError: The canvas is not written into another canvas.
        TypeError: ``count`` is not a whole number.
        ValueError: ``count`` is negative.
    """
    self._written_placement().left(count)
    return self

right

right(count: int = 1) -> Self

Move a written canvas right by count columns; see move.

Parameters:

  • count (int, default: 1 ) –

    How many columns to move.

Raises:

Source code in xnano/canvas.py
def right(self, count: int = 1) -> Self:
    """
    Move a written canvas right by ``count`` columns; see [`move`][xnano.canvas.Canvas.move].

    Args:
        count: How many columns to move.

    Raises:
        TerminalError: The canvas is not written into another canvas.
        TypeError: ``count`` is not a whole number.
        ValueError: ``count`` is negative.
    """
    self._written_placement().right(count)
    return self

new

new(content: Content | None = None, /, *, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, effects: Sequence[RenderableEffectSpecification] = (), **settings: Unpack[CanvasSettings]) -> Canvas[DepsT, StateT]

Create a canvas inside this one and draw it here at once, at the top-left corner.

This is new followed by canvas.write(child): the child's size is measured in this canvas ('50%' is half of it, and no size fills it), it runs on this canvas's time and hears its input, and it closes with this canvas. Move it with move and its relatives, or create it with new and write it with x and y to place it elsewhere.

Parameters:

  • content (Content | None, default: None ) –

    Something to write into the new canvas at once, such as text.

  • width (int | Percentage | str | None, default: None ) –

    The width in cells, or a percentage of this canvas such as '50%'.

  • height (int | Percentage | str | None, default: None ) –

    The height in cells, or a percentage of this canvas.

  • effects (Sequence[RenderableEffectSpecification], default: () ) –

    Effects to apply to the new canvas, as dictionaries with a kind key.

  • settings (Unpack[CanvasSettings], default: {} ) –

    The input gates and the new canvas's own look; see CanvasSettings.

Returns:

  • Canvas[DepsT, StateT] –

    The new Canvas, already drawn in this one.

Raises:

  • TypeError –

    An appearance keyword was given with content, or a keyword is unknown.

  • ValueError –

    A size, gate or border kind is out of range.

  • TerminalError –

    This canvas is closed, or a hook of the new canvas needs a gate this canvas turned off.

Examples:

import xnano

with xnano.new(width=12, height=4) as outer:
    panel = outer.new(height='50%', border='plain')
    assert (panel.width, panel.height) == (12, 2)
    assert outer.render().splitlines()[0] == '┌──────────┐'
Source code in xnano/canvas.py
def new(
    self,
    content: Content | None = None,
    /,
    *,
    width: int | Percentage | str | None = None,
    height: int | Percentage | str | None = None,
    effects: Sequence[RenderableEffectSpecification] = (),
    **settings: Unpack[CanvasSettings],
) -> Canvas[DepsT, StateT]:
    """
    Create a canvas inside this one and draw it here at once, at the top-left corner.

    This is [`new`][xnano.new] followed by ``canvas.write(child)``: the child's size is
    measured in this canvas (``'50%'`` is half of it, and no size fills it), it runs on
    this canvas's time and hears its input, and it closes with this canvas. Move it with
    [`move`][xnano.canvas.Canvas.move] and its relatives, or create it with
    [`new`][xnano.new] and write it with ``x`` and ``y`` to place it elsewhere.

    Args:
        content: Something to write into the new canvas at once, such as text.
        width: The width in cells, or a percentage of this canvas such as ``'50%'``.
        height: The height in cells, or a percentage of this canvas.
        effects: Effects to apply to the new canvas, as dictionaries with a ``kind`` key.
        settings: The input gates and the new canvas's own look; see
            [`CanvasSettings`][xnano.types.CanvasSettings].

    Returns:
        The new [`Canvas`][xnano.canvas.Canvas], already drawn in this one.

    Raises:
        TypeError: An appearance keyword was given with content, or a keyword is unknown.
        ValueError: A size, gate or border kind is out of range.
        TerminalError: This canvas is closed, or a hook of the new canvas needs a gate
            this canvas turned off.

    Examples:
        ```python
        import xnano

        with xnano.new(width=12, height=4) as outer:
            panel = outer.new(height='50%', border='plain')
            assert (panel.width, panel.height) == (12, 2)
            assert outer.render().splitlines()[0] == '┌──────────┐'
        ```
    """
    self._require_open()
    canvas: Canvas[DepsT, StateT] = _create(
        content,
        width=width,
        height=height,
        fps=self.fps,
        bpm=self.bpm,
        effects=effects,
        enclosing=cast('Canvas[object, object]', self),
        **settings,
    )
    try:
        self.write(cast('Canvas[object, object]', canvas))
    except BaseException:
        canvas.close(raise_failure=False)
        raise
    return canvas

write

write(content: Content | Canvas[WrittenDepsT, WrittenStateT] | None = None, /, *, x: Axis | None = None, y: Axis | None = None, width: Axis | None = None, height: Axis | None = None, layer: int = 0, lock: bool = False, edges: EdgeBehavior | None = None, effects: Sequence[RenderableEffectSpecification] = (), style: Style | None = None, foreground: ColorValue | None = None, background: ColorValue | None = None, bold: bool = False, dim: bool = False, italic: bool = False, underline: bool = False, blink: bool = False, reverse: bool = False, hidden: bool = False, crossed_out: bool = False, title: str | None = None, border: BorderKind | bool | None = None, border_style: Style | None = None, padding: Spacing | None = None, margin: Spacing | None = None) -> Placement[DepsT, StateT]

Put something on the canvas: text, a widget, a buffer, another canvas or an image.

On a canvas that runs by itself it appears at once: this only declares it, and the canvas's own loop draws it. Anything you leave out is taken from the content: x and y default to the top-left corner, and width and height to the size of the content, so canvas.write('hello') shows five cells of text where it is. Change the text later by assigning the placement's text.

Inside the with block of a canvas created in this one's block, what is written here is placed and clipped within that canvas's area of this one: where it is written, or the top-left corner while it is not. Positions such as y='center' are measured in that area, and that canvas's edges applies there.

A canvas from new that is written here becomes part of this one: it draws inside the placement, its own clock is set aside and it runs on this canvas's time. It may not have been given state or deps, which belong to the outermost canvas. Its audio is not carried over: a musical clock it started and a note it was playing stop, and a formula that reads its audio.beat follows the outer canvas's musical clock, which starts at the first beat.

An image is added as an Image (never a file path or a bare TerminalImage; load one with from_image). With neither width nor height it keeps its own proportions at its own size, shrunk to fit the canvas. With one of them the other follows the image's proportions, and with both the image fits inside that box. An image sized by a percentage, or by the canvas because you gave no size, is converted again when the canvas is resized, unless it is locked. An animated image plays on the canvas's clock.

Parameters:

  • content (Content | Canvas[WrittenDepsT, WrittenStateT] | None, default: None ) –

    Text (or a list of lines), a Buffer or widget, a canvas from new(), an image, or any Renderable; None for an empty placement.

  • x (Axis | None, default: None ) –

    The column of the left edge: a number of cells, a percentage string, a word ('left', 'center' or 'right'), an expression, or a range ((0, 20)) that limits how far it may travel. See Axis.

  • y (Axis | None, default: None ) –

    The row of the top edge, in the same forms ('top', 'center', 'bottom').

  • width (Axis | None, default: None ) –

    The width, in the same forms; the content's own width when None.

  • height (Axis | None, default: None ) –

    The height, in the same forms; the content's own height when None.

  • layer (int, default: 0 ) –

    Content with a higher layer is drawn over content with a lower one.

  • lock (bool, default: False ) –

    Keep the position and size the content has now, whatever the canvas's size does afterwards: percentages, words and formulas are read once.

  • edges (EdgeBehavior | None, default: None ) –

    What the content does at the edges of its area: 'clip', 'stop' or 'wrap'; see Placement.edges. Left out, it follows what it is written on.

  • effects (Sequence[RenderableEffectSpecification], default: () ) –

    Effects to start on the placement, as dictionaries with a kind key, such as [{'kind': 'effect.fade_to', 'foreground': 'red'}].

  • style (Style | None, default: None ) –

    A ready-made Style for text. The parameters below are applied over it.

  • foreground (ColorValue | None, default: None ) –

    The text color.

  • background (ColorValue | None, default: None ) –

    The color behind the text.

  • bold (bool, default: False ) –

    Whether the text is bold.

  • dim (bool, default: False ) –

    Whether the text is dimmed.

  • italic (bool, default: False ) –

    Whether the text is italic.

  • underline (bool, default: False ) –

    Whether the text is underlined.

  • blink (bool, default: False ) –

    Whether the text blinks.

  • reverse (bool, default: False ) –

    Whether foreground and background are swapped.

  • hidden (bool, default: False ) –

    Whether the text is hidden.

  • crossed_out (bool, default: False ) –

    Whether the text is struck through.

  • title (str | None, default: None ) –

    A title on the border of a box around the text.

  • border (BorderKind | bool | None, default: None ) –

    The kind of border for a box around the content, a BorderKind such as 'plain', 'rounded', 'double' or 'thick'; True for 'plain' and False for none. Giving a title alone draws a plain one.

  • border_style (Style | None, default: None ) –

    The style of the box's border.

  • padding (Spacing | None, default: None ) –

    Space between the border and the text: a number of cells, a float or a percentage of the text's size, or a dictionary with top, right, bottom, left, vertical and horizontal. Giving it draws the text in a box without a border unless border or title is also given.

  • margin (Spacing | None, default: None ) –

    Space outside the box, in the same forms.

Returns:

Raises:

  • TypeError –

    The content is a file path, raw bytes, or an operation, style or effect (apply those with canvas.update(...) instead).

  • TerminalError –

    The content is a canvas that cannot be written here.

Examples:

import xnano

with xnano.new(width=20, height=4) as canvas:
    box = canvas.write('hello', x='50%', y=1)
    assert box.x == 10
    assert (box.width, box.height) == (5, 1)
    canvas.write(
        'Ready',
        x='center',
        y='bottom',
        foreground='green',
        bold=True,
        border='rounded',
        title='Status',
    )
Source code in xnano/canvas.py
@refuse_unknown_keywords
def write(
    self,
    content: Content | Canvas[WrittenDepsT, WrittenStateT] | None = None,
    /,
    *,
    x: Axis | None = None,
    y: Axis | None = None,
    width: Axis | None = None,
    height: Axis | None = None,
    layer: int = 0,
    lock: bool = False,
    edges: EdgeBehavior | None = None,
    effects: Sequence[RenderableEffectSpecification] = (),
    style: _core.Style | None = None,
    foreground: ColorValue | None = None,
    background: ColorValue | None = None,
    bold: bool = False,
    dim: bool = False,
    italic: bool = False,
    underline: bool = False,
    blink: bool = False,
    reverse: bool = False,
    hidden: bool = False,
    crossed_out: bool = False,
    title: str | None = None,
    border: _core.BorderKind | bool | None = None,
    border_style: _core.Style | None = None,
    padding: Spacing | None = None,
    margin: Spacing | None = None,
) -> Placement[DepsT, StateT]:
    """
    Put something on the canvas: text, a widget, a buffer, another canvas or an image.

    On a canvas that runs by itself it appears at once: this only declares it, and the
    canvas's own loop draws it. Anything you leave out is taken from the content: ``x``
    and ``y`` default to the top-left corner, and ``width`` and ``height`` to the size of
    the content, so ``canvas.write('hello')`` shows five cells of text where it is.
    Change the text later by assigning the placement's
    [`text`][xnano.placement.Placement.text].

    Inside the ``with`` block of a canvas created in this one's block, what is written here
    is placed and clipped within that canvas's area of this one: where it is written, or
    the top-left corner while it is not. Positions such as ``y='center'`` are measured in
    that area, and that canvas's ``edges`` applies there.

    A canvas from [`new`][xnano.new] that is written here becomes part of this one: it
    draws inside the placement, its own clock is set aside and it runs on this canvas's
    time. It may not have been given ``state`` or ``deps``, which belong to the outermost
    canvas. Its audio is not carried over: a musical clock it started and a note it was
    playing stop, and a formula that reads its ``audio.beat`` follows the outer canvas's
    musical clock, which starts at the first beat.

    An image is added as an [`Image`][xnano.Image] (never a file path or a bare
    [`TerminalImage`][xnano_image.TerminalImage]; load one with
    [`from_image`][xnano.from_image]). With neither ``width`` nor ``height`` it keeps
    its own proportions at its own size, shrunk to fit the canvas. With one of them the
    other follows the image's proportions, and with both the image fits inside that box. An
    image sized by a percentage, or by the canvas because you gave no size, is converted
    again when the canvas is resized, unless it is locked. An animated image plays on the
    canvas's clock.

    Args:
        content: Text (or a list of lines), a [`Buffer`][xnano_core.Buffer] or widget, a
            canvas from ``new()``, an image, or any
            [`Renderable`][xnano.types.Renderable]; ``None`` for an empty placement.
        x: The column of the left edge: a number of cells, a percentage string, a word
            (``'left'``, ``'center'`` or ``'right'``), an expression, or a range
            (``(0, 20)``) that limits how far it may travel. See
            [`Axis`][xnano.types.Axis].
        y: The row of the top edge, in the same forms (``'top'``, ``'center'``,
            ``'bottom'``).
        width: The width, in the same forms; the content's own width when ``None``.
        height: The height, in the same forms; the content's own height when ``None``.
        layer: Content with a higher layer is drawn over content with a lower one.
        lock: Keep the position and size the content has now, whatever the canvas's
            size does afterwards: percentages, words and formulas are read once.
        edges: What the content does at the edges of its area: ``'clip'``, ``'stop'`` or
            ``'wrap'``; see [`Placement.edges`][xnano.placement.Placement.edges]. Left
            out, it follows what it is written on.
        effects: Effects to start on the placement, as dictionaries with a ``kind``
            key, such as ``[{'kind': 'effect.fade_to', 'foreground': 'red'}]``.
        style: A ready-made [`Style`][xnano.styles.Style] for text. The parameters below
            are applied over it.
        foreground: The text color.
        background: The color behind the text.
        bold: Whether the text is bold.
        dim: Whether the text is dimmed.
        italic: Whether the text is italic.
        underline: Whether the text is underlined.
        blink: Whether the text blinks.
        reverse: Whether foreground and background are swapped.
        hidden: Whether the text is hidden.
        crossed_out: Whether the text is struck through.
        title: A title on the border of a box around the text.
        border: The kind of border for a box around the content, a
            [`BorderKind`][xnano.types.BorderKind] such as ``'plain'``, ``'rounded'``,
            ``'double'`` or ``'thick'``; ``True`` for ``'plain'`` and ``False`` for none.
            Giving a ``title`` alone draws a plain one.
        border_style: The style of the box's border.
        padding: Space between the border and the text: a number of cells, a float or a
            percentage of the text's size, or a dictionary with ``top``, ``right``,
            ``bottom``, ``left``, ``vertical`` and ``horizontal``. Giving it draws the
            text in a box without a border unless ``border`` or ``title`` is also given.
        margin: Space outside the box, in the same forms.

    Returns:
        A [`Placement`][xnano.placement.Placement] to move, resize, listen on or remove.

    Raises:
        TypeError: The content is a file path, raw bytes, or an operation, style or effect
            (apply those with ``canvas.update(...)`` instead).
        TerminalError: The content is a canvas that cannot be written here.

    Examples:
        ```python
        import xnano

        with xnano.new(width=20, height=4) as canvas:
            box = canvas.write('hello', x='50%', y=1)
            assert box.x == 10
            assert (box.width, box.height) == (5, 1)
            canvas.write(
                'Ready',
                x='center',
                y='bottom',
                foreground='green',
                bold=True,
                border='rounded',
                title='Status',
            )
        ```
    """
    self._require_open()
    if edges is not None:
        edges = check_edges(edges)
    area = self._area_for_write(content)
    if area is not None:
        return area.write(  # type: ignore[return-value]
            content,
            x=x,
            y=y,
            width=width,
            height=height,
            layer=layer,
            lock=lock,
            edges=edges,
            effects=effects,
            style=style,
            foreground=foreground,
            background=background,
            bold=bold,
            dim=dim,
            italic=italic,
            underline=underline,
            blink=blink,
            reverse=reverse,
            hidden=hidden,
            crossed_out=crossed_out,
            title=title,
            border=border,
            border_style=border_style,
            padding=padding,
            margin=margin,
        )
    parent_buffer = self._content
    slot: _core.Buffer | None = None
    if self._layout != 'overlay':
        slot, width, height = self._open_slot(content, width, height)
        parent_buffer = slot
    try:
        placement = self._canvas._place_on(
            parent_buffer,
            content,
            parent_owner=cast('Canvas[object, object]', self),
            x=x,
            y=y,
            width=width,
            height=height,
            layer=layer,
            lock=lock,
            style=style,
            foreground=foreground,
            background=background,
            bold=bold,
            dim=dim,
            italic=italic,
            underline=underline,
            blink=blink,
            reverse=reverse,
            hidden=hidden,
            crossed_out=crossed_out,
            title=title,
            border=border,
            border_style=border_style,
            padding=padding,
            margin=margin,
        )
    except BaseException:
        if slot is not None:
            self._close_slot(slot)
        raise
    if slot is not None:
        self._slots[-1] = (
            cast('Placement[object, object]', placement),
            *self._slots[-1][1:],
        )
    if effects:
        apply_effects(cast('Placement[object, object]', placement), effects)
    placement._take_edges(edges, content)
    return placement

clear

clear() -> None

Erase everything written or drawn directly on the canvas's cells.

Source code in xnano/canvas.py
def clear(self) -> None:
    """Erase everything written or drawn directly on the canvas's cells."""
    self._require_open()
    self._flush_look()
    self._content.update(_core.Fill(' '))

update

update(*operations: Operation | Style) -> Self

Change the canvas's own cells with operations, styles or effects.

canvas | operation does the same. To put content on the canvas, use write instead.

Parameters:

  • operations (Operation | Style, default: () ) –

    What to apply, in order: a core operation such as Fill or Write, a style, or an xnano customization such as an effect.

Returns:

  • Self –

    The canvas, so calls chain.

Examples:

import xnano
from xnano_core import Fill

with xnano.new(width=4, height=1) as canvas:
    canvas.update(Fill('.'))
    assert canvas.render() == '....'
    canvas | Fill('#')
    assert canvas.render() == '####'
Source code in xnano/canvas.py
def update(self, *operations: _core.Operation | _core.Style) -> Self:
    """
    Change the canvas's own cells with operations, styles or effects.

    ``canvas | operation`` does the same. To put content on the canvas, use
    [`write`][xnano.canvas.Canvas.write] instead.

    Args:
        operations: What to apply, in order: a core operation such as ``Fill`` or
            ``Write``, a style, or an xnano customization such as an effect.

    Returns:
        The canvas, so calls chain.

    Examples:
        ```python
        import xnano
        from xnano_core import Fill

        with xnano.new(width=4, height=1) as canvas:
            canvas.update(Fill('.'))
            assert canvas.render() == '....'
            canvas | Fill('#')
            assert canvas.render() == '####'
        ```
    """
    self._require_open()
    for item in operations:
        if isinstance(item, ContextualOperation):
            cast('ContextualOperation[DepsT, StateT]', item).apply_to(self)
        elif isinstance(item, Customization):
            item.apply_to(cast('Canvas[object, object]', self))
        elif isinstance(item, _core.Style):
            self._content.update(_core.ApplyStyle(item))
        else:
            self._content.update(item)
    return self

__or__

__or__(other: Operation | Style | Sequence[Operation]) -> Self

Apply an operation, customization or style with canvas | operation.

Source code in xnano/canvas.py
def __or__(
    self,
    other: _core.Operation | _core.Style | Sequence[_core.Operation],
) -> Self:
    """Apply an operation, customization or style with ``canvas | operation``."""
    if isinstance(other, (_core.Operation, _core.Style)):
        self.update(other)
    else:
        self.update(*other)
    return self

__ior__

__ior__(other: Operation | Style | Sequence[Operation]) -> Self

Apply an operation, customization or style in place with canvas |= operation.

Source code in xnano/canvas.py
def __ior__(
    self,
    other: _core.Operation | _core.Style | Sequence[_core.Operation],
) -> Self:
    """Apply an operation, customization or style in place with ``canvas |= operation``."""
    return self.__or__(other)

inspect

inspect(expression: Expression[ValueT]) -> ValueT

Evaluate an expression against the canvas and return its value.

Parameters:

  • expression (Expression[ValueT]) –

    Any expression, such as Width, TimeElapsed % 1 or canvas.audio.level.

Examples:

import xnano
from xnano_core import Width

with xnano.new(width=12, height=2) as canvas:
    assert canvas.inspect(Width) == 12
Source code in xnano/canvas.py
def inspect(self, expression: _core.Expression[ValueT]) -> ValueT:
    """
    Evaluate an expression against the canvas and return its value.

    Args:
        expression: Any expression, such as ``Width``, ``TimeElapsed % 1`` or
            ``canvas.audio.level``.

    Examples:
        ```python
        import xnano
        from xnano_core import Width

        with xnano.new(width=12, height=2) as canvas:
            assert canvas.inspect(Width) == 12
        ```
    """
    self._require_open()
    return self._canvas._host.call(lambda: self._root.inspect(expression))

iter

iter(*, count: int | None = None, seconds: float | None = None) -> Iterator[Frame]

Iterate over the canvas's frames, drawing between them; pass count= or seconds=, or break out of the loop.

Each iteration yields a Frame, and whatever you change before asking for the next one is drawn. On a canvas from new() each iteration moves the clock by one frame of its fps, so the same loop produces the same frames every run; without count or seconds it runs as fast as your code. A canvas that runs by itself draws as you declare, so there this only paces your code at its fps: if you are slow, frames are skipped rather than queued.

stop freezes the time iter() follows: the iterations go on drawing and hooks still run, but the clock stands still until resume. To move a stopped canvas one frame, call step.

The iteration ends when count frames have been yielded, when the canvas's time has run for seconds (whichever comes first), when the canvas is asked to quit or closed, or when you break out of the loop.

Parameters:

  • count (int | None, default: None ) –

    The most frames to yield.

  • seconds (float | None, default: None ) –

    The most seconds of the canvas's time to run for.

Raises:

Examples:

import xnano

with xnano.new(width=8, height=1, fps=10) as canvas:
    frames = list(canvas.iter(count=3))
assert [frame.elapsed_seconds for frame in frames] == [0.0, 0.1, 0.2]
Source code in xnano/canvas.py
def iter(
    self, *, count: int | None = None, seconds: float | None = None
) -> Iterator[Frame]:
    """
    Iterate over the canvas's frames, drawing between them; pass ``count=`` or ``seconds=``,
    or break out of the loop.

    Each iteration yields a [`Frame`][xnano.context.Frame], and whatever you change
    before asking for the next one is drawn. On a canvas from ``new()`` each iteration
    moves the clock by one frame of its ``fps``, so the same loop produces the same
    frames every run; without ``count`` or ``seconds`` it runs as fast as your code. A
    canvas that runs by itself draws as you declare, so there this only paces your code
    at its ``fps``: if you are slow, frames are skipped rather than queued.

    [`stop`][xnano.canvas.Canvas.stop] freezes the time ``iter()`` follows: the
    iterations go on drawing and hooks still run, but the clock stands still until
    [`resume`][xnano.canvas.Canvas.resume]. To move a stopped canvas one frame, call
    [`step`][xnano.canvas.Canvas.step].

    The iteration ends when ``count`` frames have been yielded, when the canvas's time has
    run for ``seconds`` (whichever comes first), when the canvas is asked to
    [`quit`][xnano.canvas.Canvas.quit] or closed, or when you break out of the loop.

    Args:
        count: The most frames to yield.
        seconds: The most seconds of the canvas's time to run for.

    Raises:
        TerminalError: ``seconds`` without ``count`` on a stopped canvas, which would
            never reach the end.
        TerminalClosedError: The canvas is closed.

    Examples:
        ```python
        import xnano

        with xnano.new(width=8, height=1, fps=10) as canvas:
            frames = list(canvas.iter(count=3))
        assert [frame.elapsed_seconds for frame in frames] == [0.0, 0.1, 0.2]
        ```
    """
    self._require_own_presence()
    canvas = self._canvas
    canvas._require_open()
    if count is not None and (
        isinstance(count, bool) or not isinstance(count, int) or count < 0
    ):
        raise ValueError(
            f'count must be a whole number of frames, 0 or more, but was {count!r}.'
        )
    if seconds is not None and (
        isinstance(seconds, bool)
        or not isinstance(seconds, (int, float))
        or not seconds > 0
    ):
        raise ValueError(
            f'seconds must be a number above zero, but was {seconds!r}. Pass count= to '
            f'stop after a number of frames instead.'
        )
    if seconds is not None and count is None:
        canvas._refuse_endless_seconds(seconds)
    return canvas._frames(count, seconds)

step

step(*, delta_seconds: float | None = None) -> Frame

Move time by delta_seconds, one frame by default, and draw exactly one frame.

On a canvas from new() this always works, and it moves time even while the canvas is stopped, which leaves it stopped: it is how a stopped canvas is walked one frame at a time. A canvas that runs by itself can be stepped only while it is stopped. One frame is 1 / fps seconds; default steps and the frames of iter share one count, so sixty of them at fps=60 are exactly one second. A large delta_seconds is one jump: timers that it crossed all run, and so do fixed-step every hooks, at most 100000 of their steps in one step; the steps that remain run in the steps that follow.

Parameters:

  • delta_seconds (float | None, default: None ) –

    How far to move, in seconds, above zero; one frame when None.

Returns:

Raises:

Examples:

import xnano

with xnano.new(width=8, height=1, fps=10) as canvas:
    frame = canvas.step()
    assert frame.elapsed_seconds == 0.1
    canvas.stop()
    canvas.step(delta_seconds=0.5)
    assert canvas.time.elapsed == 0.6
    assert canvas.time.is_stopped
Source code in xnano/canvas.py
def step(self, *, delta_seconds: float | None = None) -> Frame:
    """
    Move time by ``delta_seconds``, one frame by default, and draw exactly one frame.

    On a canvas from ``new()`` this always works, and it moves time even while the
    canvas is stopped, which leaves it stopped: it is how a stopped canvas is walked one
    frame at a time. A canvas that runs by itself can be stepped only while it is
    stopped. One frame is ``1 / fps`` seconds; default steps and the frames of
    [`iter`][xnano.canvas.Canvas.iter] share one count, so sixty of them at ``fps=60``
    are exactly one second. A large ``delta_seconds`` is one jump: timers that it crossed
    all run, and so do fixed-step ``every`` hooks, at most 100000 of their steps in one
    step; the steps that remain run in the steps that follow.

    Args:
        delta_seconds: How far to move, in seconds, above zero; one frame when ``None``.

    Returns:
        The [`Frame`][xnano.context.Frame] that was drawn.

    Raises:
        ValueError: ``delta_seconds`` is not above zero.
        TerminalError: The canvas runs by itself and is not stopped.
        TerminalClosedError: The canvas was closed.

    Examples:
        ```python
        import xnano

        with xnano.new(width=8, height=1, fps=10) as canvas:
            frame = canvas.step()
            assert frame.elapsed_seconds == 0.1
            canvas.stop()
            canvas.step(delta_seconds=0.5)
            assert canvas.time.elapsed == 0.6
            assert canvas.time.is_stopped
        ```
    """
    self._require_own_presence()
    return self._canvas._step(delta_seconds)

deliver_events

deliver_events() -> None

Deliver pushed events to their hooks and draw a frame, without moving time.

Events pushed with push reach their hooks at the canvas's next step; this is that step on a canvas whose clock you move. A canvas that runs by itself delivers them on its own.

Examples:

import xnano
from xnano_core import Event

with xnano.new(width=8, height=1) as canvas:
    heard = []
    canvas.hook(lambda context: heard.append(context.event.kind))
    canvas.push(Event.custom('ping'))
    canvas.deliver_events()
    assert heard == ['custom']
    assert canvas.time.elapsed == 0.0
Source code in xnano/canvas.py
def deliver_events(self) -> None:
    """
    Deliver pushed events to their hooks and draw a frame, without moving time.

    Events pushed with [`push`][xnano.canvas.Canvas.push] reach their hooks at the
    canvas's next step; this is that step on a canvas whose clock you move. A canvas that
    runs by itself delivers them on its own.

    Examples:
        ```python
        import xnano
        from xnano_core import Event

        with xnano.new(width=8, height=1) as canvas:
            heard = []
            canvas.hook(lambda context: heard.append(context.event.kind))
            canvas.push(Event.custom('ping'))
            canvas.deliver_events()
            assert heard == ['custom']
            assert canvas.time.elapsed == 0.0
        ```
    """
    self._require_own_presence()
    canvas = self._canvas
    canvas._require_open()
    if canvas._host.is_autonomous:
        canvas._host.call(lambda: canvas._runtime.request_frame())
        return
    canvas._raise_failure()
    canvas._runtime.step()
    canvas._pushed_pointer = None
    canvas._raise_failure()

stop

stop() -> None

Freeze time: the same as Time.stop.

The canvas keeps drawing and hooks still hear input, but animations, effects, formulas, timers and the beat stand still until resume. step moves a stopped canvas one frame at a time. To end a loop, call quit.

Source code in xnano/canvas.py
def stop(self) -> None:
    """
    Freeze time: the same as [`Time.stop`][xnano.time.Time.stop].

    The canvas keeps drawing and hooks still hear input, but animations, effects,
    formulas, timers and the beat stand still until [`resume`][xnano.canvas.Canvas.resume].
    [`step`][xnano.canvas.Canvas.step] moves a stopped canvas one frame at a time. To end a
    loop, call [`quit`][xnano.canvas.Canvas.quit].
    """
    self._require_own_presence()
    self._canvas._time.stop()

resume

resume() -> None

Let time run on from where stop froze it.

Source code in xnano/canvas.py
def resume(self) -> None:
    """Let time run on from where [`stop`][xnano.canvas.Canvas.stop] froze it."""
    self._require_own_presence()
    self._canvas._time.resume()

quit

quit() -> None

Ask the canvas's loop to end.

Safe from any thread, including from a hook. A running iter(), run(), wait_for_key() or prompt() ends. On a canvas from new() the canvas stays open for writing and rendering; on a canvas that runs by itself (realtime=True, or on the screen) the loop ends and the canvas closes.

Source code in xnano/canvas.py
def quit(self) -> None:
    """
    Ask the canvas's loop to end.

    Safe from any thread, including from a hook. A running ``iter()``, ``run()``,
    ``wait_for_key()`` or ``prompt()`` ends. On a canvas from ``new()`` the canvas stays
    open for writing and rendering; on a canvas that runs by itself (``realtime=True``, or
    on the screen) the loop ends and the canvas closes.
    """
    self._require_own_presence()
    canvas = self._canvas
    canvas._stop_requested.set()
    if not canvas._is_closed and canvas._host.is_autonomous:
        canvas._host.post(lambda: canvas._stop_runtime())

run

run(*, quit_on: KeyName | Sequence[KeyName] | None = 'ctrl+c') -> None

Wait until the canvas's loop ends.

A canvas that runs by itself draws and listens on its own, so this only keeps your program alive until a quit key is pressed, or something calls quit (a hook, another thread). Declare what you want before calling it.

Parameters:

  • quit_on (KeyName | Sequence[KeyName] | None, default: 'ctrl+c' ) –

    The key, or keys, that end the loop, such as 'q' or ('q', 'escape'). 'ctrl+c' by default; give None to leave ending to your own code.

Raises:

  • TerminalError –

    A callback raised on the canvas's loop, or the canvas has a clock that only moves when you move it, so waiting would never end.

Source code in xnano/canvas.py
def run(self, *, quit_on: KeyName | Sequence[KeyName] | None = 'ctrl+c') -> None:
    """
    Wait until the canvas's loop ends.

    A canvas that runs by itself draws and listens on its own, so this only keeps your
    program alive until a quit key is pressed, or something calls
    [`quit`][xnano.canvas.Canvas.quit] (a hook, another thread). Declare what you want
    before calling it.

    Args:
        quit_on: The key, or keys, that end the loop, such as ``'q'`` or
            ``('q', 'escape')``. ``'ctrl+c'`` by default; give ``None`` to leave ending
            to your own code.

    Raises:
        TerminalError: A callback raised on the canvas's loop, or the canvas has a clock
            that only moves when you move it, so waiting would never end.
    """
    canvas = self._canvas
    canvas._require_open()
    host = canvas._host
    if not isinstance(host, _ThreadHost):
        raise TerminalError(
            'A canvas with a manual clock never moves by itself, so run() would wait '
            'forever. Drive it with canvas.iter() or canvas.step(), open it on the screen '
            'with canvas.open(), or create it with realtime=True.'
        )
    if quit_on is not None:
        keys = (quit_on,) if isinstance(quit_on, str) else tuple(quit_on)
        if canvas._is_live and canvas._resolved_gates.get('keyboard') is False:
            # With the keyboard off, only the interrupt gate can end the loop on Ctrl-C.
            keys = tuple(key for key in keys if key != 'ctrl+c')
        if keys:

            @canvas.hook(on='key', keys=keys)
            def quit_key(context: Context[DepsT, StateT]) -> None:
                context.canvas.quit()

    try:
        while not host.wait(0.1):
            pass
    except KeyboardInterrupt:
        canvas.quit()
        raise
    failure = canvas._failure
    if failure is not None:
        canvas._failure = None
        raise TerminalError(canvas._failure_text(failure)) from failure

wait_for_key

wait_for_key(*keys: KeyName) -> str

Wait until a key is pressed, and return its name.

Your program waits here while the canvas goes on drawing and animating. It reads like input(): nothing else runs on your thread until the key arrives.

Parameters:

  • keys (KeyName, default: () ) –

    The keys to wait for, such as 'enter' or 'q'. Give none to take any key.

Returns:

  • str –

    The name of the key that was pressed.

Raises:

  • TerminalError –

    The canvas ended, or has a manual clock, so no key could arrive.

Examples:

In a program: canvas.wait_for_key() after adding "Press any key to start". Here a timer plays the part of the person pressing the key:

import xnano
from xnano.events import Event

with xnano.new(realtime=True, width=30, height=2) as canvas:
    canvas.write('Press enter to start')

    @canvas.hook(on='tick', after=0.05)
    def press(context: xnano.Context) -> None:
        canvas.push(Event.key_press('enter'))

    assert canvas.wait_for_key('enter') == 'enter'
Source code in xnano/canvas.py
def wait_for_key(self, *keys: KeyName) -> str:
    """
    Wait until a key is pressed, and return its name.

    Your program waits here while the canvas goes on drawing and animating. It reads
    like ``input()``: nothing else runs on your thread until the key arrives.

    Args:
        keys: The keys to wait for, such as ``'enter'`` or ``'q'``. Give none to take any key.

    Returns:
        The name of the key that was pressed.

    Raises:
        TerminalError: The canvas ended, or has a manual clock, so no key could arrive.

    Examples:
        In a program: ``canvas.wait_for_key()`` after adding "Press any key to start".
        Here a timer plays the part of the person pressing the key:

        ```python
        import xnano
        from xnano.events import Event

        with xnano.new(realtime=True, width=30, height=2) as canvas:
            canvas.write('Press enter to start')

            @canvas.hook(on='tick', after=0.05)
            def press(context: xnano.Context) -> None:
                canvas.push(Event.key_press('enter'))

            assert canvas.wait_for_key('enter') == 'enter'
        ```
    """
    pressed: list[str] = []
    done = threading.Event()

    @self.hook(on='key', keys=keys or None)
    def hear(context: Context[DepsT, StateT]) -> None:
        key = context.keyboard.key
        if key is not None:
            pressed.append(key)
        context.hook.remove()
        done.set()

    self._wait_until(done, 'a key')
    return pressed[0]

prompt

prompt(text: str, *, x: int = 0, y: int = 0, default: str = '') -> str

Prompt for a line of text, and return what was typed.

The prompt and the answer appear on the canvas while your program waits, like input(). Enter submits and Backspace deletes.

Parameters:

  • text (str) –

    What to ask, such as 'Your name: '.

  • x (int, default: 0 ) –

    The column of the prompt.

  • y (int, default: 0 ) –

    The row of the prompt.

  • default (str, default: '' ) –

    Text already in the answer, which the user can edit.

Returns:

  • str –

    What was typed, without the prompt.

Raises:

  • TerminalError –

    The canvas ended, or has a manual clock, so no answer could arrive.

Examples:

In a program: name = canvas.prompt('Your name: '). Here a timer plays the part of the person typing:

import xnano
from xnano.events import Event

with xnano.new(realtime=True, width=30, height=2) as canvas:

    @canvas.hook(on='tick', after=0.05)
    def type_answer(context: xnano.Context) -> None:
        for letter in 'Ada':
            canvas.push(Event.key_press(letter))
        canvas.push(Event.key_press('enter'))

    assert canvas.prompt('Your name: ') == 'Ada'
Source code in xnano/canvas.py
def prompt(self, text: str, *, x: int = 0, y: int = 0, default: str = '') -> str:
    """
    Prompt for a line of text, and return what was typed.

    The prompt and the answer appear on the canvas while your program waits, like
    ``input()``. ``Enter`` submits and ``Backspace`` deletes.

    Args:
        text: What to ask, such as ``'Your name: '``.
        x: The column of the prompt.
        y: The row of the prompt.
        default: Text already in the answer, which the user can edit.

    Returns:
        What was typed, without the prompt.

    Raises:
        TerminalError: The canvas ended, or has a manual clock, so no answer could arrive.

    Examples:
        In a program: ``name = canvas.prompt('Your name: ')``. Here a timer plays the part
        of the person typing:

        ```python
        import xnano
        from xnano.events import Event

        with xnano.new(realtime=True, width=30, height=2) as canvas:

            @canvas.hook(on='tick', after=0.05)
            def type_answer(context: xnano.Context) -> None:
                for letter in 'Ada':
                    canvas.push(Event.key_press(letter))
                canvas.push(Event.key_press('enter'))

            assert canvas.prompt('Your name: ') == 'Ada'
        ```
    """
    answer = [default]
    done = threading.Event()

    shown = self.write(text + default + ' ', x=x, y=y)

    def show() -> None:
        line = text + answer[0]
        shown.text = line + ' '
        self.cursor.position = (x + _core.display_width(line), y)

    cursor = self.cursor
    previous = (cursor.shape, cursor.is_blinking, cursor.is_visible, cursor.position)
    following = cursor._canvas._cursor_follow
    self.cursor.shape = 'bar'
    self.cursor.is_blinking = True
    self.cursor.is_visible = True
    show()

    @self.hook(on='key', when="keyboard.kind != 'release'")
    def edit(context: Context[DepsT, StateT]) -> None:
        key = context.keyboard
        if key.key is None:
            return
        if key.key == 'enter':
            done.set()
            return
        if key.key == 'backspace':
            answer[0] = answer[0][:-1]
        elif key.text is not None and not key.modifiers - {'shift'}:
            answer[0] += key.text
        show()

    try:
        self._wait_until(done, 'an answer')
    finally:
        edit.remove()
        cursor.is_visible = False
        cursor.is_blinking = previous[1]
        cursor.shape = previous[0]
        if following is None:
            cursor.position = previous[3]
        else:
            cursor.follow(following[0], x=following[1], y=following[2])
        cursor.is_visible = previous[2]
        shown.remove()
    return answer[0]

render

render(*, format: Literal['text', 'ansi'] = 'text') -> str

Draw the current frame and return it, without a screen.

A canvas that was written into another one returns its own cells alone, at its own size, as they stand in the outer canvas's current frame.

Parameters:

  • format (Literal['text', 'ansi'], default: 'text' ) –

    'text' for the characters alone, 'ansi' for the characters with their colors and styles as escape codes.

Raises:

  • ValueError –

    format is neither 'text' nor 'ansi'.

Examples:

import xnano

with xnano.new(width=6, height=2) as canvas:
    canvas.write('hello', y=1)
    assert canvas.render().splitlines() == ['', 'hello']
    assert 'hello' in canvas.render(format='ansi')
Source code in xnano/canvas.py
def render(self, *, format: Literal['text', 'ansi'] = 'text') -> str:
    """
    Draw the current frame and return it, without a screen.

    A canvas that was written into another one returns its own cells alone, at its own
    size, as they stand in the outer canvas's current frame.

    Args:
        format: ``'text'`` for the characters alone, ``'ansi'`` for the characters with
            their colors and styles as escape codes.

    Raises:
        ValueError: ``format`` is neither ``'text'`` nor ``'ansi'``.

    Examples:
        ```python
        import xnano

        with xnano.new(width=6, height=2) as canvas:
            canvas.write('hello', y=1)
            assert canvas.render().splitlines() == ['', 'hello']
            assert 'hello' in canvas.render(format='ansi')
        ```
    """
    if format not in ('text', 'ansi'):
        raise ValueError(
            f"format must be 'text' or 'ansi', but was {format!r}. 'text' gives the "
            f"characters, 'ansi' the characters with their colors."
        )
    self._require_own_presence()
    canvas = self._canvas
    canvas._require_open()
    if canvas is not self:
        root_buffer = self._root

        def render_child() -> str:
            canvas._runtime.render()
            return root_buffer.render(format=format)

        return canvas._host.call(render_child)
    snapshot = canvas._host.call(lambda: canvas._runtime.render())
    if format == 'ansi':
        return snapshot.ansi
    return '\n'.join(snapshot.lines())

push

push(event: Event) -> None

Deliver an event to the canvas as if it had been typed, clicked or reported.

Hooks hear it the same way as input from the keyboard and mouse, at the canvas's next step, whatever the input gates say. Build events with Event; Event.custom(name, data=...) makes one of your own.

Parameters:

  • event (Event) –

    The event to deliver.

Raises:

Examples:

import xnano
from xnano_core import Event

with xnano.new(width=8, height=1) as canvas:
    heard = []
    canvas.hook(
        lambda context: heard.append(context.event.custom_name),
        when=lambda context: context.event.kind == 'custom',
    )
    canvas.push(Event.custom('volume_changed'))
    canvas.deliver_events()
assert heard == ['volume_changed']
Source code in xnano/canvas.py
def push(self, event: _core.Event) -> None:
    """
    Deliver an event to the canvas as if it had been typed, clicked or reported.

    Hooks hear it the same way as input from the keyboard and mouse, at the canvas's
    next step, whatever the input gates say. Build events with
    [`Event`][xnano_core.Event]; ``Event.custom(name, data=...)`` makes one of your own.

    Args:
        event: The event to deliver.

    Raises:
        TypeError: The value is not an [`Event`][xnano_core.Event].

    Examples:
        ```python
        import xnano
        from xnano_core import Event

        with xnano.new(width=8, height=1) as canvas:
            heard = []
            canvas.hook(
                lambda context: heard.append(context.event.custom_name),
                when=lambda context: context.event.kind == 'custom',
            )
            canvas.push(Event.custom('volume_changed'))
            canvas.deliver_events()
        assert heard == ['volume_changed']
        ```
    """
    if not isinstance(event, _core.Event):
        raise TypeError(
            f'push() takes an Event such as xnano_core.Event.custom("ping"), but got '
            f'{event!r}. To press a key, push xnano_core.Event.key_press("q"), or call '
            f'context.keyboard.push("q") inside a hook.'
        )
    self._require_own_presence()
    canvas = self._canvas
    canvas._require_open()
    canvas._host.post(lambda: canvas._runtime.inject(event))

play

play(playable: PlaySource, /, **options: Unpack[PlayOptions]) -> PlayHandle

Play a note, a sound, a symbol name, a melody or a sequence of those, and return at once.

This is audio.play; see it for what playable may be and for every option. On a canvas from new() the sound is recorded instead of played; read it with audio.render_sound.

Parameters:

  • playable (PlaySource) –

    What to play: a note, a sound, a symbol name, melody text such as 'C4 E4 G4:2', a MIDI number, a frequency, or a sequence of those.

  • options (Unpack[PlayOptions], default: {} ) –

    The settings of PlayOptions, such as instrument, volume, pan, beats and at_beat.

Returns:

Examples:

import xnano

with xnano.new(width=8, height=1, bpm=100) as canvas:
    canvas.play('C4 E4 G4:2 rest C5+E5+G5', instrument='bell')
    canvas.step(delta_seconds=1.0)
    assert canvas.audio.render_sound().duration_milliseconds == 1000.0
Source code in xnano/canvas.py
def play(self, playable: PlaySource, /, **options: Unpack[PlayOptions]) -> PlayHandle:
    """
    Play a note, a sound, a symbol name, a melody or a sequence of those, and return at once.

    This is [`audio.play`][xnano.audio.AudioDevice.play]; see it for what ``playable`` may
    be and for every option. On a canvas from ``new()`` the sound is recorded instead of
    played; read it with [`audio.render_sound`][xnano.audio.AudioDevice.render_sound].

    Args:
        playable: What to play: a note, a sound, a symbol name, melody text such as
            ``'C4 E4 G4:2'``, a MIDI number, a frequency, or a sequence of those.
        options: The settings of [`PlayOptions`][xnano.types.PlayOptions], such as
            ``instrument``, ``volume``, ``pan``, ``beats`` and ``at_beat``.

    Returns:
        A [`PlayHandle`][xnano.music.PlayHandle] that tells when the sound is over.

    Examples:
        ```python
        import xnano

        with xnano.new(width=8, height=1, bpm=100) as canvas:
            canvas.play('C4 E4 G4:2 rest C5+E5+G5', instrument='bell')
            canvas.step(delta_seconds=1.0)
            assert canvas.audio.render_sound().duration_milliseconds == 1000.0
        ```
    """
    self._require_own_presence()
    if hasattr(playable, '__xnano_play__') or not isinstance(playable, str):
        handle = _play_component(
            cast('Canvas[object, object]', self._canvas), playable, options
        )
        if handle is not None:
            return handle
    return self.audio.play(playable, **options)

state_reference

state_reference(reader: Callable[[StateT], ValueT]) -> StateReference[ValueT]

A core reference for one field of the canvas's state.

The reference can be used as an axis, a range bound or a widget parameter.

Parameters:

  • reader (Callable[[StateT], ValueT]) –

    A function or lambda that reads the field, such as lambda state: state.volume.

Raises:

Source code in xnano/canvas.py
def state_reference(
    self, reader: Callable[[StateT], ValueT]
) -> _core.StateReference[ValueT]:
    """
    A core reference for one field of the canvas's state.

    The reference can be used as an axis, a range bound or a widget parameter.

    Args:
        reader: A function or lambda that reads the field, such as
            ``lambda state: state.volume``.

    Raises:
        TypeError: ``reader`` is not a function.
        TerminalError: The canvas was created without ``state``.
    """
    if not callable(reader):
        raise TypeError(
            f'state_reference() takes a function that reads one field, but got '
            f'{reader!r}. Pass a function such as lambda state: state.volume.'
        )
    store = self._canvas._state_store
    if store is None:
        raise TerminalError(
            'This canvas has no state, so state_reference() has nothing to read. Create it with '
            'xnano.new(state=YourDataclass()).'
        )
    return _reference_for(store, reader)

open

open(canvas: Canvas[DepsT, StateT], /, *, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, **settings: Unpack[OpenSettings]) -> Terminal[DepsT, StateT]
open(content: Content | None = None, /, *, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, fps: float = 60.0, bpm: float = 120.0, effects: Sequence[RenderableEffectSpecification] = (), **settings: Unpack[CanvasSettings]) -> Terminal[None, None]
open(content: Content | None = None, /, *, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, fps: float = 60.0, bpm: float = 120.0, effects: Sequence[RenderableEffectSpecification] = (), state: StateT, **settings: Unpack[CanvasSettings]) -> Terminal[None, StateT]
open(content: Content | None = None, /, *, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, fps: float = 60.0, bpm: float = 120.0, effects: Sequence[RenderableEffectSpecification] = (), deps: DepsT, **settings: Unpack[CanvasSettings]) -> Terminal[DepsT, None]
open(content: Content | None = None, /, *, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, fps: float = 60.0, bpm: float = 120.0, effects: Sequence[RenderableEffectSpecification] = (), deps: DepsT, state: StateT, **settings: Unpack[CanvasSettings]) -> Terminal[DepsT, StateT]
open(canvas_or_content: Canvas[object, object] | Content | None = None, /, *, title: str | None = None, width: int | Percentage | str | None = None, height: int | Percentage | str | None = None, **settings: object) -> Terminal[DepsT, StateT]

Put a canvas on your screen and return it as a Terminal.

Given a canvas from new, this is Canvas.open: the same object is attached to the screen and its time continues. Given content or nothing, it creates the canvas with new() (the same keywords, except realtime) and attaches that. From here on, anything you write, remove or draw appears at once, on a loop of the canvas's own that your code never waits for. A with block closes it and gives the screen back.

By default the canvas takes over the whole window, on the alternate screen so that your shell's history comes back untouched; width='100%', height='100%' asks for the same. Give height (and optionally width) to draw in a region of that size below the cursor instead, in cells or as a percentage of the window such as '50%'; that region is erased when the canvas closes, unless you pass clear=False. title is the window title.

The input gates say what the screen reports: mouse for buttons and drags, scroll for the wheel, keyboard, paste, focus and interrupt (Ctrl-C), and capture holds back what the program prints while the canvas is on the screen. See GateSettings. mouse, scroll and paste are turned on by themselves when a hook registered before opening needs them.

Parameters:

  • canvas_or_content (Canvas[object, object] | Content | None, default: None ) –

    A canvas from new(), or content to write into a new canvas.

  • title (str | None, default: None ) –

    The window title; None keeps the canvas's title.

  • width (int | Percentage | str | None, default: None ) –

    The width of the region to draw in, in cells or as a percentage of the window; the whole window when neither width nor height is given.

  • height (int | Percentage | str | None, default: None ) –

    The height of the region to draw in, in cells or as a percentage of the window.

  • settings (object, default: {} ) –

    The other keywords of new() except realtime: the gates, the canvas's look, fps, bpm, effects, deps and state.

Returns:

Raises:

  • TerminalAlreadyOpenError –

    Another canvas is already on the screen.

  • TerminalError –

    There is no terminal to draw on.

  • TypeError –

    realtime was given, an appearance keyword was given with content, or a keyword is unknown.

  • ValueError –

    A width was given without a height, or a value is out of range.

Examples:

with xnano.open(height=3, mouse=True) as terminal:
    terminal.write('hello')
    terminal.run(quit_on='q')
Source code in xnano/terminal.py
def open(
    canvas_or_content: Canvas[object, object] | Content | None = None,
    /,
    *,
    title: str | None = None,
    width: int | Percentage | str | None = None,
    height: int | Percentage | str | None = None,
    **settings: object,
) -> Terminal[DepsT, StateT]:
    """
    Put a canvas on your screen and return it as a [`Terminal`][xnano.Terminal].

    Given a canvas from [`new`][xnano.new], this is [`Canvas.open`][xnano.canvas.Canvas.open]:
    the same object is attached to the screen and its time continues. Given content or
    nothing, it creates the canvas with ``new()`` (the same keywords, except ``realtime``)
    and attaches that. From here on, anything you write, remove or draw appears at once, on a
    loop of the canvas's own that your code never waits for. A ``with`` block closes it and
    gives the screen back.

    By default the canvas takes over the whole window, on the alternate screen so that your
    shell's history comes back untouched; ``width='100%', height='100%'`` asks for the same.
    Give ``height`` (and optionally ``width``) to draw in a region of that size below the cursor
    instead, in cells or as a percentage of the window such as ``'50%'``; that region is erased
    when the canvas closes, unless you pass ``clear=False``. ``title`` is the window title.

    The input gates say what the screen reports: ``mouse`` for buttons and drags, ``scroll``
    for the wheel, ``keyboard``, ``paste``, ``focus`` and ``interrupt`` (``Ctrl-C``), and
    ``capture`` holds back what the program prints while the canvas is on the screen. See
    [`GateSettings`][xnano.types.GateSettings]. ``mouse``, ``scroll`` and ``paste`` are turned
    on by themselves when a hook registered before opening needs them.

    Args:
        canvas_or_content: A canvas from ``new()``, or content to write into a new canvas.
        title: The window title; ``None`` keeps the canvas's title.
        width: The width of the region to draw in, in cells or as a percentage of the
            window; the whole window when neither ``width`` nor ``height`` is given.
        height: The height of the region to draw in, in cells or as a percentage of the
            window.
        settings: The other keywords of ``new()`` except ``realtime``: the gates, the
            canvas's look, ``fps``, ``bpm``, ``effects``, ``deps`` and ``state``.

    Returns:
        The open [`Terminal`][xnano.Terminal], which is also the one module-level functions
        such as [`active`][xnano.active] find.

    Raises:
        TerminalAlreadyOpenError: Another canvas is already on the screen.
        TerminalError: There is no terminal to draw on.
        TypeError: ``realtime`` was given, an appearance keyword was given with content, or a
            keyword is unknown.
        ValueError: A ``width`` was given without a ``height``, or a value is out of range.

    Examples:
        ```text
        with xnano.open(height=3, mouse=True) as terminal:
            terminal.write('hello')
            terminal.run(quit_on='q')
        ```
    """
    if isinstance(canvas_or_content, Canvas):
        return cast('Canvas[DepsT, StateT]', canvas_or_content).open(
            title=title,
            width=width,
            height=height,
            **settings,  # type: ignore[arg-type]
        )
    if 'realtime' in settings:
        raise TypeError(
            'open() takes no realtime: a canvas on the screen always runs on the wall clock. '
            'Remove realtime=..., or keep the canvas off the screen with '
            'xnano.new(realtime=True).'
        )
    if height is None and not _is_whole_window(_check_size('width', width)):
        raise ValueError(
            'A canvas on the screen with a width needs a height too, because it draws in a '
            "region of that size. Add height=... (cells, or a percentage such as '50%'), or "
            'remove width.'
        )
    # Checked here as well as in new(), so that an error names open().
    own = {'fps', 'bpm', 'effects', 'deps', 'state', 'layout', 'edges'}
    _, appearance = _split_settings(
        'open', {name: value for name, value in settings.items() if name not in own}
    )
    if canvas_or_content is not None and appearance:
        raise _appearance_with_content_error('open', appearance)
    canvas: Canvas[DepsT, StateT] = _create(
        canvas_or_content,  # type: ignore[arg-type]
        title=title,
        width=width,
        height=height,
        **settings,  # type: ignore[arg-type]
    )
    try:
        return canvas.open()
    except BaseException:
        canvas.close(raise_failure=False)
        raise