1// +build !windows 2// This file contains a simple and incomplete implementation of the terminfo 3// database. Information was taken from the ncurses manpages term(5) and 4// terminfo(5). Currently, only the string capabilities for special keys and for 5// functions without parameters are actually used. Colors are still done with 6// ANSI escape sequences. Other special features that are not (yet?) supported 7// are reading from ~/.terminfo, the TERMINFO_DIRS variable, Berkeley database 8// format and extended capabilities. 9 10package termbox 11 12import ( 13 "bytes" 14 "encoding/binary" 15 "encoding/hex" 16 "errors" 17 "fmt" 18 "io/ioutil" 19 "os" 20 "strings" 21) 22 23const ( 24 ti_magic = 0432 25 ti_header_length = 12 26 ti_mouse_enter = "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h" 27 ti_mouse_leave = "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l" 28) 29 30func load_terminfo() ([]byte, error) { 31 var data []byte 32 var err error 33 34 term := os.Getenv("TERM") 35 if term == "" { 36 return nil, fmt.Errorf("termbox: TERM not set") 37 } 38 39 // The following behaviour follows the one described in terminfo(5) as 40 // distributed by ncurses. 41 42 terminfo := os.Getenv("TERMINFO") 43 if terminfo != "" { 44 // if TERMINFO is set, no other directory should be searched 45 return ti_try_path(terminfo) 46 } 47 48 // next, consider ~/.terminfo 49 home := os.Getenv("HOME") 50 if home != "" { 51 data, err = ti_try_path(home + "/.terminfo") 52 if err == nil { 53 return data, nil 54 } 55 } 56 57 // next, TERMINFO_DIRS 58 dirs := os.Getenv("TERMINFO_DIRS") 59 if dirs != "" { 60 for _, dir := range strings.Split(dirs, ":") { 61 if dir == "" { 62 // "" -> "/usr/share/terminfo" 63 dir = "/usr/share/terminfo" 64 } 65 data, err = ti_try_path(dir) 66 if err == nil { 67 return data, nil 68 } 69 } 70 } 71 72 // fall back to /usr/share/terminfo 73 return ti_try_path("/usr/share/terminfo") 74} 75 76func ti_try_path(path string) (data []byte, err error) { 77 // load_terminfo already made sure it is set 78 term := os.Getenv("TERM") 79 80 // first try, the typical *nix path 81 terminfo := path + "/" + term[0:1] + "/" + term 82 data, err = ioutil.ReadFile(terminfo) 83 if err == nil { 84 return 85 } 86 87 // fallback to darwin specific dirs structure 88 terminfo = path + "/" + hex.EncodeToString([]byte(term[:1])) + "/" + term 89 data, err = ioutil.ReadFile(terminfo) 90 return 91} 92 93func setup_term_builtin() error { 94 name := os.Getenv("TERM") 95 if name == "" { 96 return errors.New("termbox: TERM environment variable not set") 97 } 98 99 for _, t := range terms { 100 if t.name == name { 101 keys = t.keys 102 funcs = t.funcs 103 return nil 104 } 105 } 106 107 compat_table := []struct { 108 partial string 109 keys []string 110 funcs []string 111 }{ 112 {"xterm", xterm_keys, xterm_funcs}, 113 {"rxvt", rxvt_unicode_keys, rxvt_unicode_funcs}, 114 {"linux", linux_keys, linux_funcs}, 115 {"Eterm", eterm_keys, eterm_funcs}, 116 {"screen", screen_keys, screen_funcs}, 117 // let's assume that 'cygwin' is xterm compatible 118 {"cygwin", xterm_keys, xterm_funcs}, 119 {"st", xterm_keys, xterm_funcs}, 120 } 121 122 // try compatibility variants 123 for _, it := range compat_table { 124 if strings.Contains(name, it.partial) { 125 keys = it.keys 126 funcs = it.funcs 127 return nil 128 } 129 } 130 131 return errors.New("termbox: unsupported terminal") 132} 133 134func setup_term() (err error) { 135 var data []byte 136 var header [6]int16 137 var str_offset, table_offset int16 138 139 data, err = load_terminfo() 140 if err != nil { 141 return setup_term_builtin() 142 } 143 144 rd := bytes.NewReader(data) 145 // 0: magic number, 1: size of names section, 2: size of boolean section, 3: 146 // size of numbers section (in integers), 4: size of the strings section (in 147 // integers), 5: size of the string table 148 149 err = binary.Read(rd, binary.LittleEndian, header[:]) 150 if err != nil { 151 return 152 } 153 154 if (header[1]+header[2])%2 != 0 { 155 // old quirk to align everything on word boundaries 156 header[2] += 1 157 } 158 str_offset = ti_header_length + header[1] + header[2] + 2*header[3] 159 table_offset = str_offset + 2*header[4] 160 161 keys = make([]string, 0xFFFF-key_min) 162 for i, _ := range keys { 163 keys[i], err = ti_read_string(rd, str_offset+2*ti_keys[i], table_offset) 164 if err != nil { 165 return 166 } 167 } 168 funcs = make([]string, t_max_funcs) 169 // the last two entries are reserved for mouse. because the table offset is 170 // not there, the two entries have to fill in manually 171 for i, _ := range funcs[:len(funcs)-2] { 172 funcs[i], err = ti_read_string(rd, str_offset+2*ti_funcs[i], table_offset) 173 if err != nil { 174 return 175 } 176 } 177 funcs[t_max_funcs-2] = ti_mouse_enter 178 funcs[t_max_funcs-1] = ti_mouse_leave 179 return nil 180} 181 182func ti_read_string(rd *bytes.Reader, str_off, table int16) (string, error) { 183 var off int16 184 185 _, err := rd.Seek(int64(str_off), 0) 186 if err != nil { 187 return "", err 188 } 189 err = binary.Read(rd, binary.LittleEndian, &off) 190 if err != nil { 191 return "", err 192 } 193 _, err = rd.Seek(int64(table+off), 0) 194 if err != nil { 195 return "", err 196 } 197 var bs []byte 198 for { 199 b, err := rd.ReadByte() 200 if err != nil { 201 return "", err 202 } 203 if b == byte(0x00) { 204 break 205 } 206 bs = append(bs, b) 207 } 208 return string(bs), nil 209} 210 211// "Maps" the function constants from termbox.go to the number of the respective 212// string capability in the terminfo file. Taken from (ncurses) term.h. 213var ti_funcs = []int16{ 214 28, 40, 16, 13, 5, 39, 36, 27, 26, 34, 89, 88, 215} 216 217// Same as above for the special keys. 218var ti_keys = []int16{ 219 66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69, 70, 220 71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61, 79, 83, 221} 222