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  }