// Package terminfo implements reading terminfo files in pure go. package terminfo import ( "io" "io/ioutil" "path" "strconv" "strings" ) // Error is a terminfo error. type Error string // Error satisfies the error interface. func (err Error) Error() string { return string(err) } const ( // ErrInvalidFileSize is the invalid file size error. ErrInvalidFileSize Error = "invalid file size" // ErrUnexpectedFileEnd is the unexpected file end error. ErrUnexpectedFileEnd Error = "unexpected file end" // ErrInvalidStringTable is the invalid string table error. ErrInvalidStringTable Error = "invalid string table" // ErrInvalidMagic is the invalid magic error. ErrInvalidMagic Error = "invalid magic" // ErrInvalidHeader is the invalid header error. ErrInvalidHeader Error = "invalid header" // ErrInvalidNames is the invalid names error. ErrInvalidNames Error = "invalid names" // ErrInvalidExtendedHeader is the invalid extended header error. ErrInvalidExtendedHeader Error = "invalid extended header" // ErrEmptyTermName is the empty term name error. ErrEmptyTermName Error = "empty term name" // ErrDatabaseDirectoryNotFound is the database directory not found error. ErrDatabaseDirectoryNotFound Error = "database directory not found" // ErrFileNotFound is the file not found error. ErrFileNotFound Error = "file not found" // ErrInvalidTermProgramVersion is the invalid TERM_PROGRAM_VERSION error. ErrInvalidTermProgramVersion Error = "invalid TERM_PROGRAM_VERSION" ) // Terminfo describes a terminal's capabilities. type Terminfo struct { // File is the original source file. File string // Names are the provided cap names. Names []string // Bools are the bool capabilities. Bools map[int]bool // BoolsM are the missing bool capabilities. BoolsM map[int]bool // Nums are the num capabilities. Nums map[int]int // NumsM are the missing num capabilities. NumsM map[int]bool // Strings are the string capabilities. Strings map[int][]byte // StringsM are the missing string capabilities. StringsM map[int]bool // ExtBools are the extended bool capabilities. ExtBools map[int]bool // ExtBoolsNames is the map of extended bool capabilities to their index. ExtBoolNames map[int][]byte // ExtNums are the extended num capabilities. ExtNums map[int]int // ExtNumsNames is the map of extended num capabilities to their index. ExtNumNames map[int][]byte // ExtStrings are the extended string capabilities. ExtStrings map[int][]byte // ExtStringsNames is the map of extended string capabilities to their index. ExtStringNames map[int][]byte } // Decode decodes the terminfo data contained in buf. func Decode(buf []byte) (*Terminfo, error) { var err error // check max file length if len(buf) >= maxFileLength { return nil, ErrInvalidFileSize } d := &decoder{ buf: buf, len: len(buf), } // read header h, err := d.readInts(6, 16) if err != nil { return nil, err } var numWidth int // check magic if h[fieldMagic] == magic { numWidth = 16 } else if h[fieldMagic] == magicExtended { numWidth = 32 } else { return nil, ErrInvalidMagic } // check header if hasInvalidCaps(h) { return nil, ErrInvalidHeader } // check remaining length if d.len-d.pos < capLength(h) { return nil, ErrUnexpectedFileEnd } // read names names, err := d.readBytes(h[fieldNameSize]) if err != nil { return nil, err } // check name is terminated properly i := findNull(names, 0) if i == -1 { return nil, ErrInvalidNames } names = names[:i] // read bool caps bools, boolsM, err := d.readBools(h[fieldBoolCount]) if err != nil { return nil, err } // read num caps nums, numsM, err := d.readNums(h[fieldNumCount], numWidth) if err != nil { return nil, err } // read string caps strs, strsM, err := d.readStrings(h[fieldStringCount], h[fieldTableSize]) if err != nil { return nil, err } ti := &Terminfo{ Names: strings.Split(string(names), "|"), Bools: bools, BoolsM: boolsM, Nums: nums, NumsM: numsM, Strings: strs, StringsM: strsM, } // at the end of file, so no extended caps if d.pos >= d.len { return ti, nil } // decode extended header eh, err := d.readInts(5, 16) if err != nil { return nil, err } // check extended offset field if hasInvalidExtOffset(eh) { return nil, ErrInvalidExtendedHeader } // check extended cap lengths if d.len-d.pos != extCapLength(eh, numWidth) { return nil, ErrInvalidExtendedHeader } // read extended bool caps ti.ExtBools, _, err = d.readBools(eh[fieldExtBoolCount]) if err != nil { return nil, err } // read extended num caps ti.ExtNums, _, err = d.readNums(eh[fieldExtNumCount], numWidth) if err != nil { return nil, err } // read extended string data table indexes extIndexes, err := d.readInts(eh[fieldExtOffsetCount], 16) if err != nil { return nil, err } // read string data table extData, err := d.readBytes(eh[fieldExtTableSize]) if err != nil { return nil, err } // precautionary check that exactly at end of file if d.pos != d.len { return nil, ErrUnexpectedFileEnd } var last int // read extended string caps ti.ExtStrings, last, err = readStrings(extIndexes, extData, eh[fieldExtStringCount]) if err != nil { return nil, err } extIndexes, extData = extIndexes[eh[fieldExtStringCount]:], extData[last:] // read extended bool names ti.ExtBoolNames, _, err = readStrings(extIndexes, extData, eh[fieldExtBoolCount]) if err != nil { return nil, err } extIndexes = extIndexes[eh[fieldExtBoolCount]:] // read extended num names ti.ExtNumNames, _, err = readStrings(extIndexes, extData, eh[fieldExtNumCount]) if err != nil { return nil, err } extIndexes = extIndexes[eh[fieldExtNumCount]:] // read extended string names ti.ExtStringNames, _, err = readStrings(extIndexes, extData, eh[fieldExtStringCount]) if err != nil { return nil, err } //extIndexes = extIndexes[eh[fieldExtStringCount]:] return ti, nil } // Open reads the terminfo file name from the specified directory dir. func Open(dir, name string) (*Terminfo, error) { var err error var buf []byte var filename string for _, f := range []string{ path.Join(dir, name[0:1], name), path.Join(dir, strconv.FormatUint(uint64(name[0]), 16), name), } { buf, err = ioutil.ReadFile(f) if err == nil { filename = f break } } if buf == nil { return nil, ErrFileNotFound } // decode ti, err := Decode(buf) if err != nil { return nil, err } // save original file name ti.File = filename // add to cache termCache.Lock() for _, n := range ti.Names { termCache.db[n] = ti } termCache.Unlock() return ti, nil } // boolCaps returns all bool and extended capabilities using f to format the // index key. func (ti *Terminfo) boolCaps(f func(int) string, extended bool) map[string]bool { m := make(map[string]bool, len(ti.Bools)+len(ti.ExtBools)) if !extended { for k, v := range ti.Bools { m[f(k)] = v } } else { for k, v := range ti.ExtBools { m[string(ti.ExtBoolNames[k])] = v } } return m } // BoolCaps returns all bool capabilities. func (ti *Terminfo) BoolCaps() map[string]bool { return ti.boolCaps(BoolCapName, false) } // BoolCapsShort returns all bool capabilities, using the short name as the // index. func (ti *Terminfo) BoolCapsShort() map[string]bool { return ti.boolCaps(BoolCapNameShort, false) } // ExtBoolCaps returns all extended bool capabilities. func (ti *Terminfo) ExtBoolCaps() map[string]bool { return ti.boolCaps(BoolCapName, true) } // ExtBoolCapsShort returns all extended bool capabilities, using the short // name as the index. func (ti *Terminfo) ExtBoolCapsShort() map[string]bool { return ti.boolCaps(BoolCapNameShort, true) } // numCaps returns all num and extended capabilities using f to format the // index key. func (ti *Terminfo) numCaps(f func(int) string, extended bool) map[string]int { m := make(map[string]int, len(ti.Nums)+len(ti.ExtNums)) if !extended { for k, v := range ti.Nums { m[f(k)] = v } } else { for k, v := range ti.ExtNums { m[string(ti.ExtNumNames[k])] = v } } return m } // NumCaps returns all num capabilities. func (ti *Terminfo) NumCaps() map[string]int { return ti.numCaps(NumCapName, false) } // NumCapsShort returns all num capabilities, using the short name as the // index. func (ti *Terminfo) NumCapsShort() map[string]int { return ti.numCaps(NumCapNameShort, false) } // ExtNumCaps returns all extended num capabilities. func (ti *Terminfo) ExtNumCaps() map[string]int { return ti.numCaps(NumCapName, true) } // ExtNumCapsShort returns all extended num capabilities, using the short // name as the index. func (ti *Terminfo) ExtNumCapsShort() map[string]int { return ti.numCaps(NumCapNameShort, true) } // stringCaps returns all string and extended capabilities using f to format the // index key. func (ti *Terminfo) stringCaps(f func(int) string, extended bool) map[string][]byte { m := make(map[string][]byte, len(ti.Strings)+len(ti.ExtStrings)) if !extended { for k, v := range ti.Strings { m[f(k)] = v } } else { for k, v := range ti.ExtStrings { m[string(ti.ExtStringNames[k])] = v } } return m } // StringCaps returns all string capabilities. func (ti *Terminfo) StringCaps() map[string][]byte { return ti.stringCaps(StringCapName, false) } // StringCapsShort returns all string capabilities, using the short name as the // index. func (ti *Terminfo) StringCapsShort() map[string][]byte { return ti.stringCaps(StringCapNameShort, false) } // ExtStringCaps returns all extended string capabilities. func (ti *Terminfo) ExtStringCaps() map[string][]byte { return ti.stringCaps(StringCapName, true) } // ExtStringCapsShort returns all extended string capabilities, using the short // name as the index. func (ti *Terminfo) ExtStringCapsShort() map[string][]byte { return ti.stringCaps(StringCapNameShort, true) } // Has determines if the bool cap i is present. func (ti *Terminfo) Has(i int) bool { return ti.Bools[i] } // Num returns the num cap i, or -1 if not present. func (ti *Terminfo) Num(i int) int { n, ok := ti.Nums[i] if !ok { return -1 } return n } // Printf formats the string cap i, interpolating parameters v. func (ti *Terminfo) Printf(i int, v ...interface{}) string { return Printf(ti.Strings[i], v...) } // Fprintf prints the string cap i to writer w, interpolating parameters v. func (ti *Terminfo) Fprintf(w io.Writer, i int, v ...interface{}) { Fprintf(w, ti.Strings[i], v...) } // Color takes a foreground and background color and returns string that sets // them for this terminal. func (ti *Terminfo) Colorf(fg, bg int, str string) string { maxColors := int(ti.Nums[MaxColors]) // map bright colors to lower versions if the color table only holds 8. if maxColors == 8 { if fg > 7 && fg < 16 { fg -= 8 } if bg > 7 && bg < 16 { bg -= 8 } } var s string if maxColors > fg && fg >= 0 { s += ti.Printf(SetAForeground, fg) } if maxColors > bg && bg >= 0 { s += ti.Printf(SetABackground, bg) } return s + str + ti.Printf(ExitAttributeMode) } // Goto returns a string suitable for addressing the cursor at the given // row and column. The origin 0, 0 is in the upper left corner of the screen. func (ti *Terminfo) Goto(row, col int) string { return Printf(ti.Strings[CursorAddress], row, col) } // Puts emits the string to the writer, but expands inline padding indications // (of the form $<[delay]> where [delay] is msec) to a suitable number of // padding characters (usually null bytes) based upon the supplied baud. At // high baud rates, more padding characters will be inserted. /*func (ti *Terminfo) Puts(w io.Writer, s string, lines, baud int) (int, error) { var err error for { start := strings.Index(s, "$<") if start == -1 { // most strings don't need padding, which is good news! return io.WriteString(w, s) } end := strings.Index(s, ">") if end == -1 { // unterminated... just emit bytes unadulterated. return io.WriteString(w, "$<"+s) } var c int c, err = io.WriteString(w, s[:start]) if err != nil { return n + c, err } n += c s = s[start+2:] val := s[:end] s = s[end+1:] var ms int var dot, mandatory, asterisk bool unit := 1000 for _, ch := range val { switch { case ch >= '0' && ch <= '9': ms = (ms * 10) + int(ch-'0') if dot { unit *= 10 } case ch == '.' && !dot: dot = true case ch == '*' && !asterisk: ms *= lines asterisk = true case ch == '/': mandatory = true default: break } } z, pad := ((baud/8)/unit)*ms, ti.Strings[PadChar] b := make([]byte, len(pad)*z) for bp := copy(b, pad); bp < len(b); bp *= 2 { copy(b[bp:], b[:bp]) } if (!ti.Bools[XonXoff] && baud > int(ti.Nums[PaddingBaudRate])) || mandatory { c, err = w.Write(b) if err != nil { return n + c, err } n += c } } return n, nil }*/