From b8fc56ecb49e26ab3fcc37fae7565c279fa43b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 2 Jan 2022 11:06:09 +0100 Subject: [PATCH 01/10] script: add generate-builtin-terminfo.py This script parses a terminfo source file and generates a C header file with a static struct containing terminfo names and their values. The table is sorted on the capability names --- meson.build | 12 +- scripts/generate-builtin-terminfo.py | 202 +++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100755 scripts/generate-builtin-terminfo.py 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()) From 1a91cbecc7c3931f0b406fe9c4d5c98efc7d8863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 2 Jan 2022 18:28:10 +0100 Subject: [PATCH 02/10] uri: move hex2nibble() to util.h --- uri.c | 25 ++----------------------- util.h | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/uri.c b/uri.c index e747991d..39073bde 100644 --- a/uri.c +++ b/uri.c @@ -9,30 +9,9 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" +#include "util.h" #include "xmalloc.h" -enum { - HEX_DIGIT_INVALID = 16 -}; - -static uint8_t -hex2nibble(char c) -{ - switch (c) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - return c - '0'; - - case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': - return c - 'a' + 10; - - case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': - return c - 'A' + 10; - } - - return HEX_DIGIT_INVALID; -} - bool uri_parse(const char *uri, size_t len, char **scheme, char **user, char **password, char **host, @@ -118,7 +97,7 @@ uri_parse(const char *uri, size_t len, LOG_DBG("user: \"%.*s\"", (int)user_len, start); } - + start = user_pw_end + 1; left = len - (start - uri); auth_left -= user_pw_len + 1; diff --git a/util.h b/util.h index aa9fc8ba..683dbd4a 100644 --- a/util.h +++ b/util.h @@ -35,3 +35,25 @@ sdbm_hash(const char *s) return hash; } + +enum { + HEX_DIGIT_INVALID = 16 +}; + +static inline uint8_t +hex2nibble(char c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + } + + return HEX_DIGIT_INVALID; +} From 3fa6bec91213525b031f76a67c6b685912dc6720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 2 Jan 2022 18:28:40 +0100 Subject: [PATCH 03/10] dcs: implement XTGETTCAP We emit one DCS reply for each queried capability (like Kitty, but unlike XTerm), as this allows us to a) not skip any capabilities in the query, and b) reply with correct success/fail flag for each capability. We do not batch the entire reply - as soon as the reply for _one_ capability is done, we write it to the PTY. Closes #846 --- dcs.c | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/dcs.c b/dcs.c index 79b04df9..6fed8010 100644 --- a/dcs.c +++ b/dcs.c @@ -1,10 +1,14 @@ #include "dcs.h" +#include #define LOG_MODULE "dcs" #define LOG_ENABLE_DBG 0 #include "log.h" +#include "foot-terminfo.h" #include "sixel.h" +#include "util.h" #include "vt.h" +#include "xmalloc.h" static void bsu(struct terminal *term) @@ -30,6 +34,139 @@ esu(struct terminal *term) term_disable_app_sync_updates(term); } +/* Decode hex-encoded string *inline*. NULL terminates */ +static char * +hex_decode(const char *s, size_t len) +{ + if (len % 2) + return NULL; + + char *hex = xmalloc(len / 2 + 1); + char *o = hex; + + /* TODO: error checking */ + for (size_t i = 0; i < len; i += 2) { + uint8_t nib1 = hex2nibble(*s); s++; + uint8_t nib2 = hex2nibble(*s); s++; + + if (nib1 == HEX_DIGIT_INVALID || nib2 == HEX_DIGIT_INVALID) + goto err; + + *o = nib1 << 4 | nib2; o++; + } + + *o = '\0'; + return hex; + +err: + free(hex); + return NULL; +} + +UNITTEST +{ + /* Verify table is sorted */ + for (size_t i = 1; i < ALEN(terminfo_capabilities); i++) { + xassert(strcmp(terminfo_capabilities[i - 1].name, + terminfo_capabilities[i].name ) <= 0); + } +} + +static int +terminfo_entry_compar(const void *_key, const void *_entry) +{ + const char *key = _key; + const struct foot_terminfo_entry *entry = _entry; + + return strcmp(key, entry->name); +} + +static void +xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) +{ + char *name = hex_decode(hex_cap_name, len); + if (name == NULL) + goto err; + + const struct foot_terminfo_entry *entry = + bsearch(name, terminfo_capabilities, ALEN(terminfo_capabilities), + sizeof(*entry), &terminfo_entry_compar); + + LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s", + name, (int)len, hex_cap_name, + entry != NULL ? entry->value : ""); + + if (entry == NULL) + goto err; + + /* + * Reply format: + * \EP 1 + r cap=value \E\\ + * Where ‘cap’ and ‘value are hex encoded ascii strings + */ + char *reply = xmalloc( + 5 + /* DCS 1 + r (\EP1+r) */ + len + /* capability name, hex encoded */ + 1 + /* ‘=’ */ + strlen(entry->value) * 2 + /* capability value, hex encoded */ + 2 + /* ST (\E\\) */ + 1); + + int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name); + + for (const char *c = entry->value; *c != '\0'; c++) { + uint8_t nib1 = (uint8_t)*c >> 4; + uint8_t nib2 = (uint8_t)*c & 0xf; + + reply[idx] = nib1 >= 0xa ? 'A' + nib1 - 0xa : '0' + nib1; idx++; + reply[idx] = nib2 >= 0xa ? 'A' + nib2 - 0xa : '0' + nib2; idx++; + } + + reply[idx] = '\033'; idx++; + reply[idx] = '\\'; idx++; + term_to_slave(term, reply, idx); + + free(reply); + goto out; + +err: + term_to_slave(term, "\033P0+r", 5); + term_to_slave(term, hex_cap_name, len); + term_to_slave(term, "\033\\", 2); + +out: + free(name); +} + +static void +xtgettcap_unhook(struct terminal *term) +{ + size_t left = term->vt.dcs.idx; + + const char *const end = (const char *)&term->vt.dcs.data[left]; + const char *p = (const char *)term->vt.dcs.data; + + while (true) { + const char *sep = memchr(p, ';', left); + size_t cap_len; + + if (sep == NULL) { + /* Last capability */ + cap_len = end - p; + } else { + cap_len = sep - p; + } + + xtgettcap_reply(term, p, cap_len); + + left -= cap_len + 1; + p += cap_len + 1; + + if (sep == NULL) + break; + } +} + void dcs_hook(struct terminal *term, uint8_t final) { @@ -67,6 +204,14 @@ dcs_hook(struct terminal *term, uint8_t final) break; } break; + + case '+': + switch (final) { + case 'q': /* XTGETTCAP */ + term->vt.dcs.unhook_handler = &xtgettcap_unhook; + break; + } + break; } } @@ -93,7 +238,8 @@ ensure_size(struct terminal *term, size_t required_size) void dcs_put(struct terminal *term, uint8_t c) { - LOG_DBG("PUT: %c", c); + /* LOG_DBG("PUT: %c", c); */ + if (term->vt.dcs.put_handler != NULL) term->vt.dcs.put_handler(term, c); else { From 7feab6092cf20f2b03f16aa0f147020971268e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 2 Jan 2022 20:22:01 +0100 Subject: [PATCH 04/10] =?UTF-8?q?scripts:=20generate-builtin-terminfo:=20a?= =?UTF-8?q?dd=20custom=20capability=20=E2=80=98Co=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a “special feature”, similar to “TN” and “RGB”: Co for termcap colors (or colors for terminfo colors) --- scripts/generate-builtin-terminfo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index dd27913b..a58b7da3 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -152,8 +152,9 @@ def main(): except KeyError: pass - entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(IntCapability('Co', 256)) entry.add_capability(StringCapability('TN', target_entry_name)) + entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel target.write('#pragma once\n') target.write('\n') From 44aad0941fff3e314ae42c3c578cfa6d61913198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 4 Jan 2022 22:01:36 +0100 Subject: [PATCH 05/10] dcs: xtgettcap: handle boolean capabilities Reply with DCS 1 + r ST --- dcs.c | 8 ++++++++ scripts/generate-builtin-terminfo.py | 9 +++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dcs.c b/dcs.c index 6fed8010..07fec5c7 100644 --- a/dcs.c +++ b/dcs.c @@ -99,6 +99,14 @@ xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) if (entry == NULL) goto err; + if (entry->value == NULL) { + /* Boolean */ + term_to_slave(term, "\033P1+r", 5); + term_to_slave(term, hex_cap_name, len); + term_to_slave(term, "\033\\", 2); + goto out; + } + /* * Reply format: * \EP 1 + r cap=value \E\\ diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index a58b7da3..9c8807bf 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -174,9 +174,6 @@ def main(): '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) @@ -194,7 +191,11 @@ def main(): # Do escape ‘“‘ name = name.replace('"', '\"') value = value.replace('"', '\"') - target.write(f' {{"{name}", "{value}"}},\n') + + if isinstance(cap, BoolCapability): + target.write(f' {{"{name}", NULL}},\n') + else: + target.write(f' {{"{name}", "{value}"}},\n') target.write('};\n') From 7258e0b00544784cf8f6a9ed7d9d81d7d9444a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 6 Jan 2022 21:29:42 +0100 Subject: [PATCH 06/10] doc: ctlseq: add XTGETTCAP --- doc/foot-ctlseqs.7.scd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd index 1dee1550..b290fe4f 100644 --- a/doc/foot-ctlseqs.7.scd +++ b/doc/foot-ctlseqs.7.scd @@ -679,6 +679,9 @@ and are terminated by *\\E\\* (ST). : Begin application synchronized updates | \\EP = 2 s \\E\\ : End application synchronized updates +| \\EP + q \\E\\ +: Query builtin terminfo database (XTGETTCAP) + # FOOTNOTE From 5a032c4c6f6517e664ac7b920c3cfae8d5a25b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 13 Jan 2022 13:37:44 +0100 Subject: [PATCH 07/10] xtgettcap: switch from a table based internal structure, to a single char array Having the builtin terminfo DB as a table, with one entry per capability/value pair was ineffective memory-wise - we ended up adding ~20K to the final binary size. This patch changes the internal representation from a table, to a single NULL-separated (and NULL-terminated) char array: cap1-name \0 cap1-value \0 cap2-name \0 cap2-value \0 The capabilities are still sorted; to lookup a capability we need to scan from the beginning until we either find a match, or until the capabilities from the DB sort higher (lexicographically) than the searched-for capability. The terminfo char array is 3.3K - more better than before. --- dcs.c | 64 ++++++++++++++++++++++------ scripts/generate-builtin-terminfo.py | 57 +++++++++++-------------- 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/dcs.c b/dcs.c index 07fec5c7..1a388863 100644 --- a/dcs.c +++ b/dcs.c @@ -66,19 +66,52 @@ err: UNITTEST { /* Verify table is sorted */ - for (size_t i = 1; i < ALEN(terminfo_capabilities); i++) { - xassert(strcmp(terminfo_capabilities[i - 1].name, - terminfo_capabilities[i].name ) <= 0); + const char *p = terminfo_capabilities; + size_t left = sizeof(terminfo_capabilities); + + const char *last_cap = NULL; + + while (left > 0) { + const char *cap = p; + const char *val = cap + strlen(cap) + 1; + + size_t size = strlen(cap) + 1 + strlen(val) + 1;; + xassert(size <= left); + p += size; + left -= size; + + if (last_cap != NULL) + xassert(strcmp(last_cap, cap) < 0); + + last_cap = cap; } } -static int -terminfo_entry_compar(const void *_key, const void *_entry) +static bool +lookup_capability(const char *name, const char **value) { - const char *key = _key; - const struct foot_terminfo_entry *entry = _entry; + const char *p = terminfo_capabilities; + size_t left = sizeof(terminfo_capabilities); - return strcmp(key, entry->name); + while (left > 0) { + const char *cap = p; + const char *val = cap + strlen(cap) + 1; + + size_t size = strlen(cap) + 1 + strlen(val) + 1;; + xassert(size <= left); + p += size; + left -= size; + + int r = strcmp(cap, name); + if (r == 0) { + *value = val; + return true; + } else if (r > 0) + break; + } + + *value = NULL; + return false; } static void @@ -88,18 +121,23 @@ xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) if (name == NULL) goto err; +#if 0 const struct foot_terminfo_entry *entry = bsearch(name, terminfo_capabilities, ALEN(terminfo_capabilities), sizeof(*entry), &terminfo_entry_compar); +#endif + const char *value; + bool valid_capability = lookup_capability(name, &value); + xassert(!valid_capability || value != NULL); LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s", name, (int)len, hex_cap_name, - entry != NULL ? entry->value : ""); + valid_capability ? value : ""); - if (entry == NULL) + if (!valid_capability) goto err; - if (entry->value == NULL) { + if (value[0] == '\0') { /* Boolean */ term_to_slave(term, "\033P1+r", 5); term_to_slave(term, hex_cap_name, len); @@ -116,13 +154,13 @@ xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) 5 + /* DCS 1 + r (\EP1+r) */ len + /* capability name, hex encoded */ 1 + /* ‘=’ */ - strlen(entry->value) * 2 + /* capability value, hex encoded */ + strlen(value) * 2 + /* capability value, hex encoded */ 2 + /* ST (\E\\) */ 1); int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name); - for (const char *c = entry->value; *c != '\0'; c++) { + for (const char *c = value; *c != '\0'; c++) { uint8_t nib1 = (uint8_t)*c >> 4; uint8_t nib2 = (uint8_t)*c & 0xf; diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py index 9c8807bf..c8d3be4b 100755 --- a/scripts/generate-builtin-terminfo.py +++ b/scripts/generate-builtin-terminfo.py @@ -49,7 +49,19 @@ class IntCapability(Capability): class StringCapability(Capability): - pass + def __init__(self, name: str, value: str): + # 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('\\:', ':') + + super().__init__(name, value) class Fragment: @@ -156,48 +168,27 @@ def main(): entry.add_capability(StringCapability('TN', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel - 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') - + terminfo_parts = [] for cap in sorted(entry.caps.values()): 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 ‘“‘ + # Escape ‘“‘ name = name.replace('"', '\"') value = value.replace('"', '\"') + terminfo_parts.append(name) if isinstance(cap, BoolCapability): - target.write(f' {{"{name}", NULL}},\n') + terminfo_parts.append('') else: - target.write(f' {{"{name}", "{value}"}},\n') + terminfo_parts.append(value) - target.write('};\n') + terminfo = '\\0" "'.join(terminfo_parts) + + target.write('#pragma once\n') + target.write('\n') + target.write(f'static const char terminfo_capabilities[] = "{terminfo}";') + target.write('\n') if __name__ == '__main__': From da9d732855035f3b0b6fd8922f31679aef631927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jan 2022 13:33:16 +0100 Subject: [PATCH 08/10] readme: document XTGETTCAP behavior --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 1be27ab6..16600d59 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [DPI and font size](#dpi-and-font-size) 1. [Supported OSCs](#supported-oscs) 1. [Programmatically checking if running in foot](#programmatically-checking-if-running-in-foot) +1. [XTGETTCAP](#xtgettcap) 1. [Credits](#Credits) 1. [Bugs](#bugs) 1. [Contact](#contact) @@ -432,6 +433,54 @@ e.g. “1.8.2” for a regular release, or “1.8.2-36-g7db8e06f” for a git build. +# XTGETTCAP + +`XTGETTCAP` is an escape sequence initially introduced by XTerm, and +also implemented (and extended, to some degree) by Kitty. + +It allows querying the terminal for terminfo +capabilities. Applications using this feature do not need to use the +classic, file-based, terminfo definition. For example, if all +applications used this feature, you would no longer have to install +foot’s terminfo on remote hosts you SSH into. + +XTerm’s implementation (as of XTerm-370) only supports querying key +(as in keyboard keys) capabilities, and three custom capabilities: + +* `TN` - terminal name +* `Co` - number of colors (alias for the `colors` capability) +* `RGB` - number of bits per color channel (different semantics from + the `RGB` capability in file-based terminfo definitions!). + +Kitty has extended this, and also supports querying all integer and +string capabilities. + +Foot supports this, and extends it even further, to also include +boolean capabilities. This means foot’s entire terminfo can be queried +via `XTGETTCAP`. + +Note that both Kitty and foot handles **responses** to +multi-capability queries slightly differently, compared to XTerm. + +XTerm will send a single DCS reply, with `;`-separated +capability/value pairs. There are a couple of issues with this: + +* The success/fail flag in the beginning of the response is always `1` + (success), unless the very **first** queried capability is invalid. +* XTerm will not respond **at all** to an invalid capability, unless + it’s the first one in the `XTGETTCAP` query. +* XTerm will end the response at the first invalid capability. + +In other words, if you send a large multi-capability query, you will +only get responses up to, but not including, the first invalid +capability. All subsequent capabilities will be dropped. + +Kitty and foot on the other hand, send one DCS response for **each** +capability in the multi query. This allows us to send a proper +success/fail flag for each queried capability. Responses for **all** +queried capabilities are **always** sent. No queries are ever dropped. + + # Credits * [Ordoviz](https://codeberg.org/Ordoviz), for designing and From b819d38f1e40bd9f0340f2511b00340f5a1c146c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jan 2022 13:34:23 +0100 Subject: [PATCH 09/10] changelog: XTGETTCAP --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07205da0..e38700ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,9 @@ when auto-detecting URLs. * [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported (https://codeberg.org/dnkl/foot/issues/762). - +* `XTGETTCAP` - builtin terminfo. See + [README.md::XTGETTCAP](README.md#xtgettcap) for details + (https://codeberg.org/dnkl/foot/issues/846). ### Changed From 49a8c7fc765f6ed2874e270fa2166850b48eeb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 14 Jan 2022 13:36:31 +0100 Subject: [PATCH 10/10] doc: foot.1: XTGETTCAP --- doc/foot.1.scd | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 9f7211b4..90851965 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -373,6 +373,53 @@ distro package for foot's terminfo entries, you can install foot's terminfo entries manually, by copying *foot* and *foot-direct* to *~/.terminfo/f/*. +# XTGETTCAP + +*XTGETTCAP* is an escape sequence initially introduced by XTerm, and +also implemented (and extended, to some degree) by Kitty. + +It allows querying the terminal for terminfo classic, file-based, +terminfo definition. For example, if all applications used this +feature, you would no longer have to install foot’s terminfo on remote +hosts you SSH into. + +XTerm’s implementation (as of XTerm-370) only supports querying key +(as in keyboard keys) capabilities, and three custom capabilities: + +- TN - terminal name +- Co - number of colors (alias for the colors capability) +- RGB - number of bits per color channel (different semantics from + the RGB capability in file-based terminfo definitions!). + +Kitty has extended this, and also supports querying all integer and +string capabilities. + +Foot supports this, and extends it even further, to also include +boolean capabilities. This means foot’s entire terminfo can be queried +via *XTGETTCAP*. + +Note that both Kitty and foot handles responses to multi-capability +queries slightly differently, compared to XTerm. + +XTerm will send a single DCS reply, with ;-separated +capability/value pairs. There are a couple of issues with this: + +- The success/fail flag in the beginning of the response is always 1 + (success), unless the very first queried capability is invalid. +- XTerm will not respond at all to an invalid capability, unless it’s + the first one in the XTGETTCAP query. +- XTerm will end the response at the first invalid capability. + +In other words, if you send a large multi-capability query, you will +only get responses up to, but not including, the first invalid +capability. All subsequent capabilities will be dropped. + +Kitty and foot on the other hand, send one DCS response for each +capability in the multi query. This allows us to send a proper +success/fail flag for each queried capability. Responses for all +queried capabilities are always sent. No queries are ever dropped. + + # ENVIRONMENT The following environment variables are used by foot: