import re
import requests
import string
+import sys
import time
import text
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():
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', '')
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)
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)
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
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):
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'):
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 <H>",
"HHHHHHHHHHHHHHHHHKH"))
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():
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)