aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--surfaces/main.c112
-rw-r--r--tests/check.py22
2 files changed, 93 insertions, 41 deletions
diff --git a/surfaces/main.c b/surfaces/main.c
index 70958e1..3b5a95a 100644
--- a/surfaces/main.c
+++ b/surfaces/main.c
@@ -426,53 +426,83 @@ static void handle_text(const char *bytes, size_t length) {
handle_key(name);
}
-/* Parse terminal input: escape sequences, control keys and UTF-8 text. */
-static bool handle_input(const unsigned char *in, size_t length) {
+/* Terminal input: escape sequences, control keys and UTF-8 text. A read may
+ * end inside a key (the terminal's reads are not its keys): an escape
+ * sequence, a mouse report or a character cut short waits in pending for
+ * the next read. An escape alone that nothing follows within ESCAPE_WAIT ms
+ * (vim's ttimeoutlen) is the Escape key; the rest of a key already begun
+ * waits up to SEQUENCE_WAIT ms (interactive gives up waiting). */
+enum { PENDING_MOST = 64, ESCAPE_WAIT = 100, SEQUENCE_WAIT = 1000 };
+static unsigned char pending[PENDING_MOST];
+static size_t pending_length;
+static double pending_since; /* vega_clock, when what waits began to */
+
+static double pending_wait(void) { return pending_length == 1 && pending[0] == 0x1B ? ESCAPE_WAIT : SEQUENCE_WAIT; }
+
+/* The key at an escape (length bytes from it): the bytes it took, or 0 when
+ * it may be cut short and more may come (final: none will). */
+static size_t escape(const unsigned char *in, size_t length, bool final) {
static const struct { const char *sequence, *key; } sequences[] = {
{"[A", "ArrowUp"}, {"[B", "ArrowDown"}, {"[C", "ArrowRight"}, {"[D", "ArrowLeft"}, {"[H", "Home"},
{"[F", "End"}, {"OA", "ArrowUp"}, {"OB", "ArrowDown"}, {"OC", "ArrowRight"}, {"OD", "ArrowLeft"},
{"OH", "Home"}, {"OF", "End"}, {"[1~", "Home"}, {"[7~", "Home"}, {"[4~", "End"}, {"[8~", "End"},
{"[3~", "Delete"}, {"[5~", "PageUp"}, {"[6~", "PageDown"}};
- for (size_t at = 0; at < length;) {
- unsigned char c = in[at];
- if (c == 0x03 || c == 0x11) return false; /* Ctrl-C, Ctrl-Q */
- if (c == 0x1B && at + 2 < length && in[at + 1] == '[' && in[at + 2] == '<') {
- /* A mouse report: ESC [ < button ; column ; line, M on press, m on release. */
- unsigned fields[3] = {0, 0, 0}, field = 0;
- size_t end = at + 3;
- for (; end < length && field < 3; ++end) {
- if (in[end] >= '0' && in[end] <= '9') fields[field] = fields[field] * 10 + (unsigned)(in[end] - '0');
- else if (in[end] == ';') ++field;
- else break;
- }
- if (end < length && in[end] == 'M' && field == 2 && fields[2]) mouse(fields[0], fields[2] - 1);
- at = end + 1;
- continue;
+ if (length >= 3 && in[1] == '[' && in[2] == '<') {
+ /* A mouse report: ESC [ < button ; column ; line, M on press, m on release. */
+ unsigned fields[3] = {0, 0, 0}, field = 0;
+ size_t end = 3;
+ for (; end < length && field < 3; ++end) {
+ if (in[end] >= '0' && in[end] <= '9') fields[field] = fields[field] * 10 + (unsigned)(in[end] - '0');
+ else if (in[end] == ';') ++field;
+ else break;
}
- if (c == 0x1B) {
- size_t matched = 0;
- for (size_t i = 0; i < sizeof(sequences) / sizeof(*sequences) && !matched; ++i) {
- size_t n = strlen(sequences[i].sequence);
- if (at + 1 + n <= length && !memcmp(in + at + 1, sequences[i].sequence, n)) {
- handle_key(sequences[i].key);
- matched = 1 + n;
- }
- }
- if (!matched) { handle_key("Escape"); matched = 1; }
- at += matched;
- continue;
+ if (end == length) return final || length == PENDING_MOST ? length : 0; /* cut short (or no report: let go) */
+ if (in[end] == 'M' && field == 2 && fields[2]) mouse(fields[0], fields[2] - 1);
+ return end + 1;
+ }
+ bool longer = false; /* what came begins a sequence still to come whole */
+ for (size_t i = 0; i < sizeof(sequences) / sizeof(*sequences); ++i) {
+ size_t n = strlen(sequences[i].sequence);
+ if (1 + n <= length && !memcmp(in + 1, sequences[i].sequence, n)) {
+ handle_key(sequences[i].key);
+ return 1 + n;
}
- if (c == '\r' || c == '\n') handle_key("Enter");
+ longer = longer || (1 + n > length && !memcmp(in + 1, sequences[i].sequence, length - 1));
+ }
+ if (longer && !final) return 0;
+ handle_key("Escape");
+ return 1;
+}
+
+/* What came (after what waited): false on Ctrl-C or Ctrl-Q. */
+static bool handle_input(const unsigned char *in, size_t length, bool final) {
+ unsigned char bytes[PENDING_MOST + 256];
+ size_t total = pending_length;
+ memcpy(bytes, pending, pending_length);
+ if (length > sizeof(bytes) - total) length = sizeof(bytes) - total;
+ if (length) memcpy(bytes + total, in, length);
+ total += length;
+ pending_length = 0;
+ for (size_t at = 0; at < total;) {
+ unsigned char c = bytes[at];
+ size_t left = total - at, used = 1;
+ if (c == 0x03 || c == 0x11) return false; /* Ctrl-C, Ctrl-Q */
+ if (c == 0x1B) used = escape(bytes + at, left, final);
+ else if (c == '\r' || c == '\n') handle_key("Enter");
else if (c == 0x7F || c == 0x08) handle_key("Backspace");
else if (c == '\t') handle_key("Tab");
else if (c >= 0x20) {
- size_t n = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4;
- if (at + n > length) n = length - at;
- handle_text((const char *)in + at, n);
- at += n;
- continue;
+ used = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4;
+ if (used > left) used = final ? left : 0;
+ if (used) handle_text((const char *)bytes + at, used);
}
- ++at;
+ if (!used) { /* the rest waits for the next read */
+ memcpy(pending, bytes + at, left < PENDING_MOST ? left : PENDING_MOST);
+ pending_length = left < PENDING_MOST ? left : PENDING_MOST;
+ pending_since = vega_clock();
+ return true;
+ }
+ at += used;
}
return true;
}
@@ -501,8 +531,16 @@ static int interactive(void) {
struct pollfd wait = {STDIN_FILENO, POLLIN, 0};
int timeout = screen.ticks ? 16 : watched ? 200 : -1;
if (due >= 0 && (timeout < 0 || due < timeout)) timeout = (int)due;
+ if (pending_length) { /* a key begun waits for its rest (an escape: or is the Escape key) */
+ double left = pending_since + pending_wait() - vega_clock();
+ if (left < 0) left = 0;
+ if (timeout < 0 || left < timeout) timeout = (int)left + 1;
+ }
int ready = poll(&wait, 1, timeout);
app.now = vega_now();
+ if (ready == 0 && pending_length && vega_clock() - pending_since >= pending_wait() &&
+ !handle_input(NULL, 0, true))
+ break;
if (ready == 0) {
if (screen.ticks) pure_app_frame(&app);
if (module_changed()) vega_timers_start(&timers, app.module, vega_clock());
@@ -511,7 +549,7 @@ static int interactive(void) {
if (ready < 0) continue; /* a signal, such as a resize */
unsigned char in[256];
ssize_t got = read(STDIN_FILENO, in, sizeof(in));
- if (got <= 0 || !handle_input(in, (size_t)got)) break;
+ if (got <= 0 || !handle_input(in, (size_t)got, false)) break;
}
restore_terminal();
drawn = NULL;
diff --git a/tests/check.py b/tests/check.py
index efe5027..b556b3a 100644
--- a/tests/check.py
+++ b/tests/check.py
@@ -801,11 +801,15 @@ def scores(vega, module):
return len(out)
-def interactive(vega, module, store=None, replace=None):
+def interactive(vega, module, store=None, replace=None, cut=False):
"""The terminal surface on a real pseudo-terminal: typed text commits
with Enter, arrow keys and letters reach the app after Tab, Ctrl-C leaves
and restores the terminal. With replace, it runs with --watch and replace
- is then written over module: the terminal shows it, with the same tasks."""
+ is then written over module: the terminal shows it, with the same tasks.
+ With cut, every key comes a byte at a time, 50 ms apart, and an arrow's
+ escape sequence 150 ms after its first two bytes: the terminal's reads
+ are not its keys (a UTF-8 character or a sequence cut short waits for
+ its rest)."""
import fcntl, pty, select, struct, termios, time
pid, fd = pty.fork()
if pid == 0:
@@ -827,7 +831,16 @@ def interactive(vega, module, store=None, replace=None):
screen = read_for(0.3)
# Then a click on line 5 (béton's row): a mouse report, press and release.
for keys in (b'milk\r', 'b\u00e9ton\r'.encode(), b'\t', b'\x1b[A', b' ', b'u', b'\x1b[<0;3;5M\x1b[<0;3;5m'):
- os.write(fd, keys)
+ if not cut:
+ os.write(fd, keys)
+ elif keys == b'\x1b[A':
+ os.write(fd, keys[:2])
+ screen += read_for(0.15)
+ os.write(fd, keys[2:])
+ else:
+ for k in range(len(keys)):
+ os.write(fd, keys[k:k + 1])
+ screen += read_for(0.05)
screen += read_for(0.2)
if replace:
shutil.copy(replace, module.with_suffix('.tmp'))
@@ -1723,7 +1736,8 @@ def main():
report['web_screens'] = web(vega, modules['todo'], work, range(args.seeds))
report['browser_steps'] = browser(vega, modules['todo'], work)
report['interactive_sessions'] = interactive(vega, modules['todo']) + \
- interactive(vega, modules['todo'], work / 'interactive-store')
+ interactive(vega, modules['todo'], work / 'interactive-store') + \
+ interactive(vega, modules['todo'], cut=True)
report['stored_surface_lines'] = stored_surfaces(vega, modules['todo'], work, range(args.seeds))
report['hangul'] = hangul(vega, modules['todo'])
report['convention_findings'] = conventions(vega, aup, work, modules)