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.
5package minterm
6
7import (
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.
18type 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
29var 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.
34func 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.
43func (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.
57func (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.
63func (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.
69func (m *MinTerm) PromptPassword(prompt string) (string, error) {
70	if !strings.HasSuffix(prompt, ": ") {
71		prompt += ": "
72	}
73	return m.readSecret(prompt)
74}
75
76func (m *MinTerm) fdIn() int { return int(m.termIn.Fd()) }
77
78func (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
91func (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
105func (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
119func (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
132func (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
144func convertErr(e error) error {
145	if e == io.ErrUnexpectedEOF {
146		e = ErrPromptInterrupted
147	}
148	return e
149}
150