Toot-Mastodon-CLI-TUI-clien.../toot/tui/app.py

200 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)