From c9795c8e19d6cebaa3bf243a4efc7c70a78b5c02 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 5 Dec 2023 07:09:40 +0000 Subject: [PATCH] VERY UNFINISHED attempt to start reading a file in curses. --- client.py | 9 +++++-- cursesclient.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++--- text.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ util.py | 15 +++++++++++ 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 util.py diff --git a/client.py b/client.py index ebd27f2..830572f 100644 --- 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): diff --git a/cursesclient.py b/cursesclient.py index bd821fd..837edb6 100644 --- a/cursesclient.py +++ b/cursesclient.py @@ -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 ", + "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 21d0063..4448548 100644 --- 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 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 -- 2.30.2