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,170 +0,0 @@
class Element:
pass
class ElementSupply:
pass
class TreeDisplay:
pass
# 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
#
# ???