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.

recvline.py 19KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. # -*- test-case-name: twisted.conch.test.test_recvline -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Basic line editing support.
  6. @author: Jp Calderone
  7. """
  8. import string
  9. from typing import Dict
  10. from zope.interface import implementer
  11. from twisted.conch.insults import helper, insults
  12. from twisted.logger import Logger
  13. from twisted.python import reflect
  14. from twisted.python.compat import iterbytes
  15. _counters: Dict[str, int] = {}
  16. class Logging:
  17. """
  18. Wrapper which logs attribute lookups.
  19. This was useful in debugging something, I guess. I forget what.
  20. It can probably be deleted or moved somewhere more appropriate.
  21. Nothing special going on here, really.
  22. """
  23. def __init__(self, original):
  24. self.original = original
  25. key = reflect.qual(original.__class__)
  26. count = _counters.get(key, 0)
  27. _counters[key] = count + 1
  28. self._logFile = open(key + "-" + str(count), "w")
  29. def __str__(self) -> str:
  30. return str(super().__getattribute__("original"))
  31. def __repr__(self) -> str:
  32. return repr(super().__getattribute__("original"))
  33. def __getattribute__(self, name):
  34. original = super().__getattribute__("original")
  35. logFile = super().__getattribute__("_logFile")
  36. logFile.write(name + "\n")
  37. return getattr(original, name)
  38. @implementer(insults.ITerminalTransport)
  39. class TransportSequence:
  40. """
  41. An L{ITerminalTransport} implementation which forwards calls to
  42. one or more other L{ITerminalTransport}s.
  43. This is a cheap way for servers to keep track of the state they
  44. expect the client to see, since all terminal manipulations can be
  45. send to the real client and to a terminal emulator that lives in
  46. the server process.
  47. """
  48. for keyID in (
  49. b"UP_ARROW",
  50. b"DOWN_ARROW",
  51. b"RIGHT_ARROW",
  52. b"LEFT_ARROW",
  53. b"HOME",
  54. b"INSERT",
  55. b"DELETE",
  56. b"END",
  57. b"PGUP",
  58. b"PGDN",
  59. b"F1",
  60. b"F2",
  61. b"F3",
  62. b"F4",
  63. b"F5",
  64. b"F6",
  65. b"F7",
  66. b"F8",
  67. b"F9",
  68. b"F10",
  69. b"F11",
  70. b"F12",
  71. ):
  72. execBytes = keyID + b" = object()"
  73. execStr = execBytes.decode("ascii")
  74. exec(execStr)
  75. TAB = b"\t"
  76. BACKSPACE = b"\x7f"
  77. def __init__(self, *transports):
  78. assert transports, "Cannot construct a TransportSequence with no transports"
  79. self.transports = transports
  80. for method in insults.ITerminalTransport:
  81. exec(
  82. """\
  83. def %s(self, *a, **kw):
  84. for tpt in self.transports:
  85. result = tpt.%s(*a, **kw)
  86. return result
  87. """
  88. % (method, method)
  89. )
  90. def getHost(self):
  91. # ITransport.getHost
  92. raise NotImplementedError("Unimplemented: TransportSequence.getHost")
  93. def getPeer(self):
  94. # ITransport.getPeer
  95. raise NotImplementedError("Unimplemented: TransportSequence.getPeer")
  96. def loseConnection(self):
  97. # ITransport.loseConnection
  98. raise NotImplementedError("Unimplemented: TransportSequence.loseConnection")
  99. def write(self, data):
  100. # ITransport.write
  101. raise NotImplementedError("Unimplemented: TransportSequence.write")
  102. def writeSequence(self, data):
  103. # ITransport.writeSequence
  104. raise NotImplementedError("Unimplemented: TransportSequence.writeSequence")
  105. def cursorUp(self, n=1):
  106. # ITerminalTransport.cursorUp
  107. raise NotImplementedError("Unimplemented: TransportSequence.cursorUp")
  108. def cursorDown(self, n=1):
  109. # ITerminalTransport.cursorDown
  110. raise NotImplementedError("Unimplemented: TransportSequence.cursorDown")
  111. def cursorForward(self, n=1):
  112. # ITerminalTransport.cursorForward
  113. raise NotImplementedError("Unimplemented: TransportSequence.cursorForward")
  114. def cursorBackward(self, n=1):
  115. # ITerminalTransport.cursorBackward
  116. raise NotImplementedError("Unimplemented: TransportSequence.cursorBackward")
  117. def cursorPosition(self, column, line):
  118. # ITerminalTransport.cursorPosition
  119. raise NotImplementedError("Unimplemented: TransportSequence.cursorPosition")
  120. def cursorHome(self):
  121. # ITerminalTransport.cursorHome
  122. raise NotImplementedError("Unimplemented: TransportSequence.cursorHome")
  123. def index(self):
  124. # ITerminalTransport.index
  125. raise NotImplementedError("Unimplemented: TransportSequence.index")
  126. def reverseIndex(self):
  127. # ITerminalTransport.reverseIndex
  128. raise NotImplementedError("Unimplemented: TransportSequence.reverseIndex")
  129. def nextLine(self):
  130. # ITerminalTransport.nextLine
  131. raise NotImplementedError("Unimplemented: TransportSequence.nextLine")
  132. def saveCursor(self):
  133. # ITerminalTransport.saveCursor
  134. raise NotImplementedError("Unimplemented: TransportSequence.saveCursor")
  135. def restoreCursor(self):
  136. # ITerminalTransport.restoreCursor
  137. raise NotImplementedError("Unimplemented: TransportSequence.restoreCursor")
  138. def setModes(self, modes):
  139. # ITerminalTransport.setModes
  140. raise NotImplementedError("Unimplemented: TransportSequence.setModes")
  141. def resetModes(self, mode):
  142. # ITerminalTransport.resetModes
  143. raise NotImplementedError("Unimplemented: TransportSequence.resetModes")
  144. def setPrivateModes(self, modes):
  145. # ITerminalTransport.setPrivateModes
  146. raise NotImplementedError("Unimplemented: TransportSequence.setPrivateModes")
  147. def resetPrivateModes(self, modes):
  148. # ITerminalTransport.resetPrivateModes
  149. raise NotImplementedError("Unimplemented: TransportSequence.resetPrivateModes")
  150. def applicationKeypadMode(self):
  151. # ITerminalTransport.applicationKeypadMode
  152. raise NotImplementedError(
  153. "Unimplemented: TransportSequence.applicationKeypadMode"
  154. )
  155. def numericKeypadMode(self):
  156. # ITerminalTransport.numericKeypadMode
  157. raise NotImplementedError("Unimplemented: TransportSequence.numericKeypadMode")
  158. def selectCharacterSet(self, charSet, which):
  159. # ITerminalTransport.selectCharacterSet
  160. raise NotImplementedError("Unimplemented: TransportSequence.selectCharacterSet")
  161. def shiftIn(self):
  162. # ITerminalTransport.shiftIn
  163. raise NotImplementedError("Unimplemented: TransportSequence.shiftIn")
  164. def shiftOut(self):
  165. # ITerminalTransport.shiftOut
  166. raise NotImplementedError("Unimplemented: TransportSequence.shiftOut")
  167. def singleShift2(self):
  168. # ITerminalTransport.singleShift2
  169. raise NotImplementedError("Unimplemented: TransportSequence.singleShift2")
  170. def singleShift3(self):
  171. # ITerminalTransport.singleShift3
  172. raise NotImplementedError("Unimplemented: TransportSequence.singleShift3")
  173. def selectGraphicRendition(self, *attributes):
  174. # ITerminalTransport.selectGraphicRendition
  175. raise NotImplementedError(
  176. "Unimplemented: TransportSequence.selectGraphicRendition"
  177. )
  178. def horizontalTabulationSet(self):
  179. # ITerminalTransport.horizontalTabulationSet
  180. raise NotImplementedError(
  181. "Unimplemented: TransportSequence.horizontalTabulationSet"
  182. )
  183. def tabulationClear(self):
  184. # ITerminalTransport.tabulationClear
  185. raise NotImplementedError("Unimplemented: TransportSequence.tabulationClear")
  186. def tabulationClearAll(self):
  187. # ITerminalTransport.tabulationClearAll
  188. raise NotImplementedError("Unimplemented: TransportSequence.tabulationClearAll")
  189. def doubleHeightLine(self, top=True):
  190. # ITerminalTransport.doubleHeightLine
  191. raise NotImplementedError("Unimplemented: TransportSequence.doubleHeightLine")
  192. def singleWidthLine(self):
  193. # ITerminalTransport.singleWidthLine
  194. raise NotImplementedError("Unimplemented: TransportSequence.singleWidthLine")
  195. def doubleWidthLine(self):
  196. # ITerminalTransport.doubleWidthLine
  197. raise NotImplementedError("Unimplemented: TransportSequence.doubleWidthLine")
  198. def eraseToLineEnd(self):
  199. # ITerminalTransport.eraseToLineEnd
  200. raise NotImplementedError("Unimplemented: TransportSequence.eraseToLineEnd")
  201. def eraseToLineBeginning(self):
  202. # ITerminalTransport.eraseToLineBeginning
  203. raise NotImplementedError(
  204. "Unimplemented: TransportSequence.eraseToLineBeginning"
  205. )
  206. def eraseLine(self):
  207. # ITerminalTransport.eraseLine
  208. raise NotImplementedError("Unimplemented: TransportSequence.eraseLine")
  209. def eraseToDisplayEnd(self):
  210. # ITerminalTransport.eraseToDisplayEnd
  211. raise NotImplementedError("Unimplemented: TransportSequence.eraseToDisplayEnd")
  212. def eraseToDisplayBeginning(self):
  213. # ITerminalTransport.eraseToDisplayBeginning
  214. raise NotImplementedError(
  215. "Unimplemented: TransportSequence.eraseToDisplayBeginning"
  216. )
  217. def eraseDisplay(self):
  218. # ITerminalTransport.eraseDisplay
  219. raise NotImplementedError("Unimplemented: TransportSequence.eraseDisplay")
  220. def deleteCharacter(self, n=1):
  221. # ITerminalTransport.deleteCharacter
  222. raise NotImplementedError("Unimplemented: TransportSequence.deleteCharacter")
  223. def insertLine(self, n=1):
  224. # ITerminalTransport.insertLine
  225. raise NotImplementedError("Unimplemented: TransportSequence.insertLine")
  226. def deleteLine(self, n=1):
  227. # ITerminalTransport.deleteLine
  228. raise NotImplementedError("Unimplemented: TransportSequence.deleteLine")
  229. def reportCursorPosition(self):
  230. # ITerminalTransport.reportCursorPosition
  231. raise NotImplementedError(
  232. "Unimplemented: TransportSequence.reportCursorPosition"
  233. )
  234. def reset(self):
  235. # ITerminalTransport.reset
  236. raise NotImplementedError("Unimplemented: TransportSequence.reset")
  237. def unhandledControlSequence(self, seq):
  238. # ITerminalTransport.unhandledControlSequence
  239. raise NotImplementedError(
  240. "Unimplemented: TransportSequence.unhandledControlSequence"
  241. )
  242. class LocalTerminalBufferMixin:
  243. """
  244. A mixin for RecvLine subclasses which records the state of the terminal.
  245. This is accomplished by performing all L{ITerminalTransport} operations on both
  246. the transport passed to makeConnection and an instance of helper.TerminalBuffer.
  247. @ivar terminalCopy: A L{helper.TerminalBuffer} instance which efforts
  248. will be made to keep up to date with the actual terminal
  249. associated with this protocol instance.
  250. """
  251. def makeConnection(self, transport):
  252. self.terminalCopy = helper.TerminalBuffer()
  253. self.terminalCopy.connectionMade()
  254. return super().makeConnection(TransportSequence(transport, self.terminalCopy))
  255. def __str__(self) -> str:
  256. return str(self.terminalCopy)
  257. class RecvLine(insults.TerminalProtocol):
  258. """
  259. L{TerminalProtocol} which adds line editing features.
  260. Clients will be prompted for lines of input with all the usual
  261. features: character echoing, left and right arrow support for
  262. moving the cursor to different areas of the line buffer, backspace
  263. and delete for removing characters, and insert for toggling
  264. between typeover and insert mode. Tabs will be expanded to enough
  265. spaces to move the cursor to the next tabstop (every four
  266. characters by default). Enter causes the line buffer to be
  267. cleared and the line to be passed to the lineReceived() method
  268. which, by default, does nothing. Subclasses are responsible for
  269. redrawing the input prompt (this will probably change).
  270. """
  271. width = 80
  272. height = 24
  273. TABSTOP = 4
  274. ps = (b">>> ", b"... ")
  275. pn = 0
  276. _printableChars = string.printable.encode("ascii")
  277. _log = Logger()
  278. def connectionMade(self):
  279. # A list containing the characters making up the current line
  280. self.lineBuffer = []
  281. # A zero-based (wtf else?) index into self.lineBuffer.
  282. # Indicates the current cursor position.
  283. self.lineBufferIndex = 0
  284. t = self.terminal
  285. # A map of keyIDs to bound instance methods.
  286. self.keyHandlers = {
  287. t.LEFT_ARROW: self.handle_LEFT,
  288. t.RIGHT_ARROW: self.handle_RIGHT,
  289. t.TAB: self.handle_TAB,
  290. # Both of these should not be necessary, but figuring out
  291. # which is necessary is a huge hassle.
  292. b"\r": self.handle_RETURN,
  293. b"\n": self.handle_RETURN,
  294. t.BACKSPACE: self.handle_BACKSPACE,
  295. t.DELETE: self.handle_DELETE,
  296. t.INSERT: self.handle_INSERT,
  297. t.HOME: self.handle_HOME,
  298. t.END: self.handle_END,
  299. }
  300. self.initializeScreen()
  301. def initializeScreen(self):
  302. # Hmm, state sucks. Oh well.
  303. # For now we will just take over the whole terminal.
  304. self.terminal.reset()
  305. self.terminal.write(self.ps[self.pn])
  306. # XXX Note: I would prefer to default to starting in insert
  307. # mode, however this does not seem to actually work! I do not
  308. # know why. This is probably of interest to implementors
  309. # subclassing RecvLine.
  310. # XXX XXX Note: But the unit tests all expect the initial mode
  311. # to be insert right now. Fuck, there needs to be a way to
  312. # query the current mode or something.
  313. # self.setTypeoverMode()
  314. self.setInsertMode()
  315. def currentLineBuffer(self):
  316. s = b"".join(self.lineBuffer)
  317. return s[: self.lineBufferIndex], s[self.lineBufferIndex :]
  318. def setInsertMode(self):
  319. self.mode = "insert"
  320. self.terminal.setModes([insults.modes.IRM])
  321. def setTypeoverMode(self):
  322. self.mode = "typeover"
  323. self.terminal.resetModes([insults.modes.IRM])
  324. def drawInputLine(self):
  325. """
  326. Write a line containing the current input prompt and the current line
  327. buffer at the current cursor position.
  328. """
  329. self.terminal.write(self.ps[self.pn] + b"".join(self.lineBuffer))
  330. def terminalSize(self, width, height):
  331. # XXX - Clear the previous input line, redraw it at the new
  332. # cursor position
  333. self.terminal.eraseDisplay()
  334. self.terminal.cursorHome()
  335. self.width = width
  336. self.height = height
  337. self.drawInputLine()
  338. def unhandledControlSequence(self, seq):
  339. pass
  340. def keystrokeReceived(self, keyID, modifier):
  341. m = self.keyHandlers.get(keyID)
  342. if m is not None:
  343. m()
  344. elif keyID in self._printableChars:
  345. self.characterReceived(keyID, False)
  346. else:
  347. self._log.warn("Received unhandled keyID: {keyID!r}", keyID=keyID)
  348. def characterReceived(self, ch, moreCharactersComing):
  349. if self.mode == "insert":
  350. self.lineBuffer.insert(self.lineBufferIndex, ch)
  351. else:
  352. self.lineBuffer[self.lineBufferIndex : self.lineBufferIndex + 1] = [ch]
  353. self.lineBufferIndex += 1
  354. self.terminal.write(ch)
  355. def handle_TAB(self):
  356. n = self.TABSTOP - (len(self.lineBuffer) % self.TABSTOP)
  357. self.terminal.cursorForward(n)
  358. self.lineBufferIndex += n
  359. self.lineBuffer.extend(iterbytes(b" " * n))
  360. def handle_LEFT(self):
  361. if self.lineBufferIndex > 0:
  362. self.lineBufferIndex -= 1
  363. self.terminal.cursorBackward()
  364. def handle_RIGHT(self):
  365. if self.lineBufferIndex < len(self.lineBuffer):
  366. self.lineBufferIndex += 1
  367. self.terminal.cursorForward()
  368. def handle_HOME(self):
  369. if self.lineBufferIndex:
  370. self.terminal.cursorBackward(self.lineBufferIndex)
  371. self.lineBufferIndex = 0
  372. def handle_END(self):
  373. offset = len(self.lineBuffer) - self.lineBufferIndex
  374. if offset:
  375. self.terminal.cursorForward(offset)
  376. self.lineBufferIndex = len(self.lineBuffer)
  377. def handle_BACKSPACE(self):
  378. if self.lineBufferIndex > 0:
  379. self.lineBufferIndex -= 1
  380. del self.lineBuffer[self.lineBufferIndex]
  381. self.terminal.cursorBackward()
  382. self.terminal.deleteCharacter()
  383. def handle_DELETE(self):
  384. if self.lineBufferIndex < len(self.lineBuffer):
  385. del self.lineBuffer[self.lineBufferIndex]
  386. self.terminal.deleteCharacter()
  387. def handle_RETURN(self):
  388. line = b"".join(self.lineBuffer)
  389. self.lineBuffer = []
  390. self.lineBufferIndex = 0
  391. self.terminal.nextLine()
  392. self.lineReceived(line)
  393. def handle_INSERT(self):
  394. assert self.mode in ("typeover", "insert")
  395. if self.mode == "typeover":
  396. self.setInsertMode()
  397. else:
  398. self.setTypeoverMode()
  399. def lineReceived(self, line):
  400. pass
  401. class HistoricRecvLine(RecvLine):
  402. """
  403. L{TerminalProtocol} which adds both basic line-editing features and input history.
  404. Everything supported by L{RecvLine} is also supported by this class. In addition, the
  405. up and down arrows traverse the input history. Each received line is automatically
  406. added to the end of the input history.
  407. """
  408. def connectionMade(self):
  409. RecvLine.connectionMade(self)
  410. self.historyLines = []
  411. self.historyPosition = 0
  412. t = self.terminal
  413. self.keyHandlers.update(
  414. {t.UP_ARROW: self.handle_UP, t.DOWN_ARROW: self.handle_DOWN}
  415. )
  416. def currentHistoryBuffer(self):
  417. b = tuple(self.historyLines)
  418. return b[: self.historyPosition], b[self.historyPosition :]
  419. def _deliverBuffer(self, buf):
  420. if buf:
  421. for ch in iterbytes(buf[:-1]):
  422. self.characterReceived(ch, True)
  423. self.characterReceived(buf[-1:], False)
  424. def handle_UP(self):
  425. if self.lineBuffer and self.historyPosition == len(self.historyLines):
  426. self.historyLines.append(b"".join(self.lineBuffer))
  427. if self.historyPosition > 0:
  428. self.handle_HOME()
  429. self.terminal.eraseToLineEnd()
  430. self.historyPosition -= 1
  431. self.lineBuffer = []
  432. self._deliverBuffer(self.historyLines[self.historyPosition])
  433. def handle_DOWN(self):
  434. if self.historyPosition < len(self.historyLines) - 1:
  435. self.handle_HOME()
  436. self.terminal.eraseToLineEnd()
  437. self.historyPosition += 1
  438. self.lineBuffer = []
  439. self._deliverBuffer(self.historyLines[self.historyPosition])
  440. else:
  441. self.handle_HOME()
  442. self.terminal.eraseToLineEnd()
  443. self.historyPosition = len(self.historyLines)
  444. self.lineBuffer = []
  445. self.lineBufferIndex = 0
  446. def handle_RETURN(self):
  447. if self.lineBuffer:
  448. self.historyLines.append(b"".join(self.lineBuffer))
  449. self.historyPosition = len(self.historyLines)
  450. return RecvLine.handle_RETURN(self)