#! /usr/bin/python3 import curses from html.parser import HTMLParser from mastodon import Mastodon def initColours(): # 0: White on black curses.init_pair(1, 4, 0) # 1: Blue on black curses.init_pair(2, 1, 0) # 2: Red on black curses.init_pair(3, 6, 0) # 3: Cyan on black curses.init_pair(4, 2, 0) # 4: Green on black def newLine(ncwin, lines=1): cy, cx = ncwin.getyx() # my, mx = ncwin.getmaxyx() try: ncwin.move(cy + lines, 0) except curses.error: pass def bump(win): my, mx = win.getmaxyx() cy, cx = win.getyx() if cx < mx and cx != 0: try: win.move(cy, cx + 1) except curses.error: pass def typeset(text, win, attr, colour): my, mx = win.getmaxyx() line = 0 position = 0 strlen = len(text) while position < (strlen): cy, cx = win.getyx() if cx == mx: if cy == my: raise curses.error("Out of space") rem = (mx - cx) + 0 nsp = text.find(' ', position) if (strlen - position <= rem): win.addnstr(text[position:], rem, attr | curses.color_pair(colour)) position = strlen elif (nsp == -1 or nsp - position > rem): # and not strlen - position <= rem: if cx == 0: win.addnstr(text[position:], rem, attr | curses.color_pair(colour)) position = position + rem else: newLine(win) else: win.addnstr(text[position:], nsp - position, attr | curses.color_pair(colour)) position = max(nsp, 0) + 1 bump(win) def printPost(win, post, parser): # win.addstr(post["account"]["acct"], curses.A_BOLD) typeset("@{}".format(post["account"]["acct"]), win, curses.A_BOLD, 0) if (post["reblog"] is not None): # win.addstr(" boosted ") typeset(" boosted ", win, 0, 0) # win.addstr(post["reblog"]["account"]["acct"], curses.A_BOLD) typeset("@{}".format(post["reblog"]["account"]["acct"]), win, curses.A_BOLD, 0) # win.addstr(":") typeset(":", win, 0, 0) newLine(win) if (post["spoiler_text"] != ""): # win.addstr(post["spoiler_text"], curses.A_UNDERLINE) typeset(post["spoiler_text"], win, curses.A_UNDERLINE, 0) newLine(win) parser.feed(post["content"]) if parser.openp: newLine(win) for att in post["media_attachments"]: # typeset("[IMG: {}, <{}>]".format(att["alt"], att["src"])) typeset("[IMG: {}]".format(att["description"] if att["description"] is not None else "(Alt text missing)"), win, 0, 4) newLine(win) if (post["reblog"] is not None): for att in post["reblog"]["media_attachments"]: # typeset("[IMG: {}, <{}>]".format(att["alt"], att["src"])) typeset("[IMG: {}]".format(att["description"] if att["description"] is not None else "(Alt text missing)"), win, 0, 4) newLine(win) botstring = "{} @ {} UTC".format(post["visibility"], post["created_at" ].strftime( "%Y-%m-%d %H:%M:%S")) cy, cx = win.getyx() my, mx = win.getmaxyx() # win.addstr(cy, mx - len(botstring), botstring, curses.A_UNDERLINE) win.move(cy, mx - len(botstring)) typeset(botstring, win, curses.A_UNDERLINE, 0) newLine(win, 1) def printAtLevel(stdscr, col1, col2, posts, parser, level): col1.clear() col2.clear() col1.move(0, 0) col2.move(0, 0) printPost(col1, posts[level], parser) newLine(col1) printPost(col1, posts[(level + 1) % len(posts)], parser) try: col2.addstr(str(posts[level]["content"])) col2.addstr(str(posts[level]["media_attachments"])) col2.addstr(str(posts[level].keys())) except curses.error: pass stdscr.noutrefresh() col1.noutrefresh() col2.noutrefresh() curses.doupdate() class PostParser(HTMLParser): def __init__(self, ncwin, defncatt): HTMLParser.__init__(self) self.win = ncwin self.defncatt = defncatt self.curatt = defncatt self.openp = False self.colour = 0 self.colstack = [] def handle_starttag(self, tag, attrs): if tag == 'p': self.curatt = self.defncatt self.colstack = [] self.colour = 0 # newLine(self.win) elif tag == 'strong' or tag == 'b': self.curatt = self.curatt ^ curses.A_REVERSE elif tag == 'em' or tag == 'i': self.curatt = self.curatt ^ curses.A_BOLD elif tag == 'br': newLine(self.win) elif tag == 'a': self.colstack.append(self.colour) self.colour = 1 elif tag == 'code': self.colstack.append(self.colour) self.colour = 3 elif tag == 'img': typeset("[IMG: {}, <{}>]".format(att["alt"], att["src"]), self.win, 0, 4) def handle_endtag(self, tag): if tag == 'p': newLine(self.win, 2) self.openp = False elif tag == 'strong' or tag == 'b': self.curatt = self.curatt ^ curses.A_REVERSE elif tag == 'em' or tag == 'i': self.curatt = self.curatt ^ curses.A_BOLD elif tag == 'a': self.colour = self.colstack.pop() elif tag == 'code': self.colour = self.colstack.pop() def handle_data(self, data): try: # self.win.addstr(data, self.curatt | curses.color_pair(self.colour)) typeset(data, self.win, self.curatt, self.colour) except curses.error: pass self.openp = True # print(mastodon.timeline()[0]["content"]) def main(stdscr): initColours() mastodon = Mastodon( access_token = 'd100.club_usercred.secret', api_base_url = 'd100.club' ) posts = mastodon.timeline() stdscr.clear() # stdscr.addstr("test") curses.halfdelay(10) my, mx = stdscr.getmaxyx() c1w = c2w = (mx - 3) // 2 if (mx % 2) != 0: c2w = c2w - 1 colw1 = curses.newwin(my - 2, c1w, 1, 1) colw2 = curses.newwin(my - 2, c2w, 1, 1 + c1w + 2) stdscr.border() # colw1.border() # colw2.border() parser = PostParser(colw1, 0) # colw1.move(0,0) # stdscr.addstr(0, 0, 'test') # colw2.addstr(0, 0, 'test2') # colw1.addstr(0, 0, 'test3') i = 'c' q = 0 printAtLevel(stdscr, colw1, colw2, posts, parser, q) con = True while con: i = stdscr.getch() if (i == ord('q')): con = False elif (i == ord('j')): q = (q + 1) % len(posts) printAtLevel(stdscr, colw1, colw2, posts, parser, q) elif (i == ord('k')): q = (q - 1) % len(posts) printAtLevel(stdscr, colw1, colw2, posts, parser, q) curses.wrapper(main)