384 lines
12 KiB
Python
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()
|