@@ -12,19 +12,19 @@ package main
1212//
1313// - Everything else goes to standard error with an "age:" prefix.
1414// No capitalized initials and no periods at the end.
15+ //
16+ // The one exception is the autogenerated passphrase, which goes to
17+ // the terminal, since we really want it to reach the user only.
1518
1619import (
1720 "bytes"
18- "errors"
1921 "fmt"
2022 "io"
2123 "log"
2224 "os"
23- "runtime"
2425
2526 "filippo.io/age/armor"
26- "filippo.io/age/plugin"
27- "golang.org/x/term"
27+ "filippo.io/age/internal/term"
2828)
2929
3030// l is a logger with no prefixes.
@@ -53,183 +53,13 @@ func errorWithHint(error string, hints ...string) {
5353 os .Exit (1 )
5454}
5555
56- // avoidTerminalEscapeSequences is set if we need to avoid using escape
57- // sequences to prevent weird characters being printed to the console. This will
58- // happen on Windows when virtual terminal processing cannot be enabled.
59- var avoidTerminalEscapeSequences bool
60-
61- // clearLine clears the current line on the terminal, or opens a new line if
62- // terminal escape codes don't work.
63- func clearLine (out io.Writer ) {
64- const (
65- CUI = "\033 [" // Control Sequence Introducer
66- CPL = CUI + "F" // Cursor Previous Line
67- EL = CUI + "K" // Erase in Line
68- )
69-
70- // First, open a new line, which is guaranteed to work everywhere. Then, try
71- // to erase the line above with escape codes, if possible.
72- //
73- // (We use CRLF instead of LF to work around an apparent bug in WSL2's
74- // handling of CONOUT$. Only when running a Windows binary from WSL2, the
75- // cursor would not go back to the start of the line with a simple LF.
76- // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
77- fmt .Fprintf (out , "\r \n " )
78- if ! avoidTerminalEscapeSequences {
79- fmt .Fprintf (out , CPL + EL )
80- }
81- }
82-
83- // withTerminal runs f with the terminal input and output files, if available.
84- // withTerminal does not open a non-terminal stdin, so the caller does not need
85- // to check stdinInUse.
86- func withTerminal (f func (in , out * os.File ) error ) error {
87- if runtime .GOOS == "windows" {
88- in , err := os .OpenFile ("CONIN$" , os .O_RDWR , 0 )
89- if err != nil {
90- return err
91- }
92- defer in .Close ()
93- out , err := os .OpenFile ("CONOUT$" , os .O_WRONLY , 0 )
94- if err != nil {
95- return err
96- }
97- defer out .Close ()
98- return f (in , out )
99- } else if tty , err := os .OpenFile ("/dev/tty" , os .O_RDWR , 0 ); err == nil {
100- defer tty .Close ()
101- return f (tty , tty )
102- } else if term .IsTerminal (int (os .Stdin .Fd ())) {
103- return f (os .Stdin , os .Stdin )
104- } else {
105- return fmt .Errorf ("standard input is not a terminal, and /dev/tty is not available: %v" , err )
106- }
107- }
108-
10956func printfToTerminal (format string , v ... interface {}) error {
110- return withTerminal (func (_ , out * os.File ) error {
57+ return term . WithTerminal (func (_ , out * os.File ) error {
11158 _ , err := fmt .Fprintf (out , "age: " + format + "\n " , v ... )
11259 return err
11360 })
11461}
11562
116- // readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
117- func readSecret (prompt string ) (s []byte , err error ) {
118- err = withTerminal (func (in , out * os.File ) error {
119- fmt .Fprintf (out , "%s " , prompt )
120- defer clearLine (out )
121- s , err = term .ReadPassword (int (in .Fd ()))
122- return err
123- })
124- return
125- }
126-
127- // readPublic reads a value from the terminal. The prompt is ephemeral.
128- func readPublic (prompt string ) (s []byte , err error ) {
129- err = withTerminal (func (in , out * os.File ) error {
130- fmt .Fprintf (out , "%s " , prompt )
131- defer clearLine (out )
132-
133- oldState , err := term .MakeRaw (int (in .Fd ()))
134- if err != nil {
135- return err
136- }
137- defer term .Restore (int (in .Fd ()), oldState )
138-
139- t := term .NewTerminal (in , "" )
140- line , err := t .ReadLine ()
141- s = []byte (line )
142- return err
143- })
144- return
145- }
146-
147- // readCharacter reads a single character from the terminal with no echo. The
148- // prompt is ephemeral.
149- func readCharacter (prompt string ) (c byte , err error ) {
150- err = withTerminal (func (in , out * os.File ) error {
151- fmt .Fprintf (out , "%s " , prompt )
152- defer clearLine (out )
153-
154- oldState , err := term .MakeRaw (int (in .Fd ()))
155- if err != nil {
156- return err
157- }
158- defer term .Restore (int (in .Fd ()), oldState )
159-
160- b := make ([]byte , 1 )
161- if _ , err := in .Read (b ); err != nil {
162- return err
163- }
164-
165- c = b [0 ]
166- return nil
167- })
168- return
169- }
170-
171- var pluginTerminalUI = & plugin.ClientUI {
172- DisplayMessage : func (name , message string ) error {
173- printf ("%s plugin: %s" , name , message )
174- return nil
175- },
176- RequestValue : func (name , message string , isSecret bool ) (s string , err error ) {
177- defer func () {
178- if err != nil {
179- warningf ("could not read value for age-plugin-%s: %v" , name , err )
180- }
181- }()
182- if isSecret {
183- secret , err := readSecret (message )
184- if err != nil {
185- return "" , err
186- }
187- return string (secret ), nil
188- } else {
189- public , err := readPublic (message )
190- if err != nil {
191- return "" , err
192- }
193- return string (public ), nil
194- }
195- },
196- Confirm : func (name , message , yes , no string ) (choseYes bool , err error ) {
197- defer func () {
198- if err != nil {
199- warningf ("could not read value for age-plugin-%s: %v" , name , err )
200- }
201- }()
202- if no == "" {
203- message += fmt .Sprintf (" (press enter for %q)" , yes )
204- _ , err := readSecret (message )
205- if err != nil {
206- return false , err
207- }
208- return true , nil
209- }
210- message += fmt .Sprintf (" (press [1] for %q or [2] for %q)" , yes , no )
211- for {
212- selection , err := readCharacter (message )
213- if err != nil {
214- return false , err
215- }
216- switch selection {
217- case '1' :
218- return true , nil
219- case '2' :
220- return false , nil
221- case '\x03' : // CTRL-C
222- return false , errors .New ("user cancelled prompt" )
223- default :
224- warningf ("reading value for age-plugin-%s: invalid selection %q" , name , selection )
225- }
226- }
227- },
228- WaitTimer : func (name string ) {
229- printf ("waiting on %s plugin..." , name )
230- },
231- }
232-
23363func bufferTerminalInput (in io.Reader ) (io.Reader , error ) {
23464 buf := & bytes.Buffer {}
23565 if _ , err := buf .ReadFrom (ReaderFunc (func (p []byte ) (n int , err error ) {
0 commit comments