You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

submission.py 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import curses
  2. import sys
  3. import time
  4. import logging
  5. import praw.errors
  6. from .content import SubmissionContent
  7. from .page import BasePage, Navigator, BaseController
  8. from .helpers import clean, open_browser, open_editor
  9. from .curses_helpers import (BULLET, UARROW, DARROW, GOLD, Color, LoadScreen,
  10. show_notification, text_input)
  11. from .docs import COMMENT_FILE
  12. __all__ = ['SubmissionController', 'SubmissionPage']
  13. _logger = logging.getLogger(__name__)
  14. class SubmissionController(BaseController):
  15. character_map = {}
  16. class SubmissionPage(BasePage):
  17. def __init__(self, stdscr, reddit, url=None, submission=None):
  18. self.controller = SubmissionController(self)
  19. self.loader = LoadScreen(stdscr)
  20. if url:
  21. content = SubmissionContent.from_url(reddit, url, self.loader)
  22. elif submission:
  23. content = SubmissionContent(submission, self.loader)
  24. else:
  25. raise ValueError('Must specify url or submission')
  26. super(SubmissionPage, self).__init__(stdscr, reddit,
  27. content, page_index=-1)
  28. def loop(self):
  29. "Main control loop"
  30. self.active = True
  31. while self.active:
  32. self.draw()
  33. cmd = self.stdscr.getch()
  34. self.controller.trigger(cmd)
  35. @SubmissionController.register(curses.KEY_RIGHT, 'l')
  36. def toggle_comment(self):
  37. "Toggle the selected comment tree between visible and hidden"
  38. current_index = self.nav.absolute_index
  39. self.content.toggle(current_index)
  40. if self.nav.inverted:
  41. # Reset the page so that the bottom is at the cursor position.
  42. # This is a workaround to handle if folding the causes the
  43. # cursor index to go out of bounds.
  44. self.nav.page_index, self.nav.cursor_index = current_index, 0
  45. @SubmissionController.register(curses.KEY_LEFT, 'h')
  46. def exit_submission(self):
  47. "Close the submission and return to the subreddit page"
  48. self.active = False
  49. @SubmissionController.register(curses.KEY_F5, 'r')
  50. def refresh_content(self):
  51. "Re-download comments reset the page index"
  52. self.content = SubmissionContent.from_url(
  53. self.reddit,
  54. self.content.name,
  55. self.loader)
  56. self.nav = Navigator(self.content.get, page_index=-1)
  57. @SubmissionController.register(curses.KEY_ENTER, 10, 'o')
  58. def open_link(self):
  59. "Open the current submission page with the webbrowser"
  60. # May want to expand at some point to open comment permalinks
  61. url = self.content.get(-1)['permalink']
  62. open_browser(url)
  63. @SubmissionController.register('c')
  64. def add_comment(self):
  65. """
  66. Add a top-level comment if the submission is selected, or reply to the
  67. selected comment.
  68. """
  69. if not self.reddit.is_logged_in():
  70. show_notification(self.stdscr, ['Login to post'])
  71. return
  72. data = self.content.get(self.nav.absolute_index)
  73. if data['type'] == 'Submission':
  74. content = data['text']
  75. elif data['type'] == 'Comment':
  76. content = data['body']
  77. else:
  78. curses.flash()
  79. return
  80. # Comment out every line of the content
  81. content = '\n'.join(['# |' + line for line in content.split('\n')])
  82. comment_info = COMMENT_FILE.format(
  83. author=data['author'],
  84. type=data['type'].lower(),
  85. content=content)
  86. curses.endwin()
  87. comment_text = open_editor(comment_info)
  88. curses.doupdate()
  89. if not comment_text:
  90. show_notification(self.stdscr, ['Comment canceled'])
  91. return
  92. try:
  93. if data['type'] == 'Submission':
  94. data['object'].add_comment(comment_text)
  95. else:
  96. data['object'].reply(comment_text)
  97. except praw.errors.APIException as e:
  98. message = ['Error: {}'.format(e.error_type), e.message]
  99. show_notification(self.stdscr, message)
  100. _logger.exception(e)
  101. except requests.HTTPError as e:
  102. show_notification(self.stdscr, ['Unexpected Error'])
  103. _logger.exception(e)
  104. else:
  105. with self.loader(delay=0, message='Posting'):
  106. time.sleep(2.0)
  107. self.refresh_content()
  108. def draw_item(self, win, data, inverted=False):
  109. if data['type'] == 'MoreComments':
  110. return self.draw_more_comments(win, data)
  111. elif data['type'] == 'HiddenComment':
  112. return self.draw_more_comments(win, data)
  113. elif data['type'] == 'Comment':
  114. return self.draw_comment(win, data, inverted=inverted)
  115. else:
  116. return self.draw_submission(win, data)
  117. @staticmethod
  118. def draw_comment(win, data, inverted=False):
  119. n_rows, n_cols = win.getmaxyx()
  120. n_cols -= 1
  121. # Handle the case where the window is not large enough to fit the text.
  122. valid_rows = range(0, n_rows)
  123. offset = 0 if not inverted else -(data['n_rows'] - n_rows)
  124. row = offset
  125. if row in valid_rows:
  126. text = clean(u'{author} '.format(**data))
  127. attr = curses.A_BOLD
  128. attr |= (Color.BLUE if not data['is_author'] else Color.GREEN)
  129. win.addnstr(row, 1, text, n_cols - 1, attr)
  130. if data['flair']:
  131. text = clean(u'{flair} '.format(**data))
  132. attr = curses.A_BOLD | Color.YELLOW
  133. win.addnstr(text, n_cols - win.getyx()[1], attr)
  134. if data['likes'] is None:
  135. text, attr = BULLET, curses.A_BOLD
  136. elif data['likes']:
  137. text, attr = UARROW, (curses.A_BOLD | Color.GREEN)
  138. else:
  139. text, attr = DARROW, (curses.A_BOLD | Color.RED)
  140. win.addnstr(text, n_cols - win.getyx()[1], attr)
  141. text = clean(u' {score} {created} '.format(**data))
  142. win.addnstr(text, n_cols - win.getyx()[1])
  143. if data['gold']:
  144. text, attr = GOLD, (curses.A_BOLD | Color.YELLOW)
  145. win.addnstr(text, n_cols - win.getyx()[1], attr)
  146. n_body = len(data['split_body'])
  147. for row, text in enumerate(data['split_body'], start=offset + 1):
  148. if row in valid_rows:
  149. text = clean(text)
  150. win.addnstr(row, 1, text, n_cols - 1)
  151. # Unfortunately vline() doesn't support custom color so we have to
  152. # build it one segment at a time.
  153. attr = Color.get_level(data['level'])
  154. for y in range(n_rows):
  155. x = 0
  156. # http://bugs.python.org/issue21088
  157. if (sys.version_info.major,
  158. sys.version_info.minor,
  159. sys.version_info.micro) == (3, 4, 0):
  160. x, y = y, x
  161. win.addch(y, x, curses.ACS_VLINE, attr)
  162. return (attr | curses.ACS_VLINE)
  163. @staticmethod
  164. def draw_more_comments(win, data):
  165. n_rows, n_cols = win.getmaxyx()
  166. n_cols -= 1
  167. text = clean(u'{body}'.format(**data))
  168. win.addnstr(0, 1, text, n_cols - 1)
  169. text = clean(u' [{count}]'.format(**data))
  170. win.addnstr(text, n_cols - win.getyx()[1], curses.A_BOLD)
  171. # Unfortunately vline() doesn't support custom color so we have to
  172. # build it one segment at a time.
  173. attr = Color.get_level(data['level'])
  174. win.addch(0, 0, curses.ACS_VLINE, attr)
  175. return (attr | curses.ACS_VLINE)
  176. @staticmethod
  177. def draw_submission(win, data):
  178. n_rows, n_cols = win.getmaxyx()
  179. n_cols -= 3 # one for each side of the border + one for offset
  180. for row, text in enumerate(data['split_title'], start=1):
  181. text = clean(text)
  182. win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
  183. row = len(data['split_title']) + 1
  184. attr = curses.A_BOLD | Color.GREEN
  185. text = clean(u'{author}'.format(**data))
  186. win.addnstr(row, 1, text, n_cols, attr)
  187. attr = curses.A_BOLD | Color.YELLOW
  188. text = clean(u' {flair}'.format(**data))
  189. win.addnstr(text, n_cols - win.getyx()[1], attr)
  190. text = clean(u' {created} {subreddit}'.format(**data))
  191. win.addnstr(text, n_cols - win.getyx()[1])
  192. row = len(data['split_title']) + 2
  193. attr = curses.A_UNDERLINE | Color.BLUE
  194. text = clean(u'{url}'.format(**data))
  195. win.addnstr(row, 1, text, n_cols, attr)
  196. offset = len(data['split_title']) + 3
  197. # Cut off text if there is not enough room to display the whole post
  198. split_text = data['split_text']
  199. if data['n_rows'] > n_rows:
  200. cutoff = data['n_rows'] - n_rows + 1
  201. split_text = split_text[:-cutoff]
  202. split_text.append('(Not enough space to display)')
  203. for row, text in enumerate(split_text, start=offset):
  204. text = clean(text)
  205. win.addnstr(row, 1, text, n_cols)
  206. row = len(data['split_title']) + len(split_text) + 3
  207. text = clean(u'{score} {comments} '.format(**data))
  208. win.addnstr(row, 1, text, n_cols, curses.A_BOLD)
  209. if data['gold']:
  210. text, attr = GOLD, (curses.A_BOLD | Color.YELLOW)
  211. win.addnstr(text, n_cols - win.getyx()[1], attr)
  212. win.border()