commit - 7e10c7fa11b86603d677fd679e4c803a1632a037
commit + c2901acefe02363a47b8ac841c82b8523d928de4
blob - ca31895e34e61ec5a0c28053bf5db4bd23eeb7ed
blob + 975f26c2f6d4db1aab0e9beafaad6c7e8337b65a
--- docs/design/m7-curses.md
+++ docs/design/m7-curses.md
Milestone M7 builds the full-screen terminal front end (behavior.md
§2.1): scrolling transcript, modal vi-like input editor, status bar,
-markdown and diff rendering, selectable terminal text, bracketed paste,
+markdown and diff rendering, mouse-wheel scrollback, bracketed paste,
the external editor, the conversation drawer, and the model picker. It
is by far the largest milestone, so it is split into testable pieces
that land green one at a time.
earlier lines. It sends no coordinator message and does not alter conversation
context, journal state, current input, prompt history, or queued messages.
-Bracketed-paste mode is enabled on taking the terminal and disabled on
-every teardown path the process survives; a pledge-violation death
-leaves it unrestored, an accepted fail-closed consequence.
+Mouse tracking and bracketed-paste mode are enabled on taking the terminal and
+disabled on every teardown path the process survives; a pledge-violation death
+leaves them unrestored, an accepted fail-closed consequence. Shift-drag keeps
+the terminal emulator's selection path available while tracking is active.
## The external editor (`v`) — fugu-editor
(`markdown.c`, `editor.c`, `diff.c`, `term_input.c`, each unit-tested),
the coordinator↔tty render/input protocol and terminal-mode driver, and
the curses front end (`ui.c`) — layout, markdown/diff/tool/error
-rendering, scrollback, native terminal text selection,
+rendering, keyboard and mouse-wheel scrollback,
status bar with the context gauge, bell,
bracketed paste, resize, modal editing, completion, history, and the
message queue. A pty driver exercises the whole path end to end.
blob - 9348167eadf07acfc2c3b0dae086684318883e6d
blob + 51ef030eb6edcec09001ce866311db888f104c2a
--- handoff/behavior.md
+++ handoff/behavior.md
`v` composes in the external editor (below); `m` opens the model picker
without editing the prompt.
-On taking the terminal the curses front end explicitly disables xterm mouse
-tracking so the terminal emulator retains ordinary drag selection and copy.
-Transcript scrolling remains available through `PgUp`/`PgDn` and normal-mode
-`J`/`K`. `Up`/`Down` remain prompt-history keys.
+On taking the terminal the curses front end enables xterm mouse tracking so
+wheel up/down scrolls the transcript in either input mode. Terminal emulators
+retain selection and copy through their mouse-override modifier (normally
+Shift-drag). `PgUp`/`PgDn` and normal-mode `J`/`K` also scroll;
+`Up`/`Down` remain prompt-history keys.
Paste is atomic: on taking the terminal the curses front end enables
xterm bracketed-paste mode (and disables it on every teardown path
blob - 63d38c4b7f71a7c9c93672e2e0b57dd4c8497a52
blob + 0dc1fe6659f050ae2c253b8724384ae47f7c5c84
--- handoff/verification.md
+++ handoff/verification.md
and second-overflow errors. A recovery after a completed Tool-result batch
proves the Tool runs once while usage remains additive.
- **Paste and compose:** paste cap enforced, NUL rejected, ESC
- stripped; bracketed-paste mode restored on every teardown path;
+ stripped; mouse and bracketed-paste modes restored on every teardown path;
editor compose leaves input untouched on nonzero exit.
- **Print mode:** each pinned exit code exercised; `-o json` and
`-o ndjson` shapes stable across runs and escaping-correct for
full-pane fill, empty multiline rows, word wrapping, wide/combining UTF-8,
visible normalization of NUL/control/malformed bytes, ASCII transliteration
and matching input-cursor width, narrow-pane fallback, drawer clipping, and
- resize reflow. The curses pty path exercises the styled input and transcript
- through wide curses while retaining clean terminal-mode startup and teardown.
+ resize reflow. The curses pty path exercises the styled input and transcript,
+ enables wheel reporting, and proves an SGR wheel event moves the viewport
+ while retaining clean terminal-mode startup and teardown.
- **Empty-transcript welcome:** a pure suite pins the exact copy, centered
copy rows and shared help-column alignment in
full and drawer-narrowed viewports, ASCII punctuation, whole-row omission on
blob - 6b49d4ceb20b0bca96f81d120cd99d389bc9b04d
blob + a3158d073b6d5531c6aba74dbda4929a025371aa
--- regress/term/Makefile
+++ regress/term/Makefile
PROG= term_test
-SRCS= term_test.c term_input.c utf8.c buf.c log.c xmalloc.c
+SRCS= term_test.c term_input.c ui_terminal.c utf8.c buf.c log.c xmalloc.c
NOMAN= yes
CFLAGS+= -I${.CURDIR}/../../src/fugu-tty
blob - b562dc7a99bb00a01cdce4f625ba2c7ffab55a09
blob + 8dec05de0ac660fe86774be3745875c77d09eeab
--- regress/term/term_test.c
+++ regress/term/term_test.c
*/
#include <string.h>
+#include <unistd.h>
#include "term_input.h"
+#include "ui_terminal.h"
#include "regress.h"
+static int
+terminal_modes_are(int enable, const char *want)
+{
+ char buf[128];
+ size_t wantlen = strlen(want);
+ ssize_t n;
+ int fd[2], ok;
+
+ if (pipe(fd) == -1)
+ return (0);
+ ui_terminal_modes(fd[1], enable);
+ if (close(fd[1]) == -1) {
+ (void)close(fd[0]);
+ return (0);
+ }
+ n = read(fd[0], buf, sizeof(buf));
+ ok = n == (ssize_t)wantlen && memcmp(buf, want, wantlen) == 0;
+ (void)close(fd[0]);
+ return (ok);
+}
+
/* Feed n bytes, then return the first event's key (or -1 if none). */
static int
one_key(const char *bytes, size_t n, uint32_t *cp)
{
uint32_t cp;
+ /* Taking the terminal requests wheel reports and a non-block cursor. */
+ CHECK(terminal_modes_are(1,
+ "\033[?2004h\033[?1000h\033[?1006h\033[6 q"));
+ CHECK(terminal_modes_are(0,
+ "\033[0 q\033[?1006l\033[?1000l\033[?2004l"));
+
/* ---- plain characters ---- */
CHECK(one_key("a", 1, &cp) == K_CHAR && cp == 'a');
CHECK(one_key("\r", 1, NULL) == K_ENTER);
blob - 1033fdbbd5d1b5cd1324d258770ff1b1875c85e7
blob + b2523f52b679fba5452c5331e79bdffa6dfa8991
--- regress/turn/run.sh
+++ regress/turn/run.sh
# the request, the journal commits it) and /quit must exit cleanly.
if [ -x "$obj/ptydrive" ]; then
rm -rf "$HOME/.fugu"
- start_stub "CONTEXT"
+ start_stub "SCROLL"
write_conf "$stubport"
- printf '\000\033o\000\033\014\000m\000\000\r\014\000m\000\000\033\014\000iOWNER-BLOCK-MARKER\014\000\r/quit\r' | \
+ printf '\000\033o\000\033\014\000m\000\000\r\014\000m\000\000\033\014\000iOWNER-BLOCK-MARKER\014\000\r\000\033[<64;12;7M\000\033[<65;12;7M\000/quit\r' | \
TERM=vt100 HOME=$HOME \
FUGU_CONF=$dir/fugu.conf FUGU_LIBEXEC=$dir/libexec/fugu \
FUGU_CA_FILE=$dir/cert.pem FUGU_RETRY_BASE_MS=50 \
rc=$?
ok "curses: clean exit on /quit" $([ "$rc" -eq 0 ]; echo $?)
esc=$(printf '\033')
- ! grep -a -Fq "${esc}[?1000h" "$dir/cursesout" &&
- ! grep -a -Fq "${esc}[?1006h" "$dir/cursesout" &&
+ grep -a -Fq "${esc}[?1000h" "$dir/cursesout" &&
+ grep -a -Fq "${esc}[?1006h" "$dir/cursesout" &&
grep -a -Fq "${esc}[?1006l" "$dir/cursesout" &&
grep -a -Fq "${esc}[?1000l" "$dir/cursesout"
- ok "curses: native text selection remains available" $?
+ ok "curses: wheel reporting is enabled and restored" $?
grep -q "OWNER-BLOCK-MARKER" "$dir/reqout" 2>/dev/null
ok "curses: the typed prompt reached the provider" $?
grep -a -Fq "OWNER-BLOCK-MARKER" "$dir/cursesout"
ok "curses: pre-input picker selection drives the request" $?
grep -a -Fq "Context 46k/111k 41%" "$dir/cursesout"
ok "curses: status shows Lead cache plus session context" $?
+ grep -a -Fq -- "-- scrolled (scroll down to return) --" \
+ "$dir/cursesout"
+ ok "curses: mouse wheel moves the transcript viewport" $?
grep -q "Fugu - Japanese for pufferfish" "$dir/reqout" 2>/dev/null
ok "curses: welcome is absent from the Provider request" \
$([ $? -ne 0 ]; echo $?)
blob - 62d4ec7d88e83b7cf6a0f8b16c2a92d83638fba6
blob + 1dc8f4b5da7568ccc72e10e9ef3e1967e6ee6ef4
--- regress/turn/stub.c
+++ regress/turn/stub.c
* checks the request carried the x-api-key header, then serves a
* canned chunked SSE stream chosen per accepted connection (in argv
* order). Scenarios: OK (a two-delta text reply), CONTEXT (a reply with
- * distinct uncached/cache-read/cache-created/output usage), ERROR (an SSE
+ * distinct uncached/cache-read/cache-created/output usage), SCROLL (that
+ * usage plus more than one terminal viewport of text), ERROR (an SSE
* error event), HTTP500 (a non-2xx before any stream), HTTP401 (an
* Anthropic-style JSON error body), RETRY503/RETRY429 (retryable
* statuses, 429 carrying Retry-After: 2), EMPTY (a well-formed
"event: message_stop\n"
"data: {\"type\":\"message_stop\"}\n\n";
+/* More than one 40-row pty viewport, for real wheel-scroll coverage. */
+static const char sse_scroll[] =
+"event: message_start\n"
+"data: {\"type\":\"message_start\",\"message\":{\"usage\":"
+ "{\"input_tokens\":11000,\"output_tokens\":0,"
+ "\"cache_read_input_tokens\":23000,"
+ "\"cache_creation_input_tokens\":5000}}}\n\n"
+"event: content_block_start\n"
+"data: {\"type\":\"content_block_start\",\"index\":0,"
+ "\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n"
+"event: content_block_delta\n"
+"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":"
+ "{\"type\":\"text_delta\",\"text\":\"SCROLL-TOP\\n"
+ "line 01\\nline 02\\nline 03\\nline 04\\nline 05\\n"
+ "line 06\\nline 07\\nline 08\\nline 09\\nline 10\\n"
+ "line 11\\nline 12\\nline 13\\nline 14\\nline 15\\n"
+ "line 16\\nline 17\\nline 18\\nline 19\\nline 20\\n"
+ "line 21\\nline 22\\nline 23\\nline 24\\nline 25\\n"
+ "line 26\\nline 27\\nline 28\\nline 29\\nline 30\\n"
+ "line 31\\nline 32\\nline 33\\nline 34\\nline 35\\n"
+ "line 36\\nline 37\\nline 38\\nline 39\\nline 40\\n"
+ "line 41\\nline 42\\nline 43\\nline 44\\nSCROLL-BOTTOM\"}}\n\n"
+"event: content_block_stop\n"
+"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n"
+"event: message_delta\n"
+"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":"
+ "\"end_turn\"},\"usage\":{\"output_tokens\":7000}}\n\n"
+"event: message_stop\n"
+"data: {\"type\":\"message_stop\"}\n\n";
+
static const char sse_error[] =
"event: error\n"
"data: {\"type\":\"error\",\"error\":{\"type\":\"overloaded_error\","
write_chunk(conn, sse_context,
strlen(sse_context));
(void)tls_write_all(conn, "0\r\n\r\n", 5);
+ } else if (strcmp(argv[i], "SCROLL") == 0) {
+ (void)tls_write_all(conn, body_ok,
+ strlen(body_ok));
+ write_chunk(conn, sse_scroll,
+ strlen(sse_scroll));
+ (void)tls_write_all(conn, "0\r\n\r\n", 5);
} else if (strcmp(argv[i], "ERROR") == 0) {
(void)tls_write_all(conn, body_ok,
strlen(body_ok));
blob - cef615c14a41843149a5070594aec7c507885bd8
blob + c5f342325ba6f1e3708a8305ffba86c72a09b331
--- regress/ui_layout/ui_layout_test.c
+++ regress/ui_layout/ui_layout_test.c
"\xef\xbf\xbd(\xef\xbf\xbd(B";
struct ui_layout layout = { 0 };
struct ui_picker_layout picker;
+ short fg, bg;
int attr = 0x1234;
/* One named treatment serves color and monochrome surfaces. */
CHECK(ui_owner_attr(0) == (A_BOLD | A_REVERSE));
CHECK(ui_owner_attr(1) == (A_BOLD | COLOR_PAIR(P_OWNER)));
+ ui_owner_colors(256, &fg, &bg);
+ CHECK(fg == 255 && bg == 238);
+ ui_owner_colors(8, &fg, &bg);
+ CHECK(fg == COLOR_BLACK && bg == COLOR_WHITE);
ui_layout_plain(&layout, "hello", 5, 10, attr, "> ", " ", 1, 0);
CHECK(layout.nlines == 1);
CHECK(layout.nlines == 1 && line_is(&layout.lines[0], "> "));
CHECK(layout.lines[0].fill_width == 12);
+ /* Scrollback leaves tail-following, moves both ways, and rejoins it. */
+ ui_layout_reset(&layout);
+ for (attr = 0; attr < 30; attr++)
+ (void)ui_layout_line(&layout);
+ CHECK(ui_layout_tail_top(&layout, 10, 0) == 20);
+ attr = ui_layout_scroll_top(&layout, 10, 0, -1, -1);
+ CHECK(attr == 19);
+ attr = ui_layout_scroll_top(&layout, 10, 0, attr, -5);
+ CHECK(attr == 14);
+ attr = ui_layout_scroll_top(&layout, 10, 0, attr, 5);
+ CHECK(attr == 19);
+ CHECK(ui_layout_scroll_top(&layout, 10, 0, attr, 1) == -1);
+ CHECK(ui_layout_scroll_top(&layout, 10, 0, -1, 1) == -1);
+ /* A Ctrl-L boundary constrains only the tail; explicit up crosses it. */
+ CHECK(ui_layout_tail_top(&layout, 25, 12) == 12);
+ CHECK(ui_layout_scroll_top(&layout, 25, 12, -1, -1) == 11);
+
ui_layout_free(&layout);
CHECK(layout.lines == NULL && layout.nlines == 0 && layout.cap == 0);
REGRESS_END();
blob - 2041e5d547ead5c1b66dee703aac759d58e5889b
blob + 4d2b01f14b68045d32da30d0b4f7f4cd4ba6db73
--- src/fugu/fugu.1
+++ src/fugu/fugu.1
.El
.
.Pp
-Mouse tracking is disabled so the terminal emulator's ordinary drag selection
-and copy remain available.
-Use
+Mouse-wheel up and down scroll the transcript in either input mode.
+Terminal selection and copy remain available with the emulator's mouse
+override modifier, normally Shift-drag.
+Alternatively, use
.Ic PageUp , PageDown
or normal-mode
.Ic J , K
blob - 653d4cb2e00b5fcbb159c201000a25766325e20f
blob + 1f451f35e3ca92f504391bf8922936486df4f109
--- src/fugu-tty/Makefile
+++ src/fugu-tty/Makefile
PROG= fugu-tty
-SRCS= main.c ui.c ui_config_rx.c ui_layout.c ui_palette.c welcome.c \
- markdown.c editor.c ascii.c term_input.c utf8.c worker.c imsgev.c \
- buf.c log.c xmalloc.c
+SRCS= main.c ui.c ui_config_rx.c ui_layout.c ui_palette.c ui_terminal.c \
+ welcome.c markdown.c editor.c ascii.c term_input.c utf8.c worker.c \
+ imsgev.c buf.c log.c xmalloc.c
BINDIR= ${LIBEXECDIR}/fugu
MAN=
blob - 855c795675d38edeb497206165058d744fdbe679
blob + b8e4c6412db521d777b91d9e3f122513910f36bf
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
#include "ui_config_rx.h"
#include "ui_layout.h"
#include "ui_palette.h"
+#include "ui_terminal.h"
#include "welcome.h"
#include "ui.h"
struct welcome_field field[WELCOME_CAPTIONS_MAX];
};
-/* Terminal modes owned by fugu-tty. Mouse tracking is deliberately kept
- * off so the terminal emulator retains ordinary drag selection. */
-static const char terminal_modes_on[] =
- "\033[?1006l\033[?1000l\033[?2004h";
-static const char terminal_modes_off[] =
- "\033[?2004l";
-
-static void
-terminal_modes(int enable)
-{
- if (enable)
- (void)write(STDOUT_FILENO, terminal_modes_on,
- sizeof(terminal_modes_on) - 1);
- else
- (void)write(STDOUT_FILENO, terminal_modes_off,
- sizeof(terminal_modes_off) - 1);
-}
-
/* ---- small helpers -------------------------------------------------- */
static int
if (n < 0 || (size_t)n >= sizeof(left))
left[0] = '\0';
if (u->scroll != -1 && strlcat(left,
- " -- scrolled (End to return) --", sizeof(left)) >=
+ " -- scrolled (scroll down to return) --", sizeof(left)) >=
sizeof(left))
left[sizeof(left) - 1] = '\0';
if (u->nqueue > 0) {
attrset(A_NORMAL);
}
-/* Tail view starts after the latest Ctrl-L boundary until enough new output
- * fills the viewport. Explicit scrollback may still cross the boundary. */
-static int
-tail_top(const struct ui *u, int height)
-{
- int top = (int)u->layout.nlines - height;
-
- if (top < u->clear_line)
- top = u->clear_line;
- return (top < 0 ? 0 : top);
-}
-
static void
draw(struct ui *u)
{
welcome_visible(&u->welcome, u->layout.nlines);
if (u->scroll == -1)
- top = tail_top(u, th);
+ top = ui_layout_tail_top(&u->layout, th, u->clear_line);
else
top = u->scroll;
if (top < 0)
scroll_by(struct ui *u, int delta)
{
int th = transcript_height(u);
- int maxtop;
if (u->dirty)
- rebuild(u); /* refresh nlines before deriving maxtop */
- maxtop = tail_top(u, th);
- if (u->scroll == -1)
- u->scroll = maxtop;
- u->scroll += delta;
- if (u->scroll < 0)
- u->scroll = 0;
- if (u->scroll >= maxtop)
- u->scroll = -1; /* back to following the tail */
+ rebuild(u); /* refresh nlines before deriving bounds */
+ u->scroll = ui_layout_scroll_top(&u->layout, th, u->clear_line,
+ u->scroll, delta);
}
/* ---- input ---------------------------------------------------------- */
u->esc_pending = 0;
}
event_del(&u->ev_in);
- terminal_modes(0);
+ ui_terminal_modes(STDOUT_FILENO, 0);
endwin();
imsgev_send_chunks(&u->w->iev, FUGU_IMSG_UI_COMPOSE, 0, text, len);
imsgev_send(&u->w->iev, FUGU_IMSG_UI_COMPOSE_GO, 0,
return;
}
u->composing = 0;
- terminal_modes(1);
refresh(); /* return from endwin to program mode */
+ ui_terminal_modes(STDOUT_FILENO, 1);
clearok(stdscr, TRUE);
if (!r->ok) {
if (UI == NULL)
return;
ui_layout_free(&UI->layout);
- terminal_modes(0);
+ ui_terminal_modes(STDOUT_FILENO, 0);
endwin();
UI = NULL;
}
curs_set(1);
if (has_colors())
ui_palette_init();
- terminal_modes(1);
+ ui_terminal_modes(STDOUT_FILENO, 1);
u = xcalloc(1, sizeof(*u));
u->w = w;
blob - cc5cdd88b5362488a68cf9051d3cd5574fc24c80
blob + be0f09ceaf5086f10f76a0aefe78378e6ca814a7
--- src/fugu-tty/ui_layout.c
+++ src/fugu-tty/ui_layout.c
layout->top = (usable - layout->height) / 2;
layout->model_rows = layout->height > 3 ? layout->height - 3 : 0;
}
+
+/* Tail view honors the latest Ctrl-L boundary until output fills the pane. */
+int
+ui_layout_tail_top(const struct ui_layout *layout, int height, int clear_line)
+{
+ int lines, top;
+
+ lines = layout->nlines > INT_MAX ? INT_MAX : (int)layout->nlines;
+ if (height < 1)
+ height = 1;
+ top = lines - height;
+ if (top < clear_line)
+ top = clear_line;
+ if (top < 0)
+ top = 0;
+ if (top > lines)
+ top = lines;
+ return (top);
+}
+
+int
+ui_layout_scroll_top(const struct ui_layout *layout, int height,
+ int clear_line, int current, int delta)
+{
+ int maxtop;
+
+ maxtop = ui_layout_tail_top(layout, height, clear_line);
+ if (current == -1)
+ current = maxtop;
+ if (delta < 0) {
+ if (delta == INT_MIN || current < -delta)
+ current = 0;
+ else
+ current += delta;
+ } else if (delta > 0 && current > INT_MAX - delta)
+ current = maxtop;
+ else
+ current += delta;
+ if (current < 0)
+ current = 0;
+ if (current >= maxtop)
+ return (-1);
+ return (current);
+}
blob - fcefa1bfce29974926fa660ea36202ef6c5ac19a
blob + 87ae47abeb48903eda250048330a15d6c711ffce
--- src/fugu-tty/ui_layout.h
+++ src/fugu-tty/ui_layout.h
int, const char *, const char *, int, int);
int ui_layout_visible_fill(const struct ui_line *, int, int);
void ui_layout_picker(struct ui_picker_layout *, int, size_t);
+int ui_layout_tail_top(const struct ui_layout *, int, int);
+int ui_layout_scroll_top(const struct ui_layout *, int, int, int,
+ int);
#endif /* UI_LAYOUT_H */
blob - e7d9863b6f3d1952791f34d8555e14c3e2822a62
blob + bce04db6089c5233e9a408ceec150250de346e67
--- src/fugu-tty/ui_palette.c
+++ src/fugu-tty/ui_palette.c
void
ui_palette_init(void)
{
+ short owner_fg, owner_bg;
+
start_color();
use_default_colors();
init_pair(P_HEAD, COLOR_YELLOW, -1);
init_pair(P_ERR, COLOR_RED, -1);
init_pair(P_DIM, COLOR_BLUE, -1);
init_pair(P_TEXT, COLOR_WHITE, -1);
- init_pair(P_OWNER, COLOR_BLACK, COLOR_WHITE);
+ ui_owner_colors(COLORS, &owner_fg, &owner_bg);
+ init_pair(P_OWNER, owner_fg, owner_bg);
}
int
{
return (A_BOLD | (color ? COLOR_PAIR(P_OWNER) : A_REVERSE));
}
+
+void
+ui_owner_colors(int colors, short *fg, short *bg)
+{
+ if (colors >= 256) {
+ *fg = 255; /* light foreground */
+ *bg = 238; /* dark grey background */
+ } else {
+ *fg = COLOR_BLACK;
+ *bg = COLOR_WHITE;
+ }
+}
blob - e49ac5a61b752f0d1b47c30dd6c30d32e7aede06
blob + 3d99b43d5b11b3b85cc06dd1ef7f21092ced977e
--- src/fugu-tty/ui_palette.h
+++ src/fugu-tty/ui_palette.h
void ui_palette_init(void);
int ui_owner_attr(int);
+void ui_owner_colors(int, short *, short *);
#endif /* UI_PALETTE_H */
blob - /dev/null
blob + 1a0ab46cab2a8725e1b26269cd988536ced0ca57 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_terminal.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <unistd.h>
+
+#include "ui_terminal.h"
+
+/* Wheel reporting, bracketed paste, and a narrow cursor are UI-owned modes. */
+static const char terminal_modes_on[] =
+ "\033[?2004h\033[?1000h\033[?1006h\033[6 q";
+static const char terminal_modes_off[] =
+ "\033[0 q\033[?1006l\033[?1000l\033[?2004l";
+
+void
+ui_terminal_modes(int fd, int enable)
+{
+ const char *modes;
+ size_t len;
+
+ if (enable) {
+ modes = terminal_modes_on;
+ len = sizeof(terminal_modes_on) - 1;
+ } else {
+ modes = terminal_modes_off;
+ len = sizeof(terminal_modes_off) - 1;
+ }
+ (void)write(fd, modes, len);
+}
blob - /dev/null
blob + 887bef34b9fb8cd67db5dd094f0c8ae62e663673 (mode 644)
--- /dev/null
+++ src/fugu-tty/ui_terminal.h
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef UI_TERMINAL_H
+#define UI_TERMINAL_H
+
+void ui_terminal_modes(int, int);
+
+#endif /* UI_TERMINAL_H */