WIP repository for a ncurses fediverse/mastodon client, using python mastodon.py

fedicurses.py 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import curses
  2. from html.parser import HTMLParser
  3. from mastodon import Mastodon
  4. def initColours():
  5. # 0: White on black
  6. curses.init_pair(1, 4, 0) # 1: Blue on black
  7. curses.init_pair(2, 1, 0) # 2: Red on black
  8. curses.init_pair(3, 6, 0) # 2: Cyan on black
  9. def newLine(ncwin, lines=1):
  10. cy, cx = ncwin.getyx()
  11. # my, mx = ncwin.getmaxyx()
  12. try:
  13. ncwin.move(cy + lines, 0)
  14. except curses.error:
  15. pass
  16. def bump(win):
  17. my, mx = win.getmaxyx()
  18. cy, cx = win.getyx()
  19. if cx < mx and cx != 0:
  20. try:
  21. win.move(cy, cx + 1)
  22. except curses.error:
  23. pass
  24. def typeset(text, win, attr, colour):
  25. my, mx = win.getmaxyx()
  26. line = 0
  27. position = 0
  28. strlen = len(text)
  29. while position < (strlen):
  30. cy, cx = win.getyx()
  31. if cx == mx:
  32. if cy == my:
  33. raise curses.error("Out of space")
  34. rem = (mx - cx) + 0
  35. nsp = text.find(' ', position)
  36. if (strlen - position <= rem):
  37. win.addnstr(text[position:], rem,
  38. attr | curses.color_pair(colour))
  39. position = strlen
  40. elif (nsp == -1 or nsp - position > rem): # and not strlen - position <= rem:
  41. if cx == 0:
  42. win.addnstr(text[position:], rem, attr |
  43. curses.color_pair(colour))
  44. position = position + rem
  45. else:
  46. newLine(win)
  47. else:
  48. win.addnstr(text[position:], nsp - position,
  49. attr | curses.color_pair(colour))
  50. position = max(nsp, 0) + 1
  51. bump(win)
  52. def printPost(win, post, parser):
  53. # win.addstr(post["account"]["acct"], curses.A_BOLD)
  54. typeset(post["account"]["acct"], win, curses.A_BOLD, 0)
  55. if (post["reblog"] is not None):
  56. # win.addstr(" boosted ")
  57. typeset(" boosted ", win, 0, 0)
  58. # win.addstr(post["reblog"]["account"]["acct"], curses.A_BOLD)
  59. typeset(post["reblog"]["account"]["acct"], win, curses.A_BOLD, 0)
  60. # win.addstr(":")
  61. typeset(":", win, 0, 0)
  62. newLine(win)
  63. if (post["spoiler_text"] != ""):
  64. # win.addstr(post["spoiler_text"], curses.A_UNDERLINE)
  65. typeset(post["spoiler_text"], win, curses.A_UNDERLINE, 0)
  66. newLine(win)
  67. parser.feed(post["content"])
  68. if parser.openp:
  69. newLine(win)
  70. botstring = "{} @ {} UTC".format(post["visibility"],
  71. post["created_at"
  72. ].strftime(
  73. "%Y-%m-%d %H:%M:%S"))
  74. cy, cx = win.getyx()
  75. my, mx = win.getmaxyx()
  76. # win.addstr(cy, mx - len(botstring), botstring, curses.A_UNDERLINE)
  77. win.move(cy, mx - len(botstring))
  78. typeset(botstring, win, curses.A_UNDERLINE, 0)
  79. newLine(win, 1)
  80. def printAtLevel(stdscr, col1, col2, posts, parser, level):
  81. col1.clear()
  82. col2.clear()
  83. col1.move(0, 0)
  84. col2.move(0, 0)
  85. printPost(col1, posts[level], parser)
  86. newLine(col1)
  87. printPost(col1, posts[(level + 1) % len(posts)], parser)
  88. try:
  89. col2.addstr(str(posts[level]))
  90. except curses.error:
  91. pass
  92. stdscr.noutrefresh()
  93. col1.noutrefresh()
  94. col2.noutrefresh()
  95. curses.doupdate()
  96. class PostParser(HTMLParser):
  97. def __init__(self, ncwin, defncatt):
  98. HTMLParser.__init__(self)
  99. self.win = ncwin
  100. self.defncatt = defncatt
  101. self.curatt = defncatt
  102. self.openp = False
  103. self.colour = 0
  104. self.colstack = []
  105. def handle_starttag(self, tag, attrs):
  106. if tag == 'p':
  107. self.curatt = self.defncatt
  108. self.colstack = []
  109. self.colour = 0
  110. # newLine(self.win)
  111. elif tag == 'strong' or tag == 'b':
  112. self.curatt = self.curatt ^ curses.A_REVERSE
  113. elif tag == 'em' or tag == 'i':
  114. self.curatt = self.curatt ^ curses.A_BOLD
  115. elif tag == 'br':
  116. newLine(self.win)
  117. elif tag == 'a':
  118. self.colstack.append(self.colour)
  119. self.colour = 1
  120. elif tag == 'code':
  121. self.colstack.append(self.colour)
  122. self.colour = 3
  123. def handle_endtag(self, tag):
  124. if tag == 'p':
  125. newLine(self.win, 2)
  126. self.openp = False
  127. elif tag == 'strong' or tag == 'b':
  128. self.curatt = self.curatt ^ curses.A_REVERSE
  129. elif tag == 'em' or tag == 'i':
  130. self.curatt = self.curatt ^ curses.A_BOLD
  131. elif tag == 'a':
  132. self.colour = self.colstack.pop()
  133. elif tag == 'code':
  134. self.colour = self.colstack.pop()
  135. def handle_data(self, data):
  136. try:
  137. # self.win.addstr(data, self.curatt | curses.color_pair(self.colour))
  138. typeset(data, self.win, self.curatt, self.colour)
  139. except curses.error:
  140. pass
  141. self.openp = True
  142. # print(mastodon.timeline()[0]["content"])
  143. def main(stdscr):
  144. initColours()
  145. mastodon = Mastodon(
  146. access_token = 'd100.club_usercred.secret',
  147. api_base_url = 'd100.club'
  148. )
  149. posts = mastodon.timeline()
  150. stdscr.clear()
  151. # stdscr.addstr("test")
  152. curses.halfdelay(10)
  153. my, mx = stdscr.getmaxyx()
  154. c1w = c2w = (mx - 3) // 2
  155. if (mx % 2) != 0:
  156. c2w = c2w - 1
  157. colw1 = curses.newwin(my - 2, c1w, 1, 1)
  158. colw2 = curses.newwin(my - 2, c2w, 1, 1 + c1w + 2)
  159. stdscr.border()
  160. # colw1.border()
  161. # colw2.border()
  162. parser = PostParser(colw1, 0)
  163. # colw1.move(0,0)
  164. # stdscr.addstr(0, 0, 'test')
  165. # colw2.addstr(0, 0, 'test2')
  166. # colw1.addstr(0, 0, 'test3')
  167. i = 'c'
  168. q = 0
  169. printAtLevel(stdscr, colw1, colw2, posts, parser, q)
  170. con = True
  171. while con:
  172. i = stdscr.getch()
  173. if (i == ord('q')):
  174. con = False
  175. elif (i == ord('j')):
  176. q = (q + 1) % len(posts)
  177. printAtLevel(stdscr, colw1, colw2, posts, parser, q)
  178. elif (i == ord('k')):
  179. q = (q - 1) % len(posts)
  180. printAtLevel(stdscr, colw1, colw2, posts, parser, q)
  181. curses.wrapper(main)