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 3cc036425..b1e19e9e0 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -17,7 +17,7 @@ jobs: name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} if: github.repository == 'tmux/tmux' - timeout-minutes: 45 + timeout-minutes: 300 strategy: fail-fast: false @@ -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/CHANGES b/CHANGES index 80016b0e1..093e29fc9 100644 --- a/CHANGES +++ b/CHANGES @@ -1,7 +1,4 @@ -CHANGES FROM 3.7b TO 3.8 - -* Build with jemalloc on macOS to avoid what appears to be a bug in the system - calloc(3) (issue 5385). +CHANGES FROM 3.7c TO 3.8 * Many improvements to floating panes: @@ -121,8 +118,6 @@ CHANGES FROM 3.7b TO 3.8 * Copy mode no longer exits at the bottom while a selection is in progress (issue 5349). -* Fix scrollbar initial state so scrollbars appear on new windows (issue 5339). - * Add style attributes for dimming colours (dim=) and for hyperlinks (link= and nolink) (issues 4842 and 4280 from Moritz Angermann). @@ -169,7 +164,18 @@ CHANGES FROM 3.7b TO 3.8 * Do not crash looking for the next or previous session (issue 5344), or when no client is available. -* Check time periodically in loops rather than for every item (issue 5367). +CHANGES FROM 3.7b TO 3.7c + +* Build with jemalloc on macOS to avoid what appears to be a bug in calloc + (issue 5385). + +* Fix scrollbar initial state so they appear on new windows (issue 5339). + +* Check time periodically in loops rather than every one (issue 5367). + +* Use message-style again as default for message-format. + +* Unzoom before creating floating panes to avoid a crash. CHANGES FROM 3.7a TO 3.7b diff --git a/arguments.c b/arguments.c index a4b6a0f35..00af96311 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 @@ -762,8 +762,6 @@ args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx, cmdq_error(item, "%s", error); free(error); } - else - cmdlist->references++; args_make_commands_free(state); return (cmdlist); } @@ -827,8 +825,10 @@ args_make_commands(struct args_command_state *state, int argc, char **argv, int i; if (state->cmdlist != NULL) { - if (argc == 0) + if (argc == 0) { + state->cmdlist->references++; return (state->cmdlist); + } return (cmd_list_copy(state->cmdlist, argc, argv)); } diff --git a/client.c b/client.c index 0358c351e..97f0538ad 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -712,6 +712,9 @@ client_dispatch_wait(struct imsg *imsg) fprintf(stderr, "server version is too old for client\n"); proc_exit(client_proc); break; + default: + log_debug("unknown message type %u", imsg->hdr.type); + break; } } @@ -798,5 +801,8 @@ client_dispatch_attached(struct imsg *imsg) system(data); proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0); break; + default: + log_debug("unknown message type %u", imsg->hdr.type); + break; } } diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index ac1117347..1d135b834 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 @@ -242,9 +242,11 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s, } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } cmd_free_argv(argc, argv); diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 766710a78..e221cd85c 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 49b92a996..59d34f9d6 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 @@ -98,6 +98,7 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); return (CMD_RETURN_NORMAL); } @@ -164,9 +165,11 @@ cmd_if_shell_callback(struct job *job) } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } out: diff --git a/cmd-parse.y b/cmd-parse.y index 29b7780e0..4a6ec66b1 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 @@ -802,6 +802,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd, if (last == NULL) { pr->status = CMD_PARSE_SUCCESS; pr->cmdlist = cmd_list_new(); + cmd_parse_free_commands(cmds); return (1); } @@ -814,6 +815,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd, pi->flags |= CMD_PARSE_NOALIAS; cmd_parse_build_commands(cmds, pi, pr); pi->flags &= ~CMD_PARSE_NOALIAS; + cmd_parse_free_commands(cmds); return (1); } diff --git a/cmd-queue.c b/cmd-queue.c index 35373a32a..a9ce469bc 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 @@ -850,7 +850,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 dd2bf0345..ba005119f 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 @@ -239,9 +239,11 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg) } else if (item == NULL) { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } else { new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); cmdq_insert_after(item, new_item); + cmd_list_free(cmdlist); } if (cdata->item != NULL) diff --git a/cmd-select-pane.c b/cmd-select-pane.c index 75ebb1e02..e00b77ea7 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 @@ -164,6 +164,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 (args_has(args, 's') || args_has(args, 'S')) { if (c == NULL || c->session == NULL) { @@ -202,7 +203,11 @@ 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); if (!active_has_local_pane(c, w)) window_redraw_active_switch(w, lastwp); @@ -210,7 +215,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) 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); @@ -287,7 +292,11 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) activewp = active_get_effective_pane(c, w); if (wp == activewp) 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); if (!active_has_local_pane(c, w)) window_redraw_active_switch(w, wp); @@ -295,7 +304,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) 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-source-file.c b/cmd-source-file.c index 7c83c467d..771b03663 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ +/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha @@ -50,6 +50,7 @@ const struct cmd_entry cmd_source_file_entry = { struct cmd_source_file_data { struct cmdq_item *item; + struct client *client; int flags; struct cmdq_item *after; @@ -60,10 +61,24 @@ struct cmd_source_file_data { u_int nfiles; }; -static enum cmd_retval -cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) +static void +cmd_source_file_free_data(struct cmd_source_file_data *cdata) { - struct client *c = cmdq_get_client(item); + u_int i; + + for (i = 0; i < cdata->nfiles; i++) + free(cdata->files[i]); + free(cdata->files); + if (cdata->client != NULL) + server_client_unref(cdata->client); + free(cdata); +} + +static enum cmd_retval +cmd_source_file_complete_cb(struct cmdq_item *item, void *data) +{ + struct cmd_source_file_data *cdata = data; + struct client *c = cdata->client; if (c == NULL) { cmd_source_file_depth--; @@ -74,36 +89,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data) } cfg_print_causes(item); + cmd_source_file_free_data(cdata); return (CMD_RETURN_NORMAL); } static void -cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata) +cmd_source_file_complete(struct cmd_source_file_data *cdata) { + struct client *c = cdata->client; struct cmdq_item *new_item; - u_int i; - if (cfg_finished) { - if (cdata->retval == CMD_RETURN_ERROR && - c != NULL && - c->session == NULL) - c->retval = 1; - new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL); - cmdq_insert_after(cdata->after, new_item); + if (!cfg_finished) { + cmd_source_file_free_data(cdata); + return; } - for (i = 0; i < cdata->nfiles; i++) - free(cdata->files[i]); - free(cdata->files); - free(cdata); + if (cdata->retval == CMD_RETURN_ERROR && + c != NULL && + c->session == NULL) + c->retval = 1; + new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata); + cmdq_insert_after(cdata->after, new_item); } static void -cmd_source_file_done(struct client *c, const char *path, int error, - int closed, struct evbuffer *buffer, void *data) +cmd_source_file_done(__unused struct client *oc, const char *path, + int error, int closed, struct evbuffer *buffer, void *data) { struct cmd_source_file_data *cdata = data; struct cmdq_item *item = cdata->item; + struct client *c = cdata->client; void *bdata = EVBUFFER_DATA(buffer); size_t bsize = EVBUFFER_LENGTH(buffer); u_int n; @@ -127,7 +142,7 @@ cmd_source_file_done(struct client *c, const char *path, int error, if (n < cdata->nfiles) file_read(c, cdata->files[n], cmd_source_file_done, cdata); else { - cmd_source_file_complete(c, cdata); + cmd_source_file_complete(cdata); cmdq_continue(item); } } @@ -187,6 +202,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) cdata = xcalloc(1, sizeof *cdata); cdata->item = item; + cdata->client = c; + if (c != NULL) + c->references++; if (args_has(args, 'q')) cdata->flags |= CMD_PARSE_QUIET; @@ -249,7 +267,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item) file_read(c, cdata->files[0], cmd_source_file_done, cdata); retval = CMD_RETURN_WAIT; } else - cmd_source_file_complete(c, cdata); + cmd_source_file_complete(cdata); free(cwd); return (retval); diff --git a/cmd-split-window.c b/cmd-split-window.c index 8beeac557..c907dd152 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.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:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bCdefhIklMOPvWZ] [-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); } @@ -202,6 +215,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) */ goto fail; } + if (args_has(args, 'K') && args_has(args, 'O')) + new_wp->flags |= PANE_CAPTUREALLKEYS; if (args_has(args, 'C') && args_has(args, 'O')) new_wp->flags |= PANE_CLOSEONCLICK; @@ -271,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); } @@ -322,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 89e7c9189..24f528d40 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,8 @@ 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, window_target; + int flags, visible, window_target; + int Zflag = args_has(args, 'Z'); struct client *c = cmdq_get_client(item); struct client *tc = cmdq_get_target_client(item); struct session *s; @@ -145,11 +146,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) if (window_target && 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 (window_target && wl != NULL) { diff --git a/control.c b/control.c index 7f76ff9e3..c3bb82b01 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -388,6 +388,25 @@ control_pause_pane(struct client *c, struct window_pane *wp) } } +/* + * Reset a pane after its buffer has been replaced: drop any output still + * queued from the old buffer and start again from the pane's own offset. + */ +void +control_reset_pane(struct client *c, struct window_pane *wp) +{ + struct control_pane *cp; + + if (c->control_state == NULL) + return; + cp = control_get_pane(c, wp); + if (cp == NULL) + return; + control_discard_pane(c, cp); + memcpy(&cp->offset, &wp->offset, sizeof cp->offset); + memcpy(&cp->queued, &wp->offset, sizeof cp->queued); +} + /* Write an already-formatted line, queueing it behind %output if needed. */ static void control_write_line(struct client *c, char *line) diff --git a/format.c b/format.c index 61c32a39a..bfc10d70e 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 @@ -1889,6 +1889,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) @@ -2309,6 +2340,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) @@ -3742,12 +3786,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 }, @@ -3937,6 +3990,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 }, @@ -4475,7 +4531,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 f9b60144d..471e4d58b 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 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; @@ -1978,7 +1979,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx) screen_write_mode_clear(sctx, MODE_BRACKETPASTE); break; case 2026: - screen_write_stop_sync(ictx->wp); + screen_write_end_sync(sctx); break; case 2031: screen_write_mode_clear(sctx, MODE_THEME_UPDATES); 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 208f9ad5c..fc996408f 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, @@ -1241,7 +1251,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 43bdbb61f..42babe237 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 cd891281d..b4217aa0a 100644 --- a/regress/modal-pane.sh +++ b/regress/modal-pane.sh @@ -105,6 +105,24 @@ drag() sleep 1 } +meta_drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + cleanup check_ok new-session -d -s modal -x 80 -y 24 'cat' @@ -127,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' @@ -159,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 @@ -303,6 +327,164 @@ 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" +check_ok kill-pane -t "$ignored" + +$TMUX set -g @modal-prefix no +$TMUX set -g @modal-root no +$TMUX bind -n z set -g @modal-root yes + +modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \ + -x 20 -y 5 -X 20 -Y 10 'cat') || + fail "new-pane -OK failed" +sleep 1 +$TMUX2 send-keys -t "$OUTER" C-b x z Enter +sleep 1 +must_equal "$($TMUX show -gv @modal-prefix)" no +must_equal "$($TMUX show -gv @modal-root)" no +case "$($TMUX capture-pane -pt "$modal")" in +*xz*) ;; +*) fail "keys did not reach key-capturing modal pane" ;; +esac +left=$(fmt "$modal" '#{pane_left}') +top=$(fmt "$modal" '#{pane_top}') +meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4)) +new_left=$(fmt "$modal" '#{pane_left}') +new_top=$(fmt "$modal" '#{pane_top}') +[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] || + fail "key-capturing modal pane did not move" +check_ok kill-pane -t "$modal" +sleep 1 + +# 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/respawn-pane-control-lag.sh b/regress/respawn-pane-control-lag.sh new file mode 100644 index 000000000..b71ce1946 --- /dev/null +++ b/regress/respawn-pane-control-lag.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +# respawn-pane frees the pane's buffer and creates an empty one. The pane +# offsets and each control client's offsets used to keep pointing into the old +# buffer, so if an attached control client had not caught up on the pane's +# output, the next read from the new process ran off the end of the new buffer +# and the server crashed in input_parse. +# +# Two ways for a client to be behind are tested: a control client whose output +# goes down a fifo that is never read (a second, healthy control client keeps +# the pane being read), and a healthy control client viewing a session that +# the pane's window has been moved out of. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +SERVER= + +mkfifo "$FIFO" || exit 1 + +cleanup() { + [ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +alive() { + if ! kill -0 "$SERVER" 2>/dev/null; then + SERVER= + echo "server died after $1" + exit 1 + fi + $TMUX has -t rt || exit 1 +} + +wait_clients() { + n=0 + while [ $n -lt 50 ]; do + [ "$($TMUX lsc 2>/dev/null | wc -l)" -ge $1 ] && return + sleep 0.1 + n=$((n + 1)) + done + echo "control clients did not attach"; exit 1 +} + +# A detached session with a pane that writes a line every 10 milliseconds. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt \ + 'while :; do echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; sleep 0.01; done' || + exit 1 +SERVER=$($TMUX display -pt rt '#{pid}') + +# One control client with output down the unread fifo and one reading +# normally; stdin of both held open by sleep so they stay attached. +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >/dev/null 2>&1 & +wait_clients 2 + +# Let the first client fall behind, then respawn the pane. The new process +# writes at once and the server must survive reading it. +sleep 3 +$TMUX respawn-pane -k -t rt:0 'echo respawned; sleep 60' || exit 1 +sleep 1 +alive "respawn-pane with a lagging control client" + +# Now a pane whose window is moved out of the session both clients view: their +# offsets for it stop advancing, so they fall behind until it is respawned. +$TMUX neww -d -t rt \ + 'while :; do echo yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy; sleep 0.01; done' || + exit 1 +sleep 1 +$TMUX new -d -s other || exit 1 +$TMUX movew -d -s rt:1 -t other: || exit 1 +sleep 2 +$TMUX respawn-pane -k -t other:1 'echo respawned; sleep 60' || exit 1 +sleep 1 +alive "respawn-pane on a window moved out of the clients' session" + +exit 0 diff --git a/regress/sync-output-atomic.sh b/regress/sync-output-atomic.sh new file mode 100644 index 000000000..842813d16 --- /dev/null +++ b/regress/sync-output-atomic.sh @@ -0,0 +1,173 @@ +#!/bin/sh + +# Application synchronized updates must remain one physical client transaction, +# including when BSU, printable cells and ESU arrive in one pane read. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +TMUX_TMPDIR=$DIR +export TMUX_TMPDIR + +INNER="$TEST_TMUX -Li$$ -f/dev/null" +OUTER="$TEST_TMUX -Lo$$ -f/dev/null" +CLIENT_BYTES=$DIR/client-bytes +CONTROL=$DIR/control +EMITTER=$DIR/emitter.pl +ASSERT=$DIR/assert-sync-output.pl + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $INNER list-clients -F '#{client_termfeatures}' 2>/dev/null | + grep -q 'sync' && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "sync-capable client did not attach" +} + +wait_for_marker() +{ + i=0 + while [ "$i" -lt 50 ]; do + grep -q 'FRAME_000001_ROW_23' "$CLIENT_BYTES" 2>/dev/null && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client did not receive both synchronized frames" +} + +wait_for_stable_bytes() +{ + previous=-1 + stable=0 + i=0 + while [ "$i" -lt 50 ]; do + current=$(wc -c <"$CLIENT_BYTES" 2>/dev/null) || current=0 + if [ "$current" -gt 0 ] && [ "$current" -eq "$previous" ]; then + stable=$((stable + 1)) + [ "$stable" -eq 5 ] && return 0 + else + stable=0 + fi + previous=$current + sleep 0.1 + i=$((i + 1)) + done + fail "client byte stream did not become stable" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +my $control = $ENV{CONTROL}; +(my $dir = $control) =~ s{/[^/]+$}{}; +open my $ready, '>', "$dir/ready" or die "$dir/ready: $!\n"; +close $ready; +while (!-e $control) { + select undef, undef, undef, 0.01; +} +for my $frame (0 .. 1) { + my @rows; + for my $row (0 .. 23) { + my $marker = sprintf "FRAME_%06d_ROW_%02d_", $frame, $row; + push @rows, substr($marker . ('X' x 79), 0, 79); + } + my $frame = "\e[?2026h\e[H" . join("\r\n", @rows) . + "\e[?2026l"; + my $written = syswrite STDOUT, $frame; + die "short synchronized frame write\n" + unless defined $written && $written == length $frame; + select undef, undef, undef, 0.2; +} +select undef, undef, undef, 2; +PERL + +$INNER new-session -d -s inner -x 80 -y 24 \ + "CONTROL='$CONTROL' perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -as terminal-features '*:sync' || exit 1 + +$OUTER new-session -d -s outer -x 80 -y 24 \ + "$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || + exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_for_client + +i=0 +while [ "$i" -lt 50 ] && [ ! -e "$DIR/ready" ]; do + sleep 0.1 + i=$((i + 1)) +done +[ -e "$DIR/ready" ] || fail "application emitter did not become ready" +$OUTER pipe-pane -O -t outer:0.0 "cat >'$CLIENT_BYTES'" || exit 1 +: >"$CONTROL" +wait_for_marker +wait_for_stable_bytes +$OUTER pipe-pane -t outer:0.0 || exit 1 + +cat >"$ASSERT" <<'PERL' +use strict; +use warnings; + +my $path = shift; +open my $fh, '<:raw', $path or die "$path: $!\n"; +local $/; +my $bytes = <$fh>; +my ($on, $off) = ("\e[?2026h", "\e[?2026l"); +my ($active, %seen); + +for (my $i = 0; $i < length($bytes);) { + if (substr($bytes, $i, length($on)) eq $on) { + die "duplicate synchronized-output start at byte $i\n" if $active; + $active = 1; + $i += length($on); + next; + } + if (substr($bytes, $i, length($off)) eq $off) { + die "unmatched synchronized-output end at byte $i\n" unless $active; + $active = 0; + $i += length($off); + next; + } + if (substr($bytes, $i) =~ /\AFRAME_(\d{6})_ROW_(\d{2})_/) { + my $marker = "$1:$2"; + die "marker $marker outside synchronized output\n" unless $active; + $seen{$marker}++; + } + $i++; +} +die "unterminated synchronized-output transaction\n" if $active; +for my $frame (0 .. 1) { + for my $row (0 .. 23) { + my $marker = sprintf "%06d:%02d", $frame, $row; + die "missing marker $marker\n" unless $seen{$marker}; + } +} +PERL + +perl "$ASSERT" "$CLIENT_BYTES" || exit 1 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 014318b3d..ecfb9c8ff 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 584bba93a..2336020e4 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -308,12 +308,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) @@ -341,10 +343,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); } @@ -1062,6 +1072,19 @@ screen_write_stop_sync(struct window_pane *wp) log_debug("%s: %%%u stopped sync mode", __func__, wp->id); } +/* Flush pending output before clearing sync mode. */ +void +screen_write_end_sync(struct screen_write_ctx *ctx) +{ + struct window_pane *wp = ctx->wp; + + if (wp == NULL) + return; + if (wp->base.mode & MODE_SYNC) + screen_write_collect_flush(ctx, 0, __func__); + screen_write_stop_sync(wp); +} + /* Cursor up by ny. */ void screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny) @@ -2842,10 +2865,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. */ @@ -2939,9 +2963,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 6e455d531..8ed7b1616 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.503 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.509 2026/08/28 07:36:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -45,6 +45,7 @@ static void server_client_set_path(struct client *); static void server_client_set_progress_bar(struct client *); static void server_client_reset_state(struct client *); static void server_client_update_latest(struct client *); +static int server_client_handle_dead_key(struct window_pane *, key_code); static void server_client_dispatch(struct imsg *, void *); static int server_client_dispatch_command(struct client *, struct imsg *); static int server_client_dispatch_identify(struct client *, struct imsg *); @@ -526,7 +527,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); @@ -803,8 +807,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) @@ -1391,6 +1394,21 @@ server_client_repeat_time(struct client *c, struct key_binding *bd) return (repeat); } +/* Handle a key press on a dead pane waiting for a key. */ +static int +server_client_handle_dead_key(struct window_pane *wp, key_code key) +{ + if (wp == NULL || + (~wp->flags & PANE_EXITED) || + KEYC_IS_MOUSE(key) || + KEYC_IS_PASTE(key) || + options_get_number(wp->options, "remain-on-exit") != 3) + return (0); + options_set_number(wp->options, "remain-on-exit", 0); + server_destroy_pane(wp, 0); + return (1); +} + /* * Handle data key input from client. This owns and can modify the key event it * is given and is responsible for freeing it. @@ -1476,6 +1494,18 @@ server_client_key_callback(struct cmdq_item *item, void *data) server_client_is_assume_paste(c)) goto paste_key; + /* Forward keys directly if this pane is capturing all keys. */ + if (wp != NULL && + (wp->flags & PANE_CAPTUREALLKEYS) && + (~wp->flags & PANE_EXITED) && + !KEYC_IS_MOUSE(key) && + TAILQ_EMPTY(&wp->modes)) + goto forward_key; + + /* 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. @@ -1640,15 +1670,8 @@ try_again: } forward_key: - if (wp != NULL && - (wp->flags & PANE_EXITED) && - !KEYC_IS_MOUSE(key) && - !KEYC_IS_PASTE(key) && - options_get_number(wp->options, "remain-on-exit") == 3) { - options_set_number(wp->options, "remain-on-exit", 0); - server_destroy_pane(wp, 0); + if (server_client_handle_dead_key(wp, key)) goto out; - } if (c->flags & CLIENT_READONLY) goto out; if (wp != NULL) @@ -1737,9 +1760,9 @@ server_client_handle_key0(struct client *c, struct key_event *event, } /* - * Key presses in overlay mode and the command prompt are a special - * case. The queue might be blocked so they need to be processed - * immediately rather than queued. + * Key presses in overlay mode, for panes capturing all keys and in the + * command prompt are a special case. The queue might be blocked so they + * need to be processed immediately rather than queued. */ if (~c->flags & CLIENT_READONLY) { if (c->message_string != NULL) { @@ -1759,6 +1782,21 @@ server_client_handle_key0(struct client *c, struct key_event *event, } server_client_clear_overlay(c); + + wp = s->curw->window->active; + if (wp != NULL && + (wp->flags & PANE_CAPTUREALLKEYS) && + TAILQ_EMPTY(&wp->modes) && + !KEYC_IS_MOUSE(event->key)) { + if (server_client_handle_dead_key(wp, event->key)) + return (0); + if (~wp->flags & PANE_EXITED) { + window_pane_key(wp, c, s, s->curw, event->key, + &event->m); + return (0); + } + } + if (server_client_handle_menu_key(c, event)) return (0); if (c->prompt != NULL) { @@ -2381,8 +2419,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. */ @@ -2459,7 +2495,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; @@ -2485,8 +2521,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; } @@ -2505,7 +2547,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); @@ -2535,7 +2578,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 fb0861849..d6245231d 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.151 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -223,10 +223,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); } } @@ -418,15 +419,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 @@ -548,6 +551,7 @@ server_check_unattached(void) continue; break; } + server_destroy_session(s); session_destroy(s, 1, __func__); } } diff --git a/server.c b/server.c index 7f9b203c5..27ff80c29 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */ +/* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -129,6 +129,7 @@ server_create_socket(uint64_t flags, char **cause) mask = umask(S_IXUSR|S_IRWXG|S_IRWXO); if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) { saved_errno = errno; + umask(mask); close(fd); errno = saved_errno; goto fail; 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 03553e70c..361d207e5 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.52 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -243,7 +243,7 @@ struct window_pane * spawn_pane(struct spawn_context *sc, char **cause) { struct cmdq_item *item = sc->item; - struct client *c; + struct client *c, *loop; struct session *s = sc->s; struct session *ts; struct window *w = sc->wl->window; @@ -331,6 +331,20 @@ spawn_pane(struct spawn_context *sc, char **cause) input_free(sc->wp0->ictx); sc->wp0->ictx = NULL; } + + /* + * The old buffer is gone and the new one starts empty, so + * offsets into the old buffer no longer mean anything. Reset + * them, and drop output control clients still had queued. + */ + sc->wp0->offset.used = 0; + sc->wp0->base_offset = 0; + sc->wp0->pipe_offset.used = 0; + TAILQ_FOREACH(loop, &clients, entry) { + if (loop->flags & CLIENT_CONTROL) + control_reset_pane(loop, sc->wp0); + } + new_wp = sc->wp0; new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN); } else { @@ -346,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 @@ -731,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); } @@ -751,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 b97db4cfe..9cd91cf9e 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.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 6 2026 $ +.Dd $Mdocdate: September 1 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2208,14 +2208,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) @@ -2289,7 +2289,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 @@ -2574,8 +2574,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. @@ -3644,7 +3644,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bCdefhIkLMOPvWZ +.Op Fl AbCdefhIkKLMOPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3693,14 +3693,17 @@ 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 panes while it is active. A window can only have one modal pane and it must be a floating pane. With +.Fl K , +all keys, including the prefix key, are passed directly to the modal pane. +With .Fl C , the modal pane is closed when the mouse is clicked outside it. .Pp @@ -4772,6 +4775,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 @@ -5068,6 +5080,10 @@ This is an array option where each entry is a colon-separated string made up of a terminal type pattern (matched using .Xr glob 7 patterns) followed by a list of terminal features. +A feature may be suffixed with +.Ql @ +to disable it; for example, +.Ql xterm*:sync@ . The available features are: .Bl -tag -width Ds .It 256 @@ -5118,6 +5134,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 @@ -5652,6 +5670,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: @@ -7324,7 +7347,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" @@ -7425,6 +7451,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.c b/tmux.c index c25e0d428..ab7c5133a 100644 --- a/tmux.c +++ b/tmux.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.c,v 1.222 2026/07/19 19:09:30 nicm Exp $ */ +/* $OpenBSD: tmux.c,v 1.223 2026/08/17 14:47:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -425,7 +425,7 @@ main(int argc, char **argv) while ((opt = getopt(argc, argv, "2c:CDdf:hlL:NqS:T:uUvV")) != -1) { switch (opt) { case '2': - tty_add_features(&feat, "256", ":,"); + tty_parse_features("256", ":,", &feat, NULL); break; case 'c': shell_command = optarg; @@ -473,7 +473,7 @@ main(int argc, char **argv) path = xstrdup(optarg); break; case 'T': - tty_add_features(&feat, optarg, ":,"); + tty_parse_features(optarg, ":,", &feat, NULL); break; case 'u': flags |= CLIENT_UTF8; diff --git a/tmux.h b/tmux.h index 4bc72d651..adab29828 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1423 2026/08/17 07:56:56 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -483,6 +483,7 @@ enum tty_code_code { TTYC_ICH1, TTYC_IL, TTYC_IL1, + TTYC_IND, TTYC_INDN, TTYC_INVIS, TTYC_KCBT, @@ -1335,6 +1336,8 @@ struct window_pane { #define PANE_CMDRUNNING 0x20000 #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; @@ -1354,9 +1357,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; @@ -1441,6 +1445,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; @@ -1483,7 +1488,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; @@ -1718,7 +1722,7 @@ struct key_event { struct tty_term { char *name; struct tty *tty; - int features; + int applied_features; char acs[UCHAR_MAX + 1][2]; @@ -1811,8 +1815,9 @@ struct tty { #define TTY_WAITFG 0x2000 #define TTY_WAITBG 0x4000 #define TTY_BRACKETPASTE 0x8000 +#define TTY_HAVESYNC 0x10000 #define TTY_ALL_REQUEST_FLAGS \ - (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA) + (TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC) int flags; struct tty_term *term; @@ -2226,6 +2231,7 @@ struct client { char *term_name; int term_features; + int term_nofeatures; char *term_type; char **term_caps; u_int term_ncaps; @@ -2286,7 +2292,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 @@ -2532,6 +2538,7 @@ struct spawn_context { #define SPAWN_HORIZONTAL 0x200 #define SPAWN_SPLIT 0x400 #define SPAWN_MODAL 0x800 +#define SPAWN_FLOATOVERZOOM 0x1000 }; /* Paste buffer. */ @@ -3036,8 +3043,7 @@ extern struct tty_terms tty_terms; u_int tty_term_ncodes(void); void tty_term_apply(struct tty_term *, const char *, int); void tty_term_apply_overrides(struct tty_term *); -struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, int *, - char **); +struct tty_term *tty_term_create(struct tty *, char *, char **, u_int, char **); void tty_term_free(struct tty_term *); int tty_term_read_list(const char *, int, char ***, u_int *, char **); @@ -3059,11 +3065,13 @@ int tty_term_flag(struct tty_term *, enum tty_code_code); const char *tty_term_describe(struct tty_term *, enum tty_code_code); /* tty-features.c */ -void tty_add_features(int *, const char *, const char *); +void tty_parse_client_features(struct client *, const char *, + const char *); +void tty_parse_features(const char *, const char *, int *, int *); const char *tty_get_features(int); int tty_feature_present(struct tty_term *, const char *); -int tty_apply_features(struct tty_term *, int); -void tty_default_features(int *, const char *, u_int); +int tty_apply_features(struct tty_term *); +void tty_default_features(struct client *, const char *, u_int); /* tty-acs.c */ int tty_acs_needed(struct tty *); @@ -3635,6 +3643,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int); void screen_write_mode_clear(struct screen_write_ctx *, int); void screen_write_start_sync(struct window_pane *); void screen_write_stop_sync(struct window_pane *); +void screen_write_end_sync(struct screen_write_ctx *); void screen_write_clear_dirty(struct window_pane *); void screen_write_cursorup(struct screen_write_ctx *, u_int); void screen_write_cursordown(struct screen_write_ctx *, u_int); @@ -3764,8 +3773,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 *); @@ -4070,6 +4079,7 @@ void control_set_pane_on(struct client *, struct window_pane *); void control_set_pane_off(struct client *, struct window_pane *); void control_continue_pane(struct client *, struct window_pane *); void control_pause_pane(struct client *, struct window_pane *); +void control_reset_pane(struct client *, struct window_pane *); void control_set_window_size(struct client *, u_int, u_int, u_int); int control_get_window_size(struct client *, u_int, u_int *, u_int *); void control_clear_window_size(struct client *, u_int); @@ -4134,6 +4144,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 263894c6c..0b12745e2 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.41 2026/08/17 07:52:16 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,20 +397,33 @@ 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. */ void -tty_add_features(int *feat, const char *s, const char *separators) +tty_parse_client_features(struct client *c, const char *s, const char *sep) +{ + tty_parse_features(s, sep, &c->term_features, &c->term_nofeatures); +} + +/* Parse features list. */ +void +tty_parse_features(const char *s, const char *sep, int *enabled, int *disabled) { const struct tty_feature *tf; char *next, *loop, *copy; u_int i; + int remove; log_debug("adding terminal features %s", s); loop = copy = xstrdup(s); - while ((next = strsep(&loop, separators)) != NULL) { + while ((next = strsep(&loop, sep)) != NULL) { + remove = (*next != '\0' && next[strlen(next) - 1] == '@'); + if (remove) + next[strlen(next) - 1] = '\0'; for (i = 0; i < nitems(tty_features); i++) { tf = tty_features[i]; if (strcasecmp(tf->name, next) == 0) @@ -413,14 +433,24 @@ tty_add_features(int *feat, const char *s, const char *separators) log_debug("unknown terminal feature: %s", next); break; } - if (~(*feat) & (1 << i)) { + if (remove) { + log_debug("removing terminal feature: %s", tf->name); + *enabled &= ~(1 << i); + if (disabled != NULL) + *disabled |= 1 << i; + continue; + } + if (disabled != NULL && *disabled & (1 << i)) + continue; + if (~(*enabled) & (1 << i)) { log_debug("adding terminal feature: %s", tf->name); - (*feat) |= (1 << i); + (*enabled) |= (1 << i); } } free(copy); } +/* Get features as string. */ const char * tty_get_features(int feat) { @@ -442,6 +472,7 @@ tty_get_features(int feat) return (s); } +/* Check if feature is present. */ int tty_feature_present(struct tty_term *term, const char *name) { @@ -450,11 +481,14 @@ 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) { - if (term->features & (1 << i)) - return (1); + if (term->applied_features & (1 << i)) + return (1); break; } } @@ -463,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); @@ -481,19 +516,23 @@ tty_feature_present(struct tty_term *term, const char *name) return (1); } +/* Apply featurs to terminal. */ int -tty_apply_features(struct tty_term *term, int feat) +tty_apply_features(struct tty_term *term) { + struct client *c = term->tty->client; const struct tty_feature *tf; const char *const *capability; + int feat; u_int i; + feat = (c->term_features & ~c->term_nofeatures); if (feat == 0) return (0); log_debug("applying terminal features: %s", tty_get_features(feat)); for (i = 0; i < nitems(tty_features); i++) { - if ((term->features & (1 << i)) || (~feat & (1 << i))) + if ((term->applied_features & (1 << i)) || (~feat & (1 << i))) continue; tf = tty_features[i]; @@ -507,15 +546,18 @@ tty_apply_features(struct tty_term *term, int feat) } } term->flags |= tf->flags; + if (tf == &tty_feature_utf8) + c->flags |= CLIENT_UTF8; } - if ((term->features | feat) == term->features) + if ((term->applied_features|feat) == term->applied_features) return (0); - term->features |= feat; + term->applied_features |= feat; return (1); } +/* Add default features for a terminal identified by name and version. */ void -tty_default_features(int *feat, const char *name, u_int version) +tty_default_features(struct client *c, const char *name, u_int version) { static const struct { const char *name; @@ -628,6 +670,6 @@ tty_default_features(int *feat, const char *name, u_int version) continue; if (version != 0 && version < table[i].version) continue; - tty_add_features(feat, table[i].features, ","); + tty_parse_client_features(c, table[i].features, ","); } } diff --git a/tty-keys.c b/tty-keys.c index 0d95208a6..d0abc3894 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.212 2026/08/17 07:52:16 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.214 2026/08/18 09:01:20 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t, size_t *); static int tty_keys_extended_device_attributes(struct tty *, const char *, size_t, size_t *); +static int tty_keys_sync(struct tty *, const char *, size_t, size_t *); static int tty_keys_palette(struct tty *, const char *, size_t, size_t *); /* A key tree entry. */ @@ -771,6 +772,17 @@ tty_keys_next(struct tty *tty) goto partial_key; } + /* Is this a synchronized update mode response? */ + switch (tty_keys_sync(tty, buf, len, &size)) { + case 0: /* yes */ + key = KEYC_UNKNOWN; + goto complete_key; + case -1: /* no, or not valid */ + break; + case 1: /* partial */ + goto partial_key; + } + /* Is this a primary device attributes response? */ switch (tty_keys_device_attributes(tty, buf, len, &size)) { case 0: /* yes */ @@ -1447,7 +1459,6 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i, n = 0; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; @@ -1504,13 +1515,13 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, for (i = 1; i < n; i++) { log_debug("%s: DA feature: %d", c->name, p[i]); if (p[i] == 4) - tty_add_features(features, "sixel", ","); + tty_parse_client_features(c, "sixel", ","); if (p[i] == 21) - tty_add_features(features, "margins", ","); + tty_parse_client_features(c, "margins", ","); if (p[i] == 28) - tty_add_features(features, "rectfill", ","); + tty_parse_client_features(c, "rectfill", ","); if (p[i] == 52) - tty_add_features(features, "clipboard", ","); + tty_parse_client_features(c, "clipboard", ","); } break; } @@ -1522,6 +1533,54 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, return (0); } +/* + * Handle a synchronized update mode response. Returns 0 for success, -1 for + * failure, 1 for partial. + */ +static int +tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size) +{ + struct client *c = tty->client; + static const char prefix[] = "\033[?2026;"; + size_t i; + int status; + + *size = 0; + if (tty->flags & TTY_HAVESYNC) + return (-1); + + /* The response is always \033[?2026;Ps$y. */ + for (i = 0; i < (sizeof prefix) - 1; i++) { + if (i == len) + return (1); + if (buf[i] != prefix[i]) + return (-1); + } + if (i == len) + return (1); + if (buf[i] < '0' || buf[i] > '4') + return (-1); + status = buf[i++] - '0'; + if (i == len) + return (1); + if (buf[i++] != '$') + return (-1); + if (i == len) + return (1); + if (buf[i++] != 'y') + return (-1); + *size = i; + + if (status == 1 || status == 2 || status == 3) { + tty_parse_client_features(c, "sync", ","); + tty_update_features(tty); + } + log_debug("%s: received DECRPM %.*s", c->name, (int)*size, buf); + tty->flags |= TTY_HAVESYNC; + + return (0); +} + /* * Handle secondary device attributes input. Returns 0 for success, -1 for * failure, 1 for partial. @@ -1531,7 +1590,6 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i, n = 0; char tmp[128], *endptr, p[32] = { 0 }, *cp, *next; @@ -1585,13 +1643,13 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, */ switch (p[0]) { case 'M': /* mintty */ - tty_default_features(features, "mintty", 0); + tty_default_features(c, "mintty", 0); break; case 'T': /* tmux */ - tty_default_features(features, "tmux", 0); + tty_default_features(c, "tmux", 0); break; case 'U': /* rxvt-unicode */ - tty_default_features(features, "rxvt-unicode", 0); + tty_default_features(c, "rxvt-unicode", 0); break; } log_debug("%s: received secondary DA %.*s", c->name, (int)*size, buf); @@ -1611,7 +1669,6 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, size_t len, size_t *size) { struct client *c = tty->client; - int *features = &c->term_features; u_int i; char tmp[128]; @@ -1654,21 +1711,21 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, /* Add terminal features. */ if (strncmp(tmp, "iTerm2 ", 7) == 0) - tty_default_features(features, "iTerm2", 0); + tty_default_features(c, "iTerm2", 0); else if (strncmp(tmp, "tmux ", 5) == 0) - tty_default_features(features, "tmux", 0); + tty_default_features(c, "tmux", 0); else if (strncmp(tmp, "XTerm(", 6) == 0) - tty_default_features(features, "XTerm", 0); + tty_default_features(c, "XTerm", 0); else if (strncmp(tmp, "mintty ", 7) == 0) - tty_default_features(features, "mintty", 0); + tty_default_features(c, "mintty", 0); else if (strncmp(tmp, "foot(", 5) == 0) - tty_default_features(features, "foot", 0); + tty_default_features(c, "foot", 0); else if (strncmp(tmp, "WezTerm ", 7) == 0) - tty_default_features(features, "WezTerm", 0); + tty_default_features(c, "WezTerm", 0); else if (strncmp(tmp, "ghostty ", 8) == 0) - tty_default_features(features, "ghostty", 0); + tty_default_features(c, "ghostty", 0); else if (strncmp(tmp, "Rio ", 4) == 0) - tty_default_features(features, "Rio", 0); + tty_default_features(c, "Rio", 0); log_debug("%s: received extended DA %.*s", c->name, (int)*size, buf); free(c->term_type); diff --git a/tty-term.c b/tty-term.c index 850adf31c..6b149fcee 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.107 2026/08/05 08:54:56 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.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" }, @@ -544,8 +545,9 @@ tty_term_validate(struct tty_term *term) struct tty_term * tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, - int *feat, char **cause) + char **cause) { + struct client *c = tty->client; struct tty_term *term; const struct tty_term_code_entry *ent; struct tty_code *code; @@ -617,7 +619,7 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, offset = 0; first = tty_term_override_next(s, &offset); if (first != NULL && fnmatch(first, term->name, 0) == 0) - tty_add_features(feat, s + offset, ":"); + tty_parse_client_features(c, s + offset, ":"); a = options_array_next(a); } @@ -627,14 +629,14 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, del_curterm(cur_term); #endif /* Check for COLORTERM. */ - envent = environ_find(tty->client->environ, "COLORTERM"); + envent = environ_find(c->environ, "COLORTERM"); if (envent != NULL) { - log_debug("%s COLORTERM=%s", tty->client->name, envent->value); + log_debug("%s COLORTERM=%s", c->name, envent->value); if (strcasecmp(envent->value, "truecolor") == 0 || strcasecmp(envent->value, "24bit") == 0) - tty_add_features(feat, "RGB", ","); + tty_parse_client_features(c, "RGB", ","); else if (strstr(envent->value, "256") != NULL) - tty_add_features(feat, "256", ","); + tty_parse_client_features(c, "256", ","); } /* Apply overrides so any capabilities used for features are changed. */ @@ -665,17 +667,17 @@ tty_term_create(struct tty *tty, char *name, char **caps, u_int ncaps, s = tty_term_string(term, TTYC_CLEAR); if (tty_term_flag(term, TTYC_XT) || strncmp(s, "\033[", 2) == 0) { term->flags |= TERM_VT100LIKE; - tty_add_features(feat, "bpaste,focus,title", ","); + tty_parse_client_features(c, "bpaste,focus,title", ","); } /* Add RGB feature if terminal has RGB colours. */ if ((tty_term_flag(term, TTYC_TC) || tty_term_has(term, TTYC_RGB)) && (!tty_term_has(term, TTYC_SETRGBF) || !tty_term_has(term, TTYC_SETRGBB))) - tty_add_features(feat, "RGB", ","); + tty_parse_client_features(c, "RGB", ","); /* Apply the features and overrides again. */ - if (tty_apply_features(term, *feat)) + if (tty_apply_features(term)) tty_term_apply_overrides(term); /* Log the capabilities. */ diff --git a/tty.c b/tty.c index 8b51f6b24..e0fa96831 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.477 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.480 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -277,7 +277,7 @@ tty_open(struct tty *tty, char **cause) struct client *c = tty->client; tty->term = tty_term_create(tty, c->term_name, c->term_caps, - c->term_ncaps, &c->term_features, cause); + c->term_ncaps, cause); if (tty->term == NULL) { tty_close(tty); return (-1); @@ -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); @@ -403,6 +415,8 @@ tty_send_requests(struct tty *tty) tty_puts(tty, "\033[>c"); if (~tty->flags & TTY_HAVEXDA) tty_puts(tty, "\033[>q"); + if (~tty->flags & TTY_HAVESYNC) + tty_puts(tty, "\033[?2026$p"); tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\"); tty->flags |= (TTY_WAITBG|TTY_WAITFG); } else @@ -470,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)); @@ -495,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"); @@ -536,7 +554,7 @@ tty_update_features(struct tty *tty) { struct client *c = tty->client; - if (tty_apply_features(tty->term, c->term_features)) + if (tty_apply_features(tty->term)) tty_term_apply_overrides(tty->term); if (tty_use_margin(tty)) 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 329dc6908..e18643a6e 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 f1104b4f3..e65e59606 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 @@ -1009,6 +1009,7 @@ window_panes_run_command(struct window_panes_modedata *data, struct client *c, } else { new_item = cmdq_get_command(cmdlist, NULL); cmdq_append(c, new_item); + cmd_list_free(cmdlist); } free(expanded); } diff --git a/window-switch.c b/window-switch.c index 6712d8a89..f9cb16ec5 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 2559a530d..37d92e919 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 @@ -722,7 +722,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; @@ -923,13 +923,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; @@ -940,6 +945,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); @@ -951,11 +972,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; @@ -966,6 +1002,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) { @@ -977,48 +1029,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); } @@ -1070,8 +1143,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) { @@ -1098,7 +1171,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); } @@ -1106,13 +1178,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); } @@ -1268,6 +1336,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'; @@ -1413,6 +1483,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; @@ -1480,16 +1552,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); } @@ -1974,7 +2046,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