diff --git a/docs/conf.py b/docs/conf.py index 2755b60fa..7bbdbd308 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -525,9 +525,9 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None: from kittens.runner import get_kitten_conf_docs for kitten in all_kitten_names: - definition = get_kitten_conf_docs(kitten) - if definition: - generate_default_config(definition, f'kitten-{kitten}') + defn = get_kitten_conf_docs(kitten) + if defn is not None: + generate_default_config(defn, f'kitten-{kitten}') from kitty.actions import as_rst with open('generated/actions.rst', 'w', encoding='utf-8') as f: diff --git a/gen-config.py b/gen-config.py index 8cc3e4350..a59e5a1e1 100755 --- a/gen-config.py +++ b/gen-config.py @@ -51,8 +51,6 @@ def main() -> None: from kittens.diff.options.definition import definition as kd write_output('kittens.diff', kd) - from kittens.ssh.options.definition import definition as sd - write_output('kittens.ssh', sd) if __name__ == '__main__': diff --git a/gen-go-code.py b/gen-go-code.py index 51fed7493..5de53d009 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -22,6 +22,8 @@ from kitty.cli import ( parse_option_spec, serialize_as_go_string, ) +from kitty.conf.generate import gen_go_code +from kitty.conf.types import Definition from kitty.guess_mime_type import text_mimes from kitty.key_encoding import config_mod_map from kitty.key_names import character_key_name_aliases, functional_key_name_aliases @@ -318,8 +320,18 @@ def wrapped_kittens() -> Sequence[str]: raise Exception('Failed to read wrapped kittens from kitty wrapper script') +def generate_conf_parser(kitten: str, defn: Definition) -> None: + with replace_if_needed(f'tools/cmd/{kitten}/conf_generated.go'): + print(f'package {kitten}') + print(gen_go_code(defn)) + + def kitten_clis() -> None: + from kittens.runner import get_kitten_conf_docs for kitten in wrapped_kittens(): + defn = get_kitten_conf_docs(kitten) + if defn is not None: + generate_conf_parser(kitten, defn) with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'): od = [] kcd = kitten_cli_docs(kitten) diff --git a/kittens/runner.py b/kittens/runner.py index be56c5335..656afb6d1 100644 --- a/kittens/runner.py +++ b/kittens/runner.py @@ -7,7 +7,7 @@ import os import sys from contextlib import contextmanager from functools import partial -from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast +from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, Optional, cast from kitty.constants import list_kitty_resources from kitty.types import run_once @@ -171,7 +171,7 @@ def get_kitten_completer(kitten: str) -> Any: return ans -def get_kitten_conf_docs(kitten: str) -> Definition: +def get_kitten_conf_docs(kitten: str) -> Optional[Definition]: setattr(sys, 'options_definition', None) run_kitten(kitten, run_name='__conf__') ans = getattr(sys, 'options_definition') diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 0e270acf9..7f0ab0842 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -36,7 +36,6 @@ from ..tui.utils import kitty_opts, running_in_tmux from .config import init_config from .copy import CopyInstruction from .options.types import Options as SSHOptions -from .options.utils import DELETE_ENV_VAR from .utils import create_shared_memory, get_ssh_cli, is_extra_arg, passthrough_args diff --git a/kittens/ssh/options/definition.py b/kittens/ssh/options/definition.py index 14fe00ca7..bc9684ca4 100644 --- a/kittens/ssh/options/definition.py +++ b/kittens/ssh/options/definition.py @@ -13,7 +13,7 @@ remote. Directories are copied recursively. If absolute paths are used, they are copied as is.''' definition = Definition( - 'kittens.ssh', + '!kittens.ssh', ) agr = definition.add_group @@ -22,7 +22,7 @@ opt = definition.add_option agr('bootstrap', 'Host bootstrap configuration') # {{{ -opt('hostname', '*', option_type='hostname', long_text=''' +opt('hostname', '*', long_text=''' The hostname that the following options apply to. A glob pattern to match multiple hosts can be used. Multiple hostnames can also be specified, separated by spaces. The hostname can include an optional username in the form @@ -44,7 +44,7 @@ The location on the remote host where the files needed for this kitten are installed. Relative paths are resolved with respect to :code:`$HOME`. ''') -opt('+copy', '', option_type='copy', add_to_default=False, long_text=f''' +opt('+copy', '', add_to_default=False, long_text=f''' {copy_message} For example:: copy .vimrc .zshrc .config/some-dir @@ -80,7 +80,7 @@ The login shell to execute on the remote host. By default, the remote user account's login shell is used. ''') -opt('+env', '', option_type='env', add_to_default=False, long_text=''' +opt('+env', '', add_to_default=False, long_text=''' Specify the environment variables to be set on the remote host. Using the name with an equal sign (e.g. :code:`env VAR=`) will set it to the empty string. Specifying only the name (e.g. :code:`env VAR`) will remove the variable from diff --git a/kittens/ssh/options/types.py b/kittens/ssh/options/types.py deleted file mode 100644 index 90ce49fc5..000000000 --- a/kittens/ssh/options/types.py +++ /dev/null @@ -1,93 +0,0 @@ -# generated by gen-config.py DO NOT edit - -# isort: skip_file -import typing -import kittens.ssh.copy - -if typing.TYPE_CHECKING: - choices_for_askpass = typing.Literal['unless-set', 'ssh', 'native'] - choices_for_remote_kitty = typing.Literal['if-needed', 'no', 'yes'] -else: - choices_for_askpass = str - choices_for_remote_kitty = str - -option_names = ( # {{{ - 'askpass', - 'color_scheme', - 'copy', - 'cwd', - 'env', - 'hostname', - 'interpreter', - 'login_shell', - 'remote_dir', - 'remote_kitty', - 'share_connections', - 'shell_integration') # }}} - - -class Options: - askpass: choices_for_askpass = 'unless-set' - color_scheme: str = '' - cwd: str = '' - hostname: str = '*' - interpreter: str = 'sh' - login_shell: str = '' - remote_dir: str = '.local/share/kitty-ssh-kitten' - remote_kitty: choices_for_remote_kitty = 'if-needed' - share_connections: bool = True - shell_integration: str = 'inherited' - copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {} - env: typing.Dict[str, str] = {} - config_paths: typing.Tuple[str, ...] = () - config_overrides: typing.Tuple[str, ...] = () - - def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None: - if options_dict is not None: - null = object() - for key in option_names: - val = options_dict.get(key, null) - if val is not null: - setattr(self, key, val) - - @property - def _fields(self) -> typing.Tuple[str, ...]: - return option_names - - def __iter__(self) -> typing.Iterator[str]: - return iter(self._fields) - - def __len__(self) -> int: - return len(self._fields) - - def _copy_of_val(self, name: str) -> typing.Any: - ans = getattr(self, name) - if isinstance(ans, dict): - ans = ans.copy() - elif isinstance(ans, list): - ans = ans[:] - return ans - - def _asdict(self) -> typing.Dict[str, typing.Any]: - return {k: self._copy_of_val(k) for k in self} - - def _replace(self, **kw: typing.Any) -> "Options": - ans = Options() - for name in self: - setattr(ans, name, self._copy_of_val(name)) - for name, val in kw.items(): - setattr(ans, name, val) - return ans - - def __getitem__(self, key: typing.Union[int, str]) -> typing.Any: - k = option_names[key] if isinstance(key, int) else key - try: - return getattr(self, k) - except AttributeError: - pass - raise KeyError(f"No option named: {k}") - - -defaults = Options() -defaults.copy = {} -defaults.env = {} diff --git a/kittens/ssh/options/utils.py b/kittens/ssh/options/utils.py deleted file mode 100644 index 135bb2704..000000000 --- a/kittens/ssh/options/utils.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python -# License: GPLv3 Copyright: 2022, Kovid Goyal - -from typing import Any, Dict, Iterable, Optional, Tuple - -from ..copy import CopyInstruction, parse_copy_instructions - -DELETE_ENV_VAR = '_delete_this_env_var_' - - -def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]: - val = val.strip() - if val: - if '=' in val: - key, v = val.split('=', 1) - key, v = key.strip(), v.strip() - if key: - yield key, v - else: - yield val, DELETE_ENV_VAR - - -def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]: - yield from parse_copy_instructions(val, current_val) - - -def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]: - ans['hostname'] = '*' - ans['per_host_dicts'] = {} - return ans - - -def get_per_hosts_dict(results_dict: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: - ans: Dict[str, Dict[str, Any]] = results_dict.get('per_host_dicts', {}).copy() - h = results_dict['hostname'] - hd = {k: v for k, v in results_dict.items() if k != 'per_host_dicts'} - ans[h] = hd - return ans - - -first_seen_positions: Dict[str, int] = {} - - -def hostname(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> str: - if dict_with_parse_results is not None: - ch = dict_with_parse_results['hostname'] - if val != ch: - from .parse import create_result_dict - phd = get_per_hosts_dict(dict_with_parse_results) - dict_with_parse_results.clear() - dict_with_parse_results.update(phd.pop(val, create_result_dict())) - dict_with_parse_results['per_host_dicts'] = phd - dict_with_parse_results['hostname'] = val - if val not in first_seen_positions: - first_seen_positions[val] = len(first_seen_positions) - return val diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index 1075663da..8927fa98e 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -9,7 +9,7 @@ import re import textwrap from typing import Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints -from kitty.conf.types import Definition, MultiOption, Option, unset +from kitty.conf.types import Definition, MultiOption, Option, ParserFuncType, unset from kitty.types import _T @@ -442,6 +442,114 @@ def write_output(loc: str, defn: Definition) -> None: f.write(f'{c}\n') +def go_type_data(parser_func: ParserFuncType, type_defs: List[str]) -> Tuple[str, str]: + p = parser_func.__name__ + if p == 'int': + return 'int64', 'strconv.ParseInt(val, 10, 64)' + if p == 'str': + return 'string', 'val, nil' + if p == 'float': + return 'float64', 'strconv.ParseFloat(val, 10, 64)' + if p == 'to_bool': + return 'bool', 'utils.StringToBool(val), nil' + th = get_type_hints(parser_func) + rettype = th['return'] + return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)' + + +def gen_go_code(defn: Definition) -> str: + lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/utils"', 'var _ = fmt.Println', 'var _ = utils.StringToBool', 'var _ = strconv.Atoi'] + a = lines.append + choices = {} + go_types = {} + go_parsers = {} + defaults = {} + multiopts = {''} + type_defs = [''] + for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)): + name = option.name.capitalize() + if isinstance(option, MultiOption): + go_types[name], go_parsers[name] = go_type_data(option.parser_func, type_defs) + multiopts.add(name) + else: + defaults[name] = option.defval_as_string + if option.choices: + choices[name] = option.choices + go_types[name] = f'{name}_Choice_Type' + go_parsers[name] = f'Parse_{name}(val)' + continue + go_types[name], go_parsers[name] = go_type_data(option.parser_func, type_defs) + + for oname in choices: + a(f'type {go_types[oname]} int') + for td in type_defs: + a(td) + a('type Config struct {') + for name, gotype in go_types.items(): + if name in multiopts: + a(f'{name} []{gotype}') + else: + a(f'{name} {gotype}') + a('}') + + a('func NewConfig() *Config {') + a('ans := Config{}') + a('var err error') + a('var val string') + for name, pname in go_parsers.items(): + if name in multiopts: + a(f'ans.{name} = make([]{go_types[name]}, 0, 8)') + continue + a(f'val = `{defaults[name]}`') + a(f'ans.{name}, err = {pname}') + a('if err != nil { panic(err) }') + a('return &ans') + a('}') + + def cval(x: str) -> str: + return x.replace('-', '_') + + for oname, choice_vals in choices.items(): + a('const (') + for i, c in enumerate(choice_vals): + c = cval(c) + if i == 0: + a(f'{oname}_{c} {oname}_Choice_Type = iota') + else: + a(f'{oname}_{c}') + a(')') + a(f'func (x {oname}_Choice_Type) String() string'' {') + a('switch x {') + a('default: return ""') + for c in choice_vals: + a(f'case {oname}_{cval(c)}: return "{c}"') + a('}''}') + a(f'func {go_parsers[oname].split("(")[0]}(val string) (ans {go_types[oname]}, err error) ''{') + a('switch val {') + for c in choice_vals: + a(f'case "{c}": return {oname}_{cval(c)}, nil') + vals = ', '.join(choice_vals) + a(f'default: return ans, fmt.Errorf("%#v is not a valid value for %s. Valid values are: %s", val, "{c}", "{vals}")') + a('}''}') + + a('func (c *Config) Parse(key, val string) (err error) {') + a('switch key {') + a('default: return fmt.Errorf("Unknown configuration key: %#v", key)') + for oname, pname in go_parsers.items(): + ol = oname.lower() + a(f'case "{ol}":') + a(f'var temp_val {go_types[oname]}') + a(f'temp_val, err = {pname}') + a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}') + if oname in multiopts: + a(f'c.{oname} = append(c.{oname}, temp_val)') + else: + a(f'c.{oname} = temp_val') + a('}') + a('return}') + return '\n'.join(lines) + + def main() -> None: # To use run it as: # kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten/file.py diff --git a/tools/utils/config.go b/tools/utils/config.go new file mode 100644 index 000000000..5fdd93368 --- /dev/null +++ b/tools/utils/config.go @@ -0,0 +1,32 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package utils + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +var _ = fmt.Print + +func StringToBool(x string) bool { + x = strings.ToLower(x) + return x == "y" || x == "yes" || x == "true" +} + +func ParseConfData(src io.Reader, callback func(key, val string, line int)) error { + scanner := bufio.NewScanner(src) + lnum := 0 + for scanner.Scan() { + line := strings.TrimLeft(scanner.Text(), " ") + lnum++ + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, val, _ := strings.Cut(line, " ") + callback(key, val, lnum) + } + return scanner.Err() +}