github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/display/internal/terminal/term.go (about) 1 package terminal 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 10 "github.com/muesli/cancelreader" 11 "golang.org/x/term" 12 ) 13 14 type Terminal interface { 15 io.WriteCloser 16 17 IsRaw() bool 18 Size() (width, height int, err error) 19 20 ClearLine() 21 CursorUp(count int) 22 CursorDown(count int) 23 24 ReadKey() (string, error) 25 } 26 27 var ErrNotATerminal = errors.New("not a terminal") 28 29 type terminal struct { 30 fd int 31 info Info 32 raw bool 33 save *term.State 34 35 out io.Writer 36 in cancelreader.CancelReader 37 } 38 39 func Open(in io.Reader, out io.Writer, raw bool) (Terminal, error) { 40 type fileLike interface { 41 Fd() uintptr 42 } 43 44 outFile, ok := out.(fileLike) 45 if !ok { 46 return nil, ErrNotATerminal 47 } 48 outFd := int(outFile.Fd()) 49 50 width, height, err := term.GetSize(outFd) 51 if err != nil { 52 return nil, fmt.Errorf("getting dimensions: %w", err) 53 } 54 if width == 0 || height == 0 { 55 return nil, fmt.Errorf("unusable dimensions (%v x %v)", width, height) 56 } 57 58 termType := os.Getenv("TERM") 59 if termType == "" { 60 termType = "vt102" 61 } 62 info := OpenInfo(termType) 63 64 var save *term.State 65 var inFile cancelreader.CancelReader 66 if raw { 67 if save, err = term.MakeRaw(outFd); err != nil { 68 return nil, fmt.Errorf("enabling raw mode: %w", err) 69 } 70 if inFile, err = cancelreader.NewReader(in); err != nil { 71 return nil, ErrNotATerminal 72 } 73 } 74 75 return &terminal{ 76 fd: outFd, 77 info: info, 78 raw: raw, 79 save: save, 80 out: out, 81 in: inFile, 82 }, nil 83 } 84 85 func (t *terminal) IsRaw() bool { 86 return t.raw 87 } 88 89 func (t *terminal) Close() error { 90 t.in.Cancel() 91 if t.save != nil { 92 return term.Restore(t.fd, t.save) 93 } 94 return nil 95 } 96 97 func (t *terminal) Size() (width, height int, err error) { 98 return term.GetSize(t.fd) 99 } 100 101 func (t *terminal) Write(b []byte) (int, error) { 102 if !t.raw { 103 return t.out.Write(b) 104 } 105 106 written := 0 107 for { 108 newline := bytes.IndexByte(b, '\n') 109 if newline == -1 { 110 w, err := t.out.Write(b) 111 written += w 112 return written, err 113 } 114 115 w, err := t.out.Write(b[:newline]) 116 written += w 117 if err != nil { 118 return written, err 119 } 120 121 if _, err = t.out.Write([]byte{'\r', '\n'}); err != nil { 122 return written, err 123 } 124 written++ 125 126 b = b[newline+1:] 127 } 128 } 129 130 func (t *terminal) ClearLine() { 131 t.info.ClearLine(t.out) 132 } 133 134 func (t *terminal) CursorUp(count int) { 135 t.info.CursorUp(t.out, count) 136 } 137 138 func (t *terminal) CursorDown(count int) { 139 t.info.CursorDown(t.out, count) 140 } 141 142 func (t *terminal) ReadKey() (string, error) { 143 if t.in == nil { 144 return "", io.EOF 145 } 146 147 type stateFunc func(b byte) (stateFunc, string) 148 149 var stateIntermediate stateFunc 150 stateIntermediate = func(b byte) (stateFunc, string) { 151 if b >= 0x20 && b < 0x30 { 152 return stateIntermediate, "" 153 } 154 switch b { 155 case 'A': 156 return nil, "up" 157 case 'B': 158 return nil, "down" 159 default: 160 return nil, "<control>" 161 } 162 } 163 var stateParameter stateFunc 164 stateParameter = func(b byte) (stateFunc, string) { 165 if b >= 0x30 && b < 0x40 { 166 return stateParameter, "" 167 } 168 return stateIntermediate(b) 169 } 170 stateBracket := func(b byte) (stateFunc, string) { 171 if b == '[' { 172 return stateParameter, "" 173 } 174 return nil, "<control>" 175 } 176 stateEscape := func(b byte) (stateFunc, string) { 177 if b == 0x1b { 178 return stateBracket, "" 179 } 180 if b == 3 { 181 return nil, "ctrl+c" 182 } 183 return nil, string([]byte{b}) 184 } 185 186 state := stateEscape 187 for { 188 var b [1]byte 189 if _, err := t.in.Read(b[:]); err != nil { 190 if errors.Is(err, cancelreader.ErrCanceled) { 191 err = io.EOF 192 } 193 return "", err 194 } 195 196 next, key := state(b[0]) 197 if next == nil { 198 return key, nil 199 } 200 state = next 201 } 202 }