Skip to content

Commit

Permalink
Query terminal for bce, rep and tsl capabilities via XTGETTCAP
Browse files Browse the repository at this point in the history
This is supported by xterm, foot, ghostty, iterm2 and perhaps a few
other terminals. It serves a similar purpose to querying `terminfo(5)`
capabilities from the filesystem, but with the following advantages:

* Doesn't require a terminfo(5) entry (or database) to be installed
* Doesn't require linking to libtinfo (or reimplementing parts of it)
* Works reliably over SSH
* Works reliably (in principle, at least) when spoofing `$TERM`, as
  is done automatically in some contexts (e.g. Docker)

See also:

* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
* https://codeberg.org/dnkl/foot/src/commit/a1ac37e771edf9fe907ee09dbec0e90d447679e1/doc/foot-ctlseqs.7.scd#L756-L757
* moby/moby#42323
* https://docs.docker.com/engine/reference/run/#environment-variables
  • Loading branch information
craigbarnes committed Apr 19, 2024
1 parent 8f6b3f4 commit 9723150
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 10 deletions.
4 changes: 2 additions & 2 deletions mk/build.mk
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ syntax_objects := $(call prefix-obj, build/syntax/, \
color highlight merge state syntax )

terminal_objects := $(call prefix-obj, build/terminal/, \
color cursor input ioctl key linux mode osc52 output parse paste rxvt \
style terminal )
color cursor input ioctl key linux mode osc52 output parse paste \
query rxvt style terminal )

editor_objects := $(call prefix-obj, build/, \
bind block block-iter bookmark buffer change cmdline commands \
Expand Down
16 changes: 9 additions & 7 deletions src/terminal/input.c
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,14 @@ static bool is_text(const char *str, size_t len)

static const char *tflag_to_str(TermFeatureFlags flag)
{
// Note: this only handles a small subset of individual flags,
// as returned by parse_csi_query_reply()
if (flag == TFLAG_KITTY_KEYBOARD) {
return "KITTYKBD";
} else if (flag == TFLAG_SYNC) {
return "SYNC";
// This only handles a subset of individual flags, as returned
// by parse_csi_query_reply() and parse_dcs_query_reply()
switch ((unsigned int)flag) {
case TFLAG_KITTY_KEYBOARD: return "KITTYKBD";
case TFLAG_SYNC: return "SYNC";
case TFLAG_BACK_COLOR_ERASE: return "BCE";
case TFLAG_ECMA48_REPEAT: return "REP";
case TFLAG_SET_WINDOW_TITLE: return "TITLE";
}
return "??";
}
Expand All @@ -179,7 +181,7 @@ static KeyCode handle_query_reply(Terminal *term, KeyCode key)
{
TermFeatureFlags flag = key & ~KEYCODE_QUERY_REPLY_BIT;
BUG_ON(!IS_POWER_OF_2(flag)); // Only 1 flag should be set
LOG_INFO("detected terminal feature '%s' via query", tflag_to_str(flag));
LOG_INFO("detected terminal feature %s via query", tflag_to_str(flag));
term->features |= flag;
return KEY_IGNORE;
}
Expand Down
3 changes: 3 additions & 0 deletions src/terminal/output.c
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ void term_put_queries(TermOutputBuffer *obuf)
"\033[?u" // Kitty keyboard protocol flags
"\033[?4m" // XTQMODKEYS 4 (xterm modifyOtherKeys mode)
"\033[?2026$p" // DECRQM 2026 (terminal-wg synchronized updates)
"\033P+q626365\033\\" // XTGETTCAP "bce"
"\033P+q726570\033\\" // XTGETTCAP "rep"
"\033P+q74736C\033\\" // XTGETTCAP "tsl"
;

LOG_INFO("querying terminal");
Expand Down
3 changes: 2 additions & 1 deletion src/terminal/parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <stdbool.h>
#include <stdint.h>
#include "parse.h"
#include "query.h"
#include "terminal.h"
#include "util/ascii.h"
#include "util/debug.h"
Expand Down Expand Up @@ -717,7 +718,7 @@ static ssize_t parse_dcs(const char *buf, size_t len, size_t i, KeyCode *k)
*k = KEY_IGNORE;
return i;
case 0x1B: // ESC (https://vt100.net/emu/dec_ansi_parser#STESC)
*k = KEY_IGNORE;
*k = parse_dcs_query_reply(data, pos, pos >= sizeof(data));
return i - 1;
}
continue;
Expand Down
66 changes: 66 additions & 0 deletions src/terminal/query.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include "query.h"
#include "cursor.h"
#include "terminal.h"
#include "util/log.h"
#include "util/string-view.h"
#include "util/strtonum.h"

KeyCode parse_dcs_query_reply(const char *data, size_t len, bool truncated)
{
const char *note = "";
if (unlikely(len == 0 || truncated)) {
note = truncated ? " (truncated)" : " (empty)";
goto unhandled;
}

StringView seq = string_view(data, len);
if (strview_has_prefix(&seq, "1+r")) {
strview_remove_prefix(&seq, 3);
if (strview_equal_cstring(&seq, "626365")) { // "bce"
return KEYCODE_QUERY_REPLY_BIT | TFLAG_BACK_COLOR_ERASE;
}
if (strview_equal_cstring_icase(&seq, "74736C=1B5D323B")) { // "tsl=\e]2;"
return KEYCODE_QUERY_REPLY_BIT | TFLAG_SET_WINDOW_TITLE;
}
if (
strview_has_prefix(&seq, "726570=") // "rep="
&& strview_has_suffix(&seq, "62") // "b"
&& (seq.length & 1) // Odd length (2x even hex strings + "=")
) {
return KEYCODE_QUERY_REPLY_BIT | TFLAG_ECMA48_REPEAT;
}
LOG_DEBUG("unhandled XTGETTCAP reply: %.*s", (int)len, data);
return KEY_IGNORE;
}

if (strview_has_prefix(&seq, "1$r")) {
strview_remove_prefix(&seq, 3);
if (strview_has_suffix(&seq, " q")) {
size_t n = seq.length - 2;
unsigned int x;
if (n >= 1 && n == buf_parse_uint(seq.data, n, &x)) {
const char *str = (x <= CURSOR_STEADY_BAR) ? cursor_type_to_str(x) : "??";
LOG_DEBUG("DECRQSS DECSCUSR (cursor style) reply: %u (%s)", x, str);
return KEY_IGNORE;
}
}
// TODO: Detect RGB and italic support from DECRQSS "1$r0;3;38:2::60:70:80m" reply
// TODO: To facilitate the above, convert Terminal::color_type to TermFeatureFlags bits
LOG_DEBUG("unhandled DECRQSS reply: %.*s", (int)len, data);
return KEY_IGNORE;
}

if (strview_has_prefix(&seq, "0+r")) {
note = " (XTGETTCAP; 'invalid request')";
goto unhandled;
}

if (strview_equal_cstring(&seq, "0$r")) {
note = " (DECRQSS; 'invalid request')";
goto unhandled;
}

unhandled:
LOG_DEBUG("unhandled DCS string%s: %.*s", note, (int)len, data);
return KEY_IGNORE;
}
11 changes: 11 additions & 0 deletions src/terminal/query.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef TERMINAL_QUERY_H
#define TERMINAL_QUERY_H

#include <stdbool.h>
#include <stddef.h>
#include "key.h"
#include "util/macros.h"

KeyCode parse_dcs_query_reply(const char *data, size_t len, bool truncated) NONNULL_ARGS WARN_UNUSED_RESULT;

#endif

0 comments on commit 9723150

Please sign in to comment.