Projects : yrc : genesis

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://vt100.net/docs/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
60 # to scrolling region
61ORIGIN_SCREEN = rst_priv_mode(6) # row 1 at top of screen
62ERASE_TO_END = cseq('J')
63ERASE_FROM_START = cseq('J', 1)
64ERASE_FULL = cseq('J', 2)
65ERASE_LINE_TO_END = cseq('K')
66ERASE_LINE_FROM_START = cseq('K', 1)
67ERASE_LINE_FULL = cseq('K', 2)
68attrs = lambda *args: cseq('m', *args)
69A_NONE = 0
70A_BOLD = 1
71A_ULINE = 4
72A_BLINK = 5
73A_REVERSE = 7
74ATTR_NONE = attrs()
75ATTR_BOLD = attrs(A_BOLD)
76ATTR_ULINE = attrs(A_ULINE)
77ATTR_BLINK = attrs(A_BLINK)
78ATTR_REVERSE = attrs(A_REVERSE)
79
80# DEC special graphics characters http://vt100.net/docs/vt100-ug/table3-9.html
81SGC_BLANK = '_'
82SGC_DIAMOND = '`'
83SGC_CHECKER = 'a'
84SGC_HT = 'b'
85SGC_FF = 'c'
86SGC_CR = 'd'
87SGC_LF = 'e'
88SGC_DEGREE = 'f'
89SGC_PLUSMINUS = 'g'
90SGC_NL = 'h'
91SGC_VT = 'i' # ACS_LANTERN in ncurses, unclear where that comes from
92SGC_LRCORNER = 'j'
93SGC_URCORNER = 'k'
94SGC_ULCORNER = 'l'
95SGC_LLCORNER = 'm'
96SGC_CROSSING = 'n'
97SGC_HLINE1 = 'o'
98SGC_HLINE3 = 'p'
99SGC_HLINE5 = 'q'
100SGC_HLINE7 = 'r'
101SGC_HLINE9 = 's'
102SGC_LTEE = 't'
103SGC_RTEE = 'u'
104SGC_BTEE = 'v'
105SGC_TTEE = 'w'
106SGC_VBAR = 'x'
107SGC_LEQ = 'y'
108SGC_GEQ = 'z'
109SGC_PI = '{'
110SGC_NEQ = '|'
111SGC_STERLING = '}'
112SGC_CENTERDOT = '~'
113
114# XTerm http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
115ALT_SCREEN_ON = set_priv_mode(47)
116ALT_SCREEN_OFF = rst_priv_mode(47)
117BRACKETED_PASTE_ON = set_priv_mode(2004)
118BRACKETED_PASTE_OFF = rst_priv_mode(2004)
119BRACKETED_PASTE_START = cseq('~', 200)
120BRACKETED_PASTE_END = cseq('~', 201)
121
122def init(term_file=stdout, nonblock_read=False):
123 """
124 Initialize the terminal for full-screen graphics and raw input,
125 returning a cleanup function to be called to restore it at exit.
126
127 Parameters:
128 term_file -- file object representing the terminal, default stdout (must
129 support write, flush, and fileno methods)
130 nonblock_read -- if false (default), reading from the terminal is blocking
131 and returns at least one character (except on EOF); otherwise it is
132 nonblocking and may return no characters.
133
134 Details:
135 - Sets terminal driver attributes for raw input: no line buffering; no flow
136 control (^S/^Q), signal (^C etc.), or EOF (^D) characters; no CR/LF
137 translation; 8-bit character size; no echo
138 - Saves terminal cursor, attribute, and character set state (DECSC)
139 - Initializes terminal character sets: G0 to ASCII and G1 to the DEC
140 special graphics characters, initially selecting G0 (SI selects G0; SO
141 selects G1)
142 - Selects the alternate screen (to preserve XTerm scrollback)
143 - Initializes terminal modes: standard linefeed handling (LNM); keypad
144 produces ASCII characters (DECKPNM); jump scrolling (DECSCLM); no scroll
145 margins (DECSTBM); no character attributes
146
147 Cleanup details:
148 - Selects G0 character set
149 - Disables the alternate screen
150 - Restores terminal cursor, attribute, and character set state (DECRC)
151 - Restores original terminal driver state
152 """
153 old_attrs = t.tcgetattr(term_file.fileno())
154 # "It's a bad idea to simply initialize a struct termios structure to a
155 # chosen set of attributes and pass it directly to tcsetattr. Your program
156 # may be run years from now, on systems that support members not
157 # documented in this manual. The way to avoid setting these members to
158 # unreasonable values is to avoid changing them."
159 # -- glibc manual
160 #
161 # Nope. It's 2017; we don't need new pheaturez in termios. If forcing them
162 # to zero does something "unreasonable", then blindly accepting prior state
163 # is worse as it could just as well do the same, but unpredictably.
164 iflag = oflag = lflag = 0
165 cflag = t.CS8 | t.CREAD
166 ispeed, ospeed = old_attrs[4:6]
167 cc = [NUL]*t.NCCS
168 if nonblock_read:
169 cc[t.VMIN] = 0
170 else:
171 cc[t.VMIN] = 1
172 cc[t.VTIME] = 0
173 t.tcsetattr(term_file.fileno(), t.TCSANOW,
174 [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
175 term_file.write(''.join((
176 CURSOR_SAVE, ALT_SCREEN_ON,
177 SELECT_G0_ASCII, SI, SELECT_G1_SPECIAL,
178 LINEFEED_OFF, KEYPAD_NUM, SCROLL_JUMP,
179 ATTR_NONE, MARGINS_NONE, ERASE_FULL,
180 )))
181 term_file.flush()
182 def cleanup():
183 term_file.write(''.join((
184 SI, ALT_SCREEN_OFF, CURSOR_RESTORE,
185 )))
186 term_file.flush()
187 t.tcsetattr(term_file.fileno(), t.TCSANOW, old_attrs)
188 return cleanup
189
190def screen_size(term_file=stdout):
191 """
192 Return the screen size (rows, columns) of the given terminal as reported by
193 the driver.
194 """
195 rows, cols = unpack('HH', ioctl(term_file.fileno(), t.TIOCGWINSZ, ' '*4))
196 return rows, cols
197
198def render(tree, stack=None):
199 """
200 Render a text attribute tree to a single string with escape sequences.
201
202 A node is either:
203 - a string, or
204 - a sequence of attribute code followed by zero or more child nodes
205
206 An attribute code is an integer (e.g. the A_* constants).
207
208 Example:
209 >>> t = (A_NONE, 'normal, ',
210 ... (A_REVERSE, 'reverse, ', (A_BOLD, 'bold reverse,'),
211 ... ' reverse again,'),
212 ... ' and back to normal.')
213 >>> print render(t)
214 """
215 if isinstance(tree, str):
216 return tree
217 if stack is None:
218 stack = [A_NONE]
219 tag, children = tree[0], tree[1:]
220 stack.append(tag)
221 body = ''.join(render(c, stack) for c in children)
222 stack.pop()
223 return ''.join((attrs(tag), body, attrs(*stack)))