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.
This commit is contained in:
Kovid Goyal 2025-01-05 15:28:30 +05:30
parent b0c9262652
commit 76ebc591db
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
7 changed files with 73 additions and 30 deletions

View file

@ -37,7 +37,7 @@ from .clipboard import (
)
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 @@ class Boss:
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 @@ class Boss:
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):

View file

@ -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)
with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f:
f.write(f'{cls}\n')
f.write(extra_after_type_defn)
with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f:
f.write(f'{tc}\n')
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')
if ctypes:
c = generate_c_conversion(loc, ctypes)
with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f:

View file

@ -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 @@ class Option:
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 @@ class MultiVal:
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 @@ class Definition:
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 @@ class Definition:
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

View file

@ -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'<env var: {x}>'):
_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]:

View file

@ -6,7 +6,7 @@ import os
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]

View file

@ -3002,7 +3002,7 @@ their stdout/stderr/stdin no longer work.
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 @@ is applied. See also :opt:`clipboard_control`.
'''
)
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
</kittens/transfer>` to skip the transfer confirmation prompt. This should only
be used when initiating transfers from trusted computers, over trusted networks

View file

@ -1052,3 +1052,6 @@ nullable_colors = frozenset({
'selection_foreground'
'selection_background'
})
secret_options = ('remote_control_password', 'file_transfer_confirmation_bypass')