git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2 8/9] built-in add -p: handle Escape sequences more efficiently

From
Johannes Schindelin via GitGitGadget <gitgitgadget@gmail.com>
Date
Dec 25, 2019, 11:56 UTC
Message-ID
<703719ffce4e69fa1d22fd2b740ddfaa0ef7283d.1577275020.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.175.v2.git.1577275020.gitgitgadget@gmail.com>
From: Johannes Schindelin <johannes.schindelin@gmx.de>

When `interactive.singlekey = true`, we react immediately to keystrokes, even to Escape sequences (e.g. when pressing a cursor key).

The problem with Escape sequences is that we do not really know when they are done, and as a heuristic we poll standard input for half a second to make sure that we got all of it.

While waiting half a second is not asking for a whole lot, it can become quite annoying over time, therefore with this patch, we read the terminal capabilities (if available) and extract known Escape sequences from there, then stop polling immediately when we detected that the user pressed a key that generated such a known sequence.

This recapitulates the remaining part of b5cc003253c8 (add -i: ignore terminal escape sequences, 2011-05-17).

Note: We do *not* query the terminal capabilities directly. That would
either require a lot of platform-specific code, or it would require
linking to a library such as ncurses.

Linking to a library in the built-ins is something we try very hard to avoid (we even kicked the libcurl dependency to a non-built-in remote helper, just to shave off a tiny fraction of a second from Git's startup time). And the platform-specific code would be a maintenance nightmare.

Even worse: in Git for Windows' case, we would need to query MSYS2 pseudo terminals, which `git.exe` simply cannot do (because it is intentionally *not* an MSYS2 program).

To address this, we simply spawn `infocmp -L -1` and parse its output (which works even in Git for Windows, because that helper is included in the end-user facing installations).

This is done only once, as in the Perl version, but it is done only when the first Escape sequence is encountered, not upon startup of `git add -i`; This saves on startup time, yet makes reacting to the first Escape sequence slightly more sluggish. But it allows us to keep the terminal-related code encapsulated in the `compat/terminal.c` file.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
---
 compat/terminal.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 72 insertions(+), 1 deletion(-)
diff --git a/compat/terminal.c b/compat/terminal.c
index b7f58d1781..35bca03d14 100644
--- a/compat/terminal.c
+++ b/compat/terminal.c
@@ -4,6 +4,7 @@
 #include "strbuf.h"
 #include "run-command.h"
 #include "string-list.h"
+#include "hashmap.h"
 
 #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE)
 
@@ -238,6 +239,71 @@ char *git_terminal_prompt(const char *prompt, int echo)
 	return buf.buf;
 }
 
+/*
+ * The `is_known_escape_sequence()` function returns 1 if the passed string
+ * corresponds to an Escape sequence that the terminal capabilities contains.
+ *
+ * To avoid depending on ncurses or other platform-specific libraries, we rely
+ * on the presence of the `infocmp` executable to do the job for us (failing
+ * silently if the program is not available or refused to run).
+ */
+struct escape_sequence_entry {
+	struct hashmap_entry entry;
+	char sequence[FLEX_ARRAY];
+};
+
+static int sequence_entry_cmp(const void *hashmap_cmp_fn_data,
+			      const struct escape_sequence_entry *e1,
+			      const struct escape_sequence_entry *e2,
+			      const void *keydata)
+{
+	return strcmp(e1->sequence, keydata ? keydata : e2->sequence);
+}
+
+static int is_known_escape_sequence(const char *sequence)
+{
+	static struct hashmap sequences;
+	static int initialized;
+
+	if (!initialized) {
+		struct child_process cp = CHILD_PROCESS_INIT;
+		struct strbuf buf = STRBUF_INIT;
+		char *p, *eol;
+
+		hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp,
+			     NULL, 0);
+
+		argv_array_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
+		if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0))
+			strbuf_setlen(&buf, 0);
+
+		for (eol = p = buf.buf; *p; p = eol + 1) {
+			p = strchr(p, '=');
+			if (!p)
+				break;
+			p++;
+			eol = strchrnul(p, '\n');
+
+			if (starts_with(p, "\\E")) {
+				char *comma = memchr(p, ',', eol - p);
+				struct escape_sequence_entry *e;
+
+				p[0] = '^';
+				p[1] = '[';
+				FLEX_ALLOC_MEM(e, sequence, p, comma - p);
+				hashmap_entry_init(&e->entry,
+						   strhash(e->sequence));
+				hashmap_add(&sequences, &e->entry);
+			}
+			if (!*eol)
+				break;
+		}
+		initialized = 1;
+	}
+
+	return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence);
+}
+
 int read_key_without_echo(struct strbuf *buf)
 {
 	static int warning_displayed;
@@ -271,7 +337,12 @@ int read_key_without_echo(struct strbuf *buf)
 		 * Start by replacing the Escape byte with ^[ */
 		strbuf_splice(buf, buf->len - 1, 1, "^[", 2);
 
-		for (;;) {
+		/*
+		 * Query the terminal capabilities once about all the Escape
+		 * sequences it knows about, so that we can avoid waiting for
+		 * half a second when we know that the sequence is complete.
+		 */
+		while (!is_known_escape_sequence(buf->buf)) {
 			struct pollfd pfd = { .fd = 0, .events = POLLIN };
 
 			if (poll(&pfd, 1, 500) < 1)
-- 
gitgitgadget
Previous: Johannes Schindelin via GitGitGadgetNext: Johannes Schindelin via GitGitGadget
Message 28 of 63 in “built-in add -p: add support for the same config settings as the Perl version”
  1. 0/9 built-in add -p: add support for the same config settings as the Perl versionJohannes Schindelin via GitGitGadget, Dec 21, 2019
  2. 1/9 built-in add -p: support interactive.diffFilterJohannes Schindelin via GitGitGadget, Dec 21, 2019
  3. 2/9 built-in add -p: handle diff.algorithmJohannes Schindelin via GitGitGadget, Dec 21, 2019
  4. 6/9 built-in add -p: respect the `interactive.singlekey` config settingJohannes Schindelin via GitGitGadget, Dec 21, 2019
  5. 8/9 built-in add -p: handle Escape sequences more efficientlyJohannes Schindelin via GitGitGadget, Dec 21, 2019
  6. 4/9 terminal: accommodate Git for Windows' default terminalJohannes Schindelin via GitGitGadget, Dec 21, 2019
  7. 3/9 terminal: make the code of disable_echo() reusableJohannes Schindelin via GitGitGadget, Dec 21, 2019
  8. 7/9 built-in add -p: handle Escape sequences in interactive.singlekey modeJohannes Schindelin via GitGitGadget, Dec 21, 2019
  9. 5/9 terminal: add a new function to read a single keystrokeJohannes Schindelin via GitGitGadget, Dec 21, 2019
  10. 9/9 ci: include the built-in `git add -i` in the `linux-gcc` jobJohannes Schindelin via GitGitGadget, Dec 21, 2019
  11. SZEDER GáborDec 21, 2019
  12. Johannes SchindelinDec 25, 2019
  13. Junio C HamanoDec 22, 2019
  14. Johannes SchindelinDec 25, 2019
  15. Junio C HamanoDec 24, 2019
  16. Junio C HamanoDec 24, 2019
  17. Simon RuderichDec 25, 2019
  18. Johannes SchindelinDec 25, 2019
  19. Johannes SchindelinDec 25, 2019
  20. 0/9 built-in add -p: add support for the same config settings as the Perl versionJohannes Schindelin via GitGitGadget, Dec 25, 2019
  21. 1/9 built-in add -p: support interactive.diffFilterJohannes Schindelin via GitGitGadget, Dec 25, 2019
  22. SZEDER GáborJan 7, 2020
  23. Johannes SchindelinJan 13, 2020
  24. 2/9 built-in add -p: handle diff.algorithmJohannes Schindelin via GitGitGadget, Dec 25, 2019
  25. 3/9 terminal: make the code of disable_echo() reusableJohannes Schindelin via GitGitGadget, Dec 25, 2019
  26. 4/9 terminal: accommodate Git for Windows' default terminalJohannes Schindelin via GitGitGadget, Dec 25, 2019
  27. 6/9 built-in add -p: respect the `interactive.singlekey` config settingJohannes Schindelin via GitGitGadget, Dec 25, 2019
  28. 8/9 built-in add -p: handle Escape sequences more efficientlyJohannes Schindelin via GitGitGadget, Dec 25, 2019
  29. 9/9 ci: include the built-in `git add -i` in the `linux-gcc` jobJohannes Schindelin via GitGitGadget, Dec 25, 2019
  30. Derrick StoleeDec 26, 2019
  31. Johannes SchindelinJan 1, 2020
  32. 7/9 built-in add -p: handle Escape sequences in interactive.singlekey modeJohannes Schindelin via GitGitGadget, Dec 25, 2019
  33. 5/9 terminal: add a new function to read a single keystrokeJohannes Schindelin via GitGitGadget, Dec 25, 2019
  34. Junio C HamanoDec 26, 2019
  35. 00/10 built-in add -p: add support for the same config settings as the Perl versionJohannes Schindelin via GitGitGadget, Jan 13, 2020
  36. 01/10 built-in add -i/-p: treat SIGPIPE as EOFJohannes Schindelin via GitGitGadget, Jan 13, 2020
  37. SZEDER GáborJan 13, 2020
  38. Jeff KingJan 13, 2020
  39. Junio C HamanoJan 15, 2020
  40. Jeff KingJan 15, 2020
  41. Johannes SchindelinJan 14, 2020
  42. SZEDER GáborJan 17, 2020
  43. Jeff KingJan 17, 2020
  44. 02/10 built-in add -p: support interactive.diffFilterJohannes Schindelin via GitGitGadget, Jan 13, 2020
  45. 03/10 built-in add -p: handle diff.algorithmJohannes Schindelin via GitGitGadget, Jan 13, 2020
  46. 05/10 terminal: accommodate Git for Windows' default terminalJohannes Schindelin via GitGitGadget, Jan 13, 2020
  47. 04/10 terminal: make the code of disable_echo() reusableJohannes Schindelin via GitGitGadget, Jan 13, 2020
  48. 06/10 terminal: add a new function to read a single keystrokeJohannes Schindelin via GitGitGadget, Jan 13, 2020
  49. 07/10 built-in add -p: respect the `interactive.singlekey` config settingJohannes Schindelin via GitGitGadget, Jan 13, 2020
  50. 10/10 ci: include the built-in `git add -i` in the `linux-gcc` jobJohannes Schindelin via GitGitGadget, Jan 13, 2020
  51. 09/10 built-in add -p: handle Escape sequences more efficientlyJohannes Schindelin via GitGitGadget, Jan 13, 2020
  52. 08/10 built-in add -p: handle Escape sequences in interactive.singlekey modeJohannes Schindelin via GitGitGadget, Jan 13, 2020
  53. 00/10 built-in add -p: add support for the same config settings as the Perl versionJohannes Schindelin via GitGitGadget, Jan 14, 2020
  54. 01/10 t3701: adjust difffilter testJohannes Schindelin via GitGitGadget, Jan 14, 2020
  55. 02/10 built-in add -p: support interactive.diffFilterJohannes Schindelin via GitGitGadget, Jan 14, 2020
  56. 05/10 terminal: accommodate Git for Windows' default terminalJohannes Schindelin via GitGitGadget, Jan 14, 2020
  57. 08/10 built-in add -p: handle Escape sequences in interactive.singlekey modeJohannes Schindelin via GitGitGadget, Jan 14, 2020
  58. 10/10 ci: include the built-in `git add -i` in the `linux-gcc` jobJohannes Schindelin via GitGitGadget, Jan 14, 2020
  59. 07/10 built-in add -p: respect the `interactive.singlekey` config settingJohannes Schindelin via GitGitGadget, Jan 14, 2020
  60. 03/10 built-in add -p: handle diff.algorithmJohannes Schindelin via GitGitGadget, Jan 14, 2020
  61. 06/10 terminal: add a new function to read a single keystrokeJohannes Schindelin via GitGitGadget, Jan 14, 2020
  62. 04/10 terminal: make the code of disable_echo() reusableJohannes Schindelin via GitGitGadget, Jan 14, 2020
  63. 09/10 built-in add -p: handle Escape sequences more efficientlyJohannes Schindelin via GitGitGadget, Jan 14, 2020

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.