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):
#