199 lines
5.8 KiB
Python
199 lines
5.8 KiB
Python
import json
|
|
import logging
|
|
import urwid
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from toot import api
|
|
|
|
from .constants import PALETTE
|
|
from .entities import Status
|
|
from .timeline import Timeline
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Header(urwid.WidgetWrap):
|
|
def __init__(self, app, user):
|
|
self.app = app
|
|
self.user = user
|
|
|
|
self.text = urwid.Text("")
|
|
self.cols = urwid.Columns([
|
|
("pack", urwid.Text(('header_bold', 'toot'))),
|
|
("pack", urwid.Text(('header', f' | {user.username}@{app.instance}'))),
|
|
("pack", self.text),
|
|
])
|
|
|
|
widget = urwid.AttrMap(self.cols, 'header')
|
|
widget = urwid.Padding(widget)
|
|
self._wrapped_widget = widget
|
|
|
|
def clear_text(self, text):
|
|
self.text.set_text("")
|
|
|
|
def set_text(self, text):
|
|
self.text.set_text(" | " + text)
|
|
|
|
|
|
class Footer(urwid.Pile):
|
|
def __init__(self):
|
|
self.status = urwid.Text("")
|
|
self.message = urwid.Text("")
|
|
|
|
return super().__init__([
|
|
urwid.AttrMap(self.status, "footer_status"),
|
|
urwid.AttrMap(self.message, "footer_message"),
|
|
])
|
|
|
|
def set_status(self, text):
|
|
self.status.set_text(text)
|
|
|
|
def clear_status(self, text):
|
|
self.status.set_text("")
|
|
|
|
def set_message(self, text):
|
|
self.message.set_text(text)
|
|
|
|
def clear_message(self):
|
|
self.message.set_text("")
|
|
|
|
|
|
class TUI(urwid.Frame):
|
|
"""Main TUI frame."""
|
|
|
|
@classmethod
|
|
def create(cls, app, user):
|
|
"""Factory method, sets up TUI and an event loop."""
|
|
|
|
tui = cls(app, user)
|
|
loop = urwid.MainLoop(
|
|
tui,
|
|
palette=PALETTE,
|
|
event_loop=urwid.AsyncioEventLoop(),
|
|
unhandled_input=tui.unhandled_input,
|
|
)
|
|
tui.loop = loop
|
|
|
|
return tui
|
|
|
|
def __init__(self, app, user):
|
|
self.app = app
|
|
self.user = user
|
|
|
|
self.loop = None # set in `create`
|
|
self.executor = ThreadPoolExecutor(max_workers=1)
|
|
self.timeline_generator = api.home_timeline_generator(app, user, limit=40)
|
|
# self.timeline_generator = api.public_timeline_generator(app.instance, local=False, limit=40)
|
|
|
|
self.body = urwid.Filler(urwid.Text("Loading toots...", align="center"))
|
|
self.header = Header(app, user)
|
|
self.footer = Footer()
|
|
self.footer.set_status("Loading...")
|
|
|
|
self.timeline = None
|
|
self.overlay = None
|
|
|
|
super().__init__(self.body, header=self.header, footer=self.footer)
|
|
|
|
def run(self):
|
|
self.loop.set_alarm_in(0, self.schedule_load_statuses)
|
|
self.loop.run()
|
|
self.executor.shutdown(wait=False)
|
|
|
|
def run_in_thread(self, fn, args=[], kwargs={}, done_callback=None):
|
|
future = self.executor.submit(fn)
|
|
if done_callback:
|
|
future.add_done_callback(done_callback)
|
|
|
|
def schedule_load_statuses(self, *args):
|
|
self.run_in_thread(self.load_statuses, done_callback=self.statuses_loaded_initial)
|
|
|
|
def load_statuses(self):
|
|
self.footer.set_message("Loading statuses...")
|
|
try:
|
|
data = next(self.timeline_generator)
|
|
except StopIteration:
|
|
return []
|
|
finally:
|
|
self.footer.clear_message()
|
|
|
|
# # FIXME: REMOVE DEBUGGING
|
|
# with open("tmp/statuses2.json", "w") as f:
|
|
# import json
|
|
# json.dump(data, f, indent=4)
|
|
|
|
return [Status(s, self.app.instance) for s in data]
|
|
|
|
def schedule_load_next(self):
|
|
self.run_in_thread(self.load_statuses, done_callback=self.statuses_loaded_next)
|
|
|
|
def statuses_loaded_initial(self, future):
|
|
# TODO: handle errors in future
|
|
self.timeline = Timeline(self, future.result())
|
|
|
|
urwid.connect_signal(self.timeline, "status_focused",
|
|
lambda _, args: self.status_focused(*args))
|
|
urwid.connect_signal(self.timeline, "next",
|
|
lambda *args: self.schedule_load_next())
|
|
self.timeline.status_focused() # Draw first status
|
|
self.body = self.timeline
|
|
|
|
def statuses_loaded_next(self, future):
|
|
# TODO: handle errors in future
|
|
self.timeline.add_statuses(future.result())
|
|
|
|
def status_focused(self, status, index, count):
|
|
self.footer.set_status([
|
|
("footer_status_bold", "[home] "), status.id,
|
|
" - status ", str(index + 1), " of ", str(count),
|
|
])
|
|
|
|
def show_status_source(self, status):
|
|
self.open_overlay(
|
|
widget=StatusSource(status),
|
|
title="Status source",
|
|
options={
|
|
"align": 'center',
|
|
"width": ('relative', 80),
|
|
"valign": 'middle',
|
|
"height": ('relative', 80),
|
|
},
|
|
)
|
|
|
|
# --- Overlay handling -----------------------------------------------------
|
|
|
|
def open_overlay(self, widget, options={}, title=""):
|
|
top_widget = urwid.LineBox(widget, title=title)
|
|
bottom_widget = self.body
|
|
|
|
self.overlay = urwid.Overlay(
|
|
top_widget,
|
|
bottom_widget,
|
|
**options
|
|
)
|
|
self.body = self.overlay
|
|
|
|
def close_overlay(self):
|
|
self.body = self.overlay.bottom_w
|
|
self.overlay = None
|
|
|
|
# --- Keys -----------------------------------------------------------------
|
|
|
|
def unhandled_input(self, key):
|
|
if key in ('q', 'Q'):
|
|
if self.overlay:
|
|
self.close_overlay()
|
|
else:
|
|
raise urwid.ExitMainLoop()
|
|
|
|
|
|
class StatusSource(urwid.ListBox):
|
|
"""Shows status data, as returned by the server, as formatted JSON."""
|
|
def __init__(self, status):
|
|
source = json.dumps(status.data, indent=4)
|
|
lines = source.splitlines()
|
|
walker = urwid.SimpleFocusListWalker([
|
|
urwid.Text(line) for line in lines
|
|
])
|
|
super().__init__(walker)
|