github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/pkg/terminal/basic.go (about)

     1  package terminal
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"strings"
    11  	"text/tabwriter"
    12  
    13  	"github.com/bgentry/speakeasy"
    14  	"github.com/containerd/console"
    15  	"github.com/fatih/color"
    16  	"github.com/mattn/go-isatty"
    17  	sshterm "golang.org/x/crypto/ssh/terminal"
    18  )
    19  
    20  // basicUI
    21  type basicUI struct {
    22  	ctx    context.Context
    23  	status *spinnerStatus
    24  }
    25  
    26  // Returns a UI which will write to the current processes
    27  // stdout/stderr.
    28  func ConsoleUI(ctx context.Context, plain bool) UI {
    29  	// We do both of these checks because some sneaky environments fool
    30  	// one or the other and we really only want the glint-based UI in
    31  	// truly interactive environments.
    32  	if plain {
    33  		return NonInteractiveUI(ctx)
    34  	}
    35  
    36  	glint := isatty.IsTerminal(os.Stdout.Fd()) && sshterm.IsTerminal(int(os.Stdout.Fd()))
    37  	if glint {
    38  		glint = false
    39  		if c, err := console.ConsoleFromFile(os.Stdout); err == nil {
    40  			if sz, err := c.Size(); err == nil {
    41  				glint = sz.Height > 0 && sz.Width > 0
    42  			}
    43  		}
    44  	}
    45  
    46  	if glint {
    47  		return GlintUI(ctx)
    48  	} else {
    49  		return NonInteractiveUI(ctx)
    50  	}
    51  }
    52  
    53  // Input implements UI
    54  func (ui *basicUI) Input(input *Input) (string, error) {
    55  	var buf bytes.Buffer
    56  
    57  	// Write the prompt, add a space.
    58  	ui.Output(input.Prompt, WithStyle(input.Style), WithWriter(&buf))
    59  	fmt.Fprint(color.Output, strings.TrimRight(buf.String(), "\r\n"))
    60  	fmt.Fprint(color.Output, " ")
    61  
    62  	// Ask for input in a go-routine so that we can ignore it.
    63  	errCh := make(chan error, 1)
    64  	lineCh := make(chan string, 1)
    65  	go func() {
    66  		var line string
    67  		var err error
    68  		if input.Secret && isatty.IsTerminal(os.Stdin.Fd()) {
    69  			line, err = speakeasy.Ask("")
    70  		} else {
    71  			r := bufio.NewReader(os.Stdin)
    72  			line, err = r.ReadString('\n')
    73  		}
    74  		if err != nil {
    75  			errCh <- err
    76  			return
    77  		}
    78  
    79  		lineCh <- strings.TrimRight(line, "\r\n")
    80  	}()
    81  
    82  	select {
    83  	case err := <-errCh:
    84  		return "", err
    85  	case line := <-lineCh:
    86  		return line, nil
    87  	case <-ui.ctx.Done():
    88  		// Print newline so that any further output starts properly
    89  		fmt.Fprintln(color.Output)
    90  		return "", ui.ctx.Err()
    91  	}
    92  }
    93  
    94  // Interactive implements UI
    95  func (ui *basicUI) Interactive() bool {
    96  	return isatty.IsTerminal(os.Stdin.Fd())
    97  }
    98  
    99  // Output implements UI
   100  func (ui *basicUI) Output(msg string, raw ...interface{}) {
   101  	msg, style, w := Interpret(msg, raw...)
   102  
   103  	switch style {
   104  	case HeaderStyle:
   105  		msg = colorHeader.Sprintf("\n==> %s", msg)
   106  	case ErrorStyle:
   107  		msg = colorError.Sprint(msg)
   108  	case ErrorBoldStyle:
   109  		msg = colorErrorBold.Sprint(msg)
   110  	case WarningStyle:
   111  		msg = colorWarning.Sprint(msg)
   112  	case WarningBoldStyle:
   113  		msg = colorWarningBold.Sprint(msg)
   114  	case SuccessStyle:
   115  		msg = colorSuccess.Sprint(msg)
   116  	case SuccessBoldStyle:
   117  		msg = colorSuccessBold.Sprint(msg)
   118  	case InfoStyle:
   119  		lines := strings.Split(msg, "\n")
   120  		for i, line := range lines {
   121  			lines[i] = colorInfo.Sprintf("    %s", line)
   122  		}
   123  
   124  		msg = strings.Join(lines, "\n")
   125  	}
   126  
   127  	st := ui.status
   128  	if st != nil {
   129  		if st.Pause() {
   130  			defer st.Start()
   131  		}
   132  	}
   133  
   134  	// Write it
   135  	fmt.Fprintln(w, msg)
   136  }
   137  
   138  // NamedValues implements UI
   139  func (ui *basicUI) NamedValues(rows []NamedValue, opts ...Option) {
   140  	cfg := &config{Writer: color.Output}
   141  	for _, opt := range opts {
   142  		opt(cfg)
   143  	}
   144  
   145  	var buf bytes.Buffer
   146  	tr := tabwriter.NewWriter(&buf, 1, 8, 0, ' ', tabwriter.AlignRight)
   147  	for _, row := range rows {
   148  		switch v := row.Value.(type) {
   149  		case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
   150  			fmt.Fprintf(tr, "  %s: \t%d\n", row.Name, row.Value)
   151  		case float32, float64:
   152  			fmt.Fprintf(tr, "  %s: \t%f\n", row.Name, row.Value)
   153  		case bool:
   154  			fmt.Fprintf(tr, "  %s: \t%v\n", row.Name, row.Value)
   155  		case string:
   156  			if v == "" {
   157  				continue
   158  			}
   159  			fmt.Fprintf(tr, "  %s: \t%s\n", row.Name, row.Value)
   160  		default:
   161  			fmt.Fprintf(tr, "  %s: \t%s\n", row.Name, row.Value)
   162  		}
   163  	}
   164  
   165  	tr.Flush()
   166  	colorInfo.Fprintln(cfg.Writer, buf.String())
   167  }
   168  
   169  // OutputWriters implements UI
   170  func (ui *basicUI) OutputWriters() (io.Writer, io.Writer, error) {
   171  	return os.Stdout, os.Stderr, nil
   172  }
   173  
   174  // Status implements UI
   175  func (ui *basicUI) Status() Status {
   176  	if ui.status == nil {
   177  		ui.status = newSpinnerStatus(ui.ctx)
   178  	}
   179  
   180  	return ui.status
   181  }
   182  
   183  func (ui *basicUI) StepGroup() StepGroup {
   184  	ctx, cancel := context.WithCancel(ui.ctx)
   185  	display := NewDisplay(ctx, color.Output)
   186  
   187  	return &fancyStepGroup{
   188  		ctx:     ctx,
   189  		cancel:  cancel,
   190  		display: display,
   191  		done:    make(chan struct{}),
   192  	}
   193  }