]> git.lizzy.rs Git - micro.git/blob - cmd/micro/terminfo/mkinfo.go
Code optimisation (#1117)
[micro.git] / cmd / micro / terminfo / mkinfo.go
1 // Copyright 2017 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 // This command is used to generate suitable configuration files in either
16 // go syntax or in JSON.  It defaults to JSON output on stdout.  If no
17 // term values are specified on the command line, then $TERM is used.
18 //
19 // Usage is like this:
20 //
21 // mkinfo [-init] [-go file.go] [-json file.json] [-quiet] [-nofatal] [<term>...]
22 //
23 // -gzip     specifies output should be compressed (json only)
24 // -go       specifies Go output into the named file.  Use - for stdout.
25 // -json     specifies JSON output in the named file.  Use - for stdout
26 // -nofatal  indicates that errors loading definitions should not be fatal
27 //
28
29 package terminfo
30
31 import (
32         "bytes"
33         "encoding/json"
34         "errors"
35         "fmt"
36         "io"
37         "os"
38         "os/exec"
39         "regexp"
40         "strconv"
41         "strings"
42 )
43
44 type termcap struct {
45         name    string
46         desc    string
47         aliases []string
48         bools   map[string]bool
49         nums    map[string]int
50         strs    map[string]string
51 }
52
53 func (tc *termcap) getnum(s string) int {
54         return (tc.nums[s])
55 }
56
57 func (tc *termcap) getflag(s string) bool {
58         return (tc.bools[s])
59 }
60
61 func (tc *termcap) getstr(s string) string {
62         return (tc.strs[s])
63 }
64
65 const (
66         NONE = iota
67         CTRL
68         ESC
69 )
70
71 func unescape(s string) string {
72         // Various escapes are in \x format.  Control codes are
73         // encoded as ^M (carat followed by ASCII equivalent).
74         // Escapes are: \e, \E - escape
75         //  \0 NULL, \n \l \r \t \b \f \s for equivalent C escape.
76         buf := &bytes.Buffer{}
77         esc := NONE
78
79         for i := 0; i < len(s); i++ {
80                 c := s[i]
81                 switch esc {
82                 case NONE:
83                         switch c {
84                         case '\\':
85                                 esc = ESC
86                         case '^':
87                                 esc = CTRL
88                         default:
89                                 buf.WriteByte(c)
90                         }
91                 case CTRL:
92                         buf.WriteByte(c - 0x40)
93                         esc = NONE
94                 case ESC:
95                         switch c {
96                         case 'E', 'e':
97                                 buf.WriteByte(0x1b)
98                         case '0', '1', '2', '3', '4', '5', '6', '7':
99                                 if i+2 < len(s) && s[i+1] >= '0' && s[i+1] <= '7' && s[i+2] >= '0' && s[i+2] <= '7' {
100                                         buf.WriteByte(((c - '0') * 64) + ((s[i+1] - '0') * 8) + (s[i+2] - '0'))
101                                         i = i + 2
102                                 } else if c == '0' {
103                                         buf.WriteByte(0)
104                                 }
105                         case 'n':
106                                 buf.WriteByte('\n')
107                         case 'r':
108                                 buf.WriteByte('\r')
109                         case 't':
110                                 buf.WriteByte('\t')
111                         case 'b':
112                                 buf.WriteByte('\b')
113                         case 'f':
114                                 buf.WriteByte('\f')
115                         case 's':
116                                 buf.WriteByte(' ')
117                         case 'l':
118                                 panic("WTF: weird format: " + s)
119                         default:
120                                 buf.WriteByte(c)
121                         }
122                         esc = NONE
123                 }
124         }
125         return (buf.String())
126 }
127
128 func (tc *termcap) setupterm(name string) error {
129         cmd := exec.Command("infocmp", "-1", name)
130         output := &bytes.Buffer{}
131         cmd.Stdout = output
132
133         tc.strs = make(map[string]string)
134         tc.bools = make(map[string]bool)
135         tc.nums = make(map[string]int)
136
137         err := cmd.Run()
138         if err != nil {
139                 return err
140         }
141
142         // Now parse the output.
143         // We get comment lines (starting with "#"), followed by
144         // a header line that looks like "<name>|<alias>|...|<desc>"
145         // then capabilities, one per line, starting with a tab and ending
146         // with a comma and newline.
147         lines := strings.Split(output.String(), "\n")
148         for len(lines) > 0 && strings.HasPrefix(lines[0], "#") {
149                 lines = lines[1:]
150         }
151
152         // Ditch trailing empty last line
153         if lines[len(lines)-1] == "" {
154                 lines = lines[:len(lines)-1]
155         }
156         header := lines[0]
157         if strings.HasSuffix(header, ",") {
158                 header = header[:len(header)-1]
159         }
160         names := strings.Split(header, "|")
161         tc.name = names[0]
162         names = names[1:]
163         if len(names) > 0 {
164                 tc.desc = names[len(names)-1]
165                 names = names[:len(names)-1]
166         }
167         tc.aliases = names
168         for _, val := range lines[1:] {
169                 if (!strings.HasPrefix(val, "\t")) ||
170                         (!strings.HasSuffix(val, ",")) {
171                         return (errors.New("malformed infocmp: " + val))
172                 }
173
174                 val = val[1:]
175                 val = val[:len(val)-1]
176
177                 if k := strings.SplitN(val, "=", 2); len(k) == 2 {
178                         tc.strs[k[0]] = unescape(k[1])
179                 } else if k := strings.SplitN(val, "#", 2); len(k) == 2 {
180                         if u, err := strconv.ParseUint(k[1], 10, 0); err != nil {
181                                 return (err)
182                         } else {
183                                 tc.nums[k[0]] = int(u)
184                         }
185                 } else {
186                         tc.bools[val] = true
187                 }
188         }
189         return nil
190 }
191
192 // This program is used to collect data from the system's terminfo library,
193 // and write it into Go source code.  That is, we maintain our terminfo
194 // capabilities encoded in the program.  It should never need to be run by
195 // an end user, but developers can use this to add codes for additional
196 // terminal types.
197 //
198 // If a terminal name ending with -truecolor is given, and we cannot find
199 // one, we will try to fabricate one from either the -256color (if present)
200 // or the unadorned base name, adding the XTerm specific 24-bit color
201 // escapes.  We believe that all 24-bit capable terminals use the same
202 // escape sequences, and terminfo has yet to evolve to support this.
203 func getinfo(name string) (*Terminfo, string, error) {
204         var tc termcap
205         addTrueColor := false
206         if err := tc.setupterm(name); err != nil {
207                 if strings.HasSuffix(name, "-truecolor") {
208                         base := name[:len(name)-len("-truecolor")]
209                         // Probably -256color is closest to what we want
210                         if err = tc.setupterm(base + "-256color"); err != nil {
211                                 err = tc.setupterm(base)
212                         }
213                         if err == nil {
214                                 addTrueColor = true
215                         }
216                         tc.name = name
217                 }
218                 if err != nil {
219                         return nil, "", err
220                 }
221         }
222         t := &Terminfo{}
223         // If this is an alias record, then just emit the alias
224         t.Name = tc.name
225         if t.Name != name {
226                 return t, "", nil
227         }
228         t.Aliases = tc.aliases
229         t.Colors = tc.getnum("colors")
230         t.Columns = tc.getnum("cols")
231         t.Lines = tc.getnum("lines")
232         t.Bell = tc.getstr("bel")
233         t.Clear = tc.getstr("clear")
234         t.EnterCA = tc.getstr("smcup")
235         t.ExitCA = tc.getstr("rmcup")
236         t.ShowCursor = tc.getstr("cnorm")
237         t.HideCursor = tc.getstr("civis")
238         t.AttrOff = tc.getstr("sgr0")
239         t.Underline = tc.getstr("smul")
240         t.Bold = tc.getstr("bold")
241         t.Blink = tc.getstr("blink")
242         t.Dim = tc.getstr("dim")
243         t.Reverse = tc.getstr("rev")
244         t.EnterKeypad = tc.getstr("smkx")
245         t.ExitKeypad = tc.getstr("rmkx")
246         t.SetFg = tc.getstr("setaf")
247         t.SetBg = tc.getstr("setab")
248         t.SetCursor = tc.getstr("cup")
249         t.CursorBack1 = tc.getstr("cub1")
250         t.CursorUp1 = tc.getstr("cuu1")
251         t.KeyF1 = tc.getstr("kf1")
252         t.KeyF2 = tc.getstr("kf2")
253         t.KeyF3 = tc.getstr("kf3")
254         t.KeyF4 = tc.getstr("kf4")
255         t.KeyF5 = tc.getstr("kf5")
256         t.KeyF6 = tc.getstr("kf6")
257         t.KeyF7 = tc.getstr("kf7")
258         t.KeyF8 = tc.getstr("kf8")
259         t.KeyF9 = tc.getstr("kf9")
260         t.KeyF10 = tc.getstr("kf10")
261         t.KeyF11 = tc.getstr("kf11")
262         t.KeyF12 = tc.getstr("kf12")
263         t.KeyF13 = tc.getstr("kf13")
264         t.KeyF14 = tc.getstr("kf14")
265         t.KeyF15 = tc.getstr("kf15")
266         t.KeyF16 = tc.getstr("kf16")
267         t.KeyF17 = tc.getstr("kf17")
268         t.KeyF18 = tc.getstr("kf18")
269         t.KeyF19 = tc.getstr("kf19")
270         t.KeyF20 = tc.getstr("kf20")
271         t.KeyF21 = tc.getstr("kf21")
272         t.KeyF22 = tc.getstr("kf22")
273         t.KeyF23 = tc.getstr("kf23")
274         t.KeyF24 = tc.getstr("kf24")
275         t.KeyF25 = tc.getstr("kf25")
276         t.KeyF26 = tc.getstr("kf26")
277         t.KeyF27 = tc.getstr("kf27")
278         t.KeyF28 = tc.getstr("kf28")
279         t.KeyF29 = tc.getstr("kf29")
280         t.KeyF30 = tc.getstr("kf30")
281         t.KeyF31 = tc.getstr("kf31")
282         t.KeyF32 = tc.getstr("kf32")
283         t.KeyF33 = tc.getstr("kf33")
284         t.KeyF34 = tc.getstr("kf34")
285         t.KeyF35 = tc.getstr("kf35")
286         t.KeyF36 = tc.getstr("kf36")
287         t.KeyF37 = tc.getstr("kf37")
288         t.KeyF38 = tc.getstr("kf38")
289         t.KeyF39 = tc.getstr("kf39")
290         t.KeyF40 = tc.getstr("kf40")
291         t.KeyF41 = tc.getstr("kf41")
292         t.KeyF42 = tc.getstr("kf42")
293         t.KeyF43 = tc.getstr("kf43")
294         t.KeyF44 = tc.getstr("kf44")
295         t.KeyF45 = tc.getstr("kf45")
296         t.KeyF46 = tc.getstr("kf46")
297         t.KeyF47 = tc.getstr("kf47")
298         t.KeyF48 = tc.getstr("kf48")
299         t.KeyF49 = tc.getstr("kf49")
300         t.KeyF50 = tc.getstr("kf50")
301         t.KeyF51 = tc.getstr("kf51")
302         t.KeyF52 = tc.getstr("kf52")
303         t.KeyF53 = tc.getstr("kf53")
304         t.KeyF54 = tc.getstr("kf54")
305         t.KeyF55 = tc.getstr("kf55")
306         t.KeyF56 = tc.getstr("kf56")
307         t.KeyF57 = tc.getstr("kf57")
308         t.KeyF58 = tc.getstr("kf58")
309         t.KeyF59 = tc.getstr("kf59")
310         t.KeyF60 = tc.getstr("kf60")
311         t.KeyF61 = tc.getstr("kf61")
312         t.KeyF62 = tc.getstr("kf62")
313         t.KeyF63 = tc.getstr("kf63")
314         t.KeyF64 = tc.getstr("kf64")
315         t.KeyInsert = tc.getstr("kich1")
316         t.KeyDelete = tc.getstr("kdch1")
317         t.KeyBackspace = tc.getstr("kbs")
318         t.KeyHome = tc.getstr("khome")
319         t.KeyEnd = tc.getstr("kend")
320         t.KeyUp = tc.getstr("kcuu1")
321         t.KeyDown = tc.getstr("kcud1")
322         t.KeyRight = tc.getstr("kcuf1")
323         t.KeyLeft = tc.getstr("kcub1")
324         t.KeyPgDn = tc.getstr("knp")
325         t.KeyPgUp = tc.getstr("kpp")
326         t.KeyBacktab = tc.getstr("kcbt")
327         t.KeyExit = tc.getstr("kext")
328         t.KeyCancel = tc.getstr("kcan")
329         t.KeyPrint = tc.getstr("kprt")
330         t.KeyHelp = tc.getstr("khlp")
331         t.KeyClear = tc.getstr("kclr")
332         t.AltChars = tc.getstr("acsc")
333         t.EnterAcs = tc.getstr("smacs")
334         t.ExitAcs = tc.getstr("rmacs")
335         t.EnableAcs = tc.getstr("enacs")
336         t.Mouse = tc.getstr("kmous")
337         t.KeyShfRight = tc.getstr("kRIT")
338         t.KeyShfLeft = tc.getstr("kLFT")
339         t.KeyShfHome = tc.getstr("kHOM")
340         t.KeyShfEnd = tc.getstr("kEND")
341
342         // Terminfo lacks descriptions for a bunch of modified keys,
343         // but modern XTerm and emulators often have them.  Let's add them,
344         // if the shifted right and left arrows are defined.
345         if t.KeyShfRight == "\x1b[1;2C" && t.KeyShfLeft == "\x1b[1;2D" {
346                 t.KeyShfUp = "\x1b[1;2A"
347                 t.KeyShfDown = "\x1b[1;2B"
348                 t.KeyMetaUp = "\x1b[1;9A"
349                 t.KeyMetaDown = "\x1b[1;9B"
350                 t.KeyMetaRight = "\x1b[1;9C"
351                 t.KeyMetaLeft = "\x1b[1;9D"
352                 t.KeyAltUp = "\x1b[1;3A"
353                 t.KeyAltDown = "\x1b[1;3B"
354                 t.KeyAltRight = "\x1b[1;3C"
355                 t.KeyAltLeft = "\x1b[1;3D"
356                 t.KeyCtrlUp = "\x1b[1;5A"
357                 t.KeyCtrlDown = "\x1b[1;5B"
358                 t.KeyCtrlRight = "\x1b[1;5C"
359                 t.KeyCtrlLeft = "\x1b[1;5D"
360                 t.KeyAltShfUp = "\x1b[1;4A"
361                 t.KeyAltShfDown = "\x1b[1;4B"
362                 t.KeyAltShfRight = "\x1b[1;4C"
363                 t.KeyAltShfLeft = "\x1b[1;4D"
364
365                 t.KeyMetaShfUp = "\x1b[1;10A"
366                 t.KeyMetaShfDown = "\x1b[1;10B"
367                 t.KeyMetaShfRight = "\x1b[1;10C"
368                 t.KeyMetaShfLeft = "\x1b[1;10D"
369
370                 t.KeyCtrlShfUp = "\x1b[1;6A"
371                 t.KeyCtrlShfDown = "\x1b[1;6B"
372                 t.KeyCtrlShfRight = "\x1b[1;6C"
373                 t.KeyCtrlShfLeft = "\x1b[1;6D"
374         }
375         // And also for Home and End
376         if t.KeyShfHome == "\x1b[1;2H" && t.KeyShfEnd == "\x1b[1;2F" {
377                 t.KeyCtrlHome = "\x1b[1;5H"
378                 t.KeyCtrlEnd = "\x1b[1;5F"
379                 t.KeyAltHome = "\x1b[1;9H"
380                 t.KeyAltEnd = "\x1b[1;9F"
381                 t.KeyCtrlShfHome = "\x1b[1;6H"
382                 t.KeyCtrlShfEnd = "\x1b[1;6F"
383                 t.KeyAltShfHome = "\x1b[1;4H"
384                 t.KeyAltShfEnd = "\x1b[1;4F"
385                 t.KeyMetaShfHome = "\x1b[1;10H"
386                 t.KeyMetaShfEnd = "\x1b[1;10F"
387         }
388
389         // And the same thing for rxvt and workalikes (Eterm, aterm, etc.)
390         // It seems that urxvt at least send ESC as ALT prefix for these,
391         // although some places seem to indicate a separate ALT key sesquence.
392         if t.KeyShfRight == "\x1b[c" && t.KeyShfLeft == "\x1b[d" {
393                 t.KeyShfUp = "\x1b[a"
394                 t.KeyShfDown = "\x1b[b"
395                 t.KeyCtrlUp = "\x1b[Oa"
396                 t.KeyCtrlDown = "\x1b[Ob"
397                 t.KeyCtrlRight = "\x1b[Oc"
398                 t.KeyCtrlLeft = "\x1b[Od"
399         }
400         if t.KeyShfHome == "\x1b[7$" && t.KeyShfEnd == "\x1b[8$" {
401                 t.KeyCtrlHome = "\x1b[7^"
402                 t.KeyCtrlEnd = "\x1b[8^"
403         }
404
405         // If the kmous entry is present, then we need to record the
406         // the codes to enter and exit mouse mode.  Sadly, this is not
407         // part of the terminfo databases anywhere that I've found, but
408         // is an extension.  The escape codes are documented in the XTerm
409         // manual, and all terminals that have kmous are expected to
410         // use these same codes, unless explicitly configured otherwise
411         // vi XM.  Note that in any event, we only known how to parse either
412         // x11 or SGR mouse events -- if your terminal doesn't support one
413         // of these two forms, you maybe out of luck.
414         t.MouseMode = tc.getstr("XM")
415         if t.Mouse != "" && t.MouseMode == "" {
416                 // we anticipate that all xterm mouse tracking compatible
417                 // terminals understand mouse tracking (1000), but we hope
418                 // that those that don't understand any-event tracking (1003)
419                 // will at least ignore it.  Likewise we hope that terminals
420                 // that don't understand SGR reporting (1006) just ignore it.
421                 t.MouseMode = "%?%p1%{1}%=%t%'h'%Pa%e%'l'%Pa%;" +
422                         "\x1b[?1000%ga%c\x1b[?1002%ga%c\x1b[?1003%ga%c\x1b[?1006%ga%c"
423         }
424
425         // We only support colors in ANSI 8 or 256 color mode.
426         if t.Colors < 8 || t.SetFg == "" {
427                 t.Colors = 0
428         }
429         if t.SetCursor == "" {
430                 return nil, "", errors.New("terminal not cursor addressable")
431         }
432
433         // For padding, we lookup the pad char.  If that isn't present,
434         // and npc is *not* set, then we assume a null byte.
435         t.PadChar = tc.getstr("pad")
436         if t.PadChar == "" {
437                 if !tc.getflag("npc") {
438                         t.PadChar = "\u0000"
439                 }
440         }
441
442         // For some terminals we fabricate a -truecolor entry, that may
443         // not exist in terminfo.
444         if addTrueColor {
445                 t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm"
446                 t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm"
447                 t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" +
448                         "48;2;%p4%d;%p5%d;%p6%dm"
449         }
450
451         // For terminals that use "standard" SGR sequences, lets combine the
452         // foreground and background together.
453         if strings.HasPrefix(t.SetFg, "\x1b[") &&
454                 strings.HasPrefix(t.SetBg, "\x1b[") &&
455                 strings.HasSuffix(t.SetFg, "m") &&
456                 strings.HasSuffix(t.SetBg, "m") {
457                 fg := t.SetFg[:len(t.SetFg)-1]
458                 r := regexp.MustCompile("%p1")
459                 bg := r.ReplaceAllString(t.SetBg[2:], "%p2")
460                 t.SetFgBg = fg + ";" + bg
461         }
462
463         return t, tc.desc, nil
464 }
465
466 func WriteDB(filename string) error {
467         var e error
468         js := []byte{}
469         args := []string{os.Getenv("TERM")}
470
471         tdata := make(map[string]*Terminfo)
472         descs := make(map[string]string)
473
474         for _, term := range args {
475                 if t, desc, e := getinfo(term); e != nil {
476                         return e
477                 } else {
478                         tdata[term] = t
479                         descs[term] = desc
480                 }
481         }
482
483         if len(tdata) == 0 {
484                 // No data.
485                 return errors.New("No data")
486         }
487         o := os.Stdout
488         if o, e = os.Create(filename); e != nil {
489                 return e
490         }
491         var w io.WriteCloser
492         w = o
493         for _, term := range args {
494                 if t := tdata[term]; t != nil {
495                         js, e = json.Marshal(t)
496                         fmt.Fprintln(w, string(js))
497                 }
498                 // arguably if there is more than one term, this
499                 // should be a javascript array, but that's not how
500                 // we load it.  We marshal objects one at a time from
501                 // the file.
502         }
503         if e != nil {
504                 return e
505         }
506         w.Close()
507         if w != o {
508                 o.Close()
509         }
510
511         return nil
512 }