How to Develop Rich Text Terminal UI Application

1. The Development History of Terminal

Terminal is the input and output equipment of computer system. For historical reasons, the concept of terminal is very confusing.
The development of terminal has gone through three stages: character terminal, graphics terminal and network terminal.

1.1 Teletypewriter TTY(TeleTYpe)

In the early days, because computers were very expensive, dozens of users shared a single host.
In order to satisfy the simultaneous use of multiple users, a device called teletypewriter, TTY(TeleTYpe), was initially used.
Connecting with the central computer through dedicated cables,
Teletypewriter transmits code signal to host computer through keyboard, and receives output of host program and prints it on paper tape. The disadvantage of teletypewriter is waste of paper. TTY device is the ancestor of Console.

1.2 VT100

In the late 1970s, VT100 was produced by DEC. The machine has a monochrome display screen.
We still can't change color, but it can express rich visual effects, such as flickering, deleting text, and making text bold or italic.
Many control sequences are defined for specific operations.

VT100 is an old definition of terminal, which is compatible with almost all the terminals appearing later. VT100 cannot express color because it is embedded in monochrome displays.
VT100 control code is used to expand the display code in the terminal.
For example, if any coordinates on the terminal display characters in different colors. VT100 control code is sometimes called ANSI Escape Sequence.
If you are interested in continuing to understand the history of VT, please visit vt100.net

VT100 Control Code ANSI Escape Sequence
As the name implies, all control sequences begin to escape from x1b to ASCII code table. Today most personal computer Telnet clients provide the most common terminal (VT100) simulation.
VT100 cannot express color because it has a monochrome display embedded in it. But I don't know why the VT100 control code ANSI Escape Sequence has the details of the control sequence that changes color.
But VT 241 terminal is a high-end model embedded color graphics display.

Let's look at the VT100 control code. All the controllers are_or e headers (that is, ASCII codes of ESC) which are output by character statements.
You can use echo command on the command line or printf in C program to output control characters of VT100.

1.2.1 VT100 Control Code

\033[0m        // Close all properties
\033[1m        // Set to highlight
\033[4m        // Underline
\033[5m        // Twinkle
\033[7m        // Anti display
\033[8m        // Blanking
\033[nA        // Cursor up n lines
\033[nB        // Cursor down n lines
\033[nC        // Move the cursor right n rows
\033[nD        // Cursor left shift n lines
\033[y;xH    // Setting cursor position
\033[2J        // Clean screen
\033[K        // Clear content from cursor to end of line
\033[s        // Save the cursor position
\033[u        // restore cursor position
\033[?25l    // hide cursor
\033[?25h    // Display cursor

1.2.2

30:Black
 31:Red
 32:Green
 33:Yellow
 34:Blue
 35:Purple
 36:Blue
 37:White

1.2.3 3[40m-3[47m] Sets Background Color

40:Black
 41:Red
 42:Green
 43:Yellow
 44:Blue
 45:Purple
 46:Cyan
 47:White

ANSI/VT100 Control Code Document

1.3 PTY(pseudoTTY) pseudo-terminal/network terminal

In some operating systems, including Unix, a pseudo terminal, pseudotty, or PTY is a pair of pseudo devices.
Among them, the subordinate of the device imitates the hardware text terminal device, in which the other masters provide the process control subordinate of the device terminal simulator.
The terminal emulator process must also process terminal control commands, for example, to adjust the screen size.
The widely used terminal simulation programs include xterm, GNOME terminal, Konsole and terminal.
Remote login handlers (such as ssh and telnet servers) play the same role, but communicate with remote users rather than local users.
Consider procedures such as expectations.

2. Go Language Terminal colorful-text

Examples of printing color text

package main
import "fmt"
func main() {
   fmt.Print("\x1b[4;30;46m")//Set the color style
   fmt.Print("Hello World")//Print text content
   fmt.Println("\x1b[0m")//Style terminator, clear before the display properties
}

Operation effect

Source code parsing, please pay attention to line 4, this is the VT100 control code to change the color. x1b[4;30;46m consists of three parts.

  • \ x1b [: Control Sequence Importer
  • 4; 30; 46: The semicolon-separated parameter. 4 represents the underline, 30 sets the foreground color black, 46 sets the background color blue.
  • m: The last character (always a character).

After printing Hello World, printx1b[0m contains 0 to indicate clearance of display properties.

Open Source Library fatih/color The principle is to use golang print
VT100 Control Code (ANSI Escape Sequence) Marks Text Content, Colorful Terminal Text

3. progress bar of Go language terminal

The principle of displaying progress bar code:

  1. Terminals need to be erased.
  2. Print progress bar.
  3. And move the cursor position
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> means ASCII "escape" character, 0x1B
        fmt.Print("\x1b7")   // Save the cursor position, save the cursor and Attrs < ESC > 7
        fmt.Print("\x1b[2k") // Clear the content erase line of the current line < ESC > [2K]
        renderbar(i, total)
        time.Sleep(50 * time.Millisecond)
        fmt.Print("\x1b8") // Recovery cursor position recovery cursor and Atrs < ESC > 8
    }
    fmt.Println()
}

This part of the code flaw is that the value of barwidth is fixed, but in practice this variable is determined by the width of the following terminal.

4. Window Size of Terminal Simulator

We can change the window size because we use pty.
In this section, let's see how to get the size of the terminal emulator.
To get the window size, you need syscall.SYS_IOCTL to be called below 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
}

But for ease of use and simplicity, it's better to call unix.IoctlGetWinsize directly. Note that the GetWinsize API is not good for 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()
}

Not only do you need to know how to get the window size, but you also need to know how to receive events and notify event window size changes.

Take the macOS/unix system as an example.
You can receive notifications from UNIX OS. You only need to process SIGWINCH os signals, as shown below.

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")       // Save the cursor position
    fmt.Print("\x1b[2k")     // Clear the current line content
    defer fmt.Print("\x1b8") // restore cursor position

    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()
}

By calling ioctl and TIOCGWINSZ, when you receive SIGWINCH signal, you can get the size of the window. From this information, you can control the terminal UI.
But it's hard to erase the screen correctly.
In fact, if you make the terminal window smaller in this code, the output will crash. The simplest way is to erase the entire screen every time.

Concluding remarks

Thought Extension: You can develop your own rich text terminal UI app according to the ANSI/VT100 terminal control code document combined with the print function of python/bash/go/java/c/php and other languages.

Reference Documents

Keywords: Go Unix ascii network

Added by Ryaan on Thu, 29 Aug 2019 18:08:10 +0300