Skip to content

xnano.input

xnano.input

xnano.input

What a hook reads from the keyboard, the mouse, the scroll wheel and the clipboard, and which keys and mouse buttons are held down right now, for programs that poll instead of listening.

Classes:

  • Input –

    The keys held down right now.

  • KeyboardInput –

    The key of the current event, as properties of a hook's context.

  • MouseInput –

    The mouse button or movement of the current event, as properties of a hook's context.

  • ScrollInput –

    The mouse wheel steps of the current event, as properties of a hook's context.

  • ClipboardInput –

    The pasted text of the current event, as a property of a hook's context.

KeyNames module-attribute

KeyNames = str | Sequence[str]

One key or mouse button name, or several that count the same, such as ('a', 'left').

The mouse buttons are called 'mouse_left', 'mouse_right' and 'mouse_middle'; see HeldMouseButtonName.

Input

Input(canvas: Canvas[object, object])

The keys held down right now.

A hook hears each key press once. A game wants the opposite: ask every step which keys are down. Get one from Canvas.input and read it anywhere, from any thread: inside a hook, in a hook(on='tick') update, or in your own loop.

A terminal that reports key releases (kitty, ghostty and wezterm, when the canvas is opened with keyboard='release_events') tells exactly when a key is let go. Any other terminal only sends repeats while a key is held, so a key counts as held for a short time after its last repeat.

The held mouse buttons are in the same vocabulary, named 'mouse_left', 'mouse_right' and 'mouse_middle', so canvas.input.is_down('mouse_left') asks whether the left button is held.

Attributes:

  • held_keys (frozenset[str]) –

    The names of the keys and mouse buttons held down right now.

  • pointer (tuple[int, int] | None) –

    The (x, y) cell the pointer was last seen at, with x growing to the right and y down, or None before it has moved. It is the cell that context.mouse.position gives as a point.

Examples:

import xnano

with xnano.new(width=20, height=3) as canvas:
    hero = canvas.write('@')

    @canvas.hook(on='tick', every=1 / 30)
    def walk(context: xnano.Context) -> None:
        step_x, step_y = canvas.input.direction()
        hero.x += step_x
        hero.y += step_y

    canvas.step()

Create the canvas's input. Use canvas.input instead of calling this.

Parameters:

Methods:

  • is_down –

    Whether any of the keys or mouse buttons is held down.

  • axis –

    -1, 0 or 1 from a pair of keys: the negative one, neither or both, the

  • direction –

    The way the arrow keys or wasd point, as (step_x, step_y).

Source code in xnano/input.py
def __init__(self, canvas: Canvas[object, object]) -> None:
    """
    Create the canvas's input. Use ``canvas.input`` instead of calling this.

    Args:
        canvas: The canvas the input belongs to.
    """
    self._canvas = canvas
    self._state.estimate_holds(timeout_seconds=_ESTIMATED_HOLD_SECONDS)

held_keys property

held_keys: frozenset[str]

The names of the keys and mouse buttons held down right now.

pointer property

pointer: tuple[int, int] | None

The (x, y) cell the pointer was last seen at, or None before it moved.

x grows to the right and y down. context.mouse.position gives the same cell as a point.

is_down

is_down(keys: KeyNames) -> bool

Whether any of the keys or mouse buttons is held down.

Parameters:

  • keys (KeyNames) –

    One key name such as 'space', or several such as ('a', 'left'). A mouse button is 'mouse_left', 'mouse_right' or 'mouse_middle'. A key is spelled as in KeyName; a chord such as 'ctrl+s' or a modifier alone is not held input.

Raises:

  • TypeError –

    A name is not text.

  • ValueError –

    A name is not a key or a mouse button, or is a chord or a modifier.

Source code in xnano/input.py
def is_down(self, keys: KeyNames) -> bool:
    """
    Whether any of the keys or mouse buttons is held down.

    Args:
        keys: One key name such as ``'space'``, or several such as ``('a', 'left')``. A
            mouse button is ``'mouse_left'``, ``'mouse_right'`` or ``'mouse_middle'``.
            A key is spelled as in [`KeyName`][xnano.types.KeyName]; a chord such as
            ``'ctrl+s'`` or a modifier alone is not held input.

    Raises:
        TypeError: A name is not text.
        ValueError: A name is not a key or a mouse button, or is a chord or a modifier.
    """
    held = self.held_keys
    return any(name in held for name in _names(keys))

axis

axis(negative: KeyNames, positive: KeyNames) -> int

-1, 0 or 1 from a pair of keys: the negative one, neither or both, the positive one.

Parameters:

  • negative (KeyNames) –

    The key or keys that push toward -1.

  • positive (KeyNames) –

    The key or keys that push toward 1.

Raises:

  • TypeError –

    A name is not text.

  • ValueError –

    A name is not a key or a mouse button, or is a chord or a modifier.

Source code in xnano/input.py
def axis(self, negative: KeyNames, positive: KeyNames) -> int:
    """
    ``-1``, ``0`` or ``1`` from a pair of keys: the negative one, neither or both, the
    positive one.

    Args:
        negative: The key or keys that push toward ``-1``.
        positive: The key or keys that push toward ``1``.

    Raises:
        TypeError: A name is not text.
        ValueError: A name is not a key or a mouse button, or is a chord or a modifier.
    """
    return int(self.is_down(positive)) - int(self.is_down(negative))

direction

direction(*, left: KeyNames = ('a', 'left'), right: KeyNames = ('d', 'right'), up: KeyNames = ('w', 'up'), down: KeyNames = ('s', 'down')) -> tuple[int, int]

The way the arrow keys or wasd point, as (step_x, step_y).

Each step is -1, 0 or 1; y grows downward, like the screen.

Parameters:

  • left (KeyNames, default: ('a', 'left') ) –

    The key or keys that mean left.

  • right (KeyNames, default: ('d', 'right') ) –

    The key or keys that mean right.

  • up (KeyNames, default: ('w', 'up') ) –

    The key or keys that mean up.

  • down (KeyNames, default: ('s', 'down') ) –

    The key or keys that mean down.

Raises:

  • TypeError –

    A name is not text.

  • ValueError –

    A name is not a key or a mouse button, or is a chord or a modifier.

Source code in xnano/input.py
def direction(
    self,
    *,
    left: KeyNames = ('a', 'left'),
    right: KeyNames = ('d', 'right'),
    up: KeyNames = ('w', 'up'),
    down: KeyNames = ('s', 'down'),
) -> tuple[int, int]:
    """
    The way the arrow keys or ``wasd`` point, as ``(step_x, step_y)``.

    Each step is ``-1``, ``0`` or ``1``; ``y`` grows downward, like the screen.

    Args:
        left: The key or keys that mean left.
        right: The key or keys that mean right.
        up: The key or keys that mean up.
        down: The key or keys that mean down.

    Raises:
        TypeError: A name is not text.
        ValueError: A name is not a key or a mouse button, or is a chord or a modifier.
    """
    return self.axis(left, right), self.axis(up, down)

KeyboardInput

KeyboardInput(canvas: Canvas[object, object], event: Event | None)

The key of the current event, as properties of a hook's context.

Get one from context.keyboard. It is always there: key is None unless the event the hook is handling is a key. push delivers a key to the canvas, so a hook can press a key for the program.

Attributes:

  • key (str | None) –

    The name of the key, such as 'q' or 'enter', or None.

  • kind (Literal['press', 'repeat', 'release'] | None) –

    'press', 'repeat' or 'release', or None.

  • modifiers (frozenset[str]) –

    The modifier keys held with it.

  • text (str | None) –

    The text the key types, or None for control keys and chords.

  • chord (str | None) –

    The key with its modifiers as one text such as 'ctrl+s', or None.

Examples:

import xnano

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

    @canvas.hook(on='key')
    def listen(context: xnano.Context) -> None:
        heard.append(context.keyboard.key)

    @canvas.hook(on='tick', after=0.1)
    def press_for_the_program(context: xnano.Context) -> None:
        context.keyboard.push('q')

    canvas.step(delta_seconds=0.2)
    canvas.deliver_events()
    assert heard == ['q']

Create the key view of an event. Use context.keyboard instead of calling this.

Parameters:

  • canvas (Canvas[object, object]) –

    The canvas the event happened in.

  • event (Event | None) –

    The event being handled, or None.

Methods:

  • get –

    The key event being handled, or None when the event is not a key.

  • push –

    Press a key on the canvas, as if it had been typed.

Source code in xnano/input.py
def __init__(self, canvas: Canvas[object, object], event: _core.Event | None) -> None:
    """
    Create the key view of an event. Use ``context.keyboard`` instead of calling this.

    Args:
        canvas: The canvas the event happened in.
        event: The event being handled, or ``None``.
    """
    self._canvas = canvas
    self._event = event

key property

key: str | None

The name of the key, such as 'q' or 'enter', or None.

kind property

kind: Literal['press', 'repeat', 'release'] | None

'press', 'repeat' or 'release', or None for any other event.

modifiers property

modifiers: frozenset[str]

The modifier keys held with the key; empty for any other event.

text property

text: str | None

The text the key types, or None for control keys, chords and other events.

chord property

chord: str | None

The key with its modifiers as one text such as 'ctrl+s', or None.

get

get() -> KeyEvent | None

The key event being handled, or None when the event is not a key.

Source code in xnano/input.py
def get(self) -> _core.KeyEvent | None:
    """The key event being handled, or ``None`` when the event is not a key."""
    event = self._event
    return event.key if event is not None else None

push

push(value: str | KeyEvent) -> None

Press a key on the canvas, as if it had been typed.

The canvas's hooks hear it at its next step, whatever the input gates say.

Parameters:

Raises:

  • TypeError –

    value is neither text nor a key event.

  • ValueError –

    value is text that does not name a key. The message suggests the closest key names.

Source code in xnano/input.py
def push(self, value: str | _core.KeyEvent) -> None:
    """
    Press a key on the canvas, as if it had been typed.

    The canvas's hooks hear it at its next step, whatever the input gates say.

    Args:
        value: A key name or chord such as ``'q'`` or ``'ctrl+s'``, or a
            [`KeyEvent`][xnano_core.KeyEvent].

    Raises:
        TypeError: ``value`` is neither text nor a key event.
        ValueError: ``value`` is text that does not name a key. The message suggests the
            closest key names.
    """
    if isinstance(value, _core.KeyEvent):
        self._canvas.push(
            _core.Event.key_press(_modifier_text(value), kind=value.kind)
        )
    else:
        if not isinstance(value, str):
            raise TypeError(
                f"keyboard.push takes a key name such as 'q' or 'ctrl+s', or a KeyEvent, "
                f'but got {value!r}. Pass the key as text: keyboard.push("q").'
            )
        from xnano._when import normalize_chord

        normalize_chord(value)
        self._canvas.push(_core.Event.key_press(value))

MouseInput

MouseInput(canvas: Canvas[object, object], event: Event | None)

The mouse button or movement of the current event, as properties of a hook's context.

Get one from context.mouse. It is always there: position is None unless the event the hook is handling is a mouse press, release, drag or move. A mouse wheel report is not a mouse event here; ScrollInput owns it.

Attributes:

  • position (Point | None) –

    The cell of the event, or None.

  • button (MouseButton | None) –

    'left', 'right' or 'middle', or None.

  • kind (Literal['press', 'release', 'drag', 'move'] | None) –

    'press', 'release', 'drag' or 'move', or None.

  • is_down (bool) –

    Whether any mouse button is held right now.

Create the mouse view of an event. Use context.mouse instead of calling this.

Parameters:

  • canvas (Canvas[object, object]) –

    The canvas the event happened in.

  • event (Event | None) –

    The event being handled, or None.

Methods:

  • get –

    The mouse event being handled, or None for any other event and for the wheel.

  • push –

    Move or click the mouse on the canvas, as if the person had.

Source code in xnano/input.py
def __init__(self, canvas: Canvas[object, object], event: _core.Event | None) -> None:
    """
    Create the mouse view of an event. Use ``context.mouse`` instead of calling this.

    Args:
        canvas: The canvas the event happened in.
        event: The event being handled, or ``None``.
    """
    self._canvas = canvas
    self._event = event

position property

position: Point | None

The cell of the event, or None.

button property

button: MouseButton | None

'left', 'right' or 'middle', or None.

kind property

kind: Literal['press', 'release', 'drag', 'move'] | None

'press', 'release', 'drag' or 'move', or None.

is_down property

is_down: bool

Whether any mouse button is held right now.

get

get() -> MouseEvent | None

The mouse event being handled, or None for any other event and for the wheel.

Source code in xnano/input.py
def get(self) -> _core.MouseEvent | None:
    """The mouse event being handled, or ``None`` for any other event and for the wheel."""
    event = self._event
    if event is None:
        return None
    mouse = event.mouse
    if mouse is None or mouse.kind == 'scroll':
        return None
    return mouse

push

push(value: MouseEvent | tuple[int, int]) -> None

Move or click the mouse on the canvas, as if the person had.

Parameters:

  • value (MouseEvent | tuple[int, int]) –

    A MouseEvent of kind press, release, drag or move (or an Event that holds one), or an (x, y) pair, which moves the pointer to that cell. A context.scroll.push((x, y)) that follows turns the wheel at that cell.

Raises:

  • TypeError –

    The value is a mouse wheel event, which belongs to context.scroll.push, or a pair that is not two whole numbers.

  • ValueError –

    A number of the pair is outside the range of a cell number.

Source code in xnano/input.py
def push(self, value: _core.MouseEvent | tuple[int, int]) -> None:
    """
    Move or click the mouse on the canvas, as if the person had.

    Args:
        value: A [`MouseEvent`][xnano_core.MouseEvent] of kind press, release, drag or
            move (or an [`Event`][xnano_core.Event] that holds one), or an ``(x, y)``
            pair, which moves the pointer to that cell. A ``context.scroll.push((x, y))``
            that follows turns the wheel at that cell.

    Raises:
        TypeError: The value is a mouse wheel event, which belongs to
            ``context.scroll.push``, or a pair that is not two whole numbers.
        ValueError: A number of the pair is outside the range of a cell number.
    """
    if isinstance(value, tuple):
        column, row = _cell_pair(value, 'mouse.push')
        state = self._canvas._input_state
        seen = None if state is None else state.pointer_cell
        event = _core.Event.mouse_event(column=column, row=row, kind='move')
        self._canvas._pushed_pointer = (
            (column, row),
            seen,
            self._canvas._time_source.frame,
        )
        self._canvas.push(event)
        return
    if isinstance(value, _core.Event):
        if value.mouse is None:
            raise TypeError(
                f'mouse.push takes a mouse event, but got a {value.kind!r} event. Push '
                f'it with canvas.push(event), or build a mouse event with '
                f'Event.mouse_event(...).'
            )
        value = value.mouse
    if not isinstance(value, _core.MouseEvent):
        raise TypeError(
            f'mouse.push takes a MouseEvent or an (x, y) pair, but got {value!r}.'
        )
    if value.kind == 'scroll':
        raise TypeError(
            'mouse.push cannot send a mouse wheel event, because the wheel belongs to '
            'scroll. Use context.scroll.push((x, y)) to scroll by that many steps.'
        )
    self._canvas.push(
        _core.Event.mouse_event(
            column=value.column, row=value.row, kind=value.kind, button=value.button
        )
    )

ScrollInput

ScrollInput(canvas: Canvas[object, object], event: Event | None)

The mouse wheel steps of the current event, as properties of a hook's context.

Get one from context.scroll. x and y are signed steps that grow right and down, so scrolling up is y == -1; both are 0 unless the event is a wheel report.

Attributes:

  • x (int) –

    Horizontal steps, positive to the right.

  • y (int) –

    Vertical steps, positive downward.

Create the wheel view of an event. Use context.scroll instead of calling this.

Parameters:

  • canvas (Canvas[object, object]) –

    The canvas the event happened in.

  • event (Event | None) –

    The event being handled, or None.

Methods:

  • get –

    The wheel event being handled, or None for any other event.

  • push –

    Turn the mouse wheel on the canvas, as if the person had.

Source code in xnano/input.py
def __init__(self, canvas: Canvas[object, object], event: _core.Event | None) -> None:
    """
    Create the wheel view of an event. Use ``context.scroll`` instead of calling this.

    Args:
        canvas: The canvas the event happened in.
        event: The event being handled, or ``None``.
    """
    self._canvas = canvas
    self._event = event

x property

x: int

Horizontal steps, positive to the right; 0 unless the event is a wheel report.

y property

y: int

Vertical steps, positive downward; 0 unless the event is a wheel report.

get

get() -> MouseEvent | None

The wheel event being handled, or None for any other event.

Source code in xnano/input.py
def get(self) -> _core.MouseEvent | None:
    """The wheel event being handled, or ``None`` for any other event."""
    event = self._event
    if event is None:
        return None
    mouse = event.mouse
    if mouse is None or mouse.kind != 'scroll':
        return None
    return mouse

push

push(value: tuple[int, int] | MouseEvent) -> None

Turn the mouse wheel on the canvas, as if the person had.

Parameters:

  • value (tuple[int, int] | MouseEvent) –

    The (x, y) steps to scroll by, at the cell the pointer was last seen at (a pointer moved with context.mouse.push counts as seen there at once), or a MouseEvent of kind scroll.

Raises:

  • TypeError –

    The value is a mouse event that is not a wheel report, which belongs to context.mouse.push, or a pair that is not two whole numbers.

  • ValueError –

    A number of the pair is outside the range of a step count.

Source code in xnano/input.py
def push(self, value: tuple[int, int] | _core.MouseEvent) -> None:
    """
    Turn the mouse wheel on the canvas, as if the person had.

    Args:
        value: The ``(x, y)`` steps to scroll by, at the cell the pointer was last seen
            at (a pointer moved with ``context.mouse.push`` counts as seen there at
            once), or a [`MouseEvent`][xnano_core.MouseEvent] of kind scroll.

    Raises:
        TypeError: The value is a mouse event that is not a wheel report, which belongs
            to ``context.mouse.push``, or a pair that is not two whole numbers.
        ValueError: A number of the pair is outside the range of a step count.
    """
    if isinstance(value, tuple):
        steps_x, steps_y = _cell_pair(
            value, 'scroll.push', description='steps of the wheel, such as (0, 1)'
        )
        state = self._canvas._input_state
        cell = None if state is None else state.pointer_cell
        pushed = self._canvas._pushed_pointer
        if (
            pushed is not None
            and pushed[1] == cell
            and pushed[2] == self._canvas._time_source.frame
        ):
            cell = pushed[0]
        column, row = cell if cell is not None else (0, 0)
    else:
        if not isinstance(value, _core.MouseEvent):
            raise TypeError(
                f'scroll.push takes a MouseEvent of kind scroll or an (x, y) pair of '
                f'steps, but got {value!r}.'
            )
        if value.kind != 'scroll':
            raise TypeError(
                f'scroll.push takes a mouse wheel event, but this is a {value.kind} '
                f'event. Use context.mouse.push(event) for presses, releases, drags and '
                f'moves.'
            )
        column, row = value.column, value.row
        steps_x, steps_y = value.scroll_x, value.scroll_y
    self._canvas.push(
        _core.Event.mouse_event(
            column=column, row=row, kind='scroll', scroll_x=steps_x, scroll_y=steps_y
        )
    )

ClipboardInput

ClipboardInput(canvas: Canvas[object, object], event: Event | None)

The pasted text of the current event, as a property of a hook's context.

Get one from context.clipboard. text is None unless the event is a paste.

Attributes:

  • text (str | None) –

    The text that was pasted, or None.

Create the paste view of an event. Use context.clipboard instead of calling this.

Parameters:

  • canvas (Canvas[object, object]) –

    The canvas the event happened in.

  • event (Event | None) –

    The event being handled, or None.

Methods:

  • get –

    The text that was pasted, or None when the event is not a paste.

  • push –

    Paste text on the canvas, as if the person had.

Source code in xnano/input.py
def __init__(self, canvas: Canvas[object, object], event: _core.Event | None) -> None:
    """
    Create the paste view of an event. Use ``context.clipboard`` instead of calling this.

    Args:
        canvas: The canvas the event happened in.
        event: The event being handled, or ``None``.
    """
    self._canvas = canvas
    self._event = event

text property

text: str | None

The text that was pasted, or None.

get

get() -> str | None

The text that was pasted, or None when the event is not a paste.

Source code in xnano/input.py
def get(self) -> str | None:
    """The text that was pasted, or ``None`` when the event is not a paste."""
    event = self._event
    return event.pasted_text if event is not None else None

push

push(value: str) -> None

Paste text on the canvas, as if the person had.

Parameters:

  • value (str) –

    The text to paste.

Raises:

Source code in xnano/input.py
def push(self, value: str) -> None:
    """
    Paste text on the canvas, as if the person had.

    Args:
        value: The text to paste.

    Raises:
        TypeError: The value is not text.
    """
    if not isinstance(value, str):
        raise TypeError(
            f'clipboard.push takes the text to paste, but received {value!r}. Pass a str, '
            f"such as context.clipboard.push('hello')."
        )
    self._canvas.push(_core.Event.paste(value))