Accepts text input from keyboard and emits content as message on hitting enter. Work in progress.
based on the brilliant pure-python TUI tools Textual and Rich from @textualizeio
Accepts text input from keyboard and emits content as message on hitting enter. Work in progress.
based on the brilliant pure-python TUI tools Textual and Rich from @textualizeio
| from __future__ import annotations | |
| from rich.panel import Panel | |
| from rich.align import Align | |
| from rich.pretty import Pretty | |
| from rich import box | |
| from rich.traceback import Traceback | |
| from rich.console import RenderableType | |
| import rich.repr | |
| from textual import events | |
| from textual.app import App | |
| from textual.reactive import Reactive | |
| from textual.widget import Widget | |
| from textual.message import Message, MessageTarget | |
| from textual.widgets import Placeholder, Footer, Header, Button, ScrollView | |
| from logging import getLogger | |
| log = getLogger("rich") | |
| @rich.repr.auto | |
| class dataFinal(Message, bubble=True): | |
| SQL = 0 | |
| TXT = 1 | |
| MD = 2 | |
| CSV = 3 | |
| def __rich_repr__(self) -> rich.repr.Result: | |
| yield "datatype", ["SQL", "TXT", "MD", "CSV"][self.datatype] | |
| yield "message:", self.mymessage | |
| def __init__( | |
| self, sender: MessageTarget, mymessage: str, datatype: int = 0 | |
| ) -> None: | |
| self.mymessage = mymessage | |
| self.datatype = datatype | |
| super().__init__(sender) | |
| @rich.repr.auto(angular=False) | |
| class Reader(Widget, can_focus=True): | |
| """Accepts text input from keyboard when has focus. | |
| Emits content as dataFinal message on hitting [enter]. | |
| Definitely a work in progress. | |
| """ | |
| has_focus: Reactive[bool] = Reactive(False) | |
| mouse_over = Reactive(False) | |
| style: Reactive[str] = Reactive("") | |
| height: Reactive[int | None] = Reactive(None) | |
| Reader_count: int = 0 | |
| def __init__( | |
| self, | |
| *, | |
| name: str | None = None, | |
| height: int | None = None, | |
| showdetail: int = 0, | |
| datatype: int = dataFinal.SQL, | |
| ) -> None: | |
| super().__init__(name=name) | |
| self.height = height | |
| self.showdetail = showdetail | |
| self.name = ( | |
| name | |
| if name is not None | |
| else f"{self.__class__.__name__}_{Reader.Reader_count}" | |
| ) | |
| self.datatype = datatype | |
| self.NNdata = None | |
| self.dataHistory = [] | |
| Reader.Reader_count += 1 | |
| def __rich_repr__(self) -> rich.repr.Result: | |
| yield "name", self.name | |
| yield "Data:", self.NNdata | |
| yield "has_focus", self.has_focus, False | |
| yield "mouse_over", self.mouse_over, False | |
| def render(self) -> RenderableType: | |
| return Panel( | |
| Align.center( | |
| Pretty( | |
| self | |
| if self.showdetail > 1 | |
| else (self.dataHistory if self.showdetail > 0 else self.NNdata) | |
| ), | |
| vertical="middle", | |
| ), | |
| title=self.name, | |
| border_style="green" if self.mouse_over else "blue", | |
| box=box.DOUBLE_EDGE if self.has_focus else box.ROUNDED, | |
| style=self.style, | |
| height=self.height, | |
| ) | |
| async def on_focus(self, event: events.Focus) -> None: | |
| self.has_focus = True | |
| async def on_blur(self, event: events.Blur) -> None: | |
| self.has_focus = False | |
| async def on_enter(self, event: events.Enter) -> None: | |
| self.mouse_over = True | |
| async def on_leave(self, event: events.Leave) -> None: | |
| self.mouse_over = False | |
| async def on_key(self, event: events.Key): | |
| if self.has_focus: | |
| if event.key == "enter" or event.key == "ctrl+r": | |
| msg = dataFinal(self, self.NNdata) | |
| self.dataHistory = [self.NNdata] + self.dataHistory | |
| await self.emit(dataFinal(self, self.NNdata, self.datatype)) | |
| self.NNdata = None | |
| elif event.key == "left" or event.key == "ctrl+h": | |
| self.NNdata = self.NNdata[:-1] if not self.NNdata is None else None | |
| elif event.key == "ctrl+d": | |
| self.showdetail = (self.showdetail + 1) % 3 | |
| elif event.key == "up": | |
| self.dataHistory.append(self.NNdata) | |
| self.NNdata = self.dataHistory[0] | |
| self.dataHistory = self.dataHistory[1:] + [self.dataHistory[0]] | |
| elif event.key.isalnum() or event.key[0] in "!@#$%^&*()[]\{\}-+,.;_<>='\" ": | |
| if self.NNdata is None: | |
| self.NNdata = event.key | |
| else: | |
| self.NNdata = self.NNdata + event.key | |
| else: | |
| self.log( | |
| f"READER: {self.name} on_key event - unknown key '{event.key}'" | |
| ) | |
| self.refresh() | |
| if __name__ == "__main__": | |
| class ReaderApp(App): | |
| """Demonstrates Character Reading Class""" | |
| async def on_load(self, event: events.Load) -> None: | |
| """Bind keys with the app loads (but before entering application mode)""" | |
| await self.bind("up", "view.toggle('Reader_0')", "Toggle sidebar") | |
| # await self.bind("q", "quit", "Quit") | |
| await self.bind("escape", "quit", "Quit") | |
| async def on_mount(self) -> None: | |
| self.body = ScrollView(gutter=1) | |
| """Build layout here.""" | |
| await self.view.dock(Header(), edge="top") | |
| await self.view.dock(Footer(), edge="bottom") | |
| readers = (Reader() for _ in range(3)) | |
| await self.view.dock(self.body, edge="right", size=60) | |
| await self.view.dock(*readers, edge="top") | |
| async def handle_data_final(self, thismessage: dataFinal) -> None: | |
| """A message sent by the reader when change is made.""" | |
| syntax: RenderableType | |
| try: | |
| # Construct a Syntax object for the path in the message | |
| syntax = str(thismessage.mymessage) | |
| await self.body.update(syntax) | |
| except Exception: | |
| # Possibly a binary file | |
| # For demonstration purposes we will show the traceback | |
| syntax = Traceback(theme="monokai", width=None, show_locals=True) | |
| await self.body.update(syntax) | |
| ReaderApp.run(log="textual.log") |