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 }