Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
38 changed files with 718 additions and 487 deletions
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -2,21 +2,8 @@
|
||||||
|
|
||||||
## Next version
|
## Next version
|
||||||
|
|
||||||
- Add demo gif to readme
|
Nothing yet.
|
||||||
- Fix indentation of multi-line messages
|
|
||||||
- Stop using dataclass (for backwards compatibility with Python 3.6)
|
|
||||||
|
|
||||||
## 1.0.0 (2019-06-21)
|
## 0.1.0 (2019-04-12)
|
||||||
|
|
||||||
- Add a readme
|
- use setuptools
|
||||||
- Add ability to connect as human
|
|
||||||
- Add cookie support
|
|
||||||
- Add nick list
|
|
||||||
- Clean up code
|
|
||||||
- Fix crash on "Choose a room" screen
|
|
||||||
- Fix fetching old logs
|
|
||||||
- Rename project from "cheuph" to "bowl"
|
|
||||||
|
|
||||||
## 0.1.0 (2019-06-21)
|
|
||||||
|
|
||||||
- Use setuptools
|
|
||||||
|
|
|
||||||
63
README.md
63
README.md
|
|
@ -1,63 +0,0 @@
|
||||||
# bowl
|
|
||||||
|
|
||||||
A TUI client for [euphoria.io](https://euphoria.io)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Ensure that you have at least Python 3.7 installed.
|
|
||||||
|
|
||||||
To install **bowl** or update your installation to the latest version, run the
|
|
||||||
following command wherever you want to install or have installed **bowl**:
|
|
||||||
```
|
|
||||||
$ pip install git+https://github.com/Garmelon/bowl@v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
The use of [venv](https://docs.python.org/3/library/venv.html) is recommended.
|
|
||||||
|
|
||||||
## Example setup
|
|
||||||
|
|
||||||
In this example, `python` refers to at least Python 3.7, as mentioned above.
|
|
||||||
|
|
||||||
This example uses `venv`, so that `pip install` does not install any packages
|
|
||||||
globally.
|
|
||||||
|
|
||||||
First, create a folder and a venv environment inside that folder.
|
|
||||||
```
|
|
||||||
$ mkdir bowl
|
|
||||||
$ cd bowl
|
|
||||||
$ python -m venv .
|
|
||||||
$ . bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, install **bowl**.
|
|
||||||
```
|
|
||||||
$ pip install git+https://github.com/Garmelon/bowl@v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a config file containing all default values in the default config file
|
|
||||||
location.
|
|
||||||
```
|
|
||||||
$ mkdir -p ~/.config/bowl/
|
|
||||||
$ bowl --export-defaults ~/.config/bowl/bowl.yaml
|
|
||||||
$ vim ~/.config/bowl/bowl.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Run **bowl** (have fun!).
|
|
||||||
```
|
|
||||||
$ bowl
|
|
||||||
```
|
|
||||||
|
|
||||||
Exit the venv environment again.
|
|
||||||
```
|
|
||||||
$ deactivate
|
|
||||||
```
|
|
||||||
|
|
||||||
Subsequent runs of the program might look like this:
|
|
||||||
```
|
|
||||||
$ cd bowl
|
|
||||||
$ . bin/activate
|
|
||||||
$ bowl
|
|
||||||
$ deactivate
|
|
||||||
```
|
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
import urwid
|
|
||||||
import yaboli
|
|
||||||
|
|
||||||
from ..attributed_lines import AttributedLines
|
|
||||||
from ..attributed_lines_widget import AttributedLinesWidget
|
|
||||||
from ..markup import AT, Attributes
|
|
||||||
|
|
||||||
__all__ = ["NickListWidget"]
|
|
||||||
|
|
||||||
class NickListWidget(urwid.WidgetWrap):
|
|
||||||
"""
|
|
||||||
This widget displays the nicks of the users currently connected to a Room.
|
|
||||||
|
|
||||||
It must be notified of changes in the user list by the RoomWidget it is a
|
|
||||||
part of.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
session: Optional[yaboli.Session] = None,
|
|
||||||
users: Optional[yaboli.LiveSessionListing] = None,
|
|
||||||
heading_attrs: Attributes = {},
|
|
||||||
counter_attrs: Attributes = {},
|
|
||||||
nick_attrs: Attributes = {},
|
|
||||||
own_nick_attrs: Attributes = {},
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self._session = session
|
|
||||||
self._users = users
|
|
||||||
|
|
||||||
self._heading_attrs = heading_attrs
|
|
||||||
self._counter_attrs = counter_attrs
|
|
||||||
self._nick_attrs = nick_attrs
|
|
||||||
self._own_nick_attrs = own_nick_attrs
|
|
||||||
|
|
||||||
self._offset = 0
|
|
||||||
self._lines = AttributedLinesWidget()
|
|
||||||
|
|
||||||
super().__init__(self._lines)
|
|
||||||
|
|
||||||
self.rerender()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session(self) -> Optional[yaboli.Session]:
|
|
||||||
return self._session
|
|
||||||
|
|
||||||
@session.setter
|
|
||||||
def session(self, session: Optional[yaboli.Session]) -> None:
|
|
||||||
self._session = session
|
|
||||||
self.rerender()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def users(self) -> Optional[yaboli.LiveSessionListing]:
|
|
||||||
return self._users
|
|
||||||
|
|
||||||
@users.setter
|
|
||||||
def users(self, users: Optional[yaboli.LiveSessionListing]) -> None:
|
|
||||||
self._users = users
|
|
||||||
self.rerender()
|
|
||||||
|
|
||||||
def _sort_users(self) -> Tuple[List[yaboli.Session], List[yaboli.Session], List[yaboli.Session], List[yaboli.Session]]:
|
|
||||||
people = []
|
|
||||||
bots = []
|
|
||||||
lurkers = []
|
|
||||||
nurkers = []
|
|
||||||
|
|
||||||
users = [] if self._users is None else self._users.all
|
|
||||||
|
|
||||||
if self.session is not None:
|
|
||||||
users.append(self.session)
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
if user.nick:
|
|
||||||
if user.is_bot:
|
|
||||||
bots.append(user)
|
|
||||||
else:
|
|
||||||
people.append(user)
|
|
||||||
else:
|
|
||||||
if user.is_bot:
|
|
||||||
nurkers.append(user)
|
|
||||||
else:
|
|
||||||
lurkers.append(user)
|
|
||||||
|
|
||||||
return people, bots, lurkers, nurkers
|
|
||||||
|
|
||||||
def _render_section(self,
|
|
||||||
name: str,
|
|
||||||
sessions: List[yaboli.Session],
|
|
||||||
) -> AttributedLines:
|
|
||||||
|
|
||||||
lines = AttributedLines()
|
|
||||||
|
|
||||||
sessions.sort(key=lambda sess: sess.nick)
|
|
||||||
count = len(sessions)
|
|
||||||
|
|
||||||
title = AT(name, attributes=self._heading_attrs)
|
|
||||||
title += AT(f" ({count})", attributes=self._counter_attrs)
|
|
||||||
lines.append_below({}, title)
|
|
||||||
|
|
||||||
for sess in sessions:
|
|
||||||
if not sess.nick:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if sess is self._session:
|
|
||||||
attributes = self._own_nick_attrs
|
|
||||||
else:
|
|
||||||
attributes = self._nick_attrs
|
|
||||||
|
|
||||||
lines.append_below({}, AT(sess.nick, attributes=attributes))
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def rerender(self) -> None:
|
|
||||||
lines = AttributedLines()
|
|
||||||
people, bots, lurkers, nurkers = self._sort_users()
|
|
||||||
|
|
||||||
sections = []
|
|
||||||
|
|
||||||
if people:
|
|
||||||
sections.append(self._render_section("People", people))
|
|
||||||
if bots:
|
|
||||||
sections.append(self._render_section("Bots", bots))
|
|
||||||
if lurkers:
|
|
||||||
sections.append(self._render_section("Lurkers", lurkers))
|
|
||||||
if nurkers:
|
|
||||||
sections.append(self._render_section("Nurkers", nurkers))
|
|
||||||
|
|
||||||
lines = AttributedLines()
|
|
||||||
lines.upper_offset = self._lines.upper_offset
|
|
||||||
|
|
||||||
if len(sections) < 1:
|
|
||||||
lines.extend_below(self._render_section("Nobody", []))
|
|
||||||
else:
|
|
||||||
lines.extend_below(sections[0])
|
|
||||||
for section in sections[1:]:
|
|
||||||
lines.append_below({}, AT())
|
|
||||||
lines.extend_below(section)
|
|
||||||
|
|
||||||
self._lines.set_lines(lines)
|
|
||||||
|
|
||||||
def scroll(self, delta: int) -> None:
|
|
||||||
self._lines.upper_offset += delta
|
|
||||||
|
|
||||||
self._lines.lower_offset = max(1, self._lines.lower_offset)
|
|
||||||
self._lines.upper_offset = min(0, self._lines.upper_offset)
|
|
||||||
|
|
||||||
self._invalidate()
|
|
||||||
|
|
@ -15,6 +15,7 @@ from .utils import *
|
||||||
|
|
||||||
__all__: List[str] = []
|
__all__: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
__all__ += attributed_lines.__all__
|
__all__ += attributed_lines.__all__
|
||||||
__all__ += attributed_lines_widget.__all__
|
__all__ += attributed_lines_widget.__all__
|
||||||
__all__ += attributed_text_widget.__all__
|
__all__ += attributed_text_widget.__all__
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
import collections
|
import collections
|
||||||
from typing import Any, Deque, Iterator, List, Optional, Set, Tuple
|
from typing import Any, Deque, Iterator, List, Optional, Set, Tuple
|
||||||
|
|
||||||
from .markup import AT, Attributes
|
from .markup import AT, AttributedText, Attributes
|
||||||
|
|
||||||
__all__ = ["Line", "AttributedLines"]
|
__all__ = ["Line", "AttributedLines"]
|
||||||
|
|
||||||
Line = Tuple[Attributes, AT]
|
Line = Tuple[Attributes, AttributedText]
|
||||||
|
|
||||||
class AttributedLines:
|
class AttributedLines:
|
||||||
"""
|
"""
|
||||||
|
|
@ -49,8 +49,7 @@ class AttributedLines:
|
||||||
|
|
||||||
def append_above(self,
|
def append_above(self,
|
||||||
attributes: Attributes,
|
attributes: Attributes,
|
||||||
text: AT,
|
text: AttributedText) -> None:
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Append a line above all already existing lines. The existing lines'
|
Append a line above all already existing lines. The existing lines'
|
||||||
offsets do not change.
|
offsets do not change.
|
||||||
|
|
@ -61,8 +60,7 @@ class AttributedLines:
|
||||||
|
|
||||||
def append_below(self,
|
def append_below(self,
|
||||||
attributes: Attributes,
|
attributes: Attributes,
|
||||||
text: AT,
|
text: AttributedText) -> None:
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Append a line below all already existing lines. The existing lines'
|
Append a line below all already existing lines. The existing lines'
|
||||||
offsets do not change.
|
offsets do not change.
|
||||||
|
|
@ -135,7 +133,7 @@ class AttributedLines:
|
||||||
horizontal_offset: int,
|
horizontal_offset: int,
|
||||||
offset_char: str = " ",
|
offset_char: str = " ",
|
||||||
overlap_char: str = "…",
|
overlap_char: str = "…",
|
||||||
) -> AT:
|
) -> AttributedText:
|
||||||
"""
|
"""
|
||||||
Renders a single line to a specified width with a specified horizontal
|
Renders a single line to a specified width with a specified horizontal
|
||||||
offset, applying all line-wide attributes to the result. The length of
|
offset, applying all line-wide attributes to the result. The length of
|
||||||
|
|
@ -154,7 +152,7 @@ class AttributedLines:
|
||||||
start_offset = horizontal_offset
|
start_offset = horizontal_offset
|
||||||
end_offset = start_offset + text_width
|
end_offset = start_offset + text_width
|
||||||
|
|
||||||
result: AT = AT()
|
result: AttributedText = AT()
|
||||||
|
|
||||||
if start_offset < 0:
|
if start_offset < 0:
|
||||||
pad_length = min(text_width, -start_offset)
|
pad_length = min(text_width, -start_offset)
|
||||||
|
|
@ -191,7 +189,7 @@ class AttributedLines:
|
||||||
width: int,
|
width: int,
|
||||||
height: int,
|
height: int,
|
||||||
horizontal_offset: int,
|
horizontal_offset: int,
|
||||||
) -> List[AT]:
|
) -> List[AttributedText]:
|
||||||
"""
|
"""
|
||||||
Renders all lines individually.
|
Renders all lines individually.
|
||||||
"""
|
"""
|
||||||
|
|
@ -207,7 +205,7 @@ class AttributedLines:
|
||||||
width: int,
|
width: int,
|
||||||
height: int,
|
height: int,
|
||||||
horizontal_offset: int,
|
horizontal_offset: int,
|
||||||
) -> AT:
|
) -> AttributedText:
|
||||||
"""
|
"""
|
||||||
Renders all lines and combines them into a single AttributedText by
|
Renders all lines and combines them into a single AttributedText by
|
||||||
joining them with a newline.
|
joining them with a newline.
|
||||||
|
|
@ -5,7 +5,7 @@ from typing import Optional, Tuple
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from .attributed_lines import AttributedLines
|
from .attributed_lines import AttributedLines
|
||||||
from .attributed_text_widget import ATWidget
|
from .attributed_text_widget import AttributedTextWidget
|
||||||
from .markup import AT
|
from .markup import AT
|
||||||
|
|
||||||
__all__ = ["AttributedLinesWidget"]
|
__all__ = ["AttributedLinesWidget"]
|
||||||
|
|
@ -17,7 +17,7 @@ class AttributedLinesWidget(urwid.WidgetWrap):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, lines: Optional[AttributedLines] = None) -> None:
|
def __init__(self, lines: Optional[AttributedLines] = None) -> None:
|
||||||
self._text = ATWidget(AT())
|
self._text = AttributedTextWidget(AT())
|
||||||
self._filler = urwid.Filler(self._text, valign=urwid.TOP)
|
self._filler = urwid.Filler(self._text, valign=urwid.TOP)
|
||||||
super().__init__(self._filler)
|
super().__init__(self._filler)
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@ from typing import Any, List, Tuple, Union
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
from .markup import AT
|
from .markup import AttributedText
|
||||||
|
|
||||||
__all__ = ["AttributedTextWidget", "ATWidget"]
|
__all__ = ["AttributedTextWidget", "ATWidget"]
|
||||||
|
|
||||||
|
|
@ -15,9 +15,9 @@ class AttributedTextWidget(urwid.Text):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
text: AT,
|
text: AttributedText,
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
text - an AttributedText object
|
text - an AttributedText object
|
||||||
|
|
@ -30,7 +30,9 @@ class AttributedTextWidget(urwid.Text):
|
||||||
super().__init__(self._convert_to_markup(text), *args, **kwargs)
|
super().__init__(self._convert_to_markup(text), *args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_to_markup(text: AT) -> List[Union[str, Tuple[str, str]]]:
|
def _convert_to_markup(text: AttributedText
|
||||||
|
) -> List[Union[str, Tuple[str, str]]]:
|
||||||
|
|
||||||
# Wonder why mypy can't figure out the type signature of markup on its
|
# Wonder why mypy can't figure out the type signature of markup on its
|
||||||
# own... :P
|
# own... :P
|
||||||
markup: List[Union[str, Tuple[str, str]]]
|
markup: List[Union[str, Tuple[str, str]]]
|
||||||
|
|
@ -41,7 +43,7 @@ class AttributedTextWidget(urwid.Text):
|
||||||
|
|
||||||
return markup or [""]
|
return markup or [""]
|
||||||
|
|
||||||
def set_attributed_text(self, text: AT) -> None:
|
def set_attributed_text(self, text: AttributedText) -> None:
|
||||||
"""
|
"""
|
||||||
Set the content of the AttributedTextWidget.
|
Set the content of the AttributedTextWidget.
|
||||||
"""
|
"""
|
||||||
|
|
@ -49,7 +51,7 @@ class AttributedTextWidget(urwid.Text):
|
||||||
self._attributed_text = text
|
self._attributed_text = text
|
||||||
super().set_text(self._convert_to_markup(text))
|
super().set_text(self._convert_to_markup(text))
|
||||||
|
|
||||||
def get_attributed_text(self) -> AT:
|
def get_attributed_text(self) -> AttributedText:
|
||||||
"""
|
"""
|
||||||
Returns the currently used AttributedText.
|
Returns the currently used AttributedText.
|
||||||
|
|
||||||
|
|
@ -61,7 +63,7 @@ class AttributedTextWidget(urwid.Text):
|
||||||
return self._attributed_text
|
return self._attributed_text
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attributed_text(self) -> AT:
|
def attributed_text(self) -> AttributedText:
|
||||||
return self.get_attributed_text()
|
return self.get_attributed_text()
|
||||||
|
|
||||||
ATWidget = AttributedTextWidget
|
ATWidget = AttributedTextWidget
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
from typing import (Any, Callable, Dict, Iterable, List, Optional, Tuple,
|
||||||
|
TypeVar)
|
||||||
|
|
||||||
__all__ = ["ConfigException", "ConfigValueException", "TransparentConfig",
|
__all__ = ["ConfigException", "ConfigValueException", "TransparentConfig",
|
||||||
"Kind", "Condition", "Option", "TreeLoader"]
|
"Kind", "Condition", "Option", "TreeLoader"]
|
||||||
|
|
@ -41,7 +43,6 @@ class TransparentConfig:
|
||||||
# Special config reading and writing classes
|
# Special config reading and writing classes
|
||||||
|
|
||||||
class Kind(Enum):
|
class Kind(Enum):
|
||||||
|
|
||||||
BOOL = auto()
|
BOOL = auto()
|
||||||
DICT = auto()
|
DICT = auto()
|
||||||
FLOAT = auto()
|
FLOAT = auto()
|
||||||
|
|
@ -61,22 +62,15 @@ class Kind(Enum):
|
||||||
|
|
||||||
Condition = Callable[[Any], bool]
|
Condition = Callable[[Any], bool]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
class Option:
|
class Option:
|
||||||
|
kind: Kind
|
||||||
def __init__(self,
|
default: Any
|
||||||
kind: Kind,
|
conditions: Iterable[Tuple[Condition, str]] = field(default_factory=list)
|
||||||
default: Any,
|
|
||||||
conditions: Iterable[Tuple[Condition, str]] = frozenset(),
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self.kind = kind
|
|
||||||
self.default = default
|
|
||||||
self.conditions = conditions
|
|
||||||
|
|
||||||
def check_valid(self, value: Any) -> None:
|
def check_valid(self, value: Any) -> None:
|
||||||
if not self.kind.matches(value):
|
if not self.kind.matches(value):
|
||||||
raise ConfigValueException(
|
raise ConfigValueException(f"value {value!r} does not match {self.kind}")
|
||||||
f"value {value!r} does not match {self.kind}")
|
|
||||||
|
|
||||||
self.apply_conditions(value)
|
self.apply_conditions(value)
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ from .attributed_lines import AttributedLines
|
||||||
from .element import Element, Id, Message, RenderedElement, RenderedMessage
|
from .element import Element, Id, Message, RenderedElement, RenderedMessage
|
||||||
from .element_supply import ElementSupply
|
from .element_supply import ElementSupply
|
||||||
from .exceptions import ShouldNeverHappen
|
from .exceptions import ShouldNeverHappen
|
||||||
from .markup import AT, Attributes
|
from .markup import AT, AttributedText, Attributes
|
||||||
from .rendered_element_cache import RenderedElementCache
|
from .rendered_element_cache import RenderedElementCache
|
||||||
|
|
||||||
__all__ = ["CursorRenderer", "CursorTreeRenderer", "BasicCursorRenderer"]
|
__all__ = ["CursorRenderer", "CursorTreeRenderer", "BasicCursorRenderer"]
|
||||||
|
|
@ -28,7 +28,7 @@ class CursorRenderer(ABC, Generic[E, R]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def render_cursor(self, width: int) -> AT:
|
def render_cursor(self, width: int) -> AttributedText:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class CursorTreeRenderer(Generic[E]):
|
class CursorTreeRenderer(Generic[E]):
|
||||||
|
|
@ -157,7 +157,7 @@ class CursorTreeRenderer(Generic[E]):
|
||||||
|
|
||||||
def _render_message(self,
|
def _render_message(self,
|
||||||
message_id: Id,
|
message_id: Id,
|
||||||
indent: AT,
|
indent: AttributedText,
|
||||||
) -> AttributedLines:
|
) -> AttributedLines:
|
||||||
|
|
||||||
width = self._width - len(indent) - self._renderer.meta_width - 1
|
width = self._width - len(indent) - self._renderer.meta_width - 1
|
||||||
|
|
@ -175,7 +175,9 @@ class CursorTreeRenderer(Generic[E]):
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _render_cursor(self, indent: AT = AT(),) -> AttributedLines:
|
def _render_cursor(self,
|
||||||
|
indent: AttributedText = AT(),
|
||||||
|
) -> AttributedLines:
|
||||||
lines = AttributedLines()
|
lines = AttributedLines()
|
||||||
width = self._width - len(indent) - self._renderer.meta_width - 1
|
width = self._width - len(indent) - self._renderer.meta_width - 1
|
||||||
meta_spaces = AT(" " * self._renderer.meta_width)
|
meta_spaces = AT(" " * self._renderer.meta_width)
|
||||||
|
|
@ -187,7 +189,7 @@ class CursorTreeRenderer(Generic[E]):
|
||||||
def _render_indent(self,
|
def _render_indent(self,
|
||||||
cursor: bool = False,
|
cursor: bool = False,
|
||||||
cursor_line: bool = False,
|
cursor_line: bool = False,
|
||||||
) -> AT:
|
) -> AttributedText:
|
||||||
|
|
||||||
if self._indent_width < 1:
|
if self._indent_width < 1:
|
||||||
return AT()
|
return AT()
|
||||||
|
|
@ -213,7 +215,7 @@ class CursorTreeRenderer(Generic[E]):
|
||||||
def _render_subtree(self,
|
def _render_subtree(self,
|
||||||
lines: AttributedLines,
|
lines: AttributedLines,
|
||||||
root_id: Id,
|
root_id: Id,
|
||||||
indent: AT = AT(),
|
indent: AttributedText = AT(),
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if self._anchor_id == root_id:
|
if self._anchor_id == root_id:
|
||||||
|
|
@ -633,5 +635,5 @@ class BasicCursorRenderer(CursorRenderer):
|
||||||
|
|
||||||
return RenderedMessage(message.id, lines, meta)
|
return RenderedMessage(message.id, lines, meta)
|
||||||
|
|
||||||
def render_cursor(self, width: int) -> AT:
|
def render_cursor(self, width: int) -> AttributedText:
|
||||||
return AT("<cursor>")
|
return AT("<cursor>")
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Hashable, List, Optional
|
from typing import Hashable, List, Optional
|
||||||
|
|
||||||
from .markup import AT
|
from .markup import AttributedText
|
||||||
|
|
||||||
__all__ = ["Id", "Element", "RenderedElement", "Message", "RenderedMessage"]
|
__all__ = ["Id", "Element", "RenderedElement", "Message", "RenderedMessage"]
|
||||||
|
|
||||||
|
|
@ -26,8 +26,10 @@ class Element:
|
||||||
return self._parent_id
|
return self._parent_id
|
||||||
|
|
||||||
class RenderedElement:
|
class RenderedElement:
|
||||||
|
def __init__(self,
|
||||||
def __init__(self, id: Id, lines: List[AT]) -> None:
|
id: Id,
|
||||||
|
lines: List[AttributedText],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
self._id = id
|
self._id = id
|
||||||
self._lines = lines
|
self._lines = lines
|
||||||
|
|
@ -37,7 +39,7 @@ class RenderedElement:
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lines(self) -> List[AT]:
|
def lines(self) -> List[AttributedText]:
|
||||||
return self._lines
|
return self._lines
|
||||||
|
|
||||||
class Message(Element):
|
class Message(Element):
|
||||||
|
|
@ -69,10 +71,15 @@ class Message(Element):
|
||||||
|
|
||||||
class RenderedMessage(RenderedElement):
|
class RenderedMessage(RenderedElement):
|
||||||
|
|
||||||
def __init__(self, id: Id, lines: List[AT], meta: AT) -> None:
|
def __init__(self,
|
||||||
|
id: Id,
|
||||||
|
lines: List[AttributedText],
|
||||||
|
meta: AttributedText,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
super().__init__(id, lines)
|
super().__init__(id, lines)
|
||||||
self._meta = meta
|
self._meta = meta
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meta(self) -> AT:
|
def meta(self) -> AttributedText:
|
||||||
return self._meta
|
return self._meta
|
||||||
|
|
@ -4,9 +4,9 @@ from .edit_widgets import *
|
||||||
from .euph_config import *
|
from .euph_config import *
|
||||||
from .euph_renderer import *
|
from .euph_renderer import *
|
||||||
from .launch_application import *
|
from .launch_application import *
|
||||||
from .nick_list_widget import *
|
|
||||||
from .room_widget import *
|
from .room_widget import *
|
||||||
from .single_room_application import *
|
from .single_room_application import *
|
||||||
|
from .user_list_widget import *
|
||||||
|
|
||||||
__all__: List[str] = []
|
__all__: List[str] = []
|
||||||
|
|
||||||
|
|
@ -14,6 +14,6 @@ __all__ += edit_widgets.__all__
|
||||||
__all__ += euph_config.__all__
|
__all__ += euph_config.__all__
|
||||||
__all__ += euph_renderer.__all__
|
__all__ += euph_renderer.__all__
|
||||||
__all__ += launch_application.__all__
|
__all__ += launch_application.__all__
|
||||||
__all__ += nick_list_widget.__all__
|
|
||||||
__all__ += room_widget.__all__
|
__all__ += room_widget.__all__
|
||||||
__all__ += single_room_application.__all__
|
__all__ += single_room_application.__all__
|
||||||
|
__all__ += user_list_widget.__all__
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import Any, Dict, List, Optional, Set
|
from typing import Any, Dict, List, Optional, Set, TypeVar
|
||||||
|
|
||||||
from ..config import ConfigValueException, Kind, TransparentConfig, TreeLoader
|
from ..config import (ConfigValueException, Kind, Option, TransparentConfig,
|
||||||
|
TreeLoader)
|
||||||
|
|
||||||
__all__ = ["EuphConfig", "EuphLoader"]
|
__all__ = ["EuphConfig", "EuphLoader"]
|
||||||
|
|
||||||
|
|
@ -9,16 +10,6 @@ class EuphConfig(TransparentConfig):
|
||||||
def __init__(self, parent: Optional[TransparentConfig] = None) -> None:
|
def __init__(self, parent: Optional[TransparentConfig] = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# behavior
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cookie_file(self) -> Optional[str]:
|
|
||||||
return self["behavior.cookie_file"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def human(self) -> bool:
|
|
||||||
return self["behavior.human"]
|
|
||||||
|
|
||||||
# basic styles
|
# basic styles
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -177,16 +168,6 @@ class EuphConfig(TransparentConfig):
|
||||||
def borders_style(self) -> str:
|
def borders_style(self) -> str:
|
||||||
return self["visual.borders.style"]
|
return self["visual.borders.style"]
|
||||||
|
|
||||||
# nick list
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nick_list_heading_style(self) -> str:
|
|
||||||
return self["visual.nick_list.heading_style"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nick_list_counter_style(self) -> str:
|
|
||||||
return self["visual.nick_list.counter_style"]
|
|
||||||
|
|
||||||
# other
|
# other
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -214,18 +195,12 @@ class EuphLoader(TreeLoader):
|
||||||
SINGLE_CHAR = (lambda x: len(x) == 1, "must be single character")
|
SINGLE_CHAR = (lambda x: len(x) == 1, "must be single character")
|
||||||
AT_LEAST_0 = (lambda x: x >= 0, "must be at least 0")
|
AT_LEAST_0 = (lambda x: x >= 0, "must be at least 0")
|
||||||
AT_LEAST_1 = (lambda x: x >= 1, "must be at least 1")
|
AT_LEAST_1 = (lambda x: x >= 1, "must be at least 1")
|
||||||
OPTIONAL_STR = (lambda x: x is None or type(x) is str,
|
|
||||||
"must be a string or empty")
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._styles: Set[str] = set()
|
self._styles: Set[str] = set()
|
||||||
|
|
||||||
# behavior
|
|
||||||
self.add("behavior.cookie_file", Kind.RAW, None, self.OPTIONAL_STR)
|
|
||||||
self.add("behavior.human", Kind.BOOL, True)
|
|
||||||
|
|
||||||
# basic styles
|
# basic styles
|
||||||
self.add_style("visual.room_style", "room")
|
self.add_style("visual.room_style", "room")
|
||||||
self.add_style("visual.nick_style", "nick")
|
self.add_style("visual.nick_style", "nick")
|
||||||
|
|
@ -288,10 +263,6 @@ class EuphLoader(TreeLoader):
|
||||||
self.SINGLE_CHAR)
|
self.SINGLE_CHAR)
|
||||||
self.add_style("visual.borders.style", "gray")
|
self.add_style("visual.borders.style", "gray")
|
||||||
|
|
||||||
# nick list
|
|
||||||
self.add_style("visual.nick_list.heading_style", "bold")
|
|
||||||
self.add_style("visual.nick_list.counter_style", "gray")
|
|
||||||
|
|
||||||
# other
|
# other
|
||||||
self.add("styles", Kind.DICT, self.DEFAULT_STYLES)
|
self.add("styles", Kind.DICT, self.DEFAULT_STYLES)
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ class EuphRenderer(CursorRenderer):
|
||||||
right = AT(self._surround_right, attributes=self._surround_attrs)
|
right = AT(self._surround_right, attributes=self._surround_attrs)
|
||||||
|
|
||||||
nick_str = left + nick + right + AT(" ")
|
nick_str = left + nick + right + AT(" ")
|
||||||
nick_spaces = AT(" " * len(nick_str))
|
nick_spaces = AT(" " * len(nick))
|
||||||
|
|
||||||
content = self._filter_unicode(message.content)
|
content = self._filter_unicode(message.content)
|
||||||
lines = []
|
lines = []
|
||||||
|
|
@ -12,12 +12,12 @@ from .euph_config import EuphConfig, EuphLoader
|
||||||
__all__ = ["DEFAULT_CONFIG_PATHS", "launch"]
|
__all__ = ["DEFAULT_CONFIG_PATHS", "launch"]
|
||||||
|
|
||||||
DEFAULT_CONFIG_PATHS = [
|
DEFAULT_CONFIG_PATHS = [
|
||||||
"~/.config/bowl/bowl.yaml",
|
"~/.config/cheuph/cheuph.yaml",
|
||||||
"~/.bowl/bowl.yaml",
|
"~/.cheuph/cheuph.yaml",
|
||||||
"~/.bowl.yaml",
|
"~/.cheuph.yaml",
|
||||||
]
|
]
|
||||||
|
|
||||||
GITHUB_URL = "https://github.com/Garmelon/bowl"
|
GITHUB_URL = "https://github.com/Garmelon/cheuph"
|
||||||
|
|
||||||
def parse_arguments() -> argparse.Namespace:
|
def parse_arguments() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
|
@ -57,8 +57,8 @@ def load_config(args: Any) -> EuphConfig:
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def export_defaults(path_str: str) -> None:
|
def export_defaults(path: str) -> None:
|
||||||
path = pathlib.Path(path_str).expanduser()
|
path = pathlib.Path(path).expanduser()
|
||||||
print(f"Exporting default config to {path}")
|
print(f"Exporting default config to {path}")
|
||||||
|
|
||||||
loader = EuphLoader()
|
loader = EuphLoader()
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import pathlib
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Any, Awaitable, Callable, List, Optional, Tuple, TypeVar
|
from typing import Any, Awaitable, Callable, List, Optional, Tuple, TypeVar
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
|
||||||
import yaboli
|
import yaboli
|
||||||
|
|
||||||
from ..attributed_text_widget import ATWidget
|
from ..attributed_text_widget import ATWidget
|
||||||
|
|
@ -15,7 +14,6 @@ from ..markup import AT, AttributedText, Attributes
|
||||||
from .edit_widgets import EditWidget
|
from .edit_widgets import EditWidget
|
||||||
from .euph_config import EuphConfig
|
from .euph_config import EuphConfig
|
||||||
from .euph_renderer import EuphRenderer
|
from .euph_renderer import EuphRenderer
|
||||||
from .nick_list_widget import NickListWidget
|
|
||||||
|
|
||||||
__all__ = ["RoomWidget"]
|
__all__ = ["RoomWidget"]
|
||||||
|
|
||||||
|
|
@ -131,33 +129,23 @@ class RoomLayout(urwid.WidgetWrap):
|
||||||
string = self._edit_separator * tree_width
|
string = self._edit_separator * tree_width
|
||||||
return AT(string, attributes=self._border_attrs)
|
return AT(string, attributes=self._border_attrs)
|
||||||
|
|
||||||
def set_edit_visible(self, visible: bool) -> None:
|
def set_edit_visible(self, visible: bool):
|
||||||
if visible:
|
if visible:
|
||||||
self._left_wrap._w = self._edit_pile
|
self._left_wrap._w = self._edit_pile
|
||||||
else:
|
else:
|
||||||
self._left_wrap._w = self._tree
|
self._left_wrap._w = self._tree
|
||||||
|
|
||||||
def focus_on_edit(self) -> None:
|
def focus_on_edit(self):
|
||||||
self._edit_pile.focus_position = 2
|
self._edit_pile.focus_position = 2
|
||||||
self._columns.focus_position = 0
|
self._columns.focus_position = 0
|
||||||
|
|
||||||
def focus_on_tree(self) -> None:
|
def focus_on_tree(self):
|
||||||
self._edit_pile.focus_position = 0
|
self._edit_pile.focus_position = 0
|
||||||
self._columns.focus_position = 0
|
self._columns.focus_position = 0
|
||||||
|
|
||||||
def focus_on_nick_list(self) -> None:
|
def focus_on_user_list(self):
|
||||||
self._columns.focus_position = 2
|
self._columns.focus_position = 2
|
||||||
|
|
||||||
class UiMode(Enum):
|
|
||||||
|
|
||||||
CONNECTING = "connecting"
|
|
||||||
CONNECTION_FAILED = "connection failed"
|
|
||||||
SETTING_PASSWORD = "setting password"
|
|
||||||
AUTHENTICATING = "authenticating"
|
|
||||||
SETTING_NICK = "setting nick"
|
|
||||||
VIEWING = "viewing"
|
|
||||||
EDITING = "editing"
|
|
||||||
|
|
||||||
class RoomWidget(urwid.WidgetWrap):
|
class RoomWidget(urwid.WidgetWrap):
|
||||||
"""
|
"""
|
||||||
The RoomWidget connects to and displays a single yaboli room.
|
The RoomWidget connects to and displays a single yaboli room.
|
||||||
|
|
@ -171,6 +159,11 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
event
|
event
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
CONNECTING = "connecting"
|
||||||
|
CONNECTION_FAILED = "connection_failed"
|
||||||
|
VIEWING = "viewing"
|
||||||
|
EDITING = "editing"
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
roomname: str,
|
roomname: str,
|
||||||
config: EuphConfig,
|
config: EuphConfig,
|
||||||
|
|
@ -179,36 +172,17 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
|
|
||||||
self.c = config
|
self.c = config
|
||||||
|
|
||||||
|
if log_amount < 1:
|
||||||
|
raise ValueError() # TODO add better text
|
||||||
self._log_amount = log_amount
|
self._log_amount = log_amount
|
||||||
if self._log_amount < 1:
|
|
||||||
raise ValueError("log request amount must be at least 1")
|
|
||||||
|
|
||||||
self._mode: UiMode
|
self._mode: str
|
||||||
self._requesting_logs = False
|
self._requesting_logs = False
|
||||||
self._hit_top_of_supply = False
|
self._hit_top_of_supply = False
|
||||||
|
|
||||||
url_format = yaboli.Room.URL_FORMAT
|
self._room = yaboli.Room(roomname)
|
||||||
if self.c.human:
|
|
||||||
url_format += "?h=1"
|
|
||||||
|
|
||||||
cookie_file = self.c.cookie_file
|
|
||||||
if cookie_file is not None:
|
|
||||||
cookie_file = str(pathlib.Path(cookie_file).expanduser())
|
|
||||||
|
|
||||||
self._room = yaboli.Room(
|
|
||||||
roomname,
|
|
||||||
url_format=url_format,
|
|
||||||
cookie_file=cookie_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._room.register_event("connected", self.on_connected)
|
|
||||||
self._room.register_event("snapshot", self.on_snapshot)
|
self._room.register_event("snapshot", self.on_snapshot)
|
||||||
self._room.register_event("send", self.on_send)
|
self._room.register_event("send", self.on_send)
|
||||||
self._room.register_event("join", self.on_join)
|
|
||||||
self._room.register_event("part", self.on_part)
|
|
||||||
self._room.register_event("nick", self.on_nick)
|
|
||||||
self._room.register_event("edit", self.on_edit)
|
|
||||||
self._room.register_event("disconnect", self.on_disconnect)
|
|
||||||
|
|
||||||
self._supply = InMemorySupply[Message]()
|
self._supply = InMemorySupply[Message]()
|
||||||
self._renderer = self._create_euph_renderer()
|
self._renderer = self._create_euph_renderer()
|
||||||
|
|
@ -323,12 +297,7 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
return urwid.Edit(multiline=True)
|
return urwid.Edit(multiline=True)
|
||||||
|
|
||||||
def _create_nick_list_widget(self) -> Any:
|
def _create_nick_list_widget(self) -> Any:
|
||||||
return NickListWidget(
|
return urwid.SolidFill("n")
|
||||||
heading_attrs={"style": self.c.nick_list_heading_style},
|
|
||||||
counter_attrs={"style": self.c.nick_list_counter_style},
|
|
||||||
nick_attrs={"style": self.c.nick_style},
|
|
||||||
own_nick_attrs={"style": self.c.own_nick_style},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_room_layout_widget(self,
|
def _create_room_layout_widget(self,
|
||||||
room_name: Any,
|
room_name: Any,
|
||||||
|
|
@ -367,42 +336,50 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
|
|
||||||
## UI mode and mode switching
|
## UI mode and mode switching
|
||||||
|
|
||||||
|
CONNECTING = "connecting"
|
||||||
|
CONNECTION_FAILED = "connection_failed"
|
||||||
|
SETTING_PASSWORD = "setting_password"
|
||||||
|
AUTHENTICATING = "authenticating"
|
||||||
|
SETTING_NICK = "setting_nick"
|
||||||
|
VIEWING = "viewing"
|
||||||
|
EDITING = "editing"
|
||||||
|
|
||||||
def switch_connecting(self) -> None:
|
def switch_connecting(self) -> None:
|
||||||
self._w = self._connecting
|
self._w = self._connecting
|
||||||
self._mode = UiMode.CONNECTING
|
self._mode = self.CONNECTING
|
||||||
|
|
||||||
def switch_connection_failed(self) -> None:
|
def switch_connection_failed(self) -> None:
|
||||||
self._w = self._connection_failed
|
self._w = self._connection_failed
|
||||||
self._mode = UiMode.CONNECTION_FAILED
|
self._mode = self.CONNECTION_FAILED
|
||||||
|
|
||||||
def switch_setting_password(self) -> None:
|
def switch_setting_password(self) -> None:
|
||||||
self._w = self._overlay
|
self._w = self._overlay
|
||||||
self._overlay.set_top(self._edit_password)
|
self._overlay.set_top(self._edit_password)
|
||||||
self._mode = UiMode.SETTING_PASSWORD
|
self._mode = self.SETTING_PASSWORD
|
||||||
|
|
||||||
def switch_authenticating(self) -> None:
|
def switch_authenticating(self) -> None:
|
||||||
self._w = self._overlay
|
self._w = self._overlay
|
||||||
self._overlay.set_top(self._authenticating)
|
self._overlay.set_top(self._authenticating)
|
||||||
self._mode = UiMode.AUTHENTICATING
|
self._mode = self.AUTHENTICATING
|
||||||
|
|
||||||
def switch_setting_nick(self) -> None:
|
def switch_setting_nick(self) -> None:
|
||||||
self._w = self._overlay
|
self._w = self._overlay
|
||||||
self._box.original_widget = self._edit_nick
|
self._box.original_widget = self._edit_nick
|
||||||
self._edit_nick.text = self._room.session.nick
|
self._edit_nick.text = self._room.session.nick
|
||||||
self.update_edit_nick()
|
self.update_edit_nick()
|
||||||
self._mode = UiMode.SETTING_NICK
|
self._mode = self.SETTING_NICK
|
||||||
|
|
||||||
def switch_view(self) -> None:
|
def switch_view(self) -> None:
|
||||||
self._w = self._layout
|
self._w = self._layout
|
||||||
self._layout.set_edit_visible(False)
|
self._layout.set_edit_visible(False)
|
||||||
self._layout.focus_on_tree()
|
self._layout.focus_on_tree()
|
||||||
self._mode = UiMode.VIEWING
|
self._mode = self.VIEWING
|
||||||
|
|
||||||
def switch_edit(self) -> None:
|
def switch_edit(self) -> None:
|
||||||
self._w = self._layout
|
self._w = self._layout
|
||||||
self._layout.set_edit_visible(True)
|
self._layout.set_edit_visible(True)
|
||||||
self._layout.focus_on_edit()
|
self._layout.focus_on_edit()
|
||||||
self._mode = UiMode.EDITING
|
self._mode = self.EDITING
|
||||||
|
|
||||||
# Updating various parts of the UI
|
# Updating various parts of the UI
|
||||||
|
|
||||||
|
|
@ -411,15 +388,12 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
|
|
||||||
def update_nick_list(self) -> None:
|
def update_nick_list(self) -> None:
|
||||||
# Ensure that self._room.session and self._room.users exist
|
# Ensure that self._room.session and self._room.users exist
|
||||||
allowed = {UiMode.SETTING_NICK, UiMode.VIEWING, UiMode.EDITING}
|
if self._mode not in {self.SETTING_NICK, self.VIEWING, self.EDITING}:
|
||||||
if self._mode not in allowed:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Automatically rerenders
|
#self._nick_list.update(self._room.session, self._room.users)
|
||||||
self._nick_list.session = self._room.session
|
|
||||||
self._nick_list.users = self._room.users
|
|
||||||
|
|
||||||
def update_edit_nick(self) -> None:
|
def update_edit_nick(self):
|
||||||
width = self._edit_nick.width
|
width = self._edit_nick.width
|
||||||
self._overlay.set_overlay_parameters(
|
self._overlay.set_overlay_parameters(
|
||||||
align=urwid.CENTER,
|
align=urwid.CENTER,
|
||||||
|
|
@ -429,15 +403,15 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
)
|
)
|
||||||
self._overlay._invalidate()
|
self._overlay._invalidate()
|
||||||
|
|
||||||
def change_own_nick(self) -> None:
|
# Reacting to changes
|
||||||
|
|
||||||
|
def own_nick_change(self):
|
||||||
self._renderer.nick = self._room.session.nick
|
self._renderer.nick = self._room.session.nick
|
||||||
self._tree.invalidate_all()
|
self._tree.invalidate_all()
|
||||||
self.update_tree()
|
self.update_tree()
|
||||||
|
|
||||||
self._nick_list.session = self._room.session
|
|
||||||
self.update_nick_list()
|
self.update_nick_list()
|
||||||
|
|
||||||
def receive_message(self, msg: yaboli.Message) -> None:
|
def receive_message(self, msg: yaboli.Message):
|
||||||
self._supply.add(Message(
|
self._supply.add(Message(
|
||||||
msg.message_id,
|
msg.message_id,
|
||||||
msg.parent_id,
|
msg.parent_id,
|
||||||
|
|
@ -453,15 +427,14 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
def render(self, size: Tuple[int, int], focus: bool) -> None:
|
def render(self, size: Tuple[int, int], focus: bool) -> None:
|
||||||
canvas = super().render(size, focus)
|
canvas = super().render(size, focus)
|
||||||
|
|
||||||
if not self._hit_top_of_supply:
|
if self._tree.hit_top and not self._requesting_logs:
|
||||||
if self._tree.hit_top and not self._requesting_logs:
|
self._requesting_logs = True
|
||||||
self._requesting_logs = True
|
self.request_logs()
|
||||||
self.request_logs()
|
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]:
|
def keypress(self, size: Tuple[int, int], key: str) -> Optional[str]:
|
||||||
if self._mode == UiMode.VIEWING:
|
if self._mode == self.VIEWING:
|
||||||
if key in {"enter", "meta enter"} and not self._room.session.nick:
|
if key in {"enter", "meta enter"} and not self._room.session.nick:
|
||||||
self.switch_setting_nick()
|
self.switch_setting_nick()
|
||||||
elif key == "enter":
|
elif key == "enter":
|
||||||
|
|
@ -479,7 +452,7 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
else:
|
else:
|
||||||
return super().keypress(size, key)
|
return super().keypress(size, key)
|
||||||
|
|
||||||
elif self._mode == UiMode.EDITING:
|
elif self._mode == self.EDITING:
|
||||||
if key == "enter":
|
if key == "enter":
|
||||||
if self._edit.edit_text:
|
if self._edit.edit_text:
|
||||||
self.send(self._edit.edit_text, self._tree.cursor_id)
|
self.send(self._edit.edit_text, self._tree.cursor_id)
|
||||||
|
|
@ -491,7 +464,7 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
else:
|
else:
|
||||||
return super().keypress(size, key)
|
return super().keypress(size, key)
|
||||||
|
|
||||||
elif self._mode == UiMode.SETTING_NICK:
|
elif self._mode == self.SETTING_NICK:
|
||||||
if key == "enter":
|
if key == "enter":
|
||||||
if self._edit_nick.text:
|
if self._edit_nick.text:
|
||||||
self.nick(self._edit_nick.text)
|
self.nick(self._edit_nick.text)
|
||||||
|
|
@ -512,66 +485,37 @@ class RoomWidget(urwid.WidgetWrap):
|
||||||
|
|
||||||
# Reacting to euph events
|
# Reacting to euph events
|
||||||
|
|
||||||
async def on_connected(self) -> None:
|
async def on_snapshot(self, messages: List[yaboli.Message]):
|
||||||
pass
|
|
||||||
|
|
||||||
async def on_snapshot(self, messages: List[yaboli.LiveMessage]) -> None:
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self.receive_message(message)
|
self.receive_message(message)
|
||||||
|
self.update_tree()
|
||||||
|
|
||||||
self.change_own_nick()
|
async def on_send(self, message: yaboli.Message):
|
||||||
self.update_nick_list()
|
|
||||||
|
|
||||||
async def on_send(self, message: yaboli.LiveMessage) -> None:
|
|
||||||
self.receive_message(message)
|
self.receive_message(message)
|
||||||
|
self.update_tree()
|
||||||
async def on_join(self, user: yaboli.LiveSession) -> None:
|
|
||||||
self.update_nick_list()
|
|
||||||
|
|
||||||
async def on_part(self, user: yaboli.LiveSession) -> None:
|
|
||||||
self.update_nick_list()
|
|
||||||
|
|
||||||
async def on_nick(self,
|
|
||||||
user: yaboli.LiveSession,
|
|
||||||
from_: str,
|
|
||||||
to: str,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self.update_nick_list()
|
|
||||||
|
|
||||||
async def on_edit(self, message: yaboli.LiveMessage) -> None:
|
|
||||||
self.receive_message(message)
|
|
||||||
|
|
||||||
async def on_disconnect(self, reason: str) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Euph actions
|
# Euph actions
|
||||||
|
|
||||||
@synchronous
|
@synchronous
|
||||||
async def request_logs(self) -> None:
|
async def request_logs(self):
|
||||||
oldest_id = self._supply.oldest_id()
|
oldest_id = self._supply.oldest_id()
|
||||||
if oldest_id is not None:
|
if oldest_id is not None:
|
||||||
messages = await self._room.log(self._log_amount, oldest_id)
|
messages = await self._room.log(self._log_amount, oldest_id)
|
||||||
|
|
||||||
if len(messages) == 0:
|
|
||||||
self._hit_top_of_supply = True
|
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
self.receive_message(message)
|
self.receive_message(message)
|
||||||
|
self.update_tree()
|
||||||
|
|
||||||
self._requesting_logs = False
|
self._requesting_logs = False
|
||||||
|
|
||||||
@synchronous
|
@synchronous
|
||||||
async def nick(self, nick: str) -> None:
|
async def nick(self, nick: str):
|
||||||
try:
|
new_nick = await self._room.nick(nick)
|
||||||
await self._room.nick(nick)
|
self.own_nick_change()
|
||||||
self.change_own_nick()
|
|
||||||
except yaboli.EuphException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@synchronous
|
@synchronous
|
||||||
async def send(self, content: str, parent_id: Optional[str]) -> None:
|
async def send(self, content: str, parent_id: Optional[str]):
|
||||||
message = await self._room.send(content, parent_id=parent_id)
|
message = await self._room.send(content, parent_id=parent_id)
|
||||||
self.receive_message(message)
|
self.receive_message(message)
|
||||||
|
self.update_tree()
|
||||||
|
|
||||||
urwid.register_signal(RoomWidget, ["close"])
|
urwid.register_signal(RoomWidget, ["close"])
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import urwid
|
import urwid
|
||||||
|
import yaml
|
||||||
|
|
||||||
from ..attributed_text_widget import ATWidget
|
from ..attributed_text_widget import ATWidget
|
||||||
from ..markup import AT
|
from ..markup import AT, Attributes
|
||||||
from .edit_widgets import EditWidget
|
from .edit_widgets import EditWidget
|
||||||
from .euph_config import EuphConfig
|
|
||||||
from .launch_application import launch
|
from .launch_application import launch
|
||||||
|
from .euph_config import EuphConfig, EuphLoader
|
||||||
from .room_widget import RoomWidget
|
from .room_widget import RoomWidget
|
||||||
|
|
||||||
__all__ = ["SingleRoomApplication", "launch_single_room_application"]
|
__all__ = ["SingleRoomApplication", "launch_single_room_application"]
|
||||||
|
|
@ -59,11 +63,11 @@ class ChooseRoomWidget(urwid.WidgetWrap):
|
||||||
|
|
||||||
def invalid_room_name(self, reason: str) -> None:
|
def invalid_room_name(self, reason: str) -> None:
|
||||||
text = AT(f"Invalid room name: {reason}\n",
|
text = AT(f"Invalid room name: {reason}\n",
|
||||||
style=self._error_style)
|
attributes=self._error_attrs)
|
||||||
self.set_error(ATWidget(text, align=urwid.CENTER))
|
self.set_error(ATWidget(text, align=urwid.CENTER))
|
||||||
|
|
||||||
class SingleRoomApplication(urwid.WidgetWrap):
|
class SingleRoomApplication(urwid.WidgetWrap):
|
||||||
|
#
|
||||||
# The characters in the ALPHABET make up the characters that are allowed in
|
# The characters in the ALPHABET make up the characters that are allowed in
|
||||||
# room names.
|
# room names.
|
||||||
ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
|
ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
@ -126,5 +130,5 @@ class SingleRoomApplication(urwid.WidgetWrap):
|
||||||
|
|
||||||
return key
|
return key
|
||||||
|
|
||||||
def launch_single_room_application() -> None:
|
def launch_single_room_application():
|
||||||
launch(SingleRoomApplication)
|
launch(SingleRoomApplication)
|
||||||
|
|
@ -4,7 +4,6 @@ class MessageSupplyException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ShouldNeverHappen(Exception):
|
class ShouldNeverHappen(Exception):
|
||||||
|
|
||||||
def __init__(self, number: int) -> None:
|
def __init__(self, number: int) -> None:
|
||||||
message = (f"SNV{number:05} - please contact @Garmy with the code on"
|
message = (f"SNV{number:05} - please contact @Garmy with the code on"
|
||||||
" the left if you see this")
|
" the left if you see this")
|
||||||
|
|
@ -32,7 +32,10 @@ class Chunk:
|
||||||
|
|
||||||
# Common special methods
|
# Common special methods
|
||||||
|
|
||||||
def __init__(self, text: str, attributes: Attributes = {}) -> None:
|
def __init__(self,
|
||||||
|
text: str,
|
||||||
|
attributes: Attributes = {},
|
||||||
|
) -> None:
|
||||||
self._text = text
|
self._text = text
|
||||||
self._attributes = dict(attributes)
|
self._attributes = dict(attributes)
|
||||||
|
|
||||||
|
|
@ -48,8 +51,7 @@ class Chunk:
|
||||||
if not isinstance(other, Chunk):
|
if not isinstance(other, Chunk):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
return (self._text == other._text and
|
return self._text == other._text and self._attributes == other._attributes
|
||||||
self._attributes == other._attributes)
|
|
||||||
|
|
||||||
def __getitem__(self, key: Union[int, slice]) -> "Chunk":
|
def __getitem__(self, key: Union[int, slice]) -> "Chunk":
|
||||||
return Chunk(self.text[key], self._attributes)
|
return Chunk(self.text[key], self._attributes)
|
||||||
|
|
@ -245,13 +247,9 @@ class AttributedText:
|
||||||
name: str,
|
name: str,
|
||||||
default: Any = None,
|
default: Any = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
|
||||||
return self._at(pos).get(name, default)
|
return self._at(pos).get(name, default)
|
||||||
|
|
||||||
def split_by(self,
|
def split_by(self, attribute_name: str) -> List[Tuple["AttributedText", Any]]:
|
||||||
attribute_name: str,
|
|
||||||
) -> List[Tuple["AttributedText", Any]]:
|
|
||||||
|
|
||||||
blocks = []
|
blocks = []
|
||||||
|
|
||||||
chunks: List[Chunk] = []
|
chunks: List[Chunk] = []
|
||||||
|
|
@ -299,7 +297,6 @@ class AttributedText:
|
||||||
start: Optional[int] = None,
|
start: Optional[int] = None,
|
||||||
stop: Optional[int] = None,
|
stop: Optional[int] = None,
|
||||||
) -> "AttributedText":
|
) -> "AttributedText":
|
||||||
|
|
||||||
if start is None and stop is None:
|
if start is None and stop is None:
|
||||||
chunks = (chunk.set(name, value) for chunk in self._chunks)
|
chunks = (chunk.set(name, value) for chunk in self._chunks)
|
||||||
return AttributedText.from_chunks(chunks)
|
return AttributedText.from_chunks(chunks)
|
||||||
|
|
@ -322,7 +319,6 @@ class AttributedText:
|
||||||
start: Optional[int] = None,
|
start: Optional[int] = None,
|
||||||
stop: Optional[int] = None,
|
stop: Optional[int] = None,
|
||||||
) -> "AttributedText":
|
) -> "AttributedText":
|
||||||
|
|
||||||
if start is None and stop is None:
|
if start is None and stop is None:
|
||||||
chunks = (chunk.remove(name) for chunk in self._chunks)
|
chunks = (chunk.remove(name) for chunk in self._chunks)
|
||||||
return AttributedText.from_chunks(chunks)
|
return AttributedText.from_chunks(chunks)
|
||||||
BIN
demo.gif
BIN
demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 786 KiB |
10
setup.py
10
setup.py
|
|
@ -1,15 +1,15 @@
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="bowl",
|
name="cheuph",
|
||||||
version="1.0.0",
|
version="0.0.1",
|
||||||
packages=[
|
packages=[
|
||||||
"bowl",
|
"cheuph",
|
||||||
"bowl.euphoria",
|
"cheuph.euphoria",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
"bowl = bowl.euphoria:launch_single_room_application",
|
"cheuph = cheuph.euphoria:launch_single_room_application",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
|
|
||||||
80
test.py
Normal file
80
test.py
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import curses
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
|
from cheuph.element import Element, Id, RenderedElement
|
||||||
|
from cheuph.element_supply import MemoryElementSupply
|
||||||
|
from cheuph.markup import AttributedText
|
||||||
|
from cheuph.tree_display import TreeDisplay
|
||||||
|
|
||||||
|
|
||||||
|
class TestElement(Element):
|
||||||
|
DEPTHSTR = "| "
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
id: Id,
|
||||||
|
parent_id: Optional[Id],
|
||||||
|
text: List[str],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
super().__init__(id, parent_id)
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
def render(self,
|
||||||
|
width: int,
|
||||||
|
depth: int,
|
||||||
|
highlighted: bool = False,
|
||||||
|
folded: bool = False,
|
||||||
|
) -> RenderedElement:
|
||||||
|
|
||||||
|
depth_text = self.DEPTHSTR * depth
|
||||||
|
lines = [f"{depth_text}{line}" for line in self.text]
|
||||||
|
attributed_lines = [AttributedText(line) for line in lines]
|
||||||
|
return RenderedElement(self, attributed_lines)
|
||||||
|
|
||||||
|
def main(stdscr: Any) -> None:
|
||||||
|
messages = MemoryElementSupply()
|
||||||
|
messages.add(TestElement("a", None, ["test element a"]))
|
||||||
|
messages.add(TestElement("b", "a", ["test element b","child of a"]))
|
||||||
|
messages.add(TestElement("c", None, ["test element c"]))
|
||||||
|
|
||||||
|
display = TreeDisplay(messages, 80, 15)
|
||||||
|
display.anchor_id = "a"
|
||||||
|
display.anchor_offset = 5
|
||||||
|
|
||||||
|
display.rerender()
|
||||||
|
display.render_display_lines()
|
||||||
|
|
||||||
|
print("-"*80)
|
||||||
|
for line in display.display_lines:
|
||||||
|
print(str(line))
|
||||||
|
print("-"*80)
|
||||||
|
|
||||||
|
# while True:
|
||||||
|
# key = stdscr.getkey()
|
||||||
|
#
|
||||||
|
# if key in {"\x1b", "q"}:
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# elif key == "e":
|
||||||
|
# with tempfile.TemporaryDirectory() as tmpdirname:
|
||||||
|
# tmpfilename = tmpdirname + "/" + "tempfile"
|
||||||
|
# #stdscr.addstr(f"{curses.COLOR_PAIRS!r}\n")
|
||||||
|
# stdscr.addstr(f"{tmpdirname!r} | {tmpfilename!r}\n")
|
||||||
|
#
|
||||||
|
# stdscr.getkey()
|
||||||
|
#
|
||||||
|
# curses.endwin()
|
||||||
|
# subprocess.run(["nvim", tmpfilename])
|
||||||
|
# stdscr.refresh()
|
||||||
|
#
|
||||||
|
# stdscr.getkey()
|
||||||
|
#
|
||||||
|
# with open(tmpfilename) as f:
|
||||||
|
# for line in f:
|
||||||
|
# stdscr.addstr(line)
|
||||||
|
|
||||||
|
|
||||||
|
#curses.wrapper(main)
|
||||||
|
main(None)
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from bowl import CursorTreeRenderer
|
import cheuph
|
||||||
|
|
||||||
__all__ = ["TestCursorTreeRenderer"]
|
__all__ = ["TestCursorTreeRenderer"]
|
||||||
|
|
||||||
class TestCursorTreeRenderer(unittest.TestCase):
|
class TestCursorTreeRenderer(unittest.TestCase):
|
||||||
|
|
||||||
def test_static_offset(self):
|
def test_static_offset(self):
|
||||||
gao = CursorTreeRenderer.get_absolute_offset
|
gao = cheuph.CursorTreeRenderer.get_absolute_offset
|
||||||
gro = CursorTreeRenderer.get_relative_offset
|
gro = cheuph.CursorTreeRenderer.get_relative_offset
|
||||||
|
|
||||||
self.assertEqual(0, gao(0.0, 6))
|
self.assertEqual(0, gao(0.0, 6))
|
||||||
self.assertEqual(1, gao(0.2, 6))
|
self.assertEqual(1, gao(0.2, 6))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from bowl import AT
|
from cheuph import AT
|
||||||
|
|
||||||
__all__ = ["TestAttributedText"]
|
__all__ = ["TestAttributedText"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from bowl import Element, RenderedElementCache
|
from cheuph import Element, RenderedElementCache
|
||||||
|
|
||||||
__all__ = ["TestRenderedElementCache"]
|
__all__ = ["TestRenderedElementCache"]
|
||||||
|
|
||||||
|
|
|
||||||
2
test_scripts/.gitignore
vendored
Normal file
2
test_scripts/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
cheuph
|
||||||
|
yaboli
|
||||||
60
test_scripts/display_attr_lines_wiget.py
Normal file
60
test_scripts/display_attr_lines_wiget.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import urwid
|
||||||
|
import urwid.curses_display
|
||||||
|
|
||||||
|
import cheuph
|
||||||
|
from cheuph import AT, AttributedLines, AttributedLinesWidget
|
||||||
|
|
||||||
|
|
||||||
|
class TestWidget(urwid.WidgetWrap):
|
||||||
|
def __init__(self):
|
||||||
|
long_line = AT("super", style="red")
|
||||||
|
long_line += AT(" long", style="cyan")
|
||||||
|
long_line += AT(" line", style="magenta")
|
||||||
|
lines = [
|
||||||
|
({}, AT("abc", style="green")),
|
||||||
|
({"style": "blue"}, AT("Hello world")),
|
||||||
|
({}, AT(" ").join([long_line] * 10)),
|
||||||
|
]
|
||||||
|
self.lines = AttributedLinesWidget(AttributedLines(lines))
|
||||||
|
super().__init__(self.lines)
|
||||||
|
|
||||||
|
def selectable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
if key == "left":
|
||||||
|
self.lines.horizontal_offset -= 1
|
||||||
|
elif key == "right":
|
||||||
|
self.lines.horizontal_offset += 1
|
||||||
|
elif key == "home":
|
||||||
|
self.lines.horizontal_offset = 0
|
||||||
|
elif key == "up":
|
||||||
|
self.lines.upper_offset += 1
|
||||||
|
elif key == "down":
|
||||||
|
self.lines.upper_offset -= 1
|
||||||
|
|
||||||
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
|
if event == "mouse press":
|
||||||
|
if button == 4:
|
||||||
|
self.lines.upper_offset += 1
|
||||||
|
if button == 5:
|
||||||
|
self.lines.upper_offset -= 1
|
||||||
|
|
||||||
|
def main():
|
||||||
|
screen = urwid.curses_display.Screen()
|
||||||
|
palette = [
|
||||||
|
("red", "light red", ""),
|
||||||
|
("yellow", "yellow", ""),
|
||||||
|
("green", "light green", ""),
|
||||||
|
("blue", "light blue", ""),
|
||||||
|
("magenta", "light magenta", ""),
|
||||||
|
("cyan", "light cyan", ""),
|
||||||
|
]
|
||||||
|
loop = urwid.MainLoop(
|
||||||
|
TestWidget(),
|
||||||
|
screen=screen,
|
||||||
|
palette=palette,
|
||||||
|
)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
main()
|
||||||
20
test_scripts/display_attr_text_widget.py
Normal file
20
test_scripts/display_attr_text_widget.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import urwid
|
||||||
|
import urwid.curses_display
|
||||||
|
|
||||||
|
import cheuph
|
||||||
|
from cheuph import AT, AttributedTextWidget
|
||||||
|
|
||||||
|
|
||||||
|
class TestWidget(urwid.WidgetWrap):
|
||||||
|
def __init__(self):
|
||||||
|
text = AT("Hello world!\nThis is some text.\nThird line.")
|
||||||
|
self.text = AttributedTextWidget(text)
|
||||||
|
self.filler = urwid.Filler(self.text)
|
||||||
|
super().__init__(self.filler)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
screen = urwid.curses_display.Screen()
|
||||||
|
loop = urwid.MainLoop(TestWidget(), screen=screen)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
main()
|
||||||
31
test_scripts/display_pressed_keys.py
Normal file
31
test_scripts/display_pressed_keys.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import urwid
|
||||||
|
import urwid.curses_display
|
||||||
|
|
||||||
|
class TestWidget(urwid.WidgetWrap):
|
||||||
|
KEY_LIMIT = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.last_keys = []
|
||||||
|
self.text = urwid.Text("No key pressed yet", align=urwid.CENTER)
|
||||||
|
self.filler = urwid.Filler(self.text)
|
||||||
|
super().__init__(self.filler)
|
||||||
|
|
||||||
|
def selectable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def keypress(self, size, key):
|
||||||
|
self.last_keys.append(repr(key))
|
||||||
|
self.last_keys = self.last_keys[-self.KEY_LIMIT:]
|
||||||
|
self.text.set_text("\n".join(self.last_keys))
|
||||||
|
|
||||||
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
|
self.last_keys.append(f"{event!r} {button!r} ({row}, {col})")
|
||||||
|
self.last_keys = self.last_keys[-self.KEY_LIMIT:]
|
||||||
|
self.text.set_text("\n".join(self.last_keys))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
screen = urwid.curses_display.Screen()
|
||||||
|
loop = urwid.MainLoop(TestWidget(), screen=screen)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
main()
|
||||||
30
test_scripts/display_room_layout.py
Normal file
30
test_scripts/display_room_layout.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import urwid
|
||||||
|
import urwid.curses_display
|
||||||
|
|
||||||
|
import cheuph
|
||||||
|
from cheuph import AT, AttributedTextWidget
|
||||||
|
from cheuph.euphoria.room_widget import RoomLayout
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
widget = RoomLayout(
|
||||||
|
AttributedTextWidget(AT("&test"), align=urwid.CENTER),
|
||||||
|
urwid.SolidFill("n"),
|
||||||
|
urwid.SolidFill("t"),
|
||||||
|
AttributedTextWidget(AT("edit\ning")),
|
||||||
|
nick_list_width = 15,
|
||||||
|
border_attrs = {"style": "dim"},
|
||||||
|
)
|
||||||
|
widget.set_edit_visible(True)
|
||||||
|
palette = [
|
||||||
|
("dim", "dark gray,bold", ""),
|
||||||
|
]
|
||||||
|
screen = urwid.curses_display.Screen()
|
||||||
|
loop = urwid.MainLoop(
|
||||||
|
widget,
|
||||||
|
palette=palette,
|
||||||
|
#screen=screen,
|
||||||
|
)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
main()
|
||||||
35
test_scripts/test_cursor_tree_widget.py
Normal file
35
test_scripts/test_cursor_tree_widget.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
import urwid.curses_display
|
||||||
|
|
||||||
|
import cheuph
|
||||||
|
from cheuph import (AT, BasicCursorRenderer, CursorTreeRenderer,
|
||||||
|
CursorTreeWidget, InMemorySupply, Message)
|
||||||
|
|
||||||
|
|
||||||
|
def add(supply, level, text, amount=4):
|
||||||
|
t = datetime.datetime(2019, 5, 7, 13, 25, 6)
|
||||||
|
if level < 0: return
|
||||||
|
for i in range(amount):
|
||||||
|
new_text = f"{text}->{i}"
|
||||||
|
supply.add(Message(new_text, text or None, t, str(i), new_text))
|
||||||
|
add(supply, level - 1, new_text, amount=amount)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
s = InMemorySupply()
|
||||||
|
r = BasicCursorRenderer()
|
||||||
|
t = CursorTreeRenderer(s, r)
|
||||||
|
|
||||||
|
add(s, 4, "")
|
||||||
|
|
||||||
|
#screen = urwid.curses_display.Screen()
|
||||||
|
event_loop = urwid.AsyncioEventLoop()
|
||||||
|
loop = urwid.MainLoop(
|
||||||
|
cheuph.CursorTreeWidget(t),
|
||||||
|
#screen=screen,
|
||||||
|
event_loop=event_loop,
|
||||||
|
)
|
||||||
|
loop.run()
|
||||||
|
|
||||||
|
main()
|
||||||
21
test_scripts/test_launch_single_room_widget.py
Normal file
21
test_scripts/test_launch_single_room_widget.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import urwid
|
||||||
|
|
||||||
|
from cheuph.euphoria.single_room_application import SingleRoomApplication
|
||||||
|
|
||||||
|
logging.disable()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
main_loop = urwid.MainLoop(
|
||||||
|
SingleRoomApplication(),
|
||||||
|
event_loop=urwid.AsyncioEventLoop(loop=loop),
|
||||||
|
)
|
||||||
|
|
||||||
|
main_loop.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
37
todo.txt
37
todo.txt
|
|
@ -1,37 +0,0 @@
|
||||||
- config
|
|
||||||
x colors
|
|
||||||
- key bindings
|
|
||||||
- documentation (especially of the config)
|
|
||||||
|
|
||||||
- profiling/optimisation
|
|
||||||
|
|
||||||
- detail mode
|
|
||||||
- fold threads
|
|
||||||
- nick list
|
|
||||||
- better key bindings/controls
|
|
||||||
- center cursor on screen (after scrolling the view without scrolling the cursor)
|
|
||||||
- mouse support
|
|
||||||
- searching for messages
|
|
||||||
- better message editing when the screen is full
|
|
||||||
- detect when the dimensions are too small (meta width etc.) and display warning
|
|
||||||
- green "unread message" markers
|
|
||||||
- highlight things in messages
|
|
||||||
- offline log browsing
|
|
||||||
- @mentions
|
|
||||||
- &rooms
|
|
||||||
- https://links
|
|
||||||
- :emojis:
|
|
||||||
- /me s
|
|
||||||
- word wrapping for messages
|
|
||||||
- multi-room support
|
|
||||||
- db backend
|
|
||||||
- download room log
|
|
||||||
- auto repair gaps in log
|
|
||||||
|
|
||||||
x robust starting script
|
|
||||||
x install via pip from github
|
|
||||||
x runnable script
|
|
||||||
x parse command-line parameters
|
|
||||||
x nick list
|
|
||||||
x room_widget refactor
|
|
||||||
x save cookies
|
|
||||||
295
tree_display.py
Normal file
295
tree_display.py
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
# Element supply of some sort
|
||||||
|
# Dict-/map-like
|
||||||
|
# Tree structure
|
||||||
|
# ↓
|
||||||
|
# List of already formatted elements
|
||||||
|
# Each with a line height, indentation, ...
|
||||||
|
# List structure
|
||||||
|
# ↓
|
||||||
|
# Messages and UI elements rendered to lines
|
||||||
|
# with meta-information, links/ids
|
||||||
|
# List structure, but on lines, not individual messages
|
||||||
|
|
||||||
|
class Element:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ElementSupply:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TreeDisplay:
|
||||||
|
"""
|
||||||
|
Message line coordinates:
|
||||||
|
|
||||||
|
n - Highest message
|
||||||
|
...
|
||||||
|
1 - Higher message
|
||||||
|
0 - Lowest message
|
||||||
|
|
||||||
|
Screen/line coordinates:
|
||||||
|
|
||||||
|
h-1 - First line
|
||||||
|
h-2 - Second line
|
||||||
|
...
|
||||||
|
1 - Second to last line
|
||||||
|
0 - Last line
|
||||||
|
|
||||||
|
Terms:
|
||||||
|
|
||||||
|
<base>
|
||||||
|
| ...
|
||||||
|
| ... (above)
|
||||||
|
| ...
|
||||||
|
| <stem>
|
||||||
|
| | ...
|
||||||
|
| | | ... | <anchor>
|
||||||
|
| ...
|
||||||
|
| ... (below)
|
||||||
|
| ...
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
<base>
|
||||||
|
| ...
|
||||||
|
| ... (above)
|
||||||
|
| ...
|
||||||
|
| <stem and anchor>
|
||||||
|
| ...
|
||||||
|
| ... (below)
|
||||||
|
| ...
|
||||||
|
|
||||||
|
The stem is a child of the base. The anchor is a direct or indirect child
|
||||||
|
of the stem, or it is the stem itself.
|
||||||
|
|
||||||
|
The base id may also be None (the implicit parent of all top-level
|
||||||
|
messages in a room)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, window: Any) -> None:
|
||||||
|
self.window = window
|
||||||
|
|
||||||
|
self._anchor_id = None
|
||||||
|
# Position of the formatted anchor's uppermost line on the screen
|
||||||
|
self._anchor_screen_pos = 0
|
||||||
|
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
"""
|
||||||
|
Intermediate structures:
|
||||||
|
- Upwards and downwards list of elements + focused element
|
||||||
|
- Upwards and downwards list of rendered elements + focused element
|
||||||
|
- List of visible lines (saved and used for mouse clicks etc.)
|
||||||
|
|
||||||
|
Steps of rendering:
|
||||||
|
1. Load all necessary elements
|
||||||
|
2. Render all messages with indentation
|
||||||
|
3. Compile lines
|
||||||
|
|
||||||
|
Steps of advanced rendering:
|
||||||
|
1. Load focused element + render
|
||||||
|
2. Load tree of focused element + render
|
||||||
|
3. Load trees above and below + render, as many as necessary
|
||||||
|
4. While loading and rendering trees, auto-collapse
|
||||||
|
5. Move focus if focused element was hidden in an auto-collapse
|
||||||
|
...?
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Step 1: Find and render the tree the anchor is in.
|
||||||
|
|
||||||
|
stem_id = self._supply.find_stem_id(self._anchor_id,
|
||||||
|
base=self._base_id)
|
||||||
|
|
||||||
|
tree = self._supply.get_tree(stem_id)
|
||||||
|
# The render might modify self._anchor_id, if the original anchor can't
|
||||||
|
# be displayed.
|
||||||
|
self._render_tree(tree)
|
||||||
|
|
||||||
|
above, anchor, below = self._split_at_anchor(tree)
|
||||||
|
|
||||||
|
# Step 2: Add more trees above and below, until the screen can be
|
||||||
|
# filled or there aren't any elements left in the store.
|
||||||
|
|
||||||
|
# h_win = 7
|
||||||
|
# 6 | <- h_above = 3
|
||||||
|
# 5 |
|
||||||
|
# 4 |
|
||||||
|
# 3 | <- anchor, self._anchor_screen_pos = 3, anchor.height = 2
|
||||||
|
# 2 |
|
||||||
|
# 1 | <- h_below = 2
|
||||||
|
# 0 |
|
||||||
|
#
|
||||||
|
# 7 - 3 - 1 = 3 -- correct
|
||||||
|
# 3 - 2 + 1 = 2 -- correct
|
||||||
|
|
||||||
|
height_window = None # TODO
|
||||||
|
|
||||||
|
# All values are converted to zero indexed values in the calculations
|
||||||
|
height_above = (height_window - 1) - self._anchor_screen_pos
|
||||||
|
height_below = self._anchor_screen_pos - (anchor.height - 1)
|
||||||
|
|
||||||
|
self._extend(above, height_above, base=self._base_id)
|
||||||
|
self._extend(below, height_below, base=self._base_id)
|
||||||
|
|
||||||
|
self._lines = self._render_to_lines(above, anchor, below)
|
||||||
|
self._update_window(self._lines)
|
||||||
|
|
||||||
|
# TreeDisplay plan(s):
|
||||||
|
#
|
||||||
|
# [ ] 1. Render a basic tree thing from an ElementSupply
|
||||||
|
# [ ] 1.1. Take/own a curses window
|
||||||
|
# [ ] 1.2. Keep track of the text internally
|
||||||
|
# [ ] 1.3. Retrieve elements from an ElementSupply
|
||||||
|
# [ ] 1.4. Render elements to text depending on width of curses window
|
||||||
|
# [ ] 1.5. Do indentation
|
||||||
|
# [ ] 1.6. Use "..." where a thing can't be rendered
|
||||||
|
# [ ] 2. Scroll and anchor to messages
|
||||||
|
# [ ] 2.1. Listen to key presses
|
||||||
|
# [ ] 2.2. Scroll up/down single lines
|
||||||
|
# [ ] 2.3. Render starting from the anchor
|
||||||
|
# [ ] 2.4. Some sort of culling, but preserve CONSISTENCY!
|
||||||
|
# [ ] 3. Focus on single message
|
||||||
|
# [ ] 3.1. Keep track of focused message
|
||||||
|
# [ ] 3.2. Move focused message
|
||||||
|
# [ ] 3.3. Keep message visible on screen
|
||||||
|
# [ ] 3.4. Set anchor to focus when focus is visible
|
||||||
|
# [ ] 3.5. Find anchor solution for when focus is offscreen
|
||||||
|
# [ ] 4. Collapse element threads
|
||||||
|
# [ ] 4.1. Collapse thread at any element
|
||||||
|
# [ ] 4.2. Auto-collapse threads when they can't be displayed
|
||||||
|
# [ ] 4.3. Focus collapsed messages
|
||||||
|
# [ ] 4.4. Move focus when a hidden message would have focus
|
||||||
|
# [ ] 5. Interaction with elements
|
||||||
|
# [ ] 5.1. Forward key presses
|
||||||
|
# [ ] 5.2. Mouse clicks + message attributes
|
||||||
|
# [ ] 5.3. Element visibility
|
||||||
|
# [ ] 5.4. Request more elements when the top of the element supply is hit
|
||||||
|
# [ ] 5.5. ... and various other things
|
||||||
|
|
||||||
|
# STRUCTURE
|
||||||
|
#
|
||||||
|
# No async!
|
||||||
|
#
|
||||||
|
# The TreeView "owns" and completely fills one curses window.
|
||||||
|
#
|
||||||
|
# When rendering things, the TreeDisplay takes elements from the ElementSupply
|
||||||
|
# as needed. This should be a fast operation.
|
||||||
|
#
|
||||||
|
# When receiving key presses, the ones that are not interpreted by the TreeView
|
||||||
|
# are passed onto the currently focused element (if any).
|
||||||
|
#
|
||||||
|
# When receiving mouse clicks, the clicked-on element is focused and then the
|
||||||
|
# click and attributes of the clicked character are passed onto the focused
|
||||||
|
# element.
|
||||||
|
#
|
||||||
|
# (TODO: Notify focused elements? Make "viewed/new" state for elements
|
||||||
|
# possible?)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# DESIGN PRINCIPLES
|
||||||
|
#
|
||||||
|
# Layout: See below
|
||||||
|
#
|
||||||
|
# Color support: Definitely.
|
||||||
|
# No-color-mode: Not planned.
|
||||||
|
# => Colors required.
|
||||||
|
|
||||||
|
# The tree display can display a tree-like structure of elements.
|
||||||
|
#
|
||||||
|
# Each element consists of:
|
||||||
|
# 1. a way to display the element
|
||||||
|
# 2. a way to forward key presses to the element
|
||||||
|
# 3. element-specific attributes (Attributes), including:
|
||||||
|
# 3.1 "id", the element's hashable id
|
||||||
|
# 3.2 optionally "parent_id", the element's parent's hashable id
|
||||||
|
#
|
||||||
|
# (TODO: A way to notify the element that it is visible?)
|
||||||
|
#
|
||||||
|
# (TODO: Jump to unread messages, mark messages as read, up/down arrows like
|
||||||
|
# instant, message jump tags?)
|
||||||
|
#
|
||||||
|
# (TODO: Curses + threading/interaction etc.?)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# LAYOUT
|
||||||
|
#
|
||||||
|
# A tree display's basic structure is something like this:
|
||||||
|
#
|
||||||
|
# <element>
|
||||||
|
# | <element>
|
||||||
|
# | | <element>
|
||||||
|
# | | <element>
|
||||||
|
# | <element>
|
||||||
|
# | <element>
|
||||||
|
# | | <element>
|
||||||
|
# | | | <element>
|
||||||
|
# | <element>
|
||||||
|
# <element>
|
||||||
|
# | <element>
|
||||||
|
#
|
||||||
|
# It has an indentation string ("| " in the above example) that is prepended to
|
||||||
|
# each line according to its indentation. (TODO: Possibly add different types
|
||||||
|
# of indentation strings?)
|
||||||
|
#
|
||||||
|
# In general, "..." is inserted any time a message or other placeholder can't
|
||||||
|
# be displayed. (TODO: If "..." can't be displayed, it is shortened until it
|
||||||
|
# can be displayed?)
|
||||||
|
#
|
||||||
|
# Elements can be collapsed. Collapsed elements are displayed as "+ (<n>)"
|
||||||
|
# where <n> is the number of elements in the hidden subtree.
|
||||||
|
#
|
||||||
|
# If an element is so far right that it can't be displayed, the tree display
|
||||||
|
# first tries to collapse the tree. If the collapsed message can't be displayed
|
||||||
|
# either, it uses "..." as mentioned above.
|
||||||
|
#
|
||||||
|
# <element>
|
||||||
|
# | <element>
|
||||||
|
# | | <element>
|
||||||
|
# | | <element>
|
||||||
|
# | <element>
|
||||||
|
# | + (3)
|
||||||
|
# | <element>
|
||||||
|
# <element>
|
||||||
|
# | <element>
|
||||||
|
# | | <element>
|
||||||
|
# | | | <element>
|
||||||
|
# | | | | ...
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# NAVIGATION
|
||||||
|
#
|
||||||
|
# For navigation, the following movements/keys are used (all other key presses
|
||||||
|
# are forwarded to the currently selected element, if there is one):
|
||||||
|
#
|
||||||
|
# LEFT (left arrow, h): move to the selected element's parent
|
||||||
|
#
|
||||||
|
# RIGHT (right arrow, l): move to the selected element's first child
|
||||||
|
#
|
||||||
|
# UP (up arrow, k): move to the element visually below the selected element
|
||||||
|
#
|
||||||
|
# DOWN (down arrow, j): move to the element visually above the selected element
|
||||||
|
#
|
||||||
|
# Mod + LEFT: move to the selected element's previous sibling, if one exists
|
||||||
|
#
|
||||||
|
# Mod + RIGHT: move to the selected element's next sibling, if one exists
|
||||||
|
#
|
||||||
|
# Mod + UP: scroll up by scroll step
|
||||||
|
#
|
||||||
|
# Mod + DOWN: scroll down by scroll step
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# CURSES
|
||||||
|
#
|
||||||
|
# Main thread:
|
||||||
|
# - async
|
||||||
|
# - yaboli
|
||||||
|
# - curses: non-blocking input
|
||||||
|
# - curses: update visuals
|
||||||
|
# - run editor in async variant of subprocess or separate thread
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# STRUCTURE
|
||||||
|
#
|
||||||
|
# ???
|
||||||
Loading…
Add table
Add a link
Reference in a new issue