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