diff --git a/CHANGES b/CHANGES index 80016b0e1..093e29fc9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,4 @@ -CHANGES FROM 3.7b TO 3.8 - -* Build with jemalloc on macOS to avoid what appears to be a bug in the system - calloc(3) (issue 5385). +CHANGES FROM 3.7c TO 3.8 * 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 (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 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 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 diff --git a/client.c b/client.c index c7f574159..0358c351e 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -276,7 +276,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, proc_set_signals(client_proc, client_signal); /* Save the flags. */ - client_flags = flags; + client_flags = flags|CLIENT_WRITE_ACK; log_debug("flags are %#llx", (unsigned long long)client_flags); /* Initialize the client socket and start the server. */ diff --git a/cmd-break-pane.c b/cmd-break-pane.c index dcd4e8188..f2f1aff25 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -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 @@ -174,13 +174,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) } 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); wp->flags |= PANE_CHANGED; 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__); events_fire_window("window-created", w); window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); diff --git a/compat/getpeereid.c b/compat/getpeereid.c index b79f420ad..ed2ed8743 100644 --- a/compat/getpeereid.c +++ b/compat/getpeereid.c @@ -31,7 +31,7 @@ getpeereid(int s, uid_t *uid, gid_t *gid) { #ifdef HAVE_SO_PEERCRED struct ucred uc; - int len = sizeof uc; + socklen_t len = sizeof uc; if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1) return (-1); diff --git a/file.c b/file.c index 83691db54..dce8d0f67 100644 --- a/file.c +++ b/file.c @@ -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 @@ -502,7 +502,8 @@ file_push(struct client_file *cf) } else if (cf->stream > 2) { close.stream = cf->stream; 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); } @@ -527,14 +528,48 @@ file_write_left(struct client_files *files) 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. */ 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) { 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); + cf->error = error; bufferevent_free(cf->event); cf->event = NULL; @@ -542,7 +577,9 @@ file_write_error_callback(__unused struct bufferevent *bev, __unused short what, close(cf->fd); 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); } @@ -554,15 +591,10 @@ file_write_callback(__unused struct bufferevent *bev, void *arg) 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); - - 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). */ @@ -663,14 +695,10 @@ file_write_close(struct client_files *files, struct imsg *imsg) if ((cf = RB_FIND(client_files, files, &find)) == NULL) fatalx("unknown stream number"); log_debug("close file %d", cf->stream); + cf->closed = 1; if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) { - if (cf->event != NULL) - bufferevent_free(cf->event); - if (cf->fd != -1) - close(cf->fd); - RB_REMOVE(client_files, files, cf); - file_free(cf); + file_write_finished(cf); } } @@ -829,6 +857,28 @@ file_write_ready(struct client_files *files, struct imsg *imsg) 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). */ int file_read_data(struct client_files *files, struct imsg *imsg) diff --git a/menu.c b/menu.c index aab25d7c5..2816056a4 100644 --- a/menu.c +++ b/menu.c @@ -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 @@ -337,6 +337,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) struct cmdq_state *state; enum cmd_parse_status status; char *error; + key_code key; if (KEYC_IS_MOUSE(event->key)) { /* @@ -387,7 +388,8 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) name = menu->items[i].name; if (name == NULL || *name == '-') 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; goto chosen; } diff --git a/prompt.c b/prompt.c index c12a249a9..cc808a54e 100644 --- a/prompt.c +++ b/prompt.c @@ -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 @@ -1334,7 +1334,7 @@ process_key: if (pr->index != size) { memmove(pr->buffer + pr->index, pr->buffer + pr->index + 1, - (size + 1 - pr->index) * + (size - pr->index) * sizeof *pr->buffer); goto changed; } diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh index 96927e995..aedeaca19 100644 --- a/regress/mode-mutation.sh +++ b/regress/mode-mutation.sh @@ -269,6 +269,22 @@ test_customize_mode() 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() { 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_client test_customize_mode +test_customize_break_pane test_copy_mode cleanup exit 0 diff --git a/regress/sync-output-atomic.sh b/regress/sync-output-atomic.sh new file mode 100644 index 000000000..d0979ebe1 --- /dev/null +++ b/regress/sync-output-atomic.sh @@ -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 -Lsync-output-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lsync-output-outer-$$ -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 -Lsync-output-inner-$$ -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 diff --git a/server-client.c b/server-client.c index 78e92f35b..4b472633c 100644 --- a/server-client.c +++ b/server-client.c @@ -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.503 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -2695,6 +2695,10 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (file_write_ready(&c->files, imsg) != 0) goto bad; break; + case MSG_WRITE_DONE: + if (file_write_done(&c->files, imsg) != 0) + goto bad; + break; case MSG_READ: if (file_read_data(&c->files, imsg) != 0) goto bad; diff --git a/tmux-protocol.h b/tmux-protocol.h index d823ce7e2..396f10bc9 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -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 @@ -67,7 +67,8 @@ enum msgtype { MSG_WRITE, MSG_WRITE_READY, MSG_WRITE_CLOSE, - MSG_READ_CANCEL + MSG_READ_CANCEL, + MSG_WRITE_DONE }; /* @@ -116,4 +117,9 @@ struct msg_write_close { int stream; }; +struct msg_write_done { + int stream; + int error; +}; + #endif /* TMUX_PROTOCOL_H */ diff --git a/tmux.1 b/tmux.1 index 7cf0755da..fef3ab95d 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1156 2026/08/17 14:47:41 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: August 6 2026 $ +.Dd $Mdocdate: August 17 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -5047,6 +5047,10 @@ This is an array option where each entry is a colon-separated string made up of a terminal type pattern (matched using .Xr glob 7 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: .Bl -tag -width Ds .It 256 diff --git a/tmux.c b/tmux.c index c25e0d428..ab7c5133a 100644 --- a/tmux.c +++ b/tmux.c @@ -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 @@ -425,7 +425,7 @@ main(int argc, char **argv) while ((opt = getopt(argc, argv, "2c:CDdf:hlL:NqS:T:uUvV")) != -1) { switch (opt) { case '2': - tty_add_features(&feat, "256", ":,"); + tty_parse_features("256", ":,", &feat, NULL); break; case 'c': shell_command = optarg; @@ -473,7 +473,7 @@ main(int argc, char **argv) path = xstrdup(optarg); break; case 'T': - tty_add_features(&feat, optarg, ":,"); + tty_parse_features(optarg, ":,", &feat, NULL); break; case 'u': flags |= CLIENT_UTF8; diff --git a/tmux.h b/tmux.h index fbd85836a..13a2c333e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1424 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1709,7 +1709,7 @@ struct key_event { struct tty_term { char *name; struct tty *tty; - int features; + int applied_features; char acs[UCHAR_MAX + 1][2]; @@ -2222,6 +2222,7 @@ struct client { char *term_name; int term_features; + int term_nofeatures; char *term_type; char **term_caps; u_int term_ncaps; @@ -2289,7 +2290,7 @@ struct client { /* 0x800000000ULL unused */ #define CLIENT_BRACKETPASTING 0x1000000000ULL #define CLIENT_ASSUMEPASTING 0x2000000000ULL -/* 0x4000000000ULL unused */ +#define CLIENT_WRITE_ACK 0x4000000000ULL #define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL #define CLIENT_ALLREDRAWFLAGS \ (CLIENT_REDRAWWINDOW| \ @@ -2991,8 +2992,7 @@ extern struct tty_terms tty_terms; u_int tty_term_ncodes(void); void tty_term_apply(struct tty_term *, const char *, int); void tty_term_apply_overrides(struct tty_term *); -struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, int *, - char **); +struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, char **); void tty_term_free(struct tty_term *); int tty_term_read_list(const char *, int, char ***, u_int *, char **); @@ -3014,11 +3014,13 @@ int tty_term_flag(struct tty_term *, enum tty_code_code); const char *tty_term_describe(struct tty_term *, enum tty_code_code); /* 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); int tty_feature_present(struct tty_term *, const char *); -int tty_apply_features(struct tty_term *, int); -void tty_default_features(int *, const char *, u_int); +int tty_apply_features(struct tty_term *); +void tty_default_features(struct client *, const char *, u_int); /* tty-acs.c */ int tty_acs_needed(struct tty *); @@ -3272,6 +3274,7 @@ void file_write_close(struct client_files *, struct imsg *); void file_read_open(struct client_files *, struct tmuxpeer *, struct imsg *, int, int, client_file_cb, void *); 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_done(struct client_files *, struct imsg *); void file_read_cancel(struct client_files *, struct imsg *); diff --git a/tty-features.c b/tty-features.c index 6e2d35fa3..b92d2118f 100644 --- a/tty-features.c +++ b/tty-features.c @@ -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 @@ -408,17 +408,29 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_usstyle }; +/* Parse features for client. */ 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; char *next, *loop, *copy; u_int i; + int remove; log_debug("adding terminal features %s", 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++) { tf = tty_features[i]; if (strcasecmp(tf->name, next) == 0) @@ -428,14 +440,24 @@ tty_add_features(int *feat, const char *s, const char *separators) log_debug("unknown terminal feature: %s", next); 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); - (*feat) |= (1 << i); + (*enabled) |= (1 << i); } } free(copy); } +/* Get features as string. */ const char * tty_get_features(int feat) { @@ -457,6 +479,7 @@ tty_get_features(int feat) return (s); } +/* Check if feature is present. */ int tty_feature_present(struct tty_term *term, const char *name) { @@ -468,8 +491,8 @@ tty_feature_present(struct tty_term *term, const char *name) for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcmp(tf->name, name) == 0) { - if (term->features & (1 << i)) - return (1); + if (term->applied_features & (1 << i)) + return (1); break; } } @@ -496,19 +519,23 @@ tty_feature_present(struct tty_term *term, const char *name) return (1); } +/* Apply featurs to terminal. */ 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 char *const *capability; + int feat; u_int i; + feat = (c->term_features & ~c->term_nofeatures); if (feat == 0) return (0); log_debug("applying terminal features: %s", tty_get_features(feat)); 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; tf = tty_features[i]; @@ -523,14 +550,15 @@ tty_apply_features(struct tty_term *term, int feat) } term->flags |= tf->flags; } - if ((term->features | feat) == term->features) + if ((term->applied_features|feat) == term->applied_features) return (0); - term->features |= feat; + term->applied_features |= feat; return (1); } +/* Add default features for a terminal identified by name and version. */ 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 { const char *name; @@ -625,6 +653,18 @@ tty_default_features(int *feat, const char *name, u_int version) "usstyle," "kitty" }, + { .name = "Rio", + .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "ccolour," + "cstyle," + "focus," + "overline," + "hyperlinks," + "osc7," + "sync," + "usstyle," + "progressbar" + }, { .name = "XTerm", /* * xterm also supports DECSLRM and DECFRA, but they can be @@ -645,6 +685,6 @@ tty_default_features(int *feat, const char *name, u_int version) continue; if (version != 0 && version < table[i].version) continue; - tty_add_features(feat, table[i].features, ","); + tty_parse_client_features(c, table[i].features, ","); } } diff --git a/tty-keys.c b/tty-keys.c index c409e69c9..65ddae338 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -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.213 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1448,7 +1448,6 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i, n = 0; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; @@ -1505,13 +1504,13 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, for (i = 1; i < n; i++) { log_debug("%s: DA feature: %d", c->name, p[i]); if (p[i] == 4) - tty_add_features(features, "sixel", ","); + tty_parse_client_features(c, "sixel", ","); if (p[i] == 21) - tty_add_features(features, "margins", ","); + tty_parse_client_features(c, "margins", ","); if (p[i] == 28) - tty_add_features(features, "rectfill", ","); + tty_parse_client_features(c, "rectfill", ","); if (p[i] == 52) - tty_add_features(features, "clipboard", ","); + tty_parse_client_features(c, "clipboard", ","); } break; } @@ -1532,7 +1531,6 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i, n = 0; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; @@ -1586,13 +1584,13 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, */ switch (p[0]) { case 'M': /* mintty */ - tty_default_features(features, "mintty", 0); + tty_default_features(c, "mintty", 0); break; case 'T': /* tmux */ - tty_default_features(features, "tmux", 0); + tty_default_features(c, "tmux", 0); break; case 'U': /* rxvt-unicode */ - tty_default_features(features, "rxvt-unicode", 0); + tty_default_features(c, "rxvt-unicode", 0); break; } log_debug("%s: received secondary DA %.*s", c->name, (int)*size, buf); @@ -1612,7 +1610,6 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i; char tmp[128]; @@ -1655,19 +1652,21 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, /* Add terminal features. */ 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) - tty_default_features(features, "tmux", 0); + tty_default_features(c, "tmux", 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) - tty_default_features(features, "mintty", 0); + tty_default_features(c, "mintty", 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) - tty_default_features(features, "WezTerm", 0); + tty_default_features(c, "WezTerm", 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); free(c->term_type); diff --git a/tty-term.c b/tty-term.c index f9299263e..0720ecd06 100644 --- a/tty-term.c +++ b/tty-term.c @@ -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 @@ -545,8 +545,9 @@ tty_term_validate(struct tty_term *term) struct tty_term * 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; const struct tty_term_code_entry *ent; struct tty_code *code; @@ -618,7 +619,7 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, offset = 0; first = tty_term_override_next(s, &offset); 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); } @@ -628,14 +629,14 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, del_curterm(cur_term); #endif /* Check for COLORTERM. */ - envent = environ_find(tty->client->environ, "COLORTERM"); + envent = environ_find(c->environ, "COLORTERM"); 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 || strcasecmp(envent->value, "24bit") == 0) - tty_add_features(feat, "RGB", ","); + tty_parse_client_features(c, "RGB", ","); 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. */ @@ -666,17 +667,17 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, s = tty_term_string(term, TTYC_CLEAR); if (tty_term_flag(term, TTYC_XT) || strncmp(s, "\033[", 2) == 0) { 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. */ if ((tty_term_flag(term, TTYC_TC) || tty_term_has(term, TTYC_RGB)) && (!tty_term_has(term, TTYC_SETRGBF) || !tty_term_has(term, TTYC_SETRGBB))) - tty_add_features(feat, "RGB", ","); + tty_parse_client_features(c, "RGB", ","); /* Apply the features and overrides again. */ - if (tty_apply_features(term, *feat)) + if (tty_apply_features(term)) tty_term_apply_overrides(term); /* Log the capabilities. */ diff --git a/tty.c b/tty.c index e7e9fbd01..1d758b893 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.477 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.478 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -283,7 +283,7 @@ tty_open(struct tty *tty, char **cause) struct client *c = tty->client; 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) { tty_close(tty); return (-1); @@ -552,7 +552,7 @@ tty_update_features(struct tty *tty) { 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); #ifdef ENABLE_IMAGES image_tty_update(tty);