Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

helper.py 16KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. # -*- test-case-name: twisted.conch.test.test_helper -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Partial in-memory terminal emulator
  6. @author: Jp Calderone
  7. """
  8. import re
  9. import string
  10. from zope.interface import implementer
  11. from incremental import Version
  12. from twisted.conch.insults import insults
  13. from twisted.internet import defer, protocol, reactor
  14. from twisted.logger import Logger
  15. from twisted.python import _textattributes
  16. from twisted.python.compat import iterbytes
  17. from twisted.python.deprecate import deprecated, deprecatedModuleAttribute
  18. FOREGROUND = 30
  19. BACKGROUND = 40
  20. BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = range(9)
  21. class _FormattingState(_textattributes._FormattingStateMixin):
  22. """
  23. Represents the formatting state/attributes of a single character.
  24. Character set, intensity, underlinedness, blinkitude, video
  25. reversal, as well as foreground and background colors made up a
  26. character's attributes.
  27. """
  28. compareAttributes = (
  29. "charset",
  30. "bold",
  31. "underline",
  32. "blink",
  33. "reverseVideo",
  34. "foreground",
  35. "background",
  36. "_subtracting",
  37. )
  38. def __init__(
  39. self,
  40. charset=insults.G0,
  41. bold=False,
  42. underline=False,
  43. blink=False,
  44. reverseVideo=False,
  45. foreground=WHITE,
  46. background=BLACK,
  47. _subtracting=False,
  48. ):
  49. self.charset = charset
  50. self.bold = bold
  51. self.underline = underline
  52. self.blink = blink
  53. self.reverseVideo = reverseVideo
  54. self.foreground = foreground
  55. self.background = background
  56. self._subtracting = _subtracting
  57. @deprecated(Version("Twisted", 13, 1, 0))
  58. def wantOne(self, **kw):
  59. """
  60. Add a character attribute to a copy of this formatting state.
  61. @param kw: An optional attribute name and value can be provided with
  62. a keyword argument.
  63. @return: A formatting state instance with the new attribute.
  64. @see: L{DefaultFormattingState._withAttribute}.
  65. """
  66. k, v = kw.popitem()
  67. return self._withAttribute(k, v)
  68. def toVT102(self):
  69. # Spit out a vt102 control sequence that will set up
  70. # all the attributes set here. Except charset.
  71. attrs = []
  72. if self._subtracting:
  73. attrs.append(0)
  74. if self.bold:
  75. attrs.append(insults.BOLD)
  76. if self.underline:
  77. attrs.append(insults.UNDERLINE)
  78. if self.blink:
  79. attrs.append(insults.BLINK)
  80. if self.reverseVideo:
  81. attrs.append(insults.REVERSE_VIDEO)
  82. if self.foreground != WHITE:
  83. attrs.append(FOREGROUND + self.foreground)
  84. if self.background != BLACK:
  85. attrs.append(BACKGROUND + self.background)
  86. if attrs:
  87. return "\x1b[" + ";".join(map(str, attrs)) + "m"
  88. return ""
  89. CharacterAttribute = _FormattingState
  90. deprecatedModuleAttribute(
  91. Version("Twisted", 13, 1, 0),
  92. "Use twisted.conch.insults.text.assembleFormattedText instead.",
  93. "twisted.conch.insults.helper",
  94. "CharacterAttribute",
  95. )
  96. # XXX - need to support scroll regions and scroll history
  97. @implementer(insults.ITerminalTransport)
  98. class TerminalBuffer(protocol.Protocol):
  99. """
  100. An in-memory terminal emulator.
  101. """
  102. for keyID in (
  103. b"UP_ARROW",
  104. b"DOWN_ARROW",
  105. b"RIGHT_ARROW",
  106. b"LEFT_ARROW",
  107. b"HOME",
  108. b"INSERT",
  109. b"DELETE",
  110. b"END",
  111. b"PGUP",
  112. b"PGDN",
  113. b"F1",
  114. b"F2",
  115. b"F3",
  116. b"F4",
  117. b"F5",
  118. b"F6",
  119. b"F7",
  120. b"F8",
  121. b"F9",
  122. b"F10",
  123. b"F11",
  124. b"F12",
  125. ):
  126. execBytes = keyID + b" = object()"
  127. execStr = execBytes.decode("ascii")
  128. exec(execStr)
  129. TAB = b"\t"
  130. BACKSPACE = b"\x7f"
  131. width = 80
  132. height = 24
  133. fill = b" "
  134. void = object()
  135. _log = Logger()
  136. def getCharacter(self, x, y):
  137. return self.lines[y][x]
  138. def connectionMade(self):
  139. self.reset()
  140. def write(self, data):
  141. """
  142. Add the given printable bytes to the terminal.
  143. Line feeds in L{bytes} will be replaced with carriage return / line
  144. feed pairs.
  145. """
  146. for b in iterbytes(data.replace(b"\n", b"\r\n")):
  147. self.insertAtCursor(b)
  148. def _currentFormattingState(self):
  149. return _FormattingState(self.activeCharset, **self.graphicRendition)
  150. def insertAtCursor(self, b):
  151. """
  152. Add one byte to the terminal at the cursor and make consequent state
  153. updates.
  154. If b is a carriage return, move the cursor to the beginning of the
  155. current row.
  156. If b is a line feed, move the cursor to the next row or scroll down if
  157. the cursor is already in the last row.
  158. Otherwise, if b is printable, put it at the cursor position (inserting
  159. or overwriting as dictated by the current mode) and move the cursor.
  160. """
  161. if b == b"\r":
  162. self.x = 0
  163. elif b == b"\n":
  164. self._scrollDown()
  165. elif b in string.printable.encode("ascii"):
  166. if self.x >= self.width:
  167. self.nextLine()
  168. ch = (b, self._currentFormattingState())
  169. if self.modes.get(insults.modes.IRM):
  170. self.lines[self.y][self.x : self.x] = [ch]
  171. self.lines[self.y].pop()
  172. else:
  173. self.lines[self.y][self.x] = ch
  174. self.x += 1
  175. def _emptyLine(self, width):
  176. return [(self.void, self._currentFormattingState()) for i in range(width)]
  177. def _scrollDown(self):
  178. self.y += 1
  179. if self.y >= self.height:
  180. self.y -= 1
  181. del self.lines[0]
  182. self.lines.append(self._emptyLine(self.width))
  183. def _scrollUp(self):
  184. self.y -= 1
  185. if self.y < 0:
  186. self.y = 0
  187. del self.lines[-1]
  188. self.lines.insert(0, self._emptyLine(self.width))
  189. def cursorUp(self, n=1):
  190. self.y = max(0, self.y - n)
  191. def cursorDown(self, n=1):
  192. self.y = min(self.height - 1, self.y + n)
  193. def cursorBackward(self, n=1):
  194. self.x = max(0, self.x - n)
  195. def cursorForward(self, n=1):
  196. self.x = min(self.width, self.x + n)
  197. def cursorPosition(self, column, line):
  198. self.x = column
  199. self.y = line
  200. def cursorHome(self):
  201. self.x = self.home.x
  202. self.y = self.home.y
  203. def index(self):
  204. self._scrollDown()
  205. def reverseIndex(self):
  206. self._scrollUp()
  207. def nextLine(self):
  208. """
  209. Update the cursor position attributes and scroll down if appropriate.
  210. """
  211. self.x = 0
  212. self._scrollDown()
  213. def saveCursor(self):
  214. self._savedCursor = (self.x, self.y)
  215. def restoreCursor(self):
  216. self.x, self.y = self._savedCursor
  217. del self._savedCursor
  218. def setModes(self, modes):
  219. for m in modes:
  220. self.modes[m] = True
  221. def resetModes(self, modes):
  222. for m in modes:
  223. try:
  224. del self.modes[m]
  225. except KeyError:
  226. pass
  227. def setPrivateModes(self, modes):
  228. """
  229. Enable the given modes.
  230. Track which modes have been enabled so that the implementations of
  231. other L{insults.ITerminalTransport} methods can be properly implemented
  232. to respect these settings.
  233. @see: L{resetPrivateModes}
  234. @see: L{insults.ITerminalTransport.setPrivateModes}
  235. """
  236. for m in modes:
  237. self.privateModes[m] = True
  238. def resetPrivateModes(self, modes):
  239. """
  240. Disable the given modes.
  241. @see: L{setPrivateModes}
  242. @see: L{insults.ITerminalTransport.resetPrivateModes}
  243. """
  244. for m in modes:
  245. try:
  246. del self.privateModes[m]
  247. except KeyError:
  248. pass
  249. def applicationKeypadMode(self):
  250. self.keypadMode = "app"
  251. def numericKeypadMode(self):
  252. self.keypadMode = "num"
  253. def selectCharacterSet(self, charSet, which):
  254. self.charsets[which] = charSet
  255. def shiftIn(self):
  256. self.activeCharset = insults.G0
  257. def shiftOut(self):
  258. self.activeCharset = insults.G1
  259. def singleShift2(self):
  260. oldActiveCharset = self.activeCharset
  261. self.activeCharset = insults.G2
  262. f = self.insertAtCursor
  263. def insertAtCursor(b):
  264. f(b)
  265. del self.insertAtCursor
  266. self.activeCharset = oldActiveCharset
  267. self.insertAtCursor = insertAtCursor
  268. def singleShift3(self):
  269. oldActiveCharset = self.activeCharset
  270. self.activeCharset = insults.G3
  271. f = self.insertAtCursor
  272. def insertAtCursor(b):
  273. f(b)
  274. del self.insertAtCursor
  275. self.activeCharset = oldActiveCharset
  276. self.insertAtCursor = insertAtCursor
  277. def selectGraphicRendition(self, *attributes):
  278. for a in attributes:
  279. if a == insults.NORMAL:
  280. self.graphicRendition = {
  281. "bold": False,
  282. "underline": False,
  283. "blink": False,
  284. "reverseVideo": False,
  285. "foreground": WHITE,
  286. "background": BLACK,
  287. }
  288. elif a == insults.BOLD:
  289. self.graphicRendition["bold"] = True
  290. elif a == insults.UNDERLINE:
  291. self.graphicRendition["underline"] = True
  292. elif a == insults.BLINK:
  293. self.graphicRendition["blink"] = True
  294. elif a == insults.REVERSE_VIDEO:
  295. self.graphicRendition["reverseVideo"] = True
  296. else:
  297. try:
  298. v = int(a)
  299. except ValueError:
  300. self._log.error(
  301. "Unknown graphic rendition attribute: {attr!r}", attr=a
  302. )
  303. else:
  304. if FOREGROUND <= v <= FOREGROUND + N_COLORS:
  305. self.graphicRendition["foreground"] = v - FOREGROUND
  306. elif BACKGROUND <= v <= BACKGROUND + N_COLORS:
  307. self.graphicRendition["background"] = v - BACKGROUND
  308. else:
  309. self._log.error(
  310. "Unknown graphic rendition attribute: {attr!r}", attr=a
  311. )
  312. def eraseLine(self):
  313. self.lines[self.y] = self._emptyLine(self.width)
  314. def eraseToLineEnd(self):
  315. width = self.width - self.x
  316. self.lines[self.y][self.x :] = self._emptyLine(width)
  317. def eraseToLineBeginning(self):
  318. self.lines[self.y][: self.x + 1] = self._emptyLine(self.x + 1)
  319. def eraseDisplay(self):
  320. self.lines = [self._emptyLine(self.width) for i in range(self.height)]
  321. def eraseToDisplayEnd(self):
  322. self.eraseToLineEnd()
  323. height = self.height - self.y - 1
  324. self.lines[self.y + 1 :] = [self._emptyLine(self.width) for i in range(height)]
  325. def eraseToDisplayBeginning(self):
  326. self.eraseToLineBeginning()
  327. self.lines[: self.y] = [self._emptyLine(self.width) for i in range(self.y)]
  328. def deleteCharacter(self, n=1):
  329. del self.lines[self.y][self.x : self.x + n]
  330. self.lines[self.y].extend(self._emptyLine(min(self.width - self.x, n)))
  331. def insertLine(self, n=1):
  332. self.lines[self.y : self.y] = [self._emptyLine(self.width) for i in range(n)]
  333. del self.lines[self.height :]
  334. def deleteLine(self, n=1):
  335. del self.lines[self.y : self.y + n]
  336. self.lines.extend([self._emptyLine(self.width) for i in range(n)])
  337. def reportCursorPosition(self):
  338. return (self.x, self.y)
  339. def reset(self):
  340. self.home = insults.Vector(0, 0)
  341. self.x = self.y = 0
  342. self.modes = {}
  343. self.privateModes = {}
  344. self.setPrivateModes(
  345. [insults.privateModes.AUTO_WRAP, insults.privateModes.CURSOR_MODE]
  346. )
  347. self.numericKeypad = "app"
  348. self.activeCharset = insults.G0
  349. self.graphicRendition = {
  350. "bold": False,
  351. "underline": False,
  352. "blink": False,
  353. "reverseVideo": False,
  354. "foreground": WHITE,
  355. "background": BLACK,
  356. }
  357. self.charsets = {
  358. insults.G0: insults.CS_US,
  359. insults.G1: insults.CS_US,
  360. insults.G2: insults.CS_ALTERNATE,
  361. insults.G3: insults.CS_ALTERNATE_SPECIAL,
  362. }
  363. self.eraseDisplay()
  364. def unhandledControlSequence(self, buf):
  365. print("Could not handle", repr(buf))
  366. def __bytes__(self):
  367. lines = []
  368. for L in self.lines:
  369. buf = []
  370. length = 0
  371. for (ch, attr) in L:
  372. if ch is not self.void:
  373. buf.append(ch)
  374. length = len(buf)
  375. else:
  376. buf.append(self.fill)
  377. lines.append(b"".join(buf[:length]))
  378. return b"\n".join(lines)
  379. def getHost(self):
  380. # ITransport.getHost
  381. raise NotImplementedError("Unimplemented: TerminalBuffer.getHost")
  382. def getPeer(self):
  383. # ITransport.getPeer
  384. raise NotImplementedError("Unimplemented: TerminalBuffer.getPeer")
  385. def loseConnection(self):
  386. # ITransport.loseConnection
  387. raise NotImplementedError("Unimplemented: TerminalBuffer.loseConnection")
  388. def writeSequence(self, data):
  389. # ITransport.writeSequence
  390. raise NotImplementedError("Unimplemented: TerminalBuffer.writeSequence")
  391. def horizontalTabulationSet(self):
  392. # ITerminalTransport.horizontalTabulationSet
  393. raise NotImplementedError(
  394. "Unimplemented: TerminalBuffer.horizontalTabulationSet"
  395. )
  396. def tabulationClear(self):
  397. # TerminalTransport.tabulationClear
  398. raise NotImplementedError("Unimplemented: TerminalBuffer.tabulationClear")
  399. def tabulationClearAll(self):
  400. # TerminalTransport.tabulationClearAll
  401. raise NotImplementedError("Unimplemented: TerminalBuffer.tabulationClearAll")
  402. def doubleHeightLine(self, top=True):
  403. # ITerminalTransport.doubleHeightLine
  404. raise NotImplementedError("Unimplemented: TerminalBuffer.doubleHeightLine")
  405. def singleWidthLine(self):
  406. # ITerminalTransport.singleWidthLine
  407. raise NotImplementedError("Unimplemented: TerminalBuffer.singleWidthLine")
  408. def doubleWidthLine(self):
  409. # ITerminalTransport.doubleWidthLine
  410. raise NotImplementedError("Unimplemented: TerminalBuffer.doubleWidthLine")
  411. class ExpectationTimeout(Exception):
  412. pass
  413. class ExpectableBuffer(TerminalBuffer):
  414. _mark = 0
  415. def connectionMade(self):
  416. TerminalBuffer.connectionMade(self)
  417. self._expecting = []
  418. def write(self, data):
  419. TerminalBuffer.write(self, data)
  420. self._checkExpected()
  421. def cursorHome(self):
  422. TerminalBuffer.cursorHome(self)
  423. self._mark = 0
  424. def _timeoutExpected(self, d):
  425. d.errback(ExpectationTimeout())
  426. self._checkExpected()
  427. def _checkExpected(self):
  428. s = self.__bytes__()[self._mark :]
  429. while self._expecting:
  430. expr, timer, deferred = self._expecting[0]
  431. if timer and not timer.active():
  432. del self._expecting[0]
  433. continue
  434. for match in expr.finditer(s):
  435. if timer:
  436. timer.cancel()
  437. del self._expecting[0]
  438. self._mark += match.end()
  439. s = s[match.end() :]
  440. deferred.callback(match)
  441. break
  442. else:
  443. return
  444. def expect(self, expression, timeout=None, scheduler=reactor):
  445. d = defer.Deferred()
  446. timer = None
  447. if timeout:
  448. timer = scheduler.callLater(timeout, self._timeoutExpected, d)
  449. self._expecting.append((re.compile(expression), timer, d))
  450. self._checkExpected()
  451. return d
  452. __all__ = ["CharacterAttribute", "TerminalBuffer", "ExpectableBuffer"]