基於xtermjs實現的web terminal


基於xtermjs實現的web terminal

背景

xterm.js 一個用TypeScript編寫的前端組件,它可以讓應用程序在瀏覽器中為用戶提供功能齊全的終端.

很強大的一個前端終端組件,但里面的各種按鍵事件有時需要自己編寫實現(若無ssh交互)。

本demo基於xtermjs,簡單模擬實現了:

  • 退格
  • 方向鍵上下左右(歷史命令)
  • 粘貼復制

效果預覽:

preview

PS: 程序無socket交互(如需要可使用socket.io-client),其他功能都可以自主定制實現,有什么不清楚的可以直接留言,咱們一起交流。

實現

直接拋代碼。源碼地址:https://github.com/AshinWu/webterminal

<template>
  <div class="hello">
    <div id="terminal-container"></div>
  </div>
</template>

<script>
import 'xterm/dist/xterm.css'
import 'xterm/dist/xterm'
import * as fit from 'xterm/dist/addons/fit/fit'
import * as attach from 'xterm/dist/addons/attach/attach'
import { Terminal } from 'xterm'

export default {
  name: 'HelloWorld',
  data () {
    return {
      terminal: Object,
      termOptions: {
        rows: 40,
        scrollback: 800
      },
      input: '',
      prefix: 'ashin$ ',
      // 歷史指令
      histIndex: 0,
      histCommandList: [],
      currentOffset: Number
    }
  },
  mounted() {
    this.terminal = this.initTerm()
  },
  methods: {
    initTerm() {
      Terminal.applyAddon(fit)
      Terminal.applyAddon(attach)
      let term = new Terminal({
        rendererType: 'canvas',
        cursorBlink: true,
        convertEol: true,
        scrollback: this.termOptions.scrollback,
        row: this.termOptions.rows,
        theme: {
          foreground: 'white',
          background: '#060101'
        }
      })
      let terminalContainer = document.querySelector('#terminal-container')
      term.open(terminalContainer)
      term.fit()
      term.focus()
      term.writeln(`Hello from web terminal`)
      term.prompt = () => {
        term.write(this.prefix)
      }

      // 實際需要使用socket來交互, 這里不做演示
      if ('WebSocket' in window) {
        term.writeln('\x1b[1;1;32mThe Browser supports websocket!\x1b[0m')
        term.prompt()
        // 這里創建socket.io客戶端實例
        // socket監聽事件
      } else {
        term.writeln('\x1b[1;1;31mThe Browser does not support websocket!\x1b[0m')
      }
      
      term.on('key', function(key, ev) {
        const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
        // 每行開頭前綴長度 @ashinWu:$ 
        const threshold = this.prefix.length
        // 總偏移(長度) = 輸入+前綴
        let fixation = this.input.length + threshold
        // 當前x偏移量
        let offset = term._core.buffer.x
        this.currentOffset = fixation
        // 禁用Home、PgUp、PgDn、Ins、Del鍵
        if ([36, 33, 34, 45, 46].indexOf(ev.keyCode) !== -1) return

        switch(ev.keyCode) {
          // 回車鍵
          case 13:
            this.handleInput()
            this.input = ''
            break;
          // 退格鍵
          case 8:
            if (offset > threshold) {
              term._core.buffer.x = offset - 1
              // \x1b[?K: 清除光標至行末的"可清除"字符
              term.write('\x1b[?K' + this.input.slice(offset - threshold))
              // 保留原來光標位置
              const cursor = this.bulidData(fixation - offset, '\x1b[D')
              term.write(cursor)
              this.input = `${this.input.slice(0, offset - threshold - 1)}${this.input.slice(offset - threshold)}`
            }
            break;
          case 35:
            const cursor = this.bulidData(fixation - offset, '\x1b[C')
            term.write(cursor)
            break
          // 方向盤上鍵
          case 38:
            if (this.histCommandList[this.histIndex - 1]) {
              // 將光標重置到末端
              term._core.buffer.x = fixation
              let b1 = '', b2 = '', b3 = '';
              // 構造退格(模擬替換效果) \b \b標識退一格; \b\b  \b\b表示退兩格...
              for (let i = 0; i < this.input.length; i++) {
                b1 = b1 + '\b'
                b2 = b2 + ' '
                b3 = b3 + '\b'
              }
              term.write(b1 + b2 + b3)
              this.input = this.histCommandList[this.histIndex - 1]
              term.write(this.histCommandList[this.histIndex - 1])
              this.histIndex--
            }
            break;
          // 方向盤下鍵
          case 40:
            if (this.histCommandList[this.histIndex + 1]) {
              // 將光標重置到末端
              term._core.buffer.x = fixation  
              let b1 = '', b2 = '', b3 = '';
              // 構造退格(模擬替換效果) \b \b標識退一格; \b\b  \b\b表示退兩格...
              for (let i = 0; i < this.histCommandList[this.histIndex].length; i++) {
                b1 = b1 + '\b'
                b2 = b2 + ' '
                b3 = b3 + '\b'
              }
              this.input = this.histCommandList[this.histIndex + 1]
              term.write(b1 + b2 + b3)
              term.write(this.histCommandList[this.histIndex + 1])
              this.histIndex++
            }
            break;
          // 方向盤左鍵
          case 37:
            if (offset > threshold) {
              term.write(key)
            }
            break;
          // 方向盤右鍵
          case 39:
            if (offset < fixation) {
              term.write(key)
            }
            break;
          default:
            if (printable) {
              // 限制輸入最大長度 防止換行bug
              if (fixation >= term.cols)  return

              // 不在末尾插入時 要拼接
              if (offset < fixation) {
                term.write('\x1b[?K' + `${key}${this.input.slice(offset - threshold)}`)
                const cursor = this.bulidData(fixation - offset, '\x1b[D')
                term.write(cursor)
                this.input = `${this.input.slice(0, offset - threshold)}${key}${this.input.slice(offset - threshold)}`
              } else {
                term.write(key)
                this.input += key
              }
              this.histIndex = this.histCommandList.length
            }
            break;
        }
        
      }.bind(this))

      // 選中復制
      term.on('selection', function() {
        if (term.hasSelection()) {
          this.copy = term.getSelection()
        }
      }.bind(this))

      term.attachCustomKeyEventHandler(function (ev) {
        // curl+v
        if (ev.keyCode === 86 && ev.ctrlKey) {
          const inline = (this.currentOffset + this.copy.length) >= term.cols
          if (inline) return
          if (this.copy) {
            term.write(this.copy)
            this.input += this.copy
          }
        }
      }.bind(this))

      // 若需要中文輸入, 使用on data監聽
      // term.on('data', function(data){
        // todo something
      // })

      return term
    },
    // 在這里處理自定義輸入...
    handleInput() {
      // 判斷空值
      this.terminal.write('\r\n')
      if (this.input.trim()) {
        // 記錄歷史命令
        if (this.histCommandList[this.histCommandList.length - 1] !== this.input) {
          this.histCommandList.push(this.input)
          this.histIndex = this.histCommandList.length
        }
        const command = this.input.trim().split(' ')
        // 可限制可用命令
        // 這里進行socket交互
        switch (command[0]) {
          case 'help': 
            this.terminal.writeln('\x1b[40;33;1m\nthis is a web terminal demo based on xterm!\x1b[0m\n此demo模擬shell上下左右和退格鍵效果\n')
            break
          default:
            this.terminal.writeln(this.input) 
            break
        }
      }
      this.terminal.prompt()
    },

    bulidData(length, subString) {
      let cursor = ''
      for (let i = 0; i < length; i++) {
        cursor += subString
      }
      return cursor;
    }
  },
}
</script>

附錄

常用終端特殊字符

//屏幕屬性命令,23
"\x1b[12h",//禁止本端回顯,鍵盤數據僅送給主機
"\x1b[12l",//允許本端回顯,鍵盤數據送給主機和屏幕
"\x1b[?5h",//屏幕顯示為白底黑字
"\x1b[?5l",//顯示為黑底白字
"\x1b[?3h",//132列顯示
"\x1b[?3l",//80列顯示
"\x1b[?6h",//以用戶指定的滾動區域的首行行首為參考原點
"\x1b[?6l",//以屏幕的首行行首為參考原點
"\x1b[?7h",//當字符顯示到行末時,自動回到下行行首接着顯示;如果在滾動區域底行行末,則上滾一行再顯示
"\x1b[?7l",//當字符顯示到行末時,仍在行末光標位置顯示,覆蓋原有的字符,除非接收到移動光標的命令
"\x1b[?4h",//平滑滾動
"\x1b[?4l",//跳躍滾動
"\x1b[/0s",//不滾動
"\x1b[/1s",//平滑慢滾
"\x1b[/2s",//跳躍滾動
"\x1b[/3s",//平滑快滾
"\x1b[3h",//監督有效,顯示控制符,供程序員調試程序用
"\x1b[3l",//監督無效,執行控制符,正常運行程序
"\x1b[0$~",//禁止狀態行(VT300有效
"\x1b[1$~",//允許狀態行(VT300有效)
"\x1b[2$~",//主機可寫狀態行(VT300有效)
"\x1b[0$|",//主機可寫狀態行時,在主屏顯示數據(VT300有效)
"\x1b[1$|",//主機可寫狀態行時,在狀態行顯示數據(VT300有效)   

//光標命令,14
"\x1b[?25h",//光標顯示
"\x1b[?25l",//光標消隱
"\x1b[/0j",//閃爍塊光標
"\x1b[/1j",//閃爍線光標
"\x1b[/2j",//穩態塊光標
"\x1b[/3j",//穩態線光標
"\x1bH",//在當前列上設置制表位
"\x1b[g",//清除當前列上的制表位
"\x1b[0g",//清除當前列上的制表位
"\x1b[3g",//清除所有列上的制表位
"\x1b\x45",//光標下移1行
"\x1b\x4d",//光標上移1行
"\x1b\x37",//保存終端當前狀態
"\x1b\x38",//恢復上述狀態   

//行屬性和字符屬性命令,4
"\x1b#3",//設置當前行為倍寬倍高(上半部分)
"\x1b#4",//設置當前行為倍寬倍高(下半部分)
"\x1b#5",//設置當前行為單寬單高
"\x1b#6",//設置當前行為倍寬單高   

//編緝命令,22
"\x1b[A",
"\x1b[B",
"\x1b[C",
"\x1b[D",
"\x1b[4h",//插入方式:新顯示字符使光標位置后的原來顯示字符右移,移出邊界的字符丟失。
"\x1b[4l",//替代方式:新顯示字符替代光標位置字符顯示
"\x1b[K",//清除光標至行末字符,包括光標位置,行屬性不受影響。
"\x1b[0K",//清除光標至行末字符,包括光標位置,行屬性不受影響。
"\x1b[1K",//清除行首至光標位置字符,包括光標位置,行屬性不受影響。
"\x1b[2K",//清除光標所在行的所有字符
"\x1b[J",//清除從光標至屏末字符,整行被清的行屬性變成單寬單高
"\x1b[0J",//清除從光標至屏末字符,整行被清的行屬性變成單寬單高
"\x1b[1J",//清除從屏首至光標字符,整行被清的行屬性變成單寬單高
"\x1b[2J",//清除整個屏幕,行屬性變成單寬單高,光標位置不變
"\x1b[?K",//清除光標至行末的"可清除"字符,不影響其它字符和行屬性
"\x1b[?0K",//清除光標至行末的"可清除"字符,不影響其它字符和行屬性
"\x1b[?1K",//清除行首至光標位置的"可清除"字符,不影響其它字符和行屬性
"\x1b[?2K",//清除光標所在行的所有"可清除"字符,不影響其它字符和行屬性
"\x1b[?J",//清除從光標至屏末的"可清除"字符,不影響其它字符和行屬性
"\x1b[?0J",//清除從光標至屏末的"可清除"字符,不影響其它字符和行屬性
"\x1b[?1J",//清除從屏首至光標的"可清除"字符,不影響其它字符和行屬性
"\x1b[?2J",//清除整個屏幕中的"可清除"字符,不影響其它字符和行屬性   

//鍵盤16
"\x1b[2h",//鎖存鍵盤數據(不超過15個)暫停向主機發送,直到開放為止。
"\x1b[2l",//允許鍵盤向主機發送數據。
"\x1b[?8h",//鍵盤連發有效
"\x1b[?8l",//鍵盤連發無效
"\x1b[5h",//擊鍵聲有效
"\x1b[5l",//擊鍵聲無效
"\x1b[?1h",//光標鍵產生"應用"控制序列。見鍵盤代碼一節。
"\x1b[?1l",//光標鍵產生ANSI標准的控制序列。見鍵盤代碼一節。
"\x1b=",//副鍵盤產生"應用"控制序列。見鍵盤代碼一節。
"\x1b&gt;",//副鍵盤產生數字等字符序列,PF鍵不變。見鍵盤代碼一節。
"\x1b[20h",//接收LF、FF或VT控制碼后,光標移至下一行行首;Return鍵發送CR和LF控制碼。
"\x1b[20l",//接收LF、FF或VT控制碼后,光標移至下一行當前列;Return鍵發送CR控制碼。
"\x1b[?67h",//作為退格鍵發送BS。
"\x1b[?67l",//作為刪除鍵發送DEL。
"\x1b[/2h", // 頂排功能鍵作為應用程序功能使用CTRL功能鍵作為本端功能鍵使用
"\x1b[/2l",//頂排功能鍵作為本端功能鍵使用CTRL功能鍵作為應用程序功能使用

常用終端顏色

使用格式:

\x1b[xx;xx;xxm ${content} \x1b[0m

xx:xx:xx數值和編碼的前后順序沒有關系。可以選擇的編碼如下所示:

0 重新設置屬性到缺省設置 
1 設置粗體 
2 設置一半亮度(模擬彩色顯示器的顏色) 
4 設置下划線(模擬彩色顯示器的顏色) 
5 設置閃爍 
7 設置反向圖象 
22 設置一般密度 
24 關閉下划線 
25 關閉閃爍 
27 關閉反向圖象 
30 設置黑色前景 
31 設置紅色前景 
32 設置綠色前景 
33 設置黃色前景 
34 設置藍色前景 
35 設置紫色前景 
36 設置青色前景 
37 設置白色前景 
38 在缺省的前景顏色上設置下划線 
39 在缺省的前景顏色上關閉下划線 
40 設置黑色背景 
41 設置紅色背景 
42 設置綠色背景 
43 設置黃色背景 
44 設置藍色背景 
45 設置紫色背景 
46 設置青色背景 
47 設置白色背景 
49 設置缺省黑色背景

這里有一張圖更直觀(出處

colorcode


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM