|
1 import curses.ascii |
|
2 import math |
|
3 import logging |
|
4 |
|
5 from tuikit.driver.driver import Driver |
|
6 |
|
7 |
|
8 class CursesWDriver(Driver): |
|
9 |
|
10 key_names = { |
|
11 '\t': 'tab', |
|
12 '\n': 'enter', |
|
13 '\x1b': 'escape', |
|
14 } |
|
15 |
|
16 key_map = { |
|
17 curses.KEY_UP: 'up', |
|
18 curses.KEY_DOWN: 'down', |
|
19 curses.KEY_LEFT: 'left', |
|
20 curses.KEY_RIGHT: 'right', |
|
21 curses.KEY_IC: 'insert', |
|
22 curses.KEY_DC: 'delete', |
|
23 curses.KEY_HOME: 'home', |
|
24 curses.KEY_END: 'end', |
|
25 curses.KEY_PPAGE: 'pageup', |
|
26 curses.KEY_NPAGE: 'pagedown', |
|
27 curses.KEY_BACKSPACE: 'backspace', |
|
28 curses.KEY_BTAB: 'shift+tab', |
|
29 } |
|
30 for _i in range(1, 13): |
|
31 key_map[curses.KEY_F0 + _i] = 'f' + str(_i) |
|
32 |
|
33 color_map = { |
|
34 'default': (-1, 0), |
|
35 'black': (curses.COLOR_BLACK, 0), |
|
36 'blue': (curses.COLOR_BLUE, 0), |
|
37 'green': (curses.COLOR_GREEN, 0), |
|
38 'cyan': (curses.COLOR_CYAN, 0), |
|
39 'red': (curses.COLOR_RED, 0), |
|
40 'magenta': (curses.COLOR_MAGENTA, 0), |
|
41 'brown': (curses.COLOR_YELLOW, 0), |
|
42 'lightgray': (curses.COLOR_WHITE, 0), |
|
43 'gray': (curses.COLOR_BLACK, curses.A_BOLD), |
|
44 'lightblue': (curses.COLOR_BLUE, curses.A_BOLD), |
|
45 'lightgreen': (curses.COLOR_GREEN, curses.A_BOLD), |
|
46 'lightcyan': (curses.COLOR_CYAN, curses.A_BOLD), |
|
47 'lightred': (curses.COLOR_RED, curses.A_BOLD), |
|
48 'lightmagenta': (curses.COLOR_MAGENTA, curses.A_BOLD), |
|
49 'yellow': (curses.COLOR_YELLOW, curses.A_BOLD), |
|
50 'white': (curses.COLOR_WHITE, curses.A_BOLD), |
|
51 } |
|
52 |
|
53 attr_map = { |
|
54 'bold': curses.A_BOLD, |
|
55 'underline': curses.A_UNDERLINE, |
|
56 'standout': curses.A_STANDOUT, # inverse bg/fg |
|
57 'blink': curses.A_BLINK, |
|
58 } |
|
59 |
|
60 def __init__(self): |
|
61 Driver.__init__(self) |
|
62 self._log = logging.getLogger(__name__) |
|
63 self.stdscr = None |
|
64 self.cursor = None |
|
65 self.colors = {} # maps names to curses attributes |
|
66 self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair |
|
67 self._mouse_last_pos = (None, None) |
|
68 self._mouse_last_bstate = None |
|
69 |
|
70 ## initialization, finalization ## |
|
71 |
|
72 def init(self): |
|
73 """Initialize curses""" |
|
74 self.stdscr = curses.initscr() |
|
75 curses.start_color() |
|
76 curses.use_default_colors() |
|
77 curses.noecho() |
|
78 curses.cbreak() |
|
79 self.stdscr.keypad(1) |
|
80 self.stdscr.immedok(0) |
|
81 |
|
82 self.size.h, self.size.w = self.stdscr.getmaxyx() |
|
83 |
|
84 curses.curs_set(False) # hide cursor |
|
85 curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) |
|
86 curses.mouseinterval(0) # do not wait to detect clicks, we use only press/release |
|
87 |
|
88 def close(self): |
|
89 self.stdscr.keypad(0) |
|
90 curses.echo() |
|
91 curses.nocbreak() |
|
92 curses.endwin() |
|
93 |
|
94 ## drawing ## |
|
95 |
|
96 def clear(self): |
|
97 self.stdscr.erase() |
|
98 |
|
99 def putch(self, ch, x, y): |
|
100 try: |
|
101 if isinstance(ch, int): |
|
102 self.stdscr.addch(y, x, ch) |
|
103 elif isinstance(ch, str) and len(ch) == 1: |
|
104 self.stdscr.addstr(y, x, ch) |
|
105 else: |
|
106 raise TypeError('Integer or one-char string is required.') |
|
107 except curses.error as e: |
|
108 self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y)) |
|
109 |
|
110 def draw(self, buffer, x=0, y=0): |
|
111 for bufy in range(buffer.size.h): |
|
112 for bufx in range(buffer.size.w): |
|
113 char, attr_desc = buffer.get(bufx, bufy) |
|
114 self.setattr(attr_desc) |
|
115 self.putch(char, x + bufx, y + bufy) |
|
116 |
|
117 def flush(self): |
|
118 if self.cursor: |
|
119 self.stdscr.move(self.cursor.y, self.cursor.x) |
|
120 curses.curs_set(True) |
|
121 else: |
|
122 curses.curs_set(False) |
|
123 self.stdscr.refresh() |
|
124 |
|
125 ## colors, attributes ## |
|
126 |
|
127 def setattr(self, attr_desc): |
|
128 """Set attribute to be used for subsequent draw operations.""" |
|
129 attr = self.colors.get(attr_desc, None) |
|
130 if attr is None: |
|
131 # first encountered `attr_desc`, initialize |
|
132 fg, bg, attrs = self._parse_attr_desc(attr_desc) |
|
133 fgcol, fgattr = self.color_map[fg] |
|
134 bgcol, _bgattr = self.color_map[bg] |
|
135 colpair = self._getcolorpair(fgcol, bgcol) |
|
136 attr = curses.color_pair(colpair) | self._parseattrs(attrs) | fgattr |
|
137 self.colors[attr_desc] = attr |
|
138 self.stdscr.attrset(attr) |
|
139 |
|
140 def _getcolorpair(self, fgcol, bgcol): |
|
141 pair = (fgcol, bgcol) |
|
142 if pair in self.colorpairs: |
|
143 return self.colorpairs[pair] |
|
144 num = len(self.colorpairs) + 1 |
|
145 curses.init_pair(num, fgcol, bgcol) |
|
146 self.colorpairs[pair] = num |
|
147 return num |
|
148 |
|
149 def _parseattrs(self, attrs): |
|
150 res = 0 |
|
151 for a in attrs: |
|
152 res = res | self.attr_map[a] |
|
153 return res |
|
154 |
|
155 ## input, events ## |
|
156 |
|
157 def getevents(self, timeout=None): |
|
158 """Process input, return list of events. |
|
159 |
|
160 timeout -- float, in seconds (None=infinite) |
|
161 |
|
162 Returns: |
|
163 [('event', param1, ...), ...] |
|
164 |
|
165 """ |
|
166 # Set timeout |
|
167 if timeout is None: |
|
168 # wait indefinitely |
|
169 curses.cbreak() |
|
170 elif timeout > 0: |
|
171 # wait |
|
172 timeout_tenths = math.ceil(timeout * 10) |
|
173 curses.halfdelay(timeout_tenths) |
|
174 else: |
|
175 # timeout = 0 -> no wait |
|
176 self.stdscr.nodelay(1) |
|
177 |
|
178 # Get key or char |
|
179 c = self.stdscr.get_wch() |
|
180 |
|
181 res = [] |
|
182 |
|
183 if c == -1: |
|
184 # Timeout |
|
185 return res |
|
186 elif c == curses.KEY_MOUSE: |
|
187 res += self._process_mouse() |
|
188 elif c == curses.KEY_RESIZE: |
|
189 self.size.h, self.size.w = self.stdscr.getmaxyx() |
|
190 res += [('resize', self.size.w, self.size.h)] |
|
191 elif isinstance(c, int): |
|
192 keyname, mod = self._split_keyname_mod(self.key_map[c]) |
|
193 res += [('keypress', keyname, None, mod)] |
|
194 else: |
|
195 keyname = self.key_names.get(c) |
|
196 res += [('keypress', keyname, c, set())] |
|
197 |
|
198 return res |
|
199 |
|
200 def _process_mouse(self): |
|
201 out = [] |
|
202 try: |
|
203 _id, x, y, _z, bstate = curses.getmouse() |
|
204 except curses.error: |
|
205 return out |
|
206 |
|
207 if bstate & curses.REPORT_MOUSE_POSITION: |
|
208 if self._mouse_last_pos != (x, y): |
|
209 if self._mouse_last_pos[0] is not None: |
|
210 relx = x - (self._mouse_last_pos[0] or 0) |
|
211 rely = y - (self._mouse_last_pos[1] or 0) |
|
212 out += [('mousemove', 0, x, y, relx, rely)] |
|
213 self._mouse_last_pos = (x, y) |
|
214 |
|
215 # we are interested only in changes, not buttons already pressed before event |
|
216 if self._mouse_last_bstate is not None: |
|
217 old = self._mouse_last_bstate |
|
218 new = bstate |
|
219 bstate = ~old & new |
|
220 self._mouse_last_bstate = new |
|
221 else: |
|
222 self._mouse_last_bstate = bstate |
|
223 |
|
224 if bstate & curses.BUTTON1_PRESSED: |
|
225 out += [('mousedown', 1, x, y)] |
|
226 if bstate & curses.BUTTON2_PRESSED: |
|
227 out += [('mousedown', 2, x, y)] |
|
228 if bstate & curses.BUTTON3_PRESSED: |
|
229 out += [('mousedown', 3, x, y)] |
|
230 if bstate & curses.BUTTON1_RELEASED: |
|
231 out += [('mouseup', 1, x, y)] |
|
232 if bstate & curses.BUTTON2_RELEASED: |
|
233 out += [('mouseup', 2, x, y)] |
|
234 if bstate & curses.BUTTON3_RELEASED: |
|
235 out += [('mouseup', 3, x, y)] |
|
236 |
|
237 # reset last pos when pressed/released |
|
238 if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'): |
|
239 self._mouse_last_pos = (None, None) |
|
240 |
|
241 return out |
|
242 |
|
243 def _split_keyname_mod(self, keyname): |
|
244 """Parse keynames in form "shift+tab", return (keyname, mod).""" |
|
245 mod_set = set() |
|
246 if '+' in keyname: |
|
247 parts = keyname.split('+') |
|
248 for mod in parts[:-1]: |
|
249 assert(mod in ('shift', 'alt', 'ctrl', 'meta')) |
|
250 mod_set.add(mod) |
|
251 keyname = parts[-1] |
|
252 |
|
253 return keyname, mod_set |
|
254 |
|
255 |
|
256 driver_class = CursesWDriver |