pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/usage/completion/zsh/zsh.go (about)

     1  // Package zsh provides methods for generating zsh completion
     2  package zsh
     3  
     4  // ////////////////////////////////////////////////////////////////////////////////// //
     5  //                                                                                    //
     6  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
     7  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     8  //                                                                                    //
     9  // ////////////////////////////////////////////////////////////////////////////////// //
    10  
    11  import (
    12  	"fmt"
    13  	"strings"
    14  
    15  	"pkg.re/essentialkaos/ek.v12/fmtc"
    16  	"pkg.re/essentialkaos/ek.v12/options"
    17  	"pkg.re/essentialkaos/ek.v12/usage"
    18  )
    19  
    20  // ////////////////////////////////////////////////////////////////////////////////// //
    21  
    22  // _ZSH_TEMPLATE is zsh completion template
    23  const _ZSH_TEMPLATE = `#compdef {{COMPNAME}}
    24  
    25  # This completion is automatically generated
    26  
    27  typeset -A opt_args
    28  
    29  _{{COMPNAME}}() {
    30    _arguments \
    31  {{GLOBAL_ARGS}}
    32  
    33  {{COMMANDS_HANDLERS}}
    34  {{FILES_HANDLER}}
    35  }
    36  
    37  {{COMMANDS_FUNC}}
    38  _{{COMPNAME}} "$@"
    39  `
    40  
    41  // ////////////////////////////////////////////////////////////////////////////////// //
    42  
    43  // Generate generates zsh completion code
    44  func Generate(info *usage.Info, opts options.Map, name string, fileGlob ...string) string {
    45  	result := _ZSH_TEMPLATE
    46  
    47  	result = strings.Replace(result, "{{GLOBAL_ARGS}}", genGlobalOptionList(info, opts), -1)
    48  	result = strings.Replace(result, "{{COMMANDS_HANDLERS}}", genCommandsHandlers(info, opts), -1)
    49  	result = strings.Replace(result, "{{COMMANDS_FUNC}}", genCommandsFunc(info), -1)
    50  	result = strings.Replace(result, "{{FILES_HANDLER}}", genFilesHandler(info, fileGlob), -1)
    51  	result = strings.Replace(result, "{{COMPNAME}}", name, -1)
    52  
    53  	nameSafe := strings.Replace(name, "-", "_", -1)
    54  
    55  	result = strings.Replace(result, "{{COMPNAME_SAFE}}", nameSafe, -1)
    56  
    57  	return result
    58  }
    59  
    60  // ////////////////////////////////////////////////////////////////////////////////// //
    61  
    62  // genFilesHandler generates handler for showing files
    63  func genFilesHandler(info *usage.Info, fileGlob []string) string {
    64  	if len(info.Args) == 0 && len(fileGlob) == 0 {
    65  		return ""
    66  	}
    67  
    68  	if len(fileGlob) != 0 {
    69  		return "  _files -g \"" + fileGlob[0] + "\""
    70  	}
    71  
    72  	return "  _files"
    73  }
    74  
    75  // genGlobalOptionList generates list with global options
    76  func genGlobalOptionList(info *usage.Info, opts options.Map) string {
    77  	var result string
    78  
    79  	nonGlobalOptions := make(map[string]bool)
    80  
    81  	for _, cmd := range info.Commands {
    82  		for _, opt := range cmd.BoundOptions {
    83  			nonGlobalOptions[opt] = true
    84  		}
    85  	}
    86  
    87  	for _, opt := range info.Options {
    88  		if nonGlobalOptions[opt.Long] {
    89  			continue
    90  		}
    91  
    92  		result += genOptionDesc(opt, opts, 4)
    93  	}
    94  
    95  	if len(info.Commands) != 0 {
    96  		result += "    '1: :_{{COMPNAME_SAFE}}_cmds' \\\n"
    97  	}
    98  
    99  	result += "    '*:: :->cmd_args' && ret=0"
   100  
   101  	return result
   102  }
   103  
   104  // genOptionDesc generates description for some option
   105  func genOptionDesc(opt *usage.Option, opts options.Map, prefixSize int) string {
   106  	result := strings.Repeat(" ", prefixSize)
   107  
   108  	var isBool, isMergeble bool
   109  	var optV *options.V
   110  
   111  	optLong := opt.Long
   112  
   113  	if opts != nil {
   114  		optV = opts[getOptionFullName(opt)]
   115  	}
   116  
   117  	if optV != nil {
   118  		isBool = optV.Type == options.BOOL
   119  		isMergeble = optV.Mergeble
   120  	}
   121  
   122  	if !isBool {
   123  		optLong += "="
   124  	}
   125  
   126  	if !isMergeble {
   127  		var exclusion string
   128  
   129  		if opt.Short != "" {
   130  			exclusion = fmt.Sprintf("-%s --%s", opt.Short, optLong)
   131  		} else {
   132  			exclusion = fmt.Sprintf("--%s", optLong)
   133  		}
   134  
   135  		if optV != nil && optV.Conflicts != "" {
   136  			exclusion += genConflictsExclusion(optV.Conflicts)
   137  		}
   138  
   139  		result += "'(" + exclusion + ")'"
   140  	}
   141  
   142  	if opt.Short != "" {
   143  		result += fmt.Sprintf("{-%s,--%s}", opt.Short, optLong)
   144  	} else {
   145  		result += fmt.Sprintf("--%s", opt.Long)
   146  	}
   147  
   148  	result += fmt.Sprintf("'[%s]'", fmtc.Clean(opt.Desc))
   149  	result += " \\\n"
   150  
   151  	return result
   152  }
   153  
   154  // genCommandsHandlers generates handlers for commands
   155  func genCommandsHandlers(info *usage.Info, opts options.Map) string {
   156  	if !isCommandHandlersRequired(info) {
   157  		return ""
   158  	}
   159  
   160  	result := "  case $state in\n"
   161  	result += "    cmd_args)\n"
   162  	result += "      case $words[1] in\n"
   163  
   164  	for _, cmd := range info.Commands {
   165  		if len(cmd.BoundOptions) != 0 {
   166  			result += genCommandHandler(cmd, info, opts)
   167  		}
   168  	}
   169  
   170  	result += "      esac\n"
   171  	result += "    ;;\n"
   172  	result += "  esac\n"
   173  
   174  	return result
   175  }
   176  
   177  // genCommandHandler generates handler for given command
   178  func genCommandHandler(cmd *usage.Command, info *usage.Info, opts options.Map) string {
   179  	result := fmt.Sprintf("        %s)\n", cmd.Name)
   180  	result += "          _arguments \\\n"
   181  
   182  	for _, optName := range cmd.BoundOptions {
   183  		opt := info.GetOption(optName)
   184  
   185  		if opt == nil {
   186  			continue
   187  		}
   188  
   189  		result += genOptionDesc(opt, opts, 12)
   190  	}
   191  
   192  	result += "        ;;\n"
   193  
   194  	return result
   195  }
   196  
   197  // genCommandsFunc generates function which generates list with all supported commands
   198  func genCommandsFunc(info *usage.Info) string {
   199  	if len(info.Commands) == 0 {
   200  		return ""
   201  	}
   202  
   203  	result := "_{{COMPNAME_SAFE}}_cmds() {\n"
   204  	result += "  local -a commands\n"
   205  	result += "  commands=(\n"
   206  
   207  	for _, cmd := range info.Commands {
   208  		result += fmt.Sprintf("    '%s:%s'\n", cmd.Name, fmtc.Clean(cmd.Desc))
   209  	}
   210  
   211  	result += "  )\n"
   212  	result += "  _describe 'command' commands\n"
   213  	result += "}\n"
   214  
   215  	return result
   216  }
   217  
   218  // genConflictsExclusion generates list with conflicts exlusions
   219  func genConflictsExclusion(opts string) string {
   220  	var result []string
   221  
   222  	for _, opt := range strings.Split(opts, " ") {
   223  		long, short := options.ParseOptionName(opt)
   224  
   225  		if short != "" {
   226  			result = append(result, "-"+short, "--"+long)
   227  		} else {
   228  			result = append(result, "--"+long)
   229  		}
   230  	}
   231  
   232  	return " " + strings.Join(result, " ")
   233  }
   234  
   235  // isCommandHandlersRequired returns true if commands have bound options
   236  func isCommandHandlersRequired(info *usage.Info) bool {
   237  	for _, cmd := range info.Commands {
   238  		if len(cmd.BoundOptions) != 0 {
   239  			return true
   240  		}
   241  	}
   242  
   243  	return false
   244  }
   245  
   246  // getOptionFullName generates combined option name
   247  func getOptionFullName(opt *usage.Option) string {
   248  	if opt.Short != "" {
   249  		return opt.Short + ":" + opt.Long
   250  	}
   251  
   252  	return opt.Long
   253  }