commit 3a70517b1b3371da4e8c33fc991db7fea4f84caf from: Isaac Meerleo date: Fri Jul 17 14:00:06 2026 UTC add mouse-wheel transcript scrolling 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.