github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/minterm/minterm.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  // Package minterm implements minimal terminal functions.
     5  package minterm
     6  
     7  import (
     8  	"errors"
     9  	"io"
    10  	"os"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/keybase/go-crypto/ssh/terminal"
    15  )
    16  
    17  // MinTerm is a minimal terminal interface.
    18  type MinTerm struct {
    19  	termIn       *os.File
    20  	termOut      *os.File
    21  	closeTermOut bool
    22  	width        int
    23  	height       int
    24  	stateMu      sync.Mutex // protects raw, oldState
    25  	raw          bool
    26  	oldState     *terminal.State
    27  }
    28  
    29  var ErrPromptInterrupted = errors.New("prompt interrupted")
    30  
    31  // New creates a new MinTerm and opens the terminal file.  Any
    32  // errors that happen while opening or getting the terminal size
    33  // are returned.
    34  func New() (*MinTerm, error) {
    35  	m := &MinTerm{}
    36  	if err := m.open(); err != nil {
    37  		return nil, err
    38  	}
    39  	return m, nil
    40  }
    41  
    42  // Shutdown closes the terminal.
    43  func (m *MinTerm) Shutdown() error {
    44  	m.restore()
    45  	// this can hang waiting for newline, so do it in a goroutine.
    46  	// application shutting down, so will get closed by os anyway...
    47  	if m.termIn != nil {
    48  		go m.termIn.Close()
    49  	}
    50  	if m.termOut != nil && m.closeTermOut {
    51  		go m.termOut.Close()
    52  	}
    53  	return nil
    54  }
    55  
    56  // Size returns the width and height of the terminal.
    57  func (m *MinTerm) Size() (int, int) {
    58  	return m.width, m.height
    59  }
    60  
    61  // Prompt gets a line of input from the terminal.  It displays the text in
    62  // the prompt parameter first.
    63  func (m *MinTerm) Prompt(prompt string) (string, error) {
    64  	return m.readLine(prompt)
    65  }
    66  
    67  // PromptPassword gets a line of input from the terminal, but
    68  // nothing is echoed to the terminal to hide the text.
    69  func (m *MinTerm) PromptPassword(prompt string) (string, error) {
    70  	if !strings.HasSuffix(prompt, ": ") {
    71  		prompt += ": "
    72  	}
    73  	return m.readSecret(prompt)
    74  }
    75  
    76  func (m *MinTerm) fdIn() int { return int(m.termIn.Fd()) }
    77  
    78  func (m *MinTerm) getNewTerminal(prompt string) (*terminal.Terminal, error) {
    79  	term := terminal.NewTerminal(m.getReadWriter(), prompt)
    80  	a, b := m.Size()
    81  	if a < 80 {
    82  		a = 80
    83  	}
    84  	err := term.SetSize(a, b)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	return term, nil
    89  }
    90  
    91  func (m *MinTerm) readLine(prompt string) (string, error) {
    92  	err := m.makeRaw()
    93  	if err != nil {
    94  		return "", convertErr(err)
    95  	}
    96  	defer m.restore()
    97  	term, err := m.getNewTerminal(prompt)
    98  	if err != nil {
    99  		return "", convertErr(err)
   100  	}
   101  	ret, err := term.ReadLine()
   102  	return ret, convertErr(err)
   103  }
   104  
   105  func (m *MinTerm) readSecret(prompt string) (string, error) {
   106  	err := m.makeRaw()
   107  	if err != nil {
   108  		return "", convertErr(err)
   109  	}
   110  	defer m.restore()
   111  	term, err := m.getNewTerminal("")
   112  	if err != nil {
   113  		return "", convertErr(err)
   114  	}
   115  	ret, err := term.ReadPassword(prompt)
   116  	return ret, convertErr(err)
   117  }
   118  
   119  func (m *MinTerm) makeRaw() error {
   120  	m.stateMu.Lock()
   121  	defer m.stateMu.Unlock()
   122  	fd := m.fdIn()
   123  	oldState, err := terminal.MakeRaw(fd)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	m.raw = true
   128  	m.oldState = oldState
   129  	return nil
   130  }
   131  
   132  func (m *MinTerm) restore() {
   133  	m.stateMu.Lock()
   134  	defer m.stateMu.Unlock()
   135  	if !m.raw {
   136  		return
   137  	}
   138  	fd := m.fdIn()
   139  	_ = terminal.Restore(fd, m.oldState)
   140  	m.raw = false
   141  	m.oldState = nil
   142  }
   143  
   144  func convertErr(e error) error {
   145  	if e == io.ErrUnexpectedEOF {
   146  		e = ErrPromptInterrupted
   147  	}
   148  	return e
   149  }