""" yterm: a simple, standalone interface library for VT100-like terminals on UNIX. Written by Jacob Welsh for yrc, March 2017. Tested with Python 2.7 (should support at least 2.4). """ __version__ = '9 Kelvin' from sys import stdout from fcntl import ioctl from struct import unpack import termios as t NUL = chr(0) BS = chr(8) HT = chr(9) LF = chr(10) CR = chr(13) SO = chr(14) SI = chr(15) ESC = chr(27) # VT100 http://vt100.net/docs/vt100-ug/chapter3.html RESET = ESC + 'c' CURSOR_SAVE = ESC + '7' CURSOR_RESTORE = ESC + '8' KEYPAD_APP = ESC + '=' # keypad produces escape sequences KEYPAD_NUM = ESC + '>' # keypad produces numbers/symbols select_g0 = lambda c: ESC + '(' + c select_g1 = lambda c: ESC + ')' + c SELECT_G0_ASCII = select_g0('B') SELECT_G1_SPECIAL = select_g1('0') CSI = ESC + '[' cseq = lambda code, *params: ''.join((CSI, ';'.join(map(str, params)), code)) # 1-based coordinates, from origin downward cursor_pos = lambda row, col: cseq('H', row, col) CURSOR_HOME = cseq('H') cursor_up = lambda n: cseq('A', n) CURSOR_UP1 = cseq('A') cursor_down = lambda n: cseq('B', n) CURSOR_DOWN1 = cseq('B') cursor_back = lambda n: cseq('D', n) CURSOR_BACK1 = cseq('D') cursor_forward = lambda n: cseq('C', n) CURSOR_FORWARD1 = cseq('C') # Top/bottom margins of scrolling region margins = lambda top, bot: cseq('r', top, bot) MARGINS_NONE = cseq('r') set_mode = lambda m: cseq('h', m) rst_mode = lambda m: cseq('l', m) set_priv_mode = lambda m: set_mode('?' + str(m)) rst_priv_mode = lambda m: rst_mode('?' + str(m)) LINEFEED_ON = set_mode(20) # LF moves to first column of next row LINEFEED_OFF = rst_mode(20) # LF moves down only CURSOR_APP = set_priv_mode(1) # DECCKM CURSOR_ANSI = rst_priv_mode(1) # implied by KEYPAD_NUM SCROLL_SMOOTH = set_priv_mode(4) # DECSCLM SCROLL_JUMP = rst_priv_mode(4) ORIGIN_MARGIN = set_priv_mode(6) # DECOM: row 1 at top margin, cursor confined # to scrolling region ORIGIN_SCREEN = rst_priv_mode(6) # row 1 at top of screen ERASE_TO_END = cseq('J') ERASE_FROM_START = cseq('J', 1) ERASE_FULL = cseq('J', 2) ERASE_LINE_TO_END = cseq('K') ERASE_LINE_FROM_START = cseq('K', 1) ERASE_LINE_FULL = cseq('K', 2) attrs = lambda *args: cseq('m', *args) A_NONE = 0 A_BOLD = 1 A_ULINE = 4 A_BLINK = 5 A_REVERSE = 7 ATTR_NONE = attrs() ATTR_BOLD = attrs(A_BOLD) ATTR_ULINE = attrs(A_ULINE) ATTR_BLINK = attrs(A_BLINK) ATTR_REVERSE = attrs(A_REVERSE) # DEC special graphics characters http://vt100.net/docs/vt100-ug/table3-9.html SGC_BLANK = '_' SGC_DIAMOND = '`' SGC_CHECKER = 'a' SGC_HT = 'b' SGC_FF = 'c' SGC_CR = 'd' SGC_LF = 'e' SGC_DEGREE = 'f' SGC_PLUSMINUS = 'g' SGC_NL = 'h' SGC_VT = 'i' # ACS_LANTERN in ncurses, unclear where that comes from SGC_LRCORNER = 'j' SGC_URCORNER = 'k' SGC_ULCORNER = 'l' SGC_LLCORNER = 'm' SGC_CROSSING = 'n' SGC_HLINE1 = 'o' SGC_HLINE3 = 'p' SGC_HLINE5 = 'q' SGC_HLINE7 = 'r' SGC_HLINE9 = 's' SGC_LTEE = 't' SGC_RTEE = 'u' SGC_BTEE = 'v' SGC_TTEE = 'w' SGC_VBAR = 'x' SGC_LEQ = 'y' SGC_GEQ = 'z' SGC_PI = '{' SGC_NEQ = '|' SGC_STERLING = '}' SGC_CENTERDOT = '~' # XTerm http://invisible-island.net/xterm/ctlseqs/ctlseqs.html ALT_SCREEN_ON = set_priv_mode(47) ALT_SCREEN_OFF = rst_priv_mode(47) BRACKETED_PASTE_ON = set_priv_mode(2004) BRACKETED_PASTE_OFF = rst_priv_mode(2004) BRACKETED_PASTE_START = cseq('~', 200) BRACKETED_PASTE_END = cseq('~', 201) def init(term_file=stdout, nonblock_read=False): """ Initialize the terminal for full-screen graphics and raw input, returning a cleanup function to be called to restore it at exit. Parameters: term_file -- file object representing the terminal, default stdout (must support write, flush, and fileno methods) nonblock_read -- if false (default), reading from the terminal is blocking and returns at least one character (except on EOF); otherwise it is nonblocking and may return no characters. Details: - Sets terminal driver attributes for raw input: no line buffering; no flow control (^S/^Q), signal (^C etc.), or EOF (^D) characters; no CR/LF translation; 8-bit character size; no echo - Saves terminal cursor, attribute, and character set state (DECSC) - Initializes terminal character sets: G0 to ASCII and G1 to the DEC special graphics characters, initially selecting G0 (SI selects G0; SO selects G1) - Selects the alternate screen (to preserve XTerm scrollback) - Initializes terminal modes: standard linefeed handling (LNM); keypad produces ASCII characters (DECKPNM); jump scrolling (DECSCLM); no scroll margins (DECSTBM); no character attributes Cleanup details: - Selects G0 character set - Disables the alternate screen - Restores terminal cursor, attribute, and character set state (DECRC) - Restores original terminal driver state """ old_attrs = t.tcgetattr(term_file.fileno()) # "It's a bad idea to simply initialize a struct termios structure to a # chosen set of attributes and pass it directly to tcsetattr. Your program # may be run years from now, on systems that support members not # documented in this manual. The way to avoid setting these members to # unreasonable values is to avoid changing them." # -- glibc manual # # Nope. It's 2017; we don't need new pheaturez in termios. If forcing them # to zero does something "unreasonable", then blindly accepting prior state # is worse as it could just as well do the same, but unpredictably. iflag = oflag = lflag = 0 cflag = t.CS8 | t.CREAD ispeed, ospeed = old_attrs[4:6] cc = [NUL]*t.NCCS if nonblock_read: cc[t.VMIN] = 0 else: cc[t.VMIN] = 1 cc[t.VTIME] = 0 t.tcsetattr(term_file.fileno(), t.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) term_file.write(''.join(( CURSOR_SAVE, ALT_SCREEN_ON, SELECT_G0_ASCII, SI, SELECT_G1_SPECIAL, LINEFEED_OFF, KEYPAD_NUM, SCROLL_JUMP, ATTR_NONE, MARGINS_NONE, ERASE_FULL, ))) term_file.flush() def cleanup(): term_file.write(''.join(( SI, ALT_SCREEN_OFF, CURSOR_RESTORE, ))) term_file.flush() t.tcsetattr(term_file.fileno(), t.TCSANOW, old_attrs) return cleanup def screen_size(term_file=stdout): """ Return the screen size (rows, columns) of the given terminal as reported by the driver. """ rows, cols = unpack('HH', ioctl(term_file.fileno(), t.TIOCGWINSZ, ' '*4)) return rows, cols def render(tree, stack=None): """ Render a text attribute tree to a single string with escape sequences. A node is either: - a string, or - a sequence of attribute code followed by zero or more child nodes An attribute code is an integer (e.g. the A_* constants). Example: >>> t = (A_NONE, 'normal, ', ... (A_REVERSE, 'reverse, ', (A_BOLD, 'bold reverse,'), ... ' reverse again,'), ... ' and back to normal.') >>> print render(t) """ if isinstance(tree, str): return tree if stack is None: stack = [A_NONE] tag, children = tree[0], tree[1:] stack.append(tag) body = ''.join(render(c, stack) for c in children) stack.pop() return ''.join((attrs(tag), body, attrs(*stack)))