summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ChangeLog6
-rw-r--r--Doc/Zsh/params.yo71
-rw-r--r--Src/Zle/termquery.c510
-rw-r--r--Src/Zle/zle.mdd2
-rw-r--r--Src/Zle/zle_main.c4
-rw-r--r--Test/X04zlehighlight.ztst1
-rw-r--r--Test/X06termquery.ztst89
7 files changed, 682 insertions, 1 deletions
diff --git a/ChangeLog b/ChangeLog
index 88ecc1d7b..b9988234d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+2025-11-10 Oliver Kiddle <opk@zsh.org>
+
+ * 53372, 53375: Doc/Zsh/params.yo, Src/Zle/termquery.c,
+ Src/Zle/zle.mdd, Src/Zle/zle_main.c, Test/X04zlehighlight.ztst,
+ Test/X06termquery.ztst: query terminal properties on ZLE startup
+
2025-11-09 Bart Schaefer <schaefer@zsh.org>
* Philippe: 54057: Src/params.c, Src/zsh.h, Test/K01nameref.ztst:
diff --git a/Doc/Zsh/params.yo b/Doc/Zsh/params.yo
index dd4519622..90ed6e5d1 100644
--- a/Doc/Zsh/params.yo
+++ b/Doc/Zsh/params.yo
@@ -965,6 +965,40 @@ there is a block of reserved or unused signal numbers before the POSIX
real-time signals so the array index can't be used as an accurate indicator
of their signal number. Use, for example, tt(kill -l SIGRTMIN) instead.
)
+vindex(.term.bg)
+item(tt(.term.bg))(
+The background color of the terminal if the terminal supports the
+necessary interface for retrieving this information.
+
+See also the tt(.term.extensions) parameter.
+)
+vindex(.term.fg)
+item(tt(.term.fg))(
+Like tt(.term.bg) but for the foreground color.
+)
+vindex(.term.id)
+item(tt(.term.id))(
+Like the tt(TERM) parameter, this identifies the terminal but is obtained by
+querying the terminal. Many terminals name a different common terminal in
+tt(TERM) that they then emulate, often imperfectly. This allows them to work
+without the need to distribute updates to terminal capability databases. The
+more accurate identification in this parameter may be useful when configuring
+aspects of the shell differently for specific terminals.
+
+See also the tt(.term.extensions) parameter.
+)
+vindex(.term.version)
+item(tt(.term.version))(
+The version number of the terminal application, set in conjunction with
+tt(.term.id).
+)
+vindex(.term.mode)
+item(tt(.term.mode))(
+Either tt(light) or tt(dark). In cases where the terminal background color is
+detected and set in tt(.term.bg), this is set to provide a basic indication of
+how light that color is. This may help with configuring readable color
+combinations.
+)
vindex(TRY_BLOCK_ERROR)
item(tt(TRY_BLOCK_ERROR) <S>)(
In an tt(always) block, indicates whether the preceding list of code
@@ -1695,6 +1729,43 @@ causes the shell to reinitialise the terminal, making the workaround
`tt(TERM=$TERM)' unnecessary. Note that unlike other colon-separated
arrays this is not tied to a zsh array.
)
+vindex(.term.extensions)
+item(tt(.term.extensions))(
+An array containing a list of extension features of the terminal to be enabled
+or disabled (prefixed with `tt(-)'). When ZLE starts, it will add entries for
+features that were auto-detected. This auto-detection uses extensions itself,
+all named with a `tt(query)' prefix. As these are used when ZLE starts they
+would need to be disabled early in the startup files if they are to be
+disabled. A value of `tt(-query)' will disable all terminal queries on
+startup. Extensions can be any of the following, where the marks `<D>' and
+`<E>' indicate whether they are disabled or enabled by default:
+
+startitem()
+item(tt(query-bg) <E>)(
+Query the terminal background color which is used for tt(.term.bg) and
+tt(.term.mode).
+)
+item(tt(query-fg) <E>)(
+Query the terminal foreground color which is used for tt(.term.fg).
+)
+item(tt(query-id) <E>)(
+Query the terminal identification which is used for tt(.term.id) and
+tt(.term.version).
+)
+xitem(tt(modkeys-kitty) <D>)
+item(tt(query-modkeys-kitty) <E>)(
+Support for the kitty keyboard handling protocol which enables reporting of a
+wider range of key combinations and resolves problems with ambiguous key
+sequences. Currently there is only support for detecting whether the terminal
+supports this feature.
+)
+xitem(tt(truecolor) <E>)
+item(tt(query-truecolor) <E>)(
+Support for 24-bit truecolor escape sequences. Auto-detection also tries
+termcap and the tt(COLORTERM) environment variable.
+)
+enditem()
+)
vindex(TIMEFMT)
item(tt(TIMEFMT))(
The format of process time reports with the tt(time) keyword.
diff --git a/Src/Zle/termquery.c b/Src/Zle/termquery.c
new file mode 100644
index 000000000..2488319e2
--- /dev/null
+++ b/Src/Zle/termquery.c
@@ -0,0 +1,510 @@
+/*
+ * termquery.c - terminal feature probes
+ *
+ * This file is part of zsh, the Z shell.
+ *
+ * Copyright (c) 2025 Oliver Kiddle
+ * All rights reserved.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and to distribute modified versions of this software for any
+ * purpose, provided that the above copyright notice and the following
+ * two paragraphs appear in all copies of this software.
+ *
+ * In no event shall Oliver Kiddle or the Zsh Development Group be liable
+ * to any party for direct, indirect, special, incidental, or consequential
+ * damages arising out of the use of this software and its documentation,
+ * even if Oliver Kiddle and the Zsh Development Group have been advised of
+ * the possibility of such damage.
+ *
+ * Oliver Kiddle and the Zsh Development Group specifically disclaim any
+ * warranties, including, but not limited to, the implied warranties of
+ * merchantability and fitness for a particular purpose. The software
+ * provided hereunder is on an "as is" basis, and Oliver Kiddle and the
+ * Zsh Development Group have no obligation to provide maintenance,
+ * support, updates, enhancements, or modifications.
+ *
+ */
+
+#include "zle.mdh"
+#include "termquery.pro"
+
+/* On a 9600 serial link, 0.2 seconds is about the minimum to safely work.
+ * 0.5s should be generous enough. We only incur this delay on a terminal
+ * that doesn't respond to device attributes requests. */
+#define TIMEOUT -51L
+
+#define TAG (1 << 7)
+#define SEQ (TAG | (1 << 6))
+
+/* tags */
+#define T_BEGIN 0x80 /* group start */
+#define T_END 0x81 /* group end */
+#define T_OR 0x82 /* alternation (within group) */
+#define T_REPEAT 0x83 /* repeat preceding block */
+#define T_NUM 0x84 /* decimal number, defaults to 0 if absent */
+#define T_HEX 0x85 /* hexadecimal digit used as part of a number */
+#define T_HEXCH 0x86 /* hexadecimal digit that is thrown away */
+#define T_WILDCARD 0x87 /* match any character */
+#define T_RECORD 0x88 /* start text capture */
+#define T_CAPTURE 0x89 /* end text capture */
+#define T_DROP 0x91 /* drop input + restart without matching a sequence */
+#define T_CONTINUE 0x92 /* when matching don't go back to first state */
+#define T_NEXT 0x94 /* advance to next stored number */
+
+typedef const unsigned char seqstate_t;
+
+/* macros for entries in the parse state table */
+#define OR "\x82"
+#define NUM "\x84"
+#define HEX "\x85"
+#define HEXCH "\x86"
+#define WILDCARD "\x87"
+#define RECORD "\x88"
+#define CAPTURE "\x89"
+#define EITHER(group) "\x80" group "\x81"
+#define OPT(opt) EITHER( opt OR EITHER() )
+#define REPEAT(r) EITHER( r ) "\x83" /* would use OPT() but done in C code */
+/* Sequence completion tags and other actions need to precede the state
+ * where the final character is matched. This macro just reverses the
+ * order so that they come in a more logical order. */
+#define MATCH(ch, arg) arg ch
+#define DROP "\x91"
+#define CONTINUE "\x92"
+#define NEXT "\x94"
+#define DA "\xc0"
+#define COLOR "\xc1"
+#define KITTY "\xc2"
+#define TINFO "\xc3"
+#define XTID "\xc4"
+#define XTVER "\xc5"
+
+/* Deterministic finite state automata for parsing terminal response sequences.
+ * - alternatives decided by the first character (no back-tracking)
+ * - matching literal characters is 7-bit ASCII only
+ */
+#define QUERY_STATES \
+ "\033" \
+ EITHER( /* default terminal colours */ \
+ "]1" NUM ";" \
+ EITHER( "rgb:" \
+ HEX OPT( HEX ) OPT( HEXCH HEXCH ) "/" \
+ HEX OPT( HEX ) OPT( HEXCH HEXCH ) "/" \
+ HEX OPT( HEX ) OPT( HEXCH HEXCH ) \
+ OR "#" \
+ HEX HEX NEXT HEX HEX NEXT HEX HEX ) \
+ EITHER( \
+ MATCH("\033", COLOR CONTINUE ) \
+ MATCH("\\", DROP) /* urxvt 9.31 has bug: it omits the backslash */ \
+ OR MATCH("\007", COLOR ) ) \
+ OR "P" /* DCS */ \
+ EITHER( /* terminal name and version */ \
+ ">|" RECORD \
+ REPEAT( \
+ CAPTURE EITHER( MATCH("(", XTID CONTINUE) \
+ OR MATCH(" ", XTID CONTINUE) ) RECORD \
+ OR \
+ CAPTURE OPT( ")" ) "\033" MATCH( "\\", XTVER ) \
+ OR \
+ WILDCARD \
+ ) \
+ OR /* 24-bit colour support */ \
+ EITHER( \
+ "0+r" OPT( "524742" ) /* mlterm responds without numbers */ \
+ "\033" MATCH("\\", DROP) /* kitty does 24-bit but 0 => no */ \
+ OR "1+r524742=" REPEAT( HEXCH HEXCH ) /* hex encoded bytes */ \
+ "\033" MATCH("\\", TINFO) )) /* any value => truecolor */ \
+ OR /* keyboard protocol and device attributes */ \
+ "[?" \
+ REPEAT( NUM \
+ EITHER( ";" \
+ OR MATCH("u", KITTY ) \
+ OR MATCH("c", DA) )))
+
+static char *EXTVAR = ".term.extensions";
+static char *IDVAR = ".term.id";
+static char *VERVAR = ".term.version";
+static char *BGVAR = ".term.bg";
+static char *FGVAR = ".term.fg";
+static char *MODEVAR = ".term.mode";
+
+/* Query sequences
+ * using ESC\\ as ST instead of BEL because the bell was emitted on
+ * old xterm. */
+
+/* Terminal default colors. Probably best that these are queried first
+ * because tmux will need to pass these on. */
+#define TQ_BGCOLOR "\033]11;?\033\\"
+#define TQ_FGCOLOR "\033]10;?\033\\"
+
+/* Kitty / fixterms keyboard protocol which allows wider support for keys
+ * and modifiers. This clears the screen in terminology. */
+#define TQ_KITTYKB "\033[?u"
+
+/* Query for 24-bit color support, This is an XTTERMCAP sequence which with
+ * some terminals allows terminfo entries to be retrieved. */
+#define TQ_RGB "\033P+q524742\033\\"
+
+/* Query terminal name and version which is useful with terminals that
+ * lie in $TERM so that they work without terminfo entries. */
+#define TQ_XTVERSION "\033[>0q"
+
+/* Device attributes, Response isn't especially interesting but we get a
+ * response from most terminals so by putting this last we can short-circuit
+ * the time delay waiting for less well-supported responses that might never
+ * come.
+ * Following two spaces + CR works to clear fragments of sequences that
+ * appeared. GNU screen is an example terminal that needs this. */
+#define TQ_DA "\033[c \r"
+
+static seqstate_t*
+find_branch(seqstate_t* pos)
+{
+ int nested = 0;
+ seqstate_t* cur = pos + 1;
+
+ for (; *cur && (nested || (*cur != T_END && *cur != T_OR)); cur++) {
+ if (*cur == T_BEGIN)
+ nested++;
+ else if (*cur == T_END)
+ nested--;
+ }
+ return cur;
+}
+
+static seqstate_t*
+find_matching(seqstate_t* pos, int direction)
+{
+ int nested = 1;
+ seqstate_t* cur = pos + direction;
+
+ for (; *cur && nested; cur += direction) {
+ if (*cur == T_BEGIN && !(nested += direction)) {
+ break; /* going backward, stop on begin */
+ } else if (*cur == T_END)
+ nested -= direction;
+ }
+ return cur;
+}
+
+static void
+probe_terminal(const char *tquery, seqstate_t *states,
+ void (*handle_seq) (int seq, int *numbers, int len,
+ char *capture, int clen))
+{
+ size_t blen = 256, nlen = 16;
+ char *buf = zhalloc(blen);
+ char *start = buf, *current = buf, *illgotten = buf;
+ size_t record = 0, capture = 0;
+ int *numbers = hcalloc(nlen * sizeof(int));
+ int *num = numbers;
+ int finish = 0, number = 0;
+ int ch;
+ struct ttyinfo ti;
+
+ seqstate_t *curstate = states;
+
+ gettyinfo(&ti);
+#ifdef HAS_TIO
+ ti.tio.c_lflag &= (~ECHO & ~ICANON & ~ISIG);
+ ti.tio.c_iflag &= ~ICRNL;
+#else
+ ti.sgttyb.sg_flags &= ~ECHO;
+ ti.sgttyb.sg_flags |= CBREAK;
+#endif
+ settyinfo(&ti);
+
+ fputs(tquery, shout);
+ fflush(shout);
+
+ while (!finish && *curstate) {
+ int consumed = 0; /* whether an input token has been matched */
+ int branches = 1; /* count of untried paths encountered */
+ int triedstart = curstate == states; /* current char tried at start */
+ unsigned char action = 0, sequence = 0;
+
+ if (illgotten < current) {
+ ch = *illgotten++;
+ } else {
+ if (current == buf + blen) {
+ current = hrealloc(buf, blen, blen * 2);
+ illgotten = current + (illgotten - buf);
+ start = current + (start - buf);
+ buf = current;
+ current = buf + blen;
+ memset(current, 0, blen);
+ blen *= 2;
+ }
+ if ((ch = getbyte(TIMEOUT, 0, 1)) == EOF)
+ break;
+ *current++ = ch;
+ illgotten = current;
+ }
+
+ while (!consumed && branches >= 1 && *curstate) {
+ int increment = 0, base = 1, tryhere = 0;
+
+ do {
+ switch (*curstate) {
+ case T_BEGIN:
+ branches++;
+ curstate++;
+ break;
+ case T_END:
+ branches--;
+ sequence = action = 0;
+ curstate++;
+ break;
+ case T_OR:
+ curstate = find_matching(curstate, 1);
+ break;
+ case T_REPEAT:
+ sequence = action = 0;
+ if (branches > 1) {
+ branches--;
+ curstate++;
+ } else {
+ branches++;
+ curstate = find_matching(curstate - 1, -1);
+ }
+ break;
+ case T_NUM:
+ if (!(tryhere = (ch >= '0' && ch <= '9')))
+ curstate++;
+ break;
+ case T_RECORD:
+ record = current - buf - 1;
+ curstate++;
+ break;
+ case T_CAPTURE:
+ capture = current - buf - 1;
+ curstate++;
+ break;
+ case T_DROP:
+ case T_CONTINUE:
+ case T_NEXT:
+ action |= *curstate;
+ curstate++;
+ break;
+ default:
+ if ((*curstate & SEQ) == SEQ) {
+ sequence = *curstate;
+ curstate++;
+ } else {
+ tryhere = 1;
+ }
+ }
+ } while (!tryhere);
+
+ switch (*curstate) {
+ case T_HEX:
+ if (ch >= '0' && ch <= '9') {
+ increment = ch - '0';
+ } else if (ch >= 'a' && ch <= 'f') {
+ increment = ch - 'a' + 10;
+ } else if (ch >= 'A' && ch <= 'F') {
+ increment = ch - 'A' + 10;
+ } else
+ break;
+ consumed = number = 1;
+ base = 16;
+ if (action & 4) /* NEXT was used */
+ ++num;
+ break;
+ case T_HEXCH:
+ consumed = (ch >= '0' && ch <= '9') ||
+ (ch >= 'a' && ch <= 'f') ||
+ (ch >= 'A' && ch <= 'F');
+ if (consumed && number) {
+ ++num;
+ number = 0;
+ }
+ break;
+ case T_NUM:
+ if (ch >= '0' && ch <= '9') {
+ increment = ch - '0';
+ base = 10;
+ consumed = number = 1;
+ curstate--; /* allow repetition */
+ }
+ break;
+ case T_WILDCARD:
+ consumed = 1;
+ if (number) {
+ ++num;
+ number = 0;
+ }
+ break;
+ default:
+ if (!(*curstate & TAG) && (consumed = (*curstate == ch)) &&
+ number)
+ {
+ ++num;
+ number = 0;
+ }
+ break;
+ }
+ if (num == numbers + nlen) {
+ numbers = hrealloc((char *) numbers, nlen * sizeof(int),
+ sizeof(int) * nlen * 2);
+ memset(numbers + nlen, 0, nlen * sizeof(int));
+ num = numbers + nlen; /* restore relative position */;
+ nlen *= 2;
+ }
+ if (number)
+ *num = *num * base + increment;
+
+ /* if it didn't match, move to the next OR */
+ if (!consumed && branches > 1) {
+ sequence = action = 0;
+ for (; branches > 1; branches--) {
+ curstate = find_branch(curstate);
+ if (*curstate == T_OR) {
+ curstate++;
+ break;
+ /* repeated group can match zero times */
+ } else if (curstate[1] == T_REPEAT)
+ break;
+ }
+ }
+ /* Retry character at the start if it is the only buffered
+ * character and was tried from a later state. */
+ if (!consumed && branches <= 1) {
+ if (triedstart || start + 1 != current)
+ break;
+ branches = 1;
+ curstate = states;
+ triedstart = 1;
+ memset((num = numbers), 0, nlen * sizeof(int));
+ sequence = action = number = 0;
+ }
+ }
+
+ if (!consumed) {
+ illgotten = ++start;
+ curstate = states; /* return to first state */
+ memset((num = numbers), 0, nlen * sizeof(int));
+ number = 0;
+ } else {
+ if (sequence && !(finish = sequence == SEQ))
+ handle_seq(sequence & ~SEQ, numbers, num - numbers,
+ buf + record, capture - record);
+
+ if ((sequence || (action & 1)) &&
+ (current = start) && /* drop input from sequence */
+ (!(action & 2)))
+ {
+ curstate = states;
+ memset((num = numbers), 0, nlen * sizeof(int));
+ number = 0;
+ } else { /* CONTINUE */
+ while (*++curstate == T_END)
+ ;
+ }
+ }
+ }
+
+ /* put back any type-ahead text */
+ if (current > buf)
+ ungetbytes(buf, current - buf);
+
+ settyinfo(&shttyinfo);
+}
+
+static void
+handle_color(int bg, int red, int green, int blue)
+{
+ char *colour;
+
+ switch (bg) {
+ case 1: /* background color */
+ /* scale by Rec.709 coefficients for lightness */
+ setsparam(MODEVAR, ztrdup(
+ 0.2126f * red + 0.7152f * green + 0.0722f * blue <= 127 ?
+ "dark" : "light"));
+ /* fall-through */
+ case 0:
+ colour = zalloc(8);
+ sprintf(colour, "#%02x%02x%02x", red, green, blue);
+ setsparam(bg ? BGVAR : FGVAR, colour);
+ break;
+ default: break;
+ }
+}
+
+/* roughly corresponding feature names */
+static const char *features[] =
+ { "bg", "fg", "modkeys-kitty", "truecolor", "id" };
+static const char *queries[] =
+ { TQ_BGCOLOR, TQ_FGCOLOR, TQ_KITTYKB, TQ_RGB, TQ_XTVERSION, TQ_DA };
+
+static void
+handle_query(int sequence, int *numbers, int len, char *capture, int clen)
+{
+ char **feat;
+
+ switch (sequence) {
+ case 1: /* default colour */
+ if (len == 4)
+ handle_color(numbers[0], numbers[1], numbers[2], numbers[3]);
+ break;
+ case 2: /* kitty keyboard */
+ feat = zshcalloc(2 * sizeof(char *));
+ *feat = ztrdup(features[2]);
+ assignaparam(EXTVAR, feat, ASSPM_WARN|ASSPM_AUGMENT);
+ break;
+ case 3: /* truecolor */
+ feat = zshcalloc(2 * sizeof(char *));
+ *feat = ztrdup(features[3]);
+ assignaparam(EXTVAR, feat, ASSPM_WARN|ASSPM_AUGMENT);
+ break;
+ case 4: /* id */
+ setsparam(IDVAR, ztrduppfx(capture, clen));
+ break;
+ case 5: /* version */
+ setsparam(VERVAR, ztrduppfx(capture, clen));
+ break;
+ }
+}
+
+/**/
+void
+query_terminal(void) {
+ char tquery[sizeof(TQ_BGCOLOR TQ_FGCOLOR TQ_KITTYKB TQ_RGB TQ_XTVERSION TQ_DA)];
+ char *tqend = tquery;
+ static seqstate_t states[] = QUERY_STATES;
+ char **f, **flist = getaparam(EXTVAR);
+ size_t i;
+
+ for (f = flist; f && *f; f++)
+ if (!strcmp(*f, "-query"))
+ return; /* disable all queries */
+
+ for (i=0; i < sizeof(queries)/sizeof(*queries); i++) {
+ int last = i >= sizeof(features)/sizeof(*features);
+ int found = (last && tqend == tquery);
+ char *cterm;
+
+ /* skip if the query or corresponding feature is already in the list */
+ for (f = flist; !last && !found && f && *f; f++)
+ found = !strcmp(*f + (**f == '-'), features[i]) ||
+ (!strncmp(*f, "-query-", 7) && !strcmp(*f + 7, features[i]));
+ if (found)
+ continue;
+ /* if termcap indicates 24-bit color, assume support - even
+ * though this is only based on the initial $TERM
+ * failing that, check $COLORTERM */
+ if (i == 3 && (tccolours == 1 << 24 ||
+ ((cterm = getsparam("COLORTERM")) &&
+ (!strcmp(cterm, "truecolor") ||
+ !strcmp(cterm, "24bit")))))
+ handle_query(3, 0, 0, 0, 0);
+ else
+ struncpy(&tqend, (char *) queries[i], /* collate escape sequences */
+ sizeof(tquery) - (tqend - tquery));
+ }
+
+ if (tqend != tquery) /* unless nothing left after filtering */
+ probe_terminal(tquery, states, &handle_query);
+}
diff --git a/Src/Zle/zle.mdd b/Src/Zle/zle.mdd
index dd69eff2c..c7c914712 100644
--- a/Src/Zle/zle.mdd
+++ b/Src/Zle/zle.mdd
@@ -8,7 +8,7 @@ autofeatures="b:bindkey b:vared b:zle"
objects="zle_bindings.o zle_hist.o zle_keymap.o zle_main.o \
zle_misc.o zle_move.o zle_params.o zle_refresh.o \
zle_thingy.o zle_tricky.o zle_utils.o zle_vi.o zle_word.o \
-textobjects.o"
+termquery.o textobjects.o"
headers="zle.h zle_things.h"
diff --git a/Src/Zle/zle_main.c b/Src/Zle/zle_main.c
index ab0b51374..99ee3c804 100644
--- a/Src/Zle/zle_main.c
+++ b/Src/Zle/zle_main.c
@@ -2239,6 +2239,10 @@ setup_(UNUSED(Module m))
comprecursive = 0;
rdstrs = NULL;
+ /* detect terminal color and features */
+ if (shout)
+ query_terminal();
+
/* initialise the keymap system */
init_keymaps();
diff --git a/Test/X04zlehighlight.ztst b/Test/X04zlehighlight.ztst
index 87a59fde5..723bb05d7 100644
--- a/Test/X04zlehighlight.ztst
+++ b/Test/X04zlehighlight.ztst
@@ -12,6 +12,7 @@
zpty zsh "${(q)ZTST_testdir}/../Src/zsh -fiV +Z"
zpty -w zsh "module_path=( ${(j< >)${(@q-)module_path}} \$module_path )"
zpty -w zsh 'zle_highlight=( fg_start_code:"CDE|3" fg_end_code:"|" bg_start_code:"BCDE|4" bg_end_code:"|" )'
+ zpty -w zsh '.term.extensions=( -query )'
}
zpty_input() {
zpty ${${(M)2:#nonl}:+-n} -w zsh "$1"
diff --git a/Test/X06termquery.ztst b/Test/X06termquery.ztst
new file mode 100644
index 000000000..52f39ddf1
--- /dev/null
+++ b/Test/X06termquery.ztst
@@ -0,0 +1,89 @@
+# Tests for handling terminal responses to escape sequences
+%prep
+
+ if zmodload zsh/zpty 2> /dev/null; then
+ termresp() {
+ setopt localoptions extendedglob
+ export PS1= PS2= COLORTERM= TERM=
+ typeset +x -m .term.\*
+ zpty -d
+ zpty zsh "${(q)ZTST_testdir}/../Src/zsh -fiV +Z"
+ zpty -w zsh "module_path=( ${(j< >)${(@q-)module_path}} \$module_path )"
+ zpty -w zsh "setopt zle"
+ zpty -r zsh REPLY $'\e*\r'
+ zpty -n -w zsh "$1"
+ zpty -w zsh "typeset -p -m .term.\*"
+ zpty -w zsh 'exit'
+ { zpty -r zsh } | tr -d '\015' | grep -v '^ '
+ zpty -d
+ }
+ else
+ ZTST_unimplemented='the zsh/zpty module is not available'
+ fi
+
+%test
+
+ termresp $'\e]11;rgb:ffff/ffff/dddd\e\\\e]10;rgb:0000/0000/0000\e\\\e[?0u\eP1+r524742=38\e\\\eP>|foot(1.20.2)\e\\\e[?62;4;22;28c'
+0:foot response to terminal queries
+>typeset .term.id=foot
+>typeset .term.fg='#000000'
+>typeset .term.bg='#ffffdd'
+>typeset .term.version=1.20.2
+>typeset .term.mode=light
+>typeset -a .term.extensions=( modkeys-kitty truecolor )
+
+ termresp $'\e]11;rgb:0/0/0\e\\\e]10;rgb:ff/ff/ff\e\\\eP>|Wayst(0.0.0)\e\\\e[?63;1;4c'
+0:wayst response to terminal queries (shorter colour sequences)
+>typeset .term.id=Wayst
+>typeset .term.fg='#ffffff'
+>typeset .term.bg='#000000'
+>typeset .term.version=0.0.0
+>typeset .term.mode=dark
+
+ termresp $'\e]11;rgb:0000/0000/0000\e\\\e]10;rgb:b2b2/b2b2/b2b2\e\\\eP1+r524742=382F382F38\e\\\eP>|WezTerm 20240203-110809-5046fc22\e\\\e[?65;4;6;18;22c'
+0:WezTerm response to terminal queries (space separates version and longer RGB response)
+>typeset .term.id=WezTerm
+>typeset .term.fg='#b2b2b2'
+>typeset .term.bg='#000000'
+>typeset .term.version=20240203-110809-5046fc22
+>typeset .term.mode=dark
+>typeset -a .term.extensions=( truecolor )
+
+ termresp $'\e]11;rgb:9600/8700/7900\e\e]10;rgb:0000/0000/0000\e\e[?1;2c'
+0:urxvt response to terminal queries (bug in end of colour sequences)
+>typeset .term.fg='#000000'
+>typeset .term.bg='#968779'
+>typeset .term.mode=light
+
+ termresp $'\e]11;rgb:0000/0000/0000\e\\\e]10;rgb:dddd/dddd/dddd\e\\\e[?0u\eP0+r524742\e\\\eP>|kitty(0.36.4)\e\\\e[?62;c'
+0:kitty response to terminal queries (responds with error to RGB request)
+>typeset .term.id=kitty
+>typeset .term.fg='#dddddd'
+>typeset .term.bg='#000000'
+>typeset .term.version=0.36.4
+>typeset .term.mode=dark
+>typeset -a .term.extensions=( modkeys-kitty )
+
+ termresp $'\e]11;rgb:0000/ffff/8c8c\e\\\e]10;rgb:0000/0000/0000\e\\\eP1+r524742=38\e\\\eP>|XTerm(396)\e\\\e[?64;1;2;6;9;15;16;17;18;21;22;28c'
+0:xterm response to terminal queries
+>typeset .term.id=XTerm
+>typeset .term.fg='#000000'
+>typeset .term.bg='#00ff8c'
+>typeset .term.version=396
+>typeset .term.mode=light
+>typeset -a .term.extensions=( truecolor )
+
+ termresp $'echo type\e]11;rgb:0/0/0\aah\e]10;rgb:A/B/C\aea\e[?0u\eP0+r\e\\d\n\e[?0;c'
+0:type-ahead
+>typeahead
+>typeset .term.fg='#0a0b0c'
+>typeset .term.bg='#000000'
+>typeset .term.mode=dark
+>typeset -a .term.extensions=( modkeys-kitty )
+
+ termresp ''
+0:no response - timeout
+
+%clean
+
+ zmodload -ui zsh/zpty