Merge branch 'master' into command_parser

This commit is contained in:
Nicholas Marriott
2026-08-19 20:57:59 +01:00
29 changed files with 758 additions and 142 deletions

View File

@@ -17,7 +17,7 @@ jobs:
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
if: github.repository == 'tmux/tmux' if: github.repository == 'tmux/tmux'
timeout-minutes: 45 timeout-minutes: 300
strategy: strategy:
fail-fast: false fail-fast: false

20
CHANGES
View File

@@ -1,7 +1,4 @@
CHANGES FROM 3.7b TO 3.8 CHANGES FROM 3.7c TO 3.8
* Build with jemalloc on macOS to avoid what appears to be a bug in the system
calloc(3) (issue 5385).
* Many improvements to floating panes: * Many improvements to floating panes:
@@ -121,8 +118,6 @@ CHANGES FROM 3.7b TO 3.8
* Copy mode no longer exits at the bottom while a selection is in progress * Copy mode no longer exits at the bottom while a selection is in progress
(issue 5349). (issue 5349).
* Fix scrollbar initial state so scrollbars appear on new windows (issue 5339).
* Add style attributes for dimming colours (dim=) and for hyperlinks (link= and * Add style attributes for dimming colours (dim=) and for hyperlinks (link= and
nolink) (issues 4842 and 4280 from Moritz Angermann). nolink) (issues 4842 and 4280 from Moritz Angermann).
@@ -169,7 +164,18 @@ CHANGES FROM 3.7b TO 3.8
* Do not crash looking for the next or previous session (issue 5344), or when * Do not crash looking for the next or previous session (issue 5344), or when
no client is available. no client is available.
* Check time periodically in loops rather than for every item (issue 5367). CHANGES FROM 3.7b TO 3.7c
* Build with jemalloc on macOS to avoid what appears to be a bug in calloc
(issue 5385).
* Fix scrollbar initial state so they appear on new windows (issue 5339).
* Check time periodically in loops rather than every one (issue 5367).
* Use message-style again as default for message-format.
* Unzoom before creating floating panes to avoid a crash.
CHANGES FROM 3.7a TO 3.7b CHANGES FROM 3.7a TO 3.7b

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ /* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -279,7 +279,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
proc_set_signals(client_proc, client_signal); proc_set_signals(client_proc, client_signal);
/* Save the flags. */ /* Save the flags. */
client_flags = flags; client_flags = flags|CLIENT_WRITE_ACK;
log_debug("flags are %#llx", (unsigned long long)client_flags); log_debug("flags are %#llx", (unsigned long long)client_flags);
/* Initialize the client socket and start the server. */ /* Initialize the client socket and start the server. */
@@ -715,6 +715,9 @@ client_dispatch_wait(struct imsg *imsg)
fprintf(stderr, "server version is too old for client\n"); fprintf(stderr, "server version is too old for client\n");
proc_exit(client_proc); proc_exit(client_proc);
break; break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
} }
} }
@@ -801,5 +804,8 @@ client_dispatch_attached(struct imsg *imsg)
system(data); system(data);
proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0); proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0);
break; break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
} }
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-break-pane.c,v 1.75 2026/07/23 09:38:27 nicm Exp $ */ /* $OpenBSD: cmd-break-pane.c,v 1.76 2026/08/17 07:04:45 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -174,13 +174,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
} }
window_set_fill_cells(w); window_set_fill_cells(w);
if (idx == -1)
idx = -1 - options_get_number(dst_s->options, "base-index");
wl = session_attach(dst_s, w, idx, &cause); /* can't fail */
layout_init(w, wp); layout_init(w, wp);
wp->flags |= PANE_CHANGED; wp->flags |= PANE_CHANGED;
colour_palette_from_option(&wp->palette, wp->options); colour_palette_from_option(&wp->palette, wp->options);
if (idx == -1)
idx = -1 - options_get_number(dst_s->options, "base-index");
wl = session_attach(dst_s, w, idx, &cause); /* can't fail */
window_remove_ref(w, __func__); window_remove_ref(w, __func__);
events_fire_window("window-created", w); events_fire_window("window-created", w);
window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ /* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */
/* /*
* Copyright (c) 2008 Tiago Cunha <me@tiagocunha.org> * Copyright (c) 2008 Tiago Cunha <me@tiagocunha.org>
@@ -50,6 +50,7 @@ const struct cmd_entry cmd_source_file_entry = {
struct cmd_source_file_data { struct cmd_source_file_data {
struct cmdq_item *item; struct cmdq_item *item;
struct client *client;
int flags; int flags;
struct cmdq_item *after; struct cmdq_item *after;
@@ -60,10 +61,24 @@ struct cmd_source_file_data {
u_int nfiles; u_int nfiles;
}; };
static enum cmd_retval static void
cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) cmd_source_file_free_data(struct cmd_source_file_data *cdata)
{ {
struct client *c = cmdq_get_client(item); u_int i;
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
if (cdata->client != NULL)
server_client_unref(cdata->client);
free(cdata);
}
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, void *data)
{
struct cmd_source_file_data *cdata = data;
struct client *c = cdata->client;
if (c == NULL) { if (c == NULL) {
cmd_source_file_depth--; cmd_source_file_depth--;
@@ -74,36 +89,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
} }
cfg_print_causes(item); cfg_print_causes(item);
cmd_source_file_free_data(cdata);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
static void static void
cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata) cmd_source_file_complete(struct cmd_source_file_data *cdata)
{ {
struct client *c = cdata->client;
struct cmdq_item *new_item; struct cmdq_item *new_item;
u_int i;
if (cfg_finished) { if (!cfg_finished) {
if (cdata->retval == CMD_RETURN_ERROR && cmd_source_file_free_data(cdata);
c != NULL && return;
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL);
cmdq_insert_after(cdata->after, new_item);
} }
for (i = 0; i < cdata->nfiles; i++) if (cdata->retval == CMD_RETURN_ERROR &&
free(cdata->files[i]); c != NULL &&
free(cdata->files); c->session == NULL)
free(cdata); c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata);
cmdq_insert_after(cdata->after, new_item);
} }
static void static void
cmd_source_file_done(struct client *c, const char *path, int error, cmd_source_file_done(__unused struct client *oc, const char *path,
int closed, struct evbuffer *buffer, void *data) int error, int closed, struct evbuffer *buffer, void *data)
{ {
struct cmd_source_file_data *cdata = data; struct cmd_source_file_data *cdata = data;
struct cmdq_item *item = cdata->item; struct cmdq_item *item = cdata->item;
struct client *c = cdata->client;
void *bdata = EVBUFFER_DATA(buffer); void *bdata = EVBUFFER_DATA(buffer);
size_t bsize = EVBUFFER_LENGTH(buffer); size_t bsize = EVBUFFER_LENGTH(buffer);
u_int n; u_int n;
@@ -127,7 +142,7 @@ cmd_source_file_done(struct client *c, const char *path, int error,
if (n < cdata->nfiles) if (n < cdata->nfiles)
file_read(c, cdata->files[n], cmd_source_file_done, cdata); file_read(c, cdata->files[n], cmd_source_file_done, cdata);
else { else {
cmd_source_file_complete(c, cdata); cmd_source_file_complete(cdata);
cmdq_continue(item); cmdq_continue(item);
} }
} }
@@ -187,6 +202,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
cdata = xcalloc(1, sizeof *cdata); cdata = xcalloc(1, sizeof *cdata);
cdata->item = item; cdata->item = item;
cdata->client = c;
if (c != NULL)
c->references++;
if (args_has(args, 'q')) if (args_has(args, 'q'))
cdata->flags |= CMD_PARSE_QUIET; cdata->flags |= CMD_PARSE_QUIET;
@@ -249,7 +267,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
file_read(c, cdata->files[0], cmd_source_file_done, cdata); file_read(c, cdata->files[0], cmd_source_file_done, cdata);
retval = CMD_RETURN_WAIT; retval = CMD_RETURN_WAIT;
} else } else
cmd_source_file_complete(c, cdata); cmd_source_file_complete(cdata);
free(cwd); free(cwd);
return (retval); return (retval);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */ /* $OpenBSD: cmd-split-window.c,v 1.149 2026/08/19 10:56:10 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane", .name = "new-pane",
.alias = "newp", .alias = "newp",
.args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, .args = { "bB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bCdefhIklMOPvWZ] [-B border-lines] " .usage = "[-bCdefhIkKLMOPvWZ] [-B border-lines] "
"[-c start-directory] [-e environment] " "[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] " "[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] " "[-s style] [-S active-border-style] "
@@ -202,6 +202,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
*/ */
goto fail; goto fail;
} }
if (args_has(args, 'K') && args_has(args, 'O'))
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (args_has(args, 'C') && args_has(args, 'O')) if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK; new_wp->flags |= PANE_CLOSEONCLICK;

View File

@@ -31,7 +31,7 @@ getpeereid(int s, uid_t *uid, gid_t *gid)
{ {
#ifdef HAVE_SO_PEERCRED #ifdef HAVE_SO_PEERCRED
struct ucred uc; struct ucred uc;
int len = sizeof uc; socklen_t len = sizeof uc;
if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1) if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1)
return (-1); return (-1);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */ /* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */
/* /*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -388,6 +388,25 @@ control_pause_pane(struct client *c, struct window_pane *wp)
} }
} }
/*
* Reset a pane after its buffer has been replaced: drop any output still
* queued from the old buffer and start again from the pane's own offset.
*/
void
control_reset_pane(struct client *c, struct window_pane *wp)
{
struct control_pane *cp;
if (c->control_state == NULL)
return;
cp = control_get_pane(c, wp);
if (cp == NULL)
return;
control_discard_pane(c, cp);
memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
}
/* Write an already-formatted line, queueing it behind %output if needed. */ /* Write an already-formatted line, queueing it behind %output if needed. */
static void static void
control_write_line(struct client *c, char *line) control_write_line(struct client *c, char *line)

86
file.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: file.c,v 1.21 2026/07/26 15:08:15 nicm Exp $ */ /* $OpenBSD: file.c,v 1.22 2026/08/17 07:56:56 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -502,7 +502,8 @@ file_push(struct client_file *cf)
} else if (cf->stream > 2) { } else if (cf->stream > 2) {
close.stream = cf->stream; close.stream = cf->stream;
proc_send(cf->peer, MSG_WRITE_CLOSE, -1, &close, sizeof close); proc_send(cf->peer, MSG_WRITE_CLOSE, -1, &close, sizeof close);
file_fire_done(cf); if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK))
file_fire_done(cf);
} }
free(msg); free(msg);
} }
@@ -527,14 +528,48 @@ file_write_left(struct client_files *files)
return (waiting != 0); return (waiting != 0);
} }
/* Finish writing a client file. */
static void
file_write_finished(struct client_file *cf)
{
struct msg_write_done msg;
if (cf->event != NULL) {
bufferevent_free(cf->event);
cf->event = NULL;
}
if (cf->fd != -1) {
if (close(cf->fd) != 0 && cf->error == 0)
cf->error = errno;
cf->fd = -1;
}
msg.stream = cf->stream;
msg.error = cf->error;
proc_send(cf->peer, MSG_WRITE_DONE, -1, &msg, sizeof msg);
if (cf->cb != NULL)
cf->cb(NULL, NULL, 0, -1, NULL, cf->data);
file_free(cf);
}
/* Client file write error callback. */ /* Client file write error callback. */
static void static void
file_write_error_callback(__unused struct bufferevent *bev, __unused short what, file_write_error_callback(__unused struct bufferevent *bev, short what,
void *arg) void *arg)
{ {
struct client_file *cf = arg; struct client_file *cf = arg;
int error;
if (what & EVBUFFER_ERROR)
error = errno;
else
error = EIO;
if (error == 0)
error = EIO;
log_debug("write error file %d", cf->stream); log_debug("write error file %d", cf->stream);
cf->error = error;
bufferevent_free(cf->event); bufferevent_free(cf->event);
cf->event = NULL; cf->event = NULL;
@@ -542,7 +577,9 @@ file_write_error_callback(__unused struct bufferevent *bev, __unused short what,
close(cf->fd); close(cf->fd);
cf->fd = -1; cf->fd = -1;
if (cf->cb != NULL) if (cf->closed)
file_write_finished(cf);
else if (cf->cb != NULL)
cf->cb(NULL, NULL, 0, -1, NULL, cf->data); cf->cb(NULL, NULL, 0, -1, NULL, cf->data);
} }
@@ -554,15 +591,10 @@ file_write_callback(__unused struct bufferevent *bev, void *arg)
log_debug("write check file %d", cf->stream); log_debug("write check file %d", cf->stream);
if (cf->cb != NULL) if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0)
file_write_finished(cf);
else if (cf->cb != NULL)
cf->cb(NULL, NULL, 0, -1, NULL, cf->data); cf->cb(NULL, NULL, 0, -1, NULL, cf->data);
if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) {
bufferevent_free(cf->event);
close(cf->fd);
RB_REMOVE(client_files, cf->tree, cf);
file_free(cf);
}
} }
/* Handle a file write open message (client). */ /* Handle a file write open message (client). */
@@ -663,14 +695,10 @@ file_write_close(struct client_files *files, struct imsg *imsg)
if ((cf = RB_FIND(client_files, files, &find)) == NULL) if ((cf = RB_FIND(client_files, files, &find)) == NULL)
fatalx("unknown stream number"); fatalx("unknown stream number");
log_debug("close file %d", cf->stream); log_debug("close file %d", cf->stream);
cf->closed = 1;
if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) { if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) {
if (cf->event != NULL) file_write_finished(cf);
bufferevent_free(cf->event);
if (cf->fd != -1)
close(cf->fd);
RB_REMOVE(client_files, files, cf);
file_free(cf);
} }
} }
@@ -829,6 +857,28 @@ file_write_ready(struct client_files *files, struct imsg *imsg)
return (0); return (0);
} }
/* Handle a write done message (server). */
int
file_write_done(struct client_files *files, struct imsg *imsg)
{
struct msg_write_done *msg = imsg->data;
size_t msglen = imsg->hdr.len - IMSG_HEADER_SIZE;
struct client_file find, *cf;
if (msglen != sizeof *msg)
return (-1);
find.stream = msg->stream;
if ((cf = RB_FIND(client_files, files, &find)) == NULL)
return (0);
if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK))
return (0);
log_debug("file %d write done", cf->stream);
cf->error = msg->error;
file_fire_done(cf);
return (0);
}
/* Handle read data message (server). */ /* Handle read data message (server). */
int int
file_read_data(struct client_files *files, struct imsg *imsg) file_read_data(struct client_files *files, struct imsg *imsg)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 nicm Exp $ */ /* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1978,7 +1978,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx)
screen_write_mode_clear(sctx, MODE_BRACKETPASTE); screen_write_mode_clear(sctx, MODE_BRACKETPASTE);
break; break;
case 2026: case 2026:
screen_write_stop_sync(ictx->wp); screen_write_end_sync(sctx);
break; break;
case 2031: case 2031:
screen_write_mode_clear(sctx, MODE_THEME_UPDATES); screen_write_mode_clear(sctx, MODE_THEME_UPDATES);

6
menu.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: menu.c,v 1.69 2026/07/14 19:07:03 nicm Exp $ */ /* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -338,6 +338,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
struct key_event saved_event; struct key_event saved_event;
struct cmdq_state *state; struct cmdq_state *state;
char *error; char *error;
key_code key;
if (KEYC_IS_MOUSE(event->key)) { if (KEYC_IS_MOUSE(event->key)) {
/* /*
@@ -388,7 +389,8 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
name = menu->items[i].name; name = menu->items[i].name;
if (name == NULL || *name == '-') if (name == NULL || *name == '-')
continue; continue;
if ((event->key & ~KEYC_MASK_FLAGS) == menu->items[i].key) { key = (event->key & ~KEYC_MASK_FLAGS);
if (key == (menu->items[i].key & ~KEYC_MASK_FLAGS)) {
md->choice = i; md->choice = i;
goto chosen; goto chosen;
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */ /* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1334,7 +1334,7 @@ process_key:
if (pr->index != size) { if (pr->index != size) {
memmove(pr->buffer + pr->index, memmove(pr->buffer + pr->index,
pr->buffer + pr->index + 1, pr->buffer + pr->index + 1,
(size + 1 - pr->index) * (size - pr->index) *
sizeof *pr->buffer); sizeof *pr->buffer);
goto changed; goto changed;
} }

View File

@@ -105,6 +105,24 @@ drag()
sleep 1 sleep 1
} }
meta_drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup cleanup
check_ok new-session -d -s modal -x 80 -y 24 'cat' check_ok new-session -d -s modal -x 80 -y 24 'cat'
@@ -304,5 +322,35 @@ sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" '' must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
must_equal "$(fmt modal:0 '#{pane_id}')" "$p0" must_equal "$(fmt modal:0 '#{pane_id}')" "$p0"
ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') ||
fail "new-pane -K without -O failed"
check_ok kill-pane -t "$ignored"
$TMUX set -g @modal-prefix no
$TMUX set -g @modal-root no
$TMUX bind -n z set -g @modal-root yes
modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -OK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
must_equal "$($TMUX show -gv @modal-prefix)" no
must_equal "$($TMUX show -gv @modal-root)" no
case "$($TMUX capture-pane -pt "$modal")" in
*xz*) ;;
*) fail "keys did not reach key-capturing modal pane" ;;
esac
left=$(fmt "$modal" '#{pane_left}')
top=$(fmt "$modal" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$modal" '#{pane_left}')
new_top=$(fmt "$modal" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "key-capturing modal pane did not move"
check_ok kill-pane -t "$modal"
sleep 1
cleanup cleanup
exit 0 exit 0

View File

@@ -269,6 +269,22 @@ test_customize_mode()
assert_alive "customize-mode exit" assert_alive "customize-mode exit"
} }
test_customize_break_pane()
{
start_client option-break
side=$($TMUX split-window -d -P -F '#{pane_id}' \
-t option-break:0 'cat') || fail "customize split failed"
$TMUX customize-mode -t "$side" || fail "customize-mode failed"
wait_mode "$side" 1
$TMUX break-pane -d -s "$side" || fail "customize break-pane failed"
assert_alive "customize-mode break-pane"
wait_mode "$side" 1
$TMUX send-keys -t "$side" q || fail "customize-mode quit failed"
wait_mode "$side" 0
}
test_copy_mode() test_copy_mode()
{ {
start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat'
@@ -302,6 +318,7 @@ test_choose_tree
test_choose_buffer test_choose_buffer
test_choose_client test_choose_client
test_customize_mode test_customize_mode
test_customize_break_pane
test_copy_mode test_copy_mode
cleanup cleanup
exit 0 exit 0

View File

@@ -0,0 +1,87 @@
#!/bin/sh
# respawn-pane frees the pane's buffer and creates an empty one. The pane
# offsets and each control client's offsets used to keep pointing into the old
# buffer, so if an attached control client had not caught up on the pane's
# output, the next read from the new process ran off the end of the new buffer
# and the server crashed in input_parse.
#
# Two ways for a client to be behind are tested: a control client whose output
# goes down a fifo that is never read (a second, healthy control client keeps
# the pane being read), and a healthy control client viewing a session that
# the pane's window has been moved out of.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)
FIFO=$DIR/fifo
SERVER=
mkfifo "$FIFO" || exit 1
cleanup() {
[ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null
$TMUX kill-server 2>/dev/null
exec 8<&- 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
alive() {
if ! kill -0 "$SERVER" 2>/dev/null; then
SERVER=
echo "server died after $1"
exit 1
fi
$TMUX has -t rt || exit 1
}
wait_clients() {
n=0
while [ $n -lt 50 ]; do
[ "$($TMUX lsc 2>/dev/null | wc -l)" -ge $1 ] && return
sleep 0.1
n=$((n + 1))
done
echo "control clients did not attach"; exit 1
}
# A detached session with a pane that writes a line every 10 milliseconds.
$TMUX -f/dev/null new -d -x 80 -y 24 -s rt \
'while :; do echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; sleep 0.01; done' ||
exit 1
SERVER=$($TMUX display -pt rt '#{pid}')
# One control client with output down the unread fifo and one reading
# normally; stdin of both held open by sleep so they stay attached.
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 &
exec 8<"$FIFO"
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >/dev/null 2>&1 &
wait_clients 2
# Let the first client fall behind, then respawn the pane. The new process
# writes at once and the server must survive reading it.
sleep 3
$TMUX respawn-pane -k -t rt:0 'echo respawned; sleep 60' || exit 1
sleep 1
alive "respawn-pane with a lagging control client"
# Now a pane whose window is moved out of the session both clients view: their
# offsets for it stop advancing, so they fall behind until it is respawned.
$TMUX neww -d -t rt \
'while :; do echo yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy; sleep 0.01; done' ||
exit 1
sleep 1
$TMUX new -d -s other || exit 1
$TMUX movew -d -s rt:1 -t other: || exit 1
sleep 2
$TMUX respawn-pane -k -t other:1 'echo respawned; sleep 60' || exit 1
sleep 1
alive "respawn-pane on a window moved out of the clients' session"
exit 0

View File

@@ -0,0 +1,173 @@
#!/bin/sh
# Application synchronized updates must remain one physical client transaction,
# including when BSU, printable cells and ESU arrive in one pane read.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
CLIENT_BYTES=$DIR/client-bytes
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
ASSERT=$DIR/assert-sync-output.pl
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
$INNER list-clients -F '#{client_termfeatures}' 2>/dev/null |
grep -q 'sync' && return 0
sleep 0.1
i=$((i + 1))
done
fail "sync-capable client did not attach"
}
wait_for_marker()
{
i=0
while [ "$i" -lt 50 ]; do
grep -q 'FRAME_000001_ROW_23' "$CLIENT_BYTES" 2>/dev/null && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive both synchronized frames"
}
wait_for_stable_bytes()
{
previous=-1
stable=0
i=0
while [ "$i" -lt 50 ]; do
current=$(wc -c <"$CLIENT_BYTES" 2>/dev/null) || current=0
if [ "$current" -gt 0 ] && [ "$current" -eq "$previous" ]; then
stable=$((stable + 1))
[ "$stable" -eq 5 ] && return 0
else
stable=0
fi
previous=$current
sleep 0.1
i=$((i + 1))
done
fail "client byte stream did not become stable"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
my $control = $ENV{CONTROL};
(my $dir = $control) =~ s{/[^/]+$}{};
open my $ready, '>', "$dir/ready" or die "$dir/ready: $!\n";
close $ready;
while (!-e $control) {
select undef, undef, undef, 0.01;
}
for my $frame (0 .. 1) {
my @rows;
for my $row (0 .. 23) {
my $marker = sprintf "FRAME_%06d_ROW_%02d_", $frame, $row;
push @rows, substr($marker . ('X' x 79), 0, 79);
}
my $frame = "\e[?2026h\e[H" . join("\r\n", @rows) .
"\e[?2026l";
my $written = syswrite STDOUT, $frame;
die "short synchronized frame write\n"
unless defined $written && $written == length $frame;
select undef, undef, undef, 0.2;
}
select undef, undef, undef, 2;
PERL
$INNER new-session -d -s inner -x 80 -y 24 \
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
$OUTER new-session -d -s outer -x 80 -y 24 \
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
wait_for_client
i=0
while [ "$i" -lt 50 ] && [ ! -e "$DIR/ready" ]; do
sleep 0.1
i=$((i + 1))
done
[ -e "$DIR/ready" ] || fail "application emitter did not become ready"
$OUTER pipe-pane -O -t outer:0.0 "cat >'$CLIENT_BYTES'" || exit 1
: >"$CONTROL"
wait_for_marker
wait_for_stable_bytes
$OUTER pipe-pane -t outer:0.0 || exit 1
cat >"$ASSERT" <<'PERL'
use strict;
use warnings;
my $path = shift;
open my $fh, '<:raw', $path or die "$path: $!\n";
local $/;
my $bytes = <$fh>;
my ($on, $off) = ("\e[?2026h", "\e[?2026l");
my ($active, %seen);
for (my $i = 0; $i < length($bytes);) {
if (substr($bytes, $i, length($on)) eq $on) {
die "duplicate synchronized-output start at byte $i\n" if $active;
$active = 1;
$i += length($on);
next;
}
if (substr($bytes, $i, length($off)) eq $off) {
die "unmatched synchronized-output end at byte $i\n" unless $active;
$active = 0;
$i += length($off);
next;
}
if (substr($bytes, $i) =~ /\AFRAME_(\d{6})_ROW_(\d{2})_/) {
my $marker = "$1:$2";
die "marker $marker outside synchronized output\n" unless $active;
$seen{$marker}++;
}
$i++;
}
die "unterminated synchronized-output transaction\n" if $active;
for my $frame (0 .. 1) {
for my $row (0 .. 23) {
my $marker = sprintf "%06d:%02d", $frame, $row;
die "missing marker $marker\n" unless $seen{$marker};
}
}
PERL
perl "$ASSERT" "$CLIENT_BYTES" || exit 1

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */ /* $OpenBSD: screen-write.c,v 1.287 2026/08/17 20:04:00 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1076,6 +1076,19 @@ screen_write_stop_sync(struct window_pane *wp)
log_debug("%s: %%%u stopped sync mode", __func__, wp->id); log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
} }
/* Flush pending output before clearing sync mode. */
void
screen_write_end_sync(struct screen_write_ctx *ctx)
{
struct window_pane *wp = ctx->wp;
if (wp == NULL)
return;
if (wp->base.mode & MODE_SYNC)
screen_write_collect_flush(ctx, 0, __func__);
screen_write_stop_sync(wp);
}
/* Cursor up by ny. */ /* Cursor up by ny. */
void void
screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny) screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.502 2026/08/04 11:18:22 nicm Exp $ */ /* $OpenBSD: server-client.c,v 1.504 2026/08/19 10:56:10 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -45,6 +45,7 @@ static void server_client_set_path(struct client *);
static void server_client_set_progress_bar(struct client *); static void server_client_set_progress_bar(struct client *);
static void server_client_reset_state(struct client *); static void server_client_reset_state(struct client *);
static void server_client_update_latest(struct client *); static void server_client_update_latest(struct client *);
static int server_client_handle_dead_key(struct window_pane *, key_code);
static void server_client_dispatch(struct imsg *, void *); static void server_client_dispatch(struct imsg *, void *);
static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_command(struct client *, struct imsg *);
static int server_client_dispatch_identify(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *);
@@ -1388,6 +1389,21 @@ server_client_repeat_time(struct client *c, struct key_binding *bd)
return (repeat); return (repeat);
} }
/* Handle a key press on a dead pane waiting for a key. */
static int
server_client_handle_dead_key(struct window_pane *wp, key_code key)
{
if (wp == NULL ||
(~wp->flags & PANE_EXITED) ||
KEYC_IS_MOUSE(key) ||
KEYC_IS_PASTE(key) ||
options_get_number(wp->options, "remain-on-exit") != 3)
return (0);
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
return (1);
}
/* /*
* Handle data key input from client. This owns and can modify the key event it * Handle data key input from client. This owns and can modify the key event it
* is given and is responsible for freeing it. * is given and is responsible for freeing it.
@@ -1473,6 +1489,14 @@ server_client_key_callback(struct cmdq_item *item, void *data)
server_client_is_assume_paste(c)) server_client_is_assume_paste(c))
goto paste_key; goto paste_key;
/* Forward keys directly if this pane is capturing all keys. */
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
(~wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
TAILQ_EMPTY(&wp->modes))
goto forward_key;
/* /*
* Work out the current key table. If the pane is in a mode, use * Work out the current key table. If the pane is in a mode, use
* the mode table instead of the default key table. * the mode table instead of the default key table.
@@ -1637,15 +1661,8 @@ try_again:
} }
forward_key: forward_key:
if (wp != NULL && if (server_client_handle_dead_key(wp, key))
(wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
!KEYC_IS_PASTE(key) &&
options_get_number(wp->options, "remain-on-exit") == 3) {
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
goto out; goto out;
}
if (c->flags & CLIENT_READONLY) if (c->flags & CLIENT_READONLY)
goto out; goto out;
if (wp != NULL) if (wp != NULL)
@@ -1733,9 +1750,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
} }
/* /*
* Key presses in overlay mode and the command prompt are a special * Key presses in overlay mode, for panes capturing all keys and in the
* case. The queue might be blocked so they need to be processed * command prompt are a special case. The queue might be blocked so they
* immediately rather than queued. * need to be processed immediately rather than queued.
*/ */
if (~c->flags & CLIENT_READONLY) { if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) { if (c->message_string != NULL) {
@@ -1755,6 +1772,21 @@ server_client_handle_key0(struct client *c, struct key_event *event,
} }
server_client_clear_overlay(c); server_client_clear_overlay(c);
wp = s->curw->window->active;
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
TAILQ_EMPTY(&wp->modes) &&
!KEYC_IS_MOUSE(event->key)) {
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (~wp->flags & PANE_EXITED) {
window_pane_key(wp, c, s, s->curw, event->key,
&event->m);
return (0);
}
}
if (server_client_handle_menu_key(c, event)) if (server_client_handle_menu_key(c, event))
return (0); return (0);
if (c->prompt != NULL) { if (c->prompt != NULL) {
@@ -2695,6 +2727,10 @@ server_client_dispatch(struct imsg *imsg, void *arg)
if (file_write_ready(&c->files, imsg) != 0) if (file_write_ready(&c->files, imsg) != 0)
goto bad; goto bad;
break; break;
case MSG_WRITE_DONE:
if (file_write_done(&c->files, imsg) != 0)
goto bad;
break;
case MSG_READ: case MSG_READ:
if (file_read_data(&c->files, imsg) != 0) if (file_read_data(&c->files, imsg) != 0)
goto bad; goto bad;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */ /* $OpenBSD: server-fn.c,v 1.150 2026/08/18 07:32:09 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -551,6 +551,7 @@ server_check_unattached(void)
continue; continue;
break; break;
} }
server_destroy_session(s);
session_destroy(s, 1, __func__); session_destroy(s, 1, __func__);
} }
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */ /* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -129,6 +129,7 @@ server_create_socket(uint64_t flags, char **cause)
mask = umask(S_IXUSR|S_IRWXG|S_IRWXO); mask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) { if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) {
saved_errno = errno; saved_errno = errno;
umask(mask);
close(fd); close(fd);
errno = saved_errno; errno = saved_errno;
goto fail; goto fail;

18
spawn.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */ /* $OpenBSD: spawn.c,v 1.51 2026/08/18 07:43:44 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -243,7 +243,7 @@ struct window_pane *
spawn_pane(struct spawn_context *sc, char **cause) spawn_pane(struct spawn_context *sc, char **cause)
{ {
struct cmdq_item *item = sc->item; struct cmdq_item *item = sc->item;
struct client *c; struct client *c, *loop;
struct session *s = sc->s; struct session *s = sc->s;
struct session *ts; struct session *ts;
struct window *w = sc->wl->window; struct window *w = sc->wl->window;
@@ -331,6 +331,20 @@ spawn_pane(struct spawn_context *sc, char **cause)
input_free(sc->wp0->ictx); input_free(sc->wp0->ictx);
sc->wp0->ictx = NULL; sc->wp0->ictx = NULL;
} }
/*
* The old buffer is gone and the new one starts empty, so
* offsets into the old buffer no longer mean anything. Reset
* them, and drop output control clients still had queued.
*/
sc->wp0->offset.used = 0;
sc->wp0->base_offset = 0;
sc->wp0->pipe_offset.used = 0;
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->flags & CLIENT_CONTROL)
control_reset_pane(loop, sc->wp0);
}
new_wp = sc->wp0; new_wp = sc->wp0;
new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN); new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN);
} else { } else {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ /* $OpenBSD: tmux-protocol.h,v 1.3 2026/08/17 07:56:56 nicm Exp $ */
/* /*
* Copyright (c) 2021 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2021 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -67,7 +67,8 @@ enum msgtype {
MSG_WRITE, MSG_WRITE,
MSG_WRITE_READY, MSG_WRITE_READY,
MSG_WRITE_CLOSE, MSG_WRITE_CLOSE,
MSG_READ_CANCEL MSG_READ_CANCEL,
MSG_WRITE_DONE
}; };
/* /*
@@ -116,4 +117,9 @@ struct msg_write_close {
int stream; int stream;
}; };
struct msg_write_done {
int stream;
int error;
};
#endif /* TMUX_PROTOCOL_H */ #endif /* TMUX_PROTOCOL_H */

13
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 nicm Exp $ .\" $OpenBSD: tmux.1,v 1.1157 2026/08/19 10:56:10 nicm Exp $
.\" .\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> .\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\" .\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\" .\"
.Dd $Mdocdate: August 6 2026 $ .Dd $Mdocdate: August 19 2026 $
.Dt TMUX 1 .Dt TMUX 1
.Os .Os
.Sh NAME .Sh NAME
@@ -3666,7 +3666,7 @@ but a different format may be specified with
.Fl F . .Fl F .
.Tg newp .Tg newp
.It Xo Ic new\-pane .It Xo Ic new\-pane
.Op Fl bCdefhIkLMOPvWZ .Op Fl bCdefhIkKLMOPvWZ
.Op Fl B Ar border\-lines .Op Fl B Ar border\-lines
.Op Fl c Ar start\-directory .Op Fl c Ar start\-directory
.Op Fl e Ar environment .Op Fl e Ar environment
@@ -3723,6 +3723,9 @@ A modal pane is always the active pane and prevents interaction with any other
panes while it is active. panes while it is active.
A window can only have one modal pane and it must be a floating pane. A window can only have one modal pane and it must be a floating pane.
With With
.Fl K ,
all keys, including the prefix key, are passed directly to the modal pane.
With
.Fl C , .Fl C ,
the modal pane is closed when the mouse is clicked outside it. the modal pane is closed when the mouse is clicked outside it.
.Pp .Pp
@@ -5076,6 +5079,10 @@ This is an array option where each entry is a colon-separated string made up
of a terminal type pattern (matched using of a terminal type pattern (matched using
.Xr glob 7 .Xr glob 7
patterns) followed by a list of terminal features. patterns) followed by a list of terminal features.
A feature may be suffixed with
.Ql @
to disable it; for example,
.Ql xterm*:sync@ .
The available features are: The available features are:
.Bl -tag -width Ds .Bl -tag -width Ds
.It 256 .It 256

6
tmux.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.c,v 1.222 2026/07/19 19:09:30 nicm Exp $ */ /* $OpenBSD: tmux.c,v 1.223 2026/08/17 14:47:41 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -425,7 +425,7 @@ main(int argc, char **argv)
while ((opt = getopt(argc, argv, "2c:CDdf:hlL:NqS:T:uUvV")) != -1) { while ((opt = getopt(argc, argv, "2c:CDdf:hlL:NqS:T:uUvV")) != -1) {
switch (opt) { switch (opt) {
case '2': case '2':
tty_add_features(&feat, "256", ":,"); tty_parse_features("256", ":,", &feat, NULL);
break; break;
case 'c': case 'c':
shell_command = optarg; shell_command = optarg;
@@ -473,7 +473,7 @@ main(int argc, char **argv)
path = xstrdup(optarg); path = xstrdup(optarg);
break; break;
case 'T': case 'T':
tty_add_features(&feat, optarg, ":,"); tty_parse_features(optarg, ":,", &feat, NULL);
break; break;
case 'u': case 'u':
flags |= CLIENT_UTF8; flags |= CLIENT_UTF8;

25
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */ /* $OpenBSD: tmux.h,v 1.1428 2026/08/19 10:56:10 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1338,6 +1338,7 @@ struct window_pane {
#define PANE_CMDRUNNING 0x20000 #define PANE_CMDRUNNING 0x20000
#define PANE_ACTIVITY 0x40000 #define PANE_ACTIVITY 0x40000
#define PANE_CLOSEONCLICK 0x80000 #define PANE_CLOSEONCLICK 0x80000
#define PANE_CAPTUREALLKEYS 0x100000
bitstr_t *sync_dirty; bitstr_t *sync_dirty;
u_int sync_dirty_size; u_int sync_dirty_size;
@@ -1721,7 +1722,7 @@ struct key_event {
struct tty_term { struct tty_term {
char *name; char *name;
struct tty *tty; struct tty *tty;
int features; int applied_features;
char acs[UCHAR_MAX + 1][2]; char acs[UCHAR_MAX + 1][2];
@@ -1814,8 +1815,9 @@ struct tty {
#define TTY_WAITFG 0x2000 #define TTY_WAITFG 0x2000
#define TTY_WAITBG 0x4000 #define TTY_WAITBG 0x4000
#define TTY_BRACKETPASTE 0x8000 #define TTY_BRACKETPASTE 0x8000
#define TTY_HAVESYNC 0x10000
#define TTY_ALL_REQUEST_FLAGS \ #define TTY_ALL_REQUEST_FLAGS \
(TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA) (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC)
int flags; int flags;
struct tty_term *term; struct tty_term *term;
@@ -2202,6 +2204,7 @@ struct client {
char *term_name; char *term_name;
int term_features; int term_features;
int term_nofeatures;
char *term_type; char *term_type;
char **term_caps; char **term_caps;
u_int term_ncaps; u_int term_ncaps;
@@ -2269,7 +2272,7 @@ struct client {
/* 0x800000000ULL unused */ /* 0x800000000ULL unused */
#define CLIENT_BRACKETPASTING 0x1000000000ULL #define CLIENT_BRACKETPASTING 0x1000000000ULL
#define CLIENT_ASSUMEPASTING 0x2000000000ULL #define CLIENT_ASSUMEPASTING 0x2000000000ULL
/* 0x4000000000ULL unused */ #define CLIENT_WRITE_ACK 0x4000000000ULL
#define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL #define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL
#define CLIENT_ALLREDRAWFLAGS \ #define CLIENT_ALLREDRAWFLAGS \
(CLIENT_REDRAWWINDOW| \ (CLIENT_REDRAWWINDOW| \
@@ -2975,8 +2978,7 @@ extern struct tty_terms tty_terms;
u_int tty_term_ncodes(void); u_int tty_term_ncodes(void);
void tty_term_apply(struct tty_term *, const char *, int); void tty_term_apply(struct tty_term *, const char *, int);
void tty_term_apply_overrides(struct tty_term *); void tty_term_apply_overrides(struct tty_term *);
struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, int *, struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, char **);
char **);
void tty_term_free(struct tty_term *); void tty_term_free(struct tty_term *);
int tty_term_read_list(const char *, int, char ***, u_int *, int tty_term_read_list(const char *, int, char ***, u_int *,
char **); char **);
@@ -2998,11 +3000,13 @@ int tty_term_flag(struct tty_term *, enum tty_code_code);
const char *tty_term_describe(struct tty_term *, enum tty_code_code); const char *tty_term_describe(struct tty_term *, enum tty_code_code);
/* tty-features.c */ /* tty-features.c */
void tty_add_features(int *, const char *, const char *); void tty_parse_client_features(struct client *, const char *,
const char *);
void tty_parse_features(const char *, const char *, int *, int *);
const char *tty_get_features(int); const char *tty_get_features(int);
int tty_feature_present(struct tty_term *, const char *); int tty_feature_present(struct tty_term *, const char *);
int tty_apply_features(struct tty_term *, int); int tty_apply_features(struct tty_term *);
void tty_default_features(int *, const char *, u_int); void tty_default_features(struct client *, const char *, u_int);
/* tty-acs.c */ /* tty-acs.c */
int tty_acs_needed(struct tty *); int tty_acs_needed(struct tty *);
@@ -3231,6 +3235,7 @@ void file_write_close(struct client_files *, struct imsg *);
void file_read_open(struct client_files *, struct tmuxpeer *, struct imsg *, void file_read_open(struct client_files *, struct tmuxpeer *, struct imsg *,
int, int, client_file_cb, void *); int, int, client_file_cb, void *);
int file_write_ready(struct client_files *, struct imsg *); int file_write_ready(struct client_files *, struct imsg *);
int file_write_done(struct client_files *, struct imsg *);
int file_read_data(struct client_files *, struct imsg *); int file_read_data(struct client_files *, struct imsg *);
int file_read_done(struct client_files *, struct imsg *); int file_read_done(struct client_files *, struct imsg *);
void file_read_cancel(struct client_files *, struct imsg *); void file_read_cancel(struct client_files *, struct imsg *);
@@ -3548,6 +3553,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int);
void screen_write_mode_clear(struct screen_write_ctx *, int); void screen_write_mode_clear(struct screen_write_ctx *, int);
void screen_write_start_sync(struct window_pane *); void screen_write_start_sync(struct window_pane *);
void screen_write_stop_sync(struct window_pane *); void screen_write_stop_sync(struct window_pane *);
void screen_write_end_sync(struct screen_write_ctx *);
void screen_write_clear_dirty(struct window_pane *); void screen_write_clear_dirty(struct window_pane *);
void screen_write_cursorup(struct screen_write_ctx *, u_int); void screen_write_cursorup(struct screen_write_ctx *, u_int);
void screen_write_cursordown(struct screen_write_ctx *, u_int); void screen_write_cursordown(struct screen_write_ctx *, u_int);
@@ -3983,6 +3989,7 @@ void control_set_pane_on(struct client *, struct window_pane *);
void control_set_pane_off(struct client *, struct window_pane *); void control_set_pane_off(struct client *, struct window_pane *);
void control_continue_pane(struct client *, struct window_pane *); void control_continue_pane(struct client *, struct window_pane *);
void control_pause_pane(struct client *, struct window_pane *); void control_pause_pane(struct client *, struct window_pane *);
void control_reset_pane(struct client *, struct window_pane *);
void control_set_window_size(struct client *, u_int, u_int, u_int); void control_set_window_size(struct client *, u_int, u_int, u_int);
int control_get_window_size(struct client *, u_int, u_int *, u_int *); int control_get_window_size(struct client *, u_int, u_int *, u_int *);
void control_clear_window_size(struct client *, u_int); void control_clear_window_size(struct client *, u_int);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ /* $OpenBSD: tty-features.c,v 1.42 2026/08/17 14:47:41 nicm Exp $ */
/* /*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -393,17 +393,29 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_usstyle &tty_feature_usstyle
}; };
/* Parse features for client. */
void void
tty_add_features(int *feat, const char *s, const char *separators) tty_parse_client_features(struct client *c, const char *s, const char *sep)
{
tty_parse_features(s, sep, &c->term_features, &c->term_nofeatures);
}
/* Parse features list. */
void
tty_parse_features(const char *s, const char *sep, int *enabled, int *disabled)
{ {
const struct tty_feature *tf; const struct tty_feature *tf;
char *next, *loop, *copy; char *next, *loop, *copy;
u_int i; u_int i;
int remove;
log_debug("adding terminal features %s", s); log_debug("adding terminal features %s", s);
loop = copy = xstrdup(s); loop = copy = xstrdup(s);
while ((next = strsep(&loop, separators)) != NULL) { while ((next = strsep(&loop, sep)) != NULL) {
remove = (*next != '\0' && next[strlen(next) - 1] == '@');
if (remove)
next[strlen(next) - 1] = '\0';
for (i = 0; i < nitems(tty_features); i++) { for (i = 0; i < nitems(tty_features); i++) {
tf = tty_features[i]; tf = tty_features[i];
if (strcasecmp(tf->name, next) == 0) if (strcasecmp(tf->name, next) == 0)
@@ -413,14 +425,24 @@ tty_add_features(int *feat, const char *s, const char *separators)
log_debug("unknown terminal feature: %s", next); log_debug("unknown terminal feature: %s", next);
break; break;
} }
if (~(*feat) & (1 << i)) { if (remove) {
log_debug("removing terminal feature: %s", tf->name);
*enabled &= ~(1 << i);
if (disabled != NULL)
*disabled |= 1 << i;
continue;
}
if (disabled != NULL && *disabled & (1 << i))
continue;
if (~(*enabled) & (1 << i)) {
log_debug("adding terminal feature: %s", tf->name); log_debug("adding terminal feature: %s", tf->name);
(*feat) |= (1 << i); (*enabled) |= (1 << i);
} }
} }
free(copy); free(copy);
} }
/* Get features as string. */
const char * const char *
tty_get_features(int feat) tty_get_features(int feat)
{ {
@@ -442,6 +464,7 @@ tty_get_features(int feat)
return (s); return (s);
} }
/* Check if feature is present. */
int int
tty_feature_present(struct tty_term *term, const char *name) tty_feature_present(struct tty_term *term, const char *name)
{ {
@@ -453,8 +476,8 @@ tty_feature_present(struct tty_term *term, const char *name)
for (i = 0; i < nitems(tty_features); i++) { for (i = 0; i < nitems(tty_features); i++) {
tf = tty_features[i]; tf = tty_features[i];
if (strcmp(tf->name, name) == 0) { if (strcmp(tf->name, name) == 0) {
if (term->features & (1 << i)) if (term->applied_features & (1 << i))
return (1); return (1);
break; break;
} }
} }
@@ -481,19 +504,23 @@ tty_feature_present(struct tty_term *term, const char *name)
return (1); return (1);
} }
/* Apply featurs to terminal. */
int int
tty_apply_features(struct tty_term *term, int feat) tty_apply_features(struct tty_term *term)
{ {
struct client *c = term->tty->client;
const struct tty_feature *tf; const struct tty_feature *tf;
const char *const *capability; const char *const *capability;
int feat;
u_int i; u_int i;
feat = (c->term_features & ~c->term_nofeatures);
if (feat == 0) if (feat == 0)
return (0); return (0);
log_debug("applying terminal features: %s", tty_get_features(feat)); log_debug("applying terminal features: %s", tty_get_features(feat));
for (i = 0; i < nitems(tty_features); i++) { for (i = 0; i < nitems(tty_features); i++) {
if ((term->features & (1 << i)) || (~feat & (1 << i))) if ((term->applied_features & (1 << i)) || (~feat & (1 << i)))
continue; continue;
tf = tty_features[i]; tf = tty_features[i];
@@ -508,14 +535,15 @@ tty_apply_features(struct tty_term *term, int feat)
} }
term->flags |= tf->flags; term->flags |= tf->flags;
} }
if ((term->features | feat) == term->features) if ((term->applied_features|feat) == term->applied_features)
return (0); return (0);
term->features |= feat; term->applied_features |= feat;
return (1); return (1);
} }
/* Add default features for a terminal identified by name and version. */
void void
tty_default_features(int *feat, const char *name, u_int version) tty_default_features(struct client *c, const char *name, u_int version)
{ {
static const struct { static const struct {
const char *name; const char *name;
@@ -596,6 +624,18 @@ tty_default_features(int *feat, const char *name, u_int version)
"usstyle," "usstyle,"
"progressbar" "progressbar"
}, },
{ .name = "Rio",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
"ccolour,"
"cstyle,"
"focus,"
"overline,"
"hyperlinks,"
"osc7,"
"sync,"
"usstyle,"
"progressbar"
},
{ .name = "XTerm", { .name = "XTerm",
/* /*
* xterm also supports DECSLRM and DECFRA, but they can be * xterm also supports DECSLRM and DECFRA, but they can be
@@ -616,6 +656,6 @@ tty_default_features(int *feat, const char *name, u_int version)
continue; continue;
if (version != 0 && version < table[i].version) if (version != 0 && version < table[i].version)
continue; continue;
tty_add_features(feat, table[i].features, ","); tty_parse_client_features(c, table[i].features, ",");
} }
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-keys.c,v 1.211 2026/07/21 07:12:49 nicm Exp $ */ /* $OpenBSD: tty-keys.c,v 1.214 2026/08/18 09:01:20 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t,
size_t *); size_t *);
static int tty_keys_extended_device_attributes(struct tty *, const char *, static int tty_keys_extended_device_attributes(struct tty *, const char *,
size_t, size_t *); size_t, size_t *);
static int tty_keys_sync(struct tty *, const char *, size_t, size_t *);
static int tty_keys_palette(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. */ /* A key tree entry. */
@@ -771,6 +772,17 @@ tty_keys_next(struct tty *tty)
goto partial_key; goto partial_key;
} }
/* Is this a synchronized update mode response? */
switch (tty_keys_sync(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 primary device attributes response? */ /* Is this a primary device attributes response? */
switch (tty_keys_device_attributes(tty, buf, len, &size)) { switch (tty_keys_device_attributes(tty, buf, len, &size)) {
case 0: /* yes */ case 0: /* yes */
@@ -1447,7 +1459,6 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len,
size_t *size) size_t *size)
{ {
struct client *c = tty->client; struct client *c = tty->client;
int *features = &c->term_features;
u_int i, n = 0; u_int i, n = 0;
char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next;
@@ -1504,13 +1515,13 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len,
for (i = 1; i < n; i++) { for (i = 1; i < n; i++) {
log_debug("%s: DA feature: %d", c->name, p[i]); log_debug("%s: DA feature: %d", c->name, p[i]);
if (p[i] == 4) if (p[i] == 4)
tty_add_features(features, "sixel", ","); tty_parse_client_features(c, "sixel", ",");
if (p[i] == 21) if (p[i] == 21)
tty_add_features(features, "margins", ","); tty_parse_client_features(c, "margins", ",");
if (p[i] == 28) if (p[i] == 28)
tty_add_features(features, "rectfill", ","); tty_parse_client_features(c, "rectfill", ",");
if (p[i] == 52) if (p[i] == 52)
tty_add_features(features, "clipboard", ","); tty_parse_client_features(c, "clipboard", ",");
} }
break; break;
} }
@@ -1522,6 +1533,54 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len,
return (0); return (0);
} }
/*
* Handle a synchronized update mode response. Returns 0 for success, -1 for
* failure, 1 for partial.
*/
static int
tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size)
{
struct client *c = tty->client;
static const char prefix[] = "\033[?2026;";
size_t i;
int status;
*size = 0;
if (tty->flags & TTY_HAVESYNC)
return (-1);
/* The response is always \033[?2026;Ps$y. */
for (i = 0; i < (sizeof prefix) - 1; i++) {
if (i == len)
return (1);
if (buf[i] != prefix[i])
return (-1);
}
if (i == len)
return (1);
if (buf[i] < '0' || buf[i] > '4')
return (-1);
status = buf[i++] - '0';
if (i == len)
return (1);
if (buf[i++] != '$')
return (-1);
if (i == len)
return (1);
if (buf[i++] != 'y')
return (-1);
*size = i;
if (status == 1 || status == 2 || status == 3) {
tty_parse_client_features(c, "sync", ",");
tty_update_features(tty);
}
log_debug("%s: received DECRPM %.*s", c->name, (int)*size, buf);
tty->flags |= TTY_HAVESYNC;
return (0);
}
/* /*
* Handle secondary device attributes input. Returns 0 for success, -1 for * Handle secondary device attributes input. Returns 0 for success, -1 for
* failure, 1 for partial. * failure, 1 for partial.
@@ -1531,7 +1590,6 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len,
size_t *size) size_t *size)
{ {
struct client *c = tty->client; struct client *c = tty->client;
int *features = &c->term_features;
u_int i, n = 0; u_int i, n = 0;
char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next;
@@ -1585,13 +1643,13 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len,
*/ */
switch (p[0]) { switch (p[0]) {
case 'M': /* mintty */ case 'M': /* mintty */
tty_default_features(features, "mintty", 0); tty_default_features(c, "mintty", 0);
break; break;
case 'T': /* tmux */ case 'T': /* tmux */
tty_default_features(features, "tmux", 0); tty_default_features(c, "tmux", 0);
break; break;
case 'U': /* rxvt-unicode */ case 'U': /* rxvt-unicode */
tty_default_features(features, "rxvt-unicode", 0); tty_default_features(c, "rxvt-unicode", 0);
break; break;
} }
log_debug("%s: received secondary DA %.*s", c->name, (int)*size, buf); log_debug("%s: received secondary DA %.*s", c->name, (int)*size, buf);
@@ -1611,7 +1669,6 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf,
size_t len, size_t *size) size_t len, size_t *size)
{ {
struct client *c = tty->client; struct client *c = tty->client;
int *features = &c->term_features;
u_int i; u_int i;
char tmp[128]; char tmp[128];
@@ -1654,19 +1711,21 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf,
/* Add terminal features. */ /* Add terminal features. */
if (strncmp(tmp, "iTerm2 ", 7) == 0) if (strncmp(tmp, "iTerm2 ", 7) == 0)
tty_default_features(features, "iTerm2", 0); tty_default_features(c, "iTerm2", 0);
else if (strncmp(tmp, "tmux ", 5) == 0) else if (strncmp(tmp, "tmux ", 5) == 0)
tty_default_features(features, "tmux", 0); tty_default_features(c, "tmux", 0);
else if (strncmp(tmp, "XTerm(", 6) == 0) else if (strncmp(tmp, "XTerm(", 6) == 0)
tty_default_features(features, "XTerm", 0); tty_default_features(c, "XTerm", 0);
else if (strncmp(tmp, "mintty ", 7) == 0) else if (strncmp(tmp, "mintty ", 7) == 0)
tty_default_features(features, "mintty", 0); tty_default_features(c, "mintty", 0);
else if (strncmp(tmp, "foot(", 5) == 0) else if (strncmp(tmp, "foot(", 5) == 0)
tty_default_features(features, "foot", 0); tty_default_features(c, "foot", 0);
else if (strncmp(tmp, "WezTerm ", 7) == 0) else if (strncmp(tmp, "WezTerm ", 7) == 0)
tty_default_features(features, "WezTerm", 0); tty_default_features(c, "WezTerm", 0);
else if (strncmp(tmp, "ghostty ", 8) == 0) else if (strncmp(tmp, "ghostty ", 8) == 0)
tty_default_features(features, "ghostty", 0); tty_default_features(c, "ghostty", 0);
else if (strncmp(tmp, "Rio ", 4) == 0)
tty_default_features(c, "Rio", 0);
log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf);
free(c->term_type); free(c->term_type);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-term.c,v 1.107 2026/08/05 08:54:56 nicm Exp $ */ /* $OpenBSD: tty-term.c,v 1.108 2026/08/17 14:47:41 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -544,8 +544,9 @@ tty_term_validate(struct tty_term *term)
struct tty_term * struct tty_term *
tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps,
int *feat, char **cause) char **cause)
{ {
struct client *c = tty->client;
struct tty_term *term; struct tty_term *term;
const struct tty_term_code_entry *ent; const struct tty_term_code_entry *ent;
struct tty_code *code; struct tty_code *code;
@@ -617,7 +618,7 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps,
offset = 0; offset = 0;
first = tty_term_override_next(s, &offset); first = tty_term_override_next(s, &offset);
if (first != NULL && fnmatch(first, term->name, 0) == 0) if (first != NULL && fnmatch(first, term->name, 0) == 0)
tty_add_features(feat, s + offset, ":"); tty_parse_client_features(c, s + offset, ":");
a = options_array_next(a); a = options_array_next(a);
} }
@@ -627,14 +628,14 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps,
del_curterm(cur_term); del_curterm(cur_term);
#endif #endif
/* Check for COLORTERM. */ /* Check for COLORTERM. */
envent = environ_find(tty->client->environ, "COLORTERM"); envent = environ_find(c->environ, "COLORTERM");
if (envent != NULL) { if (envent != NULL) {
log_debug("%s COLORTERM=%s", tty->client->name, envent->value); log_debug("%s COLORTERM=%s", c->name, envent->value);
if (strcasecmp(envent->value, "truecolor") == 0 || if (strcasecmp(envent->value, "truecolor") == 0 ||
strcasecmp(envent->value, "24bit") == 0) strcasecmp(envent->value, "24bit") == 0)
tty_add_features(feat, "RGB", ","); tty_parse_client_features(c, "RGB", ",");
else if (strstr(envent->value, "256") != NULL) else if (strstr(envent->value, "256") != NULL)
tty_add_features(feat, "256", ","); tty_parse_client_features(c, "256", ",");
} }
/* Apply overrides so any capabilities used for features are changed. */ /* Apply overrides so any capabilities used for features are changed. */
@@ -665,17 +666,17 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps,
s = tty_term_string(term, TTYC_CLEAR); s = tty_term_string(term, TTYC_CLEAR);
if (tty_term_flag(term, TTYC_XT) || strncmp(s, "\033[", 2) == 0) { if (tty_term_flag(term, TTYC_XT) || strncmp(s, "\033[", 2) == 0) {
term->flags |= TERM_VT100LIKE; term->flags |= TERM_VT100LIKE;
tty_add_features(feat, "bpaste,focus,title", ","); tty_parse_client_features(c, "bpaste,focus,title", ",");
} }
/* Add RGB feature if terminal has RGB colours. */ /* Add RGB feature if terminal has RGB colours. */
if ((tty_term_flag(term, TTYC_TC) || tty_term_has(term, TTYC_RGB)) && if ((tty_term_flag(term, TTYC_TC) || tty_term_has(term, TTYC_RGB)) &&
(!tty_term_has(term, TTYC_SETRGBF) || (!tty_term_has(term, TTYC_SETRGBF) ||
!tty_term_has(term, TTYC_SETRGBB))) !tty_term_has(term, TTYC_SETRGBB)))
tty_add_features(feat, "RGB", ","); tty_parse_client_features(c, "RGB", ",");
/* Apply the features and overrides again. */ /* Apply the features and overrides again. */
if (tty_apply_features(term, *feat)) if (tty_apply_features(term))
tty_term_apply_overrides(term); tty_term_apply_overrides(term);
/* Log the capabilities. */ /* Log the capabilities. */

8
tty.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty.c,v 1.477 2026/07/17 12:42:51 nicm Exp $ */ /* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -277,7 +277,7 @@ tty_open(struct tty *tty, char **cause)
struct client *c = tty->client; struct client *c = tty->client;
tty->term = tty_term_create(tty, c->term_name, c->term_caps, tty->term = tty_term_create(tty, c->term_name, c->term_caps,
c->term_ncaps, &c->term_features, cause); c->term_ncaps, cause);
if (tty->term == NULL) { if (tty->term == NULL) {
tty_close(tty); tty_close(tty);
return (-1); return (-1);
@@ -403,6 +403,8 @@ tty_send_requests(struct tty *tty)
tty_puts(tty, "\033[>c"); tty_puts(tty, "\033[>c");
if (~tty->flags & TTY_HAVEXDA) if (~tty->flags & TTY_HAVEXDA)
tty_puts(tty, "\033[>q"); tty_puts(tty, "\033[>q");
if (~tty->flags & TTY_HAVESYNC)
tty_puts(tty, "\033[?2026$p");
tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\"); tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\");
tty->flags |= (TTY_WAITBG|TTY_WAITFG); tty->flags |= (TTY_WAITBG|TTY_WAITFG);
} else } else
@@ -536,7 +538,7 @@ tty_update_features(struct tty *tty)
{ {
struct client *c = tty->client; struct client *c = tty->client;
if (tty_apply_features(tty->term, c->term_features)) if (tty_apply_features(tty->term))
tty_term_apply_overrides(tty->term); tty_term_apply_overrides(tty->term);
if (tty_use_margin(tty)) if (tty_use_margin(tty))