diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 9bce72248..902783e08 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -16,8 +16,10 @@ concurrency: 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 8f6633624..b1e19e9e0 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -16,7 +16,8 @@ jobs: regress: name: ${{ matrix.name }} runs-on: ${{ matrix.runner }} - timeout-minutes: 45 + if: github.repository == 'tmux/tmux' + timeout-minutes: 300 strategy: fail-fast: false @@ -37,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' @@ -61,6 +64,7 @@ jobs: automake \ bison \ libevent \ + jemalloc \ make \ ncurses \ utf8proc \ @@ -79,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 eaf7576da..a447de2c3 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,4 @@ -CHANGES FROM 3.7b TO 3.8 +CHANGES FROM 3.7c TO 3.8 * Many improvements to floating panes: @@ -9,7 +9,12 @@ CHANGES FROM 3.7b TO 3.8 - modal panes may be created with new-pane -O; a window can have one modal pane and it prevents interaction with the other panes while it is active. - A modal pane is now used for the editor in buffer mode; + -C closes the pane when the mouse is clicked outside it and -K sends all + keys, including the prefix key, to the pane. A modal pane is now used for + the editor in buffer mode; + + - new-pane -A creates a floating pane which remains above a zoomed pane; + modal panes do this by default; - move-pane and resize-pane can now move and resize floating panes, including mouse dragging; @@ -24,6 +29,9 @@ CHANGES FROM 3.7b TO 3.8 -k for choose commands kills the pane when the mode exits, which is useful for modes in floating panes; + - command-prompt -P opens a prompt inside a pane rather than on the status + line; + - pane-border-status has top-floating and bottom-floating, and there are new default bindings under C-b g for common move and resize operations; @@ -74,7 +82,7 @@ CHANGES FROM 3.7b TO 3.8 - new hooks to cover areas including client create and destroy, pane activity, pane mode and prompt changes, pane movement and resizing, session - group changes and window creation, close and zoom; + group changes and window creation, close, swap and zoom; - OSC 133 escape sequences now trigger events: pane-command-started, pane-command-finished and pane-shell-prompt. @@ -87,6 +95,27 @@ CHANGES FROM 3.7b TO 3.8 while scrolling or while hovered and disappears after pane-scrollbars-timeout (Michael Grant). +* Add a clear-on-attach server option. When disabled, tmux does not enter the + alternate screen on attach and instead scrolls the existing terminal content + into the scrollback buffer so it remains accessible (issue 5508). + +* Copy mode can now automatically refresh as pane content changes with the + refresh-on, refresh-off and refresh-toggle commands. Automatic refresh is off + by default; refresh-now refreshes once and is bound to r. These replace + refresh-from-pane (issue 5165). + +* Extend the fill-character option so both inside and outside the window can be + changed, with a new default for unused areas inside the window. + +* Change show-options and show-hooks to use formats and add a -F flag to each. + +* Allow individual terminal features to be disabled by adding @ to their names + in terminal-features, and add a utf8 feature for terminals which support + UTF-8 output. + +* Add a #{A/count:frames} modifier to show a series of frames as an animation + in the status line (issue 5412 from Fernando Daciuk). + * Change command templates so %% is escaped for single quotes while %1 remains unquoted, preventing single quotes in session or target names from terminating quoted command sections (reported by Aliz Hammond). @@ -97,6 +126,8 @@ CHANGES FROM 3.7b TO 3.8 * Add new-window -E, respawn-pane -E and respawn-window -E as more convenient methods to create an empty pane (rather than using '' for the command). +* Add a default C-b T binding to change the current pane title. + * Menus now belong to the window, so appear on all clients. * Dragging over an existing copy mode selection now adjusts it (Michael Grant). @@ -110,7 +141,12 @@ 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 copy-mode-current-line-style to set the style of the line containing the + cursor in copy mode (issue 5391). + +* Copy mode word commands now recognise all Unicode whitespace characters; a + space in word-separators matches any Unicode whitespace character (issue + 5562). * Add style attributes for dimming colours (dim=) and for hyperlinks (link= and nolink) (issues 4842 and 4280 from Moritz Angermann). @@ -122,7 +158,9 @@ CHANGES FROM 3.7b TO 3.8 * Add or improve format variables including client_colours, pane_start_command_list, window_manual_width, window_manual_height, - pane_last_output_time, pane_modal_flag and window_modal_pane. + pane_last_output_time, pane_modal_flag, window_modal_pane, pane_private_modes, + pane_output_generation, history_added, history_collected, history_generation, + hook_fire_count and hook_fire_time. * Add additional pane sort orders, and a z sort order for choose-tree so floating panes can be sorted by z-index. @@ -139,7 +177,10 @@ CHANGES FROM 3.7b TO 3.8 * Fix control mode clients hanging on exit if pty data was still queued (Ben Maurer, issue 5356), and avoid sending notifications to clients which are - already exiting (Ben Maurer, issue 5357). + already exiting (Ben Maurer, issue 5357). Queue notifications so they are not + sent inside %begin/%end (issue 5458), do not let a stuck client prevent the + server from exiting (issue 5444), and reset control mode offsets when a pane + is respawned (issue 5498). * Fix grouped sessions sometimes being left as unusable command targets while they are being killed (Bryce Miller, issue 5180). @@ -158,7 +199,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/cfg.c b/cfg.c index 8ab128184..78d164363 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cfg.c,v 1.90 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cfg.c,v 1.91 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -223,13 +223,15 @@ cfg_print_causes(struct cmdq_item *item) { struct client *c = cmdq_get_client(item); u_int i; + char *cause; for (i = 0; i < cfg_ncauses; i++) { + cause = cfg_causes[i]; if (c != NULL && (c->flags & CLIENT_CONTROL)) - control_write(c, "%%config-error %s", cfg_causes[i]); + control_notify_write(c, "%%config-error %s", cause); else - cmdq_print(item, "%s", cfg_causes[i]); - free(cfg_causes[i]); + cmdq_print(item, "%s", cause); + free(cause); } free(cfg_causes); @@ -244,14 +246,16 @@ cfg_show_causes(struct session *s) struct window_pane *wp; struct window_mode_entry *wme; u_int i; + char *cause; if (cfg_ncauses == 0) return; if (c != NULL && (c->flags & CLIENT_CONTROL)) { for (i = 0; i < cfg_ncauses; i++) { - control_write(c, "%%config-error %s", cfg_causes[i]); - free(cfg_causes[i]); + cause = cfg_causes[i]; + control_notify_write(c, "%%config-error %s", cause); + free(cause); } goto out; } diff --git a/client.c b/client.c index c7f574159..97f0538ad 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -276,7 +276,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, proc_set_signals(client_proc, client_signal); /* Save the flags. */ - client_flags = flags; + client_flags = flags|CLIENT_WRITE_ACK; log_debug("flags are %#llx", (unsigned long long)client_flags); /* Initialize the client socket and start the server. */ @@ -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-break-pane.c b/cmd-break-pane.c index 4fe6397c9..f2f1aff25 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.74 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.76 2026/08/17 07:04:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -172,14 +172,16 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->name = clean_name(name, 0); options_set_number(w->options, "automatic-rename", 0); } + window_set_fill_cells(w); + + if (idx == -1) + idx = -1 - options_get_number(dst_s->options, "base-index"); + wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ layout_init(w, wp); wp->flags |= PANE_CHANGED; colour_palette_from_option(&wp->palette, wp->options); - if (idx == -1) - idx = -1 - options_get_number(dst_s->options, "base-index"); - wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ window_remove_ref(w, __func__); events_fire_window("window-created", w); window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); diff --git a/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 2aaf3238c..0600c610c 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -591,6 +591,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) server_client_clear_overlay(tc); return (CMD_RETURN_NORMAL); } + if (tc->flags & CLIENT_CONTROL) + return (CMD_RETURN_NORMAL); if (!modify && tc->overlay_draw != NULL) return (CMD_RETURN_NORMAL); diff --git a/cmd-if-shell.c b/cmd-if-shell.c index 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-join-pane.c b/cmd-join-pane.c index 3ecd0fd2b..34bc95c8e 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.73 2026/07/17 15:24:30 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -431,25 +431,23 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_move_pane_entry) { if (args_has(args, 'M')) return (cmd_join_pane_mouse_update(item)); - if (!window_pane_is_floating(dst_wp)) { - cmdq_error(item, "pane is not floating"); - return (CMD_RETURN_ERROR); - } - if ((s = args_get(args, 'P')) != NULL) { - server_unzoom_window(dst_w); - return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); - } - if ((s = args_get(args, 'z')) != NULL) { - server_unzoom_window(dst_w); - return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s)); - } - if (args_has(args, 'X') || + if (args_has(args, 'P') || + args_has(args, 'z') || + args_has(args, 'X') || args_has(args, 'Y') || args_has(args, 'U') || args_has(args, 'D') || args_has(args, 'L') || args_has(args, 'R')) { + if (!window_pane_is_floating(dst_wp)) { + cmdq_error(item, "pane is not floating"); + return (CMD_RETURN_ERROR); + } server_unzoom_window(dst_w); + if ((s = args_get(args, 'P')) != NULL) + return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); + if ((s = args_get(args, 'z')) != NULL) + return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s)); return (cmd_join_pane_move(item, args, dst_wl, dst_wp)); } } diff --git a/cmd-new-window.c b/cmd-new-window.c index d1e2ab4e5..6ec55fe64 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-new-window.c,v 1.103 2026/07/08 08:07:42 nicm Exp $ */ +/* $OpenBSD: cmd-new-window.c,v 1.104 2026/09/03 21:04:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -60,7 +60,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) struct client *tc = cmdq_get_target_client(item); struct session *s = target->s; struct winlink *wl = target->wl, *new_wl = NULL; - int idx = target->idx, before, count = args_count(args); + int idx = target->idx, before; + int count = args_count(args); char *cause = NULL, *cp, *expanded, *wname = NULL; const char *template, *name; struct cmd_find_state fs; @@ -74,8 +75,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) } /* - * If -S and -n are given and -t is not and a single window with this - * name already exists, select it. + * If -S is given, select an existing window instead of creating a + * new one: preferring -t if it already points at a window, or + * otherwise -n if one exists with that name. If neither matches, + * fall through and create a window as normal. */ name = args_get(args, 'n'); if (name != NULL) { @@ -88,32 +91,39 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) wname = clean_name(expanded, 0); free(expanded); } - if (args_has(args, 'S') && wname != NULL && target->idx == -1) { - expanded = format_single(item, wname, c, s, NULL, NULL); - RB_FOREACH(wl, winlinks, &s->windows) { - if (strcmp(wl->window->name, expanded) != 0) - continue; - if (new_wl == NULL) { - new_wl = wl; - continue; + if (args_has(args, 'S')) { + if (idx != -1) + new_wl = winlink_find_by_index(&s->windows, idx); + else if (wname != NULL) { + expanded = format_single(item, wname, c, s, NULL, NULL); + RB_FOREACH(wl, winlinks, &s->windows) { + if (strcmp(wl->window->name, expanded) != 0) + continue; + if (new_wl == NULL) { + new_wl = wl; + continue; + } + cmdq_error(item, "multiple windows named %s", + wname); + free(wname); + free(expanded); + return (CMD_RETURN_ERROR); } - cmdq_error(item, "multiple windows named %s", wname); - free(wname); free(expanded); - return (CMD_RETURN_ERROR); } - free(expanded); - if (new_wl != NULL) { - free(wname); - if (args_has(args, 'd')) - return (CMD_RETURN_NORMAL); - if (session_set_current(s, new_wl) == 0) - server_redraw_session(s); - if (c != NULL && c->session != NULL) - s->curw->window->latest = c; - recalculate_sizes(); + } + + /* Found an existing window to select instead of creating a new one. */ + if (new_wl != NULL) { + free(wname); + if (args_has(args, 'd')) return (CMD_RETURN_NORMAL); - } + if (session_set_current(s, new_wl) == 0) + server_redraw_session(s); + if (c != NULL && c->session != NULL) + s->curw->window->latest = c; + recalculate_sizes(); + return (CMD_RETURN_NORMAL); } before = args_has(args, 'b'); 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-pipe-pane.c b/cmd-pipe-pane.c index d96d6c7d2..1041c2199 100644 --- a/cmd-pipe-pane.c +++ b/cmd-pipe-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-pipe-pane.c,v 1.63 2026/04/28 08:47:55 nicm Exp $ */ +/* $OpenBSD: cmd-pipe-pane.c,v 1.64 2026/07/27 14:25:46 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -194,9 +194,13 @@ static void cmd_pipe_pane_read_callback(__unused struct bufferevent *bufev, void *data) { struct window_pane *wp = data; - struct evbuffer *evb = wp->pipe_event->input; + struct evbuffer *evb; size_t available; + if (wp->pipe_event == NULL) + return; + evb = wp->pipe_event->input; + available = EVBUFFER_LENGTH(evb); log_debug("%%%u pipe read %zu", wp->id, available); diff --git a/cmd-queue.c b/cmd-queue.c index 82cbf4309..a9ce469bc 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-queue.c,v 1.121 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -802,7 +802,7 @@ cmdq_guard(struct cmdq_item *item, const char *guard, int flags) u_int number = item->number; if (c != NULL && (c->flags & CLIENT_CONTROL)) - control_write(c, "%%%s %ld %u %d", guard, t, number, flags); + control_write_guard(c, guard, t, number, flags); } /* Show message from command. */ @@ -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 59d5aeea6..7e0c62e56 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 @@ -233,9 +233,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 dbf147cb5..e23ca0adc 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -160,6 +160,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) char *title; const char *style; struct options_entry *o; + int visible, Zflag = args_has(args, 'Z'); if (entry == &cmd_last_pane_entry || args_has(args, 'l')) { /* @@ -185,14 +186,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) server_redraw_window_borders(lastwp->window); server_status_window(lastwp->window); } else { - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && lastwp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(lastwp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, lastwp); if (window_set_active_pane(w, lastwp, 1)) { cmd_find_from_winlink(current, wl, 0); cmd_select_pane_redraw(w); } - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); } return (CMD_RETURN_NORMAL); @@ -268,14 +273,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (wp == w->active) return (CMD_RETURN_NORMAL); - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && wp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(wp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, wp); if (window_set_active_pane(w, wp, 1)) cmd_find_from_winlink_pane(current, wl, wp, 0); cmdq_insert_hook(s, item, current, "after-select-pane"); cmd_select_pane_redraw(w); - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); return (CMD_RETURN_NORMAL); diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 6e640fec6..3e540f706 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "%s", cause); goto fail; } + free(bufname); return (CMD_RETURN_NORMAL); } @@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) cmdq_error(item, "no data specified"); goto fail; } - if ((newsize = strlen(args_string(args, 0))) == 0) + if ((newsize = strlen(args_string(args, 0))) == 0) { + free(bufname); return (CMD_RETURN_NORMAL); + } if (args_has(args, 'a') && pb != NULL) { olddata = paste_buffer_data(pb, &bufsize); @@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'w') && tc != NULL) tty_set_selection(&tc->tty, "", bufdata, bufsize); + free(bufname); return (CMD_RETURN_NORMAL); fail: diff --git a/cmd-show-options.c b/cmd-show-options.c index 94017a6be..cba601ad8 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-options.c,v 1.73 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-show-options.c,v 1.76 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -27,14 +27,23 @@ * Show options. */ +#define SHOW_OPTIONS_TEMPLATE \ + "#{?option_value_only," \ + "#{option_value}," \ + "#{option_name}#{?option_has_array_key," \ + "[#{option_array_key}],}" \ + "#{?option_is_parent,*,}" \ + "#{?option_has_value, " \ + "#{?option_is_string,#{q/a:option_value},#{option_value}},}}" +#define SHOW_HOOKS_MONITOR_TEMPLATE \ + "#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}" + static enum cmd_retval cmd_show_options_exec(struct cmd *, struct cmdq_item *); static void cmd_show_options_print(struct cmd *, struct cmdq_item *, struct options_entry *, const char *, int); -static void cmd_show_hooks_print_monitor(struct cmdq_item *, - struct options_entry *); -static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *, - int, struct options *); +static void cmd_show_hooks_print_monitor(struct cmd *, + struct cmdq_item *, struct options_entry *); static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *, int, struct options *); @@ -42,8 +51,8 @@ const struct cmd_entry cmd_show_options_entry = { .name = "show-options", .alias = "show", - .args = { "AgHpqst:vw", 0, 1, NULL }, - .usage = "[-AgHpqsvw] " CMD_TARGET_PANE_USAGE " [option]", + .args = { "AgF:Hpqst:vw", 0, 1, NULL }, + .usage = "[-AgHpqsvw] [-F format] " CMD_TARGET_PANE_USAGE " [option]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -55,8 +64,8 @@ const struct cmd_entry cmd_show_window_options_entry = { .name = "show-window-options", .alias = "showw", - .args = { "gvt:", 0, 1, NULL }, - .usage = "[-gv] " CMD_TARGET_WINDOW_USAGE " [option]", + .args = { "F:gvt:", 0, 1, NULL }, + .usage = "[-gv] [-F format] " CMD_TARGET_WINDOW_USAGE " [option]", .target = { 't', CMD_FIND_WINDOW, CMD_FIND_CANFAIL }, @@ -68,8 +77,8 @@ const struct cmd_entry cmd_show_hooks_entry = { .name = "show-hooks", .alias = NULL, - .args = { "Bgpt:w", 0, 1, NULL }, - .usage = "[-Bgpw] " CMD_TARGET_PANE_USAGE " [hook]", + .args = { "BF:gpt:w", 0, 1, NULL }, + .usage = "[-Bgpw] [-F format] " CMD_TARGET_PANE_USAGE " [hook]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -85,7 +94,7 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) struct options *oo; char *argument, *name = NULL, *cause; char *array_key = NULL; - int window, ambiguous, parent, scope; + int window, ambiguous, parent, print_parent, scope; struct options_entry *o; window = (cmd_get_entry(self) == &cmd_show_window_options_entry); @@ -101,8 +110,14 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } if (cmd_get_entry(self) == &cmd_show_hooks_entry && - args_has(args, 'B')) - return (cmd_show_hooks_monitor(self, item, scope, oo)); + args_has(args, 'B')) { + o = options_first(oo); + while (o != NULL) { + cmd_show_hooks_print_monitor(self, item, o); + o = options_next(o); + } + return (CMD_RETURN_NORMAL); + } return (cmd_show_options_all(self, item, scope, oo)); } argument = format_single_from_target(item, args_string(args, 0)); @@ -135,11 +150,17 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) if (o != NULL) { if (cmd_get_entry(self) == &cmd_show_hooks_entry && args_has(args, 'B')) - cmd_show_hooks_print_monitor(item, o); - else - cmd_show_options_print(self, item, o, array_key, parent); - } - else if (*name == '@') { + cmd_show_hooks_print_monitor(self, item, o); + else { + print_parent = parent; + if (array_key == NULL && options_is_array(o) && + options_array_first(o) == NULL) { + print_parent = 0; + } + cmd_show_options_print(self, item, o, array_key, + print_parent); + } + } else if (*name == '@') { if (args_has(args, 'q')) goto out; cmdq_error(item, "invalid option: %s", argument); @@ -163,22 +184,24 @@ static void cmd_show_options_print(struct cmd *self, struct cmdq_item *item, struct options_entry *o, const char *array_key, int parent) { - struct args *args = cmd_get_args(self); - struct options_array_item *a; - const char *name = options_name(o); - char *value, *tmp = NULL, *escaped; + struct args *args = cmd_get_args(self); + struct options_array_item *a; + struct format_tree *ft; + const char *name = options_name(o); + const char *template = args_get(args, 'F'); + char *value, *line; + struct timeval tv = { 0 }; + u_int fire_count; + time_t fire_time; + int is_hook = 0, is_user = 0; + int has_value = 1; + const struct options_table_entry *oe = options_table_entry(o); - if (array_key != NULL) { - xasprintf(&tmp, "%s[%s]", name, array_key); - name = tmp; - } else { - if (options_is_array(o)) { - a = options_array_first(o); - if (a == NULL) { - if (!args_has(args, 'v')) - cmdq_print(item, "%s", name); - return; - } + if (array_key != NULL) + value = options_to_string(o, array_key, 0); + else if (options_is_array(o)) { + a = options_array_first(o); + if (a != NULL) { while (a != NULL) { array_key = options_array_item_key(a); cmd_show_options_print(self, item, o, array_key, @@ -187,54 +210,130 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, } return; } - } + if (template == NULL && args_has(args, 'v')) + return; + value = xstrdup(""); + has_value = 0; + } else + value = options_to_string(o, NULL, 0); - value = options_to_string(o, array_key, 0); - if (args_has(args, 'v')) - cmdq_print(item, "%s", value); - else if (options_is_string(o)) { - escaped = args_escape(value); - if (parent) - cmdq_print(item, "%s* %s", name, escaped); - else - cmdq_print(item, "%s %s", name, escaped); - free(escaped); + if (template == NULL) + template = SHOW_OPTIONS_TEMPLATE; + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + is_hook = 1; + else if (oe == NULL) + is_user = 1; + + ft = format_create_from_target(item); + format_add(ft, "option_name", "%s", name); + format_add(ft, "option_value", "%s", value); + format_add(ft, "option_value_only", "%d", args_has(args, 'v')); + format_add(ft, "option_is_parent", "%d", parent); + format_add(ft, "option_is_array", "%d", options_is_array(o)); + format_add(ft, "option_is_string", "%d", options_is_string(o)); + format_add(ft, "option_is_hook", "%d", is_hook); + format_add(ft, "option_is_user", "%d", is_user); + format_add(ft, "option_has_value", "%d", has_value); + if (cmd_get_entry(self) == &cmd_show_hooks_entry) { + fire_count = options_get_fire_count(o); + format_add(ft, "hook_fire_count", "%u", fire_count); + + fire_time = options_get_fire_time(o); + if (fire_time != 0) { + tv.tv_sec = fire_time; + format_add_tv(ft, "hook_fire_time", &tv); + } + } + if (array_key != NULL) { + format_add(ft, "option_array_key", "%s", array_key); + format_add(ft, "option_has_array_key", "1"); } else { - if (parent) - cmdq_print(item, "%s* %s", name, value); - else - cmdq_print(item, "%s %s", name, value); + format_add(ft, "option_array_key", "%s", ""); + format_add(ft, "option_has_array_key", "0"); } - free(value); + line = format_expand(ft, template); + format_free(ft); - free(tmp); + cmdq_print(item, "%s", line); + free(line); + free(value); } static void -cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o) +cmd_show_hooks_print_monitor(struct cmd *self, struct cmdq_item *item, + struct options_entry *o) { - char *value; + struct args *args = cmd_get_args(self); + struct format_tree *ft; + enum monitor_type type; + const char *template = args_get(args, 'F'), *format; + char *value, *target, *line; + struct timeval tv = { 0 }; + u_int fire_count; + time_t fire_time; + int id; value = hooks_monitor_to_string(o); if (value == NULL) return; - cmdq_print(item, "%s", value); - free(value); -} - -/* Show all hook monitors. */ -static enum cmd_retval -cmd_show_hooks_monitor(__unused struct cmd *self, struct cmdq_item *item, - __unused int scope, struct options *oo) -{ - struct options_entry *o; - - o = options_first(oo); - while (o != NULL) { - cmd_show_hooks_print_monitor(item, o); - o = options_next(o); + if (!hooks_monitor_get(o, &type, &id, &format)) { + free(value); + return; } - return (CMD_RETURN_NORMAL); + if (template == NULL) + template = SHOW_HOOKS_MONITOR_TEMPLATE; + + switch (type) { + case MONITOR_SESSION: + target = xstrdup(""); + break; + case MONITOR_PANE: + xasprintf(&target, "%%%d", id); + break; + case MONITOR_ALL_PANES: + target = xstrdup("%*"); + break; + case MONITOR_WINDOW: + xasprintf(&target, "@%d", id); + break; + case MONITOR_ALL_WINDOWS: + target = xstrdup("@*"); + break; + } + + ft = format_create_from_target(item); + format_add(ft, "option_name", "%s", options_name(o)); + format_add(ft, "option_value", "%s", value); + format_add(ft, "option_value_only", "%d", 0); + format_add(ft, "option_is_parent", "%d", 0); + format_add(ft, "option_is_array", "%d", 0); + format_add(ft, "option_is_string", "%d", 1); + format_add(ft, "option_is_hook", "%d", 1); + format_add(ft, "option_is_user", "%d", 1); + format_add(ft, "option_has_value", "%d", 1); + format_add(ft, "option_array_key", "%s", ""); + format_add(ft, "option_has_array_key", "0"); + + format_add(ft, "hook_monitor_target", "%s", target); + format_add(ft, "hook_monitor_format", "%s", format); + + fire_count = hooks_monitor_get_fire_count(o); + format_add(ft, "hook_fire_count", "%u", fire_count); + + fire_time = hooks_monitor_get_fire_time(o); + if (fire_time != 0) { + tv.tv_sec = fire_time; + format_add_tv(ft, "hook_fire_time", &tv); + } + + line = format_expand(ft, template); + format_free(ft); + + cmdq_print(item, "%s", line); + free(line); + free(target); + free(value); } static enum cmd_retval @@ -244,17 +343,29 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, struct args *args = cmd_get_args(self); const struct options_table_entry *oe; struct options_entry *o; - struct options_array_item *a; - const char *name, *array_key; - int parent; + const char *name; + int parent, is_user_hook; - if (cmd_get_entry(self) != &cmd_show_hooks_entry) { - o = options_first(oo); - while (o != NULL) { - if (options_table_entry(o) == NULL) + o = options_first(oo); + while (o != NULL) { + if (options_table_entry(o) == NULL) { + name = options_name(o); + is_user_hook = 0; + if (*name == '@') { + if (hooks_is_event(name) || + options_get_monitor_data(o) != NULL) { + is_user_hook = 1; + } + } + if (cmd_get_entry(self) != &cmd_show_hooks_entry) { + if (!is_user_hook || args_has(args, 'H')) { + cmd_show_options_print(self, item, o, + NULL, 0); + } + } else if (is_user_hook) cmd_show_options_print(self, item, o, NULL, 0); - o = options_next(o); } + o = options_next(o); } for (oe = options_table; oe->name != NULL; oe++) { if (~oe->scope & scope) @@ -278,24 +389,7 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, } else parent = 0; - if (!options_is_array(o)) - cmd_show_options_print(self, item, o, NULL, parent); - else if ((a = options_array_first(o)) == NULL) { - if (!args_has(args, 'v')) { - name = options_name(o); - if (parent) - cmdq_print(item, "%s*", name); - else - cmdq_print(item, "%s", name); - } - } else { - while (a != NULL) { - array_key = options_array_item_key(a); - cmd_show_options_print(self, item, o, array_key, - parent); - a = options_array_next(a); - } - } + cmd_show_options_print(self, item, o, NULL, parent); } return (CMD_RETURN_NORMAL); } 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 c5fb8effb..c907dd152 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.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,9 +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 { + 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; } @@ -132,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')) @@ -168,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); } @@ -201,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; @@ -270,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); } @@ -321,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-swap-window.c b/cmd-swap-window.c index 980da0395..0141e6a57 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-swap-window.c,v 1.29 2025/10/30 13:52:08 nicm Exp $ */ +/* $OpenBSD: cmd-swap-window.c,v 1.30 2026/07/27 08:03:01 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -38,7 +38,7 @@ const struct cmd_entry cmd_swap_window_entry = { .source = { 's', CMD_FIND_WINDOW, CMD_FIND_DEFAULT_MARKED }, .target = { 't', CMD_FIND_WINDOW, 0 }, - .flags = 0, + .flags = CMD_AFTERHOOK, .exec = cmd_swap_window_exec }; diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 177107b47..44ea43cbc 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */ +/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -53,7 +53,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state target; const char *tflag = args_get(args, 't'); enum cmd_find_type type; - int flags; + int flags, visible, Zflag = args_has(args, 'Z'); struct client *c = cmdq_get_client(item); struct client *tc = cmdq_get_target_client(item); struct session *s; @@ -139,11 +139,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); if (wl != NULL && wp != NULL && wp != wl->window->active) { w = wl->window; - if (window_push_zoom(w, 0, args_has(args, 'Z'))) + if (w->modal != NULL && wp != w->modal) + visible = 1; + else + visible = window_pane_is_visible(wp); + if (!visible && window_push_zoom(w, 0, Zflag)) server_redraw_window(w); window_redraw_active_switch(w, wp); window_set_active_pane(w, wp, 1); - if (window_pop_zoom(w)) + if (!visible && window_pop_zoom(w)) server_redraw_window(w); } if (wl != NULL) { diff --git a/compat/getpeereid.c b/compat/getpeereid.c index b79f420ad..ed2ed8743 100644 --- a/compat/getpeereid.c +++ b/compat/getpeereid.c @@ -31,7 +31,7 @@ getpeereid(int s, uid_t *uid, gid_t *gid) { #ifdef HAVE_SO_PEERCRED struct ucred uc; - int len = sizeof uc; + socklen_t len = sizeof uc; if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1) return (-1); diff --git a/configure.ac b/configure.ac index 51700a2ad..b09bc5d4e 100644 --- a/configure.ac +++ b/configure.ac @@ -76,9 +76,7 @@ AM_CONDITIONAL(IS_OPTIMIZED, test "x$enable_optimizations" = xyes) # Is this --enable-asan? AC_ARG_ENABLE( asan, - AS_HELP_STRING(--enable-asan, enable ASAN build flags), - , - [case "x$host_os" in *darwin*) enable_asan=yes;; esac] + AS_HELP_STRING(--enable-asan, enable ASAN build flags) ) AM_CONDITIONAL(IS_ASAN, test "x$enable_asan" = xyes) @@ -240,6 +238,7 @@ AC_LIBOBJ(getopt_long) # Look for libevent. Try libevent_core or libevent with pkg-config first then # look for the library. +libevent_version=off PKG_CHECK_MODULES( LIBEVENT_CORE, [libevent_core >= 2], @@ -248,6 +247,8 @@ PKG_CHECK_MODULES( CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBEVENT_CORE_LIBS $LIBS" found_libevent=yes + libevent_version=`$PKG_CONFIG --modversion libevent_core 2>/dev/null` + test "x$libevent_version" = x && libevent_version=on ], found_libevent=no ) @@ -260,6 +261,8 @@ if test x$found_libevent = xno; then CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBEVENT_LIBS $LIBS" found_libevent=yes + libevent_version=`$PKG_CONFIG --modversion libevent 2>/dev/null` + test "x$libevent_version" = x && libevent_version=on ], found_libevent=no ) @@ -268,7 +271,8 @@ if test x$found_libevent = xno; then AC_SEARCH_LIBS( event_init, [event_core event event-1.4], - found_libevent=yes, + [found_libevent=yes + libevent_version=on], found_libevent=no ) fi @@ -295,6 +299,7 @@ fi # Look for ncurses or curses. Try pkg-config first then directly for the # library. +ncurses_version=off PKG_CHECK_MODULES( LIBTINFOW, tinfow, @@ -303,6 +308,8 @@ PKG_CHECK_MODULES( CPPFLAGS="$LIBTINFOW_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBTINFOW_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion tinfow 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -315,6 +322,8 @@ if test "x$found_ncurses" = xno; then CPPFLAGS="$LIBTINFO_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBTINFO_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion tinfo 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -328,6 +337,8 @@ if test "x$found_ncurses" = xno; then CPPFLAGS="$LIBNCURSESW_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBNCURSESW_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion ncursesw 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -341,6 +352,8 @@ if test "x$found_ncurses" = xno; then CPPFLAGS="$LIBNCURSES_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBNCURSES_LIBS $LIBS" found_ncurses=yes + ncurses_version=`$PKG_CONFIG --modversion ncurses 2>/dev/null` + test "x$ncurses_version" = x && ncurses_version=on ], found_ncurses=no ) @@ -349,7 +362,8 @@ if test "x$found_ncurses" = xno; then AC_SEARCH_LIBS( setupterm, [tinfow tinfo terminfo ncursesw ncurses], - found_ncurses=yes, + [found_ncurses=yes + ncurses_version=on], found_ncurses=no ) if test "x$found_ncurses" = xyes; then @@ -379,6 +393,7 @@ else LIBS="$LIBS -lcurses" CPPFLAGS="$CPPFLAGS -DHAVE_CURSES_H" AC_DEFINE(HAVE_CURSES_H) + ncurses_version=on else AC_MSG_ERROR("curses not found") fi @@ -389,6 +404,7 @@ AC_CHECK_FUNCS([ \ ]) # Look for utempter. +utempter_version=off AC_ARG_ENABLE( utempter, AS_HELP_STRING(--enable-utempter, use utempter if it is installed) @@ -405,17 +421,34 @@ if test "x$enable_utempter" = xyes; then fi if test "x$enable_utempter" = xyes; then AC_DEFINE(HAVE_UTEMPTER) + utempter_version=on else AC_MSG_ERROR("utempter not found") fi fi # Look for utf8proc. +utf8proc_version=off AC_ARG_ENABLE( utf8proc, AS_HELP_STRING(--enable-utf8proc, use utf8proc if it is installed) ) +try_utf8proc=no +require_utf8proc=no if test "x$enable_utf8proc" = xyes; then + try_utf8proc=yes +elif test "x$enable_utf8proc" = x; then + case "$host_os" in + *darwin*) + try_utf8proc=yes + require_utf8proc=yes + ;; + esac +fi +if test "x$try_utf8proc" = xyes; then + SAVED_AM_CPPFLAGS="$AM_CPPFLAGS" + SAVED_OLD_CPPFLAGS="$CPPFLAGS" + SAVED_LIBS="$LIBS" PKG_CHECK_MODULES( LIBUTF8PROC, libutf8proc, @@ -423,7 +456,10 @@ if test "x$enable_utf8proc" = xyes; then AM_CPPFLAGS="$LIBUTF8PROC_CFLAGS $AM_CPPFLAGS" CPPFLAGS="$LIBUTF8PROC_CFLAGS $SAVED_CPPFLAGS" LIBS="$LIBUTF8PROC_LIBS $LIBS" - ] + utf8proc_version=`$PKG_CONFIG --modversion libutf8proc 2>/dev/null` + test "x$utf8proc_version" = x && utf8proc_version=on + ], + [:] ) AC_CHECK_HEADER(utf8proc.h, enable_utf8proc=yes, enable_utf8proc=no) if test "x$enable_utf8proc" = xyes; then @@ -436,13 +472,23 @@ if test "x$enable_utf8proc" = xyes; then fi if test "x$enable_utf8proc" = xyes; then AC_DEFINE(HAVE_UTF8PROC) + test "x$utf8proc_version" = xoff && utf8proc_version=on else - AC_MSG_ERROR("utf8proc not found") + AM_CPPFLAGS="$SAVED_AM_CPPFLAGS" + CPPFLAGS="$SAVED_OLD_CPPFLAGS" + LIBS="$SAVED_LIBS" + utf8proc_version=off + if test "x$require_utf8proc" = xyes; then + enable_utf8proc= + else + AC_MSG_ERROR("utf8proc not found") + fi fi fi AM_CONDITIONAL(HAVE_UTF8PROC, [test "x$enable_utf8proc" = xyes]) # Check for systemd support. +systemd_version=off AC_ARG_ENABLE( systemd, AS_HELP_STRING(--enable-systemd, enable systemd integration) @@ -456,6 +502,8 @@ if test x"$enable_systemd" = xyes; then CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$SYSTEMD_LIBS $LIBS" found_systemd=yes + systemd_version=`$PKG_CONFIG --modversion libsystemd 2>/dev/null` + test "x$systemd_version" = x && systemd_version=on ], found_systemd=no ) @@ -580,11 +628,34 @@ if test "x$found_malloc_trim" = xyes; then fi # Build against jemalloc if requested. +jemalloc_version=off AC_ARG_ENABLE( jemalloc, AS_HELP_STRING(--enable-jemalloc, use jemalloc if it is installed) ) +try_jemalloc=no +require_jemalloc=no +if test "x$enable_asan" = xyes && test "x$enable_jemalloc" = xyes; then + AC_MSG_ERROR([--enable-asan and --enable-jemalloc cannot be used together]) +fi if test "x$enable_jemalloc" = xyes; then + try_jemalloc=yes +elif test "x$enable_jemalloc" = x; then + case "$host_os" in + *darwin*) + if test "x$enable_asan" = xyes; then + enable_jemalloc=no + else + try_jemalloc=yes + require_jemalloc=yes + fi + ;; + esac +fi +if test "x$try_jemalloc" = xyes; then + SAVED_AM_CPPFLAGS="$AM_CPPFLAGS" + SAVED_OLD_CPPFLAGS="$CPPFLAGS" + SAVED_LIBS="$LIBS" PKG_CHECK_MODULES( JEMALLOC, jemalloc, @@ -592,9 +663,26 @@ if test "x$enable_jemalloc" = xyes; then AM_CPPFLAGS="$JEMALLOC_CFLAGS $AM_CPPFLAGS" CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS" LIBS="$LIBS $JEMALLOC_LIBS" + jemalloc_version=`$PKG_CONFIG --modversion jemalloc 2>/dev/null` + test "x$jemalloc_version" = x && jemalloc_version=on ], - AC_MSG_ERROR("jemalloc not found") + enable_jemalloc=no ) + if test "x$enable_jemalloc" = xno; then + AM_CPPFLAGS="$SAVED_AM_CPPFLAGS" + CPPFLAGS="$SAVED_OLD_CPPFLAGS" + LIBS="$SAVED_LIBS" + jemalloc_version=off + if test "x$require_jemalloc" = xyes; then + enable_jemalloc= + else + AC_MSG_ERROR("jemalloc not found") + fi + else + enable_jemalloc=yes + AC_DEFINE(HAVE_JEMALLOC) + test "x$jemalloc_version" = xoff && jemalloc_version=on + fi fi # Check for CMSG_DATA. On some platforms like HP-UX this requires UNIX 95 @@ -941,6 +1029,20 @@ case "$host_os" in AC_MSG_NOTICE([]) AC_MSG_ERROR([must give --enable-utf8proc or --disable-utf8proc]) fi + # + # macOS calloc(3) does not appear to always zero memory correctly, + # so complain and suggest using jemalloc instead. + # + if test "x$enable_jemalloc" = x; then + AC_MSG_NOTICE([]) + AC_MSG_NOTICE([ macOS calloc(3) appears not to correctly]) + AC_MSG_NOTICE([ zero allocations in some circumstances;]) + AC_MSG_NOTICE([ to avoid this, configuring with]) + AC_MSG_NOTICE([ --enable-jemalloc is recommended. To build]) + AC_MSG_NOTICE([ without anyway, use --disable-jemalloc]) + AC_MSG_NOTICE([]) + AC_MSG_ERROR([must give --enable-jemalloc or --disable-jemalloc]) + fi ;; *dragonfly*) AC_MSG_RESULT(dragonfly) @@ -1028,6 +1130,31 @@ AC_MSG_CHECKING(lock-command) AC_MSG_RESULT($DEFAULT_LOCK_CMD) AC_SUBST(DEFAULT_LOCK_CMD) +# Print a summary. +AC_MSG_NOTICE([]) +if test "x$enable_asan" = xyes; then + AC_MSG_NOTICE([ASAN: on]) +else + AC_MSG_NOTICE([ASAN: off]) +fi +if test "x$enable_sixel" = xyes; then + AC_MSG_NOTICE([SIXEL: on]) +else + AC_MSG_NOTICE([SIXEL: off]) +fi +if test "x$enable_debug" = xyes; then + AC_MSG_NOTICE([debug: on]) +else + AC_MSG_NOTICE([debug: off]) +fi +AC_MSG_NOTICE([jemalloc: $jemalloc_version]) +AC_MSG_NOTICE([libevent: $libevent_version]) +AC_MSG_NOTICE([ncurses: $ncurses_version]) +AC_MSG_NOTICE([systemd: $systemd_version]) +AC_MSG_NOTICE([utempter: $utempter_version]) +AC_MSG_NOTICE([utf8proc: $utf8proc_version]) +AC_MSG_NOTICE([]) + # Save our CFLAGS/CPPFLAGS/LDFLAGS for the Makefile and restore the old user # variables. AC_SUBST(AM_CPPFLAGS) diff --git a/control-notify.c b/control-notify.c index 6fec1683f..adfc9dcb0 100644 --- a/control-notify.c +++ b/control-notify.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -44,7 +44,8 @@ control_pane_mode_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%pane-mode-changed %%%u", wp->id); + control_notify_write(c, "%%pane-mode-changed %%%u", + wp->id); } return; } @@ -53,8 +54,9 @@ control_pane_mode_changed_cb(__unused const char *name, if (value == NULL) return; TAILQ_FOREACH(c, &clients, entry) { - if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) - control_write(c, "%%pane-mode-changed %s", value); + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_notify_write(c, "%%pane-mode-changed %s", value); } free(value); } @@ -92,7 +94,7 @@ control_window_layout_changed_cb(__unused const char *name, continue; s = c->session; if (winlink_find_by_window_id(&s->windows, w->id) != NULL) - control_write(c, "%s", cp); + control_notify_write(c, "%s", cp); } free(cp); } @@ -111,7 +113,7 @@ control_window_pane_changed_cb(__unused const char *name, if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%window-pane-changed @%u %%%u", w->id, + control_notify_write(c, "%%window-pane-changed @%u %%%u", w->id, w->active->id); } } @@ -133,9 +135,11 @@ control_window_unlinked_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) - control_write(c, "%%window-close @%u", w->id); - else - control_write(c, "%%unlinked-window-close @%u", w->id); + control_notify_write(c, "%%window-close @%u", w->id); + else { + control_notify_write(c, "%%unlinked-window-close @%u", + w->id); + } } } @@ -156,9 +160,11 @@ control_window_linked_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) - control_write(c, "%%window-add @%u", w->id); - else - control_write(c, "%%unlinked-window-add @%u", w->id); + control_notify_write(c, "%%window-add @%u", w->id); + else { + control_notify_write(c, "%%unlinked-window-add @%u", + w->id); + } } } @@ -179,11 +185,11 @@ control_window_renamed_cb(__unused const char *name, struct event_payload *ep, cs = c->session; if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) { - control_write(c, "%%window-renamed @%u %s", w->id, - w->name); - } else { - control_write(c, "%%unlinked-window-renamed @%u %s", + control_notify_write(c, "%%window-renamed @%u %s", w->id, w->name); + } else { + control_notify_write(c, + "%%unlinked-window-renamed @%u %s", w->id, w->name); } } } @@ -206,11 +212,12 @@ control_client_session_changed_cb(__unused const char *name, continue; if (cc == c) { - control_write(c, "%%session-changed $%u %s", s->id, - s->name); + control_notify_write(c, "%%session-changed $%u %s", + s->id, s->name); } else { - control_write(c, "%%client-session-changed %s $%u %s", - cc->name, s->id, s->name); + control_notify_write(c, + "%%client-session-changed %s $%u %s", cc->name, + s->id, s->name); } } } @@ -226,8 +233,9 @@ control_client_detached_cb(__unused const char *name, struct event_payload *ep, if (cc == NULL) return; TAILQ_FOREACH(c, &clients, entry) { - if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) - control_write(c, "%%client-detached %s", cc->name); + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_notify_write(c, "%%client-detached %s", cc->name); } } @@ -244,8 +252,8 @@ control_session_renamed_cb(__unused const char *name, struct event_payload *ep, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%session-renamed $%u %s", s->id, s->name); + control_notify_write(c, "%%session-renamed $%u %s", s->id, + s->name); } } @@ -259,8 +267,7 @@ control_session_created_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%sessions-changed"); + control_notify_write(c, "%%sessions-changed"); } } @@ -274,8 +281,7 @@ control_session_closed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%sessions-changed"); + control_notify_write(c, "%%sessions-changed"); } } @@ -298,9 +304,8 @@ control_session_window_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%session-window-changed $%u @%u", s->id, - s->curw->window->id); + control_notify_write(c, "%%session-window-changed $%u @%u", + s->id, s->curw->window->id); } } @@ -317,8 +322,7 @@ control_paste_buffer_changed_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%paste-buffer-changed %s", pbname); + control_notify_write(c, "%%paste-buffer-changed %s", pbname); } } @@ -335,8 +339,7 @@ control_paste_buffer_deleted_cb(__unused const char *name, TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - - control_write(c, "%%paste-buffer-deleted %s", pbname); + control_notify_write(c, "%%paste-buffer-deleted %s", pbname); } } diff --git a/control.c b/control.c index 92645a8bc..af0d8b91a 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.62 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.67 2026/09/03 21:35:38 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -53,6 +53,17 @@ struct control_block { TAILQ_ENTRY(control_block) all_entry; }; +/* + * A notification line deferred because it was generated while a command's + * %begin/%end guard block was open. Notifications must never appear inside a + * guard block, so they are held here and flushed once the block closes. + */ +struct control_line { + char *line; + + TAILQ_ENTRY(control_line) entry; +}; + /* Control client pane. */ struct control_pane { u_int pane; @@ -102,6 +113,13 @@ struct control_state { struct bufferevent *write_event; struct monitor_set *subs; + + /* + * Depth of open %begin/%end guard blocks and notifications deferred + * until the outermost block closes. + */ + int guard_depth; + TAILQ_HEAD(, control_line) deferred; }; /* Low and high watermarks. */ @@ -352,7 +370,7 @@ control_continue_pane(struct client *c, struct window_pane *wp) cp->flags &= ~CONTROL_PANE_PAUSED; memcpy(&cp->offset, &wp->offset, sizeof cp->offset); memcpy(&cp->queued, &wp->offset, sizeof cp->queued); - control_write(c, "%%continue %%%u", wp->id); + control_notify_write(c, "%%continue %%%u", wp->id); } } @@ -366,52 +384,149 @@ control_pause_pane(struct client *c, struct window_pane *wp) if (~cp->flags & CONTROL_PANE_PAUSED) { cp->flags |= CONTROL_PANE_PAUSED; control_discard_pane(c, cp); - control_write(c, "%%pause %%%u", wp->id); + control_notify_write(c, "%%pause %%%u", wp->id); } } -/* Write a line. */ -static void printflike(2, 0) -control_vwrite(struct client *c, const char *fmt, va_list ap) +/* + * 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_state *cs = c->control_state; - char *s; + struct control_pane *cp; - xvasprintf(&s, fmt, ap); - log_debug("%s: %s: writing line: %s", __func__, c->name, s); - - bufferevent_write(cs->write_event, s, strlen(s)); - bufferevent_write(cs->write_event, "\n", 1); - - bufferevent_enable(cs->write_event, EV_WRITE); - free(s); + 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 a line. */ -void -control_write(struct client *c, const char *fmt, ...) +/* Write an already-formatted line, queueing it behind %output if needed. */ +static void +control_write_line(struct client *c, char *line) { struct control_state *cs = c->control_state; struct control_block *cb; - va_list ap; - - va_start(ap, fmt); if (TAILQ_EMPTY(&cs->all_blocks)) { - control_vwrite(c, fmt, ap); - va_end(ap); + log_debug("%s: %s: writing line: %s", __func__, c->name, line); + bufferevent_write(cs->write_event, line, strlen(line)); + bufferevent_write(cs->write_event, "\n", 1); + bufferevent_enable(cs->write_event, EV_WRITE); + free(line); return; } cb = xcalloc(1, sizeof *cb); - xvasprintf(&cb->line, fmt, ap); + cb->line = line; TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry); cb->t = get_timer(); log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line); bufferevent_enable(cs->write_event, EV_WRITE); +} +/* Flush notifications that were deferred while a guard block was open. */ +static void +control_flush_deferred(struct client *c) +{ + struct control_state *cs = c->control_state; + struct control_line *cl, *cl1; + + TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) { + TAILQ_REMOVE(&cs->deferred, cl, entry); + control_write_line(c, cl->line); + free(cl); + } +} + +/* + * Write a line of command output or error text. This is a sink for arbitrary + * user-controlled text (command output, capture-pane, error messages), so it + * must never try to interpret the content: guard tracking is done only in + * control_write_guard. + */ +void +control_write(struct client *c, const char *fmt, ...) +{ + struct control_state *cs = c->control_state; + char *line; + va_list ap; + + if (cs == NULL) + return; + + va_start(ap, fmt); + xvasprintf(&line, fmt, ap); va_end(ap); + + control_write_line(c, line); +} + +/* + * Write a %begin, %end or %error guard around a command's output. This is the + * only place guard lines are produced, so the block depth is maintained here; + * when the outermost block closes any deferred notifications are flushed after + * it. "guard" is always one of the fixed strings from cmdq_guard, never user + * text. + */ +void +control_write_guard(struct client *c, const char *guard, long t, u_int number, + int flags) +{ + struct control_state *cs = c->control_state; + char *line; + + if (cs == NULL) + return; + + if (strcmp(guard, "begin") == 0) + cs->guard_depth++; + + xasprintf(&line, "%%%s %ld %u %d", guard, t, number, flags); + control_write_line(c, line); + + if (strcmp(guard, "begin") != 0 && cs->guard_depth > 0 && + --cs->guard_depth == 0) + control_flush_deferred(c); +} + +/* + * Write a notification line. Notifications must never appear inside a command's + * %begin/%end guard block, so if one is open the line is deferred until it + * closes. + */ +void +control_notify_write(struct client *c, const char *fmt, ...) +{ + struct control_state *cs = c->control_state; + struct control_line *cl; + va_list ap; + char *line; + + if (cs == NULL) + return; + + va_start(ap, fmt); + xvasprintf(&line, fmt, ap); + va_end(ap); + + if (cs->guard_depth == 0) { + control_write_line(c, line); + return; + } + + log_debug("%s: %s: deferring notification: %s", __func__, c->name, + line); + cl = xcalloc(1, sizeof *cl); + cl->line = line; + TAILQ_INSERT_TAIL(&cs->deferred, cl, entry); } /* Check age for this pane. */ @@ -438,7 +553,7 @@ control_check_age(struct client *c, struct window_pane *wp, return (0); cp->flags |= CONTROL_PANE_PAUSED; control_discard_pane(c, cp); - control_write(c, "%%pause %%%u", wp->id); + control_notify_write(c, "%%pause %%%u", wp->id); } else { if (age < CONTROL_MAXIMUM_AGE) return (0); @@ -805,14 +920,17 @@ control_sub_change(struct monitor_change *change, __unused void *data) if (wp != NULL) { w = wp->window; - control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s", + control_notify_write(c, + "%%subscription-changed %s $%u @%u %u %%%u : %s", change->name, s->id, w->id, wl->idx, wp->id, change->value); } else if (wl != NULL) { w = wl->window; - control_write(c, "%%subscription-changed %s $%u @%u %u - : %s", + control_notify_write(c, + "%%subscription-changed %s $%u @%u %u - : %s", change->name, s->id, w->id, wl->idx, change->value); } else { - control_write(c, "%%subscription-changed %s $%u - - - : %s", + control_notify_write(c, + "%%subscription-changed %s $%u - - - : %s", change->name, s->id, change->value); } } @@ -835,6 +953,7 @@ control_start(struct client *c) RB_INIT(&cs->windows); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); + TAILQ_INIT(&cs->deferred); cs->subs = monitor_create_client(c, control_sub_change, NULL); cs->read_event = bufferevent_new(c->fd, control_read_callback, @@ -878,6 +997,19 @@ control_discard(struct client *c) bufferevent_disable(cs->read_event, EV_READ); } +/* Discard all tmux-owned queued control blocks and stop writing. */ +void +control_discard_all(struct client *c) +{ + struct control_state *cs = c->control_state; + struct control_block *cb, *cb1; + + control_discard(c); + TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) + control_free_block(cs, cb); + bufferevent_disable(cs->write_event, EV_WRITE); +} + /* Stop control mode. */ void control_stop(struct client *c) @@ -885,12 +1017,19 @@ control_stop(struct client *c) struct control_state *cs = c->control_state; struct control_block *cb, *cb1; struct control_window *cw, *cw1; + struct control_line *cl, *cl1; if (cs == NULL) return; monitor_destroy(cs->subs); + TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) { + TAILQ_REMOVE(&cs->deferred, cl, entry); + free(cl->line); + free(cl); + } + if (~c->flags & CLIENT_CONTROLCONTROL) bufferevent_free(cs->write_event); bufferevent_free(cs->read_event); diff --git a/file.c b/file.c index 2eed63403..dce8d0f67 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: file.c,v 1.20 2026/05/17 10:54:01 nicm Exp $ */ +/* $OpenBSD: file.c,v 1.22 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -185,6 +185,7 @@ file_can_print(struct client *c) { if (c == NULL || (c->flags & CLIENT_ATTACHED) || + (c->flags & CLIENT_DEAD) || (c->flags & CLIENT_CONTROL)) return (0); return (1); @@ -501,7 +502,8 @@ file_push(struct client_file *cf) } else if (cf->stream > 2) { close.stream = cf->stream; proc_send(cf->peer, MSG_WRITE_CLOSE, -1, &close, sizeof close); - file_fire_done(cf); + if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK)) + file_fire_done(cf); } free(msg); } @@ -526,14 +528,48 @@ file_write_left(struct client_files *files) return (waiting != 0); } +/* Finish writing a client file. */ +static void +file_write_finished(struct client_file *cf) +{ + struct msg_write_done msg; + + if (cf->event != NULL) { + bufferevent_free(cf->event); + cf->event = NULL; + } + if (cf->fd != -1) { + if (close(cf->fd) != 0 && cf->error == 0) + cf->error = errno; + cf->fd = -1; + } + + msg.stream = cf->stream; + msg.error = cf->error; + proc_send(cf->peer, MSG_WRITE_DONE, -1, &msg, sizeof msg); + + if (cf->cb != NULL) + cf->cb(NULL, NULL, 0, -1, NULL, cf->data); + file_free(cf); +} + /* Client file write error callback. */ static void -file_write_error_callback(__unused struct bufferevent *bev, __unused short what, +file_write_error_callback(__unused struct bufferevent *bev, short what, void *arg) { struct client_file *cf = arg; + int error; + + if (what & EVBUFFER_ERROR) + error = errno; + else + error = EIO; + if (error == 0) + error = EIO; log_debug("write error file %d", cf->stream); + cf->error = error; bufferevent_free(cf->event); cf->event = NULL; @@ -541,7 +577,9 @@ file_write_error_callback(__unused struct bufferevent *bev, __unused short what, close(cf->fd); cf->fd = -1; - if (cf->cb != NULL) + if (cf->closed) + file_write_finished(cf); + else if (cf->cb != NULL) cf->cb(NULL, NULL, 0, -1, NULL, cf->data); } @@ -553,15 +591,10 @@ file_write_callback(__unused struct bufferevent *bev, void *arg) log_debug("write check file %d", cf->stream); - if (cf->cb != NULL) + if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) + file_write_finished(cf); + else if (cf->cb != NULL) cf->cb(NULL, NULL, 0, -1, NULL, cf->data); - - if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) { - bufferevent_free(cf->event); - close(cf->fd); - RB_REMOVE(client_files, cf->tree, cf); - file_free(cf); - } } /* Handle a file write open message (client). */ @@ -662,14 +695,10 @@ file_write_close(struct client_files *files, struct imsg *imsg) if ((cf = RB_FIND(client_files, files, &find)) == NULL) fatalx("unknown stream number"); log_debug("close file %d", cf->stream); + cf->closed = 1; if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) { - if (cf->event != NULL) - bufferevent_free(cf->event); - if (cf->fd != -1) - close(cf->fd); - RB_REMOVE(client_files, files, cf); - file_free(cf); + file_write_finished(cf); } } @@ -828,6 +857,28 @@ file_write_ready(struct client_files *files, struct imsg *imsg) return (0); } +/* Handle a write done message (server). */ +int +file_write_done(struct client_files *files, struct imsg *imsg) +{ + struct msg_write_done *msg = imsg->data; + size_t msglen = imsg->hdr.len - IMSG_HEADER_SIZE; + struct client_file find, *cf; + + if (msglen != sizeof *msg) + return (-1); + find.stream = msg->stream; + if ((cf = RB_FIND(client_files, files, &find)) == NULL) + return (0); + if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK)) + return (0); + + log_debug("file %d write done", cf->stream); + cf->error = msg->error; + file_fire_done(cf); + return (0); +} + /* Handle read data message (server). */ int file_read_data(struct client_files *files, struct imsg *imsg) diff --git a/format.c b/format.c index 6321f9e9d..78e3f0fb5 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.407 2026/07/20 07:42:13 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -128,6 +128,7 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_OPTIONS 0x40000000 #define FORMAT_ENVIRON 0x80000000ULL #define FORMAT_DIFFERENCE 0x100000000ULL +#define FORMAT_CYCLE 0x200000000ULL /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -138,9 +139,13 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) /* How often to check the time in long loops. */ #define FORMAT_TIME_LOOP_CHECK 10000 +/* Fixed animation period (ms): redraw interval and shortest frame step. */ +#define FORMAT_CYCLE_PERIOD 100 + /* Format expand flags. */ #define FORMAT_EXPAND_TIME 0x1 #define FORMAT_EXPAND_NOJOBS 0x2 +#define FORMAT_EXPAND_NOCYCLE 0x4 /* Entry in format tree. */ struct format_entry { @@ -413,7 +418,8 @@ format_job_get(struct format_expand_state *es, const char *cmd) RB_INSERT(format_job_tree, jobs, fj); } - format_copy_state(&next, es, FORMAT_EXPAND_NOJOBS); + format_copy_state(&next, es, FORMAT_EXPAND_NOJOBS| + FORMAT_EXPAND_NOCYCLE); next.flags &= ~FORMAT_EXPAND_TIME; expanded = format_expand1(&next, cmd); @@ -1881,6 +1887,37 @@ format_cb_cursor_blinking(struct format_tree *ft) return (NULL); } +/* Callback for history_added. */ +static void * +format_cb_history_added(struct format_tree *ft) +{ + if (ft->wp != NULL) + return (format_printf("%u", ft->wp->base.grid->scroll_added)); + return (NULL); +} + +/* Callback for history_collected. */ +static void * +format_cb_history_collected(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_collected)); + return (NULL); +} + +/* Callback for history_generation. */ +static void * +format_cb_history_generation(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%u", wp->base.grid->scroll_generation)); + return (NULL); +} + /* Callback for history_limit. */ static void * format_cb_history_limit(struct format_tree *ft) @@ -2095,6 +2132,60 @@ format_cb_synchronized_output_flag(struct format_tree *ft) return (NULL); } +/* Callback for pane_private_modes. */ +static void * +format_cb_pane_private_modes(struct format_tree *ft) +{ + static const struct { + int mode; + int number; + } table[] = { + { MODE_KCURSOR, 1 }, /* DECCKM */ + { MODE_ORIGIN, 6 }, /* DECOM */ + { MODE_WRAP, 7 }, /* DECAWM */ + { MODE_CURSOR_BLINKING, 12 }, /* cursor blinking */ + { MODE_CURSOR, 25 }, /* DECTCEM */ + { MODE_MOUSE_STANDARD, 1000 }, /* mouse normal tracking */ + { MODE_MOUSE_BUTTON, 1002 }, /* mouse button tracking */ + { MODE_MOUSE_ALL, 1003 }, /* mouse any tracking */ + { MODE_FOCUSON, 1004 }, /* focus reporting */ + { MODE_MOUSE_UTF8, 1005 }, /* mouse: UTF-8 */ + { MODE_MOUSE_SGR, 1006 }, /* mouse: SGR */ + { MODE_BRACKETPASTE, 2004 }, /* bracketed paste */ + { MODE_SYNC, 2026 }, /* synchronized output */ + { MODE_THEME_UPDATES, 2031 }, /* theme update notifications */ + }; + int mode; + char *value = NULL, *tmp; + u_int i; + + if (ft->wp == NULL) + return (NULL); + mode = ft->wp->base.mode; + + for (i = 0; i < nitems(table); i++) { + if (~mode & table[i].mode) + continue; + /* + * Only report cursor blinking when set by the application, not + * when it comes from the cursor-style option. + */ + if (table[i].mode == MODE_CURSOR_BLINKING && + (~mode & MODE_CURSOR_BLINKING_SET)) + continue; + if (value == NULL) + xasprintf(&value, "%d", table[i].number); + else { + xasprintf(&tmp, "%s,%d", value, table[i].number); + free(value); + value = tmp; + } + } + if (value == NULL) + return (xstrdup("")); + return (value); +} + /* Callback for pane_active. */ static void * format_cb_pane_active(struct format_tree *ft) @@ -2216,6 +2307,19 @@ format_cb_pane_last_output_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_output_generation. */ +static void * +format_cb_pane_output_generation(struct format_tree *ft) +{ + unsigned long long value; + + if (ft->wp != NULL) { + value = ft->wp->output_generation; + return (format_printf("%llu", value)); + } + return (NULL); +} + /* Callback for pane_last_prompt_time. */ static void * format_cb_pane_last_prompt_time(struct format_tree *ft) @@ -2622,20 +2726,23 @@ format_cb_pane_unzoomed_width(struct format_tree *ft) { struct window_pane *wp = ft->wp; struct layout_cell *lc; - int sb_w, sb_pad; + int saved, sb_w, sb_pad; u_int sx; if (wp == NULL) return (NULL); lc = wp->saved_layout_cell; + saved = (lc != NULL); if (lc == NULL) lc = wp->layout_cell; if (lc == NULL) return (NULL); sx = lc->g.sx; - if (window_pane_scrollbar_reserve(wp)) { + if ((saved && !SCREEN_IS_ALTERNATE(&wp->base) && + wp->window->sb == PANE_SCROLLBARS_ALWAYS) || + (!saved && window_pane_scrollbar_reserve(wp))) { sb_w = wp->scrollbar_style.width; sb_pad = wp->scrollbar_style.pad; if (sb_w < 1) @@ -3600,12 +3707,21 @@ static const struct format_table_entry format_table[] = { { "cursor_y", FORMAT_TABLE_STRING, format_cb_cursor_y }, + { "history_added", FORMAT_TABLE_STRING, + format_cb_history_added + }, { "history_all_bytes", FORMAT_TABLE_STRING, format_cb_history_all_bytes }, { "history_bytes", FORMAT_TABLE_STRING, format_cb_history_bytes }, + { "history_collected", FORMAT_TABLE_STRING, + format_cb_history_collected + }, + { "history_generation", FORMAT_TABLE_STRING, + format_cb_history_generation + }, { "history_limit", FORMAT_TABLE_STRING, format_cb_history_limit }, @@ -3786,6 +3902,9 @@ static const struct format_table_entry format_table[] = { { "pane_mode", FORMAT_TABLE_STRING, format_cb_pane_mode }, + { "pane_output_generation", FORMAT_TABLE_STRING, + format_cb_pane_output_generation + }, { "pane_path", FORMAT_TABLE_STRING, format_cb_pane_path }, @@ -3804,6 +3923,9 @@ static const struct format_table_entry format_table[] = { { "pane_pipe_pid", FORMAT_TABLE_STRING, format_cb_pane_pipe_pid }, + { "pane_private_modes", FORMAT_TABLE_STRING, + format_cb_pane_private_modes + }, { "pane_right", FORMAT_TABLE_STRING, format_cb_pane_right }, @@ -4191,6 +4313,8 @@ format_log_debug_cb(const char *key, const char *value, void *arg) void format_log_debug(struct format_tree *ft, const char *prefix) { + if (log_get_level() == 0) + return; format_each(ft, format_log_debug_cb, (void *)prefix); } @@ -4319,7 +4443,7 @@ format_quote_shell(const char *s) at = out = xmalloc(strlen(s) * 2 + 1); for (cp = s; *cp != '\0'; cp++) { - if (strchr("|&;<>()$`\\\"'*?[# =%", *cp) != NULL) + if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL) *at++ = '\\'; *at++ = *cp; } @@ -4797,7 +4921,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, /* * Modifiers are a ; separated list of the forms: - * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,<,> + * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,A,<,> * =a * =/a * =/a/ @@ -4816,7 +4940,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, break; /* Check single character modifiers with no arguments. */ - if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL && + if (strchr("labdnwETSWPOVL!<>A", cp[0]) != NULL && format_is_end(cp[1])) { format_add_modifier(&list, count, cp, 1, NULL, 0); cp++; @@ -4838,7 +4962,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, } /* Now try single character with arguments. */ - if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL) + if (strchr("ImCLNPSOVst=pReqWcA", cp[0]) == NULL) break; c = cp[0]; @@ -5797,6 +5921,74 @@ fail: return (NULL); } +/* Callback for the cycle timer; redraw the status line. */ +static void +format_cycle_callback(__unused int fd, __unused short events, void *arg) +{ + struct client *c = arg; + + if (c->message_string == NULL && c->prompt == NULL) + c->flags |= CLIENT_REDRAWSTATUS; +} + +/* Arm the cycle timer to redraw the status line if it is not already. */ +static void +format_cycle_start_timer(struct client *c) +{ + struct timeval tv; + + tv.tv_sec = FORMAT_CYCLE_PERIOD / 1000; + tv.tv_usec = (FORMAT_CYCLE_PERIOD % 1000) * 1000L; + + if (!event_initialized(&c->cycle_timer)) + evtimer_set(&c->cycle_timer, format_cycle_callback, c); + if (!evtimer_pending(&c->cycle_timer, NULL)) + evtimer_add(&c->cycle_timer, &tv); +} + +/* Expand the "A" animation modifier; see the manual for the syntax. */ +static char * +format_cycle(struct format_expand_state *es, const char *frames, u_int count) +{ + struct format_tree *ft = es->ft; + const char *start, *end, *cp; + u_int n, index, i; + + /* + * A cycle is only expanded in a status format, and never in the + * command or output of #() where it would change on every frame and + * make the job run again. + */ + if (!(ft->flags & FORMAT_STATUS) || (es->flags & FORMAT_EXPAND_NOCYCLE)) + return (xstrdup("")); + if (*frames == '\0') + return (xstrdup("")); + + /* Count the comma-separated frames (there is at least one). */ + n = 1; + for (cp = frames; *cp != '\0'; cp++) { + if (*cp == ',') + n++; + } + index = (es->start_time / (count * FORMAT_CYCLE_PERIOD)) % n; + + /* + * Redraw the status line so the frames advance on their own; a + * single frame never changes so there is nothing to redraw for. + */ + if (n > 1 && ft->client != NULL) + format_cycle_start_timer(ft->client); + + /* Walk to the chosen frame and return a copy of it. */ + start = frames; + for (i = 0; i < index; i++) + start = strchr(start, ',') + 1; + end = strchr(start, ','); + if (end == NULL) + end = start + strlen(start); + return (xstrndup(start, end - start)); +} + /* Replace a key. */ static int format_replace(struct format_expand_state *es, const char *key, size_t keylen, @@ -5817,6 +6009,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, struct format_modifier *list, *cmp = NULL, *search = NULL; struct format_modifier **sub = NULL, *mexp = NULL, *fm; struct format_modifier *bool_op_n = NULL; + u_int cycle_count = 1; u_int i, count, nsub = 0, nrep, check = 0; const char *loop_flags = ""; struct format_expand_state next; @@ -5877,6 +6070,15 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, if (errstr != NULL) width = 0; break; + case 'A': + modifiers |= FORMAT_CYCLE; + if (fm->argc < 1) + break; + cycle_count = strtonum(fm->argv[0], 1, 100, + &errstr); + if (errstr != NULL) + cycle_count = 1; + break; case 'w': modifiers |= FORMAT_WIDTH; break; @@ -6098,6 +6300,13 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, goto done; } + /* Is this an animation cycle? */ + if (modifiers & FORMAT_CYCLE) { + value = format_cycle(es, copy, cycle_count); + format_log(es, "cycle '%s' is: %s", copy, value); + goto done; + } + /* Is this a literal string? */ if (modifiers & FORMAT_LITERAL) { format_log(es, "literal string is '%s'", copy); diff --git a/grid-reader.c b/grid-reader.c index 9b40def6c..4cf47b8cb 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ +/* $OpenBSD: grid-reader.c,v 1.11 2026/07/29 17:42:56 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee @@ -54,11 +54,8 @@ grid_reader_cursor_right(struct grid_reader *gr, int wrap, int all, int onemore) px = gr->gd->sx; else if (onemore) px = grid_reader_line_length(gr); - else { - px = grid_reader_line_length(gr); - if (px != 0) - px--; - } + else + px = grid_line_limit(gr->gd, gr->cy); if (wrap && gr->cx >= px && gr->cy < gr->gd->hsize + gr->gd->sy - 1) { grid_reader_cursor_start_of_line(gr, 0); diff --git a/grid-view.c b/grid-view.c index 86317526f..5659d5763 100644 --- a/grid-view.c +++ b/grid-view.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ +/* $OpenBSD: grid-view.c,v 1.39 2026/08/03 12:58:53 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -47,9 +47,9 @@ grid_view_set_cell(struct grid *gd, u_int px, u_int py, /* Set padding. */ void -grid_view_set_padding(struct grid *gd, u_int px, u_int py) +grid_view_set_padding(struct grid *gd, u_int px, u_int py, int bg) { - grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py)); + grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py), bg); } /* Set cells. */ diff --git a/grid.c b/grid.c index 7d9de4dc4..8eb9bf8cd 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.154 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -682,9 +682,13 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc) /* Set padding at position. */ void -grid_set_padding(struct grid *gd, u_int px, u_int py) +grid_set_padding(struct grid *gd, u_int px, u_int py, int bg) { - grid_set_cell(gd, px, py, &grid_padding_cell); + struct grid_cell gc; + + memcpy(&gc, &grid_padding_cell, sizeof gc); + gc.bg = bg; + grid_set_cell(gd, px, py, &gc); } /* Set cells at position. */ @@ -1631,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) @@ -1645,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 { @@ -1681,27 +1685,55 @@ grid_line_length(struct grid *gd, u_int py) return (px); } +/* Get last position on line, not including padding. */ +u_int +grid_line_limit(struct grid *gd, u_int py) +{ + struct grid_cell gc; + u_int px; + + px = grid_line_length(gd, py); + if (px == 0) + return (0); + px--; + while (px > 0) { + grid_get_cell(gd, px, py, &gc); + if (~gc.flags & GRID_FLAG_PADDING) + break; + px--; + } + return (px); +} + /* Check if character is in set. */ int grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) { 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/hooks.c b/hooks.c index ae7d90de4..acc11704d 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.14 2026/07/13 22:03:08 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.16 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -25,7 +25,7 @@ #include "tmux.h" /* Hook monitor state owned by an option entry. */ -struct hook_monitor { +struct hooks_monitor { struct options *oo; struct monitor_set *set; @@ -143,6 +143,7 @@ hooks_insert(struct cmdq_item *item, struct hooks_data *hd) log_debug("%s: hook %s not found", __func__, hd->name); return; } + options_hook_fired(o); if (item == NULL) state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); @@ -233,7 +234,7 @@ hooks_event_cb(const char *name, struct event_payload *ep, { struct cmdq_item *item; - if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) + if (event_payload_get_pointer(ep, "_hooks_monitor") != NULL) return; item = event_payload_get_pointer(ep, "_cmdq_item"); @@ -324,7 +325,7 @@ hooks_run(struct cmdq_item *item, const char *name) void hooks_monitor_free(void *data) { - struct hook_monitor *hm = data; + struct hooks_monitor *hm = data; events_remove_sink(hm->sink); monitor_destroy(hm->set); @@ -337,7 +338,7 @@ void hooks_monitor_remove(struct options *oo, const char *name) { struct options_entry *o; - struct hook_monitor *hm; + struct hooks_monitor *hm; o = options_get_only(oo, name); if (o == NULL) @@ -355,9 +356,9 @@ static void hooks_monitor_hook_cb(const char *name, struct event_payload *ep, void *sink_data) { - struct hook_monitor *hm = sink_data; + struct hooks_monitor *hm = sink_data; - if (event_payload_get_pointer(ep, "_hook_monitor") == hm) + if (event_payload_get_pointer(ep, "_hooks_monitor") == hm) hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); } @@ -365,14 +366,14 @@ hooks_monitor_hook_cb(const char *name, struct event_payload *ep, static void hooks_monitor_cb(struct monitor_change *change, void *data) { - struct hook_monitor *hm = data; + struct hooks_monitor *hm = data; struct event_payload *ep; struct winlink *wl = change->wl; struct window_pane *wp = change->wp; struct cmd_find_state fs; ep = event_payload_create(); - event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); + event_payload_set_pointer(ep, "_hooks_monitor", data, NULL, NULL); cmd_find_clear_state(&fs, 0); if (wl != NULL && wp != NULL && wp->window == wl->window) @@ -402,8 +403,7 @@ hooks_monitor_cb(struct monitor_change *change, void *data) event_payload_set_session(ep, "session", change->s); if (wl != NULL) { if (change->s == NULL) - event_payload_set_session(ep, "session", - wl->session); + event_payload_set_session(ep, "session", wl->session); event_payload_set_window(ep, "window", wl->window); event_payload_set_int(ep, "window_index", wl->idx); } @@ -423,7 +423,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, int flags, struct cmd_find_state *fs, struct session *s) { struct options_entry *o; - struct hook_monitor *hm; + struct hooks_monitor *hm; hooks_monitor_remove(oo, name); o = options_get_only(oo, name); @@ -446,7 +446,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, char * hooks_monitor_to_string(struct options_entry *o) { - struct hook_monitor *hm = options_get_monitor_data(o); + struct hooks_monitor *hm = options_get_monitor_data(o); const char *name = options_name(o); char *s; @@ -472,3 +472,40 @@ hooks_monitor_to_string(struct options_entry *o) } return (s); } + +/* Get the parts of a hook monitor. */ +int +hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id, + const char **format) +{ + struct hooks_monitor *hm = options_get_monitor_data(o); + + if (hm == NULL) + return (0); + *type = hm->type; + *id = hm->id; + *format = hm->format; + return (1); +} + +/* Get hook monitor firing count. */ +u_int +hooks_monitor_get_fire_count(struct options_entry *o) +{ + struct hooks_monitor *hm = options_get_monitor_data(o); + + if (hm == NULL) + return (0); + return (monitor_get_fire_count(hm->set, options_name(o))); +} + +/* Get hook monitor firing time. */ +time_t +hooks_monitor_get_fire_time(struct options_entry *o) +{ + struct hooks_monitor *hm = options_get_monitor_data(o); + + if (hm == NULL) + return (0); + return (monitor_get_fire_time(hm->set, options_name(o))); +} 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 5efb5c355..9d1ee3e1e 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/key-bindings.c b/key-bindings.c index 11e64a248..4b40e3517 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.189 2026/07/15 10:38:31 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.192 2026/08/06 09:05:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -45,6 +45,9 @@ " ''" \ " 'New After' 'w' {new-window -a}" \ " 'New At End' 'W' {new-window}" +#define DEFAULT_EMPTY_MENU \ + " 'New Pane' 'p' {new-pane; join-pane}" \ + " 'New Window' 'w' {new-window}" #define DEFAULT_PANE_MENU \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ @@ -419,6 +422,7 @@ key_bindings_init(void) "bind -N 'Spread panes out evenly' E { select-layout -E }", "bind -N 'Switch to the last client' L { switch-client -l }", "bind -N 'Clear the marked pane' M { select-pane -M }", + "bind -N 'Change the pane title' T { command-prompt -I'#T' { select-pane -T '%%' } }", "bind -N 'Enter copy mode' [ { copy-mode }", "bind -N 'Paste the most recent paste buffer' ] { paste-buffer -p }", "bind -N 'Create a new window' c { new-window }", @@ -552,6 +556,8 @@ key_bindings_init(void) /* Mouse button 3 down on pane. */ "bind -n MouseDown3Pane { if -Ft= '#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}' { select-pane -t=; send -M } { display-menu -t= -xM -yM -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " } }", "bind -n M-MouseDown3Pane { display-menu -t= -xM -yM -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " }", + "bind -n MouseDown3Empty { display-menu -t= -xM -yM -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_EMPTY_MENU " }", + "bind -n M-MouseDown3Empty { display-menu -t= -xM -yM -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_EMPTY_MENU " }", /* Mouse on scrollbar. */ "bind -n MouseDown1ScrollbarUp { if -Ft= '#{pane_in_mode}' { send -X page-up } {copy-mode -u } }", @@ -594,7 +600,7 @@ key_bindings_init(void) "bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode n { send -X search-again }", "bind -Tcopy-mode q { send -X cancel }", - "bind -Tcopy-mode r { send -X refresh-toggle }", + "bind -Tcopy-mode r { send -X refresh-now }", "bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode Home { send -X start-of-line }", "bind -Tcopy-mode End { send -X end-of-line }", @@ -707,7 +713,7 @@ key_bindings_init(void) "bind -Tcopy-mode-vi n { send -X search-again }", "bind -Tcopy-mode-vi o { send -X other-end }", "bind -Tcopy-mode-vi q { send -X cancel }", - "bind -Tcopy-mode-vi r { send -X refresh-toggle }", + "bind -Tcopy-mode-vi r { send -X refresh-now }", "bind -Tcopy-mode-vi t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode-vi v { send -X rectangle-toggle }", "bind -Tcopy-mode-vi w { send -X next-word }", diff --git a/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/menu.c b/menu.c index aab25d7c5..2816056a4 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: menu.c,v 1.69 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -337,6 +337,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) struct cmdq_state *state; enum cmd_parse_status status; char *error; + key_code key; if (KEYC_IS_MOUSE(event->key)) { /* @@ -387,7 +388,8 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event) name = menu->items[i].name; if (name == NULL || *name == '-') continue; - if ((event->key & ~KEYC_MASK_FLAGS) == menu->items[i].key) { + key = (event->key & ~KEYC_MASK_FLAGS); + if (key == (menu->items[i].key & ~KEYC_MASK_FLAGS)) { md->choice = i; goto chosen; } diff --git a/mode-tree.c b/mode-tree.c index defd587bb..5132eb94c 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: mode-tree.c,v 1.100 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -47,7 +47,7 @@ enum mode_tree_preview { "#[acs]x" MODE_TREE_PREFIX_STYLE " }," \ "#{mode_tree_repeat}}" \ "#{?mode_tree_branch," \ - "#[acs]#{?mode_tree_last,mq,tq}" MODE_TREE_PREFIX_STYLE "> ,}" \ + "#[acs]#{?mode_tree_last,mq,tq}+" MODE_TREE_PREFIX_STYLE " ,}" \ "#{?mode_tree_has_children," \ "#{?mode_tree_expanded,#[fg=themered]-" MODE_TREE_PREFIX_STYLE " ," \ "#[fg=themegreen]+" MODE_TREE_PREFIX_STYLE " }," \ diff --git a/monitor.c b/monitor.c index d6338e878..ff0c6e5ed 100644 --- a/monitor.c +++ b/monitor.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: monitor.c,v 1.7 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -59,6 +59,9 @@ struct monitor_item { struct monitor_panes panes; struct monitor_windows windows; + u_int fire_count; + time_t fire_time; + RB_ENTRY(monitor_item) entry; }; RB_HEAD(monitor_items, monitor_item); @@ -180,6 +183,9 @@ monitor_report(struct monitor_set *ms, struct monitor_item *me, log_debug("%s: %s changed to %s", __func__, me->name, value); + me->fire_count++; + me->fire_time = current_time; + change.name = me->name; change.value = value; change.last = last; @@ -659,3 +665,25 @@ monitor_remove(struct monitor_set *ms, const char *name) if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer)) evtimer_del(&ms->timer); } + +/* Get subscription firing count. */ +u_int +monitor_get_fire_count(struct monitor_set *ms, const char *name) +{ + struct monitor_item *me, find = { .name = (char *)name }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) == NULL) + return (0); + return (me->fire_count); +} + +/* Get subscription firing time. */ +time_t +monitor_get_fire_time(struct monitor_set *ms, const char *name) +{ + struct monitor_item *me, find = { .name = (char *)name }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) == NULL) + return (0); + return (me->fire_time); +} diff --git a/options-table.c b/options-table.c index dfa2e09c1..6bee92544 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.240 2026/07/21 11:52:13 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -304,6 +304,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, @@ -1239,7 +1249,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. */ @@ -1465,8 +1476,8 @@ const struct options_table_entry options_table[] = { { .name = "fill-character", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, - .default_str = "", - .text = "Character used to fill unused parts of window." + .default_str = "#{?is_inside,#[bg=themedarkgrey] ,#[fg=themelightgrey]#[acs]~}", + .text = "Format used to fill unused parts of window." }, { .name = "main-pane-height", @@ -1950,6 +1961,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_AFTER_HOOK("show-messages"), OPTIONS_TABLE_AFTER_HOOK("show-options"), OPTIONS_TABLE_AFTER_HOOK("split-window"), + OPTIONS_TABLE_AFTER_HOOK("swap-window"), OPTIONS_TABLE_AFTER_HOOK("unbind-key"), OPTIONS_TABLE_HOOK("alert-activity", "", "Run when a window has activity."), diff --git a/options.c b/options.c index b5238bc69..fa10d8a7d 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options.c,v 1.90 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: options.c,v 1.92 2026/07/27 19:15:58 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -107,7 +107,10 @@ struct options_entry { int cached; struct style style; + void *monitor_data; + u_int fire_count; + time_t fire_time; RB_ENTRY(options_entry) entry; }; @@ -440,6 +443,25 @@ options_set_monitor_data(struct options_entry *o, void *data) o->monitor_data = data; } +void +options_hook_fired(struct options_entry *o) +{ + o->fire_count++; + o->fire_time = current_time; +} + +u_int +options_get_fire_count(struct options_entry *o) +{ + return (o->fire_count); +} + +time_t +options_get_fire_time(struct options_entry *o) +{ + return (o->fire_time); +} + const struct options_table_entry * options_table_entry(struct options_entry *o) { @@ -1372,7 +1394,7 @@ options_push_changes(const char *name) } if (strcmp(name, "fill-character") == 0) { RB_FOREACH(w, windows, &windows) - window_set_fill_character(w); + window_set_fill_cells(w); } if (strcmp(name, "key-table") == 0) { TAILQ_FOREACH(loop, &clients, entry) diff --git a/popup.c b/popup.c index 3bc7eafa6..0345fa367 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -79,7 +79,8 @@ popup_free(struct popup_data *pd) if (pd->job != NULL) job_free(pd->job); - input_free(pd->ictx); + if (pd->ictx != NULL) + input_free(pd->ictx); free(pd->r.ranges); screen_free(&pd->s); diff --git a/proc.c b/proc.c index 7da1b430c..bfe1fbe26 100644 --- a/proc.c +++ b/proc.c @@ -1,4 +1,4 @@ -/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ +/* $OpenBSD: proc.c,v 1.32 2026/08/04 13:16:03 claudio Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -31,6 +31,10 @@ #include #endif +#ifdef HAVE_JEMALLOC +#include +#endif + #include "tmux.h" struct tmuxproc { @@ -75,7 +79,7 @@ static void proc_event_cb(__unused int fd, short events, void *arg) { struct tmuxpeer *peer = arg; - ssize_t n; + int n; struct imsg imsg; if (!(peer->flags & PEER_BAD) && (events & EV_READ)) { @@ -84,7 +88,7 @@ proc_event_cb(__unused int fd, short events, void *arg) return; } for (;;) { - if ((n = imsg_get(&peer->ibuf, &imsg)) == -1) { + if ((n = imsgbuf_get(&peer->ibuf, &imsg)) == -1) { peer->dispatchcb(NULL, peer->arg); return; } @@ -181,6 +185,10 @@ proc_start(const char *name) { struct tmuxproc *tp; struct utsname u; +#ifdef HAVE_JEMALLOC + const char *version; + size_t size = sizeof version; +#endif log_open(name); setproctitle("%s (%s)", name, socket_path); @@ -195,6 +203,11 @@ proc_start(const char *name) #ifdef HAVE_UTF8PROC log_debug("using utf8proc %s", utf8proc_version()); #endif +#ifdef HAVE_JEMALLOC + if (mallctl("version", &version, &size, NULL, 0) != 0) + version = "(unknown version)"; + log_debug("using jemalloc %s", version); +#endif #ifdef NCURSES_VERSION log_debug("using ncurses %s %06u", NCURSES_VERSION, NCURSES_VERSION_PATCH); #endif diff --git a/prompt.c b/prompt.c index c12a249a9..cc808a54e 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */ +/* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1334,7 +1334,7 @@ process_key: if (pr->index != size) { memmove(pr->buffer + pr->index, pr->buffer + pr->index + 1, - (size + 1 - pr->index) * + (size - pr->index) * sizeof *pr->buffer); goto changed; } diff --git a/regress/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/cfg-client-lost.sh b/regress/cfg-client-lost.sh old mode 100755 new mode 100644 diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh index 132890730..4656963e6 100644 --- a/regress/choose-tree.sh +++ b/regress/choose-tree.sh @@ -290,4 +290,92 @@ wait_count ': G4' 7 fail "window 1 of zzz not killed" exit_mode q +# --- help and information preview -------------------------------------------- +# The complete tree help is taller than the 24-line test terminal, where the +# help renderer deliberately leaves the tree unchanged. +$TMUX2 resize-window -t out:0 -x 80 -y 50 || exit 1 +i=0 +while [ "$i" -lt 50 ]; do + # The inner session has a one-line status, leaving a 49-line window. + [ "$($TMUX display-message -p -t aaa:0 '#{window_height}')" -ge 49 ] && + break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "attached client did not resize for tree help" +$TMUX choose-tree -t aaa:0 -F 'G5' -O index || exit 1 +wait_count ': G5' 7 +$TMUX2 send-keys -t out:0 F1 || fail "send-keys F1 failed" +wait_for 'Swap current and previous window' +# The first key closes help; the second exits the tree. +$TMUX send-keys -t aaa:0 q q || fail "send-keys help exit failed" +wait_mode aaa:0 0 + +$TMUX choose-tree -t aaa:0 -F 'G6' -O index || exit 1 +wait_count ': G6' 7 +$TMUX send-keys -t aaa:0 i || fail "send-keys info failed" +wait_for 'Session' +$TMUX send-keys -t aaa:0 i || fail "send-keys preview failed" +exit_mode q + +# --- window swapping and command prompt for current/tagged items ------------- +# Restrict a window-only tree to two disposable windows in aaa. J (the +# Shift-Down equivalent) swaps their window objects while leaving their indexes +# in place. +$TMUX new-window -d -t aaa: -n swap-a 'cat' || exit 1 +$TMUX new-window -d -t aaa: -n swap-b 'cat' || exit 1 +aidx=$($TMUX list-windows -t aaa -F '#{window_index}:#{window_name}' | \ + awk -F: '$2 == "swap-a" {print $1}') +bidx=$($TMUX list-windows -t aaa -F '#{window_index}:#{window_name}' | \ + awk -F: '$2 == "swap-b" {print $1}') +$TMUX choose-tree -w -t aaa:0 -O index -F 'WT #{window_name}' \ + -f '#{m:swap-*,#{window_name}}' || exit 1 +wait_count 'WT swap-' 2 +$TMUX send-keys -t aaa:0 g j J || fail "window tree swap failed" +i=0 +while [ "$i" -lt 50 ]; do + aname=$($TMUX display-message -p -t "aaa:$aidx" '#{window_name}') + bname=$($TMUX display-message -p -t "aaa:$bidx" '#{window_name}') + [ "$aname:$bname" = 'swap-b:swap-a' ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "window tree did not swap the two windows" + +# ':' runs an entered command for the current item and rebuilds the tree when +# its queued command completes. +$TMUX set-option -g @tree-command '' || exit 1 +$TMUX send-keys -t aaa:0 : || fail "window tree command prompt failed" +wait_for '(current)' +$TMUX send-keys -t aaa:0 -l "set-option -g @tree-command '%%'" || exit 1 +$TMUX send-keys -t aaa:0 Enter || exit 1 +i=0 +while [ "$i" -lt 50 ]; do + value=$($TMUX show-option -gqv @tree-command) + [ -n "$value" ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "window tree entered command did not run" +case "$value" in +=aaa:*.) ;; +*) fail "window tree command target was '$value'" ;; +esac + +# Tag both disposable windows and kill them through the tagged confirmation +# callback. The mode-hosting window is outside the filter and survives. +$TMUX send-keys -t aaa:0 g j t j t X || fail "tagged kill keys failed" +wait_for 'Kill 2 tagged?' +$TMUX send-keys -t aaa:0 y || fail "tagged kill confirmation failed" +i=0 +while [ "$i" -lt 50 ]; do + left=$($TMUX list-windows -t aaa -F '#{window_name}' | \ + grep -c '^swap-' || true) + [ "$left" -eq 0 ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "tagged windows were not killed" +exit_mode q + exit 0 diff --git a/regress/clear-on-attach.sh b/regress/clear-on-attach.sh new file mode 100644 index 000000000..012064660 --- /dev/null +++ b/regress/clear-on-attach.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +# With clear-on-attach disabled tmux must preserve the terminal contents by +# scrolling them away, rather than entering and clearing the alternate screen. +# Capture the bytes written by a real attached client for both settings. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER_ON="$TEST_TMUX -LtestIon$$ -f/dev/null" +OUTER_ON="$TEST_TMUX -LtestOon$$ -f/dev/null" +INNER_OFF="$TEST_TMUX -LtestIoff$$ -f/dev/null" +OUTER_OFF="$TEST_TMUX -LtestOoff$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER_ON kill-server 2>/dev/null + $INNER_ON kill-server 2>/dev/null + $OUTER_OFF kill-server 2>/dev/null + $INNER_OFF kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $current_inner list-clients 2>/dev/null | grep -q . && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +capture_attach() +{ + setting=$1 + output=$2 + go=$DIR/go-$setting + if [ "$setting" = on ]; then + current_inner=$INNER_ON + current_outer=$OUTER_ON + else + current_inner=$INNER_OFF + current_outer=$OUTER_OFF + fi + + $current_inner new-session -d -s inner -x40 -y10 \ + 'exec sleep 100' || exit 1 + $current_inner set-option -g status off || exit 1 + $current_inner set-option -g clear-on-attach "$setting" || exit 1 + $current_outer new-session -d -s outer -x40 -y10 \ + "while [ ! -e '$go' ]; do sleep 0.01; done; exec $current_inner attach-session -t inner" || exit 1 + $current_outer set-option -g status off || exit 1 + $current_outer pipe-pane -O "cat >'$output'" || exit 1 + touch "$go" + wait_for_client + sleep 0.5 + $current_outer pipe-pane -O || exit 1 +} + +on=$DIR/on +off=$DIR/off +capture_attach on "$on" +capture_attach off "$off" + +smcup=$(printf '\033[?1049h') +grep -Fq "$smcup" "$on" || fail "clear-on-attach on did not use smcup" +if grep -Fq "$smcup" "$off"; then + fail "clear-on-attach off used smcup" +fi + +# Some screen terminfo entries have indn (CSI Ps S), while older ones only +# have ind (newline). The preserving path uses indn once or repeats ind once +# for every line in the terminal plus one. +escape=$(printf '\033') +if ! LC_ALL=C grep -Eq "${escape}\\[[0-9]+S" "$off"; then + hex=$(od -An -tx1 -v "$off" | tr -d ' \n') + case "$hex" in + *0a0a0a0a0a0a0a0a0a0a0a*) ;; + *) fail "clear-on-attach off did not scroll the old contents away" ;; + esac +fi + +exit 0 diff --git a/regress/client-key-order-focus.sh b/regress/client-key-order-focus.sh new file mode 100644 index 000000000..4b0c27eb5 --- /dev/null +++ b/regress/client-key-order-focus.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +# send-keys -K injects a key into a client. Commands produced by that key must +# be inserted immediately after send-keys, before later commands in the same +# command list. Focus events arriving between a prefix and its following key +# must be forwarded without cancelling the prefix table. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER="$TEST_TMUX -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + client=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$client" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_for_option() +{ + option=$1 + expected=$2 + i=0 + while [ "$i" -lt 50 ]; do + actual=$($INNER show-option -gv "$option" 2>/dev/null) + [ "$actual" = "$expected" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "$option is '$actual', expected '$expected'" +} + +cleanup +$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g focus-events on || exit 1 +$OUTER new-session -d -s outer -x40 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g status off || exit 1 +wait_for_client + +# The binding generated by send-keys -K must run before the command which +# follows send-keys in this command list. Before the queue-ordering fix this +# produces AB rather than BA. +$INNER set-option -g @order '' || exit 1 +$INNER bind-key -n a set-option -gF @order '#{@order}B' || exit 1 +$INNER send-keys -K -c "$client" a \; \ + set-option -gF @order '#{@order}A' || exit 1 +wait_for_option @order BA + +# Put the real attached client in the prefix table, deliver FocusIn, then the +# bound key. FocusIn is not bindable and must not reset the prefix table. +$INNER bind-key x set-option -g @focus-prefix yes || exit 1 +$OUTER send-keys C-b || exit 1 +focus_in=$(printf '\033[I') +$OUTER send-keys -l "$focus_in" || exit 1 +$OUTER send-keys x || exit 1 +wait_for_option @focus-prefix yes + +exit 0 diff --git a/regress/clock-mode.sh b/regress/clock-mode.sh new file mode 100644 index 000000000..846e8dc7e --- /dev/null +++ b/regress/clock-mode.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +# Exercise clock-mode initialization, all four styles, its one-second timer, +# resize into the compact renderer, arbitrary-key exit, and cleanup. + +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 -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +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 + +capture() +{ + $OUTER capture-pane -p -t outer:0.0 2>/dev/null +} + +wait_mode() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER display-message -p -t clock:0 '#{pane_mode}' 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "pane mode is '$got', expected '$want'" +} + +wait_hashes() +{ + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + printf '%s\n' "$captured" | grep -q '#' && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "large clock did not render" +} + +$INNER new-session -d -s clock -x80 -y24 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -w clock-mode-colour red || exit 1 +$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t clock" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +sleep 1 + +for style in 12 24 12-with-seconds 24-with-seconds; do + $INNER set-option -w -t clock:0 clock-mode-style "$style" || exit 1 + $INNER clock-mode -t clock:0 || exit 1 + wait_mode clock-mode + wait_hashes + # Let at least one timer callback observe a new second. + [ "$style" != 12-with-seconds ] || sleep 1.2 + $INNER send-keys -t clock:0 x || exit 1 + wait_mode '' +done + +# Resizing an active mode below the large-glyph threshold selects the compact +# renderer. A visible time contains a colon and no block-clock hash. +$INNER clock-mode -t clock:0 || exit 1 +wait_mode clock-mode +$INNER resize-window -t clock:0 -x20 -y5 || exit 1 +i=0 +while [ "$i" -lt 50 ]; do + captured=$(capture) + printf '%s\n' "$captured" | grep -Eq '[0-9][0-9]?:[0-9][0-9]' && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "compact clock did not render after resize" +printf '%s\n' "$captured" | grep -q '#' && + fail "compact clock still used large glyphs" +$INNER send-keys -t clock:0 Enter || exit 1 +wait_mode '' + +$INNER has-session -t clock || fail "server died during clock-mode tests" +exit 0 diff --git a/regress/confirm-before-lock.sh b/regress/confirm-before-lock.sh new file mode 100644 index 000000000..db570ab10 --- /dev/null +++ b/regress/confirm-before-lock.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +# Exercise confirm-before's waiting and background callbacks, custom keys and +# prompts, default-yes handling and errors. Also cover all three lock command +# entry points with a harmless lock-command which leaves a marker. + +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 -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +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 + +capture() +{ + $OUTER capture-pane -p -t outer:0.0 2>/dev/null +} + +wait_capture() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + printf '%s\n' "$captured" | grep -Fq "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "timed out waiting for '$marker'" +} + +wait_option() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER show-option -gqv @confirmed 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "@confirmed is '$got', expected '$want'" +} + +wait_file() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + [ -f "$marker" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "lock command did not create $marker" +} + +$INNER new-session -d -s test -x80 -y24 'exec sleep 100' || exit 1 +$INNER set-option -g status on || exit 1 +$INNER set-option -g status-position bottom || exit 1 +$INNER set-option -g window-size manual || exit 1 +$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t test" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +sleep 1 + +client=$($INNER list-clients -F '#{client_name}') +[ -n "$client" ] || fail "inner client did not attach" + +$INNER bind-key -n M-a confirm-before \ + 'set-option -g @confirmed accepted' || exit 1 +$INNER bind-key -n M-b confirm-before -b -p 'Background?' \ + 'set-option -g @confirmed background' || exit 1 +$INNER bind-key -n M-c confirm-before -c x -p 'Custom?' \ + 'set-option -g @confirmed custom' || exit 1 +$INNER bind-key -n M-y confirm-before -y -p 'Default?' \ + 'set-option -g @confirmed default' || exit 1 + +# A non-confirming response closes the waiting prompt without running its +# command; reopening and accepting inserts the command after the waiting item. +$INNER set-option -g @confirmed sentinel || exit 1 +$OUTER send-keys M-a || exit 1 +wait_capture "Confirm 'set-option'? (y/n)" +$OUTER send-keys n || exit 1 +sleep 0.2 +wait_option sentinel +$OUTER send-keys M-a || exit 1 +wait_capture "Confirm 'set-option'? (y/n)" +$OUTER send-keys y || exit 1 +wait_option accepted + +# -b has no waiting queue item and appends its command to the client queue. +$OUTER send-keys M-b || exit 1 +wait_capture 'Background?' +$OUTER send-keys y || exit 1 +wait_option background + +# A custom printable key is accepted, and -y makes Enter affirmative. +$OUTER send-keys M-c || exit 1 +wait_capture 'Custom?' +$OUTER send-keys x || exit 1 +wait_option custom +$OUTER send-keys M-y || exit 1 +wait_capture 'Default?' +$OUTER send-keys Enter || exit 1 +wait_option default + +# Reject invalid multi-character and control confirmation keys. +if $INNER confirm-before -b -t "$client" -c xx 'display-message x' \ + >/dev/null 2>&1; then + fail "multi-character confirm key was accepted" +fi +if $INNER confirm-before -b -t "$client" -c "$(printf '\001')" \ + 'display-message x' >/dev/null 2>&1; then + fail "control confirm key was accepted" +fi + +# lock-client, lock-session and lock-server all send lock-command to the real +# attached client. The command returns immediately and unlocks the client. +for kind in client session server; do + marker="$DIR/locked-$kind" + $INNER set-option -t test lock-command "printf locked >$marker" || exit 1 + case "$kind" in + client) $INNER lock-client -t "$client" || exit 1 ;; + session) $INNER lock-session -t test || exit 1 ;; + server) $INNER lock-server || exit 1 ;; + esac + wait_file "$marker" +done + +$INNER has-session -t test || fail "server died during prompt or lock tests" +exit 0 diff --git a/regress/control-client-exit-stalled.sh b/regress/control-client-exit-stalled.sh new file mode 100644 index 000000000..431fd66d8 --- /dev/null +++ b/regress/control-client-exit-stalled.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# A control client whose terminal has stopped accepting output (for example +# its ssh connection or terminal emulator died) never drains the output that +# server_client_check_exit waits on before sending MSG_EXIT, and may then +# never close its socket either. This must not prevent the server from +# exiting: after kill-server the server previously stayed alive forever with +# server_exit set, closing every new connection immediately, so every new +# client failed with "server exited unexpectedly" until the server was killed +# by hand. The server now bounds the exit handshake: it discards output it +# can never deliver and drops the client if it still does not close. +# +# The stalled terminal is simulated by a fifo whose read end is held open but +# never read. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +SERVER= + +mkfifo "$FIFO" || exit 1 + +cleanup() { + [ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +# A detached session whose pane floods printable output forever. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1 +SERVER=$($TMUX display -pt rt '#{pid}') + +# Attach a control client with output down the fifo; stdin held open by sleep +# so it stays attached. The fifo fills and is never read, so a backlog forms +# in the server that can never be delivered. +( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" + +n=0 +while [ $n -lt 50 ]; do + $TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break + sleep 0.1 + n=$((n + 1)) +done +$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . || + { echo "control client did not attach"; exit 1; } + +# Let the pane flood fill the fifo so the client's output is stuck. +sleep 2 + +# Ask the server to exit. This sets CLIENT_EXIT on the stalled client, whose +# output can never drain. +$TMUX kill-server 2>/dev/null + +# The server must exit within the exit timeout (10 seconds) plus slack, +# rather than staying in limbo forever rejecting new clients. +n=0 +while [ $n -lt 100 ]; do + if ! kill -0 "$SERVER" 2>/dev/null; then + SERVER= + exit 0 + fi + sleep 0.2 + n=$((n + 1)) +done + +echo "server did not exit after kill-server; new clients see:" +$TMUX ls 2>&1 +exit 1 diff --git a/regress/control-client-popup.sh b/regress/control-client-popup.sh new file mode 100644 index 000000000..8a98d34e9 --- /dev/null +++ b/regress/control-client-popup.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# Popups require a tty overlay and cannot be displayed by a control client. +# A popup command from control mode must be ignored cleanly, leaving the +# client command queue and server usable. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 +FIFO=$DIR/input +OUT=$DIR/output +RAN=$DIR/popup-ran + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + exec 3>&- + $TMUX kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +mkfifo "$FIFO" || exit 1 +$TMUX new-session -d -s control -x40 -y10 'exec sleep 100' || exit 1 +$TMUX -C attach-session -t control <"$FIFO" >"$OUT" 2>&1 & +control_pid=$! +exec 3>"$FIFO" + +i=0 +while [ "$i" -lt 50 ]; do + $TMUX list-clients -F '#{client_flags}' 2>/dev/null | + grep -q 'control-mode' && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "control client did not attach" + +printf '%s\n' "display-popup -E 'touch $RAN'" >&3 +printf '%s\n' "display-message -p CONTROL-POPUP-ALIVE" >&3 + +i=0 +while [ "$i" -lt 50 ]; do + grep -q 'CONTROL-POPUP-ALIVE' "$OUT" 2>/dev/null && break + if ! kill -0 "$control_pid" 2>/dev/null; then + fail "control client exited after display-popup" + fi + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "command after control-client popup did not run" +$TMUX has-session -t control || fail "server exited after control-client popup" +[ ! -e "$RAN" ] || fail "control client started a popup command" + +exit 0 diff --git a/regress/control-client-run-shell-exit.sh b/regress/control-client-run-shell-exit.sh new file mode 100644 index 000000000..e8acc0419 --- /dev/null +++ b/regress/control-client-run-shell-exit.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# A waited run-shell command can complete after a control client has closed its +# input and exited. The command queue still references the client until the job +# callback runs, but control_stop has already destroyed c->control_state. Late +# command output must be discarded instead of dereferencing the stopped control +# state and killing the server. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +OUT=$(mktemp) +trap "$TMUX kill-server 2>/dev/null; rm -f $OUT" 0 1 15 + +$TMUX -f/dev/null new-session -d -s main || exit 1 + +printf '%s\n' 'run-shell "sleep 0.2; echo TEST"' | + $TMUX -f/dev/null -C >"$OUT" 2>&1 + +sleep 1 + +$TMUX has-session -t main 2>/dev/null || { + echo "server exited after late run-shell output" + cat "$OUT" + exit 1 +} + +grep -q '^%exit' "$OUT" || { + echo "control client did not exit" + cat "$OUT" + exit 1 +} + +exit 0 diff --git a/regress/control-notify-guard.sh b/regress/control-notify-guard.sh new file mode 100644 index 000000000..459ec9f35 --- /dev/null +++ b/regress/control-notify-guard.sh @@ -0,0 +1,158 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +BADCFG="$TMPDIR/bad.conf" +PID= + +cleanup() +{ + exec 3>&- 2>/dev/null + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + [ -s "$OUT" ] && cat "$OUT" >&2 + exit 1 +} + +wait_for() +{ + pattern=$1 + timeout=${2:-60} + i=0 + + while [ "$i" -lt "$timeout" ]; do + grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "missing: $pattern" +} + +wait_for_count() +{ + pattern=$1 + expected=$2 + timeout=${3:-60} + i=0 + + while [ "$i" -lt "$timeout" ]; do + count=$(grep -F -c -- "$pattern" "$OUT" 2>/dev/null) + [ "$count" -ge "$expected" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "missing count $expected for: $pattern" +} + +send() +{ + printf '%s\n' "$1" >&3 || fail "failed to send: $1" +} + +# A notification is a standalone protocol message. It must never be part of +# the response between the real %begin and matching %end or %error for a +# command. Ignore guard-looking command output while a real guard is open. +check_guards() +{ + awk ' + function notification(line) { + return line ~ /^%(sessions-changed|session-changed |unlinked-window-add )/ || + line ~ /^%(pause |continue |config-error )/ + } + !open && $0 ~ /^%begin [0-9]+ [0-9]+ [0-9]+$/ { + open = 1 + time = $2 + number = $3 + flags = $4 + next + } + open && ($0 == "%end " time " " number " " flags || + $0 == "%error " time " " number " " flags) { + open = 0 + next + } + notification($0) { + seen[$1]++ + if (open) { + print "notification inside guard: " $0 > "/dev/stderr" + bad = 1 + } + } + END { + if (open) { + print "unterminated command guard" > "/dev/stderr" + bad = 1 + } + if (seen["%sessions-changed"] < 2 || + seen["%session-changed"] < 1 || + seen["%unlinked-window-add"] < 2 || + seen["%pause"] < 1 || seen["%continue"] < 1 || + seen["%config-error"] < 1) { + print "missing expected notification" > "/dev/stderr" + bad = 1 + } + exit bad + }' "$OUT" || fail "bad control protocol notification ordering" +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s guard -x 80 -y 24 || exit 1 +pane=$($TMUX display-message -p -t guard:0.0 '#{pane_id}') || exit 1 + +mkfifo "$IN" || exit 1 +: >"$OUT" +$TMUX -C attach-session -t guard <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +# Attaching generates a session notification synchronously from inside the +# attach command. It must follow the closing guard. +wait_for '%session-changed ' + +# Guard-looking output is arbitrary command output, not protocol state. Make +# new-session both generate notifications and print an unmatched fake %begin; +# it must not leave those notifications permanently deferred. +send "set-option -g @fake-begin '%begin 1 2 3'" +send "new-session -d -P -F '#{@fake-begin}' -s fake-begin" +wait_for_count '%sessions-changed' 1 + +# Likewise, fake %end output from a command that generates multiple +# notifications must not flush them before the command actually ends. +send "set-option -g @fake-end '%end 1 2 3'" +send "new-session -d -P -F '#{@fake-end}' -s fake-end" +wait_for_count '%sessions-changed' 2 + +# Exercise notification paths outside control-notify.c. +send "refresh-client -A '$pane:pause'" +wait_for "%pause $pane" +send "refresh-client -A '$pane:continue'" +wait_for "%continue $pane" + +printf '%s\n' 'not-a-command' >"$BADCFG" +send "source-file '$BADCFG'" +wait_for '%config-error ' + +# A final command guarantees all earlier command guards have completed before +# the transcript is checked. +send "display-message -p 'guard-check-done'" +wait_for 'guard-check-done' +check_guards + +exit 0 diff --git a/regress/copy-mode-commands.sh b/regress/copy-mode-commands.sh new file mode 100644 index 000000000..aa94e9ef2 --- /dev/null +++ b/regress/copy-mode-commands.sh @@ -0,0 +1,253 @@ +#!/bin/sh + +# Exercise the broad non-mouse copy-mode command surface: navigation, search, +# jumps, state toggles, line/end-of-line copies, append, and pipe variants. + +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 +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +x() +{ + $TMUX send-keys -t copy:0 -X "$@" || fail "copy command failed: $*" +} + +wait_mode() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t copy:0 '#{pane_in_mode}' 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "pane_in_mode is '$got', expected '$want'" +} + +fresh() +{ + $TMUX copy-mode -t copy:0 || exit 1 + wait_mode 1 + x history-top + x start-of-line +} + +select_text() +{ + fresh + x begin-selection + x cursor-right + x cursor-right +} + +$TMUX new-session -d -s copy -x40 -y8 \ + "sh -c 'i=0; while [ \$i -lt 40 ]; do printf \" line-%02d needle (a[b]c) end\\n\" \$i; i=\$((i + 1)); done; printf \"\\nparagraph two needle\\n\\nparagraph three\\n\"; printf \"\\033]133;A\\007prompt-one\\n\\033]133;C\\007output-one\\n\\033]133;A\\007prompt-two\\n\\033]133;C\\007output-two\\n\"; exec sleep 100'" || + exit 1 +$TMUX set-option -g status off || exit 1 +$TMUX set-option -g history-limit 200 || exit 1 + +i=0 +while [ "$i" -lt 50 ]; do + history=$($TMUX display-message -p -t copy:0 '#{history_size}') + [ "$history" -ge 30 ] && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "history did not fill" + +fresh + +# Copy-mode-specific format callbacks expose the word, line, hyperlink and +# active search match at the cursor. +formats=$($TMUX display-message -p -t copy:0 \ + '#{copy_cursor_word}|#{copy_cursor_line}|#{copy_cursor_hyperlink}|#{search_match}') || + exit 1 +[ -n "$formats" ] || fail "copy-mode cursor formats were empty" + +# Cursor, viewport and paragraph commands. Each is asserted by successful +# dispatch and the final mode-liveness check; detailed word/selection geometry +# is covered by the existing copy-mode tests. +for command in back-to-indentation bottom-line top-line middle-line \ + cursor-centre-vertical cursor-centre-horizontal end-of-line history-bottom \ + history-top halfpage-down halfpage-up page-down page-up next-paragraph \ + previous-paragraph next-matching-bracket previous-matching-bracket; do + x "$command" +done +x goto-line 15 +x set-mark +x cursor-down +x jump-to-mark + +# OSC 133 prompt and output markers support shell-integration navigation. +x history-top +x next-prompt +case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in +*prompt-one*) ;; +*) fail "next-prompt did not find prompt-one" ;; +esac +x next-prompt +x previous-prompt +case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in +*prompt-one*) ;; +*) fail "previous-prompt did not return to prompt-one" ;; +esac +x next-prompt -o +case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in +*output-one*) ;; +*) fail "next-prompt -o did not find output-one" ;; +esac + +# Character jumps remember their direction and character for repeat/reverse. +x jump-forward e +x jump-again +x jump-reverse +x jump-backward e +x jump-to-forward e +x jump-to-backward e + +# Direct, text and incremental searches cover both directions and the saved +# search used by again/reverse. +x search-forward needle +x search-again +x search-reverse +x search-backward needle +x search-forward-text needle +x search-backward-text needle +x search-forward-incremental '=needle' +x search-forward-incremental '+needle' +x search-backward-incremental '=needle' +x search-backward-incremental '-needle' + +# Stateful display and behaviour toggles. +for command in rectangle-on rectangle-off rectangle-toggle \ + line-numbers-on line-numbers-off line-numbers-toggle \ + refresh-on refresh-now refresh-off refresh-toggle \ + scroll-exit-on scroll-exit-off scroll-exit-toggle toggle-position; do + x "$command" +done +wait_mode 1 +x cancel +wait_mode 0 + +# Line and end-of-line copy commands work without an explicit selection. +fresh +x select-line +x copy-selection-and-cancel +case "$($TMUX show-buffer)" in +*line-00*) ;; +*) fail "select-line did not select the current line" ;; +esac + +fresh +x copy-line +[ -n "$($TMUX show-buffer)" ] || fail "copy-line produced an empty buffer" +x cancel + +fresh +x copy-end-of-line +[ -n "$($TMUX show-buffer)" ] || fail "copy-end-of-line produced an empty buffer" +x cancel + +# Append variants retain or close the mode as advertised. +$TMUX set-buffer -b append-buffer prefix || exit 1 +select_text +x append-selection +appended=$($TMUX show-buffer -b append-buffer) +case "$appended" in +prefix*) ;; +*) fail "append-selection produced '$appended'" ;; +esac +x cancel + +$TMUX set-buffer -b append-cancel-buffer prefix || exit 1 +select_text +x append-selection-and-cancel +wait_mode 0 +appended=$($TMUX show-buffer -b append-cancel-buffer) +case "$appended" in +prefix*) ;; +*) fail "append-selection-and-cancel produced '$appended'" ;; +esac + +# The explicit cancel variants close copy mode. +fresh +x copy-line-and-cancel +wait_mode 0 +fresh +x copy-end-of-line-and-cancel +wait_mode 0 +select_text +x copy-selection-and-cancel +wait_mode 0 + +# Pipe wrappers have distinct command handlers. /dev/null keeps the test +# deterministic while exercising asynchronous pipe job creation and cleanup. +fresh +x copy-pipe-line 'cat >/dev/null' +x cancel +fresh +x copy-pipe-line-and-cancel 'cat >/dev/null' +wait_mode 0 +fresh +x copy-pipe-end-of-line 'cat >/dev/null' +x cancel +fresh +x copy-pipe-end-of-line-and-cancel 'cat >/dev/null' +wait_mode 0 + +select_text +x copy-pipe-no-clear 'cat >/dev/null' +x pipe-no-clear 'cat >/dev/null' +x copy-pipe 'cat >/dev/null' +x pipe 'cat >/dev/null' +x cancel +select_text +x copy-pipe-and-cancel 'cat >/dev/null' +wait_mode 0 +select_text +x pipe-and-cancel 'cat >/dev/null' +wait_mode 0 + +# Commands which cancel on downward movement have separate handlers. +fresh +x history-bottom +x bottom-line +x cursor-down-and-cancel +wait_mode 0 +fresh +x history-bottom +x halfpage-down-and-cancel +wait_mode 0 +fresh +x history-bottom +x page-down-and-cancel +wait_mode 0 +fresh +x history-bottom +x bottom-line +x scroll-down-and-cancel +wait_mode 0 + +$TMUX has-session -t copy || fail "server died during copy-mode commands" +exit 0 diff --git a/regress/copy-mode-selection-mode.sh b/regress/copy-mode-selection-mode.sh new file mode 100644 index 000000000..bcd070d59 --- /dev/null +++ b/regress/copy-mode-selection-mode.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Changing an existing character selection to line mode must reset both ends +# to complete lines, including when the cursor subsequently crosses the fixed +# end. An invalid regular-expression search must also discard marks left by a +# previous successful search. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null +} +trap cleanup 0 1 15 + +$TMUX new-session -d -x30 -y6 \ + "printf 'alpha\nbeta\ngamma\ndelta\n'; exec sleep 100" || exit 1 +$TMUX set-option -g window-size manual || exit 1 +sleep 1 + +# A valid regular-expression search creates marks. Replacing it with an +# invalid expression, growing the mode, and scrolling must not leave an old +# mark array indexed with the new geometry (the original bug was an ASan heap +# overflow in this sequence). +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -X search-forward beta || exit 1 +[ "$($TMUX display-message -p '#{search_present}')" = 1 ] || + fail "successful search did not create marks" +$TMUX send-keys -X search-forward '[' || exit 1 +$TMUX resize-window -x40 -y8 || exit 1 +$TMUX send-keys -X scroll-down || exit 1 +[ "$($TMUX display-message -p '#{search_present}')" = 0 ] || + fail "invalid regular expression left stale marks after resize" +$TMUX send-keys -X cancel || exit 1 + +# Select part of alpha through part of beta, then change the existing +# selection to line mode. Both lines, and no partial columns, must be copied. +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -X cursor-down || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X selection-mode line || exit 1 +[ "$($TMUX display-message -p '#{selection_mode}')" = line ] || + fail "selection did not change to line mode" +$TMUX send-keys -X copy-selection || exit 1 +expected=$(printf 'alpha\nbeta') +[ "$($TMUX show-buffer)" = "$expected" ] || + fail "line-mode selection did not expand to complete lines" + +# Start with the cursor above the fixed end so selection-mode itself takes the +# reverse-direction path, then extend it through another line. +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -N2 -X cursor-down || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -X cursor-up || exit 1 +$TMUX send-keys -N2 -X cursor-right || exit 1 +$TMUX send-keys -X selection-mode line || exit 1 +$TMUX send-keys -X cursor-up || exit 1 +$TMUX send-keys -X copy-selection || exit 1 +expected=$(printf 'alpha\nbeta\ngamma') +[ "$($TMUX show-buffer)" = "$expected" ] || + fail "reversed line-mode selection did not keep complete lines" + +exit 0 diff --git a/regress/copy-mode-test-vi.sh b/regress/copy-mode-test-vi.sh index 71810b006..817a5d4af 100644 --- a/regress/copy-mode-test-vi.sh +++ b/regress/copy-mode-test-vi.sh @@ -114,5 +114,30 @@ $TMUX send-keys -X next-space-end $TMUX send-keys -X copy-selection [ "$($TMUX show-buffer)" = "500xyz" ] || exit 1 +# Test that vi cursor movement does not stop on the padding cell of a wide +# character at the end of a line. +$TMUX kill-server 2>/dev/null +sleep 1 +$TMUX new -d -x20 -y5 \ + "printf 'abc中\nxyz\n'; exec cat" || exit 1 +$TMUX set-window-option -g mode-keys vi +$TMUX copy-mode +$TMUX send-keys -X history-top +$TMUX send-keys -X start-of-line +$TMUX send-keys -X cursor-right +$TMUX send-keys -X cursor-right +$TMUX send-keys -X cursor-right +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] || + exit 1 +$TMUX send-keys -X cursor-right +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "0,1" ] || + exit 1 +$TMUX send-keys -X cursor-left +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] || + exit 1 +$TMUX send-keys -X cursor-left +[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "2,0" ] || + exit 1 + $TMUX kill-server 2>/dev/null exit 0 diff --git a/regress/copy-mode-unicode-whitespace.sh b/regress/copy-mode-unicode-whitespace.sh new file mode 100644 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/customize-mode.sh b/regress/customize-mode.sh index 89ebc6a42..a80b7e2a5 100644 --- a/regress/customize-mode.sh +++ b/regress/customize-mode.sh @@ -280,6 +280,7 @@ test_user_hook() settle assert_equals "$($TMUX show-option -qv @cm_hook)" \ "display-message hook" "add user hook" + $TMUX set-hook -E @cm_hook || fail "fire user hook failed" send q wait_mode 0 @@ -287,7 +288,10 @@ test_user_hook() repeat_key j 3 send Right j settle - assert_contains "$(capture)" "@cm_hook" "user hook shown as hook" + screen=$(capture) + assert_contains "$screen" "@cm_hook" "user hook shown as hook" + assert_contains "$screen" "This hook has been fired 1 times, last " \ + "user hook fire count and time" send u y settle @@ -302,10 +306,17 @@ test_hook_array() $TMUX set-hook -g 'after-new-session[0]' 'display-message old' || fail "set hook array failed" + $TMUX set-hook -g -R after-new-session || fail "fire hook array failed" open_customize '#{==:#{option_name},after-new-session}' repeat_key j 3 - send Right j Right j + send Right j + settle + screen=$(capture) + assert_contains "$screen" "This hook has been fired " \ + "array hook fire count" + assert_contains "$screen" ", last " "array hook fire time" + send Right j settle send a diff --git a/regress/find-window.sh b/regress/find-window.sh new file mode 100644 index 000000000..820a4777d --- /dev/null +++ b/regress/find-window.sh @@ -0,0 +1,118 @@ +#!/bin/sh + +# Exercise find-window's content, name and title filters in every combination, +# glob/regex and case-insensitive suffixes, plus zoom restoration. + +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 -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +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 + +capture() +{ + $OUTER capture-pane -p -t outer:0.0 2>/dev/null +} + +wait_mode() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER display-message -p -t find:base.0 '#{pane_mode}' 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "pane mode is '$got', expected '$want'" +} + +run_find() +{ + label=$1 + expected=$2 + shift 2 + $INNER find-window -t find:base.0 "$@" || fail "$label failed" + wait_mode tree-mode + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + printf '%s\n' "$captured" | grep -Fq "$expected" && break + sleep 0.1 + i=$((i + 1)) + done + [ "$i" -lt 50 ] || fail "$label did not show '$expected'" + $INNER send-keys -t find:base.0 q || exit 1 + wait_mode '' +} + +$INNER new-session -d -s find -n base -x80 -y24 \ + "sh -c 'printf \"BaseBody\\n\"; exec sleep 100'" || exit 1 +$INNER split-window -d -h -t find:base \ + "sh -c 'printf \"OtherBody\\n\"; exec sleep 100'" || exit 1 +$INNER new-window -d -t find: -n NameNeedle 'exec sleep 100' || exit 1 +$INNER new-window -d -t find: -n body \ + "sh -c 'printf \"BodyNeedle\\n\"; exec sleep 100'" || exit 1 +$INNER new-window -d -t find: -n titled 'exec sleep 100' || exit 1 +$INNER select-pane -t find:titled.0 -T TitleNeedle || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t find:base" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +sleep 1 + +# Wait until the body text is in the pane history before searching it. +i=0 +while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t find:body.0 | grep -Fq BodyNeedle && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "pane content was not ready" + +# No flags means C+N+T. Then cover all three pairs and all three singletons. +run_find default NameNeedle NameNeedle +run_find content-name NameNeedle -C -N NameNeedle +run_find content-title titled -C -T TitleNeedle +run_find name-title titled -N -T TitleNeedle +run_find content-only body -C BodyNeedle +run_find name-only NameNeedle -N NameNeedle +run_find title-only titled -T TitleNeedle + +# Glob case folding and all regular-expression suffix variants. +run_find insensitive NameNeedle -N -i nameneedle +run_find regex NameNeedle -N -r '^NameNeedle$' +run_find regex-insensitive NameNeedle -N -r -i '^nameneedle$' + +# -Z temporarily zooms the target window and restores its prior state on exit. +$INNER find-window -Z -t find:base.0 -N NameNeedle || exit 1 +wait_mode tree-mode +[ "$($INNER display-message -p -t find:base '#{window_zoomed_flag}')" = 1 ] || + fail "find-window -Z did not zoom" +$INNER send-keys -t find:base.0 q || exit 1 +wait_mode '' +[ "$($INNER display-message -p -t find:base '#{window_zoomed_flag}')" = 0 ] || + fail "find-window -Z did not restore zoom" + +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/format-animation.sh b/regress/format-animation.sh new file mode 100644 index 000000000..b62b04444 --- /dev/null +++ b/regress/format-animation.sh @@ -0,0 +1,109 @@ +#!/bin/sh + +# Exercise the A format modifier (format_cycle in format.c). It is expanded +# only in a status format and arms the client cycle timer to redraw the status +# line as the frames advance, so the only place its behaviour shows is a +# rendered status line. +# +# The status line is rendered by an inner tmux attached inside an outer tmux +# pane; capturing the outer pane shows what the inner tmux drew. The inner +# window runs sleep and every other status line is blanked, so the capture +# contains the frame and nothing else. +# +# Each frame lasts 700 milliseconds and the capture is repeated once a second, +# so consecutive samples always land on a different frame: no exact period is +# assumed and nothing in the test asks for a redraw, so seeing the frame change +# means the status line animated on its own. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ + 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +# Capture the outer pane and return the text the inner tmux drew. +capture() { + $TMUX capturep -p >$TMP || fail "capture failed" + tr -d '[:space:]' <$TMP +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# Inner: a window which draws nothing and a status line which is only the +# animation, so the outer capture is the current frame on its own. +$TMUX2 new -d -x30 -y8 "sh -c 'exec sleep 100'" || exit 1 +$TMUX2 set -g window-size latest || exit 1 +i=1 +while [ $i -le 4 ]; do + $TMUX2 set -g status-format[$i] "" || exit 1 + i=$((i + 1)) +done +$TMUX2 set -g status-format[0] "#{A/7:AAAA,BBBB,CCCC}" || exit 1 + +$TMUX new -d -x30 -y8 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX set -g default-terminal "tmux-256color" || exit 1 +$TMUX send -l "$TMUX2 attach" || exit 1 +$TMUX send Enter || exit 1 +sleep 1 + +# Sample the frames. Every sample must be one of the frames and at least two +# different ones must be seen; status-interval is 15 seconds so only the cycle +# timer can have redrawn the status line. +seen="" +i=0 +while [ $i -lt 4 ]; do + [ $i -eq 0 ] || sleep 1 + frame=$(capture) + case "$frame" in + AAAA|BBBB|CCCC) ;; + *) fail "status line is '$frame', not a frame" ;; + esac + case " $seen " in + *" $frame "*) ;; + *) seen="$seen $frame" ;; + esac + i=$((i + 1)) +done +set -- $seen +[ $# -ge 2 ] || fail "status line did not animate, only saw$seen" + +# Outside a status format there is no animation at all. +out=$($TMUX2 display-message -p "#{A:ZZZZ,YYYY}") || exit 1 +[ -z "$out" ] || fail "display-message gave '$out', want empty" +out=$($TMUX2 list-panes -F "#{A:ZZZZ,YYYY}") || exit 1 +[ -z "$out" ] || fail "list-panes gave '$out', want empty" + +# Nor inside #(), where the frames would change the command on every frame. +$TMUX2 set -g status-format[0] '#(echo "[#{A:ZZZZ,YYYY}]")' || exit 1 +sleep 1 +out=$(capture) +case "$out" in +*ZZZZ*|*YYYY*) fail "job command saw a frame: '$out'" ;; +*"[]"*) ;; +*) fail "job output is '$out', want []" ;; +esac + +# Empty frames and a missing or bad count must not upset the server. +for f in '#{A:}' '#{A/0:a,b}' '#{A/x:a,b}' '#{A:,,,}'; do + $TMUX2 set -g status-format[0] "$f" || fail "setting $f failed" + $TMUX2 has-session >/dev/null 2>&1 || fail "server lost with $f" +done +sleep 1 +$TMUX2 has-session >/dev/null 2>&1 || fail "server lost after empty frames" + +exit 0 diff --git a/regress/format-variables.sh b/regress/format-variables.sh index 449712e34..6908ad956 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -71,7 +71,10 @@ cursor_very_visible cursor_x cursor_y history_all_bytes +history_added history_bytes +history_collected +history_generation history_limit history_size host @@ -103,6 +106,11 @@ pane_at_right pane_at_top pane_bg pane_bottom +pane_command_duration +pane_command_end_time +pane_command_running +pane_command_start_time +pane_command_status pane_current_command pane_current_path pane_dead @@ -120,16 +128,21 @@ pane_index pane_input_off pane_key_mode pane_last +pane_last_output_time +pane_last_prompt_time pane_left pane_marked pane_marked_set +pane_modal_flag pane_mode +pane_output_generation pane_path pane_pb_progress pane_pb_state pane_pid pane_pipe pane_pipe_pid +pane_private_modes pane_right pane_search_string pane_start_command @@ -141,6 +154,8 @@ pane_title pane_top pane_tty pane_unseen_changes +pane_unzoomed_height +pane_unzoomed_width pane_width pane_x pane_y @@ -206,7 +221,10 @@ window_layout window_linked window_linked_sessions window_linked_sessions_list +window_manual_height +window_manual_width window_marked_flag +window_modal_pane window_name window_offset_x window_offset_y diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 6eb6ae49e..6c7029463 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -218,11 +218,13 @@ $TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed" # pane-exited when a pane's command exits with remain-on-exit off. $TMUX set -g @x 0 || fail "set @x failed" -$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' || +$TMUX set-hook -g pane-exited \ + 'set -gF @x "#{hook}:#{hook_pane}:#{hook_exit_status}:#{hook_exit_success}"' || fail "set-hook pane-exited failed" -pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || - fail "split-window true failed" -wait_for @x "pane-exited:$pane" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ + 'trap "" HUP; exec /dev/null 2>&1; sleep 1; exit 42') || + fail "split-window exit 42 failed" +wait_for @x "pane-exited:$pane:42:0" $TMUX set-hook -gu pane-exited || fail "unset pane-exited failed" # pane-died when a pane's command exits with remain-on-exit on. diff --git a/regress/hooks.sh b/regress/hooks.sh index e1d75bf67..96a5064d0 100644 --- a/regress/hooks.sh +++ b/regress/hooks.sh @@ -85,6 +85,24 @@ echo "$shown" | grep -q '^session-created\[0\]' || fail "missing first array item: $shown" echo "$shown" | grep -q '^session-created\[1\]' || fail "missing second array item: $shown" +shown=$($TMUX show-hooks -gF '#{option_name}:#{hook_fire_count}:#{t/p:hook_fire_time}' \ + session-created) || + fail "show-hooks -gF failed" +echo "$shown" | grep -q '^session-created:[1-9][0-9]*:[^-][^ ]*$' || + fail "missing hook fire formats: $shown" + +# User hooks are options, but show-hooks should list only registered @ hooks +# and not ordinary user options. +$TMUX set -g @not-a-hook value || fail "set @not-a-hook failed" +$TMUX set-hook -g @user-hook 'lsk' || + fail "set-hook @user-hook failed" +shown=$($TMUX show-hooks -g) || + fail "show-hooks -g all failed" +echo "$shown" | grep -q '^@user-hook lsk$' || + fail "missing user hook: $shown" +if echo "$shown" | grep -q '^@not-a-hook '; then + fail "show-hooks listed user option: $shown" +fi # Unsetting removes the whole hook. $TMUX set-hook -gu session-created || fail "set-hook -gu failed" diff --git a/regress/input-common.inc b/regress/input-common.inc index 100a7be0f..ac04dbc24 100644 --- a/regress/input-common.inc +++ b/regress/input-common.inc @@ -174,3 +174,17 @@ check_raw_matches() fi done } + +check_raw_no_matches() +{ + name=$1 + shift + + capture_raw "$name" >"$TMP" + for unexpected in "$@"; do + if grep -Eq "$unexpected" "$TMP"; then + printf "%s\n" "$unexpected" >"$EXP" + fail "$name raw unexpected" + fi + done +} diff --git a/regress/input-osc.sh b/regress/input-osc.sh index 19b9d400f..b2c4d25d9 100644 --- a/regress/input-osc.sh +++ b/regress/input-osc.sh @@ -22,6 +22,16 @@ check_capture rename 'X' start_pane apc-title 20 3 '\033_test-title\033\\X\n' check_capture apc-title 'X' +cmd='i=0; while [ "$i" -lt 12 ]; do ' +cmd="${cmd}printf '\\033[22;0t'; i=\$((i + 1)); done; " +cmd="${cmd}printf X; sleep 2" +start_cmd title-stack 20 3 "$cmd" +check_capture title-stack 'X' +$TMUX respawn-pane -k -t title-stack: \ + "printf '\\033[22;0tY'; sleep 2" || exit 1 +sleep 0.3 +check_capture title-stack 'Y' + $TMUX kill-server 2>/dev/null sleep 0.1 $TMUX new-session -d -x 20 -y 3 -s osc52 "sleep 2" || exit 1 diff --git a/regress/input-unicode.sh b/regress/input-unicode.sh index ca74a82e8..98c4506e2 100644 --- a/regress/input-unicode.sh +++ b/regress/input-unicode.sh @@ -41,5 +41,9 @@ start_pane combining-left 10 3 '\314\201A\n' check_capture combining-left 'A' check_cursor combining-left '0,1' +start_pane combining-overflow 10 3 'u\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\n' +check_raw_matches combining-overflow '^ C 0,0 data=\(1,31,u' +check_raw_no_matches combining-overflow 'data=\(0,' + $TMUX kill-server 2>/dev/null exit $exit_status diff --git a/regress/layout-set.sh b/regress/layout-set.sh new file mode 100644 index 000000000..ea02195a9 --- /dev/null +++ b/regress/layout-set.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +# Exercise every predefined layout, next/previous cycling, lookup and restore +# paths, one-pane early returns, minimum-size handling and floating panes. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +DIR=$(mktemp -d) || exit 1 +TMUX_TMPDIR=$DIR +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" +SEEN="$DIR/layouts" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +layout() +{ + $TMUX display-message -p -t layouts:many '#{window_layout}' +} + +check_layout() +{ + name=$1 + $TMUX select-layout -t layouts:many "$name" || + fail "select-layout $name failed" + value=$(layout) + [ -n "$value" ] || fail "$name produced an empty layout" + if grep -Fxq "$value" "$SEEN"; then + fail "$name produced the same layout as an earlier preset" + fi + printf '%s\n' "$value" >>"$SEEN" + [ "$($TMUX list-panes -t layouts:many -F x | wc -l)" -eq 5 ] || + fail "$name lost a pane" + [ "$($TMUX list-panes -t layouts:many -F '#{pane_floating_flag}' | \ + grep -c '^1$')" -eq 1 ] || fail "$name did not preserve the floating pane" +} + +$TMUX new-session -d -s layouts -n many -x100 -y40 'exec sleep 100' || exit 1 +$TMUX set-option -g window-size manual || exit 1 +$TMUX split-window -d -h -t layouts:many 'exec sleep 100' || exit 1 +$TMUX split-window -d -v -t layouts:many 'exec sleep 100' || exit 1 +$TMUX split-window -d -h -t layouts:many 'exec sleep 100' || exit 1 +floating=$($TMUX new-pane -dP -F '#{pane_id}' -t layouts:many \ + -x20 -y8 -X10 -Y5 'exec sleep 100') || exit 1 +: >"$SEEN" + +# Options exercise percentage and explicit secondary-size calculations. +$TMUX set-option -w -t layouts:many main-pane-height 30% || exit 1 +$TMUX set-option -w -t layouts:many other-pane-height 12 || exit 1 +$TMUX set-option -w -t layouts:many main-pane-width 35% || exit 1 +$TMUX set-option -w -t layouts:many other-pane-width 20 || exit 1 + +for name in even-horizontal even-vertical main-horizontal \ + main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do + check_layout "$name" +done + +# Exact and unique-prefix lookup work; ambiguous and invalid names fail. +$TMUX select-layout -t layouts:many even-h || fail "unique layout prefix failed" +$TMUX select-layout -t layouts:many main >/dev/null 2>&1 && + fail "ambiguous layout prefix succeeded" +$TMUX select-layout -t layouts:many no-such-layout >/dev/null 2>&1 && + fail "invalid layout name succeeded" + +# Cycle through all layouts in both directions, including wraparound. The +# aliases and select-layout flags share layout_set_next/previous. +first=$(layout) +i=0 +while [ "$i" -lt 7 ]; do + $TMUX next-layout -t layouts:many || exit 1 + i=$((i + 1)) +done +[ "$(layout)" = "$first" ] || fail "next-layout did not wrap" +i=0 +while [ "$i" -lt 7 ]; do + $TMUX previous-layout -t layouts:many || exit 1 + i=$((i + 1)) +done +[ "$(layout)" = "$first" ] || fail "previous-layout did not wrap" +$TMUX select-layout -t layouts:many -n || exit 1 +$TMUX select-layout -t layouts:many -p || exit 1 + +# No argument reapplies the last preset; -o restores the previous layout; -E +# spreads the current cell without changing the pane set. +$TMUX kill-pane -t "$floating" || exit 1 +$TMUX select-layout -t layouts:many tiled || exit 1 +tiled=$(layout) +$TMUX select-layout -t layouts:many even-horizontal || exit 1 +$TMUX select-layout -t layouts:many -o || exit 1 +[ "$(layout)" = "$tiled" ] || fail "select-layout -o did not restore layout" +$TMUX select-layout -t layouts:many || exit 1 +$TMUX select-layout -t layouts:many -E || exit 1 + +# Each arranger has a deliberate one-pane early return. +$TMUX new-window -d -t layouts: -n one 'exec sleep 100' || exit 1 +one=$($TMUX display-message -p -t layouts:one '#{window_layout}') +for name in even-horizontal even-vertical main-horizontal \ + main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do + $TMUX select-layout -t layouts:one "$name" || exit 1 + [ "$($TMUX display-message -p -t layouts:one '#{window_layout}')" = "$one" ] || + fail "$name changed a one-pane layout" +done + +# Force the minimum-size paths without requiring a terminal client. +$TMUX resize-window -t layouts:many -x10 -y6 || exit 1 +for name in even-horizontal even-vertical main-horizontal \ + main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do + $TMUX select-layout -t layouts:many "$name" || exit 1 +done + +$TMUX has-session -t layouts || fail "server died during layout tests" +exit 0 diff --git a/regress/list-commands.sh b/regress/list-commands.sh new file mode 100644 index 000000000..9e1463bea --- /dev/null +++ b/regress/list-commands.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# Exercise list-commands for the full table, a single command, aliases, custom +# and empty formats, and lookup errors. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +DIR=$(mktemp -d) || exit 1 +TMUX_TMPDIR=$DIR +export TMUX_TMPDIR +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +# Keep a server alive across commands. Otherwise a STARTSERVER-only command +# may exit between invocations and leave a short socket-cleanup race. +$TMUX new-session -d -s list-commands 'exec sleep 100' || exit 1 + +all=$($TMUX list-commands) || exit 1 +printf '%s\n' "$all" | grep -q '^attach-session (attach)' || exit 1 +printf '%s\n' "$all" | grep -q '^list-commands (lscm)' || exit 1 + +one=$($TMUX list-commands -F \ + '#{command_list_name}|#{command_list_alias}|#{command_list_usage}' \ + list-commands) || exit 1 +case "$one" in +'list-commands|lscm|'*) ;; +*) exit 1 ;; +esac + +# Aliases resolve to their command, while an empty expansion prints no line. +[ "$($TMUX list-commands -F '#{command_list_name}' lscm)" = list-commands ] || + exit 1 +[ -z "$($TMUX list-commands -F '' list-commands)" ] || exit 1 + +$TMUX list-commands tmux-no-command-$$ >/dev/null 2>&1 && exit 1 +$TMUX list-commands list >/dev/null 2>&1 && exit 1 +exit 0 diff --git a/regress/menu-mouse.sh b/regress/menu-mouse.sh new file mode 100644 index 000000000..e1b53b55f --- /dev/null +++ b/regress/menu-mouse.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +# Check that mouse selection in an active menu uses the correct coordinates +# when the status line is at the top. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} + +# click COL ROW +# +# Write an SGR mouse press then release at a 1-based position to the outer pane +# holding the inner client. +click() +{ + col="$1" + row="$2" + + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' \ + "$col" "$row" "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +$TMUX new-session -d -s inner -x 80 -y 24 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g status-position top || exit 1 +$TMUX set -g @menu-choice '' || exit 1 + +$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +$TMUX display-menu -M -x 5 -y 7 \ + "First item" f "set -g @menu-choice first" \ + "Second item" s "set -g @menu-choice second" || exit 1 +sleep 1 + +# -y is the bottom of the menu, so with four menu lines this puts the menu at +# window y=3. The first item is then at window y=4. With one status line at the +# top, this is terminal row 6 in SGR's 1-based coordinates. +click 8 6 + +choice=$($TMUX show -gv @menu-choice 2>/dev/null) +[ "$choice" = "first" ] || fail "got '$choice', expected 'first'" + +cleanup +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/mode-kill.sh b/regress/mode-kill.sh old mode 100755 new mode 100644 diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh index 96927e995..aedeaca19 100644 --- a/regress/mode-mutation.sh +++ b/regress/mode-mutation.sh @@ -269,6 +269,22 @@ test_customize_mode() assert_alive "customize-mode exit" } +test_customize_break_pane() +{ + start_client option-break + side=$($TMUX split-window -d -P -F '#{pane_id}' \ + -t option-break:0 'cat') || fail "customize split failed" + + $TMUX customize-mode -t "$side" || fail "customize-mode failed" + wait_mode "$side" 1 + $TMUX break-pane -d -s "$side" || fail "customize break-pane failed" + + assert_alive "customize-mode break-pane" + wait_mode "$side" 1 + $TMUX send-keys -t "$side" q || fail "customize-mode quit failed" + wait_mode "$side" 0 +} + test_copy_mode() { start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' @@ -302,6 +318,7 @@ test_choose_tree test_choose_buffer test_choose_client test_customize_mode +test_customize_break_pane test_copy_mode cleanup exit 0 diff --git a/regress/new-pane-mouse.sh b/regress/new-pane-mouse.sh index 81f0e805b..06d594c8d 100644 --- a/regress/new-pane-mouse.sh +++ b/regress/new-pane-mouse.sh @@ -49,11 +49,33 @@ drag() sleep 1 } +# right_click COL ROW KEY +# +# Open the right-click menu at a 1-based position and choose KEY. +right_click() +{ + col="$1" + row="$2" + key="$3" + + seq=$(printf '\033[<2;%s;%sM' "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + $TMUX2 send-keys -t "$OUTER" "$key" 2>/dev/null + sleep 1 +} + cleanup $TMUX new-session -d -s inner -x 80 -y 24 || exit 1 $TMUX set -g mouse on $TMUX set -g default-command 'sleep 100' +keys=$($TMUX list-keys -T root -F '#{key_command}' MouseDown3Empty) || + fail "list MouseDown3Empty failed" +case "$keys" in +*"New Pane"*"New Window"*) ;; +*) fail "missing empty-area menu binding: $keys" ;; +esac $TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1 sleep 1 @@ -80,6 +102,17 @@ $TMUX break-pane -W -s "$BASE" || fail "break base pane failed" $TMUX resize-pane -t "$BASE" -x10 -y4 || fail "resize floating base failed" $TMUX move-pane -t "$BASE" -P top-left || fail "move floating base failed" +# Right-click in empty window space and choose New Pane. This creates a new +# floating pane and tiles it. +right_click 20 8 p + +id=$($TMUX list-panes -F '#{?pane_active,#{pane_id},}' | tail -n 1) +[ -n "$id" ] || fail "no pane created from empty-area menu" +[ "$id" != "$BASE" ] || fail "empty-area menu did not create a new pane" + +must_equal "$($TMUX display-message -p -t "$id" '#{pane_floating_flag}')" 0 +$TMUX kill-pane -t "$id" || fail "kill empty-area menu pane failed" + # Drag in empty window space with no tiled pane underneath. drag 40 10 50 15 @@ -92,6 +125,12 @@ must_equal "$($TMUX display-message -p -t "$id" '#{pane_top}')" 10 must_equal "$($TMUX display-message -p -t "$id" '#{pane_width}')" 9 must_equal "$($TMUX display-message -p -t "$id" '#{pane_height}')" 4 +TILED=$($TMUX new-pane -PF '#{pane_id}' 'sleep 100') || + fail "new pane from only floating panes failed" +$TMUX join-pane -t "$TILED" || + fail "tile new pane from only floating panes failed" +must_equal "$($TMUX display-message -p -t "$TILED" '#{pane_floating_flag}')" 0 + TILED=$($TMUX new-window -dPF '#{pane_id}' 'sleep 100') || fail "new tiled window failed" $TMUX new-pane -d -M -L -t "$TILED" 'sleep 100' || fail "new-pane -M -L failed" diff --git a/regress/options-array.sh b/regress/options-array.sh index fbaff6ae2..d707fb7d9 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -140,6 +140,24 @@ update-environment[notify] EEE" # sorts by ascending numeric key and keeps the gap at [1]. check_ok set -g status-format "" check_array "-g status-format" "status-format" +out=$($TMUX show -gF \ + '#{option_name}:#{option_has_value}:#{option_value}:#{option_is_array}:#{option_has_array_key}:#{option_array_key}' \ + status-format 2>&1) +[ "$out" = "status-format:0::1:0:" ] || { + echo "show -F empty status-format failed." + echo "Expected: 'status-format:0::1:0:'" + echo "But got: '$out'" + exit 1 +} +out=$($TMUX show -gvF \ + '#{option_name}:#{option_value_only}:#{option_has_value}:#{option_value}' \ + status-format 2>&1) +[ "$out" = "status-format:1:0:" ] || { + echo "show -vF empty status-format failed." + echo "Expected: 'status-format:1:0:'" + echo "But got: '$out'" + exit 1 +} check_ok set -g status-format[5] "five" check_ok set -g status-format[0] "zero" check_ok set -g status-format[2] "two" @@ -154,6 +172,21 @@ status-format[5] five status-format[foo-bar] foo-bar status-format[xterm-256color] xterm status-format[zoom] zoom" +out=$($TMUX show -gF \ + '#{option_name}:#{option_array_key}:#{option_is_array}:#{option_has_value}:#{option_has_array_key}:#{option_value}' \ + status-format 2>&1) +[ "$out" = "$(printf '%s' 'status-format:0:1:1:1:zero +status-format:1:1:1:1:one +status-format:2:1:1:1:two +status-format:5:1:1:1:five +status-format:foo-bar:1:1:1:foo-bar +status-format:xterm-256color:1:1:1:xterm +status-format:zoom:1:1:1:zoom')" ] || { + echo "show -F status-format failed." + echo "Expected formatted array output" + echo "But got:"; printf '%s\n' "$out" + exit 1 +} check_value "-gv status-format[01]" "one" check_ok set -gu status-format[zoom] check_value "-gv status-format[zoom]" "" diff --git a/regress/options-values.sh b/regress/options-values.sh index 75da52e01..be08cc136 100644 --- a/regress/options-values.sh +++ b/regress/options-values.sh @@ -168,6 +168,30 @@ check_ok set -g @str "foo" check_ok set -ga @str "bar" check_value "-gv @str" "foobar" +# --- show -F custom format ------------------------------------------------ +# +# show-options and show-hooks use a format for their output, with the default +# format preserving the traditional output. +check_ok set -g @str "two words" +check_value "-g @str" '@str "two words"' +out=$($TMUX show -gF \ + '#{option_name}=#{option_value}=#{option_is_user}=#{option_is_string}=#{option_value_only}' \ + @str 2>&1) +[ "$out" = "@str=two words=1=1=0" ] || { + echo "show -F @str failed." + echo "Expected: '@str=two words=1=1=0'" + echo "But got: '$out'" + exit 1 +} +out=$($TMUX show -gvF '#{option_name}=#{option_value}=#{option_value_only}' \ + @str 2>&1) +[ "$out" = "@str=two words=1" ] || { + echo "show -vF @str failed." + echo "Expected: '@str=two words=1'" + echo "But got: '$out'" + exit 1 +} + # --- -F expands at set time ----------------------------------------------- # # With -F the value is expanded as a format once, at set time; without -F it is diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 29c79e2e2..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 @@ -192,13 +200,13 @@ check_fail "invalid window name: $(printf 'a\377b')" \ break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P: # join-pane can move a pane from one window to another without destroying -# the source window if other panes remain. (On this branch move-pane is -# reserved for floating panes, covered by floating-pane-geometry.sh.) +# the source window if other panes remain. move-pane does the same when no +# floating-pane movement flags are given. check_ok new-window -d -t P:5 -n other check_ok join-pane -d -s "$p1" -t P:5.0 check_fmt 'P:5' '#{window_panes}' '2' check_fmt 'P:0' '#{window_panes}' '3' -check_ok join-pane -d -v -s "$p1" -t "$p2" +check_ok move-pane -d -v -s "$p1" -t "$p2" check_fmt 'P:0' '#{window_panes}' '4' check_fmt 'P:5' '#{window_panes}' '1' @@ -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/pipe-pane.sh b/regress/pipe-pane.sh new file mode 100644 index 000000000..310861a5e --- /dev/null +++ b/regress/pipe-pane.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +# Tests of pipe-pane behaviour. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +fail() +{ + echo "$1" + $TMUX kill-server 2>/dev/null + exit 1 +} + +check_ok() +{ + if ! $TMUX "$@"; then + fail "Command failed: $*" + fi +} + +check_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + fail "Server died" + fi +} + +# A pipe-pane -I child may write after the pane process has exited. With +# remain-on-exit, the pane stays around but its bufferevent has been freed. +check_ok new-session -d -s pipe -x 80 -y 24 'sleep 1' +check_ok set-option -t pipe:0 remain-on-exit on +check_ok pipe-pane -t pipe:0.0 -I 'sleep 2; printf x' + +i=0 +while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do + i=$((i + 1)) + [ "$i" -gt 50 ] && fail "Pane did not die" + sleep 0.1 +done + +sleep 2 +check_alive + +$TMUX kill-server 2>/dev/null diff --git a/regress/prompt-words-history.sh b/regress/prompt-words-history.sh new file mode 100644 index 000000000..6aaa5689c --- /dev/null +++ b/regress/prompt-words-history.sh @@ -0,0 +1,165 @@ +#!/bin/sh + +# Cover prompt word movement in emacs and vi modes, ambiguous inline command +# completion, and the show/clear-prompt-history command surface. + +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 -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +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 + +capture() +{ + $OUTER capture-pane -p -t outer:0.0 2>/dev/null +} + +wait_result() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER show-option -gqv @result 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "prompt result is '$got', expected '$want'" +} + +bind_prompt() +{ + initial=$1 + $INNER bind-key -n M-r command-prompt -I "$initial" -p '(word)' \ + "set-option -g @result '%%'" || exit 1 +} + +run_prompt() +{ + want=$1 + shift + $INNER set-option -g @result sentinel || exit 1 + $OUTER send-keys M-r || exit 1 + sleep 0.2 + $OUTER send-keys "$@" || exit 1 + $OUTER send-keys Enter || exit 1 + wait_result "$want" +} + +run_vi_prompt() +{ + want=$1 + shift + $INNER set-option -g @result sentinel || exit 1 + $OUTER send-keys M-r || exit 1 + sleep 0.2 + # Send Escape separately so the terminal's escape-time handling does not + # combine it with the first vi command as a Meta key. + $OUTER send-keys Escape || exit 1 + # The inner client must see Escape as a complete key, not the prefix of a + # Meta sequence (the default escape-time is 500 milliseconds). + sleep 0.7 + $OUTER send-keys "$@" || exit 1 + $OUTER send-keys Enter || exit 1 + wait_result "$want" +} + +$INNER new-session -d -s prompt -x80 -y24 'exec sleep 100' || exit 1 +$INNER set-option -g status on || exit 1 +$INNER set-option -g status-position bottom || exit 1 +$INNER set-option -g window-size manual || exit 1 +$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t prompt" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +sleep 1 + +# Emacs Meta-f stops after the first word; Meta-b returns to the start of the +# previous word. Inserting a marker makes the cursor position observable. +$INNER set-option -g status-keys emacs || exit 1 +bind_prompt 'one two' +run_prompt 'oneX two' Home M-f X +bind_prompt 'one two' +run_prompt 'one Xtwo' M-b X + +# Vi translation and the distinct separator-aware and WORD motions. +$INNER set-option -g status-keys vi || exit 1 +bind_prompt 'one-two three' +run_vi_prompt 'one-two Xthree' b i X +bind_prompt 'one-two three' +run_vi_prompt 'one-two Xthree' B i X +bind_prompt 'one-two three' +run_vi_prompt 'oneX-two three' 0 w i X +bind_prompt 'one-two three' +run_vi_prompt 'one-two Xthree' 0 W i X +bind_prompt 'one-two three' +run_vi_prompt 'oneX-two three' 0 e a X +bind_prompt 'one-two three' +run_vi_prompt 'one-twoX three' 0 E a X + +# "show-" has several command matches and no longer common prefix. Tab keeps +# the input and draws the sorted candidates inline. +$INNER set-option -g status-keys emacs || exit 1 +bind_prompt '' +$OUTER send-keys M-r || exit 1 +sleep 0.2 +$OUTER send-keys -l 'show-' || exit 1 +$OUTER send-keys Tab || exit 1 +sleep 0.2 +captured=$(capture) +printf '%s\n' "$captured" | grep -Fq 'show-buffer' || + fail "ambiguous completion list was not drawn" +printf '%s\n' "$captured" | grep -Fq 'show-environment' || + fail "ambiguous completion list was incomplete" +$OUTER send-keys Escape || exit 1 + +# Add entries to both history rings through real prompts. +bind_prompt 'history-command' +run_prompt 'history-command' +$INNER bind-key -n M-s command-prompt -T search -I history-search \ + -p '(search-history)' "set-option -g @result '%%'" || exit 1 +$OUTER send-keys M-s || exit 1 +$OUTER send-keys Enter || exit 1 +wait_result history-search + +command_history=$($INNER show-prompt-history -T command) || exit 1 +printf '%s\n' "$command_history" | grep -Fq 'history-command' || + fail "command history entry missing" +search_history=$($INNER show-prompt-history -T search) || exit 1 +printf '%s\n' "$search_history" | grep -Fq 'history-search' || + fail "search history entry missing" +all_history=$($INNER show-prompt-history) || exit 1 +printf '%s\n' "$all_history" | grep -Fq 'History for command:' || exit 1 +printf '%s\n' "$all_history" | grep -Fq 'History for search:' || exit 1 + +$INNER show-prompt-history -T invalid >/dev/null 2>&1 && + fail "invalid show history type succeeded" +$INNER clear-prompt-history -T invalid >/dev/null 2>&1 && + fail "invalid clear history type succeeded" +$INNER clear-prompt-history -T command || exit 1 +$INNER show-prompt-history -T command | grep -Fq history-command && + fail "type-specific history clear failed" +$INNER clear-prompt-history || exit 1 +$INNER show-prompt-history | grep -Fq history-search && + fail "all-history clear failed" + +exit 0 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/run-shell-output.sh b/regress/run-shell-output.sh index 926a520c9..68c98e5c3 100644 --- a/regress/run-shell-output.sh +++ b/regress/run-shell-output.sh @@ -11,7 +11,7 @@ TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap "$TMUX kill-server 2>/dev/null; rm -f $TMP" 0 1 15 $TMUX -f/dev/null new -d "$TMUX run 'echo foo' >$TMP; sleep 10" || exit 1 sleep 1 && [ "$(cat $TMP)" = "foo" ] || exit 1 @@ -20,6 +20,15 @@ $TMUX -f/dev/null new -d "$TMUX run -t: 'echo foo' >$TMP; sleep 10" || exit 1 sleep 1 && [ "$(cat $TMP)" = "" ] || exit 1 [ "$($TMUX display -p '#{pane_mode}')" = "view-mode" ] || exit 1 +$TMUX -f/dev/null new -d -s t1 'sleep 10' || exit 1 +$TMUX -f/dev/null run -d 1 'echo delayed' >$TMP 2>&1 & +pid=$! +sleep 0.2 +kill -9 "$pid" 2>/dev/null +wait "$pid" 2>/dev/null +sleep 2 +$TMUX has-session -t t1 || exit 1 + $TMUX kill-server 2>/dev/null exit 0 diff --git a/regress/screen-redraw-fill-character.sh b/regress/screen-redraw-fill-character.sh new file mode 100644 index 000000000..deb7b0a11 --- /dev/null +++ b/regress/screen-redraw-fill-character.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# Exercise fill-character as a format. The window is smaller than the attached +# client so OUTSIDE spans exist, then the only tiled pane is removed so EMPTY +# spans exist inside the window around a floating pane. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ + 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +must_equal() { + if [ "$1" != "$2" ]; then + fail "expected '$2', got '$1'" + fi +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +$TMUX2 new -d -x40 -y12 "sh -c 'printf base; exec sleep 100'" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g window-size manual || exit 1 +$TMUX2 setw fill-character '#{?is_inside,I,#{?is_outside,O,X}}' || exit 1 +$TMUX2 resizew -x28 -y8 || exit 1 +$TMUX2 new-pane -x12 -y4 -X8 -Y2 "sh -c 'printf FLOAT; exec sleep 100'" || exit 1 +tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \ + awk '$1==0{print $2; exit}') || exit 1 +$TMUX2 kill-pane -t "$tiled" || exit 1 + +$TMUX new -d -x40 -y12 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX set -g default-terminal "tmux-256color" || exit 1 +$TMUX send -l "$TMUX2 attach" || exit 1 +$TMUX send Enter || exit 1 +sleep 1 + +$TMUX capturep -p >$TMP || exit 1 + +must_equal "$(sed -n '1p' $TMP | cut -c1-28)" \ + "IIIIIIIIIIIIIIIIIIIIIIIIIIII" +must_equal "$(sed -n '1p' $TMP | cut -c29-40)" "OOOOOOOOOOOO" +must_equal "$(sed -n '9p' $TMP)" "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" + +if grep -q X "$TMP"; then + fail "fill-character used neither inside nor outside" +fi + +exit 0 diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh index d0b03fc7f..a7b389fef 100644 --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -215,9 +215,9 @@ $TMUX2 new-pane -x20 -y6 -X8 -Y7 "sh -c 'printf OVERST; exec sleep 100'" || exit compare floating-over-status # A window left with only a floating pane: killing the single tiled pane removes -# it from the layout, so the area it occupied is no longer owned by any pane and -# is drawn as EMPTY cells (middle dots) around the float. This is the only way to -# produce a REDRAW_SPAN_EMPTY span. +# it from the layout, so the area it occupied is no longer owned by any pane. +# Use a visible fill character for the EMPTY cells around the float. +$TMUX2 set -g fill-character '#[acs]~' || exit 1 new_scene 40 12 $TMUX2 new-pane -x20 -y6 -X8 -Y3 "sh -c 'printf FLOAT; exec sleep 100'" || exit 1 tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \ diff --git a/regress/screen-redraw-scrollbars-auto-hide.sh b/regress/screen-redraw-scrollbars-auto-hide.sh new file mode 100644 index 000000000..7357b5cbe --- /dev/null +++ b/regress/screen-redraw-scrollbars-auto-hide.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +# Auto-hide scrollbars appear on activity or pointer hover, then disappear +# after pane-scrollbars-timeout. Compare captures of the same copy-mode view so +# only the overlay scrollbar changes. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUTER="$TEST_TMUX -LtestA$$ -f/dev/null" +INNER="$TEST_TMUX -LtestB$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 +HIDDEN=$DIR/hidden +VISIBLE=$DIR/visible +AFTER=$DIR/after + +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 + +capture() +{ + $OUTER capture-pane -pe -t outer:0.0 >"$1" || exit 1 +} + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + $INNER list-clients 2>/dev/null | grep -q . && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +$INNER new-session -d -s inner -x40 -y12 \ + "sh -c 'seq 50; exec sleep 100'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER set-window-option pane-scrollbars auto-hide || exit 1 +$INNER set-window-option pane-scrollbars-timeout 3000 || exit 1 +$INNER set-window-option pane-scrollbars-style \ + 'bg=colour196,fg=colour231,width=1,pad=0' || exit 1 + +$OUTER new-session -d -s outer -x40 -y12 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_for_client + +# Enter copy mode and choose a stable view. The initial show timer is allowed +# to expire before taking the hidden reference capture. +$INNER copy-mode -H || exit 1 +$INNER send-keys -X history-top || exit 1 +sleep 3.5 +capture "$HIDDEN" + +# A copy-mode page movement shows the overlay and starts its timer. Capture +# that view immediately, then again after the timeout without moving it. +$INNER send-keys -X page-down || exit 1 +sleep 0.1 +capture "$VISIBLE" +sleep 3.5 +capture "$AFTER" +cmp -s "$VISIBLE" "$AFTER" && fail "scrollbar did not hide after timeout" + +# Use the post-movement hidden scene as the reference for pointer hover. SGR +# mouse column 40 is the right-hand scrollbar; moving to column 5 restarts the +# timer without changing the copy-mode view. +cp "$AFTER" "$HIDDEN" +hover=$(printf '\033[<35;40;5M') +$OUTER send-keys -l "$hover" || exit 1 +sleep 0.1 +capture "$VISIBLE" +cmp -s "$HIDDEN" "$VISIBLE" && fail "scrollbar did not appear on hover" +away=$(printf '\033[<35;5;5M') +$OUTER send-keys -l "$away" || exit 1 +sleep 3.5 +capture "$AFTER" +cmp -s "$HIDDEN" "$AFTER" || fail "scrollbar did not hide after hover" + +exit 0 diff --git a/regress/server-access.sh b/regress/server-access.sh new file mode 100644 index 000000000..a6e44efca --- /dev/null +++ b/regress/server-access.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# Exercise the unprivileged server-access surface: user and group ACL entries, +# read-only/writable transitions, listing, removal, implied addition, and +# command errors. Actual connections from another UID require privileges and +# deliberately do not form part of this test. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +OUT=$(mktemp) || exit 1 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null + rm -f "$OUT" +} +trap cleanup 0 1 15 + +expect_fail() +{ + message=$1 + shift + if $TMUX "$@" >"$OUT" 2>&1; then + fail "$message: command unexpectedly succeeded" + fi +} + +assert_entry() +{ + entry=$1 + $TMUX server-access -l >"$OUT" || exit 1 + grep -Fxq "$entry" "$OUT" || + fail "missing ACL entry '$entry'" +} + +assert_no_entry() +{ + entry=$1 + $TMUX server-access -l >"$OUT" || exit 1 + if grep -Fxq "$entry" "$OUT"; then + fail "unexpected ACL entry '$entry'" + fi +} + +owner_uid=$(id -u) +owner_name=$(id -un) + +# nobody is the non-owner test identity. Derive its primary group rather than +# assuming whether the system calls it nobody, nogroup, or something else. +acl_user=nobody +acl_group=$(id -gn "$acl_user" 2>/dev/null) || + fail "the nobody account is unavailable" +[ "$owner_name" != "$acl_user" ] || fail "test cannot run as nobody" + +$TMUX new-session -d -s access 'exec sleep 100' || exit 1 + +# The owner is installed as writable at server startup (root is intentionally +# omitted from display output). +if [ "$owner_uid" != 0 ]; then + assert_entry "$owner_name (U,W)" +fi + +# User ACL lifecycle, including duplicate and missing-entry errors. +$TMUX server-access -a "$acl_user" || exit 1 +assert_entry "$acl_user (U,W)" +expect_fail "duplicate user addition" server-access -a "$acl_user" +$TMUX server-access -r "$acl_user" || exit 1 +assert_entry "$acl_user (U,R)" +$TMUX server-access -w "$acl_user" || exit 1 +assert_entry "$acl_user (U,W)" +$TMUX server-access -d "$acl_user" || exit 1 +assert_no_entry "$acl_user (U,W)" +expect_fail "removing absent user" server-access -d "$acl_user" + +# -r and -w imply -a when an entry is absent. +$TMUX server-access -r "$acl_user" || exit 1 +assert_entry "$acl_user (U,R)" +$TMUX server-access -d "$acl_user" || exit 1 + +# The same lifecycle for a group exercises the separate ACL key space. +$TMUX server-access -g -a -r "$acl_group" || exit 1 +assert_entry "$acl_group (G,R)" +$TMUX server-access -g -w "$acl_group" || exit 1 +assert_entry "$acl_group (G,W)" +expect_fail "duplicate group addition" server-access -g -a "$acl_group" +$TMUX server-access -g -d "$acl_group" || exit 1 +assert_no_entry "$acl_group (G,W)" +expect_fail "removing absent group" server-access -g -d "$acl_group" + +# Parser and lookup errors, plus the immutable owner/root entries. +expect_fail "missing ACL subject" server-access -a +expect_fail "unknown user" server-access -a "tmux-no-user-$$" +expect_fail "unknown group" server-access -g -a "tmux-no-group-$$" +expect_fail "conflicting add/delete" server-access -a -d "$acl_user" +expect_fail "conflicting read/write" server-access -r -w "$acl_user" +expect_fail "changing server owner" server-access -r "$owner_name" +if [ "$owner_name" != root ]; then + expect_fail "changing root access" server-access -a root +fi + +$TMUX has-session -t access || fail "server exited during ACL updates" +exit 0 diff --git a/regress/server-socket-error.sh b/regress/server-socket-error.sh new file mode 100644 index 000000000..60ff4c07d --- /dev/null +++ b/regress/server-socket-error.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Server socket creation failures must produce a nonzero client exit status. + +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +# Root bypasses directory mode bits, so it cannot trigger this failure. +[ "$(id -u)" -eq 0 ] && exit 0 + +TMP=$(mktemp -d) || exit 1 +SOCKET="$TMP/ro/socket" + +cleanup() +{ + chmod 700 "$TMP/ro" 2>/dev/null + rm -rf "$TMP" +} + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +mkdir "$TMP/ro" || exit 1 +chmod 500 "$TMP/ro" || exit 1 +trap cleanup 0 1 15 + +check_failure() +{ + output=$($TEST_TMUX -S "$SOCKET" -f/dev/null "$@" 2>&1) + retval=$? + + [ "$retval" -ne 0 ] || fail "tmux $* exited zero: $output" + case "$output" in + "error creating $SOCKET ("*) ;; + *) fail "tmux $* produced unexpected error: $output" ;; + esac +} + +check_failure start-server +check_failure new-session -d + +exit 0 diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh index 0144884dc..11f8d7f86 100644 --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -67,10 +67,25 @@ shown=$($TMUX show-hooks -g -B @session-name) || fail "show-hooks -B failed" [ "$shown" = '@session-name::#{session_name}' ] || fail "unexpected show-hooks -B output: $shown" +shown=$($TMUX show-hooks -g -BF \ + '#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}:#{option_value}:#{option_is_hook}:#{option_is_user}' \ + @session-name) || + fail "show-hooks -BF failed" +[ "$shown" = '@session-name::#{session_name}:@session-name::#{session_name}:1:1' ] || + fail "unexpected show-hooks -BF output: $shown" +shown=$($TMUX show-hooks -g) || + fail "show-hooks -g failed" +echo "$shown" | grep -q '^@session-name ' || + fail "show-hooks -g did not show monitor hook: $shown" assert_unchanged @seen 0 $TMUX rename-session two || fail "rename-session two failed" wait_for @seen '@session-name:two' +shown=$($TMUX show-hooks -g -BF '#{hook_fire_count}:#{t/p:hook_fire_time}' \ + @session-name) || + fail "show-hooks -BF fire failed" +echo "$shown" | grep -q '^1:[^-][^ ]*$' || + fail "unexpected show-hooks -BF fire output: $shown" $TMUX set -g @seen-last 0 || fail "set @seen-last failed" $TMUX set-hook -g -B '@session-name::#{session_name}' \ @@ -112,6 +127,11 @@ $TMUX set -g @pane-seen 0 || fail "set @pane-seen failed" $TMUX set-hook -g -B "@pane:%$pane_number:#{pane_width}" \ 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || fail "set-hook -B pane selector failed" +shown=$($TMUX show-hooks -g -BF \ + '#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}' @pane) || + fail "show-hooks -BF pane failed" +[ "$shown" = "@pane:%$pane_number:#{pane_width}" ] || + fail "unexpected show-hooks -BF pane output: $shown" assert_unchanged @pane-seen 0 $TMUX set-hook -g -B "@pane:%$pane_number:#{@pane-value}" \ 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || diff --git a/regress/set-hook-E.sh b/regress/set-hook-E.sh old mode 100755 new mode 100644 diff --git a/regress/show-options-output.sh b/regress/show-options-output.sh new file mode 100644 index 000000000..6b36da5d1 --- /dev/null +++ b/regress/show-options-output.sh @@ -0,0 +1,136 @@ +#!/bin/sh + +# Tests of the default output from show-options and show-hooks. This is +# intended to guard the output compatibility when the implementation is changed +# to use formats internally, so it deliberately does not use show -F. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +check_value() +{ + out=$($TMUX show $1 2>&1) + if [ "$out" != "$(printf '%s' "$2")" ]; then + echo "show $1 failed." + echo "Expected:"; printf '%s\n' "$2" + echo "But got:"; printf '%s\n' "$out" + exit 1 + fi +} + +check_hooks() +{ + out=$($TMUX show-hooks $1 2>&1) + if [ "$out" != "$(printf '%s' "$2")" ]; then + echo "show-hooks $1 failed." + echo "Expected:"; printf '%s\n' "$2" + echo "But got:"; printf '%s\n' "$out" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# Scalar options: strings are escaped, non-strings are not, and -v suppresses +# the option name. +check_ok set -g @words "two words" +check_value "-g @words" '@words "two words"' +check_value "-gv @words" 'two words' +check_ok set -g display-time 1234 +check_value "-g display-time" 'display-time 1234' +check_value "-gv display-time" '1234' + +# Inherited options shown with -A are marked with an asterisk. +check_ok set -g status-left "GLOBAL" +check_ok set -u status-left +out=$($TMUX show -A 2>/dev/null | grep '^status-left\*') +if [ "$out" != "status-left* GLOBAL" ]; then + echo "show -A did not mark inherited status-left." + echo "But got:"; printf '%s\n' "$out" + exit 1 +fi + +# Arrays: an empty array prints just the name unless -v is used; populated +# arrays include every key, including key 0. +check_ok set -g status-format "" +check_value "-g status-format" 'status-format' +check_value "-gv status-format" '' +out=$($TMUX show -A 2>/dev/null | grep '^status-format\*') +if [ "$out" != "status-format*" ]; then + echo "show -A did not mark inherited empty status-format." + echo "But got:"; printf '%s\n' "$out" + exit 1 +fi +check_value "-A status-format" 'status-format' +check_ok set -g update-environment "AAA BBB,CCC" +check_value "-g update-environment" 'update-environment[0] AAA +update-environment[1] BBB +update-environment[2] CCC' +check_value "-gv update-environment[0]" 'AAA' + +# Hooks use the same array output style, and monitor hooks have their own +# default output form. +check_ok set-hook -g window-renamed[first] "display-message renamed" +check_hooks "-g window-renamed" 'window-renamed[first] display-message renamed' +check_ok set-hook -g -B '@monitor:%*:#{pane_width}' +check_hooks "-g -B @monitor" '@monitor:%*:#{pane_width}' +check_ok set-hook -g @user-hook "display-message user" +check_hooks "-g @user-hook" '@user-hook "display-message user"' +out=$($TMUX show -g 2>&1) +echo "$out" | grep -q "^@monitor ''$" && { + echo "show -g showed monitor hook without -H." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +echo "$out" | grep -q '^@user-hook "display-message user"$' && { + echo "show -g showed user hook without -H." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +out=$($TMUX show -gH 2>&1) +echo "$out" | grep -q "^@monitor ''$" || { + echo "show -gH did not show monitor hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +echo "$out" | grep -q '^@user-hook "display-message user"$' || { + echo "show -gH did not show user hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +out=$($TMUX show-hooks -g 2>&1) +echo "$out" | grep -q "^@monitor ''$" || { + echo "show-hooks -g did not show monitor hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} +echo "$out" | grep -q '^@user-hook "display-message user"$' || { + echo "show-hooks -g did not show user hook." + echo "But got:"; printf '%s\n' "$out" + exit 1 +} + +assert_alive "after show-options output tests" + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/switch-mode.sh b/regress/switch-mode.sh new file mode 100644 index 000000000..5f3fe8c12 --- /dev/null +++ b/regress/switch-mode.sh @@ -0,0 +1,203 @@ +#!/bin/sh + +# Exercise switch-mode's session and window lists with a real attached client: +# fuzzy filtering, movement and selection, the default and custom commands, +# no-match cancellation, resize, zoom restoration, and -k mode teardown. + +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 -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +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 + +capture() +{ + $OUTER capture-pane -p -t outer:0.0 2>/dev/null +} + +wait_capture() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + printf '%s\n' "$captured" | grep -Fq "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "timed out waiting for '$marker'" +} + +wait_count() +{ + marker=$1 + want=$2 + i=0 + while [ "$i" -lt 50 ]; do + captured=$(capture) + count=$(printf '%s\n' "$captured" | grep -Fc "$marker") + [ "$count" -eq "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "found $count '$marker' rows, expected $want" +} + +wait_format() +{ + target=$1 + format=$2 + want=$3 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER display-message -p -t "$target" "$format" \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "$target format $format is '$got', expected '$want'" +} + +wait_client_session() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($INNER list-clients -F '#{client_session}' 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client session is '$got', expected '$want'" +} + +# Three alphabetically ordered sessions. alpha:main has two panes so -Z can be +# observed; the two pick-* windows make a deterministic two-row window filter. +$INNER new-session -d -s alpha -n main -x60 -y15 'exec sleep 100' || exit 1 +$INNER split-window -d -h -t alpha:main 'exec sleep 100' || exit 1 +$INNER new-window -d -t alpha: -n pick-one 'exec sleep 100' || exit 1 +$INNER new-session -d -s bravo -n pick-two -x60 -y15 \ + 'exec sleep 100' || exit 1 +$INNER new-session -d -s charlie -n last -x60 -y15 \ + 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +$INNER select-window -t alpha:main || exit 1 + +$OUTER new-session -d -s outer -x60 -y15 "$INNER attach -t alpha" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +wait_client_session alpha + +# Session rows are sorted alpha, bravo, charlie. Up from the first row wraps to +# charlie; Enter runs a custom command with the selected session target. +$INNER set-option -g @picked '' || exit 1 +$INNER switch-mode -s -t alpha:main.0 -F 'SESSION #{session_name}' \ + "set-option -g @picked '%%'" || exit 1 +wait_count 'SESSION ' 3 +$INNER send-keys -t alpha:main.0 Up Enter || exit 1 +wait_format alpha:main.0 '#{@picked}' '=charlie:' +wait_format alpha:main.0 '#{pane_in_mode}' 0 + +# Fuzzy filtering keeps only bravo for "brv". The default command switches the +# real client to that session and closes the mode. +$INNER switch-mode -s -t alpha:main.0 -F 'SESSION #{session_name}' || exit 1 +wait_count 'SESSION ' 3 +$INNER send-keys -t alpha:main.0 -l brv || exit 1 +wait_count 'SESSION ' 1 +printf '%s\n' "$captured" | grep -Fq 'SESSION bravo' || + fail "fuzzy session filter did not select bravo" +$INNER send-keys -t alpha:main.0 Enter || exit 1 +wait_client_session bravo +client=$($INNER list-clients -F '#{client_name}') +$INNER switch-client -c "$client" -t alpha || exit 1 +wait_client_session alpha + +# Window mode sees pick-one then pick-two for the "pick" filter. Down selects +# the second row, and the custom command receives its window target. +$INNER set-option -g @picked '' || exit 1 +$INNER switch-mode -w -t alpha:main.0 -F \ + 'WINDOW #{session_name}:#{window_name}' \ + "set-option -g @picked '%%'" || exit 1 +wait_capture 'WINDOW ' +$INNER send-keys -t alpha:main.0 -l pick || exit 1 +wait_count 'WINDOW ' 2 +$INNER send-keys -t alpha:main.0 Down Enter || exit 1 +picked=$($INNER show-option -gv @picked) +case "$picked" in +=bravo:0.) ;; +*) fail "window selection produced '$picked', expected '=bravo:0.'" ;; +esac + +# Navigation with an empty result set must be harmless, and Escape cancels +# without running the custom command. +$INNER set-option -g @picked unchanged || exit 1 +$INNER switch-mode -s -t alpha:main.0 -F 'NONE #{session_name}' \ + "set-option -g @picked '%%'" || exit 1 +wait_capture 'NONE ' +$INNER send-keys -t alpha:main.0 -l no-such-session || exit 1 +wait_count 'NONE ' 0 +$INNER send-keys -t alpha:main.0 Up Down PPage NPage Home End || exit 1 +wait_format alpha:main.0 '#{pane_mode}' switch-mode +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{pane_in_mode}' 0 +[ "$($INNER show-option -gv @picked)" = unchanged ] || + fail "cancelled switch-mode ran its command" + +# -Z temporarily zooms an unzoomed window. Resizing while the mode is open +# rebuilds and redraws it; exiting restores the original unzoomed state. +$INNER switch-mode -Zs -t alpha:main.0 -F 'RESIZE #{session_name}' || exit 1 +wait_capture 'RESIZE alpha' +wait_format alpha:main.0 '#{window_zoomed_flag}' 1 +$INNER resize-window -t alpha:main -x50 -y12 || exit 1 +wait_format alpha:main.0 '#{window_width}x#{window_height}' 50x12 +wait_format alpha:main.0 '#{pane_mode}' switch-mode +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{window_zoomed_flag}' 0 + +# If the window was already zoomed, leaving -Z mode must keep it zoomed. +$INNER resize-pane -Z -t alpha:main.0 || exit 1 +$INNER switch-mode -Zs -t alpha:main.0 -F 'ZOOMED #{session_name}' || exit 1 +wait_capture 'ZOOMED alpha' +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{window_zoomed_flag}' 1 +$INNER resize-pane -Z -t alpha:main.0 || exit 1 + +# -k uses the shared mode teardown flag. Put it on a disposable pane and make +# sure exiting the mode removes that pane. +kill_pane=$($INNER split-window -d -P -F '#{pane_id}' -t alpha:main \ + 'exec sleep 100') || exit 1 +$INNER switch-mode -ks -t "$kill_pane" -F 'KILL #{session_name}' || exit 1 +wait_format "$kill_pane" '#{pane_mode}' switch-mode +$INNER send-keys -t "$kill_pane" Escape || exit 1 +i=0 +while [ "$i" -lt 50 ]; do + $INNER list-panes -s -t alpha -F '#{pane_id}' 2>/dev/null | \ + grep -q -x "$kill_pane" || break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "switch-mode -k did not kill its pane" + +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/sync-output-cursor.sh b/regress/sync-output-cursor.sh new file mode 100644 index 000000000..e805f373f --- /dev/null +++ b/regress/sync-output-cursor.sh @@ -0,0 +1,160 @@ +#!/bin/sh + +# A synchronized update elsewhere in the pane must not hide a stationary +# cursor while tmux waits for the end of the update. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +if [ "$#" -eq 0 ]; then + TEST_TMUX="$TEST_TMUX" sh "$0" pane || exit 1 + TEST_TMUX="$TEST_TMUX" sh "$0" prompt || exit 1 + exit 0 +fi +case $1 in +pane|prompt) MODE=$1 ;; +*) echo "usage: $0 [pane|prompt]" >&2; exit 1 ;; +esac + +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-cursor.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" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +my $control = $ENV{CONTROL}; +(my $dir = $control) =~ s{/[^/]+$}{}; + +# Draw a prompt and leave its cursor stationary on the last line. +syswrite STDOUT, "\e[2J\e[HWorking .\e[24;1H> "; +open my $ready, '>', "$dir/ready" or die "$dir/ready: $!\n"; +close $ready; +while (!-e $control) { + select undef, undef, undef, 0.01; +} + +# Deliberately split BSU, content, and ESU across pane reads. This mirrors an +# animation producer which begins a frame before it has finished rendering it. +for my $spinner (qw(| / - \\)) { + syswrite STDOUT, "\e[?2026h"; + select undef, undef, undef, 0.05; + syswrite STDOUT, "\e[1;9H$spinner\e[24;3H"; + select undef, undef, undef, 0.01; + syswrite STDOUT, "\e[?2026l"; + select undef, undef, undef, 0.15; +} +select undef, undef, undef, 5; +PERL + +cat >"$ASSERT" <<'PERL' +use strict; +use warnings; + +my $path = shift; +open my $fh, '<:raw', $path or die "$path: $!\n"; +local $/; +my $bytes = <$fh>; +my ($bsu, $esu, $hide, $show) = + ("\e[?2026h", "\e[?2026l", "\e[?25l", "\e[?25h"); +my ($sync, $outside, $hides, $shows) = (0, 0, 0, 0); + +for (my $i = 0; $i < length($bytes);) { + if (substr($bytes, $i, length($bsu)) eq $bsu) { + $sync++; + $i += length($bsu); + next; + } + if (substr($bytes, $i, length($esu)) eq $esu) { + $sync-- if $sync; + $i += length($esu); + next; + } + if (substr($bytes, $i, length($hide)) eq $hide) { + $hides++; + $outside++ unless $sync; + $i += length($hide); + next; + } + if (substr($bytes, $i, length($show)) eq $show) { + $shows++; + $outside++ unless $sync; + $i += length($show); + next; + } + $i++; +} + +printf "%s cursor hides: %d; cursor shows: %d; changes outside sync: %d\n", + $ENV{MODE}, $hides, $shows, $outside; +die "stationary cursor changed outside synchronized output\n" if $outside; +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 + +if [ "$MODE" = prompt ]; then + $OUTER send-keys -t outer:0.0 C-b : || exit 1 + sleep 0.2 +fi + +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" +sleep 2 +$OUTER pipe-pane -t outer:0.0 || exit 1 + +MODE=$MODE perl "$ASSERT" "$CLIENT_BYTES" diff --git a/regress/tab-cell-background.sh b/regress/tab-cell-background.sh new file mode 100644 index 000000000..350b77854 --- /dev/null +++ b/regress/tab-cell-background.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# When a write breaks up a wide character or a tab, the columns it covered must +# keep the background colour, both in the grid and on the terminal. The second +# server is attached to the first so that what is actually sent is checked, not +# just what tmux has stored. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +$TMUX2 kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP" 0 1 15 + +# 1. Write inside a tab. 2. Overwrite the right half of a wide character. +# 3. Two tabs of different colours, the first partly deleted so that padding +# from both ends up next to each other, then written over. +# The sleep is so that the client is attached before anything is written and +# the changes are sent as they happen rather than as one redraw. +$TMUX2 -f/dev/null new -d -x24 -y3 " + sleep 2 + printf '\033[44m\033[K\t\033[4GX' + printf '\033[2;1H\033[44m\033[K\344\270\226\033[2GX' + printf '\033[3;1H\033[42m\033[K\t\033[43m\033[K\t\033[0m\033[4G\033[8P\rx' + cat" || exit 1 +$TMUX2 set -g status off || exit 1 + +$TMUX -f/dev/null new -x24 -y4 -d "$TMUX2 attach" || exit 1 +sleep 4 + +$TMUX capturep -pe -S0 -E2 >$TMP || exit 1 +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +printf '\033[44m X\n X\n\033[49mx\033[42m \033[43m \033[49m\n' | + cmp - $TMP || exit 1 + +exit 0 diff --git a/regress/terminal-feature-utf8.sh b/regress/terminal-feature-utf8.sh new file mode 100644 index 000000000..b1e0a1fbd --- /dev/null +++ b/regress/terminal-feature-utf8.sh @@ -0,0 +1,58 @@ +#!/bin/sh + +# The utf8 terminal feature must mark an attached client as UTF-8 even when +# neither its environment nor -u does so. It must also be reported by the +# client terminal-feature format lookup. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER="$TEST_TMUX -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null +} +trap cleanup 0 1 15 + +cleanup +$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 + +# A clean C-locale environment avoids the ordinary UTF-8 detection paths. +# -T utf8 is therefore solely responsible for setting client_utf8. +client_command="env -i PATH=/bin:/usr/bin TERM=screen LC_ALL=C $TEST_TMUX -T utf8 -LtestI$$ -f/dev/null attach-session -t inner" +$OUTER new-session -d -s outer -x40 -y10 "$client_command" || exit 1 +$OUTER set-option -g status off || exit 1 + +i=0 +while [ "$i" -lt 50 ]; do + client=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + utf8=$($INNER list-clients -F '#{client_utf8}' 2>/dev/null) + [ -n "$client" ] && [ "$utf8" = 1 ] && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "utf8 terminal feature did not set client_utf8" + +features=$($INNER list-clients -F '#{client_termfeatures}') +case ",$features," in +*,utf8,*) ;; +*) fail "utf8 is missing from client_termfeatures: $features" ;; +esac + +present=$($INNER display-message -c "$client" -p '#{I/f:utf8}') +[ "$present" = 1 ] || fail "utf8 terminal feature lookup returned '$present'" + +exit 0 diff --git a/regress/theme-report.sh b/regress/theme-report.sh old mode 100755 new mode 100644 diff --git a/regress/tty-draw-line.sh b/regress/tty-draw-line.sh index 59331aa0d..aa33982c9 100644 --- a/regress/tty-draw-line.sh +++ b/regress/tty-draw-line.sh @@ -34,6 +34,18 @@ captureen() { $TMUX capturep -peNS0 -E- >$TMP || exit 1 } +timed() { + if command -v timeout >/dev/null 2>&1; then + timeout 5 "$@" + else + "$@" + fi +} + +capture_timed() { + timed $TMUX capturep -pS0 -E- >$TMP || exit 1 +} + check_line() { line=$1 want=$2 @@ -81,6 +93,8 @@ $TMUX2 neww -d \ "printf '\033(0x\033(B'; exec sleep 100" || exit 1 $TMUX2 neww -d \ "awk 'BEGIN { for (i = 0; i < 1100; i++) printf \"a\" }'; exec sleep 100" || exit 1 +$TMUX2 neww -d \ + "printf 'u\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245'; exec sleep 100" || exit 1 $TMUX2 selectw -t:0 || exit 1 $TMUX -f/dev/null new -d -x20 -y6 || exit 1 @@ -91,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" @@ -245,4 +274,12 @@ capture len=$(sed -n 1p $TMP | wc -c) [ "$len" -ge 1100 ] || fail "long same-style line was truncated" +# Too many combining marks on one base character must not leave a standalone +# width-zero cell that can make tty_draw_line loop forever on redraw. +$TMUX resizew -x20 -y6 || exit 1 +timed $TMUX2 selectw -t:13 || fail "zero-width overflow select hung" +sleep 1 +capture_timed +check_grep '^u' + exit 0 diff --git a/regress/window-ops.sh b/regress/window-ops.sh index bdb1b5fb9..c385a3328 100644 --- a/regress/window-ops.sh +++ b/regress/window-ops.sh @@ -8,7 +8,7 @@ # This exercises: # - new-window placement: next free index, explicit index, index in use with # and without -k, -a (after) and -b (before) insertion with shuffling, and -# -S selecting an existing window by name instead of creating; +# -S selecting an existing window by name or target instead of creating; # - move-window to a free index, to an occupied index with and without -k, # -a insertion and -r renumbering (including base-index); # - renumber-windows closing gaps; @@ -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. # @@ -139,6 +140,36 @@ check_ok new-window -S -t W: -n w3 check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' check_fmt 'W:' '#{window_index}:#{window_name}' '5:w3' +# -S with a target-window that already exists selects it directly, ignoring +# -n (with -d it does not switch). +check_ok select-window -t W:0 +check_ok new-window -S -t W:9 -n ignored +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' +check_fmt 'W:' '#{window_index}:#{window_name}' '9:w9k' +check_ok select-window -t W:0 +check_ok new-window -S -d -t W:9 -n ignored +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' +check_fmt 'W:' '#{window_index}:#{window_name}' '0:wB' + +# -S with a target-window that does not yet exist still creates it normally. +check_ok new-window -S -t W:20 -n w20 +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k 20:w20' +check_fmt 'W:' '#{window_index}:#{window_name}' '20:w20' +check_ok kill-window -t W:20 + +# -S also works with a relative target-window (+N): selects the window at +# that offset if it exists, or creates it there if not. +check_ok select-window -t W:0 +check_ok new-window -S -t W:+1 +check_fmt 'W:' '#{window_index}:#{window_name}' '1:w0' +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' + +check_ok select-window -t W:0 +check_ok new-window -S -t W:+6 -n rel6 +check_fmt 'W:' '#{window_index}:#{window_name}' '6:rel6' +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 6:rel6 9:w9k' +check_ok kill-window -t W:6 + # Clean up to a known arrangement. check_ok kill-window -t W:wB check_ok kill-window -t W:wA @@ -306,6 +337,24 @@ check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1" check_ok rotate-window -D -t R:0 check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0" +# Rotation without -Z unzooms. With -Z it preserves zoom and transfers it to +# the pane which arrives at the active position. +layout=$($TMUX display-message -p -t R:0 '#{window_layout}') +check_ok resize-pane -Z -t "$p0" +check_ok rotate-window -U -t R:0 +check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '0:0:1' +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{window_layout}' "$layout" + +check_ok resize-pane -Z -t "$p0" +check_ok rotate-window -U -Z -t R:0 +check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \ + '1:1:1' +check_ok resize-pane -Z -t "$p1" +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{window_layout}' "$layout" + # --------------------------------------------------------------------------- # kill-window. diff --git a/resize.c b/resize.c index 4f7ee490a..67562522d 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -42,8 +42,8 @@ resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy) void resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) { - u_int old_sx = w->sx, old_sy = w->sy; - int zoomed; + struct window_pane *zwp; + u_int old_sx = w->sx, old_sy = w->sy; /* Check size limits. */ if (sx < WINDOW_MINIMUM) @@ -56,8 +56,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) sy = WINDOW_MAXIMUM; /* If the window is zoomed, unzoom. */ - zoomed = w->flags & WINDOW_ZOOMED; - if (zoomed) + zwp = window_zoomed_pane(w); + if (zwp != NULL) window_unzoom(w, 1); /* Resize the layout first. */ @@ -73,8 +73,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) sx, sy, w->layout_root->g.sx, w->layout_root->g.sy); /* Restore the window zoom state. */ - if (zoomed) - window_zoom(w->active); + if (zwp != NULL && window_has_pane(w, zwp)) + window_zoom(zwp); tty_update_window_offset(w); server_redraw_window(w); diff --git a/screen-redraw.c b/screen-redraw.c index c58beb646..03dfcd4da 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.155 2026/07/21 13:27:41 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.158 2026/09/01 19:50:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -320,7 +320,7 @@ redraw_reset_cell(struct redraw_build_ctx *bctx, u_int x, u_int y) struct window *w = bctx->w; memset(bc, 0, sizeof *bc); - if (bctx->ox + x <= w->sx && bctx->oy + y <= w->sy) + if (bctx->ox + x < w->sx && bctx->oy + y < w->sy) bc->data.type = REDRAW_SPAN_EMPTY; else bc->data.type = REDRAW_SPAN_OUTSIDE; @@ -547,7 +547,8 @@ redraw_mark_border_cell(struct redraw_build_ctx *bctx, int wx, int wy, * merged. */ if (!floating) { - if (bc->data.type == REDRAW_SPAN_EMPTY) + if (bc->data.type == REDRAW_SPAN_EMPTY || + bc->data.type == REDRAW_SPAN_OUTSIDE) reset = 1; else if (bc->data.type != REDRAW_SPAN_BORDER) return; @@ -1232,9 +1233,15 @@ redraw_draw_border_span(struct redraw_draw_ctx *dctx, if (wp == NULL) { redraw_get_default_border_style(dctx, &gc, &pane_lines); - if (span->data.type != REDRAW_SPAN_BORDER) - pane_lines = PANE_LINES_SINGLE; - window_get_border_cell(w, NULL, pane_lines, cell_type, &gc); + if (span->data.type == REDRAW_SPAN_OUTSIDE) + window_get_fill_cell(w, 0, &gc); + else if (span->data.type == REDRAW_SPAN_EMPTY) + window_get_fill_cell(w, 1, &gc); + else { + if (span->data.type != REDRAW_SPAN_BORDER) + pane_lines = PANE_LINES_SINGLE; + window_get_border_cell(NULL, pane_lines, cell_type, &gc); + } } else { window_pane_get_border_style(wp, c, &gc); window_pane_get_border_cell(wp, cell_type, &gc); @@ -1731,8 +1738,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) else loop->flags &= ~PANE_NEWSTATUS; - width = redraw_pane_status_width(&dctx, loop, - &first); + width = redraw_pane_status_width(&dctx, loop, &first); if (width == 0) continue; @@ -1763,7 +1769,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) } } } - tty_sync_start(tty); + tty_sync_start(tty); /* end in server_client_reset_state */ tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL); if (wp != NULL) @@ -1808,7 +1814,6 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) c->overlay_draw(c, c->overlay_data); tty_reset(tty); - tty_sync_end(tty); #ifdef ENABLE_SIXEL if (wp != NULL) diff --git a/screen-write.c b/screen-write.c index 1b888849f..1b101ed94 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.284 2026/07/20 11:16:33 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -305,12 +305,14 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, if (~ctx->flags & SCREEN_WRITE_SYNC) { /* - * For the active pane or for an overlay (no pane), we want to - * only use synchronized updates if requested (commands that - * move the cursor); for other panes, always use it, since the - * cursor will have to move. + * For the active pane showing its base screen or for an + * overlay (no pane), only use synchronized updates if + * requested (commands that move the cursor); for other panes + * or a pane in a mode, always use it, since the cursor will + * have to move. */ - if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active) + if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active || + ctx->wp->screen != &ctx->wp->base)) ttyctx->flags |= TTY_CTX_SYNC; else { if (ctx->wp == NULL) @@ -338,10 +340,18 @@ screen_write_make_list(struct screen *s) void screen_write_free_list(struct screen *s) { - u_int y; + struct screen_write_cline *cl; + struct screen_write_citem *ci, *ci1; + u_int y; - for (y = 0; y < screen_size_y(s); y++) - free(s->write_list[y].data); + for (y = 0; y < screen_size_y(s); y++) { + cl = &s->write_list[y]; + TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) { + TAILQ_REMOVE(&cl->items, ci, entry); + screen_write_free_citem(ci); + } + free(cl->data); + } free(s->write_list); } @@ -1059,6 +1069,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) @@ -2480,15 +2503,62 @@ screen_write_collect_insert_clear(struct screen_write_ctx *ctx, u_int px, } } +/* + * Clear a cell that is being broken up by a write over part of it, keeping the + * background so the columns it covered do not lose their colour. + */ +static void +screen_write_clear_cell(struct grid *gd, u_int px, u_int py) +{ + struct grid_cell gc; + int bg; + + grid_view_get_cell(gd, px, py, &gc); + bg = gc.bg; + + memcpy(&gc, &grid_default_cell, sizeof gc); + gc.bg = bg; + grid_view_set_cell(gd, px, py, &gc); +} + +/* + * Insert clears for a range of cells already cleared in the grid. Adjacent + * cells may have come from different characters and so have different + * backgrounds, so this is done in runs of the same colour. + */ +static void +screen_write_insert_clears(struct screen_write_ctx *ctx, u_int px, u_int nx) +{ + struct screen *s = ctx->s; + struct grid_cell gc; + u_int xx, start = px, n; + int bg = 8; + + for (xx = px; xx < px + nx; xx++) { + grid_view_get_cell(s->grid, xx, s->cy, &gc); + if (xx == start) + bg = gc.bg; + else if (gc.bg != bg) { + n = xx - start; + log_debug("%s: from %u, size %u", __func__, start, n); + screen_write_collect_insert_clear(ctx, start, n, bg); + start = xx; + bg = gc.bg; + } + } + log_debug("%s: from %u, size %u", __func__, start, xx - start); + screen_write_collect_insert_clear(ctx, start, xx - start, bg); +} + /* Finish and store collected cells. */ void screen_write_collect_end(struct screen_write_ctx *ctx) { struct screen *s = ctx->s; - struct screen_write_citem *ci = ctx->item, *bci = NULL, *aci; + struct screen_write_citem *ci = ctx->item; struct screen_write_cline *cl = &s->write_list[s->cy]; struct grid_cell gc; - u_int xx; + u_int xx, bx = 0, bnx = 0; if (ci->used == 0) return; @@ -2504,8 +2574,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - grid_view_set_cell(s->grid, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(s->grid, xx, s->cy); log_debug("%s: padding erased (before) at %u (cx %u)", __func__, xx, s->cx); } @@ -2514,20 +2583,12 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_get_cell(s->grid, 0, s->cy, &gc); if (gc.data.width > 1 || (gc.flags & GRID_FLAG_PADDING)) { - grid_view_set_cell(s->grid, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(s->grid, xx, s->cy); log_debug("%s: padding erased (before) at %u " "(cx %u)", __func__, xx, s->cx); } - } - if (xx != s->cx) { - bci = ctx->item; - bci->type = CLEAR; - bci->x = xx; - bci->bg = 8; - bci->used = s->cx - xx; - log_debug("%s: padding erased (before): from %u, " - "size %u", __func__, bci->x, bci->used); + bx = xx; + bnx = s->cx - xx; } } @@ -2538,28 +2599,20 @@ screen_write_collect_end(struct screen_write_ctx *ctx) grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, ci->used); - if (bci != NULL) - screen_write_collect_insert(ctx, bci); + if (bnx != 0) + screen_write_insert_clears(ctx, bx, bnx); screen_write_set_cursor(ctx, s->cx + ci->used, -1); for (xx = s->cx; xx < screen_size_x(s); xx++) { grid_view_get_cell(s->grid, xx, s->cy, &gc); if (~gc.flags & GRID_FLAG_PADDING) break; - grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); - log_debug("%s: padding erased (after) at %u (cx %u)", - __func__, xx, s->cx); - } - if (xx != s->cx) { - aci = ctx->item; - aci->type = CLEAR; - aci->x = s->cx; - aci->bg = 8; - aci->used = xx - s->cx; - log_debug("%s: padding erased (after): from %u, size %u", - __func__, aci->x, aci->used); - screen_write_collect_insert(ctx, aci); + screen_write_clear_cell(s->grid, xx, s->cy); + log_debug("%s: padding erased (after) at %u (cx %u)", __func__, + xx, s->cx); } + if (xx != s->cx) + screen_write_insert_clears(ctx, s->cx, xx - s->cx); } /* Write cell data, collecting if necessary. */ @@ -2687,7 +2740,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) */ for (xx = s->cx + 1; xx < s->cx + width; xx++) { log_debug("%s: new padding at %u,%u", __func__, xx, s->cy); - grid_view_set_padding(gd, xx, s->cy); + grid_view_set_padding(gd, xx, s->cy, gc->bg); skip = 0; } @@ -2809,10 +2862,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. */ @@ -2875,7 +2929,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) /* Check if this combined character would be too long. */ if (last.data.size + ud->size > sizeof last.data.data) - return (0); + return (zero_width); /* Combining; flush any pending output. */ screen_write_collect_flush(ctx, 0, __func__); @@ -2899,16 +2953,18 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) /* Set the new cell. */ grid_view_set_cell(gd, cx - n, cy, &last); if (force_wide) - grid_view_set_padding(gd, cx - 1, cy); + grid_view_set_padding(gd, cx - 1, cy, last.bg); /* * Check if all of this character is visible. No character will be * 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) { @@ -2968,12 +3024,12 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, if (~tmp_gc.flags & GRID_FLAG_PADDING) break; log_debug("%s: padding at %u,%u", __func__, xx, s->cy); - grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); } /* Overwrite the character at the start of this padding. */ log_debug("%s: character at %u,%u", __func__, xx, s->cy); - grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); done = 1; } @@ -2991,17 +3047,7 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc, break; log_debug("%s: overwrite at %u,%u", __func__, xx, s->cy); - if (gc->flags & GRID_FLAG_TAB) { - memcpy(&tmp_gc, gc, sizeof tmp_gc); - memset(tmp_gc.data.data, 0, - sizeof tmp_gc.data.data); - *tmp_gc.data.data = ' '; - tmp_gc.data.width = tmp_gc.data.size = - tmp_gc.data.have = 1; - grid_view_set_cell(gd, xx, s->cy, &tmp_gc); - } else - grid_view_set_cell(gd, xx, s->cy, - &grid_default_cell); + screen_write_clear_cell(gd, xx, s->cy); done = 1; } } diff --git a/screen.c b/screen.c index 6b539b382..f9ca89af5 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen.c,v 1.105 2026/06/29 18:17:28 nicm Exp $ */ +/* $OpenBSD: screen.c,v 1.107 2026/07/26 09:20:54 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -69,6 +69,7 @@ screen_free_titles(struct screen *s) free(s->titles); s->titles = NULL; + s->ntitles = 0; } /* Create a new screen. */ @@ -80,6 +81,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->title = xstrdup(""); s->titles = NULL; + s->ntitles = 0; s->path = NULL; s->cstyle = SCREEN_CURSOR_DEFAULT; diff --git a/server-client.c b/server-client.c index 8c208b1e5..27ae9307b 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.498 2026/07/21 12:28:43 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.510 2026/09/01 19:50:58 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -36,7 +36,8 @@ static void server_client_check_window_resize(struct window *); static key_code server_client_check_mouse(struct client *, struct key_event *); static void server_client_repeat_timer(int, short, void *); static void server_client_click_timer(int, short, void *); -static void server_client_check_exit(struct client *); +static void server_client_check_exit(struct client *, int); +static void server_client_exit_timer(int, short, void *); static void server_client_check_redraw(struct client *); static void server_client_check_modes(struct client *); static void server_client_set_title(struct client *); @@ -44,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 *); @@ -309,6 +311,7 @@ server_client_create(int fd) evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c); + evtimer_set(&c->exit_timer, server_client_exit_timer, c); c->click_wp = -1; @@ -521,10 +524,16 @@ 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); + evtimer_del(&c->exit_timer); + if (event_initialized(&c->cycle_timer)) + evtimer_del(&c->cycle_timer); key_bindings_unref_table(c->keytable); @@ -795,8 +804,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, } else { /* Try the pane borders. */ TAILQ_FOREACH(fwp, &w->panes, entry) { - if ((w->flags & WINDOW_ZOOMED) && - (~fwp->flags & PANE_ZOOMED)) + if (!window_pane_is_visible(fwp)) continue; if (window_pane_is_floating(fwp) && window_pane_get_pane_lines(fwp) == PANE_LINES_NONE) @@ -1383,6 +1391,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. @@ -1468,6 +1491,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. @@ -1632,15 +1667,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) @@ -1680,6 +1708,9 @@ server_client_handle_menu_key(struct client *c, struct key_event *event) memcpy(&new_event, event, sizeof new_event); if (KEYC_IS_MOUSE(event->key)) { m = &new_event.m; + m->statusat = status_at_line(c); + m->statuslines = status_line_size(c); + tty_window_offset(&c->tty, &ox, &oy, &sx, &sy); m->x += ox; if (m->statusat == 0) { @@ -1687,8 +1718,7 @@ server_client_handle_menu_key(struct client *c, struct key_event *event) m->x = m->y = UINT_MAX; else m->y = m->y - m->statuslines + oy; - } else if (m->statusat > 0 && - m->y >= (u_int)m->statusat) + } else if (m->statusat > 0 && m->y >= (u_int)m->statusat) m->x = m->y = UINT_MAX; else m->y += oy; @@ -1726,9 +1756,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) { @@ -1748,6 +1778,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) { @@ -1845,7 +1890,7 @@ server_client_loop(void) /* Check clients. */ TAILQ_FOREACH(c, &clients, entry) { - server_client_check_exit(c); + server_client_check_exit(c, 0); if (c->session != NULL && c->session->curw != NULL) { server_client_check_modes(c); server_client_check_redraw(c); @@ -2199,7 +2244,7 @@ server_client_reset_state(struct client *c) cy += status_line_size(c); } - if ((pane_mode & MODE_SYNC) || !cursor) + if (!cursor) mode &= ~MODE_CURSOR; } } else if (c->overlay_mode == NULL || s == NULL) @@ -2207,6 +2252,10 @@ server_client_reset_state(struct client *c) if (~pane_mode & MODE_SYNC) { log_debug("%s: cursor to %u,%u", __func__, cx, cy); tty_cursor(tty, cx, cy); + } else { + mode &= ~CURSOR_MODES; + mode |= tty->mode & CURSOR_MODES; + s = NULL; } /* @@ -2281,9 +2330,37 @@ server_client_click_timer(__unused int fd, __unused short events, void *data) c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK); } -/* Check if client should be exited. */ +/* Start client exit timer. */ static void -server_client_check_exit(struct client *c) +server_client_start_exit_timer(struct client *c) +{ + struct timeval tv = { .tv_sec = 10 }; + + if (!evtimer_pending(&c->exit_timer, NULL)) + evtimer_add(&c->exit_timer, &tv); +} + +/* Exit timer has expired: stop waiting for the client. */ +static void +server_client_exit_timer(__unused int fd, __unused short events, void *data) +{ + struct client *c = data; + + if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED)) + return; + + if (c->flags & CLIENT_EXITED) { + log_debug("%s: %s took too long to exit", __func__, c->name); + server_client_lost(c); + } else if (c->flags & CLIENT_EXIT) { + log_debug("%s: %s took too long to flush", __func__, c->name); + server_client_check_exit(c, 1); + } +} + +/* Check if client should be exited, abandoning buffered output if forced. */ +static void +server_client_check_exit(struct client *c, int force) { struct client_file *cf; const char *name = c->exit_session; @@ -2296,16 +2373,29 @@ server_client_check_exit(struct client *c) return; if (c->flags & CLIENT_CONTROL) { - control_discard(c); - if (!control_all_done(c)) - return; + if (force) + control_discard_all(c); + else { + control_discard(c); + if (!control_all_done(c)) { + server_client_start_exit_timer(c); + return; + } + } } - RB_FOREACH(cf, client_files, &c->files) { - if (EVBUFFER_LENGTH(cf->buffer) != 0) - return; + if (!force) { + RB_FOREACH(cf, client_files, &c->files) { + if (EVBUFFER_LENGTH(cf->buffer) != 0) { + server_client_start_exit_timer(c); + return; + } + } } c->flags |= CLIENT_EXITED; + evtimer_del(&c->exit_timer); + server_client_start_exit_timer(c); + switch (c->exit_type) { case CLIENT_EXIT_RETURN: if (c->exit_message != NULL) @@ -2327,8 +2417,6 @@ server_client_check_exit(struct client *c) proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1); break; } - free(c->exit_session); - free(c->exit_message); } /* Redraw timer callback. */ @@ -2404,7 +2492,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; @@ -2430,8 +2518,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; } @@ -2450,7 +2544,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); @@ -2480,7 +2575,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); } @@ -2647,6 +2743,10 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (file_write_ready(&c->files, imsg) != 0) goto bad; break; + case MSG_WRITE_DONE: + if (file_write_done(&c->files, imsg) != 0) + goto bad; + break; case MSG_READ: if (file_read_data(&c->files, imsg) != 0) goto bad; diff --git a/server-fn.c b/server-fn.c index 862f4d836..697d7abc5 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-fn.c,v 1.148 2026/07/14 19:07:03 nicm Exp $ */ +/* $OpenBSD: server-fn.c,v 1.152 2026/09/03 19:12:36 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -226,10 +226,11 @@ server_kill_pane(struct window_pane *wp) server_kill_window(w, 1); recalculate_sizes(); } else { - server_unzoom_window(w); + window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM); server_client_remove_pane(wp); layout_close_pane(wp); window_remove_pane(w, wp); + window_pop_zoom(w); server_redraw_window(w); } } @@ -371,10 +372,16 @@ server_destroy_pane(struct window_pane *wp, int notify) close(wp->fd); wp->fd = -1; } + if (wp->pipe_fd != -1) { + bufferevent_free(wp->pipe_event); + wp->pipe_event = NULL; + close(wp->pipe_fd); + wp->pipe_fd = -1; + } - remain_on_exit = options_get_number(wp->options, "remain-on-exit"); - if (remain_on_exit != 0 && (~wp->flags & PANE_STATUSREADY)) + if (~wp->flags & PANE_STATUSREADY) return; + remain_on_exit = options_get_number(wp->options, "remain-on-exit"); switch (remain_on_exit) { case 0: break; @@ -415,15 +422,17 @@ server_destroy_pane(struct window_pane *wp, int notify) if (notify) server_fire_pane_exit("pane-exited", wp); - server_unzoom_window(w); + window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM); server_client_remove_pane(wp); layout_close_pane(wp); window_remove_pane(w, wp); if (TAILQ_EMPTY(&w->panes)) server_kill_window(w, 1); - else + else { + window_pop_zoom(w); server_redraw_window(w); + } } static void @@ -545,6 +554,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 4142272ae..27ff80c29 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: server.c,v 1.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; @@ -240,6 +241,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, if (cause != NULL) { if (c != NULL) { c->exit_message = cause; + c->retval = 1; c->flags |= CLIENT_EXIT; } else { fprintf(stderr, "%s\n", cause); @@ -398,6 +400,7 @@ server_accept(int fd, short events, __unused void *data) c = server_client_create(newfd); if (!server_acl_join(c)) { c->exit_message = xstrdup("access not allowed"); + c->retval = 1; c->flags |= CLIENT_EXIT; } } diff --git a/session.c b/session.c index d52b70b88..491f48904 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: session.c,v 1.107 2026/08/05 07:35:35 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -709,8 +709,10 @@ session_group_synchronize1(struct session *target, struct session *s) /* Fix up the current window. */ if (s->curw != NULL) s->curw = winlink_find_by_index(&s->windows, s->curw->idx); - else + else if (target->curw != NULL) s->curw = winlink_find_by_index(&s->windows, target->curw->idx); + if (s->curw == NULL) + s->curw = RB_MIN(winlinks, &s->windows); /* Fix up the last window stack. */ memcpy(&old_lastw, &s->lastw, sizeof old_lastw); diff --git a/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 1b382202b..361d207e5 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.49 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.52 2026/08/20 09:19:24 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -222,6 +222,7 @@ spawn_window(struct spawn_context *sc, char **cause) w->name = xstrdup(sc->name); options_set_number(w->options, "automatic-rename", 0); } + window_set_fill_cells(w); } /* Switch to the new window if required. */ @@ -242,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; @@ -330,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 { @@ -345,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 @@ -730,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); } @@ -750,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-protocol.h b/tmux-protocol.h index d823ce7e2..396f10bc9 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ +/* $OpenBSD: tmux-protocol.h,v 1.3 2026/08/17 07:56:56 nicm Exp $ */ /* * Copyright (c) 2021 Nicholas Marriott @@ -67,7 +67,8 @@ enum msgtype { MSG_WRITE, MSG_WRITE_READY, MSG_WRITE_CLOSE, - MSG_READ_CANCEL + MSG_READ_CANCEL, + MSG_WRITE_DONE }; /* @@ -116,4 +117,9 @@ struct msg_write_close { int stream; }; +struct msg_write_done { + int stream; + int error; +}; + #endif /* TMUX_PROTOCOL_H */ diff --git a/tmux.1 b/tmux.1 index 6ae074f72..df1ca6e44 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1146 2026/07/21 12:28:43 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1164 2026/09/03 21:04:11 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: July 21 2026 $ +.Dd $Mdocdate: September 3 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -356,6 +356,8 @@ Force redraw of the attached client. Select a new session for the attached client interactively. .It t Show the time. +.It T +Change the current pane title. .It w Choose the current window interactively. .It x @@ -2227,14 +2229,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) @@ -2308,7 +2310,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 @@ -2340,10 +2342,14 @@ the bottom and is paused while a selection is in progress. .Xc Turn off automatic refresh of the content from the pane. .It Xo -.Ic refresh\-toggle +.Ic refresh\-now (vi: r) (emacs: r) .Xc +Refresh the content from the pane once. +.It Xo +.Ic refresh\-toggle +.Xc Toggle automatic refresh of the content from the pane. .It Xo .Ic scroll\-bottom @@ -2589,8 +2595,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. @@ -3612,13 +3618,20 @@ represents the window to be created; if the target already exists an error is shown, unless the .Fl k flag is used, in which case it is destroyed. +.Ar window\-name +is the name given to the new window, if any. +.Pp If .Fl S -is given and a window named -.Ar window\-name -already exists, it is selected (unless +is given, an existing window is selected instead of creating a new one +(unless .Fl d -is also given in which case the command does nothing). +is also given in which case the command does nothing): +.Ar target\-window +is preferred if it already identifies an existing window; otherwise a +window named +.Ar window\-name +is selected if one exists. .Pp .Ar shell\-command is the command to execute. @@ -3666,7 +3679,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 @@ -3715,14 +3728,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 @@ -4719,6 +4735,7 @@ the result would be the default background and a blue foreground. .Tg show .It Xo Ic show\-options .Op Fl AgHpqsvw +.Op Fl F Ar format .Op Fl t Ar target\-pane .Op Ar option .Xc @@ -4746,6 +4763,10 @@ Global session or window options are listed if is used. .Fl v shows only the option value, not the name. +With +.Fl F , +.Ar format +is used for each option. If .Fl q is set, no error will be returned if @@ -4768,6 +4789,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 @@ -5064,6 +5094,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 @@ -5114,6 +5148,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 @@ -5648,6 +5684,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: @@ -5737,8 +5778,13 @@ Set the time in milliseconds for which the indicators shown by the .Ic display\-panes command appear. .Pp -.It Ic fill\-character Ar character -Set the character used to fill areas of the terminal unused by a window. +.It Ic fill\-character Ar format +Set the format used to fill areas of the terminal unused by a pane. +The format is expanded once for areas inside the window with +.Ql is_inside +set, and once for areas outside the window with +.Ql is_outside +set. .Pp .It Ic main\-pane\-height Ar height .It Ic main\-pane\-width Ar width @@ -6637,6 +6683,7 @@ run immediately. .It Xo Ic show\-hooks .Op Fl Bgpw +.Op Fl F Ar format .Op Fl t Ar target\-pane .Op Ar hook .Xc @@ -6649,6 +6696,10 @@ With shows the subscriptions installed with .Em set\-hook .Fl B . +With +.Fl F , +.Ar format +is used for each hook. .El .Sh MOUSE SUPPORT If the @@ -7063,6 +7114,20 @@ escape command arguments; with .Ql s use single quotes. +.Pp +Some variables such as +.Ic pane_title +and +.Ic pane_path +are set by escape sequences received from pane and may contain arbitrary +content including shell metacharacters. +.Ql q:\& +should be used to escape these with commands passed to +.Ic run\-shell , +.Ic if\-shell , +or +.Ic pipe\-pane . +.Pp .Ql E:\& will expand the format twice, for example .Ql #{E:status\-left} @@ -7227,6 +7292,26 @@ For example will be replaced by .Ql #{?pane_in_mode,yes,no} . .Pp +The +.Ql A +modifier shows one of a list of frames in turn: +.Ql #{A/count:frame,frame,...} +or +.Ql #{A:frame,frame,...} . +Each frame is shown for +.Ar count +periods of 100 milliseconds before the next, wrapping back to the first after +the last. +If no count is given, one is used. +The +.Ql A +modifier is expanded only in the status line and +.Ic pane\-border\-format ; +if present, the status line or pane borders are redrawn every 100 milliseconds. +For example, +.Ql #{A/2:|,/,\-,\e} +shows the four frames in turn, changing every 200 milliseconds. +.Pp The following variables are available, where appropriate: .Bl -column "XXXXXXXXXXXXXXXXXXX" "XXXXX" .It Sy "Variable name" Ta Sy "Alias" Ta Sy "Replaced with" @@ -7287,12 +7372,19 @@ 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" .It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any" +.It Li "hook_fire_count" Ta "" Ta "Number of times hook has fired" +.It Li "hook_fire_time" Ta "" Ta "Time hook last fired" .It Li "hook_last" Ta "" Ta "Previous value for a monitor hook" +.It Li "hook_monitor_format" Ta "" Ta "Format for a monitor hook" +.It Li "hook_monitor_target" Ta "" Ta "Target for a monitor hook" .It Li "hook_pane" Ta "" Ta "ID of pane where hook was run, if any" .It Li "hook_session" Ta "" Ta "ID of session where hook was run, if any" .It Li "hook_session_name" Ta "" Ta "Name of session where hook was run, if any" @@ -7334,6 +7426,17 @@ The following variables are available, where appropriate: .It Li "next_session_id" Ta "" Ta "Unique session ID for next new session" .It Li "next_window_active" Ta "" Ta "1 if next window in W: loop is active" .It Li "next_window_index" Ta "" Ta "Index of next window in W: loop" +.It Li "option_array_key" Ta "" Ta "Array key, if option is an array item" +.It Li "option_has_array_key" Ta "" Ta "1 if option array key is present" +.It Li "option_has_value" Ta "" Ta "1 if option value is present" +.It Li "option_is_array" Ta "" Ta "1 if option is an array" +.It Li "option_is_hook" Ta "" Ta "1 if option is a hook" +.It Li "option_is_parent" Ta "" Ta "1 if option is inherited" +.It Li "option_is_string" Ta "" Ta "1 if option is a string" +.It Li "option_is_user" Ta "" Ta "1 if option is a user option" +.It Li "option_name" Ta "" Ta "Name of option" +.It Li "option_value" Ta "" Ta "Value of option" +.It Li "option_value_only" Ta "" Ta "1 if only option values will be shown" .It Li "origin_flag" Ta "" Ta "Pane origin flag" .It Li "pane_active" Ta "" Ta "1 if active pane" .It Li "pane_at_bottom" Ta "" Ta "1 if pane is at the bottom of window" @@ -7371,10 +7474,12 @@ 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" .It Li "pane_pipe_pid" Ta "" Ta "PID of pipe process, if any" +.It Li "pane_private_modes" Ta "" Ta "List of enabled DECSET private modes" .It Li "pane_pb_state" Ta "" Ta "Pane progress bar state, one of hidden, normal, error, indeterminate, paused (can be set by application)" .It Li "pane_pb_progress" Ta "" Ta "Pane progress bar progress percentage (can be set by application)" .It Li "pane_right" Ta "" Ta "Right of pane" @@ -8730,7 +8835,7 @@ values are given, they are available as and so on. For example: .Bd -literal -offset indent -run-shell 'myscript.sh #1 #2' foo bar +run-shell 'myscript.sh #{1} #{2}' foo bar .Ed .Pp With 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 8cd7410b0..0db381fd6 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1412 2026/07/21 12:28:43 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, @@ -1196,6 +1197,8 @@ struct window_mode { int flags; #define WINDOW_MODE_HIDE_PANE_STATUS 0x1 #define WINDOW_MODE_NO_STACK 0x2 +#define WINDOW_MODE_FILL_WINDOW 0x4 +#define WINDOW_MODE_HIDE_SCROLLBARS 0x8 struct screen *(*init)(struct window_mode_entry *, struct cmdq_item *, struct cmd_find_state *, @@ -1335,6 +1338,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 +1359,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 +1447,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; @@ -1474,7 +1481,8 @@ struct window { int sb; int sb_pos; - struct utf8_data *fill_character; + struct grid_cell inside_cell; + struct grid_cell outside_cell; int flags; #define WINDOW_BELL 0x1 #define WINDOW_ACTIVITY 0x2 @@ -1482,7 +1490,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; @@ -1717,7 +1724,7 @@ struct key_event { struct tty_term { char *name; struct tty *tty; - int features; + int applied_features; char acs[UCHAR_MAX + 1][2]; @@ -1730,6 +1737,7 @@ struct tty_term { #define TERM_RGBCOLOURS 0x10 #define TERM_VT100LIKE 0x20 #define TERM_SIXEL 0x40 +#define TERM_INVALIDMS 0x80 int flags; LIST_ENTRY(tty_term) entry; @@ -1809,8 +1817,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; @@ -2224,6 +2233,7 @@ struct client { char *term_name; int term_features; + int term_nofeatures; char *term_type; char **term_caps; u_int term_ncaps; @@ -2242,10 +2252,13 @@ struct client { struct event click_timer; int click_loc; int click_wp; + + struct event exit_timer; u_int click_button; struct mouse_event click_event; struct status_line status; + struct event cycle_timer; enum client_theme theme; struct input_requests input_requests; @@ -2281,14 +2294,14 @@ 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 /* 0x800000000ULL unused */ #define CLIENT_BRACKETPASTING 0x1000000000ULL #define CLIENT_ASSUMEPASTING 0x2000000000ULL -/* 0x4000000000ULL unused */ +#define CLIENT_WRITE_ACK 0x4000000000ULL #define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL #define CLIENT_ALLREDRAWFLAGS \ (CLIENT_REDRAWWINDOW| \ @@ -2527,6 +2540,7 @@ struct spawn_context { #define SPAWN_HORIZONTAL 0x200 #define SPAWN_SPLIT 0x400 #define SPAWN_MODAL 0x800 +#define SPAWN_FLOATOVERZOOM 0x1000 }; /* Paste buffer. */ @@ -2784,6 +2798,10 @@ void hooks_monitor_add(struct cmdq_item *, struct options *, void hooks_monitor_remove(struct options *, const char *); void hooks_monitor_free(void *); char *hooks_monitor_to_string(struct options_entry *); +int hooks_monitor_get(struct options_entry *, enum monitor_type *, int *, + const char **); +u_int hooks_monitor_get_fire_count(struct options_entry *); +time_t hooks_monitor_get_fire_time(struct options_entry *); /* options.c */ struct options *options_create(struct options *); @@ -2801,6 +2819,9 @@ const char *options_name(struct options_entry *); struct options *options_owner(struct options_entry *); void *options_get_monitor_data(struct options_entry *); void options_set_monitor_data(struct options_entry *, void *); +void options_hook_fired(struct options_entry *); +u_int options_get_fire_count(struct options_entry *); +time_t options_get_fire_time(struct options_entry *); const struct options_table_entry *options_table_entry(struct options_entry *); struct options_entry *options_get_only(struct options *, const char *); struct options_entry *options_get(struct options *, const char *); @@ -2987,8 +3008,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 **); @@ -3010,11 +3030,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 *); @@ -3268,6 +3290,7 @@ void file_write_close(struct client_files *, struct imsg *); void file_read_open(struct client_files *, struct tmuxpeer *, struct imsg *, int, int, client_file_cb, void *); int file_write_ready(struct client_files *, struct imsg *); +int file_write_done(struct client_files *, struct imsg *); int file_read_data(struct client_files *, struct imsg *); int file_read_done(struct client_files *, struct imsg *); void file_read_cancel(struct client_files *, struct imsg *); @@ -3484,7 +3507,7 @@ void grid_clear_history(struct grid *); const struct grid_line *grid_peek_line(struct grid *, u_int); void grid_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); -void grid_set_padding(struct grid *, u_int, u_int); +void grid_set_padding(struct grid *, u_int, u_int, int); void grid_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, const char *, size_t); struct grid_line *grid_get_line(struct grid *, u_int); @@ -3501,6 +3524,7 @@ void grid_reflow(struct grid *, u_int); void grid_wrap_position(struct grid *, u_int, u_int, u_int *, u_int *); void grid_unwrap_position(struct grid *, u_int *, u_int *, u_int, u_int); u_int grid_line_length(struct grid *, u_int); +u_int grid_line_limit(struct grid *, u_int); int grid_in_set(struct grid *, u_int, u_int, const char *); /* grid-reader.c */ @@ -3528,7 +3552,7 @@ void grid_reader_cursor_back_to_indentation(struct grid_reader *); void grid_view_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_view_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); -void grid_view_set_padding(struct grid *, u_int, u_int); +void grid_view_set_padding(struct grid *, u_int, u_int, int); void grid_view_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, const char *, size_t); void grid_view_clear_history(struct grid *, u_int); @@ -3584,6 +3608,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); @@ -3713,8 +3738,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 *); @@ -3781,7 +3806,6 @@ void *window_pane_get_new_data(struct window_pane *, struct window_pane_offset *, size_t *); void window_pane_update_used_data(struct window_pane *, struct window_pane_offset *, size_t); -void window_set_fill_character(struct window *); void window_pane_default_cursor(struct window_pane *); int window_pane_mode(struct window_pane *); int window_pane_show_scrollbar(struct window_pane *); @@ -3809,8 +3833,10 @@ struct style_range *window_pane_status_get_range(struct window_pane *, u_int, int window_pane_is_floating(struct window_pane *); /* window-border.c */ -void window_get_border_cell(struct window *, struct window_pane *, - enum pane_lines, int, struct grid_cell *); +void window_set_fill_cells(struct window *); +void window_get_border_cell(struct window_pane *, enum pane_lines, + int, struct grid_cell *); +void window_get_fill_cell(struct window *, int, struct grid_cell *); void window_pane_get_border_cell(struct window_pane *, int, struct grid_cell *); void window_pane_get_border_style(struct window_pane *, @@ -4004,9 +4030,12 @@ int monitor_parse(const char *, char **, enum monitor_type *, int *, void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, const char *, int); void monitor_remove(struct monitor_set *, const char *); +u_int monitor_get_fire_count(struct monitor_set *, const char *); +time_t monitor_get_fire_time(struct monitor_set *, const char *); /* control.c */ void control_discard(struct client *); +void control_discard_all(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); @@ -4015,6 +4044,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); @@ -4022,6 +4052,8 @@ struct window_pane_offset *control_pane_offset(struct client *, struct window_pane *, int *); void control_reset_offsets(struct client *); void printflike(2, 3) control_write(struct client *, const char *, ...); +void printflike(2, 3) control_notify_write(struct client *, const char *, ...); +void control_write_guard(struct client *, const char *, long, u_int, int); void control_write_output(struct client *, struct window_pane *); int control_all_done(struct client *); void control_add_sub(struct client *, const char *, enum monitor_type, int, @@ -4077,6 +4109,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-draw.c b/tty-draw.c index d47be66bc..06d520e11 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-draw.c,v 1.14 2026/06/19 10:38:29 nicm Exp $ */ +/* $OpenBSD: tty-draw.c,v 1.15 2026/07/26 09:02:08 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -100,6 +100,8 @@ tty_draw_line_get_empty(const struct grid_cell *gc, empty = nx; else if (gc->flags & GRID_FLAG_PADDING) empty = 1; + else if (gc->data.width == 0) + empty = 1; else if (gc->flags & GRID_FLAG_SELECTED) empty = 0; else if (gc->bg == last->bg && gc->attr == 0 && gc->link == 0) { diff --git a/tty-features.c b/tty-features.c index 656100ba9..0b12745e2 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ +/* $OpenBSD: tty-features.c,v 1.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; @@ -596,6 +638,18 @@ tty_default_features(int *feat, const char *name, u_int version) "usstyle," "progressbar" }, + { .name = "Rio", + .features = TTY_FEATURES_BASE_MODERN_XTERM "," + "ccolour," + "cstyle," + "focus," + "overline," + "hyperlinks," + "osc7," + "sync," + "usstyle," + "progressbar" + }, { .name = "XTerm", /* * xterm also supports DECSLRM and DECFRA, but they can be @@ -616,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 2d4454b6c..8462e0d9b 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.211 2026/07/21 07:12:49 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.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,19 +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(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 164b9f977..6b149fcee 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ +/* $OpenBSD: tty-term.c,v 1.109 2026/08/25 08:37:08 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -31,6 +31,7 @@ #include "tmux.h" static char *tty_term_strip(const char *); +static void tty_term_validate(struct tty_term *); struct tty_terms tty_terms = LIST_HEAD_INITIALIZER(tty_terms); @@ -110,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" }, @@ -519,12 +521,33 @@ tty_term_apply_overrides(struct tty_term *term) acs = "a#j+k+l+m+n+o-p-q-r-s-t+u+v+w+x|y~."; for (; acs[0] != '\0' && acs[1] != '\0'; acs += 2) term->acs[(u_char) acs[0]][0] = acs[1]; + + tty_term_validate(term); +} + +static void +tty_term_validate(struct tty_term *term) +{ + struct tty_code *code = &term->codes[TTYC_MS]; + + if (code->type != TTYCODE_STRING) + return; + if (*tty_term_string_ss(term, TTYC_MS, "c", "?") != '\0') { + term->flags &= ~TERM_INVALIDMS; + return; + } + + log_debug("removing invalid Ms capability"); + term->flags |= TERM_INVALIDMS; + free(code->value.string); + code->type = TTYCODE_NONE; } struct tty_term * 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; @@ -596,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); } @@ -606,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. */ @@ -644,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 c5b443cee..9bac687eb 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-border.c b/window-border.c index 5cd88c2bb..8f2209d0e 100644 --- a/window-border.c +++ b/window-border.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-border.c,v 1.2 2026/07/17 12:42:51 nicm Exp $ */ +/* $OpenBSD: window-border.c,v 1.3 2026/07/23 09:38:27 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -23,18 +23,82 @@ #include "tmux.h" +/* Set window fill cell. */ +static void +window_set_fill_cell(struct window *w, int inside, struct grid_cell *gc) +{ + struct format_tree *ft; + struct screen s; + struct screen_write_ctx ctx; + struct grid_cell new_gc; + const char *value; + char *expanded; + + memcpy(gc, &grid_default_cell, sizeof *gc); + gc->attr |= GRID_ATTR_CHARSET; + utf8_set(&gc->data, CELL_BORDERS[CELL_NONE]); + + ft = format_create(NULL, NULL, FORMAT_WINDOW|w->id, FORMAT_NOJOBS); + format_defaults(ft, NULL, NULL, NULL, w->active); + format_add(ft, "is_inside", "%d", inside); + format_add(ft, "is_outside", "%d", !inside); + + value = options_get_string(w->options, "fill-character"); + expanded = format_expand(ft, value); + format_free(ft); + + screen_init(&s, 1, 1, 0); + screen_write_start(&ctx, &s); + format_draw(&ctx, &grid_default_cell, 1, expanded, NULL, 0); + screen_write_stop(&ctx); + free(expanded); + + grid_view_get_cell(s.grid, 0, 0, &new_gc); + if (new_gc.data.width == 1) + memcpy(gc, &new_gc, sizeof *gc); + screen_free(&s); +} + +/* Set window fill cells. */ +void +window_set_fill_cells(struct window *w) +{ + window_set_fill_cell(w, 1, &w->inside_cell); + window_set_fill_cell(w, 0, &w->outside_cell); +} + +/* Merge a window fill cell over an existing style. */ +static void +window_copy_fill_cell(struct grid_cell *gc, const struct grid_cell *fill) +{ + utf8_copy(&gc->data, &fill->data); + gc->attr |= fill->attr; + gc->flags |= fill->flags; + if (fill->fg != 8) + gc->fg = fill->fg; + if (fill->bg != 8) + gc->bg = fill->bg; + if (fill->us != 8) + gc->us = fill->us; +} + +/* Get window fill cell. */ +void +window_get_fill_cell(struct window *w, int inside, struct grid_cell *gc) +{ + if (inside) + window_copy_fill_cell(gc, &w->inside_cell); + else + window_copy_fill_cell(gc, &w->outside_cell); +} + /* Get border cell. */ void -window_get_border_cell(struct window *w, struct window_pane *wp, - enum pane_lines pane_lines, int cell_type, struct grid_cell *gc) +window_get_border_cell(struct window_pane *wp, enum pane_lines pane_lines, + int cell_type, struct grid_cell *gc) { u_int idx; - if (cell_type == CELL_NONE && w->fill_character != NULL) { - utf8_copy(&gc->data, &w->fill_character[0]); - return; - } - switch (pane_lines) { case PANE_LINES_NUMBER: if (cell_type == CELL_NONE) { @@ -79,7 +143,7 @@ window_pane_get_border_cell(struct window_pane *wp, int cell_type, { enum pane_lines pane_lines = window_pane_get_pane_lines(wp); - window_get_border_cell(wp->window, wp, pane_lines, cell_type, gc); + window_get_border_cell(wp, pane_lines, cell_type, gc); } /* Get pane border style. */ @@ -147,7 +211,7 @@ window_make_pane_status(struct window_pane *wp, struct client *c, u_int width, pane_lines = window_pane_get_pane_lines(wp); for (i = 0; i < width; i++) { cell_type = redraw_get_status_border_cell_type(&span, i); - window_get_border_cell(wp->window, wp, pane_lines, cell_type, &gc); + window_get_border_cell(wp, pane_lines, cell_type, &gc); screen_write_cell(&ctx, &gc); } gc.attr &= ~GRID_ATTR_CHARSET; 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 b94c22119..3c2f81097 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-client.c,v 1.47 2026/07/14 17:17:18 nicm Exp $ */ +/* $OpenBSD: window-client.c,v 1.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] " @@ -113,14 +114,16 @@ static const char *window_client_info_lines[] = { "#{?#{I/c:kmous},,#[align=right]unavailable: [kmous] missing}", "#[fg=themelightgrey]set-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " - "#{?#{!=:#{set-clipboard},off},#{?#{I/f:clipboard},," + "#{?#{!=:#{set-clipboard},off},#{?#{I/c:Ms},," "#[fg=themered]}#{set-clipboard},#[fg=themelightgrey]off} " - "#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", + "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] " + "#{?clipboard_invalid,invalid,missing}}", "#[fg=themelightgrey]get-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " - "#{?#{!=:#{get-clipboard},off},#{?#{I/f:clipboard},," + "#{?#{!=:#{get-clipboard},off},#{?#{I/c:Ms},," "#[fg=themered]}#{get-clipboard},#[fg=themelightgrey]off} " - "#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", + "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] " + "#{?clipboard_invalid,invalid,missing}}", "#[fg=themelightgrey]focus-events #[#{E:tree-mode-border-style},acs]x#[default] " "#{?focus-events,#{?#{I/f:focus},,#[fg=themered]}on,#[fg=themelightgrey]off} " @@ -273,6 +276,10 @@ window_client_draw_info(__unused void *modedata, void *itemdata, char *expanded; ft = format_create_defaults(NULL, c, NULL, NULL, NULL); + if (c->tty.term->flags & TERM_INVALIDMS) + format_add(ft, "clipboard_invalid", "1"); + else + format_add(ft, "clipboard_invalid", "0"); screen_write_cursormove(ctx, cx, cy, 0); for (i = 0; i < nitems(window_client_info_lines); i++) { diff --git a/window-copy.c b/window-copy.c index 0ffbe9b75..598426d02 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.423 2026/07/21 11:52:13 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.429 2026/09/01 13:04:29 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -61,6 +61,7 @@ static void window_copy_redraw_screen(struct window_mode_entry *); static void window_copy_do_refresh(struct window_mode_entry *, int); static void window_copy_refresh_timer(int, short, void *); static void window_copy_refresh_arm(struct window_mode_entry *); +static int window_copy_refresh_allowed(struct window_mode_entry *); static void window_copy_refresh_start(struct window_mode_entry *); static void window_copy_refresh_stop(struct window_mode_entry *); static void window_copy_style_changed(struct window_mode_entry *); @@ -2738,15 +2739,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); } @@ -3574,6 +3636,20 @@ window_copy_refresh_arm(struct window_mode_entry *wme) evtimer_add(&data->refresh_timer, &tv); } +static int +window_copy_refresh_allowed(struct window_mode_entry *wme) +{ + struct window_copy_mode_data *data = wme->data; + + /* + * Do not refresh a view of another pane (copy-mode -s): the source may + * disappear and changes are not tracked on this pane. + */ + if (data->viewmode || wme->swp != wme->wp) + return (0); + return (1); +} + static void window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) { @@ -3608,11 +3684,7 @@ window_copy_refresh_start(struct window_mode_entry *wme) { struct window_copy_mode_data *data = wme->data; - /* - * Do not refresh a view of another pane (copy-mode -s): the source may - * disappear and changes are not tracked on this pane. - */ - if (data->viewmode || wme->swp != wme->wp || data->refresh_active) + if (!window_copy_refresh_allowed(wme) || data->refresh_active) return; data->refresh_active = 1; window_copy_refresh_arm(wme); @@ -3627,6 +3699,25 @@ window_copy_refresh_stop(struct window_mode_entry *wme) evtimer_del(&data->refresh_timer); } +static enum window_copy_cmd_action +window_copy_cmd_refresh_now(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + struct window_pane *wp = wme->wp; + int follow; + + if (!window_copy_refresh_allowed(wme)) + return (WINDOW_COPY_CMD_NOTHING); + + follow = (data->oy == 0 && + data->cy == screen_size_y(&data->screen) - 1); + window_copy_do_refresh(wme, follow); + wp->flags &= ~PANE_UNSEENCHANGES; + + return (WINDOW_COPY_CMD_REDRAW); +} + static enum window_copy_cmd_action window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) { @@ -4548,6 +4639,12 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_NEVER, .f = window_copy_cmd_refresh_off }, + { .command = "refresh-now", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_refresh_now + }, { .command = "refresh-toggle", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -4980,7 +5077,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) @@ -5039,7 +5136,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) @@ -5188,7 +5285,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; } @@ -5240,6 +5337,7 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy, break; } } + ncells = cell; /* Locate starting cell. */ cell = 0; @@ -5737,6 +5835,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); @@ -7221,16 +7321,13 @@ static u_int window_copy_cursor_limit(struct window_mode_entry *wme, u_int py, int allow_onemore) { + struct window_copy_mode_data *data = wme->data; struct options *oo = wme->wp->window->options; - u_int len; - len = window_copy_find_length(wme, py); if (allow_onemore || options_get_number(oo, "mode-keys") != MODEKEY_VI) - return (len); - if (len == 0) - return (0); - return (len - 1); + return (window_copy_find_length(wme, py)); + return (grid_line_limit(data->backing->grid, py)); } static void diff --git a/window-customize.c b/window-customize.c index 624ef21d7..1bf311ff2 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.35 2026/07/15 12:45:39 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.36 2026/07/28 10:35:31 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -276,6 +276,36 @@ window_customize_scope_text(enum window_customize_scope scope, return (s); } +static int +window_customize_write_hook_fire(struct screen_write_ctx *ctx, u_int cx, + u_int sx, u_int sy, struct options_entry *o) +{ + char *fire_time_string; + u_int fire_count; + time_t fire_time; + + if (options_get_monitor_data(o) != NULL) { + fire_count = hooks_monitor_get_fire_count(o); + fire_time = hooks_monitor_get_fire_time(o); + } else { + fire_count = options_get_fire_count(o); + fire_time = options_get_fire_time(o); + } + if (fire_time != 0) { + fire_time_string = format_pretty_time(fire_time, 0); + if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, + "This hook has been fired %u times, last %s.", fire_count, + fire_time_string)) { + free(fire_time_string); + return (0); + } + free(fire_time_string); + return (1); + } + return (screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, + "This hook has been fired %u times.", fire_count)); +} + static struct window_customize_itemdata * window_customize_add_item(struct window_customize_modedata *data) { @@ -654,7 +684,7 @@ window_customize_build_option(struct window_customize_modedata *data, char *text, *expanded, *value; int global = 0, array = 0; int is_hook = 0, is_monitor = 0; - int is_user_hook = 0; + int is_user_hook = 0, is_any_hook; uint64_t tag; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) @@ -663,13 +693,14 @@ window_customize_build_option(struct window_customize_modedata *data, is_monitor = 1; if (*name == '@' && hooks_is_event(name)) is_user_hook = 1; + is_any_hook = (is_hook || is_monitor || is_user_hook); switch (type) { case WINDOW_CUSTOMIZE_OPTIONS: - if (is_hook || is_monitor || is_user_hook) + if (is_any_hook) return (0); break; case WINDOW_CUSTOMIZE_HOOKS: - if (!is_hook && !is_monitor && !is_user_hook) + if (!is_any_hook) return (0); break; } @@ -1188,6 +1219,7 @@ window_customize_draw_option(struct window_customize_modedata *data, struct cmd_find_state fs; struct format_tree *ft; int is_hook, is_monitor, is_user_hook; + int is_any_hook; if (!window_customize_check_item(data, item, &fs)) return; @@ -1201,6 +1233,7 @@ window_customize_draw_option(struct window_customize_modedata *data, is_hook = (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); is_monitor = (options_get_monitor_data(o) != NULL); is_user_hook = (*name == '@' && hooks_is_event(name)); + is_any_hook = (is_hook || is_monitor || is_user_hook); if (oe != NULL && oe->unit != NULL) { space = " "; @@ -1267,8 +1300,16 @@ monitor: } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { if (is_hook) { - if (array_key == NULL) + if (array_key == NULL) { + if (!screen_write_text(ctx, cx, sx, + sy - (s->cy - cy), 0, &grid_default_cell, + "This is an array hook.")) + goto out; + if (!window_customize_write_hook_fire(ctx, cx, + sx, sy - (s->cy - cy), o)) + goto out; goto out; + } } else if (array_key != NULL) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, @@ -1294,10 +1335,13 @@ monitor: default_value = NULL; } } - if (is_hook || is_monitor || is_user_hook) { + if (is_any_hook) { if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), 0, "Hook command: ", "%s%s%s", value, space, unit)) goto out; + if (!window_customize_write_hook_fire(ctx, cx, sx, + sy - (s->cy - cy), o)) + goto out; } else { if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy), 0, "Option value: ", "%s%s%s", value, space, unit)) diff --git a/window-panes.c b/window-panes.c index 981121460..e65e59606 100644 --- a/window-panes.c +++ b/window-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-panes.c,v 1.3 2026/07/19 19:53:11 nicm Exp $ */ +/* $OpenBSD: window-panes.c,v 1.5 2026/08/25 06:04:33 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -35,7 +35,8 @@ static void window_panes_key(struct window_mode_entry *, const struct window_mode window_panes_mode = { .name = "panes-mode", - .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK, + .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK| + WINDOW_MODE_FILL_WINDOW|WINDOW_MODE_HIDE_SCROLLBARS, .init = window_panes_init, .free = window_panes_free, @@ -1008,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..1ff1007bb 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.4 2026/09/01 21:03:55 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; @@ -373,6 +374,9 @@ window_switch_init(struct window_mode_entry *wme, data->prompt = prompt_create(&pd); prompt_update(data->prompt, "(search) ", data->filter); + s = &data->screen; + screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0); + if (!args_has(args, 'Z')) data->zoomed = -1; else { @@ -381,9 +385,6 @@ window_switch_init(struct window_mode_entry *wme, server_redraw_window(wp->window); } - s = &data->screen; - screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0); - window_switch_build(data); prompt_incremental_start(data->prompt); window_switch_draw_screen(wme); 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 5f7866bdc..618ca5ff8 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.367 2026/07/21 13:04:01 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -437,8 +437,6 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel) w->id = next_window_id++; RB_INSERT(windows, &windows, w); - window_set_fill_character(w); - if (gettimeofday(&w->creation_time, NULL) != 0) fatal("gettimeofday failed"); window_update_activity(w); @@ -472,7 +470,6 @@ window_destroy(struct window *w) event_del(&w->offset_timer); options_free(w->options); - free(w->fill_character); free(w->name); free(w); @@ -721,7 +718,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) return (0); if (w->modal != NULL && wp != w->modal) return (0); - if (w->flags & WINDOW_ZOOMED) + if ((w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp)) window_unzoom(w, 1); lastwp = w->active; @@ -921,13 +918,18 @@ window_zoom(struct window_pane *wp) { struct window *w = wp->window; struct window_pane *wp1; + struct layout_cell *lc; + struct layout_geometry lg; if (w->flags & WINDOW_ZOOMED) return (-1); if (window_count_panes(w, 1) == 1) return (-1); - if (w->active != wp) + if (w->active != wp && + (w->active == NULL || + (~w->active->flags & PANE_FLOATOVERZOOM) || + !window_pane_is_floating(w->active))) window_set_active_pane(w, wp, 1); wp->flags |= PANE_ZOOMED; @@ -938,6 +940,22 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); + TAILQ_FOREACH(wp1, &w->panes, entry) { + lc = wp1->saved_layout_cell; + if (wp1 == wp || + (~wp1->flags & PANE_FLOATOVERZOOM) || + lc == NULL || + (~lc->flags & LAYOUT_CELL_FLOATING)) + continue; + memcpy(&lg, &lc->g, sizeof lg); + lc = layout_floating_pane(w, wp, &lg); + layout_assign_pane(lc, wp1, 0); + } + /* A floating zoom target is now tiled, so put it behind the floats. */ + if (wp->saved_layout_cell->flags & LAYOUT_CELL_FLOATING) { + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + } w->flags |= WINDOW_ZOOMED; events_fire_window("window-zoomed", w); events_fire_window("window-layout-changed", w); @@ -949,11 +967,26 @@ window_zoom(struct window_pane *wp) int window_unzoom(struct window *w, int notify) { - struct window_pane *wp; + struct window_pane *wp, *zoomed = NULL; + struct layout_cell *slc; - if (!(w->flags & WINDOW_ZOOMED)) + if (~w->flags & WINDOW_ZOOMED) return (-1); + TAILQ_FOREACH(wp, &w->panes, entry) { + if (wp->flags & PANE_ZOOMED) + zoomed = wp; + if (~wp->flags & PANE_FLOATOVERZOOM) + continue; + if (wp->flags & PANE_ZOOMED) + continue; + slc = wp->saved_layout_cell; + if (slc == NULL || wp->layout_cell == NULL) + continue; + memcpy(&slc->g, &wp->layout_cell->g, sizeof slc->g); + memcpy(&slc->fg, &wp->layout_cell->fg, sizeof slc->fg); + } + w->flags &= ~WINDOW_ZOOMED; layout_free(w, 0); w->layout_root = w->saved_layout_root; @@ -964,6 +997,22 @@ window_unzoom(struct window *w, int notify) wp->saved_layout_cell = NULL; wp->flags &= ~PANE_ZOOMED; } + /* Put a floating zoom target back into the floating part of the list. */ + if (zoomed != NULL && window_pane_is_floating(zoomed)) { + TAILQ_REMOVE(&w->z_index, zoomed, zentry); + if (zoomed == w->active) + TAILQ_INSERT_HEAD(&w->z_index, zoomed, zentry); + else { + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_is_floating(wp)) + break; + } + if (wp == NULL) + TAILQ_INSERT_TAIL(&w->z_index, zoomed, zentry); + else + TAILQ_INSERT_BEFORE(wp, zoomed, zentry); + } + } layout_fix_panes(w, NULL); if (notify) { @@ -975,48 +1024,69 @@ window_unzoom(struct window *w, int notify) return (0); } -void -window_push_modal_zoom(struct window *w) +struct window_pane * +window_zoomed_pane(struct window *w) { - if (w->flags & WINDOW_ZOOMED) - w->flags |= WINDOW_WASMODALZOOMED; - else - w->flags &= ~WINDOW_WASMODALZOOMED; - window_unzoom(w, 1); + struct window_pane *wp; + + if (~w->flags & WINDOW_ZOOMED) + return (NULL); + TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) { + if (wp->layout_cell != NULL && !window_pane_is_floating(wp)) + return (wp); + } + return (NULL); } int -window_pop_modal_zoom(struct window *w) +window_active_pane_is_over_zoom(struct window *w) { - struct window_pane *wp = w->active; - - if (~w->flags & WINDOW_WASMODALZOOMED) + if (~w->flags & WINDOW_ZOOMED) return (0); - w->flags &= ~WINDOW_WASMODALZOOMED; - if (wp != NULL && window_has_pane(w, wp)) - return (window_zoom(wp) == 0); - return (0); + if (w->active == NULL) + return (0); + if (~w->active->flags & PANE_FLOATOVERZOOM) + return (0); + return (window_pane_is_floating(w->active)); } int window_push_zoom(struct window *w, int always, int flag) { + struct window_pane *wp = window_zoomed_pane(w); + log_debug("%s: @%u %d", __func__, w->id, flag && (w->flags & WINDOW_ZOOMED)); if (flag && (always || (w->flags & WINDOW_ZOOMED))) w->flags |= WINDOW_WASZOOMED; else w->flags &= ~WINDOW_WASZOOMED; + if (w->flags & WINDOW_WASZOOMED) + w->was_zoomed = wp; + else + w->was_zoomed = NULL; return (window_unzoom(w, 1) == 0); } int window_pop_zoom(struct window *w) { + struct window_pane *wp = w->was_zoomed; + log_debug("%s: @%u %d", __func__, w->id, !!(w->flags & WINDOW_WASZOOMED)); - if (w->flags & WINDOW_WASZOOMED) - return (window_zoom(w->active) == 0); + if (w->flags & WINDOW_WASZOOMED) { + w->flags &= ~WINDOW_WASZOOMED; + w->was_zoomed = NULL; + if (w->active != NULL && + ((~w->active->flags & PANE_FLOATOVERZOOM) || + !window_pane_is_floating(w->active))) + wp = w->active; + if (wp == NULL || !window_has_pane(w, wp)) + wp = w->active; + if (wp != NULL) + return (window_zoom(wp) == 0); + } return (0); } @@ -1068,8 +1138,8 @@ window_lost_pane(struct window *w, struct window_pane *wp) server_clear_marked(); if (wp == w->modal_last) w->modal_last = NULL; - if (w->modal_last == NULL) - w->flags &= ~WINDOW_WASMODALZOOMED; + if (wp == w->was_zoomed) + w->was_zoomed = NULL; window_pane_stack_remove(&w->last_panes, wp); if (wp == w->active) { @@ -1096,7 +1166,6 @@ window_lost_pane(struct window *w, struct window_pane *wp) } } else if (wp == w->modal) { w->modal = w->modal_last = NULL; - w->flags &= ~WINDOW_WASMODALZOOMED; } redraw_invalidate_scene(w); } @@ -1104,13 +1173,9 @@ window_lost_pane(struct window *w, struct window_pane *wp) void window_remove_pane(struct window *w, struct window_pane *wp) { - int pop = (wp == w->modal); - window_lost_pane(w, wp); TAILQ_REMOVE(&w->panes, wp, entry); TAILQ_REMOVE(&w->z_index, wp, zentry); - if (pop && window_pop_modal_zoom(w)) - server_redraw_window(w); redraw_invalidate_scene(w); window_pane_destroy(wp); } @@ -1266,6 +1331,8 @@ window_pane_printable_flags(struct window_pane *wp) flags[pos++] = 'Z'; if (window_pane_is_floating(wp)) flags[pos++] = 'F'; + if (wp->flags & PANE_FLOATOVERZOOM) + flags[pos++] = 'A'; if (wp == w->modal) flags[pos++] = 'O'; flags[pos] = '\0'; @@ -1411,6 +1478,8 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp) void window_pane_scrollbar_redraw(struct window_pane *wp) { + if (!window_pane_scrollbar_visible(wp)) + return; if (window_pane_scrollbar_overlay_visible(wp)) { wp->flags |= PANE_REDRAW; return; @@ -1478,16 +1547,16 @@ window_pane_free(struct window_pane *wp) log_debug("pane %%%u freed (%d references)", wp->id, wp->references); free(wp->searchstr); - screen_free(&wp->status_screen); screen_free(&wp->base); - + free(wp->r.ranges); options_free(wp->options); free((void *)wp->cwd); free(wp->shell); cmd_free_argv(wp->argc, wp->argv); colour_palette_free(&wp->palette); style_ranges_free(&wp->border_status_line.ranges); + free(wp->border_status_line.expanded); free(wp); } @@ -1972,7 +2041,7 @@ window_pane_is_visible(struct window_pane *wp) { if (~wp->window->flags & WINDOW_ZOOMED) return (1); - return (wp == wp->window->active); + return (wp->layout_cell != NULL); } int @@ -2442,25 +2511,6 @@ window_pane_update_used_data(struct window_pane *wp, wpo->used += size; } -void -window_set_fill_character(struct window *w) -{ - const char *value; - struct utf8_data *ud; - - free(w->fill_character); - w->fill_character = NULL; - - value = options_get_string(w->options, "fill-character"); - if (*value != '\0' && utf8_isvalid(value)) { - ud = utf8_fromcstr(value); - if (ud != NULL && ud[0].width == 1) - w->fill_character = ud; - else - free(ud); - } -} - void window_pane_default_cursor(struct window_pane *wp) { @@ -2482,11 +2532,21 @@ window_pane_mode(struct window_pane *wp) int window_pane_show_scrollbar(struct window_pane *wp) { + struct window *w = wp->window; + struct window_mode_entry *wme; + if (SCREEN_IS_ALTERNATE(&wp->base)) return (0); - if (wp->window->sb == PANE_SCROLLBARS_ALWAYS || - wp->window->sb == PANE_SCROLLBARS_AUTOHIDE || - (wp->window->sb == PANE_SCROLLBARS_MODAL && + if ((w->flags & WINDOW_ZOOMED) && w->active != NULL) { + wme = TAILQ_FIRST(&w->active->modes); + if (wme != NULL && + (wme->mode->flags & WINDOW_MODE_HIDE_SCROLLBARS)) { + return (0); + } + } + if (w->sb == PANE_SCROLLBARS_ALWAYS || + w->sb == PANE_SCROLLBARS_AUTOHIDE || + (w->sb == PANE_SCROLLBARS_MODAL && window_pane_mode(wp) != WINDOW_PANE_NO_MODE)) return (1); return (0);