Commit Diff


commit - f3d1acfbd1e73e25b5c6dd5ac6e2e9c7c9ddfd35
commit + 3a70517b1b3371da4e8c33fc991db7fea4f84caf
blob - 37bf30d1809764ae9daa9373ae61d981ff86ae69
blob + 798f9e90dbe3a9a6332e8b66247410dc489e689f
--- regress/editor/editor_test.c
+++ regress/editor/editor_test.c
@@ -338,5 +338,15 @@ main(void)
 	CHECK(ed_feed(&e, K_PGUP, 0, NULL, 0) == R_PAGE_UP);
 	ed_free(&e);
 
+	/* Mouse-wheel events scroll the transcript without recalling history. */
+	ed_init(&e);
+	ed_history_add(&e, "old command", 11);
+	type(&e, "draft");
+	CHECK(ed_feed(&e, K_WHEEL_UP, 0, NULL, 0) == R_SCROLL_LINE_UP);
+	CHECK(text_is(&e, "draft"));
+	CHECK(ed_feed(&e, K_WHEEL_DOWN, 0, NULL, 0) == R_SCROLL_LINE_DOWN);
+	CHECK(text_is(&e, "draft"));
+	ed_free(&e);
+
 	REGRESS_END();
 }
blob - c40e8d28c1b55f1f46af4bb9b948aeba1739b689
blob + b562dc7a99bb00a01cdce4f625ba2c7ffab55a09
--- regress/term/term_test.c
+++ regress/term/term_test.c
@@ -65,6 +65,42 @@ main(void)
 	CHECK(one_key("\x1b[6~", 4, NULL) == K_PGDN);
 	CHECK(one_key("\x1b[Z", 3, NULL) == K_BTAB);
 
+	/* ---- SGR mouse wheel: distinct from editor-history arrows ---- */
+	CHECK(one_key("\x1b[<64;12;7M", sizeof("\x1b[<64;12;7M") - 1,
+	    NULL) == K_WHEEL_UP);
+	CHECK(one_key("\x1b[<65;12;7M", sizeof("\x1b[<65;12;7M") - 1,
+	    NULL) == K_WHEEL_DOWN);
+	/* Ctrl-modified wheel still uses the low button bits. */
+	CHECK(one_key("\x1b[<80;12;7M", sizeof("\x1b[<80;12;7M") - 1,
+	    NULL) == K_WHEEL_UP);
+	{
+		struct term_input	ti;
+		struct ti_key		k;
+
+		ti_init(&ti);
+		ti_feed(&ti, (const unsigned char *)"\x1b[<64;12", 8);
+		CHECK(ti_next(&ti, &k) == TI_NONE);
+		ti_feed(&ti, (const unsigned char *)";7M", 3);
+		CHECK(ti_next(&ti, &k) == TI_KEY && k.key == K_WHEEL_UP);
+		ti_free(&ti);
+	}
+	/* Unused mouse events are consumed whole; coordinates never become
+	 * editor input, and the next ordinary key remains available. */
+	{
+		struct term_input	ti;
+		struct ti_key		k;
+
+		ti_init(&ti);
+		ti_feed(&ti, (const unsigned char *)"\x1b[<0;12;7Mx",
+		    sizeof("\x1b[<0;12;7Mx") - 1);
+		CHECK(ti_next(&ti, &k) == TI_KEY && k.key == K_CHAR &&
+		    k.cp == 'x');
+		CHECK(ti_next(&ti, &k) == TI_NONE);
+		ti_free(&ti);
+	}
+	CHECK(one_key("\x1b[<64;12;7m", sizeof("\x1b[<64;12;7m") - 1,
+	    NULL) == -1);
+
 	/* ---- SS3 arrows ---- */
 	CHECK(one_key("\x1bOA", 3, NULL) == K_UP);
 	CHECK(one_key("\x1bOF", 3, NULL) == K_END);
blob - 67abed12fae8a686c29bd7db01941150df10884b
blob + 5409a30504055bbd8724f303b025b81677ac02d2
--- src/fugu-tty/editor.c
+++ src/fugu-tty/editor.c
@@ -586,6 +586,10 @@ feed_insert(struct editor *e, enum ed_key k, uint32_t 
 	case K_BTAB:
 		do_complete(e, 0);
 		return (R_NONE);
+	case K_WHEEL_UP:
+		return (R_SCROLL_LINE_UP);
+	case K_WHEEL_DOWN:
+		return (R_SCROLL_LINE_DOWN);
 	case K_PGUP:
 		return (R_PAGE_UP);
 	case K_PGDN:
@@ -637,6 +641,10 @@ feed_normal(struct editor *e, enum ed_key k, uint32_t 
 		case K_DOWN:
 			recall_next(e);
 			return (R_NONE);
+		case K_WHEEL_UP:
+			return (R_SCROLL_LINE_UP);
+		case K_WHEEL_DOWN:
+			return (R_SCROLL_LINE_DOWN);
 		case K_PGUP:
 			return (R_PAGE_UP);
 		case K_PGDN:
blob - 90a435e08fdadb892bf4903e1095c85f24c25c06
blob + e1002aeb280f617e8da7ed1cef8f8a3bc4fcd134
--- src/fugu-tty/editor.h
+++ src/fugu-tty/editor.h
@@ -56,6 +56,8 @@ enum ed_key {
 	K_END,
 	K_PGUP,
 	K_PGDN,
+	K_WHEEL_UP,
+	K_WHEEL_DOWN,
 	K_CTRL_C,
 	K_CTRL_D,
 	K_CTRL_L,
blob - 9d083687063c4e4c052a31aa87a8db4a17ec48d2
blob + 8253851f7ef29861121167ec0be2a7fdb8bde858
--- src/fugu-tty/term_input.c
+++ src/fugu-tty/term_input.c
@@ -127,6 +127,75 @@ csi_key(int param, unsigned char final, struct ti_key 
 	return (0);
 }
 
+/*
+ * Parse an xterm SGR mouse frame: CSI < Cb ; Cx ; Cy M/m.  Basic mouse
+ * tracking reports wheel motion as button 64/65; modifier bits do not alter
+ * the low button bits.  Every complete mouse frame is consumed, including
+ * clicks, releases, and horizontal-wheel events that fugu does not use, so
+ * their coordinates can never leak into the editor as typed text.
+ */
+static enum ti_result
+step_sgr_mouse(struct term_input *ti, struct ti_key *out, int *again)
+{
+	size_t		 i = 3;
+	unsigned int	 button = 0;
+	int		 field;
+	unsigned char	 final;
+
+	for (field = 0; field < 3; field++) {
+		if (i >= ti->len)
+			return (TI_NONE);
+		if (ti->buf[i] < '0' || ti->buf[i] > '9')
+			goto malformed;
+		while (i < ti->len && ti->buf[i] >= '0' &&
+		    ti->buf[i] <= '9') {
+			if (field == 0) {
+				if (button <= 255)
+					button = button * 10 +
+					    (ti->buf[i] - '0');
+				if (button > 255)
+					button = 256;
+			}
+			i++;
+			if (i > 32)
+				goto malformed;
+		}
+		if (i >= ti->len)
+			return (TI_NONE);
+		if (field < 2) {
+			if (ti->buf[i] != ';')
+				goto malformed;
+			i++;
+		}
+	}
+
+	if (i >= ti->len)
+		return (TI_NONE);
+	final = ti->buf[i];
+	if (final != 'M' && final != 'm')
+		goto malformed;
+	consume(ti, i + 1);
+	*again = 1;
+	if (final != 'M' || (button & 64) == 0)
+		return (TI_NONE);
+	if ((button & 3) == 0) {
+		out->key = K_WHEEL_UP;
+		return (TI_KEY);
+	}
+	if ((button & 3) == 1) {
+		out->key = K_WHEEL_DOWN;
+		return (TI_KEY);
+	}
+	return (TI_NONE);
+
+malformed:
+	/* Make progress on a malformed bounded frame without swallowing the
+	 * ordinary byte that follows it. */
+	consume(ti, i + 1);
+	*again = 1;
+	return (TI_NONE);
+}
+
 /* Parse one CSI/SS3 sequence at buf[0]. */
 static enum ti_result
 step_escape(struct term_input *ti, struct ti_key *out, int *again)
@@ -150,6 +219,8 @@ step_escape(struct term_input *ti, struct ti_key *out,
 			*again = 1;
 			return (TI_NONE);
 		}
+		if (ti->len >= 3 && ti->buf[2] == '<')
+			return (step_sgr_mouse(ti, out, again));
 		{
 			size_t	i = 2;
 			int	param = 0, haveparam = 0;
blob - 33e812698b9756cd4f898ba089f68dfac855b13c
blob + b52badcb7e0f7370c675ffac5a63db6c48a127fc
--- src/fugu-tty/term_input.h
+++ src/fugu-tty/term_input.h
@@ -26,8 +26,9 @@
 /*
  * The terminal input decoder (behavior.md 2.1): a pure state machine
  * turning a raw byte stream from the terminal into editor key events --
- * UTF-8 assembly, CSI/SS3 escape sequences to key symbols, and xterm
- * bracketed paste (ESC[200~ ... ESC[201~) collected as one atomic event.
+ * UTF-8 assembly, CSI/SS3 escape sequences to key symbols, xterm SGR mouse
+ * wheel reports, and bracketed paste (ESC[200~ ... ESC[201~) collected as
+ * one atomic event.
  * It is fed bytes incrementally (input arrives in bursts) and drained one
  * event at a time, so nothing about input handling depends on wgetch and
  * the whole thing is unit-tested headless.