diff --git a/gen-go-code.py b/gen-go-code.py index d439c834e..316299964 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -13,14 +13,13 @@ import kitty.constants as kc from kittens.tui.operations import Mode from kitty.cli import ( CompletionSpec, GoOption, go_options_for_seq, parse_option_spec, - serialize_as_go_string + serialize_as_go_string, ) from kitty.key_encoding import config_mod_map -from kitty.key_names import ( - character_key_name_aliases, functional_key_name_aliases -) +from kitty.key_names import character_key_name_aliases, functional_key_name_aliases from kitty.options.types import Options from kitty.rc.base import RemoteCommand, all_command_names, command_for_name +from kitty.remote_control import global_options_spec from kitty.rgb import color_names changed: List[str] = [] @@ -59,7 +58,9 @@ def generate_completion_for_rc(name: str) -> None: def generate_kittens_completion() -> None: - from kittens.runner import all_kitten_names, get_kitten_cli_docs, get_kitten_wrapper_of + from kittens.runner import ( + all_kitten_names, get_kitten_cli_docs, get_kitten_wrapper_of, + ) for kitten in sorted(all_kitten_names()): kn = 'kitten_' + kitten print(f'{kn} := plus_kitten.add_command("{kitten}", "Kittens")') @@ -84,7 +85,7 @@ def completion_for_launch_wrappers(*names: str) -> None: opts = tuple(go_options_for_seq(parse_option_spec(options_spec())[0])) allowed = clone_safe_opts() for o in opts: - if o.dest in allowed: + if o.obj_dict['name'] in allowed: for name in names: print(o.as_completion_option(name)) @@ -184,36 +185,19 @@ class JSONField: return self.struct_field_name + ' ' + go_field_type(self.field_type) + f'`json:"{self.field},omitempty"`' -def render_alias_map(alias_map: Dict[str, Tuple[str, ...]]) -> str: - if not alias_map: - return '' - amap = 'switch name {\n' - for name, aliases in alias_map.items(): - for alias in aliases: - amap += f'\ncase "{alias}":\nname = "{name}"\n' - amap += '}' - return amap - - def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> str: template = '\n' + template[len('//go:build exclude'):] NO_RESPONSE_BASE = 'false' af: List[str] = [] a = af.append - alias_map = {} od: List[str] = [] - ov: List[str] = [] option_map: Dict[str, GoOption] = {} for o in rc_command_options(name): - field_dest = o.go_var_name.rstrip('_') - option_map[field_dest] = o - if o.aliases: - alias_map[o.long] = tuple(o.aliases) - a(o.to_flag_definition()) - if o.dest in ('no_response', 'response_timeout'): + option_map[o.go_var_name] = o + a(o.as_option('ans')) + if o.go_var_name in ('NoResponse', 'ResponseTimeout'): continue - od.append(f'{o.go_var_name} {o.go_type}') - ov.append(o.set_flag_value(f'options_{name}')) + od.append(o.struct_declaration()) jd: List[str] = [] json_fields = [] field_types: Dict[str, str] = {} @@ -233,6 +217,7 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> used_options = set() for field in json_fields: oq = (cmd.field_to_option_map or {}).get(field.field, field.field) + oq = ''.join(x.capitalize() for x in oq.split('_')) if oq in option_map: o = option_map[oq] used_options.add(oq) @@ -242,16 +227,16 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> else: unhandled[field.field] = field for x in tuple(unhandled): - if x == 'match_window' and 'match' in option_map and 'match' not in used_options: - used_options.add('match') - o = option_map['match'] + if x == 'match_window' and 'Match' in option_map and 'Match' not in used_options: + used_options.add('Match') + o = option_map['Match'] field = unhandled[x] jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}') del unhandled[x] if unhandled: raise SystemExit(f'Cant map fields: {", ".join(unhandled)} for cmd: {name}') if name != 'send_text': - unused_options = set(option_map) - used_options - {'no_response', 'response_timeout'} + unused_options = set(option_map) - used_options - {'NoResponse', 'ResponseTimeout'} if unused_options: raise SystemExit(f'Unused options: {", ".join(unused_options)} for command: {name}') @@ -266,9 +251,7 @@ def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> IS_ASYNC='true' if cmd.is_asynchronous else 'false', NO_RESPONSE_BASE=NO_RESPONSE_BASE, ADD_FLAGS_CODE='\n'.join(af), WAIT_TIMEOUT=str(cmd.response_timeout), - ALIAS_NORMALIZE_CODE=render_alias_map(alias_map), OPTIONS_DECLARATION_CODE='\n'.join(od), - SET_OPTION_VALUES_CODE='\n'.join(ov), JSON_DECLARATION_CODE='\n'.join(jd), JSON_INIT_CODE='\n'.join(jc), ARGSPEC=argspec, STRING_RESPONSE_IS_ERROR='true' if cmd.string_return_is_error else 'false', @@ -352,6 +335,27 @@ def update_at_commands() -> None: dest = f'tools/cmd/at/cmd_{name}_generated.go' with replace_if_needed(dest) as f: f.write(code) + struct_def = [] + opt_def = [] + for o in go_options_for_seq(parse_option_spec(global_options_spec())[0]): + struct_def.append(o.struct_declaration()) + opt_def.append(o.as_option(depth=1, group="Global options")) + sdef = '\n'.join(struct_def) + odef = '\n'.join(opt_def) + code = f''' +package at +import "kitty/tools/cli" +type rc_global_options struct {{ +{sdef} +}} +var rc_global_opts rc_global_options + +func add_rc_global_opts(cmd *cli.Command) {{ +{odef} +}} +''' + with replace_if_needed('tools/cmd/at/global_opts_generated.go') as f: + f.write(code) def update_completion() -> None: diff --git a/go.mod b/go.mod index 5a78f0570..ba6ebd207 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,10 @@ require ( github.com/google/uuid v1.3.0 github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f github.com/seancfoley/ipaddress-go v1.2.1 - github.com/spf13/cobra v1.5.0 - github.com/spf13/pflag v1.0.5 - golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 + golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 ) -require ( - github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/seancfoley/bintree v1.1.0 // indirect -) +require github.com/seancfoley/bintree v1.1.0 // indirect diff --git a/go.sum b/go.sum index f70095672..983372e83 100644 --- a/go.sum +++ b/go.sum @@ -1,26 +1,17 @@ github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924 h1:DG4UyTVIujioxwJc8Zj8Nabz1L1wTgQ/xNBSQDfdP3I= github.com/ALTree/bigfloat v0.0.0-20220102081255-38c8b72a9924/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f h1:Ko4+g6K16vSyUrtd/pPXuQnWsiHe5BYptEtTxfwYwCc= github.com/jamesruan/go-rfc1924 v0.0.0-20170108144916-2767ca7c638f/go.mod h1:eHzfhOKbTGJEGPSdMHzU6jft192tHHt2Bu2vIZArvC0= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/seancfoley/bintree v1.1.0 h1:6J0rj9hLNLIcWSsfYdZ4ZHkMHokaK/PHkak8qyBO/mc= github.com/seancfoley/bintree v1.1.0/go.mod h1:CtE6qO6/n9H3V2CAGEC0lpaYr6/OijhNaMG/dt7P70c= github.com/seancfoley/ipaddress-go v1.2.1 h1:yEZxnyC6NQEDDPflyQm4KkWozffx1vHWsx+knKBr/n0= github.com/seancfoley/ipaddress-go v1.2.1/go.mod h1:/UEVHyrBg1ASVap2ffdY2cq5UMYIX9f3QW3uWSVqpbo= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= -golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 h1:WJywXQVIb56P2kAvXeMGTIgQ1ZHQxR60+F9dLsodECc= +golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= @@ -29,5 +20,3 @@ golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RA golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/kitty/cli.py b/kitty/cli.py index 6884c41df..b03dd4a55 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -119,10 +119,6 @@ def serialize_as_go_string(x: str) -> str: go_type_map = { 'bool-set': 'bool', 'bool-reset': 'bool', 'int': 'int', 'float': 'float64', '': 'string', 'list': '[]string', 'choices': 'string', 'str': 'string'} -go_getter_map = { - 'bool-set': 'GetBool', 'bool-reset': 'GetBool', 'int': 'GetInt', 'float': 'GetFloat64', '': 'GetString', - 'list': 'GetStringArray', 'choices': 'GetString' -} class GoOption: @@ -137,54 +133,38 @@ class GoOption: for f in flags: q = f[2:] if f.startswith('--') else f[1:] self.aliases.append(q) - self.usage = serialize_as_go_string(x['help'].strip()) self.type = x['type'] - self.dest = x['dest'] + if x['choices']: + self.type = 'choices' self.default = x['default'] self.obj_dict = x self.go_type = go_type_map[self.type] - self.go_var_name = self.long.replace('-', '_') - if self.go_var_name == 'type': - self.go_var_name += '_' - - def to_flag_definition(self, base: str = 'ans.Flags()') -> str: - if self.type.startswith('bool-'): - defval = 'false' if self.type == 'bool-set' else 'true' - if self.short: - return f'{base}.BoolP("{self.long}", "{self.short}", {defval}, "{self.usage}")' - return f'{base}.Bool("{self.long}", {defval}, "{self.usage}")' - elif not self.type: - defval = f'''"{serialize_as_go_string(self.default or '')}"''' - if self.short: - return f'{base}.StringP("{self.long}", "{self.short}", {defval}, "{self.usage}")' - return f'{base}.String("{self.long}", {defval}, "{self.usage}")' - elif self.type == 'int': - if self.short: - return f'{base}.IntP("{self.long}", "{self.short}", {self.default or 0}, "{self.usage}")' - return f'{base}.Int("{self.long}", {self.default or 0}, "{self.usage}")' - elif self.type == 'float': - if self.short: - return f'{base}.Float64P("{self.long}", "{self.short}", {self.default or 0}, "{self.usage}")' - return f'{base}.Float64("{self.long}", {self.default or 0}, "{self.usage}")' - elif self.type == 'list': - defval = f'[]string{{"{serialize_as_go_string(self.default)}"}}' if self.default else '[]string{}' - if self.short: - return f'{base}.StringArrayP("{self.long}", "{self.short}", {defval}, "{self.usage}")' - return f'{base}.StringArray("{self.long}", {defval}, "{self.usage}")' - elif self.type == 'choices': - choices = self.sorted_choices - cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in choices) - if self.short: - return f'cli.ChoicesP({base}, "{self.long}", "{self.short}", "{self.usage}", {cx})' - return f'cli.Choices({base}, "{self.long}", "{self.usage}", {cx})' + if x['dest']: + self.go_var_name = ''.join(x.capitalize() for x in x['dest'].replace('-', '_').split('_')) else: - raise TypeError(f'Unknown type of CLI option: {self.type} for {self.long}') + self.go_var_name = ''.join(x.capitalize() for x in self.long.replace('-', '_').split('_')) + self.help_text = serialize_as_go_string(self.obj_dict['help'].strip()) - def set_flag_value(self, struct_name: str, cmd: str = 'cmd') -> str: - func = go_getter_map[self.type] - ans = f'{self.go_var_name}_temp, err := {cmd}.Flags().{func}("{self.long}")\n if err != nil {{ return err }}' - ans += f'\n{struct_name}.{self.go_var_name} = {self.go_var_name}_temp' - return ans + def struct_declaration(self) -> str: + return f'{self.go_var_name} {self.go_type}' + + def as_option(self, cmd_name: str = 'cmd', depth: int = 0, group: str = '') -> str: + add = f'AddToGroup("{serialize_as_go_string(group)}", ' if group else 'Add(' + aliases = ' '.join(self.obj_dict['aliases']) + ans = f'''{cmd_name}.{add}cli.OptionSpec{{ + Name: "{serialize_as_go_string(aliases)}", + Type: "{self.type}", + Dest: "{serialize_as_go_string(self.go_var_name)}", + Help: "{self.help_text}", + ''' + if self.type in ('choice', 'choices'): + c = ', '.join(self.sorted_choices) + ans += f'\nChoices: "{serialize_as_go_string(c)}",\n' + if depth > 0: + ans += f'\nDepth: {depth},\n' + if self.default: + ans += f'\nDefault: "{serialize_as_go_string(self.default)}",\n' + return ans + '})' @property def sorted_choices(self) -> List[str]: @@ -195,12 +175,12 @@ class GoOption: def as_completion_option(self, command_name: str) -> str: ans = f'{command_name}.add_option(&' 'Option{Name: ' f'"{serialize_as_go_string(self.long)}", ' - ans += f'Description: "{self.usage}", ' + ans += f'Description: "{self.help_text}", ' aliases = (f'"{serialize_as_go_string(x)}"' for x in self.aliases) ans += 'Aliases: []string{' f'{", ".join(aliases)}' '}, ' if self.go_type != 'bool': ans += 'Has_following_arg: true, ' - if self.type == 'choices': + if self.type in ('choices', 'choice'): cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in self.sorted_choices) ans += f'Completion_for_arg: names_completer("Choices for {self.long}", {cx}),' elif self.obj_dict['completion'].type is not CompletionType.none: diff --git a/kitty/rc/ls.py b/kitty/rc/ls.py index 97aa2a037..6e68aac3f 100644 --- a/kitty/rc/ls.py +++ b/kitty/rc/ls.py @@ -25,7 +25,7 @@ class LS(RemoteCommand): 'List all windows. The list is returned as JSON tree. The top-level is a list of' f' operating system {appname} windows. Each OS window has an :italic:`id` and a list' ' of :italic:`tabs`. Each tab has its own :italic:`id`, a :italic:`title` and a list of :italic:`windows`.' - ' Each window has an :italic:`id`, :italic:`title`, :italic:`current working directory`, :italic:`process id (PID)`, ' + ' Each window has an :italic:`id`, :italic:`title`, :italic:`current working directory`, :italic:`process id (PID)`,' ' :italic:`command-line` and :italic:`environment` of the process running in the window. Additionally, when' ' running the command inside a kitty window, that window can be identified by the :italic:`is_self` parameter.\n\n' 'You can use these criteria to select windows/tabs for the other commands.' diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 23fe3aad1..f90a2a778 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -252,7 +252,7 @@ Used if no :option:`kitty @ --password` is supplied. Defaults to checking for th default=KITTY_RC_PASSWORD The name of an environment variable to read the password from. Used if no :option:`kitty @ --password-file` is supplied. Defaults -to checking the :envvar:`KITTY_RC_PASSWORD`. +to checking the environment variable :envvar:`KITTY_RC_PASSWORD`. --use-password diff --git a/tools/cli/help.go b/tools/cli/help.go index 4234818c6..4d8a06c2d 100644 --- a/tools/cli/help.go +++ b/tools/cli/help.go @@ -31,7 +31,6 @@ func (self *Command) ShowVersion() { } func format_with_indent(output io.Writer, text string, indent string, screen_width int) { - text = formatter.Prettify(text) indented := style.WrapText(text, indent, screen_width, "#placeholder_for_formatting#") io.WriteString(output, indented) } @@ -41,10 +40,12 @@ func (self *Command) FormatSubCommands(output io.Writer, formatter *markup.Conte if !g.HasVisibleSubCommands() { continue } - if g.Title != "" { - fmt.Fprintln(output) - fmt.Fprintln(output, formatter.Title(g.Title)) + title := g.Title + if title == "" { + title = "Commands" } + fmt.Fprintln(output) + fmt.Fprintln(output, formatter.Title(title)+":") for _, c := range g.SubCommands { if c.Hidden { continue @@ -64,22 +65,23 @@ func (self *Option) FormatOption(output io.Writer, formatter *markup.Context, sc fmt.Fprint(output, ", ") } } - defval := "" + defval := self.Default switch self.OptionType { - case BoolOption: - default: - defval = self.Default - fallthrough case StringOption: if self.IsList { defval = "" } + case BoolOption, CountOption: + defval = "" } if defval != "" { fmt.Fprintf(output, " [=%s]", formatter.Italic(defval)) } fmt.Fprintln(output) format_with_indent(output, formatter.Prettify(prepare_help_text_for_display(self.Help)), " ", screen_width) + if self.Choices != nil { + format_with_indent(output, "Choices: "+strings.Join(self.Choices, ", "), " ", screen_width) + } } func (self *Command) ShowHelp() { @@ -111,7 +113,6 @@ func (self *Command) ShowHelp() { if self.HasVisibleSubCommands() { fmt.Fprintln(&output) - fmt.Fprintln(&output, formatter.Title("Commands")+":") self.FormatSubCommands(&output, formatter, screen_width) fmt.Fprintln(&output) format_with_indent(&output, "Get help for an individual command by running:", "", screen_width) @@ -121,12 +122,12 @@ func (self *Command) ShowHelp() { group_titles, gmap := self.GetVisibleOptions() if len(group_titles) > 0 { fmt.Fprintln(&output) - fmt.Fprintln(&output, formatter.Title("Options")+":") for _, title := range group_titles { - if title != "" { - fmt.Fprintln(&output) - fmt.Fprintln(&output, formatter.Title(title)) + ptitle := title + if title == "" { + ptitle = "Options" } + fmt.Fprintln(&output, formatter.Title(ptitle)+":") for _, opt := range gmap[title] { opt.FormatOption(&output, formatter, screen_width) fmt.Fprintln(&output) diff --git a/tools/cli/infrastructure.go b/tools/cli/infrastructure.go deleted file mode 100644 index ff05c89b6..000000000 --- a/tools/cli/infrastructure.go +++ /dev/null @@ -1,265 +0,0 @@ -// License: GPLv3 Copyright: 2022, Kovid Goyal, - -package cli - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "golang.org/x/sys/unix" - - "kitty" - "kitty/tools/cli/markup" - "kitty/tools/tty" -) - -var RootCmd *cobra.Command - -func key_in_slice(vals []string, key string) bool { - for _, q := range vals { - if q == key { - return true - } - } - return false -} - -type ChoicesVal struct { - name, Choice string - allowed []string -} -type choicesVal ChoicesVal - -func (i *choicesVal) String() string { return ChoicesVal(*i).Choice } -func (i *choicesVal) Type() string { return "string" } -func (i *choicesVal) Set(s string) error { - (*i).Choice = s - return nil -} -func newChoicesVal(val ChoicesVal, p *ChoicesVal) *choicesVal { - *p = val - return (*choicesVal)(p) -} - -func add_choices(flags *pflag.FlagSet, p *ChoicesVal, choices []string, name string, short string, usage string) { - usage = strings.TrimSpace(usage) + "\n" + "Choices: " + strings.Join(choices, ", ") - value := ChoicesVal{Choice: choices[0], allowed: choices} - flags.VarP(newChoicesVal(value, p), name, short, usage) -} - -func Choices(flags *pflag.FlagSet, name string, usage string, choices ...string) *ChoicesVal { - p := new(ChoicesVal) - add_choices(flags, p, choices, name, "", usage) - return p -} - -func ChoicesP(flags *pflag.FlagSet, name string, short string, usage string, choices ...string) *ChoicesVal { - p := new(ChoicesVal) - add_choices(flags, p, choices, name, short, usage) - return p -} - -var formatter *markup.Context - -func full_command_name(cmd *cobra.Command) string { - var parent_names []string - cmd.VisitParents(func(p *cobra.Command) { - parent_names = append([]string{p.Name()}, parent_names...) - }) - parent_names = append(parent_names, cmd.Name()) - return strings.Join(parent_names, " ") -} - -func show_usage(cmd *cobra.Command, use_pager bool) error { - screen_width := 80 - if formatter.EscapeCodesAllowed() { - var sz *unix.Winsize - var tty_size_err error - for { - sz, tty_size_err = unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) - if tty_size_err != unix.EINTR { - break - } - } - if tty_size_err == nil && sz.Col < 80 { - screen_width = int(sz.Col) - } - } - var output strings.Builder - use := cmd.Use - idx := strings.Index(use, " ") - if idx > -1 { - use = use[idx+1:] - } else { - use = "" - } - fmt.Fprintln(&output, formatter.Title("Usage")+":", formatter.Exe(full_command_name(cmd)), use) - fmt.Fprintln(&output) - if len(cmd.Long) > 0 { - format_with_indent(&output, cmd.Long, "", screen_width) - } else if len(cmd.Short) > 0 { - format_with_indent(&output, cmd.Short, "", screen_width) - } - if cmd.HasAvailableSubCommands() { - fmt.Fprintln(&output) - fmt.Fprintln(&output, formatter.Title("Commands")+":") - for _, child := range cmd.Commands() { - if child.Hidden { - continue - } - fmt.Fprintln(&output, " ", formatter.Opt(child.Name())) - format_with_indent(&output, child.Short, " ", screen_width) - } - fmt.Fprintln(&output) - format_with_indent(&output, "Get help for an individual command by running:", "", screen_width) - fmt.Fprintln(&output, " ", full_command_name(cmd), formatter.Italic("command"), "-h") - } - if cmd.HasAvailableFlags() { - options_title := cmd.Annotations["options_title"] - if len(options_title) == 0 { - options_title = "Options" - } - fmt.Fprintln(&output) - fmt.Fprintln(&output, formatter.Title(options_title)+":") - flag_set := cmd.LocalFlags() - flag_set.VisitAll(func(flag *pflag.Flag) { - fmt.Fprint(&output, formatter.Opt(" --"+flag.Name)) - if flag.Shorthand != "" { - fmt.Fprint(&output, ", ", formatter.Opt("-"+flag.Shorthand)) - } - defval := "" - switch flag.Value.Type() { - default: - if flag.DefValue != "" { - defval = fmt.Sprintf("[=%s]", formatter.Italic(flag.DefValue)) - } - case "stringArray": - if flag.DefValue != "[]" { - defval = fmt.Sprintf("[=%s]", formatter.Italic(flag.DefValue)) - } - case "bool": - case "count": - } - if defval != "" { - fmt.Fprint(&output, " ", defval) - } - fmt.Fprintln(&output) - msg := flag.Usage - switch flag.Name { - case "help": - msg = "Print this help message" - case "version": - msg = "Print the version of " + RootCmd.Name() + ": " + formatter.Italic(RootCmd.Version) - } - format_with_indent(&output, msg, " ", screen_width) - fmt.Fprintln(&output) - }) - } - if cmd.Annotations["usage-suffix"] != "" { - fmt.Fprintln(&output, cmd.Annotations["usage-suffix"]) - } else { - fmt.Fprintln(&output, formatter.Italic(RootCmd.Name()), formatter.Opt(kitty.VersionString), "created by", formatter.Title("Kovid Goyal")) - } - output_text := output.String() - // fmt.Printf("%#v\n", output_text) - if use_pager && formatter.EscapeCodesAllowed() && cmd.Annotations["allow-pager"] != "no" { - pager := exec.Command(kitty.DefaultPager[0], kitty.DefaultPager[1:]...) - pager.Stdin = strings.NewReader(output_text) - pager.Stdout = os.Stdout - pager.Stderr = os.Stderr - pager.Run() - } else { - cmd.OutOrStdout().Write([]byte(output_text)) - } - return nil -} - -func FlagNormalizer(name string) string { - return strings.ReplaceAll(name, "_", "-") -} - -func DisallowArgs(cmd *cobra.Command, args []string) error { - if cmd.HasSubCommands() { - if len(args) == 0 { - return fmt.Errorf("No sub-command specified. Use %s -h to get a list of available sub-commands", full_command_name(cmd)) - } - cmd.SuggestionsMinimumDistance = 2 - suggestions := cmd.SuggestionsFor(args[0]) - es := "Not a valid subcommand: " + args[0] - trailer := fmt.Sprintf("Use %s to get a list of available sub-commands", formatter.Bold(full_command_name(cmd)+" -h")) - if len(suggestions) > 0 { - es += "\nDid you mean?\n" - for _, s := range suggestions { - es += fmt.Sprintf("\t%s\n", formatter.Italic(s)) - } - es += trailer - } else { - es += ". " + trailer - } - return fmt.Errorf("%s", es) - } - return nil -} - -func CreateCommand(cmd *cobra.Command) *cobra.Command { - cmd.Annotations = make(map[string]string) - cmd.SilenceErrors = true - cmd.SilenceUsage = true - cmd.PersistentFlags().SortFlags = false - cmd.Flags().SortFlags = false - cmd.Flags().SetNormalizeFunc(func(fs *pflag.FlagSet, name string) pflag.NormalizedName { - return pflag.NormalizedName(FlagNormalizer(name)) - }) - cmd.PersistentFlags().SetNormalizeFunc(cmd.Flags().GetNormalizeFunc()) - if !cmd.Runnable() { - cmd.Args = DisallowArgs - cmd.RunE = func(cmd *cobra.Command, args []string) error { - return nil - } - } - return cmd -} - -func show_help(cmd *cobra.Command, args []string) { - show_usage(cmd, true) -} - -func PrintError(err error) { - fmt.Println(formatter.Err("Error")+":", err) -} - -func Init(root *cobra.Command) { - vs := kitty.VersionString - if kitty.VCSRevision != "" { - vs = vs + " (" + kitty.VCSRevision + ")" - } - formatter = markup.New(tty.IsTerminal(os.Stdout.Fd())) - RootCmd = root - root.Version = vs - root.SetUsageFunc(func(cmd *cobra.Command) error { return show_usage(cmd, false) }) - root.SetHelpFunc(show_help) - root.SetHelpCommand(&cobra.Command{Hidden: true}) - root.CompletionOptions.DisableDefaultCmd = true -} - -func Execute(root *cobra.Command) error { - return root.Execute() -} - -type FlagValGetter struct { - Flags *pflag.FlagSet - Err error -} - -func (self *FlagValGetter) String(name string) string { - if self.Err != nil { - return "" - } - ans, err := self.Flags.GetString(name) - self.Err = err - return ans -} diff --git a/tools/cli/types.go b/tools/cli/types.go index cbe033721..222c18384 100644 --- a/tools/cli/types.go +++ b/tools/cli/types.go @@ -238,11 +238,11 @@ type OptionGroup struct { } func (self *OptionGroup) Clone(parent *Command) *OptionGroup { - ans := OptionGroup{Title: self.Title, Options: make([]*Option, 0, len(self.Options))} + ans := OptionGroup{Title: self.Title, Options: make([]*Option, len(self.Options))} for i, o := range self.Options { c := *o c.Parent = parent - self.Options[i] = &c + ans.Options[i] = &c } return &ans } @@ -302,8 +302,8 @@ func (self *Command) Clone(parent *Command) *Command { ans := *self ans.Args = make([]string, 0, 8) ans.Parent = parent - ans.SubCommandGroups = make([]*CommandGroup, 0, len(self.SubCommandGroups)) - ans.OptionGroups = make([]*OptionGroup, 0, len(self.OptionGroups)) + ans.SubCommandGroups = make([]*CommandGroup, len(self.SubCommandGroups)) + ans.OptionGroups = make([]*OptionGroup, len(self.OptionGroups)) for i, o := range self.OptionGroups { ans.OptionGroups[i] = o.Clone(&ans) @@ -314,14 +314,11 @@ func (self *Command) Clone(parent *Command) *Command { return &ans } -func (self *Command) AddClone(group string, src *Command) (*Command, error) { +func (self *Command) AddClone(group string, src *Command) *Command { c := src.Clone(self) g := self.AddSubCommandGroup(group) - if g.FindSubCommand(c.Name) != nil { - return nil, fmt.Errorf("A sub command with the name: %s already exists in %s", c.Name, self.Name) - } g.SubCommands = append(g.SubCommands, c) - return c, nil + return c } func NewRootCommand() *Command { @@ -605,6 +602,19 @@ type Context struct { SeenCommands []*Command } +func GetOptionValue[T any](self *Command, name string) (ans T, err error) { + opt := self.option_map[name] + if opt == nil { + err = fmt.Errorf("No option with the name: %s", name) + return + } + ans, ok := opt.parsed_value().(T) + if !ok { + err = fmt.Errorf("The option %s is not of the correct type", name) + } + return +} + func (self *Command) GetOptionValues(pointer_to_options_struct any) error { val := reflect.ValueOf(pointer_to_options_struct).Elem() if val.Kind() != reflect.Struct { @@ -689,7 +699,7 @@ func (self *Command) Exec(args ...string) { } else if cmd.Run != nil { exit_code, err = cmd.Run(cmd, cmd.Args) if err != nil { - PrintError(err) + ShowError(err) if exit_code == 0 { exit_code = 1 } diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index fd044b290..2965648c1 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -13,8 +13,6 @@ import ( "strings" "time" - "github.com/spf13/cobra" - "github.com/spf13/pflag" "golang.org/x/sys/unix" "kitty" @@ -30,13 +28,6 @@ import ( var ProtocolVersion [3]int = [3]int{0, 26, 0} -func add_bool_set(cmd *cobra.Command, name string, short string, usage string) *bool { - if short == "" { - return cmd.Flags().Bool(name, false, usage) - } - return cmd.Flags().BoolP(name, short, false, usage) -} - type GlobalOptions struct { to_network, to_address, password string to_address_is_from_env_var bool @@ -142,7 +133,7 @@ type Response struct { } type rc_io_data struct { - cmd *cobra.Command + cmd *cli.Command rc *utils.RemoteControlCmd serializer serializer_func on_key_event func(lp *loop.Loop, ke *loop.KeyEvent) error @@ -294,82 +285,46 @@ func get_password(password string, password_file string, password_env string, us return ans, nil } -var all_commands map[string]func(*cobra.Command) *cobra.Command = make(map[string]func(*cobra.Command) *cobra.Command) -var command_objects map[string]*cobra.Command = make(map[string]*cobra.Command) +var all_commands map[string]func(*cli.Command) *cli.Command = make(map[string]func(*cli.Command) *cli.Command) -func add_global_options(fs *pflag.FlagSet) { - fs.String("to", "", - "An address for the kitty instance to control. Corresponds to the address given"+ - " to the kitty instance via the :option:`kitty --listen-on` option or the :opt:`listen_on` setting in :file:`kitty.conf`. If not"+ - " specified, the environment variable :envvar:`KITTY_LISTEN_ON` is checked. If that"+ - " is also not found, messages are sent to the controlling terminal for this"+ - " process, i.e. they will only work if this process is run within a kitty window.") - - fs.String("password", "", - "A password to use when contacting kitty. This will cause kitty to ask the user"+ - " for permission to perform the specified action, unless the password has been"+ - " accepted before or is pre-configured in :file:`kitty.conf`.") - - fs.String("password-file", "rc-pass", - "A file from which to read the password. Trailing whitespace is ignored. Relative"+ - " paths are resolved from the kitty configuration directory. Use - to read from STDIN."+ - " Used if no :option:`--password` is supplied. Defaults to checking for the"+ - " :file:`rc-pass` file in the kitty configuration directory.") - - fs.String("password-env", "KITTY_RC_PASSWORD", - "The name of an environment variable to read the password from."+ - " Used if no :option:`--password-file` or :option:`--password` is supplied.") - - cli.Choices(fs, "use-password", "If no password is available, kitty will usually just send the remote control command without a password. This option can be used to force it to always or never use the supplied password.", "if-available", "always", "never") - -} - -func setup_global_options(cmd *cobra.Command) (err error) { - var v = cli.FlagValGetter{Flags: cmd.Flags()} - to := v.String("to") - password := v.String("password") - password_file := v.String("password-file") - password_env := v.String("password-env") - use_password := v.String("use-password") - if v.Err != nil { - return v.Err +func setup_global_options(cmd *cli.Command) (err error) { + err = cmd.GetOptionValues(&rc_global_opts) + if err != nil { + return err } - if to == "" { - to = os.Getenv("KITTY_LISTEN_ON") + if rc_global_opts.To == "" { + rc_global_opts.To = os.Getenv("KITTY_LISTEN_ON") global_options.to_address_is_from_env_var = true } - if to != "" { - network, address, err := utils.ParseSocketAddress(to) + if rc_global_opts.To != "" { + network, address, err := utils.ParseSocketAddress(rc_global_opts.To) if err != nil { return err } global_options.to_network = network global_options.to_address = address } - q, err := get_password(password, password_file, password_env, use_password) + q, err := get_password(rc_global_opts.Password, rc_global_opts.PasswordFile, rc_global_opts.PasswordEnv, rc_global_opts.UsePassword) global_options.password = q return err } -func EntryPoint(tool_root *cobra.Command) *cobra.Command { - at_root_command := cli.CreateCommand(&cobra.Command{ - Use: "@ [global options] command [command options] [command args]", - Short: "Control kitty remotely", - Long: "Control kitty by sending it commands. Set the allow_remote_control option in :file:`kitty.conf` or use a password, for this to work.", - }) - at_root_command.Annotations["options_title"] = "Global options" - add_global_options(at_root_command.PersistentFlags()) +func EntryPoint(tool_root *cli.Command) *cli.Command { + at_root_command := tool_root.AddSubCommand("", "@") + at_root_command.Usage = "[global options] [sub-command] [sub-command options] [sub-command args]" + at_root_command.ShortDescription = "Control kitty remotely" + at_root_command.HelpText = "Control kitty by sending it commands. Set the allow_remote_control option in :file:`kitty.conf` for this to work. When run without any sub-commands this will start an interactive shell to control kitty." + add_rc_global_opts(at_root_command) - for cmd_name, reg_func := range all_commands { + global_options_group := at_root_command.OptionGroups[0] + + for _, reg_func := range all_commands { c := reg_func(at_root_command) - at_root_command.AddCommand(c) - command_objects[cmd_name] = c - alias := *c - alias.Use = "@" + alias.Use - alias.Hidden = true - add_global_options(alias.Flags()) - tool_root.AddCommand(&alias) + clone := tool_root.AddClone("", c) + clone.Name = "@" + c.Name + clone.Hidden = true + clone.OptionGroups = append(clone.OptionGroups, global_options_group.Clone(clone)) } return at_root_command } diff --git a/tools/cmd/at/send_text.go b/tools/cmd/at/send_text.go index e04dcb6f1..762bdced1 100644 --- a/tools/cmd/at/send_text.go +++ b/tools/cmd/at/send_text.go @@ -32,8 +32,8 @@ func parse_send_text(io_data *rc_io_data, args []string) error { generators = append(generators, text_gen) } - if options_send_text.from_file != "" { - f, err := os.Open(options_send_text.from_file) + if options_send_text.FromFile != "" { + f, err := os.Open(options_send_text.FromFile) if err != nil { return err } @@ -49,7 +49,7 @@ func parse_send_text(io_data *rc_io_data, args []string) error { generators = append(generators, file_gen) } - if options_send_text.stdin { + if options_send_text.Stdin { pending_key_events := make([]string, 0, 1) io_data.on_key_event = func(lp *loop.Loop, ke *loop.KeyEvent) error { diff --git a/tools/cmd/at/template.go b/tools/cmd/at/template.go index 9a3c7bdfb..609137286 100644 --- a/tools/cmd/at/template.go +++ b/tools/cmd/at/template.go @@ -2,7 +2,7 @@ // License: GPLv3 Copyright: 2022, Kovid Goyal, -// this file is autogenerated by __FILE__ do not edit +// Code generated by gen-go-code.py; DO NOT EDIT. package at @@ -11,9 +11,6 @@ import ( "strings" "time" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "kitty/tools/cli" "kitty/tools/utils" ) @@ -31,7 +28,7 @@ type CMD_NAME_json_type struct { JSON_DECLARATION_CODE } -func create_payload_CMD_NAME(io_data *rc_io_data, flags *pflag.FlagSet, args []string) (err error) { +func create_payload_CMD_NAME(io_data *rc_io_data, cmd *cli.Command, args []string) (err error) { payload := CMD_NAME_json_type{} JSON_INIT_CODE io_data.rc.Payload = payload @@ -62,19 +59,22 @@ func create_rc_CMD_NAME(args []string) (*utils.RemoteControlCmd, error) { return &rc, nil } -func run_CMD_NAME(cmd *cobra.Command, args []string) (err error) { - SET_OPTION_VALUES_CODE +func run_CMD_NAME(cmd *cli.Command, args []string) (return_code int, err error) { + err = cmd.GetOptionValues(&options_CMD_NAME) + if err != nil { + return + } rc, err := create_rc_CMD_NAME(args) if err != nil { - return err + return } - nrv, err := cmd.Flags().GetBool("no-response") + nrv, err := cli.GetOptionValue[bool](cmd, "NoResponse") if err == nil { rc.NoResponse = nrv } var timeout float64 = WAIT_TIMEOUT - rt, err := cmd.Flags().GetFloat64("response-timeout") + rt, err := cli.GetOptionValue[float64](cmd, "ResponseTimeout") if err == nil { timeout = rt } @@ -84,31 +84,22 @@ func run_CMD_NAME(cmd *cobra.Command, args []string) (err error) { timeout: time.Duration(timeout * float64(time.Second)), string_response_is_err: STRING_RESPONSE_IS_ERROR, } - err = create_payload_CMD_NAME(&io_data, cmd.Flags(), args) + err = create_payload_CMD_NAME(&io_data, cmd, args) if err != nil { - return err + return } err = send_rc_command(&io_data) return } -func aliasNormalizeFunc_CMD_NAME(f *pflag.FlagSet, name string) pflag.NormalizedName { - name = cli.FlagNormalizer(name) - ALIAS_NORMALIZE_CODE - return pflag.NormalizedName(name) -} - -func setup_CMD_NAME(root *cobra.Command) *cobra.Command { - ans := cli.CreateCommand(&cobra.Command{ - Use: "CLI_NAME [options]" + "ARGSPEC", - Short: "SHORT_DESC", - Long: "LONG_DESC", - RunE: run_CMD_NAME, - }) +func setup_CMD_NAME(parent *cli.Command) *cli.Command { + ans := parent.AddSubCommand("", "CMD_NAME") + ans.Usage = "ARGSPEC" + ans.ShortDescription = "SHORT_DESC" + ans.HelpText = "LONG_DESC" + ans.Run = run_CMD_NAME ADD_FLAGS_CODE - ans.Flags().SetNormalizeFunc(aliasNormalizeFunc_CMD_NAME) - return ans } diff --git a/tools/cmd/main.go b/tools/cmd/main.go index 51bdb70d5..650688bec 100644 --- a/tools/cmd/main.go +++ b/tools/cmd/main.go @@ -4,7 +4,7 @@ package main import ( "kitty/tools/cli" - _ "kitty/tools/cmd/at" + "kitty/tools/cmd/at" "kitty/tools/completion" ) @@ -12,8 +12,9 @@ func main() { root := cli.NewRootCommand() root.ShortDescription = "Fast, statically compiled implementations for various kitty command-line tools" root.Usage = "command [command options] [command args]" - // root.AddCommand(at.EntryPoint(root)) + at.EntryPoint(root) completion.EntryPoint(root) + root.Exec() }