diff --git a/meson.build b/meson.build index 29f27ec9..fb2c8937 100644 --- a/meson.build +++ b/meson.build @@ -136,6 +136,16 @@ version = custom_target( output: 'version.h', command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) +python = find_program('python3', native: true) +generate_builtin_terminfo_py = files('scripts/generate-builtin-terminfo.py') +foot_terminfo = files('foot.info') +builtin_terminfo = custom_target( + 'generate_builtin_terminfo', + output: 'foot-terminfo.h', + command: [python, generate_builtin_terminfo_py, + '@default_terminfo@', foot_terminfo, 'foot', '@OUTPUT@'] +) + common = static_library( 'common', 'log.c', 'log.h', @@ -213,7 +223,7 @@ executable( 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', - wl_proto_src + wl_proto_headers, version, + builtin_terminfo, wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, tllist, fcft], link_with: pgolib, diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py new file mode 100755 index 00000000..dd27913b --- /dev/null +++ b/scripts/generate-builtin-terminfo.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 + +import argparse +import re +import sys + +from typing import Dict, Union + + +class Capability: + def __init__(self, name: str, value: Union[bool, int, str]): + self._name = name + self._value = value + + @property + def name(self) -> str: + return self._name + + @property + def value(self) -> Union[bool, int, str]: + return self._value + + def __lt__(self, other): + return self._name < other._name + + def __le__(self, other): + return self._name <= other._name + + def __eq__(self, other): + return self._name == other._name + + def __ne__(self, other): + return self._name != other._name + + def __gt__(self, other): + return self._name > other._name + + def __ge__(self, other): + return self._name >= other._name + + +class BoolCapability(Capability): + def __init__(self, name: str): + super().__init__(name, True) + + +class IntCapability(Capability): + pass + + +class StringCapability(Capability): + pass + + +class Fragment: + def __init__(self, name: str, description: str): + self._name = name + self._description = description + self._caps = {} + + @property + def name(self) -> str: + return self._name + + @property + def description(self) -> str: + return self._description + + @property + def caps(self) -> Dict[str, Capability]: + return self._caps + + def add_capability(self, cap: Capability): + assert cap.name not in self._caps + self._caps[cap.name] = cap + + def del_capability(self, name: str): + del self._caps[name] + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('source_entry_name') + parser.add_argument('source', type=argparse.FileType('r')) + parser.add_argument('target_entry_name') + parser.add_argument('target', type=argparse.FileType('w')) + + opts = parser.parse_args() + source_entry_name = opts.source_entry_name + target_entry_name = opts.target_entry_name + source = opts.source + target = opts.target + + lines = [] + for l in source.readlines(): + l = l.strip() + if l.startswith('#'): + continue + lines.append(l) + + fragments = {} + cur_fragment = None + + for m in re.finditer( + r'(?P(?P[-+\w@]+)\|(?P.+?),)|' + r'(?P(?P\w+),)|' + r'(?P(?P\w+)#(?P(0x)?[0-9a-fA-F]+),)|' + r'(?P(?P\w+)=(?P(.+?)),)', + ''.join(lines)): + + if m.group('name') is not None: + name = m.group('entry_name') + description = m.group('entry_desc') + + assert name not in fragments + fragments[name] = Fragment(name, description) + cur_fragment = fragments[name] + + elif m.group('bool_cap') is not None: + name = m.group('bool_name') + cur_fragment.add_capability(BoolCapability(name)) + + elif m.group('int_cap') is not None: + name = m.group('int_name') + value = int(m.group('int_val'), 0) + cur_fragment.add_capability(IntCapability(name, value)) + + elif m.group('str_cap') is not None: + name = m.group('str_name') + value = m.group('str_val') + cur_fragment.add_capability(StringCapability(name, value)) + + else: + assert False + + # Expand ‘use’ capabilities + for frag in fragments.values(): + for cap in frag.caps.values(): + if cap.name == 'use': + use_frag = fragments[cap.value] + for use_cap in use_frag.caps.values(): + frag.add_capability(use_cap) + + + frag.del_capability(cap.name) + break + + entry = fragments[source_entry_name] + + try: + entry.del_capability('RGB') + except KeyError: + pass + + entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(StringCapability('TN', target_entry_name)) + + target.write('#pragma once\n') + target.write('\n') + # target.write('enum terminfo_capability_type {\n') + # target.write(' TERMINFO_CAP_BOOL,\n') + # target.write(' TERMINFO_CAP_INT,\n') + # target.write(' TERMINFO_CAP_STRING,\n') + # target.write('};\n') + # target.write('\n') + target.write('struct foot_terminfo_entry {\n') + target.write(' const char *name;\n') + # target.write(' enum terminfo_capability_type type;\n') + target.write(' const char *value;\n') + target.write('};\n') + target.write('\n') + target.write( + 'static const struct foot_terminfo_entry terminfo_capabilities[] = {\n') + + for cap in sorted(entry.caps.values()): + if isinstance(cap, BoolCapability): + continue + + name = cap.name + value = str(cap.value) + + # Expand \E to literal ESC in non-parameterized capabilities + if '%' not in value: + value = re.sub(r'\\E([0-7])', r'\\033" "\1', value) + value = re.sub(r'\\E', r'\\033', value) + else: + # Need to double-escape \E in C string literals + value = value.replace('\\E', '\\\\E') + + # Don’t escape ‘:’ + value = value.replace('\\:', ':') + + # Do escape ‘“‘ + name = name.replace('"', '\"') + value = value.replace('"', '\"') + target.write(f' {{"{name}", "{value}"}},\n') + + target.write('};\n') + + +if __name__ == '__main__': + sys.exit(main())