Satisfy mypy and (re-)move files

This commit is contained in:
Joscha 2019-05-10 11:13:55 +00:00
parent ef5320058c
commit 2e56b1b925
10 changed files with 209 additions and 351 deletions

View file

@ -1,8 +0,0 @@
from typing import List
from .markup import *
from .message import *
__all__: List[str] = []
__all__ += markup.__all__
__all__ += message.__all__

4
cheuph/exceptions.py Normal file
View file

@ -0,0 +1,4 @@
__all__ = ["RenderException"]
class RenderException(Exception):
pass

View file

@ -1,285 +0,0 @@
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
__all__ = ["Attributes", "Chunk", "AttributedText"]
Attributes = Dict[str, Any]
class Chunk:
@staticmethod
def join_chunks(chunks: List["Chunk"]) -> List["Chunk"]:
if not chunks:
return []
new_chunks: List[Chunk] = []
current_chunk = chunks[0]
for chunk in chunks[1:]:
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 __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)
# 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)
return Chunk(self.text, new_attributes)
class AttributedText:
"""
Objects of this class are immutable and behave str-like. Supported
operations are len, + and splicing.
"""
@classmethod
def from_chunks(cls, chunks: Iterable[Chunk]) -> "AttributedText":
new = cls()
new._chunks = Chunk.join_chunks(list(chunks))
return new
# Common special methods
def __init__(self, text: Optional[str] = None) -> None:
self._chunks: List[Chunk] = []
if text is not None:
self._chunks.append(Chunk(text))
def __str__(self) -> str:
return self.text
def __repr__(self) -> str:
return f"AttributedText.from_chunks({self._chunks!r})"
# Uncommon special methods
def __add__(self, other: "AttributedText") -> "AttributedText":
return AttributedText.from_chunks(self._chunks + other._chunks)
def __getitem__(self, key: Union[int, slice]) -> "AttributedText":
chunks: List[Chunk]
if isinstance(key, slice):
chunks = Chunk.join_chunks(self._slice(key))
else:
chunks = [self._at(key)]
return AttributedText.from_chunks(chunks)
def __len__(self) -> int:
return sum(map(len, self._chunks))
# Properties
@property
def text(self) -> str:
return "".join(chunk.text for chunk in self._chunks)
@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
# Public methods
def at(self, pos: int) -> Attributes:
return self._at(pos).attributes
def get(self,
pos: int,
name: str,
default: Any = None,
) -> Any:
return self._at(pos).get(name, default)
# "find all separate blocks with this property"
def find_all(self, name: str) -> List[Tuple[Any, int, int]]:
blocks = []
pos = 0
block = None
for chunk in self._chunks:
if name in chunk.attributes:
attribute = chunk.attributes[name]
start = pos
stop = pos + len(chunk)
if block is None:
block = (attribute, start, stop)
continue
block_attr, block_start, _ = block
if block_attr == attribute:
block = (attribute, block_start, stop)
else:
blocks.append(block)
block = (attribute, start, stop)
pos += len(chunk)
if block is not None:
blocks.append(block)
return blocks
def set(self,
name: str,
value: Any,
start: Optional[int] = None,
stop: Optional[int] = None,
) -> "AttributedText":
if start is None and stop is None:
chunks = (chunk.set(name, value) for chunk in self._chunks)
return AttributedText.from_chunks(chunks)
elif start is None:
return self[:stop].set(name, value) + self[stop:]
elif stop is None:
return self[:start] + self[start:].set(name, value)
elif start > stop:
# set value everywhere BUT the specified interval
return self.set(name, value, stop=stop).set(name, value, start=start)
else:
middle = self[start:stop].set(name, value)
return self[:start] + middle + self[stop:]
def set_at(self, name: str, value: Any, pos: int) -> "AttributedText":
return self.set(name, value, pos, pos)
def remove(self,
name: str,
start: Optional[int] = None,
stop: Optional[int] = None,
) -> "AttributedText":
if start is None and stop is None:
chunks = (chunk.remove(name) for chunk in self._chunks)
return AttributedText.from_chunks(chunks)
elif start is None:
return self[:stop].remove(name) + self[stop:]
elif stop is None:
return self[:start] + self[start:].remove(name)
elif start > stop:
# remove value everywhere BUT the specified interval
return self.remove(name, stop=stop).remove(name, start=start)
else:
middle = self[start:stop].remove(name)
return self[:start] + middle + self[stop:]

View file

@ -1,37 +0,0 @@
from typing import Hashable, Optional
from .markup import AttributedText
__all__ = ["Message"]
class Message:
def __init__(self,
message_id: Hashable,
parent_id: Optional[Hashable],
author: str,
content: str,
) -> None:
self._message_id = message_id
self._parent_id = parent_id
self._author = author
self._content = content
@property
def message_id(self) -> Hashable:
return self._message_id
@property
def parent_id(self) -> Optional[Hashable]:
return self._parent_id
@property
def author(self) -> str:
return self._author
@property
def content(self) -> str:
return self._content
def render_content(self) -> AttributedText:
return AttributedText(self.content)

10
cheuph/plan.txt Normal file
View file

@ -0,0 +1,10 @@
General/generic features:
Text with attributes attached to the characters
Can be...
- split
- joined
- converted to raw text or other formats
The attributes of certain characters can be read

View file

@ -1,8 +1,10 @@
from typing import List
from .element import *
from .markup import *
from .tree_display import *
__all__ = []
__all__: List[str] = []
__all__ += element.__all__
__all__ += markup.__all__
__all__ += tree_display.__all__

View file

@ -1,7 +1,8 @@
import collections
from typing import Any, Deque, Optional, Set
from typing import Any, List, Optional, Set
from .element import ElementSupply, Id, RenderedElement
from .element import Element, ElementSupply, Id, RenderedElement
from .tree_list import TreeList
__all__ = ["TreeDisplay"]
@ -23,15 +24,15 @@ class TreeDisplay:
# Object references
self._supply = supply
self._rendered: Optional[TreeList]
self._rendered: Optional[TreeList] = None
self._folded: Set[Id] = set()
def resize(self, width: int, height: int):
def resize(self, width: int, height: int) -> None:
# TODO maybe empty _rendered/invalidate caches etc.?
self._width = width
self._height = height
def render(self):
def render(self) -> None:
# Steps:
#
# 1. Find and render anchor's branch to TreeList
@ -67,7 +68,10 @@ class TreeDisplay:
self._fill_screen_upwards()
self._fill_screen_downwards()
def _render_tree(self, tree: Element, depth=0):
def _render_tree(self,
tree: Element,
depth: int = 0
) -> List[RenderedElement]:
elements: List[RenderedElement] = []
highlighted = tree.id == self._cursor_id
@ -82,7 +86,10 @@ class TreeDisplay:
return elements
def _fill_screen_upwards(self):
def _fill_screen_upwards(self) -> None:
if self._rendered is None:
return # TODO
while True:
if self._rendered.upper_offset <= 0:
break
@ -96,11 +103,14 @@ class TreeDisplay:
above_tree = self._supply.get_tree(above_tree_id)
self._rendered.add_above(self._render_tree(above_tree))
def _fill_screen_downwards(self):
def _fill_screen_downwards(self) -> None:
"""
Eerily similar to _fill_screen_upwards()...
"""
if self._rendered is None:
return # TODO
while True:
if self._rendered.lower_offset >= self._height - 1:
break
@ -114,7 +124,7 @@ class TreeDisplay:
below_tree = self._supply.get_tree(below_tree_id)
self._rendered.add_below(self._render_tree(below_tree))
def draw_to(self, window: Any):
def draw_to(self, window: Any) -> None:
pass
# Terminology:

View file

@ -1,3 +1,6 @@
import collections
from typing import Deque, List
from .element import Id, RenderedElement
__all__ = ["TreeList"]
@ -7,9 +10,7 @@ class TreeList:
tree: List[RenderedElement],
anchor_id: Id,
) -> None:
self._deque = collections.deque()
self._anchor_id = anchor_id
self._deque: Deque = collections.deque()
# The offsets can be thought of as the index of a line relative to the
# anchor's first line.
@ -19,8 +20,8 @@ class TreeList:
#
# The lower offset is the index of the lowermost message's LAST line.
# lower_offset >= 0.
self._upper_offset: Int
self._lower_offset: Int
self._upper_offset: int
self._lower_offset: int
# The upper and lower tree ids are the ids of the uppermost or
# lowermost tree added to the TreeList. They can be used to request the
@ -28,14 +29,14 @@ class TreeList:
self._upper_tree_id: Id
self._lower_tree_id: Id
self._add_first_tree(tree)
self._add_first_tree(tree, anchor_id)
@property
def upper_offset(self) -> Int:
def upper_offset(self) -> int:
return self._upper_offset
@property
def lower_offset(self) -> Int:
def lower_offset(self) -> int:
return self._lower_offset
@property
@ -46,7 +47,10 @@ class TreeList:
def lower_tree_id(self) -> Id:
return self._lower_tree_id
def _add_first_tree(self, tree: List[RenderedElement]) -> None:
def _add_first_tree(self,
tree: List[RenderedElement],
anchor_id: Id
) -> None:
if len(tree) == 0:
raise ValueError("The tree must contain at least one element")
@ -57,7 +61,7 @@ class TreeList:
offset = 0
found_anchor = False
for rendered in elements:
for rendered in tree:
if rendered.element.id == anchor_id:
found_anchor = True
self._upper_offset = -offset

33
cheuph/test.py Normal file
View file

@ -0,0 +1,33 @@
import curses
import subprocess
import tempfile
from typing import Any
def main(stdscr: Any) -> None:
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)

View file

@ -1,3 +1,15 @@
# 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
@ -5,7 +17,120 @@ class ElementSupply:
pass
class TreeDisplay:
pass
"""
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):
#