pkg.re/essentialkaos/ek@v12.36.0+incompatible/terminal/terminal.go (about) 1 // +build linux, darwin, !windows 2 3 // Package terminal provides methods for working with user input 4 package terminal 5 6 // ////////////////////////////////////////////////////////////////////////////////// // 7 // // 8 // Copyright (c) 2021 ESSENTIAL KAOS // 9 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 10 // // 11 // ////////////////////////////////////////////////////////////////////////////////// // 12 13 import ( 14 "fmt" 15 "os" 16 "strings" 17 "unicode/utf8" 18 19 "pkg.re/essentialkaos/go-linenoise.v3" 20 21 "pkg.re/essentialkaos/ek.v12/ansi" 22 "pkg.re/essentialkaos/ek.v12/fmtc" 23 "pkg.re/essentialkaos/ek.v12/fsutil" 24 ) 25 26 // ////////////////////////////////////////////////////////////////////////////////// // 27 28 // ErrKillSignal is error type when user cancel input 29 var ErrKillSignal = linenoise.ErrKillSignal 30 31 // Prompt is prompt string 32 var Prompt = "> " 33 34 // MaskSymbol is symbol used for masking passwords 35 var MaskSymbol = "*" 36 37 // MaskSymbolColorTag is fmtc color tag used for MaskSymbol output 38 var MaskSymbolColorTag = "" 39 40 // ////////////////////////////////////////////////////////////////////////////////// // 41 42 var tmux int8 43 44 // ////////////////////////////////////////////////////////////////////////////////// // 45 46 // ReadUI reads user input 47 func ReadUI(title string, nonEmpty bool) (string, error) { 48 return readUserInput(title, nonEmpty, false) 49 } 50 51 // ReadAnswer reads user answer for yes/no question 52 func ReadAnswer(title string, defaultAnswers ...string) (bool, error) { 53 var defaultAnswer string 54 55 if len(defaultAnswers) != 0 { 56 defaultAnswer = defaultAnswers[0] 57 } 58 59 for { 60 answer, err := readUserInput( 61 getAnswerTitle(title, defaultAnswer), false, false, 62 ) 63 64 if err != nil { 65 return false, err 66 } 67 68 if answer == "" { 69 answer = defaultAnswer 70 } 71 72 switch strings.ToUpper(answer) { 73 case "Y": 74 return true, nil 75 case "N": 76 return false, nil 77 default: 78 PrintWarnMessage("\nPlease enter Y or N\n") 79 } 80 } 81 } 82 83 // ReadPassword reads password or some private input which will be hidden 84 // after pressing Enter 85 func ReadPassword(title string, nonEmpty bool) (string, error) { 86 return readUserInput(title, nonEmpty, true) 87 } 88 89 // PrintErrorMessage prints error message 90 func PrintErrorMessage(message string, args ...interface{}) { 91 if len(args) == 0 { 92 fmtc.Fprintf(os.Stderr, "{r}%s{!}\n", message) 93 } else { 94 fmtc.Fprintf(os.Stderr, "{r}%s{!}\n", fmt.Sprintf(message, args...)) 95 } 96 } 97 98 // PrintWarnMessage prints warning message 99 func PrintWarnMessage(message string, args ...interface{}) { 100 if len(args) == 0 { 101 fmtc.Fprintf(os.Stderr, "{y}%s{!}\n", message) 102 } else { 103 fmtc.Fprintf(os.Stderr, "{y}%s{!}\n", fmt.Sprintf(message, args...)) 104 } 105 } 106 107 // PrintActionMessage prints message about action currently in progress 108 func PrintActionMessage(message string) { 109 fmtc.Printf("{*}%s:{!} ", message) 110 } 111 112 // PrintActionStatus prints message with action execution status 113 func PrintActionStatus(status int) { 114 switch status { 115 case 0: 116 fmtc.Println("{g}OK{!}") 117 case 1: 118 fmtc.Println("{r}ERROR{!}") 119 } 120 } 121 122 // AddHistory adds line to input history 123 func AddHistory(data string) { 124 linenoise.AddHistory(data) 125 } 126 127 // SetCompletionHandler adds function for autocompletion 128 func SetCompletionHandler(h func(input string) []string) { 129 linenoise.SetCompletionHandler(h) 130 } 131 132 // SetHintHandler adds function for input hints 133 func SetHintHandler(h func(input string) string) { 134 linenoise.SetHintHandler(h) 135 } 136 137 // ////////////////////////////////////////////////////////////////////////////////// // 138 139 // getMask returns mask for password 140 func getMask(message string) string { 141 var masking string 142 143 // Remove fmtc color tags and ANSI escape codes 144 prompt := fmtc.Clean(ansi.RemoveCodes(Prompt)) 145 prefix := strings.Repeat(" ", utf8.RuneCountInString(prompt)) 146 147 if isTmuxSession() { 148 masking = strings.Repeat("*", utf8.RuneCountInString(message)) 149 } else { 150 masking = strings.Repeat(MaskSymbol, utf8.RuneCountInString(message)) 151 } 152 153 if !fsutil.IsCharacterDevice("/dev/stdin") && os.Getenv("FAKETTY") == "" { 154 return fmtc.Sprintf(Prompt) + masking 155 } 156 157 return fmt.Sprintf("%s\033[1A%s", prefix, masking) 158 } 159 160 // getAnswerTitle returns title with info about default answer 161 func getAnswerTitle(title, defaultAnswer string) string { 162 if title == "" { 163 return "" 164 } 165 166 switch strings.ToUpper(defaultAnswer) { 167 case "Y": 168 return fmt.Sprintf("{c}%s ({c*}Y{!*}/n){!}", title) 169 case "N": 170 return fmt.Sprintf("{c}%s (y/{c*}N{!*}){!}", title) 171 default: 172 return fmt.Sprintf("{c}%s (y/n){!}", title) 173 } 174 } 175 176 // readUserInput reads user input 177 func readUserInput(title string, nonEmpty, private bool) (string, error) { 178 if title != "" { 179 fmtc.Println("{c}" + title + "{!}") 180 } 181 182 var input string 183 var err error 184 185 for { 186 input, err = linenoise.Line(fmtc.Sprintf(Prompt)) 187 188 if err != nil { 189 return "", err 190 } 191 192 if nonEmpty && strings.TrimSpace(input) == "" { 193 PrintWarnMessage("\nYou must enter non-empty value\n") 194 continue 195 } 196 197 if private && input != "" { 198 if MaskSymbolColorTag == "" { 199 fmt.Println(getMask(input)) 200 } else { 201 fmtc.Println(MaskSymbolColorTag + getMask(input) + "{!}") 202 } 203 } else { 204 if !fsutil.IsCharacterDevice("/dev/stdin") && os.Getenv("FAKETTY") == "" { 205 fmt.Println(fmtc.Sprintf(Prompt) + input) 206 } 207 } 208 209 break 210 } 211 212 return input, err 213 } 214 215 // isTmuxSession returns true if we work in tmux session 216 func isTmuxSession() bool { 217 if tmux == 0 { 218 if os.Getenv("TMUX") == "" { 219 tmux = -1 220 } else { 221 tmux = 1 222 } 223 } 224 225 return tmux == 1 226 }