...

Source file src/github.com/gdamore/tcell/v2/terminfo/dynamic/dynamic.go

Documentation: github.com/gdamore/tcell/v2/terminfo/dynamic

     1  // Copyright 2021 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  
    15  // The dynamic package is used to generate a terminal description dynamically,
    16  // using infocmp.  This is really a method of last resort, as the performance
    17  // will be slow, and it requires a working infocmp.  But, the hope is that it
    18  // will assist folks who have to deal with a terminal description that isn't
    19  // already built in.  This requires infocmp to be in the user's path, and to
    20  // support reasonably the -1 option.
    21  
    22  package dynamic
    23  
    24  import (
    25  	"bytes"
    26  	"errors"
    27  	"os/exec"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  
    32  	"github.com/gdamore/tcell/v2/terminfo"
    33  )
    34  
    35  type termcap struct {
    36  	name    string
    37  	desc    string
    38  	aliases []string
    39  	bools   map[string]bool
    40  	nums    map[string]int
    41  	strs    map[string]string
    42  }
    43  
    44  func (tc *termcap) getnum(s string) int {
    45  	return (tc.nums[s])
    46  }
    47  
    48  func (tc *termcap) getflag(s string) bool {
    49  	return (tc.bools[s])
    50  }
    51  
    52  func (tc *termcap) getstr(s string) string {
    53  	return (tc.strs[s])
    54  }
    55  
    56  const (
    57  	none = iota
    58  	control
    59  	escaped
    60  )
    61  
    62  var errNotAddressable = errors.New("terminal not cursor addressable")
    63  
    64  func unescape(s string) string {
    65  	// Various escapes are in \x format.  Control codes are
    66  	// encoded as ^M (carat followed by ASCII equivalent).
    67  	// escapes are: \e, \E - escape
    68  	//  \0 NULL, \n \l \r \t \b \f \s for equivalent C escape.
    69  	buf := &bytes.Buffer{}
    70  	esc := none
    71  
    72  	for i := 0; i < len(s); i++ {
    73  		c := s[i]
    74  		switch esc {
    75  		case none:
    76  			switch c {
    77  			case '\\':
    78  				esc = escaped
    79  			case '^':
    80  				esc = control
    81  			default:
    82  				buf.WriteByte(c)
    83  			}
    84  		case control:
    85  			buf.WriteByte(c ^ 1<<6)
    86  			esc = none
    87  		case escaped:
    88  			switch c {
    89  			case 'E', 'e':
    90  				buf.WriteByte(0x1b)
    91  			case '0', '1', '2', '3', '4', '5', '6', '7':
    92  				if i+2 < len(s) && s[i+1] >= '0' && s[i+1] <= '7' && s[i+2] >= '0' && s[i+2] <= '7' {
    93  					buf.WriteByte(((c - '0') * 64) + ((s[i+1] - '0') * 8) + (s[i+2] - '0'))
    94  					i = i + 2
    95  				} else if c == '0' {
    96  					buf.WriteByte(0)
    97  				}
    98  			case 'n':
    99  				buf.WriteByte('\n')
   100  			case 'r':
   101  				buf.WriteByte('\r')
   102  			case 't':
   103  				buf.WriteByte('\t')
   104  			case 'b':
   105  				buf.WriteByte('\b')
   106  			case 'f':
   107  				buf.WriteByte('\f')
   108  			case 's':
   109  				buf.WriteByte(' ')
   110  			default:
   111  				buf.WriteByte(c)
   112  			}
   113  			esc = none
   114  		}
   115  	}
   116  	return (buf.String())
   117  }
   118  
   119  func (tc *termcap) setupterm(name string) error {
   120  	cmd := exec.Command("infocmp", "-1", name)
   121  	output := &bytes.Buffer{}
   122  	cmd.Stdout = output
   123  
   124  	tc.strs = make(map[string]string)
   125  	tc.bools = make(map[string]bool)
   126  	tc.nums = make(map[string]int)
   127  
   128  	if err := cmd.Run(); err != nil {
   129  		return err
   130  	}
   131  
   132  	// Now parse the output.
   133  	// We get comment lines (starting with "#"), followed by
   134  	// a header line that looks like "<name>|<alias>|...|<desc>"
   135  	// then capabilities, one per line, starting with a tab and ending
   136  	// with a comma and newline.
   137  	lines := strings.Split(output.String(), "\n")
   138  	for len(lines) > 0 && strings.HasPrefix(lines[0], "#") {
   139  		lines = lines[1:]
   140  	}
   141  
   142  	// Ditch trailing empty last line
   143  	if lines[len(lines)-1] == "" {
   144  		lines = lines[:len(lines)-1]
   145  	}
   146  	header := lines[0]
   147  	if strings.HasSuffix(header, ",") {
   148  		header = header[:len(header)-1]
   149  	}
   150  	names := strings.Split(header, "|")
   151  	tc.name = names[0]
   152  	names = names[1:]
   153  	if len(names) > 0 {
   154  		tc.desc = names[len(names)-1]
   155  		names = names[:len(names)-1]
   156  	}
   157  	tc.aliases = names
   158  	for _, val := range lines[1:] {
   159  		if (!strings.HasPrefix(val, "\t")) ||
   160  			(!strings.HasSuffix(val, ",")) {
   161  			return (errors.New("malformed infocmp: " + val))
   162  		}
   163  
   164  		val = val[1:]
   165  		val = val[:len(val)-1]
   166  
   167  		if k := strings.SplitN(val, "=", 2); len(k) == 2 {
   168  			tc.strs[k[0]] = unescape(k[1])
   169  		} else if k := strings.SplitN(val, "#", 2); len(k) == 2 {
   170  			u, err := strconv.ParseUint(k[1], 0, 0)
   171  			if err != nil {
   172  				return (err)
   173  			}
   174  			tc.nums[k[0]] = int(u)
   175  		} else {
   176  			tc.bools[val] = true
   177  		}
   178  	}
   179  	return nil
   180  }
   181  
   182  // LoadTerminfo creates a Terminfo by for named terminal by attempting to parse
   183  // the output from infocmp.  This returns the terminfo entry, a description of
   184  // the terminal, and either nil or an error.
   185  func LoadTerminfo(name string) (*terminfo.Terminfo, string, error) {
   186  	var tc termcap
   187  	if err := tc.setupterm(name); err != nil {
   188  		if err != nil {
   189  			return nil, "", err
   190  		}
   191  	}
   192  	t := &terminfo.Terminfo{}
   193  	// If this is an alias record, then just emit the alias
   194  	t.Name = tc.name
   195  	if t.Name != name {
   196  		return t, "", nil
   197  	}
   198  	t.Aliases = tc.aliases
   199  	t.Colors = tc.getnum("colors")
   200  	t.Columns = tc.getnum("cols")
   201  	t.Lines = tc.getnum("lines")
   202  	t.Bell = tc.getstr("bel")
   203  	t.Clear = tc.getstr("clear")
   204  	t.EnterCA = tc.getstr("smcup")
   205  	t.ExitCA = tc.getstr("rmcup")
   206  	t.ShowCursor = tc.getstr("cnorm")
   207  	t.HideCursor = tc.getstr("civis")
   208  	t.AttrOff = tc.getstr("sgr0")
   209  	t.Underline = tc.getstr("smul")
   210  	t.Bold = tc.getstr("bold")
   211  	t.Blink = tc.getstr("blink")
   212  	t.Dim = tc.getstr("dim")
   213  	t.Italic = tc.getstr("sitm")
   214  	t.Reverse = tc.getstr("rev")
   215  	t.EnterKeypad = tc.getstr("smkx")
   216  	t.ExitKeypad = tc.getstr("rmkx")
   217  	t.SetFg = tc.getstr("setaf")
   218  	t.SetBg = tc.getstr("setab")
   219  	t.SetCursor = tc.getstr("cup")
   220  	t.CursorBack1 = tc.getstr("cub1")
   221  	t.CursorUp1 = tc.getstr("cuu1")
   222  	t.KeyF1 = tc.getstr("kf1")
   223  	t.KeyF2 = tc.getstr("kf2")
   224  	t.KeyF3 = tc.getstr("kf3")
   225  	t.KeyF4 = tc.getstr("kf4")
   226  	t.KeyF5 = tc.getstr("kf5")
   227  	t.KeyF6 = tc.getstr("kf6")
   228  	t.KeyF7 = tc.getstr("kf7")
   229  	t.KeyF8 = tc.getstr("kf8")
   230  	t.KeyF9 = tc.getstr("kf9")
   231  	t.KeyF10 = tc.getstr("kf10")
   232  	t.KeyF11 = tc.getstr("kf11")
   233  	t.KeyF12 = tc.getstr("kf12")
   234  	t.KeyF13 = tc.getstr("kf13")
   235  	t.KeyF14 = tc.getstr("kf14")
   236  	t.KeyF15 = tc.getstr("kf15")
   237  	t.KeyF16 = tc.getstr("kf16")
   238  	t.KeyF17 = tc.getstr("kf17")
   239  	t.KeyF18 = tc.getstr("kf18")
   240  	t.KeyF19 = tc.getstr("kf19")
   241  	t.KeyF20 = tc.getstr("kf20")
   242  	t.KeyF21 = tc.getstr("kf21")
   243  	t.KeyF22 = tc.getstr("kf22")
   244  	t.KeyF23 = tc.getstr("kf23")
   245  	t.KeyF24 = tc.getstr("kf24")
   246  	t.KeyF25 = tc.getstr("kf25")
   247  	t.KeyF26 = tc.getstr("kf26")
   248  	t.KeyF27 = tc.getstr("kf27")
   249  	t.KeyF28 = tc.getstr("kf28")
   250  	t.KeyF29 = tc.getstr("kf29")
   251  	t.KeyF30 = tc.getstr("kf30")
   252  	t.KeyF31 = tc.getstr("kf31")
   253  	t.KeyF32 = tc.getstr("kf32")
   254  	t.KeyF33 = tc.getstr("kf33")
   255  	t.KeyF34 = tc.getstr("kf34")
   256  	t.KeyF35 = tc.getstr("kf35")
   257  	t.KeyF36 = tc.getstr("kf36")
   258  	t.KeyF37 = tc.getstr("kf37")
   259  	t.KeyF38 = tc.getstr("kf38")
   260  	t.KeyF39 = tc.getstr("kf39")
   261  	t.KeyF40 = tc.getstr("kf40")
   262  	t.KeyF41 = tc.getstr("kf41")
   263  	t.KeyF42 = tc.getstr("kf42")
   264  	t.KeyF43 = tc.getstr("kf43")
   265  	t.KeyF44 = tc.getstr("kf44")
   266  	t.KeyF45 = tc.getstr("kf45")
   267  	t.KeyF46 = tc.getstr("kf46")
   268  	t.KeyF47 = tc.getstr("kf47")
   269  	t.KeyF48 = tc.getstr("kf48")
   270  	t.KeyF49 = tc.getstr("kf49")
   271  	t.KeyF50 = tc.getstr("kf50")
   272  	t.KeyF51 = tc.getstr("kf51")
   273  	t.KeyF52 = tc.getstr("kf52")
   274  	t.KeyF53 = tc.getstr("kf53")
   275  	t.KeyF54 = tc.getstr("kf54")
   276  	t.KeyF55 = tc.getstr("kf55")
   277  	t.KeyF56 = tc.getstr("kf56")
   278  	t.KeyF57 = tc.getstr("kf57")
   279  	t.KeyF58 = tc.getstr("kf58")
   280  	t.KeyF59 = tc.getstr("kf59")
   281  	t.KeyF60 = tc.getstr("kf60")
   282  	t.KeyF61 = tc.getstr("kf61")
   283  	t.KeyF62 = tc.getstr("kf62")
   284  	t.KeyF63 = tc.getstr("kf63")
   285  	t.KeyF64 = tc.getstr("kf64")
   286  	t.KeyInsert = tc.getstr("kich1")
   287  	t.KeyDelete = tc.getstr("kdch1")
   288  	t.KeyBackspace = tc.getstr("kbs")
   289  	t.KeyHome = tc.getstr("khome")
   290  	t.KeyEnd = tc.getstr("kend")
   291  	t.KeyUp = tc.getstr("kcuu1")
   292  	t.KeyDown = tc.getstr("kcud1")
   293  	t.KeyRight = tc.getstr("kcuf1")
   294  	t.KeyLeft = tc.getstr("kcub1")
   295  	t.KeyPgDn = tc.getstr("knp")
   296  	t.KeyPgUp = tc.getstr("kpp")
   297  	t.KeyBacktab = tc.getstr("kcbt")
   298  	t.KeyExit = tc.getstr("kext")
   299  	t.KeyCancel = tc.getstr("kcan")
   300  	t.KeyPrint = tc.getstr("kprt")
   301  	t.KeyHelp = tc.getstr("khlp")
   302  	t.KeyClear = tc.getstr("kclr")
   303  	t.AltChars = tc.getstr("acsc")
   304  	t.EnterAcs = tc.getstr("smacs")
   305  	t.ExitAcs = tc.getstr("rmacs")
   306  	t.EnableAcs = tc.getstr("enacs")
   307  	t.Mouse = tc.getstr("kmous")
   308  	t.KeyShfRight = tc.getstr("kRIT")
   309  	t.KeyShfLeft = tc.getstr("kLFT")
   310  	t.KeyShfHome = tc.getstr("kHOM")
   311  	t.KeyShfEnd = tc.getstr("kEND")
   312  
   313  	// Terminfo lacks descriptions for a bunch of modified keys,
   314  	// but modern XTerm and emulators often have them.  Let's add them,
   315  	// if the shifted right and left arrows are defined.
   316  	if t.KeyShfRight == "\x1b[1;2C" && t.KeyShfLeft == "\x1b[1;2D" {
   317  		t.KeyShfUp = "\x1b[1;2A"
   318  		t.KeyShfDown = "\x1b[1;2B"
   319  		t.KeyMetaUp = "\x1b[1;9A"
   320  		t.KeyMetaDown = "\x1b[1;9B"
   321  		t.KeyMetaRight = "\x1b[1;9C"
   322  		t.KeyMetaLeft = "\x1b[1;9D"
   323  		t.KeyAltUp = "\x1b[1;3A"
   324  		t.KeyAltDown = "\x1b[1;3B"
   325  		t.KeyAltRight = "\x1b[1;3C"
   326  		t.KeyAltLeft = "\x1b[1;3D"
   327  		t.KeyCtrlUp = "\x1b[1;5A"
   328  		t.KeyCtrlDown = "\x1b[1;5B"
   329  		t.KeyCtrlRight = "\x1b[1;5C"
   330  		t.KeyCtrlLeft = "\x1b[1;5D"
   331  		t.KeyAltShfUp = "\x1b[1;4A"
   332  		t.KeyAltShfDown = "\x1b[1;4B"
   333  		t.KeyAltShfRight = "\x1b[1;4C"
   334  		t.KeyAltShfLeft = "\x1b[1;4D"
   335  
   336  		t.KeyMetaShfUp = "\x1b[1;10A"
   337  		t.KeyMetaShfDown = "\x1b[1;10B"
   338  		t.KeyMetaShfRight = "\x1b[1;10C"
   339  		t.KeyMetaShfLeft = "\x1b[1;10D"
   340  
   341  		t.KeyCtrlShfUp = "\x1b[1;6A"
   342  		t.KeyCtrlShfDown = "\x1b[1;6B"
   343  		t.KeyCtrlShfRight = "\x1b[1;6C"
   344  		t.KeyCtrlShfLeft = "\x1b[1;6D"
   345  
   346  		t.KeyShfPgUp = "\x1b[5;2~"
   347  		t.KeyShfPgDn = "\x1b[6;2~"
   348  	}
   349  	// And also for Home and End
   350  	if t.KeyShfHome == "\x1b[1;2H" && t.KeyShfEnd == "\x1b[1;2F" {
   351  		t.KeyCtrlHome = "\x1b[1;5H"
   352  		t.KeyCtrlEnd = "\x1b[1;5F"
   353  		t.KeyAltHome = "\x1b[1;9H"
   354  		t.KeyAltEnd = "\x1b[1;9F"
   355  		t.KeyCtrlShfHome = "\x1b[1;6H"
   356  		t.KeyCtrlShfEnd = "\x1b[1;6F"
   357  		t.KeyAltShfHome = "\x1b[1;4H"
   358  		t.KeyAltShfEnd = "\x1b[1;4F"
   359  		t.KeyMetaShfHome = "\x1b[1;10H"
   360  		t.KeyMetaShfEnd = "\x1b[1;10F"
   361  	}
   362  
   363  	// And the same thing for rxvt and workalikes (Eterm, aterm, etc.)
   364  	// It seems that urxvt at least send escaped as ALT prefix for these,
   365  	// although some places seem to indicate a separate ALT key sesquence.
   366  	if t.KeyShfRight == "\x1b[c" && t.KeyShfLeft == "\x1b[d" {
   367  		t.KeyShfUp = "\x1b[a"
   368  		t.KeyShfDown = "\x1b[b"
   369  		t.KeyCtrlUp = "\x1b[Oa"
   370  		t.KeyCtrlDown = "\x1b[Ob"
   371  		t.KeyCtrlRight = "\x1b[Oc"
   372  		t.KeyCtrlLeft = "\x1b[Od"
   373  	}
   374  	if t.KeyShfHome == "\x1b[7$" && t.KeyShfEnd == "\x1b[8$" {
   375  		t.KeyCtrlHome = "\x1b[7^"
   376  		t.KeyCtrlEnd = "\x1b[8^"
   377  	}
   378  
   379  	// Technically the RGB flag that is provided for xterm-direct is not
   380  	// quite right.  The problem is that the -direct flag that was introduced
   381  	// with ncurses 6.1 requires a parsing for the parameters that we lack.
   382  	// For this case we'll just assume it's XTerm compatible.  Someday this
   383  	// may be incorrect, but right now it is correct, and nobody uses it
   384  	// anyway.
   385  	if tc.getflag("Tc") {
   386  		// This presumes XTerm 24-bit true color.
   387  		t.TrueColor = true
   388  	} else if tc.getflag("RGB") {
   389  		// This is for xterm-direct, which uses a different scheme entirely.
   390  		// (ncurses went a very different direction from everyone else, and
   391  		// so it's unlikely anything is using this definition.)
   392  		t.TrueColor = true
   393  		t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"
   394  		t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"
   395  	}
   396  
   397  	// We only support colors in ANSI 8 or 256 color mode.
   398  	if t.Colors < 8 || t.SetFg == "" {
   399  		t.Colors = 0
   400  	}
   401  	if t.SetCursor == "" {
   402  		return nil, "", errNotAddressable
   403  	}
   404  
   405  	// For padding, we lookup the pad char.  If that isn't present,
   406  	// and npc is *not* set, then we assume a null byte.
   407  	t.PadChar = tc.getstr("pad")
   408  	if t.PadChar == "" {
   409  		if !tc.getflag("npc") {
   410  			t.PadChar = "\u0000"
   411  		}
   412  	}
   413  
   414  	// For terminals that use "standard" SGR sequences, lets combine the
   415  	// foreground and background together.
   416  	if strings.HasPrefix(t.SetFg, "\x1b[") &&
   417  		strings.HasPrefix(t.SetBg, "\x1b[") &&
   418  		strings.HasSuffix(t.SetFg, "m") &&
   419  		strings.HasSuffix(t.SetBg, "m") {
   420  		fg := t.SetFg[:len(t.SetFg)-1]
   421  		r := regexp.MustCompile("%p1")
   422  		bg := r.ReplaceAllString(t.SetBg[2:], "%p2")
   423  		t.SetFgBg = fg + ";" + bg
   424  	}
   425  
   426  	return t, tc.desc, nil
   427  }
   428  

View as plain text