Coverage for openhcs/textual_tui/services/terminal.py: 0.0%
1988 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1# -*- coding: utf-8 -*-
2#
3# Copyright 2011 Liftoff Software Corporation
4#
6# Meta
7__version__ = '1.1'
8__version_info__ = (1, 1)
9__license__ = "AGPLv3 or Proprietary (see LICENSE.txt)"
10__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
12__doc__ = """\
13About This Module
14=================
15This crux of this module is the Terminal class which is a pure-Python
16implementation of the quintessential Unix terminal emulator. It does its best
17to emulate an xterm and along with that comes support for the majority of the
18relevant portions of ECMA-48. This includes support for emulating varous VT-*
19terminal types as well as the "linux" terminal type.
21The Terminal class's VT-* emulation support is not complete but it should
22suffice for most terminal emulation needs (e.g. all your typical command line
23programs should work wonderfully). If something doesn't look quite right or you
24need support for certain modes added please feel free to open a ticket on Gate
25One's issue tracker: https://github.com/liftoff/GateOne/issues
27Note that Terminal was written from scratch in order to be as fast as possible.
28It is extensively commented and implements some interesting patterns in order to
29maximize execution speed (most notably for things that loop). Some bits of code
30may seem "un-Pythonic" and/or difficult to grok but understand that this is
31probably due to optimizations. If you know "a better way" please feel free to
32submit a patch, open a ticket, or send us an email. There's a reason why open
33source software is a superior development model!
35Supported Emulation Types
36-------------------------
37Without any special mode settings or parameters Terminal should effectively
38emulate the following terminal types:
40 * xterm (the most important one)
41 * ECMA-48/ANSI X3.64
42 * Nearly all the VT-* types: VT-52, VT-100, VT-220, VT-320, VT-420, and VT-520
43 * Linux console ("linux")
45If you want Terminal to support something else or it's missing a feature from
46any given terminal type please `let us know <https://github.com/liftoff/GateOne/issues/new>`_.
47We'll implement it!
49What Terminal Doesn't Do
50------------------------
51The Terminal class is meant to emulate the display portion of a given terminal.
52It does not translate keystrokes into escape sequences or special control
53codes--you'll have to take care of that in your application (or at the
54client-side like Gate One). It does, however, keep track of many
55keystroke-specific modes of operation such as Application Cursor Keys and the G0
56and G1 charset modes *with* callbacks that can be used to notify your
57application when such things change.
59Special Considerations
60----------------------
61Many methods inside Terminal start with an underscore. This was done to
62indicate that such methods shouldn't be called directly (from a program that
63imported the module). If it was thought that a situation might arise where a
64method could be used externally by a controlling program, the underscore was
65omitted.
67Asynchronous Use
68----------------
69To support asynchronous usage (and make everything faster), Terminal was written
70to support extensive callbacks that are called when certain events are
71encountered. Here are the events and their callbacks:
73.. _callback_constants:
75==================================== ================================================================================
76Callback Constant (ID) Called when...
77==================================== ================================================================================
78:attr:`terminal.CALLBACK_SCROLL_UP` The terminal is scrolled up (back).
79:attr:`terminal.CALLBACK_CHANGED` The screen is changed/updated.
80:attr:`terminal.CALLBACK_CURSOR_POS` The cursor position changes.
81:attr:`terminal.CALLBACK_DSR` A Device Status Report (DSR) is requested (via the DSR escape sequence).
82:attr:`terminal.CALLBACK_TITLE` The terminal title changes (xterm-style)
83:attr:`terminal.CALLBACK_BELL` The bell character (^G) is encountered.
84:attr:`terminal.CALLBACK_OPT` The special optional escape sequence is encountered.
85:attr:`terminal.CALLBACK_MODE` The terminal mode setting changes (e.g. use alternate screen buffer).
86:attr:`terminal.CALLBACK_MESSAGE` The terminal needs to send the user a message (without messing with the screen).
87==================================== ================================================================================
89Note 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.
91Also, in most cases it is unwise to override CALLBACK_MODE since this method is primarily meant for internal use within the Terminal class.
93Using Terminal
94--------------
95Gate One makes extensive use of the Terminal class and its callbacks. So that's
96a great place to look for specific examples (gateone.py and termio.py,
97specifically). Having said that, implementing Terminal is pretty
98straightforward::
100 >>> import terminal
101 >>> term = terminal.Terminal(24, 80)
102 >>> term.write("This text will be written to the terminal screen.")
103 >>> term.dump()
104 [u'This text will be written to the terminal screen. ',
105 <snip>
106 u' ']
108Here's an example with some basic callbacks:
110 >>> def mycallback():
111 ... "This will be called whenever the screen changes."
112 ... print("Screen update! Perfect time to dump the terminal screen.")
113 ... print(term.dump()[0]) # Only need to see the top line for this demo =)
114 ... print("Just dumped the screen.")
115 >>> import terminal
116 >>> term = terminal.Terminal(24, 80)
117 >>> term.callbacks[term.CALLBACK_CHANGED] = mycallback
118 >>> term.write("This should result in mycallback() being called")
119 Screen update! Perfect time to dump the terminal screen.
120 This should result in mycallback() being called
121 Just dumped the screen.
123.. 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).
125About The Scrollback Bufffer
126----------------------------
127The Terminal class implements a scrollback buffer. Here's how it works:
128Whenever a :meth:`Terminal.scroll_up` event occurs, the line (or lines) that
129will be removed from the top of the screen will be placed into
130:attr:`Terminal.scrollback_buf`. Then whenever :meth:`Terminal.dump_html` is
131called the scrollback buffer will be returned along with the screen output and
132reset to an empty state.
134Why do this? In the event that a very large :meth:`Terminal.write` occurs (e.g.
135'ps aux'), it gives the controlling program the ability to capture what went
136past the screen without some fancy tracking logic surrounding
137:meth:`Terminal.write`.
139More information about how this works can be had by looking at the
140:meth:`Terminal.dump_html` function itself.
142.. note:: There's more than one function that empties :attr:`Terminal.scrollback_buf` when called. You'll just have to have a look around =)
144Class Docstrings
145================
146"""
148# Import stdlib stuff
149import os, sys, re, logging, base64, codecs, unicodedata, tempfile, struct
150from io import BytesIO
151from array import array
152from datetime import datetime, timedelta
153from functools import partial
154from collections import defaultdict
155# Python 3 compatibility - imap and izip are now map and zip
156try:
157 from itertools import imap, izip
158except ImportError:
159 # Python 3
160 imap = map
161 izip = zip
163# Python 3 compatibility - generator.next() is now next(generator)
164def next_compat(generator):
165 """Compatibility function for generator.next() vs next(generator)"""
166 try:
167 return generator.next() # Python 2
168 except AttributeError:
169 return next(generator) # Python 3
170try:
171 from collections import OrderedDict
172except ImportError: # Python <2.7 didn't have OrderedDict in collections
173 try:
174 from ordereddict import OrderedDict
175 except ImportError:
176 logging.error(
177 "Error: Could not import OrderedDict. Please install it:")
178 logging.error("\tsudo pip install ordereddict")
179 logging.error(
180 "...or download it from http://pypi.python.org/pypi/ordereddict")
181 sys.exit(1)
182try:
183 xrange = xrange
184except NameError: # Python 3 doesn't have xrange()
185 xrange = range
186try:
187 unichr = unichr
188except NameError: # Python 3 doesn't have unichr()
189 unichr = chr
190try:
191 basestring = basestring
192except NameError: # Python 3 doesn't have basestring
193 basestring = (str, bytes)
195# Inernationalization support
196_ = str # So pyflakes doesn't complain
197import gettext
198gettext.install('terminal')
200# Globals
201_logged_pil_warning = False # Used so we don't spam the user with warnings
202_logged_mutagen_warning = False # Ditto
203CALLBACK_SCROLL_UP = 1 # Called after a scroll up event (new line)
204CALLBACK_CHANGED = 2 # Called after the screen is updated
205CALLBACK_CURSOR_POS = 3 # Called after the cursor position is updated
206# <waives hand in air> You are not concerned with the number 4
207CALLBACK_DSR = 5 # Called when a DSR requires a response
208# NOTE: CALLBACK_DSR must accept 'response' as either the first argument or
209# as a keyword argument.
210CALLBACK_TITLE = 6 # Called when the terminal sets the window title
211CALLBACK_BELL = 7 # Called after ASCII_BEL is encountered.
212CALLBACK_OPT = 8 # Called when we encounter the optional ESC sequence
213# NOTE: CALLBACK_OPT must accept 'chars' as either the first argument or as
214# a keyword argument.
215CALLBACK_MODE = 9 # Called when the terminal mode changes (e.g. DECCKM)
216CALLBACK_RESET = 10 # Called when a terminal reset (^[[!p) is encountered
217CALLBACK_LEDS = 11 # Called when the state of the LEDs changes
218# Called when the terminal emulator encounters a situation where it wants to
219# tell the user about something (say, an error decoding an image) without
220# interfering with the terminal's screen.
221CALLBACK_MESSAGE = 12
223# These are for HTML output:
224RENDITION_CLASSES = defaultdict(lambda: None, {
225 0: 'reset', # Special: Return everything to defaults
226 1: 'bold',
227 2: 'dim',
228 3: 'italic',
229 4: 'underline',
230 5: 'blink',
231 6: 'fastblink',
232 7: 'reverse',
233 8: 'hidden',
234 9: 'strike',
235 10: 'fontreset', # NOTE: The font renditions don't do anything right now
236 11: 'font11', # Mostly because I have no idea what they are supposed to look
237 12: 'font12', # like.
238 13: 'font13',
239 14: 'font14',
240 15: 'font15',
241 16: 'font16',
242 17: 'font17',
243 18: 'font18',
244 19: 'font19',
245 20: 'fraktur',
246 21: 'boldreset',
247 22: 'dimreset',
248 23: 'italicreset',
249 24: 'underlinereset',
250 27: 'reversereset',
251 28: 'hiddenreset',
252 29: 'strikereset',
253 # Foregrounds
254 30: 'f0', # Black
255 31: 'f1', # Red
256 32: 'f2', # Green
257 33: 'f3', # Yellow
258 34: 'f4', # Blue
259 35: 'f5', # Magenta
260 36: 'f6', # Cyan
261 37: 'f7', # White
262 38: '', # 256-color support uses this like so: \x1b[38;5;<color num>sm
263 39: 'foregroundreset', # Special: Set FG to default
264 # Backgrounds
265 40: 'b0', # Black
266 41: 'b1', # Red
267 42: 'b2', # Green
268 43: 'b3', # Yellow
269 44: 'b4', # Blue
270 45: 'b5', # Magenta
271 46: 'b6', # Cyan
272 47: 'b7', # White
273 48: '', # 256-color support uses this like so: \x1b[48;5;<color num>sm
274 49: 'backgroundreset', # Special: Set BG to default
275 51: 'frame',
276 52: 'encircle',
277 53: 'overline',
278 60: 'rightline',
279 61: 'rightdoubleline',
280 62: 'leftline',
281 63: 'leftdoubleline',
282 # aixterm colors (aka '16 color support'). They're supposed to be 'bright'
283 # versions of the first 8 colors (hence the 'b').
284 # 'Bright' Foregrounds
285 90: 'bf0', # Bright black (whatever that is =)
286 91: 'bf1', # Bright red
287 92: 'bf2', # Bright green
288 93: 'bf3', # Bright yellow
289 94: 'bf4', # Bright blue
290 95: 'bf5', # Bright magenta
291 96: 'bf6', # Bright cyan
292 97: 'bf7', # Bright white
293# TODO: Handle the ESC sequence that sets the colors from 90-87 (e.g. ESC]91;orange/brown^G)
294 # 'Bright' Backgrounds
295 100: 'bb0', # Bright black
296 101: 'bb1', # Bright red
297 102: 'bb2', # Bright green
298 103: 'bb3', # Bright yellow
299 104: 'bb4', # Bright blue
300 105: 'bb5', # Bright magenta
301 106: 'bb6', # Bright cyan
302 107: 'bb7' # Bright white
303})
304# Generate the dict of 256-color (xterm) foregrounds and backgrounds
305for i in xrange(256):
306 RENDITION_CLASSES[(i+1000)] = "fx%s" % i
307 RENDITION_CLASSES[(i+10000)] = "bx%s" % i
308del i # Cleanup
310RESET_CLASSES = set([
311 'backgroundreset',
312 'boldreset',
313 'dimreset',
314 'italicreset',
315 'underlinereset',
316 'reversereset',
317 'hiddenreset',
318 'strikereset',
319 'resetfont'
320])
322try:
323 unichr(0x10000) # Will throw a ValueError on narrow Python builds
324 SPECIAL = 1048576 # U+100000 or unichr(SPECIAL) (start of Plane 16)
325except (ValueError, NameError):
326 # Narrow Python builds or Python 3 (no unichr function)
327 SPECIAL = 63561
329def handle_special(e):
330 """
331 Used in conjunction with :py:func:`codecs.register_error`, will replace
332 special ascii characters such as 0xDA and 0xc4 (which are used by ncurses)
333 with their Unicode equivalents.
334 """
335 # TODO: Get this using curses special characters when appropriate
336 #curses_specials = {
337 ## NOTE: When $TERM is set to "Linux" these end up getting used by things
338 ## like ncurses-based apps. In other words, it makes a whole lot
339 ## of ugly look pretty again.
340 #0xda: u'┌', # ACS_ULCORNER
341 #0xc0: u'└', # ACS_LLCORNER
342 #0xbf: u'┐', # ACS_URCORNER
343 #0xd9: u'┘', # ACS_LRCORNER
344 #0xb4: u'├', # ACS_RTEE
345 #0xc3: u'┤', # ACS_LTEE
346 #0xc1: u'┴', # ACS_BTEE
347 #0xc2: u'┬', # ACS_TTEE
348 #0xc4: u'─', # ACS_HLINE
349 #0xb3: u'│', # ACS_VLINE
350 #0xc5: u'┼', # ACS_PLUS
351 #0x2d: u'', # ACS_S1
352 #0x5f: u'', # ACS_S9
353 #0x60: u'◆', # ACS_DIAMOND
354 #0xb2: u'▒', # ACS_CKBOARD
355 #0xf8: u'°', # ACS_DEGREE
356 #0xf1: u'±', # ACS_PLMINUS
357 #0xf9: u'•', # ACS_BULLET
358 #0x3c: u'←', # ACS_LARROW
359 #0x3e: u'→', # ACS_RARROW
360 #0x76: u'↓', # ACS_DARROW
361 #0x5e: u'↑', # ACS_UARROW
362 #0xb0: u'⊞', # ACS_BOARD
363 #0x0f: u'⨂', # ACS_LANTERN
364 #0xdb: u'█', # ACS_BLOCK
365 #}
366 specials = {
367# Note to self: Why did I bother with these overly descriptive comments? Ugh
368# I've been staring at obscure symbols far too much lately ⨀_⨀
369 128: u'€', # Euro sign
370 129: u' ', # Unknown (Using non-breaking spaces for all unknowns)
371 130: u'‚', # Single low-9 quotation mark
372 131: u'ƒ', # Latin small letter f with hook
373 132: u'„', # Double low-9 quotation mark
374 133: u'…', # Horizontal ellipsis
375 134: u'†', # Dagger
376 135: u'‡', # Double dagger
377 136: u'ˆ', # Modifier letter circumflex accent
378 137: u'‰', # Per mille sign
379 138: u'Š', # Latin capital letter S with caron
380 139: u'‹', # Single left-pointing angle quotation
381 140: u'Œ', # Latin capital ligature OE
382 141: u' ', # Unknown
383 142: u'Ž', # Latin captial letter Z with caron
384 143: u' ', # Unknown
385 144: u' ', # Unknown
386 145: u'‘', # Left single quotation mark
387 146: u'’', # Right single quotation mark
388 147: u'“', # Left double quotation mark
389 148: u'”', # Right double quotation mark
390 149: u'•', # Bullet
391 150: u'–', # En dash
392 151: u'—', # Em dash
393 152: u'˜', # Small tilde
394 153: u'™', # Trade mark sign
395 154: u'š', # Latin small letter S with caron
396 155: u'›', # Single right-pointing angle quotation mark
397 156: u'œ', # Latin small ligature oe
398 157: u'Ø', # Upper-case slashed zero--using same as empty set (216)
399 158: u'ž', # Latin small letter z with caron
400 159: u'Ÿ', # Latin capital letter Y with diaeresis
401 160: u' ', # Non-breaking space
402 161: u'¡', # Inverted exclamation mark
403 162: u'¢', # Cent sign
404 163: u'£', # Pound sign
405 164: u'¤', # Currency sign
406 165: u'¥', # Yen sign
407 166: u'¦', # Pipe, Broken vertical bar
408 167: u'§', # Section sign
409 168: u'¨', # Spacing diaeresis - umlaut
410 169: u'©', # Copyright sign
411 170: u'ª', # Feminine ordinal indicator
412 171: u'«', # Left double angle quotes
413 172: u'¬', # Not sign
414 173: u"\u00AD", # Soft hyphen
415 174: u'®', # Registered trade mark sign
416 175: u'¯', # Spacing macron - overline
417 176: u'°', # Degree sign
418 177: u'±', # Plus-or-minus sign
419 178: u'²', # Superscript two - squared
420 179: u'³', # Superscript three - cubed
421 180: u'´', # Acute accent - spacing acute
422 181: u'µ', # Micro sign
423 182: u'¶', # Pilcrow sign - paragraph sign
424 183: u'·', # Middle dot - Georgian comma
425 184: u'¸', # Spacing cedilla
426 185: u'¹', # Superscript one
427 186: u'º', # Masculine ordinal indicator
428 187: u'»', # Right double angle quotes
429 188: u'¼', # Fraction one quarter
430 189: u'½', # Fraction one half
431 190: u'¾', # Fraction three quarters
432 191: u'¿', # Inverted question mark
433 192: u'À', # Latin capital letter A with grave
434 193: u'Á', # Latin capital letter A with acute
435 194: u'Â', # Latin capital letter A with circumflex
436 195: u'Ã', # Latin capital letter A with tilde
437 196: u'Ä', # Latin capital letter A with diaeresis
438 197: u'Å', # Latin capital letter A with ring above
439 198: u'Æ', # Latin capital letter AE
440 199: u'Ç', # Latin capital letter C with cedilla
441 200: u'È', # Latin capital letter E with grave
442 201: u'É', # Latin capital letter E with acute
443 202: u'Ê', # Latin capital letter E with circumflex
444 203: u'Ë', # Latin capital letter E with diaeresis
445 204: u'Ì', # Latin capital letter I with grave
446 205: u'Í', # Latin capital letter I with acute
447 206: u'Î', # Latin capital letter I with circumflex
448 207: u'Ï', # Latin capital letter I with diaeresis
449 208: u'Ð', # Latin capital letter ETH
450 209: u'Ñ', # Latin capital letter N with tilde
451 210: u'Ò', # Latin capital letter O with grave
452 211: u'Ó', # Latin capital letter O with acute
453 212: u'Ô', # Latin capital letter O with circumflex
454 213: u'Õ', # Latin capital letter O with tilde
455 214: u'Ö', # Latin capital letter O with diaeresis
456 215: u'×', # Multiplication sign
457 216: u'Ø', # Latin capital letter O with slash (aka "empty set")
458 217: u'Ù', # Latin capital letter U with grave
459 218: u'Ú', # Latin capital letter U with acute
460 219: u'Û', # Latin capital letter U with circumflex
461 220: u'Ü', # Latin capital letter U with diaeresis
462 221: u'Ý', # Latin capital letter Y with acute
463 222: u'Þ', # Latin capital letter THORN
464 223: u'ß', # Latin small letter sharp s - ess-zed
465 224: u'à', # Latin small letter a with grave
466 225: u'á', # Latin small letter a with acute
467 226: u'â', # Latin small letter a with circumflex
468 227: u'ã', # Latin small letter a with tilde
469 228: u'ä', # Latin small letter a with diaeresis
470 229: u'å', # Latin small letter a with ring above
471 230: u'æ', # Latin small letter ae
472 231: u'ç', # Latin small letter c with cedilla
473 232: u'è', # Latin small letter e with grave
474 233: u'é', # Latin small letter e with acute
475 234: u'ê', # Latin small letter e with circumflex
476 235: u'ë', # Latin small letter e with diaeresis
477 236: u'ì', # Latin small letter i with grave
478 237: u'í', # Latin small letter i with acute
479 238: u'î', # Latin small letter i with circumflex
480 239: u'ï', # Latin small letter i with diaeresis
481 240: u'ð', # Latin small letter eth
482 241: u'ñ', # Latin small letter n with tilde
483 242: u'ò', # Latin small letter o with grave
484 243: u'ó', # Latin small letter o with acute
485 244: u'ô', # Latin small letter o with circumflex
486 245: u'õ', # Latin small letter o with tilde
487 246: u'ö', # Latin small letter o with diaeresis
488 247: u'÷', # Division sign
489 248: u'ø', # Latin small letter o with slash
490 249: u'ù', # Latin small letter u with grave
491 250: u'ú', # Latin small letter u with acute
492 251: u'û', # Latin small letter u with circumflex
493 252: u'ü', # Latin small letter u with diaeresis
494 253: u'ý', # Latin small letter y with acute
495 254: u'þ', # Latin small letter thorn
496 255: u'ÿ', # Latin small letter y with diaeresis
497 }
498 # I left this in its odd state so I could differentiate between the two
499 # in the future.
500 chars = e.object
501 if bytes == str: # Python 2
502 # Convert e.object to a bytearray for an easy switch to integers.
503 # It is quicker than calling ord(char) on each char in e.object
504 chars = bytearray(e.object)
505 # NOTE: In Python 3 when you iterate over bytes they appear as integers.
506 # So we don't need to convert to a bytearray in Python 3.
507 if isinstance(e, (UnicodeEncodeError, UnicodeTranslateError)):
508 s = [u'%s' % specials[c] for c in chars[e.start:e.end]]
509 return ''.join(s), e.end
510 else:
511 s = [u'%s' % specials[c] for c in chars[e.start:e.end]]
512 return ''.join(s), e.end
513codecs.register_error('handle_special', handle_special)
515# TODO List:
516#
517# * We need unit tests!
518# * Add a function that can dump the screen with text renditions represented as their usual escape sequences so applications that try to perform screen-scraping can match things like '\x1b[41mAuthentication configuration' without having to find specific character positions and then examining the renditions on that line.
520# Helper functions
521def _reduce_renditions(renditions):
522 """
523 Takes a list, *renditions*, and reduces it to its logical equivalent (as
524 far as renditions go). Example::
526 [0, 32, 0, 34, 0, 32]
528 Would become::
530 [0, 32]
532 Other Examples::
534 [0, 1, 36, 36] -> [0, 1, 36]
535 [0, 30, 42, 30, 42] -> [0, 30, 42]
536 [36, 32, 44, 42] -> [32, 42]
537 [36, 35] -> [35]
538 """
539 out_renditions = []
540 foreground = None
541 background = None
542 for rend in renditions:
543 if rend < 29:
544 if rend not in out_renditions:
545 out_renditions.append(rend)
546 elif rend > 29 and rend < 40:
547 # Regular 8-color foregrounds
548 foreground = rend
549 elif rend > 39 and rend < 50:
550 # Regular 8-color backgrounds
551 background = rend
552 elif rend > 91 and rend < 98:
553 # 'Bright' (16-color) foregrounds
554 foreground = rend
555 elif rend > 99 and rend < 108:
556 # 'Bright' (16-color) backgrounds
557 background = rend
558 elif rend > 1000 and rend < 10000:
559 # 256-color foregrounds
560 foreground = rend
561 elif rend > 10000 and rend < 20000:
562 # 256-color backgrounds
563 background = rend
564 else:
565 out_renditions.append(rend)
566 if foreground:
567 out_renditions.append(foreground)
568 if background:
569 out_renditions.append(background)
570 return out_renditions
572def unicode_counter():
573 """
574 A generator that returns incrementing Unicode characters that can be used as
575 references inside a Unicode array. For example::
577 >>> counter = unicode_counter()
578 >>> mapping_dict = {}
579 >>> some_array = array('u')
580 >>> # Pretend 'marker ...' below is a reference to something important
581 >>> for i, c in enumerate(u'some string'):
582 ... if c == u' ': # Mark the location of spaces
583 ... # Perform some operation where we need to save a value
584 ... result = some_evaluation(i, c)
585 ... # Save some memory by storing a reference to result instead
586 ... # of the same result over and over again
587 ... if result not in mapping_dict.values():
588 ... marker = counter.next()
589 ... some_array.append(marker)
590 ... mapping_dict[marker] = result
591 ... else: # Find the existing reference so we can use it again
592 ... for k, v in mapping_dict.items():
593 ... if v == result: # Use the existing value
594 ... some_array.append(k)
595 ... else:
596 ... some_array.append('\x00') # \x00 == "not interesting" placeholder
597 >>>
599 Now we could iterate over 'some string' and some_array simultaneously using
600 zip(u'some string', some_array) to access those reference markers when we
601 encountered the correct position. This can save a lot of memory if you need
602 to store objects in memory that have a tendancy to repeat (e.g. text
603 rendition lists in a terminal).
605 .. note:: Meant to be used inside the renditions array to reference text rendition lists such as `[0, 1, 34]`.
606 """
607 n = 1000 # Start at 1000 so we can use lower characters for other things
608 while True:
609 yield unichr(n)
610 if n == 65535: # The end of unicode in narrow builds of Python
611 n = 0 # Reset
612 else:
613 n += 1
615# NOTE: Why use a unicode array() to store references instead of just a regular array()? Two reasons: 1) Large namespace. 2) Only need to use one kind of array for everything (convenience). It is also a large memory savings over "just using a list with references to items in a dict."
616def pua_counter():
617 """
618 A generator that returns a Unicode Private Use Area (PUA) character starting
619 at the beginning of Plane 16 (U+100000); counting up by one with each
620 successive call. If this is a narrow Python build the tail end of Plane 15
621 will be used as a fallback (with a lot less characters).
623 .. note::
625 Meant to be used as references to non-text objects in the screen array()
626 (since it can only contain unicode characters)
627 """
628 if SPECIAL == 1048576: # Not a narrow build of Python
629 n = SPECIAL # U+100000 or unichr(SPECIAL) (start of Plane 16)
630 while True:
631 yield unichr(n)
632 if n == 1114111:
633 n = SPECIAL # Reset--would be impressive to make it this far!
634 else:
635 n += 1
636 else:
637 # This Python build is 'narrow' so we have to settle for less
638 # Hopefully no real-world terminal will actually want to use one of
639 # these characters. In my research I couldn't find a font that used
640 # them. Please correct me if I'm wrong!
641 n = SPECIAL # u'\uf849'
642 while True:
643 yield unichr(n)
644 if n == 63717: # The end of nothing-but-block-chars in Plane 15
645 n = SPECIAL # Reset
646 else:
647 n += 1
649def convert_to_timedelta(time_val):
650 """
651 Given a *time_val* (string) such as '5d', returns a `datetime.timedelta`
652 object representing the given value (e.g. `timedelta(days=5)`). Accepts the
653 following '<num><char>' formats:
655 ========= ============ =========================
656 Character Meaning Example
657 ========= ============ =========================
658 (none) Milliseconds '500' -> 500 Milliseconds
659 s Seconds '60s' -> 60 Seconds
660 m Minutes '5m' -> 5 Minutes
661 h Hours '24h' -> 24 Hours
662 d Days '7d' -> 7 Days
663 M Months '2M' -> 2 Months
664 y Years '10y' -> 10 Years
665 ========= ============ =========================
667 Examples::
669 >>> convert_to_timedelta('7d')
670 datetime.timedelta(7)
671 >>> convert_to_timedelta('24h')
672 datetime.timedelta(1)
673 >>> convert_to_timedelta('60m')
674 datetime.timedelta(0, 3600)
675 >>> convert_to_timedelta('120s')
676 datetime.timedelta(0, 120)
677 """
678 try:
679 num = int(time_val)
680 return timedelta(milliseconds=num)
681 except ValueError:
682 pass
683 num = int(time_val[:-1])
684 if time_val.endswith('s'):
685 return timedelta(seconds=num)
686 elif time_val.endswith('m'):
687 return timedelta(minutes=num)
688 elif time_val.endswith('h'):
689 return timedelta(hours=num)
690 elif time_val.endswith('d'):
691 return timedelta(days=num)
692 elif time_val.endswith('M'):
693 return timedelta(days=(num*30)) # Yeah this is approximate
694 elif time_val.endswith('y'):
695 return timedelta(days=(num*365)) # Sorry, no leap year support
697def total_seconds(td):
698 """
699 Given a timedelta (*td*) return an integer representing the equivalent of
700 Python 2.7's :meth:`datetime.timdelta.total_seconds`.
701 """
702 return (((
703 td.microseconds +
704 (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6))
706# NOTE: This is something I'm investigating as a way to use the new go_async
707# module. A work-in-progress. Ignore for now...
708def spanify_screen(state_obj):
709 """
710 Iterates over the lines in *screen* and *renditions*, applying HTML
711 markup (span tags) where appropriate and returns the result as a list of
712 lines. It also marks the cursor position via a <span> tag at the
713 appropriate location.
714 """
715 #logging.debug("_spanify_screen()")
716 results = []
717 # NOTE: Why these duplicates of self.* and globals? Local variable
718 # lookups are faster--especially in loops.
719 #special = SPECIAL
720 rendition_classes = RENDITION_CLASSES
721 html_cache = state_obj['html_cache']
722 screen = state_obj['screen']
723 renditions = state_obj['renditions']
724 renditions_store = state_obj['renditions_store']
725 cursorX = state_obj['cursorX']
726 cursorY = state_obj['cursorY']
727 show_cursor = state_obj['show_cursor']
728 class_prefix = state_obj['class_prefix']
729 #captured_files = state_obj['captured_files']
730 spancount = 0
731 current_classes = set()
732 prev_rendition = None
733 foregrounds = ('f0','f1','f2','f3','f4','f5','f6','f7')
734 backgrounds = ('b0','b1','b2','b3','b4','b5','b6','b7')
735 html_entities = {"&": "&", '<': '<', '>': '>'}
736 cursor_span = '<span class="%scursor">' % class_prefix
737 for linecount, line_rendition in enumerate(izip(screen, renditions)):
738 line = line_rendition[0]
739 rendition = line_rendition[1]
740 combined = (line + rendition).tounicode()
741 if html_cache and combined in html_cache:
742 # Always re-render the line with the cursor (or just had it)
743 if cursor_span not in html_cache[combined]:
744 # Use the cache...
745 results.append(html_cache[combined])
746 continue
747 if not len(line.tounicode().rstrip()) and linecount != cursorY:
748 results.append(line.tounicode())
749 continue # Line is empty so we don't need to process renditions
750 outline = ""
751 if current_classes:
752 outline += '<span class="%s%s">' % (
753 class_prefix,
754 (" %s" % class_prefix).join(current_classes))
755 charcount = 0
756 for char, rend in izip(line, rendition):
757 rend = renditions_store[rend] # Get actual rendition
758 #if ord(char) >= special: # Special stuff =)
759 ## Obviously, not really a single character
760 #if char in captured_files:
761 #outline += captured_files[char].html()
762 #continue
763 changed = True
764 if char in "&<>":
765 # Have to convert ampersands and lt/gt to HTML entities
766 char = html_entities[char]
767 if rend == prev_rendition:
768 # Shortcut... So we can skip all the logic below
769 changed = False
770 else:
771 prev_rendition = rend
772 if changed and rend:
773 classes = imap(rendition_classes.get, rend)
774 for _class in classes:
775 if _class and _class not in current_classes:
776 # Something changed... Start a new span
777 if spancount:
778 outline += "</span>"
779 spancount -= 1
780 if 'reset' in _class:
781 if _class == 'reset':
782 current_classes = set()
783 if spancount:
784 for i in xrange(spancount):
785 outline += "</span>"
786 spancount = 0
787 else:
788 reset_class = _class.split('reset')[0]
789 if reset_class == 'foreground':
790 # Remove any foreground classes
791 [current_classes.pop(i) for i, a in
792 enumerate(current_classes) if a in
793 foregrounds
794 ]
795 elif reset_class == 'background':
796 [current_classes.pop(i) for i, a in
797 enumerate(current_classes) if a in
798 backgrounds
799 ]
800 else:
801 try:
802 current_classes.remove(reset_class)
803 except KeyError:
804 # Trying to reset something that was
805 # never set. Ignore
806 pass
807 else:
808 if _class in foregrounds:
809 [current_classes.pop(i) for i, a in
810 enumerate(current_classes) if a in
811 foregrounds
812 ]
813 elif _class in backgrounds:
814 [current_classes.pop(i) for i, a in
815 enumerate(current_classes) if a in
816 backgrounds
817 ]
818 current_classes.add(_class)
819 if current_classes:
820 outline += '<span class="%s%s">' % (
821 class_prefix,
822 (" %s" % class_prefix).join(current_classes))
823 spancount += 1
824 if linecount == cursorY and charcount == cursorX: # Cursor
825 if show_cursor:
826 outline += '<span class="%scursor">%s</span>' % (
827 class_prefix, char)
828 else:
829 outline += char
830 else:
831 outline += char
832 charcount += 1
833 if outline:
834 # Make sure all renditions terminate at the end of the line
835 for whatever in xrange(spancount):
836 outline += "</span>"
837 results.append(outline)
838 if html_cache:
839 html_cache[combined] = outline
840 else:
841 results.append(None) # null is shorter than 4 spaces
842 # NOTE: The client has been programmed to treat None (aka null in
843 # JavaScript) as blank lines.
844 for whatever in xrange(spancount): # Bit of cleanup to be safe
845 results[-1] += "</span>"
846 return (html_cache, results)
848# Exceptions
849class InvalidParameters(Exception):
850 """
851 Raised when `Terminal` is passed invalid parameters.
852 """
853 pass
855# Classes
856class AutoExpireDict(dict):
857 """
858 An override of Python's `dict` that expires keys after a given
859 *_expire_timeout* timeout (`datetime.timedelta`). The default expiration
860 is one hour. It is used like so::
862 >>> expiring_dict = AutoExpireDict(timeout=timedelta(minutes=10))
863 >>> expiring_dict['somekey'] = 'some value'
864 >>> # You can see when this key was created:
865 >>> print(expiring_dict.creation_times['somekey'])
866 2013-04-15 18:44:18.224072
868 10 minutes later your key will be gone::
870 >>> 'somekey' in expiring_dict
871 False
873 The 'timeout' may be be given as a `datetime.timedelta` object or a string
874 like, "1d", "30s" (will be passed through the `convert_to_timedelta`
875 function).
877 By default `AutoExpireDict` will check for expired keys every 30 seconds but
878 this can be changed by setting the 'interval'::
880 >>> expiring_dict = AutoExpireDict(interval=5000) # 5 secs
881 >>> # Or to change it after you've created one:
882 >>> expiring_dict.interval = "10s"
884 The 'interval' may be an integer, a `datetime.timedelta` object, or a string
885 such as '10s' or '5m' (will be passed through the `convert_to_timedelta`
886 function).
888 If there are no keys remaining the `tornado.ioloop.PeriodicCallback` (
889 ``self._key_watcher``) that checks expiration will be automatically stopped.
890 As soon as a new key is added it will be started back up again.
892 .. note::
894 Only works if there's a running instances of `tornado.ioloop.IOLoop`.
895 """
896 def __init__(self, *args, **kwargs):
897 self.io_loop = IOLoop.current()
898 self.creation_times = {}
899 if 'timeout' in kwargs:
900 self.timeout = kwargs.pop('timeout')
901 if 'interval' in kwargs:
902 self.interval = kwargs.pop('interval')
903 super(AutoExpireDict, self).__init__(*args, **kwargs)
904 # Set the start time on every key
905 for k in self.keys():
906 self.creation_times[k] = datetime.now()
907 self._key_watcher = PeriodicCallback(
908 self._timeout_checker, self.interval, io_loop=self.io_loop)
909 self._key_watcher.start() # Will shut down at the next interval if empty
911 @property
912 def timeout(self):
913 """
914 A `property` that controls how long a key will last before being
915 automatically removed. May be be given as a `datetime.timedelta`
916 object or a string like, "1d", "30s" (will be passed through the
917 `convert_to_timedelta` function).
918 """
919 if not hasattr(self, "_timeout"):
920 self._timeout = timedelta(hours=1) # Default is 1-hour timeout
921 return self._timeout
923 @timeout.setter
924 def timeout(self, value):
925 if isinstance(value, basestring):
926 value = convert_to_timedelta(value)
927 self._timeout = value
929 @property
930 def interval(self):
931 """
932 A `property` that controls how often we check for expired keys. May be
933 given as milliseconds (integer), a `datetime.timedelta` object, or a
934 string like, "1d", "30s" (will be passed through the
935 `convert_to_timedelta` function).
936 """
937 if not hasattr(self, "_interval"):
938 self._interval = 10000 # Default is every 10 seconds
939 return self._interval
941 @interval.setter
942 def interval(self, value):
943 if isinstance(value, basestring):
944 value = convert_to_timedelta(value)
945 if isinstance(value, timedelta):
946 value = total_seconds(value) * 1000 # PeriodicCallback uses ms
947 self._interval = value
948 # Restart the PeriodicCallback
949 if hasattr(self, '_key_watcher'):
950 self._key_watcher.stop()
951 self._key_watcher = PeriodicCallback(
952 self._timeout_checker, value, io_loop=self.io_loop)
954 def renew(self, key):
955 """
956 Resets the timeout on the given *key*; like it was just created.
957 """
958 self.creation_times[key] = datetime.now() # Set/renew the start time
959 # Start up the key watcher if it isn't already running
960 if not self._key_watcher._running:
961 self._key_watcher.start()
963 def __setitem__(self, key, value):
964 """
965 An override that tracks when keys are updated.
966 """
967 super(AutoExpireDict, self).__setitem__(key, value) # Set normally
968 self.renew(key) # Set/renew the start time
970 def __delitem__(self, key):
971 """
972 An override that makes sure *key* gets removed from
973 ``self.creation_times`` dict.
974 """
975 del self.creation_times[key]
976 super(AutoExpireDict, self).__delitem__(key)
978 def __del__(self):
979 """
980 Ensures that our `tornado.ioloop.PeriodicCallback`
981 (``self._key_watcher``) gets stopped.
982 """
983 self._key_watcher.stop()
985 def update(self, *args, **kwargs):
986 """
987 An override that calls ``self.renew()`` for every key that gets updated.
988 """
989 super(AutoExpireDict, self).update(*args, **kwargs)
990 for key, value in kwargs.items():
991 self.renew(key)
993 def clear(self):
994 """
995 An override that empties ``self.creation_times`` and calls
996 ``self._key_watcher.stop()``.
997 """
998 super(AutoExpireDict, self).clear()
999 self.creation_times.clear()
1000 # Shut down the key watcher right away
1001 self._key_watcher.stop()
1003 def _timeout_checker(self):
1004 """
1005 Walks ``self`` and removes keys that have passed the expiration point.
1006 """
1007 if not self.creation_times:
1008 self._key_watcher.stop() # Nothing left to watch
1009 for key, starttime in list(self.creation_times.items()):
1010 if datetime.now() - starttime > self.timeout:
1011 del self[key]
1013# AutoExpireDict only works if Tornado is present.
1014# Don't use the HTML_CACHE if Tornado isn't available.
1015# For OpenHCS, we disable HTML_CACHE since we don't use Tornado
1016HTML_CACHE = None
1018class FileType(object):
1019 """
1020 An object to hold the attributes of a supported file capture/output type.
1021 """
1022 # These attributes are here to prevent AttributeErrors if not overridden
1023 thumbnail = None
1024 html_template = "" # Must be overridden
1025 html_icon_template = "" # Must be overridden
1026 # This is for things like PDFs which can contain other FileTypes:
1027 is_container = False # Must be overridden
1028 helper = None # Optional function to be called when a capture is started
1029 original_file = None # Can be used when the file is modified
1030 def __init__(self,
1031 name, mimetype, re_header, re_capture, suffix="", path="", linkpath="", icondir=None):
1032 """
1033 **name:** Name of the file type.
1034 **mimetype:** Mime type of the file.
1035 **re_header:** The regex to match the start of the file.
1036 **re_capture:** The regex to carve the file out of the stream.
1037 **suffix:** (optional) The suffix to be appended to the end of the filename (if one is generated).
1038 **path:** (optional) The path to a file or directory where the file should be stored. If *path* is a directory a random filename will be chosen.
1039 **linkpath:** (optional) The path to use when generating a link in HTML output.
1040 **icondir:** (optional) A path to look for a relevant icon to display when generating HTML output.
1041 """
1042 self.name = name
1043 self.mimetype = mimetype
1044 self.re_header = re_header
1045 self.re_capture = re_capture
1046 self.suffix = suffix
1047 # A path just in case something needs to access it outside of Python:
1048 self.path = path
1049 self.linkpath = linkpath
1050 self.icondir = icondir
1051 self.file_obj = None
1053 def __repr__(self):
1054 return "<%s>" % self.name
1056 def __str__(self):
1057 "Override if the defined file type warrants a text-based output."
1058 return self.__repr__()
1060 def __del__(self):
1061 """
1062 Make sure that self.file_obj gets closed/deleted.
1063 """
1064 logging.debug("FileType __del__(): Closing/deleting temp file(s)")
1065 try:
1066 self.file_obj.close() # Ensures it gets deleted
1067 except AttributeError:
1068 pass # Probably never got opened properly (bad file); no big
1069 try:
1070 self.original_file.close()
1071 except AttributeError:
1072 pass # Probably never got opened/saved properly
1074 def raw(self):
1075 self.file_obj.seek(0)
1076 data = open(self.file_obj).read()
1077 self.file_obj.seek(0)
1078 return data
1080 def html(self):
1081 """
1082 Returns the object as an HTML-formatted string. Must be overridden.
1083 """
1084 raise NotImplementedError
1086 def capture(self, data, term_instance=None):
1087 """
1088 Stores *data* as a temporary file and returns that file's object.
1089 *term_instance* can be used by overrides of this function to make
1090 adjustments to the terminal emulator after the *data* is captured e.g.
1091 to make room for an image.
1092 """
1093 # Remove the extra \r's that the terminal adds:
1094 data = data.replace(b'\r\n', b'\n')
1095 logging.debug("capture() len(data): %s" % len(data))
1096 # Write the data to disk in a temporary location
1097 self.file_obj = tempfile.TemporaryFile()
1098 self.file_obj.write(data)
1099 self.file_obj.flush()
1100 # Leave it open
1101 return self.file_obj
1103 def close(self):
1104 """
1105 Closes :attr:`self.file_obj`
1106 """
1107 try:
1108 self.file_obj.close()
1109 except AttributeError:
1110 pass # file object never got created properly (probably missing PIL)
1112class ImageFile(FileType):
1113 """
1114 A subclass of :class:`FileType` for images (specifically to override
1115 :meth:`self.html` and :meth:`self.capture`).
1116 """
1117 def capture(self, data, term_instance):
1118 """
1119 Captures the image contained within *data*. Will use *term_instance*
1120 to make room for the image in the terminal screen.
1122 .. note:: Unlike :class:`FileType`, *term_instance* is mandatory.
1123 """
1124 logging.debug('ImageFile.capture()')
1125 global _logged_pil_warning
1126 Image = False
1127 try:
1128 from PIL import Image
1129 except ImportError:
1130 if _logged_pil_warning:
1131 return
1132 _logged_pil_warning = True
1133 logging.warning(_(
1134 "Could not import the Python Imaging Library (PIL). "
1135 "Images will not be displayed in the terminal."))
1136 logging.info(_(
1137 "TIP: Pillow is a 'friendly fork' of PIL that has been updated "
1138 "to work with Python 3 (also works in Python 2.X). You can "
1139 "install it with: pip install --upgrade pillow"))
1140 return # No PIL means no images. Don't bother wasting memory.
1141 if _logged_pil_warning:
1142 _logged_pil_warning = False
1143 logging.info(_(
1144 "Good job installing PIL! Terminal image suppport has been "
1145 "re-enabled. Aren't dynamic imports grand?"))
1146 #open('/tmp/lastimage.img', 'w').write(data) # Use for debug
1147 # Image file formats don't usually like carriage returns:
1148 data = data.replace(b'\r\n', b'\n') # shell adds an extra /r
1149 i = BytesIO(data)
1150 try:
1151 im = Image.open(i)
1152 except (AttributeError, IOError) as e:
1153 # i.e. PIL couldn't identify the file
1154 message = _("PIL couldn't process the image (%s)" % e)
1155 logging.error(message)
1156 term_instance.send_message(message)
1157 return # Don't do anything--bad image
1158 # Save a copy of the data so the user can have access to the original
1159 if self.path:
1160 if os.path.exists(self.path):
1161 if os.path.isdir(self.path):
1162 self.original_file = tempfile.NamedTemporaryFile(
1163 suffix=self.suffix, dir=self.path)
1164 self.original_file.write(data)
1165 self.original_file.flush()
1166 self.original_file.seek(0) # Just in case
1167 # Resize the image to be small enough to fit within a typical terminal
1168 if im.size[0] > 640 or im.size[1] > 480:
1169 im.thumbnail((640, 480), Image.ANTIALIAS)
1170 # Get the current image location and reference so we can move it around
1171 img_Y = term_instance.cursorY
1172 img_X = term_instance.cursorX
1173 ref = term_instance.screen[img_Y][img_X]
1174 if term_instance.em_dimensions:
1175 # Make sure the image will fit properly in the screen
1176 width = im.size[0]
1177 height = im.size[1]
1178 if height <= term_instance.em_dimensions['height']:
1179 # Fits within a line. No need for a newline
1180 num_chars = int(width/term_instance.em_dimensions['width'])
1181 # Move the cursor an equivalent number of characters
1182 term_instance.cursor_right(num_chars)
1183 else:
1184 # This is how many newlines the image represents:
1185 newlines = int(height/term_instance.em_dimensions['height'])
1186 term_instance.screen[img_Y][img_X] = u' ' # Empty old location
1187 term_instance.cursorX = 0
1188 term_instance.newline() # Start with a newline
1189 if newlines > term_instance.cursorY:
1190 # Shift empty lines at the bottom to the top to kinda sorta
1191 # make room for the images so the user doesn't have to
1192 # scroll (hey, it works!)
1193 for i in xrange(newlines):
1194 line = term_instance.screen.pop()
1195 rendition = term_instance.renditions.pop()
1196 term_instance.screen.insert(0, line)
1197 term_instance.renditions.insert(0, rendition)
1198 if term_instance.cursorY < (term_instance.rows - 1):
1199 term_instance.cursorY += 1
1200 # Save the new image location
1201 term_instance.screen[
1202 term_instance.cursorY][term_instance.cursorX] = ref
1203 term_instance.newline() # Follow-up newline
1204 elif term_instance.em_dimensions == None:
1205 # No way to calculate the number of lines the image will take
1206 term_instance.screen[img_Y][img_X] = u' ' # Empty old location
1207 term_instance.cursorY = term_instance.rows - 1 # Move to the end
1208 # ... so it doesn't get cut off at the top
1209 # Save the new image location
1210 term_instance.screen[
1211 term_instance.cursorY][term_instance.cursorX] = ref
1212 # Make some space at the bottom too just in case
1213 term_instance.newline()
1214 term_instance.newline()
1215 else:
1216 # When em_dimensions are set to 0 assume the user intentionally
1217 # wants things to be sized as inline as possible.
1218 term_instance.newline()
1219 # Write the captured image to disk
1220 if self.path:
1221 if os.path.exists(self.path):
1222 if os.path.isdir(self.path):
1223 # Assume that a path was given for a reason and use a
1224 # NamedTemporaryFile instead of TemporaryFile.
1225 self.file_obj = tempfile.NamedTemporaryFile(
1226 suffix=self.suffix, dir=self.path)
1227 # Update self.path to use the new, actual file path
1228 self.path = self.file_obj.name
1229 else:
1230 self.file_obj = open(self.path, 'rb+')
1231 else:
1232 self.file_obj = tempfile.TemporaryFile(suffix=self.suffix)
1233 try:
1234 im.save(self.file_obj, im.format)
1235 except (AttributeError, IOError):
1236 # PIL was compiled without (complete) support for this format
1237 logging.error(_(
1238 "PIL is missing support for this image type (%s). You probably"
1239 " need to install zlib-devel and libjpeg-devel then re-install "
1240 "it with 'pip install --upgrade PIL' or 'pip install "
1241 "--upgrade Pillow'" % self.name))
1242 try:
1243 self.file_obj.close() # Can't do anything with it
1244 except AttributeError:
1245 pass # File was probably just never opened/saved properly
1246 return None
1247 self.file_obj.flush()
1248 self.file_obj.seek(0) # Go back to the start
1249 return self.file_obj
1251 def html(self):
1252 """
1253 Returns :attr:`self.file_obj` as an <img> tag with the src set to a
1254 data::URI.
1255 """
1256 try:
1257 from PIL import Image
1258 except ImportError:
1259 return # Warnings will have already been printed by this point
1260 if not self.file_obj:
1261 return u""
1262 self.file_obj.seek(0)
1263 try:
1264 im = Image.open(self.file_obj)
1265 except IOError:
1266 # i.e. PIL couldn't identify the file
1267 return u"<i>Error displaying image</i>"
1268 self.file_obj.seek(0)
1269 # Need to encode base64 to create a data URI
1270 encoded = base64.b64encode(self.file_obj.read())
1271 data_uri = "data:image/{type};base64,{encoded}".format(
1272 type=im.format.lower(), encoded=encoded.decode('utf-8'))
1273 link = "%s/%s" % (self.linkpath, os.path.split(self.path)[1])
1274 if self.original_file:
1275 link = "%s/%s" % (
1276 self.linkpath, os.path.split(self.original_file.name)[1])
1277 if self.thumbnail:
1278 return self.html_icon_template.format(
1279 link=link,
1280 src=data_uri,
1281 width=im.size[0],
1282 height=im.size[1])
1283 return self.html_template.format(
1284 link=link, src=data_uri, width=im.size[0], height=im.size[1])
1286class PNGFile(ImageFile):
1287 """
1288 An override of :class:`ImageFile` for PNGs to hard-code the name, regular
1289 expressions, mimetype, and suffix.
1290 """
1291 name = _("PNG Image")
1292 mimetype = "image/png"
1293 suffix = ".png"
1294 re_header = re.compile(b'.*\x89PNG\r', re.DOTALL)
1295 re_capture = re.compile(b'(\x89PNG\r.+?IEND\xaeB`\x82)', re.DOTALL)
1296 html_template = (
1297 '<a target="_blank" href="{link}" '
1298 'title="Click to open the original file in a new window (full size)">'
1299 '<img src="{src}" width="{width}" height="{height}">'
1300 '</a>'
1301 )
1303 def __init__(self, path="", linkpath="", **kwargs):
1304 """
1305 **path:** (optional) The path to a file or directory where the file
1306 should be stored. If *path* is a directory a random filename will be
1307 chosen.
1308 """
1309 self.path = path
1310 self.linkpath = linkpath
1311 self.file_obj = None
1312 # Images will be displayed inline so no icons unless overridden:
1313 self.html_icon_template = self.html_template
1315class JPEGFile(ImageFile):
1316 """
1317 An override of :class:`ImageFile` for JPEGs to hard-code the name, regular
1318 expressions, mimetype, and suffix.
1319 """
1320 name = _("JPEG Image")
1321 mimetype = "image/jpeg"
1322 suffix = ".jpeg"
1323 re_header = re.compile(
1324 b'.*\xff\xd8\xff.+JFIF\x00|.*\xff\xd8\xff.+Exif\x00', re.DOTALL)
1325 re_capture = re.compile(b'(\xff\xd8\xff.+?\xff\xd9)', re.DOTALL)
1326 html_template = (
1327 '<a target="_blank" href="{link}" '
1328 'title="Click to open the original file in a new window (full size)">'
1329 '<img src="{src}" width="{width}" height="{height}">'
1330 '</a>'
1331 )
1332 def __init__(self, path="", linkpath="", **kwargs):
1333 """
1334 **path:** (optional) The path to a file or directory where the file
1335 should be stored. If *path* is a directory a random filename will be
1336 chosen.
1337 """
1338 self.path = path
1339 self.linkpath = linkpath
1340 self.file_obj = None
1341 # Images will be displayed inline so no icons unless overridden:
1342 self.html_icon_template = self.html_template
1344class SoundFile(FileType):
1345 """
1346 A subclass of :class:`FileType` for sound files (e.g. .wav). Overrides
1347 :meth:`self.html` and :meth:`self.capture`.
1348 """
1349 # NOTE: I disabled autoplay on these sounds because it causes the browser to
1350 # play back the sound with every screen update! Press return a few times
1351 # and the sound will play a few times; annoying!
1352 html_template = (
1353 '<audio controls>'
1354 '<source src="{src}" type="{mimetype}">'
1355 'Your browser does not support this audio format.'
1356 '</audio>'
1357 )
1358 display_metadata = None # Can be overridden to send a message to the user
1359 def capture(self, data, term_instance):
1360 """
1361 Captures the sound contained within *data*. Will use *term_instance*
1362 to make room for the embedded sound control in the terminal screen.
1364 .. note:: Unlike :class:`FileType`, *term_instance* is mandatory.
1365 """
1366 logging.debug('SoundFile.capture()')
1367 # Fix any carriage returns (generated by the shell):
1368 data = data.replace(b'\r\n', b'\n')
1369 # Make some room for the audio controls:
1370 term_instance.newline()
1371 # Write the captured image to disk
1372 if self.path:
1373 if os.path.exists(self.path):
1374 if os.path.isdir(self.path):
1375 # Assume that a path was given for a reason and use a
1376 # NamedTemporaryFile instead of TemporaryFile.
1377 self.file_obj = tempfile.NamedTemporaryFile(
1378 suffix=self.suffix, dir=self.path)
1379 # Update self.path to use the new, actual file path
1380 self.path = self.file_obj.name
1381 else:
1382 self.file_obj = open(self.path, 'rb+')
1383 else:
1384 self.file_obj = tempfile.TemporaryFile(suffix=self.suffix)
1385 self.file_obj.write(data)
1386 self.file_obj.flush()
1387 self.file_obj.seek(0) # Go back to the start
1388 if self.display_metadata:
1389 self.display_metadata(term_instance)
1390 return self.file_obj
1392 def html(self):
1393 """
1394 Returns :attr:`self.file_obj` as an <img> tag with the src set to a
1395 data::URI.
1396 """
1397 if not self.file_obj:
1398 return u""
1399 self.file_obj.seek(0)
1400 # Need to encode base64 to create a data URI
1401 encoded = base64.b64encode(self.file_obj.read())
1402 data_uri = "data:{mimetype};base64,{encoded}".format(
1403 mimetype=self.mimetype, encoded=encoded.decode('utf-8'))
1404 link = "%s/%s" % (self.linkpath, os.path.split(self.path)[1])
1405 if self.original_file:
1406 link = "%s/%s" % (
1407 self.linkpath, os.path.split(self.original_file.name)[1])
1408 if self.thumbnail:
1409 return self.html_icon_template.format(
1410 link=link,
1411 src=data_uri,
1412 icon=self.thumbnail,
1413 mimetype=self.mimetype)
1414 return self.html_template.format(
1415 link=link, src=data_uri, mimetype=self.mimetype)
1417class WAVFile(SoundFile):
1418 """
1419 An override of :class:`SoundFile` for WAVs to hard-code the name, regular
1420 expressions, mimetype, and suffix. Also, a :func:`helper` function is
1421 provided that adjusts the `self.re_capture` regex so that it precisely
1422 matches the WAV file being captured.
1423 """
1424 name = _("WAV Sound")
1425 mimetype = "audio/x-wav"
1426 suffix = ".wav"
1427 re_header = re.compile(b'RIFF....WAVEfmt', re.DOTALL)
1428 re_capture = re.compile(b'(RIFF....WAVEfmt.+?\r\n)', re.DOTALL)
1429 re_wav_header = re.compile(b'(RIFF.{40})', re.DOTALL)
1430 def __init__(self, path="", linkpath="", **kwargs):
1431 """
1432 **path:** (optional) The path to a file or directory where the file
1433 should be stored. If *path* is a directory a random filename will be
1434 chosen.
1435 """
1436 self.path = path
1437 self.linkpath = linkpath
1438 self.file_obj = None
1439 self.sent_message = False
1440 # Sounds will be displayed inline so no icons unless overridden:
1441 self.html_icon_template = self.html_template
1443 def helper(self, term_instance):
1444 """
1445 Called at the start of a WAV file capture. Calculates the length of the
1446 file and modifies `self.re_capture` with laser precision.
1447 """
1448 data = term_instance.capture
1449 self.wav_header = struct.unpack(
1450 '4si4s4sihhiihh4si', self.re_wav_header.match(data).group())
1451 self.wav_length = self.wav_header[1] + 8
1452 if not self.sent_message:
1453 channels = "mono"
1454 if self.wav_header[6] == 2:
1455 channels = "stereo"
1456 if self.wav_length != self.wav_header[12] + 44:
1457 # Corrupt WAV file
1458 message = _("WAV File is corrupted: Header data mismatch.")
1459 term_instance.send_message(message)
1460 term_instance.cancel_capture = True
1461 message = _("WAV File: %skHz (%s)" % (self.wav_header[7], channels))
1462 term_instance.send_message(message)
1463 self.sent_message = True
1464 # Update the capture regex with laser precision:
1465 self.re_capture = re.compile(
1466 b'(RIFF....WAVE.{%s})' % (self.wav_length-12), re.DOTALL)
1468class OGGFile(SoundFile):
1469 """
1470 An override of :class:`SoundFile` for OGGs to hard-code the name, regular
1471 expressions, mimetype, and suffix.
1472 """
1473 name = _("OGG Sound")
1474 mimetype = "audio/ogg"
1475 suffix = ".ogg"
1476 # NOTE: \x02 below marks "start of stream" (\x04 is "end of stream")
1477 re_header = re.compile(b'OggS\x00\x02', re.DOTALL)
1478 # NOTE: This should never actually match since it will be replaced by the
1479 # helper() function:
1480 re_capture = re.compile(b'(OggS\x00\x02.+OggS\x00\x04\r\n)', re.DOTALL)
1481 re_ogg_header = re.compile(b'(OggS\x00\x02.{21})', re.DOTALL)
1482 re_last_segment = re.compile(b'(OggS\x00\x04.{21})', re.DOTALL)
1483 def __init__(self, path="", linkpath="", **kwargs):
1484 """
1485 **path:** (optional) The path to a file or directory where the file
1486 should be stored. If *path* is a directory a random filename will be
1487 chosen.
1488 """
1489 self.path = path
1490 self.linkpath = linkpath
1491 self.file_obj = None
1492 self.sent_message = False
1493 # Sounds will be displayed inline so no icons unless overridden:
1494 self.html_icon_template = self.html_template
1496 def helper(self, term_instance):
1497 """
1498 Called at the start of a OGG file capture. Calculates the length of the
1499 file and modifies `self.re_capture` with laser precision. Returns
1500 `True` if the entire ogg has been captured.
1501 """
1502 data = term_instance.capture
1503 last_segment_header = self.re_last_segment.search(data)
1504 if not last_segment_header:
1505 #print("No last segment header yet")
1506 #print(repr(data.split('OggS')[-1]))
1507 #print('-----------------------------')
1508 return # Haven't reached the end of the OGG yet
1509 else:
1510 last_segment_header = last_segment_header.group()
1511 # This decodes the OGG page header
1512 (oggs, version, type_flags, position,
1513 serial, sequence, crc, segments) = struct.unpack(
1514 "<4sBBqIIiB", last_segment_header)
1515 # Figuring out the length of the last set of segments is a little bit
1516 # involved...
1517 lacing_size = 0
1518 lacings = []
1519 last_segment_header = re.search( # Include the segment table
1520 b'(OggS\x00\x04.{%s})' % (21+segments), data, re.DOTALL).group()
1521 lacing_bytes = last_segment_header[27:][:segments]
1522 for c in map(ord, lacing_bytes):
1523 lacing_size += c
1524 if c < 255:
1525 lacings.append(lacing_size)
1526 lacing_size = 0
1527 segment_size = 27 # Initial header size
1528 segment_size += sum(ord(e) for e in last_segment_header[27:])
1529 segment_size += len(lacings)
1530 # Update the capture regex with laser precision:
1531 self.re_capture = re.compile(
1532 b'(OggS\x00\x02\x00.+OggS\x00\x04..{%s})'
1533 % (segment_size), re.DOTALL)
1534 return True
1536 def display_metadata(self, term_instance):
1537 """
1538 Sends a message to the user that displays the OGG file metadata. Things
1539 like ID3 tags, bitrate, channels, etc.
1540 """
1541 if not self.sent_message:
1542 global _logged_mutagen_warning
1543 try:
1544 import mutagen.oggvorbis
1545 except ImportError:
1546 if not _logged_mutagen_warning:
1547 _logged_mutagen_warning = True
1548 logging.warning(_(
1549 "Could not import the mutagen Python module. "
1550 "Displaying audio file metadata will be disabled."))
1551 logging.info(_(
1552 "TIP: Install mutagen: sudo pip install mutagen"))
1553 return
1554 oggfile = mutagen.oggvorbis.Open(self.file_obj.name)
1555 message = "<pre>%s</pre>" % oggfile.pprint()
1556 term_instance.send_message(message)
1557 self.sent_message = True
1559class PDFFile(FileType):
1560 """
1561 A subclass of :class:`FileType` for PDFs (specifically to override
1562 :meth:`self.html`). Has hard-coded name, mimetype, suffix, and regular
1563 expressions. This class will also utilize :attr:`self.icondir` to look for
1564 an icon named, 'pdf.svg'. If found it will be utilized by
1565 :meth:`self.html` when generating output.
1566 """
1567 name = _("PDF Document")
1568 mimetype = "application/pdf"
1569 suffix = ".pdf"
1570 re_header = re.compile(br'.*%PDF-[0-9]\.[0-9]{1,2}.+?obj', re.DOTALL)
1571 re_capture = re.compile(br'(%PDF-[0-9]\.[0-9]{1,2}.+%%EOF)', re.DOTALL)
1572 icon = "pdf.svg" # Name of the file inside of self.icondir
1573 # NOTE: Using two separate links below so the whitespace doesn't end up
1574 # underlined. Looks much nicer this way.
1575 html_icon_template = (
1576 '<span class="pdfcontainer"><a class="pdflink" target="_blank" '
1577 'href="{link}">{icon}</a><br>'
1578 ' <a class="pdflink" href="{link}">{name}</a></span>')
1579 html_template = (
1580 '<span class="pdfcontainer"><a target="_blank" href="{link}">{name}</a>'
1581 '</span>')
1582 is_container = True
1584 def __init__(self, path="", linkpath="", icondir=None):
1585 """
1586 **path:** (optional) The path to the file.
1587 **linkpath:** (optional) The path to use when generating a link in HTML output.
1588 **icondir:** (optional) A path to look for a relevant icon to display when generating HTML output.
1589 """
1590 self.path = path
1591 self.linkpath = linkpath
1592 self.icondir = icondir
1593 self.file_obj = None
1594 self.thumbnail = None
1596 def generate_thumbnail(self):
1597 """
1598 If available, will use ghostscript (gs) to generate a thumbnail of this
1599 PDF in the form of an <img> tag with the src set to a data::URI.
1600 """
1601 from commands import getstatusoutput
1602 thumb = tempfile.NamedTemporaryFile()
1603 params = [
1604 'gs', # gs must be in your path
1605 '-dPDFFitPage',
1606 '-dPARANOIDSAFER',
1607 '-dBATCH',
1608 '-dNOPAUSE',
1609 '-dNOPROMPT',
1610 '-dMaxBitmap=500000000',
1611 '-dAlignToPixels=0',
1612 '-dGridFitTT=0',
1613 '-dDEVICEWIDTH=90',
1614 '-dDEVICEHEIGHT=120',
1615 '-dORIENT1=true',
1616 '-sDEVICE=jpeg',
1617 '-dTextAlphaBits=4',
1618 '-dGraphicsAlphaBits=4',
1619 '-sOutputFile=%s' % thumb.name,
1620 self.path
1621 ]
1622 retcode, output = getstatusoutput(" ".join(params))
1623 if retcode == 0:
1624 # Success
1625 data = None
1626 with open(thumb.name) as f:
1627 data = f.read()
1628 thumb.close() # Make sure it gets removed now we've read it
1629 if data:
1630 encoded = base64.b64encode(data)
1631 data_uri = "data:image/jpeg;base64,%s" % encoded.decode('utf-8')
1632 return '<img src="%s">' % data_uri
1634 def capture(self, data, term_instance):
1635 """
1636 Stores *data* as a temporary file and returns that file's object.
1637 *term_instance* can be used by overrides of this function to make
1638 adjustments to the terminal emulator after the *data* is captured e.g.
1639 to make room for an image.
1640 """
1641 logging.debug("PDFFile.capture()")
1642 # Remove the extra \r's that the terminal adds:
1643 data = data.replace(b'\r\n', b'\n')
1644 # Write the data to disk in a temporary location
1645 if self.path:
1646 if os.path.exists(self.path):
1647 if os.path.isdir(self.path):
1648 # Assume that a path was given for a reason and use a
1649 # NamedTemporaryFile instead of TemporaryFile.
1650 self.file_obj = tempfile.NamedTemporaryFile(
1651 suffix=self.suffix, dir=self.path)
1652 # Update self.path to use the new, actual file path
1653 self.path = self.file_obj.name
1654 else:
1655 self.file_obj = open(self.path, 'rb+')
1656 else:
1657 # Use the terminal emulator's temppath
1658 self.file_obj = tempfile.NamedTemporaryFile(
1659 suffix=self.suffix, dir=term_instance.temppath)
1660 self.path = self.file_obj.name
1661 self.file_obj.write(data)
1662 self.file_obj.flush()
1663 # Ghostscript-based thumbnail generation disabled due to its slow,
1664 # blocking nature. Works great though!
1665 #self.thumbnail = self.generate_thumbnail()
1666 # TODO: Figure out a way to do non-blocking thumbnail generation
1667 if self.icondir:
1668 pdf_icon = os.path.join(self.icondir, self.icon)
1669 if os.path.exists(pdf_icon):
1670 with open(pdf_icon) as f:
1671 self.thumbnail = f.read()
1672 if self.thumbnail:
1673 # Make room for our link
1674 img_Y = term_instance.cursorY
1675 img_X = term_instance.cursorX
1676 ref = term_instance.screen[img_Y][img_X]
1677 term_instance.screen[img_Y][img_X] = u' ' # No longer at this loc
1678 if term_instance.cursorY < 8: # Icons are about ~8 newlines high
1679 for line in xrange(8 - term_instance.cursorY):
1680 term_instance.newline()
1681 # Save the new location
1682 term_instance.screen[
1683 term_instance.cursorY][term_instance.cursorX] = ref
1684 term_instance.newline()
1685 else:
1686 # Make room for the characters in the name, "PDF Document"
1687 for i in xrange(len(self.name)):
1688 term_instance.screen[term_instance.cursorY].pop()
1689 # Leave it open
1690 return self.file_obj
1692 def html(self):
1693 """
1694 Returns a link to download the PDF using :attr:`self.linkpath` for the
1695 href attribute. Will use :attr:`self.html_icon_template` if
1696 :attr:`self.icon` can be found. Otherwise it will just output
1697 :attr:`self.name` as a clickable link.
1698 """
1699 link = "%s/%s" % (self.linkpath, os.path.split(self.path)[1])
1700 if self.thumbnail:
1701 return self.html_icon_template.format(
1702 link=link, icon=self.thumbnail, name=self.name)
1703 return self.html_template.format(
1704 link=link, icon=self.thumbnail, name=self.name)
1706class NotFoundError(Exception):
1707 """
1708 Raised by :meth:`Terminal.remove_magic` if a given filetype was not found in
1709 :attr:`Terminal.supported_magic`.
1710 """
1711 pass
1713class Terminal(object):
1714 """
1715 Terminal controller class.
1716 """
1717 ASCII_NUL = 0 # Null
1718 ASCII_BEL = 7 # Bell (BEL)
1719 ASCII_BS = 8 # Backspace
1720 ASCII_HT = 9 # Horizontal Tab
1721 ASCII_LF = 10 # Line Feed
1722 ASCII_VT = 11 # Vertical Tab
1723 ASCII_FF = 12 # Form Feed
1724 ASCII_CR = 13 # Carriage Return
1725 ASCII_SO = 14 # Ctrl-N; Shift out (switches to the G0 charset)
1726 ASCII_SI = 15 # Ctrl-O; Shift in (switches to the G1 charset)
1727 ASCII_XON = 17 # Resume Transmission
1728 ASCII_XOFF = 19 # Stop Transmission or Ignore Characters
1729 ASCII_CAN = 24 # Cancel Escape Sequence
1730 ASCII_SUB = 26 # Substitute: Cancel Escape Sequence and replace with ?
1731 ASCII_ESC = 27 # Escape
1732 ASCII_CSI = 155 # Control Sequence Introducer (that nothing uses)
1733 ASCII_HTS = 210 # Horizontal Tab Stop (HTS)
1735 class_prefix = u'✈' # Prefix used with HTML output span class names
1736 # (to avoid namespace conflicts)
1738 charsets = {
1739 'B': {}, # Default is USA (aka 'B')
1740 '0': { # Line drawing mode
1741 95: u' ',
1742 96: u'◆',
1743 97: u'▒',
1744 98: u'\t',
1745 99: u'\x0c',
1746 100: u'\r',
1747 101: u'\n',
1748 102: u'°',
1749 103: u'±',
1750 104: u'\n',
1751 105: u'\x0b',
1752 106: u'┘',
1753 107: u'┐',
1754 108: u'┌',
1755 109: u'└',
1756 110: u'┼',
1757 111: u'⎺', # All these bars and not a drink!
1758 112: u'⎻',
1759 113: u'─',
1760 114: u'⎼',
1761 115: u'⎽',
1762 116: u'├',
1763 117: u'┤',
1764 118: u'┴',
1765 119: u'┬',
1766 120: u'│',
1767 121: u'≤',
1768 122: u'≥',
1769 123: u'π',
1770 124: u'≠',
1771 125: u'£',
1772 126: u'·' # Centered dot--who comes up with this stuff?!?
1773 }
1774 }
1776 RE_CSI_ESC_SEQ = re.compile(r'\x1B\[([?A-Za-z0-9>;@:\!]*?)([A-Za-z@_])')
1777 RE_ESC_SEQ = re.compile(
1778 r'\x1b(.*\x1b\\|[ABCDEFGHIJKLMNOQRSTUVWXYZa-z0-9=<>]|[()# %*+].)')
1779 RE_TITLE_SEQ = re.compile(r'\x1b\][0-2]\;(.*?)(\x07|\x1b\\)')
1780 # The below regex is used to match our optional (non-standard) handler
1781 RE_OPT_SEQ = re.compile(r'\x1b\]_\;(.+?)(\x07|\x1b\\)')
1782 RE_NUMBERS = re.compile(r'\d*') # Matches any number
1783 RE_SIGINT = re.compile(rb'.*\^C', re.MULTILINE|re.DOTALL)
1785 def __init__(self, rows=24, cols=80, em_dimensions=None, temppath='/tmp',
1786 linkpath='/tmp', icondir=None, encoding='utf-8', async_mode=None, debug=False,
1787 enabled_filetypes="all"):
1788 """
1789 Initializes the terminal by calling *self.initialize(rows, cols)*. This
1790 is so we can have an equivalent function in situations where __init__()
1791 gets overridden.
1793 If *em_dimensions* are provided they will be used to determine how many
1794 lines images will take when they're drawn in the terminal. This is to
1795 prevent images that are written to the top of the screen from having
1796 their tops cut off. *em_dimensions* must be a dict in the form of::
1798 {'height': <px>, 'width': <px>}
1800 The *temppath* will be used to store files that are captured/saved by
1801 the terminal emulator. In conjunction with this is the *linkpath* which
1802 will be used when creating links to these temporary files. For example,
1803 a web-based application may wish to have the terminal emulator store
1804 temporary files in /tmp but give clients a completely unrelated URL to
1805 retrieve these files (for security or convenience reasons). Here's a
1806 real world example of how it works::
1808 >>> term = Terminal(
1809 ... rows=10, cols=40, temppath='/var/tmp', linkpath='/terminal')
1810 >>> term.write('About to write a PDF\\n')
1811 >>> pdf = open('/path/to/somefile.pdf').read()
1812 >>> term.write(pdf)
1813 >>> term.dump_html()
1814 ([u'About to write a PDF ',
1815 # <unnecessary lines of whitespace have been removed for this example>
1816 u'<a target="_blank" href="/terminal/tmpZoOKVM.pdf">PDF Document</a>'])
1818 The PDF file in question will reside in `/var/tmp` but the link was
1819 created as `href="/terminal/tmpZoOKVM.pdf"`. As long as your web app
1820 knows to look in /var/tmp for incoming '/terminal' requests users should
1821 be able to retrieve their documents.
1823 http://yourapp.company.com/terminal/tmpZoOKVM.pdf
1825 The *icondir* parameter, if given, will be used to provide a relevant
1826 icon when outputing a link to a file. When a supported
1827 :class:`FileType` is captured the instance will be given the *icondir*
1828 as a parameter in a manner similar to this::
1830 filetype_instance = filetype_class(icondir=self.icondir)
1832 That way when filetype_instance.html() is called it can display a nice
1833 icon to the user... if that particular :class:`FileType` supports icons
1834 and the icon it is looking for happens to be available at *icondir*.
1836 If *debug* is True, the root logger will have its level set to DEBUG.
1838 If *enabled_filetypes* are given (iterable of strings or `FileType`
1839 classes) the provided file types will be enabled for this terminal.
1840 If not given it will default to enabling 'all' file types. To disable
1841 support for all file types simply pass ``None``, ``False``, or an empty
1842 list.
1843 """
1844 if rows < 2 or cols < 2:
1845 raise InvalidParameters(_(
1846 "Invalid value(s) given for rows ({rows}) and/or cols "
1847 "({cols}). Both must be > 1.").format(rows=rows, cols=cols))
1848 if em_dimensions:
1849 if not isinstance(em_dimensions, dict):
1850 raise InvalidParameters(_(
1851 "The em_dimensions keyword argument must be a dict. "
1852 "Here's what was given instead: {0}").format(
1853 repr(em_dimensions)))
1854 if 'width' not in em_dimensions or 'height' not in em_dimensions:
1855 raise InvalidParameters(_(
1856 "The given em_dimensions dict ({0}) is missing either "
1857 "'height' or 'width'").format(repr(em_dimensions)))
1858 if not os.path.exists(temppath):
1859 raise InvalidParameters(_(
1860 "The given temppath ({0}) does not exist.").format(temppath))
1861 if icondir:
1862 if not os.path.exists(icondir):
1863 logging.warning(_(
1864 "The given icondir ({0}) does not exist.").format(icondir))
1865 if debug:
1866 logger = logging.getLogger()
1867 logger.level = logging.DEBUG
1868 self.temppath = temppath
1869 self.linkpath = linkpath
1870 self.icondir = icondir
1871 self.encoding = encoding
1872 self.async_mode = async_mode
1873 if enabled_filetypes == "all":
1874 enabled_filetypes = [
1875 PDFFile,
1876 PNGFile,
1877 JPEGFile,
1878 WAVFile,
1879 OGGFile,
1880 ]
1881 elif enabled_filetypes:
1882 for i, filetype in enumerate(list(enabled_filetypes)):
1883 if isinstance(filetype, basestring):
1884 # Attempt to convert into a proper class with Python voodoo
1885 _class = globals().get(filetype)
1886 if _class:
1887 enabled_filetypes[i] = _class # Update in-place
1888 else:
1889 enabled_filetypes = []
1890 self.enabled_filetypes = enabled_filetypes
1891 # This controls how often we send a message to the client when capturing
1892 # a special file type. The default is to update the user of progress
1893 # once every 1.5 seconds.
1894 self.message_interval = timedelta(seconds=1.5)
1895 self.notified = False # Used to tell if we have notified the user before
1896 self.cancel_capture = False
1897 # Used by cursor_left() and cursor_right() to handle double-width chars:
1898 self.double_width_right = False
1899 self.double_width_left = False
1900 self.prev_char = u''
1901 self.max_scrollback = 1000 # Max number of lines kept in the buffer
1902 self.initialize(rows, cols, em_dimensions)
1904 def initialize(self, rows=24, cols=80, em_dimensions=None):
1905 """
1906 Initializes the terminal (the actual equivalent to :meth:`__init__`).
1907 """
1908 self.cols = cols
1909 self.rows = rows
1910 self.em_dimensions = em_dimensions
1911 self.scrollback_buf = []
1912 self.scrollback_renditions = []
1913 self.title = "Gate One"
1914 # This variable can be referenced by programs implementing Terminal() to
1915 # determine if anything has changed since the last dump*()
1916 self.modified = False
1917 self.local_echo = True
1918 self.insert_mode = False
1919 self.esc_buffer = '' # For holding escape sequences as they're typed.
1920 self.cursor_home = 0
1921 self.cur_rendition = unichr(1000) # Should always be reset ([0])
1922 self.init_screen()
1923 self.init_renditions()
1924 self.current_charset = 0
1925 self.set_G0_charset('B')
1926 self.set_G1_charset('B')
1927 self.use_g0_charset()
1928 # Set the default window margins
1929 self.top_margin = 0
1930 self.bottom_margin = self.rows - 1
1931 self.timeout_capture = None
1932 self.specials = {
1933 self.ASCII_NUL: self.__ignore,
1934 self.ASCII_BEL: self.bell,
1935 self.ASCII_BS: self.backspace,
1936 self.ASCII_HT: self.horizontal_tab,
1937 self.ASCII_LF: self.newline,
1938 self.ASCII_VT: self.newline,
1939 self.ASCII_FF: self.newline,
1940 self.ASCII_CR: self.carriage_return,
1941 self.ASCII_SO: self.use_g1_charset,
1942 self.ASCII_SI: self.use_g0_charset,
1943 self.ASCII_XON: self._xon,
1944 self.ASCII_CAN: self._cancel_esc_sequence,
1945 self.ASCII_XOFF: self._xoff,
1946 #self.ASCII_ESC: self._sub_esc_sequence,
1947 self.ASCII_ESC: self._escape,
1948 self.ASCII_CSI: self._csi,
1949 }
1950 self.esc_handlers = {
1951 # TODO: Make a different set of these for each respective emulation mode (VT-52, VT-100, VT-200, etc etc)
1952 '#': self._set_line_params, # Varies
1953 '\\': self._string_terminator, # ST
1954 'c': self.clear_screen, # Reset terminal
1955 'D': self.__ignore, # Move/scroll window up one line IND
1956 'M': self.reverse_linefeed, # Move/scroll window down one line RI
1957 'E': self.next_line, # Move to next line NEL
1958 'F': self.__ignore, # Enter Graphics Mode
1959 'G': self.next_line, # Exit Graphics Mode
1960 '6': self._dsr_get_cursor_position, # Get cursor position DSR
1961 '7': self.save_cursor_position, # Save cursor position and attributes DECSC
1962 '8': self.restore_cursor_position, # Restore cursor position and attributes DECSC
1963 'H': self._set_tabstop, # Set a tab at the current column HTS
1964 'I': self.reverse_linefeed,
1965 '(': self.set_G0_charset, # Designate G0 Character Set
1966 ')': self.set_G1_charset, # Designate G1 Character Set
1967 'N': self.__ignore, # Set single shift 2 SS2
1968 'O': self.__ignore, # Set single shift 3 SS3
1969 '5': self._device_status_report, # Request: Device status report DSR
1970 '0': self.__ignore, # Response: terminal is OK DSR
1971 'P': self._dcs_handler, # Device Control String DCS
1972 # NOTE: = and > are ignored because the user can override/control
1973 # them via the numlock key on their keyboard. To do otherwise would
1974 # just confuse people.
1975 '=': self.__ignore, # Application Keypad DECPAM
1976 '>': self.__ignore, # Exit alternate keypad mode
1977 '<': self.__ignore, # Exit VT-52 mode
1978 'Z': self._csi_device_identification,
1979 }
1980 self.csi_handlers = {
1981 'A': self.cursor_up,
1982 'B': self.cursor_down,
1983 'C': self.cursor_right,
1984 'D': self.cursor_left,
1985 'E': self.cursor_next_line, # NOTE: Not the same as next_line()
1986 'F': self.cursor_previous_line,
1987 'G': self.cursor_horizontal_absolute,
1988 'H': self.cursor_position,
1989 'L': self.insert_line,
1990 'M': self.delete_line,
1991 #'b': self.repeat_last_char, # TODO
1992 'c': self._csi_device_identification, # Device status report (DSR)
1993 'g': self.__ignore, # TODO: Tab clear
1994 'h': self.set_expanded_mode,
1995 'i': self.__ignore, # ESC[5i is "redirect to printer", ESC[4i ends it
1996 'l': self.reset_expanded_mode,
1997 'f': self.cursor_position,
1998 'd': self.cursor_position_vertical, # Vertical Line Position Absolute (VPA)
1999 #'e': self.cursor_position_vertical_relative, # VPR TODO
2000 'J': self.clear_screen_from_cursor,
2001 'K': self.clear_line_from_cursor,
2002 'S': self.scroll_up,
2003 'T': self.scroll_down,
2004 's': self.save_cursor_position,
2005 'u': self.restore_cursor_position,
2006 'm': self._set_rendition,
2007 'n': self._csi_device_status_report, # <ESC>[6n is the only one I know of (request cursor position)
2008 'p': self.reset, # TODO: "!p" is "Soft terminal reset". Also, "Set conformance level" (VT100, VT200, or VT300)
2009 'r': self._set_top_bottom, # DECSTBM (used by many apps)
2010 'q': self.set_led_state, # Seems a bit silly but you never know
2011 'P': self.delete_characters, # DCH Deletes the specified number of chars
2012 'X': self._erase_characters, # ECH Same as DCH but also deletes renditions
2013 'Z': self.insert_characters, # Inserts the specified number of chars
2014 '@': self.insert_characters, # Inserts the specified number of chars
2015 #'`': self._char_position_row, # Position cursor (row only)
2016 #'t': self.window_manipulation, # TODO
2017 #'z': self.locator, # TODO: DECELR "Enable locator reporting"
2018 }
2019 # Used to store what expanded modes are active
2020 self.expanded_modes = {
2021 # Important defaults
2022 '1': False, # Application Cursor Keys
2023 '7': False, # Autowrap
2024 '25': True, # Show Cursor
2025 }
2026 self.expanded_mode_handlers = {
2027 # Expanded modes take a True/False argument for set/reset
2028 '1': partial(self.expanded_mode_toggle, '1'),
2029 '2': self.__ignore, # DECANM and set VT100 mode (and lock keyboard)
2030 '3': self.__ignore, # 132 Column Mode (DECCOLM)
2031 '4': self.__ignore, # Smooth (Slow) Scroll (DECSCLM)
2032 '5': self.__ignore, # Reverse video (might support in future)
2033 '6': self.__ignore, # Origin Mode (DECOM)
2034 # Wraparound Mode (DECAWM):
2035 '7': partial(self.expanded_mode_toggle, '7'),
2036 '8': self.__ignore, # Auto-repeat Keys (DECARM)
2037 # Send Mouse X & Y on button press:
2038 '9': partial(self.expanded_mode_toggle, '9'),
2039 '12': self.__ignore, # SRM or Start Blinking Cursor (att610)
2040 '18': self.__ignore, # Print form feed (DECPFF)
2041 '19': self.__ignore, # Set print extent to full screen (DECPEX)
2042 '25': partial(self.expanded_mode_toggle, '25'),
2043 '38': self.__ignore, # Enter Tektronix Mode (DECTEK)
2044 '41': self.__ignore, # more(1) fix (whatever that is)
2045 '42': self.__ignore, # Enable Nation Replacement Character sets (DECNRCM)
2046 '44': self.__ignore, # Turn On Margin Bell
2047 '45': self.__ignore, # Reverse-wraparound Mode
2048 '46': self.__ignore, # Start Logging
2049 '47': self.toggle_alternate_screen_buffer, # Use Alternate Screen Buffer
2050 '66': self.__ignore, # Application keypad (DECNKM)
2051 '67': self.__ignore, # Backarrow key sends delete (DECBKM)
2052 # Send Mouse X/Y on button press and release:
2053 '1000': partial(self.expanded_mode_toggle, '1000'),
2054 # Use Hilite Mouse Tracking:
2055 '1001': partial(self.expanded_mode_toggle, '1001'),
2056 # Use Cell Motion Mouse Tracking:
2057 '1002': partial(self.expanded_mode_toggle, '1002'),
2058 # Use All Motion Mouse Tracking:
2059 '1003': partial(self.expanded_mode_toggle, '1003'),
2060 # Send FocusIn/FocusOut events:
2061 '1004': partial(self.expanded_mode_toggle, '1004'),
2062 # Enable UTF-8 Mouse Mode:
2063 '1005': partial(self.expanded_mode_toggle, '1005'),
2064 # Enable SGR Mouse Mode:
2065 '1006': partial(self.expanded_mode_toggle, '1006'),
2066 '1010': self.__ignore, # Scroll to bottom on tty output
2067 '1011': self.__ignore, # Scroll to bottom on key press
2068 '1035': self.__ignore, # Enable special modifiers for Alt and NumLock keys
2069 '1036': self.__ignore, # Send ESC when Meta modifies a key
2070 '1037': self.__ignore, # Send DEL from the editing-keypad Delete key
2071 '1047': self.__ignore, # Use Alternate Screen Buffer
2072 '1048': self.__ignore, # Save cursor as in DECSC
2073 # Save cursor as in DECSC and use Alternate Screen Buffer,
2074 # clearing it first:
2075 '1049': self.toggle_alternate_screen_buffer_cursor,
2076 '1051': self.__ignore, # Set Sun function-key mode
2077 '1052': self.__ignore, # Set HP function-key mode
2078 '1060': self.__ignore, # Set legacy keyboard emulation (X11R6)
2079 '1061': self.__ignore, # Set Sun/PC keyboard emulation of VT220 keyboard
2080 }
2081 self.callbacks = {
2082 CALLBACK_SCROLL_UP: {},
2083 CALLBACK_CHANGED: {},
2084 CALLBACK_CURSOR_POS: {},
2085 CALLBACK_DSR: {},
2086 CALLBACK_TITLE: {},
2087 CALLBACK_BELL: {},
2088 CALLBACK_OPT: {},
2089 CALLBACK_MODE: {},
2090 CALLBACK_RESET: {},
2091 CALLBACK_LEDS: {},
2092 CALLBACK_MESSAGE: {},
2093 }
2094 self.leds = {
2095 1: False,
2096 2: False,
2097 3: False,
2098 4: False
2099 }
2100 # supported_magic gets assigned via self.add_magic() below
2101 self.supported_magic = []
2102 # Dict for magic "numbers" so we can tell when a particular type of
2103 # file begins and ends (so we can capture it in binary form and
2104 # later dump it out via dump_html())
2105 # The format is 'beginning': 'whole'
2106 self.magic = OrderedDict()
2107 # magic_map is like magic except it is in the format of:
2108 # 'beginning': <filetype class>
2109 self.magic_map = {}
2110 # Supported magic (defaults)
2111 for filetype in self.enabled_filetypes:
2112 self.add_magic(filetype)
2113 # NOTE: The order matters! Some file formats are containers that can
2114 # hold other file formats. For example, PDFs can contain JPEGs. So if
2115 # we match JPEGs before PDFs we might make a match when we really wanted
2116 # to match the overall container (the PDF).
2117 self.matched_header = None
2118 # These are for saving self.screen and self.renditions so we can support
2119 # an "alternate buffer"
2120 self.alt_screen = None
2121 self.alt_renditions = None
2122 self.alt_cursorX = 0
2123 self.alt_cursorY = 0
2124 self.saved_cursorX = 0
2125 self.saved_cursorY = 0
2126 self.saved_rendition = [None]
2127 self.capture = b""
2128 self.captured_files = {}
2129 self.file_counter = pua_counter()
2130 # This is for creating a new point of reference every time there's a new
2131 # unique rendition at a given coordinate
2132 self.rend_counter = unicode_counter()
2133 # Used for mapping unicode chars to acutal renditions (to save memory):
2134 self.renditions_store = {
2135 u' ': [], # Nada, nothing, no rendition. Not the same as below
2136 next_compat(self.rend_counter): [0] # Default is actually reset
2137 }
2138 self.watcher = None # Placeholder for the file watcher thread (if used)
2140 def add_magic(self, filetype):
2141 """
2142 Adds the given *filetype* to :attr:`self.supported_magic` and generates
2143 the necessary bits in :attr:`self.magic` and :attr:`self.magic_map`.
2145 *filetype* is expected to be a subclass of :class:`FileType`.
2146 """
2147 #logging.debug("add_magic(%s)" % filetype)
2148 if filetype in self.supported_magic:
2149 return # Nothing to do; it's already there
2150 self.supported_magic.append(filetype)
2151 # Wand ready...
2152 for Type in self.supported_magic:
2153 self.magic.update({Type.re_header: Type.re_capture})
2154 # magic_map is just a convenient way of performing magic, er, I
2155 # mean referencing filetypes that match the supported magic numbers.
2156 for Type in self.supported_magic:
2157 self.magic_map.update({Type.re_header: Type})
2159 def remove_magic(self, filetype):
2160 """
2161 Removes the given *filetype* from :attr:`self.supported_magic`,
2162 :attr:`self.magic`, and :attr:`self.magic_map`.
2164 *filetype* may be the specific filetype class or a string that can be
2165 either a filetype.name or filetype.mimetype.
2166 """
2167 found = None
2168 if isinstance(filetype, basestring):
2169 for Type in self.supported_magic:
2170 if Type.name == filetype:
2171 found = Type
2172 break
2173 elif Type.mimetype == filetype:
2174 found = Type
2175 break
2176 else:
2177 for Type in self.supported_magic:
2178 if Type == filetype:
2179 found = Type
2180 break
2181 if not found:
2182 raise NotFoundError("%s not found in supported magic" % filetype)
2183 self.supported_magic.remove(Type)
2184 del self.magic[Type.re_header]
2185 del self.magic_map[Type.re_header]
2187 def update_magic(self, filetype, mimetype):
2188 """
2189 Replaces an existing FileType with the given *mimetype* in
2190 :attr:`self.supported_magic` with the given *filetype*. Example::
2192 >>> import terminal
2193 >>> term = terminal.Terminal()
2194 >>> class NewPDF = class(terminal.PDFile)
2195 >>> # Open PDFs immediately in a new window
2196 >>> NewPDF.html_template = "<script>window.open({link})</script>"
2197 >>> NewPDF.html_icon_template = NewPDF.html_template # Ignore icon
2198 >>> term.update_magic(NewPDF, mimetype="application/pdf")
2199 """
2200 # Find the matching magic filetype
2201 for i, Type in enumerate(self.supported_magic):
2202 if Type.mimetype == mimetype:
2203 break
2204 # Replace self.magic and self.magic_map
2205 del self.magic[Type.re_header]
2206 del self.magic_map[Type.re_header]
2207 self.magic.update({filetype.re_header: filetype.re_capture})
2208 self.magic_map.update({filetype.re_header: filetype})
2209 # Finally replace the existing filetype in supported_magic
2210 self.supported_magic[i] = filetype
2212 def init_screen(self):
2213 """
2214 Fills :attr:`screen` with empty lines of (unicode) spaces using
2215 :attr:`self.cols` and :attr:`self.rows` for the dimensions.
2217 .. 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.
2218 """
2219 logging.debug('init_screen()')
2220 self.screen = [array('u', u' ' * self.cols) for a in xrange(self.rows)]
2221 # Tabstops
2222 self.tabstops = set(range(7, self.cols, 8))
2223 # Base cursor position
2224 self.cursorX = 0
2225 self.cursorY = 0
2226 self.rendition_set = False
2228 def init_renditions(self, rendition=unichr(1000)): # Match unicode_counter
2229 """
2230 Replaces :attr:`self.renditions` with arrays of *rendition* (characters)
2231 using :attr:`self.cols` and :attr:`self.rows` for the dimenions.
2232 """
2233 logging.debug(
2234 "init_renditions(%s)" % rendition.encode('unicode_escape'))
2235 # The actual renditions at various coordinates:
2236 self.renditions = [
2237 array('u', rendition * self.cols) for a in xrange(self.rows)]
2239 def init_scrollback(self):
2240 """
2241 Empties the scrollback buffers (:attr:`self.scrollback_buf` and
2242 :attr:`self.scrollback_renditions`).
2243 """
2244 self.scrollback_buf = []
2245 self.scrollback_renditions = []
2247 def add_callback(self, event, callback, identifier=None):
2248 """
2249 Attaches the given *callback* to the given *event*. If given,
2250 *identifier* can be used to reference this callback leter (e.g. when you
2251 want to remove it). Otherwise an identifier will be generated
2252 automatically. If the given *identifier* is already attached to a
2253 callback at the given event that callback will be replaced with
2254 *callback*.
2256 :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.
2257 :callback: The function you're attaching to the *event*.
2258 :identifier: A string or number to be used as a reference point should you wish to remove or update this callback later.
2260 Returns the identifier of the callback. to Example::
2262 >>> term = Terminal()
2263 >>> def somefunc(): pass
2264 >>> id = "myref"
2265 >>> ref = term.add_callback(term.CALLBACK_BELL, somefunc, id)
2267 .. note:: This allows the controlling program to have multiple callbacks for the same event.
2268 """
2269 if not identifier:
2270 identifier = callback.__hash__()
2271 self.callbacks[event][identifier] = callback
2272 return identifier
2274 def remove_callback(self, event, identifier):
2275 """
2276 Removes the callback referenced by *identifier* that is attached to the
2277 given *event*. Example::
2279 >>> term.remove_callback(CALLBACK_BELL, "myref")
2280 """
2281 del self.callbacks[event][identifier]
2283 def remove_all_callbacks(self, identifier):
2284 """
2285 Removes all callbacks associated with *identifier*.
2286 """
2287 for event, identifiers in self.callbacks.items():
2288 try:
2289 del self.callbacks[event][identifier]
2290 except KeyError:
2291 pass # No match, no biggie
2293 def send_message(self, message):
2294 """
2295 A convenience function for calling all CALLBACK_MESSAGE callbacks.
2296 """
2297 logging.debug('send_message(%s)' % message)
2298 try:
2299 for callback in self.callbacks[CALLBACK_MESSAGE].values():
2300 callback(message)
2301 except TypeError:
2302 pass
2304 def send_update(self):
2305 """
2306 A convenience function for calling all CALLBACK_CHANGED callbacks.
2307 """
2308 #logging.debug('send_update()')
2309 try:
2310 for callback in self.callbacks[CALLBACK_CHANGED].values():
2311 callback()
2312 except TypeError:
2313 pass
2315 def send_cursor_update(self):
2316 """
2317 A convenience function for calling all CALLBACK_CURSOR_POS callbacks.
2318 """
2319 #logging.debug('send_cursor_update()')
2320 try:
2321 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
2322 callback()
2323 except TypeError:
2324 pass
2326 def reset(self, *args, **kwargs):
2327 """
2328 Resets the terminal back to an empty screen with all defaults. Calls
2329 :meth:`Terminal.callbacks[CALLBACK_RESET]` when finished.
2331 .. 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).
2332 """
2333 logging.debug('reset()')
2334 self.leds = {
2335 1: False,
2336 2: False,
2337 3: False,
2338 4: False
2339 }
2340 self.expanded_modes = {
2341 # Important defaults
2342 '1': False,
2343 '7': False,
2344 '25': True,
2345 }
2346 self.local_echo = True
2347 self.title = "Gate One"
2348 self.esc_buffer = ''
2349 self.insert_mode = False
2350 self.rendition_set = False
2351 self.current_charset = 0
2352 self.set_G0_charset('B')
2353 self.set_G1_charset('B')
2354 self.use_g0_charset()
2355 self.top_margin = 0
2356 self.bottom_margin = self.rows - 1
2357 self.alt_screen = None
2358 self.alt_renditions = None
2359 self.alt_cursorX = 0
2360 self.alt_cursorY = 0
2361 self.saved_cursorX = 0
2362 self.saved_cursorY = 0
2363 self.saved_rendition = [None]
2364 self.init_screen()
2365 self.init_renditions()
2366 self.init_scrollback()
2367 try:
2368 for callback in self.callbacks[CALLBACK_RESET].values():
2369 callback()
2370 except TypeError:
2371 pass
2373 def __ignore(self, *args, **kwargs):
2374 """
2375 Does nothing (on purpose!). Used as a placeholder for unimplemented
2376 functions.
2377 """
2378 pass
2380 def resize(self, rows, cols, em_dimensions=None):
2381 """
2382 Resizes the terminal window, adding or removing *rows* or *cols* as
2383 needed. If *em_dimensions* are provided they will be stored in
2384 *self.em_dimensions* (which is currently only used by image output).
2385 """
2386 logging.debug(
2387 "resize(%s, %s, em_dimensions: %s)" % (rows, cols, em_dimensions))
2388 if em_dimensions:
2389 self.em_dimensions = em_dimensions
2390 if rows == self.rows and cols == self.cols:
2391 return # Nothing to do--don't mess with the margins or the cursor
2392 if rows < self.rows: # Remove rows from the top
2393 for i in xrange(self.rows - rows):
2394 line = self.screen.pop(0)
2395 # Add it to the scrollback buffer so it isn't lost forever
2396 self.scrollback_buf.append(line)
2397 rend = self.renditions.pop(0)
2398 self.scrollback_renditions.append(rend)
2399 elif rows > self.rows: # Add rows at the bottom
2400 for i in xrange(rows - self.rows):
2401 line = array('u', u' ' * self.cols)
2402 renditions = array('u', unichr(1000) * self.cols)
2403 self.screen.append(line)
2404 self.renditions.append(renditions)
2405 self.rows = rows
2406 self.top_margin = 0
2407 self.bottom_margin = self.rows - 1
2408 # Fix the cursor location:
2409 if self.cursorY >= self.rows:
2410 self.cursorY = self.rows - 1
2411 if cols > self.cols: # Add cols to the right
2412 for i in xrange(self.rows):
2413 for j in xrange(cols - self.cols):
2414 self.screen[i].append(u' ')
2415 self.renditions[i].append(unichr(1000))
2416 self.cols = cols
2418 # Fix the cursor location:
2419 if self.cursorX >= self.cols:
2420 self.cursorX = self.cols - 1
2421 self.rendition_set = False
2423 def _set_top_bottom(self, settings):
2424 """
2425 DECSTBM - Sets :attr:`self.top_margin` and :attr:`self.bottom_margin`
2426 using the provided settings in the form of '<top_margin>;<bottom_margin>'.
2428 .. note:: This also handles restore/set "DEC Private Mode Values".
2429 """
2430 #logging.debug("_set_top_bottom(%s)" % settings)
2431 # NOTE: Used by screen and vi so this needs to work and work well!
2432 if len(settings):
2433 if settings.startswith('?'):
2434 # This is a set/restore DEC PMV sequence
2435 return # Ignore (until I figure out what this should do)
2436 top, bottom = settings.split(';')
2437 self.top_margin = max(0, int(top) - 1) # These are 0-based
2438 if bottom:
2439 self.bottom_margin = min(self.rows - 1, int(bottom) - 1)
2440 else:
2441 # Reset to defaults (full screen margins)
2442 self.top_margin, self.bottom_margin = 0, self.rows - 1
2444 def get_cursor_position(self):
2445 """
2446 Returns the current cursor positition as a tuple::
2448 (row, col)
2449 """
2450 return (self.cursorY, self.cursorX)
2452 def set_title(self, title):
2453 """
2454 Sets :attr:`self.title` to *title* and executes
2455 :meth:`Terminal.callbacks[CALLBACK_TITLE]`
2456 """
2457 self.title = title
2458 try:
2459 for callback in self.callbacks[CALLBACK_TITLE].values():
2460 callback()
2461 except TypeError as e:
2462 logging.error(_("Got TypeError on CALLBACK_TITLE..."))
2463 logging.error(repr(self.callbacks[CALLBACK_TITLE]))
2464 logging.error(e)
2466 def get_title(self):
2467 """Returns :attr:`self.title`"""
2468 return self.title
2470# TODO: put some logic in these save/restore functions to walk the current
2471# rendition line to come up with a logical rendition for that exact spot.
2472 def save_cursor_position(self, mode=None):
2473 """
2474 Saves the cursor position and current rendition settings to
2475 :attr:`self.saved_cursorX`, :attr:`self.saved_cursorY`, and
2476 :attr:`self.saved_rendition`
2478 .. note:: Also handles the set/restore "Private Mode Settings" sequence.
2479 """
2480 if mode: # Set DEC private mode
2481 # TODO: Need some logic here to save the current expanded mode
2482 # so we can restore it in _set_top_bottom().
2483 self.set_expanded_mode(mode)
2484 # NOTE: args and kwargs are here to make sure we don't get an exception
2485 # when we're called via escape sequences.
2486 self.saved_cursorX = self.cursorX
2487 self.saved_cursorY = self.cursorY
2488 self.saved_rendition = self.cur_rendition
2490 def restore_cursor_position(self, *args, **kwargs):
2491 """
2492 Restores the cursor position and rendition settings from
2493 :attr:`self.saved_cursorX`, :attr:`self.saved_cursorY`, and
2494 :attr:`self.saved_rendition` (if they're set).
2495 """
2496 if self.saved_cursorX and self.saved_cursorY:
2497 self.cursorX = self.saved_cursorX
2498 self.cursorY = self.saved_cursorY
2499 self.cur_rendition = self.saved_rendition
2501 def _dsr_get_cursor_position(self):
2502 """
2503 Returns the current cursor positition as a DSR response in the form of::
2505 '\x1b<self.cursorY>;<self.cursorX>R'
2507 Also executes CALLBACK_DSR with the same output as the first argument.
2508 Example::
2510 self.callbacks[CALLBACK_DSR]('\x1b20;123R')
2511 """
2512 esc_cursor_pos = '\x1b%s;%sR' % (self.cursorY, self.cursorX)
2513 try:
2514 for callback in self.callbacks[CALLBACK_DSR].values():
2515 callback(esc_cursor_pos)
2516 except TypeError:
2517 pass
2518 return esc_cursor_pos
2520 def _dcs_handler(self, string=None):
2521 """
2522 Handles Device Control String sequences. Unimplemented. Probablye not
2523 appropriate for Gate One. If you believe this to be false please open
2524 a ticket in the issue tracker.
2525 """
2526 pass
2527 #print("TODO: Handle this DCS: %s" % string)
2529 def _set_line_params(self, param):
2530 """
2531 This function handles the control sequences that set double and single
2532 line heights and widths. It also handles the "screen alignment test" (
2533 fill the screen with Es).
2535 .. note::
2537 Double-line height text is currently unimplemented (does anything
2538 actually use it?).
2539 """
2540 try:
2541 param = int(param)
2542 except ValueError:
2543 logging.warning("Couldn't handle escape sequence #%s" % repr(param))
2544 if param == 8:
2545 # Screen alignment test
2546 self.init_renditions()
2547 self.screen = [
2548 array('u', u'E' * self.cols) for a in xrange(self.rows)]
2549 # TODO: Get this handling double line height stuff... For kicks
2551 def set_G0_charset(self, char):
2552 """
2553 Sets the terminal's G0 (default) charset to the type specified by
2554 *char*.
2556 Here's the possibilities::
2558 0 DEC Special Character and Line Drawing Set
2559 A United Kingdom (UK)
2560 B United States (USASCII)
2561 4 Dutch
2562 C Finnish
2563 5 Finnish
2564 R French
2565 Q French Canadian
2566 K German
2567 Y Italian
2568 E Norwegian/Danish
2569 6 Norwegian/Danish
2570 Z Spanish
2571 H Swedish
2572 7 Swedish
2573 = Swiss
2574 """
2575 #logging.debug("Setting G0 charset to %s" % repr(char))
2576 try:
2577 self.G0_charset = self.charsets[char]
2578 except KeyError:
2579 self.G0_charset = self.charsets['B']
2580 if self.current_charset == 0:
2581 self.charset = self.G0_charset
2583 def set_G1_charset(self, char):
2584 """
2585 Sets the terminal's G1 (alt) charset to the type specified by *char*.
2587 Here's the possibilities::
2589 0 DEC Special Character and Line Drawing Set
2590 A United Kingdom (UK)
2591 B United States (USASCII)
2592 4 Dutch
2593 C Finnish
2594 5 Finnish
2595 R French
2596 Q French Canadian
2597 K German
2598 Y Italian
2599 E Norwegian/Danish
2600 6 Norwegian/Danish
2601 Z Spanish
2602 H Swedish
2603 7 Swedish
2604 = Swiss
2605 """
2606 #logging.debug("Setting G1 charset to %s" % repr(char))
2607 try:
2608 self.G1_charset = self.charsets[char]
2609 except KeyError:
2610 self.G1_charset = self.charsets['B']
2611 if self.current_charset == 1:
2612 self.charset = self.G1_charset
2614 def use_g0_charset(self):
2615 """
2616 Sets the current charset to G0. This should get called when ASCII_SO
2617 is encountered.
2618 """
2619 #logging.debug(
2620 #"Switching to G0 charset (which is %s)" % repr(self.G0_charset))
2621 self.current_charset = 0
2622 self.charset = self.G0_charset
2624 def use_g1_charset(self):
2625 """
2626 Sets the current charset to G1. This should get called when ASCII_SI
2627 is encountered.
2628 """
2629 #logging.debug(
2630 #"Switching to G1 charset (which is %s)" % repr(self.G1_charset))
2631 self.current_charset = 1
2632 self.charset = self.G1_charset
2634 def abort_capture(self):
2635 """
2636 A convenience function that takes care of canceling a file capture and
2637 cleaning up the output.
2638 """
2639 logging.debug('abort_capture()')
2640 self.cancel_capture = True
2641 self.write(b'\x00') # This won't actually get written
2642 self.send_update()
2643 self.send_message(_(u'File capture aborted.'))
2645 def write(self, chars, special_checks=True):
2646 """
2647 Write *chars* to the terminal at the current cursor position advancing
2648 the cursor as it does so. If *chars* is not unicode, it will be
2649 converted to unicode before being stored in self.screen.
2651 if *special_checks* is True (default), Gate One will perform checks for
2652 special things like image files coming in via *chars*.
2653 """
2654 # NOTE: This is the slowest function in all of Gate One. All
2655 # suggestions on how to speed it up are welcome!
2657 # Speedups (don't want dots in loops if they can be avoided)
2658 specials = self.specials
2659 esc_handlers = self.esc_handlers
2660 csi_handlers = self.csi_handlers
2661 RE_ESC_SEQ = self.RE_ESC_SEQ
2662 RE_CSI_ESC_SEQ = self.RE_CSI_ESC_SEQ
2663 magic = self.magic
2664 magic_map = self.magic_map
2665 changed = False
2666 # This is commented because of how noisy it is. Uncomment to debug the
2667 # terminal emualtor:
2668 #logging.debug('handling chars: %s' % repr(chars))
2669 # Only perform special checks (for FileTYpe stuff) if we're given bytes.
2670 # Incoming unicode chars should NOT be binary data.
2671 if not isinstance(chars, bytes):
2672 special_checks = False
2673 if special_checks:
2674 before_chars = b""
2675 after_chars = b""
2676 if not self.capture:
2677 for magic_header in magic:
2678 try:
2679 if magic_header.match(chars):
2680 self.matched_header = magic_header
2681 self.timeout_capture = datetime.now()
2682 self.progress_timer = datetime.now()
2683 # Create an instance of the filetype
2684 self._filetype_instance()
2685 break
2686 except UnicodeEncodeError:
2687 # Gibberish; drop it and pretend it never happened
2688 logging.debug(_(
2689 "Got UnicodeEncodeError trying to check FileTypes"))
2690 self.esc_buffer = ""
2691 # Make it so it won't barf below
2692 chars = chars.encode(self.encoding, 'ignore')
2693 if self.capture or self.matched_header:
2694 self.capture += chars
2695 if self.cancel_capture:
2696 # Try to split the garbage from the post-ctrl-c output
2697 split_capture = self.RE_SIGINT.split(self.capture)
2698 after_chars = split_capture[-1]
2699 self.capture = b''
2700 self.matched_header = None
2701 self.cancel_capture = False
2702 self.write(u'^C\r\n', special_checks=False)
2703 self.write(after_chars, special_checks=False)
2704 return
2705 ref = self.screen[self.cursorY][self.cursorX]
2706 ft_instance = self.captured_files[ref]
2707 if ft_instance.helper:
2708 ft_instance.helper(self)
2709 now = datetime.now()
2710 if now - self.progress_timer > self.message_interval:
2711 # Send an update of the progress to the user
2712 # NOTE: This message will only get sent if it takes longer
2713 # than self.message_interval to capture a file. So it is
2714 # nice and user friendly: Small things output instantly
2715 # without notifications while larger files that take longer
2716 # to capture will keep the user abreast of the progress.
2717 ft = magic_map[self.matched_header].name
2718 indicator = 'K'
2719 size = float(len(self.capture))/1024 # Kb
2720 if size > 1024: # Switch to Mb
2721 size = size/1024
2722 indicator = 'M'
2723 message = _(
2724 "%s: %.2f%s captured..." % (ft, size, indicator))
2725 self.notified = True
2726 self.send_message(message)
2727 self.progress_timer = datetime.now()
2728 match = ft_instance.re_capture.search(self.capture)
2729 if match:
2730 logging.debug(
2731 "Matched %s format (%s, %s). Capturing..." % (
2732 self.magic_map[self.matched_header].name,
2733 self.cursorY, self.cursorX))
2734 split_capture = ft_instance.re_capture.split(self.capture,1)
2735 before_chars = split_capture[0]
2736 self.capture = split_capture[1]
2737 after_chars = b"".join(split_capture[2:])
2738 if after_chars:
2739 is_container = magic_map[self.matched_header].is_container
2740 if is_container and len(after_chars) > 500:
2741 # Could be more to this file. Let's wait until output
2742 # slows down before attempting to perform a match
2743 logging.debug(
2744 "> 500 characters after capture. Waiting for more")
2745 return
2746 else:
2747 # These need to be written before the capture so that
2748 # the FileType.capture() method can position things
2749 # appropriately.
2750 if before_chars:
2751 # Empty out self.capture temporarily so these chars
2752 # get handled properly
2753 cap_temp = self.capture
2754 self.capture = b""
2755 # This will overwrite our ref:
2756 self.write(before_chars, special_checks=False)
2757 # Put it back for the rest of the processing
2758 self.capture = cap_temp
2759 # Perform the capture and start anew
2760 self._capture_file(ref)
2761 if self.notified:
2762 # Send a final notice of how big the file was (just
2763 # to keep things consistent).
2764 ft = magic_map[self.matched_header].name
2765 indicator = 'K'
2766 size = float(len(self.capture))/1024 # Kb
2767 if size > 1024: # Switch to Mb
2768 size = size/1024
2769 indicator = 'M'
2770 message = _(
2771 "%s: Capture complete (%.2f%s)" % (
2772 ft, size, indicator))
2773 self.notified = False
2774 self.send_message(message)
2775 self.capture = b"" # Empty it now that is is captured
2776 self.matched_header = None # Ditto
2777 self.write(after_chars, special_checks=True)
2778 return
2779 return
2780 # Have to convert to unicode
2781 try:
2782 chars = chars.decode(self.encoding, "handle_special")
2783 except UnicodeDecodeError:
2784 # Just in case
2785 try:
2786 chars = chars.decode(self.encoding, "ignore")
2787 except UnicodeDecodeError:
2788 logging.error(
2789 _("Double UnicodeDecodeError in terminal.Terminal."))
2790 return
2791 except AttributeError:
2792 # In Python 3 strings don't have .decode()
2793 pass # Already Unicode
2794 for char in chars:
2795 charnum = ord(char)
2796 if charnum in specials:
2797 specials[charnum]()
2798 else:
2799 # Now handle the regular characters and escape sequences
2800 if self.esc_buffer: # We've got an escape sequence going on...
2801 try:
2802 self.esc_buffer += char
2803 # First try to handle non-CSI ESC sequences (the basics)
2804 match_obj = RE_ESC_SEQ.match(self.esc_buffer)
2805 if match_obj:
2806 seq_type = match_obj.group(1) # '\x1bA' -> 'A'
2807 # Call the matching ESC handler
2808 #logging.debug('ESC seq: %s' % seq_type)
2809 if len(seq_type) == 1: # Single-character sequnces
2810 esc_handlers[seq_type]()
2811 else: # Multi-character stuff like '\x1b)B'
2812 esc_handlers[seq_type[0]](seq_type[1:])
2813 self.esc_buffer = '' # All done with this one
2814 continue
2815 # Next try to handle CSI ESC sequences
2816 match_obj = RE_CSI_ESC_SEQ.match(self.esc_buffer)
2817 if match_obj:
2818 csi_values = match_obj.group(1) # e.g. '0;1;37'
2819 csi_type = match_obj.group(2) # e.g. 'm'
2820 #logging.debug(
2821 #'CSI: %s, %s' % (csi_type, csi_values))
2822 # Call the matching CSI handler
2823 try:
2824 csi_handlers[csi_type](csi_values)
2825 except ValueError:
2826 # Commented this out because it can be super noisy
2827 #logging.error(_(
2828 #"CSI Handler Error: Type: %s, Values: %s" %
2829 #(csi_type, csi_values)
2830 #))
2831 pass
2832 self.esc_buffer = ''
2833 continue
2834 except KeyError:
2835 # No handler for this, try some alternatives
2836 if self.esc_buffer.endswith('\x1b\\'):
2837 self._osc_handler()
2838 else:
2839 logging.warning(_(
2840 "Warning: No ESC sequence handler for %s"
2841 % repr(self.esc_buffer)
2842 ))
2843 self.esc_buffer = ''
2844 continue # We're done here
2845 changed = True
2846 if self.cursorX >= self.cols:
2847 self.cursorX = 0
2848 self.newline()
2849 # Non-autowrap has been disabled due to issues with browser
2850 # wrapping.
2851 #if self.expanded_modes['7']:
2852 #self.cursorX = 0
2853 #self.newline()
2854 #else:
2855 #self.screen[self.cursorY].append(u' ') # Make room
2856 #self.renditions[self.cursorY].append(u' ')
2857 try:
2858 self.renditions[self.cursorY][
2859 self.cursorX] = self.cur_rendition
2860 if self.insert_mode:
2861 # Insert mode dictates that we move everything to the
2862 # right for every character we insert. Normally the
2863 # program itself will take care of this but older
2864 # programs and shells will simply set call ESC[4h,
2865 # insert the character, then call ESC[4i to return the
2866 # terminal to its regular state.
2867 self.insert_characters(1)
2868 if charnum in self.charset:
2869 char = self.charset[charnum]
2870 self.screen[self.cursorY][self.cursorX] = char
2871 else:
2872 # Double check this isn't a unicode diacritic (accent)
2873 # which simply modifies the character before it
2874 if unicodedata.combining(char):
2875 # This is a diacritic. Combine it with existing:
2876 current = self.screen[self.cursorY][self.cursorX]
2877 combined = unicodedata.normalize(
2878 'NFC', u'%s%s' % (current, char))
2879 # Sometimes a joined combining char can still result
2880 # a string of length > 1. So we need to handle that
2881 if len(combined) > 1:
2882 for i, c in enumerate(combined):
2883 self.screen[self.cursorY][
2884 self.cursorX] = c
2885 if i < len(combined) - 1:
2886 self.cursorX += 1
2887 else:
2888 self.screen[self.cursorY][
2889 self.cursorX] = combined
2890 else:
2891 # Normal character
2892 self.screen[self.cursorY][self.cursorX] = char
2893 except IndexError as e:
2894 # This can happen when escape sequences go haywire
2895 # Only log the error if debugging is enabled (because we
2896 # really don't care that much 99% of the time)
2897 logger = logging.getLogger()
2898 if logger.level < 20:
2899 logging.error(_(
2900 "IndexError in write(): %s" % e))
2901 import traceback, sys
2902 traceback.print_exc(file=sys.stdout)
2903 self.cursorX += 1
2904 #self.cursor_right()
2905 self.prev_char = char
2906 if changed:
2907 self.modified = True
2908 # Execute our callbacks
2909 self.send_update()
2910 self.send_cursor_update()
2912 def flush(self):
2913 """
2914 Only here to make Terminal compatible with programs that want to use
2915 file-like methods.
2916 """
2917 pass
2919 def scroll_up(self, n=1):
2920 """
2921 Scrolls up the terminal screen by *n* lines (default: 1). The callbacks
2922 CALLBACK_CHANGED and CALLBACK_SCROLL_UP are called after scrolling the
2923 screen.
2925 .. note::
2927 This will only scroll up the region within `self.top_margin` and
2928 `self.bottom_margin` (if set).
2929 """
2930 #logging.debug("scroll_up(%s)" % n)
2931 empty_line = array('u', u' ' * self.cols) # Line full of spaces
2932 empty_rend = array('u', unichr(1000) * self.cols)
2933 for x in xrange(int(n)):
2934 line = self.screen.pop(self.top_margin) # Remove the top line
2935 self.scrollback_buf.append(line) # Add it to the scrollback buffer
2936 if len(self.scrollback_buf) > self.max_scrollback:
2937 self.init_scrollback()
2938 # NOTE: This would only be the # of lines piled up before the
2939 # next dump_html() or dump().
2940 # Add it to the bottom of the window:
2941 self.screen.insert(self.bottom_margin, empty_line[:]) # A copy
2942 # Remove top line's rendition information
2943 rend = self.renditions.pop(self.top_margin)
2944 self.scrollback_renditions.append(rend)
2945 # Insert a new empty rendition as well:
2946 self.renditions.insert(self.bottom_margin, empty_rend[:])
2947 # Execute our callback indicating lines have been updated
2948 try:
2949 for callback in self.callbacks[CALLBACK_CHANGED].values():
2950 callback()
2951 except TypeError:
2952 pass
2953 # Execute our callback to scroll up the screen
2954 try:
2955 for callback in self.callbacks[CALLBACK_SCROLL_UP].values():
2956 callback()
2957 except TypeError:
2958 pass
2960 def scroll_down(self, n=1):
2961 """
2962 Scrolls down the terminal screen by *n* lines (default: 1). The
2963 callbacks CALLBACK_CHANGED and CALLBACK_SCROLL_DOWN are called after
2964 scrolling the screen.
2965 """
2966 #logging.debug("scroll_down(%s)" % n)
2967 for x in xrange(int(n)):
2968 self.screen.pop(self.bottom_margin) # Remove the bottom line
2969 empty_line = array('u', u' ' * self.cols) # Line full of spaces
2970 self.screen.insert(self.top_margin, empty_line) # Add it to the top
2971 # Remove bottom line's style information:
2972 self.renditions.pop(self.bottom_margin)
2973 # Insert a new empty one:
2974 empty_line = array('u', unichr(1000) * self.cols)
2975 self.renditions.insert(self.top_margin, empty_line)
2976 # Execute our callback indicating lines have been updated
2977 try:
2978 for callback in self.callbacks[CALLBACK_CHANGED].values():
2979 callback()
2980 except TypeError:
2981 pass
2983 # Execute our callback to scroll up the screen
2984 try:
2985 for callback in self.callbacks[CALLBACK_SCROLL_UP].values():
2986 callback()
2987 except TypeError:
2988 pass
2990 def insert_line(self, n=1):
2991 """
2992 Inserts *n* lines at the current cursor position.
2993 """
2994 #logging.debug("insert_line(%s)" % n)
2995 if not n: # Takes care of an empty string
2996 n = 1
2997 n = int(n)
2998 for i in xrange(n):
2999 self.screen.pop(self.bottom_margin) # Remove the bottom line
3000 # Remove bottom line's style information as well:
3001 self.renditions.pop(self.bottom_margin)
3002 empty_line = array('u', u' ' * self.cols) # Line full of spaces
3003 self.screen.insert(self.cursorY, empty_line) # Insert at cursor
3004 # Insert a new empty rendition as well:
3005 empty_rend = array('u', unichr(1000) * self.cols)
3006 self.renditions.insert(self.cursorY, empty_rend) # Insert at cursor
3008 def delete_line(self, n=1):
3009 """
3010 Deletes *n* lines at the current cursor position.
3011 """
3012 #logging.debug("delete_line(%s)" % n)
3013 if not n: # Takes care of an empty string
3014 n = 1
3015 n = int(n)
3016 for i in xrange(n):
3017 self.screen.pop(self.cursorY) # Remove the line at the cursor
3018 # Remove the line's style information as well:
3019 self.renditions.pop(self.cursorY)
3020 # Now add an empty line and empty set of renditions to the bottom of
3021 # the view
3022 empty_line = array('u', u' ' * self.cols) # Line full of spaces
3023 # Add it to the bottom of the view:
3024 self.screen.insert(self.bottom_margin, empty_line) # Insert at bottom
3025 # Insert a new empty rendition as well:
3026 empty_rend = array('u', unichr(1000) * self.cols)
3027 self.renditions.insert(self.bottom_margin, empty_rend)
3029 def backspace(self):
3030 """Execute a backspace (\\x08)"""
3031 self.cursor_left(1)
3033 def horizontal_tab(self):
3034 """Execute horizontal tab (\\x09)"""
3035 for stop in sorted(self.tabstops):
3036 if self.cursorX < stop:
3037 self.cursorX = stop + 1
3038 break
3039 else:
3040 self.cursorX = self.cols - 1
3042 def _set_tabstop(self):
3043 """Sets a tabstop at the current position of :attr:`self.cursorX`."""
3044 if self.cursorX not in self.tabstops:
3045 for tabstop in self.tabstops:
3046 if self.cursorX > tabstop:
3047 self.tabstops.add(self.cursorX)
3048 break
3050 def linefeed(self):
3051 """
3052 LF - Executes a line feed.
3054 .. note:: This actually just calls :meth:`Terminal.newline`.
3055 """
3056 self.newline()
3058 def next_line(self):
3059 """
3060 CNL - Moves the cursor down one line to the home position. Will not
3061 result in a scrolling event like newline() does.
3063 .. note:: This is not the same thing as :meth:`Terminal.cursor_next_line` which preserves the cursor's column position.
3064 """
3065 self.cursorX = self.cursor_home
3066 if self.cursorY < self.rows -1:
3067 self.cursorY += 1
3069 def reverse_linefeed(self):
3070 """
3071 RI - Executes a reverse line feed: Move the cursor up one line to the
3072 home position. If the cursor move would result in going past the top
3073 margin of the screen (upwards) this will execute a scroll_down() event.
3074 """
3075 self.cursorX = 0
3076 self.cursorY -= 1
3077 if self.cursorY < self.top_margin:
3078 self.scroll_down()
3079 self.cursorY = self.top_margin
3081 def newline(self):
3082 """
3083 Increases :attr:`self.cursorY` by 1 and calls :meth:`Terminal.scroll_up`
3084 if that action will move the curor past :attr:`self.bottom_margin`
3085 (usually the bottom of the screen).
3086 """
3087 cols = self.cols
3088 self.cursorY += 1
3089 if self.cursorY > self.bottom_margin:
3090 self.scroll_up()
3091 self.cursorY = self.bottom_margin
3092 self.clear_line()
3093 # Shorten the line if it is longer than the number of columns
3094 # NOTE: This lets us keep the width of existing lines even if the number
3095 # of columns is reduced while at the same time accounting for apps like
3096 # 'top' that merely overwrite existing lines. If we didn't do this
3097 # the output from 'top' would get all messed up from leftovers at the
3098 # tail end of every line when self.cols had a larger value.
3099 if len(self.screen[self.cursorY]) >= cols:
3100 self.screen[self.cursorY] = self.screen[self.cursorY][:cols]
3101 self.renditions[self.cursorY] = self.renditions[self.cursorY][:cols]
3102 # NOTE: The above logic is placed inside of this function instead of
3103 # inside self.write() in order to reduce CPU utilization. There's no
3104 # point in performing a conditional check for every incoming character
3105 # when the only time it will matter is when a newline is being written.
3107 def carriage_return(self):
3108 """
3109 Executes a carriage return (sets :attr:`self.cursorX` to 0). In other
3110 words it moves the cursor back to position 0 on the line.
3111 """
3112 if self.cursorX == 0:
3113 return # Nothing to do
3114 if divmod(self.cursorX, self.cols+1)[1] == 0:
3115 # A carriage return at the precise end of line means the program is
3116 # assuming vt100-style autowrap. Since we let the browser handle
3117 # that we need to discard this carriage return since we're not
3118 # actually making a newline.
3119 if self.prev_char not in [u'\x1b', u'\n']:
3120 # These are special cases where the underlying shell is assuming
3121 # autowrap so we have to emulate it.
3122 self.newline()
3123 else:
3124 return
3125 if not self.capture:
3126 self.cursorX = 0
3128 def _xon(self):
3129 """
3130 Handles the XON character (stop ignoring).
3132 .. note:: Doesn't actually do anything (this feature was probably meant for the underlying terminal program).
3133 """
3134 logging.debug('_xon()')
3135 self.local_echo = True
3137 def _xoff(self):
3138 """
3139 Handles the XOFF character (start ignoring)
3141 .. note:: Doesn't actually do anything (this feature was probably meant for the underlying terminal program).
3142 """
3143 logging.debug('_xoff()')
3144 self.local_echo = False
3146 def _cancel_esc_sequence(self):
3147 """
3148 Cancels any escape sequence currently being processed. In other words
3149 it empties :attr:`self.esc_buffer`.
3150 """
3151 self.esc_buffer = ''
3153 def _sub_esc_sequence(self):
3154 """
3155 Cancels any escape sequence currently in progress and replaces
3156 :attr:`self.esc_buffer` with single question mark (?).
3158 .. note:: Nothing presently uses this function and I can't remember what it was supposed to be part of (LOL!). Obviously it isn't very important.
3159 """
3160 self.esc_buffer = ''
3161 self.write('?')
3163 def _escape(self):
3164 """
3165 Handles the escape character as well as escape sequences that may end
3166 with an escape character.
3167 """
3168 buf = self.esc_buffer
3169 if buf.startswith('\x1bP') or buf.startswith('\x1b]'):
3170 # CSRs and OSCs are special
3171 self.esc_buffer += '\x1b'
3172 else:
3173 # Get rid of whatever's there since we obviously didn't know what to
3174 # do with it
3175 self.esc_buffer = '\x1b'
3177 def _csi(self):
3178 """
3179 Marks the start of a CSI escape sequence (which is itself a character)
3180 by setting :attr:`self.esc_buffer` to '\\\\x1b[' (which is the CSI
3181 escape sequence).
3182 """
3183 self.esc_buffer = '\x1b['
3185 def _filetype_instance(self):
3186 """
3187 Instantiates a new instance of the given :class:`FileType` (using
3188 `self.matched_header`) and stores the result in `self.captured_files`
3189 and creates a reference to that location at the current cursor location.
3190 """
3191 ref = next_compat(self.file_counter)
3192 logging.debug("_filetype_instance(%s)" % repr(ref))
3193 # Before doing anything else we need to mark the current cursor
3194 # location as belonging to our file
3195 self.screen[self.cursorY][self.cursorX] = ref
3196 # Create an instance of the filetype we can reference
3197 filetype_instance = self.magic_map[self.matched_header](
3198 path=self.temppath,
3199 linkpath=self.linkpath,
3200 icondir=self.icondir)
3201 self.captured_files[ref] = filetype_instance
3203 def _capture_file(self, ref):
3204 """
3205 This function gets called by :meth:`Terminal.write` when the incoming
3206 character stream matches a value in :attr:`self.magic`. It will call
3207 whatever function is associated with the matching regex in
3208 :attr:`self.magic_map`. It also stores the current file capture
3209 reference (*ref*) at the current cursor location.
3210 """
3211 logging.debug("_capture_file(%s)" % repr(ref))
3212 self.screen[self.cursorY][self.cursorX] = ref
3213 filetype_instance = self.captured_files[ref]
3214 filetype_instance.capture(self.capture, self)
3215 # Start up an open file watcher so leftover file objects get
3216 # closed when they're no longer being used
3217 if not self.watcher or not self.watcher.isAlive():
3218 import threading
3219 self.watcher = threading.Thread(
3220 name='watcher', target=self._captured_fd_watcher)
3221 self.watcher.setDaemon(True)
3222 self.watcher.start()
3223 return
3225 def _captured_fd_watcher(self):
3226 """
3227 Meant to be run inside of a thread, calls
3228 :meth:`Terminal.close_captured_fds` until there are no more open image
3229 file descriptors.
3230 """
3231 logging.debug("starting _captured_fd_watcher()")
3232 import time
3233 self.quitting = False
3234 while not self.quitting:
3235 if self.captured_files:
3236 self.close_captured_fds()
3237 time.sleep(5)
3238 else:
3239 self.quitting = True
3240 logging.debug('_captured_fd_watcher() quitting: No more images.')
3242 def close_captured_fds(self):
3243 """
3244 Closes the file descriptors of any captured files that are no longer on
3245 the screen.
3246 """
3247 #logging.debug('close_captured_fds()') # Commented because it's kinda noisy
3248 if self.captured_files:
3249 for ref in list(self.captured_files.keys()):
3250 found = False
3251 for line in self.screen:
3252 if ref in line:
3253 found = True
3254 break
3255 if self.alt_screen:
3256 for line in self.alt_screen:
3257 if ref in line:
3258 found = True
3259 break
3260 if not found:
3261 try:
3262 self.captured_files[ref].close()
3263 except AttributeError:
3264 pass # File already closed or never captured properly
3265 del self.captured_files[ref]
3267 def _string_terminator(self):
3268 """
3269 Handle the string terminator (ST).
3271 .. note:: Doesn't actually do anything at the moment. Probably not needed since :meth:`Terminal._escape` and/or :meth:`Terminal.bell` will end up handling any sort of sequence that would end in an ST anyway.
3272 """
3273 # NOTE: Might this just call _cancel_esc_sequence? I need to double-check.
3274 pass
3276 def _osc_handler(self):
3277 """
3278 Handles Operating System Command (OSC) escape sequences which need
3279 special care since they are of indeterminiate length and end with
3280 either a bell (\\\\x07) or a sequence terminator (\\\\x9c aka ST). This
3281 will usually be called from :meth:`Terminal.bell` to set the title of
3282 the terminal (just like an xterm) but it is also possible to be called
3283 directly whenever an ST is encountered.
3284 """
3285 # Try the title sequence first
3286 match_obj = self.RE_TITLE_SEQ.match(self.esc_buffer)
3287 if match_obj:
3288 self.esc_buffer = ''
3289 title = match_obj.group(1)
3290 self.set_title(title) # Sets self.title
3291 return
3292 # Next try our special optional handler sequence
3293 match_obj = self.RE_OPT_SEQ.match(self.esc_buffer)
3294 if match_obj:
3295 self.esc_buffer = ''
3296 text = match_obj.group(1)
3297 self._opt_handler(text)
3298 return
3299 # At this point we've encountered something unusual
3300 logging.warning(_("Warning: No special ESC sequence handler for %s" %
3301 repr(self.esc_buffer)))
3302 self.esc_buffer = ''
3304 def bell(self):
3305 """
3306 Handles the bell character and executes
3307 :meth:`Terminal.callbacks[CALLBACK_BELL]` (if we are not in the middle
3308 of an escape sequence that ends with a bell character =). If we *are*
3309 in the middle of an escape sequence, calls :meth:`self._osc_handler`
3310 since we can be nearly certain that we're simply terminating an OSC
3311 sequence. Isn't terminal emulation grand? ⨀_⨀
3312 """
3313 # NOTE: A little explanation is in order: The bell character (\x07) by
3314 # itself should play a bell (pretty straighforward). However, if
3315 # the bell character is at the tail end of a particular escape
3316 # sequence (string starting with \x1b]0;) this indicates an xterm
3317 # title (everything between \x1b]0;...\x07).
3318 if not self.esc_buffer: # We're not in the middle of an esc sequence
3319 logging.debug('Regular bell')
3320 try:
3321 for callback in self.callbacks[CALLBACK_BELL].values():
3322 callback()
3323 except TypeError:
3324 pass
3325 else: # We're (likely) setting a title
3326 self.esc_buffer += '\x07' # Add the bell char so we don't lose it
3327 self._osc_handler()
3329 def _device_status_report(self, n=None):
3330 """
3331 Returns '\\\\x1b[0n' (terminal OK) and executes:
3333 .. code-block:: python
3335 self.callbacks[CALLBACK_DSR]("\\x1b[0n")
3336 """
3337 logging.debug("_device_status_report()")
3338 response = u"\x1b[0n"
3339 try:
3340 for callback in self.callbacks[CALLBACK_DSR].values():
3341 callback(response)
3342 except TypeError:
3343 pass
3344 return response
3346 def _csi_device_identification(self, request=None):
3347 """
3348 If we're responding to ^[Z, ^[c, or ^[0c, returns '\\\\x1b[1;2c'
3349 (Meaning: I'm a vt220 terminal, version 1.0) and
3350 executes:
3352 .. code-block:: python
3354 self.callbacks[self.CALLBACK_DSR]("\\x1b[1;2c")
3356 If we're responding to ^[>c or ^[>0c, executes:
3358 .. code-block:: python
3360 self.callbacks[self.CALLBACK_DSR]("\\x1b[>0;271;0c")
3361 """
3362 logging.debug("_csi_device_identification(%s)" % request)
3363 if request and u">" in request:
3364 response = u"\x1b[>0;271;0c"
3365 else:
3366 response = u"\x1b[?1;2c"
3367 try:
3368 for callback in self.callbacks[CALLBACK_DSR].values():
3369 callback(response)
3370 except TypeError:
3371 pass
3372 return response
3374 def _csi_device_status_report(self, request=None):
3375 """
3376 Calls :meth:`self.callbacks[self.CALLBACK_DSR]` with an appropriate
3377 response to the given *request*.
3379 .. code-block:: python
3381 self.callbacks[self.CALLBACK_DSR](response)
3383 Supported requests and their responses:
3385 ============================= ==================
3386 Request Response
3387 ============================= ==================
3388 ^[5n (Status Report) ^[[0n
3389 ^[6n (Report Cursor Position) ^[[<row>;<column>R
3390 ^[15n (Printer Ready?) ^[[10n (Ready)
3391 ============================= ==================
3392 """
3393 logging.debug("_csi_device_status_report(%s)" % request)
3394 supported_requests = [
3395 u"5",
3396 u"6",
3397 u"15",
3398 ]
3399 if not request:
3400 return # Nothing to do
3401 response = u""
3402 if request.startswith('?'):
3403 # Get rid of it
3404 request = request[1:]
3405 if request in supported_requests:
3406 if request == u"5":
3407 response = u"\x1b[0n"
3408 elif request == u"6":
3409 rows = self.cursorY + 1
3410 cols = self.cursorX + 1
3411 response = u"\x1b[%s;%sR" % (rows, cols)
3412 elif request == u"15":
3413 response = u"\x1b[10n"
3414 try:
3415 for callback in self.callbacks[CALLBACK_DSR].values():
3416 callback(response)
3417 except TypeError:
3418 pass
3419 return response
3421 def set_expanded_mode(self, setting):
3422 """
3423 Accepts "standard mode" settings. Typically '\\\\x1b[?25h' to hide cursor.
3425 Notes on modes::
3427 '?1h' - Application Cursor Keys
3428 '?5h' - DECSCNM (default off): Set reverse-video mode
3429 '?7h' - DECAWM: Autowrap mode
3430 '?12h' - Local echo (SRM or Send Receive Mode)
3431 '?25h' - Hide cursor
3432 '?1000h' - Send Mouse X/Y on button press and release
3433 '?1001h' - Use Hilite Mouse Tracking
3434 '?1002h' - Use Cell Motion Mouse Tracking
3435 '?1003h' - Use All Motion Mouse Tracking
3436 '?1004h' - Send focus in/focus out events
3437 '?1005h' - Enable UTF-8 Mouse Mode
3438 '?1006h' - Enable SGR Mouse Mode
3439 '?1015h' - Enable urxvt Mouse Mode
3440 '?1049h' - Save cursor and screen
3441 """
3442 # TODO: Add support for the following:
3443 # * 3: 132 column mode (might be "or greater")
3444 # * 4: Smooth scroll (for animations and also makes things less choppy)
3445 # * 5: Reverse video (should be easy: just need some extra CSS)
3446 # * 6: Origin mode
3447 # * 7: Wraparound mode
3448 logging.debug("set_expanded_mode(%s)" % setting)
3449 if setting.startswith('?'):
3450 # DEC Private Mode Set
3451 setting = setting[1:] # Don't need the ?
3452 settings = setting.split(';')
3453 for setting in settings:
3454 try:
3455 self.expanded_mode_handlers[setting](True)
3456 except (KeyError, TypeError):
3457 pass # Unsupported expanded mode
3458 try:
3459 for callback in self.callbacks[CALLBACK_MODE].values():
3460 callback(setting, True)
3461 except TypeError:
3462 pass
3463 else:
3464 # There's a couple mode settings that are just "[Nh" where N==number
3465 # [2h Keyboard Action Mode (AM)
3466 # [4h Insert Mode
3467 # [12h Send/Receive Mode (SRM)
3468 # [24h Automatic Newline (LNM)
3469 if setting == '4':
3470 self.insert_mode = True
3472 def reset_expanded_mode(self, setting):
3473 """
3474 Accepts "standard mode" settings. Typically '\\\\x1b[?25l' to show
3475 cursor.
3476 """
3477 logging.debug("reset_expanded_mode(%s)" % setting)
3478 if setting.startswith('?'):
3479 setting = setting[1:] # Don't need the ?
3480 settings = setting.split(';')
3481 for setting in settings:
3482 try:
3483 self.expanded_mode_handlers[setting](False)
3484 except (KeyError, TypeError):
3485 pass # Unsupported expanded mode
3486 try:
3487 for callback in self.callbacks[CALLBACK_MODE].values():
3488 callback(setting, False)
3489 except TypeError:
3490 pass
3491 else:
3492 # There's a couple mode settings that are just "[Nh" where N==number
3493 # [2h Keyboard Action Mode (AM)
3494 # [4h Insert Mode
3495 # [12h Send/Receive Mode (SRM)
3496 # [24h Automatic Newline (LNM)
3497 # The only one we care about is 4 (insert mode)
3498 if setting == '4':
3499 self.insert_mode = False
3501 def toggle_alternate_screen_buffer(self, alt):
3502 """
3503 If *alt* is True, copy the current screen and renditions to
3504 :attr:`self.alt_screen` and :attr:`self.alt_renditions` then re-init
3505 :attr:`self.screen` and :attr:`self.renditions`.
3507 If *alt* is False, restore the saved screen buffer and renditions then
3508 nullify :attr:`self.alt_screen` and :attr:`self.alt_renditions`.
3509 """
3510 #logging.debug('toggle_alternate_screen_buffer(%s)' % alt)
3511 if alt:
3512 # Save the existing screen and renditions
3513 self.alt_screen = self.screen[:]
3514 self.alt_renditions = self.renditions[:]
3515 # Make a fresh one
3516 self.clear_screen()
3517 else:
3518 # Restore the screen
3519 if self.alt_screen and self.alt_renditions:
3520 self.screen = self.alt_screen[:]
3521 self.renditions = self.alt_renditions[:]
3522 # Empty out the alternate buffer (to save memory)
3523 self.alt_screen = None
3524 self.alt_renditions = None
3525 # These all need to be reset no matter what
3526 self.cur_rendition = unichr(1000)
3528 def toggle_alternate_screen_buffer_cursor(self, alt):
3529 """
3530 Same as :meth:`Terminal.toggle_alternate_screen_buffer` but also
3531 saves/restores the cursor location.
3532 """
3533 #logging.debug('toggle_alternate_screen_buffer_cursor(%s)' % alt)
3534 if alt:
3535 self.alt_cursorX = self.cursorX
3536 self.alt_cursorY = self.cursorY
3537 else:
3538 self.cursorX = self.alt_cursorX
3539 self.cursorY = self.alt_cursorY
3540 self.toggle_alternate_screen_buffer(alt)
3542 def expanded_mode_toggle(self, mode, boolean):
3543 """
3544 Meant to be used with (simple) expanded mode settings that merely set or
3545 reset attributes for tracking purposes; sets `self.expanded_modes[mode]`
3546 to *boolean*. Example usage::
3548 >>> self.expanded_mode_handlers['1000'] = partial(self.expanded_mode_toggle, 'mouse_button_events')
3549 """
3550 self.expanded_modes[mode] = boolean
3552 def insert_characters(self, n=1):
3553 """
3554 Inserts the specified number of characters at the cursor position.
3555 Overwriting whatever is already present.
3556 """
3557 #logging.debug("insert_characters(%s)" % n)
3558 n = int(n)
3559 for i in xrange(n):
3560 self.screen[self.cursorY].pop() # Take one down, pass it around
3561 self.screen[self.cursorY].insert(self.cursorX, u' ')
3563 def delete_characters(self, n=1):
3564 """
3565 DCH - Deletes (to the left) the specified number of characters at the
3566 cursor position. As characters are deleted, the remaining characters
3567 between the cursor and right margin move to the left. Character
3568 attributes (renditions) move with the characters. The terminal adds
3569 blank spaces with no visual character attributes at the right margin.
3570 DCH has no effect outside the scrolling margins.
3572 .. note:: Deletes renditions too. You'd *think* that would be in one of the VT-* manuals... Nope!
3573 """
3574 #logging.debug("delete_characters(%s)" % n)
3575 if not n: # e.g. n == ''
3576 n = 1
3577 else:
3578 n = int(n)
3579 for i in xrange(n):
3580 try:
3581 self.screen[self.cursorY].pop(self.cursorX)
3582 self.screen[self.cursorY].append(u' ')
3583 self.renditions[self.cursorY].pop(self.cursorX)
3584 self.renditions[self.cursorY].append(unichr(1000))
3585 except IndexError:
3586 # At edge of screen, ignore
3587 #print('IndexError in delete_characters(): %s' % e)
3588 pass
3590 def _erase_characters(self, n=1):
3591 """
3592 Erases (to the right) the specified number of characters at the cursor
3593 position.
3595 .. note:: Deletes renditions too.
3596 """
3597 #logging.debug("_erase_characters(%s)" % n)
3598 if not n: # e.g. n == ''
3599 n = 1
3600 else:
3601 n = int(n)
3602 distance = self.cols - self.cursorX
3603 n = min(n, distance)
3604 for i in xrange(n):
3605 self.screen[self.cursorY][self.cursorX+i] = u' '
3606 self.renditions[self.cursorY][self.cursorX+i] = unichr(1000)
3608 def cursor_left(self, n=1):
3609 """ESCnD CUB (Cursor Back)"""
3610 # Commented out to save CPU (and the others below too)
3611 #logging.debug('cursor_left(%s)' % n)
3612 n = int(n)
3613 # This logic takes care of double-width unicode characters
3614 if self.double_width_left:
3615 self.double_width_left = False
3616 return
3617 self.cursorX = max(0, self.cursorX - n) # Ensures positive value
3618 try:
3619 char = self.screen[self.cursorY][self.cursorX]
3620 except IndexError: # Cursor is past the right-edge of the screen; ignore
3621 char = u' ' # This is a safe default/fallback
3622 if unicodedata.east_asian_width(char) == 'W':
3623 # This lets us skip the next call (get called 2x for 2x width)
3624 self.double_width_left = True
3625 try:
3626 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3627 callback()
3628 except TypeError:
3629 pass
3631 def cursor_right(self, n=1):
3632 """ESCnC CUF (Cursor Forward)"""
3633 #logging.debug('cursor_right(%s)' % n)
3634 if not n:
3635 n = 1
3636 n = int(n)
3637 # This logic takes care of double-width unicode characters
3638 if self.double_width_right:
3639 self.double_width_right = False
3640 return
3641 self.cursorX += n
3642 try:
3643 char = self.screen[self.cursorY][self.cursorX]
3644 except IndexError: # Cursor is past the right-edge of the screen; ignore
3645 char = u' ' # This is a safe default/fallback
3646 if unicodedata.east_asian_width(char) == 'W':
3647 # This lets us skip the next call (get called 2x for 2x width)
3648 self.double_width_right = True
3649 try:
3650 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3651 callback()
3652 except TypeError:
3653 pass
3655 def cursor_up(self, n=1):
3656 """ESCnA CUU (Cursor Up)"""
3657 #logging.debug('cursor_up(%s)' % n)
3658 if not n:
3659 n = 1
3660 n = int(n)
3661 self.cursorY = max(0, self.cursorY - n)
3662 try:
3663 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3664 callback()
3665 except TypeError:
3666 pass
3668 def cursor_down(self, n=1):
3669 """ESCnB CUD (Cursor Down)"""
3670 #logging.debug('cursor_down(%s)' % n)
3671 if not n:
3672 n = 1
3673 n = int(n)
3674 self.cursorY = min(self.rows, self.cursorY + n)
3675 try:
3676 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3677 callback()
3678 except TypeError:
3679 pass
3681 def cursor_next_line(self, n):
3682 """ESCnE CNL (Cursor Next Line)"""
3683 #logging.debug("cursor_next_line(%s)" % n)
3684 if not n:
3685 n = 1
3686 n = int(n)
3687 self.cursorY = min(self.rows, self.cursorY + n)
3688 self.cursorX = 0
3689 try:
3690 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3691 callback()
3692 except TypeError:
3693 pass
3695 def cursor_previous_line(self, n):
3696 """ESCnF CPL (Cursor Previous Line)"""
3697 #logging.debug("cursor_previous_line(%s)" % n)
3698 if not n:
3699 n = 1
3700 n = int(n)
3701 self.cursorY = max(0, self.cursorY - n)
3702 self.cursorX = 0
3703 try:
3704 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3705 callback()
3706 except TypeError:
3707 pass
3709 def cursor_horizontal_absolute(self, n):
3710 """ESCnG CHA (Cursor Horizontal Absolute)"""
3711 if not n:
3712 n = 1
3713 n = int(n)
3714 self.cursorX = n - 1 # -1 because cols is 0-based
3715 try:
3716 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3717 callback()
3718 except TypeError:
3719 pass
3721 def cursor_position(self, coordinates):
3722 """
3723 ESCnH CUP (Cursor Position). Move the cursor to the given coordinates.
3725 :coordinates: Should be something like, 'row;col' (1-based) but, 'row', 'row;', and ';col' are also valid (assumes 1 on missing value).
3727 .. note::
3729 If coordinates is '' (an empty string), the cursor will be moved to
3730 the top left (1;1).
3731 """
3732 # NOTE: Since this is 1-based we have to subtract 1 from everything to
3733 # match how we store these values internally.
3734 if not coordinates:
3735 row, col = 0, 0
3736 elif ';' in coordinates:
3737 row, col = coordinates.split(';')
3738 else:
3739 row = coordinates
3740 col = 0
3741 try:
3742 row = int(row)
3743 except ValueError:
3744 row = 0
3745 try:
3746 col = int(col)
3747 except ValueError:
3748 col = 0
3749 # These ensure a positive integer while reducing row and col by 1:
3750 row = max(0, row - 1)
3751 col = max(0, col - 1)
3752 self.cursorY = row
3753 # The column needs special attention in case there's double-width
3754 # characters.
3755 double_width = 0
3756 if self.cursorY < self.rows:
3757 for i, char in enumerate(self.screen[self.cursorY]):
3758 if i == col - double_width:
3759 # No need to continue further
3760 break
3761 if unicodedata.east_asian_width(char) == 'W':
3762 double_width += 1
3763 if double_width:
3764 col = col - double_width
3765 self.cursorX = col
3766 try:
3767 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3768 callback()
3769 except TypeError:
3770 pass
3772 def cursor_position_vertical(self, n):
3773 """
3774 Vertical Line Position Absolute (VPA) - Moves the cursor to given line.
3775 """
3776 n = int(n)
3777 self.cursorY = n - 1
3779 def clear_screen(self):
3780 """
3781 Clears the screen. Also used to emulate a terminal reset.
3783 .. note::
3785 The current rendition (self.cur_rendition) will be applied to all
3786 characters on the screen when this function is called.
3787 """
3788 logging.debug('clear_screen()')
3789 self.scroll_up(len(self.screen) - 1)
3790 self.init_screen()
3791 self.init_renditions(self.cur_rendition)
3792 self.cursorX = 0
3793 self.cursorY = 0
3795 def clear_screen_from_cursor_down(self):
3796 """
3797 Clears the screen from the cursor down (ESC[J or ESC[0J).
3799 .. note:: This method actually erases from the cursor position to the end of the screen.
3800 """
3801 #logging.debug('clear_screen_from_cursor_down()')
3802 self.clear_line_from_cursor_right()
3803 if self.cursorY == self.rows - 1:
3804 # Bottom of screen; nothing to do
3805 return
3806 self.screen[self.cursorY+1:] = [
3807 array('u', u' ' * self.cols) for a in self.screen[self.cursorY+1:]
3808 ]
3809 c = self.cur_rendition # Just to save space below
3810 self.renditions[self.cursorY+1:] = [
3811 array('u', c * self.cols) for a in self.renditions[self.cursorY+1:]
3812 ]
3814 def clear_screen_from_cursor_up(self):
3815 """
3816 Clears the screen from the cursor up (ESC[1J).
3817 """
3818 #logging.debug('clear_screen_from_cursor_up()')
3819 self.screen[:self.cursorY+1] = [
3820 array('u', u' ' * self.cols) for a in self.screen[:self.cursorY]
3821 ]
3822 c = self.cur_rendition
3823 self.renditions[:self.cursorY+1] = [
3824 array('u', c * self.cols) for a in self.renditions[:self.cursorY]
3825 ]
3826 self.cursorY = 0
3828 def clear_screen_from_cursor(self, n):
3829 """
3830 CSI *n* J ED (Erase Data). This escape sequence uses the following rules:
3832 ====== ============================= ===
3833 Esc[J Clear screen from cursor down ED0
3834 Esc[0J Clear screen from cursor down ED0
3835 Esc[1J Clear screen from cursor up ED1
3836 Esc[2J Clear entire screen ED2
3837 ====== ============================= ===
3838 """
3839 #logging.debug('clear_screen_from_cursor(%s)' % n)
3840 try:
3841 n = int(n)
3842 except ValueError: # Esc[J
3843 n = 0
3844 clear_types = {
3845 0: self.clear_screen_from_cursor_down,
3846 1: self.clear_screen_from_cursor_up,
3847 2: self.clear_screen
3848 }
3849 try:
3850 clear_types[n]()
3851 except KeyError:
3852 logging.error(_("Error: Unsupported number for escape sequence J"))
3853 # Execute our callbacks
3854 try:
3855 for callback in self.callbacks[CALLBACK_CHANGED].values():
3856 callback()
3857 except TypeError:
3858 pass
3859 try:
3860 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3861 callback()
3862 except TypeError:
3863 pass
3865 def clear_line_from_cursor_right(self):
3866 """
3867 Clears the screen from the cursor right (ESC[K or ESC[0K).
3868 """
3869 #logging.debug("clear_line_from_cursor_right()")
3870 saved = self.screen[self.cursorY][:self.cursorX]
3871 saved_renditions = self.renditions[self.cursorY][:self.cursorX]
3872 spaces = array('u', u' '*len(self.screen[self.cursorY][self.cursorX:]))
3873 renditions = array('u',
3874 self.cur_rendition * len(self.screen[self.cursorY][self.cursorX:]))
3875 self.screen[self.cursorY] = saved + spaces
3876 # Reset the cursor position's rendition to the end of the line
3877 self.renditions[self.cursorY] = saved_renditions + renditions
3879 def clear_line_from_cursor_left(self):
3880 """
3881 Clears the screen from the cursor left (ESC[1K).
3882 """
3883 #logging.debug("clear_line_from_cursor_left()")
3884 saved = self.screen[self.cursorY][self.cursorX:]
3885 saved_renditions = self.renditions[self.cursorY][self.cursorX:]
3886 spaces = array('u', u' '*len(self.screen[self.cursorY][:self.cursorX]))
3887 renditions = array('u',
3888 self.cur_rendition * len(self.screen[self.cursorY][:self.cursorX]))
3889 self.screen[self.cursorY] = spaces + saved
3890 self.renditions[self.cursorY] = renditions + saved_renditions
3892 def clear_line(self):
3893 """
3894 Clears the entire line (ESC[2K).
3895 """
3896 #logging.debug("clear_line()")
3897 self.screen[self.cursorY] = array('u', u' ' * self.cols)
3898 c = self.cur_rendition
3899 self.renditions[self.cursorY] = array('u', c * self.cols)
3900 self.cursorX = 0
3902 def clear_line_from_cursor(self, n):
3903 """
3904 CSI*n*K EL (Erase in Line). This escape sequence uses the following
3905 rules:
3907 ====== ============================== ===
3908 Esc[K Clear screen from cursor right EL0
3909 Esc[0K Clear screen from cursor right EL0
3910 Esc[1K Clear screen from cursor left EL1
3911 Esc[2K Clear entire line ED2
3912 ====== ============================== ===
3913 """
3914 #logging.debug('clear_line_from_cursor(%s)' % n)
3915 try:
3916 n = int(n)
3917 except ValueError: # Esc[J
3918 n = 0
3919 clear_types = {
3920 0: self.clear_line_from_cursor_right,
3921 1: self.clear_line_from_cursor_left,
3922 2: self.clear_line
3923 }
3924 try:
3925 clear_types[n]()
3926 except KeyError:
3927 logging.error(_(
3928 "Error: Unsupported number for CSI escape sequence K"))
3929 # Execute our callbacks
3930 try:
3931 for callback in self.callbacks[CALLBACK_CHANGED].values():
3932 callback()
3933 except TypeError:
3934 pass
3935 try:
3936 for callback in self.callbacks[CALLBACK_CURSOR_POS].values():
3937 callback()
3938 except TypeError:
3939 pass
3941 def set_led_state(self, n):
3942 """
3943 Sets the values the dict, self.leds depending on *n* using the following
3944 rules:
3946 ====== ====================== ======
3947 Esc[0q Turn off all four leds DECLL0
3948 Esc[1q Turn on LED #1 DECLL1
3949 Esc[2q Turn on LED #2 DECLL2
3950 Esc[3q Turn on LED #3 DECLL3
3951 Esc[4q Turn on LED #4 DECLL4
3952 ====== ====================== ======
3954 .. note:: These aren't implemented in Gate One's GUI (yet) but they certainly kept track of!
3955 """
3956 logging.debug("set_led_state(%s)" % n)
3957 leds = n.split(';')
3958 for led in leds:
3959 led = int(led)
3960 if led == 0:
3961 self.leds[1] = False
3962 self.leds[2] = False
3963 self.leds[3] = False
3964 self.leds[4] = False
3965 else:
3966 self.leds[led] = True
3967 try:
3968 for callback in self.callbacks[CALLBACK_LEDS].values():
3969 callback(led)
3970 except TypeError:
3971 pass
3973 def _set_rendition(self, n):
3974 """
3975 Sets :attr:`self.renditions[self.cursorY][self.cursorX]` equal to
3976 *n.split(';')*.
3978 *n* is expected to be a string of ECMA-48 rendition numbers separated by
3979 semicolons. Example::
3981 '0;1;31'
3983 ...will result in::
3985 [0, 1, 31]
3987 Note that the numbers were converted to integers and the order was
3988 preserved.
3989 """
3990 #logging.debug("_set_rendition(%s)" % n)
3991 cursorY = self.cursorY
3992 cursorX = self.cursorX
3993 if cursorX >= self.cols: # We're at the end of the row
3994 try:
3995 if len(self.renditions[cursorY]) <= cursorX:
3996 # Make it all longer
3997 self.renditions[cursorY].append(u' ') # Make it longer
3998 self.screen[cursorY].append(u'\x00') # This needs to match
3999 except IndexError:
4000 # This can happen if the rate limiter kicks in and starts
4001 # cutting off escape sequences at random.
4002 return # Don't bother attempting to process anything else
4003 if cursorY >= self.rows:
4004 logging.error(_(
4005 "cursorY >= self.rows! This is either a bug or just a symptom "
4006 "of the rate limiter kicking in."))
4007 return # Don't bother setting renditions past the bottom
4008 if not n: # or \x1b[m (reset)
4009 # First char in PUA Plane 16 is always the default:
4010 self.cur_rendition = unichr(1000) # Should be reset (e.g. [0])
4011 return # No need for further processing; save some CPU
4012 # Convert the string (e.g. '0;1;32') to a list (e.g. [0,1,32]
4013 new_renditions = [int(a) for a in n.split(';') if a != '']
4014 # Handle 256-color renditions by getting rid of the (38|48);5 part and
4015 # incrementing foregrounds by 1000 and backgrounds by 10000 so we can
4016 # tell them apart in _spanify_screen().
4017 try:
4018 if 38 in new_renditions:
4019 foreground_index = new_renditions.index(38)
4020 if len(new_renditions[foreground_index:]) >= 2:
4021 if new_renditions[foreground_index+1] == 5:
4022 # This is a valid 256-color rendition (38;5;<num>)
4023 new_renditions.pop(foreground_index) # Goodbye 38
4024 new_renditions.pop(foreground_index) # Goodbye 5
4025 new_renditions[foreground_index] += 1000
4026 if 48 in new_renditions:
4027 background_index = new_renditions.index(48)
4028 if len(new_renditions[background_index:]) >= 2:
4029 if new_renditions[background_index+1] == 5:
4030 # This is a valid 256-color rendition (48;5;<num>)
4031 new_renditions.pop(background_index) # Goodbye 48
4032 new_renditions.pop(background_index) # Goodbye 5
4033 new_renditions[background_index] += 10000
4034 except IndexError:
4035 # Likely that the rate limiter has caused all sorts of havoc with
4036 # escape sequences. Just ignore it and halt further processing
4037 return
4038 out_renditions = []
4039 for rend in new_renditions:
4040 if rend == 0:
4041 out_renditions = [0]
4042 else:
4043 out_renditions.append(rend)
4044 if out_renditions[0] == 0:
4045 # If it starts with 0 there's no need to combine it with the
4046 # previous rendition...
4047 reduced = _reduce_renditions(out_renditions)
4048 if reduced not in self.renditions_store.values():
4049 new_ref_point = next_compat(self.rend_counter)
4050 self.renditions_store.update({new_ref_point: reduced})
4051 self.cur_rendition = new_ref_point
4052 else: # Find the right reference point to use
4053 for k, v in self.renditions_store.items():
4054 if reduced == v:
4055 self.cur_rendition = k
4056 return
4057 new_renditions = out_renditions
4058 cur_rendition_list = self.renditions_store[self.cur_rendition]
4059 reduced = _reduce_renditions(cur_rendition_list + new_renditions)
4060 if reduced not in self.renditions_store.values():
4061 new_ref_point = next_compat(self.rend_counter)
4062 self.renditions_store.update({new_ref_point: reduced})
4063 self.cur_rendition = new_ref_point
4064 else: # Find the right reference point to use
4065 for k, v in self.renditions_store.items():
4066 if reduced == v:
4067 self.cur_rendition = k
4069 def _opt_handler(self, chars):
4070 """
4071 Optional special escape sequence handler for sequences matching
4072 RE_OPT_SEQ. If CALLBACK_OPT is defined it will be called like so::
4074 self.callbacks[CALLBACK_OPT](chars)
4076 Applications can use this escape sequence to define whatever special
4077 handlers they like. It works like this: If an escape sequence is
4078 encountered matching RE_OPT_SEQ this method will be called with the
4079 inbetween *chars* (e.g. \x1b]_;<chars>\x07) as the argument.
4081 Applications can then do what they wish with *chars*.
4083 .. note::
4085 I added this functionality so that plugin authors would have a
4086 mechanism to communicate with terminal applications. See the SSH
4087 plugin for an example of how this can be done (there's channels of
4088 communication amongst ssh_connect.py, ssh.js, and ssh.py).
4089 """
4090 try:
4091 for callback in self.callbacks[CALLBACK_OPT].values():
4092 callback(chars)
4093 except TypeError:
4094 # High likelyhood that nothing is defined. No biggie.
4095 pass
4097# NOTE: This was something I was testing to simplify the code. It works
4098# (mostly) but the performance was TERRIBLE. Still needs investigation...
4099 #def _classify_renditions(self):
4100 #"""
4101 #Returns ``self.renditions`` as a list of HTML classes for each position.
4102 #"""
4103 #return [[map(RENDITION_CLASSES.get, rend) for rend in map(
4104 #self.renditions_store.get, rendition)]
4105 #for rendition in self.renditions]
4107 #def _spanify_line(self, line, rendition, current_classes=None, cursor=False):
4108 #"""
4109 #Returns a string containing *line* with HTML spans applied representing
4110 #*renditions*.
4111 #"""
4112 #outline = ""
4113 #reset_classes = RESET_CLASSES # TODO
4114 #html_entities = {"&": "&", '<': '<', '>': '>'}
4115 #foregrounds = ('f0','f1','f2','f3','f4','f5','f6','f7')
4116 #backgrounds = ('b0','b1','b2','b3','b4','b5','b6','b7')
4117 #prev_rendition = None
4118 #if current_classes:
4119 #outline += '<span class="%s%s">' % (
4120 #self.class_prefix,
4121 #(" %s" % self.class_prefix).join(current_classes))
4122 #charcount = 0
4123 #for char, rend in izip(line, rendition):
4124 #changed = True
4125 #if char in "&<>":
4126 ## Have to convert ampersands and lt/gt to HTML entities
4127 #char = html_entities[char]
4128 #if rend == prev_rendition:
4129 ## Shortcut... So we can skip all the logic below
4130 #changed = False
4131 #else:
4132 #prev_rendition = rend
4133 #if changed:
4134 #outline += "</span>"
4135 #current_classes = [a for a in rend if a and 'reset' not in a]
4136 ##if rend and rend[0] == 'reset':
4137 ##if len(current_classes) > 1:
4138 ##classes = (
4139 ##" %s" % self.class_prefix).join(current_classes)
4140 ##else:
4141 #classes = (" %s" % self.class_prefix).join(current_classes)
4142 #if current_classes != ['reset']:
4143 #outline += '<span class="%s%s">' % (
4144 #self.class_prefix, classes)
4145 #if cursor and charcount == cursor:
4146 #outline += '<span class="%scursor">%s</span>' % (
4147 #self.class_prefix, char)
4148 #else:
4149 #outline += char
4150 #charcount += 1
4151 #open_spans = outline.count('<span')
4152 #close_spans = outline.count('</span')
4153 #if open_spans != close_spans:
4154 #for i in xrange(open_spans - close_spans):
4155 #outline += '</span>'
4156 #return current_classes, outline
4158 #def _spanify_screen_test(self):
4159 #"""
4160 #Iterates over the lines in *screen* and *renditions*, applying HTML
4161 #markup (span tags) where appropriate and returns the result as a list of
4162 #lines. It also marks the cursor position via a <span> tag at the
4163 #appropriate location.
4164 #"""
4165 ##logging.debug("_spanify_screen()")
4166 #results = []
4167 ## NOTE: Why these duplicates of self.* and globals? Local variable
4168 ## lookups are faster--especially in loops.
4169 #special = SPECIAL
4170 ##rendition_classes = RENDITION_CLASSES
4171 #html_cache = HTML_CACHE
4172 #has_cache = isinstance(html_cache, AutoExpireDict)
4173 #screen = self.screen
4174 #renditions = self.renditions
4175 #renditions_store = self.renditions_store
4176 #classified_renditions = self._classify_renditions()
4177 #cursorX = self.cursorX
4178 #cursorY = self.cursorY
4179 #show_cursor = self.expanded_modes['25']
4180 ##spancount = 0
4181 #current_classes = []
4182 ##prev_rendition = None
4183 ##foregrounds = ('f0','f1','f2','f3','f4','f5','f6','f7')
4184 ##backgrounds = ('b0','b1','b2','b3','b4','b5','b6','b7')
4185 ##html_entities = {"&": "&", '<': '<', '>': '>'}
4186 #cursor_span = '<span class="%scursor">' % self.class_prefix
4187 #for linecount, line in enumerate(screen):
4188 #rendition = classified_renditions[linecount]
4189 #combined = (line + renditions[linecount]).tounicode()
4190 #if has_cache and combined in html_cache:
4191 ## Always re-render the line with the cursor (or just had it)
4192 #if cursor_span not in html_cache[combined]:
4193 ## Use the cache...
4194 #results.append(html_cache[combined])
4195 #continue
4196 #if not len(line.tounicode().rstrip()) and linecount != cursorY:
4197 #results.append(line.tounicode())
4198 #continue # Line is empty so we don't need to process renditions
4199 #if linecount == cursorY and show_cursor:
4200 #current_classes, outline = self._spanify_line(
4201 #line, rendition,
4202 #current_classes=current_classes,
4203 #cursor=cursorX)
4204 #else:
4205 #current_classes, outline = self._spanify_line(
4206 #line, rendition,
4207 #current_classes=current_classes,
4208 #cursor=False)
4209 #if outline:
4210 #results.append(outline)
4211 #if html_cache:
4212 #html_cache[combined] = outline
4213 #else:
4214 #results.append(None) # null is less memory than spaces
4215 ## NOTE: The client has been programmed to treat None (aka null in
4216 ## JavaScript) as blank lines.
4217 #return results
4219 def _spanify_screen(self):
4220 """
4221 Iterates over the lines in *screen* and *renditions*, applying HTML
4222 markup (span tags) where appropriate and returns the result as a list of
4223 lines. It also marks the cursor position via a <span> tag at the
4224 appropriate location.
4225 """
4226 #logging.debug("_spanify_screen()")
4227 results = []
4228 # NOTE: Why these duplicates of self.* and globals? Local variable
4229 # lookups are faster--especially in loops.
4230 special = SPECIAL
4231 rendition_classes = RENDITION_CLASSES
4232 html_cache = HTML_CACHE
4233 has_cache = isinstance(html_cache, AutoExpireDict)
4234 screen = self.screen
4235 renditions = self.renditions
4236 renditions_store = self.renditions_store
4237 cursorX = self.cursorX
4238 cursorY = self.cursorY
4239 show_cursor = self.expanded_modes['25']
4240 spancount = 0
4241 current_classes = set()
4242 prev_rendition = None
4243 foregrounds = ('f0','f1','f2','f3','f4','f5','f6','f7')
4244 backgrounds = ('b0','b1','b2','b3','b4','b5','b6','b7')
4245 html_entities = {"&": "&", '<': '<', '>': '>'}
4246 cursor_span = '<span class="%scursor">' % self.class_prefix
4247 for linecount, line in enumerate(screen):
4248 rendition = renditions[linecount]
4249 line_chars = line.tounicode()
4250 combined = line_chars + rendition.tounicode()
4251 cursor_line = True if linecount == cursorY else False
4252 if not cursor_line and has_cache and combined in html_cache:
4253 # Always re-render the line with the cursor (or just had it)
4254 if cursor_span not in html_cache[combined]:
4255 # Use the cache...
4256 results.append(html_cache[combined])
4257 continue
4258 if not len(line_chars.rstrip()) and not cursor_line:
4259 results.append(line_chars)
4260 continue # Line is empty so we don't need to process renditions
4261 outline = ""
4262 if current_classes:
4263 outline += '<span class="%s%s">' % (
4264 self.class_prefix,
4265 (" %s" % self.class_prefix).join(current_classes))
4266 charcount = 0
4267 for char, rend in izip(line, rendition):
4268 rend = renditions_store[rend] # Get actual rendition
4269 if ord(char) >= special: # Special stuff =)
4270 # Obviously, not really a single character
4271 if char in self.captured_files:
4272 outline += self.captured_files[char].html()
4273 continue
4274 changed = True
4275 if char in "&<>":
4276 # Have to convert ampersands and lt/gt to HTML entities
4277 char = html_entities[char]
4278 if rend == prev_rendition:
4279 # Shortcut... So we can skip all the logic below
4280 changed = False
4281 else:
4282 prev_rendition = rend
4283 if changed and rend:
4284 classes = imap(rendition_classes.get, rend)
4285 for _class in classes:
4286 if _class and _class not in current_classes:
4287 # Something changed... Start a new span
4288 if spancount:
4289 outline += "</span>"
4290 spancount -= 1
4291 if 'reset' in _class:
4292 if _class == 'reset':
4293 current_classes = set()
4294 if spancount:
4295 for i in xrange(spancount):
4296 outline += "</span>"
4297 spancount = 0
4298 else:
4299 reset_class = _class.split('reset')[0]
4300 if reset_class == 'foreground':
4301 [current_classes.remove(a) for a in
4302 current_classes if a in foregrounds]
4303 elif reset_class == 'background':
4304 [current_classes.remove(a) for a in
4305 current_classes if a in backgrounds]
4306 elif reset_class in current_classes:
4307 current_classes.remove(reset_class)
4308 else:
4309 if _class in foregrounds:
4310 [current_classes.remove(a) for a in
4311 current_classes if a in foregrounds]
4312 elif _class in backgrounds:
4313 [current_classes.remove(a) for a in
4314 current_classes if a in backgrounds]
4315 current_classes.add(_class)
4316 if current_classes:
4317 outline += '<span class="%s%s">' % (
4318 self.class_prefix,
4319 (" %s" % self.class_prefix).join(current_classes))
4320 spancount += 1
4321 if cursor_line and show_cursor and charcount == cursorX:
4322 outline += '<span class="%scursor">%s</span>' % (
4323 self.class_prefix, char)
4324 else:
4325 outline += char
4326 charcount += 1
4327 if outline:
4328 # Make sure all renditions terminate at the end of the line
4329 for whatever in xrange(spancount):
4330 outline += "</span>"
4331 results.append(outline)
4332 if has_cache:
4333 html_cache[combined] = outline
4334 else:
4335 results.append(None) # null is shorter than spaces
4336 # NOTE: The client has been programmed to treat None (aka null in
4337 # JavaScript) as blank lines.
4338 for whatever in xrange(spancount): # Bit of cleanup to be safe
4339 results[-1] += "</span>"
4340 return results
4342 def _spanify_scrollback(self):
4343 """
4344 Spanifies (turns renditions into `<span>` elements) everything inside
4345 `self.scrollback` using `self.renditions`. This differs from
4346 `_spanify_screen` in that it doesn't apply any logic to detect the
4347 location of the cursor (to make it just a tiny bit faster).
4348 """
4349 # NOTE: See the comments in _spanify_screen() for details on this logic
4350 results = []
4351 special = SPECIAL
4352 html_cache = HTML_CACHE
4353 has_cache = isinstance(html_cache, AutoExpireDict)
4354 screen = self.scrollback_buf
4355 renditions = self.scrollback_renditions
4356 rendition_classes = RENDITION_CLASSES
4357 renditions_store = self.renditions_store
4358 spancount = 0
4359 current_classes = set()
4360 prev_rendition = None
4361 foregrounds = ('f0','f1','f2','f3','f4','f5','f6','f7')
4362 backgrounds = ('b0','b1','b2','b3','b4','b5','b6','b7')
4363 html_entities = {"&": "&", '<': '<', '>': '>'}
4364 cursor_span = '<span class="%scursor">' % self.class_prefix
4365 for line, rendition in izip(screen, renditions):
4366 combined = (line + rendition).tounicode()
4367 if has_cache and combined in html_cache:
4368 # Most lines should be in the cache because they were rendered
4369 # while they were on the screen.
4370 if cursor_span not in html_cache[combined]:
4371 results.append(html_cache[combined])
4372 continue
4373 if not len(line.tounicode().rstrip()):
4374 results.append(line.tounicode())
4375 continue # Line is empty so we don't need to process renditions
4376 outline = ""
4377 if current_classes:
4378 outline += '<span class="%s%s">' % (
4379 self.class_prefix,
4380 (" %s" % self.class_prefix).join(current_classes))
4381 for char, rend in izip(line, rendition):
4382 rend = renditions_store[rend] # Get actual rendition
4383 if ord(char) >= special: # Special stuff =)
4384 # Obviously, not really a single character
4385 if char in self.captured_files:
4386 outline += self.captured_files[char].html()
4387 continue
4388 changed = True
4389 if char in "&<>":
4390 # Have to convert ampersands and lt/gt to HTML entities
4391 char = html_entities[char]
4392 if rend == prev_rendition:
4393 changed = False
4394 else:
4395 prev_rendition = rend
4396 if changed and rend != None:
4397 classes = imap(rendition_classes.get, rend)
4398 for _class in classes:
4399 if _class and _class not in current_classes:
4400 if spancount:
4401 outline += "</span>"
4402 spancount -= 1
4403 if 'reset' in _class:
4404 if _class == 'reset':
4405 current_classes = set()
4406 else:
4407 reset_class = _class.split('reset')[0]
4408 if reset_class == 'foreground':
4409 [current_classes.remove(a) for a in
4410 current_classes if a in foregrounds]
4411 elif reset_class == 'background':
4412 [current_classes.remove(a) for a in
4413 current_classes if a in backgrounds]
4414 elif reset_class in current_classes:
4415 current_classes.remove(reset_class)
4416 else:
4417 if _class in foregrounds:
4418 [current_classes.remove(a) for a in
4419 current_classes if a in foregrounds]
4420 elif _class in backgrounds:
4421 [current_classes.remove(a) for a in
4422 current_classes if a in backgrounds]
4423 current_classes.add(_class)
4424 if current_classes:
4425 outline += '<span class="%s%s">' % (
4426 self.class_prefix,
4427 (" %s" % self.class_prefix).join(current_classes))
4428 spancount += 1
4429 outline += char
4430 if outline:
4431 # Make sure all renditions terminate at the end of the line
4432 for whatever in xrange(spancount):
4433 outline += "</span>"
4434 results.append(outline)
4435 else:
4436 results.append(None)
4437 for whatever in xrange(spancount): # Bit of cleanup to be safe
4438 results[-1] += "</span>"
4439 return results
4441 def dump_html(self, renditions=True):
4442 """
4443 Dumps the terminal screen as a list of HTML-formatted lines. If
4444 *renditions* is True (default) then terminal renditions will be
4445 converted into HTML <span> elements so they will be displayed properly
4446 in a browser. Otherwise only the cursor <span> will be added to mark
4447 its location.
4449 .. note::
4451 This places <span class="cursor">(current character)</span> around
4452 the cursor location.
4453 """
4454 if renditions: # i.e. Use stylized text (the default)
4455 screen = self._spanify_screen()
4456 scrollback = []
4457 if self.scrollback_buf:
4458 scrollback = self._spanify_scrollback()
4459 else:
4460 cursorX = self.cursorX
4461 cursorY = self.cursorY
4462 screen = []
4463 for y, row in enumerate(self.screen):
4464 if y == cursorY:
4465 cursor_row = ""
4466 for x, char in enumerate(row):
4467 if x == cursorX:
4468 cursor_row += (
4469 '<span class="%scursor">%s</span>' % (
4470 self.class_prefix, char))
4471 else:
4472 cursor_row += char
4473 screen.append(cursor_row)
4474 else:
4475 screen.append("".join(row))
4476 scrollback = [a.tounicode() for a in self.scrollback_buf]
4477 # Empty the scrollback buffer:
4478 self.init_scrollback()
4479 self.modified = False
4480 return (scrollback, screen)
4482# NOTE: This is a work-in-progress. Don't use it.
4483 def dump_html_async(self, identifier=None, renditions=True, callback=None):
4484 """
4485 Dumps the terminal screen as a list of HTML-formatted lines. If
4486 *renditions* is True (default) then terminal renditions will be
4487 converted into HTML <span> elements so they will be displayed properly
4488 in a browser. Otherwise only the cursor <span> will be added to mark
4489 its location.
4491 .. note::
4493 This places <span class="cursor">(current character)</span> around
4494 the cursor location.
4495 """
4496 if self.async_mode:
4497 state_obj = {
4498 'html_cache': HTML_CACHE,
4499 'screen': self.screen,
4500 'renditions': self.renditions,
4501 'renditions_store': self.renditions_store,
4502 'cursorX': self.cursorX,
4503 'cursorY': self.cursorY,
4504 'show_cursor': self.expanded_modes['25'],
4505 'class_prefix': self.class_prefix
4506 }
4507 self.async_mode.call_singleton(
4508 spanify_screen, identifier, state_obj, callback=callback)
4509 else:
4510 scrollback, screen = self.dump_html(renditions=renditions)
4511 callback(scrollback, screen)
4513 def dump_plain(self):
4514 """
4515 Dumps the screen and the scrollback buffer as-is then empties the
4516 scrollback buffer.
4517 """
4518 screen = self.screen
4519 scrollback = self.scrollback_buf
4520 # Empty the scrollback buffer:
4521 self.init_scrollback()
4522 self.modified = False
4523 return (scrollback, screen)
4525 def dump_components(self):
4526 """
4527 Dumps the screen and renditions as-is, the scrollback buffer as HTML,
4528 and the current cursor coordinates. Also, empties the scrollback buffer
4530 .. note:: This was used in some performance-related experiments but might be useful for other patterns in the future so I've left it here.
4531 """
4532 screen = [a.tounicode() for a in self.screen]
4533 scrollback = []
4534 if self.scrollback_buf:
4535 # Process the scrollback buffer into HTML
4536 scrollback = self._spanify_scrollback(
4537 self.scrollback_buf, self.scrollback_renditions)
4538 # Empty the scrollback buffer:
4539 self.init_scrollback()
4540 self.modified = False
4541 return (scrollback, screen, self.renditions, self.cursorY, self.cursorX)
4543 def dump(self):
4544 """
4545 Returns self.screen as a list of strings with no formatting.
4546 No scrollback buffer. No renditions. It is meant to be used to get a
4547 quick glance of what is being displayed (when debugging).
4549 .. note:: This method does not empty the scrollback buffer.
4550 """
4551 out = []
4552 for line in self.screen:
4553 line_out = ""
4554 for char in line:
4555 if len(char) > 1: # This is an image (or similar)
4556 line_out += u'⬚' # Use a dotted square as a placeholder
4557 else:
4558 line_out += char
4559 out.append(line_out)
4560 self.modified = False
4561 return out
4563# This is here to make it easier for someone to produce an HTML app that uses
4564# terminal.py
4565def css_renditions(selector=None):
4566 """
4567 Returns a (long) string containing all the CSS styles in order to support
4568 terminal text renditions (different colors, bold, etc) in an HTML terminal
4569 using the dump_html() function. If *selector* is provided, all styles will
4570 be prefixed with said selector like so::
4572 ${selector} span.f0 { color: #5C5C5C; }
4574 Example::
4576 >>> css_renditions("#gateone").splitlines()[7]
4577 '#gateone span.f0 { color: #5C5C5C; } /* Black */'
4578 """
4579 from string import Template
4580 # Try looking for the fallback CSS template in two locations:
4581 # * The same directory that holds terminal.py
4582 # * A 'templates' directory in the same location as terminal.py
4583 template_name = 'terminal_renditions_fallback.css'
4584 template_path = os.path.join(os.path.split(__file__)[0], template_name)
4585 if not os.path.exists(template_path):
4586 # Try looking in a 'templates' directory
4587 template_path = os.path.join(
4588 os.path.split(__file__)[0], 'templates', template_name)
4589 if not os.path.exists(template_path):
4590 raise IOError("File not found: %s" % template_name)
4591 with open(template_path) as f:
4592 css = f.read()
4593 renditions_template = Template(css)
4594 return renditions_template.substitute(selector=selector)