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