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