Prechádzať zdrojové kódy

Merge pull request #28304 from ijc25/jsonmessage-use-terminfo

use terminfo in pkg/jsonmessage
Alexander Morozov 8 rokov pred
rodič
commit
2c948ac3e6

+ 80 - 12
pkg/jsonmessage/jsonmessage.go

@@ -4,9 +4,12 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"os"
 	"strings"
 	"time"
 
+	"github.com/Nvveen/Gotty"
+
 	"github.com/docker/docker/pkg/jsonlog"
 	"github.com/docker/docker/pkg/term"
 	"github.com/docker/go-units"
@@ -106,10 +109,60 @@ type JSONMessage struct {
 	Aux *json.RawMessage `json:"aux,omitempty"`
 }
 
-// Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
+/* Satisfied by gotty.TermInfo as well as noTermInfo from below */
+type termInfo interface {
+	Parse(attr string, params ...interface{}) (string, error)
+}
+
+type noTermInfo struct{} // canary used when no terminfo.
+
+func (ti *noTermInfo) Parse(attr string, params ...interface{}) (string, error) {
+	return "", fmt.Errorf("noTermInfo")
+}
+
+func clearLine(out io.Writer, ti termInfo) {
+	// el2 (clear whole line) is not exposed by terminfo.
+
+	// First clear line from beginning to cursor
+	if attr, err := ti.Parse("el1"); err == nil {
+		fmt.Fprintf(out, "%s", attr)
+	} else {
+		fmt.Fprintf(out, "\x1b[1K")
+	}
+	// Then clear line from cursor to end
+	if attr, err := ti.Parse("el"); err == nil {
+		fmt.Fprintf(out, "%s", attr)
+	} else {
+		fmt.Fprintf(out, "\x1b[K")
+	}
+}
+
+func cursorUp(out io.Writer, ti termInfo, l int) {
+	if l == 0 { // Should never be the case, but be tolerant
+		return
+	}
+	if attr, err := ti.Parse("cuu", l); err == nil {
+		fmt.Fprintf(out, "%s", attr)
+	} else {
+		fmt.Fprintf(out, "\x1b[%dA", l)
+	}
+}
+
+func cursorDown(out io.Writer, ti termInfo, l int) {
+	if l == 0 { // Should never be the case, but be tolerant
+		return
+	}
+	if attr, err := ti.Parse("cud", l); err == nil {
+		fmt.Fprintf(out, "%s", attr)
+	} else {
+		fmt.Fprintf(out, "\x1b[%dB", l)
+	}
+}
+
+// Display displays the JSONMessage to `out`. `termInfo` is non-nil if `out`
 // is a terminal. If this is the case, it will erase the entire current line
 // when displaying the progressbar.
-func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
+func (jm *JSONMessage) Display(out io.Writer, termInfo termInfo) error {
 	if jm.Error != nil {
 		if jm.Error.Code == 401 {
 			return fmt.Errorf("Authentication is required.")
@@ -117,10 +170,10 @@ func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
 		return jm.Error
 	}
 	var endl string
-	if isTerminal && jm.Stream == "" && jm.Progress != nil {
-		// <ESC>[2K = erase entire current line
-		fmt.Fprintf(out, "%c[2K\r", 27)
+	if termInfo != nil && jm.Stream == "" && jm.Progress != nil {
+		clearLine(out, termInfo)
 		endl = "\r"
+		fmt.Fprintf(out, endl)
 	} else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
 		return nil
 	}
@@ -135,7 +188,7 @@ func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
 	if jm.From != "" {
 		fmt.Fprintf(out, "(from %s) ", jm.From)
 	}
-	if jm.Progress != nil && isTerminal {
+	if jm.Progress != nil && termInfo != nil {
 		fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
 	} else if jm.ProgressMessage != "" { //deprecated
 		fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
@@ -155,6 +208,21 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
 		dec = json.NewDecoder(in)
 		ids = make(map[string]int)
 	)
+
+	var termInfo termInfo
+
+	if isTerminal {
+		term := os.Getenv("TERM")
+		if term == "" {
+			term = "vt102"
+		}
+
+		var err error
+		if termInfo, err = gotty.OpenTermInfo(term); err != nil {
+			termInfo = &noTermInfo{}
+		}
+	}
+
 	for {
 		diff := 0
 		var jm JSONMessage
@@ -186,13 +254,13 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
 				// with no ID.
 				line = len(ids)
 				ids[jm.ID] = line
-				if isTerminal {
+				if termInfo != nil {
 					fmt.Fprintf(out, "\n")
 				}
 			}
 			diff = len(ids) - line
-			if isTerminal && diff > 0 {
-				fmt.Fprintf(out, "%c[%dA", 27, diff)
+			if termInfo != nil {
+				cursorUp(out, termInfo, diff)
 			}
 		} else {
 			// When outputting something that isn't progress
@@ -202,9 +270,9 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
 			// with multiple tags).
 			ids = make(map[string]int)
 		}
-		err := jm.Display(out, isTerminal)
-		if jm.ID != "" && isTerminal && diff > 0 {
-			fmt.Fprintf(out, "%c[%dB", 27, diff)
+		err := jm.Display(out, termInfo)
+		if jm.ID != "" && termInfo != nil {
+			cursorDown(out, termInfo, diff)
 		}
 		if err != nil {
 			return err

+ 21 - 13
pkg/jsonmessage/jsonmessage_test.go

@@ -3,6 +3,7 @@ package jsonmessage
 import (
 	"bytes"
 	"fmt"
+	"os"
 	"strings"
 	"testing"
 	"time"
@@ -132,7 +133,7 @@ func TestJSONMessageDisplay(t *testing.T) {
 			Progress: &JSONProgress{Current: 1},
 		}: {
 			"",
-			fmt.Sprintf("%c[2K\rstatus      1 B\r", 27),
+			fmt.Sprintf("%c[1K%c[K\rstatus      1 B\r", 27, 27),
 		},
 	}
 
@@ -140,19 +141,19 @@ func TestJSONMessageDisplay(t *testing.T) {
 	for jsonMessage, expectedMessages := range messages {
 		// Without terminal
 		data := bytes.NewBuffer([]byte{})
-		if err := jsonMessage.Display(data, false); err != nil {
+		if err := jsonMessage.Display(data, nil); err != nil {
 			t.Fatal(err)
 		}
 		if data.String() != expectedMessages[0] {
-			t.Fatalf("Expected [%v], got [%v]", expectedMessages[0], data.String())
+			t.Fatalf("Expected %q,got %q", expectedMessages[0], data.String())
 		}
 		// With terminal
 		data = bytes.NewBuffer([]byte{})
-		if err := jsonMessage.Display(data, true); err != nil {
+		if err := jsonMessage.Display(data, &noTermInfo{}); err != nil {
 			t.Fatal(err)
 		}
 		if data.String() != expectedMessages[1] {
-			t.Fatalf("Expected [%v], got [%v]", expectedMessages[1], data.String())
+			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
 		}
 	}
 }
@@ -162,15 +163,15 @@ func TestJSONMessageDisplayWithJSONError(t *testing.T) {
 	data := bytes.NewBuffer([]byte{})
 	jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}}
 
-	err := jsonMessage.Display(data, true)
+	err := jsonMessage.Display(data, &noTermInfo{})
 	if err == nil || err.Error() != "Can't find it" {
-		t.Fatalf("Expected a JSONError 404, got [%v]", err)
+		t.Fatalf("Expected a JSONError 404, got %q", err)
 	}
 
 	jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}}
-	err = jsonMessage.Display(data, true)
+	err = jsonMessage.Display(data, &noTermInfo{})
 	if err == nil || err.Error() != "Authentication is required." {
-		t.Fatalf("Expected an error [Authentication is required.], got [%v]", err)
+		t.Fatalf("Expected an error \"Authentication is required.\", got %q", err)
 	}
 }
 
@@ -183,7 +184,7 @@ func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
 	inFd, _ = term.GetFdInfo(reader)
 
 	if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil && err.Error()[:17] != "invalid character" {
-		t.Fatalf("Should have thrown an error (invalid character in ..), got [%v]", err)
+		t.Fatalf("Should have thrown an error (invalid character in ..), got %q", err)
 	}
 }
 
@@ -215,9 +216,15 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
 		// With progressDetail
 		"{ \"id\": \"ID\", \"status\": \"status\", \"progressDetail\": { \"Current\": 1} }": {
 			"", // progressbar is disabled in non-terminal
-			fmt.Sprintf("\n%c[%dA%c[2K\rID: status      1 B\r%c[%dB", 27, 1, 27, 27, 1),
+			fmt.Sprintf("\n%c[%dA%c[1K%c[K\rID: status      1 B\r%c[%dB", 27, 1, 27, 27, 27, 1),
 		},
 	}
+
+	// Use $TERM which is unlikely to exist, forcing DisplayJSONMessageStream to
+	// (hopefully) use &noTermInfo.
+	origTerm := os.Getenv("TERM")
+	os.Setenv("TERM", "xyzzy-non-existent-terminfo")
+
 	for jsonMessage, expectedMessages := range messages {
 		data := bytes.NewBuffer([]byte{})
 		reader := strings.NewReader(jsonMessage)
@@ -228,7 +235,7 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
 			t.Fatal(err)
 		}
 		if data.String() != expectedMessages[0] {
-			t.Fatalf("Expected an [%v], got [%v]", expectedMessages[0], data.String())
+			t.Fatalf("Expected an %q, got %q", expectedMessages[0], data.String())
 		}
 
 		// With terminal
@@ -238,8 +245,9 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
 			t.Fatal(err)
 		}
 		if data.String() != expectedMessages[1] {
-			t.Fatalf("Expected an [%v], got [%v]", expectedMessages[1], data.String())
+			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
 		}
 	}
+	os.Setenv("TERM", origTerm)
 
 }

+ 1 - 0
vendor.conf

@@ -129,6 +129,7 @@ github.com/spf13/cobra v1.5 https://github.com/dnephin/cobra.git
 github.com/spf13/pflag dabebe21bf790f782ea4c7bbd2efc430de182afd
 github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
 github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
+github.com/Nvveen/Gotty 6018b68f96b839edfbe3fb48668853f5dbad88a3 https://github.com/ijc25/Gotty
 
 # metrics
 github.com/docker/go-metrics 86138d05f285fd9737a99bee2d9be30866b59d72

+ 26 - 0
vendor/github.com/Nvveen/Gotty/LICENSE

@@ -0,0 +1,26 @@
+Copyright (c) 2012, Neal van Veen (nealvanveen@gmail.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met: 
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer. 
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution. 
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies, 
+either expressed or implied, of the FreeBSD Project.

+ 514 - 0
vendor/github.com/Nvveen/Gotty/attributes.go

@@ -0,0 +1,514 @@
+// 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.
+
+package gotty
+
+// Boolean capabilities
+var BoolAttr = [...]string{
+	"auto_left_margin", "bw",
+	"auto_right_margin", "am",
+	"no_esc_ctlc", "xsb",
+	"ceol_standout_glitch", "xhp",
+	"eat_newline_glitch", "xenl",
+	"erase_overstrike", "eo",
+	"generic_type", "gn",
+	"hard_copy", "hc",
+	"has_meta_key", "km",
+	"has_status_line", "hs",
+	"insert_null_glitch", "in",
+	"memory_above", "da",
+	"memory_below", "db",
+	"move_insert_mode", "mir",
+	"move_standout_mode", "msgr",
+	"over_strike", "os",
+	"status_line_esc_ok", "eslok",
+	"dest_tabs_magic_smso", "xt",
+	"tilde_glitch", "hz",
+	"transparent_underline", "ul",
+	"xon_xoff", "nxon",
+	"needs_xon_xoff", "nxon",
+	"prtr_silent", "mc5i",
+	"hard_cursor", "chts",
+	"non_rev_rmcup", "nrrmc",
+	"no_pad_char", "npc",
+	"non_dest_scroll_region", "ndscr",
+	"can_change", "ccc",
+	"back_color_erase", "bce",
+	"hue_lightness_saturation", "hls",
+	"col_addr_glitch", "xhpa",
+	"cr_cancels_micro_mode", "crxm",
+	"has_print_wheel", "daisy",
+	"row_addr_glitch", "xvpa",
+	"semi_auto_right_margin", "sam",
+	"cpi_changes_res", "cpix",
+	"lpi_changes_res", "lpix",
+	"backspaces_with_bs", "",
+	"crt_no_scrolling", "",
+	"no_correctly_working_cr", "",
+	"gnu_has_meta_key", "",
+	"linefeed_is_newline", "",
+	"has_hardware_tabs", "",
+	"return_does_clr_eol", "",
+}
+
+// Numerical capabilities
+var NumAttr = [...]string{
+	"columns", "cols",
+	"init_tabs", "it",
+	"lines", "lines",
+	"lines_of_memory", "lm",
+	"magic_cookie_glitch", "xmc",
+	"padding_baud_rate", "pb",
+	"virtual_terminal", "vt",
+	"width_status_line", "wsl",
+	"num_labels", "nlab",
+	"label_height", "lh",
+	"label_width", "lw",
+	"max_attributes", "ma",
+	"maximum_windows", "wnum",
+	"max_colors", "colors",
+	"max_pairs", "pairs",
+	"no_color_video", "ncv",
+	"buffer_capacity", "bufsz",
+	"dot_vert_spacing", "spinv",
+	"dot_horz_spacing", "spinh",
+	"max_micro_address", "maddr",
+	"max_micro_jump", "mjump",
+	"micro_col_size", "mcs",
+	"micro_line_size", "mls",
+	"number_of_pins", "npins",
+	"output_res_char", "orc",
+	"output_res_line", "orl",
+	"output_res_horz_inch", "orhi",
+	"output_res_vert_inch", "orvi",
+	"print_rate", "cps",
+	"wide_char_size", "widcs",
+	"buttons", "btns",
+	"bit_image_entwining", "bitwin",
+	"bit_image_type", "bitype",
+	"magic_cookie_glitch_ul", "",
+	"carriage_return_delay", "",
+	"new_line_delay", "",
+	"backspace_delay", "",
+	"horizontal_tab_delay", "",
+	"number_of_function_keys", "",
+}
+
+// String capabilities
+var StrAttr = [...]string{
+	"back_tab", "cbt",
+	"bell", "bel",
+	"carriage_return", "cr",
+	"change_scroll_region", "csr",
+	"clear_all_tabs", "tbc",
+	"clear_screen", "clear",
+	"clr_eol", "el",
+	"clr_eos", "ed",
+	"column_address", "hpa",
+	"command_character", "cmdch",
+	"cursor_address", "cup",
+	"cursor_down", "cud1",
+	"cursor_home", "home",
+	"cursor_invisible", "civis",
+	"cursor_left", "cub1",
+	"cursor_mem_address", "mrcup",
+	"cursor_normal", "cnorm",
+	"cursor_right", "cuf1",
+	"cursor_to_ll", "ll",
+	"cursor_up", "cuu1",
+	"cursor_visible", "cvvis",
+	"delete_character", "dch1",
+	"delete_line", "dl1",
+	"dis_status_line", "dsl",
+	"down_half_line", "hd",
+	"enter_alt_charset_mode", "smacs",
+	"enter_blink_mode", "blink",
+	"enter_bold_mode", "bold",
+	"enter_ca_mode", "smcup",
+	"enter_delete_mode", "smdc",
+	"enter_dim_mode", "dim",
+	"enter_insert_mode", "smir",
+	"enter_secure_mode", "invis",
+	"enter_protected_mode", "prot",
+	"enter_reverse_mode", "rev",
+	"enter_standout_mode", "smso",
+	"enter_underline_mode", "smul",
+	"erase_chars", "ech",
+	"exit_alt_charset_mode", "rmacs",
+	"exit_attribute_mode", "sgr0",
+	"exit_ca_mode", "rmcup",
+	"exit_delete_mode", "rmdc",
+	"exit_insert_mode", "rmir",
+	"exit_standout_mode", "rmso",
+	"exit_underline_mode", "rmul",
+	"flash_screen", "flash",
+	"form_feed", "ff",
+	"from_status_line", "fsl",
+	"init_1string", "is1",
+	"init_2string", "is2",
+	"init_3string", "is3",
+	"init_file", "if",
+	"insert_character", "ich1",
+	"insert_line", "il1",
+	"insert_padding", "ip",
+	"key_backspace", "kbs",
+	"key_catab", "ktbc",
+	"key_clear", "kclr",
+	"key_ctab", "kctab",
+	"key_dc", "kdch1",
+	"key_dl", "kdl1",
+	"key_down", "kcud1",
+	"key_eic", "krmir",
+	"key_eol", "kel",
+	"key_eos", "ked",
+	"key_f0", "kf0",
+	"key_f1", "kf1",
+	"key_f10", "kf10",
+	"key_f2", "kf2",
+	"key_f3", "kf3",
+	"key_f4", "kf4",
+	"key_f5", "kf5",
+	"key_f6", "kf6",
+	"key_f7", "kf7",
+	"key_f8", "kf8",
+	"key_f9", "kf9",
+	"key_home", "khome",
+	"key_ic", "kich1",
+	"key_il", "kil1",
+	"key_left", "kcub1",
+	"key_ll", "kll",
+	"key_npage", "knp",
+	"key_ppage", "kpp",
+	"key_right", "kcuf1",
+	"key_sf", "kind",
+	"key_sr", "kri",
+	"key_stab", "khts",
+	"key_up", "kcuu1",
+	"keypad_local", "rmkx",
+	"keypad_xmit", "smkx",
+	"lab_f0", "lf0",
+	"lab_f1", "lf1",
+	"lab_f10", "lf10",
+	"lab_f2", "lf2",
+	"lab_f3", "lf3",
+	"lab_f4", "lf4",
+	"lab_f5", "lf5",
+	"lab_f6", "lf6",
+	"lab_f7", "lf7",
+	"lab_f8", "lf8",
+	"lab_f9", "lf9",
+	"meta_off", "rmm",
+	"meta_on", "smm",
+	"newline", "_glitch",
+	"pad_char", "npc",
+	"parm_dch", "dch",
+	"parm_delete_line", "dl",
+	"parm_down_cursor", "cud",
+	"parm_ich", "ich",
+	"parm_index", "indn",
+	"parm_insert_line", "il",
+	"parm_left_cursor", "cub",
+	"parm_right_cursor", "cuf",
+	"parm_rindex", "rin",
+	"parm_up_cursor", "cuu",
+	"pkey_key", "pfkey",
+	"pkey_local", "pfloc",
+	"pkey_xmit", "pfx",
+	"print_screen", "mc0",
+	"prtr_off", "mc4",
+	"prtr_on", "mc5",
+	"repeat_char", "rep",
+	"reset_1string", "rs1",
+	"reset_2string", "rs2",
+	"reset_3string", "rs3",
+	"reset_file", "rf",
+	"restore_cursor", "rc",
+	"row_address", "mvpa",
+	"save_cursor", "row_address",
+	"scroll_forward", "ind",
+	"scroll_reverse", "ri",
+	"set_attributes", "sgr",
+	"set_tab", "hts",
+	"set_window", "wind",
+	"tab", "s_magic_smso",
+	"to_status_line", "tsl",
+	"underline_char", "uc",
+	"up_half_line", "hu",
+	"init_prog", "iprog",
+	"key_a1", "ka1",
+	"key_a3", "ka3",
+	"key_b2", "kb2",
+	"key_c1", "kc1",
+	"key_c3", "kc3",
+	"prtr_non", "mc5p",
+	"char_padding", "rmp",
+	"acs_chars", "acsc",
+	"plab_norm", "pln",
+	"key_btab", "kcbt",
+	"enter_xon_mode", "smxon",
+	"exit_xon_mode", "rmxon",
+	"enter_am_mode", "smam",
+	"exit_am_mode", "rmam",
+	"xon_character", "xonc",
+	"xoff_character", "xoffc",
+	"ena_acs", "enacs",
+	"label_on", "smln",
+	"label_off", "rmln",
+	"key_beg", "kbeg",
+	"key_cancel", "kcan",
+	"key_close", "kclo",
+	"key_command", "kcmd",
+	"key_copy", "kcpy",
+	"key_create", "kcrt",
+	"key_end", "kend",
+	"key_enter", "kent",
+	"key_exit", "kext",
+	"key_find", "kfnd",
+	"key_help", "khlp",
+	"key_mark", "kmrk",
+	"key_message", "kmsg",
+	"key_move", "kmov",
+	"key_next", "knxt",
+	"key_open", "kopn",
+	"key_options", "kopt",
+	"key_previous", "kprv",
+	"key_print", "kprt",
+	"key_redo", "krdo",
+	"key_reference", "kref",
+	"key_refresh", "krfr",
+	"key_replace", "krpl",
+	"key_restart", "krst",
+	"key_resume", "kres",
+	"key_save", "ksav",
+	"key_suspend", "kspd",
+	"key_undo", "kund",
+	"key_sbeg", "kBEG",
+	"key_scancel", "kCAN",
+	"key_scommand", "kCMD",
+	"key_scopy", "kCPY",
+	"key_screate", "kCRT",
+	"key_sdc", "kDC",
+	"key_sdl", "kDL",
+	"key_select", "kslt",
+	"key_send", "kEND",
+	"key_seol", "kEOL",
+	"key_sexit", "kEXT",
+	"key_sfind", "kFND",
+	"key_shelp", "kHLP",
+	"key_shome", "kHOM",
+	"key_sic", "kIC",
+	"key_sleft", "kLFT",
+	"key_smessage", "kMSG",
+	"key_smove", "kMOV",
+	"key_snext", "kNXT",
+	"key_soptions", "kOPT",
+	"key_sprevious", "kPRV",
+	"key_sprint", "kPRT",
+	"key_sredo", "kRDO",
+	"key_sreplace", "kRPL",
+	"key_sright", "kRIT",
+	"key_srsume", "kRES",
+	"key_ssave", "kSAV",
+	"key_ssuspend", "kSPD",
+	"key_sundo", "kUND",
+	"req_for_input", "rfi",
+	"key_f11", "kf11",
+	"key_f12", "kf12",
+	"key_f13", "kf13",
+	"key_f14", "kf14",
+	"key_f15", "kf15",
+	"key_f16", "kf16",
+	"key_f17", "kf17",
+	"key_f18", "kf18",
+	"key_f19", "kf19",
+	"key_f20", "kf20",
+	"key_f21", "kf21",
+	"key_f22", "kf22",
+	"key_f23", "kf23",
+	"key_f24", "kf24",
+	"key_f25", "kf25",
+	"key_f26", "kf26",
+	"key_f27", "kf27",
+	"key_f28", "kf28",
+	"key_f29", "kf29",
+	"key_f30", "kf30",
+	"key_f31", "kf31",
+	"key_f32", "kf32",
+	"key_f33", "kf33",
+	"key_f34", "kf34",
+	"key_f35", "kf35",
+	"key_f36", "kf36",
+	"key_f37", "kf37",
+	"key_f38", "kf38",
+	"key_f39", "kf39",
+	"key_f40", "kf40",
+	"key_f41", "kf41",
+	"key_f42", "kf42",
+	"key_f43", "kf43",
+	"key_f44", "kf44",
+	"key_f45", "kf45",
+	"key_f46", "kf46",
+	"key_f47", "kf47",
+	"key_f48", "kf48",
+	"key_f49", "kf49",
+	"key_f50", "kf50",
+	"key_f51", "kf51",
+	"key_f52", "kf52",
+	"key_f53", "kf53",
+	"key_f54", "kf54",
+	"key_f55", "kf55",
+	"key_f56", "kf56",
+	"key_f57", "kf57",
+	"key_f58", "kf58",
+	"key_f59", "kf59",
+	"key_f60", "kf60",
+	"key_f61", "kf61",
+	"key_f62", "kf62",
+	"key_f63", "kf63",
+	"clr_bol", "el1",
+	"clear_margins", "mgc",
+	"set_left_margin", "smgl",
+	"set_right_margin", "smgr",
+	"label_format", "fln",
+	"set_clock", "sclk",
+	"display_clock", "dclk",
+	"remove_clock", "rmclk",
+	"create_window", "cwin",
+	"goto_window", "wingo",
+	"hangup", "hup",
+	"dial_phone", "dial",
+	"quick_dial", "qdial",
+	"tone", "tone",
+	"pulse", "pulse",
+	"flash_hook", "hook",
+	"fixed_pause", "pause",
+	"wait_tone", "wait",
+	"user0", "u0",
+	"user1", "u1",
+	"user2", "u2",
+	"user3", "u3",
+	"user4", "u4",
+	"user5", "u5",
+	"user6", "u6",
+	"user7", "u7",
+	"user8", "u8",
+	"user9", "u9",
+	"orig_pair", "op",
+	"orig_colors", "oc",
+	"initialize_color", "initc",
+	"initialize_pair", "initp",
+	"set_color_pair", "scp",
+	"set_foreground", "setf",
+	"set_background", "setb",
+	"change_char_pitch", "cpi",
+	"change_line_pitch", "lpi",
+	"change_res_horz", "chr",
+	"change_res_vert", "cvr",
+	"define_char", "defc",
+	"enter_doublewide_mode", "swidm",
+	"enter_draft_quality", "sdrfq",
+	"enter_italics_mode", "sitm",
+	"enter_leftward_mode", "slm",
+	"enter_micro_mode", "smicm",
+	"enter_near_letter_quality", "snlq",
+	"enter_normal_quality", "snrmq",
+	"enter_shadow_mode", "sshm",
+	"enter_subscript_mode", "ssubm",
+	"enter_superscript_mode", "ssupm",
+	"enter_upward_mode", "sum",
+	"exit_doublewide_mode", "rwidm",
+	"exit_italics_mode", "ritm",
+	"exit_leftward_mode", "rlm",
+	"exit_micro_mode", "rmicm",
+	"exit_shadow_mode", "rshm",
+	"exit_subscript_mode", "rsubm",
+	"exit_superscript_mode", "rsupm",
+	"exit_upward_mode", "rum",
+	"micro_column_address", "mhpa",
+	"micro_down", "mcud1",
+	"micro_left", "mcub1",
+	"micro_right", "mcuf1",
+	"micro_row_address", "mvpa",
+	"micro_up", "mcuu1",
+	"order_of_pins", "porder",
+	"parm_down_micro", "mcud",
+	"parm_left_micro", "mcub",
+	"parm_right_micro", "mcuf",
+	"parm_up_micro", "mcuu",
+	"select_char_set", "scs",
+	"set_bottom_margin", "smgb",
+	"set_bottom_margin_parm", "smgbp",
+	"set_left_margin_parm", "smglp",
+	"set_right_margin_parm", "smgrp",
+	"set_top_margin", "smgt",
+	"set_top_margin_parm", "smgtp",
+	"start_bit_image", "sbim",
+	"start_char_set_def", "scsd",
+	"stop_bit_image", "rbim",
+	"stop_char_set_def", "rcsd",
+	"subscript_characters", "subcs",
+	"superscript_characters", "supcs",
+	"these_cause_cr", "docr",
+	"zero_motion", "zerom",
+	"char_set_names", "csnm",
+	"key_mouse", "kmous",
+	"mouse_info", "minfo",
+	"req_mouse_pos", "reqmp",
+	"get_mouse", "getm",
+	"set_a_foreground", "setaf",
+	"set_a_background", "setab",
+	"pkey_plab", "pfxl",
+	"device_type", "devt",
+	"code_set_init", "csin",
+	"set0_des_seq", "s0ds",
+	"set1_des_seq", "s1ds",
+	"set2_des_seq", "s2ds",
+	"set3_des_seq", "s3ds",
+	"set_lr_margin", "smglr",
+	"set_tb_margin", "smgtb",
+	"bit_image_repeat", "birep",
+	"bit_image_newline", "binel",
+	"bit_image_carriage_return", "bicr",
+	"color_names", "colornm",
+	"define_bit_image_region", "defbi",
+	"end_bit_image_region", "endbi",
+	"set_color_band", "setcolor",
+	"set_page_length", "slines",
+	"display_pc_char", "dispc",
+	"enter_pc_charset_mode", "smpch",
+	"exit_pc_charset_mode", "rmpch",
+	"enter_scancode_mode", "smsc",
+	"exit_scancode_mode", "rmsc",
+	"pc_term_options", "pctrm",
+	"scancode_escape", "scesc",
+	"alt_scancode_esc", "scesa",
+	"enter_horizontal_hl_mode", "ehhlm",
+	"enter_left_hl_mode", "elhlm",
+	"enter_low_hl_mode", "elohlm",
+	"enter_right_hl_mode", "erhlm",
+	"enter_top_hl_mode", "ethlm",
+	"enter_vertical_hl_mode", "evhlm",
+	"set_a_attributes", "sgr1",
+	"set_pglen_inch", "slength",
+	"termcap_init2", "",
+	"termcap_reset", "",
+	"linefeed_if_not_lf", "",
+	"backspace_if_not_bs", "",
+	"other_non_function_keys", "",
+	"arrow_key_map", "",
+	"acs_ulcorner", "",
+	"acs_llcorner", "",
+	"acs_urcorner", "",
+	"acs_lrcorner", "",
+	"acs_ltee", "",
+	"acs_rtee", "",
+	"acs_btee", "",
+	"acs_ttee", "",
+	"acs_hline", "",
+	"acs_vline", "",
+	"acs_plus", "",
+	"memory_lock", "",
+	"memory_unlock", "",
+	"box_chars_1", "",
+}

+ 246 - 0
vendor/github.com/Nvveen/Gotty/gotty.go

@@ -0,0 +1,246 @@
+// 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
+}

+ 362 - 0
vendor/github.com/Nvveen/Gotty/parser.go

@@ -0,0 +1,362 @@
+// 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.
+
+package gotty
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+var exp = [...]string{
+	"%%",
+	"%c",
+	"%s",
+	"%p(\\d)",
+	"%P([A-z])",
+	"%g([A-z])",
+	"%'(.)'",
+	"%{([0-9]+)}",
+	"%l",
+	"%\\+|%-|%\\*|%/|%m",
+	"%&|%\\||%\\^",
+	"%=|%>|%<",
+	"%A|%O",
+	"%!|%~",
+	"%i",
+	"%(:[\\ #\\-\\+]{0,4})?(\\d+\\.\\d+|\\d+)?[doxXs]",
+	"%\\?(.*?);",
+}
+
+var regex *regexp.Regexp
+var staticVar map[byte]stacker
+
+// Parses the attribute that is received with name attr and parameters params.
+func (term *TermInfo) Parse(attr string, params ...interface{}) (string, error) {
+	// Get the attribute name first.
+	iface, err := term.GetAttribute(attr)
+	str, ok := iface.(string)
+	if err != nil {
+		return "", err
+	}
+	if !ok {
+		return str, errors.New("Only string capabilities can be parsed.")
+	}
+	// Construct the hidden parser struct so we can use a recursive stack based
+	// parser.
+	ps := &parser{}
+	// Dynamic variables only exist in this context.
+	ps.dynamicVar = make(map[byte]stacker, 26)
+	ps.parameters = make([]stacker, len(params))
+	// Convert the parameters to insert them into the parser struct.
+	for i, x := range params {
+		ps.parameters[i] = x
+	}
+	// Recursively walk and return.
+	result, err := ps.walk(str)
+	return result, err
+}
+
+// Parses the attribute that is received with name attr and parameters params.
+// Only works on full name of a capability that is given, which it uses to
+// search for the termcap name.
+func (term *TermInfo) ParseName(attr string, params ...interface{}) (string, error) {
+	tc := GetTermcapName(attr)
+	return term.Parse(tc, params)
+}
+
+// Identify each token in a stack based manner and do the actual parsing.
+func (ps *parser) walk(attr string) (string, error) {
+	// We use a buffer to get the modified string.
+	var buf bytes.Buffer
+	// Next, find and identify all tokens by their indices and strings.
+	tokens := regex.FindAllStringSubmatch(attr, -1)
+	if len(tokens) == 0 {
+		return attr, nil
+	}
+	indices := regex.FindAllStringIndex(attr, -1)
+	q := 0 // q counts the matches of one token
+	// Iterate through the string per character.
+	for i := 0; i < len(attr); i++ {
+		// If the current position is an identified token, execute the following
+		// steps.
+		if q < len(indices) && i >= indices[q][0] && i < indices[q][1] {
+			// Switch on token.
+			switch {
+			case tokens[q][0][:2] == "%%":
+				// Literal percentage character.
+				buf.WriteByte('%')
+			case tokens[q][0][:2] == "%c":
+				// Pop a character.
+				c, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				buf.WriteByte(c.(byte))
+			case tokens[q][0][:2] == "%s":
+				// Pop a string.
+				str, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				if _, ok := str.(string); !ok {
+					return buf.String(), errors.New("Stack head is not a string")
+				}
+				buf.WriteString(str.(string))
+			case tokens[q][0][:2] == "%p":
+				// Push a parameter on the stack.
+				index, err := strconv.ParseInt(tokens[q][1], 10, 8)
+				index--
+				if err != nil {
+					return buf.String(), err
+				}
+				if int(index) >= len(ps.parameters) {
+					return buf.String(), errors.New("Parameters index out of bound")
+				}
+				ps.st.push(ps.parameters[index])
+			case tokens[q][0][:2] == "%P":
+				// Pop a variable from the stack as a dynamic or static variable.
+				val, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				index := tokens[q][2]
+				if len(index) > 1 {
+					errorStr := fmt.Sprintf("%s is not a valid dynamic variables index",
+						index)
+					return buf.String(), errors.New(errorStr)
+				}
+				// Specify either dynamic or static.
+				if index[0] >= 'a' && index[0] <= 'z' {
+					ps.dynamicVar[index[0]] = val
+				} else if index[0] >= 'A' && index[0] <= 'Z' {
+					staticVar[index[0]] = val
+				}
+			case tokens[q][0][:2] == "%g":
+				// Push a variable from the stack as a dynamic or static variable.
+				index := tokens[q][3]
+				if len(index) > 1 {
+					errorStr := fmt.Sprintf("%s is not a valid static variables index",
+						index)
+					return buf.String(), errors.New(errorStr)
+				}
+				var val stacker
+				if index[0] >= 'a' && index[0] <= 'z' {
+					val = ps.dynamicVar[index[0]]
+				} else if index[0] >= 'A' && index[0] <= 'Z' {
+					val = staticVar[index[0]]
+				}
+				ps.st.push(val)
+			case tokens[q][0][:2] == "%'":
+				// Push a character constant.
+				con := tokens[q][4]
+				if len(con) > 1 {
+					errorStr := fmt.Sprintf("%s is not a valid character constant", con)
+					return buf.String(), errors.New(errorStr)
+				}
+				ps.st.push(con[0])
+			case tokens[q][0][:2] == "%{":
+				// Push an integer constant.
+				con, err := strconv.ParseInt(tokens[q][5], 10, 32)
+				if err != nil {
+					return buf.String(), err
+				}
+				ps.st.push(con)
+			case tokens[q][0][:2] == "%l":
+				// Push the length of the string that is popped from the stack.
+				popStr, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				if _, ok := popStr.(string); !ok {
+					errStr := fmt.Sprintf("Stack head is not a string")
+					return buf.String(), errors.New(errStr)
+				}
+				ps.st.push(len(popStr.(string)))
+			case tokens[q][0][:2] == "%?":
+				// If-then-else construct. First, the whole string is identified and
+				// then inside this substring, we can specify which parts to switch on.
+				ifReg, _ := regexp.Compile("%\\?(.*)%t(.*)%e(.*);|%\\?(.*)%t(.*);")
+				ifTokens := ifReg.FindStringSubmatch(tokens[q][0])
+				var (
+					ifStr string
+					err   error
+				)
+				// Parse the if-part to determine if-else.
+				if len(ifTokens[1]) > 0 {
+					ifStr, err = ps.walk(ifTokens[1])
+				} else { // else
+					ifStr, err = ps.walk(ifTokens[4])
+				}
+				// Return any errors
+				if err != nil {
+					return buf.String(), err
+				} else if len(ifStr) > 0 {
+					// Self-defined limitation, not sure if this is correct, but didn't
+					// seem like it.
+					return buf.String(), errors.New("If-clause cannot print statements")
+				}
+				var thenStr string
+				// Pop the first value that is set by parsing the if-clause.
+				choose, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				// Switch to if or else.
+				if choose.(int) == 0 && len(ifTokens[1]) > 0 {
+					thenStr, err = ps.walk(ifTokens[3])
+				} else if choose.(int) != 0 {
+					if len(ifTokens[1]) > 0 {
+						thenStr, err = ps.walk(ifTokens[2])
+					} else {
+						thenStr, err = ps.walk(ifTokens[5])
+					}
+				}
+				if err != nil {
+					return buf.String(), err
+				}
+				buf.WriteString(thenStr)
+			case tokens[q][0][len(tokens[q][0])-1] == 'd': // Fallthrough for printing
+				fallthrough
+			case tokens[q][0][len(tokens[q][0])-1] == 'o': // digits.
+				fallthrough
+			case tokens[q][0][len(tokens[q][0])-1] == 'x':
+				fallthrough
+			case tokens[q][0][len(tokens[q][0])-1] == 'X':
+				fallthrough
+			case tokens[q][0][len(tokens[q][0])-1] == 's':
+				token := tokens[q][0]
+				// Remove the : that comes before a flag.
+				if token[1] == ':' {
+					token = token[:1] + token[2:]
+				}
+				digit, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				// The rest is determined like the normal formatted prints.
+				digitStr := fmt.Sprintf(token, digit.(int))
+				buf.WriteString(digitStr)
+			case tokens[q][0][:2] == "%i":
+				// Increment the parameters by one.
+				if len(ps.parameters) < 2 {
+					return buf.String(), errors.New("Not enough parameters to increment.")
+				}
+				val1, val2 := ps.parameters[0].(int), ps.parameters[1].(int)
+				val1++
+				val2++
+				ps.parameters[0], ps.parameters[1] = val1, val2
+			default:
+				// The rest of the tokens is a special case, where two values are
+				// popped and then operated on by the token that comes after them.
+				op1, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				op2, err := ps.st.pop()
+				if err != nil {
+					return buf.String(), err
+				}
+				var result stacker
+				switch tokens[q][0][:2] {
+				case "%+":
+					// Addition
+					result = op2.(int) + op1.(int)
+				case "%-":
+					// Subtraction
+					result = op2.(int) - op1.(int)
+				case "%*":
+					// Multiplication
+					result = op2.(int) * op1.(int)
+				case "%/":
+					// Division
+					result = op2.(int) / op1.(int)
+				case "%m":
+					// Modulo
+					result = op2.(int) % op1.(int)
+				case "%&":
+					// Bitwise AND
+					result = op2.(int) & op1.(int)
+				case "%|":
+					// Bitwise OR
+					result = op2.(int) | op1.(int)
+				case "%^":
+					// Bitwise XOR
+					result = op2.(int) ^ op1.(int)
+				case "%=":
+					// Equals
+					result = op2 == op1
+				case "%>":
+					// Greater-than
+					result = op2.(int) > op1.(int)
+				case "%<":
+					// Lesser-than
+					result = op2.(int) < op1.(int)
+				case "%A":
+					// Logical AND
+					result = op2.(bool) && op1.(bool)
+				case "%O":
+					// Logical OR
+					result = op2.(bool) || op1.(bool)
+				case "%!":
+					// Logical complement
+					result = !op1.(bool)
+				case "%~":
+					// Bitwise complement
+					result = ^(op1.(int))
+				}
+				ps.st.push(result)
+			}
+
+			i = indices[q][1] - 1
+			q++
+		} else {
+			// We are not "inside" a token, so just skip until the end or the next
+			// token, and add all characters to the buffer.
+			j := i
+			if q != len(indices) {
+				for !(j >= indices[q][0] && j < indices[q][1]) {
+					j++
+				}
+			} else {
+				j = len(attr)
+			}
+			buf.WriteString(string(attr[i:j]))
+			i = j
+		}
+	}
+	// Return the buffer as a string.
+	return buf.String(), nil
+}
+
+// Push a stacker-value onto the stack.
+func (st *stack) push(s stacker) {
+	*st = append(*st, s)
+}
+
+// Pop a stacker-value from the stack.
+func (st *stack) pop() (stacker, error) {
+	if len(*st) == 0 {
+		return nil, errors.New("Stack is empty.")
+	}
+	newStack := make(stack, len(*st)-1)
+	val := (*st)[len(*st)-1]
+	copy(newStack, (*st)[:len(*st)-1])
+	*st = newStack
+	return val, nil
+}
+
+// Initialize regexes and the static vars (that don't get changed between
+// calls.
+func init() {
+	// Initialize the main regex.
+	expStr := strings.Join(exp[:], "|")
+	regex, _ = regexp.Compile(expStr)
+	// Initialize the static variables.
+	staticVar = make(map[byte]stacker, 26)
+}

+ 23 - 0
vendor/github.com/Nvveen/Gotty/types.go

@@ -0,0 +1,23 @@
+// 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.
+
+package gotty
+
+type TermInfo struct {
+	boolAttributes map[string]bool
+	numAttributes  map[string]int16
+	strAttributes  map[string]string
+	// The various names of the TermInfo file.
+	Names []string
+}
+
+type stacker interface {
+}
+type stack []stacker
+
+type parser struct {
+	st         stack
+	parameters []stacker
+	dynamicVar map[byte]stacker
+}