OutputTerminal.js 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { Terminal } from '@xterm/xterm'
  2. import { FitAddon } from '@xterm/addon-fit'
  3. import { WebLinksAddon } from '@xterm/addon-web-links'
  4. import { Mutex } from './Mutex.js'
  5. /**
  6. * xterm.js based terminal output for the execution dialog.
  7. *
  8. * the xterm.js methods for write(), reset() and clear() appear to be async,
  9. * but they do not return a Promise and instead use a callback. When calling
  10. * these methods in quick succession, the output can get garbled due to race
  11. * conditions.
  12. *
  13. * To avoid this, this class uses Mutex around those methods to ensure that
  14. * only one write OR reset is executed at a time, is completed, and the calls
  15. * occour in sequential order.
  16. */
  17. export class OutputTerminal {
  18. constructor (executionTrackingId) {
  19. this.executionTrackingId = executionTrackingId
  20. this.writeMutex = new Mutex()
  21. const linkHandler = {
  22. activate (event, text, _range) {
  23. event.preventDefault()
  24. window.open(text, '_blank')
  25. }
  26. }
  27. this.terminal = new Terminal({
  28. convertEol: true,
  29. linkHandler
  30. })
  31. const fitAddon = new FitAddon()
  32. this.terminal.loadAddon(fitAddon)
  33. this.terminal.fit = fitAddon
  34. this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
  35. this.linkHandlerConfigured = true
  36. }
  37. async write (out, then) {
  38. const unlock = await this.writeMutex.lock()
  39. try {
  40. await new Promise(resolve => {
  41. this.terminal.write(out, () => {
  42. resolve()
  43. })
  44. })
  45. } finally {
  46. unlock()
  47. if (then != null && then !== undefined) {
  48. then()
  49. }
  50. }
  51. }
  52. async reset () {
  53. const unlock = await this.writeMutex.lock()
  54. try {
  55. await new Promise(resolve => {
  56. this.terminal.clear()
  57. this.terminal.reset()
  58. resolve()
  59. })
  60. } finally {
  61. unlock()
  62. }
  63. }
  64. fit () {
  65. this.terminal.fit.fit()
  66. }
  67. open (el) {
  68. this.terminal.open(el)
  69. }
  70. close () {
  71. this.terminal.dispose()
  72. }
  73. resize (cols, rows) {
  74. this.terminal.resize(cols, rows)
  75. }
  76. /**
  77. * Get the terminal buffer content as a string.
  78. * This method is intended for use in integration tests to verify output.
  79. * @returns {string} The terminal buffer content as a string
  80. */
  81. getBufferAsString () {
  82. if (!this.terminal) {
  83. return ''
  84. }
  85. const buffer = this.terminal.buffer.active
  86. let text = ''
  87. for (let i = 0; i < buffer.length; i++) {
  88. const line = buffer.getLine(i)
  89. if (line) {
  90. text += line.translateToString(true) + '\n'
  91. }
  92. }
  93. return text
  94. }
  95. }