chiark / gitweb /
VERY UNFINISHED attempt to start reading a file in curses.
authorSimon Tatham <anakin@pobox.com>
Tue, 5 Dec 2023 07:09:40 +0000 (07:09 +0000)
committerSimon Tatham <anakin@pobox.com>
Tue, 5 Dec 2023 07:09:40 +0000 (07:09 +0000)
client.py
cursesclient.py
text.py
util.py [new file with mode: 0644]

index ebd27f2f4d1cc13954376bed3b5f4a2ca8284a51..830572fcaa505b03a861b30090e3e139226772bc 100644 (file)
--- a/client.py
+++ b/client.py
@@ -105,6 +105,7 @@ class Client:
 
     def get_incremental_start(self, path, base='api', **params):
         params.setdefault('limit', 32)
+        links = {}
         data = self.method(requests.get, path, base, params, links)
         return data, links
 
@@ -142,6 +143,9 @@ class Client:
         return (account_name if '@' in account_name
                 else account_name + '@' + self.instance_domain)
 
+    def home_timeline_feed(self):
+        return HomeTimelineFeed(self)
+
 class Feed:
     """Base class that encapsulates some kind of collection of _things_ we
     can get from the server, with both the ability to go backwards in
@@ -161,7 +165,8 @@ class IncrementalServerFeed(Feed):
         self.get = get
 
     def start(self):
-        data, links = self.client.get_incremental_start(self.url, self.params)
+        data, self.links = self.client.get_incremental_start(
+            self.url, **self.params)
         self.data = list(reversed(data))
         self.origin = len(self.data)
         self.prev_link = self.links['prev']
@@ -187,7 +192,7 @@ class IncrementalServerFeed(Feed):
 
 class HomeTimelineFeed(IncrementalServerFeed):
     def __init__(self, client):
-        super().__init__(client, "timelines/home")
+        super().__init__(client, "timelines/home", {})
 
 class MentionsFeed(IncrementalServerFeed):
     def __init__(self, client):
index bd821fda2b0061d00c3ac7e930db916c9a4cc554..837edb6e1fc2b6fd15ee2239392c979b2d148b7a 100644 (file)
@@ -79,13 +79,77 @@ class CursesUI(client.Client):
             self.scr.addstr(y, x, frag, self.attrs[colour])
             x += w
 
+    def clear(self):
+        for y in range(self.scr_h):
+            self.print_at(y, 0, text.ColouredString(' ' * self.scr_w))
+
     def run(self):
+        self.home_timeline = StatusFile(
+            self, self.home_timeline_feed(),
+            text.ColouredString("Home timeline   <H>",
+                                "HHHHHHHHHHHHHHHHHKH"))
+
         try:
             self.curses_setup()
-            self.print_at(5, 3, text.ColouredString(
-                'testing testing tésting t\uFF45sting one two three',
-                '        SSSSSSS DDDDDDD FFFFFFF ccc ### @@@@@'))
+            self.home_timeline.render()
             self.scr.refresh()
             self.scr.getch()
         finally:
             self.curses_shutdown()
+
+class StatusFile:
+    def __init__(self, cc, feed, title):
+        self.cc = cc
+        self.feed = feed
+        self.feed.start()
+
+        self.header = text.FileHeader(title)
+
+        self.history_closed = False
+        self.minpos = self.feed.min_index()
+        self.maxpos = self.feed.max_index()
+        self.statuses = {i: client.Status(self.feed[i], cc)
+                         for i in range(self.minpos, self.maxpos)}
+        self.itempos = self.maxpos - 1
+
+        self.lines = None
+        self.linepos = None
+
+    def iter_text_indexed(self):
+        yield self.header, None
+        if not self.history_closed:
+            yield text.ExtendableIndicator(), None
+        for i in range(self.minpos, self.maxpos):
+            for thing in self.statuses[i].text():
+                yield thing, i # FIXME: maybe just yield the last?
+
+    def resize(self, width):
+        self.lines = []
+        self.linepos = 0
+        for thing, itemindex in self.iter_text_indexed():
+            for line in thing.render(width):
+                self.lines.append(line)
+            if itemindex == self.itempos:
+                self.linepos = len(self.lines)
+
+    def render(self):
+        self.cc.clear()
+        self.resize(self.cc.scr_w)
+        topline = max(0, self.linepos - self.cc.scr_w - 1)
+        for y, line in enumerate(self.lines[topline:topline+self.cc.scr_h-1]):
+            self.cc.print_at(y, 0, line)
+
+        sl = text.FileStatusLine()
+        sl.percentage = self.linepos / len(self.lines)
+        sl_rendered = next(sl.render(self.cc.scr_w))
+        # FIXME: keys
+        self.cc.print_at(self.cc.scr_h - 1, 0, sl_rendered)
+
+class Activity:
+    pass
+
+class FileActivity(Activity):
+    pass
+
+class StatusFileActivity(FileActivity):
+    pass
diff --git a/text.py b/text.py
index 21d0063b2c0566bc6679cfaeefd7d70b2b39a6d0..4448548d58fd9b9e359638e204e5d37b76f77136 100644 (file)
--- a/text.py
+++ b/text.py
@@ -25,6 +25,11 @@ colourmap = {
     'M': [0, 1, 4, 35], # media URL
     'm': [0, 35], # media description
     '!': [0, 1, 7, 43, 31], # error report
+    'J': [0, 1, 7, 47, 34], # Mastodonochrome logo in file headers
+    '~': [0, 34], # ~~~~~ underline in file headers
+    'H': [0, 36], # actual header text in file headers
+    'K': [0, 1, 36], # keypress / keypath names in file headers
+    'k': [0, 1], # keypresses in file status lines
 }
 
 class ColouredString:
@@ -142,6 +147,69 @@ class Media:
                     yield ColouredString("  ") + line
         yield ColouredString("")
 
+class FileHeader:
+    def __init__(self, text):
+        if not isinstance(text, ColouredString):
+            text = ColouredString(text, "H")
+        self.text = text
+
+    def render(self, width):
+        logo = [ColouredString('(o)', 'J'), ColouredString('/J\\', 'J')]
+        logowidth = logo[0].width
+        assert(all(s.width == logowidth for s in logo))
+
+        # FIXME: truncate
+        headertext = self.text
+
+        space = width - 2 * logowidth - 2 - headertext.width
+        lspace = space // 2 + 1
+        rspace = space - lspace + 1
+
+        yield (logo[0] + ColouredString(" " * lspace) +
+               headertext + ColouredString(" " * rspace) + logo[0])
+        yield (logo[1] + ColouredString(" ") +
+               ColouredString("~" * space, '~') + ColouredString(" ") +
+               logo[1])
+
+class ExtendableIndicator:
+    def render(self, width):
+        message = ColouredString("FIXME: extendability message here", 'H')
+        space = width - message.width
+        lspace = space // 2 + 1
+        rspace = space - lspace + 1
+
+        yield ColouredString("")
+        yield (ColouredString(" " * lspace) + message +
+               ColouredString(" " * rspace))
+        yield ColouredString("")
+
+class FileStatusLine:
+    def __init__(self):
+        self.keys = []
+        self.proportion = None
+
+    def render(self, width):
+        message = ColouredString('')
+        sep = ColouredString('')
+        for key, action in self.keys:
+            message += (
+                sep + ColouredString('[') + ColouredString(key, 'k') +
+                ColouredString(']:' + action))
+            sep = ColouredString('  ')
+        if self.proportion is not None:
+            message += (
+                sep + ColouredString('({:d}%)'.format(self.proportion * 100)))
+            sep = ColouredString('  ')
+
+        space = width - message.width
+        lspace = space // 2 + 1
+        rspace = space - lspace + 1
+
+        yield ColouredString("")
+        yield (ColouredString(" " * lspace) + message +
+               ColouredString(" " * rspace))
+        yield ColouredString("")
+
 class Paragraph:
     def __init__(self):
         self.words = []
diff --git a/util.py b/util.py
new file mode 100644 (file)
index 0000000..dcec58b
--- /dev/null
+++ b/util.py
@@ -0,0 +1,15 @@
+import os
+
+class SelfPipe:
+    def __init__(self):
+        self.rfd, self.wfd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC)
+
+    def signal(self):
+        os.write(self.wfd, b'x')
+
+    def check(self):
+        if len(os.read(self, 4096)) == 0:
+            return False
+        while len(os.read(self, 4096)) != 0:
+            pass
+        return True