Source code for pylib.tui

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""TUI module.

:Date: 2020-01-13

.. module:: tui
  :platform: *nix, Windows
  :synopsis: TUI module.

.. moduleauthor:: Daniel Weschke <daniel.weschke@directbox.de>
"""
# TODO: two classes to distinguish between curses (main tui) and
#       window (to handle sub windows)?
# TODO: width resizing, min width now 6 because of text and border
# TODO: height resizing, min height
import sys
import curses


[docs]def newwin(height, width, y, x): win = Window() win.window = curses.newwin(height, width, y, x) return win
[docs]class StdOutWrapper: """Send print to stdout (print to the standard console). :Example: :: # catch all prints into StdOutWrapper with StdOutWrapper() as mystdout: # begin curses (curses.initscr(), ...) # do your stuff here # print("foo") # you can also output mystdout.get_text() in a ncurses widget in # runtime # end curses (curses.endwin()) # go back to normal state and print all catched prints to stdout """ # https://stackoverflow.com/a/14010948 def __init__(self): self.text = "" def __enter__(self): """catch all prints into StdOutWrapper """ #self.stdout = StdOutWrapper() sys.stdout = self sys.stderr = self return self # to use as: with StdOutWrapper() as mystdout: def __exit__(self, etype, value, traceback): """go back to normal state and print all catched prints to stdout """ sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ sys.stdout.write(self.get_text())
[docs] def write(self, txt): """print uses write() """ self.text += txt self.text = '\n'.join(self.text.split('\n')[-30:])
[docs] def get_text(self): return '\n'.join(self.text.split('\n'))
#def get_text(self,beg,end): # return '\n'.join(self.text.split('\n')[beg:end])
[docs]class App: """TUI text-based user interface initscr is the encapsulation window object of the stdscr stdsc is the curses.initscr """ def __init__(self, delay=5): """ :param delay: sets the curses.halfdelay, value between 1 and 255. :param type: int """ if not isinstance(delay, int): try: delay = int(delay) except ValueError: print("App(delay)") print("Could not convert the argument to an integer.") # Define the appearance of some interface elements hotkey_attr = curses.A_BOLD | curses.A_UNDERLINE menu_attr = curses.A_NORMAL self.initscr = Window() self.initscr.initscr() self.stdscr = self.initscr.window #print(type(self.stdscr)) #print(type(self.initscr)) try: curses.cbreak() curses.halfdelay(delay) # How many tenths of a second are waited, from 1 to 255 #stdscr.nodelay(1) curses.noecho() self.cursor_visibility = 0 # Set the cursor state. visibility can be set to 0, 1, or 2, for invisible, normal, or very visible. curses.curs_set(self.cursor_visibility) self.stdscr.keypad(1) curses.mousemask(1) # activate mouse events # init colors if curses.has_colors(): curses.start_color() # initializes 8 basic colors. 0:black, 1:red, 2:green, 3:yellow, 4:blue, 5:magenta, 6:cyan, and 7:white. curses.use_default_colors() for i in range(0, curses.COLORS): curses.init_pair(i + 1, i, -1) # Color pair 0 is hard-wired to white on black, and cannot be changed. except: pass self.last_pressed_ch = 0 # getch() Note that the integer returned does not have to be in ASCII range: function keys, keypad keys and so on return numbers higher than 256. self.last_pressed_mouse_ch = 0
[docs] def main_loop(self): with StdOutWrapper(): try: print('kkkk') while True: self.clear() print('kkkk') if self.last_pressed_ch == ord('q'): break self.refresh() # update the screen print('kkkk') self.getch() except Exception as e: print(e) self.end()
[docs] def refresh(self): self.initscr.refresh() # update the screen
[docs] def getch(self): # keep last key value last_pressed_ch_current = self.initscr.getch() if last_pressed_ch_current != curses.ERR: # keep last key while window resizing if last_pressed_ch_current == curses.KEY_MOUSE: self.last_pressed_mouse_ch = last_pressed_ch_current if last_pressed_ch_current in [curses.KEY_MOUSE, curses.KEY_RESIZE]: pass else: self.last_pressed_ch = last_pressed_ch_current
[docs] def color_table(self, window=None): """Print all available colors with default background. Check if curses.has_colors() is True. """ # TODO: position # FIXME: use own text method? # FIXME: build full str first and then print? if not curses.has_colors(): return if window is None: window = self.stdscr window.addstr(2, 1, '{0} colors available'.format(curses.COLORS)) if curses.can_change_color(): window.addstr(' (can change color: {0})'.format(curses.can_change_color())) _, maxx = window.getmaxyx() step_size = 4 maxx = maxx - maxx % step_size x = 0 y = 3 try: for i in range(0, curses.COLORS): window.addstr(y, x, '{0:{1}}'.format(i, step_size), curses.color_pair(i)) x = (x + step_size) % maxx if x == 0: y += 1 except: # End of screen reached pass
[docs] def color_def(self): if curses.can_change_color(): # if True curses.init_color(color_number, r, g, b) can be used pass
# changes colors for the terminal also after closing the program. #curses.color_content(0) #tmp = curses.color_content(1) #print(curses.color_content(1)) #curses.init_color(0, 1000, 0, 0) # color_number = init_pair number - 1 (maybe because init_pair number 0 is hard-wired and not changeable) #curses.init_color(1, 1000, 500, 1000) #print(curses.color_content(1)) #curses.init_color(1, *tmp) #print(curses.color_content(1))
[docs] def clear(self): self.stdscr.clear()
[docs] def end(self): """clean up""" self.stdscr.clear() curses.nocbreak() self.stdscr.keypad(0) curses.echo() curses.endwin()
[docs]class Window: """Window """ def __init__(self): self.window = None
[docs] def initscr(self): self.window = curses.initscr()
[docs] def derwin(self, height, width, y, x): win = Window() win.window = self.window.derwin(height, width, y, x) return win
[docs] def clear(self): return self.window.clear()
[docs] def getmaxyx(self): return self.window.getmaxyx()
[docs] def getch(self): return self.window.getch()
[docs] def instr(self, y, x, n): return self.window.instr(y, x, n).decode()
[docs] def text(self, string, padding_left=0, padding_top=0, attribute=0, color_pair=0): r"""Test to screen. If multiline than keep the x position for each new line. :Example: :: text(stdscr, 2, 1, "1 - Show test page") text(stdscr, 3, 1, "h - Show help page") text(stdscr, 4, 1, "q - Exit") text(stdscr, 2, 1, "1 - Show test page\\n" + "h - Show help page\\n" + "q - Exit") .. note:: Writing in the last char of the window (last row bottom and last column right) is suppressed """ win_height, win_width = self.window.getmaxyx() if win_width-padding_left < 2: # unicode can have 2 char width return yi = padding_top #for row in string.split("\n"): # window.addnstr(yi, x, row, win_width-x-1, curses.color_pair(9)) # 5 # yi += 1 for row in string.split("\n"): # TODO: os.linesep? xi = padding_left for char in row: # write only inside the window if not yi >= win_height and not xi >= win_width: # do not write in the last char of window (last row bottom and last column right) # unicodes may use multiple chars, this will raise an error for the last char in the window if yi == win_height-1 and xi >= win_width-1: break #if yi == win_height-1 and xi >= win_width-1: # try: # char.encode("ascii") # except UnicodeEncodeError: # break # dont add str if empty braille char or simple space character if char != chr(0x2800) and char != " " and xi < win_width and yi < win_height: self.window.addstr(yi, xi, char, attribute|curses.color_pair(color_pair)) xi += 1 yi += 1
[docs] def border(self, title="", footer_left="", footer_right="", style="horizontal"): """Set border around the window with optional title and footer labels. :param window: the window to draw a border :type window: curses.window :param title: the title for the window (default = "") :type title: str :param footer_left: the footer label (default = ""). If footer_left is a list than every element of the list will be printed sperated by one column. This is useful to not overwright the border with a space character. :type footer_left: str or list """ win_height, win_width = self.window.getmaxyx() if win_width < 3: return if isinstance(style, str): if style == "horizontal": self.window.border( ' ', ' ', curses.ACS_HLINE, curses.ACS_HLINE, curses.ACS_HLINE, curses.ACS_HLINE, curses.ACS_HLINE, curses.ACS_HLINE, ) elif style == "full": self.window.border(0, 0, 0, 0, 0, 0, 0, 0, ) else: # list self.window.border(*style) if title: self.text(title, 1) if footer_left: last_win_row = win_height - 1 if isinstance(footer_left, str): self.text(footer_left, 1, last_win_row) else: # list pos = 1 for footer_left_i in footer_left: self.text(footer_left_i, pos, win_height-1) pos += len(footer_left_i)+1 if footer_right: self.text(footer_right, win_width - 1 - len(footer_right), last_win_row)
[docs] def textbox(self, height, width, y, x, borders=False): """Add sub window. :param parent_window: the parent window :type parent_window: curses.window :param height: the height of the sub window. The reference point of the sub window is the top left corner. :type height: int :param width: the width of the sub window. The reference point of the sub window is the top left corner. :type width: int :param y: the y coordinate (position) of the sub window. Start from the top. :type y: int :param x: the x coordinate (position) of the sub window. Start from the left. :returns: the sub window content and decoration :rtype: (curses.window, curses.window) """ # TODO: not to work with two window object but one with the # possibility to access the derwin(s). Either self.derwins # or another DoubleWindow class for border and text or do it # in this class? screen_height, screen_width = self.window.getmaxyx() newwin_width = screen_width-x if x+width > screen_width else width newwin_height = screen_height-y if y+height > screen_height else height if newwin_width > 0 and newwin_height > 0: win = self.derwin(newwin_height, newwin_width, y, x) if borders: win.border(style="full") if newwin_width-2 > 0 and newwin_height-1 > 0: subwin = win.derwin(newwin_height-1, newwin_width-2, 1, 1) return subwin, win # text and border return newwin(1, 1, 0, 0), win # only border, no text (top left 1 char) return win, newwin(1, 1, 0, 0) # only text, no border (top left 1 char) return newwin(1, 1, 0, 0), newwin(1, 1, 0, 0) # no border and no text (top left 1 char)
[docs] def refresh(self): self.window.refresh()