dynamic.go 12 KB

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