kopia lustrzana https://github.com/ihabunek/toot
Implement thread (status context) view
rodzic
5ec8d9411d
commit
1a8c515922
|
@ -161,13 +161,49 @@ class TUI(urwid.Frame):
|
||||||
future.add_done_callback(_done)
|
future.add_done_callback(_done)
|
||||||
|
|
||||||
def build_timeline(self, statuses):
|
def build_timeline(self, statuses):
|
||||||
timeline = Timeline(self, statuses)
|
def _close(*args):
|
||||||
urwid.connect_signal(timeline, "status_focused",
|
raise urwid.ExitMainLoop()
|
||||||
lambda _, args: self.status_focused(*args))
|
|
||||||
urwid.connect_signal(timeline, "next",
|
def _next(*args):
|
||||||
lambda *args: self.async_load_statuses(is_initial=False))
|
self.async_load_statuses(is_initial=False)
|
||||||
|
|
||||||
|
def _focus(timeline):
|
||||||
|
self.refresh_footer(timeline)
|
||||||
|
|
||||||
|
def _thread(timeline, status):
|
||||||
|
self.show_thread(status)
|
||||||
|
|
||||||
|
timeline = Timeline("home", self, statuses)
|
||||||
|
urwid.connect_signal(timeline, "focus", _focus)
|
||||||
|
urwid.connect_signal(timeline, "next", _next)
|
||||||
|
urwid.connect_signal(timeline, "close", _close)
|
||||||
|
urwid.connect_signal(timeline, "thread", _thread)
|
||||||
return timeline
|
return timeline
|
||||||
|
|
||||||
|
def show_thread(self, status):
|
||||||
|
def _close(*args):
|
||||||
|
self.body = self.timeline
|
||||||
|
self.body.refresh_status_details()
|
||||||
|
self.refresh_footer(self.timeline)
|
||||||
|
|
||||||
|
def _focus(timeline):
|
||||||
|
self.refresh_footer(timeline)
|
||||||
|
|
||||||
|
# This is pretty fast, so it's probably ok to block while context is
|
||||||
|
# loaded, can be made async later if needed
|
||||||
|
context = api.context(self.app, self.user, status.id)
|
||||||
|
ancestors = [Status(s, self.app.instance) for s in context["ancestors"]]
|
||||||
|
descendants = [Status(s, self.app.instance) for s in context["descendants"]]
|
||||||
|
focus = len(ancestors)
|
||||||
|
|
||||||
|
statuses = ancestors + [status] + descendants
|
||||||
|
timeline = Timeline("thread", self, statuses, focus, is_thread=True)
|
||||||
|
urwid.connect_signal(timeline, "focus", _focus)
|
||||||
|
urwid.connect_signal(timeline, "close", _close)
|
||||||
|
|
||||||
|
self.body = timeline
|
||||||
|
self.refresh_footer(timeline)
|
||||||
|
|
||||||
def async_load_statuses(self, is_initial):
|
def async_load_statuses(self, is_initial):
|
||||||
"""Asynchronously load a list of statuses."""
|
"""Asynchronously load a list of statuses."""
|
||||||
|
|
||||||
|
@ -185,7 +221,8 @@ class TUI(urwid.Frame):
|
||||||
def _done_initial(statuses):
|
def _done_initial(statuses):
|
||||||
"""Process initial batch of statuses, construct a Timeline."""
|
"""Process initial batch of statuses, construct a Timeline."""
|
||||||
self.timeline = self.build_timeline(statuses)
|
self.timeline = self.build_timeline(statuses)
|
||||||
self.timeline.status_focused() # Draw first status
|
self.timeline.refresh_status_details() # Draw first status
|
||||||
|
self.refresh_footer(self.timeline)
|
||||||
self.body = self.timeline
|
self.body = self.timeline
|
||||||
|
|
||||||
def _done_next(statuses):
|
def _done_next(statuses):
|
||||||
|
@ -196,10 +233,12 @@ class TUI(urwid.Frame):
|
||||||
self.run_in_thread(_load_statuses,
|
self.run_in_thread(_load_statuses,
|
||||||
done_callback=_done_initial if is_initial else _done_next)
|
done_callback=_done_initial if is_initial else _done_next)
|
||||||
|
|
||||||
def status_focused(self, status, index, count):
|
def refresh_footer(self, timeline):
|
||||||
|
"""Show status details in footer."""
|
||||||
|
status, index, count = timeline.get_focused_status_with_counts()
|
||||||
self.footer.set_status([
|
self.footer.set_status([
|
||||||
("footer_status_bold", "[home] "), status.id,
|
("footer_status_bold", "[{}] ".format(timeline.name)),
|
||||||
" - status ", str(index + 1), " of ", str(count),
|
status.id, " - status ", str(index + 1), " of ", str(count),
|
||||||
])
|
])
|
||||||
|
|
||||||
def show_status_source(self, status):
|
def show_status_source(self, status):
|
||||||
|
|
|
@ -4,7 +4,7 @@ import webbrowser
|
||||||
|
|
||||||
from toot.utils import format_content
|
from toot.utils import format_content
|
||||||
|
|
||||||
from .utils import highlight_hashtags, parse_datetime
|
from .utils import highlight_hashtags, parse_datetime, highlight_keys
|
||||||
from .widgets import SelectableText, SelectableColumns
|
from .widgets import SelectableText, SelectableColumns
|
||||||
|
|
||||||
logger = logging.getLogger("toot")
|
logger = logging.getLogger("toot")
|
||||||
|
@ -15,17 +15,19 @@ class Timeline(urwid.Columns):
|
||||||
Displays a list of statuses to the left, and status details on the right.
|
Displays a list of statuses to the left, and status details on the right.
|
||||||
"""
|
"""
|
||||||
signals = [
|
signals = [
|
||||||
"status_focused",
|
"focus",
|
||||||
"status_activated",
|
|
||||||
"next",
|
"next",
|
||||||
|
"close",
|
||||||
|
"thread",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, tui, statuses):
|
def __init__(self, name, tui, statuses, focus=0, is_thread=False):
|
||||||
|
self.name = name
|
||||||
self.tui = tui
|
self.tui = tui
|
||||||
|
self.is_thread = is_thread
|
||||||
self.statuses = statuses
|
self.statuses = statuses
|
||||||
|
self.status_list = self.build_status_list(statuses, focus=focus)
|
||||||
self.status_list = self.build_status_list(statuses)
|
self.status_details = StatusDetails(statuses[focus], is_thread)
|
||||||
self.status_details = StatusDetails(statuses[0])
|
|
||||||
|
|
||||||
super().__init__([
|
super().__init__([
|
||||||
("weight", 40, self.status_list),
|
("weight", 40, self.status_list),
|
||||||
|
@ -33,10 +35,11 @@ class Timeline(urwid.Columns):
|
||||||
("weight", 60, self.status_details),
|
("weight", 60, self.status_details),
|
||||||
], dividechars=1)
|
], dividechars=1)
|
||||||
|
|
||||||
def build_status_list(self, statuses):
|
def build_status_list(self, statuses, focus):
|
||||||
items = [self.build_list_item(status) for status in statuses]
|
items = [self.build_list_item(status) for status in statuses]
|
||||||
walker = urwid.SimpleFocusListWalker(items)
|
walker = urwid.SimpleFocusListWalker(items)
|
||||||
urwid.connect_signal(walker, "modified", self.status_focused)
|
walker.set_focus(focus)
|
||||||
|
urwid.connect_signal(walker, "modified", self.modified)
|
||||||
return urwid.ListBox(walker)
|
return urwid.ListBox(walker)
|
||||||
|
|
||||||
def build_list_item(self, status):
|
def build_list_item(self, status):
|
||||||
|
@ -57,17 +60,27 @@ class Timeline(urwid.Columns):
|
||||||
status = self.get_focused_status()
|
status = self.get_focused_status()
|
||||||
self._emit("status_activated", [status])
|
self._emit("status_activated", [status])
|
||||||
|
|
||||||
def status_focused(self):
|
def get_focused_status_with_counts(self):
|
||||||
|
"""Returns status, status index in list and number of statuses"""
|
||||||
|
return (
|
||||||
|
self.get_focused_status(),
|
||||||
|
self.status_list.body.focus,
|
||||||
|
len(self.statuses),
|
||||||
|
)
|
||||||
|
|
||||||
|
def modified(self):
|
||||||
"""Called when the list focus switches to a new status"""
|
"""Called when the list focus switches to a new status"""
|
||||||
|
status, index, count = self.get_focused_status_with_counts()
|
||||||
|
self.draw_status_details(status)
|
||||||
|
self._emit("focus")
|
||||||
|
|
||||||
|
def refresh_status_details(self):
|
||||||
|
"""Redraws the details of the focused status."""
|
||||||
status = self.get_focused_status()
|
status = self.get_focused_status()
|
||||||
self.draw_status_details(status)
|
self.draw_status_details(status)
|
||||||
|
|
||||||
index = self.status_list.body.focus
|
|
||||||
count = len(self.statuses)
|
|
||||||
self._emit("status_focused", [status, index, count])
|
|
||||||
|
|
||||||
def draw_status_details(self, status):
|
def draw_status_details(self, status):
|
||||||
self.status_details = StatusDetails(status)
|
self.status_details = StatusDetails(status, self.is_thread)
|
||||||
self.contents[2] = self.status_details, ("weight", 50, False)
|
self.contents[2] = self.status_details, ("weight", 50, False)
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
|
@ -94,6 +107,15 @@ class Timeline(urwid.Columns):
|
||||||
self.tui.async_toggle_favourite(status)
|
self.tui.async_toggle_favourite(status)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if key in ('q', 'Q'):
|
||||||
|
self._emit("close")
|
||||||
|
return
|
||||||
|
|
||||||
|
if key in ('t', 'T'):
|
||||||
|
status = self.get_focused_status()
|
||||||
|
self._emit("thread", status)
|
||||||
|
return
|
||||||
|
|
||||||
if key in ("v", "V"):
|
if key in ("v", "V"):
|
||||||
status = self.get_focused_status()
|
status = self.get_focused_status()
|
||||||
if status.data["url"]:
|
if status.data["url"]:
|
||||||
|
@ -142,7 +164,8 @@ class Timeline(urwid.Columns):
|
||||||
|
|
||||||
|
|
||||||
class StatusDetails(urwid.Pile):
|
class StatusDetails(urwid.Pile):
|
||||||
def __init__(self, status):
|
def __init__(self, status, in_thread):
|
||||||
|
self.in_thread = in_thread
|
||||||
widget_list = list(self.content_generator(status))
|
widget_list = list(self.content_generator(status))
|
||||||
return super().__init__(widget_list)
|
return super().__init__(widget_list)
|
||||||
|
|
||||||
|
@ -180,13 +203,11 @@ class StatusDetails(urwid.Pile):
|
||||||
|
|
||||||
# Push things to bottom
|
# Push things to bottom
|
||||||
yield ("weight", 1, urwid.SolidFill(" "))
|
yield ("weight", 1, urwid.SolidFill(" "))
|
||||||
yield ("pack", urwid.Text([
|
|
||||||
("cyan_bold", "B"), ("cyan", "oost"), " | ",
|
options = "[B]oost [F]avourite [V]iew {}So[u]rce [H]elp".format(
|
||||||
("cyan_bold", "F"), ("cyan", "avourite"), " | ",
|
"[T]hread " if not self.in_thread else "")
|
||||||
("cyan_bold", "V"), ("cyan", "iew"), " | ",
|
options = highlight_keys(options, "cyan_bold", "cyan")
|
||||||
("cyan", "So"), ("cyan_bold", "u"), ("cyan", "rce"), " | ",
|
yield ("pack", urwid.Text(options))
|
||||||
("cyan_bold", "H"), ("cyan", "elp"), " ",
|
|
||||||
]))
|
|
||||||
|
|
||||||
def build_linebox(self, contents):
|
def build_linebox(self, contents):
|
||||||
contents = urwid.Pile(list(contents))
|
contents = urwid.Pile(list(contents))
|
||||||
|
|
|
@ -5,13 +5,37 @@ from datetime import datetime
|
||||||
HASHTAG_PATTERN = re.compile(r'(?<!\w)(#\w+)\b')
|
HASHTAG_PATTERN = re.compile(r'(?<!\w)(#\w+)\b')
|
||||||
|
|
||||||
|
|
||||||
def highlight_hashtags(line):
|
|
||||||
return [
|
|
||||||
("hashtag", p) if p.startswith("#") else p
|
|
||||||
for p in re.split(HASHTAG_PATTERN, line)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_datetime(value):
|
def parse_datetime(value):
|
||||||
"""Returns an aware datetime in local timezone"""
|
"""Returns an aware datetime in local timezone"""
|
||||||
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z").astimezone()
|
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z").astimezone()
|
||||||
|
|
||||||
|
|
||||||
|
def highlight_keys(text, high_attr, low_attr=""):
|
||||||
|
"""
|
||||||
|
Takes a string and adds high_attr attribute to parts in square brackets,
|
||||||
|
and optionally low_attr attribute to parts outside square brackets.
|
||||||
|
|
||||||
|
The result can be rendered using a urwid.Text widget.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
>>> highlight_keys("[P]rint [V]iew", "blue")
|
||||||
|
>>> [('blue', 'P'), 'rint ', ('blue', 'V'), 'iew']
|
||||||
|
"""
|
||||||
|
def _gen():
|
||||||
|
highlighted = False
|
||||||
|
for part in re.split("\\[|\\]", text):
|
||||||
|
if part:
|
||||||
|
if highlighted:
|
||||||
|
yield (high_attr, part) if high_attr else part
|
||||||
|
else:
|
||||||
|
yield (low_attr, part) if low_attr else part
|
||||||
|
highlighted = not highlighted
|
||||||
|
return list(_gen())
|
||||||
|
|
||||||
|
|
||||||
|
def highlight_hashtags(line, attr="hashtag"):
|
||||||
|
return [
|
||||||
|
(attr, p) if p.startswith("#") else p
|
||||||
|
for p in re.split(HASHTAG_PATTERN, line)
|
||||||
|
]
|
||||||
|
|
Ładowanie…
Reference in New Issue