From 1e61e524003f7fa0f8e1b2b8c0a21cce000d3fa6 Mon Sep 17 00:00:00 2001 From: nicm Date: Thu, 30 Oct 2025 07:41:19 +0000 Subject: [PATCH] If tmux receives a palette request (OSC 4) in a pane and the palette entry has not been set, send a request to the most recently used client and forward any response instead. Based on change from Tim Culverhouse in GitHub issue 4665. --- input.c | 272 ++++++++++++++++++++++++++++++++++++++++-------- server-client.c | 8 ++ tmux.h | 26 +++++ tty-keys.c | 76 ++++++++++++++ tty.c | 6 +- 5 files changed, 340 insertions(+), 48 deletions(-) diff --git a/input.c b/input.c index 4a9a1881..bf6cb48a 100644 --- a/input.c +++ b/input.c @@ -51,6 +51,20 @@ * be passed to the underlying terminals. */ +/* Request sent by a pane. */ +struct input_request { + struct client *c; + struct input_ctx *ictx; + + enum input_request_type type; + int idx; + time_t t; + + TAILQ_ENTRY(input_request) entry; + TAILQ_ENTRY(input_request) centry; +}; +#define INPUT_REQUEST_TIMEOUT 5 + /* Input parser cell. */ struct input_cell { struct grid_cell cell; @@ -72,61 +86,69 @@ struct input_param { }; }; +/* Type of terminator. */ +enum input_end_type { + INPUT_END_ST, + INPUT_END_BEL +}; + /* Input parser context. */ struct input_ctx { - struct window_pane *wp; - struct bufferevent *event; - struct screen_write_ctx ctx; - struct colour_palette *palette; + struct window_pane *wp; + struct bufferevent *event; + struct screen_write_ctx ctx; + struct colour_palette *palette; - struct input_cell cell; + struct input_cell cell; + struct input_cell old_cell; + u_int old_cx; + u_int old_cy; + int old_mode; - struct input_cell old_cell; - u_int old_cx; - u_int old_cy; - int old_mode; + u_char interm_buf[4]; + size_t interm_len; - u_char interm_buf[4]; - size_t interm_len; - - u_char param_buf[64]; - size_t param_len; + u_char param_buf[64]; + size_t param_len; #define INPUT_BUF_START 32 - u_char *input_buf; - size_t input_len; - size_t input_space; - enum { - INPUT_END_ST, - INPUT_END_BEL - } input_end; + u_char *input_buf; + size_t input_len; + size_t input_space; + enum input_end_type input_end; - struct input_param param_list[24]; - u_int param_list_len; + struct input_param param_list[24]; + u_int param_list_len; - struct utf8_data utf8data; - int utf8started; + struct utf8_data utf8data; + int utf8started; - int ch; - struct utf8_data last; + int ch; + struct utf8_data last; - int flags; + const struct input_state *state; + int flags; #define INPUT_DISCARD 0x1 #define INPUT_LAST 0x2 - const struct input_state *state; - - struct event timer; + struct input_requests requests[INPUT_REQUEST_TYPES]; + u_int request_count; + struct event request_timer; /* * All input received since we were last in the ground state. Sent to * control clients on connection. */ - struct evbuffer *since_ground; + struct evbuffer *since_ground; + struct event ground_timer; }; /* Helper functions. */ struct input_transition; +static void input_request_timer_callback(int, short, void *); +static void input_start_request_timer(struct input_ctx *); +static int input_add_request(struct input_ctx *, enum input_request_type, + int); static int input_split(struct input_ctx *); static int input_get(struct input_ctx *, u_int, int, int); static void printflike(2, 3) input_reply(struct input_ctx *, const char *, ...); @@ -766,7 +788,7 @@ input_stop_utf8(struct input_ctx *ictx) * long, so reset to ground. */ static void -input_timer_callback(__unused int fd, __unused short events, void *arg) +input_ground_timer_callback(__unused int fd, __unused short events, void *arg) { struct input_ctx *ictx = arg; @@ -776,12 +798,12 @@ input_timer_callback(__unused int fd, __unused short events, void *arg) /* Start the timer. */ static void -input_start_timer(struct input_ctx *ictx) +input_start_ground_timer(struct input_ctx *ictx) { struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; - event_del(&ictx->timer); - event_add(&ictx->timer, &tv); + event_del(&ictx->ground_timer); + event_add(&ictx->ground_timer, &tv); } /* Reset cell state to default. */ @@ -830,6 +852,7 @@ input_init(struct window_pane *wp, struct bufferevent *bev, struct colour_palette *palette) { struct input_ctx *ictx; + u_int i; ictx = xcalloc(1, sizeof *ictx); ictx->wp = wp; @@ -842,8 +865,11 @@ input_init(struct window_pane *wp, struct bufferevent *bev, ictx->since_ground = evbuffer_new(); if (ictx->since_ground == NULL) fatalx("out of memory"); + evtimer_set(&ictx->ground_timer, input_ground_timer_callback, ictx); - evtimer_set(&ictx->timer, input_timer_callback, ictx); + for (i = 0; i < INPUT_REQUEST_TYPES; i++) + TAILQ_INIT(&ictx->requests[i]); + evtimer_set(&ictx->request_timer, input_request_timer_callback, ictx); input_reset(ictx, 0); return (ictx); @@ -853,17 +879,30 @@ input_init(struct window_pane *wp, struct bufferevent *bev, void input_free(struct input_ctx *ictx) { - u_int i; + struct input_request_list *irl; + struct input_request *ir, *ir1; + u_int i; for (i = 0; i < ictx->param_list_len; i++) { if (ictx->param_list[i].type == INPUT_STRING) free(ictx->param_list[i].str); } - event_del(&ictx->timer); + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + TAILQ_FOREACH_SAFE(ir, &ictx->requests[i], entry, ir1) { + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, ir->c->name, ictx->wp->id, i); + irl = &ir->c->input_requests[i]; + TAILQ_REMOVE(&ictx->requests[i], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + free(ir); + } + } + event_del(&ictx->request_timer); free(ictx->input_buf); evbuffer_free(ictx->since_ground); + event_del(&ictx->ground_timer); free(ictx); } @@ -1122,7 +1161,7 @@ input_reply(struct input_ctx *ictx, const char *fmt, ...) static void input_clear(struct input_ctx *ictx) { - event_del(&ictx->timer); + event_del(&ictx->ground_timer); *ictx->interm_buf = '\0'; ictx->interm_len = 0; @@ -1142,7 +1181,7 @@ input_clear(struct input_ctx *ictx) static void input_ground(struct input_ctx *ictx) { - event_del(&ictx->timer); + event_del(&ictx->ground_timer); evbuffer_drain(ictx->since_ground, EVBUFFER_LENGTH(ictx->since_ground)); if (ictx->input_space > INPUT_BUF_START) { @@ -2385,7 +2424,7 @@ input_enter_dcs(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2511,7 +2550,7 @@ input_enter_osc(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2606,7 +2645,7 @@ input_enter_apc(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2637,7 +2676,7 @@ input_enter_rename(struct input_ctx *ictx) log_debug("%s", __func__); input_clear(ictx); - input_start_timer(ictx); + input_start_ground_timer(ictx); ictx->flags &= ~INPUT_LAST; } @@ -2767,8 +2806,12 @@ input_osc_4(struct input_ctx *ictx, const char *p) s = strsep(&next, ";"); if (strcmp(s, "?") == 0) { c = colour_palette_get(palette, idx|COLOUR_FLAG_256); - if (c != -1) + if (c != -1) { input_osc_colour_reply(ictx, 4, idx, c); + s = next; + continue; + } + input_add_request(ictx, INPUT_REQUEST_PALETTE, idx); s = next; continue; } @@ -3110,6 +3153,143 @@ input_set_buffer_size(size_t buffer_size) input_buffer_size = buffer_size; } +/* Request timer. Remove any requests that are too old. */ +static void +input_request_timer_callback(__unused int fd, __unused short events, void *arg) +{ + struct input_ctx *ictx = arg; + struct input_request *ir, *ir1; + struct input_request_list *irl; + u_int i; + time_t t = time(NULL); + + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + TAILQ_FOREACH_SAFE(ir, &ictx->requests[i], entry, ir1) { + if (ir->t >= t - INPUT_REQUEST_TIMEOUT) + continue; + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, ir->c->name, ictx->wp->id, ir->type); + irl = &ir->c->input_requests[i]; + TAILQ_REMOVE(&ictx->requests[i], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + ictx->request_count--; + free(ir); + } + } + if (ictx->request_count != 0) + input_start_request_timer(ictx); +} + +/* Start the request timer. */ +static void +input_start_request_timer(struct input_ctx *ictx) +{ + struct timeval tv = { .tv_sec = 1, .tv_usec = 0 }; + + event_del(&ictx->request_timer); + event_add(&ictx->request_timer, &tv); +} + +/* Add a request. */ +static int +input_add_request(struct input_ctx *ictx, enum input_request_type type, int idx) +{ + struct window_pane *wp = ictx->wp; + struct window *w; + struct client *c = NULL, *loop; + struct input_request *ir; + char s[64]; + + if (wp == NULL) + return (-1); + w = wp->window; + + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_UNATTACHEDFLAGS) + continue; + if (loop->session == NULL || !session_has(loop->session, w)) + continue; + if (~loop->tty.flags & TTY_STARTED) + continue; + if (c == NULL) + c = loop; + else if (timercmp(&loop->activity_time, &c->activity_time, >)) + c = loop; + } + if (c == NULL) + return (-1); + + ir = xcalloc (1, sizeof *ir); + ir->c = c; + ir->ictx = ictx; + ir->type = type; + ir->idx = idx; + ir->t = time(NULL); + TAILQ_INSERT_TAIL(&ictx->requests[type], ir, entry); + TAILQ_INSERT_TAIL(&c->input_requests[type].requests, ir, centry); + if (++ictx->request_count == 1) + input_start_request_timer(ictx); + log_debug("%s: req %p: client %s, pane %%%u, type %d", __func__, ir, + c->name, wp->id, type); + + switch (type) { + case INPUT_REQUEST_PALETTE: + xsnprintf(s, sizeof s, "\033]4;%d;?\033\\", idx); + tty_puts(&c->tty, s); + break; + } + + return (0); +} + +/* Handle a reply to a request. */ +void +input_request_reply(struct client *c, enum input_request_type type, void *data) +{ + struct input_request_list *irl = &c->input_requests[type]; + struct input_request *ir, *ir1; + struct input_request_palette_data *pd = data; + + TAILQ_FOREACH_SAFE(ir, &irl->requests, centry, ir1) { + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, c->name, ir->ictx->wp->id, ir->type); + switch (type) { + case INPUT_REQUEST_PALETTE: + if (pd->idx != ir->idx) + continue; + input_osc_colour_reply(ir->ictx, 4, pd->idx, pd->c); + break; + } + TAILQ_REMOVE(&ir->ictx->requests[type], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + ir->ictx->request_count--; + free(ir); + break; + } +} + +/* Cancel pending requests for client. */ +void +input_cancel_requests(struct client *c) +{ + struct input_request_list *irl; + struct input_request *ir, *ir1; + u_int i; + + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + irl = &c->input_requests[i]; + TAILQ_FOREACH_SAFE(ir, &irl->requests, entry, ir1) { + log_debug("%s: req %p: client %s, pane %%%u, type %d", + __func__, ir, c->name, ir->ictx->wp->id, ir->type); + TAILQ_REMOVE(&ir->ictx->requests[i], ir, entry); + TAILQ_REMOVE(&irl->requests, ir, centry); + ir->ictx->request_count--; + free(ir); + } + } +} + +/* Report current theme. */ static void input_report_current_theme(struct input_ctx *ictx) { diff --git a/server-client.c b/server-client.c index 34240cdb..d2a5a80c 100644 --- a/server-client.c +++ b/server-client.c @@ -282,6 +282,7 @@ struct client * server_client_create(int fd) { struct client *c; + u_int i; setblocking(fd, 0); @@ -315,6 +316,12 @@ server_client_create(int fd) evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c); + for (i = 0; i < INPUT_REQUEST_TYPES; i++) { + c->input_requests[i].c = c; + c->input_requests[i].type = i; + TAILQ_INIT(&c->input_requests[i].requests); + } + TAILQ_INSERT_TAIL(&clients, c, entry); log_debug("new client %p", c); return (c); @@ -463,6 +470,7 @@ server_client_lost(struct client *c) tty_term_free_list(c->term_caps, c->term_ncaps); status_free(c); + input_cancel_requests(c); free(c->title); free((void *)c->cwd); diff --git a/tmux.h b/tmux.h index e386185c..2ec3bf23 100644 --- a/tmux.h +++ b/tmux.h @@ -53,6 +53,8 @@ struct format_tree; struct hyperlinks_uri; struct hyperlinks; struct input_ctx; +struct input_request; +struct input_requests; struct job; struct menu_data; struct mode_tree_data; @@ -1092,6 +1094,26 @@ struct window_mode_entry { TAILQ_ENTRY(window_mode_entry) entry; }; +/* Type of request to client. */ +enum input_request_type { + INPUT_REQUEST_PALETTE +}; +#define INPUT_REQUEST_TYPES (1) + +/* Palette request reply data. */ +struct input_request_palette_data { + int idx; + int c; +}; + +/* Request sent to client on behalf of pane. */ +TAILQ_HEAD(input_requests, input_request); +struct input_request_list { + struct client *c; + enum input_request_type type; + struct input_requests requests; +}; + /* Offsets into pane buffer. */ struct window_pane_offset { size_t used; @@ -1920,6 +1942,8 @@ struct client { struct status_line status; enum client_theme theme; + struct input_request_list input_requests[INPUT_REQUEST_TYPES]; + #define CLIENT_TERMINAL 0x1 #define CLIENT_LOGIN 0x2 #define CLIENT_EXIT 0x4 @@ -2922,6 +2946,8 @@ void input_parse_screen(struct input_ctx *, struct screen *, void input_reply_clipboard(struct bufferevent *, const char *, size_t, const char *); void input_set_buffer_size(size_t); +void input_request_reply(struct client *, enum input_request_type, void *); +void input_cancel_requests(struct client *); /* input-key.c */ void input_key_build(void); diff --git a/tty-keys.c b/tty-keys.c index a367d022..77254591 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t, size_t *); static int tty_keys_extended_device_attributes(struct tty *, const char *, size_t, size_t *); +static int tty_keys_palette(struct tty *, const char *, size_t, size_t *); /* A key tree entry. */ struct tty_key { @@ -804,6 +805,17 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this a palette response? */ + switch (tty_keys_palette(tty, buf, len, &size)) { + case 0: /* yes */ + key = KEYC_UNKNOWN; + goto complete_key; + case -1: /* no, or not valid */ + break; + case 1: /* partial */ + goto partial_key; + } + /* Is this a mouse key press? */ switch (tty_keys_mouse(tty, buf, len, &size, &m)) { case 0: /* yes */ @@ -1699,3 +1711,67 @@ tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, return (0); } + +/* Handle OSC 4 palette colour responses. */ +static int +tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) +{ + struct client *c = tty->client; + u_int i, start; + char tmp[128], *endptr; + int idx; + struct input_request_palette_data pd; + + *size = 0; + + /* First three bytes are always \033]4. */ + if (buf[0] != '\033') + return (-1); + if (len == 1) + return (1); + if (buf[1] != ']') + return (-1); + if (len == 2) + return (1); + if (buf[2] != '4') + return (-1); + if (len == 3) + return (1); + if (buf[3] != ';') + return (-1); + if (len == 4) + return (1); + + /* Parse index. */ + idx = strtol(buf + 4, &endptr, 10); + if (endptr == buf + 4 || *endptr != ';') + return (-1); + if (idx < 0 || idx > 255) + return (-1); + + /* Copy the rest up to \033\ or \007. */ + start = (endptr - buf) + 1; + for (i = start; i < len && i - start < sizeof tmp; i++) { + if (buf[i - 1] == '\033' && buf[i] == '\\') + break; + if (buf[i] == '\007') + break; + tmp[i - start] = buf[i]; + } + if (i - start == sizeof tmp) + return (-1); + if (i > 0 && buf[i - 1] == '\033') + tmp[i - start - 1] = '\0'; + else + tmp[i - start] = '\0'; + *size = i + 1; + + /* Work out the colour. */ + pd.c = colour_parseX11(tmp); + if (pd.c == -1) + return (0); + pd.idx = idx; + input_request_reply(c, INPUT_REQUEST_PALETTE, &pd); + + return (0); +} diff --git a/tty.c b/tty.c index ac28cd60..bd0c6b37 100644 --- a/tty.c +++ b/tty.c @@ -414,10 +414,12 @@ tty_repeat_requests(struct tty *tty, int force) return; if (!force && n <= TTY_REQUEST_LIMIT) { - log_debug("%s: not repeating requests (%u seconds)", c->name, n); + log_debug("%s: not repeating requests (%u seconds)", c->name, + n); return; } - log_debug("%s: %srepeating requests (%u seconds)", c->name, force ? "(force) " : "" , n); + log_debug("%s: %srepeating requests (%u seconds)", c->name, + force ? "(force) " : "" , n); tty->last_requests = t; if (tty->term->flags & TERM_VT100LIKE) {