Don't name file 'curses' to avoid conflicts - toot - Unnamed repository; edit t… | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
commit b3ef85b42db5fbb136d85cfef02edf229c403dc0 | |
parent a0f1bff0dd9f23c09d8042ded586738c5f7ff895 | |
Author: Ivan Habunek <[email protected]> | |
Date: Mon, 24 Apr 2017 09:38:07 +0200 | |
Don't name file 'curses' to avoid conflicts | |
Diffstat: | |
toot/app.py | 251 +++++++++++++++++++++++++++++++ | |
toot/commands.py | 2 +- | |
toot/curses.py | 251 ------------------------------- | |
3 files changed, 252 insertions(+), 252 deletions(-) | |
--- | |
diff --git a/toot/app.py b/toot/app.py | |
@@ -0,0 +1,251 @@ | |
+# -*- coding: utf-8 -*- | |
+from __future__ import unicode_literals | |
+from __future__ import print_function | |
+ | |
+import curses | |
+import re | |
+import webbrowser | |
+ | |
+from bs4 import BeautifulSoup | |
+from textwrap import wrap | |
+ | |
+ | |
+class Color: | |
+ @staticmethod | |
+ def setup_palette(): | |
+ curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) | |
+ curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) | |
+ curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) | |
+ | |
+ @staticmethod | |
+ def blue(): | |
+ return curses.color_pair(1) | |
+ | |
+ @staticmethod | |
+ def green(): | |
+ return curses.color_pair(2) | |
+ | |
+ @staticmethod | |
+ def yellow(): | |
+ return curses.color_pair(3) | |
+ | |
+ | |
+class TimelineApp: | |
+ def __init__(self, status_generator): | |
+ self.status_generator = status_generator | |
+ self.statuses = [] | |
+ self.selected = None | |
+ | |
+ def run(self): | |
+ curses.wrapper(self._wrapped_run) | |
+ | |
+ def _wrapped_run(self, stdscr): | |
+ self.left_width = 60 | |
+ self.right_width = curses.COLS - self.left_width | |
+ | |
+ # Setup windows | |
+ self.top = curses.newwin(2, curses.COLS, 0, 0) | |
+ self.left = curses.newpad(curses.LINES * 2, self.left_width) | |
+ self.right = curses.newwin(curses.LINES - 4, self.right_width, 2, self… | |
+ self.bottom = curses.newwin(2, curses.COLS, curses.LINES - 2, 0) | |
+ | |
+ Color.setup_palette() | |
+ | |
+ # Load some data and redraw | |
+ self.fetch_next() | |
+ self.selected = 0 | |
+ self.full_redraw() | |
+ | |
+ self.loop() | |
+ | |
+ def loop(self): | |
+ while True: | |
+ key = self.left.getkey() | |
+ | |
+ if key.lower() == 'q': | |
+ return | |
+ | |
+ elif key.lower() == 'v': | |
+ status = self.get_selected_status() | |
+ if status: | |
+ webbrowser.open(status['url']) | |
+ | |
+ elif key.lower() == 'j' or key == curses.KEY_DOWN: | |
+ self.select_next() | |
+ | |
+ elif key.lower() == 'k' or key == curses.KEY_UP: | |
+ self.select_previous() | |
+ | |
+ def select_previous(self): | |
+ """Move to the previous status in the timeline.""" | |
+ if self.selected == 0: | |
+ return | |
+ | |
+ old_index = self.selected | |
+ new_index = self.selected - 1 | |
+ | |
+ self.selected = new_index | |
+ self.redraw_after_selection_change(old_index, new_index) | |
+ | |
+ def select_next(self): | |
+ """Move to the next status in the timeline.""" | |
+ if self.selected + 1 >= len(self.statuses): | |
+ return | |
+ | |
+ old_index = self.selected | |
+ new_index = self.selected + 1 | |
+ | |
+ self.selected = new_index | |
+ self.redraw_after_selection_change(old_index, new_index) | |
+ | |
+ def redraw_after_selection_change(self, old_index, new_index): | |
+ old_status = self.statuses[old_index] | |
+ new_status = self.statuses[new_index] | |
+ | |
+ # Perform a partial redraw | |
+ self.draw_status_row(self.left, old_status, 3 * old_index - 1, False) | |
+ self.draw_status_row(self.left, new_status, 3 * new_index - 1, True) | |
+ self.draw_status_details(self.right, new_status) | |
+ | |
+ def fetch_next(self): | |
+ try: | |
+ statuses = self.status_generator.__next__() | |
+ except StopIteration: | |
+ return None | |
+ | |
+ for status in statuses: | |
+ self.statuses.append(parse_status(status)) | |
+ | |
+ return len(statuses) | |
+ | |
+ def full_redraw(self): | |
+ """Perform a full redraw of the UI.""" | |
+ self.left.clear() | |
+ self.right.clear() | |
+ self.top.clear() | |
+ self.bottom.clear() | |
+ | |
+ self.left.box() | |
+ self.right.box() | |
+ | |
+ self.top.addstr(" toot - your Mastodon command line interface\n", Colo… | |
+ self.top.addstr(" https://github.com/ihabunek/toot") | |
+ | |
+ self.draw_statuses(self.left) | |
+ self.draw_status_details(self.right, self.get_selected_status()) | |
+ self.draw_usage(self.bottom) | |
+ | |
+ self.left.refresh(0, 0, 2, 0, curses.LINES - 4, self.left_width) | |
+ | |
+ self.right.refresh() | |
+ self.top.refresh() | |
+ self.bottom.refresh() | |
+ | |
+ def draw_usage(self, window): | |
+ # Show usage on the bottom | |
+ window.addstr("Usage: | ") | |
+ window.addch("j", Color.green()) | |
+ window.addstr(" next | ") | |
+ window.addch("k", Color.green()) | |
+ window.addstr(" previous | ") | |
+ window.addch("v", Color.green()) | |
+ window.addstr(" open in browser | ") | |
+ window.addch("q", Color.green()) | |
+ window.addstr(" quit") | |
+ | |
+ window.refresh() | |
+ | |
+ def get_selected_status(self): | |
+ if len(self.statuses) > self.selected: | |
+ return self.statuses[self.selected] | |
+ | |
+ def draw_status_row(self, window, status, offset, highlight=False): | |
+ width = window.getmaxyx()[1] | |
+ color = Color.blue() if highlight else 0 | |
+ | |
+ date, time = status['created_at'] | |
+ window.addstr(offset + 2, 2, date, color) | |
+ window.addstr(offset + 3, 2, time, color) | |
+ | |
+ window.addstr(offset + 2, 15, status['author']['acct'], color) | |
+ window.addstr(offset + 3, 15, status['author']['display_name'], color) | |
+ | |
+ window.addstr(offset + 4, 1, '─' * (width - 2)) | |
+ | |
+ window.refresh(0, 0, 2, 0, curses.LINES - 4, self.left_width) | |
+ | |
+ | |
+ def draw_statuses(self, window): | |
+ for index, status in enumerate(self.statuses): | |
+ offset = 3 * index - 1 | |
+ highlight = self.selected == index | |
+ self.draw_status_row(window, status, offset, highlight) | |
+ | |
+ def draw_status_details(self, window, status): | |
+ window.erase() | |
+ window.box() | |
+ | |
+ acct = status['author']['acct'] | |
+ name = status['author']['display_name'] | |
+ | |
+ window.addstr(1, 2, "@" + acct, Color.green()) | |
+ window.addstr(2, 2, name, Color.yellow()) | |
+ | |
+ text_width = self.right_width - 4 | |
+ | |
+ y = 4 | |
+ for line in status['lines']: | |
+ for wrapped in wrap(line, text_width): | |
+ window.addstr(y, 2, wrapped.ljust(text_width)) | |
+ y += 1 | |
+ y += 1 | |
+ | |
+ window.addstr(y, 2, '─' * text_width) | |
+ y += 1 | |
+ | |
+ window.addstr(y, 2, status['url']) | |
+ y += 1 | |
+ | |
+ if status['boosted_by']: | |
+ acct = status['boosted_by']['acct'] | |
+ window.addstr(y, 2, "Boosted by ") | |
+ window.addstr("@", Color.green()) | |
+ window.addstr(acct, Color.green()) | |
+ y += 1 | |
+ | |
+ window.refresh() | |
+ | |
+ | |
+def parse_status(status): | |
+ content = status['reblog']['content'] if status['reblog'] else status['con… | |
+ account = parse_account(status['reblog']['account'] if status['reblog'] el… | |
+ boosted_by = parse_account(status['account']) if status['reblog'] else None | |
+ | |
+ lines = parse_html(content) | |
+ | |
+ created_at = status['created_at'][:19].split('T') | |
+ | |
+ return { | |
+ 'author': account, | |
+ 'boosted_by': boosted_by, | |
+ 'lines': lines, | |
+ 'url': status['url'], | |
+ 'created_at': created_at, | |
+ } | |
+ | |
+ | |
+def parse_account(account): | |
+ return { | |
+ 'id': account['id'], | |
+ 'acct': account['acct'], | |
+ 'display_name': account['display_name'], | |
+ } | |
+ | |
+ | |
+def parse_html(html): | |
+ """Attempt to convert html to plain text while keeping line breaks""" | |
+ return [ | |
+ BeautifulSoup(l, "html.parser").get_text().replace(''', "'") | |
+ for l in re.split("</?p[^>]*>", html) | |
+ if l | |
+ ] | |
diff --git a/toot/commands.py b/toot/commands.py | |
@@ -15,7 +15,7 @@ from textwrap import TextWrapper, wrap | |
from toot import api, config, DEFAULT_INSTANCE, User, App, ConsoleError | |
from toot.output import green, yellow, print_error | |
-from toot.curses import TimelineApp | |
+from toot.app import TimelineApp | |
def register_app(instance): | |
diff --git a/toot/curses.py b/toot/curses.py | |
@@ -1,251 +0,0 @@ | |
-# -*- coding: utf-8 -*- | |
-from __future__ import unicode_literals | |
-from __future__ import print_function | |
- | |
-import curses | |
-import re | |
-import webbrowser | |
- | |
-from bs4 import BeautifulSoup | |
-from textwrap import wrap | |
- | |
- | |
-class Color: | |
- @staticmethod | |
- def setup_palette(): | |
- curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK) | |
- curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) | |
- curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) | |
- | |
- @staticmethod | |
- def blue(): | |
- return curses.color_pair(1) | |
- | |
- @staticmethod | |
- def green(): | |
- return curses.color_pair(2) | |
- | |
- @staticmethod | |
- def yellow(): | |
- return curses.color_pair(3) | |
- | |
- | |
-class TimelineApp: | |
- def __init__(self, status_generator): | |
- self.status_generator = status_generator | |
- self.statuses = [] | |
- self.selected = None | |
- | |
- def run(self): | |
- curses.wrapper(self._wrapped_run) | |
- | |
- def _wrapped_run(self, stdscr): | |
- self.left_width = 60 | |
- self.right_width = curses.COLS - self.left_width | |
- | |
- # Setup windows | |
- self.top = curses.newwin(2, curses.COLS, 0, 0) | |
- self.left = curses.newpad(curses.LINES * 2, self.left_width) | |
- self.right = curses.newwin(curses.LINES - 4, self.right_width, 2, self… | |
- self.bottom = curses.newwin(2, curses.COLS, curses.LINES - 2, 0) | |
- | |
- Color.setup_palette() | |
- | |
- # Load some data and redraw | |
- self.fetch_next() | |
- self.selected = 0 | |
- self.full_redraw() | |
- | |
- self.loop() | |
- | |
- def loop(self): | |
- while True: | |
- key = self.left.getkey() | |
- | |
- if key.lower() == 'q': | |
- return | |
- | |
- elif key.lower() == 'v': | |
- status = self.get_selected_status() | |
- if status: | |
- webbrowser.open(status['url']) | |
- | |
- elif key.lower() == 'j' or key == curses.KEY_DOWN: | |
- self.select_next() | |
- | |
- elif key.lower() == 'k' or key == curses.KEY_UP: | |
- self.select_previous() | |
- | |
- def select_previous(self): | |
- """Move to the previous status in the timeline.""" | |
- if self.selected == 0: | |
- return | |
- | |
- old_index = self.selected | |
- new_index = self.selected - 1 | |
- | |
- self.selected = new_index | |
- self.redraw_after_selection_change(old_index, new_index) | |
- | |
- def select_next(self): | |
- """Move to the next status in the timeline.""" | |
- if self.selected + 1 >= len(self.statuses): | |
- return | |
- | |
- old_index = self.selected | |
- new_index = self.selected + 1 | |
- | |
- self.selected = new_index | |
- self.redraw_after_selection_change(old_index, new_index) | |
- | |
- def redraw_after_selection_change(self, old_index, new_index): | |
- old_status = self.statuses[old_index] | |
- new_status = self.statuses[new_index] | |
- | |
- # Perform a partial redraw | |
- self.draw_status_row(self.left, old_status, 3 * old_index - 1, False) | |
- self.draw_status_row(self.left, new_status, 3 * new_index - 1, True) | |
- self.draw_status_details(self.right, new_status) | |
- | |
- def fetch_next(self): | |
- try: | |
- statuses = self.status_generator.__next__() | |
- except StopIteration: | |
- return None | |
- | |
- for status in statuses: | |
- self.statuses.append(parse_status(status)) | |
- | |
- return len(statuses) | |
- | |
- def full_redraw(self): | |
- """Perform a full redraw of the UI.""" | |
- self.left.clear() | |
- self.right.clear() | |
- self.top.clear() | |
- self.bottom.clear() | |
- | |
- self.left.box() | |
- self.right.box() | |
- | |
- self.top.addstr(" toot - your Mastodon command line interface\n", Colo… | |
- self.top.addstr(" https://github.com/ihabunek/toot") | |
- | |
- self.draw_statuses(self.left) | |
- self.draw_status_details(self.right, self.get_selected_status()) | |
- self.draw_usage(self.bottom) | |
- | |
- self.left.refresh(0, 0, 2, 0, curses.LINES - 4, self.left_width) | |
- | |
- self.right.refresh() | |
- self.top.refresh() | |
- self.bottom.refresh() | |
- | |
- def draw_usage(self, window): | |
- # Show usage on the bottom | |
- window.addstr("Usage: | ") | |
- window.addch("j", Color.green()) | |
- window.addstr(" next | ") | |
- window.addch("k", Color.green()) | |
- window.addstr(" previous | ") | |
- window.addch("v", Color.green()) | |
- window.addstr(" open in browser | ") | |
- window.addch("q", Color.green()) | |
- window.addstr(" quit") | |
- | |
- window.refresh() | |
- | |
- def get_selected_status(self): | |
- if len(self.statuses) > self.selected: | |
- return self.statuses[self.selected] | |
- | |
- def draw_status_row(self, window, status, offset, highlight=False): | |
- width = window.getmaxyx()[1] | |
- color = Color.blue() if highlight else 0 | |
- | |
- date, time = status['created_at'] | |
- window.addstr(offset + 2, 2, date, color) | |
- window.addstr(offset + 3, 2, time, color) | |
- | |
- window.addstr(offset + 2, 15, status['author']['acct'], color) | |
- window.addstr(offset + 3, 15, status['author']['display_name'], color) | |
- | |
- window.addstr(offset + 4, 1, '─' * (width - 2)) | |
- | |
- window.refresh(0, 0, 2, 0, curses.LINES - 4, self.left_width) | |
- | |
- | |
- def draw_statuses(self, window): | |
- for index, status in enumerate(self.statuses): | |
- offset = 3 * index - 1 | |
- highlight = self.selected == index | |
- self.draw_status_row(window, status, offset, highlight) | |
- | |
- def draw_status_details(self, window, status): | |
- window.erase() | |
- window.box() | |
- | |
- acct = status['author']['acct'] | |
- name = status['author']['display_name'] | |
- | |
- window.addstr(1, 2, "@" + acct, Color.green()) | |
- window.addstr(2, 2, name, Color.yellow()) | |
- | |
- text_width = self.right_width - 4 | |
- | |
- y = 4 | |
- for line in status['lines']: | |
- for wrapped in wrap(line, text_width): | |
- window.addstr(y, 2, wrapped.ljust(text_width)) | |
- y += 1 | |
- y += 1 | |
- | |
- window.addstr(y, 2, '─' * text_width) | |
- y += 1 | |
- | |
- window.addstr(y, 2, status['url']) | |
- y += 1 | |
- | |
- if status['boosted_by']: | |
- acct = status['boosted_by']['acct'] | |
- window.addstr(y, 2, "Boosted by ") | |
- window.addstr("@", Color.green()) | |
- window.addstr(acct, Color.green()) | |
- y += 1 | |
- | |
- window.refresh() | |
- | |
- | |
-def parse_status(status): | |
- content = status['reblog']['content'] if status['reblog'] else status['con… | |
- account = parse_account(status['reblog']['account'] if status['reblog'] el… | |
- boosted_by = parse_account(status['account']) if status['reblog'] else None | |
- | |
- lines = parse_html(content) | |
- | |
- created_at = status['created_at'][:19].split('T') | |
- | |
- return { | |
- 'author': account, | |
- 'boosted_by': boosted_by, | |
- 'lines': lines, | |
- 'url': status['url'], | |
- 'created_at': created_at, | |
- } | |
- | |
- | |
-def parse_account(account): | |
- return { | |
- 'id': account['id'], | |
- 'acct': account['acct'], | |
- 'display_name': account['display_name'], | |
- } | |
- | |
- | |
-def parse_html(html): | |
- """Attempt to convert html to plain text while keeping line breaks""" | |
- return [ | |
- BeautifulSoup(l, "html.parser").get_text().replace(''', "'") | |
- for l in re.split("</?p[^>]*>", html) | |
- if l | |
- ] |