1. 终端(terminal)的发展历史

终端(Terminal)是计算机系统的输入输出设备,由于历史的原因终端这个概念非常混乱,
终端的发展经历了字符终端图形终端网络终端 三个阶段.

1.1 电传打字机的设备TTY(TeleTYpe)

在早期由于计算机非常昂贵,因此数十个用户共用一台主机,
为了满足多用户同时使用,最初使用一种叫电传打字机的设备,简称TTY(TeleTYpe),
通过专用线缆与中央计算机相连,
电传打字机通过键盘将电码信号发送给主机,同时接收主机程序的输出并打印在纸带上,缺点是非常浪费纸,TTY设备是现代控制台(Console)的鼻祖.

1.2 VT100

在20世纪70年代后期,VT100由DEC生产.本机具有单色显示屏.
我们仍然无法改变颜色,但它能够表达丰富的视觉效果,如闪烁,删除文本,并使文本变为粗体或斜体.
为特定操作定义了许多控制序列.

VT100是一个古老的终端定义,后面出现的终端几乎都兼容这种终端.VT100无法表达颜色,因为它嵌入了单色显示器.
VT100控制码是用来在终端扩展显示的代码.
比如果终端上任意坐标用不同的颜色显示字符.VT100控制码有时又称为ANSI Escape Sequence.
如果感兴趣继续了解VT的发展历史请访问vt100.net

\x1b
\033\e

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    // 显示光标
\033[30m – \033[37m
30: 黑色
31: 红色
32: 绿色
33: 黄色
34: 蓝色
35: 紫色
36: 青色
37: 白色
\033[40m – \033[47m
40: 黑色
41: 红色
42: 绿色
43: 黄色
44: 蓝色
45: 紫色
46: 青色
47: 白色

1.3 PTY(pseudoTTY)伪终端/网络终端

在一些操作系统中,包括Unix的,一个伪终端,pseudotty,或PTY是一对伪设备,
其中,所述一个的从属,模仿硬件文本终端装置,其中,所述其它的主,提供了这样的装置终端仿真器进程控制从站.
终端仿真器进程还必须处理终端控制命令,例如,用于调整屏幕的大小.
广泛使用的终端仿真程序包括xterm,GNOME终端,Konsole和终端.
远程登录处理程序(如ssh和telnet服务器)扮演相同的角色,但与远程用户而不是本地用户进行通信.
还要考虑诸如期望之类的程序.

2. Go语言终端colorful-text

打印色彩文字示例

package main
import "fmt"
func main() {
   fmt.Print("\x1b[4;30;46m")//设置颜色样式
   fmt.Print("Hello World")//打印文本内容
   fmt.Println("\x1b[0m")//样式结束符,清楚之前的显示属性
}

运行效果

\x1b[4;30;46m
\x1b[4;30;46m
\x1b[0m0
print

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. 关于终端仿真器的窗口大小

syscall.SYS_IOCTLTIOCGWINSZ 
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
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()
}

不仅要了解如何获取窗口大小,还需要知道如何接收事件,通知事件窗口大小更改.

UNIX OSSIGWINCH 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()
}
ioctlTIOCGWINSZSIGWINCH

结束语

ANSI / VT100终端控制码print

参考文档