retronews

a featureful fork of the luke8086/retronews hn+lobste.rs tui
Log | Files | Refs | README | LICENSE

commit 16bae1cf0a0d9732c99868b90e1e090487e790b4
parent ecd2476398687ac818f90fc33ee0d8f32b27c89f
Author: luke8086 <55237178+luke8086@users.noreply.github.com>
Date:   Sun, 17 Jul 2022 21:06:35 +0000

Add support for browsing entire stories

Diffstat:
Mretronews.py | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 69 insertions(+), 13 deletions(-)

diff --git a/retronews.py b/retronews.py @@ -15,7 +15,7 @@ import urllib.request from dataclasses import dataclass, field from datetime import datetime from textwrap import wrap -from typing import Any, List, Optional, TypedDict, TypeVar, Union +from typing import Any, Generator, List, Optional, TypedDict, TypeVar, Union T = TypeVar("T") @@ -37,6 +37,7 @@ class Colors: self.menu = init_pair(curses.COLOR_GREEN, curses.COLOR_BLUE) self.date = init_pair(curses.COLOR_CYAN, -1) self.author = init_pair(curses.COLOR_YELLOW, -1) + self.tree = init_pair(curses.COLOR_RED, -1) self.cursor = init_pair(curses.COLOR_BLACK, curses.COLOR_CYAN) @@ -48,7 +49,9 @@ class Message: title: str date: datetime lines: List[str] = field(default_factory=list) + children: List["Message"] = field(default_factory=list) index_position: int = 0 + index_tree: str = "" @dataclass @@ -129,13 +132,16 @@ def cmd_open(app: AppState) -> None: return if msg.msg_id == msg.story_id: - app_load_story(app, msg) + app_open_story(app, msg) app.pager_visible = True def cmd_close(app: AppState) -> None: - app.pager_visible = False + if app.pager_visible: + app.pager_visible = False + else: + app_close_story(app) def cmd_unknown(app: AppState) -> None: @@ -154,23 +160,58 @@ KEY_BINDINGS = { } -def app_load_messages(app: AppState, messages: List[Message]) -> None: +def app_load_messages(app: AppState, messages: List[Message], selected_message_id: Optional[str] = None) -> None: + if selected_message_id is None and app.selected_message is not None: + selected_message_id = app.selected_message.msg_id + + selected_message = None + for i, message in enumerate(messages): message.index_position = i - message.title = f"{i} {message.title}" + + if message.msg_id == selected_message_id: + selected_message = message + + if selected_message is None and len(messages) > 0: + selected_message = messages[0] app.messages = messages - app.selected_message = messages[0] if len(messages) > 0 else None + app.selected_message = selected_message -def app_load_story(app: AppState, story_msg: Message) -> None: - source_id = story_msg.story_id.split("@")[0] +def app_close_story(app: AppState) -> None: + selected_story_id = app.selected_message.story_id if app.selected_message else None + filtered_messages = [msg for msg in app.messages if msg.msg_id == msg.story_id] + app_load_messages(app, filtered_messages, selected_message_id=selected_story_id) - app.flash = f"Fetching story '{story_msg.story_id}'..." + +def app_open_story(app: AppState, story_message: Message) -> None: + source_id = story_message.story_id.split("@")[0] + + app.flash = f"Fetching story '{story_message.story_id}'..." app_render(app) - entry = hn_fetch_entry(source_id) - logging.debug(entry) + new_story_message = hn_parse_entry(hn_fetch_entry(source_id)) + + app_close_story(app) + + index_pos = story_message.index_position + story_messages = list(app_flatten_story(new_story_message, prefix="", is_last_child=False, is_top=True)) + messages = app.messages[:index_pos] + story_messages + app.messages[index_pos + 1 :] # noqa: E203 + + app_load_messages(app, messages, selected_message_id=story_message.msg_id) + + +def app_flatten_story(msg: Message, prefix, is_last_child, is_top=False) -> Generator[Message, None, None]: + msg.index_tree = "" if is_top else f"{prefix}{'└─' if is_last_child else '├─'}> " + yield msg + + children_count = len(msg.children) + + for i, child_node in enumerate(msg.children): + tree_prefix = "" if is_top else f"{prefix}{' ' if is_last_child else '│ '}" + for child in app_flatten_story(child_node, tree_prefix, i == children_count - 1): + yield child def app_render_pager(app: AppState, top: int, height: int) -> None: @@ -193,7 +234,8 @@ def app_render_index_row(app: AppState, row: int, message: Message) -> None: app.screen.addstr(row, 0, "[ ] [ ]") app.screen.addstr(row, 1, message.date.strftime("%Y-%m-%d %H:%M"), app.colors.date) app.screen.addstr(row, 21, message.author[:10], app.colors.author) - app.screen.addstr(row, 34, message.title) + app.screen.addstr(row, 34, message.index_tree, app.colors.tree) + app.screen.addstr(row, 34 + len(message.index_tree), message.title) if message == app.selected_message: app.screen.chgat(row, 0, curses.COLS, app.colors.cursor) @@ -270,8 +312,22 @@ def hn_parse_search_hit(hit: HNSearchHit) -> Message: ) +def hn_parse_entry(entry: HNEntry, story_id: str = "", parent_title: str = "") -> Message: + story_id = story_id or str(entry["id"]) + + return Message( + msg_id=f"{entry['id']}@hn", + story_id=f"{story_id}@hn", + author=entry["author"] or "unknown", + title=entry["title"] or f"Re: {parent_title}", + date=datetime.fromtimestamp(entry["created_at_i"]), + lines=wrap(entry["text"] or entry["url"] or "", 80), + children=[hn_parse_entry(child, story_id, entry["title"] or parent_title) for child in entry["children"]], + ) + + def hn_search_stories() -> List[Message]: - hits: List[HNSearchHit] = json.load(open("./tmp/index.json"))["hits"] * 5 + hits: List[HNSearchHit] = json.load(open("./tmp/index.json"))["hits"] return [hn_parse_search_hit(hit) for hit in hits]