如何開發富文本終端UI應用

1. 終端(terminal)的發展歷史

終端(Terminal)是計算機系統的輸入輸出設備,因爲歷史的緣由終端這個概念很是混亂,
終端的發展經歷了字符終端圖形終端網絡終端 三個階段.php

1.1 電傳打字機的設備TTY(TeleTYpe)

在早期因爲計算機很是昂貴,所以數十個用戶共用一臺主機,
爲了知足多用戶同時使用,最初使用一種叫電傳打字機的設備,簡稱TTY(TeleTYpe),
經過專用線纜與中央計算機相連,
電傳打字機經過鍵盤將電碼信號發送給主機,同時接收主機程序的輸出並打印在紙帶上,缺點是很是浪費紙,TTY設備是現代控制檯(Console)的鼻祖.java

TeleTYpe

1.2 VT100

在20世紀70年代後期,VT100由DEC生產.本機具備單色顯示屏.
咱們仍然沒法改變顏色,但它可以表達豐富的視覺效果,如閃爍,刪除文本,並使文本變爲粗體或斜體.
爲特定操做定義了許多控制序列.python

vt100_machine

VT100是一個古老的終端定義,後面出現的終端幾乎都兼容這種終端.VT100沒法表達顏色,由於它嵌入了單色顯示器.
VT100控制碼是用來在終端擴展顯示的代碼.
好比果終端上任意座標用不一樣的顏色顯示字符.VT100控制碼有時又稱爲ANSI Escape Sequence.
若是感興趣繼續瞭解VT的發展歷史請訪問vt100.netgit

VT100控制碼ANSI Escape Sequence
顧名思義,全部控制序列開始從\x1b 對應逃逸上ASCII碼錶.今天大多數我的計算機的Telnet用戶端提供最廣泛的終端(通常VT100)的模擬.
VT100沒法表達顏色,由於它嵌入了單色顯示器.可是不知道爲何VT100控制碼ANSI Escape Sequence有改變顏色的控制序列的細節,
但VT241終端是高端模型嵌入彩色圖形顯示器.github

讓咱們瞭解VT100控制碼.全部的控制符是\033\e打頭(即 ESC 的 ASCII 碼)用輸出字符語句來輸出.
能夠在命令行用 echo 命令,或者在 C 程序中用 printf 來輸出 VT100 的控制字符.golang

1.2.1 VT100 控制碼

\033[0m        // 關閉全部屬性
\033[1m        // 設置爲高亮
\033[4m        // 下劃線
\033[5m        // 閃爍
\033[7m        // 反顯
\033[8m        // 消隱
\033[nA        // 光標上移 n 行
\033[nB        // 光標下移 n 行
\033[nC        // 光標右移 n 行
\033[nD        // 光標左移 n 行
\033[y;xH    // 設置光標位置
\033[2J        // 清屏
\033[K        // 清除從光標到行尾的內容
\033[s        // 保存光標位置
\033[u        // 恢復光標位置
\033[?25l    // 隱藏光標
\033[?25h    // 顯示光標

1.2.2 \033[30m – \033[37m爲設置前景色

30: 黑色
31: 紅色
32: 綠色
33: 黃色
34: 藍色
35: 紫色
36: 青色
37: 白色

1.2.3 \033[40m – \033[47m 爲設置背景色

40: 黑色
41: 紅色
42: 綠色
43: 黃色
44: 藍色
45: 紫色
46: 青色
47: 白色

ANSI / VT100控制碼文檔windows

1.3 PTY(pseudoTTY)僞終端/網絡終端

在一些操做系統中,包括Unix的,一個僞終端,pseudotty,或PTY是一對僞設備,
其中,所述一個的從屬,模仿硬件文本終端裝置,其中,所述其它的主,提供了這樣的裝置終端仿真器進程控制從站.
終端仿真器進程還必須處理終端控制命令,例如,用於調整屏幕的大小.
普遍使用的終端仿真程序包括xterm,GNOME終端,Konsole和終端.
遠程登陸處理程序(如ssh和telnet服務器)扮演相同的角色,但與遠程用戶而不是本地用戶進行通訊.
還要考慮諸如指望之類的程序.bash

pseudo_ssh

2. Go語言終端colorful-text

打印色彩文字示例服務器

package main
import "fmt"
func main() {
   fmt.Print("\x1b[4;30;46m")//設置顏色樣式
   fmt.Print("Hello World")//打印文本內容
   fmt.Println("\x1b[0m")//樣式結束符,清楚以前的顯示屬性
}

運行效果網絡

源代碼解析,請關注第4行,這是VT100控制碼改變顏色.\x1b[4;30;46m由3部分組成.

  • \x1b[ :控制序列導入器
  • 4;30;46:由分號分隔的參數.4表示下劃線,30表示設置前景色黑色,46表示設置背景顏色青色.
  • m :最後一個字符(老是一個字符).

打印Hello World後, print\x1b[0m包含0用來表示清除顯示屬性.

開源庫fatih/color的原理就是使用golang print
VT100控制碼(ANSI Escape Sequence)標記文本內容,色彩豐富的終端文本

3. Go語言終端進度條progress

顯示進度條的代碼的原理:

  1. 終端須要擦除終端,
  2. 打印進度條,
  3. 並移動光標位置
package main

import (
    "fmt"
    "strings"
    "time"
)

func renderbar(count, total int) {
    barwidth := 30
    done := int(float64(barwidth) * float64(count) / float64(total))

    fmt.Printf("Progress: \x1b[33m%3d%%\x1b[0m ", count*100/total)
    fmt.Printf("[%s%s]",
        strings.Repeat("=", done),
        strings.Repeat("-", barwidth-done))
}

func main() {
    total := 50
    for i := 1; i <= total; i++ {
        //<ESC>表示ASCII「轉義」字符,0x1B
        fmt.Print("\x1b7")   // 保存光標位置 保存光標和Attrs <ESC> 7
        fmt.Print("\x1b[2k") // 清空當前行的內容 擦除線<ESC> [2K
        renderbar(i, total)
        time.Sleep(50 * time.Millisecond)
        fmt.Print("\x1b8") // 恢復光標位置 恢復光標和Attrs <ESC> 8
    }
    fmt.Println()
}

這部分代碼缺陷就是barwidth這個值是固定的,但實際中這個變量因該跟隨終端的寬度來肯定.

4. 關於終端仿真器的窗口大小

咱們能夠更改窗口大小,由於咱們使用pty(終端模擬器),
而不是終端機器.在本節中,讓咱們瞭解如何得到終端仿真器的大小.
要得到窗口大小,您須要syscall.SYS_IOCTL使用TIOCGWINSZ 如下調用.

type winsize struct {
   Row uint16
   Col uint16
   X  uint16
   Y uint16
}

func getWinSize(fd int) (row, col uint16, err error) {
   var ws *winsize
   retCode, _, errno := syscall.Syscall(
      syscall.SYS_IOCTL, uintptr(fd),
      uintptr(syscall.TIOCGWINSZ),
      uintptr(unsafe.Pointer(ws)))
   if int(retCode) == -1 {
      panic(errno)
   }
   return ws.Row, ws.Col, nil
}

但從易用性和簡單出發,最好直接調用unix.IoctlGetWinsize, 注意GetWinsizeAPi在windows上很差使

package main

import (
    "fmt"
    "strings"
    "syscall"
    "time"

    "golang.org/x/sys/unix"
)

var wscol = 30

func init() {
    ws, err := unix.IoctlGetWinsize(syscall.Stdout, unix.TIOCGWINSZ)
    if err != nil {
        panic(err)
    }
    wscol = int(ws.Col)
}

func renderbar(count, total int) {
    barwidth := wscol - len("Progress: 100% []")
    done := int(float64(barwidth) * float64(count) / float64(total))

    fmt.Printf("Progress: \x1b[33m%3d%%\x1b[0m ", count*100/total)
    fmt.Printf("[%s%s]",
        strings.Repeat("=", done),
        strings.Repeat("-", barwidth-done))
}

func main() {
    total := 50
    for i := 1; i <= total; i++ {
        fmt.Print("\x1b7")   // save the cursor position
        fmt.Print("\x1b[2k") // erase the current line
        renderbar(i, total)
        time.Sleep(50 * time.Millisecond)
        fmt.Print("\x1b8") // restore the cursor position
    }
    fmt.Println()
}

不只要了解如何獲取窗口大小,還須要知道如何接收事件,通知事件窗口大小更改.

這裏以macOS/unix系統爲例,
您能夠從UNIX OS信號接收通知.您只需處理SIGWINCH os信號,以下所示.

package main

import (
    "fmt"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"

    "golang.org/x/sys/unix"
)

var (
    total = 50
    count = 0
    wscol = 20
)

func init() {
    err := updateWSCol()
    if err != nil {
        panic(err)
    }
}

func updateWSCol() error {
    ws, err := unix.IoctlGetWinsize(syscall.Stdout, unix.TIOCGWINSZ)
    if err != nil {
        return err
    }
    wscol = int(ws.Col)
    return nil
}

func renderbar() {
    fmt.Print("\x1b7")       // 保存光標位置
    fmt.Print("\x1b[2k")     // 清除當前行內容
    defer fmt.Print("\x1b8") // 恢復光標位置

    barwidth := wscol - len("Progress: 100% []")
    done := int(float64(barwidth) * float64(count) / float64(total))

    fmt.Printf("Progress: \x1b[33m%3d%%\x1b[0m ", count*100/total)
    fmt.Printf("[%s%s]",
        strings.Repeat("=", done),
        strings.Repeat("-", barwidth-done))
}

func main() {
    // set signal handler
    sigwinch := make(chan os.Signal, 1)
    defer close(sigwinch)
    signal.Notify(sigwinch, syscall.SIGWINCH)
    go func() {
        for {
            if _, ok := <-sigwinch; !ok {
                return
            }
            _ = updateWSCol()
            renderbar()
        }
    }()

    for count = 1; count <= 50; count++ {
        renderbar()
        time.Sleep(time.Second)
    }
    fmt.Println()
}

經過調用ioctlTIOCGWINSZ當你收到SIGWINCH signal,你能夠獲得窗口的大小.您能夠今後信息控制終端UI.
可是很難正確擦除屏幕.
實際上,若是在此代碼中使終端窗口變小,則輸出將崩潰.最簡單的方法是每次都擦除整個屏幕.

結束語

思惟擴展:根據 ANSI / VT100終端控制碼 文檔結合python/bash/go/java/c/php等語言的print函數你能夠開發出本身的富文本終端UI app.

參考文檔

相關文章
相關標籤/搜索