From 43af84a3958258c3293de317ae73ce04ea9dcf9d Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 15 May 2019 16:58:24 +0000 Subject: [PATCH] Add basic euphoria single-room application --- cheuph/euphoria/__init__.py | 8 ++ cheuph/euphoria/palette.py | 11 +++ cheuph/euphoria/room_widget.py | 79 ++++++++++++++++++ cheuph/euphoria/single_room_application.py | 97 ++++++++++++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 cheuph/euphoria/__init__.py create mode 100644 cheuph/euphoria/palette.py create mode 100644 cheuph/euphoria/room_widget.py create mode 100644 cheuph/euphoria/single_room_application.py diff --git a/cheuph/euphoria/__init__.py b/cheuph/euphoria/__init__.py new file mode 100644 index 0000000..83b96a0 --- /dev/null +++ b/cheuph/euphoria/__init__.py @@ -0,0 +1,8 @@ +from typing import List + +from .palette import * +from .single_room_application import * + +__all__: List[str] = [] +__all__ += palette.__all__ +__all__ += single_room_application.__all__ diff --git a/cheuph/euphoria/palette.py b/cheuph/euphoria/palette.py new file mode 100644 index 0000000..a4c480e --- /dev/null +++ b/cheuph/euphoria/palette.py @@ -0,0 +1,11 @@ +import enum + +__all__ = ["Style", "PALETTE"] + +@enum.unique +class Style(enum.Enum): + ROOM = "room" + +PALETTE = [ + (Style.ROOM, "light blue,bold", ""), +] diff --git a/cheuph/euphoria/room_widget.py b/cheuph/euphoria/room_widget.py new file mode 100644 index 0000000..9be54c9 --- /dev/null +++ b/cheuph/euphoria/room_widget.py @@ -0,0 +1,79 @@ +import asyncio +from typing import Any, List + +import urwid + +import yaboli + +from .palette import Style +from ..markup import AT +from ..widgets import ATWidget + + +class CenteredTextWidget(urwid.WidgetWrap): + def __init__(self, lines: List[AT]): + max_width = max(map(len, lines)) + text = AT("\n").join(lines) + filler = urwid.Filler(ATWidget(text, align="center")) + super().__init__(filler) + +class RoomWidget(urwid.WidgetWrap): + """ + The RoomWidget connects to and displays a single yaboli room. + + Its life cycle looks like this: + 1. Create widget + 2. Call connect() (while the event loop is running) + 3. Keep widget around and occasionally display it + 4. Call disconnect() (while the event loop is runnning) + 5. When the room should be destroyed/forgotten about, it sends a "close" + event + """ + + def __init__(self, roomname: str) -> None: + self._room = yaboli.Room(roomname) + + super().__init__(self._connecting_widget()) + self._room_view = self._connected_widget() + + def _connecting_widget(self) -> Any: + lines = [AT("Connecting to ") + + AT("&" + self.room.name, style=Style.ROOM) + + AT("...")] + return CenteredTextWidget(lines) + + def _connected_widget(self) -> Any: + lines = [AT("Connected to ") + + AT("&" + self.room.name, style=Style.ROOM) + + AT(".")] + return CenteredTextWidget(lines) + + def _connection_failed_widget(self) -> Any: + lines = [AT("Could not connect to ") + + AT("&" + self.room.name, style=Style.ROOM) + + AT(".")] + return CenteredTextWidget(lines) + + @property + def room(self) -> yaboli.Room: + return self._room + +# Start up the connection and room + + async def _connect(self) -> None: + success = await self._room.connect() + if success: + self._w = self._room_view + else: + self._w = self._connection_failed_widget() + urwid.emit_signal(self, "close") + + def connect(self) -> None: + asyncio.create_task(self._connect()) + +# Handle input + + def selectable(self) -> bool: + return True + +urwid.register_signal(RoomWidget, ["close"]) diff --git a/cheuph/euphoria/single_room_application.py b/cheuph/euphoria/single_room_application.py new file mode 100644 index 0000000..08cd12b --- /dev/null +++ b/cheuph/euphoria/single_room_application.py @@ -0,0 +1,97 @@ +from typing import Any, Optional + +import urwid + +from .palette import Style +from .room_widget import RoomWidget + +__all__ = ["SingleRoomApplication"] + +class ChooseRoomWidget(urwid.WidgetWrap): + def __init__(self) -> None: + self.text = urwid.Text("Choose a room:", align="center") + self.edit = urwid.Edit("&", align="center") + self.pile = urwid.Pile([ + self.text, + urwid.AttrMap(self.edit, Style.ROOM), + ]) + self.filler = urwid.Filler(self.pile) + super().__init__(self.filler) + + def set_error(self, text: Any) -> None: + self.error = urwid.Text(text, align="center") + self.pile = urwid.Pile([ + self.error, + self.text, + urwid.AttrMap(self.edit, Style.ROOM), + ]) + self.filler = urwid.Filler(self.pile) + self._w = self.filler + + def unset_error(self) -> None: + self.error = None + self.pile = urwid.Pile([ + self.text, + urwid.AttrMap(self.edit, Style.ROOM), + ]) + self.filler = urwid.Filler(self.pile) + self._w = self.filler + + def could_not_connect(self, roomname: str) -> None: + text = [ + "Could not connect to ", + (Style.ROOM, "&" + roomname), + ".\n", + ] + self.set_error(text) + + def invalid_room_name(self) -> None: + # TODO animate the invalid room name thingy? + text = ["Invalid room name.\n"] + self.set_error(text) + +class SingleRoomApplication(urwid.WidgetWrap): + ALPHABET = "abcdefghijklmnopqrstuvwxyz" + ALLOWED_EDITOR_KEYS = { + "backspace", "delete", + "left", "right", + "home", "end", + } + + def __init__(self) -> None: + self.choose_room = ChooseRoomWidget() + super().__init__(self.choose_room) + + def selectable(self) -> bool: + return True + + def switch_to_choose(self) -> None: + self.choose_room.could_not_connect(self.choose_room.edit.edit_text) + self._w = self.choose_room + + def keypress(self, size: Any, key: str) -> Optional[str]: + if self._w == self.choose_room: + # This leads to the editor jumping around the screen. + # + # TODO Find a way for the editor to stay still. + self.choose_room.unset_error() + + if key == "enter": + roomname = self.choose_room.edit.edit_text + + if roomname: + room = RoomWidget(roomname) + urwid.connect_signal(room, "close", self.switch_to_choose) + room.connect() + self._w = room + + # Make sure we only enter valid room names + elif key.lower() in self.ALPHABET: + return super().keypress(size, key.lower()) + elif key in self.ALLOWED_EDITOR_KEYS: + return super().keypress(size, key) + + return None + + else: + return super().keypress(size, key)