1// Copyright 2019 The TCell Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use file except in compliance with the License.
5// You may obtain a copy of the license at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package terminfo
16
17import (
18	"bytes"
19	"errors"
20	"fmt"
21	"io"
22	"os"
23	"strconv"
24	"strings"
25	"sync"
26	"time"
27)
28
29var (
30	// ErrTermNotFound indicates that a suitable terminal entry could
31	// not be found.  This can result from either not having TERM set,
32	// or from the TERM failing to support certain minimal functionality,
33	// in particular absolute cursor addressability (the cup capability)
34	// is required.  For example, legacy "adm3" lacks this capability,
35	// whereas the slightly newer "adm3a" supports it.  This failure
36	// occurs most often with "dumb".
37	ErrTermNotFound = errors.New("terminal entry not found")
38)
39
40// Terminfo represents a terminfo entry.  Note that we use friendly names
41// in Go, but when we write out JSON, we use the same names as terminfo.
42// The name, aliases and smous, rmous fields do not come from terminfo directly.
43type Terminfo struct {
44	Name         string
45	Aliases      []string
46	Columns      int    // cols
47	Lines        int    // lines
48	Colors       int    // colors
49	Bell         string // bell
50	Clear        string // clear
51	EnterCA      string // smcup
52	ExitCA       string // rmcup
53	ShowCursor   string // cnorm
54	HideCursor   string // civis
55	AttrOff      string // sgr0
56	Underline    string // smul
57	Bold         string // bold
58	Blink        string // blink
59	Reverse      string // rev
60	Dim          string // dim
61	EnterKeypad  string // smkx
62	ExitKeypad   string // rmkx
63	SetFg        string // setaf
64	SetBg        string // setab
65	SetCursor    string // cup
66	CursorBack1  string // cub1
67	CursorUp1    string // cuu1
68	PadChar      string // pad
69	KeyBackspace string // kbs
70	KeyF1        string // kf1
71	KeyF2        string // kf2
72	KeyF3        string // kf3
73	KeyF4        string // kf4
74	KeyF5        string // kf5
75	KeyF6        string // kf6
76	KeyF7        string // kf7
77	KeyF8        string // kf8
78	KeyF9        string // kf9
79	KeyF10       string // kf10
80	KeyF11       string // kf11
81	KeyF12       string // kf12
82	KeyF13       string // kf13
83	KeyF14       string // kf14
84	KeyF15       string // kf15
85	KeyF16       string // kf16
86	KeyF17       string // kf17
87	KeyF18       string // kf18
88	KeyF19       string // kf19
89	KeyF20       string // kf20
90	KeyF21       string // kf21
91	KeyF22       string // kf22
92	KeyF23       string // kf23
93	KeyF24       string // kf24
94	KeyF25       string // kf25
95	KeyF26       string // kf26
96	KeyF27       string // kf27
97	KeyF28       string // kf28
98	KeyF29       string // kf29
99	KeyF30       string // kf30
100	KeyF31       string // kf31
101	KeyF32       string // kf32
102	KeyF33       string // kf33
103	KeyF34       string // kf34
104	KeyF35       string // kf35
105	KeyF36       string // kf36
106	KeyF37       string // kf37
107	KeyF38       string // kf38
108	KeyF39       string // kf39
109	KeyF40       string // kf40
110	KeyF41       string // kf41
111	KeyF42       string // kf42
112	KeyF43       string // kf43
113	KeyF44       string // kf44
114	KeyF45       string // kf45
115	KeyF46       string // kf46
116	KeyF47       string // kf47
117	KeyF48       string // kf48
118	KeyF49       string // kf49
119	KeyF50       string // kf50
120	KeyF51       string // kf51
121	KeyF52       string // kf52
122	KeyF53       string // kf53
123	KeyF54       string // kf54
124	KeyF55       string // kf55
125	KeyF56       string // kf56
126	KeyF57       string // kf57
127	KeyF58       string // kf58
128	KeyF59       string // kf59
129	KeyF60       string // kf60
130	KeyF61       string // kf61
131	KeyF62       string // kf62
132	KeyF63       string // kf63
133	KeyF64       string // kf64
134	KeyInsert    string // kich1
135	KeyDelete    string // kdch1
136	KeyHome      string // khome
137	KeyEnd       string // kend
138	KeyHelp      string // khlp
139	KeyPgUp      string // kpp
140	KeyPgDn      string // knp
141	KeyUp        string // kcuu1
142	KeyDown      string // kcud1
143	KeyLeft      string // kcub1
144	KeyRight     string // kcuf1
145	KeyBacktab   string // kcbt
146	KeyExit      string // kext
147	KeyClear     string // kclr
148	KeyPrint     string // kprt
149	KeyCancel    string // kcan
150	Mouse        string // kmous
151	MouseMode    string // XM
152	AltChars     string // acsc
153	EnterAcs     string // smacs
154	ExitAcs      string // rmacs
155	EnableAcs    string // enacs
156	KeyShfRight  string // kRIT
157	KeyShfLeft   string // kLFT
158	KeyShfHome   string // kHOM
159	KeyShfEnd    string // kEND
160
161	// These are non-standard extensions to terminfo.  This includes
162	// true color support, and some additional keys.  Its kind of bizarre
163	// that shifted variants of left and right exist, but not up and down.
164	// Terminal support for these are going to vary amongst XTerm
165	// emulations, so don't depend too much on them in your application.
166
167	SetFgBg         string // setfgbg
168	SetFgBgRGB      string // setfgbgrgb
169	SetFgRGB        string // setfrgb
170	SetBgRGB        string // setbrgb
171	KeyShfUp        string // shift-up
172	KeyShfDown      string // shift-down
173	KeyCtrlUp       string // ctrl-up
174	KeyCtrlDown     string // ctrl-left
175	KeyCtrlRight    string // ctrl-right
176	KeyCtrlLeft     string // ctrl-left
177	KeyMetaUp       string // meta-up
178	KeyMetaDown     string // meta-left
179	KeyMetaRight    string // meta-right
180	KeyMetaLeft     string // meta-left
181	KeyAltUp        string // alt-up
182	KeyAltDown      string // alt-left
183	KeyAltRight     string // alt-right
184	KeyAltLeft      string // alt-left
185	KeyCtrlHome     string
186	KeyCtrlEnd      string
187	KeyMetaHome     string
188	KeyMetaEnd      string
189	KeyAltHome      string
190	KeyAltEnd       string
191	KeyAltShfUp     string
192	KeyAltShfDown   string
193	KeyAltShfLeft   string
194	KeyAltShfRight  string
195	KeyMetaShfUp    string
196	KeyMetaShfDown  string
197	KeyMetaShfLeft  string
198	KeyMetaShfRight string
199	KeyCtrlShfUp    string
200	KeyCtrlShfDown  string
201	KeyCtrlShfLeft  string
202	KeyCtrlShfRight string
203	KeyCtrlShfHome  string
204	KeyCtrlShfEnd   string
205	KeyAltShfHome   string
206	KeyAltShfEnd    string
207	KeyMetaShfHome  string
208	KeyMetaShfEnd   string
209}
210
211type stackElem struct {
212	s     string
213	i     int
214	isStr bool
215	isInt bool
216}
217
218type stack []stackElem
219
220func (st stack) Push(v string) stack {
221	e := stackElem{
222		s:     v,
223		isStr: true,
224	}
225	return append(st, e)
226}
227
228func (st stack) Pop() (string, stack) {
229	v := ""
230	if len(st) > 0 {
231		e := st[len(st)-1]
232		st = st[:len(st)-1]
233		if e.isStr {
234			v = e.s
235		} else {
236			v = strconv.Itoa(e.i)
237		}
238	}
239	return v, st
240}
241
242func (st stack) PopInt() (int, stack) {
243	if len(st) > 0 {
244		e := st[len(st)-1]
245		st = st[:len(st)-1]
246		if e.isInt {
247			return e.i, st
248		} else if e.isStr {
249			i, _ := strconv.Atoi(e.s)
250			return i, st
251		}
252	}
253	return 0, st
254}
255
256func (st stack) PopBool() (bool, stack) {
257	if len(st) > 0 {
258		e := st[len(st)-1]
259		st = st[:len(st)-1]
260		if e.isStr {
261			if e.s == "1" {
262				return true, st
263			}
264			return false, st
265		} else if e.i == 1 {
266			return true, st
267		} else {
268			return false, st
269		}
270	}
271	return false, st
272}
273
274func (st stack) PushInt(i int) stack {
275	e := stackElem{
276		i:     i,
277		isInt: true,
278	}
279	return append(st, e)
280}
281
282func (st stack) PushBool(i bool) stack {
283	if i {
284		return st.PushInt(1)
285	}
286	return st.PushInt(0)
287}
288
289func nextch(s string, index int) (byte, int) {
290	if index < len(s) {
291		return s[index], index + 1
292	}
293	return 0, index
294}
295
296// static vars
297var svars [26]string
298
299// paramsBuffer handles some persistent state for TParam.  Technically we
300// could probably dispense with this, but caching buffer arrays gives us
301// a nice little performance boost.  Furthermore, we know that TParam is
302// rarely (never?) called re-entrantly, so we can just reuse the same
303// buffers, making it thread-safe by stashing a lock.
304type paramsBuffer struct {
305	out bytes.Buffer
306	buf bytes.Buffer
307	lk  sync.Mutex
308}
309
310// Start initializes the params buffer with the initial string data.
311// It also locks the paramsBuffer.  The caller must call End() when
312// finished.
313func (pb *paramsBuffer) Start(s string) {
314	pb.lk.Lock()
315	pb.out.Reset()
316	pb.buf.Reset()
317	pb.buf.WriteString(s)
318}
319
320// End returns the final output from TParam, but it also releases the lock.
321func (pb *paramsBuffer) End() string {
322	s := pb.out.String()
323	pb.lk.Unlock()
324	return s
325}
326
327// NextCh returns the next input character to the expander.
328func (pb *paramsBuffer) NextCh() (byte, error) {
329	return pb.buf.ReadByte()
330}
331
332// PutCh "emits" (rather schedules for output) a single byte character.
333func (pb *paramsBuffer) PutCh(ch byte) {
334	pb.out.WriteByte(ch)
335}
336
337// PutString schedules a string for output.
338func (pb *paramsBuffer) PutString(s string) {
339	pb.out.WriteString(s)
340}
341
342var pb = &paramsBuffer{}
343
344// TParm takes a terminfo parameterized string, such as setaf or cup, and
345// evaluates the string, and returns the result with the parameter
346// applied.
347func (t *Terminfo) TParm(s string, p ...int) string {
348	var stk stack
349	var a, b string
350	var ai, bi int
351	var ab bool
352	var dvars [26]string
353	var params [9]int
354
355	pb.Start(s)
356
357	// make sure we always have 9 parameters -- makes it easier
358	// later to skip checks
359	for i := 0; i < len(params) && i < len(p); i++ {
360		params[i] = p[i]
361	}
362
363	nest := 0
364
365	for {
366
367		ch, err := pb.NextCh()
368		if err != nil {
369			break
370		}
371
372		if ch != '%' {
373			pb.PutCh(ch)
374			continue
375		}
376
377		ch, err = pb.NextCh()
378		if err != nil {
379			// XXX Error
380			break
381		}
382
383		switch ch {
384		case '%': // quoted %
385			pb.PutCh(ch)
386
387		case 'i': // increment both parameters (ANSI cup support)
388			params[0]++
389			params[1]++
390
391		case 'c', 's':
392			// NB: these, and 'd' below are special cased for
393			// efficiency.  They could be handled by the richer
394			// format support below, less efficiently.
395			a, stk = stk.Pop()
396			pb.PutString(a)
397
398		case 'd':
399			ai, stk = stk.PopInt()
400			pb.PutString(strconv.Itoa(ai))
401
402		case '0', '1', '2', '3', '4', 'x', 'X', 'o', ':':
403			// This is pretty suboptimal, but this is rarely used.
404			// None of the mainstream terminals use any of this,
405			// and it would surprise me if this code is ever
406			// executed outside of test cases.
407			f := "%"
408			if ch == ':' {
409				ch, _ = pb.NextCh()
410			}
411			f += string(ch)
412			for ch == '+' || ch == '-' || ch == '#' || ch == ' ' {
413				ch, _ = pb.NextCh()
414				f += string(ch)
415			}
416			for (ch >= '0' && ch <= '9') || ch == '.' {
417				ch, _ = pb.NextCh()
418				f += string(ch)
419			}
420			switch ch {
421			case 'd', 'x', 'X', 'o':
422				ai, stk = stk.PopInt()
423				pb.PutString(fmt.Sprintf(f, ai))
424			case 'c', 's':
425				a, stk = stk.Pop()
426				pb.PutString(fmt.Sprintf(f, a))
427			}
428
429		case 'p': // push parameter
430			ch, _ = pb.NextCh()
431			ai = int(ch - '1')
432			if ai >= 0 && ai < len(params) {
433				stk = stk.PushInt(params[ai])
434			} else {
435				stk = stk.PushInt(0)
436			}
437
438		case 'P': // pop & store variable
439			ch, _ = pb.NextCh()
440			if ch >= 'A' && ch <= 'Z' {
441				svars[int(ch-'A')], stk = stk.Pop()
442			} else if ch >= 'a' && ch <= 'z' {
443				dvars[int(ch-'a')], stk = stk.Pop()
444			}
445
446		case 'g': // recall & push variable
447			ch, _ = pb.NextCh()
448			if ch >= 'A' && ch <= 'Z' {
449				stk = stk.Push(svars[int(ch-'A')])
450			} else if ch >= 'a' && ch <= 'z' {
451				stk = stk.Push(dvars[int(ch-'a')])
452			}
453
454		case '\'': // push(char)
455			ch, _ = pb.NextCh()
456			pb.NextCh() // must be ' but we don't check
457			stk = stk.Push(string(ch))
458
459		case '{': // push(int)
460			ai = 0
461			ch, _ = pb.NextCh()
462			for ch >= '0' && ch <= '9' {
463				ai *= 10
464				ai += int(ch - '0')
465				ch, _ = pb.NextCh()
466			}
467			// ch must be '}' but no verification
468			stk = stk.PushInt(ai)
469
470		case 'l': // push(strlen(pop))
471			a, stk = stk.Pop()
472			stk = stk.PushInt(len(a))
473
474		case '+':
475			bi, stk = stk.PopInt()
476			ai, stk = stk.PopInt()
477			stk = stk.PushInt(ai + bi)
478
479		case '-':
480			bi, stk = stk.PopInt()
481			ai, stk = stk.PopInt()
482			stk = stk.PushInt(ai - bi)
483
484		case '*':
485			bi, stk = stk.PopInt()
486			ai, stk = stk.PopInt()
487			stk = stk.PushInt(ai * bi)
488
489		case '/':
490			bi, stk = stk.PopInt()
491			ai, stk = stk.PopInt()
492			if bi != 0 {
493				stk = stk.PushInt(ai / bi)
494			} else {
495				stk = stk.PushInt(0)
496			}
497
498		case 'm': // push(pop mod pop)
499			bi, stk = stk.PopInt()
500			ai, stk = stk.PopInt()
501			if bi != 0 {
502				stk = stk.PushInt(ai % bi)
503			} else {
504				stk = stk.PushInt(0)
505			}
506
507		case '&': // AND
508			bi, stk = stk.PopInt()
509			ai, stk = stk.PopInt()
510			stk = stk.PushInt(ai & bi)
511
512		case '|': // OR
513			bi, stk = stk.PopInt()
514			ai, stk = stk.PopInt()
515			stk = stk.PushInt(ai | bi)
516
517		case '^': // XOR
518			bi, stk = stk.PopInt()
519			ai, stk = stk.PopInt()
520			stk = stk.PushInt(ai ^ bi)
521
522		case '~': // bit complement
523			ai, stk = stk.PopInt()
524			stk = stk.PushInt(ai ^ -1)
525
526		case '!': // logical NOT
527			ai, stk = stk.PopInt()
528			stk = stk.PushBool(ai != 0)
529
530		case '=': // numeric compare or string compare
531			b, stk = stk.Pop()
532			a, stk = stk.Pop()
533			stk = stk.PushBool(a == b)
534
535		case '>': // greater than, numeric
536			bi, stk = stk.PopInt()
537			ai, stk = stk.PopInt()
538			stk = stk.PushBool(ai > bi)
539
540		case '<': // less than, numeric
541			bi, stk = stk.PopInt()
542			ai, stk = stk.PopInt()
543			stk = stk.PushBool(ai < bi)
544
545		case '?': // start conditional
546
547		case 't':
548			ab, stk = stk.PopBool()
549			if ab {
550				// just keep going
551				break
552			}
553			nest = 0
554		ifloop:
555			// this loop consumes everything until we hit our else,
556			// or the end of the conditional
557			for {
558				ch, err = pb.NextCh()
559				if err != nil {
560					break
561				}
562				if ch != '%' {
563					continue
564				}
565				ch, _ = pb.NextCh()
566				switch ch {
567				case ';':
568					if nest == 0 {
569						break ifloop
570					}
571					nest--
572				case '?':
573					nest++
574				case 'e':
575					if nest == 0 {
576						break ifloop
577					}
578				}
579			}
580
581		case 'e':
582			// if we got here, it means we didn't use the else
583			// in the 't' case above, and we should skip until
584			// the end of the conditional
585			nest = 0
586		elloop:
587			for {
588				ch, err = pb.NextCh()
589				if err != nil {
590					break
591				}
592				if ch != '%' {
593					continue
594				}
595				ch, _ = pb.NextCh()
596				switch ch {
597				case ';':
598					if nest == 0 {
599						break elloop
600					}
601					nest--
602				case '?':
603					nest++
604				}
605			}
606
607		case ';': // endif
608
609		}
610	}
611
612	return pb.End()
613}
614
615// TPuts emits the string to the writer, but expands inline padding
616// indications (of the form $<[delay]> where [delay] is msec) to
617// a suitable time (unless the terminfo string indicates this isn't needed
618// by specifying npc - no padding).  All Terminfo based strings should be
619// emitted using this function.
620func (t *Terminfo) TPuts(w io.Writer, s string) {
621	for {
622		beg := strings.Index(s, "$<")
623		if beg < 0 {
624			// Most strings don't need padding, which is good news!
625			io.WriteString(w, s)
626			return
627		}
628		io.WriteString(w, s[:beg])
629		s = s[beg+2:]
630		end := strings.Index(s, ">")
631		if end < 0 {
632			// unterminated.. just emit bytes unadulterated
633			io.WriteString(w, "$<"+s)
634			return
635		}
636		val := s[:end]
637		s = s[end+1:]
638		padus := 0
639		unit := time.Millisecond
640		dot := false
641	loop:
642		for i := range val {
643			switch val[i] {
644			case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
645				padus *= 10
646				padus += int(val[i] - '0')
647				if dot {
648					unit /= 10
649				}
650			case '.':
651				if !dot {
652					dot = true
653				} else {
654					break loop
655				}
656			default:
657				break loop
658			}
659		}
660
661		// Curses historically uses padding to achieve "fine grained"
662		// delays. We have much better clocks these days, and so we
663		// do not rely on padding but simply sleep a bit.
664		if len(t.PadChar) > 0 {
665			time.Sleep(unit * time.Duration(padus))
666		}
667	}
668}
669
670// TGoto returns a string suitable for addressing the cursor at the given
671// row and column.  The origin 0, 0 is in the upper left corner of the screen.
672func (t *Terminfo) TGoto(col, row int) string {
673	return t.TParm(t.SetCursor, row, col)
674}
675
676// TColor returns a string corresponding to the given foreground and background
677// colors.  Either fg or bg can be set to -1 to elide.
678func (t *Terminfo) TColor(fi, bi int) string {
679	rv := ""
680	// As a special case, we map bright colors to lower versions if the
681	// color table only holds 8.  For the remaining 240 colors, the user
682	// is out of luck.  Someday we could create a mapping table, but its
683	// not worth it.
684	if t.Colors == 8 {
685		if fi > 7 && fi < 16 {
686			fi -= 8
687		}
688		if bi > 7 && bi < 16 {
689			bi -= 8
690		}
691	}
692	if t.Colors > fi && fi >= 0 {
693		rv += t.TParm(t.SetFg, fi)
694	}
695	if t.Colors > bi && bi >= 0 {
696		rv += t.TParm(t.SetBg, bi)
697	}
698	return rv
699}
700
701var (
702	dblock    sync.Mutex
703	terminfos = make(map[string]*Terminfo)
704	aliases   = make(map[string]string)
705)
706
707// AddTerminfo can be called to register a new Terminfo entry.
708func AddTerminfo(t *Terminfo) {
709	dblock.Lock()
710	terminfos[t.Name] = t
711	for _, x := range t.Aliases {
712		terminfos[x] = t
713	}
714	dblock.Unlock()
715}
716
717// LookupTerminfo attempts to find a definition for the named $TERM.
718func LookupTerminfo(name string) (*Terminfo, error) {
719	if name == "" {
720		// else on windows: index out of bounds
721		// on the name[0] reference below
722		return nil, ErrTermNotFound
723	}
724
725	addtruecolor := false
726	switch os.Getenv("COLORTERM") {
727	case "truecolor", "24bit", "24-bit":
728		addtruecolor = true
729	}
730	dblock.Lock()
731	t := terminfos[name]
732	dblock.Unlock()
733
734	// If the name ends in -truecolor, then fabricate an entry
735	// from the corresponding -256color, -color, or bare terminal.
736	if t == nil && strings.HasSuffix(name, "-truecolor") {
737
738		suffixes := []string{
739			"-256color",
740			"-88color",
741			"-color",
742			"",
743		}
744		base := name[:len(name)-len("-truecolor")]
745		for _, s := range suffixes {
746			if t, _ = LookupTerminfo(base + s); t != nil {
747				addtruecolor = true
748				break
749			}
750		}
751	}
752
753	if t == nil {
754		return nil, ErrTermNotFound
755	}
756
757	switch os.Getenv("TCELL_TRUECOLOR") {
758	case "":
759	case "disable":
760		addtruecolor = false
761	default:
762		addtruecolor = true
763	}
764
765	// If the user has requested 24-bit color with $COLORTERM, then
766	// amend the value (unless already present).  This means we don't
767	// need to have a value present.
768	if addtruecolor &&
769		t.SetFgBgRGB == "" &&
770		t.SetFgRGB == "" &&
771		t.SetBgRGB == "" {
772
773		// Supply vanilla ISO 8613-6:1994 24-bit color sequences.
774		t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm"
775		t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm"
776		t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" +
777			"48;2;%p4%d;%p5%d;%p6%dm"
778	}
779
780	return t, nil
781}
782