Skip to content

Commit

Permalink
Allow dynamically generating configuration by running an arbitrary pr…
Browse files Browse the repository at this point in the history
…ogram using the new geninclude directive
  • Loading branch information
kovidgoyal committed Jan 6, 2025
1 parent 6d90813 commit 1eeea70
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 192 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Detailed list of changes

- diff kitten: Automatically use dark/light color scheme based on the color scheme of the parent terminal. Can be controlled via the new :opt:`kitten-diff.color_scheme` option. Note that this is a **default behavior change** (:iss:`8170`)

- Allow dynamically generating configuration by running an arbitrary program using the new :code:`geninclude` directive in :file:`kitty.conf`

- When a program running in kitty reports progress of a task display it as a percentage in the tab title. Controlled by the :opt:`tab_title_template` option

- When mapping a custom kitten allow using shell escaping for the kitten path (:iss:`8178`)
Expand Down
10 changes: 9 additions & 1 deletion docs/conf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,21 @@ expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if
to detect the operating system. It is ``linux``, ``macos`` or ``bsd``.
Also, you can use :code:`globinclude` to include files
matching a shell glob pattern and :code:`envinclude` to include configuration
from environment variables. For example::
from environment variables. Finally, you can dynamically generate configuration
by running a program using :code:`geninclude`. For example::

# Include other.conf
include other.conf
# Include *.conf files from all subdirs of kitty.d inside the kitty config dir
globinclude kitty.d/**/*.conf
# Include the *contents* of all env vars starting with KITTY_CONF_
envinclude KITTY_CONF_*
# Run the script dynamic.py placed in the same directory as this config file
# and include its :file:`STDOUT`. Note that Python scripts are fastest
# as they use the embedded Python interpreter, but any executable script
# or program is supported, in any language. Remember to mark the script
# file executable.
geninclude dynamic.py


.. note:: Syntax highlighting for :file:`kitty.conf` in vim is available via
Expand Down
88 changes: 79 additions & 9 deletions kitty/conf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,67 @@ def __iter__(self) -> Iterator[str]:
return self.lines


def pygeninclude(path: str) -> list[str]:
import io
import runpy
before = sys.stdout
buf = sys.stdout = io.StringIO()
try:
runpy.run_path(path, run_name='__main__')
finally:
sys.stdout = before
return buf.getvalue().splitlines()


def geninclude(path: str) -> list[str]:
old = os.environ.get('KITTY_OS')
os.environ['KITTY_OS'] = os_name()
try:
if path.endswith('.py'):
return pygeninclude(path)
import subprocess
cp = subprocess.run([path], stdout=subprocess.PIPE, text=True)
return cp.stdout.splitlines()
finally:
if old is None:
os.environ.pop('KITTY_OS', None)
else:
os.environ['KITTY_OS'] = old



include_keys = 'include', 'globinclude', 'envinclude', 'geninclude'


class RecursiveInclude(Exception):
pass


class Memory:

def __init__(self, accumulate_bad_lines: Optional[List[BadLine]]) -> None:
self.s: set[str] = set()
if accumulate_bad_lines is None:
accumulate_bad_lines = []
self.accumulate_bad_lines = accumulate_bad_lines

def seen(self, path: str) -> bool:
key = os.path.normpath(path)
if key in self.s:
self.accumulate_bad_lines.append(BadLine(currently_parsing.number, currently_parsing.line.rstrip(), RecursiveInclude(
f'The file {path} has already been included, ignoring'), currently_parsing.file))
return True
self.s.add(key)
return False


def parse_line(
line: str,
parse_conf_item: ItemParser,
ans: Dict[str, Any],
base_path_for_includes: str,
effective_config_lines: Callable[[str, str], None],
memory: Memory,
accumulate_bad_lines: Optional[List[BadLine]] = None,
) -> None:
line = line.strip()
Expand All @@ -214,7 +269,7 @@ def parse_line(
log_error(f'Ignoring invalid config line: {line!r}')
return
key, val = m.groups()
if key in ('include', 'globinclude', 'envinclude'):
if key.endswith('include') and key in include_keys:
val = expandvars(os.path.expanduser(val.strip()), {'KITTY_OS': os_name()})
if key == 'globinclude':
from pathlib import Path
Expand All @@ -226,23 +281,37 @@ 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, effective_config_lines,
parse_conf_item, ans, memory, accumulate_bad_lines, effective_config_lines
)
return
elif key == 'geninclude':
if not os.path.isabs(val):
val = os.path.join(base_path_for_includes, val)
if not memory.seen(val):
try:
lines = geninclude(val)
except Exception:
log_error(f'Could not process geninclude {val}, ignoring')
else:
with currently_parsing.set_file(f'<get: {val}>'):
_parse(
NamedLineIterator(os.path.join(base_path_for_includes, ''), iter(lines)),
parse_conf_item, ans, memory, accumulate_bad_lines, effective_config_lines
)
return
else:
if not os.path.isabs(val):
val = os.path.join(base_path_for_includes, val)
vals = (val,)
for val in vals:
if memory.seen(val):
continue
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, effective_config_lines)
_parse(include, parse_conf_item, ans, memory, accumulate_bad_lines, effective_config_lines)
except FileNotFoundError:
log_error(
'Could not find included config file: {}, ignoring'.
format(val)
)
log_error(f'Could not find included config file: {val}, ignoring')
except OSError:
log_error(
'Could not read from included config file: {}, ignoring'.
Expand All @@ -260,6 +329,7 @@ def _parse(
lines: Iterable[str],
parse_conf_item: ItemParser,
ans: Dict[str, Any],
memory: Memory,
accumulate_bad_lines: Optional[List[BadLine]] = None,
effective_config_lines: Optional[Callable[[str, str], None]] = None,
) -> None:
Expand Down Expand Up @@ -301,7 +371,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, effective_config_lines, accumulate_bad_lines)
parse_line(line, parse_conf_item, ans, base_path_for_includes, effective_config_lines, memory, accumulate_bad_lines)
except Exception as e:
if accumulate_bad_lines is None:
raise
Expand All @@ -317,7 +387,7 @@ def parse_config_base(
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, effective_config_lines)
_parse(lines, parse_conf_item, ans, Memory(accumulate_bad_lines), accumulate_bad_lines, effective_config_lines)


def merge_dicts(defaults: Dict[str, Any], newvals: Dict[str, Any]) -> Dict[str, Any]:
Expand Down
Loading

0 comments on commit 1eeea70

Please sign in to comment.