feat: autocompletion

This commit is contained in:
Ayman Bagabas 2022-07-20 13:39:22 -04:00 committed by Maas Lalani
parent d5e21ea594
commit eaef49cdbe
No known key found for this signature in database
GPG key ID: 5A6ED5CBF1A0A000
12 changed files with 1574 additions and 13 deletions

View file

@ -6,9 +6,11 @@ import (
"os"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/paginator"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
"github.com/charmbracelet/lipgloss"
)
@ -89,3 +91,8 @@ func (o Options) Run() error {
return err
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
return style.HideFlags(ctx)
}

600
completion/bash.go Normal file
View file

@ -0,0 +1,600 @@
package completion
import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"github.com/alecthomas/kong"
)
type Bash struct{}
func (b Bash) BeforeApply(app *kong.Kong) error {
return GenBashCompletion(app.Model.Node, app.Stdout)
}
func writePreamble(buf io.StringWriter, name string) {
WriteStringAndCheck(buf, fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
WriteStringAndCheck(buf, fmt.Sprintf(`
__%[1]s_debug()
{
if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
fi
}
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
# _init_completion. This is a very minimal version of that function.
__%[1]s_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
__%[1]s_index_of_word()
{
local w word=$1
shift
index=0
for w in "$@"; do
[[ $w = "$word" ]] && return
index=$((index+1))
done
index=-1
}
__%[1]s_contains_word()
{
local w word=$1; shift
for w in "$@"; do
[[ $w = "$word" ]] && return
done
return 1
}
__%[1]s_handle_go_custom_completion()
{
__%[1]s_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}"
local shellCompDirectiveError=%[3]d
local shellCompDirectiveNoSpace=%[4]d
local shellCompDirectiveNoFileComp=%[5]d
local shellCompDirectiveFilterFileExt=%[6]d
local shellCompDirectiveFilterDirs=%[7]d
local out requestComp lastParam lastChar comp directive args
# Prepare the command to request completions for the program.
# Calling ${words[0]} instead of directly %[1]s allows to handle aliases
args=("${words[@]:1}")
# Disable ActiveHelp which is not supported for bash completion v1
requestComp="%[8]s=0 ${words[0]} %[2]s ${args[*]}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__%[1]s_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
__%[1]s_debug "${FUNCNAME[0]}: Adding extra empty parameter"
requestComp="${requestComp} \"\""
fi
__%[1]s_debug "${FUNCNAME[0]}: calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval "${requestComp}" 2>/dev/null)
# Extract the directive integer at the very end of the output following a colon (:)
directive=${out##*:}
# Remove the directive
out=${out%%:*}
if [ "${directive}" = "${out}" ]; then
# There is not directive specified
directive=0
fi
__%[1]s_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
__%[1]s_debug "${FUNCNAME[0]}: the completions are: ${out}"
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
# Error code. No completion.
__%[1]s_debug "${FUNCNAME[0]}: received error from custom completion go code"
return
else
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__%[1]s_debug "${FUNCNAME[0]}: activating no space"
compopt -o nospace
fi
fi
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__%[1]s_debug "${FUNCNAME[0]}: activating no file completion"
compopt +o default
fi
fi
fi
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering
local fullFilter filter filteringCmd
# Do not use quotes around the $out variable or else newline
# characters will be kept.
for filter in ${out}; do
fullFilter+="$filter|"
done
filteringCmd="_filedir $fullFilter"
__%[1]s_debug "File filtering command: $filteringCmd"
$filteringCmd
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
# File completion for directories only
local subdir
# Use printf to strip any trailing newline
subdir=$(printf "%%s" "${out}")
if [ -n "$subdir" ]; then
__%[1]s_debug "Listing directories in $subdir"
__%[1]s_handle_subdirs_in_dir_flag "$subdir"
else
__%[1]s_debug "Listing directories in ."
_filedir -d
fi
else
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${out}" -- "$cur")
fi
}
__%[1]s_handle_reply()
{
__%[1]s_debug "${FUNCNAME[0]}"
local comp
case $cur in
-*)
if [[ $(type -t compopt) = "builtin" ]]; then
compopt -o nospace
fi
local allflags
if [ ${#must_have_one_flag[@]} -ne 0 ]; then
allflags=("${must_have_one_flag[@]}")
else
allflags=("${flags[*]} ${two_word_flags[*]}")
fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${allflags[*]}" -- "$cur")
if [[ $(type -t compopt) = "builtin" ]]; then
[[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace
fi
# complete after --flag=abc
if [[ $cur == *=* ]]; then
if [[ $(type -t compopt) = "builtin" ]]; then
compopt +o nospace
fi
local index flag
flag="${cur%%=*}"
__%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}"
COMPREPLY=()
if [[ ${index} -ge 0 ]]; then
PREFIX=""
cur="${cur#*=}"
${flags_completion[${index}]}
if [ -n "${ZSH_VERSION:-}" ]; then
# zsh completion needs --flag= prefix
eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )"
fi
fi
fi
if [[ -z "${flag_parsing_disabled}" ]]; then
# If flag parsing is enabled, we have completed the flags and can return.
# If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough
# to possibly call handle_go_custom_completion.
return 0;
fi
;;
esac
# check if we are handling a flag with special work handling
local index
__%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}"
if [[ ${index} -ge 0 ]]; then
${flags_completion[${index}]}
return
fi
# we are parsing a flag and don't have a special handler, no completion
if [[ ${cur} != "${words[cword]}" ]]; then
return
fi
local completions
completions=("${commands[@]}")
if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then
completions+=("${must_have_one_noun[@]}")
elif [[ -n "${has_completion_function}" ]]; then
# if a go completion function is provided, defer to that function
__%[1]s_handle_go_custom_completion
fi
if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then
completions+=("${must_have_one_flag[@]}")
fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${completions[*]}" -- "$cur")
if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${noun_aliases[*]}" -- "$cur")
fi
if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
if declare -F __%[1]s_custom_func >/dev/null; then
# try command name qualified custom func
__%[1]s_custom_func
else
# otherwise fall back to unqualified for compatibility
declare -F __custom_func >/dev/null && __custom_func
fi
fi
# available in bash-completion >= 2, not always present on macOS
if declare -F __ltrim_colon_completions >/dev/null; then
__ltrim_colon_completions "$cur"
fi
# If there is only 1 completion and it is a flag with an = it will be completed
# but we don't want a space after the =
if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
compopt -o nospace
fi
}
# The arguments should be in the form "ext1|ext2|extn"
__%[1]s_handle_filename_extension_flag()
{
local ext="$1"
_filedir "@(${ext})"
}
__%[1]s_handle_subdirs_in_dir_flag()
{
local dir="$1"
pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return
}
__%[1]s_handle_flag()
{
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
# if a command required a flag, and we found it, unset must_have_one_flag()
local flagname=${words[c]}
local flagvalue=""
# if the word contained an =
if [[ ${words[c]} == *"="* ]]; then
flagvalue=${flagname#*=} # take in as flagvalue after the =
flagname=${flagname%%=*} # strip everything after the =
flagname="${flagname}=" # but put the = back
fi
__%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}"
if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
must_have_one_flag=()
fi
# if you set a flag which only applies to this command, don't show subcommands
if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
commands=()
fi
# keep flag value with flagname as flaghash
# flaghash variable is an associative array which is only supported in bash > 3.
if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
if [ -n "${flagvalue}" ] ; then
flaghash[${flagname}]=${flagvalue}
elif [ -n "${words[ $((c+1)) ]}" ] ; then
flaghash[${flagname}]=${words[ $((c+1)) ]}
else
flaghash[${flagname}]="true" # pad "true" for bool flag
fi
fi
# skip the argument to a two word flag
if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then
__%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
c=$((c+1))
# if we are looking for a flags value, don't show commands
if [[ $c -eq $cword ]]; then
commands=()
fi
fi
c=$((c+1))
}
__%[1]s_handle_noun()
{
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
must_have_one_noun=()
elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then
must_have_one_noun=()
fi
nouns+=("${words[c]}")
c=$((c+1))
}
__%[1]s_handle_command()
{
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local next_command
if [[ -n ${last_command} ]]; then
next_command="_${last_command}_${words[c]//:/__}"
else
if [[ $c -eq 0 ]]; then
next_command="_%[1]s_root_command"
else
next_command="_${words[c]//:/__}"
fi
fi
c=$((c+1))
__%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}"
declare -F "$next_command" >/dev/null && $next_command
}
__%[1]s_handle_word()
{
if [[ $c -ge $cword ]]; then
__%[1]s_handle_reply
return
fi
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
if [[ "${words[c]}" == -* ]]; then
__%[1]s_handle_flag
elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then
__%[1]s_handle_command
elif [[ $c -eq 0 ]]; then
__%[1]s_handle_command
elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then
# aliashash variable is an associative array which is only supported in bash > 3.
if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then
words[c]=${aliashash[${words[c]}]}
__%[1]s_handle_command
else
__%[1]s_handle_noun
fi
else
__%[1]s_handle_noun
fi
__%[1]s_handle_word
}
`, name, ShellCompNoDescRequestCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, activeHelpEnvVar(name)))
}
func writePostscript(buf io.StringWriter, name string) {
name = strings.ReplaceAll(name, ":", "__")
WriteStringAndCheck(buf, fmt.Sprintf("__start_%s()\n", name))
WriteStringAndCheck(buf, fmt.Sprintf(`{
local cur prev words cword split
declare -A flaghash 2>/dev/null || :
declare -A aliashash 2>/dev/null || :
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -s || return
else
__%[1]s_init_completion -n "=" || return
fi
local c=0
local flag_parsing_disabled=
local flags=()
local two_word_flags=()
local local_nonpersistent_flags=()
local flags_with_completion=()
local flags_completion=()
local commands=("%[1]s")
local command_aliases=()
local must_have_one_flag=()
local must_have_one_noun=()
local has_completion_function=""
local last_command=""
local nouns=()
local noun_aliases=()
__%[1]s_handle_word
}
`, name))
WriteStringAndCheck(buf, fmt.Sprintf(`if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_%s %s
else
complete -o default -o nospace -F __start_%s %s
fi
`, name, name, name, name))
WriteStringAndCheck(buf, "# ex: ts=4 sw=4 et filetype=sh\n")
}
func writeCommands(buf io.StringWriter, cmd *kong.Node) {
WriteStringAndCheck(buf, " commands=()\n")
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
WriteStringAndCheck(buf, fmt.Sprintf(" commands+=(%q)\n", c.Name))
writeCmdAliases(buf, c)
}
WriteStringAndCheck(buf, "\n")
}
func writeFlagHandler(buf io.StringWriter, name string, annotations map[string][]string, cmd *kong.Node) {
for key, value := range annotations {
switch key {
case BashCompFilenameExt:
WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
var ext string
if len(value) > 0 {
ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Parent.Name) + strings.Join(value, "|")
} else {
ext = "_filedir"
}
WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
case BashCompCustom:
WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
if len(value) > 0 {
handlers := strings.Join(value, "; ")
WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", handlers))
} else {
WriteStringAndCheck(buf, " flags_completion+=(:)\n")
}
case BashCompSubdirsInDir:
WriteStringAndCheck(buf, fmt.Sprintf(" flags_with_completion+=(%q)\n", name))
var ext string
if len(value) == 1 {
ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Parent.Name) + value[0]
} else {
ext = "_filedir -d"
}
WriteStringAndCheck(buf, fmt.Sprintf(" flags_completion+=(%q)\n", ext))
}
}
}
const cbn = "\")\n"
func writeShortFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
name := fmt.Sprintf("%c", flag.Short)
format := " "
if len(flag.DefaultValue.String()) == 0 {
format += "two_word_"
}
format += "flags+=(\"-%s" + cbn
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
writeFlagHandler(buf, "-"+name, map[string][]string{}, cmd)
}
func writeFlag(buf io.StringWriter, flag *kong.Flag, cmd *kong.Node) {
name := flag.Name
format := " flags+=(\"--%s"
if len(flag.DefaultValue.String()) == 0 {
format += "="
}
format += cbn
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
if len(flag.DefaultValue.String()) == 0 {
format = " two_word_flags+=(\"--%s" + cbn
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
}
writeFlagHandler(buf, "--"+name, map[string][]string{}, cmd)
}
func writeLocalNonPersistentFlag(buf io.StringWriter, flag *kong.Flag) {
name := flag.Name
format := " local_nonpersistent_flags+=(\"--%[1]s" + cbn
if len(flag.DefaultValue.String()) == 0 {
format += " local_nonpersistent_flags+=(\"--%[1]s=" + cbn
}
WriteStringAndCheck(buf, fmt.Sprintf(format, name))
if flag.Short > 0 {
WriteStringAndCheck(buf, fmt.Sprintf(" local_nonpersistent_flags+=(\"-%c\")\n", flag.Short))
}
}
func writeFlags(buf io.StringWriter, cmd *kong.Node) {
WriteStringAndCheck(buf, ` flags=()
two_word_flags=()
local_nonpersistent_flags=()
flags_with_completion=()
flags_completion=()
`)
for _, flag := range cmd.Flags {
if nonCompletableFlag(flag) {
continue
}
writeFlag(buf, flag, cmd)
if flag.Short != 0 {
writeShortFlag(buf, flag, cmd)
}
}
WriteStringAndCheck(buf, "\n")
}
func writeCmdAliases(buf io.StringWriter, cmd *kong.Node) {
if len(cmd.Aliases) == 0 {
return
}
sort.Strings(cmd.Aliases)
WriteStringAndCheck(buf, fmt.Sprint(` if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then`, "\n"))
for _, value := range cmd.Aliases {
WriteStringAndCheck(buf, fmt.Sprintf(" command_aliases+=(%q)\n", value))
WriteStringAndCheck(buf, fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name))
}
WriteStringAndCheck(buf, ` fi`)
WriteStringAndCheck(buf, "\n")
}
func writeArgAliases(buf io.StringWriter, cmd *kong.Node) {
WriteStringAndCheck(buf, " noun_aliases=()\n")
sort.Strings(cmd.Aliases)
for _, value := range cmd.Aliases {
WriteStringAndCheck(buf, fmt.Sprintf(" noun_aliases+=(%q)\n", value))
}
}
func gen(buf io.StringWriter, cmd *kong.Node) {
for _, c := range cmd.Children {
if c == nil || c.Hidden {
continue
}
gen(buf, c)
}
commandName := cmd.FullPath()
commandName = strings.ReplaceAll(commandName, " ", "_")
commandName = strings.ReplaceAll(commandName, ":", "__")
if cmd.Parent == nil {
WriteStringAndCheck(buf, fmt.Sprintf("_%s_root_command()\n{\n", commandName))
} else {
WriteStringAndCheck(buf, fmt.Sprintf("_%s()\n{\n", commandName))
}
WriteStringAndCheck(buf, fmt.Sprintf(" last_command=%q\n", commandName))
WriteStringAndCheck(buf, "\n")
WriteStringAndCheck(buf, " command_aliases=()\n")
WriteStringAndCheck(buf, "\n")
writeCommands(buf, cmd)
writeFlags(buf, cmd)
writeArgAliases(buf, cmd)
WriteStringAndCheck(buf, "}\n\n")
}
// GenBashCompletion generates bash completion file and writes to the passed writer.
func GenBashCompletion(c *kong.Node, w io.Writer) error {
buf := new(bytes.Buffer)
writePreamble(buf, c.Name)
gen(buf, c)
writePostscript(buf, c.Name)
_, err := buf.WriteTo(w)
return err
}

8
completion/command.go Normal file
View file

@ -0,0 +1,8 @@
package completion
type Completion struct {
Complete Complete `cmd:"" hidden:"" help:"Request shell completion"`
Bash Bash `cmd:"" help:"Generate the autocompletion script for bash"`
Zsh Zsh `cmd:"" help:"Generate the autocompletion script for zsh"`
// Fish Fish `cmd:"" help:"Generate the autocompletion script for fish"`
}

637
completion/shell.go Normal file
View file

@ -0,0 +1,637 @@
package completion
import (
"fmt"
"io"
"os"
"strings"
"sync"
"github.com/alecthomas/kong"
)
type Complete struct {
Arg []string `arg:"" passthrough:""`
}
type flagCompError struct {
subCommand string
flagName string
}
func (e *flagCompError) Error() string {
return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'"
}
// ShellCompDirective is a bit map representing the different behaviors the shell
// can be instructed to have once completions have been provided.
type ShellCompDirective int
const (
// ShellCompDirectiveError indicates an error occurred and completions should be ignored.
ShellCompDirectiveError ShellCompDirective = 1 << iota
// ShellCompDirectiveNoSpace indicates that the shell should not add a space
// after the completion even if there is a single completion provided.
ShellCompDirectiveNoSpace
// ShellCompDirectiveNoFileComp indicates that the shell should not provide
// file completion even when no completion is provided.
ShellCompDirectiveNoFileComp
// ShellCompDirectiveFilterFileExt indicates that the provided completions
// should be used as file extension filters.
// For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
// is a shortcut to using this directive explicitly. The BashCompFilenameExt
// annotation can also be used to obtain the same behavior for flags.
ShellCompDirectiveFilterFileExt
// ShellCompDirectiveFilterDirs indicates that only directory names should
// be provided in file completion. To request directory names within another
// directory, the returned completions should specify the directory within
// which to search. The BashCompSubdirsInDir annotation can be used to
// obtain the same behavior but only for flags.
ShellCompDirectiveFilterDirs
// ===========================================================================
// All directives using iota should be above this one.
// For internal use.
shellCompDirectiveMaxValue
// ShellCompDirectiveDefault indicates to let the shell perform its default
// behavior after completions have been provided.
// This one must be last to avoid messing up the iota count.
ShellCompDirectiveDefault ShellCompDirective = 0
)
// Annotations for Bash completion.
const (
// ShellCompRequestCmd is the name of the hidden command that is used to request
// completion results from the program. It is used by the shell completion scripts.
ShellCompRequestCmd = "completion complete"
// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
// completion results without their description. It is used by the shell completion scripts.
ShellCompNoDescRequestCmd = "completion completeNoDesc"
BashCompFilenameExt = "kong_annotation_bash_completion_filename_extensions"
BashCompCustom = "kong_annotation_bash_completion_custom"
BashCompOneRequiredFlag = "kong_annotation_bash_completion_one_required_flag"
BashCompSubdirsInDir = "kong_annotation_bash_completion_subdirs_in_dir"
)
// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it.
var flagCompletionFunctions = map[*kong.Flag]func(cmd *kong.Node, args []string, toComplete string) ([]string, ShellCompDirective){}
// lock for reading and writing from flagCompletionFunctions
var flagCompletionMutex = &sync.RWMutex{}
const (
activeHelpMarker = "_activeHelp_ "
// The below values should not be changed: programs will be using them explicitly
// in their user documentation, and users will be using them explicitly.
activeHelpEnvVarSuffix = "_ACTIVE_HELP"
activeHelpGlobalEnvVar = "KONG_ACTIVE_HELP"
activeHelpGlobalDisable = "0"
)
// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment
// variable. It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the
// root command in upper case, with all - replaced by _.
func activeHelpEnvVar(name string) string {
// This format should not be changed: users will be using it explicitly.
activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix))
return strings.ReplaceAll(activeHelpEnvVar, "-", "_")
}
// WriteStringAndCheck writes a string into a buffer, and checks if the error is not nil.
func WriteStringAndCheck(b io.StringWriter, s string) {
_, err := b.WriteString(s)
if err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
}
// Returns a string listing the different directive enabled in the specified parameter
func (d ShellCompDirective) string() string {
var directives []string
if d&ShellCompDirectiveError != 0 {
directives = append(directives, "ShellCompDirectiveError")
}
if d&ShellCompDirectiveNoSpace != 0 {
directives = append(directives, "ShellCompDirectiveNoSpace")
}
if d&ShellCompDirectiveNoFileComp != 0 {
directives = append(directives, "ShellCompDirectiveNoFileComp")
}
if d&ShellCompDirectiveFilterFileExt != 0 {
directives = append(directives, "ShellCompDirectiveFilterFileExt")
}
if d&ShellCompDirectiveFilterDirs != 0 {
directives = append(directives, "ShellCompDirectiveFilterDirs")
}
if len(directives) == 0 {
directives = append(directives, "ShellCompDirectiveDefault")
}
if d >= shellCompDirectiveMaxValue {
return fmt.Sprintf("ERROR: unexpected ShellCompDirective value: %d", d)
}
return strings.Join(directives, ", ")
}
func (l Complete) BeforeApply(ctx *kong.Context) error {
_, completions, directive, err := getCompletions(ctx.Model.Node, ctx.Args[2:])
if err != nil {
CompErrorln(err.Error())
// Keep going for multiple reasons:
// 1- There could be some valid completions even though there was an error
// 2- Even without completions, we need to print the directive
}
noDescriptions := false // (cmd.CalledAs() == ShellCompNoDescRequestCmd)
for _, comp := range completions {
// if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable {
// // Remove all activeHelp entries in this case
// if strings.HasPrefix(comp, activeHelpMarker) {
// continue
// }
// }
if noDescriptions {
// Remove any description that may be included following a tab character.
comp = strings.Split(comp, "\t")[0]
}
// Make sure we only write the first line to the output.
// This is needed if a description contains a linebreak.
// Otherwise the shell scripts will interpret the other lines as new flags
// and could therefore provide a wrong completion.
comp = strings.Split(comp, "\n")[0]
// Finally trim the completion. This is especially important to get rid
// of a trailing tab when there are no description following it.
// For example, a sub-command without a description should not be completed
// with a tab at the end (or else zsh will show a -- following it
// although there is no description).
comp = strings.TrimSpace(comp)
// Print each possible completion to stdout for the completion script to consume.
fmt.Fprintln(ctx.Stdout, comp)
}
// As the last printout, print the completion directive for the completion script to parse.
// The directive integer must be that last character following a single colon (:).
// The completion script expects :<directive>
fmt.Fprintf(ctx.Stdout, ":%d\n", directive)
// Print some helpful info to stderr for the user to understand.
// Output from stderr must be ignored by the completion script.
fmt.Fprintf(ctx.Stderr, "Completion ended with directive: %s\n", directive.string())
return nil
}
func parseFlags(c *kong.Node, args []string) (int, error) {
flags := make([]*kong.Flag, 0)
for _, f := range c.Flags {
if f.Hidden || f.Tag.Optional || f.Tag.Ignored {
continue
}
flags = append(flags, f)
}
return len(flags), nil
}
func traverse(c *kong.Node, args []string) (*kong.Node, []string, error) {
if len(args) == 0 {
return c, args, nil
}
// Find the sub-command to complete.
for _, c := range c.Children {
if c.Name == args[0] {
return traverse(c, args[1:])
}
}
// If we didn't find a sub-command, we are at the end of the path.
// We can complete the sub-command name.
return c, args, nil
}
func getCompletions(c *kong.Node, args []string) (*kong.Node, []string, ShellCompDirective, error) {
// The last argument, which is not completely typed by the user,
// should not be part of the list of arguments
CompDebugln(c.Name, false)
CompDebugln(fmt.Sprint(args), false)
toComplete := args[len(args)-1]
trimmedArgs := args[:len(args)-1]
CompDebugln(fmt.Sprintf("toComplete: %v", toComplete), false)
CompDebugln(fmt.Sprintf("trimmedArgs: %v", trimmedArgs), false)
var finalCmd *kong.Node
var finalArgs []string
var err error
finalCmd, finalArgs, err = traverse(c, trimmedArgs)
if err != nil {
// Unable to find the real command. E.g., <program> someInvalidCmd <TAB>
return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs)
}
CompDebugln(fmt.Sprintf("finalCmd: %v", finalCmd.Name), false)
CompDebugln(fmt.Sprintf("finalArgs: %v", finalArgs), false)
// Check if we are doing flag value completion before parsing the flags.
// This is important because if we are completing a flag value, we need to also
// remove the flag name argument from the list of finalArgs or else the parsing
// could fail due to an invalid value (incomplete) for the flag.
flag, finalArgs, toComplete, flagErr := checkIfFlagCompletion(finalCmd, finalArgs, toComplete)
// Check if interspersed is false or -- was set on a previous arg.
// This works by counting the arguments. Normally -- is not counted as arg but
// if -- was already set or interspersed is false and there is already one arg then
// the extra added -- is counted as arg.
flagCompletion := true
newArgCount, _ := parseFlags(finalCmd, append(finalArgs, "--")) //len(finalCmd.Flags) //finalCmd.Flags().NArg()
// Parse the flags early so we can check if required flags are set
realArgCount, err := parseFlags(finalCmd, finalArgs)
if err != nil {
return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error())
}
if newArgCount > realArgCount {
// don't do flag completion (see above)
flagCompletion = false
}
// Error while attempting to parse flags
if flagErr != nil {
// If error type is flagCompError and we don't want flagCompletion we should ignore the error
if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
return finalCmd, []string{}, ShellCompDirectiveDefault, flagErr
}
}
// We only remove the flags from the arguments if DisableFlagParsing is not set.
// This is important for commands which have requested to do their own flag completion.
// if !finalCmd.DisableFlagParsing {
// finalArgs = finalCmd.Flags().Args()
// }
if flag != nil && flagCompletion {
// Check if we are completing a flag value subject to annotations
// if validExts, present := flag.Annotations[BashCompFilenameExt]; present {
// if len(validExts) != 0 {
// // File completion filtered by extensions
// return finalCmd, validExts, ShellCompDirectiveFilterFileExt, nil
// }
// The annotation requests simple file completion. There is no reason to do
// that since it is the default behavior anyway. Let's ignore this annotation
// in case the program also registered a completion function for this flag.
// Even though it is a mistake on the program's side, let's be nice when we can.
// }
// if subDir, present := flag.Annotations[BashCompSubdirsInDir]; present {
// if len(subDir) == 1 {
// // Directory completion from within a directory
// return finalCmd, subDir, ShellCompDirectiveFilterDirs, nil
// }
// // Directory completion
// return finalCmd, []string{}, ShellCompDirectiveFilterDirs, nil
// }
}
var completions []string
var directive ShellCompDirective
// Enforce flag groups before doing flag completions
// finalCmd.enforceFlagGroupsForCompletion()
// Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true;
// doing this allows for completion of persistent flag names even for commands that disable flag parsing.
//
// When doing completion of a flag name, as soon as an argument starts with
// a '-' we know it is a flag. We cannot use isFlagArg() here as it requires
// the flag name to be complete
if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion {
CompDebugln("Flag name completion", false)
// First check for required flags
completions = completeRequireFlags(finalCmd, toComplete)
// If we have not found any required flags, only then can we show regular flags
if len(completions) == 0 {
for _, flag := range finalCmd.Flags {
completions = append(completions, getFlagNameCompletions(flag, toComplete)...)
}
}
directive = ShellCompDirectiveNoFileComp
if len(completions) == 1 && strings.HasSuffix(completions[0], "=") {
// If there is a single completion, the shell usually adds a space
// after the completion. We don't want that if the flag ends with an =
directive = ShellCompDirectiveNoSpace
}
// if !finalCmd.DisableFlagParsing {
// // If DisableFlagParsing==false, we have completed the flags as known by Cobra;
// // we can return what we found.
// // If DisableFlagParsing==true, Cobra may not be aware of all flags, so we
// // let the logic continue to see if ValidArgsFunction needs to be called.
// return finalCmd, completions, directive, nil
// }
} else {
directive = ShellCompDirectiveDefault
// if flag == nil {
foundLocalNonPersistentFlag := false
// If TraverseChildren is true on the root command we don't check for
// local flags because we can use a local flag on a parent command
// if !finalCmd.Root().TraverseChildren {
if finalCmd.Parent != nil && len(finalCmd.Parent.Children) == 0 {
// Check if there are any local, non-persistent flags on the command-line
// localNonPersistentFlags := finalCmd.LocalNonPersistentFlags()
// finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
// if localNonPersistentFlags.Lookup(flag.Name) != nil && flag.Changed {
// foundLocalNonPersistentFlag = true
// }
// })
}
// Complete subcommand names, including the help command
if len(finalArgs) == 0 && !foundLocalNonPersistentFlag {
// We only complete sub-commands if:
// - there are no arguments on the command-line and
// - there are no local, non-persistent flags on the command-line or TraverseChildren is true
for _, subCmd := range finalCmd.Children {
// if subCmd.IsAvailableCommand() || subCmd == finalCmd.helpCommand {
if !subCmd.Hidden {
if strings.HasPrefix(subCmd.Name, toComplete) {
completions = append(completions, fmt.Sprintf("%s\t%s", subCmd.Name, subCmd.Help))
}
directive = ShellCompDirectiveNoFileComp
}
}
}
// Complete required flags even without the '-' prefix
completions = append(completions, completeRequireFlags(finalCmd, toComplete)...)
// Always complete ValidArgs, even if we are completing a subcommand name.
// This is for commands that have both subcommands and ValidArgs.
// if len(finalCmd.ValidArgs) > 0 {
// if len(finalArgs) == 0 {
// // ValidArgs are only for the first argument
// for _, validArg := range finalCmd.ValidArgs {
// if strings.HasPrefix(validArg, toComplete) {
// completions = append(completions, validArg)
// }
// }
// directive = ShellCompDirectiveNoFileComp
// // If no completions were found within commands or ValidArgs,
// // see if there are any ArgAliases that should be completed.
// if len(completions) == 0 {
// for _, argAlias := range finalCmd.ArgAliases {
// if strings.HasPrefix(argAlias, toComplete) {
// completions = append(completions, argAlias)
// }
// }
// }
// }
// // If there are ValidArgs specified (even if they don't match), we stop completion.
// // Only one of ValidArgs or ValidArgsFunction can be used for a single command.
// return finalCmd, completions, directive, nil
// }
// Let the logic continue so as to add any ValidArgsFunction completions,
// even if we already found sub-commands.
// This is for commands that have subcommands but also specify a ValidArgsFunction.
// }
}
// Find the completion function for the flag or command
var completionFn func(cmd *kong.Node, args []string, toComplete string) ([]string, ShellCompDirective)
if flag != nil && flagCompletion {
flagCompletionMutex.RLock()
completionFn = flagCompletionFunctions[flag]
flagCompletionMutex.RUnlock()
} else {
completionFn = nil //finalCmd.ValidArgsFunction
}
if completionFn != nil {
// Go custom completion defined for this flag or command.
// Call the registered completion function to get the completions.
var comps []string
comps, directive = completionFn(finalCmd, finalArgs, toComplete)
completions = append(completions, comps...)
}
return finalCmd, completions, directive, nil
}
func completeRequireFlags(finalCmd *kong.Node, toComplete string) []string {
var completions []string
// We cannot use finalCmd.Flags() because we may not have called ParsedFlags() for commands
// that have set DisableFlagParsing; it is ParseFlags() that merges the inherited and
// non-inherited flags.
// finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
// doCompleteRequiredFlags(flag)
// })
// finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
// doCompleteRequiredFlags(flag)
// })
for _, f := range finalCmd.Flags {
if f.Required {
CompDebugln(fmt.Sprintf("flag: %s help: %s", f.Name, f.Help), false)
completions = append(completions, getFlagNameCompletions(f, toComplete)...)
}
}
return completions
}
func getFlagNameCompletions(flag *kong.Flag, toComplete string) []string {
if nonCompletableFlag(flag) {
return []string{}
}
var completions []string
flagName := "--" + flag.Name
if strings.HasPrefix(flagName, toComplete) {
// Flag without the =
flagHelp := flag.Help
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flagHelp))
// Why suggest both long forms: --flag and --flag= ?
// This forces the user to *always* have to type either an = or a space after the flag name.
// Let's be nice and avoid making users have to do that.
// Since boolean flags and shortname flags don't show the = form, let's go that route and never show it.
// The = form will still work, we just won't suggest it.
// This also makes the list of suggested flags shorter as we avoid all the = forms.
//
// if len(flag.Default) == 0 {
// // Flag requires a value, so it can be suffixed with =
// flagName += "="
// completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Help))
// }
}
flagName = "-" + fmt.Sprintf("%c", flag.Short)
if flag.Short != 0 && strings.HasPrefix(flagName, toComplete) {
completions = append(completions, fmt.Sprintf("%s\t%s", flagName, flag.Help))
}
return completions
}
func isFlagArg(arg string) bool {
return ((len(arg) >= 3 && arg[1] == '-') ||
(len(arg) >= 2 && arg[0] == '-' && arg[1] != '-'))
}
func checkIfFlagCompletion(finalCmd *kong.Node, args []string, lastArg string) (*kong.Flag, []string, string, error) {
var flagName string
trimmedArgs := args
flagWithEqual := false
orgLastArg := lastArg
// When doing completion of a flag name, as soon as an argument starts with
// a '-' we know it is a flag. We cannot use isFlagArg() here as that function
// requires the flag name to be complete
if len(lastArg) > 0 && lastArg[0] == '-' {
if index := strings.Index(lastArg, "="); index >= 0 {
// Flag with an =
if strings.HasPrefix(lastArg[:index], "--") {
// Flag has full name
flagName = lastArg[2:index]
} else {
// Flag is shorthand
// We have to get the last shorthand flag name
// e.g. `-asd` => d to provide the correct completion
// https://github.com/spf13/cobra/issues/1257
flagName = lastArg[index-1 : index]
}
lastArg = lastArg[index+1:]
flagWithEqual = true
} else {
// Normal flag completion
return nil, args, lastArg, nil
}
}
if len(flagName) == 0 {
if len(args) > 0 {
prevArg := args[len(args)-1]
if isFlagArg(prevArg) {
// Only consider the case where the flag does not contain an =.
// If the flag contains an = it means it has already been fully processed,
// so we don't need to deal with it here.
if index := strings.Index(prevArg, "="); index < 0 {
if strings.HasPrefix(prevArg, "--") {
// Flag has full name
flagName = prevArg[2:]
} else {
// Flag is shorthand
// We have to get the last shorthand flag name
// e.g. `-asd` => d to provide the correct completion
// https://github.com/spf13/cobra/issues/1257
flagName = prevArg[len(prevArg)-1:]
}
// Remove the uncompleted flag or else there could be an error created
// for an invalid value for that flag
trimmedArgs = args[:len(args)-1]
}
}
}
}
if len(flagName) == 0 {
// Not doing flag completion
return nil, trimmedArgs, lastArg, nil
}
flag := findFlag(finalCmd, flagName)
if flag == nil {
// Flag not supported by this command, the interspersed option might be set so return the original args
return nil, args, orgLastArg, &flagCompError{subCommand: finalCmd.Name, flagName: flagName}
}
if !flagWithEqual {
if len(flag.Default) != 0 {
// We had assumed dealing with a two-word flag but the flag is a boolean flag.
// In that case, there is no value following it, so we are not really doing flag completion.
// Reset everything to do noun completion.
trimmedArgs = args
flag = nil
}
}
return flag, trimmedArgs, lastArg, nil
}
func shorthandLookup(flags []*kong.Flag, name string) *kong.Flag {
for _, flag := range flags {
if flag.Name == name {
return flag
}
}
return nil
}
func findFlag(cmd *kong.Node, name string) *kong.Flag {
flagSet := cmd.Flags
for _, flag := range flagSet {
if flag.Name == name {
return flag
}
}
return nil
}
func nonCompletableFlag(flag *kong.Flag) bool {
return flag.Hidden
}
// CompDebug prints the specified string to the same file as where the
// completion script prints its logs.
// Note that completion printouts should never be on stdout as they would
// be wrongly interpreted as actual completion choices by the completion script.
func CompDebug(msg string, printToStdErr bool) {
msg = fmt.Sprintf("[Debug] %s", msg)
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
f, err := os.OpenFile(path,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
defer f.Close()
WriteStringAndCheck(f, msg)
}
}
if printToStdErr {
// Must print to stderr for this not to be read by the completion script.
fmt.Fprint(os.Stderr, msg)
}
}
// CompDebugln prints the specified string with a newline at the end
// to the same file as where the completion script prints its logs.
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
func CompDebugln(msg string, printToStdErr bool) {
CompDebug(fmt.Sprintf("%s\n", msg), printToStdErr)
}
// CompError prints the specified completion message to stderr.
func CompError(msg string) {
msg = fmt.Sprintf("[Error] %s", msg)
CompDebug(msg, true)
}
// CompErrorln prints the specified completion message to stderr with a newline at the end.
func CompErrorln(msg string) {
CompError(fmt.Sprintf("%s\n", msg))
}

250
completion/zsh.go Normal file
View file

@ -0,0 +1,250 @@
package completion
import (
"bytes"
"fmt"
"io"
"github.com/alecthomas/kong"
)
type Zsh struct{}
func (b Zsh) BeforeApply(app *kong.Kong) error {
return GenZshCompletion(app.Model.Node, app.Stdout)
}
// GenZshCompletion generates zsh completion file including descriptions
// and writes it to the passed writer.
func GenZshCompletion(c *kong.Node, w io.Writer) error {
return genZshCompletion(c, w, true)
}
// // GenZshCompletionNoDesc generates zsh completion file without descriptions
// // and writes it to the passed writer.
// func (c *Command) GenZshCompletionNoDesc(w io.Writer) error {
// return c.genZshCompletion(w, false)
// }
func genZshCompletion(c *kong.Node, w io.Writer, includeDesc bool) error {
buf := new(bytes.Buffer)
genZshComp(buf, c.Name, includeDesc)
_, err := buf.WriteTo(w)
return err
}
func genZshComp(buf io.StringWriter, name string, includeDesc bool) {
compCmd := ShellCompRequestCmd
if !includeDesc {
compCmd = ShellCompNoDescRequestCmd
}
WriteStringAndCheck(buf, fmt.Sprintf(`#compdef %[1]s
# zsh completion for %-36[1]s -*- shell-script -*-
__%[1]s_debug()
{
local file="$BASH_COMP_DEBUG_FILE"
if [[ -n ${file} ]]; then
echo "$*" >> "${file}"
fi
}
_%[1]s()
{
local shellCompDirectiveError=%[3]d
local shellCompDirectiveNoSpace=%[4]d
local shellCompDirectiveNoFileComp=%[5]d
local shellCompDirectiveFilterFileExt=%[6]d
local shellCompDirectiveFilterDirs=%[7]d
local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace
local -a completions
__%[1]s_debug "\n========= starting completion logic =========="
__%[1]s_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}"
# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $CURRENT location, so we need
# to truncate the command-line ($words) up to the $CURRENT location.
# (We cannot use $CURSOR as its value does not work when a command is an alias.)
words=("${=words[1,CURRENT]}")
__%[1]s_debug "Truncated words[*]: ${words[*]},"
lastParam=${words[-1]}
lastChar=${lastParam[-1]}
__%[1]s_debug "lastParam: ${lastParam}, lastChar: ${lastChar}"
# For zsh, when completing a flag with an = (e.g., %[1]s -n=<TAB>)
# completions must be prefixed with the flag
setopt local_options BASH_REMATCH
if [[ "${lastParam}" =~ '-.*=' ]]; then
# We are dealing with a flag with an =
flagPrefix="-P ${BASH_REMATCH}"
fi
# Prepare the command to obtain completions
requestComp="${words[1]} %[2]s ${words[2,-1]}"
if [ "${lastChar}" = "" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go completion code.
__%[1]s_debug "Adding extra empty parameter"
requestComp="${requestComp} \"\""
fi
__%[1]s_debug "About to call: eval ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval ${requestComp} 2>/dev/null)
__%[1]s_debug "completion output: ${out}"
# Extract the directive integer following a : from the last line
local lastLine
while IFS='\n' read -r line; do
lastLine=${line}
done < <(printf "%%s\n" "${out[@]}")
__%[1]s_debug "last line: ${lastLine}"
if [ "${lastLine[1]}" = : ]; then
directive=${lastLine[2,-1]}
# Remove the directive including the : and the newline
local suffix
(( suffix=${#lastLine}+2))
out=${out[1,-$suffix]}
else
# There is no directive specified. Leave $out as is.
__%[1]s_debug "No directive found. Setting do default"
directive=0
fi
__%[1]s_debug "directive: ${directive}"
__%[1]s_debug "completions: ${out}"
__%[1]s_debug "flagPrefix: ${flagPrefix}"
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
__%[1]s_debug "Completion received error. Ignoring completions."
return
fi
local activeHelpMarker="%[8]s"
local endIndex=${#activeHelpMarker}
local startIndex=$((${#activeHelpMarker}+1))
local hasActiveHelp=0
while IFS='\n' read -r comp; do
# Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
__%[1]s_debug "ActiveHelp found: $comp"
comp="${comp[$startIndex,-1]}"
if [ -n "$comp" ]; then
compadd -x "${comp}"
__%[1]s_debug "ActiveHelp will need delimiter"
hasActiveHelp=1
fi
continue
fi
if [ -n "$comp" ]; then
# If requested, completions are returned with a description.
# The description is preceded by a TAB character.
# For zsh's _describe, we need to use a : instead of a TAB.
# We first need to escape any : as part of the completion itself.
comp=${comp//:/\\:}
local tab="$(printf '\t')"
comp=${comp//$tab/:}
__%[1]s_debug "Adding completion: ${comp}"
completions+=${comp}
lastComp=$comp
fi
done < <(printf "%%s\n" "${out[@]}")
# Add a delimiter after the activeHelp statements, but only if:
# - there are completions following the activeHelp statements, or
# - file completion will be performed (so there will be choices after the activeHelp)
if [ $hasActiveHelp -eq 1 ]; then
if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
__%[1]s_debug "Adding activeHelp delimiter"
compadd -x "--"
hasActiveHelp=0
fi
fi
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
__%[1]s_debug "Activating nospace."
noSpace="-S ''"
fi
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
# File extension filtering
local filteringCmd
filteringCmd='_files'
for filter in ${completions[@]}; do
if [ ${filter[1]} != '*' ]; then
# zsh requires a glob pattern to do file filtering
filter="\*.$filter"
fi
filteringCmd+=" -g $filter"
done
filteringCmd+=" ${flagPrefix}"
__%[1]s_debug "File filtering command: $filteringCmd"
_arguments '*:filename:'"$filteringCmd"
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
# File completion for directories only
local subdir
subdir="${completions[1]}"
if [ -n "$subdir" ]; then
__%[1]s_debug "Listing directories in $subdir"
pushd "${subdir}" >/dev/null 2>&1
else
__%[1]s_debug "Listing directories in ."
fi
local result
_arguments '*:dirname:_files -/'" ${flagPrefix}"
result=$?
if [ -n "$subdir" ]; then
popd >/dev/null 2>&1
fi
return $result
else
__%[1]s_debug "Calling _describe"
if eval _describe "completions" completions $flagPrefix $noSpace; then
__%[1]s_debug "_describe found some completions"
# Return the success of having called _describe
return 0
else
__%[1]s_debug "_describe did not find completions."
__%[1]s_debug "Checking if we should do file completion."
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
__%[1]s_debug "deactivating file completion"
# We must return an error code here to let zsh know that there were no
# completions found by _describe; this is what will trigger other
# matching algorithms to attempt to find completions.
# For example zsh can match letters in the middle of words.
return 1
else
# Perform file completion
__%[1]s_debug "Activating file completion"
# We must return the result of this command, so it must be the
# last command, or else we must store its result to return it.
_arguments '*:filename:_files'" ${flagPrefix}"
fi
fi
fi
}
# don't run the completion function when being source-ed or eval-ed
if [ "$funcstack[1]" = "_%[1]s" ]; then
_%[1]s
fi
`, name, compCmd,
ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs,
activeHelpMarker))
}

View file

@ -5,10 +5,12 @@ import (
"os"
"strings"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/files"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for filtering through options, powered
@ -49,3 +51,8 @@ func (o Options) Run() error {
return err
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
return style.HideFlags(ctx)
}

3
gum.go
View file

@ -2,6 +2,7 @@ package main
import (
"github.com/charmbracelet/gum/choose"
"github.com/charmbracelet/gum/completion"
"github.com/charmbracelet/gum/filter"
"github.com/charmbracelet/gum/format"
"github.com/charmbracelet/gum/input"
@ -14,6 +15,8 @@ import (
// Gum is the command-line interface for Gum.
type Gum struct {
Completion completion.Completion `cmd:"" hidden:"" help:"Request shell completion"`
// Man is a hidden command that generates Gum man pages.
Man man.Man `cmd:"" hidden:"" help:"Generate man pages"`

View file

@ -4,9 +4,11 @@ import (
"fmt"
"os"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the text input bubble.
@ -34,3 +36,8 @@ func (o Options) Run() error {
fmt.Println(m.(model).textinput.Value())
return err
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
return style.HideFlags(ctx)
}

View file

@ -1,8 +1,10 @@
package spin
import (
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the spinner bubble.
@ -19,3 +21,8 @@ func (o Options) Run() error {
p := tea.NewProgram(m)
return p.Start()
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
return style.HideFlags(ctx)
}

View file

@ -26,6 +26,8 @@ package style
import (
"fmt"
"strings"
"github.com/alecthomas/kong"
)
// Run provides a shell script interface for the Lip Gloss styling.
@ -35,3 +37,25 @@ func (o Options) Run() error {
fmt.Println(o.Style.ToLipgloss().Render(text))
return nil
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
return HideFlags(ctx)
}
// HideFlags hides the flags from the usage output. This is used in conjunction
// with BeforeReset hook.
func HideFlags(ctx *kong.Context) error {
n := ctx.Selected()
if n == nil {
return nil
}
for _, f := range n.Flags {
if g := f.Group; g != nil && g.Key == groupName {
if !strings.HasSuffix(f.Name, "foreground") {
f.Hidden = true
}
}
}
return nil
}

View file

@ -1,5 +1,9 @@
package style
const (
groupName = "Style Flags"
)
// Options is the customization options for the style command.
type Options struct {
Text []string `arg:"" optional:"" help:"Text to which to apply the style"`
@ -14,24 +18,24 @@ type Options struct {
// components, through embedding and prefixing.
type Styles struct {
// Colors
Background string `help:"Background color of the ${name=element}" default:"${defaultBackground}" hidden:"" group:"Style Flags"`
Background string `help:"Background color of the ${name=element}" default:"${defaultBackground}" group:"Style Flags"`
Foreground string `help:"color of the ${name=element}" default:"${defaultForeground}" group:"Style Flags"`
// Border
Border string `help:"Border style to apply" enum:"none,hidden,normal,rounded,thick,double" default:"none" hidden:"" group:"Style Flags"`
BorderBackground string `help:"Border background color" hidden:"" group:"Style Flags"`
BorderForeground string `help:"Border foreground color" hidden:"" group:"Style Flags"`
Border string `help:"Border style to apply" enum:"none,hidden,normal,rounded,thick,double" default:"none" group:"Style Flags"`
BorderBackground string `help:"Border background color" group:"Style Flags"`
BorderForeground string `help:"Border foreground color" group:"Style Flags"`
// Layout
Align string `help:"Text alignment" enum:"left,center,right,bottom,middle,top" default:"left" hidden:"" group:"Style Flags"`
Height int `help:"Height of output" hidden:"" group:"Style Flags"`
Width int `help:"Width of output" hidden:"" group:"Style Flags"`
Margin string `help:"Margin to apply around the text." default:"0 0" hidden:"" group:"Style Flags"`
Padding string `help:"Padding to apply around the text." default:"0 0" hidden:""`
Align string `help:"Text alignment" enum:"left,center,right,bottom,middle,top" default:"left" group:"Style Flags"`
Height int `help:"Height of output" group:"Style Flags"`
Width int `help:"Width of output" group:"Style Flags"`
Margin string `help:"Margin to apply around the text." default:"0 0" group:"Style Flags"`
Padding string `help:"Padding to apply around the text." default:"0 0"`
// Format
Bold bool `help:"Apply bold formatting" hidden:"" group:"Style Flags"`
Faint bool `help:"Apply faint formatting" hidden:"" group:"Style Flags"`
Italic bool `help:"Apply italic formatting" hidden:"" group:"Style Flags"`
Strikethrough bool `help:"Apply strikethrough formatting" hidden:"" group:"Style Flags"`
Bold bool `help:"Apply bold formatting" group:"Style Flags"`
Faint bool `help:"Apply faint formatting" group:"Style Flags"`
Italic bool `help:"Apply italic formatting" group:"Style Flags"`
Strikethrough bool `help:"Apply strikethrough formatting" group:"Style Flags"`
}

View file

@ -4,9 +4,11 @@ import (
"fmt"
"os"
"github.com/alecthomas/kong"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/stdin"
"github.com/charmbracelet/gum/style"
)
// Run provides a shell script interface for the text area bubble.
@ -47,3 +49,8 @@ func (o Options) Run() error {
fmt.Println(m.(model).textarea.Value())
return err
}
// BeforeReset hook. Used to unclutter style flags.
func (o Options) BeforeReset(ctx *kong.Context) error {
return style.HideFlags(ctx)
}