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

1# -*- coding: utf-8 -*- 

2# 

3# Copyright 2011 Liftoff Software Corporation 

4# 

5 

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>' 

11 

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. 

20 

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 

26 

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! 

34 

35Supported Emulation Types 

36------------------------- 

37Without any special mode settings or parameters Terminal should effectively 

38emulate the following terminal types: 

39 

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") 

44 

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! 

48 

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. 

58 

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. 

66 

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: 

72 

73.. _callback_constants: 

74 

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==================================== ================================================================================ 

88 

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. 

90 

91Also, in most cases it is unwise to override CALLBACK_MODE since this method is primarily meant for internal use within the Terminal class. 

92 

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:: 

99 

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' '] 

107 

108Here's an example with some basic callbacks: 

109 

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. 

122 

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). 

124 

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. 

133 

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`. 

138 

139More information about how this works can be had by looking at the 

140:meth:`Terminal.dump_html` function itself. 

141 

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 =) 

143 

144Class Docstrings 

145================ 

146""" 

147 

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 

162 

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) 

194 

195# Inernationalization support 

196_ = str # So pyflakes doesn't complain 

197import gettext 

198gettext.install('terminal') 

199 

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 

222 

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 

309 

310RESET_CLASSES = set([ 

311 'backgroundreset', 

312 'boldreset', 

313 'dimreset', 

314 'italicreset', 

315 'underlinereset', 

316 'reversereset', 

317 'hiddenreset', 

318 'strikereset', 

319 'resetfont' 

320]) 

321 

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 

328 

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) 

514 

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. 

519 

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:: 

525 

526 [0, 32, 0, 34, 0, 32] 

527 

528 Would become:: 

529 

530 [0, 32] 

531 

532 Other Examples:: 

533 

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 

571 

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:: 

576 

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 >>> 

598 

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). 

604 

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 

614 

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). 

622 

623 .. note:: 

624 

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 

648 

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: 

654 

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 ========= ============ ========================= 

666 

667 Examples:: 

668 

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 

696 

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)) 

705 

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 = {"&": "&amp;", '<': '&lt;', '>': '&gt;'} 

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) 

847 

848# Exceptions 

849class InvalidParameters(Exception): 

850 """ 

851 Raised when `Terminal` is passed invalid parameters. 

852 """ 

853 pass 

854 

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:: 

861 

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 

867 

868 10 minutes later your key will be gone:: 

869 

870 >>> 'somekey' in expiring_dict 

871 False 

872 

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). 

876 

877 By default `AutoExpireDict` will check for expired keys every 30 seconds but 

878 this can be changed by setting the 'interval':: 

879 

880 >>> expiring_dict = AutoExpireDict(interval=5000) # 5 secs 

881 >>> # Or to change it after you've created one: 

882 >>> expiring_dict.interval = "10s" 

883 

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). 

887 

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. 

891 

892 .. note:: 

893 

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 

910 

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 

922 

923 @timeout.setter 

924 def timeout(self, value): 

925 if isinstance(value, basestring): 

926 value = convert_to_timedelta(value) 

927 self._timeout = value 

928 

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 

940 

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) 

953 

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() 

962 

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 

969 

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) 

977 

978 def __del__(self): 

979 """ 

980 Ensures that our `tornado.ioloop.PeriodicCallback` 

981 (``self._key_watcher``) gets stopped. 

982 """ 

983 self._key_watcher.stop() 

984 

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) 

992 

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() 

1002 

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] 

1012 

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 

1017 

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 

1052 

1053 def __repr__(self): 

1054 return "<%s>" % self.name 

1055 

1056 def __str__(self): 

1057 "Override if the defined file type warrants a text-based output." 

1058 return self.__repr__() 

1059 

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 

1073 

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 

1079 

1080 def html(self): 

1081 """ 

1082 Returns the object as an HTML-formatted string. Must be overridden. 

1083 """ 

1084 raise NotImplementedError 

1085 

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 

1102 

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) 

1111 

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. 

1121 

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 

1250 

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]) 

1285 

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 ) 

1302 

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 

1314 

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 

1343 

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. 

1363 

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 

1391 

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) 

1416 

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 

1442 

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) 

1467 

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 

1495 

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 

1535 

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 

1558 

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 

1583 

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 

1595 

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 

1633 

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 

1691 

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) 

1705 

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 

1712 

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) 

1734 

1735 class_prefix = u'✈' # Prefix used with HTML output span class names 

1736 # (to avoid namespace conflicts) 

1737 

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 } 

1775 

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) 

1784 

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. 

1792 

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:: 

1797 

1798 {'height': <px>, 'width': <px>} 

1799 

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:: 

1807 

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>']) 

1817 

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. 

1822 

1823 http://yourapp.company.com/terminal/tmpZoOKVM.pdf 

1824 

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:: 

1829 

1830 filetype_instance = filetype_class(icondir=self.icondir) 

1831 

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*. 

1835 

1836 If *debug* is True, the root logger will have its level set to DEBUG. 

1837 

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) 

1903 

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) 

2139 

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`. 

2144 

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}) 

2158 

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`. 

2163 

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] 

2186 

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:: 

2191 

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 

2211 

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. 

2216 

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 

2227 

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)] 

2238 

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 = [] 

2246 

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*. 

2255 

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. 

2259 

2260 Returns the identifier of the callback. to Example:: 

2261 

2262 >>> term = Terminal() 

2263 >>> def somefunc(): pass 

2264 >>> id = "myref" 

2265 >>> ref = term.add_callback(term.CALLBACK_BELL, somefunc, id) 

2266 

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 

2273 

2274 def remove_callback(self, event, identifier): 

2275 """ 

2276 Removes the callback referenced by *identifier* that is attached to the 

2277 given *event*. Example:: 

2278 

2279 >>> term.remove_callback(CALLBACK_BELL, "myref") 

2280 """ 

2281 del self.callbacks[event][identifier] 

2282 

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 

2292 

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 

2303 

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 

2314 

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 

2325 

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. 

2330 

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 

2372 

2373 def __ignore(self, *args, **kwargs): 

2374 """ 

2375 Does nothing (on purpose!). Used as a placeholder for unimplemented 

2376 functions. 

2377 """ 

2378 pass 

2379 

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 

2417 

2418 # Fix the cursor location: 

2419 if self.cursorX >= self.cols: 

2420 self.cursorX = self.cols - 1 

2421 self.rendition_set = False 

2422 

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>'. 

2427 

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 

2443 

2444 def get_cursor_position(self): 

2445 """ 

2446 Returns the current cursor positition as a tuple:: 

2447 

2448 (row, col) 

2449 """ 

2450 return (self.cursorY, self.cursorX) 

2451 

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) 

2465 

2466 def get_title(self): 

2467 """Returns :attr:`self.title`""" 

2468 return self.title 

2469 

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` 

2477 

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 

2489 

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 

2500 

2501 def _dsr_get_cursor_position(self): 

2502 """ 

2503 Returns the current cursor positition as a DSR response in the form of:: 

2504 

2505 '\x1b<self.cursorY>;<self.cursorX>R' 

2506 

2507 Also executes CALLBACK_DSR with the same output as the first argument. 

2508 Example:: 

2509 

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 

2519 

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) 

2528 

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). 

2534 

2535 .. note:: 

2536 

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 

2550 

2551 def set_G0_charset(self, char): 

2552 """ 

2553 Sets the terminal's G0 (default) charset to the type specified by 

2554 *char*. 

2555 

2556 Here's the possibilities:: 

2557 

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 

2582 

2583 def set_G1_charset(self, char): 

2584 """ 

2585 Sets the terminal's G1 (alt) charset to the type specified by *char*. 

2586 

2587 Here's the possibilities:: 

2588 

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 

2613 

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 

2623 

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 

2633 

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.')) 

2644 

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. 

2650 

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! 

2656 

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() 

2911 

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 

2918 

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. 

2924 

2925 .. note:: 

2926 

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 

2959 

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 

2982 

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 

2989 

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 

3007 

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) 

3028 

3029 def backspace(self): 

3030 """Execute a backspace (\\x08)""" 

3031 self.cursor_left(1) 

3032 

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 

3041 

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 

3049 

3050 def linefeed(self): 

3051 """ 

3052 LF - Executes a line feed. 

3053 

3054 .. note:: This actually just calls :meth:`Terminal.newline`. 

3055 """ 

3056 self.newline() 

3057 

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. 

3062 

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 

3068 

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 

3080 

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. 

3106 

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 

3127 

3128 def _xon(self): 

3129 """ 

3130 Handles the XON character (stop ignoring). 

3131 

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 

3136 

3137 def _xoff(self): 

3138 """ 

3139 Handles the XOFF character (start ignoring) 

3140 

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 

3145 

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 = '' 

3152 

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 (?). 

3157 

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('?') 

3162 

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' 

3176 

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[' 

3184 

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 

3202 

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 

3224 

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.') 

3241 

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] 

3266 

3267 def _string_terminator(self): 

3268 """ 

3269 Handle the string terminator (ST). 

3270 

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 

3275 

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 = '' 

3303 

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() 

3328 

3329 def _device_status_report(self, n=None): 

3330 """ 

3331 Returns '\\\\x1b[0n' (terminal OK) and executes: 

3332 

3333 .. code-block:: python 

3334 

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 

3345 

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: 

3351 

3352 .. code-block:: python 

3353 

3354 self.callbacks[self.CALLBACK_DSR]("\\x1b[1;2c") 

3355 

3356 If we're responding to ^[>c or ^[>0c, executes: 

3357 

3358 .. code-block:: python 

3359 

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 

3373 

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*. 

3378 

3379 .. code-block:: python 

3380 

3381 self.callbacks[self.CALLBACK_DSR](response) 

3382 

3383 Supported requests and their responses: 

3384 

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 

3420 

3421 def set_expanded_mode(self, setting): 

3422 """ 

3423 Accepts "standard mode" settings. Typically '\\\\x1b[?25h' to hide cursor. 

3424 

3425 Notes on modes:: 

3426 

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 

3471 

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 

3500 

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`. 

3506 

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) 

3527 

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) 

3541 

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:: 

3547 

3548 >>> self.expanded_mode_handlers['1000'] = partial(self.expanded_mode_toggle, 'mouse_button_events') 

3549 """ 

3550 self.expanded_modes[mode] = boolean 

3551 

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' ') 

3562 

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. 

3571 

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 

3589 

3590 def _erase_characters(self, n=1): 

3591 """ 

3592 Erases (to the right) the specified number of characters at the cursor 

3593 position. 

3594 

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) 

3607 

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 

3630 

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 

3654 

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 

3667 

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 

3680 

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 

3694 

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 

3708 

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 

3720 

3721 def cursor_position(self, coordinates): 

3722 """ 

3723 ESCnH CUP (Cursor Position). Move the cursor to the given coordinates. 

3724 

3725 :coordinates: Should be something like, 'row;col' (1-based) but, 'row', 'row;', and ';col' are also valid (assumes 1 on missing value). 

3726 

3727 .. note:: 

3728 

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 

3771 

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 

3778 

3779 def clear_screen(self): 

3780 """ 

3781 Clears the screen. Also used to emulate a terminal reset. 

3782 

3783 .. note:: 

3784 

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 

3794 

3795 def clear_screen_from_cursor_down(self): 

3796 """ 

3797 Clears the screen from the cursor down (ESC[J or ESC[0J). 

3798 

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 ] 

3813 

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 

3827 

3828 def clear_screen_from_cursor(self, n): 

3829 """ 

3830 CSI *n* J ED (Erase Data). This escape sequence uses the following rules: 

3831 

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 

3864 

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 

3878 

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 

3891 

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 

3901 

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: 

3906 

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 

3940 

3941 def set_led_state(self, n): 

3942 """ 

3943 Sets the values the dict, self.leds depending on *n* using the following 

3944 rules: 

3945 

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 ====== ====================== ====== 

3953 

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 

3972 

3973 def _set_rendition(self, n): 

3974 """ 

3975 Sets :attr:`self.renditions[self.cursorY][self.cursorX]` equal to 

3976 *n.split(';')*. 

3977 

3978 *n* is expected to be a string of ECMA-48 rendition numbers separated by 

3979 semicolons. Example:: 

3980 

3981 '0;1;31' 

3982 

3983 ...will result in:: 

3984 

3985 [0, 1, 31] 

3986 

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 

4068 

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:: 

4073 

4074 self.callbacks[CALLBACK_OPT](chars) 

4075 

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. 

4080 

4081 Applications can then do what they wish with *chars*. 

4082 

4083 .. note:: 

4084 

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 

4096 

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] 

4106 

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 = {"&": "&amp;", '<': '&lt;', '>': '&gt;'} 

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 

4157 

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 = {"&": "&amp;", '<': '&lt;', '>': '&gt;'} 

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 

4218 

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 = {"&": "&amp;", '<': '&lt;', '>': '&gt;'} 

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 

4341 

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 = {"&": "&amp;", '<': '&lt;', '>': '&gt;'} 

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 

4440 

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. 

4448 

4449 .. note:: 

4450 

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) 

4481 

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. 

4490 

4491 .. note:: 

4492 

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) 

4512 

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) 

4524 

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 

4529 

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) 

4542 

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). 

4548 

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 

4562 

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:: 

4571 

4572 ${selector} span.f0 { color: #5C5C5C; } 

4573 

4574 Example:: 

4575 

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)