Compare commits
1 commit
master
...
markup-rew
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aafb11d3f |
5 changed files with 53 additions and 219 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
## Next version
|
||||
|
||||
- Add demo gif to readme
|
||||
- Fix indentation of multi-line messages
|
||||
- Stop using dataclass (for backwards compatibility with Python 3.6)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
A TUI client for [euphoria.io](https://euphoria.io)
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Ensure that you have at least Python 3.7 installed.
|
||||
|
|
|
|||
232
bowl/markup.py
232
bowl/markup.py
|
|
@ -1,99 +1,30 @@
|
|||
from typing import Any, Iterable, List, Mapping, Optional, Tuple, Union
|
||||
from dataclasses import dataclass
|
||||
|
||||
__all__ = ["Attributes", "Chunk", "AttributedText", "AT"]
|
||||
__all__ = ["Attributes", "AttributedText", "AT"]
|
||||
|
||||
Attributes = Mapping[str, Any]
|
||||
|
||||
class Chunk:
|
||||
@dataclass
|
||||
class Char:
|
||||
|
||||
@staticmethod
|
||||
def join_chunks(chunks: List["Chunk"]) -> List["Chunk"]:
|
||||
if not chunks:
|
||||
return []
|
||||
char: str
|
||||
attrs: Attributes
|
||||
|
||||
new_chunks: List[Chunk] = []
|
||||
def set(self, name: str, value: Any) -> "Char":
|
||||
new_attrs = dict(self.attrs)
|
||||
new_attrs[name] = value
|
||||
return Char(self.char, new_attrs)
|
||||
|
||||
current_chunk = chunks[0]
|
||||
for chunk in chunks[1:]:
|
||||
if not chunk.text:
|
||||
continue
|
||||
|
||||
joined_chunk = current_chunk._join(chunk)
|
||||
|
||||
if joined_chunk is None:
|
||||
new_chunks.append(current_chunk)
|
||||
current_chunk = chunk
|
||||
else:
|
||||
current_chunk = joined_chunk
|
||||
|
||||
new_chunks.append(current_chunk)
|
||||
|
||||
return new_chunks
|
||||
|
||||
# Common special methods
|
||||
|
||||
def __init__(self, text: str, attributes: Attributes = {}) -> None:
|
||||
self._text = text
|
||||
self._attributes = dict(attributes)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Chunk({self.text!r}, {self._attributes!r})"
|
||||
|
||||
# Uncommon special methods
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Chunk):
|
||||
return NotImplemented
|
||||
|
||||
return (self._text == other._text and
|
||||
self._attributes == other._attributes)
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> "Chunk":
|
||||
return Chunk(self.text[key], self._attributes)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.text)
|
||||
|
||||
# Properties
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
|
||||
@property
|
||||
def attributes(self) -> Attributes:
|
||||
return dict(self._attributes)
|
||||
|
||||
# Private methods
|
||||
|
||||
def _join(self, chunk: "Chunk") -> Optional["Chunk"]:
|
||||
if self._attributes == chunk._attributes:
|
||||
return Chunk(self.text + chunk.text, self._attributes)
|
||||
|
||||
return None
|
||||
|
||||
# Public methods
|
||||
|
||||
def get(self, name: str, default: Any = None) -> Any:
|
||||
return self.attributes.get(name, default)
|
||||
|
||||
def set(self, name: str, value: Any) -> "Chunk":
|
||||
new_attributes = dict(self._attributes)
|
||||
new_attributes[name] = value
|
||||
return Chunk(self.text, new_attributes)
|
||||
|
||||
def remove(self, name: str) -> "Chunk":
|
||||
new_attributes = dict(self._attributes)
|
||||
def remove(self, name: str) -> "Char":
|
||||
new_attrs = dict(self.attrs)
|
||||
|
||||
# This removes the value with that key, if it exists, and does nothing
|
||||
# if it doesn't exist. (Since we give a default value, no KeyError is
|
||||
# raised if the key isn't found.)
|
||||
new_attributes.pop(name, None)
|
||||
new_attrs.pop(name, None)
|
||||
|
||||
return Chunk(self.text, new_attributes)
|
||||
return Char(self.char, new_attrs)
|
||||
|
||||
class AttributedText:
|
||||
"""
|
||||
|
|
@ -102,9 +33,9 @@ class AttributedText:
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def from_chunks(cls, chunks: Iterable[Chunk]) -> "AttributedText":
|
||||
def from_chars(cls, chars: Iterable[Char]) -> "AttributedText":
|
||||
new = cls()
|
||||
new._chunks = Chunk.join_chunks(list(chunks))
|
||||
new._chars = list(chars)
|
||||
return new
|
||||
|
||||
# Common special methods
|
||||
|
|
@ -128,117 +59,60 @@ class AttributedText:
|
|||
attributes = dict(attributes)
|
||||
attributes.update(kwargs)
|
||||
|
||||
self._chunks: List[Chunk] = []
|
||||
if text is not None:
|
||||
self._chunks.append(Chunk(text, attributes=attributes))
|
||||
self._chars: List[Char] = []
|
||||
for char in text or "":
|
||||
self._chars.append(Char(char, attributes))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"AttributedText.from_chunks({self._chunks!r})"
|
||||
return "N/A"
|
||||
|
||||
# Uncommon special methods
|
||||
|
||||
def __add__(self, other: "AttributedText") -> "AttributedText":
|
||||
return AttributedText.from_chunks(self._chunks + other._chunks)
|
||||
return AttributedText.from_chars(self._chars + other._chars)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, AttributedText):
|
||||
return NotImplemented
|
||||
|
||||
return self._chunks == other._chunks
|
||||
return self._chars == other._chars
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> "AttributedText":
|
||||
chunks: List[Chunk]
|
||||
chars: List[Char]
|
||||
|
||||
if isinstance(key, slice):
|
||||
chunks = Chunk.join_chunks(self._slice(key))
|
||||
chars = self._chars[key]
|
||||
else:
|
||||
chunks = [self._at(key)]
|
||||
chars = [self._chars[key]]
|
||||
|
||||
return AttributedText.from_chunks(chunks)
|
||||
return AttributedText.from_chars(chars)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return sum(map(len, self._chunks))
|
||||
return len(self._chars)
|
||||
|
||||
def __mul__(self, other: int) -> "AttributedText":
|
||||
if not isinstance(other, int):
|
||||
return NotImplemented
|
||||
|
||||
return self.from_chunks(self.chunks * other)
|
||||
return self.from_chars(self._chars * other)
|
||||
|
||||
# Properties
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return "".join(chunk.text for chunk in self._chunks)
|
||||
return "".join(char.char for char in self._chars)
|
||||
|
||||
@property
|
||||
def chunks(self) -> List[Chunk]:
|
||||
return list(self._chunks)
|
||||
|
||||
# Private methods
|
||||
|
||||
def _at(self, key: int) -> Chunk:
|
||||
if key < 0:
|
||||
key = len(self) + key
|
||||
|
||||
pos = 0
|
||||
for chunk in self._chunks:
|
||||
chunk_key = key - pos
|
||||
|
||||
if 0 <= chunk_key < len(chunk):
|
||||
return chunk[chunk_key]
|
||||
|
||||
pos += len(chunk)
|
||||
|
||||
# We haven't found the chunk
|
||||
raise KeyError
|
||||
|
||||
def _slice(self, key: slice) -> List[Chunk]:
|
||||
start, stop, step = key.start, key.stop, key.step
|
||||
|
||||
if start is None:
|
||||
start = 0
|
||||
elif start < 0:
|
||||
start = len(self) + start
|
||||
|
||||
if stop is None:
|
||||
stop = len(self)
|
||||
elif stop < 0:
|
||||
stop = len(self) + stop
|
||||
|
||||
pos = 0 # cursor position
|
||||
resulting_chunks = []
|
||||
|
||||
for chunk in self._chunks:
|
||||
chunk_start = start - pos
|
||||
chunk_stop = stop - pos
|
||||
|
||||
offset: Optional[int] = None
|
||||
if step is not None:
|
||||
offset = (start - pos) % step
|
||||
|
||||
if chunk_stop <= 0 or chunk_start >= len(chunk):
|
||||
pass
|
||||
elif chunk_start < 0 and chunk_stop > len(chunk):
|
||||
resulting_chunks.append(chunk[offset::step])
|
||||
elif chunk_start < 0:
|
||||
resulting_chunks.append(chunk[offset:chunk_stop:step])
|
||||
elif chunk_stop > len(chunk):
|
||||
resulting_chunks.append(chunk[chunk_start::step])
|
||||
else:
|
||||
resulting_chunks.append(chunk[chunk_start:chunk_stop:step])
|
||||
|
||||
pos += len(chunk)
|
||||
|
||||
return resulting_chunks
|
||||
def chars(self) -> List[Char]:
|
||||
return list(self._chars)
|
||||
|
||||
# Public methods
|
||||
|
||||
def at(self, pos: int) -> Attributes:
|
||||
return self._at(pos).attributes
|
||||
return self._chars[pos].attrs
|
||||
|
||||
def get(self,
|
||||
pos: int,
|
||||
|
|
@ -246,7 +120,7 @@ class AttributedText:
|
|||
default: Any = None,
|
||||
) -> Any:
|
||||
|
||||
return self._at(pos).get(name, default)
|
||||
return self.at(pos).get(name, default)
|
||||
|
||||
def split_by(self,
|
||||
attribute_name: str,
|
||||
|
|
@ -254,26 +128,26 @@ class AttributedText:
|
|||
|
||||
blocks = []
|
||||
|
||||
chunks: List[Chunk] = []
|
||||
chars: List[Char] = []
|
||||
attribute: Any = None
|
||||
|
||||
for chunk in self._chunks:
|
||||
chunk_attr = chunk.attributes.get(attribute_name)
|
||||
for char in self._chars:
|
||||
char_attr = char.attrs.get(attribute_name)
|
||||
|
||||
if chunks:
|
||||
if attribute == chunk_attr:
|
||||
chunks.append(chunk)
|
||||
if chars:
|
||||
if attribute == char_attr:
|
||||
chars.append(char)
|
||||
else:
|
||||
blocks.append((self.from_chunks(chunks), attribute))
|
||||
blocks.append((self.from_chars(chars), attribute))
|
||||
|
||||
chunks = [chunk]
|
||||
attribute = chunk_attr
|
||||
chars = [char]
|
||||
attribute = char_attr
|
||||
else:
|
||||
chunks.append(chunk)
|
||||
attribute = chunk_attr
|
||||
chars.append(char)
|
||||
attribute = char_attr
|
||||
|
||||
if chunks:
|
||||
blocks.append((self.from_chunks(chunks), attribute))
|
||||
if chars:
|
||||
blocks.append((self.from_chars(chars), attribute))
|
||||
|
||||
return blocks
|
||||
|
||||
|
|
@ -287,11 +161,11 @@ class AttributedText:
|
|||
interspersed.append(self)
|
||||
interspersed.append(segment)
|
||||
|
||||
chunks = []
|
||||
chars = []
|
||||
for segment in interspersed:
|
||||
chunks.extend(segment.chunks)
|
||||
chars.extend(segment.chars)
|
||||
|
||||
return self.from_chunks(chunks)
|
||||
return self.from_chars(chars)
|
||||
|
||||
def set(self,
|
||||
name: str,
|
||||
|
|
@ -301,8 +175,8 @@ class AttributedText:
|
|||
) -> "AttributedText":
|
||||
|
||||
if start is None and stop is None:
|
||||
chunks = (chunk.set(name, value) for chunk in self._chunks)
|
||||
return AttributedText.from_chunks(chunks)
|
||||
chars = (char.set(name, value) for char in self._chars)
|
||||
return AttributedText.from_chars(chars)
|
||||
elif start is None:
|
||||
return self[:stop].set(name, value) + self[stop:]
|
||||
elif stop is None:
|
||||
|
|
@ -324,8 +198,8 @@ class AttributedText:
|
|||
) -> "AttributedText":
|
||||
|
||||
if start is None and stop is None:
|
||||
chunks = (chunk.remove(name) for chunk in self._chunks)
|
||||
return AttributedText.from_chunks(chunks)
|
||||
chars = (char.remove(name) for char in self._chars)
|
||||
return AttributedText.from_chars(chars)
|
||||
elif start is None:
|
||||
return self[:stop].remove(name) + self[stop:]
|
||||
elif stop is None:
|
||||
|
|
|
|||
BIN
demo.gif
BIN
demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 786 KiB |
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue