Files
gitea-tea/internal/app/completion.go
T
bircni 29f62da006 refactor: load settings once per run and reuse shared helpers
The application caches the settings snapshot and reloads it only after
an update through the store, so output and prompt helpers no longer
read and parse config.yml and hosts.yml several times per command.
canPrompt and chooser replace the inline prompt-policy checks.

Checks are tallied by one services.Tally, terminal.Plural replaces the
two app copies, refKind.article/noun replace the comment group's own
words, list commands use servicePage, and web URLs are built by
repositoryURL and webURL in browser.go.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 10:57:39 +02:00

604 lines
27 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package app
import (
"context"
"errors"
"maps"
"slices"
"strconv"
"strings"
"time"
"gitea.dev/tea/internal/auth"
"gitea.dev/tea/internal/services"
"gitea.dev/tea/internal/settings"
"gitea.dev/tea/internal/terminal"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const (
// completionLimit caps how many values an instance completer fetches and offers.
completionLimit = 50
// completionTimeout bounds the Git and API calls behind one completion.
completionTimeout = 2 * time.Second
// enumAnnotation holds the values of an enum flag, which validation and completion share.
enumAnnotation = "tea.enum"
)
// enumFlag registers a string flag that accepts one of values, case-insensitively, completes them,
// and lists them in its usage, as in "Filter by state {open|closed|all}".
func enumFlag(cmd *cobra.Command, p *string, name, shorthand, value, usage string, values ...string) {
cmd.Flags().StringVarP(p, name, shorthand, value, usage+" {"+strings.Join(values, "|")+"}")
enumValues(cmd, name, values...)
}
// enumValues restricts the values of the existing flag name, so that validation and completion share
// one list. A slice flag accepts and completes comma-separated values.
func enumValues(cmd *cobra.Command, name string, values ...string) {
_ = cmd.Flags().SetAnnotation(name, enumAnnotation, values)
complete := completeValues(values...)
if _, ok := cmd.Flags().Lookup(name).Value.(pflag.SliceValue); ok {
complete = commaSeparated(complete)
}
_ = cmd.RegisterFlagCompletionFunc(name, complete)
}
// validateEnums rejects enum flag values outside their lists and stores the listed spelling of each
// value, so that values match case-insensitively.
func validateEnums(cmd *cobra.Command) error {
var err error
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
allowed := flag.Annotations[enumAnnotation]
if err != nil || allowed == nil || !flag.Changed {
return
}
slice, isSlice := flag.Value.(pflag.SliceValue)
given := []string{flag.Value.String()}
if isSlice {
given = slice.GetSlice()
}
for i, value := range given {
at := slices.IndexFunc(allowed, func(v string) bool { return strings.EqualFold(v, strings.TrimSpace(value)) })
if at < 0 {
err = oneOf(flag.Name, value, allowed...)
return
}
given[i] = allowed[at]
}
if isSlice {
err = slice.Replace(given)
} else {
err = flag.Value.Set(given[0])
}
})
return err
}
// completeValues completes the given values.
func completeValues(values ...string) cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
return matching(values, toComplete), cobra.ShellCompDirectiveNoFileComp
}
}
// matching keeps the candidates whose value starts with prefix, at most completionLimit of them.
func matching(candidates []cobra.Completion, prefix string) []cobra.Completion {
result := []cobra.Completion{}
for _, candidate := range candidates {
if value, _, _ := strings.Cut(candidate, "\t"); strings.HasPrefix(value, prefix) {
result = append(result, candidate)
if len(result) == completionLimit {
break
}
}
}
return result
}
// commaSeparated completes the last entry of a comma-separated value with complete, without offering
// entries that are already given, and keeps the cursor after it so that another comma can follow.
func commaSeparated(complete cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
done := toComplete[:strings.LastIndexByte(toComplete, ',')+1]
given := strings.Split(done, ",")
values, directive := complete(cmd, args, toComplete[len(done):])
result := make([]cobra.Completion, 0, len(values))
for _, value := range values {
if name, _, _ := strings.Cut(value, "\t"); !slices.Contains(given, name) {
result = append(result, done+value)
}
}
return result, directive | cobra.ShellCompDirectiveNoSpace
}
}
// mergeCompletions offers the candidates of every completer in order, each value once.
func mergeCompletions(completers ...cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
result, seen := []cobra.Completion{}, map[string]bool{}
for _, complete := range completers {
values, _ := complete(cmd, args, toComplete)
for _, value := range values {
if name, _, _ := strings.Cut(value, "\t"); !seen[name] {
seen[name] = true
result = append(result, value)
}
}
}
return matching(result, toComplete), cobra.ShellCompDirectiveNoFileComp
}
}
// completeJSONFields completes the comma-separated fields that --json can select for the command.
var completeJSONFields = commaSeparated(func(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if cmd.Annotations["tea.text"] != "" || cmd.Annotations["tea.group"] != "" {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return matching(commandOutputFields(cmd), toComplete), cobra.ShellCompDirectiveNoFileComp
})
// completeJSONValue completes the word after a bare --json as its fields. Without this, cobra completes
// that word as a positional argument, because the value of --json is optional.
func completeJSONValue(args []string) []string {
if n := len(args); n >= 2 && args[n-2] == "--json" && !strings.HasPrefix(args[n-1], "-") {
return slices.Concat(args[:n-2], []string{"--json=" + args[n-1]})
}
return args
}
// describe turns items into completion candidates, each a value with a one-line description. Server
// text cannot drive the terminal: values with control characters are skipped, descriptions sanitized.
func describe[T any](items []T, candidate func(T) (value, description string)) []cobra.Completion {
result := make([]cobra.Completion, 0, len(items))
for _, item := range items {
value, description := candidate(item)
if value == "" || terminal.SafeLine(value) != value {
continue
}
if description = strings.Join(strings.Fields(terminal.SafeLine(description)), " "); description != "" {
value = cobra.CompletionWithDesc(value, description)
}
result = append(result, value)
}
return result
}
// completionContext bounds the work behind one completion.
func completionContext(cmd *cobra.Command) (context.Context, context.CancelFunc) {
return context.WithTimeout(cmd.Context(), completionTimeout)
}
// completeHosts completes the configured instances with their accounts.
func (a *application) completeHosts(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
snapshot, err := a.settings()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
hosts := slices.Sorted(maps.Keys(snapshot.Hosts.Hosts))
return matching(describe(hosts, func(host string) (string, string) {
return host, strings.Join(slices.Sorted(maps.Keys(snapshot.Hosts.Hosts[host].Users)), ", ")
}), toComplete), cobra.ShellCompDirectiveNoFileComp
}
// completeAccounts completes the accounts of the selected instance, or of every instance when
// none is selected.
func (a *application) completeAccounts(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
ctx, cancel := completionContext(cmd)
defer cancel()
snapshot, err := a.settings()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
hosts := snapshot.Hosts.Hosts
if t, err := a.resolveTarget(ctx, false, false); err == nil {
hosts = map[string]settings.Host{t.Host: hosts[t.Host]}
}
accounts := []cobra.Completion{}
for _, host := range slices.Sorted(maps.Keys(hosts)) {
accounts = append(accounts, describe(slices.Sorted(maps.Keys(hosts[host].Users)), func(user string) (string, string) {
if hosts[host].User == user {
return user, host + " (active)"
}
return user, host
})...)
}
return matching(accounts, toComplete), cobra.ShellCompDirectiveNoFileComp
}
// gitRemotes lists the remotes of the working directory with their URLs, in configuration order.
func (a *application) gitRemotes(ctx context.Context) [][2]string {
out, err := a.gitProbe().Output(ctx, a.options.cwd, false, "config", "--get-regexp", `^remote\..*\.url$`)
if err != nil {
return nil
}
var remotes [][2]string
for line := range strings.Lines(out) {
if key, url, ok := strings.Cut(strings.TrimSpace(line), " "); ok {
remotes = append(remotes, [2]string{strings.TrimSuffix(strings.TrimPrefix(key, "remote."), ".url"), url})
}
}
return remotes
}
// completeRemotes completes the Git remotes of the working directory.
func (a *application) completeRemotes(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
ctx, cancel := completionContext(cmd)
defer cancel()
return matching(describe(a.gitRemotes(ctx), func(remote [2]string) (string, string) { return remote[0], remote[1] }), toComplete), cobra.ShellCompDirectiveNoFileComp
}
// completeLocalBranches completes the branches of the local checkout.
func (a *application) completeLocalBranches(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
ctx, cancel := completionContext(cmd)
defer cancel()
out, err := a.gitProbe().Output(ctx, a.options.cwd, false, "for-each-ref", "--format=%(refname:short)", "refs/heads")
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return matching(describe(strings.Fields(out), func(branch string) (string, string) { return branch, "local branch" }), toComplete), cobra.ShellCompDirectiveNoFileComp
}
// completeAliases completes the configured alias names with their expansions.
func (a *application) completeAliases(_ *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
snapshot, err := a.settings()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
aliases := snapshot.Preferences.Aliases
return matching(describe(slices.Sorted(maps.Keys(aliases)), func(name string) (string, string) { return name, aliases[name] }), toComplete), cobra.ShellCompDirectiveNoFileComp
}
// completeConfig completes a setting name, then its value: the fixed values, the configured instances
// for default_host, or the Git remotes for remote.
func (a *application) completeConfig(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
switch {
case len(args) == 0:
return matching(configKeys, toComplete), cobra.ShellCompDirectiveNoFileComp
case len(args) > 1:
return nil, cobra.ShellCompDirectiveNoFileComp
case args[0] == "default_host":
return a.completeHosts(cmd, args, toComplete)
case args[0] == "remote":
return a.completeRemotes(cmd, args, toComplete)
}
return matching(configValues[args[0]], toComplete), cobra.ShellCompDirectiveNoFileComp
}
// apiCompletion completes the values that list fetches from the selected instance; requireRepo
// selects whether list needs a repository. It never prompts, refreshes an OAuth token or retries,
// and gives up after completionTimeout: failures complete nothing rather than file names. Values
// already given as arguments are not offered again.
func (a *application) apiCompletion(requireRepo bool, list func(ctx context.Context, t target, toComplete string) ([]cobra.Completion, error)) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
ctx, cancel := completionContext(cmd)
defer cancel()
t, err := a.completionTarget(ctx, requireRepo)
var values []cobra.Completion
if err == nil {
values, err = list(ctx, t, toComplete)
}
if err != nil {
cobra.CompDebugln("tea: "+err.Error(), false)
return nil, cobra.ShellCompDirectiveNoFileComp
}
values = slices.DeleteFunc(values, func(value cobra.Completion) bool {
name, _, _ := strings.Cut(value, "\t")
return slices.Contains(args, name)
})
return matching(values, toComplete), cobra.ShellCompDirectiveNoFileComp
}
}
// completionTarget resolves an API completion's target like resolve, but never prompts or rotates
// credentials: an expired OAuth token or an encrypted SSH key completes nothing.
func (a *application) completionTarget(ctx context.Context, requireRepo bool) (target, error) {
t, err := a.resolveTarget(ctx, requireRepo, false)
if err != nil {
return t, err
}
var signer *auth.HTTPSign
token := a.deps.Getenv("GITEA_TOKEN")
if token != "" {
if instance, err := settings.NormalizeHost(a.tokenInstance()); err != nil || instance != t.Host {
return t, errors.New("GITEA_TOKEN belongs to another instance")
}
} else {
account, exists := t.host.Users[t.User]
switch {
case !exists:
return t, errors.New("no account selected")
case account.Tokenless():
if signer, err = auth.SignerForAccount(account, nil); err != nil {
return t, err
}
default:
credential, err := a.deps.Credentials.Load(t.Host, t.User, account.CredentialStore)
if err != nil {
return t, err
}
if !credential.ExpiresAt.IsZero() && !a.deps.Now().Before(credential.ExpiresAt) {
return t, errors.New("access token expired")
}
token = credential.AccessToken
}
}
t.client = a.deps.Client(t.Host, token, t.host.Insecure)
if signer != nil {
t.client.Signer = signer
}
t.client.Retries, t.client.Sudo = 0, a.options.sudo
return t, nil
}
// completeIssues completes issue numbers in state (open, closed or all) with their titles.
func (a *application) completeIssues(state string) cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, _, err := services.Issues{Client: t.client, Repository: t.Repo}.List(ctx, services.ListOptions{State: state, Kind: "issues", Limit: completionLimit, Page: 1})
return describe(items, func(item services.Issue) (string, string) { return strconv.FormatInt(item.Number, 10), item.Title }), err
})
}
// completePulls completes pull request numbers in state (open, closed or all) with their titles.
func (a *application) completePulls(state string) cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, _, err := services.Pulls{Client: t.client, Repository: t.Repo}.List(ctx, services.PullListOptions{State: state, Limit: completionLimit, Page: 1})
return describe(items, func(item services.Pull) (string, string) { return strconv.FormatInt(item.Number, 10), item.Title }), err
})
}
// completeLabels completes comma-separated label names of the repository and, when its owner is an
// organization, of the organization.
func (a *application) completeLabels() cobra.CompletionFunc {
return commaSeparated(a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
labels, err := services.Labels{Client: t.client, Repository: t.Repo}.List(ctx, completionLimit, 1)
if err != nil {
return nil, err
}
owner, _, _ := strings.Cut(t.Repo, "/")
if orgLabels, err := (services.Labels{Client: t.client, Organization: owner}).List(ctx, completionLimit, 1); err == nil {
labels = append(labels, orgLabels...)
}
return describe(labels, func(label services.Label) (string, string) { return label.Name, label.Description }), nil
}))
}
// completeMilestones completes the titles of open milestones.
func (a *application) completeMilestones() cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Milestones{Client: t.client, Repository: t.Repo}.List(ctx, "open", completionLimit, 1)
return describe(items, func(item services.Milestone) (string, string) { return item.Title, item.Description }), err
})
}
// completeUsers completes comma-separated usernames that can be assigned in the repository.
func (a *application) completeUsers() cobra.CompletionFunc {
return commaSeparated(a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
users, err := services.Repositories{Client: t.client}.Assignees(ctx, t.Repo)
return describe(users, func(item services.Assignee) (string, string) { return item.Username, item.FullName }), err
}))
}
// completeBranches completes the local branches, then the repository's branches.
func (a *application) completeBranches() cobra.CompletionFunc {
return mergeCompletions(a.completeLocalBranches, a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Branches{Client: t.client}.List(ctx, t.Repo, completionLimit, 1)
return describe(items, func(item services.Branch) (string, string) {
if item.Protected {
return item.Name, "protected"
}
return item.Name, ""
}), err
}))
}
// completeTags completes the repository's tags.
func (a *application) completeTags() cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
tags, err := services.Repositories{Client: t.client}.Tags(ctx, t.Repo, completionLimit, 1)
return describe(tags, func(name string) (string, string) { return name, "" }), err
})
}
// completeReleases completes release tags with their titles.
func (a *application) completeReleases() cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Releases{Client: t.client, Repository: t.Repo}.List(ctx, completionLimit, 1)
return describe(items, func(item services.Release) (string, string) { return item.Tag, item.Title }), err
})
}
// completeOrgs completes the organizations of the account.
func (a *application) completeOrgs() cobra.CompletionFunc {
return a.apiCompletion(false, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Organizations{Client: t.client}.List(ctx, true, "", completionLimit, 1)
return describe(items.Items, func(item services.Organization) (string, string) { return item.Name, item.FullName }), err
})
}
// completeTeams completes teams as ORG/TEAM: first the account's organizations, then the teams of the
// organization before the slash.
func (a *application) completeTeams() cobra.CompletionFunc {
orgs := a.completeOrgs()
teams := a.apiCompletion(false, func(ctx context.Context, t target, toComplete string) ([]cobra.Completion, error) {
org, _, _ := strings.Cut(toComplete, "/")
items, err := services.Teams{Client: t.client}.List(ctx, org, completionLimit, 1)
return describe(items.Items, func(item services.Team) (string, string) {
return org + "/" + item.Name, strings.TrimSpace(item.Description + " (ID " + strconv.FormatInt(item.ID, 10) + ")")
}), err
})
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if strings.Contains(toComplete, "/") {
return teams(cmd, args, toComplete)
}
values, directive := orgs(cmd, args, toComplete)
for i, value := range values {
name, description, _ := strings.Cut(value, "\t")
values[i] = cobra.CompletionWithDesc(name+"/", description)
}
return values, directive | cobra.ShellCompDirectiveNoSpace
}
}
// completeRepos completes repositories as OWNER/NAME: those of the Git remotes on configured
// instances, then those of the owner before the slash, or else the account's repositories.
func (a *application) completeRepos() cobra.CompletionFunc {
remotes := func(cmd *cobra.Command, _ []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
ctx, cancel := completionContext(cmd)
defer cancel()
snapshot, err := a.settings()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
values := []cobra.Completion{}
for _, remote := range a.gitRemotes(ctx) {
if host, repo, err := settings.RepositoryURL(remote[1], snapshot.Hosts.Hosts); err == nil && snapshot.Hosts.Hosts[host].Users != nil {
values = append(values, cobra.CompletionWithDesc(repo, "remote "+remote[0]))
}
}
return matching(values, toComplete), cobra.ShellCompDirectiveNoFileComp
}
return mergeCompletions(remotes, a.apiCompletion(false, func(ctx context.Context, t target, toComplete string) ([]cobra.Completion, error) {
owner, _, found := strings.Cut(toComplete, "/")
if !found {
owner = ""
}
items, _, err := services.Repositories{Client: t.client}.List(ctx, services.RepositoryListOptions{Owner: owner, Limit: completionLimit, Page: 1})
return describe(items, func(item services.Repository) (string, string) { return item.FullName, item.Description }), err
}))
}
// completeWorkflows completes workflow files with their names.
func (a *application) completeWorkflows() cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Actions{Client: t.client, Repository: t.Repo}.Workflows(ctx)
return describe(items, func(item services.Workflow) (string, string) { return item.ID, item.Name }), err
})
}
// completeRuns completes the IDs of recent workflow runs with their status and title.
func (a *application) completeRuns() cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.Actions{Client: t.client, Repository: t.Repo}.Runs(ctx, services.RunListOptions{Limit: completionLimit, Page: 1})
return describe(items, func(item services.Run) (string, string) {
return strconv.FormatInt(item.ID, 10), strings.Join(slices.DeleteFunc([]string{item.Status, item.Conclusion, item.Title}, func(s string) bool { return s == "" }), " · ")
}), err
})
}
// completeSecrets completes Actions secret names in the scope that scope selects; a scope without an
// organization or account selects the repository.
func (a *application) completeSecrets(scope func() services.ActionScope) cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.ActionSecrets{Client: t.client, Scope: actionScope(scope, t)}.List(ctx, completionLimit, 1)
return describe(items, func(item services.ActionSecret) (string, string) { return item.Name, item.Description }), err
})
}
// completeVariables completes Actions variable names in the scope that scope selects; a scope without
// an organization or account selects the repository.
func (a *application) completeVariables(scope func() services.ActionScope) cobra.CompletionFunc {
return a.apiCompletion(true, func(ctx context.Context, t target, _ string) ([]cobra.Completion, error) {
items, err := services.ActionVariables{Client: t.client, Scope: actionScope(scope, t)}.List(ctx, completionLimit, 1)
return describe(items, func(item services.ActionVariable) (string, string) { return item.Name, item.Description }), err
})
}
// actionScope applies the target's repository to a scope without organization or account.
func actionScope(scope func() services.ActionScope, t target) services.ActionScope {
selected := scope()
if selected.Organization == "" && !selected.Account {
selected.Repository = t.Repo
}
return selected
}
// pathFlags take local file paths, like every flag whose name ends in -file.
var pathFlags = []string{"asset", "attachment", "file", "input", "ssh-key"}
// completeDefaults completes paths only where a command takes them: path flags and flags marked
// with MarkFlagFilename. Everything else without a completer completes nothing, by the root's
// default directive, rather than file names.
func completeDefaults(cmd *cobra.Command) {
for _, flags := range []*pflag.FlagSet{cmd.Flags(), cmd.PersistentFlags()} {
flags.VisitAll(func(flag *pflag.Flag) {
if _, registered := cmd.GetFlagCompletionFunc(flag.Name); registered {
return
}
switch {
case flag.Name == "dir":
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completeDirectories)
case strings.HasSuffix(flag.Name, "-file") || slices.Contains(pathFlags, flag.Name) || flag.Annotations[cobra.BashCompFilenameExt] != nil:
_ = cmd.RegisterFlagCompletionFunc(flag.Name, completeFiles)
}
})
}
for _, child := range cmd.Commands() {
completeDefaults(child)
}
}
// completeFiles leaves the completion of file paths to the shell.
func completeFiles(*cobra.Command, []string, string) ([]cobra.Completion, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
}
// completeDirectories leaves the completion of directory paths to the shell.
func completeDirectories(*cobra.Command, []string, string) ([]cobra.Completion, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveFilterDirs
}
// firstArg completes only the first argument with complete.
func firstArg(complete cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return complete(cmd, args, toComplete)
}
}
// pathArgs completes the arguments before position n with complete, and paths with path from
// position n on: every later argument when variadic, else only the one at n.
func pathArgs(n int, variadic bool, path, complete cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) {
switch {
case len(args) < n:
return complete(cmd, args, toComplete)
case len(args) == n || variadic:
return path(cmd, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
// completionInstallHelp explains how to load the bash script with macOS's bash 3.2, which cannot
// source <(tea completion bash); cobra's help covers the other shells.
func completionInstallHelp(root *cobra.Command) {
completion, _, err := root.Find([]string{"completion"})
if err != nil || completion == root {
return
}
completion.Short = "Generate shell completion scripts"
examples := map[string]string{
"bash": `$ eval "$(tea completion bash)"`,
"zsh": `$ source <(tea completion zsh)`,
"fish": `$ tea completion fish > ~/.config/fish/completions/tea.fish`,
"powershell": `$ tea completion powershell | Out-String | Invoke-Expression`,
}
for _, shell := range completion.Commands() {
shell.Example = examples[shell.Name()]
if flag := shell.Flags().Lookup("no-descriptions"); flag != nil {
flag.Usage = "Disable completion descriptions"
}
}
bash, _, _ := completion.Find([]string{"bash"})
bash.Long = `Generate the completion script for bash. It needs the bash-completion package.
To load completions in every new shell, add the eval line below to ~/.bashrc (on macOS,
~/.bash_profile); bash 3.2, the default on macOS, cannot source <(tea completion bash).`
}