/
terminal.py
2817 lines (2618 loc) · 116 KB
/
terminal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
#
# Copyright 2011 Liftoff Software Corporation
#
# Meta
__version__ = '1.0'
__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
__version_info__ = (1, 0)
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
__doc__ = """\
About This Module
=================
This crux of this module is the Terminal class which is a pure-Python
implementation of the quintessential Unix terminal emulator. It does its best
to emulate an xterm and along with that comes support for the majority of the
relevant portions of ECMA-48. This includes support for emulating varous VT-*
terminal types as well as the "linux" terminal type.
The Terminal class's VT-* emulation support is not complete but it should
suffice for most terminal emulation needs (e.g. all your typical command line
programs should work wonderfully). If something doesn't look quite right or you
need support for certain modes added please feel free to open a ticket on Gate
One's issue tracker: https://github.com/liftoff/GateOne/issues
Note that Terminal was written from scratch in order to be as fast as possible.
It is extensively commented and implements some interesting patterns in order to
maximize execution speed (most notably for things that loop). Some bits of code
may seem "un-Pythonic" and/or difficult to grok but understand that this is
probably due to optimizations. If you know "a better way" please feel free to
submit a patch, open a ticket, or send us an email. There's a reason why open
source software is a superior development model!
Supported Emulation Types
-------------------------
Without any special mode settings or parameters Terminal should effectively
emulate the following terminal types:
* xterm (the most important one)
* ECMA-48/ANSI X3.64
* Nearly all the VT-* types: VT-52, VT-100, VT-220, VT-320, VT-420, and VT-520
* Linux console ("linux")
If you want Terminal to support something else or it's missing a feature from
any given terminal type please `let us know <https://github.com/liftoff/GateOne/issues/new>`_.
We'll implement it!
What Terminal Doesn't Do
------------------------
The Terminal class is meant to emulate the display portion of a given terminal.
It does not translate keystrokes into escape sequences or special control
codes--you'll have to take care of that in your application (or at the
client-side like Gate One). It does, however, keep track of many
keystroke-specific modes of operation such as Application Cursor Keys and the G0
and G1 charset modes *with* callbacks that can be used to notify your
application when such things change.
Special Considerations
----------------------
Many methods inside Terminal start with an underscore. This was done to
indicate that such methods shouldn't be called directly (from a program that
imported the module). If it was thought that a situation might arise where a
method could be used externally by a controlling program, the underscore was
omitted.
Asynchronous Use
----------------
To support asynchronous usage (and make everything faster), Terminal was written
to support extensive callbacks that are called when certain events are
encountered. Here are the events and their callbacks:
.. _callback_constants:
==================================== ========================================================================
Callback Constant (ID) Called when...
==================================== ========================================================================
:attr:`terminal.CALLBACK_SCROLL_UP` The terminal is scrolled up (back).
:attr:`terminal.CALLBACK_CHANGED` The screen is changed/updated.
:attr:`terminal.CALLBACK_CURSOR_POS` The cursor position changes.
:attr:`terminal.CALLBACK_DSR` A Device Status Report (DSR) is requested (via the DSR escape sequence).
:attr:`terminal.CALLBACK_TITLE` The terminal title changes (xterm-style)
:attr:`terminal.CALLBACK_BELL` The bell character (^G) is encountered.
:attr:`terminal.CALLBACK_OPT` The special optional escape sequence is encountered.
:attr:`terminal.CALLBACK_MODE` The terminal mode setting changes (e.g. use alternate screen buffer).
==================================== ========================================================================
Note that CALLBACK_DSR is special in that it in most cases it will be called with arguments. See the code for examples of how and when this happens.
Also, in most cases it is unwise to override CALLBACK_MODE since this method is primarily meant for internal use within the Terminal class.
Using Terminal
--------------
Gate One makes extensive use of the Terminal class and its callbacks. So that's
a great place to look for specific examples (gateone.py and termio.py,
specifically). Having said that, implementing Terminal is pretty
straightforward::
>>> import terminal
>>> term = terminal.Terminal(24, 80)
>>> term.write("This text will be written to the terminal screen.")
>>> term.dump()
[u'This text will be written to the terminal screen. ',
<snip>
u' ']
Here's an example with some basic callbacks:
>>> def mycallback():
... "This will be called whenever the screen changes."
... print("Screen update! Perfect time to dump the terminal screen.")
... print(term.dump()[0]) # Only need to see the top line for this demo =)
... print("Just dumped the screen.")
>>> import terminal
>>> term = terminal.Terminal(24, 80)
>>> term.callbacks[term.CALLBACK_CHANGED] = mycallback
>>> term.write("This should result in mycallback() being called")
Screen update! Perfect time to dump the terminal screen.
This should result in mycallback() being called
Just dumped the screen.
.. note:: In testing Gate One it was determined that it is faster to perform the conversion of a terminal screen to HTML on the server side than it is on the client side (via JavaScript anyway).
About The Scrollback Bufffer
----------------------------
The Terminal class implements a scrollback buffer. Here's how it works:
Whenever a :meth:`Terminal.scroll_up` event occurs, the line (or lines) that
will be removed from the top of the screen will be placed into
:attr:`Terminal.scrollback_buf`. Then whenever :meth:`Terminal.dump_html` is
called the scrollback buffer will be returned along with the screen output and
reset to an empty state.
Why do this? In the event that a very large :meth:`Terminal.write` occurs (e.g.
'ps aux'), it gives the controlling program the ability to capture what went
past the screen without some fancy tracking logic surrounding
:meth:`Terminal.write`.
More information about how this works can be had by looking at the
:meth:`Terminal.dump_html` function itself.
.. note:: There's more than one function that empties :attr:`Terminal.scrollback_buf` when called. You'll just have to have a look around =)
Class Docstrings
================
"""
# Import stdlib stuff
import os, re, logging, base64, StringIO, codecs, unicodedata, tempfile
from array import array
from datetime import datetime, timedelta
from collections import defaultdict
from itertools import imap, izip
# Import our own stuff
from utils import get_translation
_ = get_translation()
# Import 3rd party stuff
try:
# We need PIL to detect image types and get their dimensions. Without the
# dimenions, the browser will render the terminal screen much slower than
# normal. Without PIL images will be displayed simply as:
# <i>Image file</i>
from PIL import Image
except ImportError:
Image = None
logging.warning(_(
"Could not import the Python Imaging Library (PIL) "
"so images will not be displayed in the terminal"))
# Globals
CALLBACK_SCROLL_UP = 1 # Called after a scroll up event (new line)
CALLBACK_CHANGED = 2 # Called after the screen is updated
CALLBACK_CURSOR_POS = 3 # Called after the cursor position is updated
# <waives hand in air> You are not concerned with the number 4
CALLBACK_DSR = 5 # Called when a DSR requires a response
# NOTE: CALLBACK_DSR must accept 'response' as either the first argument or
# as a keyword argument.
CALLBACK_TITLE = 6 # Called when the terminal sets the window title
CALLBACK_BELL = 7 # Called after ASCII_BEL is encountered.
CALLBACK_OPT = 8 # Called when we encounter the optional ESC sequence
# NOTE: CALLBACK_OPT must accept 'chars' as either the first argument or as
# a keyword argument.
CALLBACK_MODE = 9 # Called when the terminal mode changes (e.g. DECCKM)
CALLBACK_RESET = 10 # Called when a terminal reset (^[[!p) is encountered
CALLBACK_LEDS = 11 # Called when the state of the LEDs changes
# These are for HTML output:
RENDITION_CLASSES = defaultdict(lambda: None, {
0: 'reset', # Special: Return everything to defaults
1: 'bold',
2: 'dim',
3: 'italic',
4: 'underline',
5: 'blink',
6: 'fastblink',
7: 'reverse',
8: 'hidden',
9: 'strike',
10: 'resetfont', # NOTE: The font renditions don't do anything right now
11: 'font11', # Mostly because I have no idea what they are supposed to look
12: 'font12', # like.
13: 'font13',
14: 'font14',
15: 'font15',
16: 'font16',
17: 'font17',
18: 'font18',
19: 'font19',
20: 'fraktur',
21: 'boldreset',
22: 'dimreset',
23: 'italicreset',
24: 'underlinereset',
27: 'reversereset',
28: 'hiddenreset',
29: 'strikereset',
# Foregrounds
30: 'f0', # Black
31: 'f1', # Red
32: 'f2', # Green
33: 'f3', # Yellow
34: 'f4', # Blue
35: 'f5', # Magenta
36: 'f6', # Cyan
37: 'f7', # White
38: '', # 256-color support uses this like so: \x1b[38;5;<color num>sm
# Backgrounds
40: 'b0', # Black
41: 'b1', # Red
42: 'b2', # Green
43: 'b3', # Yellow
44: 'b4', # Blue
45: 'b5', # Magenta
46: 'b6', # Cyan
47: 'b7', # White
48: '', # 256-color support uses this like so: \x1b[48;5;<color num>sm
49: 'backgroundreset', # Special: Set BG to default
51: 'frame',
52: 'encircle',
53: 'overline',
60: 'rightline',
61: 'rightdoubleline',
62: 'leftline',
63: 'leftdoubleline',
# aixterm colors (aka '16 color support'). They're supposed to be 'bright'
# versions of the first 8 colors (hence the 'b').
# 'Bright' Foregrounds
90: 'bf0', # Bright black (whatever that is =)
91: 'bf1', # Bright red
92: 'bf2', # Bright green
93: 'bf3', # Bright yellow
94: 'bf4', # Bright blue
95: 'bf5', # Bright magenta
96: 'bf6', # Bright cyan
97: 'bf7', # Bright white
# 'Bright' Backgrounds
100: 'bb0', # Bright black
101: 'bb1', # Bright red
102: 'bb2', # Bright green
103: 'bb3', # Bright yellow
104: 'bb4', # Bright blue
105: 'bb5', # Bright magenta
106: 'bb6', # Bright cyan
107: 'bb7' # Bright white
})
# Generate the dict of 256-color (xterm) foregrounds and backgrounds
for i in xrange(256):
RENDITION_CLASSES[(i+1000)] = "fx%s" % i
RENDITION_CLASSES[(i+10000)] = "bx%s" % i
del i # Cleanup
def handle_special(e):
"""
Used in conjunction with codecs.register_error, will replace special ascii
characters such as 0xDA and 0xc4 (which are used by ncurses) with their
Unicode equivalents.
"""
# TODO: Get this using curses special characters when appropriate
curses_specials = {
# NOTE: When $TERM is set to "Linux" these end up getting used by things
# like ncurses-based apps. In other words, it makes a whole lot
# of ugly look pretty again.
0xda: u'┌', # ACS_ULCORNER
0xc0: u'└', # ACS_LLCORNER
0xbf: u'┐', # ACS_URCORNER
0xd9: u'┘', # ACS_LRCORNER
0xb4: u'├', # ACS_RTEE
0xc3: u'┤', # ACS_LTEE
0xc1: u'┴', # ACS_BTEE
0xc2: u'┬', # ACS_TTEE
0xc4: u'─', # ACS_HLINE
0xb3: u'│', # ACS_VLINE
0xc5: u'┼', # ACS_PLUS
0x2d: u'', # ACS_S1
0x5f: u'', # ACS_S9
0x60: u'◆', # ACS_DIAMOND
0xb2: u'▒', # ACS_CKBOARD
0xf8: u'°', # ACS_DEGREE
0xf1: u'±', # ACS_PLMINUS
0xf9: u'•', # ACS_BULLET
0x3c: u'←', # ACS_LARROW
0x3e: u'→', # ACS_RARROW
0x76: u'↓', # ACS_DARROW
0x5e: u'↑', # ACS_UARROW
0xb0: u'⊞', # ACS_BOARD
0x0f: u'⨂', # ACS_LANTERN
0xdb: u'█', # ACS_BLOCK
}
specials = {
# Note to self: Why did I bother with these overly descriptive comments? Ugh
# I've been staring at obscure symbols far too much lately ⨀_⨀
128: u'€', # Euro sign
129: u' ', # Unknown (Using non-breaking spaces for all unknowns)
130: u'‚', # Single low-9 quotation mark
131: u'ƒ', # Latin small letter f with hook
132: u'„', # Double low-9 quotation mark
133: u'…', # Horizontal ellipsis
134: u'†', # Dagger
135: u'‡', # Double dagger
136: u'ˆ', # Modifier letter circumflex accent
137: u'‰', # Per mille sign
138: u'Š', # Latin capital letter S with caron
139: u'‹', # Single left-pointing angle quotation
140: u'Œ', # Latin capital ligature OE
141: u' ', # Unknown
142: u'Ž', # Latin captial letter Z with caron
143: u' ', # Unknown
144: u' ', # Unknown
145: u'‘', # Left single quotation mark
146: u'’', # Right single quotation mark
147: u'“', # Left double quotation mark
148: u'”', # Right double quotation mark
149: u'•', # Bullet
150: u'–', # En dash
151: u'—', # Em dash
152: u'˜', # Small tilde
153: u'™', # Trade mark sign
154: u'š', # Latin small letter S with caron
155: u'›', # Single right-pointing angle quotation mark
156: u'œ', # Latin small ligature oe
157: u'Ø', # Upper-case slashed zero--using same as empty set (216)
158: u'ž', # Latin small letter z with caron
159: u'Ÿ', # Latin capital letter Y with diaeresis
160: u' ', # Non-breaking space
161: u'¡', # Inverted exclamation mark
162: u'¢', # Cent sign
163: u'£', # Pound sign
164: u'¤', # Currency sign
165: u'¥', # Yen sign
166: u'¦', # Pipe, Broken vertical bar
167: u'§', # Section sign
168: u'¨', # Spacing diaeresis - umlaut
169: u'©', # Copyright sign
170: u'ª', # Feminine ordinal indicator
171: u'«', # Left double angle quotes
172: u'¬', # Not sign
173: u"\u00AD", # Soft hyphen
174: u'®', # Registered trade mark sign
175: u'¯', # Spacing macron - overline
176: u'°', # Degree sign
177: u'±', # Plus-or-minus sign
178: u'²', # Superscript two - squared
179: u'³', # Superscript three - cubed
180: u'´', # Acute accent - spacing acute
181: u'µ', # Micro sign
182: u'¶', # Pilcrow sign - paragraph sign
183: u'·', # Middle dot - Georgian comma
184: u'¸', # Spacing cedilla
185: u'¹', # Superscript one
186: u'º', # Masculine ordinal indicator
187: u'»', # Right double angle quotes
188: u'¼', # Fraction one quarter
189: u'½', # Fraction one half
190: u'¾', # Fraction three quarters
191: u'¿', # Inverted question mark
192: u'À', # Latin capital letter A with grave
193: u'Á', # Latin capital letter A with acute
194: u'Â', # Latin capital letter A with circumflex
195: u'Ã', # Latin capital letter A with tilde
196: u'Ä', # Latin capital letter A with diaeresis
197: u'Å', # Latin capital letter A with ring above
198: u'Æ', # Latin capital letter AE
199: u'Ç', # Latin capital letter C with cedilla
200: u'È', # Latin capital letter E with grave
201: u'É', # Latin capital letter E with acute
202: u'Ê', # Latin capital letter E with circumflex
203: u'Ë', # Latin capital letter E with diaeresis
204: u'Ì', # Latin capital letter I with grave
205: u'Í', # Latin capital letter I with acute
206: u'Î', # Latin capital letter I with circumflex
207: u'Ï', # Latin capital letter I with diaeresis
208: u'Ð', # Latin capital letter ETH
209: u'Ñ', # Latin capital letter N with tilde
210: u'Ò', # Latin capital letter O with grave
211: u'Ó', # Latin capital letter O with acute
212: u'Ô', # Latin capital letter O with circumflex
213: u'Õ', # Latin capital letter O with tilde
214: u'Ö', # Latin capital letter O with diaeresis
215: u'×', # Multiplication sign
216: u'Ø', # Latin capital letter O with slash (aka "empty set")
217: u'Ù', # Latin capital letter U with grave
218: u'Ú', # Latin capital letter U with acute
219: u'Û', # Latin capital letter U with circumflex
220: u'Ü', # Latin capital letter U with diaeresis
221: u'Ý', # Latin capital letter Y with acute
222: u'Þ', # Latin capital letter THORN
223: u'ß', # Latin small letter sharp s - ess-zed
224: u'à', # Latin small letter a with grave
225: u'á', # Latin small letter a with acute
226: u'â', # Latin small letter a with circumflex
227: u'ã', # Latin small letter a with tilde
228: u'ä', # Latin small letter a with diaeresis
229: u'å', # Latin small letter a with ring above
230: u'æ', # Latin small letter ae
231: u'ç', # Latin small letter c with cedilla
232: u'è', # Latin small letter e with grave
233: u'é', # Latin small letter e with acute
234: u'ê', # Latin small letter e with circumflex
235: u'ë', # Latin small letter e with diaeresis
236: u'ì', # Latin small letter i with grave
237: u'í', # Latin small letter i with acute
238: u'î', # Latin small letter i with circumflex
239: u'ï', # Latin small letter i with diaeresis
240: u'ð', # Latin small letter eth
241: u'ñ', # Latin small letter n with tilde
242: u'ò', # Latin small letter o with grave
243: u'ó', # Latin small letter o with acute
244: u'ô', # Latin small letter o with circumflex
245: u'õ', # Latin small letter o with tilde
246: u'ö', # Latin small letter o with diaeresis
247: u'÷', # Division sign
248: u'ø', # Latin small letter o with slash
249: u'ù', # Latin small letter u with grave
250: u'ú', # Latin small letter u with acute
251: u'û', # Latin small letter u with circumflex
252: u'ü', # Latin small letter u with diaeresis
253: u'ý', # Latin small letter y with acute
254: u'þ', # Latin small letter thorn
255: u'ÿ', # Latin small letter y with diaeresis
}
# I left this in its odd state so I could differentiate between the two
# in the future.
if isinstance(e, (UnicodeEncodeError, UnicodeTranslateError)):
s = [u'%s' % specials[ord(c)] for c in e.object[e.start:e.end]]
return ''.join(s), e.end
else:
s = [u'%s' % specials[ord(c)] for c in e.object[e.start:e.end]]
return ''.join(s), e.end
codecs.register_error('handle_special', handle_special)
# TODO List:
#
# * We need unit tests!
# Helper functions
def _reduce_renditions(renditions):
"""
Takes a list, *renditions*, and reduces it to its logical equivalent (as
far as renditions go). Example::
[0, 32, 0, 34, 0, 32]
Would become::
[0, 32]
Other Examples::
[0, 1, 36, 36] -> [0, 1, 36]
[0, 30, 42, 30, 42] -> [0, 30, 42]
[36, 32, 44, 42] -> [32, 42]
[36, 35] -> [35]
"""
out_renditions = []
foreground = None
background = None
for i, rend in enumerate(renditions):
if rend < 29:
if rend not in out_renditions:
out_renditions.append(rend)
elif rend > 29 and rend < 40:
# Regular 8-color foregrounds
foreground = rend
elif rend > 39 and rend < 50:
# Regular 8-color backgrounds
background = rend
elif rend > 91 and rend < 98:
# 'Bright' (16-color) foregrounds
foreground = rend
elif rend > 99 and rend < 108:
# 'Bright' (16-color) backgrounds
background = rend
elif rend > 1000 and rend < 10000:
# 256-color foregrounds
foreground = rend
elif rend > 10000 and rend < 20000:
# 256-color backgrounds
background = rend
else:
out_renditions.append(rend)
if foreground:
out_renditions.append(foreground)
if background:
out_renditions.append(background)
return out_renditions
def pua_counter():
"""
A generator that returns a Unicode Private Use Area (PUA) character starting
at the beginning of Plane 16 (U+100000); counting up by one with each
successive call.
.. note:: Meant to be used as references to image objects in the screen array()
"""
n = 1048576 # U+100000 or u'\U00100000'
while True:
yield unichr(n)
if n == 1114111:
n = 1048576 # Reset--would be impressive to make it this far!
else:
n += 1
class Terminal(object):
"""
Terminal controller class.
"""
ASCII_NUL = 0 # Null
ASCII_BEL = 7 # Bell (BEL)
ASCII_BS = 8 # Backspace
ASCII_HT = 9 # Horizontal Tab
ASCII_LF = 10 # Line Feed
ASCII_VT = 11 # Vertical Tab
ASCII_FF = 12 # Form Feed
ASCII_CR = 13 # Carriage Return
ASCII_SO = 14 # Ctrl-N; Shift out (switches to the G0 charset)
ASCII_SI = 15 # Ctrl-O; Shift in (switches to the G1 charset)
ASCII_XON = 17 # Resume Transmission
ASCII_XOFF = 19 # Stop Transmission or Ignore Characters
ASCII_CAN = 24 # Cancel Escape Sequence
ASCII_SUB = 26 # Substitute: Cancel Escape Sequence and replace with ?
ASCII_ESC = 27 # Escape
ASCII_CSI = 155 # Control Sequence Introducer (that nothing uses)
ASCII_HTS = 210 # Horizontal Tab Stop (HTS)
charsets = {
'B': {}, # Default: USA
'0': { # Line drawing mode
95: u' ',
96: u'◆',
97: u'▒',
98: u'\t',
99: u'\x0c',
100: u'\r',
101: u'\n',
102: u'°',
103: u'±',
104: u'\n',
105: u'\x0b',
106: u'┘',
107: u'┐',
108: u'┌',
109: u'└',
110: u'┼',
111: u'⎺', # All these bars and not a drink!
112: u'⎻',
113: u'─',
114: u'⎼',
115: u'⎽',
116: u'├',
117: u'┤',
118: u'┴',
119: u'┬',
120: u'│',
121: u'≤',
122: u'≥',
123: u'π',
124: u'≠',
125: u'£',
126: u'·' # Centered dot--who comes up with this stuff?!?
}
}
RE_CSI_ESC_SEQ = re.compile(r'\x1B\[([?A-Za-z0-9;@:\!]*)([A-Za-z@_])')
RE_ESC_SEQ = re.compile(r'\x1b(.*\x1b\\|[ABCDEFGHIJKLMNOQRSTUVWXYZa-z0-9=<>]|[()# %*+].)')
RE_TITLE_SEQ = re.compile(r'\x1b\][0-2]\;(.*?)(\x07|\x1b\\)')
# The below regex is used to match our optional (non-standard) handler
RE_OPT_SEQ = re.compile(r'\x1b\]_\;(.+?)(\x07|\x1b\\)')
RE_NUMBERS = re.compile('\d*') # Matches any number
def __init__(self, rows=24, cols=80, em_dimensions=None):
"""
Initializes the terminal by calling *self.initialize(rows, cols)*. This
is so we can have an equivalent function in situations where __init__()
gets overridden.
If *em_dimensions* are provided they will be used to determine how many
lines images will take when they're drawn in the terminal. This is to
prevent images that are written to the top of the screen from having
their tops cut off. *em_dimensions* should be a dict in the form of::
{'height': <px>, 'width': <px>}
"""
self.initialize(rows, cols, em_dimensions)
def initialize(self, rows=24, cols=80, em_dimensions=None):
"""
Initializes the terminal (the actual equivalent to :meth:`__init__`).
"""
self.cols = cols
self.rows = rows
self.em_dimensions = em_dimensions
self.scrollback_buf = []
self.scrollback_renditions = []
self.title = "Gate One"
# This variable can be referenced by programs implementing Terminal() to
# determine if anything has changed since the last dump*()
self.modified = False
self.local_echo = True
self.esc_buffer = '' # For holding escape sequences as they're typed.
self.show_cursor = True
self.cursor_home = 0
self.cur_rendition = u'\U00100000' # Should always be reset ([0])
self.init_screen()
self.init_renditions()
self.G0_charset = self.charsets['B']
self.G1_charset = self.charsets['B']
self.current_charset = 0
self.charset = self.G0_charset
self.set_G0_charset('B')
self.set_G1_charset('B')
self.use_g0_charset()
# Set the default window margins
self.top_margin = 0
self.bottom_margin = self.rows - 1
self.timeout_image = None
self.specials = {
self.ASCII_NUL: self.__ignore,
self.ASCII_BEL: self.bell,
self.ASCII_BS: self.backspace,
self.ASCII_HT: self.horizontal_tab,
self.ASCII_LF: self.linefeed,
self.ASCII_VT: self.linefeed,
self.ASCII_FF: self.linefeed,
self.ASCII_CR: self._carriage_return,
self.ASCII_SO: self.use_g1_charset,
self.ASCII_SI: self.use_g0_charset,
self.ASCII_XON: self._xon,
self.ASCII_CAN: self._cancel_esc_sequence,
self.ASCII_XOFF: self._xoff,
#self.ASCII_ESC: self._sub_esc_sequence,
self.ASCII_ESC: self._escape,
self.ASCII_CSI: self._csi,
}
self.esc_handlers = {
# TODO: Make a different set of these for each respective emulation mode (VT-52, VT-100, VT-200, etc etc)
'#': self._set_line_params, # Varies
'\\': self._string_terminator, # ST
'c': self.clear_screen, # Reset terminal
'D': self.__ignore, # Move/scroll window up one line IND
'M': self.reverse_linefeed, # Move/scroll window down one line RI
'E': self.next_line, # Move to next line NEL
'F': self.__ignore, # Enter Graphics Mode
'G': self.next_line, # Exit Graphics Mode
'6': self._dsr_get_cursor_position, # Get cursor position DSR
'7': self.save_cursor_position, # Save cursor position and attributes DECSC
'8': self.restore_cursor_position, # Restore cursor position and attributes DECSC
'H': self._set_tabstop, # Set a tab at the current column HTS
'I': self.reverse_linefeed,
'(': self.set_G0_charset, # Designate G0 Character Set
')': self.set_G1_charset, # Designate G1 Character Set
'N': self.__ignore, # Set single shift 2 SS2
'O': self.__ignore, # Set single shift 3 SS3
'5': self._device_status_report, # Request: Device status report DSR
'0': self.__ignore, # Response: terminal is OK DSR
'P': self._dcs_handler, # Device Control String DCS
# NOTE: = and > are ignored because the user can override/control
# them via the numlock key on their keyboard. To do otherwise would
# just confuse people.
'=': self.__ignore, # Application Keypad DECPAM
'>': self.__ignore, # Exit alternate keypad mode
'<': self.__ignore, # Exit VT-52 mode
}
self.csi_handlers = {
'A': self.cursor_up,
'B': self.cursor_down,
'C': self.cursor_right,
'D': self.cursor_left,
'E': self.cursor_next_line, # NOTE: Not the same as next_line()
'F': self.cursor_previous_line,
'G': self.cursor_horizontal_absolute,
'H': self.cursor_position,
'L': self.insert_line,
'M': self.delete_line,
#'b': self.repeat_last_char, # TODO
'c': self._csi_device_status_report, # Device status report (DSR)
'g': self.__ignore, # TODO: Tab clear
'h': self.set_expanded_mode,
'l': self.reset_expanded_mode,
'f': self.cursor_position,
'd': self.cursor_position_vertical, # Vertical Line Position Absolute (VPA)
#'e': self.cursor_position_vertical_relative, # VPR TODO
'J': self.clear_screen_from_cursor,
'K': self.clear_line_from_cursor,
'S': self.scroll_up,
'T': self.scroll_down,
's': self.save_cursor_position,
'u': self.restore_cursor_position,
'm': self._set_rendition,
'n': self.__ignore, # <ESC>[6n is the only one I know of (request cursor position)
#'m': self.__ignore, # For testing how much CPU we save when not processing CSI
'p': self.reset, # TODO: "!p" is "Soft terminal reset". Also, "Set conformance level" (VT100, VT200, or VT300)
'r': self._set_top_bottom, # DECSTBM (used by many apps)
'q': self.set_led_state, # Seems a bit silly but you never know
'P': self.delete_characters, # DCH Deletes the specified number of chars
'X': self._erase_characters, # ECH Same as DCH but also deletes renditions
'Z': self.insert_characters, # Inserts the specified number of chars
'@': self.insert_characters, # Inserts the specified number of chars
#'`': self._char_position_row, # Position cursor (row only)
#'t': self.window_manipulation, # TODO
#'z': self.locator, # TODO: DECELR "Enable locator reporting"
}
self.expanded_modes = {
# Expanded modes take a True/False argument for set/reset
'1': self.set_application_mode,
'2': self.__ignore, # DECANM and set VT100 mode
'3': self.__ignore, # 132 Column Mode (DECCOLM)
'4': self.__ignore, # Smooth (Slow) Scroll (DECSCLM)
'5': self.__ignore, # Reverse video (might support in future)
'6': self.__ignore, # Origin Mode (DECOM)
'7': self.__ignore, # Wraparound Mode (DECAWM)
'8': self.__ignore, # Auto-repeat Keys (DECARM)
'9': self.__ignore, # Send Mouse X & Y on button press (maybe)
'12': self.send_receive_mode, # SRM
'18': self.__ignore, # Print form feed (DECPFF)
'19': self.__ignore, # Set print extent to full screen (DECPEX)
'25': self.show_hide_cursor,
'38': self.__ignore, # Enter Tektronix Mode (DECTEK)
'41': self.__ignore, # more(1) fix (whatever that is)
'42': self.__ignore, # Enable Nation Replacement Character sets (DECNRCM)
'44': self.__ignore, # Turn On Margin Bell
'45': self.__ignore, # Reverse-wraparound Mode
'46': self.__ignore, # Start Logging (Hmmm)
'47': self.toggle_alternate_screen_buffer, # Use Alternate Screen Buffer
'66': self.__ignore, # Application keypad (DECNKM)
'67': self.__ignore, # Backarrow key sends delete (DECBKM)
'1000': self.__ignore, # Send Mouse X/Y on button press and release
'1001': self.__ignore, # Use Hilite Mouse Tracking
'1002': self.__ignore, # Use Cell Motion Mouse Tracking
'1003': self.__ignore, # Use All Motion Mouse Tracking
'1010': self.__ignore, # Scroll to bottom on tty output
'1011': self.__ignore, # Scroll to bottom on key press
'1035': self.__ignore, # Enable special modifiers for Alt and NumLock keys
'1036': self.__ignore, # Send ESC when Meta modifies a key
'1037': self.__ignore, # Send DEL from the editing-keypad Delete key
'1047': self.__ignore, # Use Alternate Screen Buffer
'1048': self.__ignore, # Save cursor as in DECSC
'1049': self.toggle_alternate_screen_buffer_cursor, # Save cursor as in DECSC and use Alternate Screen Buffer, clearing it first
'1051': self.__ignore, # Set Sun function-key mode
'1052': self.__ignore, # Set HP function-key mode
'1060': self.__ignore, # Set legacy keyboard emulation (X11R6)
'1061': self.__ignore, # Set Sun/PC keyboard emulation of VT220 keyboard
}
self.callbacks = {
CALLBACK_SCROLL_UP: {},
CALLBACK_CHANGED: {},
CALLBACK_CURSOR_POS: {},
CALLBACK_DSR: {},
CALLBACK_TITLE: {},
CALLBACK_BELL: {},
CALLBACK_OPT: {},
CALLBACK_MODE: {},
CALLBACK_RESET: {},
CALLBACK_LEDS: {},
}
self.leds = {
1: False,
2: False,
3: False,
4: False
}
png_header = re.compile('.*\x89PNG\r', re.DOTALL)
png_whole = re.compile('\x89PNG\r.+IEND\xaeB`\x82', re.DOTALL)
# NOTE: Only matching JFIF and Exif JPEGs because "\xff\xd8" is too
# ambiguous.
jpeg_header = re.compile('.*\xff\xd8\xff.+JFIF\x00|.*\xff\xd8\xff.+Exif\x00', re.DOTALL)
jpeg_whole = re.compile(
'\xff\xd8\xff.+JFIF\x00.+\xff\xd9(?!\xff)|\xff\xd8\xff.+Exif\x00.+\xff\xd9(?!\xff)', re.DOTALL)
self.magic = {
# Dict for magic "numbers" so we can tell when a particular type of
# file begins and ends (so we can capture it in binary form and
# later dump it out via dump_html())
# The format is 'beginning': 'whole'
png_header: png_whole,
jpeg_header: jpeg_whole,
}
self.matched_header = None
# These are for saving self.screen and self.renditions so we can support
# an "alternate buffer"
self.alt_screen = None
self.alt_renditions = None
self.alt_cursorX = 0
self.alt_cursorY = 0
self.saved_cursorX = 0
self.saved_cursorY = 0
self.saved_rendition = [None]
self.application_keys = False
self.image = ""
self.images = {}
self.image_counter = pua_counter()
# This is for creating a new point of reference every time there's a new
# unique rendition at a given coordinate
self.rend_counter = pua_counter()
# Used for mapping unicode chars to acutal renditions (to save memory):
self.renditions_store = {
u' ': [0], # Nada, nothing, no rendition. Not the same as below
self.rend_counter.next(): [0] # Default is actually reset
}
self.prev_dump = [] # A cache to speed things up
self.prev_dump_rend = [] # Ditto
self.html_cache = [] # Ditto
def init_screen(self):
"""
Fills :attr:`screen` with empty lines of (unicode) spaces using
:attr:`self.cols` and :attr:`self.rows` for the dimensions.
.. note:: Just because each line starts out with a uniform length does not mean it will stay that way. Processing of escape sequences is handled when an output function is called.
"""
logging.debug('init_screen()')
self.screen = [array('u', u' ' * self.cols) for a in xrange(self.rows)]
# Tabstops
tabs, remainder = divmod(self.cols, 8) # Default is every 8 chars
self.tabstops = [(a*8)-1 for a in xrange(tabs)]
self.tabstops[0] = 0 # Fix the first tabstop (which will be -1)
# Base cursor position
self.cursorX = 0
self.cursorY = 0
self.rendition_set = False
self.prev_dump = [] # Force a full dump with an init
self.prev_dump_rend = []
self.html_cache = [] # Force this to be reset as well
def init_renditions(self, rendition=u'\U00100000'):
"""
Replaces :attr:`self.renditions` with arrays of *rendition* (characters)
using :attr:`self.cols` and :attr:`self.rows` for the dimenions.
"""
# The actual renditions at various coordinates:
self.renditions = [
array('u', rendition * self.cols) for a in xrange(self.rows)]
def init_scrollback(self):
"""
Empties the scrollback buffers (:attr:`self.scrollback_buf` and
:attr:`self.scrollback_renditions`).
"""
# Close any image files that might be associated with characters
self.scrollback_buf = []
self.scrollback_renditions = []
def add_callback(self, event, callback, identifier=None):
"""
Attaches the given *callback* to the given *event*. If given,
*identifier* can be used to reference this callback leter (e.g. when you
want to remove it). Otherwise an identifier will be generated
automatically. If the given *identifier* is already attached to a
callback at the given event that callback will be replaced with
*callback*.
:event: The numeric ID of the event you're attaching *callback* to. The :ref:`callback constants <callback_constants>` should be used as the numerical IDs.
:callback: The function you're attaching to the *event*.
:identifier: A string or number to be used as a reference point should you wish to remove or update this callback later.
Returns the identifier of the callback. to Example::
>>> term = Terminal()
>>> def somefunc(): pass
>>> id = "myref"
>>> ref = term.add_callback(term.CALLBACK_BELL, somefunc, id)
.. note:: This allows the controlling program to have multiple callbacks for the same event.
"""
if not identifier:
identifier = callback.__hash__()
self.callbacks[event][identifier] = callback
return identifier
def remove_callback(self, event, identifier):
"""
Removes the callback referenced by *identifier* that is attached to the
given *event*. Example::
>>> term.remove_callback(CALLBACK_BELL, "myref")
"""
del self.callbacks[event][identifier]
def remove_all_callbacks(self, identifier):
"""
Removes all callbacks associated with *identifier*.
"""
for event, identifiers in self.callbacks.items():
try:
del self.callbacks[event][identifier]
except KeyError:
pass # No match, no biggie
def reset(self, *args, **kwargs):
"""
Resets the terminal back to an empty screen with all defaults. Calls
:meth:`Terminal.callbacks[CALLBACK_RESET]` when finished.
.. note:: If terminal output has been suspended (e.g. via ctrl-s) this will not un-suspend it (you need to issue ctrl-q to the underlying program to do that).
"""
self.leds = {
1: False,
2: False,
3: False,
4: False
}
self.local_echo = True
self.title = "Gate One"
self.esc_buffer = ''
self.show_cursor = True
self.rendition_set = False
self.G0_charset = 'B'
self.current_charset = self.charsets['B']
self.top_margin = 0
self.bottom_margin = self.rows - 1
self.alt_screen = None
self.alt_renditions = None
self.alt_cursorX = 0
self.alt_cursorY = 0
self.saved_cursorX = 0
self.saved_cursorY = 0
self.saved_rendition = [None]
self.application_keys = False
self.init_screen()
self.init_renditions()
self.init_scrollback()
self.prev_dump = []
self.prev_dump_rend = []
self.html_cache = []
try:
self.callbacks[CALLBACK_RESET]()
except TypeError:
pass
def __ignore(self, *args, **kwargs):
"""
Does nothing (on purpose!). Used as a placeholder for unimplemented
functions.
"""
pass
def resize(self, rows, cols, em_dimensions=None):
"""
Resizes the terminal window, adding or removing *rows* or *cols* as
needed. If *em_dimensions* are provided they will be stored in
*self.em_dimensions* (which is currently only used by image output).
"""
logging.debug("resize(%s, %s)" % (rows, cols))
if em_dimensions:
self.em_dimensions = em_dimensions
if rows == self.rows and cols == self.cols:
return # Nothing to do--don't mess with the margins or the cursor
if rows < self.rows: # Remove rows from the top
for i in xrange(self.rows - rows):
self.screen.pop(0)
self.renditions.pop(0)
elif rows > self.rows: # Add rows at the bottom
for i in xrange(rows - self.rows):
line = array('u', u' ' * self.cols)
renditions = array('u', u'\U00100000' * self.cols)
self.screen.append(line)
self.renditions.append(renditions)
self.rows = rows
self.top_margin = 0
self.bottom_margin = self.rows - 1
# Fix the cursor location:
if self.cursorY >= self.rows:
self.cursorY = self.rows - 1
if cols < self.cols: # Remove cols to the right
for i in xrange(self.rows):
self.screen[i] = self.screen[i][:cols - self.cols]
self.renditions[i] = self.renditions[i][:cols - self.cols]
elif cols > self.cols: # Add cols to the right
for i in xrange(self.rows):
for j in xrange(cols - self.cols):
self.screen[i].append(u' ')
self.renditions[i].append(u'\U00100000')
self.cols = cols
# Fix the cursor location:
if self.cursorX >= self.cols:
self.cursorX = self.cols - 1
self.rendition_set = False