diff --git a/gen-config.py b/gen-config.py index 8e0fcf4b2..8fdbc2a8a 100755 --- a/gen-config.py +++ b/gen-config.py @@ -38,6 +38,8 @@ 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/kittens/diff/options/parse.py b/kittens/diff/options/parse.py index 36af21da5..6ba056c03 100644 --- a/kittens/diff/options/parse.py +++ b/kittens/diff/options/parse.py @@ -93,7 +93,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]: } -actions = frozenset(('map',)) +actions: typing.FrozenSet[str] = frozenset(('map',)) def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: diff --git a/kittens/ssh/config.py b/kittens/ssh/config.py new file mode 100644 index 000000000..00b739df1 --- /dev/null +++ b/kittens/ssh/config.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2022, Kovid Goyal + + +import os +from typing import Any, Dict, Iterable, Optional + +from kitty.conf.utils import ( + load_config as _load_config, parse_config_base, resolve_config +) +from kitty.constants import config_dir + +from .options.types import Options as SSHOptions, defaults + +SYSTEM_CONF = '/etc/xdg/kitty/ssh.conf' +defconf = os.path.join(config_dir, 'ssh.conf') + + +def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> Dict[str, SSHOptions]: + from .options.parse import ( + create_result_dict, merge_result_dicts, parse_conf_item + ) + from .options.utils import init_results_dict + + def parse_config(lines: Iterable[str]) -> Dict[str, Any]: + ans: Dict[str, Any] = init_results_dict(create_result_dict()) + parse_config_base(lines, parse_conf_item, ans) + return ans + + overrides = tuple(overrides) if overrides is not None else () + opts_dict, paths = _load_config(defaults, parse_config, merge_result_dicts, *paths, overrides=overrides) + ans: Dict[str, SSHOptions] = {} + for hostname, host_opts_dict in opts_dict['per_host_dicts'].items(): + opts = SSHOptions(host_opts_dict) + opts.config_paths = paths + opts.config_overrides = overrides + ans[hostname] = opts + return ans + + +def init_config() -> Dict[str, SSHOptions]: + config = tuple(resolve_config(SYSTEM_CONF, defconf)) + opts_dict = load_config(*config) + return opts_dict diff --git a/kittens/ssh/options/__init__.py b/kittens/ssh/options/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/ssh/options/definition.py b/kittens/ssh/options/definition.py new file mode 100644 index 000000000..8705b4190 --- /dev/null +++ b/kittens/ssh/options/definition.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2021, Kovid Goyal + +# After editing this file run ./gen-config.py to apply the changes + +from kitty.conf.types import Definition + + +definition = Definition( + 'kittens.ssh', +) + +agr = definition.add_group +egr = definition.end_group +opt = definition.add_option + +agr('global', 'Global') # {{{ + +opt('hostname', '*', option_type='hostname', + long_text=''' +The hostname the following options apply to. A glob pattern to match multiple +hosts can be used. When not specified options apply to all hosts, until the +first hostname specification is found. Note that the hostname this matches +against is the hostname used by the remote computer, not the name you pass +to SSH to connect to it. +''' + ) +egr() # }}} diff --git a/kittens/ssh/options/parse.py b/kittens/ssh/options/parse.py new file mode 100644 index 000000000..53a106e51 --- /dev/null +++ b/kittens/ssh/options/parse.py @@ -0,0 +1,42 @@ +# generated by gen-config.py DO NOT edit + +import typing +from kittens.ssh.options.utils import hostname +from kitty.conf.utils import merge_dicts + + +class Parser: + + def hostname(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + hostname(val, ans) + + +def create_result_dict() -> typing.Dict[str, typing.Any]: + return { + } + + +actions: typing.FrozenSet[str] = frozenset(()) + + +def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: + ans = {} + for k, v in defaults.items(): + if isinstance(v, dict): + ans[k] = merge_dicts(v, vals.get(k, {})) + elif k in actions: + ans[k] = v + vals.get(k, []) + else: + ans[k] = vals.get(k, v) + return ans + + +parser = Parser() + + +def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool: + func = getattr(parser, key, None) + if func is not None: + func(val, ans) + return True + return False diff --git a/kittens/ssh/options/types.py b/kittens/ssh/options/types.py new file mode 100644 index 000000000..b19cfd623 --- /dev/null +++ b/kittens/ssh/options/types.py @@ -0,0 +1,61 @@ +# generated by gen-config.py DO NOT edit + +import typing + + +option_names = ( # {{{ + 'hostname',) # }}} + + +class Options: + hostname: 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() diff --git a/kittens/ssh/options/utils.py b/kittens/ssh/options/utils.py new file mode 100644 index 000000000..280c4e700 --- /dev/null +++ b/kittens/ssh/options/utils.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2022, Kovid Goyal + +from typing import Any, Dict, Optional + + +def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]: + ans['current_hostname'] = '*' + ans['current_host_dict'] = chd = {'hostname': '*'} + ans['per_host_dicts'] = {'*': chd} + return ans + + +ignored_dict_keys = tuple(init_results_dict({})) + + +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['current_hostname'] + if val != ch: + hd = dict_with_parse_results.copy() + for k in ignored_dict_keys: + del hd[k] + phd = dict_with_parse_results['per_host_dicts'] + phd[ch] = hd + dict_with_parse_results.clear() + dict_with_parse_results['per_host_dicts'] = phd + dict_with_parse_results['current_hostname'] = val + dict_with_parse_results['current_host_dict'] = phd.setdefault(val, {'hostname': val}) + return val diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index f522f37de..3b47d4712 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -306,7 +306,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]: t('') t('') - t(f'actions = frozenset({tuple(defn.actions)!r})') + t(f'actions: typing.FrozenSet[str] = frozenset({tuple(defn.actions)!r})') t('') t('') t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:') diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 3237b7e79..4ea7d98dd 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1346,7 +1346,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]: } -actions = frozenset(('map', 'mouse_map')) +actions: typing.FrozenSet[str] = frozenset(('map', 'mouse_map')) def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: