Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
11201: [cli] Abstract out terminal interactions r=pgavlin a=pgavlin Replace direct interaction with the terminal with an abstraction. This abstraction is tightly constrained to the capabilities needed for the CLI's display. Using this abstraction allows for straightforward testing of the interactive renderers. 11296: Update YAML to 1.0.2 r=AaronFriel a=AaronFriel Co-authored-by: Pat Gavlin <pat@pulumi.com> Co-authored-by: Aaron Friel <mayreply@aaronfriel.com>
- Loading branch information
Showing
109 changed files
with
25,094 additions
and
276 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
changes: | ||
- type: fix | ||
scope: yaml | ||
description: "[Updates Pulumi YAML to v1.0.2](https://github.com/pulumi/pulumi-yaml/releases/tag/v1.0.2) which fixes a bug encountered using templates with project level config." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package terminal | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
|
||
gotty "github.com/ijc/Gotty" | ||
) | ||
|
||
type Info interface { | ||
Parse(attr string, params ...interface{}) (string, error) | ||
|
||
ClearLine(out io.Writer) | ||
CursorUp(out io.Writer, count int) | ||
CursorDown(out io.Writer, count int) | ||
} | ||
|
||
/* Satisfied by gotty.TermInfo as well as noTermInfo from below */ | ||
type termInfo interface { | ||
Parse(attr string, params ...interface{}) (string, error) | ||
} | ||
|
||
type noTermInfo int // canary used when no terminfo. | ||
|
||
func (ti noTermInfo) Parse(attr string, params ...interface{}) (string, error) { | ||
return "", fmt.Errorf("noTermInfo") | ||
} | ||
|
||
type info struct { | ||
termInfo | ||
} | ||
|
||
var _ = Info(info{}) | ||
|
||
func OpenInfo(terminal string) Info { | ||
if i, err := gotty.OpenTermInfo(terminal); err == nil { | ||
return info{i} | ||
} | ||
return info{noTermInfo(0)} | ||
} | ||
|
||
func (i info) ClearLine(out io.Writer) { | ||
// el2 (clear whole line) is not exposed by terminfo. | ||
|
||
// First clear line from beginning to cursor | ||
if attr, err := i.Parse("el1"); err == nil { | ||
fmt.Fprintf(out, "%s", attr) | ||
} else { | ||
fmt.Fprintf(out, "\x1b[1K") | ||
} | ||
// Then clear line from cursor to end | ||
if attr, err := i.Parse("el"); err == nil { | ||
fmt.Fprintf(out, "%s", attr) | ||
} else { | ||
fmt.Fprintf(out, "\x1b[K") | ||
} | ||
} | ||
|
||
func (i info) CursorUp(out io.Writer, count int) { | ||
if count == 0 { // Should never be the case, but be tolerant | ||
return | ||
} | ||
if attr, err := i.Parse("cuu", count); err == nil { | ||
fmt.Fprintf(out, "%s", attr) | ||
} else { | ||
fmt.Fprintf(out, "\x1b[%dA", count) | ||
} | ||
} | ||
|
||
func (i info) CursorDown(out io.Writer, count int) { | ||
if count == 0 { // Should never be the case, but be tolerant | ||
return | ||
} | ||
if attr, err := i.Parse("cud", count); err == nil { | ||
fmt.Fprintf(out, "%s", attr) | ||
} else { | ||
fmt.Fprintf(out, "\x1b[%dB", count) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package terminal | ||
|
||
import ( | ||
"io" | ||
"sync" | ||
) | ||
|
||
type MockTerminal struct { | ||
m sync.Mutex | ||
|
||
width, height int | ||
raw bool | ||
info Info | ||
|
||
keys chan string | ||
|
||
dest io.Writer | ||
} | ||
|
||
func NewMockTerminal(dest io.Writer, width, height int, raw bool) *MockTerminal { | ||
return &MockTerminal{ | ||
width: width, | ||
height: height, | ||
raw: raw, | ||
info: info{noTermInfo(0)}, | ||
keys: make(chan string), | ||
dest: dest, | ||
} | ||
} | ||
|
||
func (t *MockTerminal) IsRaw() bool { | ||
return t.raw | ||
} | ||
|
||
func (t *MockTerminal) Close() error { | ||
close(t.keys) | ||
return nil | ||
} | ||
|
||
func (t *MockTerminal) Size() (width, height int, err error) { | ||
t.m.Lock() | ||
defer t.m.Unlock() | ||
|
||
return t.width, t.height, nil | ||
} | ||
|
||
func (t *MockTerminal) Write(b []byte) (int, error) { | ||
return t.dest.Write(b) | ||
} | ||
|
||
func (t *MockTerminal) ClearLine() { | ||
t.info.ClearLine(t) | ||
} | ||
|
||
func (t *MockTerminal) CursorUp(count int) { | ||
t.info.CursorUp(t, count) | ||
} | ||
|
||
func (t *MockTerminal) CursorDown(count int) { | ||
t.info.CursorDown(t, count) | ||
} | ||
|
||
func (t *MockTerminal) ReadKey() (string, error) { | ||
k, ok := <-t.keys | ||
if !ok { | ||
return "", io.EOF | ||
} | ||
return k, nil | ||
} | ||
|
||
func (t *MockTerminal) SetSize(width, height int) { | ||
t.m.Lock() | ||
defer t.m.Unlock() | ||
|
||
t.width, t.height = width, height | ||
} | ||
|
||
func (t *MockTerminal) SendKey(key string) { | ||
t.keys <- key | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
package terminal | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
|
||
"github.com/muesli/cancelreader" | ||
"golang.org/x/term" | ||
) | ||
|
||
type Terminal interface { | ||
io.WriteCloser | ||
|
||
IsRaw() bool | ||
Size() (width, height int, err error) | ||
|
||
ClearLine() | ||
CursorUp(count int) | ||
CursorDown(count int) | ||
|
||
ReadKey() (string, error) | ||
} | ||
|
||
var ErrNotATerminal = errors.New("not a terminal") | ||
|
||
type terminal struct { | ||
fd int | ||
info Info | ||
raw bool | ||
save *term.State | ||
|
||
out io.Writer | ||
in cancelreader.CancelReader | ||
} | ||
|
||
func Open(in io.Reader, out io.Writer, raw bool) (Terminal, error) { | ||
type fileLike interface { | ||
Fd() uintptr | ||
} | ||
|
||
outFile, ok := out.(fileLike) | ||
if !ok { | ||
return nil, ErrNotATerminal | ||
} | ||
outFd := int(outFile.Fd()) | ||
|
||
width, height, err := term.GetSize(outFd) | ||
if err != nil { | ||
return nil, fmt.Errorf("getting dimensions: %w", err) | ||
} | ||
if width == 0 || height == 0 { | ||
return nil, fmt.Errorf("unusable dimensions (%v x %v)", width, height) | ||
} | ||
|
||
termType := os.Getenv("TERM") | ||
if termType == "" { | ||
termType = "vt102" | ||
} | ||
info := OpenInfo(termType) | ||
|
||
var save *term.State | ||
var inFile cancelreader.CancelReader | ||
if raw { | ||
if save, err = term.MakeRaw(outFd); err != nil { | ||
return nil, fmt.Errorf("enabling raw mode: %w", err) | ||
} | ||
if inFile, err = cancelreader.NewReader(in); err != nil { | ||
return nil, ErrNotATerminal | ||
} | ||
} | ||
|
||
return &terminal{ | ||
fd: outFd, | ||
info: info, | ||
raw: raw, | ||
save: save, | ||
out: out, | ||
in: inFile, | ||
}, nil | ||
} | ||
|
||
func (t *terminal) IsRaw() bool { | ||
return t.raw | ||
} | ||
|
||
func (t *terminal) Close() error { | ||
t.in.Cancel() | ||
if t.save != nil { | ||
return term.Restore(t.fd, t.save) | ||
} | ||
return nil | ||
} | ||
|
||
func (t *terminal) Size() (width, height int, err error) { | ||
return term.GetSize(t.fd) | ||
} | ||
|
||
func (t *terminal) Write(b []byte) (int, error) { | ||
if !t.raw { | ||
return t.out.Write(b) | ||
} | ||
|
||
written := 0 | ||
for { | ||
newline := bytes.IndexByte(b, '\n') | ||
if newline == -1 { | ||
w, err := t.out.Write(b) | ||
written += w | ||
return written, err | ||
} | ||
|
||
w, err := t.out.Write(b[:newline]) | ||
written += w | ||
if err != nil { | ||
return written, err | ||
} | ||
|
||
if _, err = t.out.Write([]byte{'\r', '\n'}); err != nil { | ||
return written, err | ||
} | ||
written++ | ||
|
||
b = b[newline+1:] | ||
} | ||
} | ||
|
||
func (t *terminal) ClearLine() { | ||
t.info.ClearLine(t.out) | ||
} | ||
|
||
func (t *terminal) CursorUp(count int) { | ||
t.info.CursorUp(t.out, count) | ||
} | ||
|
||
func (t *terminal) CursorDown(count int) { | ||
t.info.CursorDown(t.out, count) | ||
} | ||
|
||
func (t *terminal) ReadKey() (string, error) { | ||
if t.in == nil { | ||
return "", io.EOF | ||
} | ||
|
||
type stateFunc func(b byte) (stateFunc, string) | ||
|
||
var stateIntermediate stateFunc | ||
stateIntermediate = func(b byte) (stateFunc, string) { | ||
if b >= 0x20 && b < 0x30 { | ||
return stateIntermediate, "" | ||
} | ||
switch b { | ||
case 'A': | ||
return nil, "up" | ||
case 'B': | ||
return nil, "down" | ||
default: | ||
return nil, "<control>" | ||
} | ||
} | ||
var stateParameter stateFunc | ||
stateParameter = func(b byte) (stateFunc, string) { | ||
if b >= 0x30 && b < 0x40 { | ||
return stateParameter, "" | ||
} | ||
return stateIntermediate(b) | ||
} | ||
stateBracket := func(b byte) (stateFunc, string) { | ||
if b == '[' { | ||
return stateParameter, "" | ||
} | ||
return nil, "<control>" | ||
} | ||
stateEscape := func(b byte) (stateFunc, string) { | ||
if b == 0x1b { | ||
return stateBracket, "" | ||
} | ||
if b == 3 { | ||
return nil, "ctrl+c" | ||
} | ||
return nil, string([]byte{b}) | ||
} | ||
|
||
state := stateEscape | ||
for { | ||
var b [1]byte | ||
if _, err := t.in.Read(b[:]); err != nil { | ||
if errors.Is(err, cancelreader.ErrCanceled) { | ||
err = io.EOF | ||
} | ||
return "", err | ||
} | ||
|
||
next, key := state(b[0]) | ||
if next == nil { | ||
return key, nil | ||
} | ||
state = next | ||
} | ||
} |
Oops, something went wrong.