nt

A sensible note-taking program
git clone git://git.laack.co/nt.git
Log | Files | Refs | README

dynamic.go (12098B)


      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 		return nil, "", err
    189 	}
    190 	t := &terminfo.Terminfo{}
    191 	t.Name = tc.name
    192 	t.Aliases = tc.aliases
    193 	t.Colors = tc.getnum("colors")
    194 	t.Columns = tc.getnum("cols")
    195 	t.Lines = tc.getnum("lines")
    196 	t.Bell = tc.getstr("bel")
    197 	t.Clear = tc.getstr("clear")
    198 	t.EnterCA = tc.getstr("smcup")
    199 	t.ExitCA = tc.getstr("rmcup")
    200 	t.ShowCursor = tc.getstr("cnorm")
    201 	t.HideCursor = tc.getstr("civis")
    202 	t.AttrOff = tc.getstr("sgr0")
    203 	t.Underline = tc.getstr("smul")
    204 	t.Bold = tc.getstr("bold")
    205 	t.Blink = tc.getstr("blink")
    206 	t.Dim = tc.getstr("dim")
    207 	t.Italic = tc.getstr("sitm")
    208 	t.Reverse = tc.getstr("rev")
    209 	t.EnterKeypad = tc.getstr("smkx")
    210 	t.ExitKeypad = tc.getstr("rmkx")
    211 	t.SetFg = tc.getstr("setaf")
    212 	t.SetBg = tc.getstr("setab")
    213 	t.SetCursor = tc.getstr("cup")
    214 	t.CursorBack1 = tc.getstr("cub1")
    215 	t.CursorUp1 = tc.getstr("cuu1")
    216 	t.KeyF1 = tc.getstr("kf1")
    217 	t.KeyF2 = tc.getstr("kf2")
    218 	t.KeyF3 = tc.getstr("kf3")
    219 	t.KeyF4 = tc.getstr("kf4")
    220 	t.KeyF5 = tc.getstr("kf5")
    221 	t.KeyF6 = tc.getstr("kf6")
    222 	t.KeyF7 = tc.getstr("kf7")
    223 	t.KeyF8 = tc.getstr("kf8")
    224 	t.KeyF9 = tc.getstr("kf9")
    225 	t.KeyF10 = tc.getstr("kf10")
    226 	t.KeyF11 = tc.getstr("kf11")
    227 	t.KeyF12 = tc.getstr("kf12")
    228 	t.KeyF13 = tc.getstr("kf13")
    229 	t.KeyF14 = tc.getstr("kf14")
    230 	t.KeyF15 = tc.getstr("kf15")
    231 	t.KeyF16 = tc.getstr("kf16")
    232 	t.KeyF17 = tc.getstr("kf17")
    233 	t.KeyF18 = tc.getstr("kf18")
    234 	t.KeyF19 = tc.getstr("kf19")
    235 	t.KeyF20 = tc.getstr("kf20")
    236 	t.KeyF21 = tc.getstr("kf21")
    237 	t.KeyF22 = tc.getstr("kf22")
    238 	t.KeyF23 = tc.getstr("kf23")
    239 	t.KeyF24 = tc.getstr("kf24")
    240 	t.KeyF25 = tc.getstr("kf25")
    241 	t.KeyF26 = tc.getstr("kf26")
    242 	t.KeyF27 = tc.getstr("kf27")
    243 	t.KeyF28 = tc.getstr("kf28")
    244 	t.KeyF29 = tc.getstr("kf29")
    245 	t.KeyF30 = tc.getstr("kf30")
    246 	t.KeyF31 = tc.getstr("kf31")
    247 	t.KeyF32 = tc.getstr("kf32")
    248 	t.KeyF33 = tc.getstr("kf33")
    249 	t.KeyF34 = tc.getstr("kf34")
    250 	t.KeyF35 = tc.getstr("kf35")
    251 	t.KeyF36 = tc.getstr("kf36")
    252 	t.KeyF37 = tc.getstr("kf37")
    253 	t.KeyF38 = tc.getstr("kf38")
    254 	t.KeyF39 = tc.getstr("kf39")
    255 	t.KeyF40 = tc.getstr("kf40")
    256 	t.KeyF41 = tc.getstr("kf41")
    257 	t.KeyF42 = tc.getstr("kf42")
    258 	t.KeyF43 = tc.getstr("kf43")
    259 	t.KeyF44 = tc.getstr("kf44")
    260 	t.KeyF45 = tc.getstr("kf45")
    261 	t.KeyF46 = tc.getstr("kf46")
    262 	t.KeyF47 = tc.getstr("kf47")
    263 	t.KeyF48 = tc.getstr("kf48")
    264 	t.KeyF49 = tc.getstr("kf49")
    265 	t.KeyF50 = tc.getstr("kf50")
    266 	t.KeyF51 = tc.getstr("kf51")
    267 	t.KeyF52 = tc.getstr("kf52")
    268 	t.KeyF53 = tc.getstr("kf53")
    269 	t.KeyF54 = tc.getstr("kf54")
    270 	t.KeyF55 = tc.getstr("kf55")
    271 	t.KeyF56 = tc.getstr("kf56")
    272 	t.KeyF57 = tc.getstr("kf57")
    273 	t.KeyF58 = tc.getstr("kf58")
    274 	t.KeyF59 = tc.getstr("kf59")
    275 	t.KeyF60 = tc.getstr("kf60")
    276 	t.KeyF61 = tc.getstr("kf61")
    277 	t.KeyF62 = tc.getstr("kf62")
    278 	t.KeyF63 = tc.getstr("kf63")
    279 	t.KeyF64 = tc.getstr("kf64")
    280 	t.KeyInsert = tc.getstr("kich1")
    281 	t.KeyDelete = tc.getstr("kdch1")
    282 	t.KeyBackspace = tc.getstr("kbs")
    283 	t.KeyHome = tc.getstr("khome")
    284 	t.KeyEnd = tc.getstr("kend")
    285 	t.KeyUp = tc.getstr("kcuu1")
    286 	t.KeyDown = tc.getstr("kcud1")
    287 	t.KeyRight = tc.getstr("kcuf1")
    288 	t.KeyLeft = tc.getstr("kcub1")
    289 	t.KeyPgDn = tc.getstr("knp")
    290 	t.KeyPgUp = tc.getstr("kpp")
    291 	t.KeyBacktab = tc.getstr("kcbt")
    292 	t.KeyExit = tc.getstr("kext")
    293 	t.KeyCancel = tc.getstr("kcan")
    294 	t.KeyPrint = tc.getstr("kprt")
    295 	t.KeyHelp = tc.getstr("khlp")
    296 	t.KeyClear = tc.getstr("kclr")
    297 	t.AltChars = tc.getstr("acsc")
    298 	t.EnterAcs = tc.getstr("smacs")
    299 	t.ExitAcs = tc.getstr("rmacs")
    300 	t.EnableAcs = tc.getstr("enacs")
    301 	t.Mouse = tc.getstr("kmous")
    302 	t.KeyShfRight = tc.getstr("kRIT")
    303 	t.KeyShfLeft = tc.getstr("kLFT")
    304 	t.KeyShfHome = tc.getstr("kHOM")
    305 	t.KeyShfEnd = tc.getstr("kEND")
    306 
    307 	// Terminfo lacks descriptions for a bunch of modified keys,
    308 	// but modern XTerm and emulators often have them.  Let's add them,
    309 	// if the shifted right and left arrows are defined.
    310 	if t.KeyShfRight == "\x1b[1;2C" && t.KeyShfLeft == "\x1b[1;2D" {
    311 		t.Modifiers = terminfo.ModifiersXTerm
    312 
    313 		t.KeyShfUp = "\x1b[1;2A"
    314 		t.KeyShfDown = "\x1b[1;2B"
    315 		t.KeyMetaUp = "\x1b[1;9A"
    316 		t.KeyMetaDown = "\x1b[1;9B"
    317 		t.KeyMetaRight = "\x1b[1;9C"
    318 		t.KeyMetaLeft = "\x1b[1;9D"
    319 		t.KeyAltUp = "\x1b[1;3A"
    320 		t.KeyAltDown = "\x1b[1;3B"
    321 		t.KeyAltRight = "\x1b[1;3C"
    322 		t.KeyAltLeft = "\x1b[1;3D"
    323 		t.KeyCtrlUp = "\x1b[1;5A"
    324 		t.KeyCtrlDown = "\x1b[1;5B"
    325 		t.KeyCtrlRight = "\x1b[1;5C"
    326 		t.KeyCtrlLeft = "\x1b[1;5D"
    327 		t.KeyAltShfUp = "\x1b[1;4A"
    328 		t.KeyAltShfDown = "\x1b[1;4B"
    329 		t.KeyAltShfRight = "\x1b[1;4C"
    330 		t.KeyAltShfLeft = "\x1b[1;4D"
    331 
    332 		t.KeyMetaShfUp = "\x1b[1;10A"
    333 		t.KeyMetaShfDown = "\x1b[1;10B"
    334 		t.KeyMetaShfRight = "\x1b[1;10C"
    335 		t.KeyMetaShfLeft = "\x1b[1;10D"
    336 
    337 		t.KeyCtrlShfUp = "\x1b[1;6A"
    338 		t.KeyCtrlShfDown = "\x1b[1;6B"
    339 		t.KeyCtrlShfRight = "\x1b[1;6C"
    340 		t.KeyCtrlShfLeft = "\x1b[1;6D"
    341 
    342 		t.KeyShfPgUp = "\x1b[5;2~"
    343 		t.KeyShfPgDn = "\x1b[6;2~"
    344 	}
    345 	// And also for Home and End
    346 	if t.KeyShfHome == "\x1b[1;2H" && t.KeyShfEnd == "\x1b[1;2F" {
    347 		t.KeyCtrlHome = "\x1b[1;5H"
    348 		t.KeyCtrlEnd = "\x1b[1;5F"
    349 		t.KeyAltHome = "\x1b[1;9H"
    350 		t.KeyAltEnd = "\x1b[1;9F"
    351 		t.KeyCtrlShfHome = "\x1b[1;6H"
    352 		t.KeyCtrlShfEnd = "\x1b[1;6F"
    353 		t.KeyAltShfHome = "\x1b[1;4H"
    354 		t.KeyAltShfEnd = "\x1b[1;4F"
    355 		t.KeyMetaShfHome = "\x1b[1;10H"
    356 		t.KeyMetaShfEnd = "\x1b[1;10F"
    357 	}
    358 
    359 	// And the same thing for rxvt and workalikes (Eterm, aterm, etc.)
    360 	// It seems that urxvt at least send escaped as ALT prefix for these,
    361 	// although some places seem to indicate a separate ALT key sesquence.
    362 	if t.KeyShfRight == "\x1b[c" && t.KeyShfLeft == "\x1b[d" {
    363 		t.KeyShfUp = "\x1b[a"
    364 		t.KeyShfDown = "\x1b[b"
    365 		t.KeyCtrlUp = "\x1b[Oa"
    366 		t.KeyCtrlDown = "\x1b[Ob"
    367 		t.KeyCtrlRight = "\x1b[Oc"
    368 		t.KeyCtrlLeft = "\x1b[Od"
    369 	}
    370 	if t.KeyShfHome == "\x1b[7$" && t.KeyShfEnd == "\x1b[8$" {
    371 		t.KeyCtrlHome = "\x1b[7^"
    372 		t.KeyCtrlEnd = "\x1b[8^"
    373 	}
    374 
    375 	// Technically the RGB flag that is provided for xterm-direct is not
    376 	// quite right.  The problem is that the -direct flag that was introduced
    377 	// with ncurses 6.1 requires a parsing for the parameters that we lack.
    378 	// For this case we'll just assume it's XTerm compatible.  Someday this
    379 	// may be incorrect, but right now it is correct, and nobody uses it
    380 	// anyway.
    381 	if tc.getflag("Tc") {
    382 		// This presumes XTerm 24-bit true color.
    383 		t.TrueColor = true
    384 	} else if tc.getflag("RGB") {
    385 		// This is for xterm-direct, which uses a different scheme entirely.
    386 		// (ncurses went a very different direction from everyone else, and
    387 		// so it's unlikely anything is using this definition.)
    388 		t.TrueColor = true
    389 		t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"
    390 		t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"
    391 	}
    392 
    393 	// We only support colors in ANSI 8 or 256 color mode.
    394 	if t.Colors < 8 || t.SetFg == "" {
    395 		t.Colors = 0
    396 	}
    397 	if t.SetCursor == "" {
    398 		return nil, "", errNotAddressable
    399 	}
    400 
    401 	// For padding, we lookup the pad char.  If that isn't present,
    402 	// and npc is *not* set, then we assume a null byte.
    403 	t.PadChar = tc.getstr("pad")
    404 	if t.PadChar == "" {
    405 		if !tc.getflag("npc") {
    406 			t.PadChar = "\u0000"
    407 		}
    408 	}
    409 
    410 	// For terminals that use "standard" SGR sequences, lets combine the
    411 	// foreground and background together.
    412 	if strings.HasPrefix(t.SetFg, "\x1b[") &&
    413 		strings.HasPrefix(t.SetBg, "\x1b[") &&
    414 		strings.HasSuffix(t.SetFg, "m") &&
    415 		strings.HasSuffix(t.SetBg, "m") {
    416 		fg := t.SetFg[:len(t.SetFg)-1]
    417 		r := regexp.MustCompile("%p1")
    418 		bg := r.ReplaceAllString(t.SetBg[2:], "%p2")
    419 		t.SetFgBg = fg + ";" + bg
    420 	}
    421 
    422 	return t, tc.desc, nil
    423 }