From 2e56b1b925f2f516a561931dff5a789a7f83575f Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 10 May 2019 11:13:55 +0000 Subject: [PATCH] Satisfy mypy and (re-)move files --- cheuph/__init__.py | 8 - cheuph/exceptions.py | 4 + cheuph/markup.py | 285 ---------------------- cheuph/message.py | 37 --- cheuph/plan.txt | 10 + cheuph/render/__init__.py | 4 +- cheuph/render/tree_display.py | 28 ++- cheuph/render/tree_list.py | 24 +- cheuph/test.py | 33 +++ cheuph/tree_display.py => tree_display.py | 127 +++++++++- 10 files changed, 209 insertions(+), 351 deletions(-) delete mode 100644 cheuph/__init__.py create mode 100644 cheuph/exceptions.py delete mode 100644 cheuph/markup.py delete mode 100644 cheuph/message.py create mode 100644 cheuph/plan.txt create mode 100644 cheuph/test.py rename cheuph/tree_display.py => tree_display.py (58%) diff --git a/cheuph/__init__.py b/cheuph/__init__.py deleted file mode 100644 index 992767a..0000000 --- a/cheuph/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import List - -from .markup import * -from .message import * - -__all__: List[str] = [] -__all__ += markup.__all__ -__all__ += message.__all__ diff --git a/cheuph/exceptions.py b/cheuph/exceptions.py new file mode 100644 index 0000000..31a0c9e --- /dev/null +++ b/cheuph/exceptions.py @@ -0,0 +1,4 @@ +__all__ = ["RenderException"] + +class RenderException(Exception): + pass diff --git a/cheuph/markup.py b/cheuph/markup.py deleted file mode 100644 index 8348633..0000000 --- a/cheuph/markup.py +++ /dev/null @@ -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:] diff --git a/cheuph/message.py b/cheuph/message.py deleted file mode 100644 index 2e0e06b..0000000 --- a/cheuph/message.py +++ /dev/null @@ -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) diff --git a/cheuph/plan.txt b/cheuph/plan.txt new file mode 100644 index 0000000..398d341 --- /dev/null +++ b/cheuph/plan.txt @@ -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 diff --git a/cheuph/render/__init__.py b/cheuph/render/__init__.py index 440d10c..43361d6 100644 --- a/cheuph/render/__init__.py +++ b/cheuph/render/__init__.py @@ -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__ diff --git a/cheuph/render/tree_display.py b/cheuph/render/tree_display.py index 3aa1da8..304ef56 100644 --- a/cheuph/render/tree_display.py +++ b/cheuph/render/tree_display.py @@ -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: diff --git a/cheuph/render/tree_list.py b/cheuph/render/tree_list.py index 367fdf1..8619572 100644 --- a/cheuph/render/tree_list.py +++ b/cheuph/render/tree_list.py @@ -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 diff --git a/cheuph/test.py b/cheuph/test.py new file mode 100644 index 0000000..b899dba --- /dev/null +++ b/cheuph/test.py @@ -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) diff --git a/cheuph/tree_display.py b/tree_display.py similarity index 58% rename from cheuph/tree_display.py rename to tree_display.py index c69686f..2d9a4af 100644 --- a/cheuph/tree_display.py +++ b/tree_display.py @@ -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: + + + | ... + | ... (above) + | ... + | + | | ... + | | | ... | + | ... + | ... (below) + | ... + + or + + + | ... + | ... (above) + | ... + | + | ... + | ... (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): #