diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 9bce72248..e6765da99 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -16,6 +16,7 @@ concurrency: jobs: action: runs-on: ubuntu-latest + if: github.repository == 'tmux/tmux' steps: - uses: dessant/lock-threads@v6 with: diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 95935f2f8..3cc036425 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -16,6 +16,7 @@ jobs: regress: name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} + if: github.repository == 'tmux/tmux' timeout-minutes: 45 strategy: diff --git a/cfg.c b/cfg.c index a15ebf900..7a48cd9e1 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cfg.c,v 1.90 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cfg.c,v 1.91 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -221,13 +221,15 @@ cfg_print_causes(struct cmdq_item *item) { struct client *c = cmdq_get_client(item); u_int i; + char *cause; for (i = 0; i < cfg_ncauses; i++) { + cause = cfg_causes[i]; if (c != NULL && (c->flags & CLIENT_CONTROL)) - control_write(c, "%%config-error %s", cfg_causes[i]); + control_notify_write(c, "%%config-error %s", cause); else - cmdq_print(item, "%s", cfg_causes[i]); - free(cfg_causes[i]); + cmdq_print(item, "%s", cause); + free(cause); } free(cfg_causes); @@ -242,14 +244,16 @@ cfg_show_causes(struct session *s) struct window_pane *wp; struct window_mode_entry *wme; u_int i; + char *cause; if (cfg_ncauses == 0) return; if (c != NULL && (c->flags & CLIENT_CONTROL)) { for (i = 0; i < cfg_ncauses; i++) { - control_write(c, "%%config-error %s", cfg_causes[i]); - free(cfg_causes[i]); + cause = cfg_causes[i]; + control_notify_write(c, "%%config-error %s", cause); + free(cause); } goto out; } diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 3ecd0fd2b..34bc95c8e 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.73 2026/07/17 15:24:30 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -431,25 +431,23 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_move_pane_entry) { if (args_has(args, 'M')) return (cmd_join_pane_mouse_update(item)); - if (!window_pane_is_floating(dst_wp)) { - cmdq_error(item, "pane is not floating"); - return (CMD_RETURN_ERROR); - } - if ((s = args_get(args, 'P')) != NULL) { - server_unzoom_window(dst_w); - return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); - } - if ((s = args_get(args, 'z')) != NULL) { - server_unzoom_window(dst_w); - return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s)); - } - if (args_has(args, 'X') || + if (args_has(args, 'P') || + args_has(args, 'z') || + args_has(args, 'X') || args_has(args, 'Y') || args_has(args, 'U') || args_has(args, 'D') || args_has(args, 'L') || args_has(args, 'R')) { + if (!window_pane_is_floating(dst_wp)) { + cmdq_error(item, "pane is not floating"); + return (CMD_RETURN_ERROR); + } server_unzoom_window(dst_w); + if ((s = args_get(args, 'P')) != NULL) + return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); + if ((s = args_get(args, 'z')) != NULL) + return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s)); return (cmd_join_pane_move(item, args, dst_wl, dst_wp)); } } diff --git a/cmd-queue.c b/cmd-queue.c index 2aa8a500a..3cf4bffa6 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.121 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -773,7 +773,7 @@ cmdq_guard(struct cmdq_item *item, const char *guard, int flags) u_int number = item->number; if (c != NULL && (c->flags & CLIENT_CONTROL)) - control_write(c, "%%%s %ld %u %d", guard, t, number, flags); + control_write_guard(c, guard, t, number, flags); } /* Show message from command. */ diff --git a/cmd-split-window.c b/cmd-split-window.c index c5fb8effb..8beeac557 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -99,6 +99,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); else { + window_unzoom(w, 1); is_floating = window_pane_is_floating(wp); flags |= SPAWN_SPLIT; } diff --git a/control-notify.c b/control-notify.c index 6fec1683f..adfc9dcb0 100644 --- a/control-notify.c +++ b/control-notify.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -44,7 +44,8 @@ control_pane_mode_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%pane-mode-changed %%%u", wp->id); + control_notify_write(c, "%%pane-mode-changed %%%u", + wp->id); } return; } @@ -53,8 +54,9 @@ control_pane_mode_changed_cb(__unused const char *name, if (value == NULL) return; TAILQ_FOREACH(c, &clients, entry) { - if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) - control_write(c, "%%pane-mode-changed %s", value); + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_notify_write(c, "%%pane-mode-changed %s", value); } free(value); } @@ -92,7 +94,7 @@ control_window_layout_changed_cb(__unused const char *name, continue; s = c->session; if (winlink_find_by_window_id(&s->windows, w->id) != NULL) - control_write(c, "%s", cp); + control_notify_write(c, "%s", cp); } free(cp); } @@ -111,7 +113,7 @@ control_window_pane_changed_cb(__unused const char *name, if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%window-pane-changed @%u %%%u", w->id, + control_notify_write(c, "%%window-pane-changed @%u %%%u", w->id, w->active->id); } } @@ -133,9 +135,11 @@ control_window_unlinked_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) - control_write(c, "%%window-close @%u", w->id); - else - control_write(c, "%%unlinked-window-close @%u", w->id); + control_notify_write(c, "%%window-close @%u", w->id); + else { + control_notify_write(c, "%%unlinked-window-close @%u", + w->id); + } } } @@ -156,9 +160,11 @@ control_window_linked_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) - control_write(c, "%%window-add @%u", w->id); - else - control_write(c, "%%unlinked-window-add @%u", w->id); + control_notify_write(c, "%%window-add @%u", w->id); + else { + control_notify_write(c, "%%unlinked-window-add @%u", + w->id); + } } } @@ -179,11 +185,11 @@ control_window_renamed_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) { - control_write(c, "%%window-renamed @%u %s", w->id, - w->name); - } else { - control_write(c, "%%unlinked-window-renamed @%u %s", + control_notify_write(c, "%%window-renamed @%u %s", w->id, w->name); + } else { + control_notify_write(c, + "%%unlinked-window-renamed @%u %s", w->id, w->name); } } } @@ -206,11 +212,12 @@ control_client_session_changed_cb(__unused const char *name, continue; if (cc == c) { - control_write(c, "%%session-changed $%u %s", s->id, - s->name); + control_notify_write(c, "%%session-changed $%u %s", + s->id, s->name); } else { - control_write(c, "%%client-session-changed %s $%u %s", - cc->name, s->id, s->name); + control_notify_write(c, + "%%client-session-changed %s $%u %s", cc->name, + s->id, s->name); } } } @@ -226,8 +233,9 @@ control_client_detached_cb(__unused const char *name, struct event_payload *ep, if (cc == NULL) return; TAILQ_FOREACH(c, &clients, entry) { - if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) - control_write(c, "%%client-detached %s", cc->name); + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_notify_write(c, "%%client-detached %s", cc->name); } } @@ -244,8 +252,8 @@ control_session_renamed_cb(__unused const char *name, struct event_payload *ep, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%session-renamed $%u %s", s->id, s->name); + control_notify_write(c, "%%session-renamed $%u %s", s->id, + s->name); } } @@ -259,8 +267,7 @@ control_session_created_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%sessions-changed"); + control_notify_write(c, "%%sessions-changed"); } } @@ -274,8 +281,7 @@ control_session_closed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%sessions-changed"); + control_notify_write(c, "%%sessions-changed"); } } @@ -298,9 +304,8 @@ control_session_window_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%session-window-changed $%u @%u", s->id, - s->curw->window->id); + control_notify_write(c, "%%session-window-changed $%u @%u", + s->id, s->curw->window->id); } } @@ -317,8 +322,7 @@ control_paste_buffer_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%paste-buffer-changed %s", pbname); + control_notify_write(c, "%%paste-buffer-changed %s", pbname); } } @@ -335,8 +339,7 @@ control_paste_buffer_deleted_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%paste-buffer-deleted %s", pbname); + control_notify_write(c, "%%paste-buffer-deleted %s", pbname); } } diff --git a/control.c b/control.c index 2451fd18a..43af07dd8 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.62 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -53,6 +53,17 @@ struct control_block { TAILQ_ENTRY(control_block) all_entry; }; +/* + * A notification line deferred because it was generated while a command's + * %begin/%end guard block was open. Notifications must never appear inside a + * guard block, so they are held here and flushed once the block closes. + */ +struct control_line { + char *line; + + TAILQ_ENTRY(control_line) entry; +}; + /* Control client pane. */ struct control_pane { u_int pane; @@ -102,6 +113,13 @@ struct control_state { struct bufferevent *write_event; struct monitor_set *subs; + + /* + * Depth of open %begin/%end guard blocks and notifications deferred + * until the outermost block closes. + */ + int guard_depth; + TAILQ_HEAD(, control_line) deferred; }; /* Low and high watermarks. */ @@ -352,7 +370,7 @@ control_continue_pane(struct client *c, struct window_pane *wp) cp->flags &= ~CONTROL_PANE_PAUSED; memcpy(&cp->offset, &wp->offset, sizeof cp->offset); memcpy(&cp->queued, &wp->offset, sizeof cp->queued); - control_write(c, "%%continue %%%u", wp->id); + control_notify_write(c, "%%continue %%%u", wp->id); } } @@ -366,52 +384,120 @@ control_pause_pane(struct client *c, struct window_pane *wp) if (~cp->flags & CONTROL_PANE_PAUSED) { cp->flags |= CONTROL_PANE_PAUSED; control_discard_pane(c, cp); - control_write(c, "%%pause %%%u", wp->id); + control_notify_write(c, "%%pause %%%u", wp->id); } } -/* Write a line. */ -static void printflike(2, 0) -control_vwrite(struct client *c, const char *fmt, va_list ap) -{ - struct control_state *cs = c->control_state; - char *s; - - xvasprintf(&s, fmt, ap); - log_debug("%s: %s: writing line: %s", __func__, c->name, s); - - bufferevent_write(cs->write_event, s, strlen(s)); - bufferevent_write(cs->write_event, "\n", 1); - - bufferevent_enable(cs->write_event, EV_WRITE); - free(s); -} - -/* Write a line. */ -void -control_write(struct client *c, const char *fmt, ...) +/* Write an already-formatted line, queueing it behind %output if needed. */ +static void +control_write_line(struct client *c, char *line) { struct control_state *cs = c->control_state; struct control_block *cb; - va_list ap; - - va_start(ap, fmt); if (TAILQ_EMPTY(&cs->all_blocks)) { - control_vwrite(c, fmt, ap); - va_end(ap); + log_debug("%s: %s: writing line: %s", __func__, c->name, line); + bufferevent_write(cs->write_event, line, strlen(line)); + bufferevent_write(cs->write_event, "\n", 1); + bufferevent_enable(cs->write_event, EV_WRITE); + free(line); return; } cb = xcalloc(1, sizeof *cb); - xvasprintf(&cb->line, fmt, ap); + cb->line = line; TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry); cb->t = get_timer(); log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line); bufferevent_enable(cs->write_event, EV_WRITE); +} +/* Flush notifications that were deferred while a guard block was open. */ +static void +control_flush_deferred(struct client *c) +{ + struct control_state *cs = c->control_state; + struct control_line *cl, *cl1; + + TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) { + TAILQ_REMOVE(&cs->deferred, cl, entry); + control_write_line(c, cl->line); + free(cl); + } +} + +/* + * Write a line of command output or error text. This is a sink for arbitrary + * user-controlled text (command output, capture-pane, error messages), so it + * must never try to interpret the content: guard tracking is done only in + * control_write_guard. + */ +void +control_write(struct client *c, const char *fmt, ...) +{ + va_list ap; + char *line; + + va_start(ap, fmt); + xvasprintf(&line, fmt, ap); va_end(ap); + + control_write_line(c, line); +} + +/* + * Write a %begin, %end or %error guard around a command's output. This is the + * only place guard lines are produced, so the block depth is maintained here; + * when the outermost block closes any deferred notifications are flushed after + * it. "guard" is always one of the fixed strings from cmdq_guard, never user + * text. + */ +void +control_write_guard(struct client *c, const char *guard, long t, u_int number, + int flags) +{ + struct control_state *cs = c->control_state; + char *line; + + if (strcmp(guard, "begin") == 0) + cs->guard_depth++; + + xasprintf(&line, "%%%s %ld %u %d", guard, t, number, flags); + control_write_line(c, line); + + if (strcmp(guard, "begin") != 0 && cs->guard_depth > 0 && + --cs->guard_depth == 0) + control_flush_deferred(c); +} + +/* + * Write a notification line. Notifications must never appear inside a command's + * %begin/%end guard block, so if one is open the line is deferred until it + * closes. + */ +void +control_notify_write(struct client *c, const char *fmt, ...) +{ + struct control_state *cs = c->control_state; + struct control_line *cl; + va_list ap; + char *line; + + va_start(ap, fmt); + xvasprintf(&line, fmt, ap); + va_end(ap); + + if (cs->guard_depth == 0) { + control_write_line(c, line); + return; + } + + log_debug("%s: %s: deferring notification: %s", __func__, c->name, + line); + cl = xcalloc(1, sizeof *cl); + cl->line = line; + TAILQ_INSERT_TAIL(&cs->deferred, cl, entry); } /* Check age for this pane. */ @@ -438,7 +524,7 @@ control_check_age(struct client *c, struct window_pane *wp, return (0); cp->flags |= CONTROL_PANE_PAUSED; control_discard_pane(c, cp); - control_write(c, "%%pause %%%u", wp->id); + control_notify_write(c, "%%pause %%%u", wp->id); } else { if (age < CONTROL_MAXIMUM_AGE) return (0); @@ -811,14 +897,17 @@ control_sub_change(struct monitor_change *change, __unused void *data) if (wp != NULL) { w = wp->window; - control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s", + control_notify_write(c, + "%%subscription-changed %s $%u @%u %u %%%u : %s", change->name, s->id, w->id, wl->idx, wp->id, change->value); } else if (wl != NULL) { w = wl->window; - control_write(c, "%%subscription-changed %s $%u @%u %u - : %s", + control_notify_write(c, + "%%subscription-changed %s $%u @%u %u - : %s", change->name, s->id, w->id, wl->idx, change->value); } else { - control_write(c, "%%subscription-changed %s $%u - - - : %s", + control_notify_write(c, + "%%subscription-changed %s $%u - - - : %s", change->name, s->id, change->value); } } @@ -841,6 +930,7 @@ control_start(struct client *c) RB_INIT(&cs->windows); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); + TAILQ_INIT(&cs->deferred); cs->subs = monitor_create_client(c, control_sub_change, NULL); cs->read_event = bufferevent_new(c->fd, control_read_callback, @@ -884,6 +974,19 @@ control_discard(struct client *c) bufferevent_disable(cs->read_event, EV_READ); } +/* Discard all tmux-owned queued control blocks and stop writing. */ +void +control_discard_all(struct client *c) +{ + struct control_state *cs = c->control_state; + struct control_block *cb, *cb1; + + control_discard(c); + TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) + control_free_block(cs, cb); + bufferevent_disable(cs->write_event, EV_WRITE); +} + /* Stop control mode. */ void control_stop(struct client *c) @@ -891,12 +994,19 @@ control_stop(struct client *c) struct control_state *cs = c->control_state; struct control_block *cb, *cb1; struct control_window *cw, *cw1; + struct control_line *cl, *cl1; if (cs == NULL) return; monitor_destroy(cs->subs); + TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) { + TAILQ_REMOVE(&cs->deferred, cl, entry); + free(cl->line); + free(cl); + } + if (~c->flags & CLIENT_CONTROLCONTROL) bufferevent_free(cs->write_event); bufferevent_free(cs->read_event); diff --git a/format.c b/format.c index 9ca1f138d..d88c29d66 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.409 2026/07/26 15:21:53 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2101,6 +2101,60 @@ format_cb_synchronized_output_flag(struct format_tree *ft) return (NULL); } +/* Callback for pane_private_modes. */ +static void * +format_cb_pane_private_modes(struct format_tree *ft) +{ + static const struct { + int mode; + int number; + } table[] = { + { MODE_KCURSOR, 1 }, /* DECCKM */ + { MODE_ORIGIN, 6 }, /* DECOM */ + { MODE_WRAP, 7 }, /* DECAWM */ + { MODE_CURSOR_BLINKING, 12 }, /* cursor blinking */ + { MODE_CURSOR, 25 }, /* DECTCEM */ + { MODE_MOUSE_STANDARD, 1000 }, /* mouse normal tracking */ + { MODE_MOUSE_BUTTON, 1002 }, /* mouse button tracking */ + { MODE_MOUSE_ALL, 1003 }, /* mouse any tracking */ + { MODE_FOCUSON, 1004 }, /* focus reporting */ + { MODE_MOUSE_UTF8, 1005 }, /* mouse: UTF-8 */ + { MODE_MOUSE_SGR, 1006 }, /* mouse: SGR */ + { MODE_BRACKETPASTE, 2004 }, /* bracketed paste */ + { MODE_SYNC, 2026 }, /* synchronized output */ + { MODE_THEME_UPDATES, 2031 }, /* theme update notifications */ + }; + int mode; + char *value = NULL, *tmp; + u_int i; + + if (ft->wp == NULL) + return (NULL); + mode = ft->wp->base.mode; + + for (i = 0; i < nitems(table); i++) { + if (~mode & table[i].mode) + continue; + /* + * Only report cursor blinking when set by the application, not + * when it comes from the cursor-style option. + */ + if (table[i].mode == MODE_CURSOR_BLINKING && + (~mode & MODE_CURSOR_BLINKING_SET)) + continue; + if (value == NULL) + xasprintf(&value, "%d", table[i].number); + else { + xasprintf(&tmp, "%s,%d", value, table[i].number); + free(value); + value = tmp; + } + } + if (value == NULL) + return (xstrdup("")); + return (value); +} + /* Callback for pane_active. */ static void * format_cb_pane_active(struct format_tree *ft) @@ -2628,20 +2682,23 @@ format_cb_pane_unzoomed_width(struct format_tree *ft) { struct window_pane *wp = ft->wp; struct layout_cell *lc; - int sb_w, sb_pad; + int saved, sb_w, sb_pad; u_int sx; if (wp == NULL) return (NULL); lc = wp->saved_layout_cell; + saved = (lc != NULL); if (lc == NULL) lc = wp->layout_cell; if (lc == NULL) return (NULL); sx = lc->g.sx; - if (window_pane_scrollbar_reserve(wp)) { + if ((saved && !SCREEN_IS_ALTERNATE(&wp->base) && + wp->window->sb == PANE_SCROLLBARS_ALWAYS) || + (!saved && window_pane_scrollbar_reserve(wp))) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; if (sb_w < 1) @@ -3810,6 +3867,9 @@ static const struct format_table_entry format_table[] = { { "pane_pipe_pid", FORMAT_TABLE_STRING, format_cb_pane_pipe_pid }, + { "pane_private_modes", FORMAT_TABLE_STRING, + format_cb_pane_private_modes + }, { "pane_right", FORMAT_TABLE_STRING, format_cb_pane_right }, @@ -4197,6 +4257,8 @@ format_log_debug_cb(const char *key, const char *value, void *arg) void format_log_debug(struct format_tree *ft, const char *prefix) { + if (log_get_level() == 0) + return; format_each(ft, format_log_debug_cb, (void *)prefix); } diff --git a/grid-reader.c b/grid-reader.c index 9b40def6c..4cf47b8cb 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ +/* $OpenBSD: grid-reader.c,v 1.11 2026/07/29 17:42:56 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee @@ -54,11 +54,8 @@ grid_reader_cursor_right(struct grid_reader *gr, int wrap, int all, int onemore) px = gr->gd->sx; else if (onemore) px = grid_reader_line_length(gr); - else { - px = grid_reader_line_length(gr); - if (px != 0) - px--; - } + else + px = grid_line_limit(gr->gd, gr->cy); if (wrap && gr->cx >= px && gr->cy < gr->gd->hsize + gr->gd->sy - 1) { grid_reader_cursor_start_of_line(gr, 0); diff --git a/grid-view.c b/grid-view.c index 86317526f..5659d5763 100644 --- a/grid-view.c +++ b/grid-view.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ +/* $OpenBSD: grid-view.c,v 1.39 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -47,9 +47,9 @@ grid_view_set_cell(struct grid *gd, u_int px, u_int py, /* Set padding. */ void -grid_view_set_padding(struct grid *gd, u_int px, u_int py) +grid_view_set_padding(struct grid *gd, u_int px, u_int py, int bg) { - grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py)); + grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py), bg); } /* Set cells. */ diff --git a/grid.c b/grid.c index 1187aea5f..53c8c2cb0 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.154 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -682,9 +682,13 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc) /* Set padding at position. */ void -grid_set_padding(struct grid *gd, u_int px, u_int py) +grid_set_padding(struct grid *gd, u_int px, u_int py, int bg) { - grid_set_cell(gd, px, py, &grid_padding_cell); + struct grid_cell gc; + + memcpy(&gc, &grid_padding_cell, sizeof gc); + gc.bg = bg; + grid_set_cell(gd, px, py, &gc); } /* Set cells at position. */ @@ -1681,6 +1685,26 @@ grid_line_length(struct grid *gd, u_int py) return (px); } +/* Get last position on line, not including padding. */ +u_int +grid_line_limit(struct grid *gd, u_int py) +{ + struct grid_cell gc; + u_int px; + + px = grid_line_length(gd, py); + if (px == 0) + return (0); + px--; + while (px > 0) { + grid_get_cell(gd, px, py, &gc); + if (~gc.flags & GRID_FLAG_PADDING) + break; + px--; + } + return (px); +} + /* Check if character is in set. */ int grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) diff --git a/key-bindings.c b/key-bindings.c index 331362a59..4fd3dfac5 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.190 2026/07/22 19:23:59 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.192 2026/08/06 09:05:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -419,6 +419,7 @@ key_bindings_init(void) "bind -N 'Spread panes out evenly' E { select-layout -E }", "bind -N 'Switch to the last client' L { switch-client -l }", "bind -N 'Clear the marked pane' M { select-pane -M }", + "bind -N 'Change the pane title' T { command-prompt -I'#T' { select-pane -T '%%' } }", "bind -N 'Enter copy mode' [ { copy-mode }", "bind -N 'Paste the most recent paste buffer' ] { paste-buffer -p }", "bind -N 'Create a new window' c { new-window }", @@ -593,7 +594,7 @@ key_bindings_init(void) "bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode n { send -X search-again }", "bind -Tcopy-mode q { send -X cancel }", - "bind -Tcopy-mode r { send -X refresh-toggle }", + "bind -Tcopy-mode r { send -X refresh-now }", "bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode Home { send -X start-of-line }", "bind -Tcopy-mode End { send -X end-of-line }", @@ -703,7 +704,7 @@ key_bindings_init(void) "bind -Tcopy-mode-vi n { send -X search-again }", "bind -Tcopy-mode-vi o { send -X other-end }", "bind -Tcopy-mode-vi q { send -X cancel }", - "bind -Tcopy-mode-vi r { send -X refresh-toggle }", + "bind -Tcopy-mode-vi r { send -X refresh-now }", "bind -Tcopy-mode-vi t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode-vi v { send -X rectangle-toggle }", "bind -Tcopy-mode-vi w { send -X next-word }", diff --git a/mode-tree.c b/mode-tree.c index a5b2f935e..28c8f58eb 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mode-tree.c,v 1.100 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -47,7 +47,7 @@ enum mode_tree_preview { "#[acs]x" MODE_TREE_PREFIX_STYLE " }," \ "#{mode_tree_repeat}}" \ "#{?mode_tree_branch," \ - "#[acs]#{?mode_tree_last,mq,tq}" MODE_TREE_PREFIX_STYLE "> ,}" \ + "#[acs]#{?mode_tree_last,mq,tq}+" MODE_TREE_PREFIX_STYLE " ,}" \ "#{?mode_tree_has_children," \ "#{?mode_tree_expanded,#[fg=themered]-" MODE_TREE_PREFIX_STYLE " ," \ "#[fg=themegreen]+" MODE_TREE_PREFIX_STYLE " }," \ diff --git a/proc.c b/proc.c index d3536b17e..bfe1fbe26 100644 --- a/proc.c +++ b/proc.c @@ -1,4 +1,4 @@ -/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ +/* $OpenBSD: proc.c,v 1.32 2026/08/04 13:16:03 claudio Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -79,7 +79,7 @@ static void proc_event_cb(__unused int fd, short events, void *arg) { struct tmuxpeer *peer = arg; - ssize_t n; + int n; struct imsg imsg; if (!(peer->flags & PEER_BAD) && (events & EV_READ)) { @@ -88,7 +88,7 @@ proc_event_cb(__unused int fd, short events, void *arg) return; } for (;;) { - if ((n = imsg_get(&peer->ibuf, &imsg)) == -1) { + if ((n = imsgbuf_get(&peer->ibuf, &imsg)) == -1) { peer->dispatchcb(NULL, peer->arg); return; } diff --git a/regress/control-client-exit-stalled.sh b/regress/control-client-exit-stalled.sh new file mode 100755 index 000000000..431fd66d8 --- /dev/null +++ b/regress/control-client-exit-stalled.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# A control client whose terminal has stopped accepting output (for example +# its ssh connection or terminal emulator died) never drains the output that +# server_client_check_exit waits on before sending MSG_EXIT, and may then +# never close its socket either. This must not prevent the server from +# exiting: after kill-server the server previously stayed alive forever with +# server_exit set, closing every new connection immediately, so every new +# client failed with "server exited unexpectedly" until the server was killed +# by hand. The server now bounds the exit handshake: it discards output it +# can never deliver and drops the client if it still does not close. +# +# The stalled terminal is simulated by a fifo whose read end is held open but +# never read. + +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 + +# A detached session whose pane floods printable output forever. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1 +SERVER=$($TMUX display -pt rt '#{pid}') + +# Attach a control client with output down the fifo; stdin held open by sleep +# so it stays attached. The fifo fills and is never read, so a backlog forms +# in the server that can never be delivered. +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" + +n=0 +while [ $n -lt 50 ]; do + $TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break + sleep 0.1 + n=$((n + 1)) +done +$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . || + { echo "control client did not attach"; exit 1; } + +# Let the pane flood fill the fifo so the client's output is stuck. +sleep 2 + +# Ask the server to exit. This sets CLIENT_EXIT on the stalled client, whose +# output can never drain. +$TMUX kill-server 2>/dev/null + +# The server must exit within the exit timeout (10 seconds) plus slack, +# rather than staying in limbo forever rejecting new clients. +n=0 +while [ $n -lt 100 ]; do + if ! kill -0 "$SERVER" 2>/dev/null; then + SERVER= + exit 0 + fi + sleep 0.2 + n=$((n + 1)) +done + +echo "server did not exit after kill-server; new clients see:" +$TMUX ls 2>&1 +exit 1 diff --git a/regress/control-notify-guard.sh b/regress/control-notify-guard.sh new file mode 100755 index 000000000..459ec9f35 --- /dev/null +++ b/regress/control-notify-guard.sh @@ -0,0 +1,158 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +BADCFG="$TMPDIR/bad.conf" +PID= + +cleanup() +{ + exec 3>&- 2>/dev/null + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + [ -s "$OUT" ] && cat "$OUT" >&2 + exit 1 +} + +wait_for() +{ + pattern=$1 + timeout=${2:-60} + i=0 + + while [ "$i" -lt "$timeout" ]; do + grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "missing: $pattern" +} + +wait_for_count() +{ + pattern=$1 + expected=$2 + timeout=${3:-60} + i=0 + + while [ "$i" -lt "$timeout" ]; do + count=$(grep -F -c -- "$pattern" "$OUT" 2>/dev/null) + [ "$count" -ge "$expected" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "missing count $expected for: $pattern" +} + +send() +{ + printf '%s\n' "$1" >&3 || fail "failed to send: $1" +} + +# A notification is a standalone protocol message. It must never be part of +# the response between the real %begin and matching %end or %error for a +# command. Ignore guard-looking command output while a real guard is open. +check_guards() +{ + awk ' + function notification(line) { + return line ~ /^%(sessions-changed|session-changed |unlinked-window-add )/ || + line ~ /^%(pause |continue |config-error )/ + } + !open && $0 ~ /^%begin [0-9]+ [0-9]+ [0-9]+$/ { + open = 1 + time = $2 + number = $3 + flags = $4 + next + } + open && ($0 == "%end " time " " number " " flags || + $0 == "%error " time " " number " " flags) { + open = 0 + next + } + notification($0) { + seen[$1]++ + if (open) { + print "notification inside guard: " $0 > "/dev/stderr" + bad = 1 + } + } + END { + if (open) { + print "unterminated command guard" > "/dev/stderr" + bad = 1 + } + if (seen["%sessions-changed"] < 2 || + seen["%session-changed"] < 1 || + seen["%unlinked-window-add"] < 2 || + seen["%pause"] < 1 || seen["%continue"] < 1 || + seen["%config-error"] < 1) { + print "missing expected notification" > "/dev/stderr" + bad = 1 + } + exit bad + }' "$OUT" || fail "bad control protocol notification ordering" +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s guard -x 80 -y 24 || exit 1 +pane=$($TMUX display-message -p -t guard:0.0 '#{pane_id}') || exit 1 + +mkfifo "$IN" || exit 1 +: >"$OUT" +$TMUX -C attach-session -t guard <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +# Attaching generates a session notification synchronously from inside the +# attach command. It must follow the closing guard. +wait_for '%session-changed ' + +# Guard-looking output is arbitrary command output, not protocol state. Make +# new-session both generate notifications and print an unmatched fake %begin; +# it must not leave those notifications permanently deferred. +send "set-option -g @fake-begin '%begin 1 2 3'" +send "new-session -d -P -F '#{@fake-begin}' -s fake-begin" +wait_for_count '%sessions-changed' 1 + +# Likewise, fake %end output from a command that generates multiple +# notifications must not flush them before the command actually ends. +send "set-option -g @fake-end '%end 1 2 3'" +send "new-session -d -P -F '#{@fake-end}' -s fake-end" +wait_for_count '%sessions-changed' 2 + +# Exercise notification paths outside control-notify.c. +send "refresh-client -A '$pane:pause'" +wait_for "%pause $pane" +send "refresh-client -A '$pane:continue'" +wait_for "%continue $pane" + +printf '%s\n' 'not-a-command' >"$BADCFG" +send "source-file '$BADCFG'" +wait_for '%config-error ' + +# A final command guarantees all earlier command guards have completed before +# the transcript is checked. +send "display-message -p 'guard-check-done'" +wait_for 'guard-check-done' +check_guards + +exit 0 diff --git a/regress/copy-mode-test-vi.sh b/regress/copy-mode-test-vi.sh index 71810b006..817a5d4af 100644 --- a/regress/copy-mode-test-vi.sh +++ b/regress/copy-mode-test-vi.sh @@ -114,5 +114,30 @@ $TMUX send-keys -X next-space-end $TMUX send-keys -X copy-selection [ "$($TMUX show-buffer)" = "500xyz" ] || exit 1 +# Test that vi cursor movement does not stop on the padding cell of a wide +# character at the end of a line. +$TMUX kill-server 2>/dev/null +sleep 1 +$TMUX new -d -x20 -y5 \ + "printf 'abc中\nxyz\n'; exec cat" || exit 1 +$TMUX set-window-option -g mode-keys vi +$TMUX copy-mode +$TMUX send-keys -X history-top +$TMUX send-keys -X start-of-line +$TMUX send-keys -X cursor-right +$TMUX send-keys -X cursor-right +$TMUX send-keys -X cursor-right +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] || + exit 1 +$TMUX send-keys -X cursor-right +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "0,1" ] || + exit 1 +$TMUX send-keys -X cursor-left +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] || + exit 1 +$TMUX send-keys -X cursor-left +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "2,0" ] || + exit 1 + $TMUX kill-server 2>/dev/null exit 0 diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 29c79e2e2..41a6a2a45 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -192,13 +192,13 @@ check_fail "invalid window name: $(printf 'a\377b')" \ break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P: # join-pane can move a pane from one window to another without destroying -# the source window if other panes remain. (On this branch move-pane is -# reserved for floating panes, covered by floating-pane-geometry.sh.) +# the source window if other panes remain. move-pane does the same when no +# floating-pane movement flags are given. check_ok new-window -d -t P:5 -n other check_ok join-pane -d -s "$p1" -t P:5.0 check_fmt 'P:5' '#{window_panes}' '2' check_fmt 'P:0' '#{window_panes}' '3' -check_ok join-pane -d -v -s "$p1" -t "$p2" +check_ok move-pane -d -v -s "$p1" -t "$p2" check_fmt 'P:0' '#{window_panes}' '4' check_fmt 'P:5' '#{window_panes}' '1' diff --git a/regress/pipe-pane.sh b/regress/pipe-pane.sh index ee302ffbb..310861a5e 100644 --- a/regress/pipe-pane.sh +++ b/regress/pipe-pane.sh @@ -35,9 +35,9 @@ check_alive() # A pipe-pane -I child may write after the pane process has exited. With # remain-on-exit, the pane stays around but its bufferevent has been freed. -check_ok new-session -d -s pipe -x 80 -y 24 'sleep 0.2' +check_ok new-session -d -s pipe -x 80 -y 24 'sleep 1' check_ok set-option -t pipe:0 remain-on-exit on -check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x' +check_ok pipe-pane -t pipe:0.0 -I 'sleep 2; printf x' i=0 while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do @@ -46,7 +46,7 @@ while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do sleep 0.1 done -sleep 0.7 +sleep 2 check_alive $TMUX kill-server 2>/dev/null diff --git a/regress/server-socket-error.sh b/regress/server-socket-error.sh new file mode 100755 index 000000000..60ff4c07d --- /dev/null +++ b/regress/server-socket-error.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Server socket creation failures must produce a nonzero client exit status. + +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +# Root bypasses directory mode bits, so it cannot trigger this failure. +[ "$(id -u)" -eq 0 ] && exit 0 + +TMP=$(mktemp -d) || exit 1 +SOCKET="$TMP/ro/socket" + +cleanup() +{ + chmod 700 "$TMP/ro" 2>/dev/null + rm -rf "$TMP" +} + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +mkdir "$TMP/ro" || exit 1 +chmod 500 "$TMP/ro" || exit 1 +trap cleanup 0 1 15 + +check_failure() +{ + output=$($TEST_TMUX -S "$SOCKET" -f/dev/null "$@" 2>&1) + retval=$? + + [ "$retval" -ne 0 ] || fail "tmux $* exited zero: $output" + case "$output" in + "error creating $SOCKET ("*) ;; + *) fail "tmux $* produced unexpected error: $output" ;; + esac +} + +check_failure start-server +check_failure new-session -d + +exit 0 diff --git a/regress/tab-cell-background.sh b/regress/tab-cell-background.sh new file mode 100755 index 000000000..350b77854 --- /dev/null +++ b/regress/tab-cell-background.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# When a write breaks up a wide character or a tab, the columns it covered must +# keep the background colour, both in the grid and on the terminal. The second +# server is attached to the first so that what is actually sent is checked, not +# just what tmux has stored. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +$TMUX2 kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP" 0 1 15 + +# 1. Write inside a tab. 2. Overwrite the right half of a wide character. +# 3. Two tabs of different colours, the first partly deleted so that padding +# from both ends up next to each other, then written over. +# The sleep is so that the client is attached before anything is written and +# the changes are sent as they happen rather than as one redraw. +$TMUX2 -f/dev/null new -d -x24 -y3 " + sleep 2 + printf '\033[44m\033[K\t\033[4GX' + printf '\033[2;1H\033[44m\033[K\344\270\226\033[2GX' + printf '\033[3;1H\033[42m\033[K\t\033[43m\033[K\t\033[0m\033[4G\033[8P\rx' + cat" || exit 1 +$TMUX2 set -g status off || exit 1 + +$TMUX -f/dev/null new -x24 -y4 -d "$TMUX2 attach" || exit 1 +sleep 4 + +$TMUX capturep -pe -S0 -E2 >$TMP || exit 1 +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +printf '\033[44m X\n X\n\033[49mx\033[42m \033[43m \033[49m\n' | + cmp - $TMP || exit 1 + +exit 0 diff --git a/screen-write.c b/screen-write.c index 8fb795635..edb93866a 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.285 2026/07/26 09:02:08 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2497,15 +2497,62 @@ screen_write_collect_insert_clear(struct screen_write_ctx *ctx, u_int px, } } +/* + * Clear a cell that is being broken up by a write over part of it, keeping the + * background so the columns it covered do not lose their colour. + */ +static void +screen_write_clear_cell(struct grid *gd, u_int px, u_int py) +{ + struct grid_cell gc; + int bg; + + grid_view_get_cell(gd, px, py, &gc); + bg = gc.bg; + + memcpy(&gc, &grid_default_cell, sizeof gc); + gc.bg = bg; + grid_view_set_cell(gd, px, py, &gc); +} + +/* + * Insert clears for a range of cells already cleared in the grid. Adjacent + * cells may have come from different characters and so have different + * backgrounds, so this is done in runs of the same colour. + */ +static void +screen_write_insert_clears(struct screen_write_ctx *ctx, u_int px, u_int nx) +{ + struct screen *s = ctx->s; + struct grid_cell gc; + u_int xx, start = px, n; + int bg = 8; + + for (xx = px; xx < px + nx; xx++) { + grid_view_get_cell(s->grid, xx, s->cy, &gc); + if (xx == start) + bg = gc.bg; + else if (gc.bg != bg) { + n = xx - start; + log_debug("%s: from %u, size %u", __func__, start, n); + screen_write_collect_insert_clear(ctx, start, n, bg); + start = xx; + bg = gc.bg; + } + } + log_debug("%s: from %u, size %u", __func__, start, xx - start); + screen_write_collect_insert_clear(ctx, start, xx - start, bg); +} + /* Finish and store collected cells. */ void screen_write_collect_end(struct screen_write_ctx *ctx) { struct screen *s = ctx->s; - struct screen_write_citem *ci = ctx->item, *bci = NULL, *aci; + struct screen_write_citem *ci = ctx->item; struct screen_write_cline *cl = &s->write_list[s->cy]; struct grid_cell gc; - u_int xx; + u_int xx, bx = 0, bnx = 0; if (ci->used == 0) return; @@ -2521,8 +2568,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - grid_view_set_cell(s->grid, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(s->grid, xx, s->cy); log_debug("%s: padding erased (before) at %u (cx %u)", __func__, xx, s->cx); } @@ -2531,20 +2577,12 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, 0, s->cy, &gc); if (gc.data.width > 1 || (gc.flags & GRID_FLAG_PADDING)) { - grid_view_set_cell(s->grid, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(s->grid, xx, s->cy); log_debug("%s: padding erased (before) at %u " "(cx %u)", __func__, xx, s->cx); } - } - if (xx != s->cx) { - bci = ctx->item; - bci->type = CLEAR; - bci->x = xx; - bci->bg = 8; - bci->used = s->cx - xx; - log_debug("%s: padding erased (before): from %u, " - "size %u", __func__, bci->x, bci->used); + bx = xx; + bnx = s->cx - xx; } } @@ -2555,28 +2593,20 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, ci->used); - if (bci != NULL) - screen_write_collect_insert(ctx, bci); + if (bnx != 0) + screen_write_insert_clears(ctx, bx, bnx); screen_write_set_cursor(ctx, s->cx + ci->used, -1); for (xx = s->cx; xx < screen_size_x(s); xx++) { grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); - log_debug("%s: padding erased (after) at %u (cx %u)", - __func__, xx, s->cx); - } - if (xx != s->cx) { - aci = ctx->item; - aci->type = CLEAR; - aci->x = s->cx; - aci->bg = 8; - aci->used = xx - s->cx; - log_debug("%s: padding erased (after): from %u, size %u", - __func__, aci->x, aci->used); - screen_write_collect_insert(ctx, aci); + screen_write_clear_cell(s->grid, xx, s->cy); + log_debug("%s: padding erased (after) at %u (cx %u)", __func__, + xx, s->cx); } + if (xx != s->cx) + screen_write_insert_clears(ctx, s->cx, xx - s->cx); } /* Write cell data, collecting if necessary. */ @@ -2704,7 +2734,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) */ for (xx = s->cx + 1; xx < s->cx + width; xx++) { log_debug("%s: new padding at %u,%u", __func__, xx, s->cy); - grid_view_set_padding(gd, xx, s->cy); + grid_view_set_padding(gd, xx, s->cy, gc->bg); skip = 0; } @@ -2916,7 +2946,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) /* Set the new cell. */ grid_view_set_cell(gd, cx - n, cy, &last); if (force_wide) - grid_view_set_padding(gd, cx - 1, cy); + grid_view_set_padding(gd, cx - 1, cy, last.bg); /* * Check if all of this character is visible. No character will be @@ -2985,12 +3015,12 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, if (~tmp_gc.flags & GRID_FLAG_PADDING) break; log_debug("%s: padding at %u,%u", __func__, xx, s->cy); - grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); } /* Overwrite the character at the start of this padding. */ log_debug("%s: character at %u,%u", __func__, xx, s->cy); - grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); done = 1; } @@ -3008,17 +3038,7 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, break; log_debug("%s: overwrite at %u,%u", __func__, xx, s->cy); - if (gc->flags & GRID_FLAG_TAB) { - memcpy(&tmp_gc, gc, sizeof tmp_gc); - memset(tmp_gc.data.data, 0, - sizeof tmp_gc.data.data); - *tmp_gc.data.data = ' '; - tmp_gc.data.width = tmp_gc.data.size = - tmp_gc.data.have = 1; - grid_view_set_cell(gd, xx, s->cy, &tmp_gc); - } else - grid_view_set_cell(gd, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); done = 1; } } diff --git a/server-client.c b/server-client.c index e5e0dacee..1297f531c 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.500 2026/07/28 13:17:45 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.502 2026/08/04 11:18:22 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -36,7 +36,8 @@ static void server_client_check_window_resize(struct window *); static key_code server_client_check_mouse(struct client *, struct key_event *); static void server_client_repeat_timer(int, short, void *); static void server_client_click_timer(int, short, void *); -static void server_client_check_exit(struct client *); +static void server_client_check_exit(struct client *, int); +static void server_client_exit_timer(int, short, void *); static void server_client_check_redraw(struct client *); static void server_client_check_modes(struct client *); static void server_client_set_title(struct client *); @@ -309,6 +310,7 @@ server_client_create(int fd) evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c); + evtimer_set(&c->exit_timer, server_client_exit_timer, c); c->click_wp = -1; @@ -525,6 +527,7 @@ server_client_lost(struct client *c) evtimer_del(&c->repeat_timer); evtimer_del(&c->click_timer); + evtimer_del(&c->exit_timer); if (event_initialized(&c->cycle_timer)) evtimer_del(&c->cycle_timer); @@ -1849,7 +1852,7 @@ server_client_loop(void) /* Check clients. */ TAILQ_FOREACH(c, &clients, entry) { - server_client_check_exit(c); + server_client_check_exit(c, 0); if (c->session != NULL && c->session->curw != NULL) { server_client_check_modes(c); server_client_check_redraw(c); @@ -2285,9 +2288,37 @@ server_client_click_timer(__unused int fd, __unused short events, void *data) c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK); } -/* Check if client should be exited. */ +/* Start client exit timer. */ static void -server_client_check_exit(struct client *c) +server_client_start_exit_timer(struct client *c) +{ + struct timeval tv = { .tv_sec = 10 }; + + if (!evtimer_pending(&c->exit_timer, NULL)) + evtimer_add(&c->exit_timer, &tv); +} + +/* Exit timer has expired: stop waiting for the client. */ +static void +server_client_exit_timer(__unused int fd, __unused short events, void *data) +{ + struct client *c = data; + + if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED)) + return; + + if (c->flags & CLIENT_EXITED) { + log_debug("%s: %s took too long to exit", __func__, c->name); + server_client_lost(c); + } else if (c->flags & CLIENT_EXIT) { + log_debug("%s: %s took too long to flush", __func__, c->name); + server_client_check_exit(c, 1); + } +} + +/* Check if client should be exited, abandoning buffered output if forced. */ +static void +server_client_check_exit(struct client *c, int force) { struct client_file *cf; const char *name = c->exit_session; @@ -2300,16 +2331,29 @@ server_client_check_exit(struct client *c) return; if (c->flags & CLIENT_CONTROL) { - control_discard(c); - if (!control_all_done(c)) - return; + if (force) + control_discard_all(c); + else { + control_discard(c); + if (!control_all_done(c)) { + server_client_start_exit_timer(c); + return; + } + } } - RB_FOREACH(cf, client_files, &c->files) { - if (EVBUFFER_LENGTH(cf->buffer) != 0) - return; + if (!force) { + RB_FOREACH(cf, client_files, &c->files) { + if (EVBUFFER_LENGTH(cf->buffer) != 0) { + server_client_start_exit_timer(c); + return; + } + } } c->flags |= CLIENT_EXITED; + evtimer_del(&c->exit_timer); + server_client_start_exit_timer(c); + switch (c->exit_type) { case CLIENT_EXIT_RETURN: if (c->exit_message != NULL) diff --git a/server.c b/server.c index 4142272ae..7f9b203c5 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -240,6 +240,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, if (cause != NULL) { if (c != NULL) { c->exit_message = cause; + c->retval = 1; c->flags |= CLIENT_EXIT; } else { fprintf(stderr, "%s\n", cause); @@ -398,6 +399,7 @@ server_accept(int fd, short events, __unused void *data) c = server_client_create(newfd); if (!server_acl_join(c)) { c->exit_message = xstrdup("access not allowed"); + c->retval = 1; c->flags |= CLIENT_EXIT; } } diff --git a/session.c b/session.c index d52b70b88..491f48904 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: session.c,v 1.107 2026/08/05 07:35:35 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -709,8 +709,10 @@ session_group_synchronize1(struct session *target, struct session *s) /* Fix up the current window. */ if (s->curw != NULL) s->curw = winlink_find_by_index(&s->windows, s->curw->idx); - else + else if (target->curw != NULL) s->curw = winlink_find_by_index(&s->windows, target->curw->idx); + if (s->curw == NULL) + s->curw = RB_MIN(winlinks, &s->windows); /* Fix up the last window stack. */ memcpy(&old_lastw, &s->lastw, sizeof old_lastw); diff --git a/tmux.1 b/tmux.1 index 7785a84b4..ee88a1fb0 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1152 2026/07/29 08:58:33 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 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: July 29 2026 $ +.Dd $Mdocdate: August 6 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -356,6 +356,8 @@ Force redraw of the attached client. Select a new session for the attached client interactively. .It t Show the time. +.It T +Change the current pane title. .It w Choose the current window interactively. .It x @@ -2341,10 +2343,14 @@ the bottom and is paused while a selection is in progress. .Xc Turn off automatic refresh of the content from the pane. .It Xo -.Ic refresh\-toggle +.Ic refresh\-now (vi: r) (emacs: r) .Xc +Refresh the content from the pane once. +.It Xo +.Ic refresh\-toggle +.Xc Toggle automatic refresh of the content from the pane. .It Xo .Ic scroll\-bottom @@ -7426,6 +7432,7 @@ The following variables are available, where appropriate: .It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" .It Li "pane_pipe_pid" Ta "" Ta "PID of pipe process, if any" +.It Li "pane_private_modes" Ta "" Ta "List of enabled DECSET private modes" .It Li "pane_pb_state" Ta "" Ta "Pane progress bar state, one of hidden, normal, error, indeterminate, paused (can be set by application)" .It Li "pane_pb_progress" Ta "" Ta "Pane progress bar progress percentage (can be set by application)" .It Li "pane_right" Ta "" Ta "Right of pane" diff --git a/tmux.h b/tmux.h index 4eff98c1e..1bf54c6cc 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1416 2026/07/27 19:15:58 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1197,6 +1197,8 @@ struct window_mode { int flags; #define WINDOW_MODE_HIDE_PANE_STATUS 0x1 #define WINDOW_MODE_NO_STACK 0x2 +#define WINDOW_MODE_FILL_WINDOW 0x4 +#define WINDOW_MODE_HIDE_SCROLLBARS 0x8 struct screen *(*init)(struct window_mode_entry *, struct cmdq_item *, struct cmd_find_state *, @@ -1732,6 +1734,7 @@ struct tty_term { #define TERM_RGBCOLOURS 0x10 #define TERM_VT100LIKE 0x20 #define TERM_SIXEL 0x40 +#define TERM_INVALIDMS 0x80 int flags; LIST_ENTRY(tty_term) entry; @@ -2217,6 +2220,8 @@ struct client { struct event click_timer; int click_loc; int click_wp; + + struct event exit_timer; u_int click_button; struct mouse_event click_event; @@ -3442,7 +3447,7 @@ void grid_clear_history(struct grid *); const struct grid_line *grid_peek_line(struct grid *, u_int); void grid_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); -void grid_set_padding(struct grid *, u_int, u_int); +void grid_set_padding(struct grid *, u_int, u_int, int); void grid_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, const char *, size_t); struct grid_line *grid_get_line(struct grid *, u_int); @@ -3459,6 +3464,7 @@ void grid_reflow(struct grid *, u_int); void grid_wrap_position(struct grid *, u_int, u_int, u_int *, u_int *); void grid_unwrap_position(struct grid *, u_int *, u_int *, u_int, u_int); u_int grid_line_length(struct grid *, u_int); +u_int grid_line_limit(struct grid *, u_int); int grid_in_set(struct grid *, u_int, u_int, const char *); /* grid-reader.c */ @@ -3486,7 +3492,7 @@ void grid_reader_cursor_back_to_indentation(struct grid_reader *); void grid_view_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_view_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); -void grid_view_set_padding(struct grid *, u_int, u_int); +void grid_view_set_padding(struct grid *, u_int, u_int, int); void grid_view_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, const char *, size_t); void grid_view_clear_history(struct grid *, u_int); @@ -3968,6 +3974,7 @@ time_t monitor_get_fire_time(struct monitor_set *, const char *); /* control.c */ void control_discard(struct client *); +void control_discard_all(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); @@ -3983,6 +3990,8 @@ struct window_pane_offset *control_pane_offset(struct client *, struct window_pane *, int *); void control_reset_offsets(struct client *); void printflike(2, 3) control_write(struct client *, const char *, ...); +void printflike(2, 3) control_notify_write(struct client *, const char *, ...); +void control_write_guard(struct client *, const char *, long, u_int, int); void control_write_output(struct client *, struct window_pane *); int control_all_done(struct client *); void control_add_sub(struct client *, const char *, enum monitor_type, int, diff --git a/tty-term.c b/tty-term.c index 164b9f977..850adf31c 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.107 2026/08/05 08:54:56 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -31,6 +31,7 @@ #include "tmux.h" static char *tty_term_strip(const char *); +static void tty_term_validate(struct tty_term *); struct tty_terms tty_terms = LIST_HEAD_INITIALIZER(tty_terms); @@ -519,6 +520,26 @@ tty_term_apply_overrides(struct tty_term *term) acs = "a#j+k+l+m+n+o-p-q-r-s-t+u+v+w+x|y~."; for (; acs[0] != '\0' && acs[1] != '\0'; acs += 2) term->acs[(u_char) acs[0]][0] = acs[1]; + + tty_term_validate(term); +} + +static void +tty_term_validate(struct tty_term *term) +{ + struct tty_code *code = &term->codes[TTYC_MS]; + + if (code->type != TTYCODE_STRING) + return; + if (*tty_term_string_ss(term, TTYC_MS, "c", "?") != '\0') { + term->flags &= ~TERM_INVALIDMS; + return; + } + + log_debug("removing invalid Ms capability"); + term->flags |= TERM_INVALIDMS; + free(code->value.string); + code->type = TTYCODE_NONE; } struct tty_term * diff --git a/window-client.c b/window-client.c index b94c22119..f42aa5b7a 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.47 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -113,14 +113,16 @@ static const char *window_client_info_lines[] = { "#{?#{I/c:kmous},,#[align=right]unavailable: [kmous] missing}", "#[fg=themelightgrey]set-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " - "#{?#{!=:#{set-clipboard},off},#{?#{I/f:clipboard},," + "#{?#{!=:#{set-clipboard},off},#{?#{I/c:Ms},," "#[fg=themered]}#{set-clipboard},#[fg=themelightgrey]off} " - "#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", + "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] " + "#{?clipboard_invalid,invalid,missing}}", "#[fg=themelightgrey]get-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " - "#{?#{!=:#{get-clipboard},off},#{?#{I/f:clipboard},," + "#{?#{!=:#{get-clipboard},off},#{?#{I/c:Ms},," "#[fg=themered]}#{get-clipboard},#[fg=themelightgrey]off} " - "#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", + "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] " + "#{?clipboard_invalid,invalid,missing}}", "#[fg=themelightgrey]focus-events #[#{E:tree-mode-border-style},acs]x#[default] " "#{?focus-events,#{?#{I/f:focus},,#[fg=themered]}on,#[fg=themelightgrey]off} " @@ -273,6 +275,10 @@ window_client_draw_info(__unused void *modedata, void *itemdata, char *expanded; ft = format_create_defaults(NULL, c, NULL, NULL, NULL); + if (c->tty.term->flags & TERM_INVALIDMS) + format_add(ft, "clipboard_invalid", "1"); + else + format_add(ft, "clipboard_invalid", "0"); screen_write_cursormove(ctx, cx, cy, 0); for (i = 0; i < nitems(window_client_info_lines); i++) { diff --git a/window-copy.c b/window-copy.c index ff58dfbad..716a0f037 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.423 2026/07/21 11:52:13 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -54,6 +54,7 @@ static void window_copy_redraw_screen(struct window_mode_entry *); static void window_copy_do_refresh(struct window_mode_entry *, int); static void window_copy_refresh_timer(int, short, void *); static void window_copy_refresh_arm(struct window_mode_entry *); +static int window_copy_refresh_allowed(struct window_mode_entry *); static void window_copy_refresh_start(struct window_mode_entry *); static void window_copy_refresh_stop(struct window_mode_entry *); static void window_copy_style_changed(struct window_mode_entry *); @@ -3036,6 +3037,20 @@ window_copy_refresh_arm(struct window_mode_entry *wme) evtimer_add(&data->refresh_timer, &tv); } +static int +window_copy_refresh_allowed(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + /* + * Do not refresh a view of another pane (copy-mode -s): the source may + * disappear and changes are not tracked on this pane. + */ + if (data->viewmode || wme->swp != wme->wp) + return (0); + return (1); +} + static void window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) { @@ -3070,11 +3085,7 @@ window_copy_refresh_start(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; - /* - * Do not refresh a view of another pane (copy-mode -s): the source may - * disappear and changes are not tracked on this pane. - */ - if (data->viewmode || wme->swp != wme->wp || data->refresh_active) + if (!window_copy_refresh_allowed(wme) || data->refresh_active) return; data->refresh_active = 1; window_copy_refresh_arm(wme); @@ -3089,6 +3100,25 @@ window_copy_refresh_stop(struct window_mode_entry *wme) evtimer_del(&data->refresh_timer); } +static enum window_copy_cmd_action +window_copy_cmd_refresh_now(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + struct window_pane *wp = wme->wp; + int follow; + + if (!window_copy_refresh_allowed(wme)) + return (WINDOW_COPY_CMD_NOTHING); + + follow = (data->oy == 0 && + data->cy == screen_size_y(&data->screen) - 1); + window_copy_do_refresh(wme, follow); + wp->flags &= ~PANE_UNSEENCHANGES; + + return (WINDOW_COPY_CMD_REDRAW); +} + static enum window_copy_cmd_action window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) { @@ -3631,6 +3661,12 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_NEVER, .f = window_copy_cmd_refresh_off }, + { .command = "refresh-now", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_refresh_now + }, { .command = "refresh-toggle", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -6215,16 +6251,13 @@ static u_int window_copy_cursor_limit(struct window_mode_entry *wme, u_int py, int allow_onemore) { + struct window_copy_mode_data *data = wme->data; struct options *oo = wme->wp->window->options; - u_int len; - len = window_copy_find_length(wme, py); if (allow_onemore || options_get_number(oo, "mode-keys") != MODEKEY_VI) - return (len); - if (len == 0) - return (0); - return (len - 1); + return (window_copy_find_length(wme, py)); + return (grid_line_limit(data->backing->grid, py)); } static void diff --git a/window-panes.c b/window-panes.c index 68ee84cea..396d495eb 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.3 2026/07/19 19:53:11 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -35,7 +35,8 @@ static void window_panes_key(struct window_mode_entry *, const struct window_mode window_panes_mode = { .name = "panes-mode", - .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK, + .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK| + WINDOW_MODE_FILL_WINDOW|WINDOW_MODE_HIDE_SCROLLBARS, .init = window_panes_init, .free = window_panes_free, diff --git a/window.c b/window.c index 3a2e21ed7..3680ad80f 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.368 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2460,11 +2460,21 @@ window_pane_mode(struct window_pane *wp) int window_pane_show_scrollbar(struct window_pane *wp) { + struct window *w = wp->window; + struct window_mode_entry *wme; + if (SCREEN_IS_ALTERNATE(&wp->base)) return (0); - if (wp->window->sb == PANE_SCROLLBARS_ALWAYS || - wp->window->sb == PANE_SCROLLBARS_AUTOHIDE || - (wp->window->sb == PANE_SCROLLBARS_MODAL && + if ((w->flags & WINDOW_ZOOMED) && w->active != NULL) { + wme = TAILQ_FIRST(&w->active->modes); + if (wme != NULL && + (wme->mode->flags & WINDOW_MODE_HIDE_SCROLLBARS)) { + return (0); + } + } + if (w->sb == PANE_SCROLLBARS_ALWAYS || + w->sb == PANE_SCROLLBARS_AUTOHIDE || + (w->sb == PANE_SCROLLBARS_MODAL && window_pane_mode(wp) != WINDOW_PANE_NO_MODE)) return (1); return (0);