From 572045020f1918244fa908bb837b8fe4467e9b78 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 5 Dec 2023 13:05:36 +0000 Subject: [PATCH] Detect updates and append them to the file. --- client.py | 58 ++++++++++++++++++++++++++----------------- cursesclient.py | 66 +++++++++++++++++++++++++++++++++++++++++++------ text.py | 3 +-- util.py | 18 +++++++++++--- 4 files changed, 110 insertions(+), 35 deletions(-) diff --git a/client.py b/client.py index 830572f..d46d60b 100644 --- a/client.py +++ b/client.py @@ -5,6 +5,7 @@ import os import re import requests import string +import sys import time import text @@ -44,7 +45,7 @@ class Client: self.logfh = open(logfile, "w") pr = lambda *args, **kws: print(*args, file=self.logfh, **kws) - def log_response(rsp): + def log_response(rsp, content): pr(f"Request: {rsp.request.method} {rsp.request.url}") pr(" Request headers:") for k, v in rsp.request.headers.items(): @@ -53,22 +54,25 @@ class Client: pr(" Response headers:") for k, v in rsp.headers.items(): pr(f" {k}: {v}") - if 'application/json' not in rsp.headers.get('content-type'): - pr(f" Response: {rsp.content!r}") - else: - pr(" Response JSON:") - j = rsp.json() - for line in json.dumps(j, indent=4).splitlines(): - pr(" " + line) + if content: + if 'application/json' not in rsp.headers.get('content-type'): + pr(f" Response: {rsp.content!r}") + else: + pr(" Response JSON:") + j = rsp.json() + for line in json.dumps(j, indent=4).splitlines(): + pr(" " + line) + self.logfh.flush() self.log_response = log_response - def method_start(self, method, path, base, params, links={}): + def method_start(self, method, path, base, params, stream, links={}): headers = {} if self.bearer_token is not None: headers['Authorization'] = 'Bearer ' + self.bearer_token - rsp = method(self.urls[base] + path, params=params, headers=headers) - self.log_response(rsp) + rsp = method(self.urls[base] + path, params=params, headers=headers, + stream=stream) + self.log_response(rsp, content=not stream) if rsp.status_code != 200: raise HTTPError(rsp) linkhdr = rsp.headers.get('Link', '') @@ -81,7 +85,8 @@ class Client: return rsp def method(self, method, path, base, params, links={}): - return self.method_start(method, path, base, params, links).json() + return self.method_start(method, path, base, params, + False, links).json() def get(self, path, base='api', **params): return self.method(requests.get, path, base, params) @@ -110,13 +115,12 @@ class Client: return data, links def get_incremental_cont(self, link): + links = {} data = self.method(requests.get, link, None, {}, links) return data, links def get_streaming_lines(self, path, base='api', **params): - reqgetstream = lambda *args, **kws: requests.get( - *args, stream=True, **kws) - rsp = self.method_start(reqgetstream, path, base, params, {}) + rsp = self.method_start(requests.get, path, base, params, True, {}) if rsp.status_code != 200: raise HTTPError(rsp) @@ -163,14 +167,16 @@ class IncrementalServerFeed(Feed): self.url = url self.params = params self.get = get + self.started = False def start(self): - data, self.links = self.client.get_incremental_start( + data, 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'] - self.next_link = self.links['next'] + self.prev_link = links['prev'] + self.next_link = links['next'] + self.started = True def min_index(self): return -self.origin @@ -180,15 +186,23 @@ class IncrementalServerFeed(Feed): return self.data[n + self.origin] def extend_past(self): - data, links = self.client.get_incremental_cont(links, 'prev') + if not self.started: + return + data, links = self.client.get_incremental_cont(self.next_link) + if len(data) == 0: + return self.data[0:0] = list(reversed(data)) self.origin += len(data) - self.prev_link = self.links['prev'] + self.next_link = links['next'] def extend_future(self): - data, links = self.client.get_incremental_cont(links, 'next') + if not self.started: + return + data, links = self.client.get_incremental_cont(self.prev_link) + if len(data) == 0: + return self.data.extend(reversed(data)) - self.next_link = self.links['next'] + self.prev_link = links['prev'] class HomeTimelineFeed(IncrementalServerFeed): def __init__(self, client): diff --git a/cursesclient.py b/cursesclient.py index 489e5e9..e046ceb 100644 --- a/cursesclient.py +++ b/cursesclient.py @@ -1,12 +1,18 @@ import curses import itertools +import select import sys +import threading import client import text import util class CursesUI(client.Client): + def __init__(self): + super().__init__() + self.selfpipes = [] + def curses_setup(self): self.scr = curses.initscr() if hasattr(curses, 'start_color'): @@ -85,12 +91,36 @@ class CursesUI(client.Client): self.print_at(y, 0, text.ColouredString(' ' * self.scr_w)) def get_input(self): - # FIXME: add a select loop for self-pipes - return self.scr.getch() + rfds_in = [0] + for (sp, handler, _) in self.selfpipes: + rfds_in.append(sp.rfd) + rfds_out, _, _ = select.select(rfds_in, [], []) + rfds_out = set(rfds_out) + for (sp, handler, _) in self.selfpipes: + if sp.rfd in rfds_out and sp.check(): + handler() + if 0 in rfds_out: + return self.scr.getch() + else: + return None + + def add_selfpipe(self, url, handler): + sp = util.SelfPipe() + gen = self.get_streaming_lines(url) + def threadfn(): + for line in gen: + # ignore heartbeat lines + if line.startswith("event"): + sp.signal() + th = threading.Thread(target=threadfn, daemon=True) + th.start() + self.selfpipes.append((sp, handler, th)) def run(self): + home_feed = self.home_timeline_feed() + self.add_selfpipe("streaming/user", home_feed.extend_future) self.home_timeline = StatusFile( - self, self.home_timeline_feed(), + self, home_feed, text.ColouredString("Home timeline ", "HHHHHHHHHHHHHHHHHKH")) @@ -149,10 +179,28 @@ class StatusFile: for thing in self.statuses[i].text(): yield thing, i # FIXME: maybe just yield the last? - def resize(self, width): - if self.width == width: + def fetch_new(self): + got_any = False + + new_minpos = self.feed.min_index() + while self.minpos > new_minpos: + self.minpos -= 1 + self.statuses[self.minpos] = client.Status( + self.feed[self.minpos], self.cc) + got_any = True + + new_maxpos = self.feed.max_index() + while self.maxpos < new_maxpos: + self.statuses[self.maxpos] = client.Status( + self.feed[self.maxpos], self.cc) + self.maxpos += 1 + got_any = True + + return got_any + + def regenerate_lines(self, width): + if self.width == width and not self.fetch_new(): return - self.width = width self.lines = [] pos = 0 for thing, itemindex in self.iter_text_indexed(): @@ -161,10 +209,12 @@ class StatusFile: self.lines.append(s) if itemindex == self.itempos: pos = len(self.lines) - self.move_to(pos) + if self.width != width: + self.width = width + self.move_to(pos) def render(self): - self.resize(self.cc.scr_w) + self.regenerate_lines(self.cc.scr_w) topline = max(0, self.linepos - (self.cc.scr_h - 1)) for y, line in enumerate(self.lines[topline:topline+self.cc.scr_h-1]): self.cc.print_at(y, 0, line) diff --git a/text.py b/text.py index c5e8183..5b514cb 100644 --- a/text.py +++ b/text.py @@ -190,8 +190,7 @@ class ExtendableIndicator: rspace = space - lspace + 1 yield ColouredString("") - yield (ColouredString(" " * lspace) + message + - ColouredString(" " * rspace)) + yield ColouredString(" " * lspace) + message yield ColouredString("") class FileStatusLine: diff --git a/util.py b/util.py index fcd6578..6114807 100644 --- a/util.py +++ b/util.py @@ -4,13 +4,25 @@ class SelfPipe: def __init__(self): self.rfd, self.wfd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) + def nonblocking_read(self, size=4096): + try: + return os.read(self.rfd, size) + except BlockingIOError: + return b'' + + def nonblocking_write(self, data): + try: + os.write(self.wfd, data) + except BlockingIOError: + pass + def signal(self): - os.write(self.wfd, b'x') + self.nonblocking_write(b'x') def check(self): - if len(os.read(self, 4096)) == 0: + if len(self.nonblocking_read()) == 0: return False - while len(os.read(self, 4096)) != 0: + while len(self.nonblocking_read()) != 0: pass return True -- 2.30.2