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
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
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']
class HomeTimelineFeed(IncrementalServerFeed):
def __init__(self, client):
- super().__init__(client, "timelines/home")
+ super().__init__(client, "timelines/home", {})
class MentionsFeed(IncrementalServerFeed):
def __init__(self, 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
'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:
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 = []
--- /dev/null
+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