Files
pylib/pylib/tui.py

384 lines
12 KiB
Python

#!/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
def newwin(height, width, y, x):
win = Window()
win.window = curses.newwin(height, width, y, x)
return win
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())
def write(self, txt):
"""print uses write()
"""
self.text += txt
self.text = '\n'.join(self.text.split('\n')[-30:])
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])
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
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()
def refresh(self):
self.initscr.refresh() # update the screen
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
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
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))
def clear(self):
self.stdscr.clear()
def end(self):
"""clean up"""
self.stdscr.clear()
curses.nocbreak()
self.stdscr.keypad(0)
curses.echo()
curses.endwin()
class Window:
"""Window
"""
def __init__(self):
self.window = None
def initscr(self):
self.window = curses.initscr()
def derwin(self, height, width, y, x):
win = Window()
win.window = self.window.derwin(height, width, y, x)
return win
def clear(self):
return self.window.clear()
def getmaxyx(self):
return self.window.getmaxyx()
def getch(self):
return self.window.getch()
def instr(self, y, x, n):
return self.window.instr(y, x, n).decode()
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
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)
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)
def refresh(self):
self.window.refresh()