Projects : yrc : yrc_genesis
1 | """ |
2 | yterm: a simple, standalone interface library for VT100-like terminals on UNIX. |
3 | Written by Jacob Welsh for yrc, March 2017. |
4 | Tested with Python 2.7 (should support at least 2.4). |
5 | """ |
6 | |
7 | __version__ = '9 Kelvin' |
8 | |
9 | from sys import stdout |
10 | from fcntl import ioctl |
11 | from struct import unpack |
12 | import termios as t |
13 | |
14 | NUL = chr(0) |
15 | BS = chr(8) |
16 | HT = chr(9) |
17 | LF = chr(10) |
18 | CR = chr(13) |
19 | SO = chr(14) |
20 | SI = chr(15) |
21 | ESC = chr(27) |
22 | |
23 | # VT100 http://vt100.net/docs/vt100-ug/chapter3.html |
24 | RESET = ESC + 'c' |
25 | CURSOR_SAVE = ESC + '7' |
26 | CURSOR_RESTORE = ESC + '8' |
27 | KEYPAD_APP = ESC + '=' # keypad produces escape sequences |
28 | KEYPAD_NUM = ESC + '>' # keypad produces numbers/symbols |
29 | select_g0 = lambda c: ESC + '(' + c |
30 | select_g1 = lambda c: ESC + ')' + c |
31 | SELECT_G0_ASCII = select_g0('B') |
32 | SELECT_G1_SPECIAL = select_g1('0') |
33 | CSI = ESC + '[' |
34 | cseq = lambda code, *params: ''.join((CSI, ';'.join(map(str, params)), code)) |
35 | # 1-based coordinates, from origin downward |
36 | cursor_pos = lambda row, col: cseq('H', row, col) |
37 | CURSOR_HOME = cseq('H') |
38 | cursor_up = lambda n: cseq('A', n) |
39 | CURSOR_UP1 = cseq('A') |
40 | cursor_down = lambda n: cseq('B', n) |
41 | CURSOR_DOWN1 = cseq('B') |
42 | cursor_back = lambda n: cseq('D', n) |
43 | CURSOR_BACK1 = cseq('D') |
44 | cursor_forward = lambda n: cseq('C', n) |
45 | CURSOR_FORWARD1 = cseq('C') |
46 | # Top/bottom margins of scrolling region |
47 | margins = lambda top, bot: cseq('r', top, bot) |
48 | MARGINS_NONE = cseq('r') |
49 | set_mode = lambda m: cseq('h', m) |
50 | rst_mode = lambda m: cseq('l', m) |
51 | set_priv_mode = lambda m: set_mode('?' + str(m)) |
52 | rst_priv_mode = lambda m: rst_mode('?' + str(m)) |
53 | LINEFEED_ON = set_mode(20) # LF moves to first column of next row |
54 | LINEFEED_OFF = rst_mode(20) # LF moves down only |
55 | CURSOR_APP = set_priv_mode(1) # DECCKM |
56 | CURSOR_ANSI = rst_priv_mode(1) # implied by KEYPAD_NUM |
57 | SCROLL_SMOOTH = set_priv_mode(4) # DECSCLM |
58 | SCROLL_JUMP = rst_priv_mode(4) |
59 | ORIGIN_MARGIN = set_priv_mode(6) # DECOM: row 1 at top margin, cursor confined |
60 | # to scrolling region |
61 | ORIGIN_SCREEN = rst_priv_mode(6) # row 1 at top of screen |
62 | ERASE_TO_END = cseq('J') |
63 | ERASE_FROM_START = cseq('J', 1) |
64 | ERASE_FULL = cseq('J', 2) |
65 | ERASE_LINE_TO_END = cseq('K') |
66 | ERASE_LINE_FROM_START = cseq('K', 1) |
67 | ERASE_LINE_FULL = cseq('K', 2) |
68 | attrs = lambda *args: cseq('m', *args) |
69 | A_NONE = 0 |
70 | A_BOLD = 1 |
71 | A_ULINE = 4 |
72 | A_BLINK = 5 |
73 | A_REVERSE = 7 |
74 | ATTR_NONE = attrs() |
75 | ATTR_BOLD = attrs(A_BOLD) |
76 | ATTR_ULINE = attrs(A_ULINE) |
77 | ATTR_BLINK = attrs(A_BLINK) |
78 | ATTR_REVERSE = attrs(A_REVERSE) |
79 | |
80 | # DEC special graphics characters http://vt100.net/docs/vt100-ug/table3-9.html |
81 | SGC_BLANK = '_' |
82 | SGC_DIAMOND = '`' |
83 | SGC_CHECKER = 'a' |
84 | SGC_HT = 'b' |
85 | SGC_FF = 'c' |
86 | SGC_CR = 'd' |
87 | SGC_LF = 'e' |
88 | SGC_DEGREE = 'f' |
89 | SGC_PLUSMINUS = 'g' |
90 | SGC_NL = 'h' |
91 | SGC_VT = 'i' # ACS_LANTERN in ncurses, unclear where that comes from |
92 | SGC_LRCORNER = 'j' |
93 | SGC_URCORNER = 'k' |
94 | SGC_ULCORNER = 'l' |
95 | SGC_LLCORNER = 'm' |
96 | SGC_CROSSING = 'n' |
97 | SGC_HLINE1 = 'o' |
98 | SGC_HLINE3 = 'p' |
99 | SGC_HLINE5 = 'q' |
100 | SGC_HLINE7 = 'r' |
101 | SGC_HLINE9 = 's' |
102 | SGC_LTEE = 't' |
103 | SGC_RTEE = 'u' |
104 | SGC_BTEE = 'v' |
105 | SGC_TTEE = 'w' |
106 | SGC_VBAR = 'x' |
107 | SGC_LEQ = 'y' |
108 | SGC_GEQ = 'z' |
109 | SGC_PI = '{' |
110 | SGC_NEQ = '|' |
111 | SGC_STERLING = '}' |
112 | SGC_CENTERDOT = '~' |
113 | |
114 | # XTerm http://invisible-island.net/xterm/ctlseqs/ctlseqs.html |
115 | ALT_SCREEN_ON = set_priv_mode(47) |
116 | ALT_SCREEN_OFF = rst_priv_mode(47) |
117 | BRACKETED_PASTE_ON = set_priv_mode(2004) |
118 | BRACKETED_PASTE_OFF = rst_priv_mode(2004) |
119 | BRACKETED_PASTE_START = cseq('~', 200) |
120 | BRACKETED_PASTE_END = cseq('~', 201) |
121 | |
122 | def 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 | |
190 | def 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 | |
198 | def 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))) |