1- // These functions have been copied from the age project
2- // https://github.com/FiloSottile/age/blob/3d91014ea095e8d70f7c6c4833f89b53a96e0832/cmd/age/tui.go
3- //
4- // Copyright 2021 The age Authors. All rights reserved.
5- // Use of this source code is governed by a BSD-style
6- // license that can be found in age's LICENSE file at
7- // https://github.com/FiloSottile/age/blob/v1.0.0/LICENSE
8- //
9- // SPDX-License-Identifier: BSD-3-Clause
10-
111package age
122
133import (
14- "errors"
154 "filippo.io/age/plugin"
16- "fmt"
17- "io"
18- "os"
19- "runtime"
205 "testing"
21-
22- "golang.org/x/term"
236)
247
258var testOnlyAgePassword string
@@ -32,142 +15,24 @@ func warningf(format string, v ...interface{}) {
3215 log .Printf ("age: warning: " + format , v ... )
3316}
3417
35- // clearLine clears the current line on the terminal, or opens a new line if
36- // terminal escape codes don't work.
37- func clearLine (out io.Writer ) {
38- const (
39- CUI = "\033 [" // Control Sequence Introducer
40- CPL = CUI + "F" // Cursor Previous Line
41- EL = CUI + "K" // Erase in Line
42- )
43-
44- // First, open a new line, which is guaranteed to work everywhere. Then, try
45- // to erase the line above with escape codes.
46- //
47- // (We use CRLF instead of LF to work around an apparent bug in WSL2's
48- // handling of CONOUT$. Only when running a Windows binary from WSL2, the
49- // cursor would not go back to the start of the line with a simple LF.
50- // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
51- fmt .Fprintf (out , "\r \n " + CPL + EL )
52- }
53-
54- // withTerminal runs f with the terminal input and output files, if available.
55- // withTerminal does not open a non-terminal stdin, so the caller does not need
56- // to check stdinInUse.
57- func withTerminal (f func (in , out * os.File ) error ) error {
58- if runtime .GOOS == "windows" {
59- in , err := os .OpenFile ("CONIN$" , os .O_RDWR , 0 )
60- if err != nil {
61- return err
62- }
63- defer in .Close ()
64- out , err := os .OpenFile ("CONOUT$" , os .O_WRONLY , 0 )
65- if err != nil {
66- return err
67- }
68- defer out .Close ()
69- return f (in , out )
70- } else if tty , err := os .OpenFile ("/dev/tty" , os .O_RDWR , 0 ); err == nil {
71- defer tty .Close ()
72- return f (tty , tty )
73- } else if term .IsTerminal (int (os .Stdin .Fd ())) {
74- return f (os .Stdin , os .Stdin )
75- } else {
76- return fmt .Errorf ("standard input is not a terminal, and /dev/tty is not available: %v" , err )
77- }
78- }
79-
80- // readSecret reads a value from the terminal with no echo. The prompt is ephemeral.
81- func readSecret (prompt string ) (s []byte , err error ) {
82- if testing .Testing () {
83- if testOnlyAgePassword != "" {
84- return []byte (testOnlyAgePassword ), nil
85- }
86- }
87-
88- err = withTerminal (func (in , out * os.File ) error {
89- fmt .Fprintf (out , "%s " , prompt )
90- defer clearLine (out )
91- s , err = term .ReadPassword (int (in .Fd ()))
92- return err
93- })
94- return
95- }
96-
97- // readCharacter reads a single character from the terminal with no echo. The
98- // prompt is ephemeral.
99- func readCharacter (prompt string ) (c byte , err error ) {
100- err = withTerminal (func (in , out * os.File ) error {
101- fmt .Fprintf (out , "%s " , prompt )
102- defer clearLine (out )
103-
104- oldState , err := term .MakeRaw (int (in .Fd ()))
105- if err != nil {
106- return err
107- }
108- defer term .Restore (int (in .Fd ()), oldState )
109-
110- b := make ([]byte , 1 )
111- if _ , err := in .Read (b ); err != nil {
112- return err
113- }
114-
115- c = b [0 ]
116- return nil
117- })
118- return
119- }
18+ var pluginTerminalUIImpl = plugin .NewTerminalUI (printf , warningf )
12019
20+ // We cannot use plugin.NewTerminalUI() directly because we want to be able to
21+ // inject specific return values for RequestValue during testing.
12122var pluginTerminalUI = & plugin.ClientUI {
12223 DisplayMessage : func (name , message string ) error {
123- printf ("%s plugin: %s" , name , message )
124- return nil
24+ return pluginTerminalUIImpl .DisplayMessage (name , message )
12525 },
126- RequestValue : func (name , message string , _ bool ) (s string , err error ) {
127- defer func () {
128- if err != nil {
129- warningf ("could not read value for age-plugin-%s: %v" , name , err )
130- }
131- }()
132- secret , err := readSecret (message )
133- if err != nil {
134- return "" , err
26+ RequestValue : func (name , message string , isSecret bool ) (s string , err error ) {
27+ if testing .Testing () && testOnlyAgePassword != "" {
28+ return testOnlyAgePassword , nil
13529 }
136- return string ( secret ), nil
30+ return pluginTerminalUIImpl . RequestValue ( name , message , isSecret );
13731 },
13832 Confirm : func (name , message , yes , no string ) (choseYes bool , err error ) {
139- defer func () {
140- if err != nil {
141- warningf ("could not read value for age-plugin-%s: %v" , name , err )
142- }
143- }()
144- if no == "" {
145- message += fmt .Sprintf (" (press enter for %q)" , yes )
146- _ , err := readSecret (message )
147- if err != nil {
148- return false , err
149- }
150- return true , nil
151- }
152- message += fmt .Sprintf (" (press [1] for %q or [2] for %q)" , yes , no )
153- for {
154- selection , err := readCharacter (message )
155- if err != nil {
156- return false , err
157- }
158- switch selection {
159- case '1' :
160- return true , nil
161- case '2' :
162- return false , nil
163- case '\x03' : // CTRL-C
164- return false , errors .New ("user cancelled prompt" )
165- default :
166- warningf ("reading value for age-plugin-%s: invalid selection %q" , name , selection )
167- }
168- }
33+ return pluginTerminalUIImpl .Confirm (name , message , yes , no )
16934 },
17035 WaitTimer : func (name string ) {
171- printf ( "waiting on %s plugin..." , name )
36+ pluginTerminalUIImpl . WaitTimer ( name )
17237 },
17338}
0 commit comments