Files
kitty/kitty/config.py
Jake Stewart 1a62cde212 Add option to generate 256 color palette from first 16 colors
Needs more work to test and possibly implement reporting and setting
of the nullable color table colors.

Fixes #9426
2026-03-27 20:01:14 +05:30

222 lines
7.9 KiB
Python

#!/usr/bin/env python
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import json
import os
from collections.abc import Callable, Generator, Iterable
from contextlib import contextmanager, suppress
from functools import partial
from typing import Any
from .conf.utils import BadLine, parse_config_base
from .conf.utils import load_config as _load_config
from .constants import cache_dir, defconf
from .options.types import Options, defaults, option_names
from .options.utils import KeyboardMode, KeyboardModeMap, KeyDefinition, MouseMap, MouseMapping, build_action_aliases
from .typing_compat import TypedDict
from .utils import log_error
def option_names_for_completion() -> tuple[str, ...]:
return option_names
def atomic_save(data: bytes, path: str) -> None:
import shutil
import tempfile
path = os.path.realpath(path)
fd, p = tempfile.mkstemp(dir=os.path.dirname(path), suffix='.tmp')
try:
with os.fdopen(fd, 'wb') as f:
f.write(data)
with suppress(FileNotFoundError):
shutil.copystat(path, p)
os.utime(p)
os.replace(p, path)
finally:
try:
os.remove(p)
except FileNotFoundError:
pass
except Exception as err:
log_error(f'Failed to delete temp file {p} for atomic save with error: {err}')
@contextmanager
def cached_values_for(name: str) -> Generator[dict[str, Any], None, None]:
cached_path = os.path.join(cache_dir(), f'{name}.json')
cached_values: dict[str, Any] = {}
try:
with open(cached_path, 'rb') as f:
cached_values.update(json.loads(f.read().decode('utf-8')))
except FileNotFoundError:
pass
except Exception as err:
log_error(f'Failed to load cached in {name} values with error: {err}')
yield cached_values
try:
data = json.dumps(cached_values).encode('utf-8')
atomic_save(data, cached_path)
except Exception as err:
log_error(f'Failed to save cached values with error: {err}')
def commented_out_default_config() -> str:
from .options.definition import definition
return '\n'.join(definition.as_conf(commented=True))
def prepare_config_file_for_editing() -> str:
if not os.path.exists(defconf):
d = os.path.dirname(defconf)
with suppress(FileExistsError):
os.makedirs(d)
with open(defconf, 'w', encoding='utf-8') as f:
f.write(commented_out_default_config())
return defconf
def finalize_keys(opts: Options, accumulate_bad_lines: list[BadLine] | None = None) -> None:
defns: list[KeyDefinition] = []
for d in opts.map:
if d is None: # clear_all_shortcuts
defns = [] # type: ignore
else:
try:
defns.append(d.resolve_and_copy(opts.kitty_mod))
except Exception as err:
if accumulate_bad_lines is None:
log_error(f'Ignoring map with invalid action: {d.definition}. Error: {err}')
else:
accumulate_bad_lines.append(BadLine(d.definition_location.number, d.definition_location.line, err, d.definition_location.file))
modes: KeyboardModeMap = {'': KeyboardMode()}
for defn in defns:
if defn.options.new_mode:
modes[defn.options.new_mode] = nm = KeyboardMode(defn.options.new_mode)
nm.on_unknown = defn.options.on_unknown
nm.on_action = defn.options.on_action
nm.timeout = defn.options.timeout if defn.options.timeout is not None else opts.map_timeout
defn.definition = f'push_keyboard_mode {defn.options.new_mode}'
try:
m = modes[defn.options.mode]
except KeyError:
kerr = f'The keyboard mode {defn.options.mode} is unknown, ignoring the mapping'
if accumulate_bad_lines is None:
log_error(kerr)
else:
dl = defn.definition_location
accumulate_bad_lines.append(BadLine(dl.number, dl.line, KeyError(kerr), dl.file))
continue
items = m.keymap[defn.trigger]
if defn.is_sequence:
items = m.keymap[defn.trigger] = [kd for kd in items if defn.rest != kd.rest or defn.options.when_focus_on != kd.options.when_focus_on]
items.append(defn)
opts.keyboard_modes = modes
def finalize_mouse_mappings(opts: Options, accumulate_bad_lines: list[BadLine] | None = None) -> None:
defns: list[MouseMapping] = []
for d in opts.mouse_map:
if d is None: # clear_all_mouse_actions
defns = [] # type: ignore
else:
try:
defns.append(d.resolve_and_copy(opts.kitty_mod))
except Exception as err:
if accumulate_bad_lines is None:
log_error(f'Ignoring mouse_map with invalid action: {d.definition}. Error: {err}')
else:
accumulate_bad_lines.append(BadLine(d.definition_location.number, d.definition_location.line, err, d.definition_location.file))
mousemap: MouseMap = {}
for defn in defns:
if defn.definition:
mousemap[defn.trigger] = defn.definition
else:
mousemap.pop(defn.trigger, None)
opts.mousemap = mousemap
def parse_config(
lines: Iterable[str], accumulate_bad_lines: list[BadLine] | None = None, effective_config_lines: Callable[[str, str], None] | 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,
effective_config_lines=effective_config_lines,
)
return ans
effective_config_lines: list[str] = []
def load_config(*paths: str, overrides: Iterable[str] | None = None, accumulate_bad_lines: list[BadLine] | None = 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, 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')
opts.alias_map.update(build_action_aliases(opts.action_alias))
finalize_keys(opts, accumulate_bad_lines)
finalize_mouse_mappings(opts, accumulate_bad_lines)
# delete no longer needed definitions, replacing with empty placeholders
opts.kitten_alias = {}
opts.action_alias = {}
opts.mouse_map = []
opts.map = []
opts.config_paths = found_paths
opts.all_config_paths = paths
opts.config_overrides = overrides
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]
url_prefixes: tuple[str, ...]
def common_opts_as_dict(opts: Options | None = None) -> KittyCommonOpts:
if opts is None:
opts = defaults
return {
'select_by_word_characters': opts.select_by_word_characters,
'open_url_with': opts.open_url_with,
'url_prefixes': opts.url_prefixes,
}