Files
commandkit/completion.go

120 lines
4.0 KiB
Go

package commandkit
import (
"fmt"
"io"
"strings"
"unicode"
internalcobra "gitea.maximumdirect.net/eric/CommandKit/internal/cobra"
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
)
// Completion declares opt-in shell script generation for the executable
// basename used to invoke the application. Install it at the application root.
// Generated shell identifiers support letters, digits, underscores, dots,
// hyphens, and colons; a leading hyphen is not supported.
func Completion(executable string) Command {
children := make([]Command, 0, 3)
for _, shell := range []string{"bash", "zsh", "fish"} {
shell := shell
children = append(children, NewCommand(CommandSpec[struct{}]{
Name: shell, Summary: "Generate " + shell + " completion script.",
Run: func(invocation Invocation, _ struct{}) Outcome {
_ = invocation.Reporter().RenderOutput(func(writer io.Writer) error {
return internalcobra.CompletionScript(writer, executable, shell)
})
return Success()
},
}))
}
command := RootGroup("completion", "Generate shell completion scripts.", children...)
command.record.completion = true
if !validCompletionExecutable(executable) {
return WithError(command, fmt.Errorf("unsupported completion executable basename %q", executable))
}
return command
}
// Cobra emits the basename into shell identifiers and unquoted command words.
func validCompletionExecutable(name string) bool {
if name == "" || name == "." || name == ".." || strings.HasPrefix(name, "-") {
return false
}
for _, ch := range name {
if !(unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '-' || ch == ':' || ch == '.') {
return false
}
}
return true
}
// CompletionChoice is a literal suggested value and its optional description.
type CompletionChoice struct {
// Value is inserted as data; it is not interpreted as shell source.
Value string
// Description is optional context displayed by shells that support it.
Description string
}
// CompletionHint selects suggestions for a flag or positional value. Its zero
// value requests no value suggestions; hints do not constrain accepted input.
type CompletionHint struct {
kind completionHintKind
choices *completionChoicesData
}
// Keep the public metadata structs comparable without exposing mutable choices.
type completionChoicesData struct {
values []CompletionChoice
}
type completionHintKind uint8
const (
completionNone completionHintKind = iota
completionChoices
completionFiles
completionDirectories
)
// CompleteChoices suggests literal values matching the current value prefix.
// The supplied slice is copied. An empty list produces no suggestions. Empty
// values and values containing a newline, tab, or NUL cannot be presented as
// suggestions, but remain valid input if the command's converter accepts them.
func CompleteChoices(choices ...CompletionChoice) CompletionHint {
hint := CompletionHint{kind: completionChoices}
if len(choices) != 0 {
hint.choices = &completionChoicesData{values: append([]CompletionChoice(nil), choices...)}
}
return hint
}
// CompleteFiles requests shell filename completion.
func CompleteFiles() CompletionHint { return CompletionHint{kind: completionFiles} }
// CompleteDirectories requests shell directory completion.
func CompleteDirectories() CompletionHint { return CompletionHint{kind: completionDirectories} }
// NoCompletion requests no value suggestions.
func NoCompletion() CompletionHint { return CompletionHint{} }
func bridgeCompletion(hint CompletionHint) commandmeta.CompletionHint {
result := commandmeta.CompletionHint{}
switch hint.kind {
case completionChoices:
result.Kind = commandmeta.CompletionChoices
if hint.choices == nil {
break
}
for _, choice := range hint.choices.values {
result.Choices = append(result.Choices, commandmeta.CompletionChoice{Value: choice.Value, Description: choice.Description})
}
case completionFiles:
result.Kind = commandmeta.CompletionFiles
case completionDirectories:
result.Kind = commandmeta.CompletionDirectories
}
return result
}