Add an entry point for the completion tool
This commit is contained in:
parent
f4de6d2a10
commit
005a9c7090
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"kitty/tools/cli"
|
"kitty/tools/cli"
|
||||||
"kitty/tools/cmd/at"
|
"kitty/tools/cmd/at"
|
||||||
|
"kitty/tools/completion"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -18,6 +19,8 @@ func main() {
|
|||||||
})
|
})
|
||||||
root.AddCommand(at.EntryPoint(root))
|
root.AddCommand(at.EntryPoint(root))
|
||||||
|
|
||||||
|
root.AddCommand(completion.EntryPoint(root))
|
||||||
|
|
||||||
cli.Init(root)
|
cli.Init(root)
|
||||||
if err := cli.Execute(root); err != nil {
|
if err := cli.Execute(root); err != nil {
|
||||||
cli.PrintError(err)
|
cli.PrintError(err)
|
||||||
|
|||||||
84
tools/completion/main.go
Normal file
84
tools/completion/main.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package completion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"kitty/tools/cli"
|
||||||
|
"kitty/tools/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func json_input_parser(data []byte, shell_state map[string]string) ([]string, error) {
|
||||||
|
ans := make([]string, 0, 32)
|
||||||
|
err := json.Unmarshal(data, &ans)
|
||||||
|
return ans, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func json_output_serializer(completions *Completions, shell_state map[string]string) ([]byte, error) {
|
||||||
|
return json.Marshal(completions)
|
||||||
|
}
|
||||||
|
|
||||||
|
type parser_func func(data []byte, shell_state map[string]string) ([]string, error)
|
||||||
|
type serializer_func func(completions *Completions, shell_state map[string]string) ([]byte, error)
|
||||||
|
|
||||||
|
var input_parsers = make(map[string]parser_func, 4)
|
||||||
|
var output_serializers = make(map[string]serializer_func, 4)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
input_parsers["json"] = json_input_parser
|
||||||
|
output_serializers["json"] = json_output_serializer
|
||||||
|
}
|
||||||
|
|
||||||
|
func main(args []string) error {
|
||||||
|
output_type := "json"
|
||||||
|
if len(args) > 0 {
|
||||||
|
output_type = args[0]
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
shell_state := make(map[string]string, len(args))
|
||||||
|
for _, arg := range args {
|
||||||
|
k, v, found := utils.Cut(arg, "=")
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("Invalid shell state specification: %s", arg)
|
||||||
|
}
|
||||||
|
shell_state[k] = v
|
||||||
|
}
|
||||||
|
input_parser := input_parsers[output_type]
|
||||||
|
output_serializer := output_serializers[output_type]
|
||||||
|
if input_parser == nil || output_serializer == nil {
|
||||||
|
return fmt.Errorf("Unknown output type: %s", output_type)
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
argv, err := input_parser(data, shell_state)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
completions := GetCompletions(argv)
|
||||||
|
output, err := output_serializer(completions, shell_state)
|
||||||
|
if err == nil {
|
||||||
|
os.Stdout.Write(output)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryPoint(tool_root *cobra.Command) *cobra.Command {
|
||||||
|
complete_command := cli.CreateCommand(&cobra.Command{
|
||||||
|
Use: "__complete__ output_type [shell state...]",
|
||||||
|
Short: "Generate completions for kitty commands",
|
||||||
|
Long: "Generate completion candidates for kitty commands. The command line is read from STDIN. output_type can be one of the supported shells or 'json' for JSON output.",
|
||||||
|
Hidden: true,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return main(args)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return complete_command
|
||||||
|
}
|
||||||
@ -12,29 +12,36 @@ func (self *Completions) add_group(group *MatchGroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *command) find_option(name_including_leading_dash string) *option {
|
func (self *Command) find_option(name_including_leading_dash string) *Option {
|
||||||
q := strings.TrimLeft(name_including_leading_dash, "-")
|
var q string
|
||||||
for _, opt := range self.options {
|
if strings.HasPrefix(name_including_leading_dash, "--") {
|
||||||
for _, alias := range opt.aliases {
|
q = name_including_leading_dash[2:]
|
||||||
|
} else if strings.HasPrefix(name_including_leading_dash, "-") {
|
||||||
|
q = name_including_leading_dash[len(name_including_leading_dash)-1:]
|
||||||
|
} else {
|
||||||
|
q = name_including_leading_dash
|
||||||
|
}
|
||||||
|
for _, opt := range self.Options {
|
||||||
|
for _, alias := range opt.Aliases {
|
||||||
if alias == q {
|
if alias == q {
|
||||||
return &opt
|
return opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Completions) add_options_group(options []option, word string) {
|
func (self *Completions) add_options_group(options []*Option, word string) {
|
||||||
group := MatchGroup{Title: "Options"}
|
group := MatchGroup{Title: "Options"}
|
||||||
group.Matches = make([]*Match, 0, 8)
|
group.Matches = make([]*Match, 0, 8)
|
||||||
seen_flags := make(map[string]bool)
|
seen_flags := make(map[string]bool)
|
||||||
if strings.HasPrefix(word, "--") {
|
if strings.HasPrefix(word, "--") {
|
||||||
prefix := word[2:]
|
prefix := word[2:]
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
for _, q := range opt.aliases {
|
for _, q := range opt.Aliases {
|
||||||
if len(q) > 1 && strings.HasPrefix(q, prefix) {
|
if len(q) > 1 && strings.HasPrefix(q, prefix) {
|
||||||
seen_flags[q] = true
|
seen_flags[q] = true
|
||||||
group.Matches = append(group.Matches, &Match{Word: "--" + q, Description: opt.description})
|
group.Matches = append(group.Matches, &Match{Word: "--" + q, Description: opt.Description})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,10 +55,10 @@ func (self *Completions) add_options_group(options []option, word string) {
|
|||||||
}
|
}
|
||||||
group.WordPrefix = word
|
group.WordPrefix = word
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
for _, q := range opt.aliases {
|
for _, q := range opt.Aliases {
|
||||||
if len(q) == 1 && !seen_flags[q] {
|
if len(q) == 1 && !seen_flags[q] {
|
||||||
seen_flags[q] = true
|
seen_flags[q] = true
|
||||||
group.Matches = append(group.Matches, &Match{Word: q, FullForm: "-" + q, Description: opt.description})
|
group.Matches = append(group.Matches, &Match{Word: q, FullForm: "-" + q, Description: opt.Description})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,11 +66,11 @@ func (self *Completions) add_options_group(options []option, word string) {
|
|||||||
self.add_group(&group)
|
self.add_group(&group)
|
||||||
}
|
}
|
||||||
|
|
||||||
func complete_word(word string, completions *Completions, only_args_allowed bool, expecting_arg_for *option, arg_num int) {
|
func complete_word(word string, completions *Completions, only_args_allowed bool, expecting_arg_for *Option, arg_num int) {
|
||||||
cmd := Completions.current_cmd
|
cmd := completions.current_cmd
|
||||||
if expecting_arg_for != nil {
|
if expecting_arg_for != nil {
|
||||||
if expecting_arg_for.completion_for_arg != nil {
|
if expecting_arg_for.Completion_for_arg != nil {
|
||||||
expecting_arg_for.completion_for_arg(completions, word)
|
expecting_arg_for.Completion_for_arg(completions, word)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -72,27 +79,27 @@ func complete_word(word string, completions *Completions, only_args_allowed bool
|
|||||||
idx := strings.Index(word, "=")
|
idx := strings.Index(word, "=")
|
||||||
option := cmd.find_option(word[:idx])
|
option := cmd.find_option(word[:idx])
|
||||||
if option != nil {
|
if option != nil {
|
||||||
if option.completion_for_arg != nil {
|
if option.Completion_for_arg != nil {
|
||||||
completions.WordPrefix = word[:idx+1]
|
completions.WordPrefix = word[:idx+1]
|
||||||
option.completion_for_arg(completions, word[idx+1:])
|
option.Completion_for_arg(completions, word[idx+1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
completions.add_options_group(cmd.options, word)
|
completions.add_options_group(cmd.Options, word)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if arg_num == 1 && len(cmd.subcommands) > 0 {
|
if arg_num == 1 && len(cmd.Subcommands) > 0 {
|
||||||
for _, sc := range cmd.subcommands {
|
for _, sc := range cmd.Subcommands {
|
||||||
if strings.HasPrefix(sc.name, word) {
|
if strings.HasPrefix(sc.Name, word) {
|
||||||
title := cmd.subcommands_title
|
title := cmd.Subcommands_title
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = "Sub-commands"
|
title = "Sub-commands"
|
||||||
}
|
}
|
||||||
group := MatchGroup{Title: title}
|
group := MatchGroup{Title: title}
|
||||||
group.Matches = make([]*Match, 0, len(cmd.subcommands))
|
group.Matches = make([]*Match, 0, len(cmd.Subcommands))
|
||||||
if strings.HasPrefix(sc, word) {
|
if strings.HasPrefix(sc.Name, word) {
|
||||||
group.Matches = append(group.Matches, &Match{Word: sc.name, Description: sc.description})
|
group.Matches = append(group.Matches, &Match{Word: sc.Name, Description: sc.Description})
|
||||||
}
|
}
|
||||||
completions.add_group(&group)
|
completions.add_group(&group)
|
||||||
}
|
}
|
||||||
@ -100,20 +107,29 @@ func complete_word(word string, completions *Completions, only_args_allowed bool
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.completion_for_arg != nil {
|
if cmd.Completion_for_arg != nil {
|
||||||
cmd.completion_for_arg(completions, word)
|
cmd.Completion_for_arg(completions, word)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse_args(cmd *command, words []string, completions *Completions) {
|
func (self *Command) find_subcommand(name string) *Command {
|
||||||
|
for _, sc := range self.Subcommands {
|
||||||
|
if sc.Name == name {
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *Command) parse_args(words []string, completions *Completions) {
|
||||||
completions.current_cmd = cmd
|
completions.current_cmd = cmd
|
||||||
if len(words) == 0 {
|
if len(words) == 0 {
|
||||||
complete_word("", completions, false, nil, 0)
|
complete_word("", completions, false, nil, 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var expecting_arg_for *option
|
var expecting_arg_for *Option
|
||||||
only_args_allowed := false
|
only_args_allowed := false
|
||||||
arg_num := 0
|
arg_num := 0
|
||||||
|
|
||||||
@ -135,14 +151,17 @@ func parse_args(cmd *command, words []string, completions *Completions) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !only_args_allowed && strings.HasPrefix(word, "-") {
|
if !only_args_allowed && strings.HasPrefix(word, "-") {
|
||||||
// TODO:
|
idx := strings.Index(word, "=")
|
||||||
// handle single letter multiple options -abcd
|
if idx > -1 {
|
||||||
// handle standalone --long-opt
|
continue
|
||||||
// handle long opt ends with =
|
}
|
||||||
// handle long opt containing =
|
option := cmd.find_option(word[:idx])
|
||||||
|
if option != nil {
|
||||||
|
expecting_arg_for = option
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(cmd.subcommands) > 0 && arg_num == 1 {
|
if len(cmd.Subcommands) > 0 && arg_num == 1 {
|
||||||
sc := cmd.find_subcommand(word)
|
sc := cmd.find_subcommand(word)
|
||||||
if sc == nil {
|
if sc == nil {
|
||||||
only_args_allowed = true
|
only_args_allowed = true
|
||||||
@ -152,7 +171,7 @@ func parse_args(cmd *command, words []string, completions *Completions) {
|
|||||||
cmd = sc
|
cmd = sc
|
||||||
arg_num = 0
|
arg_num = 0
|
||||||
only_args_allowed = false
|
only_args_allowed = false
|
||||||
} else if cmd.stop_processing_at_arg > 0 && arg_num >= cmd.stop_processing_at_arg {
|
} else if cmd.Stop_processing_at_arg > 0 && arg_num >= cmd.Stop_processing_at_arg {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
only_args_allowed = true
|
only_args_allowed = true
|
||||||
|
|||||||
@ -3,44 +3,62 @@
|
|||||||
package completion
|
package completion
|
||||||
|
|
||||||
type Match struct {
|
type Match struct {
|
||||||
Word string
|
Word string `json:"word,omitempty"`
|
||||||
FullForm string
|
FullForm string `json:"full_form,omitempty"`
|
||||||
Description string
|
Description string `json:"description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MatchGroup struct {
|
type MatchGroup struct {
|
||||||
Title string
|
Title string `json:"title,omitempty"`
|
||||||
NoTrailingSpace, IsFiles bool
|
NoTrailingSpace bool `json:"no_trailing_space,omitempty"`
|
||||||
Matches []*Match
|
IsFiles bool `json:"is_files,omitempty"`
|
||||||
WordPrefix string
|
Matches []*Match `json:"matches,omitempty"`
|
||||||
|
WordPrefix string `json:"word_prefix,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Completions struct {
|
type Completions struct {
|
||||||
Groups []*MatchGroup
|
Groups []*MatchGroup `json:"groups,omitempty"`
|
||||||
WordPrefix string
|
WordPrefix string `json:"word_prefix,omitempty"`
|
||||||
|
|
||||||
current_cmd *command
|
current_cmd *Command
|
||||||
}
|
}
|
||||||
|
|
||||||
type completion_func func(completions *Completions, partial_word string)
|
type completion_func func(completions *Completions, partial_word string)
|
||||||
|
|
||||||
type option struct {
|
type Option struct {
|
||||||
name string
|
Name string
|
||||||
aliases []string
|
Aliases []string
|
||||||
description string
|
Description string
|
||||||
has_following_arg bool
|
Has_following_arg bool
|
||||||
completion_for_arg completion_func
|
Completion_for_arg completion_func
|
||||||
}
|
}
|
||||||
|
|
||||||
type command struct {
|
type Command struct {
|
||||||
name string
|
Name string
|
||||||
description string
|
Description string
|
||||||
|
|
||||||
options []option
|
// List of options for this command
|
||||||
|
Options []*Option
|
||||||
|
|
||||||
subcommands []command
|
// List of subcommands
|
||||||
subcommands_title string
|
Subcommands []*Command
|
||||||
|
// Optional title used as a header when displaying the list of matching sub-commands for a completion
|
||||||
|
Subcommands_title string
|
||||||
|
|
||||||
completion_for_arg completion_func
|
Completion_for_arg completion_func
|
||||||
stop_processing_at_arg int
|
Stop_processing_at_arg int
|
||||||
|
}
|
||||||
|
|
||||||
|
var Root = Command{Options: make([]*Option, 0), Subcommands: make([]*Command, 0, 32)}
|
||||||
|
|
||||||
|
func GetCompletions(argv []string) *Completions {
|
||||||
|
ans := Completions{Groups: make([]*MatchGroup, 0, 4)}
|
||||||
|
if len(argv) > 0 {
|
||||||
|
exe := argv[0]
|
||||||
|
cmd := Root.find_subcommand(exe)
|
||||||
|
if cmd != nil {
|
||||||
|
cmd.parse_args(argv[1:], &ans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ans
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user