mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-02-01 11:34:59 +01:00
223 lines
8.0 KiB
Python
223 lines
8.0 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 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
|
|
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 = []
|
|
if opts.background_opacity < 1.0 and opts.macos_titlebar_color > 0:
|
|
log_error('Cannot use both macos_titlebar_color and background_opacity')
|
|
opts.macos_titlebar_color = 0
|
|
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,
|
|
}
|