mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-10-31 08:03:43 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			436 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			436 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // package vt100 implements a quick-and-dirty programmable ANSI terminal emulator.
 | |
| //
 | |
| // You could, for example, use it to run a program like nethack that expects
 | |
| // a terminal as a subprocess. It tracks the position of the cursor,
 | |
| // colors, and various other aspects of the terminal's state, and
 | |
| // allows you to inspect them.
 | |
| //
 | |
| // We do very much mean the dirty part. It's not that we think it might have
 | |
| // bugs. It's that we're SURE it does. Currently, we only handle raw mode, with no
 | |
| // cooked mode features like scrolling. We also misinterpret some of the control
 | |
| // codes, which may or may not matter for your purpose.
 | |
| package vt100
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"image/color"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| )
 | |
| 
 | |
| type Intensity int
 | |
| 
 | |
| const (
 | |
| 	Normal Intensity = 0
 | |
| 	Bright           = 1
 | |
| 	Dim              = 2
 | |
| 	// TODO(jaguilar): Should this be in a subpackage, since the names are pretty collide-y?
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	// Technically RGBAs are supposed to be premultiplied. But CSS doesn't expect them
 | |
| 	// that way, so we won't do it in this file.
 | |
| 	DefaultColor = color.RGBA{0, 0, 0, 0}
 | |
| 	// Our black has 255 alpha, so it will compare negatively with DefaultColor.
 | |
| 	Black   = color.RGBA{0, 0, 0, 255}
 | |
| 	Red     = color.RGBA{255, 0, 0, 255}
 | |
| 	Green   = color.RGBA{0, 255, 0, 255}
 | |
| 	Yellow  = color.RGBA{255, 255, 0, 255}
 | |
| 	Blue    = color.RGBA{0, 0, 255, 255}
 | |
| 	Magenta = color.RGBA{255, 0, 255, 255}
 | |
| 	Cyan    = color.RGBA{0, 255, 255, 255}
 | |
| 	White   = color.RGBA{255, 255, 255, 255}
 | |
| )
 | |
| 
 | |
| func (i Intensity) alpha() uint8 {
 | |
| 	switch i {
 | |
| 	case Bright:
 | |
| 		return 255
 | |
| 	case Normal:
 | |
| 		return 170
 | |
| 	case Dim:
 | |
| 		return 85
 | |
| 	default:
 | |
| 		return 170
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Format represents the display format of text on a terminal.
 | |
| type Format struct {
 | |
| 	// Fg is the foreground color.
 | |
| 	Fg color.RGBA
 | |
| 	// Bg is the background color.
 | |
| 	Bg color.RGBA
 | |
| 	// Intensity is the text intensity (bright, normal, dim).
 | |
| 	Intensity Intensity
 | |
| 	// Various text properties.
 | |
| 	Underscore, Conceal, Negative, Blink, Inverse bool
 | |
| }
 | |
| 
 | |
| func toCss(c color.RGBA) string {
 | |
| 	return fmt.Sprintf("rgba(%d, %d, %d, %f)", c.R, c.G, c.B, float32(c.A)/255)
 | |
| }
 | |
| 
 | |
| func (f Format) css() string {
 | |
| 	parts := make([]string, 0)
 | |
| 	fg, bg := f.Fg, f.Bg
 | |
| 	if f.Inverse {
 | |
| 		bg, fg = fg, bg
 | |
| 	}
 | |
| 
 | |
| 	if f.Intensity != Normal {
 | |
| 		// Intensity only applies to the text -- i.e., the foreground.
 | |
| 		fg.A = f.Intensity.alpha()
 | |
| 	}
 | |
| 
 | |
| 	if fg != DefaultColor {
 | |
| 		parts = append(parts, "color:"+toCss(fg))
 | |
| 	}
 | |
| 	if bg != DefaultColor {
 | |
| 		parts = append(parts, "background-color:"+toCss(bg))
 | |
| 	}
 | |
| 	if f.Underscore {
 | |
| 		parts = append(parts, "text-decoration:underline")
 | |
| 	}
 | |
| 	if f.Conceal {
 | |
| 		parts = append(parts, "display:none")
 | |
| 	}
 | |
| 	if f.Blink {
 | |
| 		parts = append(parts, "text-decoration:blink")
 | |
| 	}
 | |
| 
 | |
| 	// We're not in performance sensitive code. Although this sort
 | |
| 	// isn't strictly necessary, it gives us the nice property that
 | |
| 	// the style of a particular set of attributes will always be
 | |
| 	// generated the same way. As a result, we can use the html
 | |
| 	// output in tests.
 | |
| 	sort.StringSlice(parts).Sort()
 | |
| 
 | |
| 	return strings.Join(parts, ";")
 | |
| }
 | |
| 
 | |
| // Cursor represents both the position and text type of the cursor.
 | |
| type Cursor struct {
 | |
| 	// Y and X are the coordinates.
 | |
| 	Y, X int
 | |
| 
 | |
| 	// F is the format that will be displayed.
 | |
| 	F Format
 | |
| }
 | |
| 
 | |
| // VT100 represents a simplified, raw VT100 terminal.
 | |
| type VT100 struct {
 | |
| 	// Height and Width are the dimensions of the terminal.
 | |
| 	Height, Width int
 | |
| 
 | |
| 	// Content is the text in the terminal.
 | |
| 	Content [][]rune
 | |
| 
 | |
| 	// Format is the display properties of each cell.
 | |
| 	Format [][]Format
 | |
| 
 | |
| 	// Cursor is the current state of the cursor.
 | |
| 	Cursor Cursor
 | |
| 
 | |
| 	// savedCursor is the state of the cursor last time save() was called.
 | |
| 	savedCursor Cursor
 | |
| 
 | |
| 	unparsed []byte
 | |
| }
 | |
| 
 | |
| // NewVT100 creates a new VT100 object with the specified dimensions. y and x
 | |
| // must both be greater than zero.
 | |
| //
 | |
| // Each cell is set to contain a ' ' rune, and all formats are left as the
 | |
| // default.
 | |
| func NewVT100(y, x int) *VT100 {
 | |
| 	if y == 0 || x == 0 {
 | |
| 		panic(fmt.Errorf("invalid dim (%d, %d)", y, x))
 | |
| 	}
 | |
| 
 | |
| 	v := &VT100{
 | |
| 		Height:  y,
 | |
| 		Width:   x,
 | |
| 		Content: make([][]rune, y),
 | |
| 		Format:  make([][]Format, y),
 | |
| 	}
 | |
| 
 | |
| 	for row := 0; row < y; row++ {
 | |
| 		v.Content[row] = make([]rune, x)
 | |
| 		v.Format[row] = make([]Format, x)
 | |
| 
 | |
| 		for col := 0; col < x; col++ {
 | |
| 			v.clear(row, col)
 | |
| 		}
 | |
| 	}
 | |
| 	return v
 | |
| }
 | |
| 
 | |
| func (v *VT100) UsedHeight() int {
 | |
| 	count := 0
 | |
| 	for _, l := range v.Content {
 | |
| 		for _, r := range l {
 | |
| 			if r != ' ' {
 | |
| 				count++
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return count
 | |
| }
 | |
| 
 | |
| func (v *VT100) Resize(y, x int) {
 | |
| 	if y > v.Height {
 | |
| 		n := y - v.Height
 | |
| 		for row := 0; row < n; row++ {
 | |
| 			v.Content = append(v.Content, make([]rune, v.Width))
 | |
| 			v.Format = append(v.Format, make([]Format, v.Width))
 | |
| 			for col := 0; col < v.Width; col++ {
 | |
| 				v.clear(v.Height+row, col)
 | |
| 			}
 | |
| 		}
 | |
| 		v.Height = y
 | |
| 	} else if y < v.Height {
 | |
| 		v.Content = v.Content[:y]
 | |
| 		v.Height = y
 | |
| 	}
 | |
| 
 | |
| 	if x > v.Width {
 | |
| 		for i := range v.Content {
 | |
| 			row := make([]rune, x)
 | |
| 			copy(row, v.Content[i])
 | |
| 			v.Content[i] = row
 | |
| 			format := make([]Format, x)
 | |
| 			copy(format, v.Format[i])
 | |
| 			v.Format[i] = format
 | |
| 			for j := v.Width; j < x; j++ {
 | |
| 				v.clear(i, j)
 | |
| 			}
 | |
| 		}
 | |
| 		v.Width = x
 | |
| 	} else if x < v.Width {
 | |
| 		for i := range v.Content {
 | |
| 			v.Content[i] = v.Content[i][:x]
 | |
| 			v.Format[i] = v.Format[i][:x]
 | |
| 		}
 | |
| 		v.Width = x
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *VT100) Write(dt []byte) (int, error) {
 | |
| 	n := len(dt)
 | |
| 	if len(v.unparsed) > 0 {
 | |
| 		dt = append(v.unparsed, dt...) // this almost never happens
 | |
| 		v.unparsed = nil
 | |
| 	}
 | |
| 	buf := bytes.NewBuffer(dt)
 | |
| 	for {
 | |
| 		if buf.Len() == 0 {
 | |
| 			return n, nil
 | |
| 		}
 | |
| 		cmd, err := Decode(buf)
 | |
| 		if err != nil {
 | |
| 			if l := buf.Len(); l > 0 && l < 12 { // on small leftover handle unparsed, otherwise skip
 | |
| 				v.unparsed = buf.Bytes()
 | |
| 			}
 | |
| 			return n, nil
 | |
| 		}
 | |
| 		v.Process(cmd) // ignore error
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Process handles a single ANSI terminal command, updating the terminal
 | |
| // appropriately.
 | |
| //
 | |
| // One special kind of error that this can return is an UnsupportedError. It's
 | |
| // probably best to check for these and skip, because they are likely recoverable.
 | |
| // Support errors are exported as expvars, so it is possibly not necessary to log
 | |
| // them. If you want to check what's failed, start a debug http server and examine
 | |
| // the vt100-unsupported-commands field in /debug/vars.
 | |
| func (v *VT100) Process(c Command) error {
 | |
| 	return c.display(v)
 | |
| }
 | |
| 
 | |
| // HTML renders v as an HTML fragment. One idea for how to use this is to debug
 | |
| // the current state of the screen reader.
 | |
| func (v *VT100) HTML() string {
 | |
| 	var buf bytes.Buffer
 | |
| 	buf.WriteString(`<pre style="color:white;background-color:black;">`)
 | |
| 
 | |
| 	// Iterate each row. When the css changes, close the previous span, and open
 | |
| 	// a new one. No need to close a span when the css is empty, we won't have
 | |
| 	// opened one in the past.
 | |
| 	var lastFormat Format
 | |
| 	for y, row := range v.Content {
 | |
| 		for x, r := range row {
 | |
| 			f := v.Format[y][x]
 | |
| 			if f != lastFormat {
 | |
| 				if lastFormat != (Format{}) {
 | |
| 					buf.WriteString("</span>")
 | |
| 				}
 | |
| 				if f != (Format{}) {
 | |
| 					buf.WriteString(`<span style="` + f.css() + `">`)
 | |
| 				}
 | |
| 				lastFormat = f
 | |
| 			}
 | |
| 			if s := maybeEscapeRune(r); s != "" {
 | |
| 				buf.WriteString(s)
 | |
| 			} else {
 | |
| 				buf.WriteRune(r)
 | |
| 			}
 | |
| 		}
 | |
| 		buf.WriteRune('\n')
 | |
| 	}
 | |
| 	buf.WriteString("</pre>")
 | |
| 
 | |
| 	return buf.String()
 | |
| }
 | |
| 
 | |
| // maybeEscapeRune potentially escapes a rune for display in an html document.
 | |
| // It only escapes the things that html.EscapeString does, but it works without allocating
 | |
| // a string to hold r. Returns an empty string if there is no need to escape.
 | |
| func maybeEscapeRune(r rune) string {
 | |
| 	switch r {
 | |
| 	case '&':
 | |
| 		return "&"
 | |
| 	case '\'':
 | |
| 		return "'"
 | |
| 	case '<':
 | |
| 		return "<"
 | |
| 	case '>':
 | |
| 		return ">"
 | |
| 	case '"':
 | |
| 		return """
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| // put puts r onto the current cursor's position, then advances the cursor.
 | |
| func (v *VT100) put(r rune) {
 | |
| 	v.scrollIfNeeded()
 | |
| 	v.Content[v.Cursor.Y][v.Cursor.X] = r
 | |
| 	v.Format[v.Cursor.Y][v.Cursor.X] = v.Cursor.F
 | |
| 	v.advance()
 | |
| }
 | |
| 
 | |
| // advance advances the cursor, wrapping to the next line if need be.
 | |
| func (v *VT100) advance() {
 | |
| 	v.Cursor.X++
 | |
| 	if v.Cursor.X >= v.Width {
 | |
| 		v.Cursor.X = 0
 | |
| 		v.Cursor.Y++
 | |
| 	}
 | |
| 	// if v.Cursor.Y >= v.Height {
 | |
| 	// 	// TODO(jaguilar): if we implement scroll, this should probably scroll.
 | |
| 	// 	// v.Cursor.Y = 0
 | |
| 	// 	v.scroll()
 | |
| 	// }
 | |
| }
 | |
| 
 | |
| func (v *VT100) scrollIfNeeded() {
 | |
| 	if v.Cursor.Y >= v.Height {
 | |
| 		first := v.Content[0]
 | |
| 		copy(v.Content, v.Content[1:])
 | |
| 		for i := range first {
 | |
| 			first[i] = ' '
 | |
| 		}
 | |
| 		v.Content[v.Height-1] = first
 | |
| 		v.Cursor.Y = v.Height - 1
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // home moves the cursor to the coordinates y x. If y x are out of bounds, v.Err
 | |
| // is set.
 | |
| func (v *VT100) home(y, x int) {
 | |
| 	v.Cursor.Y, v.Cursor.X = y, x
 | |
| }
 | |
| 
 | |
| // eraseDirection is the logical direction in which an erase command happens,
 | |
| // from the cursor. For both erase commands, forward is 0, backward is 1,
 | |
| // and everything is 2.
 | |
| type eraseDirection int
 | |
| 
 | |
| const (
 | |
| 	// From the cursor to the end, inclusive.
 | |
| 	eraseForward eraseDirection = iota
 | |
| 
 | |
| 	// From the beginning to the cursor, inclusive.
 | |
| 	eraseBack
 | |
| 
 | |
| 	// Everything.
 | |
| 	eraseAll
 | |
| )
 | |
| 
 | |
| // eraseColumns erases columns from the current line.
 | |
| func (v *VT100) eraseColumns(d eraseDirection) {
 | |
| 	y, x := v.Cursor.Y, v.Cursor.X // Aliases for simplicity.
 | |
| 	switch d {
 | |
| 	case eraseBack:
 | |
| 		v.eraseRegion(y, 0, y, x)
 | |
| 	case eraseForward:
 | |
| 		v.eraseRegion(y, x, y, v.Width-1)
 | |
| 	case eraseAll:
 | |
| 		v.eraseRegion(y, 0, y, v.Width-1)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // eraseLines erases lines from the current terminal. Note that
 | |
| // no matter what is selected, the entire current line is erased.
 | |
| func (v *VT100) eraseLines(d eraseDirection) {
 | |
| 	y := v.Cursor.Y // Alias for simplicity.
 | |
| 	switch d {
 | |
| 	case eraseBack:
 | |
| 		v.eraseRegion(0, 0, y, v.Width-1)
 | |
| 	case eraseForward:
 | |
| 		v.eraseRegion(y, 0, v.Height-1, v.Width-1)
 | |
| 	case eraseAll:
 | |
| 		v.eraseRegion(0, 0, v.Height-1, v.Width-1)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *VT100) eraseRegion(y1, x1, y2, x2 int) {
 | |
| 	// Do not sanitize or bounds-check these coordinates, since they come from the
 | |
| 	// programmer (me). We should panic if any of them are out of bounds.
 | |
| 	if y1 > y2 {
 | |
| 		y1, y2 = y2, y1
 | |
| 	}
 | |
| 	if x1 > x2 {
 | |
| 		x1, x2 = x2, x1
 | |
| 	}
 | |
| 
 | |
| 	for y := y1; y <= y2; y++ {
 | |
| 		for x := x1; x <= x2; x++ {
 | |
| 			v.clear(y, x)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *VT100) clear(y, x int) {
 | |
| 	if y >= len(v.Content) || x >= len(v.Content[0]) {
 | |
| 		return
 | |
| 	}
 | |
| 	v.Content[y][x] = ' '
 | |
| 	v.Format[y][x] = Format{}
 | |
| }
 | |
| 
 | |
| func (v *VT100) backspace() {
 | |
| 	v.Cursor.X--
 | |
| 	if v.Cursor.X < 0 {
 | |
| 		if v.Cursor.Y == 0 {
 | |
| 			v.Cursor.X = 0
 | |
| 		} else {
 | |
| 			v.Cursor.Y--
 | |
| 			v.Cursor.X = v.Width - 1
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (v *VT100) save() {
 | |
| 	v.savedCursor = v.Cursor
 | |
| }
 | |
| 
 | |
| func (v *VT100) unsave() {
 | |
| 	v.Cursor = v.savedCursor
 | |
| }
 | 
