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  }