Projects : yrc : yrc_minor_command_reorder
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://fixpoint.welshcomputing.com/library/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 to scrolling region |
60 | ORIGIN_SCREEN = rst_priv_mode(6) # row 1 at top of screen |
61 | ERASE_TO_END = cseq('J') |
62 | ERASE_FROM_START = cseq('J', 1) |
63 | ERASE_FULL = cseq('J', 2) |
64 | ERASE_LINE_TO_END = cseq('K') |
65 | ERASE_LINE_FROM_START = cseq('K', 1) |
66 | ERASE_LINE_FULL = cseq('K', 2) |
67 | attrs = lambda *args: cseq('m', *args) |
68 | A_NONE = 0 |
69 | A_BOLD = 1 |
70 | A_ULINE = 4 |
71 | A_BLINK = 5 |
72 | A_REVERSE = 7 |
73 | ATTR_NONE = attrs() |
74 | ATTR_BOLD = attrs(A_BOLD) |
75 | ATTR_ULINE = attrs(A_ULINE) |
76 | ATTR_BLINK = attrs(A_BLINK) |
77 | ATTR_REVERSE = attrs(A_REVERSE) |
78 | |
79 | # DEC special graphics characters http://fixpoint.welshcomputing.com/library/vt100-ug/table3-9.html |
80 | SGC_BLANK = '_' |
81 | SGC_DIAMOND = '`' |
82 | SGC_CHECKER = 'a' |
83 | SGC_HT = 'b' |
84 | SGC_FF = 'c' |
85 | SGC_CR = 'd' |
86 | SGC_LF = 'e' |
87 | SGC_DEGREE = 'f' |
88 | SGC_PLUSMINUS = 'g' |
89 | SGC_NL = 'h' |
90 | SGC_VT = 'i' # ACS_LANTERN in ncurses, unclear where that comes from |
91 | SGC_LRCORNER = 'j' |
92 | SGC_URCORNER = 'k' |
93 | SGC_ULCORNER = 'l' |
94 | SGC_LLCORNER = 'm' |
95 | SGC_CROSSING = 'n' |
96 | SGC_HLINE1 = 'o' |
97 | SGC_HLINE3 = 'p' |
98 | SGC_HLINE5 = 'q' |
99 | SGC_HLINE7 = 'r' |
100 | SGC_HLINE9 = 's' |
101 | SGC_LTEE = 't' |
102 | SGC_RTEE = 'u' |
103 | SGC_BTEE = 'v' |
104 | SGC_TTEE = 'w' |
105 | SGC_VBAR = 'x' |
106 | SGC_LEQ = 'y' |
107 | SGC_GEQ = 'z' |
108 | SGC_PI = '{' |
109 | SGC_NEQ = '|' |
110 | SGC_STERLING = '}' |
111 | SGC_CENTERDOT = '~' |
112 | |
113 | # XTerm http://fixpoint.welshcomputing.com/library/xterm/ctlseqs/ctlseqs.html |
114 | ALT_SCREEN_ON = set_priv_mode(47) |
115 | ALT_SCREEN_OFF = rst_priv_mode(47) |
116 | BRACKETED_PASTE_ON = set_priv_mode(2004) |
117 | BRACKETED_PASTE_OFF = rst_priv_mode(2004) |
118 | BRACKETED_PASTE_START = cseq('~', 200) |
119 | BRACKETED_PASTE_END = cseq('~', 201) |
120 | |
121 | def 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 | |
179 | def 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 | |
186 | def 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))) |