retronews

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

commit c3b01d461b5d38996034be2218a6004b3fe2f82e
parent 1943a84c3b88c0b65124c828149f16018a6bb61e
Author: luke8086 <55237178+luke8086@users.noreply.github.com>
Date:   Tue, 12 Jul 2022 20:22:47 +0000

Initial message index view with cursor navigation

Diffstat:
Mretronews.py | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 99 insertions(+), 5 deletions(-)

diff --git a/retronews.py b/retronews.py @@ -8,45 +8,139 @@ # import curses +import json import sys -from dataclasses import dataclass +from dataclasses import dataclass, field +from datetime import datetime +from typing import List, Optional, TypedDict, TypeVar + +T = TypeVar("T") + + +@dataclass +class Message: + msg_id: str + author: str + title: str + date: datetime + index_position: int = 0 @dataclass class AppState: screen: "curses._CursesWindow" + index_window: "curses._CursesWindow" + messages: List[Message] = field(default_factory=list) + selected_message: Optional[Message] = None + + +class HNSearchHit(TypedDict): + objectID: int + author: str + title: str + created_at_i: int + + +def list_get(lst: List[T], index: int, default: Optional[T]) -> Optional[T]: + return lst[index] if 0 <= index < len(lst) else default def cmd_quit(_: AppState): sys.exit(0) +def cmd_up(app: AppState) -> None: + pos = app.selected_message.index_position - 1 if app.selected_message else 0 + app.selected_message = list_get(app.messages, pos, app.selected_message) + + +def cmd_down(app: AppState) -> None: + pos = app.selected_message.index_position + 1 if app.selected_message else 0 + app.selected_message = list_get(app.messages, pos, app.selected_message) + + def cmd_unknown(_: AppState) -> None: pass KEY_BINDINGS = { ord("q"): cmd_quit, + curses.KEY_UP: cmd_up, + curses.KEY_DOWN: cmd_down, } +def app_load_messages(app: AppState, messages: List[Message]) -> None: + for i, message in enumerate(messages): + message.index_position = i + message.title = f"{i} {message.title}" + + app.messages = messages + app.selected_message = messages[0] if len(messages) > 0 else None + + +def app_render_index_row(app: AppState, row: int, message: Message) -> None: + cursor = "->" if message == app.selected_message else " " + app.index_window.addstr(row, 0, f"{cursor} [{message.date}] [{message.author[:10]:10}] {message.title}") + + +def app_render_index(app: AppState) -> None: + height = app.index_window.getmaxyx()[0] + + offset = app.selected_message.index_position - height // 2 if app.selected_message else 0 + offset = min(offset, len(app.messages) - height) + offset = max(offset, 0) + + rows_to_render = min(height, len(app.messages) - offset) + + for i in range(rows_to_render): + app_render_index_row(app, i, app.messages[i + offset]) + + +def app_render(app: AppState) -> None: + app.screen.erase() + + app_render_index(app) + + app.screen.refresh() + + +def app_adjust_size(app: AppState) -> None: + app.index_window.mvwin(1, 0) + app.index_window.resize((curses.LINES - 2) // 3, curses.COLS) + + def app_init(screen: "curses._CursesWindow") -> AppState: - app = AppState(screen=screen) curses.curs_set(0) + + app = AppState(screen=screen, index_window=screen.subwin(1, 0)) + app_adjust_size(app) + app_load_messages(app, hn_search_stories()) + return app -def app_render(st: AppState) -> None: - st.screen.refresh() +def hn_parse_search_hit(hit: HNSearchHit) -> Message: + return Message( + msg_id=f"{hit['objectID']}@hn", + author=hit["author"], + title=hit["title"], + date=datetime.fromtimestamp(hit["created_at_i"]), + ) + + +def hn_search_stories() -> List[Message]: + hits: List[HNSearchHit] = json.load(open("./tmp/index.json"))["hits"] + return [hn_parse_search_hit(hit) for hit in hits] def main(screen: "curses._CursesWindow") -> None: app = app_init(screen) while True: + app_render(app) c = app.screen.getch() KEY_BINDINGS.get(c, cmd_unknown)(app) - app_render(app) if __name__ == "__main__":