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

fedicurses.py 5.8KB

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