mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
f02221a794
Although our use of ANSI codes here is rather simple it is generally good practice to use terminfo in order to be portable to different terminal emulators. Vendor github.com/Nvveen/Gotty (actually my fork with a fix, see https://github.com/Nvveen/Gotty/pull/1) and use that to parse the terminfo files. Note that "\e]2K" (clear entire line) is not covered by terminfo. We can achieve the same end by first clearing from begining of line to cursor (el1="\e]1K") and then clearing from cursor to end of line (el="\e]k"). Test suite has been updated and forces (either directly or by setting $TERM to something highly unlikely to exist) the use of the non-terminfo fallbacks which retains the same output behaviour as previously. This is preferable even to relying on a well-known and relatively static terminfo (like vt102) since even that in principal might have different terminfo encodings. In case terminfo is not available at all for $TERM or doesn't expose the specific capabilities which we use then fall back to the previous manual escapes, with the exception that we avoid "\e]2K" as discussed above. Tested with a manual docker pull with rxvt-unicode ($TERM=rxvt-unicode), xterm ($TERM=xterm), mlterm ($TERM=mlterm) and aterm ($TERM=kterm). Signed-off-by: Ian Campbell <ian.campbell@docker.com>
246 lines
6.7 KiB
Go
246 lines
6.7 KiB
Go
// Copyright 2012 Neal van Veen. All rights reserved.
|
|
// Usage of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// Gotty is a Go-package for reading and parsing the terminfo database
|
|
package gotty
|
|
|
|
// TODO add more concurrency to name lookup, look for more opportunities.
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Open a terminfo file by the name given and construct a TermInfo object.
|
|
// If something went wrong reading the terminfo database file, an error is
|
|
// returned.
|
|
func OpenTermInfo(termName string) (*TermInfo, error) {
|
|
var term *TermInfo
|
|
var err error
|
|
// Find the environment variables
|
|
termloc := os.Getenv("TERMINFO")
|
|
if len(termloc) == 0 {
|
|
// Search like ncurses
|
|
locations := []string{os.Getenv("HOME") + "/.terminfo/", "/etc/terminfo/",
|
|
"/lib/terminfo/", "/usr/share/terminfo/"}
|
|
var path string
|
|
for _, str := range locations {
|
|
// Construct path
|
|
path = str + string(termName[0]) + "/" + termName
|
|
// Check if path can be opened
|
|
file, _ := os.Open(path)
|
|
if file != nil {
|
|
// Path can open, fall out and use current path
|
|
file.Close()
|
|
break
|
|
}
|
|
}
|
|
if len(path) > 0 {
|
|
term, err = readTermInfo(path)
|
|
} else {
|
|
err = errors.New(fmt.Sprintf("No terminfo file(-location) found"))
|
|
}
|
|
}
|
|
return term, err
|
|
}
|
|
|
|
// Open a terminfo file from the environment variable containing the current
|
|
// terminal name and construct a TermInfo object. If something went wrong
|
|
// reading the terminfo database file, an error is returned.
|
|
func OpenTermInfoEnv() (*TermInfo, error) {
|
|
termenv := os.Getenv("TERM")
|
|
return OpenTermInfo(termenv)
|
|
}
|
|
|
|
// Return an attribute by the name attr provided. If none can be found,
|
|
// an error is returned.
|
|
func (term *TermInfo) GetAttribute(attr string) (stacker, error) {
|
|
// Channel to store the main value in.
|
|
var value stacker
|
|
// Add a blocking WaitGroup
|
|
var block sync.WaitGroup
|
|
// Keep track of variable being written.
|
|
written := false
|
|
// Function to put into goroutine.
|
|
f := func(ats interface{}) {
|
|
var ok bool
|
|
var v stacker
|
|
// Switch on type of map to use and assign value to it.
|
|
switch reflect.TypeOf(ats).Elem().Kind() {
|
|
case reflect.Bool:
|
|
v, ok = ats.(map[string]bool)[attr]
|
|
case reflect.Int16:
|
|
v, ok = ats.(map[string]int16)[attr]
|
|
case reflect.String:
|
|
v, ok = ats.(map[string]string)[attr]
|
|
}
|
|
// If ok, a value is found, so we can write.
|
|
if ok {
|
|
value = v
|
|
written = true
|
|
}
|
|
// Goroutine is done
|
|
block.Done()
|
|
}
|
|
block.Add(3)
|
|
// Go for all 3 attribute lists.
|
|
go f(term.boolAttributes)
|
|
go f(term.numAttributes)
|
|
go f(term.strAttributes)
|
|
// Wait until every goroutine is done.
|
|
block.Wait()
|
|
// If a value has been written, return it.
|
|
if written {
|
|
return value, nil
|
|
}
|
|
// Otherwise, error.
|
|
return nil, fmt.Errorf("Erorr finding attribute")
|
|
}
|
|
|
|
// Return an attribute by the name attr provided. If none can be found,
|
|
// an error is returned. A name is first converted to its termcap value.
|
|
func (term *TermInfo) GetAttributeName(name string) (stacker, error) {
|
|
tc := GetTermcapName(name)
|
|
return term.GetAttribute(tc)
|
|
}
|
|
|
|
// A utility function that finds and returns the termcap equivalent of a
|
|
// variable name.
|
|
func GetTermcapName(name string) string {
|
|
// Termcap name
|
|
var tc string
|
|
// Blocking group
|
|
var wait sync.WaitGroup
|
|
// Function to put into a goroutine
|
|
f := func(attrs []string) {
|
|
// Find the string corresponding to the name
|
|
for i, s := range attrs {
|
|
if s == name {
|
|
tc = attrs[i+1]
|
|
}
|
|
}
|
|
// Goroutine is finished
|
|
wait.Done()
|
|
}
|
|
wait.Add(3)
|
|
// Go for all 3 attribute lists
|
|
go f(BoolAttr[:])
|
|
go f(NumAttr[:])
|
|
go f(StrAttr[:])
|
|
// Wait until every goroutine is done
|
|
wait.Wait()
|
|
// Return the termcap name
|
|
return tc
|
|
}
|
|
|
|
// This function takes a path to a terminfo file and reads it in binary
|
|
// form to construct the actual TermInfo file.
|
|
func readTermInfo(path string) (*TermInfo, error) {
|
|
// Open the terminfo file
|
|
file, err := os.Open(path)
|
|
defer file.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// magic, nameSize, boolSize, nrSNum, nrOffsetsStr, strSize
|
|
// Header is composed of the magic 0432 octal number, size of the name
|
|
// section, size of the boolean section, the amount of number values,
|
|
// the number of offsets of strings, and the size of the string section.
|
|
var header [6]int16
|
|
// Byte array is used to read in byte values
|
|
var byteArray []byte
|
|
// Short array is used to read in short values
|
|
var shArray []int16
|
|
// TermInfo object to store values
|
|
var term TermInfo
|
|
|
|
// Read in the header
|
|
err = binary.Read(file, binary.LittleEndian, &header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// If magic number isn't there or isn't correct, we have the wrong filetype
|
|
if header[0] != 0432 {
|
|
return nil, errors.New(fmt.Sprintf("Wrong filetype"))
|
|
}
|
|
|
|
// Read in the names
|
|
byteArray = make([]byte, header[1])
|
|
err = binary.Read(file, binary.LittleEndian, &byteArray)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
term.Names = strings.Split(string(byteArray), "|")
|
|
|
|
// Read in the booleans
|
|
byteArray = make([]byte, header[2])
|
|
err = binary.Read(file, binary.LittleEndian, &byteArray)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
term.boolAttributes = make(map[string]bool)
|
|
for i, b := range byteArray {
|
|
if b == 1 {
|
|
term.boolAttributes[BoolAttr[i*2+1]] = true
|
|
}
|
|
}
|
|
// If the number of bytes read is not even, a byte for alignment is added
|
|
// We know the header is an even number of bytes so only need to check the
|
|
// total of the names and booleans.
|
|
if (header[1]+header[2])%2 != 0 {
|
|
err = binary.Read(file, binary.LittleEndian, make([]byte, 1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Read in shorts
|
|
shArray = make([]int16, header[3])
|
|
err = binary.Read(file, binary.LittleEndian, &shArray)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
term.numAttributes = make(map[string]int16)
|
|
for i, n := range shArray {
|
|
if n != 0377 && n > -1 {
|
|
term.numAttributes[NumAttr[i*2+1]] = n
|
|
}
|
|
}
|
|
|
|
// Read the offsets into the short array
|
|
shArray = make([]int16, header[4])
|
|
err = binary.Read(file, binary.LittleEndian, &shArray)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Read the actual strings in the byte array
|
|
byteArray = make([]byte, header[5])
|
|
err = binary.Read(file, binary.LittleEndian, &byteArray)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
term.strAttributes = make(map[string]string)
|
|
// We get an offset, and then iterate until the string is null-terminated
|
|
for i, offset := range shArray {
|
|
if offset > -1 {
|
|
if int(offset) >= len(byteArray) {
|
|
return nil, errors.New("array out of bounds reading string section")
|
|
}
|
|
r := bytes.IndexByte(byteArray[offset:], 0)
|
|
if r == -1 {
|
|
return nil, errors.New("missing nul byte reading string section")
|
|
}
|
|
r += int(offset)
|
|
term.strAttributes[StrAttr[i*2+1]] = string(byteArray[offset:r])
|
|
}
|
|
}
|
|
return &term, nil
|
|
}
|