From f28da2648c0556e9f8d9e89d22d0e5b27ac946e2 Mon Sep 17 00:00:00 2001 From: Michael Lazar Date: Sun, 25 Jan 2015 01:50:18 -0800 Subject: [PATCH] Viewer abstracted out, cursor now detects out of bounds. --- rtv/content_generators.py | 15 ++-- rtv/subreddit.py | 147 ++++++++++---------------------------- rtv/viewer.py | 115 +++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 112 deletions(-) create mode 100644 rtv/viewer.py diff --git a/rtv/content_generators.py b/rtv/content_generators.py index 828b4d5..56b0347 100644 --- a/rtv/content_generators.py +++ b/rtv/content_generators.py @@ -121,17 +121,24 @@ class SubredditGenerator(object): return data - def get(self, index, n_cols): + def get(self, index, n_cols=70): """ Grab the `i`th submission, with the title field formatted to fit inside of a window of width `n` """ - assert(index >= 0) + if index < 0: + raise IndexError while index >= len(self._submission_data): - data = self.strip_praw_submission(self._submissions.next()) - self._submission_data.append(data) + + try: + submission = self._submissions.next() + except StopIteration: + raise IndexError + else: + data = self.strip_praw_submission(submission) + self._submission_data.append(data) # Modifies the original dict, faster than copying data = self._submission_data[index] diff --git a/rtv/subreddit.py b/rtv/subreddit.py index e74c58d..70f2585 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -4,44 +4,36 @@ import curses from content_generators import SubredditGenerator from utils import curses_session +from viewer import BaseViewer -class SubredditViewer(object): +class SubredditViewer(BaseViewer): - def __init__(self, stdscr, subreddit_generator): + def __init__(self, stdscr, subreddit_content): self.stdscr = stdscr - self.gen = subreddit_generator - self._cursor_index = 0 - self._page_index = 0 - self._rows = None - self._cols = None + self._n_rows = None + self._n_cols = None self._title_window = None self._content_window = None - self._sub_windows = [] - self._direction = True - self._window_is_partial = None + self._sub_windows = None + super(SubredditViewer, self).__init__(subreddit_content) self.draw() - def loop(self): + @property + def n_subwindows(self): + return len(self._sub_windows) + def loop(self): while True: cmd = self.stdscr.getch() - # Move cursor up one submission if cmd == curses.KEY_UP: - if self._direction: - self.move_cursor_backward() - else: - self.move_cursor_forward() + self.move_cursor_up() - # Move cursor down one submission elif cmd == curses.KEY_DOWN: - if self._direction: - self.move_cursor_forward() - else: - self.move_cursor_backward() + self.move_cursor_down() # View submission elif cmd in (curses.KEY_RIGHT, ord(' ')): @@ -65,120 +57,60 @@ class SubredditViewer(object): def draw(self): # Refresh window bounds incase the screen has been resized - self._rows, self._cols = self.stdscr.getmaxyx() - self._title_window = self.stdscr.derwin(1, self._cols, 0, 0) + self._n_rows, self._n_cols = self.stdscr.getmaxyx() + self._title_window = self.stdscr.derwin(1, self._n_cols, 0, 0) self._content_window = self.stdscr.derwin(1, 0) self.draw_header() self.draw_content() - self.draw_cursor() - - def move_cursor_forward(self): - - self.remove_cursor() - - last_index = len(self._sub_windows) - 1 - - self._cursor_index += 1 - if self._cursor_index == last_index: - - if self._direction: - self._page_index = self._page_index + self._cursor_index - self._cursor_index = 0 - self._direction = False - else: - self._page_index = self._page_index - self._cursor_index - self._cursor_index = 0 - self._direction = True - self.draw_content() - - self.draw_cursor() - - def move_cursor_backward(self): - - self.remove_cursor() - - last_index = len(self._sub_windows) - 1 - - self._cursor_index -= 1 - if self._cursor_index < 0: - - if self._direction: - self._page_index -= 1 - self._cursor_index = 0 - else: - self._page_index += 1 - self._cursor_index = 0 - self.draw_content() - - self.draw_cursor() - - def draw_cursor(self): - - window = self._sub_windows[self._cursor_index] - rows, _ = window.getmaxyx() - for row in xrange(rows): - window.chgat(row, 0, 1, curses.A_REVERSE) - window.refresh() - - def remove_cursor(self): - - window = self._sub_windows[self._cursor_index] - rows, _ = window.getmaxyx() - for row in xrange(rows): - window.chgat(row, 0, 1, curses.A_NORMAL) - window.refresh() + self.add_cursor() def draw_content(self): """ Loop through submissions and fill up the content page. """ - rows, cols = self._content_window.getmaxyx() + n_rows, n_cols = self._content_window.getmaxyx() self._content_window.erase() self._sub_windows = [] - if self._direction: - row = 0 - for data in self.gen.iterate(self._page_index, 1, cols-1): - available_rows = (rows - row) - n_rows = min(available_rows, data['n_rows']) - window = self._content_window.derwin(n_rows, cols, row, 0) - self.draw_submission(window, data, self._direction) - self._sub_windows.append(window) - row += (n_rows + 1) - if row >= rows: - break - else: - row = rows - for data in self.gen.iterate(self._page_index, -1, cols-1): - available_rows = row - n_rows = min(available_rows, data['n_rows']) - window = self._content_window.derwin(n_rows, cols, row-n_rows, 0) - self.draw_submission(window, data, self._direction) - self._sub_windows.append(window) - row -= (n_rows + 1) - if row < 0: - break + page_index, cursor_index, inverted = self.nav.position + step = self.nav.step + + # If not inverted, align the first submission with the top and draw + # downwards. If inverted, align the first submission with the bottom + # and draw upwards. + current_row = n_rows if inverted else 0 + available_rows = n_rows + for data in self.content.iterate(page_index, step, n_cols-1): + window_rows = min(available_rows, data['n_rows']) + start = current_row - window_rows if inverted else current_row + window = self._content_window.derwin(window_rows, n_cols, start, 0) + self.draw_submission(window, data, inverted) + self._sub_windows.append(window) + available_rows -= (window_rows + 1) # Add one for the blank line + current_row += step * (window_rows + 1) + if available_rows <= 0: + break - self._window_is_partial = (available_rows < data['n_rows']) self._content_window.refresh() def draw_header(self): + sub_name = self.content.display_name self._title_window.erase() - self._title_window.addnstr(0, 0, self.gen.display_name, self._cols) + self._title_window.addnstr(0, 0, sub_name, self._n_cols) self._title_window.refresh() @staticmethod - def draw_submission(win, data, direction): + def draw_submission(win, data, inverted=False): n_rows, n_cols = win.getmaxyx() n_cols -= 1 # Leave space for the cursor in the first column # Handle the case where the window is not large enough to fit the data. valid_rows = range(0, n_rows) - offset = 0 if direction else -(data['n_rows'] - n_rows) + offset = 0 if not inverted else -(data['n_rows'] - n_rows) n_title = len(data['split_title']) for row, text in enumerate(data['split_title'], start=offset): @@ -197,7 +129,6 @@ class SubredditViewer(object): if row in valid_rows: win.addnstr(row, 1, '{author} {subreddit}'.format(**data), n_cols-1) - def main(): with curses_session() as stdscr: diff --git a/rtv/viewer.py b/rtv/viewer.py new file mode 100644 index 0000000..4b22dd8 --- /dev/null +++ b/rtv/viewer.py @@ -0,0 +1,115 @@ +import curses + +class Navigator(object): + """ + Handles cursor movement and screen paging. + """ + + def __init__( + self, + valid_page_cb, + page_index=0, + cursor_index=0, + inverted=False): + + self._page_cb = valid_page_cb + + self.page_index = page_index + self.cursor_index = cursor_index + self.inverted = inverted + + @property + def step(self): + return 1 if not self.inverted else -1 + + @property + def position(self): + return (self.page_index, self.cursor_index, self.inverted) + + def move(self, direction, n_windows): + "Move the cursor down (positive direction) or up (negative direction)" + + valid, redraw = True, False + + # TODO: add variable movement + forward = ((direction*self.step) > 0) + + if forward: + self.cursor_index += 1 + if self.cursor_index >= n_windows - 1: + self.page_index += (self.step * self.cursor_index) + self.cursor_index = 0 + self.inverted = not self.inverted + redraw = True + else: + if self.cursor_index > 0: + self.cursor_index -= 1 + else: + if self._is_valid(self.page_index - self.step): + self.page_index -= self.step + redraw = True + else: + valid = False + + return valid, redraw + + def _is_valid(self, page_index): + "Check if a page index will cause entries to fall outside valid range" + + try: + self._page_cb(page_index) + self._page_cb(page_index + self.step * self.cursor_index) + except IndexError: + return False + else: + return True + + +class BaseViewer(object): + """ + Base terminal viewer incorperating a cursor to navigate content + """ + + def __init__(self, content): + + self.content = content + self.nav = Navigator(self.content.get) + + def draw_content(self): + raise NotImplementedError + + @property + def n_subwindows(self): + raise NotImplementedError + + def move_cursor_up(self): + self._move_cursor(-1) + + def move_cursor_down(self): + self._move_cursor(1) + + def add_cursor(self): + self._edit_cursor(curses.A_REVERSE) + + def remove_cursor(self): + self._edit_cursor(curses.A_NORMAL) + + def _move_cursor(self, direction): + + self.remove_cursor() + + valid, redraw = self.nav.move(direction, self.n_subwindows) + if not valid: + curses.flash() + if redraw: + self.draw_content() + + self.add_cursor() + + def _edit_cursor(self, attribute): + + window = self._sub_windows[self.nav.cursor_index] + n_rows, _ = window.getmaxyx() + for row in xrange(n_rows): + window.chgat(row, 0, 1, attribute) + window.refresh() \ No newline at end of file