]> git.lizzy.rs Git - micro.git/commitdiff
Update to use new mkinfo from tcell
authorZachary Yedidia <zyedidia@gmail.com>
Tue, 30 Jan 2018 04:36:39 +0000 (23:36 -0500)
committerZachary Yedidia <zyedidia@gmail.com>
Tue, 30 Jan 2018 04:36:39 +0000 (23:36 -0500)
This update incorporates the new terminfo updates in tcell into micro
essentially merging zyedidia/mkinfo into micro. The zyedidia/mkinfo
program should no longer be necessary and micro should automatically
generate a tcell database on its own if it cannot find a terminal
entry. The tcell database will be located in `configDir/.tcelldb`.

Ref #20
Ref #922

cmd/micro/micro.go
cmd/micro/terminfo/README.md [new file with mode: 0644]
cmd/micro/terminfo/mkinfo.go [new file with mode: 0644]
cmd/micro/terminfo/terminfo.go [new file with mode: 0644]
cmd/micro/terminfo/terminfo_test.go [new file with mode: 0644]

index ad6c461417c29a7fe00e528b2a8fe245a92fc78d..b4d129a11601827e0260ada46abf4ab92524fdc5 100644 (file)
@@ -14,6 +14,7 @@ import (
        "github.com/mitchellh/go-homedir"
        "github.com/yuin/gopher-lua"
        "github.com/zyedidia/clipboard"
+       "github.com/zyedidia/micro/cmd/micro/terminfo"
        "github.com/zyedidia/tcell"
        "github.com/zyedidia/tcell/encoding"
        "layeh.com/gopher-luar"
@@ -183,6 +184,9 @@ func InitScreen() {
        // Should we enable true color?
        truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
 
+       tcelldb := os.Getenv("TCELLDB")
+       os.Setenv("TCELLDB", configDir+"/.tcelldb")
+
        // In order to enable true color, we have to set the TERM to `xterm-truecolor` when
        // initializing tcell, but after that, we can set the TERM back to whatever it was
        oldTerm := os.Getenv("TERM")
@@ -194,12 +198,15 @@ func InitScreen() {
        var err error
        screen, err = tcell.NewScreen()
        if err != nil {
-               fmt.Println(err)
                if err == tcell.ErrTermNotFound {
-                       fmt.Println("Micro does not recognize your terminal:", oldTerm)
-                       fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
+                       terminfo.WriteDB(configDir + "/.tcelldb")
+                       screen, err = tcell.NewScreen()
+                       if err != nil {
+                               fmt.Println(err)
+                               fmt.Println("Fatal: Micro could not initialize a screen.")
+                               os.Exit(1)
+                       }
                }
-               os.Exit(1)
        }
        if err = screen.Init(); err != nil {
                fmt.Println(err)
@@ -215,6 +222,8 @@ func InitScreen() {
                screen.EnableMouse()
        }
 
+       os.Setenv("TCELLDB", tcelldb)
+
        // screen.SetStyle(defStyle)
 }
 
diff --git a/cmd/micro/terminfo/README.md b/cmd/micro/terminfo/README.md
new file mode 100644 (file)
index 0000000..957eb4f
--- /dev/null
@@ -0,0 +1,6 @@
+# Terminfo parser
+
+This terminfo parser was written by the authors of [tcell](github.com/gdamore/tcell). We are using it here
+to compile the terminal database if the terminal entry is not found in set of precompiled terminals.
+
+The source for `mkinfo.go` is adapted from tcell's `mkinfo` tool to be more of a library.
diff --git a/cmd/micro/terminfo/mkinfo.go b/cmd/micro/terminfo/mkinfo.go
new file mode 100644 (file)
index 0000000..88aa65f
--- /dev/null
@@ -0,0 +1,514 @@
+// Copyright 2017 The TCell Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use file except in compliance with the License.
+// You may obtain a copy of the license at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This command is used to generate suitable configuration files in either
+// go syntax or in JSON.  It defaults to JSON output on stdout.  If no
+// term values are specified on the command line, then $TERM is used.
+//
+// Usage is like this:
+//
+// mkinfo [-init] [-go file.go] [-json file.json] [-quiet] [-nofatal] [<term>...]
+//
+// -gzip     specifies output should be compressed (json only)
+// -go       specifies Go output into the named file.  Use - for stdout.
+// -json     specifies JSON output in the named file.  Use - for stdout
+// -nofatal  indicates that errors loading definitions should not be fatal
+//
+
+package terminfo
+
+import (
+       "bytes"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "io"
+       "os"
+       "os/exec"
+       "regexp"
+       "strconv"
+       "strings"
+
+       "github.com/zyedidia/mkinfo/terminfo"
+)
+
+type termcap struct {
+       name    string
+       desc    string
+       aliases []string
+       bools   map[string]bool
+       nums    map[string]int
+       strs    map[string]string
+}
+
+func (tc *termcap) getnum(s string) int {
+       return (tc.nums[s])
+}
+
+func (tc *termcap) getflag(s string) bool {
+       return (tc.bools[s])
+}
+
+func (tc *termcap) getstr(s string) string {
+       return (tc.strs[s])
+}
+
+const (
+       NONE = iota
+       CTRL
+       ESC
+)
+
+func unescape(s string) string {
+       // Various escapes are in \x format.  Control codes are
+       // encoded as ^M (carat followed by ASCII equivalent).
+       // Escapes are: \e, \E - escape
+       //  \0 NULL, \n \l \r \t \b \f \s for equivalent C escape.
+       buf := &bytes.Buffer{}
+       esc := NONE
+
+       for i := 0; i < len(s); i++ {
+               c := s[i]
+               switch esc {
+               case NONE:
+                       switch c {
+                       case '\\':
+                               esc = ESC
+                       case '^':
+                               esc = CTRL
+                       default:
+                               buf.WriteByte(c)
+                       }
+               case CTRL:
+                       buf.WriteByte(c - 0x40)
+                       esc = NONE
+               case ESC:
+                       switch c {
+                       case 'E', 'e':
+                               buf.WriteByte(0x1b)
+                       case '0', '1', '2', '3', '4', '5', '6', '7':
+                               if i+2 < len(s) && s[i+1] >= '0' && s[i+1] <= '7' && s[i+2] >= '0' && s[i+2] <= '7' {
+                                       buf.WriteByte(((c - '0') * 64) + ((s[i+1] - '0') * 8) + (s[i+2] - '0'))
+                                       i = i + 2
+                               } else if c == '0' {
+                                       buf.WriteByte(0)
+                               }
+                       case 'n':
+                               buf.WriteByte('\n')
+                       case 'r':
+                               buf.WriteByte('\r')
+                       case 't':
+                               buf.WriteByte('\t')
+                       case 'b':
+                               buf.WriteByte('\b')
+                       case 'f':
+                               buf.WriteByte('\f')
+                       case 's':
+                               buf.WriteByte(' ')
+                       case 'l':
+                               panic("WTF: weird format: " + s)
+                       default:
+                               buf.WriteByte(c)
+                       }
+                       esc = NONE
+               }
+       }
+       return (buf.String())
+}
+
+func (tc *termcap) setupterm(name string) error {
+       cmd := exec.Command("infocmp", "-1", name)
+       output := &bytes.Buffer{}
+       cmd.Stdout = output
+
+       tc.strs = make(map[string]string)
+       tc.bools = make(map[string]bool)
+       tc.nums = make(map[string]int)
+
+       err := cmd.Run()
+       if err != nil {
+               return err
+       }
+
+       // Now parse the output.
+       // We get comment lines (starting with "#"), followed by
+       // a header line that looks like "<name>|<alias>|...|<desc>"
+       // then capabilities, one per line, starting with a tab and ending
+       // with a comma and newline.
+       lines := strings.Split(output.String(), "\n")
+       for len(lines) > 0 && strings.HasPrefix(lines[0], "#") {
+               lines = lines[1:]
+       }
+
+       // Ditch trailing empty last line
+       if lines[len(lines)-1] == "" {
+               lines = lines[:len(lines)-1]
+       }
+       header := lines[0]
+       if strings.HasSuffix(header, ",") {
+               header = header[:len(header)-1]
+       }
+       names := strings.Split(header, "|")
+       tc.name = names[0]
+       names = names[1:]
+       if len(names) > 0 {
+               tc.desc = names[len(names)-1]
+               names = names[:len(names)-1]
+       }
+       tc.aliases = names
+       for _, val := range lines[1:] {
+               if (!strings.HasPrefix(val, "\t")) ||
+                       (!strings.HasSuffix(val, ",")) {
+                       return (errors.New("malformed infocmp: " + val))
+               }
+
+               val = val[1:]
+               val = val[:len(val)-1]
+
+               if k := strings.SplitN(val, "=", 2); len(k) == 2 {
+                       tc.strs[k[0]] = unescape(k[1])
+               } else if k := strings.SplitN(val, "#", 2); len(k) == 2 {
+                       if u, err := strconv.ParseUint(k[1], 10, 0); err != nil {
+                               return (err)
+                       } else {
+                               tc.nums[k[0]] = int(u)
+                       }
+               } else {
+                       tc.bools[val] = true
+               }
+       }
+       return nil
+}
+
+// This program is used to collect data from the system's terminfo library,
+// and write it into Go source code.  That is, we maintain our terminfo
+// capabilities encoded in the program.  It should never need to be run by
+// an end user, but developers can use this to add codes for additional
+// terminal types.
+//
+// If a terminal name ending with -truecolor is given, and we cannot find
+// one, we will try to fabricate one from either the -256color (if present)
+// or the unadorned base name, adding the XTerm specific 24-bit color
+// escapes.  We believe that all 24-bit capable terminals use the same
+// escape sequences, and terminfo has yet to evolve to support this.
+func getinfo(name string) (*terminfo.Terminfo, string, error) {
+       var tc termcap
+       addTrueColor := false
+       if err := tc.setupterm(name); err != nil {
+               if strings.HasSuffix(name, "-truecolor") {
+                       base := name[:len(name)-len("-truecolor")]
+                       // Probably -256color is closest to what we want
+                       if err = tc.setupterm(base + "-256color"); err != nil {
+                               err = tc.setupterm(base)
+                       }
+                       if err == nil {
+                               addTrueColor = true
+                       }
+                       tc.name = name
+               }
+               if err != nil {
+                       return nil, "", err
+               }
+       }
+       t := &terminfo.Terminfo{}
+       // If this is an alias record, then just emit the alias
+       t.Name = tc.name
+       if t.Name != name {
+               return t, "", nil
+       }
+       t.Aliases = tc.aliases
+       t.Colors = tc.getnum("colors")
+       t.Columns = tc.getnum("cols")
+       t.Lines = tc.getnum("lines")
+       t.Bell = tc.getstr("bel")
+       t.Clear = tc.getstr("clear")
+       t.EnterCA = tc.getstr("smcup")
+       t.ExitCA = tc.getstr("rmcup")
+       t.ShowCursor = tc.getstr("cnorm")
+       t.HideCursor = tc.getstr("civis")
+       t.AttrOff = tc.getstr("sgr0")
+       t.Underline = tc.getstr("smul")
+       t.Bold = tc.getstr("bold")
+       t.Blink = tc.getstr("blink")
+       t.Dim = tc.getstr("dim")
+       t.Reverse = tc.getstr("rev")
+       t.EnterKeypad = tc.getstr("smkx")
+       t.ExitKeypad = tc.getstr("rmkx")
+       t.SetFg = tc.getstr("setaf")
+       t.SetBg = tc.getstr("setab")
+       t.SetCursor = tc.getstr("cup")
+       t.CursorBack1 = tc.getstr("cub1")
+       t.CursorUp1 = tc.getstr("cuu1")
+       t.KeyF1 = tc.getstr("kf1")
+       t.KeyF2 = tc.getstr("kf2")
+       t.KeyF3 = tc.getstr("kf3")
+       t.KeyF4 = tc.getstr("kf4")
+       t.KeyF5 = tc.getstr("kf5")
+       t.KeyF6 = tc.getstr("kf6")
+       t.KeyF7 = tc.getstr("kf7")
+       t.KeyF8 = tc.getstr("kf8")
+       t.KeyF9 = tc.getstr("kf9")
+       t.KeyF10 = tc.getstr("kf10")
+       t.KeyF11 = tc.getstr("kf11")
+       t.KeyF12 = tc.getstr("kf12")
+       t.KeyF13 = tc.getstr("kf13")
+       t.KeyF14 = tc.getstr("kf14")
+       t.KeyF15 = tc.getstr("kf15")
+       t.KeyF16 = tc.getstr("kf16")
+       t.KeyF17 = tc.getstr("kf17")
+       t.KeyF18 = tc.getstr("kf18")
+       t.KeyF19 = tc.getstr("kf19")
+       t.KeyF20 = tc.getstr("kf20")
+       t.KeyF21 = tc.getstr("kf21")
+       t.KeyF22 = tc.getstr("kf22")
+       t.KeyF23 = tc.getstr("kf23")
+       t.KeyF24 = tc.getstr("kf24")
+       t.KeyF25 = tc.getstr("kf25")
+       t.KeyF26 = tc.getstr("kf26")
+       t.KeyF27 = tc.getstr("kf27")
+       t.KeyF28 = tc.getstr("kf28")
+       t.KeyF29 = tc.getstr("kf29")
+       t.KeyF30 = tc.getstr("kf30")
+       t.KeyF31 = tc.getstr("kf31")
+       t.KeyF32 = tc.getstr("kf32")
+       t.KeyF33 = tc.getstr("kf33")
+       t.KeyF34 = tc.getstr("kf34")
+       t.KeyF35 = tc.getstr("kf35")
+       t.KeyF36 = tc.getstr("kf36")
+       t.KeyF37 = tc.getstr("kf37")
+       t.KeyF38 = tc.getstr("kf38")
+       t.KeyF39 = tc.getstr("kf39")
+       t.KeyF40 = tc.getstr("kf40")
+       t.KeyF41 = tc.getstr("kf41")
+       t.KeyF42 = tc.getstr("kf42")
+       t.KeyF43 = tc.getstr("kf43")
+       t.KeyF44 = tc.getstr("kf44")
+       t.KeyF45 = tc.getstr("kf45")
+       t.KeyF46 = tc.getstr("kf46")
+       t.KeyF47 = tc.getstr("kf47")
+       t.KeyF48 = tc.getstr("kf48")
+       t.KeyF49 = tc.getstr("kf49")
+       t.KeyF50 = tc.getstr("kf50")
+       t.KeyF51 = tc.getstr("kf51")
+       t.KeyF52 = tc.getstr("kf52")
+       t.KeyF53 = tc.getstr("kf53")
+       t.KeyF54 = tc.getstr("kf54")
+       t.KeyF55 = tc.getstr("kf55")
+       t.KeyF56 = tc.getstr("kf56")
+       t.KeyF57 = tc.getstr("kf57")
+       t.KeyF58 = tc.getstr("kf58")
+       t.KeyF59 = tc.getstr("kf59")
+       t.KeyF60 = tc.getstr("kf60")
+       t.KeyF61 = tc.getstr("kf61")
+       t.KeyF62 = tc.getstr("kf62")
+       t.KeyF63 = tc.getstr("kf63")
+       t.KeyF64 = tc.getstr("kf64")
+       t.KeyInsert = tc.getstr("kich1")
+       t.KeyDelete = tc.getstr("kdch1")
+       t.KeyBackspace = tc.getstr("kbs")
+       t.KeyHome = tc.getstr("khome")
+       t.KeyEnd = tc.getstr("kend")
+       t.KeyUp = tc.getstr("kcuu1")
+       t.KeyDown = tc.getstr("kcud1")
+       t.KeyRight = tc.getstr("kcuf1")
+       t.KeyLeft = tc.getstr("kcub1")
+       t.KeyPgDn = tc.getstr("knp")
+       t.KeyPgUp = tc.getstr("kpp")
+       t.KeyBacktab = tc.getstr("kcbt")
+       t.KeyExit = tc.getstr("kext")
+       t.KeyCancel = tc.getstr("kcan")
+       t.KeyPrint = tc.getstr("kprt")
+       t.KeyHelp = tc.getstr("khlp")
+       t.KeyClear = tc.getstr("kclr")
+       t.AltChars = tc.getstr("acsc")
+       t.EnterAcs = tc.getstr("smacs")
+       t.ExitAcs = tc.getstr("rmacs")
+       t.EnableAcs = tc.getstr("enacs")
+       t.Mouse = tc.getstr("kmous")
+       t.KeyShfRight = tc.getstr("kRIT")
+       t.KeyShfLeft = tc.getstr("kLFT")
+       t.KeyShfHome = tc.getstr("kHOM")
+       t.KeyShfEnd = tc.getstr("kEND")
+
+       // Terminfo lacks descriptions for a bunch of modified keys,
+       // but modern XTerm and emulators often have them.  Let's add them,
+       // if the shifted right and left arrows are defined.
+       if t.KeyShfRight == "\x1b[1;2C" && t.KeyShfLeft == "\x1b[1;2D" {
+               t.KeyShfUp = "\x1b[1;2A"
+               t.KeyShfDown = "\x1b[1;2B"
+               t.KeyMetaUp = "\x1b[1;9A"
+               t.KeyMetaDown = "\x1b[1;9B"
+               t.KeyMetaRight = "\x1b[1;9C"
+               t.KeyMetaLeft = "\x1b[1;9D"
+               t.KeyAltUp = "\x1b[1;3A"
+               t.KeyAltDown = "\x1b[1;3B"
+               t.KeyAltRight = "\x1b[1;3C"
+               t.KeyAltLeft = "\x1b[1;3D"
+               t.KeyCtrlUp = "\x1b[1;5A"
+               t.KeyCtrlDown = "\x1b[1;5B"
+               t.KeyCtrlRight = "\x1b[1;5C"
+               t.KeyCtrlLeft = "\x1b[1;5D"
+               t.KeyAltShfUp = "\x1b[1;4A"
+               t.KeyAltShfDown = "\x1b[1;4B"
+               t.KeyAltShfRight = "\x1b[1;4C"
+               t.KeyAltShfLeft = "\x1b[1;4D"
+
+               t.KeyMetaShfUp = "\x1b[1;10A"
+               t.KeyMetaShfDown = "\x1b[1;10B"
+               t.KeyMetaShfRight = "\x1b[1;10C"
+               t.KeyMetaShfLeft = "\x1b[1;10D"
+
+               t.KeyCtrlShfUp = "\x1b[1;6A"
+               t.KeyCtrlShfDown = "\x1b[1;6B"
+               t.KeyCtrlShfRight = "\x1b[1;6C"
+               t.KeyCtrlShfLeft = "\x1b[1;6D"
+       }
+       // And also for Home and End
+       if t.KeyShfHome == "\x1b[1;2H" && t.KeyShfEnd == "\x1b[1;2F" {
+               t.KeyCtrlHome = "\x1b[1;5H"
+               t.KeyCtrlEnd = "\x1b[1;5F"
+               t.KeyAltHome = "\x1b[1;9H"
+               t.KeyAltEnd = "\x1b[1;9F"
+               t.KeyCtrlShfHome = "\x1b[1;6H"
+               t.KeyCtrlShfEnd = "\x1b[1;6F"
+               t.KeyAltShfHome = "\x1b[1;4H"
+               t.KeyAltShfEnd = "\x1b[1;4F"
+               t.KeyMetaShfHome = "\x1b[1;10H"
+               t.KeyMetaShfEnd = "\x1b[1;10F"
+       }
+
+       // And the same thing for rxvt and workalikes (Eterm, aterm, etc.)
+       // It seems that urxvt at least send ESC as ALT prefix for these,
+       // although some places seem to indicate a separate ALT key sesquence.
+       if t.KeyShfRight == "\x1b[c" && t.KeyShfLeft == "\x1b[d" {
+               t.KeyShfUp = "\x1b[a"
+               t.KeyShfDown = "\x1b[b"
+               t.KeyCtrlUp = "\x1b[Oa"
+               t.KeyCtrlDown = "\x1b[Ob"
+               t.KeyCtrlRight = "\x1b[Oc"
+               t.KeyCtrlLeft = "\x1b[Od"
+       }
+       if t.KeyShfHome == "\x1b[7$" && t.KeyShfEnd == "\x1b[8$" {
+               t.KeyCtrlHome = "\x1b[7^"
+               t.KeyCtrlEnd = "\x1b[8^"
+       }
+
+       // If the kmous entry is present, then we need to record the
+       // the codes to enter and exit mouse mode.  Sadly, this is not
+       // part of the terminfo databases anywhere that I've found, but
+       // is an extension.  The escape codes are documented in the XTerm
+       // manual, and all terminals that have kmous are expected to
+       // use these same codes, unless explicitly configured otherwise
+       // vi XM.  Note that in any event, we only known how to parse either
+       // x11 or SGR mouse events -- if your terminal doesn't support one
+       // of these two forms, you maybe out of luck.
+       t.MouseMode = tc.getstr("XM")
+       if t.Mouse != "" && t.MouseMode == "" {
+               // we anticipate that all xterm mouse tracking compatible
+               // terminals understand mouse tracking (1000), but we hope
+               // that those that don't understand any-event tracking (1003)
+               // will at least ignore it.  Likewise we hope that terminals
+               // that don't understand SGR reporting (1006) just ignore it.
+               t.MouseMode = "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;" +
+                       "\x1b[?1000%ga%c\x1b[?1002%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c"
+       }
+
+       // We only support colors in ANSI 8 or 256 color mode.
+       if t.Colors < 8 || t.SetFg == "" {
+               t.Colors = 0
+       }
+       if t.SetCursor == "" {
+               return nil, "", errors.New("terminal not cursor addressable")
+       }
+
+       // For padding, we lookup the pad char.  If that isn't present,
+       // and npc is *not* set, then we assume a null byte.
+       t.PadChar = tc.getstr("pad")
+       if t.PadChar == "" {
+               if !tc.getflag("npc") {
+                       t.PadChar = "\u0000"
+               }
+       }
+
+       // For some terminals we fabricate a -truecolor entry, that may
+       // not exist in terminfo.
+       if addTrueColor {
+               t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm"
+               t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm"
+               t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" +
+                       "48;2;%p4%d;%p5%d;%p6%dm"
+       }
+
+       // For terminals that use "standard" SGR sequences, lets combine the
+       // foreground and background together.
+       if strings.HasPrefix(t.SetFg, "\x1b[") &&
+               strings.HasPrefix(t.SetBg, "\x1b[") &&
+               strings.HasSuffix(t.SetFg, "m") &&
+               strings.HasSuffix(t.SetBg, "m") {
+               fg := t.SetFg[:len(t.SetFg)-1]
+               r := regexp.MustCompile("%p1")
+               bg := r.ReplaceAllString(t.SetBg[2:], "%p2")
+               t.SetFgBg = fg + ";" + bg
+       }
+
+       return t, tc.desc, nil
+}
+
+func WriteDB(filename string) error {
+       var e error
+       js := []byte{}
+       args := []string{os.Getenv("TERM")}
+
+       tdata := make(map[string]*terminfo.Terminfo)
+       descs := make(map[string]string)
+
+       for _, term := range args {
+               if t, desc, e := getinfo(term); e != nil {
+                       return e
+               } else {
+                       tdata[term] = t
+                       descs[term] = desc
+               }
+       }
+
+       if len(tdata) == 0 {
+               // No data.
+               return errors.New("No data")
+       }
+       o := os.Stdout
+       if o, e = os.Create(filename); e != nil {
+               return e
+       }
+       var w io.WriteCloser
+       w = o
+       for _, term := range args {
+               if t := tdata[term]; t != nil {
+                       js, e = json.Marshal(t)
+                       fmt.Fprintln(w, string(js))
+               }
+               // arguably if there is more than one term, this
+               // should be a javascript array, but that's not how
+               // we load it.  We marshal objects one at a time from
+               // the file.
+       }
+       if e != nil {
+               return e
+       }
+       w.Close()
+       if w != o {
+               o.Close()
+       }
+
+       return nil
+}
diff --git a/cmd/micro/terminfo/terminfo.go b/cmd/micro/terminfo/terminfo.go
new file mode 100644 (file)
index 0000000..b7fc0fa
--- /dev/null
@@ -0,0 +1,837 @@
+// Copyright 2017 The TCell Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use file except in compliance with the License.
+// You may obtain a copy of the license at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminfo
+
+import (
+       "bytes"
+       "compress/gzip"
+       "encoding/json"
+       "errors"
+       "fmt"
+       "io"
+       "os"
+       "path"
+       "strconv"
+       "strings"
+       "sync"
+)
+
+var (
+       // ErrTermNotFound indicates that a suitable terminal entry could
+       // not be found.  This can result from either not having TERM set,
+       // or from the TERM failing to support certain minimal functionality,
+       // in particular absolute cursor addressability (the cup capability)
+       // is required.  For example, legacy "adm3" lacks this capability,
+       // whereas the slightly newer "adm3a" supports it.  This failure
+       // occurs most often with "dumb".
+       ErrTermNotFound = errors.New("terminal entry not found")
+)
+
+// Terminfo represents a terminfo entry.  Note that we use friendly names
+// in Go, but when we write out JSON, we use the same names as terminfo.
+// The name, aliases and smous, rmous fields do not come from terminfo directly.
+type Terminfo struct {
+       Name         string   `json:"name"`
+       Aliases      []string `json:"aliases,omitempty"`
+       Columns      int      `json:"cols,omitempty"`   // cols
+       Lines        int      `json:"lines,omitempty"`  // lines
+       Colors       int      `json:"colors,omitempty"` // colors
+       Bell         string   `json:"bell,omitempty"`   // bell
+       Clear        string   `json:"clear,omitempty"`  // clear
+       EnterCA      string   `json:"smcup,omitempty"`  // smcup
+       ExitCA       string   `json:"rmcup,omitempty"`  // rmcup
+       ShowCursor   string   `json:"cnorm,omitempty"`  // cnorm
+       HideCursor   string   `json:"civis,omitempty"`  // civis
+       AttrOff      string   `json:"sgr0,omitempty"`   // sgr0
+       Underline    string   `json:"smul,omitempty"`   // smul
+       Bold         string   `json:"bold,omitempty"`   // bold
+       Blink        string   `json:"blink,omitempty"`  // blink
+       Reverse      string   `json:"rev,omitempty"`    // rev
+       Dim          string   `json:"dim,omitempty"`    // dim
+       EnterKeypad  string   `json:"smkx,omitempty"`   // smkx
+       ExitKeypad   string   `json:"rmkx,omitempty"`   // rmkx
+       SetFg        string   `json:"setaf,omitempty"`  // setaf
+       SetBg        string   `json:"setbg,omitempty"`  // setab
+       SetCursor    string   `json:"cup,omitempty"`    // cup
+       CursorBack1  string   `json:"cub1,omitempty"`   // cub1
+       CursorUp1    string   `json:"cuu1,omitempty"`   // cuu1
+       PadChar      string   `json:"pad,omitempty"`    // pad
+       KeyBackspace string   `json:"kbs,omitempty"`    // kbs
+       KeyF1        string   `json:"kf1,omitempty"`    // kf1
+       KeyF2        string   `json:"kf2,omitempty"`    // kf2
+       KeyF3        string   `json:"kf3,omitempty"`    // kf3
+       KeyF4        string   `json:"kf4,omitempty"`    // kf4
+       KeyF5        string   `json:"kf5,omitempty"`    // kf5
+       KeyF6        string   `json:"kf6,omitempty"`    // kf6
+       KeyF7        string   `json:"kf7,omitempty"`    // kf7
+       KeyF8        string   `json:"kf8,omitempty"`    // kf8
+       KeyF9        string   `json:"kf9,omitempty"`    // kf9
+       KeyF10       string   `json:"kf10,omitempty"`   // kf10
+       KeyF11       string   `json:"kf11,omitempty"`   // kf11
+       KeyF12       string   `json:"kf12,omitempty"`   // kf12
+       KeyF13       string   `json:"kf13,omitempty"`   // kf13
+       KeyF14       string   `json:"kf14,omitempty"`   // kf14
+       KeyF15       string   `json:"kf15,omitempty"`   // kf15
+       KeyF16       string   `json:"kf16,omitempty"`   // kf16
+       KeyF17       string   `json:"kf17,omitempty"`   // kf17
+       KeyF18       string   `json:"kf18,omitempty"`   // kf18
+       KeyF19       string   `json:"kf19,omitempty"`   // kf19
+       KeyF20       string   `json:"kf20,omitempty"`   // kf20
+       KeyF21       string   `json:"kf21,omitempty"`   // kf21
+       KeyF22       string   `json:"kf22,omitempty"`   // kf22
+       KeyF23       string   `json:"kf23,omitempty"`   // kf23
+       KeyF24       string   `json:"kf24,omitempty"`   // kf24
+       KeyF25       string   `json:"kf25,omitempty"`   // kf25
+       KeyF26       string   `json:"kf26,omitempty"`   // kf26
+       KeyF27       string   `json:"kf27,omitempty"`   // kf27
+       KeyF28       string   `json:"kf28,omitempty"`   // kf28
+       KeyF29       string   `json:"kf29,omitempty"`   // kf29
+       KeyF30       string   `json:"kf30,omitempty"`   // kf30
+       KeyF31       string   `json:"kf31,omitempty"`   // kf31
+       KeyF32       string   `json:"kf32,omitempty"`   // kf32
+       KeyF33       string   `json:"kf33,omitempty"`   // kf33
+       KeyF34       string   `json:"kf34,omitempty"`   // kf34
+       KeyF35       string   `json:"kf35,omitempty"`   // kf35
+       KeyF36       string   `json:"kf36,omitempty"`   // kf36
+       KeyF37       string   `json:"kf37,omitempty"`   // kf37
+       KeyF38       string   `json:"kf38,omitempty"`   // kf38
+       KeyF39       string   `json:"kf39,omitempty"`   // kf39
+       KeyF40       string   `json:"kf40,omitempty"`   // kf40
+       KeyF41       string   `json:"kf41,omitempty"`   // kf41
+       KeyF42       string   `json:"kf42,omitempty"`   // kf42
+       KeyF43       string   `json:"kf43,omitempty"`   // kf43
+       KeyF44       string   `json:"kf44,omitempty"`   // kf44
+       KeyF45       string   `json:"kf45,omitempty"`   // kf45
+       KeyF46       string   `json:"kf46,omitempty"`   // kf46
+       KeyF47       string   `json:"kf47,omitempty"`   // kf47
+       KeyF48       string   `json:"kf48,omitempty"`   // kf48
+       KeyF49       string   `json:"kf49,omitempty"`   // kf49
+       KeyF50       string   `json:"kf50,omitempty"`   // kf50
+       KeyF51       string   `json:"kf51,omitempty"`   // kf51
+       KeyF52       string   `json:"kf52,omitempty"`   // kf52
+       KeyF53       string   `json:"kf53,omitempty"`   // kf53
+       KeyF54       string   `json:"kf54,omitempty"`   // kf54
+       KeyF55       string   `json:"kf55,omitempty"`   // kf55
+       KeyF56       string   `json:"kf56,omitempty"`   // kf56
+       KeyF57       string   `json:"kf57,omitempty"`   // kf57
+       KeyF58       string   `json:"kf58,omitempty"`   // kf58
+       KeyF59       string   `json:"kf59,omitempty"`   // kf59
+       KeyF60       string   `json:"kf60,omitempty"`   // kf60
+       KeyF61       string   `json:"kf61,omitempty"`   // kf61
+       KeyF62       string   `json:"kf62,omitempty"`   // kf62
+       KeyF63       string   `json:"kf63,omitempty"`   // kf63
+       KeyF64       string   `json:"kf64,omitempty"`   // kf64
+       KeyInsert    string   `json:"kich,omitempty"`   // kich1
+       KeyDelete    string   `json:"kdch,omitempty"`   // kdch1
+       KeyHome      string   `json:"khome,omitempty"`  // khome
+       KeyEnd       string   `json:"kend,omitempty"`   // kend
+       KeyHelp      string   `json:"khlp,omitempty"`   // khlp
+       KeyPgUp      string   `json:"kpp,omitempty"`    // kpp
+       KeyPgDn      string   `json:"knp,omitempty"`    // knp
+       KeyUp        string   `json:"kcuu1,omitempty"`  // kcuu1
+       KeyDown      string   `json:"kcud1,omitempty"`  // kcud1
+       KeyLeft      string   `json:"kcub1,omitempty"`  // kcub1
+       KeyRight     string   `json:"kcuf1,omitempty"`  // kcuf1
+       KeyBacktab   string   `json:"kcbt,omitempty"`   // kcbt
+       KeyExit      string   `json:"kext,omitempty"`   // kext
+       KeyClear     string   `json:"kclr,omitempty"`   // kclr
+       KeyPrint     string   `json:"kprt,omitempty"`   // kprt
+       KeyCancel    string   `json:"kcan,omitempty"`   // kcan
+       Mouse        string   `json:"kmous,omitempty"`  // kmous
+       MouseMode    string   `json:"XM,omitempty"`     // XM
+       AltChars     string   `json:"acsc,omitempty"`   // acsc
+       EnterAcs     string   `json:"smacs,omitempty"`  // smacs
+       ExitAcs      string   `json:"rmacs,omitempty"`  // rmacs
+       EnableAcs    string   `json:"enacs,omitempty"`  // enacs
+       KeyShfRight  string   `json:"kRIT,omitempty"`   // kRIT
+       KeyShfLeft   string   `json:"kLFT,omitempty"`   // kLFT
+       KeyShfHome   string   `json:"kHOM,omitempty"`   // kHOM
+       KeyShfEnd    string   `json:"kEND,omitempty"`   // kEND
+
+       // These are non-standard extensions to terminfo.  This includes
+       // true color support, and some additional keys.  Its kind of bizarre
+       // that shifted variants of left and right exist, but not up and down.
+       // Terminal support for these are going to vary amongst XTerm
+       // emulations, so don't depend too much on them in your application.
+
+       SetFgBg         string `json:"_setfgbg,omitempty"`    // setfgbg
+       SetFgBgRGB      string `json:"_setfgbgrgb,omitempty"` // setfgbgrgb
+       SetFgRGB        string `json:"_setfrgb,omitempty"`    // setfrgb
+       SetBgRGB        string `json:"_setbrgb,omitempty"`    // setbrgb
+       KeyShfUp        string `json:"_kscu1,omitempty"`      // shift-up
+       KeyShfDown      string `json:"_kscud1,omitempty"`     // shift-down
+       KeyCtrlUp       string `json:"_kccu1,omitempty"`      // ctrl-up
+       KeyCtrlDown     string `json:"_kccud1,omitempty"`     // ctrl-left
+       KeyCtrlRight    string `json:"_kccuf1,omitempty"`     // ctrl-right
+       KeyCtrlLeft     string `json:"_kccub1,omitempty"`     // ctrl-left
+       KeyMetaUp       string `json:"_kmcu1,omitempty"`      // meta-up
+       KeyMetaDown     string `json:"_kmcud1,omitempty"`     // meta-left
+       KeyMetaRight    string `json:"_kmcuf1,omitempty"`     // meta-right
+       KeyMetaLeft     string `json:"_kmcub1,omitempty"`     // meta-left
+       KeyAltUp        string `json:"_kacu1,omitempty"`      // alt-up
+       KeyAltDown      string `json:"_kacud1,omitempty"`     // alt-left
+       KeyAltRight     string `json:"_kacuf1,omitempty"`     // alt-right
+       KeyAltLeft      string `json:"_kacub1,omitempty"`     // alt-left
+       KeyCtrlHome     string `json:"_kchome,omitempty"`
+       KeyCtrlEnd      string `json:"_kcend,omitempty"`
+       KeyMetaHome     string `json:"_kmhome,omitempty"`
+       KeyMetaEnd      string `json:"_kmend,omitempty"`
+       KeyAltHome      string `json:"_kahome,omitempty"`
+       KeyAltEnd       string `json:"_kaend,omitempty"`
+       KeyAltShfUp     string `json:"_kascu1,omitempty"`
+       KeyAltShfDown   string `json:"_kascud1,omitempty"`
+       KeyAltShfLeft   string `json:"_kascub1,omitempty"`
+       KeyAltShfRight  string `json:"_kascuf1,omitempty"`
+       KeyMetaShfUp    string `json:"_kmscu1,omitempty"`
+       KeyMetaShfDown  string `json:"_kmscud1,omitempty"`
+       KeyMetaShfLeft  string `json:"_kmscub1,omitempty"`
+       KeyMetaShfRight string `json:"_kmscuf1,omitempty"`
+       KeyCtrlShfUp    string `json:"_kcscu1,omitempty"`
+       KeyCtrlShfDown  string `json:"_kcscud1,omitempty"`
+       KeyCtrlShfLeft  string `json:"_kcscub1,omitempty"`
+       KeyCtrlShfRight string `json:"_kcscuf1,omitempty"`
+       KeyCtrlShfHome  string `json:"_kcHOME,omitempty"`
+       KeyCtrlShfEnd   string `json:"_kcEND,omitempty"`
+       KeyAltShfHome   string `json:"_kaHOME,omitempty"`
+       KeyAltShfEnd    string `json:"_kaEND,omitempty"`
+       KeyMetaShfHome  string `json:"_kmHOME,omitempty"`
+       KeyMetaShfEnd   string `json:"_kmEND,omitempty"`
+}
+
+type stackElem struct {
+       s     string
+       i     int
+       isStr bool
+       isInt bool
+}
+
+type stack []stackElem
+
+func (st stack) Push(v string) stack {
+       e := stackElem{
+               s:     v,
+               isStr: true,
+       }
+       return append(st, e)
+}
+
+func (st stack) Pop() (string, stack) {
+       v := ""
+       if len(st) > 0 {
+               e := st[len(st)-1]
+               st = st[:len(st)-1]
+               if e.isStr {
+                       v = e.s
+               } else {
+                       v = strconv.Itoa(e.i)
+               }
+       }
+       return v, st
+}
+
+func (st stack) PopInt() (int, stack) {
+       if len(st) > 0 {
+               e := st[len(st)-1]
+               st = st[:len(st)-1]
+               if e.isInt {
+                       return e.i, st
+               } else if e.isStr {
+                       i, _ := strconv.Atoi(e.s)
+                       return i, st
+               }
+       }
+       return 0, st
+}
+
+func (st stack) PopBool() (bool, stack) {
+       if len(st) > 0 {
+               e := st[len(st)-1]
+               st = st[:len(st)-1]
+               if e.isStr {
+                       if e.s == "1" {
+                               return true, st
+                       }
+                       return false, st
+               } else if e.i == 1 {
+                       return true, st
+               } else {
+                       return false, st
+               }
+       }
+       return false, st
+}
+
+func (st stack) PushInt(i int) stack {
+       e := stackElem{
+               i:     i,
+               isInt: true,
+       }
+       return append(st, e)
+}
+
+func (st stack) PushBool(i bool) stack {
+       if i {
+               return st.PushInt(1)
+       }
+       return st.PushInt(0)
+}
+
+func nextch(s string, index int) (byte, int) {
+       if index < len(s) {
+               return s[index], index + 1
+       }
+       return 0, index
+}
+
+// static vars
+var svars [26]string
+
+// paramsBuffer handles some persistent state for TParam.  Technically we
+// could probably dispense with this, but caching buffer arrays gives us
+// a nice little performance boost.  Furthermore, we know that TParam is
+// rarely (never?) called re-entrantly, so we can just reuse the same
+// buffers, making it thread-safe by stashing a lock.
+type paramsBuffer struct {
+       out bytes.Buffer
+       buf bytes.Buffer
+       lk  sync.Mutex
+}
+
+// Start initializes the params buffer with the initial string data.
+// It also locks the paramsBuffer.  The caller must call End() when
+// finished.
+func (pb *paramsBuffer) Start(s string) {
+       pb.lk.Lock()
+       pb.out.Reset()
+       pb.buf.Reset()
+       pb.buf.WriteString(s)
+}
+
+// End returns the final output from TParam, but it also releases the lock.
+func (pb *paramsBuffer) End() string {
+       s := pb.out.String()
+       pb.lk.Unlock()
+       return s
+}
+
+// NextCh returns the next input character to the expander.
+func (pb *paramsBuffer) NextCh() (byte, error) {
+       return pb.buf.ReadByte()
+}
+
+// PutCh "emits" (rather schedules for output) a single byte character.
+func (pb *paramsBuffer) PutCh(ch byte) {
+       pb.out.WriteByte(ch)
+}
+
+// PutString schedules a string for output.
+func (pb *paramsBuffer) PutString(s string) {
+       pb.out.WriteString(s)
+}
+
+var pb = &paramsBuffer{}
+
+// TParm takes a terminfo parameterized string, such as setaf or cup, and
+// evaluates the string, and returns the result with the parameter
+// applied.
+func (t *Terminfo) TParm(s string, p ...int) string {
+       var stk stack
+       var a, b string
+       var ai, bi int
+       var ab bool
+       var dvars [26]string
+       var params [9]int
+
+       pb.Start(s)
+
+       // make sure we always have 9 parameters -- makes it easier
+       // later to skip checks
+       for i := 0; i < len(params) && i < len(p); i++ {
+               params[i] = p[i]
+       }
+
+       nest := 0
+
+       for {
+
+               ch, err := pb.NextCh()
+               if err != nil {
+                       break
+               }
+
+               if ch != '%' {
+                       pb.PutCh(ch)
+                       continue
+               }
+
+               ch, err = pb.NextCh()
+               if err != nil {
+                       // XXX Error
+                       break
+               }
+
+               switch ch {
+               case '%': // quoted %
+                       pb.PutCh(ch)
+
+               case 'i': // increment both parameters (ANSI cup support)
+                       params[0]++
+                       params[1]++
+
+               case 'c', 's':
+                       // NB: these, and 'd' below are special cased for
+                       // efficiency.  They could be handled by the richer
+                       // format support below, less efficiently.
+                       a, stk = stk.Pop()
+                       pb.PutString(a)
+
+               case 'd':
+                       ai, stk = stk.PopInt()
+                       pb.PutString(strconv.Itoa(ai))
+
+               case '0', '1', '2', '3', '4', 'x', 'X', 'o', ':':
+                       // This is pretty suboptimal, but this is rarely used.
+                       // None of the mainstream terminals use any of this,
+                       // and it would surprise me if this code is ever
+                       // executed outside of test cases.
+                       f := "%"
+                       if ch == ':' {
+                               ch, _ = pb.NextCh()
+                       }
+                       f += string(ch)
+                       for ch == '+' || ch == '-' || ch == '#' || ch == ' ' {
+                               ch, _ = pb.NextCh()
+                               f += string(ch)
+                       }
+                       for (ch >= '0' && ch <= '9') || ch == '.' {
+                               ch, _ = pb.NextCh()
+                               f += string(ch)
+                       }
+                       switch ch {
+                       case 'd', 'x', 'X', 'o':
+                               ai, stk = stk.PopInt()
+                               pb.PutString(fmt.Sprintf(f, ai))
+                       case 'c', 's':
+                               a, stk = stk.Pop()
+                               pb.PutString(fmt.Sprintf(f, a))
+                       }
+
+               case 'p': // push parameter
+                       ch, _ = pb.NextCh()
+                       ai = int(ch - '1')
+                       if ai >= 0 && ai < len(params) {
+                               stk = stk.PushInt(params[ai])
+                       } else {
+                               stk = stk.PushInt(0)
+                       }
+
+               case 'P': // pop & store variable
+                       ch, _ = pb.NextCh()
+                       if ch >= 'A' && ch <= 'Z' {
+                               svars[int(ch-'A')], stk = stk.Pop()
+                       } else if ch >= 'a' && ch <= 'z' {
+                               dvars[int(ch-'a')], stk = stk.Pop()
+                       }
+
+               case 'g': // recall & push variable
+                       ch, _ = pb.NextCh()
+                       if ch >= 'A' && ch <= 'Z' {
+                               stk = stk.Push(svars[int(ch-'A')])
+                       } else if ch >= 'a' && ch <= 'z' {
+                               stk = stk.Push(dvars[int(ch-'a')])
+                       }
+
+               case '\'': // push(char)
+                       ch, _ = pb.NextCh()
+                       pb.NextCh() // must be ' but we don't check
+                       stk = stk.Push(string(ch))
+
+               case '{': // push(int)
+                       ai = 0
+                       ch, _ = pb.NextCh()
+                       for ch >= '0' && ch <= '9' {
+                               ai *= 10
+                               ai += int(ch - '0')
+                               ch, _ = pb.NextCh()
+                       }
+                       // ch must be '}' but no verification
+                       stk = stk.PushInt(ai)
+
+               case 'l': // push(strlen(pop))
+                       a, stk = stk.Pop()
+                       stk = stk.PushInt(len(a))
+
+               case '+':
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai + bi)
+
+               case '-':
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai - bi)
+
+               case '*':
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai * bi)
+
+               case '/':
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       if bi != 0 {
+                               stk = stk.PushInt(ai / bi)
+                       } else {
+                               stk = stk.PushInt(0)
+                       }
+
+               case 'm': // push(pop mod pop)
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       if bi != 0 {
+                               stk = stk.PushInt(ai % bi)
+                       } else {
+                               stk = stk.PushInt(0)
+                       }
+
+               case '&': // AND
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai & bi)
+
+               case '|': // OR
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai | bi)
+
+               case '^': // XOR
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai ^ bi)
+
+               case '~': // bit complement
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushInt(ai ^ -1)
+
+               case '!': // logical NOT
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushBool(ai != 0)
+
+               case '=': // numeric compare or string compare
+                       b, stk = stk.Pop()
+                       a, stk = stk.Pop()
+                       stk = stk.PushBool(a == b)
+
+               case '>': // greater than, numeric
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushBool(ai > bi)
+
+               case '<': // less than, numeric
+                       bi, stk = stk.PopInt()
+                       ai, stk = stk.PopInt()
+                       stk = stk.PushBool(ai < bi)
+
+               case '?': // start conditional
+
+               case 't':
+                       ab, stk = stk.PopBool()
+                       if ab {
+                               // just keep going
+                               break
+                       }
+                       nest = 0
+               ifloop:
+                       // this loop consumes everything until we hit our else,
+                       // or the end of the conditional
+                       for {
+                               ch, err = pb.NextCh()
+                               if err != nil {
+                                       break
+                               }
+                               if ch != '%' {
+                                       continue
+                               }
+                               ch, _ = pb.NextCh()
+                               switch ch {
+                               case ';':
+                                       if nest == 0 {
+                                               break ifloop
+                                       }
+                                       nest--
+                               case '?':
+                                       nest++
+                               case 'e':
+                                       if nest == 0 {
+                                               break ifloop
+                                       }
+                               }
+                       }
+
+               case 'e':
+                       // if we got here, it means we didn't use the else
+                       // in the 't' case above, and we should skip until
+                       // the end of the conditional
+                       nest = 0
+               elloop:
+                       for {
+                               ch, err = pb.NextCh()
+                               if err != nil {
+                                       break
+                               }
+                               if ch != '%' {
+                                       continue
+                               }
+                               ch, _ = pb.NextCh()
+                               switch ch {
+                               case ';':
+                                       if nest == 0 {
+                                               break elloop
+                                       }
+                                       nest--
+                               case '?':
+                                       nest++
+                               }
+                       }
+
+               case ';': // endif
+
+               }
+       }
+
+       return pb.End()
+}
+
+// TPuts 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.  All Terminfo based strings should be emitted using
+// this function.
+func (t *Terminfo) TPuts(w io.Writer, s string, baud int) {
+       for {
+               beg := strings.Index(s, "$<")
+               if beg < 0 {
+                       // Most strings don't need padding, which is good news!
+                       io.WriteString(w, s)
+                       return
+               }
+               io.WriteString(w, s[:beg])
+               s = s[beg+2:]
+               end := strings.Index(s, ">")
+               if end < 0 {
+                       // unterminated.. just emit bytes unadulterated
+                       io.WriteString(w, "$<"+s)
+                       return
+               }
+               val := s[:end]
+               s = s[end+1:]
+               padus := 0
+               unit := 1000
+               dot := false
+       loop:
+               for i := range val {
+                       switch val[i] {
+                       case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+                               padus *= 10
+                               padus += int(val[i] - '0')
+                               if dot {
+                                       unit *= 10
+                               }
+                       case '.':
+                               if !dot {
+                                       dot = true
+                               } else {
+                                       break loop
+                               }
+                       default:
+                               break loop
+                       }
+               }
+               cnt := int(((baud / 8) * padus) / unit)
+               for cnt > 0 {
+                       io.WriteString(w, t.PadChar)
+                       cnt--
+               }
+       }
+}
+
+// TGoto 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 (t *Terminfo) TGoto(col, row int) string {
+       return t.TParm(t.SetCursor, row, col)
+}
+
+// TColor returns a string corresponding to the given foreground and background
+// colors.  Either fg or bg can be set to -1 to elide.
+func (t *Terminfo) TColor(fi, bi int) string {
+       rv := ""
+       // As a special case, we map bright colors to lower versions if the
+       // color table only holds 8.  For the remaining 240 colors, the user
+       // is out of luck.  Someday we could create a mapping table, but its
+       // not worth it.
+       if t.Colors == 8 {
+               if fi > 7 && fi < 16 {
+                       fi -= 8
+               }
+               if bi > 7 && bi < 16 {
+                       bi -= 8
+               }
+       }
+       if t.Colors > fi && fi >= 0 {
+               rv += t.TParm(t.SetFg, fi)
+       }
+       if t.Colors > bi && bi >= 0 {
+               rv += t.TParm(t.SetBg, bi)
+       }
+       return rv
+}
+
+var (
+       dblock    sync.Mutex
+       terminfos = make(map[string]*Terminfo)
+       aliases   = make(map[string]string)
+)
+
+// AddTerminfo can be called to register a new Terminfo entry.
+func AddTerminfo(t *Terminfo) {
+       dblock.Lock()
+       terminfos[t.Name] = t
+       for _, x := range t.Aliases {
+               terminfos[x] = t
+       }
+       dblock.Unlock()
+}
+
+func loadFromFile(fname string, term string) (*Terminfo, error) {
+       var e error
+       var f io.Reader
+       if f, e = os.Open(fname); e != nil {
+               return nil, e
+       }
+       if strings.HasSuffix(fname, ".gz") {
+               if f, e = gzip.NewReader(f); e != nil {
+                       return nil, e
+               }
+       }
+       d := json.NewDecoder(f)
+       for {
+               t := &Terminfo{}
+               if e := d.Decode(t); e != nil {
+                       if e == io.EOF {
+                               return nil, ErrTermNotFound
+                       }
+                       return nil, e
+               }
+               if t.SetCursor == "" {
+                       // This must be an alias record, return it.
+                       return t, nil
+               }
+               if t.Name == term {
+                       return t, nil
+               }
+               for _, a := range t.Aliases {
+                       if a == term {
+                               return t, nil
+                       }
+               }
+       }
+}
+
+// LookupTerminfo attempts to find a definition for the named $TERM.
+// It first looks in the builtin database, which should cover just about
+// everyone.  If it can't find one there, then it will attempt to read
+// one from the JSON file located in either $TCELLDB, $HOME/.tcelldb
+// or in this package's source directory as database.json).
+func LookupTerminfo(name string) (*Terminfo, error) {
+       if name == "" {
+               // else on windows: index out of bounds
+               // on the name[0] reference below
+               return nil, ErrTermNotFound
+       }
+
+       dblock.Lock()
+       t := terminfos[name]
+       dblock.Unlock()
+
+       if t == nil {
+
+               var files []string
+               letter := fmt.Sprintf("%02x", name[0])
+               gzfile := path.Join(letter, name+".gz")
+               jsfile := path.Join(letter, name)
+
+               // Build up the search path.  Old versions of tcell used a
+               // single database file, whereas the new ones locate them
+               // in JSON (optionally compressed) files.
+               //
+               // The search path looks like:
+               //
+               // $TCELLDB/x/xterm.gz
+               // $TCELLDB/x/xterm
+               // $TCELLDB
+               // $HOME/.tcelldb/x/xterm.gz
+               // $HOME/.tcelldb/x/xterm
+               // $HOME/.tcelldb
+               // $GOPATH/terminfo/database/x/xterm.gz
+               // $GOPATH/terminfo/database/x/xterm
+               //
+               if pth := os.Getenv("TCELLDB"); pth != "" {
+                       files = append(files, path.Join(pth, gzfile))
+                       files = append(files, path.Join(pth, jsfile))
+                       files = append(files, pth)
+               }
+               if pth := os.Getenv("HOME"); pth != "" {
+                       pth = path.Join(pth, ".tcelldb")
+                       files = append(files, path.Join(pth, gzfile))
+                       files = append(files, path.Join(pth, jsfile))
+                       files = append(files, pth)
+               }
+
+               for _, pth := range strings.Split(os.Getenv("GOPATH"), string(os.PathListSeparator)) {
+                       pth = path.Join(pth, "src", "github.com", "gdamore", "tcell", "terminfo", "database")
+                       files = append(files, path.Join(pth, gzfile))
+                       files = append(files, path.Join(pth, jsfile))
+               }
+
+               for _, fname := range files {
+                       t, _ = loadFromFile(fname, name)
+                       if t != nil {
+                               break
+                       }
+               }
+               if t != nil {
+                       if t.Name != name {
+                               // Check for a database loop (no infinite
+                               // recursion).
+                               dblock.Lock()
+                               if aliases[name] != "" {
+                                       dblock.Unlock()
+                                       return nil, ErrTermNotFound
+                               }
+                               aliases[name] = t.Name
+                               dblock.Unlock()
+                               return LookupTerminfo(t.Name)
+                       }
+                       dblock.Lock()
+                       terminfos[name] = t
+                       dblock.Unlock()
+               }
+       }
+       if t == nil {
+               return nil, ErrTermNotFound
+       }
+       return t, nil
+}
diff --git a/cmd/micro/terminfo/terminfo_test.go b/cmd/micro/terminfo/terminfo_test.go
new file mode 100644 (file)
index 0000000..4466479
--- /dev/null
@@ -0,0 +1,194 @@
+// Copyright 2016 The TCell Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use file except in compliance with the License.
+// You may obtain a copy of the license at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package terminfo
+
+import (
+       "bytes"
+       "os"
+       "testing"
+
+       . "github.com/smartystreets/goconvey/convey"
+)
+
+// This terminfo entry is a stripped down version from
+// xterm-256color, but I've added some of my own entries.
+var testTerminfo = &Terminfo{
+       Name:      "simulation_test",
+       Columns:   80,
+       Lines:     24,
+       Colors:    256,
+       Bell:      "\a",
+       Blink:     "\x1b2ms$<2>",
+       Reverse:   "\x1b[7m",
+       SetFg:     "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m",
+       SetBg:     "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m",
+       AltChars:  "``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~",
+       Mouse:     "\x1b[M",
+       MouseMode: "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;\x1b[?1000%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c",
+       SetCursor: "\x1b[%i%p1%d;%p2%dH",
+       PadChar:   "\x00",
+}
+
+func TestTerminfo(t *testing.T) {
+
+       ti := testTerminfo
+
+       Convey("Terminfo parameter processing", t, func() {
+               // This tests %i, and basic parameter strings too
+               Convey("TGoto works", func() {
+                       s := ti.TGoto(7, 9)
+                       So(s, ShouldEqual, "\x1b[10;8H")
+               })
+
+               // This tests some conditionals
+               Convey("TParm extended formats work", func() {
+                       s := ti.TParm("A[%p1%2.2X]B", 47)
+                       So(s, ShouldEqual, "A[2F]B")
+               })
+
+               // This tests some conditionals
+               Convey("TParm colors work", func() {
+                       s := ti.TParm(ti.SetFg, 7)
+                       So(s, ShouldEqual, "\x1b[37m")
+
+                       s = ti.TParm(ti.SetFg, 15)
+                       So(s, ShouldEqual, "\x1b[97m")
+
+                       s = ti.TParm(ti.SetFg, 200)
+                       So(s, ShouldEqual, "\x1b[38;5;200m")
+               })
+
+               // This tests variables
+               Convey("TParm mouse mode works", func() {
+                       s := ti.TParm(ti.MouseMode, 1)
+                       So(s, ShouldEqual, "\x1b[?1000h\x1b[?1003h\x1b[?1006h")
+                       s = ti.TParm(ti.MouseMode, 0)
+                       So(s, ShouldEqual, "\x1b[?1000l\x1b[?1003l\x1b[?1006l")
+               })
+
+       })
+
+       Convey("Terminfo delay handling", t, func() {
+
+               Convey("19200 baud", func() {
+                       buf := bytes.NewBuffer(nil)
+                       ti.TPuts(buf, ti.Blink, 19200)
+                       s := string(buf.Bytes())
+                       So(s, ShouldEqual, "\x1b2ms\x00\x00\x00\x00")
+               })
+
+               Convey("50 baud", func() {
+                       buf := bytes.NewBuffer(nil)
+                       ti.TPuts(buf, ti.Blink, 50)
+                       s := string(buf.Bytes())
+                       So(s, ShouldEqual, "\x1b2ms")
+               })
+       })
+}
+
+func TestTerminfoDatabase(t *testing.T) {
+
+       Convey("Database Lookups work", t, func() {
+               Convey("Basic lookup works", func() {
+                       os.Setenv("TCELLDB", "testdata/test1")
+                       ti, err := LookupTerminfo("test1")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Columns, ShouldEqual, 80)
+
+                       ti, err = LookupTerminfo("alias1")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Columns, ShouldEqual, 80)
+
+                       os.Setenv("TCELLDB", "testdata")
+                       ti, err = LookupTerminfo("test2")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Columns, ShouldEqual, 80)
+                       So(len(ti.Aliases), ShouldEqual, 1)
+                       So(ti.Aliases[0], ShouldEqual, "alias2")
+               })
+
+               Convey("Incorrect primary name works", func() {
+                       os.Setenv("TCELLDB", "testdata")
+                       ti, err := LookupTerminfo("test3")
+                       So(err, ShouldNotBeNil)
+                       So(ti, ShouldBeNil)
+               })
+
+               Convey("Loops fail", func() {
+                       os.Setenv("TCELLDB", "testdata")
+                       ti, err := LookupTerminfo("loop1")
+                       So(ti, ShouldBeNil)
+                       So(err, ShouldNotBeNil)
+               })
+
+               Convey("Gzip database works", func() {
+                       os.Setenv("TCELLDB", "testdata")
+                       ti, err := LookupTerminfo("test-gzip")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Columns, ShouldEqual, 80)
+               })
+
+               Convey("Gzip alias lookup works", func() {
+                       os.Setenv("TCELLDB", "testdata")
+                       ti, err := LookupTerminfo("alias-gzip")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Columns, ShouldEqual, 80)
+               })
+
+               Convey("Broken alias works", func() {
+                       os.Setenv("TCELLDB", "testdata")
+                       ti, err := LookupTerminfo("alias-none")
+                       So(err, ShouldNotBeNil)
+                       So(ti, ShouldBeNil)
+               })
+
+               Convey("Combined database works", func() {
+                       os.Setenv("TCELLDB", "testdata/combined")
+                       ti, err := LookupTerminfo("combined2")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Lines, ShouldEqual, 102)
+
+                       ti, err = LookupTerminfo("alias-comb1")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Lines, ShouldEqual, 101)
+
+                       ti, err = LookupTerminfo("combined3")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Lines, ShouldEqual, 103)
+
+                       ti, err = LookupTerminfo("combined1")
+                       So(err, ShouldBeNil)
+                       So(ti, ShouldNotBeNil)
+                       So(ti.Lines, ShouldEqual, 101)
+               })
+       })
+}
+
+func BenchmarkSetFgBg(b *testing.B) {
+       ti := testTerminfo
+
+       for i := 0; i < b.N; i++ {
+               ti.TParm(ti.SetFg, 100, 200)
+               ti.TParm(ti.SetBg, 100, 200)
+       }
+}