OutputTerminal.js 2.6 KB

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