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 }