2026-11-03 03:47:23 +00:00
|
|
|
package commandkit
|
|
|
|
|
|
2026-11-03 03:50:12 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-11-03 03:47:23 +00:00
|
|
|
|
|
|
|
|
// CompletionChoice is a literal suggested value and its optional description.
|
|
|
|
|
type CompletionChoice struct {
|
2026-11-03 04:09:27 +00:00
|
|
|
// Value is inserted as data; it is not interpreted as shell source.
|
|
|
|
|
Value string
|
|
|
|
|
// Description is optional context displayed by shells that support it.
|
2026-11-03 03:47:23 +00:00
|
|
|
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
|
2026-11-03 13:12:22 +00:00
|
|
|
choices *completionChoicesData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep the public metadata structs comparable without exposing mutable choices.
|
|
|
|
|
type completionChoicesData struct {
|
|
|
|
|
values []CompletionChoice
|
2026-11-03 03:47:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type completionHintKind uint8
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
completionNone completionHintKind = iota
|
|
|
|
|
completionChoices
|
|
|
|
|
completionFiles
|
|
|
|
|
completionDirectories
|
|
|
|
|
)
|
|
|
|
|
|
2026-11-03 04:09:27 +00:00
|
|
|
// 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.
|
2026-11-03 03:47:23 +00:00
|
|
|
func CompleteChoices(choices ...CompletionChoice) CompletionHint {
|
2026-11-03 13:12:22 +00:00
|
|
|
hint := CompletionHint{kind: completionChoices}
|
|
|
|
|
if len(choices) != 0 {
|
|
|
|
|
hint.choices = &completionChoicesData{values: append([]CompletionChoice(nil), choices...)}
|
|
|
|
|
}
|
|
|
|
|
return hint
|
2026-11-03 03:47:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-11-03 13:12:22 +00:00
|
|
|
if hint.choices == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
for _, choice := range hint.choices.values {
|
2026-11-03 03:47:23 +00:00
|
|
|
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
|
|
|
|
|
}
|