chiark / gitweb /
Now we spawn an editor with the right set of recipients.
authorSimon Tatham <anakin@pobox.com>
Thu, 7 Dec 2023 18:44:20 +0000 (18:44 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 7 Dec 2023 18:44:20 +0000 (18:44 +0000)
Still can't _do_ anything with it, though.

client.py
cursesclient.py
text.py

index 38bf8dc6e3690dc0f3aeec98edfe27c85fa415f0..ebdf714531928932fb8c67fa1b3324bae9e7e1d3 100644 (file)
--- a/client.py
+++ b/client.py
@@ -273,6 +273,8 @@ class Status:
 
         self.reply_id = data.get('in_reply_to_id')
 
+        self.mentions = data.get('mentions', [])
+
         self.client = client
 
     def text(self):
@@ -298,6 +300,11 @@ class Status:
         for media in self.media:
             yield text.Media(media['url'], media.get('description'))
 
+    def get_reply_recipients(self):
+        yield self.client.fq(self.account['acct'])
+        for mention in self.mentions:
+            yield self.client.fq(mention['acct'])
+
 class Notification:
     def __init__(self, data, client):
         self.ntype = data.get('type')
@@ -319,3 +326,6 @@ class Notification:
         yield text.NotificationLog(
             self.datestamp, self.client.fq(self.account['acct']),
             self.account['display_name'], self.ntype, self.content)
+
+    def get_reply_recipients(self):
+        yield self.client.fq(self.account['acct'])
index eb27c01d71f52b72d66ce93eeab41c2fcf908987..1e9c1f6826f0c8cddb90d2a2d505145e5d115007 100644 (file)
@@ -1,3 +1,4 @@
+import collections
 import curses
 import itertools
 import os
@@ -157,6 +158,10 @@ class CursesUI(client.Client):
             self.composer = Composer(self)
         return self.composer
 
+    def new_composer(self, text):
+        self.composer = Composer(self, text)
+        return self.composer
+
     def run(self):
         home_feed = self.home_timeline_feed()
         mentions_feed = self.mentions_feed()
@@ -227,7 +232,19 @@ class CursesUI(client.Client):
         finally:
             self.curses_shutdown()
 
-class Menu:
+class Activity:
+    def chain_to(self, activity):
+        assert self.cc.activity_stack[-1] is self
+        if (len(self.cc.activity_stack) > 1 and
+            self.cc.activity_stack[-2] == activity):
+            self.cc.activity_stack.pop()
+        else:
+            self.cc.activity_stack[-1] = activity
+
+    def push_to(self, activity):
+        self.cc.activity_stack.append(activity)
+
+class Menu(Activity):
     status_extra_text = None
 
     def __init__(self, cc):
@@ -272,17 +289,6 @@ class Menu:
     def add_keys(self, sl):
         sl.keys.append(('RET', 'Back'))
 
-    def chain_to(self, activity):
-        assert self.cc.activity_stack[-1] is self
-        if (len(self.cc.activity_stack) > 1 and
-            self.cc.activity_stack[-2] == activity):
-            self.cc.activity_stack.pop()
-        else:
-            self.cc.activity_stack[-1] = activity
-
-    def push_to(self, activity):
-        self.cc.activity_stack.append(activity)
-
     def handle_key(self, ch):
         if ch in {'q', 'Q', '\n', '\r'}:
             return 'quit'
@@ -392,29 +398,45 @@ class ExitMenu(Menu):
         else:
             return super().handle_key(ch)
 
-class File:
+class File(Activity):
     # Base class for anything where you page up and down.
     def __init__(self, cc):
         self.cc = cc
+        self.mode = 'normal'
 
     def handle_key(self, ch):
-        if ch in {' ', curses.KEY_NPAGE}:
-            self.down_screen()
-        elif ch in {'-', 'b', 'B', curses.KEY_PPAGE}:
-            self.up_screen()
-        elif ch == curses.KEY_DOWN:
-            self.down_line()
-        elif ch in {'\n', '\r'}:
-            if not self.down_line():
+        if self.mode == 'normal':
+            if ch in {' ', curses.KEY_NPAGE}:
+                self.down_screen()
+            elif ch in {'-', 'b', 'B', curses.KEY_PPAGE}:
+                self.up_screen()
+            elif ch == curses.KEY_DOWN:
+                self.down_line()
+            elif ch in {'\n', '\r'}:
+                if not self.down_line():
+                    return 'quit'
+            elif ch in {curses.KEY_UP}:
+                self.up_line()
+            elif ch in {'0', curses.KEY_HOME}:
+                self.goto_top()
+            elif ch in {'z', 'Z', curses.KEY_END}:
+                self.goto_bottom()
+            elif ch in {'q', 'Q'}:
                 return 'quit'
-        elif ch in {curses.KEY_UP}:
-            self.up_line()
-        elif ch in {'0', curses.KEY_HOME}:
-            self.goto_top()
-        elif ch in {'z', 'Z', curses.KEY_END}:
-            self.goto_bottom()
-        elif ch in {'q', 'Q'}:
-            return 'quit'
+            elif ch in {'s', 'S'}:
+                self.send_mode()
+        elif self.mode == 'send':
+            if ch in {'q', 'Q'}:
+                self.mode = 'normal'
+            elif ch in {'-', 'b', 'B', curses.KEY_UP}:
+                self.prev_send_target()
+            elif ch in {'+', curses.KEY_DOWN}:
+                self.next_send_target()
+            elif ch in {' ', 'i', 'I', 'a', 'A', 'l', 'L'}:
+                self.send_complete()
+
+    def send_mode(self):
+        pass # not supported
 
 class ObjectFile(File):
     def __init__(self, cc, constructor, feed, title):
@@ -435,6 +457,9 @@ class ObjectFile(File):
         self.lines = None
         self.linepos = None
         self.width = None
+        self.send_target = None
+        self.old_display_state = None
+        self.index_by_line = []
 
     def iter_text_indexed(self):
         yield self.header, None
@@ -464,14 +489,23 @@ class ObjectFile(File):
         return got_any
 
     def regenerate_lines(self, width):
-        if self.width == width and not self.fetch_new():
+        display_state = (self.mode, self.send_target)
+        if (self.width == width and display_state == self.old_display_state and
+            not self.fetch_new()):
             return
+        self.old_display_state = display_state
         self.lines = []
+        self.index_by_line = []
         pos = 0
         for thing, itemindex in self.iter_text_indexed():
-            for line in thing.render(width):
+            params = {}
+            if (self.mode == 'send' and itemindex == self.send_target and
+                isinstance(thing, text.FromLine)):
+                params['target'] = True
+            for line in thing.render(width, **params):
                 for s in line.split(width):
                     self.lines.append(s)
+                    self.index_by_line.append(itemindex)
             if itemindex == self.itempos:
                 pos = len(self.lines)
         if self.width != width:
@@ -485,15 +519,40 @@ class ObjectFile(File):
             self.cc.print_at(y, 0, line)
 
         sl = text.FileStatusLine()
-        if self.linepos >= len(self.lines):
-            sl.keys.append(('-', 'Up'))
+        if self.mode == 'send':
+            sl.keys.append(('SPACE', 'Reply'))
+            sl.keys.append(('-', None))
+            sl.keys.append(('+', None))
+            sl.keys.append(('Q', 'Quit'))
         else:
-            sl.keys.append(('SPACE', 'More'))
-        sl.keys.append(('Q', 'Exit'))
-        sl.proportion = self.linepos / len(self.lines)
+            if self.linepos >= len(self.lines):
+                sl.keys.append(('-', 'Up'))
+            else:
+                sl.keys.append(('SPACE', 'More'))
+            sl.keys.append(('Q', 'Exit'))
+            sl.proportion = self.linepos / len(self.lines)
         sl_rendered = util.exactly_one(sl.render(self.cc.scr_w))
         self.cc.print_at(self.cc.scr_h - 1, 0, sl_rendered)
 
+    def send_mode(self):
+        self.mode = 'send'
+        self.send_target = self.index_by_line[self.linepos-1]
+    def prev_send_target(self):
+        self.send_target = max(self.minpos, self.send_target-1)
+        print("prev ->", self.send_target, file=sys.stderr)
+    def next_send_target(self):
+        self.send_target = min(self.maxpos-1, self.send_target+1)
+        print("next ->", self.send_target, file=sys.stderr)
+    def send_complete(self):
+        self.mode = 'normal'
+        recipients = collections.OrderedDict()
+        for r in self.statuses[self.send_target].get_reply_recipients():
+            if r == self.cc.fq_username or r in recipients:
+                continue
+            recipients[r] = 1
+        self.push_to(self.cc.new_composer("".join(
+            f"@{r} " for r in recipients)))
+
     def move_to(self, pos):
         old_linepos = self.linepos
         self.linepos = pos
@@ -615,6 +674,7 @@ class Composer:
         def layout(self, width):
             self.colourise()
             self.yx = [None] * (len(self.text) + 1)
+            self.yx[0] = 0, 0 # in case there's no text at all
             self.lines = []
             y = 0
             pos = 0
@@ -634,11 +694,11 @@ class Composer:
                     y += 1
                 pos = next_nl + 1
 
-    def __init__(self, cc):
+    def __init__(self, cc, initial_text=""):
         self.cc = cc
         self.header = text.FileHeader("Compose a post")
-        self.text = "@Lorem #ipsum dolor sit amet, @consectetur@adipisicing.elit, sed do eiusmod  tempor https://incididunt.ut.com/labore/et/ dolore magna\naliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
-        self.point = 95
+        self.text = initial_text
+        self.point = len(self.text)
         self.goal_column = None
 
     def render(self):
@@ -680,6 +740,18 @@ class Composer:
         if self.goal_column is None or not is_updown:
             self.goal_column = self.cx
 
+        # TODO:
+        #
+        # ^W,^T are Mono's keys for moving back/forward a word.
+        #
+        # Not sure what to do about ^K/^Y for one-line cut-paste, given
+        # that the autowrap makes the semantics a bit weird compared to
+        # the original setup.
+        #
+        # Probably don't want to exactly replicate the block
+        # operations either. Perhaps I should invent my own more sensible
+        # approach to cut, copy and paste.
+
         if ch in {ctrl('b'), curses.KEY_LEFT}:
             self.move_to(self.point - 1)
         elif ch in {ctrl('f'), curses.KEY_RIGHT}:
@@ -692,9 +764,12 @@ class Composer:
                      if yx <= (self.cy + 1, self.goal_column)))
             except ValueError:
                 new_point = len(self.text)
-                self.goal_column = None
 
             self.move_to(new_point)
+            if self.dtext.yx[self.point][0] != self.cy + 1:
+                # Failed to go down; probably went to the end; reset
+                # the goal column.
+                self.goal_column = None
         elif ch in {ctrl('p'), curses.KEY_UP}:
             try:
                 new_point = util.last(
@@ -702,9 +777,12 @@ class Composer:
                      if yx <= (self.cy - 1, self.goal_column)))
             except ValueError:
                 new_point = 0
-                self.goal_column = None
 
             self.move_to(new_point)
+            if self.dtext.yx[self.point][0] != self.cy - 1:
+                # Failed to go down; probably went to the start; reset
+                # the goal column.
+                self.goal_column = None
         elif ch in {ctrl('a'), curses.KEY_HOME}:
             new_point = next(
                 i for i, yx in enumerate(self.dtext.yx[:self.point+1])
@@ -727,13 +805,11 @@ class Composer:
             self.text = (self.text[:self.point] + '\n' +
                          self.text[self.point:])
             self.point += 1
-        elif isinstance(ch, str) and ' ' <= ch < '\x7F' or '\xA0' <= ch:
+        elif isinstance(ch, str) and (' ' <= ch < '\x7F' or '\xA0' <= ch):
             # TODO: overwrite mode
             self.text = (self.text[:self.point] + ch +
                          self.text[self.point:])
             self.point += 1
-        else:
-            pass # print("?", repr(ch), file=sys.stderr)
 
         if not is_updown:
             self.goal_column = None
@@ -762,3 +838,8 @@ class testComposerLayout(unittest.TestCase):
                                    text.ColouredString("ixjkl")])
         self.assertEqual(t.yx, ([(0,i) for i in range(10)] +
                                 [(1,i) for i in range(6)]))
+
+        t = Composer.DisplayText("")
+        t.layout(10)
+        self.assertEqual(t.lines, [])
+        self.assertEqual(t.yx, [(0,0)])
diff --git a/text.py b/text.py
index 9f124a2621bfa9448b9454090a9f08217c89b4e8..a0be501985d15efb1501ff8d5122aa609a45793d 100644 (file)
--- a/text.py
+++ b/text.py
@@ -138,9 +138,9 @@ class UsernameHeader:
         self.account = account
         self.nameline = nameline
 
-    def render(self, width):
+    def render(self, width, target=False):
         # FIXME: truncate
-        yield (ColouredString(self.header + ": ") +
+        yield (ColouredString(self.header + ": ", 'f' if target else ' ') +
                ColouredString(f"{self.nameline} ({self.account})",
                               self.colour))
 
@@ -246,7 +246,9 @@ class FileStatusLine:
         for key, action in self.keys:
             message += (
                 sep + ColouredString('[') + ColouredString(key, 'k') +
-                ColouredString(']:' + action))
+                ColouredString(']'))
+            if action is not None:
+                message += ColouredString(':' + action)
             sep = sep2
         if self.proportion is not None:
             message += sep + ColouredString('({:d}%)'.format(