Projects : yrc : yrc_reindent_docs

yrc/yterm.py

Dir - Raw

1"""
2yterm: a simple, standalone interface library for VT100-like terminals on UNIX.
3Written by Jacob Welsh for yrc, March 2017.
4Tested with Python 2.7 (should support at least 2.4).
5"""
6
7__version__ = '9 Kelvin'
8
9from sys import stdout
10from fcntl import ioctl
11from struct import unpack
12import termios as t
13
14NUL = chr(0)
15BS = chr(8)
16HT = chr(9)
17LF = chr(10)
18CR = chr(13)
19SO = chr(14)
20SI = chr(15)
21ESC = chr(27)
22
23# VT100 http://fixpoint.welshcomputing.com/library/vt100-ug/chapter3.html
24RESET = ESC + 'c'
25CURSOR_SAVE = ESC + '7'
26CURSOR_RESTORE = ESC + '8'
27KEYPAD_APP = ESC + '=' # keypad produces escape sequences
28KEYPAD_NUM = ESC + '>' # keypad produces numbers/symbols
29select_g0 = lambda c: ESC + '(' + c
30select_g1 = lambda c: ESC + ')' + c
31SELECT_G0_ASCII = select_g0('B')
32SELECT_G1_SPECIAL = select_g1('0')
33CSI = ESC + '['
34cseq = lambda code, *params: ''.join((CSI, ';'.join(map(str, params)), code))
35# 1-based coordinates, from origin downward
36cursor_pos = lambda row, col: cseq('H', row, col)
37CURSOR_HOME = cseq('H')
38cursor_up = lambda n: cseq('A', n)
39CURSOR_UP1 = cseq('A')
40cursor_down = lambda n: cseq('B', n)
41CURSOR_DOWN1 = cseq('B')
42cursor_back = lambda n: cseq('D', n)
43CURSOR_BACK1 = cseq('D')
44cursor_forward = lambda n: cseq('C', n)
45CURSOR_FORWARD1 = cseq('C')
46# Top/bottom margins of scrolling region
47margins = lambda top, bot: cseq('r', top, bot)
48MARGINS_NONE = cseq('r')
49set_mode = lambda m: cseq('h', m)
50rst_mode = lambda m: cseq('l', m)
51set_priv_mode = lambda m: set_mode('?' + str(m))
52rst_priv_mode = lambda m: rst_mode('?' + str(m))
53LINEFEED_ON = set_mode(20) # LF moves to first column of next row
54LINEFEED_OFF = rst_mode(20) # LF moves down only
55CURSOR_APP = set_priv_mode(1) # DECCKM
56CURSOR_ANSI = rst_priv_mode(1) # implied by KEYPAD_NUM
57SCROLL_SMOOTH = set_priv_mode(4) # DECSCLM
58SCROLL_JUMP = rst_priv_mode(4)
59ORIGIN_MARGIN = set_priv_mode(6) # DECOM: row 1 at top margin, cursor confined to scrolling region
60ORIGIN_SCREEN = rst_priv_mode(6) # row 1 at top of screen
61ERASE_TO_END = cseq('J')
62ERASE_FROM_START = cseq('J', 1)
63ERASE_FULL = cseq('J', 2)
64ERASE_LINE_TO_END = cseq('K')
65ERASE_LINE_FROM_START = cseq('K', 1)
66ERASE_LINE_FULL = cseq('K', 2)
67attrs = lambda *args: cseq('m', *args)
68A_NONE = 0
69A_BOLD = 1
70A_ULINE = 4
71A_BLINK = 5
72A_REVERSE = 7
73ATTR_NONE = attrs()
74ATTR_BOLD = attrs(A_BOLD)
75ATTR_ULINE = attrs(A_ULINE)
76ATTR_BLINK = attrs(A_BLINK)
77ATTR_REVERSE = attrs(A_REVERSE)
78
79# DEC special graphics characters http://fixpoint.welshcomputing.com/library/vt100-ug/table3-9.html
80SGC_BLANK = '_'
81SGC_DIAMOND = '`'
82SGC_CHECKER = 'a'
83SGC_HT = 'b'
84SGC_FF = 'c'
85SGC_CR = 'd'
86SGC_LF = 'e'
87SGC_DEGREE = 'f'
88SGC_PLUSMINUS = 'g'
89SGC_NL = 'h'
90SGC_VT = 'i' # ACS_LANTERN in ncurses, unclear where that comes from
91SGC_LRCORNER = 'j'
92SGC_URCORNER = 'k'
93SGC_ULCORNER = 'l'
94SGC_LLCORNER = 'm'
95SGC_CROSSING = 'n'
96SGC_HLINE1 = 'o'
97SGC_HLINE3 = 'p'
98SGC_HLINE5 = 'q'
99SGC_HLINE7 = 'r'
100SGC_HLINE9 = 's'
101SGC_LTEE = 't'
102SGC_RTEE = 'u'
103SGC_BTEE = 'v'
104SGC_TTEE = 'w'
105SGC_VBAR = 'x'
106SGC_LEQ = 'y'
107SGC_GEQ = 'z'
108SGC_PI = '{'
109SGC_NEQ = '|'
110SGC_STERLING = '}'
111SGC_CENTERDOT = '~'
112
113# XTerm http://fixpoint.welshcomputing.com/library/xterm/ctlseqs/ctlseqs.html
114ALT_SCREEN_ON = set_priv_mode(47)
115ALT_SCREEN_OFF = rst_priv_mode(47)
116BRACKETED_PASTE_ON = set_priv_mode(2004)
117BRACKETED_PASTE_OFF = rst_priv_mode(2004)
118BRACKETED_PASTE_START = cseq('~', 200)
119BRACKETED_PASTE_END = cseq('~', 201)
120
121def init(term_file=stdout, nonblock_read=False):
122 """
123 Initialize the terminal for full-screen graphics and raw input, returning a cleanup function to be called to restore it at exit.
124
125 Parameters:
126 term_file -- file object representing the terminal, default stdout (must support write, flush, and fileno methods)
127 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.
128
129 Details:
130 - 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
131 - Saves terminal cursor, attribute, and character set state (DECSC)
132 - Initializes terminal character sets: G0 to ASCII and G1 to the DEC special graphics characters, initially selecting G0 (SI selects G0; SO selects G1)
133 - Selects the alternate screen (to preserve XTerm scrollback)
134 - Initializes terminal modes: standard linefeed handling (LNM); keypad produces ASCII characters (DECKPNM); jump scrolling (DECSCLM); no scroll margins (DECSTBM); no character attributes
135
136 Cleanup details:
137 - Selects G0 character set
138 - Disables the alternate screen
139 - Restores terminal cursor, attribute, and character set state (DECRC)
140 - Restores original terminal driver state
141 """
142 old_attrs = t.tcgetattr(term_file.fileno())
143 # "It's a bad idea to simply initialize a struct termios structure to a
144 # chosen set of attributes and pass it directly to tcsetattr. Your program
145 # may be run years from now, on systems that support members not
146 # documented in this manual. The way to avoid setting these members to
147 # unreasonable values is to avoid changing them."
148 # -- glibc manual
149 #
150 # Nope. It's 2017; we don't need new pheaturez in termios. If forcing them
151 # to zero does something "unreasonable", then blindly accepting prior state
152 # is worse as it could just as well do the same, but unpredictably.
153 iflag = oflag = lflag = 0
154 cflag = t.CS8 | t.CREAD
155 ispeed, ospeed = old_attrs[4:6]
156 cc = [NUL]*t.NCCS
157 if nonblock_read:
158 cc[t.VMIN] = 0
159 else:
160 cc[t.VMIN] = 1
161 cc[t.VTIME] = 0
162 t.tcsetattr(term_file.fileno(), t.TCSANOW,
163 [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
164 term_file.write(''.join((
165 CURSOR_SAVE, ALT_SCREEN_ON,
166 SELECT_G0_ASCII, SI, SELECT_G1_SPECIAL,
167 LINEFEED_OFF, KEYPAD_NUM, SCROLL_JUMP,
168 ATTR_NONE, MARGINS_NONE, ERASE_FULL,
169 )))
170 term_file.flush()
171 def cleanup():
172 term_file.write(''.join((
173 SI, ALT_SCREEN_OFF, CURSOR_RESTORE,
174 )))
175 term_file.flush()
176 t.tcsetattr(term_file.fileno(), t.TCSANOW, old_attrs)
177 return cleanup
178
179def screen_size(term_file=stdout):
180 """
181 Return the screen size (rows, columns) of the given terminal as reported by the driver.
182 """
183 rows, cols = unpack('HH', ioctl(term_file.fileno(), t.TIOCGWINSZ, ' '*4))
184 return rows, cols
185
186def render(tree, stack=None):
187 """
188 Render a text attribute tree to a single string with escape sequences.
189
190 A node is either:
191 - a string, or
192 - a sequence of attribute code followed by zero or more child nodes
193
194 An attribute code is an integer (e.g. the A_* constants).
195
196 Example:
197 >>> t = (A_NONE, 'normal, ',
198 ... (A_REVERSE, 'reverse, ', (A_BOLD, 'bold reverse,'),
199 ... ' reverse again,'),
200 ... ' and back to normal.')
201 >>> print render(t)
202 """
203 if isinstance(tree, str):
204 return tree
205 if stack is None:
206 stack = [A_NONE]
207 tag, children = tree[0], tree[1:]
208 stack.append(tag)
209 body = ''.join(render(c, stack) for c in children)
210 stack.pop()
211 return ''.join((attrs(tag), body, attrs(*stack)))