From 76ebc591db130761e6c6297c7bd69c20e11369d0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 5 Jan 2025 15:28:30 +0530 Subject: [PATCH] Make the effective configuration file for the kitty process available in the cache directory This can be parsed by kittens to load effective settings, thereby making things like --override and reloading of config also affect kittens that read kitty config. Still to be implemented on the kitten side. --- kitty/boss.py | 4 +++- kitty/conf/generate.py | 13 +++++++++---- kitty/conf/types.py | 13 ++++++++----- kitty/conf/utils.py | 27 +++++++++++++++------------ kitty/config.py | 36 ++++++++++++++++++++++++++++++++---- kitty/options/definition.py | 5 ++--- kitty/options/types.py | 3 +++ 7 files changed, 72 insertions(+), 29 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index f46def8799c..48761ff60fe 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -37,7 +37,7 @@ ) from .colors import ColorSchemes, theme_colors from .conf.utils import BadLine, KeyAction, to_cmdline -from .config import common_opts_as_dict, prepare_config_file_for_editing +from .config import common_opts_as_dict, prepare_config_file_for_editing, store_effective_config from .constants import ( RC_ENCRYPTION_PROTOCOL_VERSION, appname, @@ -398,6 +398,7 @@ def __init__( set_boss(self) self.mappings: Mappings = Mappings(global_shortcuts, self.refresh_active_tab_bar) self.notification_manager: NotificationManager = NotificationManager(debug=self.args.debug_keyboard or self.args.debug_rendering) + self.atexit.unlink(store_effective_config()) def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None: si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session) @@ -2730,6 +2731,7 @@ def load_config_file(self, *paths: str, apply_overrides: bool = True, overrides: clear_caches() from .guess_mime_type import clear_mime_cache clear_mime_cache() + store_effective_config() def safe_delete_temp_file(self, path: str) -> None: if is_path_in_temp_dir(path): diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index d9e42545169..53e95f2edf9 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -442,15 +442,20 @@ def generate_c_conversion(loc: str, ctypes: List[Union[Option, MultiOption]]) -> def write_output(loc: str, defn: Definition, extra_after_type_defn: str = '') -> None: cls, tc = generate_class(defn, loc) + ctypes = [] + has_secret = [] + for opt in defn.root_group.iter_all_non_groups(): + if isinstance(opt, (Option, MultiOption)) and opt.ctype: + ctypes.append(opt) + if getattr(opt, 'has_secret', False): + has_secret.append(opt.name) with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f: f.write(f'{cls}\n') f.write(extra_after_type_defn) + if has_secret: + f.write('\n\nsecret_options = ' + repr(tuple(has_secret))) with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f: f.write(f'{tc}\n') - ctypes = [] - for opt in defn.root_group.iter_all_non_groups(): - if isinstance(opt, (Option, MultiOption)) and opt.ctype: - ctypes.append(opt) if ctypes: c = generate_c_conversion(loc, ctypes) with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f: diff --git a/kitty/conf/types.py b/kitty/conf/types.py index df1c3fceff2..ee7de683c58 100644 --- a/kitty/conf/types.py +++ b/kitty/conf/types.py @@ -220,7 +220,8 @@ class Option: def __init__( self, name: str, defval: str, macos_default: Union[Unset, str], parser_func: ParserFuncType, - long_text: str, documented: bool, group: 'Group', choices: Tuple[str, ...], ctype: str + long_text: str, documented: bool, group: 'Group', choices: Tuple[str, ...], ctype: str, + has_secret: bool = False, ): self.name = name self.ctype = ctype @@ -231,6 +232,7 @@ def __init__( self.group = group self.parser_func = parser_func self.choices = choices + self.has_secret = has_secret @property def needs_coalescing(self) -> bool: @@ -292,12 +294,13 @@ def __init__(self, val_as_str: str, add_to_default: bool, documented: bool, only class MultiOption: - def __init__(self, name: str, parser_func: ParserFuncType, long_text: str, group: 'Group', ctype: str): + def __init__(self, name: str, parser_func: ParserFuncType, long_text: str, group: 'Group', ctype: str, has_secret: bool = False): self.name = name self.ctype = ctype self.parser_func = parser_func self.long_text = long_text self.group = group + self.has_secret = has_secret self.items: List[MultiVal] = [] def add_value(self, val_as_str: str, add_to_default: bool, documented: bool, only: Only) -> None: @@ -701,7 +704,7 @@ def add_option( documented: bool = True, add_to_default: bool = False, only: Only = '', macos_default: Union[Unset, str] = unset, choices: Tuple[str, ...] = (), - ctype: str = '', + ctype: str = '', has_secret: bool = False, ) -> None: if isinstance(defval, bool): defval = 'yes' if defval else 'no' @@ -715,13 +718,13 @@ def add_option( raise TypeError(f'Cannot specify macos_default for is_multiple option: {name} use only instead') is_new = name not in self.multi_option_map if is_new: - self.multi_option_map[name] = MultiOption(name, self.parser_func(option_type), long_text, self.current_group, ctype) + self.multi_option_map[name] = MultiOption(name, self.parser_func(option_type), long_text, self.current_group, ctype, has_secret) mopt = self.multi_option_map[name] if is_new: self.current_group.append(mopt) mopt.add_value(defval, add_to_default, documented, only) return - opt = Option(name, defval, macos_default, self.parser_func(option_type), long_text, documented, self.current_group, choices, ctype) + opt = Option(name, defval, macos_default, self.parser_func(option_type), long_text, documented, self.current_group, choices, ctype, has_secret) self.current_group.append(opt) self.option_map[name] = opt diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index dbdbee6645b..7a6c1177a3e 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -203,7 +203,8 @@ def parse_line( parse_conf_item: ItemParser, ans: Dict[str, Any], base_path_for_includes: str, - accumulate_bad_lines: Optional[List[BadLine]] = None + effective_config_lines: Callable[[str, str], None], + accumulate_bad_lines: Optional[List[BadLine]] = None, ) -> None: line = line.strip() if not line or line.startswith('#'): @@ -225,9 +226,7 @@ def parse_line( with currently_parsing.set_file(f''): _parse( NamedLineIterator(os.path.join(base_path_for_includes, ''), iter(os.environ[x].splitlines())), - parse_conf_item, - ans, - accumulate_bad_lines + parse_conf_item, ans, accumulate_bad_lines, effective_config_lines, ) return else: @@ -238,7 +237,7 @@ def parse_line( try: with open(val, encoding='utf-8', errors='replace') as include: with currently_parsing.set_file(val): - _parse(include, parse_conf_item, ans, accumulate_bad_lines) + _parse(include, parse_conf_item, ans, accumulate_bad_lines, effective_config_lines) except FileNotFoundError: log_error( 'Could not find included config file: {}, ignoring'. @@ -250,17 +249,22 @@ def parse_line( format(val) ) return - if not parse_conf_item(key, val, ans): + if parse_conf_item(key, val, ans): + effective_config_lines(key, line) + else: log_error(f'Ignoring unknown config key: {key}') + def _parse( lines: Iterable[str], parse_conf_item: ItemParser, ans: Dict[str, Any], - accumulate_bad_lines: Optional[List[BadLine]] = None + accumulate_bad_lines: Optional[List[BadLine]] = None, + effective_config_lines: Optional[Callable[[str, str], None]] = None, ) -> None: name = getattr(lines, 'name', None) + effective_config_lines = effective_config_lines or (lambda a, b: None) if name: base_path_for_includes = os.path.abspath(name) if name.endswith(os.path.sep) else os.path.dirname(os.path.abspath(name)) else: @@ -297,7 +301,7 @@ def _parse( next_line = '' try: with currently_parsing.set_line(line, line_num): - parse_line(line, parse_conf_item, ans, base_path_for_includes, accumulate_bad_lines) + parse_line(line, parse_conf_item, ans, base_path_for_includes, effective_config_lines, accumulate_bad_lines) except Exception as e: if accumulate_bad_lines is None: raise @@ -310,11 +314,10 @@ def parse_config_base( lines: Iterable[str], parse_conf_item: ItemParser, ans: Dict[str, Any], - accumulate_bad_lines: Optional[List[BadLine]] = None + accumulate_bad_lines: Optional[List[BadLine]] = None, + effective_config_lines: Optional[Callable[[str, str], None]] = None, ) -> None: - _parse( - lines, parse_conf_item, ans, accumulate_bad_lines - ) + _parse(lines, parse_conf_item, ans, accumulate_bad_lines, effective_config_lines) def merge_dicts(defaults: Dict[str, Any], newvals: Dict[str, Any]) -> Dict[str, Any]: diff --git a/kitty/config.py b/kitty/config.py index df8158f8108..153ffcaa83e 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -6,7 +6,7 @@ from collections.abc import Generator, Iterable from contextlib import contextmanager, suppress from functools import partial -from typing import Any, Optional +from typing import Any, Callable, Optional from .conf.utils import BadLine, parse_config_base from .conf.utils import load_config as _load_config @@ -140,24 +140,37 @@ def finalize_mouse_mappings(opts: Options, accumulate_bad_lines: Optional[list[B opts.mousemap = mousemap -def parse_config(lines: Iterable[str], accumulate_bad_lines: Optional[list[BadLine]] = None) -> dict[str, Any]: +def parse_config( + lines: Iterable[str], accumulate_bad_lines: Optional[list[BadLine]] = None, effective_config_lines: Optional[Callable[[str, str], None]] = None +) -> dict[str, Any]: from .options.parse import create_result_dict, parse_conf_item ans: dict[str, Any] = create_result_dict() parse_config_base( lines, parse_conf_item, ans, - accumulate_bad_lines=accumulate_bad_lines + accumulate_bad_lines=accumulate_bad_lines, + effective_config_lines=effective_config_lines, ) return ans +effective_config_lines: list[str] = [] + + def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[list[BadLine]] = None) -> Options: from .options.parse import merge_result_dicts + from .options.types import secret_options + del effective_config_lines[:] + + def add_effective_config_line(key: str, line: str) -> None: + if key not in secret_options: + effective_config_lines.append(line) overrides = tuple(overrides) if overrides is not None else () opts_dict, found_paths = _load_config( - defaults, partial(parse_config, accumulate_bad_lines=accumulate_bad_lines), merge_result_dicts, *paths, overrides=overrides) + defaults, partial(parse_config, accumulate_bad_lines=accumulate_bad_lines, effective_config_lines=add_effective_config_line), + merge_result_dicts, *paths, overrides=overrides) opts = Options(opts_dict) opts.alias_map = build_action_aliases(opts.kitten_alias, 'kitten') @@ -178,6 +191,21 @@ def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumula return opts +def store_effective_config() -> str: + import os + import stat + import tempfile + dest = os.path.join(cache_dir(), 'effective-config') + os.makedirs(dest, exist_ok=True) + raw = '\n'.join(effective_config_lines) + with suppress(FileNotFoundError), tempfile.NamedTemporaryFile('w', dir=dest) as tf: + os.chmod(tf.name, stat.S_IRUSR | stat.S_IWUSR) + print(raw, file=tf) + path = os.path.join(dest, f'{os.getpid()}') + os.replace(tf.name, path) + return path + + class KittyCommonOpts(TypedDict): select_by_word_characters: str open_url_with: list[str] diff --git a/kitty/options/definition.py b/kitty/options/definition.py index bc3ef36b192..28659292677 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3002,7 +3002,7 @@ opt('+remote_control_password', '', option_type='remote_control_password', - add_to_default=False, + add_to_default=False, has_secret=True, long_text=''' Allow other programs to control kitty using passwords. This option can be specified multiple times to add multiple passwords. If no passwords are present @@ -3210,8 +3210,7 @@ ''' ) -opt('file_transfer_confirmation_bypass', '', - long_text=''' +opt('file_transfer_confirmation_bypass', '', has_secret=True, long_text=''' The password that can be supplied to the :doc:`file transfer kitten ` to skip the transfer confirmation prompt. This should only be used when initiating transfers from trusted computers, over trusted networks diff --git a/kitty/options/types.py b/kitty/options/types.py index f07f0bb91ac..823d15d6d34 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -1052,3 +1052,6 @@ def __setattr__(self, key: str, val: typing.Any) -> typing.Any: 'selection_foreground' 'selection_background' }) + + +secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass') \ No newline at end of file