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  }