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

fedicurses.py 5.3KB

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