diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index e6765da99..902783e08 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -17,8 +17,9 @@ jobs: action: runs-on: ubuntu-latest if: github.repository == 'tmux/tmux' + timeout-minutes: 10 steps: - - uses: dessant/lock-threads@v6 + - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6 with: github-token: ${{ github.token }} issue-inactive-days: '30' diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index a84478357..b1e19e9e0 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -38,7 +38,9 @@ jobs: steps: - name: checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: dependencies if: runner.os == 'Linux' @@ -81,7 +83,7 @@ jobs: - name: logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: regress-logs-${{ matrix.name }} path: regress/logs/*.log diff --git a/arguments.c b/arguments.c index a69aca8c1..b1aa54eb8 100644 --- a/arguments.c +++ b/arguments.c @@ -1,4 +1,4 @@ -/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ +/* $OpenBSD: arguments.c,v 1.67 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index ffed25ef2..501793f09 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.76 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 2aaf3238c..0600c610c 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -591,6 +591,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) server_client_clear_overlay(tc); return (CMD_RETURN_NORMAL); } + if (tc->flags & CLIENT_CONTROL) + return (CMD_RETURN_NORMAL); if (!modify && tc->overlay_draw != NULL) return (CMD_RETURN_NORMAL); diff --git a/cmd-if-shell.c b/cmd-if-shell.c index f3ce9ba57..69468b8db 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ +/* $OpenBSD: cmd-if-shell.c,v 1.87 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-parse.y b/cmd-parse.y index 37955ed2d..31c743124 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ +/* $OpenBSD: cmd-parse.y,v 1.59 2026/08/31 07:51:56 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/cmd-queue.c b/cmd-queue.c index 3cf4bffa6..0b4c2315a 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -821,7 +821,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...) if (c == NULL) { cmd_get_source(cmd, &file, &line); - cfg_add_cause("%s:%u: %s", file, line, msg); + if (!cfg_finished) { + if (file != NULL) + cfg_add_cause("%s:%u: %s", file, line, msg); + else + cfg_add_cause("%s", msg); + } else { + if (file != NULL) { + server_add_message("message: %s:%u: %s", file, + line, msg); + } else + server_add_message("message: %s", msg); + } } else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) { server_add_message("%s message: %s", c->name, msg); if (~c->flags & CLIENT_UTF8) { diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 01ddf424a..fc5062ab4 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.68 2026/08/31 07:44:39 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -92,6 +92,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } server_unzoom_window(w); + lc = wp->layout_cell; /* may have been replaced by unzoom */ if (args_has(args, 'x')) { x = args_percentage(args, 'x', 0, PANE_MAXIMUM, w->sx, &cause); diff --git a/cmd-run-shell.c b/cmd-run-shell.c index cbc31763c..788bd6759 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-select-pane.c b/cmd-select-pane.c index dbf147cb5..e23ca0adc 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -160,6 +160,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) char *title; const char *style; struct options_entry *o; + int visible, Zflag = args_has(args, 'Z'); if (entry == &cmd_last_pane_entry || args_has(args, 'l')) { /* @@ -185,14 +186,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) server_redraw_window_borders(lastwp->window); server_status_window(lastwp->window); } else { - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && lastwp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(lastwp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, lastwp); if (window_set_active_pane(w, lastwp, 1)) { cmd_find_from_winlink(current, wl, 0); cmd_select_pane_redraw(w); } - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); } return (CMD_RETURN_NORMAL); @@ -268,14 +273,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (wp == w->active) return (CMD_RETURN_NORMAL); - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && wp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(wp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, wp); if (window_set_active_pane(w, wp, 1)) cmd_find_from_winlink_pane(current, wl, wp, 0); cmdq_insert_hook(s, item, current, "after-select-pane"); cmd_select_pane_redraw(w); - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); return (CMD_RETURN_NORMAL); diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 6e640fec6..3e540f706 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "%s", cause); goto fail; } + free(bufname); return (CMD_RETURN_NORMAL); } @@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "no data specified"); goto fail; } - if ((newsize = strlen(args_string(args, 0))) == 0) + if ((newsize = strlen(args_string(args, 0))) == 0) { + free(bufname); return (CMD_RETURN_NORMAL); + } if (args_has(args, 'a') && pb != NULL) { olddata = paste_buffer_data(pb, &bufsize); @@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'w') && tc != NULL) tty_set_selection(&tc->tty, "", bufdata, bufsize); + free(bufname); return (CMD_RETURN_NORMAL); fail: diff --git a/cmd-split-window.c b/cmd-split-window.c index c69b97a5c..c907dd152 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.149 2026/08/19 10:56:10 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.150 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bCdefhIkKLMOPvWZ] [-B border-lines] " + .args = { "AbB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-AbCdefhIkKLMOPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -89,6 +89,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state fs; struct key_event *event = cmdq_get_event(item); int input, empty, is_floating, flags = 0; + int restore_zoom = 0; const char *template, *style, *value; char *cause = NULL, *cp, *title; const struct options_table_entry *oe; @@ -96,10 +97,16 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) enum pane_lines lines; u_int count = args_count(args); + if (window_active_pane_is_over_zoom(w)) + restore_zoom = 1; + if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); else { - window_unzoom(w, 1); + if (!window_pane_is_visible(wp)) + restore_zoom = 0; + if (!restore_zoom) + window_unzoom(w, 1); is_floating = window_pane_is_floating(wp); flags |= SPAWN_SPLIT; } @@ -133,7 +140,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'Z')) flags |= SPAWN_ZOOM; if (args_has(args, 'O')) - flags |= SPAWN_MODAL; + flags |= SPAWN_MODAL|SPAWN_FLOATOVERZOOM; + if (is_floating && args_has(args, 'A')) + flags |= SPAWN_FLOATOVERZOOM; + if ((w->flags & WINDOW_ZOOMED) && (flags & SPAWN_FLOATOVERZOOM)) + restore_zoom = 1; input = args_has(args, 'I'); if (input || (count == 1 && *args_string(args, 0) == '\0')) @@ -169,6 +180,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (cause != NULL) { cmdq_error(item, "%s", cause); free(cause); + if (restore_zoom) + window_pop_zoom(w); return (CMD_RETURN_ERROR); } @@ -273,7 +286,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (~flags & SPAWN_DETACHED) cmd_find_from_winlink_pane(current, wl, new_wp, 0); - if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) { + if (restore_zoom) { + window_pop_zoom(wp->window); + server_redraw_window(wp->window); + } else if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) { window_pop_zoom(wp->window); server_redraw_window(wp->window); } @@ -324,8 +340,9 @@ fail: if (!is_floating) layout_close_pane(new_wp); window_remove_pane(wp->window, new_wp); - } else if (args_has(args, 'O')) - window_pop_modal_zoom(wp->window); + } + if (restore_zoom || (~flags & SPAWN_FLOATING)) + window_pop_zoom(wp->window); if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); environ_free(sc.environ); diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 177107b47..44ea43cbc 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */ +/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -53,7 +53,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state target; const char *tflag = args_get(args, 't'); enum cmd_find_type type; - int flags; + int flags, visible, Zflag = args_has(args, 'Z'); struct client *c = cmdq_get_client(item); struct client *tc = cmdq_get_target_client(item); struct session *s; @@ -139,11 +139,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); if (wl != NULL && wp != NULL && wp != wl->window->active) { w = wl->window; - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && wp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(wp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, wp); window_set_active_pane(w, wp, 1); - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); } if (wl != NULL) { diff --git a/format.c b/format.c index d88c29d66..78e3f0fb5 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1887,6 +1887,37 @@ format_cb_cursor_blinking(struct format_tree *ft) return (NULL); } +/* Callback for history_added. */ +static void * +format_cb_history_added(struct format_tree *ft) +{ + if (ft->wp != NULL) + return (format_printf("%u", ft->wp->base.grid->scroll_added)); + return (NULL); +} + +/* Callback for history_collected. */ +static void * +format_cb_history_collected(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_collected)); + return (NULL); +} + +/* Callback for history_generation. */ +static void * +format_cb_history_generation(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_generation)); + return (NULL); +} + /* Callback for history_limit. */ static void * format_cb_history_limit(struct format_tree *ft) @@ -2276,6 +2307,19 @@ format_cb_pane_last_output_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_output_generation. */ +static void * +format_cb_pane_output_generation(struct format_tree *ft) +{ + unsigned long long value; + + if (ft->wp != NULL) { + value = ft->wp->output_generation; + return (format_printf("%llu", value)); + } + return (NULL); +} + /* Callback for pane_last_prompt_time. */ static void * format_cb_pane_last_prompt_time(struct format_tree *ft) @@ -3663,12 +3707,21 @@ static const struct format_table_entry format_table[] = { { "cursor_y", FORMAT_TABLE_STRING, format_cb_cursor_y }, + { "history_added", FORMAT_TABLE_STRING, + format_cb_history_added + }, { "history_all_bytes", FORMAT_TABLE_STRING, format_cb_history_all_bytes }, { "history_bytes", FORMAT_TABLE_STRING, format_cb_history_bytes }, + { "history_collected", FORMAT_TABLE_STRING, + format_cb_history_collected + }, + { "history_generation", FORMAT_TABLE_STRING, + format_cb_history_generation + }, { "history_limit", FORMAT_TABLE_STRING, format_cb_history_limit }, @@ -3849,6 +3902,9 @@ static const struct format_table_entry format_table[] = { { "pane_mode", FORMAT_TABLE_STRING, format_cb_pane_mode }, + { "pane_output_generation", FORMAT_TABLE_STRING, + format_cb_pane_output_generation + }, { "pane_path", FORMAT_TABLE_STRING, format_cb_pane_path }, @@ -4387,7 +4443,7 @@ format_quote_shell(const char *s) at = out = xmalloc(strlen(s) * 2 + 1); for (cp = s; *cp != '\0'; cp++) { - if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL) + if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL) *at++ = '\\'; *at++ = *cp; } diff --git a/grid.c b/grid.c index 53c8c2cb0..424f5bea3 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1635,7 +1635,7 @@ grid_wrap_position(struct grid *gd, u_int px, u_int py, u_int *wx, u_int *wy) void grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) { - u_int yy, ay = 0; + u_int yy, ay = 0, ey = gd->hsize + gd->sy - 1; for (yy = 0; yy < gd->hsize + gd->sy - 1; yy++) { if (ay == wy) @@ -1649,7 +1649,7 @@ grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) * until we find the end or the line now containing wx. */ if (wx == UINT_MAX) { - while (gd->linedata[yy].flags & GRID_LINE_WRAPPED) + while (yy < ey && gd->linedata[yy].flags & GRID_LINE_WRAPPED) yy++; wx = gd->linedata[yy].cellused; } else { @@ -1711,21 +1711,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) { struct grid_cell gc, tmp_gc; u_int pxx; + int has_tab, has_space; + + has_tab = (strchr(set, '\t') != NULL); + has_space = (strchr(set, ' ') != NULL); grid_get_cell(gd, px, py, &gc); - if (strchr(set, '\t')) { - if (gc.flags & GRID_FLAG_PADDING) { - pxx = px; - do - grid_get_cell(gd, --pxx, py, &tmp_gc); - while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); - if (tmp_gc.flags & GRID_FLAG_TAB) - return (tmp_gc.data.width - (px - pxx)); - } else if (gc.flags & GRID_FLAG_TAB) - return (gc.data.width); - } - if (gc.flags & GRID_FLAG_PADDING) + if (gc.flags & GRID_FLAG_PADDING) { + if (!has_tab && !has_space) + return (0); + pxx = px; + do + grid_get_cell(gd, --pxx, py, &tmp_gc); + while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); + if (((has_tab || has_space) && + (tmp_gc.flags & GRID_FLAG_TAB)) || + (has_space && utf8_has_whitespace(&tmp_gc.data))) + return (tmp_gc.data.width - (px - pxx)); return (0); + } + if ((has_tab || has_space) && (gc.flags & GRID_FLAG_TAB)) + return (gc.data.width); + if (has_space && utf8_has_whitespace(&gc.data)) + return (gc.data.width == 0 ? 1 : gc.data.width); return (utf8_cstrhas(set, &gc.data)); } diff --git a/image.c b/image.c index f0543dc4f..5613cf920 100644 --- a/image.c +++ b/image.c @@ -84,6 +84,11 @@ image_fallback(char **ret, u_int sx, u_int sy) char *buf, *label; u_int py, size, lsize; + if (sy == 0) { + *ret = xstrdup(""); + return; + } + /* Allocate first line. */ lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1; if (sx < lsize - 3) diff --git a/input.c b/input.c index 8c738a297..2fef558cb 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1049,6 +1049,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) if (len == 0) return; + wp->output_generation++; window_update_activity(wp->window); if (~wp->flags & PANE_ACTIVITY) { wp->flags |= PANE_ACTIVITY; diff --git a/layout.c b/layout.c index b8ba1c2ef..ac87f30e5 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.96 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -539,6 +539,10 @@ layout_resize_check(struct window *w, struct layout_cell *lc, status = window_get_pane_status(w); + /* Floating cells do not take space from the tiled layout. */ + if (!layout_cell_is_tiled(lc) && !layout_cell_has_tiled_child(lc)) + return (0); + if (lc->type == LAYOUT_WINDOWPANE) { /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { @@ -568,6 +572,9 @@ layout_resize_check(struct window *w, struct layout_cell *lc, /* Different type: minimum of available space in child cells. */ minimum = UINT_MAX; TAILQ_FOREACH(lcchild, &lc->cells, entry) { + if (!layout_cell_is_tiled(lcchild) && + !layout_cell_has_tiled_child(lcchild)) + continue; available = layout_resize_check(w, lcchild, type); if (available < minimum) minimum = available; @@ -661,7 +668,7 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc, /* Find and return the nearest neighbour to a cell in a specific direction. */ static struct layout_cell * -layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction) +layout_cell_get_neighbour_dir(struct layout_cell *lc, int direction) { struct layout_cell *lcn = lc; @@ -695,9 +702,9 @@ layout_cell_get_neighbour(struct layout_cell *lc) if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) direction = !direction; - lcother = layout_cell_get_neighbour_direction(lc, direction); + lcother = layout_cell_get_neighbour_dir(lc, direction); if (lcother == NULL) - lcother = layout_cell_get_neighbour_direction(lc, !direction); + lcother = layout_cell_get_neighbour_dir(lc, !direction); return (lcother); } @@ -860,7 +867,7 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type, size = lc->g.sx; else size = lc->g.sy; - if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) + if (layout_cell_is_last_tiled(lc)) change = size - new_size; else change = new_size - size; @@ -985,11 +992,11 @@ layout_resize_pane(struct window_pane *wp, enum layout_type type, int change, if (lcparent == NULL) return; - /* If this is the last cell, move back one. */ - if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) { - do - lc = TAILQ_PREV(lc, layout_cells, entry); - while (lc->flags & LAYOUT_CELL_FLOATING); + /* If this is the last tiled cell, move back one. */ + if (layout_cell_is_last_tiled(lc)) { + lc = layout_cell_get_neighbour_dir(lc, 0); + if (lc == NULL) + return; } layout_resize_layout(wp->window, lc, type, change, opposite); @@ -1007,22 +1014,22 @@ layout_resize_pane_grow(struct window *w, struct layout_cell *lc, lcadd = lc; /* Look towards the tail for a suitable cell for reduction. */ - lcremove = TAILQ_NEXT(lc, entry); + lcremove = layout_cell_get_neighbour_dir(lc, 1); while (lcremove != NULL) { size = layout_resize_check(w, lcremove, type); if (size > 0) break; - lcremove = TAILQ_NEXT(lcremove, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 1); } /* If none found, look towards the head. */ if (opposite && lcremove == NULL) { - lcremove = TAILQ_PREV(lc, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lc, 0); while (lcremove != NULL) { size = layout_resize_check(w, lcremove, type); if (size > 0) break; - lcremove = TAILQ_PREV(lcremove, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 0); } } if (lcremove == NULL) @@ -1050,13 +1057,13 @@ layout_resize_pane_shrink(struct window *w, struct layout_cell *lc, size = layout_resize_check(w, lcremove, type); if (size != 0) break; - lcremove = TAILQ_PREV(lcremove, layout_cells, entry); + lcremove = layout_cell_get_neighbour_dir(lcremove, 0); } while (lcremove != NULL); if (lcremove == NULL) return (0); /* And add onto the next cell (from the original cell). */ - lcadd = TAILQ_NEXT(lc, entry); + lcadd = layout_cell_get_neighbour_dir(lc, 1); if (lcadd == NULL) return (0); @@ -1650,7 +1657,10 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, return (NULL); } - window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); + if (window_active_pane_is_over_zoom(w)) + window_push_zoom(w, 0, 1); + else + window_push_zoom(w, 1, (flags & SPAWN_ZOOM)); lc = layout_split_pane(wp, type, size, flags); if (lc == NULL) *cause = xstrdup("no space for a new pane"); @@ -1677,8 +1687,10 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, return (NULL); } - if (flags & SPAWN_MODAL) - window_push_modal_zoom(w); + if (flags & SPAWN_FLOATOVERZOOM) + window_push_zoom(wp->window, 0, 1); + else if (window_active_pane_is_over_zoom(w)) + window_push_zoom(wp->window, 0, 1); else window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); lcnew = layout_floating_pane(w, wp, &fg); diff --git a/options-table.c b/options-table.c index 8fcb4d0b9..5d171b16f 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -305,6 +305,16 @@ const struct options_table_entry options_table[] = { "When this is reached, the oldest buffer is deleted." }, + { .name = "clear-on-attach", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SERVER, + .default_num = 1, + .text = "Whether to use the alternate screen and clear it when " + "a client is attached. When disabled, tmux does not " + "enter the alternate screen on attach so terminal " + "content before tmux remains in scrollback." + }, + { .name = "command-alias", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_SERVER, @@ -1240,7 +1250,8 @@ const struct options_table_entry options_table[] = { * underscore. */ .default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", - .text = "Characters considered to separate words." + .text = "Characters considered to separate words; a space matches " + "any character with the Unicode White_Space property." }, /* Window options. */ diff --git a/popup.c b/popup.c index 3bc7eafa6..0345fa367 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -79,7 +79,8 @@ popup_free(struct popup_data *pd) if (pd->job != NULL) job_free(pd->job); - input_free(pd->ictx); + if (pd->ictx != NULL) + input_free(pd->ictx); free(pd->r.ranges); screen_free(&pd->s); diff --git a/regress/buffers.sh b/regress/buffers.sh index fdacacb5b..18ae4439c 100644 --- a/regress/buffers.sh +++ b/regress/buffers.sh @@ -28,7 +28,7 @@ $TMUX kill-server 2>/dev/null TMP=$(mktemp) TMP2=$(mktemp) -trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 +trap 'rm -f "$TMP" "$TMP2" "$TMP".*; $TMUX kill-server 2>/dev/null' 0 1 15 # check_ok $cmd... # @@ -270,6 +270,27 @@ if [ $? -eq 0 ]; then exit 1 fi +# A write error after a successful open must be reported to the server. +dd if=/dev/zero of="$TMP.big" bs=100000 count=1 2>/dev/null +check_ok load-buffer -b write-error "$TMP.big" +mkfifo "$TMP.fifo" || exit 1 +dd if="$TMP.fifo" of="$TMP.got" bs=1 count=8 2>/dev/null & +reader=$! +exec 3>"$TMP.fifo" +out=$($TMUX save-buffer -b write-error "$TMP.fifo" 3>&- 2>&1) +status=$? +exec 3>&- +wait "$reader" +if [ $status -eq 0 ]; then + echo "save-buffer after write error succeeded." + exit 1 +fi +if [ "$(wc -c <"$TMP.got")" -ne 8 ]; then + echo "save-buffer write error test wrote wrong amount." + exit 1 +fi +check_ok delete-buffer -b write-error + # save-buffer - writes to stdout and load-buffer - reads from stdin. out=$($TMUX save-buffer -b sb -) if [ "$out" != "data" ]; then diff --git a/regress/copy-mode-unicode-whitespace.sh b/regress/copy-mode-unicode-whitespace.sh new file mode 100755 index 000000000..a532606a8 --- /dev/null +++ b/regress/copy-mode-unicode-whitespace.sh @@ -0,0 +1,163 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +fail() +{ + echo "$1" + [ -z "$TMUX" ] || $TMUX kill-server 2>/dev/null + exit 1 +} + +goto_cell() +{ + row=$1 + column=$2 + + $TMUX send-keys -X history-top || fail "$mode: history-top failed" + $TMUX send-keys -X start-of-line || fail "$mode: start-of-line failed" + [ "$row" -eq 0 ] || + $TMUX send-keys -X -N "$row" cursor-down || + fail "$mode: cursor-down failed" + [ "$column" -eq 0 ] || + $TMUX send-keys -X -N "$column" cursor-right || + fail "$mode: cursor-right failed" +} + +check_cursor() +{ + expected=$1 + actual=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}') + [ "$actual" = "$expected" ] || + fail "$mode: expected cursor $expected, got $actual" +} + +check_word() +{ + row=$1 + column=$2 + expected=$3 + + goto_cell "$row" "$column" + $TMUX set-buffer sentinel || fail "$mode: set-buffer failed" + $TMUX send-keys -X select-word || fail "$mode: select-word failed" + $TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed" + actual=$($TMUX show-buffer 2>/dev/null) + [ "$actual" = "$expected" ] || + fail "$mode: expected word '$expected', got '$actual'" +} + +check_next_word() +{ + row=$1 + expected=$2 + + goto_cell "$row" 0 + $TMUX send-keys -X next-word || fail "$mode: next-word failed" + $TMUX set-buffer sentinel || fail "$mode: set-buffer failed" + $TMUX send-keys -X select-word || fail "$mode: select-word failed" + $TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed" + actual=$($TMUX show-buffer 2>/dev/null) + [ "$actual" = "$expected" ] || + fail "$mode: expected next word '$expected', got '$actual'" +} + +for mode in emacs vi; do + TMUX="$TEST_TMUX -Lunicode-whitespace-$mode-$$ -f/dev/null" + $TMUX kill-server 2>/dev/null + $TMUX new-session -d -x16 -y20 \ + "printf 'aa bb\naa\tbb\naa\302\240bb\naa\343\200\200bb\naa:bb\naaaaaaaaaaaaaaa\302\240b\nabcdefghijklmnopq\nhardone\nhardtwo\naa\341\232\200bb\naa\342\200\200bb\naa\342\200\250bb\naa\342\200\251bb\naa\342\200\257bb\naa\342\201\237bb\nxa\302\205b c\n'; exec cat" || + fail "$mode: new-session failed" + $TMUX set-option -g window-size manual || fail "$mode: set size failed" + $TMUX set-window-option -g mode-keys "$mode" || + fail "$mode: set mode-keys failed" + $TMUX set-window-option -g word-separators "" || + fail "$mode: clear word-separators failed" + $TMUX copy-mode || fail "$mode: copy-mode failed" + + # ASCII space and TAB retain their existing behaviour. + goto_cell 0 0 + $TMUX send-keys -X next-space || fail "$mode: ASCII next-space failed" + check_cursor 3,0 + goto_cell 1 0 + $TMUX send-keys -X next-space || fail "$mode: TAB next-space failed" + check_cursor 8,1 + $TMUX send-keys -X previous-space || + fail "$mode: TAB previous-space failed" + check_cursor 0,1 + + # NBSP is whitespace for selection and word movement. + goto_cell 2 0 + $TMUX send-keys -X next-word || fail "$mode: NBSP next-word failed" + check_cursor 3,2 + $TMUX send-keys -X previous-word || + fail "$mode: NBSP previous-word failed" + check_cursor 0,2 + check_word 2 0 aa + goto_cell 2 0 + $TMUX send-keys -X next-word-end || + fail "$mode: NBSP next-word-end failed" + if [ "$mode" = emacs ]; then + check_cursor 2,2 + else + check_cursor 1,2 + fi + + # U+3000 is two columns wide; both its leading and padding cells must be + # skipped as one whitespace character in either direction. + goto_cell 3 0 + $TMUX send-keys -X next-word || fail "$mode: U+3000 next-word failed" + check_cursor 4,3 + $TMUX send-keys -X previous-word || + fail "$mode: U+3000 previous-word failed" + check_cursor 0,3 + goto_cell 3 0 + $TMUX send-keys -X next-space-end || + fail "$mode: U+3000 next-space-end failed" + if [ "$mode" = emacs ]; then + check_cursor 2,3 + else + check_cursor 1,3 + fi + + # Other characters in word-separators remain literal when space enables + # Unicode whitespace matching. + $TMUX set-window-option -g word-separators ': ' || + fail "$mode: set custom word-separators failed" + goto_cell 4 0 + $TMUX send-keys -X next-word || fail "$mode: literal next-word failed" + check_cursor 2,4 + $TMUX set-window-option -g word-separators "" || + fail "$mode: restore word-separators failed" + + # Whitespace in the last column remains a boundary across a soft wrap. + goto_cell 5 0 + $TMUX send-keys -X next-space || + fail "$mode: wrapped NBSP next-space failed" + check_cursor 0,6 + + # A word split only by a soft wrap remains one word, while a hard line + # boundary still terminates selection. + check_word 7 0 abcdefghijklmnopq + check_word 9 0 hardone + + # Check the remaining representative White_Space ranges and U+0085 + # combined into a cell with another character. + check_next_word 11 bb + check_next_word 12 bb + check_next_word 13 bb + check_next_word 14 bb + check_next_word 15 bb + check_next_word 16 bb + goto_cell 17 0 + $TMUX send-keys -X next-word || + fail "$mode: combined U+0085 next-word failed" + check_cursor 2,17 + + $TMUX kill-server 2>/dev/null +done + +exit 0 diff --git a/regress/floating-pane-geometry.sh b/regress/floating-pane-geometry.sh index 7e80e8942..b04344ff2 100644 --- a/regress/floating-pane-geometry.sh +++ b/regress/floating-pane-geometry.sh @@ -199,5 +199,74 @@ must_fail $TMUX resize-pane -t "$id" -x 0 must_fail $TMUX resize-pane -t "$id" -y 0 $TMUX kill-pane -t "$id" || exit 1 +# --- Tiled pane resize with floating cells in the layout --- + +$TMUX set-option -w -u pane-border-lines || exit 1 +base=$($TMUX display-message -p '#{pane_id}') + +# A floating cell after the last tiled cell must not become the recipient of a +# relative or absolute resize. This is the sequence from GitHub issue 5135. +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 50% -y 50% -X 50% -Y 50% 'sleep 100') || + fail "new-pane for tiled resize test failed" +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for tiled resize test failed" + +$TMUX resize-pane -t "$lower" -U 5 || fail "relative tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 13 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10 + +$TMUX resize-pane -t "$lower" -y 11 || fail "absolute tiled reset failed" +$TMUX resize-pane -t "$lower" -y 16 || fail "absolute tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + +# A floating cell between tiled siblings must be skipped when finding the cell +# which donates space to a resize. +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for middle floating cell test failed" +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 20 -y 8 -X 30 -Y 8 'sleep 100') || + fail "new-pane for middle floating cell test failed" + +$TMUX resize-pane -t "$base" -D 3 || fail "resize past floating cell failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 6 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + +# A small floating child must not limit the available tiled space in a nested +# layout with a different split direction. +lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') || + fail "split-window for nested floating cell test failed" +right=$($TMUX split-window -dhPF '#{pane_id}' -t "$lower" 'sleep 100') || + fail "horizontal split for nested floating cell test failed" +floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$lower" \ + -x 20 -y 3 -X 30 -Y 10 'sleep 100') || + fail "new-pane for nested floating cell test failed" + +$TMUX resize-pane -t "$base" -D 3 || fail "nested tiled resize failed" +must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$right" '#{pane_top}')" 16 +must_equal "$($TMUX display-message -p -t "$right" '#{pane_height}')" 8 +must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 1 + +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX kill-pane -t "$right" || exit 1 +$TMUX kill-pane -t "$lower" || exit 1 + $TMUX kill-server 2>/dev/null exit 0 diff --git a/regress/modal-pane.sh b/regress/modal-pane.sh index d02a75683..b4217aa0a 100644 --- a/regress/modal-pane.sh +++ b/regress/modal-pane.sh @@ -145,12 +145,16 @@ case "$(fmt "$modal" '#{pane_flags}')" in *O*) ;; *) fail "modal pane flags do not include O" ;; esac +case "$(fmt "$modal" '#{pane_flags}')" in +*A*) ;; +*) fail "modal pane flags do not include A" ;; +esac case "$(fmt modal:0 '#{window_flags}')" in *O*) ;; *) fail "modal window flags do not include O" ;; esac must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal" -must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 0 +must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1 check_fail "window already has a modal pane" \ new-pane -O -x 10 -y 4 'cat' @@ -177,12 +181,14 @@ under=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') || sleep 1 must_equal "$(fmt modal:0 '#{pane_id}')" "$modal" must_equal "$(fmt "$under" '#{pane_active}')" 0 +must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1 float=$($TMUX new-pane -PF '#{pane_id}' -x 10 -y 4 -X 5 -Y 3 'cat') || fail "new floating pane under modal failed" sleep 1 must_equal "$(fmt modal:0 '#{pane_id}')" "$modal" must_equal "$(fmt "$float" '#{pane_active}')" 0 +must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1 check_ok new-window -d -t modal: -n other 'cat' check_ok select-window -t modal:other @@ -321,6 +327,7 @@ check_ok kill-pane -t "$modal" sleep 1 must_equal "$(fmt modal:0 '#{window_modal_pane}')" '' must_equal "$(fmt modal:0 '#{pane_id}')" "$p0" +must_equal "$(fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 0:0 ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') || fail "new-pane -K without -O failed" @@ -352,5 +359,132 @@ new_top=$(fmt "$modal" '#{pane_top}') check_ok kill-pane -t "$modal" sleep 1 +# A nonmodal floating pane may remain above zoom, and switching between it and +# the zoomed tiled pane must not unzoom the window. +check_ok new-window -d -t modal: -n float-over-zoom 'cat' +base=$(fmt modal:float-over-zoom '#{pane_id}') +check_ok split-window -dh -t "$base" 'cat' +check_ok resize-pane -Z -t "$base" +over=$($TMUX new-pane -APF '#{pane_id}' -t "$base" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "new-pane -A failed" +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1 +case "$(fmt "$over" '#{pane_flags}')" in +*A*) ;; +*) fail "float-over-zoom pane flags do not include A" ;; +esac +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 +check_ok select-pane -t "$base" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok select-pane -t "$over" +must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +client=$($TMUX list-clients -F '#{client_name}' | head -1) +[ -n "$client" ] || fail "no client for switch-client test" +check_ok switch-client -c "$client" -t "$base" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok switch-client -c "$client" -t "$over" +must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok kill-pane -t "$over" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 +check_ok resize-pane -Z -t "$base" + +ignored=$($TMUX new-pane -ALdPF '#{pane_id}' -t "$base" 'cat') || + fail "new-pane -A -L failed" +case "$(fmt "$ignored" '#{pane_flags}')" in +*A*) fail "tiled pane flags include A" ;; +*) ;; +esac + +# Existing floating panes are filtered when zoom begins: -A panes remain in the +# visible layout and ordinary floating panes do not. +check_ok new-window -d -t modal: -n existing-over-zoom 'cat' +base=$(fmt modal:existing-over-zoom '#{pane_id}') +check_ok split-window -dh -t "$base" 'cat' +over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "pre-existing new-pane -A failed" +under=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 15 -y 4 -X 2 -Y 2 'cat') || + fail "pre-existing ordinary new-pane failed" +check_ok select-window -t modal:existing-over-zoom +check_ok select-pane -t "$base" +check_ok resize-pane -Z -t "$base" +must_equal "$(fmt "$over" '#{pane_floating_flag}')" 1 +must_equal "$(fmt "$under" '#{pane_floating_flag}')" 0 +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 + +# Geometry changed in the visible zoom layout is copied back when unzooming. +check_ok select-pane -t "$over" +left=$(fmt "$over" '#{pane_left}') +top=$(fmt "$over" '#{pane_top}') +meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4)) +new_left=$(fmt "$over" '#{pane_left}') +new_top=$(fmt "$over" '#{pane_top}') +[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] || + fail "float-over-zoom pane did not move" +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 +check_ok resize-pane -Z -t "$base" +must_equal "$(fmt "$over" '#{pane_left}:#{pane_top}')" \ + "$new_left:$new_top" +must_equal "$(fmt "$under" '#{pane_floating_flag}')" 1 + +# Resizing with the over-zoom pane active restores the original zoom target. +check_ok resize-pane -Z -t "$base" +check_ok select-pane -t "$over" +check_ok resize-window -t modal:existing-over-zoom -x 90 -y 30 +must_equal "$(fmt "$base" \ + '#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}')" \ + 90x30:1:1 +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1 + +# Natural pane exit uses a different removal path from kill-pane and must also +# preserve zoom. +dying=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \ + -x 12 -y 4 -X 4 -Y 3 'true') || + fail "short-lived new-pane -A failed" +i=0 +while $TMUX list-panes -a -F '#{pane_id}' | grep -qx "$dying"; do + i=$((i + 1)) + [ $i -gt 50 ] && fail "short-lived float-over-zoom pane did not exit" + sleep 0.1 +done +must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1 + +# A pane with -A is also above a zoom target which was itself floating. The +# temporary tiled target must sit behind retained floating panes, then return +# to the normal floating z order when unzoomed. +check_ok new-window -d -t modal: -n floating-zoom-target 'cat' +base=$(fmt modal:floating-zoom-target '#{pane_id}') +check_ok split-window -dh -t "$base" 'cat' +target=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \ + -x 30 -y 10 -X 10 -Y 5 'cat') || + fail "floating zoom target creation failed" +over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \ + -x 15 -y 5 -X 15 -Y 8 'cat') || + fail "float-over-zoom pane creation failed" +check_ok select-window -t modal:floating-zoom-target +check_ok select-pane -t "$target" +must_equal "$(fmt "$target" '#{pane_z}')" 0 +must_equal "$(fmt "$over" '#{pane_z}')" 1 + +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$target" \ + '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_floating_flag}:#{pane_z}')" \ + 1:1:0:2 +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0 +check_ok select-pane -t "$over" +must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1 +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$over" '#{pane_active}:#{pane_z}')" 1:0 +must_equal "$(fmt "$target" '#{pane_floating_flag}:#{pane_z}')" 1:1 + +# If the target remains active, it returns to the front on unzoom. +check_ok select-pane -t "$target" +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0 +check_ok resize-pane -Z -t "$target" +must_equal "$(fmt "$target" '#{pane_active}:#{pane_z}')" 1:0 +must_equal "$(fmt "$over" '#{pane_z}')" 1 + cleanup exit 0 diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 41a6a2a45..c69362cee 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -19,7 +19,10 @@ # - respawn-pane/respawn-window refusing a live pane without -k, working on a # dead pane (remain-on-exit) and killing with -k; # - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z -# zoom/unzoom (including implicit unzoom on split). +# zoom/unzoom, layout round trips, zoom target selection, pane removal, +# window resizing and implicit unzoom on layout changes; +# - swap-pane preserving one or two zoomed windows, including cross-window +# swaps of hidden panes and zoom targets. # # window-ops.sh covers window-level commands and buffers.sh paste buffers. @@ -144,7 +147,8 @@ check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" # break-pane and join-pane. # break-pane moves a pane to a new window; -P -F prints where it went and -n -# names the new window. +# names the new window. Moving a pane out of a zoomed window unzooms it first. +check_ok resize-pane -Z -t "$p0" out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \ -s "$p1" -t P:) if [ "$out" != "1:$p1" ]; then @@ -153,10 +157,14 @@ if [ "$out" != "1:$p1" ]; then fi check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1' check_fmt 'P:0' '#{window_panes}' '3' +check_fmt 'P:0' '#{window_zoomed_flag}' '0' -# join-pane -v moves it back (the source window, left empty, is destroyed). +# join-pane -v moves it back (the source window, left empty, is destroyed) and +# also unzooms the destination before changing its layout. +check_ok resize-pane -Z -t "$p0" check_ok join-pane -d -v -s P:broken.0 -t "$p2" check_fmt 'P:0' '#{window_panes}' '4' +check_fmt 'P:0' '#{window_zoomed_flag}' '0' if $TMUX has-session -t P:broken 2>/dev/null; then echo "Window 'broken' still exists after join-pane." exit 1 @@ -299,6 +307,122 @@ check_fmt 'P:0' '#{window_panes}' '5' p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}') check_ok kill-pane -t "$p6" +# Zoom and unzoom preserve the exact tiled layout. Selecting another pane +# without -Z unzooms, while -Z transfers zoom to the selected pane. +layout=$($TMUX display-message -p -t P:0 '#{window_layout}') +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -t "$p2" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1' +check_fmt P:0 '#{window_layout}' "$layout" + +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -Z -t "$p2" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p2" +check_fmt P:0 '#{window_layout}' "$layout" + +# Directional selection temporarily restores the full layout to find its +# neighbour, then follows the same unzoom or -Z transfer rules. +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -t "$p0" +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok select-pane -D -t "$p0" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1' + +check_ok select-pane -t "$p0" +check_ok resize-pane -Z -t "$p0" +check_ok select-pane -D -Z -t "$p0" +check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p2" +check_fmt P:0 '#{window_layout}' "$layout" + +# The last-pane path has separate zoom handling, both with and without -Z. +check_ok select-pane -t "$p0" +check_ok select-pane -t "$p2" +check_ok resize-pane -Z -t "$p2" +check_ok select-pane -l -t P:0 +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '0:0:1' + +check_ok select-pane -t "$p0" +check_ok select-pane -t "$p2" +check_ok resize-pane -Z -t "$p2" +check_ok select-pane -l -Z -t P:0 +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p0" +check_fmt P:0 '#{window_layout}' "$layout" + +# Killing either a hidden ordinary pane or the zoom target unzooms. +check_ok new-window -d -t P:12 -n zoom-kill 'cat' +zk0=$($TMUX display-message -p -t P:12.0 '#{pane_id}') +zk1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat') +zk2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat') +check_ok resize-pane -Z -t "$zk0" +check_ok kill-pane -t "$zk1" +check_fmt "$zk0" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '2:0:0' +check_ok resize-pane -Z -t "$zk0" +check_ok kill-pane -t "$zk0" +check_fmt "$zk2" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '1:0:0' +check_ok kill-window -t P:12 + +# The same cases through natural process exit exercise server_destroy_pane. +check_ok new-window -d -t P:13 -n zoom-exit 'cat' +ze0=$($TMUX display-message -p -t P:13.0 '#{pane_id}') +ze1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat') +ze2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat') +check_ok resize-pane -Z -t "$ze0" +check_ok send-keys -t "$ze1" C-d +i=0 +while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 2 ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "Hidden pane did not exit."; exit 1; } + sleep 0.1 +done +check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0' +check_ok resize-pane -Z -t "$ze2" +check_ok send-keys -t "$ze2" C-d +i=0 +while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 1 ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "Zoomed pane did not exit."; exit 1; } + sleep 0.1 +done +check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0' +check_ok kill-window -t P:13 + +# resize-window restores both tiled and floating zoom targets after rebuilding +# the layout at the new size. +check_ok new-window -d -t P:14 -n zoom-resize 'cat' +zr0=$($TMUX display-message -p -t P:14.0 '#{pane_id}') +check_ok split-window -d -t P:14.0 'cat' +check_ok resize-pane -Z -t "$zr0" +check_ok resize-window -t P:14 -x 90 -y 30 +check_fmt "$zr0" \ + '#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '90x30:1:1' +check_ok resize-pane -Z -t "$zr0" +zrf=$($TMUX new-pane -dP -F '#{pane_id}' -t P:14 -x 20 -y 6 'cat') +zrf_size=$($TMUX display-message -p -t "$zrf" \ + '#{pane_width}x#{pane_height}') +check_ok resize-pane -Z -t "$zrf" +check_ok resize-window -t P:14 -x 100 -y 32 +check_fmt "$zrf" \ + '#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \ + '100x32:1:1' +check_ok resize-pane -Z -t "$zrf" +check_fmt "$zrf" '#{pane_floating_flag}:#{pane_width}x#{pane_height}' \ + "1:$zrf_size" +check_ok kill-window -t P:14 + # --------------------------------------------------------------------------- # kill-pane. @@ -307,8 +431,10 @@ check_ok kill-pane -t "$p3" check_panes P:0 "0:$p0 1:$p2 2:$p1" # -a kills every pane except the target. +check_ok resize-pane -Z -t "$p0" check_ok kill-pane -a -t "$p0" check_panes P:0 "0:$p0" +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0' # Killing the last pane in a window kills the window. check_ok new-window -d -t P:7 -n goner @@ -516,6 +642,10 @@ check_panes P:3 "0:$r0 1:$r1 2:$r2" # Swapping a pane with itself quietly does nothing. check_ok swap-pane -d -s "$r1" -t "$r1" check_panes P:3 "0:$r0 1:$r1 2:$r2" +check_ok resize-pane -Z -t "$r1" +check_ok swap-pane -d -Z -s "$r1" -t "$r1" +check_fmt "$r1" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1' +check_ok resize-pane -Z -t "$r1" # Panes can be swapped between different windows. check_ok swap-pane -d -s "$o0" -t "$r1" @@ -525,10 +655,38 @@ check_ok swap-pane -d -s "$r1" -t "$o0" check_panes P:3 "0:$r0 1:$r1 2:$r2" check_panes P:5 "0:$o0" +# With both windows zoomed, -Z preserves each window's zoom when hidden panes +# are exchanged and when the zoom targets themselves are exchanged. +o1=$($TMUX split-window -dP -F '#{pane_id}' -t P:5.0) +check_ok resize-pane -Z -t "$r0" +check_ok resize-pane -Z -t "$o0" +check_ok swap-pane -d -Z -s "$r1" -t "$o1" +check_fmt "$r0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1' +check_fmt "$o0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1' +check_fmt "$r1" '#{window_name}' 'other' +check_fmt "$o1" '#{window_name}' 'swaps' + +check_ok swap-pane -d -Z -s "$r0" -t "$o0" +check_fmt "$o0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1' +check_fmt "$r0" \ + '#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1' +check_ok resize-pane -Z -t "$o0" +check_ok resize-pane -Z -t "$r0" + +# Restore the original panes before the remaining swap tests. +check_ok swap-pane -d -s "$r0" -t "$o0" +check_ok swap-pane -d -s "$r1" -t "$o1" +check_ok kill-pane -t "$o1" +check_panes P:3 "0:$r0 1:$r1 2:$r2" +check_panes P:5 "0:$o0" + # -Z keeps the window zoomed across the swap. check_ok resize-pane -Z -t "$r0" check_ok swap-pane -d -Z -s "$r0" -t "$r1" -check_fmt 'P:3' '#{window_zoomed_flag}' '1' +check_fmt "$r0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1' check_ok resize-pane -Z -t P:3 check_panes P:3 "0:$r1 1:$r0 2:$r2" diff --git a/regress/tty-draw-line.sh b/regress/tty-draw-line.sh index 09a956312..aa33982c9 100644 --- a/regress/tty-draw-line.sh +++ b/regress/tty-draw-line.sh @@ -105,6 +105,21 @@ sleep 1 CLIENT=$($TMUX2 list-clients -F '#{client_name}' | head -1) [ -n "$CLIENT" ] || fail "no inner client" +# A variation selector which widens the previous character must use window +# coordinates when checking visibility in a pane with a nonzero x offset. +$TMUX2 set -s variation-selector-always-wide on || exit 1 +WINDOW=$($TMUX2 neww -dPF '#{window_id}' "exec sleep 100") || exit 1 +PANE=$($TMUX2 splitw -dhPF '#{pane_id}' -t "$WINDOW" \ + "exec sleep 100") || exit 1 +$TMUX2 selectw -t "$WINDOW" || exit 1 +$TMUX2 respawnp -k -t "$PANE" \ + "printf '\nA\342\234\217\357\270\217B'; exec sleep 100" || exit 1 +sleep 1 +$TMUX2 capturep -p -t "$PANE" >$TMP || exit 1 +EXPECTED=$(printf 'A\342\234\217\357\270\217B') +check_line 2 "$EXPECTED" +$TMUX2 killw -t "$WINDOW" || exit 1 + # Long line, then short line: default cells after cellsize must clear stale text. capture check_line 1 "abcdefghijklmnopqrst" diff --git a/regress/window-ops.sh b/regress/window-ops.sh index bdb1b5fb9..25cbd8db0 100644 --- a/regress/window-ops.sh +++ b/regress/window-ops.sh @@ -17,7 +17,8 @@ # unlink the last link without -k; # - swap-window within and between sessions, -d keeping the active window, # and the grouped-sessions error; -# - rotate-window -U/-D rotating pane positions; +# - rotate-window -U/-D rotating pane positions, with and without preserving +# zoom using -Z; # - kill-window switching to the last (previously current) window, kill-window # -a killing all other windows and the "-f only valid with -a" guard. # @@ -306,6 +307,24 @@ check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1" check_ok rotate-window -D -t R:0 check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0" +# Rotation without -Z unzooms. With -Z it preserves zoom and transfers it to +# the pane which arrives at the active position. +layout=$($TMUX display-message -p -t R:0 '#{window_layout}') +check_ok resize-pane -Z -t "$p0" +check_ok rotate-window -U -t R:0 +check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '0:0:1' +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{window_layout}' "$layout" + +check_ok resize-pane -Z -t "$p0" +check_ok rotate-window -U -Z -t R:0 +check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p1" +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{window_layout}' "$layout" + # --------------------------------------------------------------------------- # kill-window. diff --git a/resize.c b/resize.c index 4f7ee490a..67562522d 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -42,8 +42,8 @@ resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy) void resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) { - u_int old_sx = w->sx, old_sy = w->sy; - int zoomed; + struct window_pane *zwp; + u_int old_sx = w->sx, old_sy = w->sy; /* Check size limits. */ if (sx < WINDOW_MINIMUM) @@ -56,8 +56,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) sy = WINDOW_MAXIMUM; /* If the window is zoomed, unzoom. */ - zoomed = w->flags & WINDOW_ZOOMED; - if (zoomed) + zwp = window_zoomed_pane(w); + if (zwp != NULL) window_unzoom(w, 1); /* Resize the layout first. */ @@ -73,8 +73,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) sx, sy, w->layout_root->g.sx, w->layout_root->g.sy); /* Restore the window zoom state. */ - if (zoomed) - window_zoom(w->active); + if (zwp != NULL && window_has_pane(w, zwp)) + window_zoom(zwp); tty_update_window_offset(w); server_redraw_window(w); diff --git a/screen-write.c b/screen-write.c index 40555c419..ab55cd705 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.287 2026/08/17 20:04:00 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -305,12 +305,14 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, if (~ctx->flags & SCREEN_WRITE_SYNC) { /* - * For the active pane or for an overlay (no pane), we want to - * only use synchronized updates if requested (commands that - * move the cursor); for other panes, always use it, since the - * cursor will have to move. + * For the active pane showing its base screen or for an + * overlay (no pane), only use synchronized updates if + * requested (commands that move the cursor); for other panes + * or a pane in a mode, always use it, since the cursor will + * have to move. */ - if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active) + if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active || + ctx->wp->screen != &ctx->wp->base)) ttyctx->flags |= TTY_CTX_SYNC; else { if (ctx->wp == NULL) @@ -338,10 +340,18 @@ screen_write_make_list(struct screen *s) void screen_write_free_list(struct screen *s) { - u_int y; + struct screen_write_cline *cl; + struct screen_write_citem *ci, *ci1; + u_int y; - for (y = 0; y < screen_size_y(s); y++) - free(s->write_list[y].data); + for (y = 0; y < screen_size_y(s); y++) { + cl = &s->write_list[y]; + TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) { + TAILQ_REMOVE(&cl->items, ci, entry); + screen_write_free_citem(ci); + } + free(cl->data); + } free(s->write_list); } @@ -2869,10 +2879,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) struct grid *gd = s->grid; const struct utf8_data *ud = &gc->data; struct options *oo = global_options; - u_int i, n, cx = s->cx, cy = s->cy, vis, yoff = 0; + u_int i, n, cx = s->cx, cy = s->cy, vis; struct grid_cell last; struct tty_ctx ttyctx; int force_wide = 0, zero_width = 0; + int xoff = 0, yoff = 0; struct visible_ranges *r; /* Ignore U+3164 HANGUL_FILLER entirely. */ @@ -2966,9 +2977,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) * obscured in the middle, only on left or right, but there could be an * empty range in the visible ranges so we add them all up. */ - if (wp != NULL) + if (wp != NULL) { + xoff = wp->xoff; yoff = wp->yoff; - r = window_visible_ranges(wp, cx - n, cy + yoff, n, NULL); + } + r = window_visible_ranges(wp, xoff + cx - n, cy + yoff, n, NULL); for (i = 0, vis = 0; i < r->used; i++) vis += r->ranges[i].nx; if (vis < n) { diff --git a/server-client.c b/server-client.c index 1d0213508..f6c69533b 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.504 2026/08/19 10:56:10 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.509 2026/08/28 07:36:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -524,7 +524,10 @@ server_client_lost(struct client *c) input_cancel_requests(c); free(c->title); + free(c->path); free((void *)c->cwd); + free(c->exit_session); + free(c->exit_message); evtimer_del(&c->repeat_timer); evtimer_del(&c->click_timer); @@ -801,8 +804,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, } else { /* Try the pane borders. */ TAILQ_FOREACH(fwp, &w->panes, entry) { - if ((w->flags & WINDOW_ZOOMED) && - (~fwp->flags & PANE_ZOOMED)) + if (!window_pane_is_visible(fwp)) continue; if (window_pane_is_floating(fwp) && window_pane_get_pane_lines(fwp) == PANE_LINES_NONE) @@ -1497,6 +1499,10 @@ server_client_key_callback(struct cmdq_item *item, void *data) TAILQ_EMPTY(&wp->modes)) goto forward_key; + /* Focus events are not keys and cannot be bound. */ + if (key == KEYC_FOCUS_IN || key == KEYC_FOCUS_OUT) + goto forward_key; + /* * Work out the current key table. If the pane is in a mode, use * the mode table instead of the default key table. @@ -2407,8 +2413,6 @@ server_client_check_exit(struct client *c, int force) proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1); break; } - free(c->exit_session); - free(c->exit_message); } /* Redraw timer callback. */ @@ -2484,7 +2488,7 @@ server_client_check_redraw(struct client *c) /* Work out if a redraw is actually needed. */ needed = 0; - if (c->flags & CLIENT_ALLREDRAWFLAGS) + if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS)) needed = 1; else if (server_client_any_pane_redraw(c)) needed = 1; @@ -2510,8 +2514,14 @@ server_client_check_redraw(struct client *c) log_debug("redraw timer started"); evtimer_add(&ev, &tv); } - if (server_client_any_pane_redraw(c)) - c->flags |= CLIENT_REDRAWWINDOW; + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_REDRAW) { + c->flags |= CLIENT_REDRAWWINDOW; + break; + } + if (wp->flags & PANE_REDRAWSCROLLBAR) + c->flags |= CLIENT_REDRAWSCROLLBARS; + } return; } @@ -2530,7 +2540,8 @@ server_client_check_redraw(struct client *c) log_debug("%s: redraw pane %%%u", __func__, wp->id); redraw_pane(c, wp); - } else if (wp->flags & PANE_REDRAWSCROLLBAR) { + } else if ((wp->flags & PANE_REDRAWSCROLLBAR) || + (c->flags & CLIENT_REDRAWSCROLLBARS)) { log_debug("%s: redraw scrollbar %%%u", __func__, wp->id); redraw_pane_scrollbar(c, wp); @@ -2560,7 +2571,8 @@ server_client_check_redraw(struct client *c) * All the redraw flags can now be cleared. Also record how many bytes * were written. */ - c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_STATUSFORCE); + c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS| + CLIENT_STATUSFORCE); c->redraw = EVBUFFER_LENGTH(tty->out); log_debug("%s: redraw added %zu bytes", c->name, c->redraw); } diff --git a/server-fn.c b/server-fn.c index 8a07c1eff..424d4a1c1 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.150 2026/08/18 07:32:09 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.151 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -226,10 +226,11 @@ server_kill_pane(struct window_pane *wp) server_kill_window(w, 1); recalculate_sizes(); } else { - server_unzoom_window(w); + window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM); server_client_remove_pane(wp); layout_close_pane(wp); window_remove_pane(w, wp); + window_pop_zoom(w); server_redraw_window(w); } } @@ -421,15 +422,17 @@ server_destroy_pane(struct window_pane *wp, int notify) if (notify) server_fire_pane_exit("pane-exited", wp); - server_unzoom_window(w); + window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM); server_client_remove_pane(wp); layout_close_pane(wp); window_remove_pane(w, wp); if (TAILQ_EMPTY(&w->panes)) server_kill_window(w, 1); - else + else { + window_pop_zoom(w); server_redraw_window(w); + } } static void diff --git a/sort.c b/sort.c index 31f6e82f1..1b71db494 100644 --- a/sort.c +++ b/sort.c @@ -1,4 +1,4 @@ -/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ +/* $OpenBSD: sort.c,v 1.10 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2026 Dane Jensen @@ -32,7 +32,7 @@ sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *, u_int i; void *tmp, **ll; - if (sort_crit->order == SORT_END) + if (len < 2 || sort_crit->order == SORT_END) return; if (sort_crit->order == SORT_ORDER) { diff --git a/spawn.c b/spawn.c index fdf535076..361d207e5 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.51 2026/08/18 07:43:44 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.52 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -360,6 +360,8 @@ spawn_pane(struct spawn_context *sc, char **cause) } if (sc->flags & SPAWN_FLOATING) new_wp->layout_cell->flags |= LAYOUT_CELL_FLOATING; + if (sc->flags & SPAWN_FLOATOVERZOOM) + new_wp->flags |= PANE_FLOATOVERZOOM; /* * If window currently zoomed, window_set_active_pane calls @@ -745,10 +747,10 @@ spawn_editor(struct client *c, const char *buf, size_t len, lg.sy = w->sy * 9 / 10; lg.xoff = w->sx / 2 - lg.sx / 2; lg.yoff = w->sy / 2 - lg.sy / 2; - window_push_modal_zoom(w); + window_push_zoom(w, 0, 1); lc = layout_floating_pane(w, NULL, &lg); if (lc == NULL) { - window_pop_modal_zoom(w); + window_pop_zoom(w); spawn_editor_free(es); return (NULL); } @@ -765,17 +767,18 @@ spawn_editor(struct client *c, const char *buf, size_t len, sc.environ = env; sc.idx = -1; sc.cwd = _PATH_TMP; - sc.flags = SPAWN_FLOATING|SPAWN_MODAL; + sc.flags = SPAWN_FLOATING|SPAWN_MODAL|SPAWN_FLOATOVERZOOM; wp = spawn_pane(&sc, &cause); free(cmd); environ_free(env); if (wp == NULL) { free(cause); - window_pop_modal_zoom(w); + window_pop_zoom(w); spawn_editor_free(es); return (NULL); } + window_pop_zoom(w); options_set_number(wp->options, "remain-on-exit", 0); es->pid = wp->pid; wp->editor = es; diff --git a/tmux.1 b/tmux.1 index 82a3c0091..9fdfb5749 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1157 2026/08/19 10:56:10 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1163 2026/09/01 12:49:49 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 19 2026 $ +.Dd $Mdocdate: September 1 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2230,14 +2230,14 @@ Move to the end of the next word. .Xc Same as .Ic next\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic next\-space\-end (vi: E) .Xc Same as .Ic next\-word\-end -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic other\-end (vi: o) @@ -2311,7 +2311,7 @@ Move to the previous word. .Xc Same as .Ic previous\-word -but use a space alone as the word separator. +but treat all non-whitespace characters as part of a word. .It Xo .Ic rectangle\-on .Xc @@ -2596,8 +2596,8 @@ Word separators can be customized with the session option. Next word moves to the start of the next word, next word end to the end of the next word and previous word to the start of the previous word. -The three next and previous space keys work similarly but use a space alone as -the word separator. +The three next and previous space keys work similarly but treat all +non-whitespace characters as part of a word. Setting .Em word\-separators to the empty string makes next/previous word equivalent to next/previous space. @@ -3666,7 +3666,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bCdefhIkKLMOPvWZ +.Op Fl AbCdefhIkKLMOPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3715,8 +3715,8 @@ sets the pane border lines for floating panes; see .Fl M may be used when bound to a mouse drag key; the new floating pane is resized to follow the mouse drag. -It has no effect with -.Fl L . +.Fl A +creates a floating pane which remains visible above a zoomed pane. .Fl O creates a modal pane. A modal pane is always the active pane and prevents interaction with any other @@ -4778,6 +4778,15 @@ for backspace. Set the number of buffers; as new buffers are added to the top of the stack, old ones are removed from the bottom if necessary to maintain this maximum length. +.It Xo Ic clear\-on\-attach +.Op Ic on | off +.Xc +When enabled (the default), the client terminal switches to the alternate +screen and clears it when a client is attached. +When disabled, +.Nm +does not enter the alternate screen; instead the existing terminal content is +scrolled into the scrollback buffer so it remains accessible. .It Xo Ic command\-alias[] .Ar name=value .Xc @@ -5133,6 +5142,8 @@ Supports title setting. .It usstyle Allows underscore style and colour to be set. +.It utf8 +Supports UTF\-8 output. .El .It Ic terminal\-overrides[] Ar string Allow terminal descriptions read using @@ -5667,6 +5678,11 @@ If set to both, a bell and a message are produced. Sets the session's conception of what characters are considered word separators, for the purposes of the next and previous word commands in copy mode. +A space in +.Ar string +matches any character with the Unicode +.Em White_Space +property. .El .Pp Available window options are: @@ -7336,7 +7352,10 @@ The following variables are available, where appropriate: .It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode" .It Li "cursor_x" Ta "" Ta "Cursor X position in pane" .It Li "cursor_y" Ta "" Ta "Cursor Y position in pane" +.It Li "history_added" Ta "" Ta "Number of lines scrolled into pane history" .It Li "history_bytes" Ta "" Ta "Number of bytes in window history" +.It Li "history_collected" Ta "" Ta "Number of oldest pane history lines discarded at the history limit" +.It Li "history_generation" Ta "" Ta "Generation incremented when pane history is cleared or reflowed" .It Li "history_limit" Ta "" Ta "Maximum window history lines" .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" @@ -7435,6 +7454,7 @@ The following variables are available, where appropriate: .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" .It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal" .It Li "pane_mode" Ta "" Ta "Name of pane mode, if any" +.It Li "pane_output_generation" Ta "" Ta "Generation incremented when pane output is processed" .It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)" .It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" diff --git a/tmux.h b/tmux.h index 0a61cf73d..87d32c82e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1428 2026/08/19 10:56:10 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -486,6 +486,7 @@ enum tty_code_code { TTYC_ICH1, TTYC_IL, TTYC_IL1, + TTYC_IND, TTYC_INDN, TTYC_INVIS, TTYC_KCBT, @@ -1339,6 +1340,7 @@ struct window_pane { #define PANE_ACTIVITY 0x40000 #define PANE_CLOSEONCLICK 0x80000 #define PANE_CAPTUREALLKEYS 0x100000 +#define PANE_FLOATOVERZOOM 0x200000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1358,9 +1360,10 @@ struct window_pane { char tty[TTY_NAME_MAX]; int status; struct timeval dead_time; - struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ + struct cmdq_item *wait_item; struct spawn_editor_state *editor; + uint64_t output_generation; time_t last_output_time; time_t last_prompt_time; time_t cmd_start_time; @@ -1445,6 +1448,7 @@ struct window { struct window_pane *active; struct window_pane *modal; struct window_pane *modal_last; + struct window_pane *was_zoomed; struct window_panes last_panes; struct window_panes z_index; struct window_panes panes; @@ -1487,7 +1491,6 @@ struct window { #define WINDOW_ZOOMED 0x8 #define WINDOW_WASZOOMED 0x10 #define WINDOW_RESIZE 0x20 -#define WINDOW_WASMODALZOOMED 0x40 #define WINDOW_ALERTFLAGS (WINDOW_BELL|WINDOW_ACTIVITY|WINDOW_SILENCE) int alerts_queued; @@ -2265,7 +2268,7 @@ struct client { #define CLIENT_STARTSERVER 0x10000000 #define CLIENT_REDRAWMENU 0x20000000 #define CLIENT_NOFORK 0x40000000 -/* 0x80000000ULL unused */ +#define CLIENT_REDRAWSCROLLBARS 0x80000000ULL #define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL #define CLIENT_CONTROL_WAITEXIT 0x200000000ULL #define CLIENT_WINDOWSIZECHANGED 0x400000000ULL @@ -2511,6 +2514,7 @@ struct spawn_context { #define SPAWN_HORIZONTAL 0x200 #define SPAWN_SPLIT 0x400 #define SPAWN_MODAL 0x800 +#define SPAWN_FLOATOVERZOOM 0x1000 }; /* Paste buffer. */ @@ -3683,8 +3687,8 @@ void window_resize(struct window *, u_int, u_int, int, int); void window_pane_send_resize(struct window_pane *, u_int, u_int); int window_zoom(struct window_pane *); int window_unzoom(struct window *, int); -void window_push_modal_zoom(struct window *); -int window_pop_modal_zoom(struct window *); +int window_active_pane_is_over_zoom(struct window *); +struct window_pane *window_zoomed_pane(struct window *); int window_push_zoom(struct window *, int, int); int window_pop_zoom(struct window *); void window_lost_pane(struct window *, struct window_pane *); @@ -4054,6 +4058,7 @@ void session_update_history(struct session *); /* utf8.c */ enum utf8_state utf8_towc (const struct utf8_data *, wchar_t *); enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *); +int utf8_has_whitespace(const struct utf8_data *); void utf8_update_width_cache(void); utf8_char utf8_build_one(u_char); enum utf8_state utf8_from_data(const struct utf8_data *, utf8_char *); diff --git a/tty-features.c b/tty-features.c index e999fb599..0b12745e2 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.42 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.43 2026/08/31 12:41:03 kirill Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -368,6 +368,13 @@ static const struct tty_feature tty_feature_progressbar = { 0 }; +/* Terminal supports UTF-8. */ +static const struct tty_feature tty_feature_utf8 = { + "utf8", + NULL, + 0 +}; + /* Available terminal features. */ static const struct tty_feature *const tty_features[] = { &tty_feature_256, @@ -390,7 +397,8 @@ static const struct tty_feature *const tty_features[] = { &tty_feature_strikethrough, &tty_feature_sync, &tty_feature_title, - &tty_feature_usstyle + &tty_feature_usstyle, + &tty_feature_utf8 }; /* Parse features for client. */ @@ -473,6 +481,9 @@ tty_feature_present(struct tty_term *term, const char *name) u_int i; char *copy; + if (strcmp(name, "utf8") == 0) + return ((term->tty->client->flags & CLIENT_UTF8) != 0); + for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcmp(tf->name, name) == 0) { @@ -486,7 +497,8 @@ tty_feature_present(struct tty_term *term, const char *name) * We don't just have the feature flag set. Check if the capabilities * supported by the client are actual set instead. */ - if (tf == NULL || strcmp(name, "ignorefkeys") == 0) + if (tf == NULL || tf->capabilities == NULL || + strcmp(name, "ignorefkeys") == 0) return (0); if (tf->flags != 0 && (term->flags & tf->flags) != tf->flags) return (0); @@ -534,6 +546,8 @@ tty_apply_features(struct tty_term *term) } } term->flags |= tf->flags; + if (tf == &tty_feature_utf8) + c->flags |= CLIENT_UTF8; } if ((term->applied_features|feat) == term->applied_features) return (0); diff --git a/tty-term.c b/tty-term.c index 8e0248e84..6b149fcee 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.108 2026/08/17 14:47:41 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.109 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -111,6 +111,7 @@ static const struct tty_term_code_entry tty_term_codes[] = { [TTYC_ICH] = { TTYCODE_STRING, "ich" }, [TTYC_IL1] = { TTYCODE_STRING, "il1" }, [TTYC_IL] = { TTYCODE_STRING, "il" }, + [TTYC_IND] = { TTYCODE_STRING, "ind" }, [TTYC_INDN] = { TTYCODE_STRING, "indn" }, [TTYC_INVIS] = { TTYCODE_STRING, "invis" }, [TTYC_KCBT] = { TTYCODE_STRING, "kcbt" }, diff --git a/tty.c b/tty.c index e8a60c68d..9bac687eb 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.480 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -338,6 +338,7 @@ tty_start_tty(struct tty *tty) { struct client *c = tty->client; struct termios tio; + u_int i; setblocking(c->fd, 0); event_add(&tty->event_in, NULL); @@ -353,10 +354,21 @@ tty_start_tty(struct tty *tty) if (tcsetattr(c->fd, TCSANOW, &tio) == 0) tcflush(c->fd, TCOFLUSH); - tty_putcode(tty, TTYC_SMCUP); - + if (options_get_number(global_options, "clear-on-attach")) { + tty_putcode(tty, TTYC_SMCUP); + tty_putcode(tty, TTYC_CLEAR); + } else { + tty_putcode_ii(tty, TTYC_CSR, 0, tty->sy - 1); + tty_putcode_ii(tty, TTYC_CUP, 0, tty->sy - 1); + if (tty_term_has(tty->term, TTYC_INDN)) + tty_putcode_i(tty, TTYC_INDN, tty->sy + 1); + else if (tty_term_has(tty->term, TTYC_IND)) { + for (i = 0; i < tty->sy + 1; i++) + tty_putcode(tty, TTYC_IND); + } else + tty_putcode(tty, TTYC_CLEAR); + } tty_putcode(tty, TTYC_SMKX); - tty_putcode(tty, TTYC_CLEAR); if (tty_acs_needed(tty)) { log_debug("%s: using capabilities for ACS", c->name); @@ -472,7 +484,8 @@ tty_stop_tty(struct tty *tty) tty_raw(tty, tty_term_string(tty->term, TTYC_RMACS)); tty_raw(tty, tty_term_string(tty->term, TTYC_SGR0)); tty_raw(tty, tty_term_string(tty->term, TTYC_RMKX)); - tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); + if (options_get_number(global_options, "clear-on-attach")) + tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); if (tty->cstyle != SCREEN_CURSOR_DEFAULT) { if (tty_term_has(tty->term, TTYC_SE)) tty_raw(tty, tty_term_string(tty->term, TTYC_SE)); @@ -497,7 +510,10 @@ tty_stop_tty(struct tty *tty) if (tty_use_margin(tty)) tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); - tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + if (options_get_number(global_options, "clear-on-attach")) + tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP)); + else + tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR)); if (tty->term->flags & TERM_VT100LIKE) tty_raw(tty, "\033[?2031l"); diff --git a/utf8.c b/utf8.c index 887d2b0f9..68c37d362 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.72 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -603,6 +603,72 @@ utf8_towc(const struct utf8_data *ud, wchar_t *wc) return (UTF8_DONE); } +/* Check for a Unicode whitespace character. */ +int +utf8_has_whitespace(const struct utf8_data *ud) +{ + struct utf8_data tmp; + wchar_t wc; + u_int offset = 0, size; + u_char ch; + + while (offset < ud->size) { + ch = ud->data[offset]; + if (ch < 0x80) { + wc = ch; + size = 1; + } else { + if (ch >= 0xc2 && ch <= 0xdf) + size = 2; + else if (ch >= 0xe0 && ch <= 0xef) + size = 3; + else if (ch >= 0xf0 && ch <= 0xf4) + size = 4; + else + return (0); + if (size > ud->size - offset) + return (0); + + memset(&tmp, 0, sizeof tmp); + memcpy(tmp.data, ud->data + offset, size); + tmp.size = tmp.have = size; + if (utf8_towc(&tmp, &wc) != UTF8_DONE) + return (0); + } + offset += size; + + switch (wc) { + case 0x0009: + case 0x000A: + case 0x000B: + case 0x000C: + case 0x000D: + case 0x0020: + case 0x0085: + case 0x00A0: + case 0x1680: + case 0x2000: + case 0x2001: + case 0x2002: + case 0x2003: + case 0x2004: + case 0x2005: + case 0x2006: + case 0x2007: + case 0x2008: + case 0x2009: + case 0x200A: + case 0x2028: + case 0x2029: + case 0x202F: + case 0x205F: + case 0x3000: + return (1); + } + } + return (0); +} + /* Convert wide character to UTF-8 character. */ enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *ud) diff --git a/window-buffer.c b/window-buffer.c index ab15d87ed..bc25de66e 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.51 2026/07/15 12:45:39 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -567,9 +567,7 @@ window_buffer_edit_close_cb(char *buf, size_t len, void *arg) } oldbuf = paste_buffer_data(pb, &oldlen); - if (oldlen != '\0' && - oldbuf[oldlen - 1] != '\n' && - buf[len - 1] == '\n') + if (oldlen != 0 && oldbuf[oldlen - 1] != '\n' && buf[len - 1] == '\n') len--; if (len != 0) paste_replace(pb, buf, len); diff --git a/window-client.c b/window-client.c index f42aa5b7a..3c2f81097 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.49 2026/08/31 12:41:03 kirill Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -102,7 +102,8 @@ static const char *window_client_info_lines[] = { WINDOW_CLIENT_FEATURE(sync) " " WINDOW_CLIENT_FEATURE(title), " #[#{E:tree-mode-border-style},acs]x#[default] " - WINDOW_CLIENT_FEATURE(usstyle), + WINDOW_CLIENT_FEATURE(usstyle) " " + WINDOW_CLIENT_FEATURE(utf8), "#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]", "#[fg=themelightgrey]prefix #[#{E:tree-mode-border-style},acs]x#[default] " diff --git a/window-copy.c b/window-copy.c index 716a0f037..c5529a3ff 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.429 2026/09/01 13:04:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2207,15 +2207,76 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct options *so = cs->s->options; struct window_copy_mode_data *data = wme->data; + struct grid_reader gr; const char *s = args_string(cs->wargs, 0); + u_int sx, sy, ex, ey, fx, fy, x, y; if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0) data->selflag = SEL_CHAR; else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) { data->separators = options_get_string(so, "word-separators"); data->selflag = SEL_WORD; - } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) + } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) { data->selflag = SEL_LINE; + if (data->screen.sel == NULL) + return (WINDOW_COPY_CMD_MOVE); + + /* + * Line selection normally starts with select-line, which sets + * up the reset positions used when the cursor changes + * direction. Do the same when changing an existing selection + * to line mode. + */ + if (data->cursordrag == CURSORDRAG_SEL) { + fx = data->endselx; + fy = data->endsely; + } else { + fx = data->selx; + fy = data->sely; + } + + sx = data->selx; + sy = data->sely; + ex = data->endselx; + ey = data->endsely; + if (ey < sy || (ey == sy && ex < sx)) { + x = sx; sx = ex; ex = x; + y = sy; sy = ey; ey = y; + } + grid_reader_start(&gr, data->backing->grid, sx, sy); + grid_reader_cursor_start_of_line(&gr, 1); + grid_reader_get_cursor(&gr, &sx, &sy); + grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_cursor_end_of_line(&gr, 1, 0); + grid_reader_get_cursor(&gr, &ex, &ey); + + data->rectflag = 0; + data->selrx = data->selx = sx; + data->selry = data->sely = sy; + data->endselrx = data->endselx = ex; + data->endselry = data->endsely = ey; + + x = data->cx; + y = screen_hsize(data->backing) + data->cy - data->oy; + data->dx = fx; + data->dy = fy; + if (data->cursordrag != CURSORDRAG_NONE && + (y < fy || (y == fy && x < fx))) { + data->lineflag = LINE_SEL_RIGHT_LEFT; + data->cursordrag = CURSORDRAG_SEL; + window_copy_scroll_to(wme, sx, sy, 1); + } else { + data->lineflag = LINE_SEL_LEFT_RIGHT; + if (data->cursordrag != CURSORDRAG_NONE) { + data->cursordrag = CURSORDRAG_ENDSEL; + x = window_copy_cursor_limit(wme, ey, 0); + window_copy_scroll_to(wme, x, ey, 1); + } + } + if (data->cursordrag == CURSORDRAG_NONE) + window_copy_set_selection(wme, 0, 0); + return (WINDOW_COPY_CMD_REDRAW); + } return (WINDOW_COPY_CMD_MOVE); } @@ -4093,7 +4154,7 @@ window_copy_search_lr_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py, endline = gd->hsize + gd->sy - 1; pywrap = py; while (buf != NULL && - pywrap <= endline && + pywrap < endline && len < WINDOW_COPY_SEARCH_MAX_LINE) { gl = grid_get_line(gd, pywrap); if (~gl->flags & GRID_LINE_WRAPPED) @@ -4152,7 +4213,7 @@ window_copy_search_rl_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py, endline = gd->hsize + gd->sy - 1; pywrap = py; while (buf != NULL && - pywrap <= endline && + pywrap < endline && len < WINDOW_COPY_SEARCH_MAX_LINE) { gl = grid_get_line(gd, pywrap); if (~gl->flags & GRID_LINE_WRAPPED) @@ -4301,7 +4362,7 @@ window_copy_stringify(struct grid *gd, u_int py, u_int first, u_int last, } if (dlen == 1) buf[bx++] = *d; - else { + else if (dlen != 0) { memcpy(buf + bx, d, dlen); bx += dlen; } @@ -4353,6 +4414,7 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy, break; } } + ncells = cell; /* Locate starting cell. */ cell = 0; @@ -4850,6 +4912,8 @@ window_copy_search_marks(struct window_mode_entry *wme, struct screen *ssp, cflags |= REG_ICASE; if (regcomp(®, sbuf, cflags) != 0) { free(sbuf); + free(data->searchmark); + data->searchmark = NULL; return (0); } free(sbuf); diff --git a/window-panes.c b/window-panes.c index 396d495eb..c20b1e185 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.5 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-switch.c b/window-switch.c index ac6156c4f..4b9015186 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-switch.c,v 1.2 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-switch.c,v 1.3 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -227,7 +227,8 @@ window_switch_build(struct window_switch_modedata *data) m = xreallocarray(m, n + 1, sizeof *m); m[n++] = item; } - qsort(m, n, sizeof *m, window_switch_compare); + if (n > 1) + qsort(m, n, sizeof *m, window_switch_compare); free(data->matches); data->matches = m; diff --git a/window-tree.c b/window-tree.c index 342a1c3bc..7e90a5a2f 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-tree.c,v 1.94 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-tree.c,v 1.96 2026/08/25 07:23:30 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -642,6 +642,7 @@ window_tree_draw_session(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, @@ -790,6 +791,7 @@ window_tree_draw_window(struct window_tree_modedata *data, struct session *s, } free(label); } + format_free(ft); if (loop != end - 1) { screen_write_cursormove(ctx, cx + offset + width, cy, @@ -1020,6 +1022,10 @@ window_tree_swap(void *cur_itemdata, void *other_itemdata, window_tree_pull_item(other, &other_session, &other_winlink, &other_pane); + if (cur_session == NULL || cur_winlink == NULL) + return (0); + if (other_session == NULL || other_winlink == NULL) + return (0); if (cur_session != other_session) return (0); diff --git a/window.c b/window.c index 3680ad80f..618ca5ff8 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -718,7 +718,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) return (0); if (w->modal != NULL && wp != w->modal) return (0); - if (w->flags & WINDOW_ZOOMED) + if ((w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp)) window_unzoom(w, 1); lastwp = w->active; @@ -918,13 +918,18 @@ window_zoom(struct window_pane *wp) { struct window *w = wp->window; struct window_pane *wp1; + struct layout_cell *lc; + struct layout_geometry lg; if (w->flags & WINDOW_ZOOMED) return (-1); if (window_count_panes(w, 1) == 1) return (-1); - if (w->active != wp) + if (w->active != wp && + (w->active == NULL || + (~w->active->flags & PANE_FLOATOVERZOOM) || + !window_pane_is_floating(w->active))) window_set_active_pane(w, wp, 1); wp->flags |= PANE_ZOOMED; @@ -935,6 +940,22 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); + TAILQ_FOREACH(wp1, &w->panes, entry) { + lc = wp1->saved_layout_cell; + if (wp1 == wp || + (~wp1->flags & PANE_FLOATOVERZOOM) || + lc == NULL || + (~lc->flags & LAYOUT_CELL_FLOATING)) + continue; + memcpy(&lg, &lc->g, sizeof lg); + lc = layout_floating_pane(w, wp, &lg); + layout_assign_pane(lc, wp1, 0); + } + /* A floating zoom target is now tiled, so put it behind the floats. */ + if (wp->saved_layout_cell->flags & LAYOUT_CELL_FLOATING) { + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + } w->flags |= WINDOW_ZOOMED; events_fire_window("window-zoomed", w); events_fire_window("window-layout-changed", w); @@ -946,11 +967,26 @@ window_zoom(struct window_pane *wp) int window_unzoom(struct window *w, int notify) { - struct window_pane *wp; + struct window_pane *wp, *zoomed = NULL; + struct layout_cell *slc; - if (!(w->flags & WINDOW_ZOOMED)) + if (~w->flags & WINDOW_ZOOMED) return (-1); + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_ZOOMED) + zoomed = wp; + if (~wp->flags & PANE_FLOATOVERZOOM) + continue; + if (wp->flags & PANE_ZOOMED) + continue; + slc = wp->saved_layout_cell; + if (slc == NULL || wp->layout_cell == NULL) + continue; + memcpy(&slc->g, &wp->layout_cell->g, sizeof slc->g); + memcpy(&slc->fg, &wp->layout_cell->fg, sizeof slc->fg); + } + w->flags &= ~WINDOW_ZOOMED; layout_free(w, 0); w->layout_root = w->saved_layout_root; @@ -961,6 +997,22 @@ window_unzoom(struct window *w, int notify) wp->saved_layout_cell = NULL; wp->flags &= ~PANE_ZOOMED; } + /* Put a floating zoom target back into the floating part of the list. */ + if (zoomed != NULL && window_pane_is_floating(zoomed)) { + TAILQ_REMOVE(&w->z_index, zoomed, zentry); + if (zoomed == w->active) + TAILQ_INSERT_HEAD(&w->z_index, zoomed, zentry); + else { + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_is_floating(wp)) + break; + } + if (wp == NULL) + TAILQ_INSERT_TAIL(&w->z_index, zoomed, zentry); + else + TAILQ_INSERT_BEFORE(wp, zoomed, zentry); + } + } layout_fix_panes(w, NULL); if (notify) { @@ -972,48 +1024,69 @@ window_unzoom(struct window *w, int notify) return (0); } -void -window_push_modal_zoom(struct window *w) +struct window_pane * +window_zoomed_pane(struct window *w) { - if (w->flags & WINDOW_ZOOMED) - w->flags |= WINDOW_WASMODALZOOMED; - else - w->flags &= ~WINDOW_WASMODALZOOMED; - window_unzoom(w, 1); + struct window_pane *wp; + + if (~w->flags & WINDOW_ZOOMED) + return (NULL); + TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) { + if (wp->layout_cell != NULL && !window_pane_is_floating(wp)) + return (wp); + } + return (NULL); } int -window_pop_modal_zoom(struct window *w) +window_active_pane_is_over_zoom(struct window *w) { - struct window_pane *wp = w->active; - - if (~w->flags & WINDOW_WASMODALZOOMED) + if (~w->flags & WINDOW_ZOOMED) return (0); - w->flags &= ~WINDOW_WASMODALZOOMED; - if (wp != NULL && window_has_pane(w, wp)) - return (window_zoom(wp) == 0); - return (0); + if (w->active == NULL) + return (0); + if (~w->active->flags & PANE_FLOATOVERZOOM) + return (0); + return (window_pane_is_floating(w->active)); } int window_push_zoom(struct window *w, int always, int flag) { + struct window_pane *wp = window_zoomed_pane(w); + log_debug("%s: @%u %d", __func__, w->id, flag && (w->flags & WINDOW_ZOOMED)); if (flag && (always || (w->flags & WINDOW_ZOOMED))) w->flags |= WINDOW_WASZOOMED; else w->flags &= ~WINDOW_WASZOOMED; + if (w->flags & WINDOW_WASZOOMED) + w->was_zoomed = wp; + else + w->was_zoomed = NULL; return (window_unzoom(w, 1) == 0); } int window_pop_zoom(struct window *w) { + struct window_pane *wp = w->was_zoomed; + log_debug("%s: @%u %d", __func__, w->id, !!(w->flags & WINDOW_WASZOOMED)); - if (w->flags & WINDOW_WASZOOMED) - return (window_zoom(w->active) == 0); + if (w->flags & WINDOW_WASZOOMED) { + w->flags &= ~WINDOW_WASZOOMED; + w->was_zoomed = NULL; + if (w->active != NULL && + ((~w->active->flags & PANE_FLOATOVERZOOM) || + !window_pane_is_floating(w->active))) + wp = w->active; + if (wp == NULL || !window_has_pane(w, wp)) + wp = w->active; + if (wp != NULL) + return (window_zoom(wp) == 0); + } return (0); } @@ -1065,8 +1138,8 @@ window_lost_pane(struct window *w, struct window_pane *wp) server_clear_marked(); if (wp == w->modal_last) w->modal_last = NULL; - if (w->modal_last == NULL) - w->flags &= ~WINDOW_WASMODALZOOMED; + if (wp == w->was_zoomed) + w->was_zoomed = NULL; window_pane_stack_remove(&w->last_panes, wp); if (wp == w->active) { @@ -1093,7 +1166,6 @@ window_lost_pane(struct window *w, struct window_pane *wp) } } else if (wp == w->modal) { w->modal = w->modal_last = NULL; - w->flags &= ~WINDOW_WASMODALZOOMED; } redraw_invalidate_scene(w); } @@ -1101,13 +1173,9 @@ window_lost_pane(struct window *w, struct window_pane *wp) void window_remove_pane(struct window *w, struct window_pane *wp) { - int pop = (wp == w->modal); - window_lost_pane(w, wp); TAILQ_REMOVE(&w->panes, wp, entry); TAILQ_REMOVE(&w->z_index, wp, zentry); - if (pop && window_pop_modal_zoom(w)) - server_redraw_window(w); redraw_invalidate_scene(w); window_pane_destroy(wp); } @@ -1263,6 +1331,8 @@ window_pane_printable_flags(struct window_pane *wp) flags[pos++] = 'Z'; if (window_pane_is_floating(wp)) flags[pos++] = 'F'; + if (wp->flags & PANE_FLOATOVERZOOM) + flags[pos++] = 'A'; if (wp == w->modal) flags[pos++] = 'O'; flags[pos] = '\0'; @@ -1408,6 +1478,8 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp) void window_pane_scrollbar_redraw(struct window_pane *wp) { + if (!window_pane_scrollbar_visible(wp)) + return; if (window_pane_scrollbar_overlay_visible(wp)) { wp->flags |= PANE_REDRAW; return; @@ -1475,16 +1547,16 @@ window_pane_free(struct window_pane *wp) log_debug("pane %%%u freed (%d references)", wp->id, wp->references); free(wp->searchstr); - screen_free(&wp->status_screen); screen_free(&wp->base); - + free(wp->r.ranges); options_free(wp->options); free((void *)wp->cwd); free(wp->shell); cmd_free_argv(wp->argc, wp->argv); colour_palette_free(&wp->palette); style_ranges_free(&wp->border_status_line.ranges); + free(wp->border_status_line.expanded); free(wp); } @@ -1969,7 +2041,7 @@ window_pane_is_visible(struct window_pane *wp) { if (~wp->window->flags & WINDOW_ZOOMED) return (1); - return (wp == wp->window->active); + return (wp->layout_cell != NULL); } int