input parsing

This commit is contained in:
Will McGugan
2021-06-11 21:26:30 +01:00
parent ebd05b2520
commit cb76022ccc
10 changed files with 811 additions and 49 deletions

View File

@@ -8,6 +8,9 @@
}
],
"settings": {
"python.pythonPath": "/Users/willmcgugan/Venvs/textual/bin/python"
"python.pythonPath": "/Users/willmcgugan/Venvs/textual/bin/python",
"cSpell.words": [
"tcgetattr"
]
}
}

View File

@@ -0,0 +1,300 @@
from typing import Dict, Tuple, Union
from .keys import Keys
# Mapping of vt100 escape codes to Keys.
ANSI_SEQUENCES: Dict[str, Union[Keys, Tuple[Keys, ...]]] = {
# Control keys.
"\x00": Keys.ControlAt, # Control-At (Also for Ctrl-Space)
"\x01": Keys.ControlA, # Control-A (home)
"\x02": Keys.ControlB, # Control-B (emacs cursor left)
"\x03": Keys.ControlC, # Control-C (interrupt)
"\x04": Keys.ControlD, # Control-D (exit)
"\x05": Keys.ControlE, # Control-E (end)
"\x06": Keys.ControlF, # Control-F (cursor forward)
"\x07": Keys.ControlG, # Control-G
"\x08": Keys.ControlH, # Control-H (8) (Identical to '\b')
"\x09": Keys.ControlI, # Control-I (9) (Identical to '\t')
"\x0a": Keys.ControlJ, # Control-J (10) (Identical to '\n')
"\x0b": Keys.ControlK, # Control-K (delete until end of line; vertical tab)
"\x0c": Keys.ControlL, # Control-L (clear; form feed)
"\x0d": Keys.ControlM, # Control-M (13) (Identical to '\r')
"\x0e": Keys.ControlN, # Control-N (14) (history forward)
"\x0f": Keys.ControlO, # Control-O (15)
"\x10": Keys.ControlP, # Control-P (16) (history back)
"\x11": Keys.ControlQ, # Control-Q
"\x12": Keys.ControlR, # Control-R (18) (reverse search)
"\x13": Keys.ControlS, # Control-S (19) (forward search)
"\x14": Keys.ControlT, # Control-T
"\x15": Keys.ControlU, # Control-U
"\x16": Keys.ControlV, # Control-V
"\x17": Keys.ControlW, # Control-W
"\x18": Keys.ControlX, # Control-X
"\x19": Keys.ControlY, # Control-Y (25)
"\x1a": Keys.ControlZ, # Control-Z
"\x1b": Keys.Escape, # Also Control-[
"\x9b": Keys.ShiftEscape,
"\x1c": Keys.ControlBackslash, # Both Control-\ (also Ctrl-| )
"\x1d": Keys.ControlSquareClose, # Control-]
"\x1e": Keys.ControlCircumflex, # Control-^
"\x1f": Keys.ControlUnderscore, # Control-underscore (Also for Ctrl-hyphen.)
# ASCII Delete (0x7f)
# Vt220 (and Linux terminal) send this when pressing backspace. We map this
# to ControlH, because that will make it easier to create key bindings that
# work everywhere, with the trade-off that it's no longer possible to
# handle backspace and control-h individually for the few terminals that
# support it. (Most terminals send ControlH when backspace is pressed.)
# See: http://www.ibb.net/~anne/keyboard.html
"\x7f": Keys.ControlH,
# --
# Various
"\x1b[1~": Keys.Home, # tmux
"\x1b[2~": Keys.Insert,
"\x1b[3~": Keys.Delete,
"\x1b[4~": Keys.End, # tmux
"\x1b[5~": Keys.PageUp,
"\x1b[6~": Keys.PageDown,
"\x1b[7~": Keys.Home, # xrvt
"\x1b[8~": Keys.End, # xrvt
"\x1b[Z": Keys.BackTab, # shift + tab
"\x1b\x09": Keys.BackTab, # Linux console
"\x1b[~": Keys.BackTab, # Windows console
# --
# Function keys.
"\x1bOP": Keys.F1,
"\x1bOQ": Keys.F2,
"\x1bOR": Keys.F3,
"\x1bOS": Keys.F4,
"\x1b[[A": Keys.F1, # Linux console.
"\x1b[[B": Keys.F2, # Linux console.
"\x1b[[C": Keys.F3, # Linux console.
"\x1b[[D": Keys.F4, # Linux console.
"\x1b[[E": Keys.F5, # Linux console.
"\x1b[11~": Keys.F1, # rxvt-unicode
"\x1b[12~": Keys.F2, # rxvt-unicode
"\x1b[13~": Keys.F3, # rxvt-unicode
"\x1b[14~": Keys.F4, # rxvt-unicode
"\x1b[15~": Keys.F5,
"\x1b[17~": Keys.F6,
"\x1b[18~": Keys.F7,
"\x1b[19~": Keys.F8,
"\x1b[20~": Keys.F9,
"\x1b[21~": Keys.F10,
"\x1b[23~": Keys.F11,
"\x1b[24~": Keys.F12,
"\x1b[25~": Keys.F13,
"\x1b[26~": Keys.F14,
"\x1b[28~": Keys.F15,
"\x1b[29~": Keys.F16,
"\x1b[31~": Keys.F17,
"\x1b[32~": Keys.F18,
"\x1b[33~": Keys.F19,
"\x1b[34~": Keys.F20,
# Xterm
"\x1b[1;2P": Keys.F13,
"\x1b[1;2Q": Keys.F14,
# '\x1b[1;2R': Keys.F15, # Conflicts with CPR response.
"\x1b[1;2S": Keys.F16,
"\x1b[15;2~": Keys.F17,
"\x1b[17;2~": Keys.F18,
"\x1b[18;2~": Keys.F19,
"\x1b[19;2~": Keys.F20,
"\x1b[20;2~": Keys.F21,
"\x1b[21;2~": Keys.F22,
"\x1b[23;2~": Keys.F23,
"\x1b[24;2~": Keys.F24,
# --
# Control + function keys.
"\x1b[1;5P": Keys.ControlF1,
"\x1b[1;5Q": Keys.ControlF2,
# "\x1b[1;5R": Keys.ControlF3, # Conflicts with CPR response.
"\x1b[1;5S": Keys.ControlF4,
"\x1b[15;5~": Keys.ControlF5,
"\x1b[17;5~": Keys.ControlF6,
"\x1b[18;5~": Keys.ControlF7,
"\x1b[19;5~": Keys.ControlF8,
"\x1b[20;5~": Keys.ControlF9,
"\x1b[21;5~": Keys.ControlF10,
"\x1b[23;5~": Keys.ControlF11,
"\x1b[24;5~": Keys.ControlF12,
"\x1b[1;6P": Keys.ControlF13,
"\x1b[1;6Q": Keys.ControlF14,
# "\x1b[1;6R": Keys.ControlF15, # Conflicts with CPR response.
"\x1b[1;6S": Keys.ControlF16,
"\x1b[15;6~": Keys.ControlF17,
"\x1b[17;6~": Keys.ControlF18,
"\x1b[18;6~": Keys.ControlF19,
"\x1b[19;6~": Keys.ControlF20,
"\x1b[20;6~": Keys.ControlF21,
"\x1b[21;6~": Keys.ControlF22,
"\x1b[23;6~": Keys.ControlF23,
"\x1b[24;6~": Keys.ControlF24,
# --
# Tmux (Win32 subsystem) sends the following scroll events.
"\x1b[62~": Keys.ScrollUp,
"\x1b[63~": Keys.ScrollDown,
"\x1b[200~": Keys.BracketedPaste, # Start of bracketed paste.
# --
# Sequences generated by numpad 5. Not sure what it means. (It doesn't
# appear in 'infocmp'. Just ignore.
"\x1b[E": Keys.Ignore, # Xterm.
"\x1b[G": Keys.Ignore, # Linux console.
# --
# Meta/control/escape + pageup/pagedown/insert/delete.
"\x1b[3;2~": Keys.ShiftDelete, # xterm, gnome-terminal.
"\x1b[5;2~": Keys.ShiftPageUp,
"\x1b[6;2~": Keys.ShiftPageDown,
"\x1b[2;3~": (Keys.Escape, Keys.Insert),
"\x1b[3;3~": (Keys.Escape, Keys.Delete),
"\x1b[5;3~": (Keys.Escape, Keys.PageUp),
"\x1b[6;3~": (Keys.Escape, Keys.PageDown),
"\x1b[2;4~": (Keys.Escape, Keys.ShiftInsert),
"\x1b[3;4~": (Keys.Escape, Keys.ShiftDelete),
"\x1b[5;4~": (Keys.Escape, Keys.ShiftPageUp),
"\x1b[6;4~": (Keys.Escape, Keys.ShiftPageDown),
"\x1b[3;5~": Keys.ControlDelete, # xterm, gnome-terminal.
"\x1b[5;5~": Keys.ControlPageUp,
"\x1b[6;5~": Keys.ControlPageDown,
"\x1b[3;6~": Keys.ControlShiftDelete,
"\x1b[5;6~": Keys.ControlShiftPageUp,
"\x1b[6;6~": Keys.ControlShiftPageDown,
"\x1b[2;7~": (Keys.Escape, Keys.ControlInsert),
"\x1b[5;7~": (Keys.Escape, Keys.ControlPageDown),
"\x1b[6;7~": (Keys.Escape, Keys.ControlPageDown),
"\x1b[2;8~": (Keys.Escape, Keys.ControlShiftInsert),
"\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageDown),
"\x1b[6;8~": (Keys.Escape, Keys.ControlShiftPageDown),
# --
# Arrows.
# (Normal cursor mode).
"\x1b[A": Keys.Up,
"\x1b[B": Keys.Down,
"\x1b[C": Keys.Right,
"\x1b[D": Keys.Left,
"\x1b[H": Keys.Home,
"\x1b[F": Keys.End,
# Tmux sends following keystrokes when control+arrow is pressed, but for
# Emacs ansi-term sends the same sequences for normal arrow keys. Consider
# it a normal arrow press, because that's more important.
# (Application cursor mode).
"\x1bOA": Keys.Up,
"\x1bOB": Keys.Down,
"\x1bOC": Keys.Right,
"\x1bOD": Keys.Left,
"\x1bOF": Keys.End,
"\x1bOH": Keys.Home,
# Shift + arrows.
"\x1b[1;2A": Keys.ShiftUp,
"\x1b[1;2B": Keys.ShiftDown,
"\x1b[1;2C": Keys.ShiftRight,
"\x1b[1;2D": Keys.ShiftLeft,
"\x1b[1;2F": Keys.ShiftEnd,
"\x1b[1;2H": Keys.ShiftHome,
# Meta + arrow keys. Several terminals handle this differently.
# The following sequences are for xterm and gnome-terminal.
# (Iterm sends ESC followed by the normal arrow_up/down/left/right
# sequences, and the OSX Terminal sends ESCb and ESCf for "alt
# arrow_left" and "alt arrow_right." We don't handle these
# explicitly, in here, because would could not distinguish between
# pressing ESC (to go to Vi navigation mode), followed by just the
# 'b' or 'f' key. These combinations are handled in
# the input processor.)
"\x1b[1;3A": (Keys.Escape, Keys.Up),
"\x1b[1;3B": (Keys.Escape, Keys.Down),
"\x1b[1;3C": (Keys.Escape, Keys.Right),
"\x1b[1;3D": (Keys.Escape, Keys.Left),
"\x1b[1;3F": (Keys.Escape, Keys.End),
"\x1b[1;3H": (Keys.Escape, Keys.Home),
# Alt+shift+number.
"\x1b[1;4A": (Keys.Escape, Keys.ShiftDown),
"\x1b[1;4B": (Keys.Escape, Keys.ShiftUp),
"\x1b[1;4C": (Keys.Escape, Keys.ShiftRight),
"\x1b[1;4D": (Keys.Escape, Keys.ShiftLeft),
"\x1b[1;4F": (Keys.Escape, Keys.ShiftEnd),
"\x1b[1;4H": (Keys.Escape, Keys.ShiftHome),
# Control + arrows.
"\x1b[1;5A": Keys.ControlUp, # Cursor Mode
"\x1b[1;5B": Keys.ControlDown, # Cursor Mode
"\x1b[1;5C": Keys.ControlRight, # Cursor Mode
"\x1b[1;5D": Keys.ControlLeft, # Cursor Mode
"\x1b[1;5F": Keys.ControlEnd,
"\x1b[1;5H": Keys.ControlHome,
# Tmux sends following keystrokes when control+arrow is pressed, but for
# Emacs ansi-term sends the same sequences for normal arrow keys. Consider
# it a normal arrow press, because that's more important.
"\x1b[5A": Keys.ControlUp,
"\x1b[5B": Keys.ControlDown,
"\x1b[5C": Keys.ControlRight,
"\x1b[5D": Keys.ControlLeft,
"\x1bOc": Keys.ControlRight, # rxvt
"\x1bOd": Keys.ControlLeft, # rxvt
# Control + shift + arrows.
"\x1b[1;6A": Keys.ControlShiftDown,
"\x1b[1;6B": Keys.ControlShiftUp,
"\x1b[1;6C": Keys.ControlShiftRight,
"\x1b[1;6D": Keys.ControlShiftLeft,
"\x1b[1;6F": Keys.ControlShiftEnd,
"\x1b[1;6H": Keys.ControlShiftHome,
# Control + Meta + arrows.
"\x1b[1;7A": (Keys.Escape, Keys.ControlDown),
"\x1b[1;7B": (Keys.Escape, Keys.ControlUp),
"\x1b[1;7C": (Keys.Escape, Keys.ControlRight),
"\x1b[1;7D": (Keys.Escape, Keys.ControlLeft),
"\x1b[1;7F": (Keys.Escape, Keys.ControlEnd),
"\x1b[1;7H": (Keys.Escape, Keys.ControlHome),
# Meta + Shift + arrows.
"\x1b[1;8A": (Keys.Escape, Keys.ControlShiftDown),
"\x1b[1;8B": (Keys.Escape, Keys.ControlShiftUp),
"\x1b[1;8C": (Keys.Escape, Keys.ControlShiftRight),
"\x1b[1;8D": (Keys.Escape, Keys.ControlShiftLeft),
"\x1b[1;8F": (Keys.Escape, Keys.ControlShiftEnd),
"\x1b[1;8H": (Keys.Escape, Keys.ControlShiftHome),
# Meta + arrow on (some?) Macs when using iTerm defaults (see issue #483).
"\x1b[1;9A": (Keys.Escape, Keys.Up),
"\x1b[1;9B": (Keys.Escape, Keys.Down),
"\x1b[1;9C": (Keys.Escape, Keys.Right),
"\x1b[1;9D": (Keys.Escape, Keys.Left),
# --
# Control/shift/meta + number in mintty.
# (c-2 will actually send c-@ and c-6 will send c-^.)
"\x1b[1;5p": Keys.Control0,
"\x1b[1;5q": Keys.Control1,
"\x1b[1;5r": Keys.Control2,
"\x1b[1;5s": Keys.Control3,
"\x1b[1;5t": Keys.Control4,
"\x1b[1;5u": Keys.Control5,
"\x1b[1;5v": Keys.Control6,
"\x1b[1;5w": Keys.Control7,
"\x1b[1;5x": Keys.Control8,
"\x1b[1;5y": Keys.Control9,
"\x1b[1;6p": Keys.ControlShift0,
"\x1b[1;6q": Keys.ControlShift1,
"\x1b[1;6r": Keys.ControlShift2,
"\x1b[1;6s": Keys.ControlShift3,
"\x1b[1;6t": Keys.ControlShift4,
"\x1b[1;6u": Keys.ControlShift5,
"\x1b[1;6v": Keys.ControlShift6,
"\x1b[1;6w": Keys.ControlShift7,
"\x1b[1;6x": Keys.ControlShift8,
"\x1b[1;6y": Keys.ControlShift9,
"\x1b[1;7p": (Keys.Escape, Keys.Control0),
"\x1b[1;7q": (Keys.Escape, Keys.Control1),
"\x1b[1;7r": (Keys.Escape, Keys.Control2),
"\x1b[1;7s": (Keys.Escape, Keys.Control3),
"\x1b[1;7t": (Keys.Escape, Keys.Control4),
"\x1b[1;7u": (Keys.Escape, Keys.Control5),
"\x1b[1;7v": (Keys.Escape, Keys.Control6),
"\x1b[1;7w": (Keys.Escape, Keys.Control7),
"\x1b[1;7x": (Keys.Escape, Keys.Control8),
"\x1b[1;7y": (Keys.Escape, Keys.Control9),
"\x1b[1;8p": (Keys.Escape, Keys.ControlShift0),
"\x1b[1;8q": (Keys.Escape, Keys.ControlShift1),
"\x1b[1;8r": (Keys.Escape, Keys.ControlShift2),
"\x1b[1;8s": (Keys.Escape, Keys.ControlShift3),
"\x1b[1;8t": (Keys.Escape, Keys.ControlShift4),
"\x1b[1;8u": (Keys.Escape, Keys.ControlShift5),
"\x1b[1;8v": (Keys.Escape, Keys.ControlShift6),
"\x1b[1;8w": (Keys.Escape, Keys.ControlShift7),
"\x1b[1;8x": (Keys.Escape, Keys.ControlShift8),
"\x1b[1;8y": (Keys.Escape, Keys.ControlShift9),
}

View File

@@ -0,0 +1,160 @@
from __future__ import annotations
import asyncio
import os
from codecs import getincrementaldecoder
import selectors
import sys
import logging
import termios
import tty
from typing import Any, TYPE_CHECKING
from threading import Event, Thread
if TYPE_CHECKING:
from rich.console import Console
from . import events
from .driver import Driver
from ._types import MessageTarget
from ._xterm_parser import XTermParser
log = logging.getLogger("rich")
class LinuxDriver(Driver):
def __init__(self, console: "Console", target: "MessageTarget") -> None:
super().__init__(console, target)
self.fileno = sys.stdin.fileno()
self.attrs_before: list[Any] | None = None
self.exit_event = Event()
self._key_thread: Thread | None = None
def enable_mouse_support(self) -> None:
write = self.console.file.write
write("\x1b[?1000h")
write("\x1b[?1015h")
write("\x1b[?1006h")
self.console.file.flush()
# Note: E.g. lxterminal understands 1000h, but not the urxvt or sgr
# extensions.
def disable_mouse_support(self) -> None:
write = self.console.file.write
write("\x1b[?1000l")
write("\x1b[?1015l")
write("\x1b[?1006l")
self.console.file.flush()
def start_application_mode(self):
self.console.set_alt_screen(True)
self.enable_mouse_support()
try:
self.attrs_before = termios.tcgetattr(self.fileno)
except termios.error:
# Ignore attribute errors.
self.attrs_before = None
try:
newattr = termios.tcgetattr(self.fileno)
except termios.error:
pass
else:
newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG])
newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG])
# VMIN defines the number of characters read at a time in
# non-canonical mode. It seems to default to 1 on Linux, but on
# Solaris and derived operating systems it defaults to 4. (This is
# because the VMIN slot is the same as the VEOF slot, which
# defaults to ASCII EOT = Ctrl-D = 4.)
newattr[tty.CC][termios.VMIN] = 1
termios.tcsetattr(self.fileno, termios.TCSANOW, newattr)
self.console.show_cursor(False)
self.console.file.write("\033[?1003h\n")
self._key_thread = Thread(
target=self.run_input_thread, args=(asyncio.get_event_loop(),)
)
self._key_thread.start()
@classmethod
def _patch_lflag(cls, attrs: int) -> int:
return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
@classmethod
def _patch_iflag(cls, attrs: int) -> int:
return attrs & ~(
# Disable XON/XOFF flow control on output and input.
# (Don't capture Ctrl-S and Ctrl-Q.)
# Like executing: "stty -ixon."
termios.IXON
| termios.IXOFF
|
# Don't translate carriage return into newline on input.
termios.ICRNL
| termios.INLCR
| termios.IGNCR
)
def stop_application_mode(self) -> None:
self.console.set_alt_screen(False)
self.console.show_cursor(True)
self.exit_event.set()
# if self._key_thread is not None:
# self._key_thread.join()
if self.attrs_before is not None:
try:
termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before)
except termios.error:
pass
self.disable_mouse_support()
def run_input_thread(self, loop) -> None:
def send_event(event: events.Event) -> None:
asyncio.run_coroutine_threadsafe(
self._target.post_message(event),
loop=loop,
)
selector = selectors.DefaultSelector()
selector.register(self.fileno, selectors.EVENT_READ)
fileno = self.fileno
parser = XTermParser()
utf8_decoder = getincrementaldecoder("utf-8")().decode
decode = utf8_decoder
read = os.read
log.debug("started key thread")
while not self.exit_event.is_set():
selector_events = selector.select(0.1)
for _selector_key, mask in selector_events:
unicode_data = decode(read(fileno, 1024))
log.debug(repr(unicode_data))
for key in parser.feed(unicode_data):
send_event(events.Key(self._target, key=key))
if __name__ == "__main__":
from time import sleep
from rich.console import Console
from . import events
console = Console()
from .app import App
class MyApp(App):
async def on_startup(self, event: events.Startup) -> None:
self.set_timer(5, callback=self.close_messages)
MyApp.run()

View File

@@ -1,4 +1,5 @@
from collections import deque
import io
from typing import (
Callable,
Deque,
@@ -26,7 +27,7 @@ class Awaitable:
"""Raise any ParseErrors"""
class _ReadBytes(Awaitable):
class _Read(Awaitable):
__slots__ = ["remaining"]
def __init__(self, count: int) -> None:
@@ -36,14 +37,31 @@ class _ReadBytes(Awaitable):
return f"_ReadBytes({self.remaining})"
class _Read1(Awaitable):
__slots__: list[str] = []
class _ReadUntil(Awaitable):
__slots__ = ["sep", "max_bytes"]
def __init__(self, sep, max_bytes=None):
self.sep = sep
self.max_bytes = max_bytes
T = TypeVar("T")
TokenCallback = Callable[[T], None]
class Parser(Generic[T]):
read = _ReadBytes
read = _Read
read1 = _Read1
read_until = _ReadUntil
def __init__(self) -> None:
self._buffer = bytearray()
self._buffer = io.StringIO()
self._eof = False
self._tokens: Deque[T] = deque()
self._gen = self.parse(self._tokens.append)
@@ -57,62 +75,92 @@ class Parser(Generic[T]):
self._gen = self.parse(self._tokens.append)
self._awaiting = next(self._gen)
def feed(self, data: bytes) -> Iterable[T]:
def feed(self, data: str) -> Iterable[T]:
if self._eof:
raise ParseError("end of file reached")
raise ParseError("end of file reached") from None
if not data:
self._eof = True
try:
self._gen.send(self._buffer[:])
self._gen.send(self._buffer.getvalue())
except StopIteration:
raise ParseError("end of file reached") from None
while self._tokens:
yield self._tokens.popleft()
del self._buffer[:]
self._buffer.truncate(0)
return
# self._gen.throw(ParseError("unexpected eof of file"))
_buffer = self._buffer
pos = 0
tokens = self._tokens
popleft = tokens.popleft
while tokens:
yield popleft()
while pos < len(data):
if isinstance(self._awaiting, _ReadBytes):
_awaiting = self._awaiting
if isinstance(_awaiting, _Read1):
self._awaiting = self._gen.send(data[pos : pos + 1])
pos += 1
remaining = self._awaiting.remaining
elif isinstance(_awaiting, _Read):
remaining = _awaiting.remaining
chunk = data[pos : pos + remaining]
chunk_size = len(chunk)
pos += chunk_size
try:
self._awaiting.validate(chunk)
except ParseError as error:
self._awaiting = self._gen.throw(error)
continue
_buffer.extend(chunk)
_buffer.write(chunk)
remaining -= chunk_size
if remaining:
self._awaiting.remaining = remaining
_awaiting.remaining = remaining
else:
self._awaiting = self._gen.send(_buffer[:])
del _buffer[:]
_awaiting = self._gen.send(_buffer.getvalue())
_buffer.truncate(0)
while self._tokens:
yield self._tokens.popleft()
elif isinstance(_awaiting, _ReadUntil):
chunk = data[pos:]
_buffer.write(chunk)
sep = _awaiting.sep
sep_index = _buffer.getvalue().find(sep)
def parse(self, on_token: Callable[[T], None]) -> Generator[Awaitable, bytes, None]:
if sep_index == -1:
pos += len(chunk)
if (
_awaiting.max_bytes is not None
and _buffer.tell() > _awaiting.max_bytes
):
self._gen.throw(ParseError(f"expected {sep}"))
else:
sep_index += len(sep)
if (
_awaiting.max_bytes is not None
and sep_index > _awaiting.max_bytes
):
self._gen.throw(ParseError(f"expected {sep}"))
data = _buffer.getvalue()[sep_index:]
pos = 0
self._awaiting = self._gen.send(_buffer.getvalue()[:sep_index])
_buffer.truncate(0)
while tokens:
yield popleft()
def parse(self, on_token: Callable[[T], None]) -> Generator[Awaitable, T, None]:
return
yield
if __name__ == "__main__":
data = b"Where there is a Will there is a way!"
data = "Where there is a Will there is a way!"
class TestParser(Parser[bytes]):
class TestParser(Parser[str]):
def parse(
self, on_token: Callable[[bytes], None]
) -> Generator[Awaitable, bytes, None]:
while data := (yield self.read(3)):
print("-", data)
self, on_token: Callable[[str], None]
) -> Generator[Awaitable, str, None]:
on_token((yield self.read_until("a")))
while data := (yield self.read1()):
on_token(data)
test_parser = TestParser()
@@ -121,6 +169,6 @@ if __name__ == "__main__":
for n in range(0, len(data), 2):
for token in test_parser.feed(data[n : n + 2]):
print(bytes(token))
for token in test_parser.feed(b""):
print(bytes(token))
print(token)
for token in test_parser.feed(""):
print(token)

View File

@@ -0,0 +1,35 @@
from typing import Iterable, Generator
from .keys import Keys
from ._parser import Awaitable, Parser, TokenCallback
from ._ansi_sequences import ANSI_SEQUENCES
class XTermParser(Parser[Keys]):
def parse(self, on_token: TokenCallback) -> Generator[Awaitable, Keys, None]:
ESC = "\x1b"
read1 = self.read1
get_ansi_sequence = ANSI_SEQUENCES.get
while not self.is_eof:
character: str = yield read1()
if character == ESC:
sequence = character
while True:
sequence += yield read1()
keys = get_ansi_sequence(sequence, None)
if keys is not None:
on_token(keys)
break
else:
on_token(character)
if __name__ == "__main__":
parser = XTermParser()
import os
import sys
for token in parser.feed(sys.stdin.read(20)):
print(token)

View File

@@ -4,7 +4,7 @@ import asyncio
import logging
import signal
from typing import Any, ClassVar
from typing import Any, ClassVar, Type
from rich.control import Control
from rich.repr import rich_repr, RichReprResult
@@ -14,7 +14,8 @@ from rich.console import Console
from . import events
from ._context import active_app
from .driver import Driver, CursesDriver
from .driver import Driver
from ._linux_driver import LinuxDriver
from .message_pump import MessagePump
from .view import View, LayoutView
@@ -33,13 +34,15 @@ class App(MessagePump):
def __init__(
self,
console: Console = None,
view: View = None,
screen: bool = True,
driver: Type[Driver] = None,
view: View = None,
title: str = "Megasoma Application",
):
super().__init__()
self.console = console or get_console()
self._screen = screen
self.driver = driver or LinuxDriver
self.title = title
self.view = view or LayoutView()
self.children: set[MessagePump] = set()
@@ -48,9 +51,11 @@ class App(MessagePump):
yield "title", self.title
@classmethod
def run(cls, console: Console = None, screen: bool = True):
def run(
cls, console: Console = None, screen: bool = True, driver: Type[Driver] = None
):
async def run_app() -> None:
app = cls(console=console, screen=screen)
app = cls(console=console, screen=screen, driver=driver)
await app.process_messages()
asyncio.run(run_app())
@@ -61,9 +66,15 @@ class App(MessagePump):
asyncio.run_coroutine_threadsafe(self.post_message(event), loop=loop)
async def process_messages(self) -> None:
log.debug("driver=%r", self.driver)
loop = asyncio.get_event_loop()
driver = CursesDriver(self.console, self)
driver.start_application_mode()
driver = self.driver(self.console, self)
try:
driver.start_application_mode()
except Exception:
log.exception("error starting application mode")
raise
loop.add_signal_handler(signal.SIGINT, self.on_keyboard_interupt)
active_app.set(self)

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
import logging
@@ -28,14 +30,22 @@ class Driver(ABC):
self._target = target
@abstractmethod
def start_application_mode(self):
def start_application_mode(self) -> None:
...
@abstractmethod
def stop_application_mode(self):
def stop_application_mode(self) -> None:
...
class LinuxDriver(Driver):
def start_application_mode(self):
pass
def stop_application_mode(self):
pass
class CursesDriver(Driver):
_MOUSE_PRESSED = [

View File

@@ -10,6 +10,7 @@ from rich.repr import rich_repr, RichReprResult
from .message import Message
from ._types import Callback, MessageTarget
from .keys import Keys
if TYPE_CHECKING:
@@ -122,20 +123,15 @@ class Shutdown(Event, type=EventType.SHUTDOWN):
@rich_repr
class Key(Event, type=EventType.KEY, bubble=True):
__slots__ = ["code"]
__slots__ = ["key"]
def __init__(self, sender: MessageTarget, code: int) -> None:
def __init__(self, sender: MessageTarget, key: Keys | str) -> None:
super().__init__(sender)
self.code = code
self.key = key.value if isinstance(key, Keys) else key
def __rich_repr__(self) -> RichReprResult:
yield "code", self.code
yield "key", self.key
@property
def key(self) -> str:
return chr(self.code)
@rich_repr
class Move(Event, type=EventType.MOVE):

198
src/textual/keys.py Normal file
View File

@@ -0,0 +1,198 @@
from enum import Enum
# Adapted from prompt toolkit https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/prompt_toolkit/keys.py
class Keys(str, Enum):
"""
List of keys for use in key bindings.
Note that this is an "StrEnum", all values can be compared against
strings.
"""
value: str
Escape = "escape" # Also Control-[
ShiftEscape = "shift+escape"
ControlAt = "ctrl+@" # Also Control-Space.
ControlA = "ctrl+a"
ControlB = "ctrl+b"
ControlC = "ctrl+c"
ControlD = "ctrl+d"
ControlE = "ctrl+e"
ControlF = "ctrl+f"
ControlG = "ctrl+g"
ControlH = "ctrl+h"
ControlI = "ctrl+i" # Tab
ControlJ = "ctrl+j" # Newline
ControlK = "ctrl+k"
ControlL = "ctrl+l"
ControlM = "ctrl+m" # Carriage return
ControlN = "ctrl+n"
ControlO = "ctrl+o"
ControlP = "ctrl+p"
ControlQ = "ctrl+q"
ControlR = "ctrl+r"
ControlS = "ctrl+s"
ControlT = "ctrl+t"
ControlU = "ctrl+u"
ControlV = "ctrl+v"
ControlW = "ctrl+w"
ControlX = "ctrl+x"
ControlY = "ctrl+y"
ControlZ = "ctrl+z"
Control1 = "ctrl+1"
Control2 = "ctrl+2"
Control3 = "ctrl+3"
Control4 = "ctrl+4"
Control5 = "ctrl+5"
Control6 = "ctrl+6"
Control7 = "ctrl+7"
Control8 = "ctrl+8"
Control9 = "ctrl+9"
Control0 = "ctrl+0"
ControlShift1 = "ctrl+shift+1"
ControlShift2 = "ctrl+shift+2"
ControlShift3 = "ctrl+shift+3"
ControlShift4 = "ctrl+shift+4"
ControlShift5 = "ctrl+shift+5"
ControlShift6 = "ctrl+shift+6"
ControlShift7 = "ctrl+shift+7"
ControlShift8 = "ctrl+shift+8"
ControlShift9 = "ctrl+shift+9"
ControlShift0 = "ctrl+shift+0"
ControlBackslash = "ctrl+\\"
ControlSquareClose = "ctrl+]"
ControlCircumflex = "ctrl+^"
ControlUnderscore = "ctrl+_"
Left = "left"
Right = "right"
Up = "up"
Down = "down"
Home = "home"
End = "end"
Insert = "insert"
Delete = "delete"
PageUp = "pageup"
PageDown = "pagedown"
ControlLeft = "ctrl+left"
ControlRight = "ctrl+right"
ControlUp = "ctrl+up"
ControlDown = "ctrl+down"
ControlHome = "ctrl+home"
ControlEnd = "ctrl+end"
ControlInsert = "ctrl+insert"
ControlDelete = "ctrl+delete"
ControlPageUp = "ctrl+pageup"
ControlPageDown = "ctrl+pagedown"
ShiftLeft = "shift+left"
ShiftRight = "shift+right"
ShiftUp = "shift+up"
ShiftDown = "shift+down"
ShiftHome = "shift+home"
ShiftEnd = "shift+end"
ShiftInsert = "shift+insert"
ShiftDelete = "shift+delete"
ShiftPageUp = "shift+pageup"
ShiftPageDown = "shift+pagedown"
ControlShiftLeft = "ctrl+shift+left"
ControlShiftRight = "ctrl+shift+right"
ControlShiftUp = "ctrl+shift+up"
ControlShiftDown = "ctrl+shift+down"
ControlShiftHome = "ctrl+shift+home"
ControlShiftEnd = "ctrl+shift+end"
ControlShiftInsert = "ctrl+shift+insert"
ControlShiftDelete = "ctrl+shift+delete"
ControlShiftPageUp = "ctrl+shift+pageup"
ControlShiftPageDown = "ctrl+shift+pagedown"
BackTab = "shift+tab" # shift + tab
F1 = "f1"
F2 = "f2"
F3 = "f3"
F4 = "f4"
F5 = "f5"
F6 = "f6"
F7 = "f7"
F8 = "f8"
F9 = "f9"
F10 = "f10"
F11 = "f11"
F12 = "f12"
F13 = "f13"
F14 = "f14"
F15 = "f15"
F16 = "f16"
F17 = "f17"
F18 = "f18"
F19 = "f19"
F20 = "f20"
F21 = "f21"
F22 = "f22"
F23 = "f23"
F24 = "f24"
ControlF1 = "ctrl+f1"
ControlF2 = "ctrl+f2"
ControlF3 = "ctrl+f3"
ControlF4 = "ctrl+f4"
ControlF5 = "ctrl+f5"
ControlF6 = "ctrl+f6"
ControlF7 = "ctrl+f7"
ControlF8 = "ctrl+f8"
ControlF9 = "ctrl+f9"
ControlF10 = "ctrl+f10"
ControlF11 = "ctrl+f11"
ControlF12 = "ctrl+f12"
ControlF13 = "ctrl+f13"
ControlF14 = "ctrl+f14"
ControlF15 = "ctrl+f15"
ControlF16 = "ctrl+f16"
ControlF17 = "ctrl+f17"
ControlF18 = "ctrl+f18"
ControlF19 = "ctrl+f19"
ControlF20 = "ctrl+f20"
ControlF21 = "ctrl+f21"
ControlF22 = "ctrl+f22"
ControlF23 = "ctrl+f23"
ControlF24 = "ctrl+f24"
# Matches any key.
Any = "<any>"
# Special.
ScrollUp = "<scroll-up>"
ScrollDown = "<scroll-down>"
CPRResponse = "<cursor-position-response>"
Vt100MouseEvent = "<vt100-mouse-event>"
WindowsMouseEvent = "<windowshift+mouse-event>"
BracketedPaste = "<bracketed-paste>"
# For internal use: key which is ignored.
# (The key binding for this key should not do anything.)
Ignore = "<ignore>"
# Some 'Key' aliases (for backwardshift+compatibility).
ControlSpace = ControlAt
Tab = ControlI
Enter = ControlM
Backspace = ControlH
# ShiftControl was renamed to ControlShift in
# 888fcb6fa4efea0de8333177e1bbc792f3ff3c24 (20 Feb 2020).
ShiftControlLeft = ControlShiftLeft
ShiftControlRight = ControlShiftRight
ShiftControlHome = ControlShiftHome
ShiftControlEnd = ControlShiftEnd

View File

@@ -147,6 +147,7 @@ class MessagePump:
except Exception:
log.exception("error getting message")
break
log.debug("%r -> %r", message, self)
# Combine any pending messages that may supersede this one
while True:
pending = self.peek_message()