From afe531fdaab48a67774e11437ea70c0dd8fbaaf0 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 9 Dec 2023 15:44:00 +0000 Subject: [PATCH] Feature to print detailed info about a status. Now I can retrieve its URL to send to people, and find out all the fiddly internals too. --- client.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++- cursesclient.py | 29 ++++++++++++++++++++ text.py | 25 +++++++++++++----- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/client.py b/client.py index e366fb9..eaecbd9 100644 --- a/client.py +++ b/client.py @@ -279,6 +279,22 @@ class ThreadFeed(Feed): def __getitem__(self, n): return self.data[n] +class StatusInfoFeed(Feed): + def __init__(self, client, status): + self.client = client + self.status = status + self.data = [Status(status, client, full_info=True)] + + def start(self): + pass + + def min_index(self): + return 0 + def max_index(self): + return len(self.data) + def __getitem__(self, n): + return self.data[n] + class EgoFeed(IncrementalServerFeed): def __init__(self, client): super().__init__(client, "notifications", { @@ -291,8 +307,14 @@ def parse_creation_time(created_at): tm = time.strptime(date, "%Y-%m-%dT%H:%M:%S") return calendar.timegm(tm) +def noneify(s, include_empty=False, colour=' '): + if include_empty and s is not None and len(s) == 0: + s = None # empty string counts as None + return (text.ColouredString("none", '0') if s is None + else text.ColouredString(s, colour)) + class Status: - def __init__(self, data, client): + def __init__(self, data, client, full_info=False): rb = data.get('reblog') if rb is not None: self.booster = data['account'] @@ -302,6 +324,7 @@ class Status: client.cache_status(data) + self.data = data self.post_id = data['id'] self.datestamp = parse_creation_time(data['created_at']) @@ -323,6 +346,8 @@ class Status: self.update_fave_boost(data) + self.full_info = full_info + def update_fave_boost(self, data): self.favourited = data.get('favourited', False) self.boosted = data.get('boosted', False) @@ -353,6 +378,49 @@ class Status: for media in self.media: yield text.Media(media['url'], media.get('description')) + if self.full_info: + yield text.SeparatorLine() + yield text.BlankLine() + yield text.Paragraph(f"Post id: " + self.post_id) + url = self.data['url'] + yield text.Paragraph("On the web: " + + text.ColouredString(url, 'u')) + yield text.BlankLine() + + created = noneify(self.data['created_at']) + yield text.Paragraph(f"Creation time: " + created) + edited = noneify(self.data['edited_at']) + yield text.Paragraph(f"Last edit time: " + edited) + reply_id = noneify(self.data.get('in_reply_to_id')) + yield text.Paragraph("Reply to post: " + reply_id) + reply_acct = noneify(self.data.get('in_reply_to_account_id')) + yield text.Paragraph("Reply to account: " + reply_acct) + yield text.BlankLine() + + lang = noneify(self.data['language']) + yield text.Paragraph("Language: " + lang) + vis = self.data['visibility'] + yield text.Paragraph("Visibility: " + vis) + sensitive = "yes" if self.data['sensitive'] else "no" + yield text.Paragraph("Sensitive: " + sensitive) + spoiler = noneify(self.data['spoiler_text'], include_empty=True) + yield text.Paragraph("Spoiler text: " + spoiler) + yield text.BlankLine() + + replies = str(self.data['replies_count']) + yield text.Paragraph("Replies: " + replies) + boosts = str(self.data['reblogs_count']) + yield text.Paragraph("Boosts: " + boosts) + faves = str(self.data['favourites_count']) + yield text.Paragraph("Favourites: " + faves) + yield text.BlankLine() + + app_subdict = self.data.get('application', {}) + client = noneify(app_subdict.get('name')) + yield text.Paragraph("Client name: " + client) + client_url = noneify(app_subdict.get('website'), colour='u') + yield text.Paragraph("Client website: " + client_url) + def get_reply_recipients(self): yield self.client.fq(self.account['acct']) for mention in self.mentions: diff --git a/cursesclient.py b/cursesclient.py index d068ed9..64b6cf1 100644 --- a/cursesclient.py +++ b/cursesclient.py @@ -456,6 +456,8 @@ class File(Activity): self.boost_mode() elif ch in {'t', 'T'}: self.thread_mode() + elif ch in {'i', 'I'}: + self.info_mode() elif self.mode == 'select': if ch in {'q', 'Q'}: self.mode = 'normal' @@ -484,6 +486,9 @@ class File(Activity): elif (self.select_type == 'thread' and ch in {'f', 'F'}): self.thread_complete(True) + elif (self.select_type == 'info' and + ch in {' '}): + self.info_complete(False) def unprime(self): pass # not supported @@ -616,6 +621,8 @@ class ObjectFile(File): elif self.select_type == 'thread': sl.keys.append(('SPACE', 'Show Thread Context')) sl.keys.append(('F', 'Show Full Thread')) + elif self.select_type == 'info': + sl.keys.append(('SPACE', 'Show Post Info')) sl.keys.append(('-', None)) sl.keys.append(('+', None)) sl.keys.append(('Q', 'Quit')) @@ -629,6 +636,8 @@ class ObjectFile(File): if self.items_are_statuses: sl.keys.append(('F', 'Fave')) sl.keys.append(('^B', 'Boost')) + # FIXME: for when we can auto-shink bottom line + # sl.keys.append(('I', 'Info')) sl.keys.append(('Q', 'Exit')) sl.proportion = self.linepos / len(self.lines) sl_rendered = util.exactly_one(sl.render(self.cc.scr_w)) @@ -654,6 +663,11 @@ class ObjectFile(File): self.mode = 'select' self.select_type = 'thread' self.select_target = self.index_by_line[self.linepos-1] + def info_mode(self): + if self.items_are_statuses: + self.mode = 'select' + self.select_type = 'info' + self.select_target = self.index_by_line[self.linepos-1] def prev_select_target(self): self.select_target = max(self.minpos, self.select_target-1) def next_select_target(self): @@ -721,6 +735,12 @@ class ObjectFile(File): self.push_to(StatusFile( self.cc, feed, text.ColouredString(title, "H"))) + def info_complete(self, full): + self.mode = 'normal' + target = self.statuses[self.select_target] + reply_id = target.get_reply_id() + self.push_to(StatusInfoFile(self.cc, reply_id)) + def move_to(self, pos): old_linepos = self.linepos self.linepos = pos @@ -762,6 +782,15 @@ class NotificationsFile(ObjectFile): def __init__(self, cc, feed, title): super().__init__(cc, client.Notification, feed, title) +class StatusInfoFile(ObjectFile): + items_are_statuses = True + items_have_authors = True + def __init__(self, cc, postid): + title = text.ColouredString(f"Information about post {postid}", 'H') + self.data = cc.get_status_by_id(postid) + super().__init__( + cc, lambda x,cc:x, client.StatusInfoFeed(cc, self.data), title) + class Composer(Activity): class DisplayText: def __init__(self, text): diff --git a/text.py b/text.py index de68152..36af714 100644 --- a/text.py +++ b/text.py @@ -33,6 +33,7 @@ colourmap = { 'K': [0, 1, 36], # keypress / keypath names in file headers 'k': [0, 1], # keypresses in file status lines '-': [0, 7, 40, 36], # separator line between editor header and content + '0': [0, 34], # something really boring, like 'none' in place of data } wcswidth_cache = {} @@ -56,6 +57,10 @@ class ColouredString: rhs = type(self)(rhs) return type(self)(self.s + rhs.s, self.c + rhs.c) + def __radd__(self, lhs): + lhs = type(self)(lhs) + return type(self)(lhs.s + self.s, lhs.c + self.c) + def __mul__(self, rhs): return type(self)(self.s * rhs, self.c * rhs) @@ -121,17 +126,19 @@ class BlankLine: yield ColouredString("") class SeparatorLine: - def __init__(self, timestamp, favourited=False, boosted=False): + def __init__(self, timestamp=None, favourited=False, boosted=False): self.timestamp = timestamp self.favourited = favourited self.boosted = boosted def render(self, width): - date = time.strftime("%a %b %e %H:%M:%S %Y", - time.localtime(self.timestamp)) - suffix = (ColouredString("[", 'S') + - ColouredString(date, 'D') + - ColouredString("]--", 'S')) + suffix = ColouredString("") + if self.timestamp is not None: + date = time.strftime("%a %b %e %H:%M:%S %Y", + time.localtime(self.timestamp)) + suffix = (ColouredString("[", 'S') + + ColouredString(date, 'D') + + ColouredString("]--", 'S')) + suffix if self.boosted: suffix = (ColouredString("[", 'S') + ColouredString('B', 'D') + @@ -311,11 +318,15 @@ class MenuKeypressLine: self.description) class Paragraph: - def __init__(self): + def __init__(self, text=None): self.words = [] self.space_colours = [] self.unfinished_word = ColouredString('') + if text is not None: + self.add(text) + self.end_word() + def render(self, width, laterwidth=None): if laterwidth is None: laterwidth = width -- 2.30.2