|
1 # Copyright 2000-2004 Michael Hudson mwh@python.net |
|
2 # |
|
3 # All Rights Reserved |
|
4 # |
|
5 # Portions Copyright (c) 2005 Nokia Corporation |
|
6 # |
|
7 # Permission to use, copy, modify, and distribute this software and |
|
8 # its documentation for any purpose is hereby granted without fee, |
|
9 # provided that the above copyright notice appear in all copies and |
|
10 # that both that copyright notice and this permission notice appear in |
|
11 # supporting documentation. |
|
12 # |
|
13 # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO |
|
14 # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY |
|
15 # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, |
|
16 # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER |
|
17 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF |
|
18 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN |
|
19 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|
20 |
|
21 import types |
|
22 try: |
|
23 import unicodedata |
|
24 except ImportError: |
|
25 import dumbunicodedata as unicodedata |
|
26 from pyrepl import commands |
|
27 import ascii |
|
28 from pyrepl import input |
|
29 |
|
30 def _make_unctrl_map(): |
|
31 uc_map = {} |
|
32 for c in map(unichr, range(256)): |
|
33 if unicodedata.category(c)[0] <> 'C': |
|
34 uc_map[c] = c |
|
35 for i in range(32): |
|
36 c = unichr(i) |
|
37 uc_map[c] = u'^' + unichr(ord('A') + i - 1) |
|
38 uc_map['\177'] = u'^?' |
|
39 for i in range(256): |
|
40 c = unichr(i) |
|
41 if not uc_map.has_key(c): |
|
42 uc_map[c] = u'\\%03o'%i |
|
43 return uc_map |
|
44 |
|
45 # disp_str proved to be a bottleneck for large inputs, so it's been |
|
46 # rewritten in C; it's not required though. |
|
47 try: |
|
48 raise ImportError # currently it's borked by the unicode support |
|
49 |
|
50 from _pyrepl_utils import disp_str, init_unctrl_map |
|
51 |
|
52 init_unctrl_map(_make_unctrl_map()) |
|
53 |
|
54 del init_unctrl_map |
|
55 except ImportError: |
|
56 def _my_unctrl(c, u=_make_unctrl_map()): |
|
57 # import unicodedata |
|
58 if c in u: |
|
59 return u[c] |
|
60 else: |
|
61 if unicodedata.category(c).startswith('C'): |
|
62 return '\u%04x'%(ord(c),) |
|
63 else: |
|
64 return c |
|
65 |
|
66 def disp_str(buffer, join=''.join, uc=_my_unctrl): |
|
67 """ disp_str(buffer:string) -> (string, [int]) |
|
68 |
|
69 Return the string that should be the printed represenation of |
|
70 |buffer| and a list detailing where the characters of |buffer| |
|
71 get used up. E.g.: |
|
72 |
|
73 >>> disp_str(chr(3)) |
|
74 ('^C', [1, 0]) |
|
75 |
|
76 the list always contains 0s or 1s at present; it could conceivably |
|
77 go higher as and when unicode support happens.""" |
|
78 s = map(uc, buffer) |
|
79 return (join(s), |
|
80 map(ord, join(map(lambda x:'\001'+(len(x)-1)*'\000', s)))) |
|
81 |
|
82 del _my_unctrl |
|
83 |
|
84 del _make_unctrl_map |
|
85 |
|
86 # syntax classes: |
|
87 |
|
88 [SYNTAX_WHITESPACE, |
|
89 SYNTAX_WORD, |
|
90 SYNTAX_SYMBOL] = range(3) |
|
91 |
|
92 def make_default_syntax_table(): |
|
93 # XXX perhaps should use some unicodedata here? |
|
94 st = {} |
|
95 for c in map(unichr, range(256)): |
|
96 st[c] = SYNTAX_SYMBOL |
|
97 for c in [a for a in map(unichr, range(256)) if a.isalpha()]: |
|
98 st[c] = SYNTAX_WORD |
|
99 st[u'\n'] = st[u' '] = SYNTAX_WHITESPACE |
|
100 return st |
|
101 |
|
102 default_keymap = tuple( |
|
103 [(r'\C-a', 'beginning-of-line'), |
|
104 (r'\C-b', 'left'), |
|
105 (r'\C-c', 'interrupt'), |
|
106 (r'\C-d', 'delete'), |
|
107 (r'\C-e', 'end-of-line'), |
|
108 (r'\C-f', 'right'), |
|
109 (r'\C-g', 'cancel'), |
|
110 (r'\C-h', 'backspace'), |
|
111 (r'\C-j', 'accept'), |
|
112 (r'\<return>', 'accept'), |
|
113 (r'\C-k', 'kill-line'), |
|
114 (r'\C-l', 'clear-screen'), |
|
115 (r'\C-m', 'accept'), |
|
116 (r'\C-q', 'quoted-insert'), |
|
117 (r'\C-t', 'transpose-characters'), |
|
118 (r'\C-u', 'unix-line-discard'), |
|
119 (r'\C-v', 'quoted-insert'), |
|
120 (r'\C-w', 'unix-word-rubout'), |
|
121 (r'\C-x\C-s', 'save-history'), |
|
122 (r'\C-x\C-u', 'upcase-region'), |
|
123 (r'\C-y', 'yank'), |
|
124 (r'\C-z', 'suspend'), |
|
125 |
|
126 (r'\M-b', 'backward-word'), |
|
127 (r'\M-c', 'capitalize-word'), |
|
128 (r'\M-d', 'kill-word'), |
|
129 (r'\M-f', 'forward-word'), |
|
130 (r'\M-l', 'downcase-word'), |
|
131 (r'\M-t', 'transpose-words'), |
|
132 (r'\M-u', 'upcase-word'), |
|
133 (r'\M-y', 'yank-pop'), |
|
134 (r'\M--', 'digit-arg'), |
|
135 (r'\M-0', 'digit-arg'), |
|
136 (r'\M-1', 'digit-arg'), |
|
137 (r'\M-2', 'digit-arg'), |
|
138 (r'\M-3', 'digit-arg'), |
|
139 (r'\M-4', 'digit-arg'), |
|
140 (r'\M-5', 'digit-arg'), |
|
141 (r'\M-6', 'digit-arg'), |
|
142 (r'\M-7', 'digit-arg'), |
|
143 (r'\M-8', 'digit-arg'), |
|
144 (r'\M-9', 'digit-arg'), |
|
145 (r'\M-\n', 'insert-nl'), |
|
146 ('\\\\', 'self-insert')] + \ |
|
147 [(c, 'self-insert') |
|
148 for c in map(chr, range(32, 127)) if c <> '\\'] + \ |
|
149 [(c, 'self-insert') |
|
150 for c in map(chr, range(128, 256)) if c.isalpha()] + \ |
|
151 [(r'\<up>', 'up'), |
|
152 (r'\<down>', 'down'), |
|
153 (r'\<left>', 'left'), |
|
154 (r'\<right>', 'right'), |
|
155 (r'\<insert>', 'quoted-insert'), |
|
156 (r'\<delete>', 'delete'), |
|
157 (r'\<backspace>', 'backspace'), |
|
158 (r'\M-\<backspace>', 'backward-kill-word'), |
|
159 (r'\<end>', 'end'), |
|
160 (r'\<home>', 'home'), |
|
161 (r'\<f1>', 'help'), |
|
162 (r'\EOF', 'end'), # the entries in the terminfo database for xterms |
|
163 (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal |
|
164 # workaround |
|
165 ]) |
|
166 |
|
167 del c # from the listcomps |
|
168 |
|
169 class Reader(object): |
|
170 """The Reader class implements the bare bones of a command reader, |
|
171 handling such details as editing and cursor motion. What it does |
|
172 not support are such things as completion or history support - |
|
173 these are implemented elsewhere. |
|
174 |
|
175 Instance variables of note include: |
|
176 |
|
177 * buffer: |
|
178 A *list* (*not* a string at the moment :-) containing all the |
|
179 characters that have been entered. |
|
180 * console: |
|
181 Hopefully encapsulates the OS dependent stuff. |
|
182 * pos: |
|
183 A 0-based index into `buffer' for where the insertion point |
|
184 is. |
|
185 * screeninfo: |
|
186 Ahem. This list contains some info needed to move the |
|
187 insertion point around reasonably efficiently. I'd like to |
|
188 get rid of it, because its contents are obtuse (to put it |
|
189 mildly) but I haven't worked out if that is possible yet. |
|
190 * cxy, lxy: |
|
191 the position of the insertion point in screen ... XXX |
|
192 * syntax_table: |
|
193 Dictionary mapping characters to `syntax class'; read the |
|
194 emacs docs to see what this means :-) |
|
195 * commands: |
|
196 Dictionary mapping command names to command classes. |
|
197 * arg: |
|
198 The emacs-style prefix argument. It will be None if no such |
|
199 argument has been provided. |
|
200 * dirty: |
|
201 True if we need to refresh the display. |
|
202 * kill_ring: |
|
203 The emacs-style kill-ring; manipulated with yank & yank-pop |
|
204 * ps1, ps2, ps3, ps4: |
|
205 prompts. ps1 is the prompt for a one-line input; for a |
|
206 multiline input it looks like: |
|
207 ps2> first line of input goes here |
|
208 ps3> second and further |
|
209 ps3> lines get ps3 |
|
210 ... |
|
211 ps4> and the last one gets ps4 |
|
212 As with the usual top-level, you can set these to instances if |
|
213 you like; str() will be called on them (once) at the beginning |
|
214 of each command. Don't put really long or newline containing |
|
215 strings here, please! |
|
216 This is just the default policy; you can change it freely by |
|
217 overriding get_prompt() (and indeed some standard subclasses |
|
218 do). |
|
219 * finished: |
|
220 handle1 will set this to a true value if a command signals |
|
221 that we're done. |
|
222 """ |
|
223 |
|
224 help_text = """\ |
|
225 This is pyrepl. Hear my roar. |
|
226 |
|
227 Helpful text may appear here at some point in the future when I'm |
|
228 feeling more loquacious than I am now.""" |
|
229 |
|
230 def __init__(self, console): |
|
231 self.buffer = [] |
|
232 self.ps1 = "->> " |
|
233 self.ps2 = "/>> " |
|
234 self.ps3 = "|.. " |
|
235 self.ps4 = "\__ " |
|
236 self.kill_ring = [] |
|
237 self.arg = None |
|
238 self.finished = 0 |
|
239 self.console = console |
|
240 self.commands = {} |
|
241 self.msg = '' |
|
242 for v in vars(commands).values(): |
|
243 if ( isinstance(v, type) |
|
244 and issubclass(v, commands.Command) |
|
245 and v.__name__[0].islower() ): |
|
246 self.commands[v.__name__] = v |
|
247 self.commands[v.__name__.replace('_', '-')] = v |
|
248 self.syntax_table = make_default_syntax_table() |
|
249 self.input_trans_stack = [] |
|
250 self.keymap = self.collect_keymap() |
|
251 self.input_trans = input.KeymapTranslator( |
|
252 self.keymap, |
|
253 invalid_cls='invalid-key', |
|
254 character_cls='self-insert') |
|
255 |
|
256 def collect_keymap(self): |
|
257 return default_keymap |
|
258 |
|
259 def calc_screen(self): |
|
260 """The purpose of this method is to translate changes in |
|
261 self.buffer into changes in self.screen. Currently it rips |
|
262 everything down and starts from scratch, which whilst not |
|
263 especially efficient is certainly simple(r). |
|
264 """ |
|
265 lines = self.get_unicode().split("\n") |
|
266 screen = [] |
|
267 screeninfo = [] |
|
268 w = self.console.width - 1 |
|
269 p = self.pos |
|
270 for ln, line in zip(range(len(lines)), lines): |
|
271 ll = len(line) |
|
272 if 0 <= p <= ll: |
|
273 if self.msg: |
|
274 for mline in self.msg.split("\n"): |
|
275 screen.append(mline) |
|
276 screeninfo.append((0, [])) |
|
277 self.lxy = p, ln |
|
278 prompt = self.get_prompt(ln, ll >= p >= 0) |
|
279 p -= ll + 1 |
|
280 lp = len(prompt) |
|
281 l, l2 = disp_str(line) |
|
282 wrapcount = (len(l) + lp) / w |
|
283 if wrapcount == 0: |
|
284 screen.append(prompt + l) |
|
285 screeninfo.append((lp, l2+[1])) |
|
286 else: |
|
287 screen.append(prompt + l[:w-lp] + "\\") |
|
288 screeninfo.append((lp, l2[:w-lp])) |
|
289 for i in range(-lp + w, -lp + wrapcount*w, w): |
|
290 screen.append(l[i:i+w] + "\\") |
|
291 screeninfo.append((0, l2[i:i + w])) |
|
292 screen.append(l[wrapcount*w - lp:]) |
|
293 screeninfo.append((0, l2[wrapcount*w - lp:]+[1])) |
|
294 self.screeninfo = screeninfo |
|
295 self.cxy = self.pos2xy(self.pos) |
|
296 return screen |
|
297 |
|
298 def bow(self, p=None): |
|
299 """Return the 0-based index of the word break preceding p most |
|
300 immediately. |
|
301 |
|
302 p defaults to self.pos; word boundaries are determined using |
|
303 self.syntax_table.""" |
|
304 if p is None: |
|
305 p = self.pos |
|
306 st = self.syntax_table |
|
307 b = self.buffer |
|
308 p -= 1 |
|
309 while p >= 0 and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD: |
|
310 p -= 1 |
|
311 while p >= 0 and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD: |
|
312 p -= 1 |
|
313 return p + 1 |
|
314 |
|
315 def eow(self, p=None): |
|
316 """Return the 0-based index of the word break following p most |
|
317 immediately. |
|
318 |
|
319 p defaults to self.pos; word boundaries are determined using |
|
320 self.syntax_table.""" |
|
321 if p is None: |
|
322 p = self.pos |
|
323 st = self.syntax_table |
|
324 b = self.buffer |
|
325 while p < len(b) and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD: |
|
326 p += 1 |
|
327 while p < len(b) and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD: |
|
328 p += 1 |
|
329 return p |
|
330 |
|
331 def bol(self, p=None): |
|
332 """Return the 0-based index of the line break preceding p most |
|
333 immediately. |
|
334 |
|
335 p defaults to self.pos.""" |
|
336 # XXX there are problems here. |
|
337 if p is None: |
|
338 p = self.pos |
|
339 b = self.buffer |
|
340 p -= 1 |
|
341 while p >= 0 and b[p] <> '\n': |
|
342 p -= 1 |
|
343 return p + 1 |
|
344 |
|
345 def eol(self, p=None): |
|
346 """Return the 0-based index of the line break following p most |
|
347 immediately. |
|
348 |
|
349 p defaults to self.pos.""" |
|
350 if p is None: |
|
351 p = self.pos |
|
352 b = self.buffer |
|
353 while p < len(b) and b[p] <> '\n': |
|
354 p += 1 |
|
355 return p |
|
356 |
|
357 def get_arg(self, default=1): |
|
358 """Return any prefix argument that the user has supplied, |
|
359 returning `default' if there is None. `default' defaults |
|
360 (groan) to 1.""" |
|
361 if self.arg is None: |
|
362 return default |
|
363 else: |
|
364 return self.arg |
|
365 |
|
366 def get_prompt(self, lineno, cursor_on_line): |
|
367 """Return what should be in the left-hand margin for line |
|
368 `lineno'.""" |
|
369 if self.arg is not None and cursor_on_line: |
|
370 return "(arg: %s) "%self.arg |
|
371 if "\n" in self.buffer: |
|
372 if lineno == 0: |
|
373 return self._ps2 |
|
374 elif lineno == self.buffer.count("\n"): |
|
375 return self._ps4 |
|
376 else: |
|
377 return self._ps3 |
|
378 else: |
|
379 return self._ps1 |
|
380 |
|
381 def push_input_trans(self, itrans): |
|
382 self.input_trans_stack.append(self.input_trans) |
|
383 self.input_trans = itrans |
|
384 |
|
385 def pop_input_trans(self): |
|
386 self.input_trans = self.input_trans_stack.pop() |
|
387 |
|
388 def pos2xy(self, pos): |
|
389 """Return the x, y coordinates of position 'pos'.""" |
|
390 # this *is* incomprehensible, yes. |
|
391 y = 0 |
|
392 assert 0 <= pos <= len(self.buffer) |
|
393 if pos == len(self.buffer): |
|
394 y = len(self.screeninfo) - 1 |
|
395 p, l2 = self.screeninfo[y] |
|
396 return p + len(l2) - 1, y |
|
397 else: |
|
398 for p, l2 in self.screeninfo: |
|
399 l = l2.count(1) |
|
400 if l > pos: |
|
401 break |
|
402 else: |
|
403 pos -= l |
|
404 y += 1 |
|
405 c = 0 |
|
406 i = 0 |
|
407 while c < pos: |
|
408 c += l2[i] |
|
409 i += 1 |
|
410 while l2[i] == 0: |
|
411 i += 1 |
|
412 return p + i, y |
|
413 |
|
414 def insert(self, text): |
|
415 """Insert 'text' at the insertion point.""" |
|
416 self.buffer[self.pos:self.pos] = list(text) |
|
417 self.pos += len(text) |
|
418 self.dirty = 1 |
|
419 |
|
420 def update_cursor(self): |
|
421 """Move the cursor to reflect changes in self.pos""" |
|
422 self.cxy = self.pos2xy(self.pos) |
|
423 self.console.move_cursor(*self.cxy) |
|
424 |
|
425 def after_command(self, cmd): |
|
426 """This function is called to allow post command cleanup.""" |
|
427 if getattr(cmd, "kills_digit_arg", 1): |
|
428 if self.arg is not None: |
|
429 self.dirty = 1 |
|
430 self.arg = None |
|
431 |
|
432 def prepare(self): |
|
433 """Get ready to run. Call restore when finished. You must not |
|
434 write to the console in between the calls to prepare and |
|
435 restore.""" |
|
436 try: |
|
437 self.console.prepare() |
|
438 self.arg = None |
|
439 self.screeninfo = [] |
|
440 self.finished = 0 |
|
441 del self.buffer[:] |
|
442 self.pos = 0 |
|
443 self.dirty = 1 |
|
444 self.last_command = None |
|
445 self._ps1, self._ps2, self._ps3, self._ps4 = \ |
|
446 map(str, [self.ps1, self.ps2, self.ps3, self.ps4]) |
|
447 except: |
|
448 self.restore() |
|
449 raise |
|
450 |
|
451 def last_command_is(self, klass): |
|
452 if not self.last_command: |
|
453 return 0 |
|
454 return issubclass(klass, self.last_command) |
|
455 |
|
456 def restore(self): |
|
457 """Clean up after a run.""" |
|
458 self.console.restore() |
|
459 |
|
460 def finish(self): |
|
461 """Called when a command signals that we're finished.""" |
|
462 pass |
|
463 |
|
464 def message(self, msg="none"): |
|
465 self.msg = "[ "+msg+" ] " |
|
466 self.dirty = 1 |
|
467 |
|
468 def error(self, msg="none"): |
|
469 self.msg = "! " + msg + " " |
|
470 self.dirty = 1 |
|
471 self.console.beep() |
|
472 |
|
473 def refresh(self): |
|
474 """Recalculate and refresh the screen.""" |
|
475 if self.console.isbusy(): |
|
476 return |
|
477 # this call sets up self.cxy, so call it first. |
|
478 screen = self.calc_screen() |
|
479 self.console.refresh(screen, self.cxy) |
|
480 self.dirty = 0 # forgot this for a while (blush) |
|
481 |
|
482 def do_cmd(self, cmd): |
|
483 if isinstance(cmd[0], str): |
|
484 cmd = self.commands.get(cmd[0], |
|
485 commands.invalid_command)(self, cmd) |
|
486 elif isinstance(cmd[0], type): |
|
487 cmd = cmd[0](self, cmd) |
|
488 else: |
|
489 cmd=cmd[0] |
|
490 |
|
491 cmd.do() |
|
492 |
|
493 self.after_command(cmd) |
|
494 |
|
495 if self.dirty: |
|
496 self.refresh() |
|
497 else: |
|
498 self.update_cursor() |
|
499 |
|
500 if not isinstance(cmd, commands.digit_arg): |
|
501 self.last_command = cmd.__class__ |
|
502 |
|
503 self.finished = cmd.finish |
|
504 if self.finished: |
|
505 self.console.finish() |
|
506 self.finish() |
|
507 |
|
508 def handle1(self, block=1): |
|
509 """Handle a single event. Wait as long as it takes if block |
|
510 is true (the default), otherwise return None if no event is |
|
511 pending.""" |
|
512 |
|
513 if self.msg: |
|
514 self.msg = '' |
|
515 self.dirty = 1 |
|
516 |
|
517 while 1: |
|
518 event = self.console.get_event(block) |
|
519 if not event: # can only happen if we're not blocking |
|
520 return None |
|
521 |
|
522 if event.evt == 'key': |
|
523 self.input_trans.push(event) |
|
524 elif event.evt == 'scroll': |
|
525 self.refresh() |
|
526 elif event.evt == 'resize': |
|
527 self.refresh() |
|
528 else: |
|
529 pass |
|
530 |
|
531 cmd = self.input_trans.get() |
|
532 |
|
533 if cmd is None: |
|
534 if block: |
|
535 continue |
|
536 else: |
|
537 return None |
|
538 |
|
539 self.do_cmd(cmd) |
|
540 return 1 |
|
541 |
|
542 def readline(self): |
|
543 """Read a line. The implementation of this method also shows |
|
544 how to drive Reader if you want more control over the event |
|
545 loop.""" |
|
546 self.prepare() |
|
547 try: |
|
548 self.refresh() |
|
549 while not self.finished: |
|
550 self.handle1() |
|
551 return self.get_buffer() |
|
552 finally: |
|
553 self.restore() |
|
554 |
|
555 def bind(self, spec, command): |
|
556 self.keymap = self.keymap + ((spec, command),) |
|
557 self.input_trans = input.KeymapTranslator( |
|
558 self.keymap, |
|
559 invalid_cls='invalid-key', |
|
560 character_cls='self-insert') |
|
561 |
|
562 def get_buffer(self, encoding=None): |
|
563 if encoding is None: |
|
564 encoding = self.console.encoding |
|
565 return u''.join(self.buffer).encode(self.console.encoding) |
|
566 |
|
567 def get_unicode(self): |
|
568 """Return the current buffer as a unicode string.""" |
|
569 return u''.join(self.buffer) |
|
570 |
|
571 def test(): |
|
572 from pyrepl.unix_console import UnixConsole |
|
573 reader = Reader(UnixConsole()) |
|
574 reader.ps1 = "**> " |
|
575 reader.ps2 = "/*> " |
|
576 reader.ps3 = "|*> " |
|
577 reader.ps4 = "\*> " |
|
578 while reader.readline(): |
|
579 pass |
|
580 |
|
581 if __name__=='__main__': |
|
582 test() |