######### # GLOBALS ######### import os import sys import shutil import yaml import urwid from urwid.widget import BOX, FLOW, FIXED ######### # HELPERS ######### CURR_DIR = os.path.dirname(os.path.realpath(__file__)) is_not_dumb = os.getenv("TERM", "dumb").lower() != "dumb" # Scroll actions SCROLL_LINE_UP = "line up" SCROLL_LINE_DOWN = "line down" SCROLL_PAGE_UP = "page up" SCROLL_PAGE_DOWN = "page down" SCROLL_TO_TOP = "to top" SCROLL_TO_END = "to end" # ASCII color codes YELLOW = '\033[33m' if is_not_dumb else '' RED = "\033[31m" if is_not_dumb else '' BOLD = '\033[1m' if is_not_dumb else '' UNDERLINE = '\033[4m' if is_not_dumb else '' END = "\033[0m" if is_not_dumb else '' class Scrollable(urwid.WidgetDecoration): # TODO: Fix scrolling behavior (works with up/down keys, not with cursor) <--- Now works with mouse though def sizing(self): return frozenset([BOX]) def selectable(self): return True def __init__(self, widget): """ Box widget (wrapper) that makes a fixed or flow widget vertically scrollable. """ self._trim_top = 0 self._scroll_action = None self._forward_keypress = None self._old_cursor_coords = None self._rows_max_cached = 0 self.__super.__init__(widget) def render(self, size, focus=False): maxcol, maxrow = size # Render complete original widget ow = self._original_widget ow_size = self._get_original_widget_size(size) canv = urwid.CompositeCanvas(ow.render(ow_size, focus)) canv_cols, canv_rows = canv.cols(), canv.rows() if canv_cols <= maxcol: pad_width = maxcol - canv_cols if pad_width > 0: # Canvas is narrower than available horizontal space canv.pad_trim_left_right(0, pad_width) if canv_rows <= maxrow: fill_height = maxrow - canv_rows if fill_height > 0: # Canvas is lower than available vertical space canv.pad_trim_top_bottom(0, fill_height) if canv_cols <= maxcol and canv_rows <= maxrow: # Canvas is small enough to fit without trimming return canv self._adjust_trim_top(canv, size) # Trim canvas if necessary trim_top = self._trim_top trim_end = canv_rows - maxrow - trim_top trim_right = canv_cols - maxcol if trim_top > 0: canv.trim(trim_top) if trim_end > 0: canv.trim_end(trim_end) if trim_right > 0: canv.pad_trim_left_right(0, -trim_right) # Disable cursor display if cursor is outside of visible canvas parts if canv.cursor is not None: curscol, cursrow = canv.cursor if cursrow >= maxrow or cursrow < 0: canv.cursor = None # Let keypress() know if original_widget should get keys self._forward_keypress = bool(canv.cursor) return canv def mouse_event(self, size, event, button, col, row, focus): if 'press' in event.split(' '): if button in (4,5): self.keypress(size, SCROLL_PAGE_DOWN if button == 5 else SCROLL_PAGE_UP) def keypress(self, size, key): if self._forward_keypress: ow = self._original_widget ow_size = self._get_original_widget_size(size) # Remember previous cursor position if possible if hasattr(ow, "get_cursor_coords"): self._old_cursor_coords = ow.get_cursor_coords(ow_size) key = ow.keypress(ow_size, key) if key is None: return None # Handle up/down, page up/down, etc command_map = self._command_map if command_map[key] == urwid.CURSOR_UP: self._scroll_action = SCROLL_LINE_UP elif command_map[key] == urwid.CURSOR_DOWN: self._scroll_action = SCROLL_LINE_DOWN elif command_map[key] == urwid.CURSOR_PAGE_UP: self._scroll_action = SCROLL_PAGE_UP elif command_map[key] == urwid.CURSOR_PAGE_DOWN: self._scroll_action = SCROLL_PAGE_DOWN elif command_map[key] == urwid.CURSOR_MAX_LEFT: # "home" self._scroll_action = SCROLL_TO_TOP elif command_map[key] == urwid.CURSOR_MAX_RIGHT: # "end" self._scroll_action = SCROLL_TO_END else: return key self._invalidate() def mouse_event(self, size, event, button, col, row, focus): ow = self._original_widget if hasattr(ow, "mouse_event"): ow_size = self._get_original_widget_size(size) row += self._trim_top return ow.mouse_event(ow_size, event, button, col, row, focus) else: return False def _adjust_trim_top(self, canv, size): """ Adjust self._trim_top according to self._scroll_action """ action = self._scroll_action self._scroll_action = None maxcol, maxrow = size trim_top = self._trim_top canv_rows = canv.rows() if trim_top < 0: # Negative trim_top values use bottom of canvas as reference trim_top = canv_rows - maxrow + trim_top + 1 if canv_rows <= maxrow: self._trim_top = 0 # Reset scroll position return def ensure_bounds(new_trim_top): return max(0, min(canv_rows - maxrow, new_trim_top)) if action == SCROLL_LINE_UP: self._trim_top = ensure_bounds(trim_top - 1) elif action == SCROLL_LINE_DOWN: self._trim_top = ensure_bounds(trim_top + 1) elif action == SCROLL_PAGE_UP: self._trim_top = ensure_bounds(trim_top - maxrow + 1) elif action == SCROLL_PAGE_DOWN: self._trim_top = ensure_bounds(trim_top + maxrow - 1) elif action == SCROLL_TO_TOP: self._trim_top = 0 elif action == SCROLL_TO_END: self._trim_top = canv_rows - maxrow else: self._trim_top = ensure_bounds(trim_top) if self._old_cursor_coords is not None and self._old_cursor_coords != canv.cursor: self._old_cursor_coords = None curscol, cursrow = canv.cursor if cursrow < self._trim_top: self._trim_top = cursrow elif cursrow >= self._trim_top + maxrow: self._trim_top = max(0, cursrow - maxrow + 1) def _get_original_widget_size(self, size): ow = self._original_widget sizing = ow.sizing() if FIXED in sizing: return () elif FLOW in sizing: return size[0], def get_scrollpos(self, size=None, focus=False): return self._trim_top def set_scrollpos(self, position): self._trim_top = int(position) self._invalidate() def rows_max(self, size=None, focus=False): if size is not None: ow = self._original_widget ow_size = self._get_original_widget_size(size) sizing = ow.sizing() if FIXED in sizing: self._rows_max_cached = ow.pack(ow_size, focus)[1] elif FLOW in sizing: self._rows_max_cached = ow.rows(ow_size, focus) else: raise RuntimeError("Not a flow/box widget: %r" % self._original_widget) return self._rows_max_cached class App(object): def __init__(self, content): self._palette = [ ("menu", "black", "light cyan", "standout"), ("title", "default,bold", "default", "bold") ] menu = urwid.Text([u'\n', ("menu", u" Q "), ("light gray", u" Quit")]) # TODO: Make like man pages (vim input) layout = urwid.Frame(body=content, footer=menu) main_loop = urwid.MainLoop(layout, self._palette, unhandled_input=App._handle_input, handle_mouse=True) main_loop.run() @staticmethod def _handle_input(inp): if inp in ('q', 'Q'): raise urwid.ExitMainLoop() def output_without_ui(content): size = shutil.get_terminal_size() canvas = content.render(size) text = ("\n".join(text.decode("utf-8") for text in canvas.text)).rstrip() print(text) def generate_content(status_code): try: code_descriptions, num, status_code = get_yaml_dictionary(status_code) content = code_descriptions[status_code] pile = urwid.Pile([ urwid.Text("STATCODE: The Manual for HTTP Status Codes and Headers\n", align="center"), urwid.Text(("title", "STATUS MESSAGE" if num else "HEADER INFO")), urwid.Padding( urwid.Text(''.join([str(status_code), ": " if num else ", Example= ", content["message"], '\n'])), left=5), urwid.Text(("title", "CATEGORY")), urwid.Padding(urwid.Text(''.join([content["category"], '\n'])), left=5), urwid.Text(("title", "DESCRIPTION")), urwid.Padding(urwid.Text(''.join([content["description"], '\n'])), left=5), urwid.Text(("title", "COPYRIGHT")), urwid.Padding(urwid.Text(''.join([__load_file_data(num), '\n'])), left=5), ]) padding = urwid.Padding(Scrollable(pile), left=1, right=1) return padding except KeyError: # None is used to print "not recognized", so KeyError. Other errors have nothing to do with it return None def __load_file_data(num): copyleft = yaml.safe_load(open('/'.join([CURR_DIR, "copyright_description.yml"]), 'r')) if num: return copyleft['statuscode'] else: return copyleft['headers'] def get_yaml_dictionary(status_code): try: status_code = int(status_code) num = True filename = "code_descriptions.yml" except (TypeError, ValueError): num = False filename = "header_descriptions.yml" try: code_descriptions = yaml.safe_load( open('/'.join([CURR_DIR, filename]), 'r')) except yaml.constructor.ConstructorError: print("Invalid file. Only support valid json and yaml files.") sys.exit(1) return code_descriptions, num, status_code def print_help(): print(''.join([BOLD, "statcode v1.0.0 – Made by @shobrook", END, '\n'])) print("Like man pages, but for HTTP status codes.\n") print(''.join([UNDERLINE, "Usage:", END, " $ statcode ", YELLOW, "status_code", END])) print(''.join([BOLD, "-h, --help:", END, " prints this help"])) print(''.join([BOLD, "-a,-l, --all,--list statucode", END, " prints all codes in compact version"])) print(''.join([BOLD, "-a,-l, --all,--list headers", END, " prints all headers in compact version"])) print(''.join([BOLD, "-n, --no-ui", END, " force output without UI"])) def print_all(status_code): if status_code == "statuscode": code_descriptions, num, status_code = get_yaml_dictionary(200) else: code_descriptions, num, status_code = get_yaml_dictionary("Accept") del status_code for k, v in code_descriptions.items(): print(''.join([RED, str(k), ':', END, " ", v["message"] if num else ""])) ###### # MAIN ###### def main(): if len(sys.argv) == 1 or sys.argv[1].lower() in ("-h", "--help"): print_help() elif sys.argv[1].lower() in ("-a", "-l", "--all", "--list"): try: status_code = sys.argv[2] if status_code not in ("statuscode", "headers"): print(''.join([BOLD, "Wrong parameter for this usage, see help", END])) return print_all(status_code) except IndexError: print_help() else: status_code = sys.argv[1] without_ui = len(sys.argv) > 2 and sys.argv[2].lower() in ("-n", "--no-ui") content = generate_content(status_code) if content: if without_ui or not is_not_dumb: output_without_ui(content) else: try: App(content) # Opens interface except NameError: output_without_ui(content) else: print(''.join([RED, "Sorry, statcode doesn't recognize: ", status_code, END])) return