diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 72a821344..ded64b7f9 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -70,6 +70,12 @@ jobs: utf8proc \ pkg-config + - name: increase pty limit + if: runner.os == 'macOS' + run: | + sysctl kern.tty.ptmx_max + sudo sysctl -w kern.tty.ptmx_max=999 + - name: build run: | sh autogen.sh diff --git a/CHANGES b/CHANGES index 40c45e2d0..c89678f11 100644 --- a/CHANGES +++ b/CHANGES @@ -136,6 +136,8 @@ CHANGES FROM 3.7c TO 3.8 * Preserve existing Ss and Se terminfo capabilities for cursor styles and restoring the default cursor (issue 5476). +* Enable left and right margins for WezTerm and Ghostty (Michael Grant). + * Add a #{A/count:frames} modifier to show a series of frames as an animation in the status line (issue 5412 from Fernando Daciuk). @@ -231,6 +233,9 @@ CHANGES FROM 3.7c TO 3.8 * Fix choose mode filtering when more than one pane exists (issue 5326), and fix an infinite loop in customize mode when a filter does not match. +* Preserve the scroll position in tree modes when rebuilding the list + (Kim A Brandt, issue 5631). + * Fix regsub when the pattern is anchored at the start (issue 5341). * Fix redraw issues around synchronized updates, copy mode, tabs, padding, diff --git a/cfg.c b/cfg.c index 78d164363..31294f92a 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cfg.c,v 1.91 2026/08/03 13:38:42 nicm Exp $ */ +/* $OpenBSD: cfg.c,v 1.92 2026/10/02 12:28:07 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -31,14 +31,17 @@ int cfg_finished; static char **cfg_causes; static u_int cfg_ncauses; static struct cmdq_item *cfg_item; +static int cfg_started; int cfg_quiet = 1; char **cfg_files; u_int cfg_nfiles; static enum cmd_retval -cfg_client_done(__unused struct cmdq_item *item, __unused void *data) +cfg_client_done(struct cmdq_item *item, __unused void *data) { + if (cmdq_get_client(item)->flags & CLIENT_DEAD) + return (CMD_RETURN_NORMAL); if (!cfg_finished) return (CMD_RETURN_WAIT); return (CMD_RETURN_NORMAL); @@ -53,14 +56,28 @@ cfg_done(__unused struct cmdq_item *item, __unused void *data) cfg_show_causes(NULL); - if (cfg_item != NULL) + if (cfg_item != NULL) { cmdq_continue(cfg_item); + cfg_item = NULL; + } prompt_load_history(); return (CMD_RETURN_NORMAL); } +void +cfg_client_lost(struct client *c) +{ + if (c != cfg_client) + return; + cfg_client = NULL; + if (cfg_item != NULL) { + cmdq_continue(cfg_item); + cfg_item = NULL; + } +} + void start_cfg(void) { @@ -77,12 +94,19 @@ start_cfg(void) * Because start_cfg() is called so early, we can be sure the client's * command queue is currently empty and our callback will be at the * front - we need to get in before MSG_COMMAND. + * + * If the initial client is lost before the configuration finishes, the + * next client to identify is first in the list and ends up here. Block + * it instead, but do not load the files again. */ cfg_client = c = TAILQ_FIRST(&clients); if (c != NULL) { cfg_item = cmdq_get_callback(cfg_client_done, NULL); cmdq_append(c, cfg_item); } + if (cfg_started) + return; + cfg_started = 1; if (cfg_quiet) flags = CMD_PARSE_QUIET; diff --git a/client.c b/client.c index 97f0538ad..21697277c 100644 --- a/client.c +++ b/client.c @@ -283,7 +283,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, #ifdef HAVE_SYSTEMD if (systemd_activated()) { /* socket-based activation, do not even try to be a client. */ - fd = server_start(client_proc, flags, base, 0, NULL); + fd = server_start(client_proc, flags, base, -1, NULL); } else #endif fd = client_connect(base, socket_path, client_flags); diff --git a/cmd-attach-session.c b/cmd-attach-session.c index 050c7ca38..923229eab 100644 --- a/cmd-attach-session.c +++ b/cmd-attach-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-attach-session.c,v 1.91 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-attach-session.c,v 1.93 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -35,6 +35,7 @@ static enum cmd_retval cmd_attach_session_exec(struct cmd *, const struct cmd_entry cmd_attach_session_entry = { .name = "attach-session", + .description = "Attach to an existing session.", .alias = "attach", .args = { "c:dEf:rt:x", 0, 0, NULL }, @@ -91,8 +92,10 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag, wp = target.wp; if (wl != NULL) { - if (wp != NULL) + if (wp != NULL) { + window_redraw_active_switch(wp->window, wp); window_set_active_pane(wp->window, wp, 1); + } session_set_current(s, wl); if (wp != NULL) cmd_find_from_winlink_pane(current, wl, wp, 0); diff --git a/cmd-bind-key.c b/cmd-bind-key.c index 0d7636ff7..6d4d21a8c 100644 --- a/cmd-bind-key.c +++ b/cmd-bind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-bind-key.c,v 1.47 2025/04/09 07:03:04 nicm Exp $ */ +/* $OpenBSD: cmd-bind-key.c,v 1.48 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -34,6 +34,7 @@ static enum cmd_retval cmd_bind_key_exec(struct cmd *, const struct cmd_entry cmd_bind_key_entry = { .name = "bind-key", + .description = "Bind a key to one or more commands.", .alias = "bind", .args = { "nrN:T:", 1, -1, cmd_bind_key_args_parse }, diff --git a/cmd-break-pane.c b/cmd-break-pane.c index f2f1aff25..357d1c6c1 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.76 2026/08/17 07:04:45 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.77 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_break_pane_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_break_pane_entry = { .name = "break-pane", + .description = "Move a pane into its own window or make it floating.", .alias = "breakp", .args = { "abdPF:n:s:t:Wx:X:y:Y:", 0, 0, NULL }, diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index 6c78e9cd8..85ff5c2e5 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-capture-pane.c,v 1.69 2026/09/08 07:31:59 nicm Exp $ */ +/* $OpenBSD: cmd-capture-pane.c,v 1.70 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Jonathan Alvarado @@ -40,6 +40,7 @@ static char *cmd_capture_pane_hyperlinks(struct grid *, struct screen *, const struct cmd_entry cmd_capture_pane_entry = { .name = "capture-pane", + .description = "Capture pane content.", .alias = "capturep", .args = { "ab:CeE:FHIJLMNpPqRS:Tt:", 0, 0, NULL }, @@ -54,6 +55,7 @@ const struct cmd_entry cmd_capture_pane_entry = { const struct cmd_entry cmd_clear_history_entry = { .name = "clear-history", + .description = "Clear a pane's history.", .alias = "clearhist", .args = { "Ht:", 0, 0, NULL }, diff --git a/cmd-choose-tree.c b/cmd-choose-tree.c index 2dc304ff2..62df6ac6c 100644 --- a/cmd-choose-tree.c +++ b/cmd-choose-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-choose-tree.c,v 1.58 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-choose-tree.c,v 1.60 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2012 Thomas Adam @@ -31,6 +31,7 @@ static enum cmd_retval cmd_choose_tree_exec(struct cmd *, const struct cmd_entry cmd_choose_tree_entry = { .name = "choose-tree", + .description = "Browse sessions and windows interactively.", .alias = NULL, .args = { "F:f:GhK:kNO:rst:wyZ", 0, 1, cmd_choose_tree_args_parse }, @@ -45,6 +46,7 @@ const struct cmd_entry cmd_choose_tree_entry = { const struct cmd_entry cmd_choose_client_entry = { .name = "choose-client", + .description = "Choose a client interactively.", .alias = NULL, .args = { "F:f:hiK:kNO:rt:yZ", 0, 1, cmd_choose_tree_args_parse }, @@ -59,6 +61,7 @@ const struct cmd_entry cmd_choose_client_entry = { const struct cmd_entry cmd_choose_buffer_entry = { .name = "choose-buffer", + .description = "Choose a paste buffer interactively.", .alias = NULL, .args = { "F:f:K:kNO:rt:yZ", 0, 1, cmd_choose_tree_args_parse }, @@ -73,6 +76,7 @@ const struct cmd_entry cmd_choose_buffer_entry = { const struct cmd_entry cmd_customize_mode_entry = { .name = "customize-mode", + .description = "Browse and edit options and key bindings.", .alias = NULL, .args = { "F:f:kNt:yZ", 0, 0, NULL }, @@ -86,10 +90,12 @@ const struct cmd_entry cmd_customize_mode_entry = { const struct cmd_entry cmd_switch_mode_entry = { .name = "switch-mode", + .description = "Switch the active pane mode.", .alias = NULL, - .args = { "F:kst:wZ", 0, 1, cmd_choose_tree_args_parse }, - .usage = "[-kswZ] [-F format] " CMD_TARGET_PANE_USAGE " [command]", + .args = { "F:kO:rst:wZ", 0, 1, cmd_choose_tree_args_parse }, + .usage = "[-krsZw] [-F format] [-O sort-order] " CMD_TARGET_PANE_USAGE + " [command]", .target = { 't', CMD_FIND_PANE, 0 }, @@ -99,6 +105,7 @@ const struct cmd_entry cmd_switch_mode_entry = { const struct cmd_entry cmd_display_panes_entry = { .name = "display-panes", + .description = "Display pane numbers.", .alias = "displayp", .args = { "d:kNs:t:Z", 0, 1, cmd_choose_tree_args_parse }, diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index 1d135b834..1c487d818 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-command-prompt.c,v 1.76 2026/08/25 06:04:33 nicm Exp $ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.77 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -40,6 +40,7 @@ static void cmd_command_prompt_free(void *); const struct cmd_entry cmd_command_prompt_entry = { .name = "command-prompt", + .description = "Open an interactive command prompt.", .alias = NULL, .args = { "1CbeFiklI:NPp:t:T:", 0, 1, cmd_command_prompt_args_parse }, diff --git a/cmd-confirm-before.c b/cmd-confirm-before.c index fda3af772..f462d27a1 100644 --- a/cmd-confirm-before.c +++ b/cmd-confirm-before.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-confirm-before.c,v 1.61 2026/06/24 10:55:39 nicm Exp $ */ +/* $OpenBSD: cmd-confirm-before.c,v 1.62 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -39,6 +39,7 @@ static void cmd_confirm_before_free(void *); const struct cmd_entry cmd_confirm_before_entry = { .name = "confirm-before", + .description = "Ask for confirmation before a command.", .alias = "confirm", .args = { "bc:p:t:y", 1, 1, cmd_confirm_before_args_parse }, diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index e6888ca7e..fd40f2ca4 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-copy-mode.c,v 1.54 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-copy-mode.c,v 1.56 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -28,6 +28,7 @@ static enum cmd_retval cmd_copy_mode_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_copy_mode_entry = { .name = "copy-mode", + .description = "Enter copy mode.", .alias = NULL, .args = { "dekHMqSs:t:u", 0, 0, NULL }, @@ -42,6 +43,7 @@ const struct cmd_entry cmd_copy_mode_entry = { const struct cmd_entry cmd_clock_mode_entry = { .name = "clock-mode", + .description = "Show a large clock in a pane.", .alias = NULL, .args = { "t:", 0, 0, NULL }, @@ -103,6 +105,8 @@ cmd_copy_mode_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'd')) window_copy_pagedown(wp, 0, args_has(args, 'e')); if (args_has(args, 'S')) { + if (c == NULL) + return (CMD_RETURN_NORMAL); tty_window_offset(&c->tty, &tty_ox, &tty_oy, &tty_sx, &tty_sy); window_copy_scroll(wp, c->tty.mouse_slider_mpos, event->m.y, tty_oy, args_has(args, 'e')); diff --git a/cmd-detach-client.c b/cmd-detach-client.c index 82ce9a00b..fe69b4bd8 100644 --- a/cmd-detach-client.c +++ b/cmd-detach-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-detach-client.c,v 1.39 2026/05/22 15:22:43 nicm Exp $ */ +/* $OpenBSD: cmd-detach-client.c,v 1.40 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_detach_client_exec(struct cmd *, const struct cmd_entry cmd_detach_client_entry = { .name = "detach-client", + .description = "Detach one or more clients.", .alias = "detach", .args = { "aE:s:t:P", 0, 0, NULL }, @@ -45,6 +46,7 @@ const struct cmd_entry cmd_detach_client_entry = { const struct cmd_entry cmd_suspend_client_entry = { .name = "suspend-client", + .description = "Suspend a client.", .alias = "suspendc", .args = { "t:", 0, 0, NULL }, diff --git a/cmd-display-menu.c b/cmd-display-menu.c index f9627ca67..98026329c 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-menu.c,v 1.55 2026/09/21 12:14:32 nicm Exp $ */ +/* $OpenBSD: cmd-display-menu.c,v 1.56 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -36,6 +36,7 @@ static enum cmd_retval cmd_display_popup_exec(struct cmd *, const struct cmd_entry cmd_display_menu_entry = { .name = "display-menu", + .description = "Display an interactive menu.", .alias = "menu", .args = { "b:c:C:H:s:S:MOt:T:x:y:", 1, -1, cmd_display_menu_args_parse }, @@ -52,6 +53,7 @@ const struct cmd_entry cmd_display_menu_entry = { const struct cmd_entry cmd_display_popup_entry = { .name = "display-popup", + .description = "Create a floating pane (compatibility command).", .alias = "popup", .args = { "Bb:Cc:d:e:Eh:ks:S:t:T:w:x:y:", 0, -1, NULL }, diff --git a/cmd-display-message.c b/cmd-display-message.c index 0e294ef50..fe6d70bc6 100644 --- a/cmd-display-message.c +++ b/cmd-display-message.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-display-message.c,v 1.66 2026/09/08 08:33:10 nicm Exp $ */ +/* $OpenBSD: cmd-display-message.c,v 1.68 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -37,6 +37,7 @@ static enum cmd_retval cmd_display_message_exec(struct cmd *, const struct cmd_entry cmd_display_message_entry = { .name = "display-message", + .description = "Display a message or expand formats.", .alias = "display", .args = { "aCc:d:jlINpt:F:v", 0, 1, NULL }, @@ -114,12 +115,10 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item) template = DISPLAY_MESSAGE_TEMPLATE; /* - * -c is intended to be the client where the message should be - * displayed if -p is not given. But it makes sense to use it for the - * formats too, assuming it matches the session. If it doesn't, use the - * best client for the session. + * -c is also used for the client formats. If it was not given, tc is + * only the current client, so use it only if it matches the session. */ - if (tc != NULL && tc->session == s) + if (tc != NULL && (args_has(args, 'c') || tc->session == s)) c = tc; else if (s != NULL) c = cmd_find_best_client(s); diff --git a/cmd-find-window.c b/cmd-find-window.c index 0091ae907..9239f89b3 100644 --- a/cmd-find-window.c +++ b/cmd-find-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-find-window.c,v 1.57 2026/07/14 17:17:17 nicm Exp $ */ +/* $OpenBSD: cmd-find-window.c,v 1.58 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -30,6 +30,7 @@ static enum cmd_retval cmd_find_window_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_find_window_entry = { .name = "find-window", + .description = "Find windows by content or name.", .alias = "findw", .args = { "CiNrt:TZ", 1, 1, NULL }, diff --git a/cmd-if-shell.c b/cmd-if-shell.c index 59d34f9d6..cafbde877 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-if-shell.c,v 1.87 2026/08/25 06:04:33 nicm Exp $ */ +/* $OpenBSD: cmd-if-shell.c,v 1.88 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -40,6 +40,7 @@ static void cmd_if_shell_free(void *); const struct cmd_entry cmd_if_shell_entry = { .name = "if-shell", + .description = "Run a command conditionally.", .alias = "if", .args = { "bFt:", 2, 3, cmd_if_shell_args_parse }, diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 44d79cafb..4d06cebd3 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.75 2026/09/21 10:33:16 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.78 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -36,6 +36,7 @@ static void cmd_join_pane_mouse_move(struct client *, const struct cmd_entry cmd_join_pane_entry = { .name = "join-pane", + .description = "Move a pane into another window.", .alias = "joinp", .args = { "bdfhvp:l:s:t:", 0, 0, NULL }, @@ -50,6 +51,7 @@ const struct cmd_entry cmd_join_pane_entry = { const struct cmd_entry cmd_move_pane_entry = { .name = "move-pane", + .description = "Move a pane or reposition a floating pane.", .alias = "movep", .args = { "bdD::fhMvl:L::P:R::s:t:U::X:Y:z:", 0, 0, NULL }, @@ -307,7 +309,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) struct window *w; struct window_pane *wp; struct layout_cell *lc; - int y, ly, x, lx; + int y, ly, x, lx, oxoff, oyoff, osx, osy; wp = cmd_mouse_pane(m, NULL, &wl); if (wp == NULL) { @@ -329,10 +331,16 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) ly = m->statusat - 1; if (x != lx || y != ly) { + oxoff = wp->xoff; + oyoff = wp->yoff; + osx = wp->sx; + osy = wp->sy; + lc->g.xoff += x - lx; lc->g.yoff += y - ly; layout_fix_panes(w, NULL); - server_redraw_window(w); + + window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy); server_redraw_window_borders(w); } } @@ -497,7 +505,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) layout_close_pane(src_wp); server_client_remove_pane(src_wp); - window_lost_pane(src_w, src_wp); + if (src_w != dst_w) + window_lost_pane(src_w, src_wp); TAILQ_REMOVE(&src_w->panes, src_wp, entry); TAILQ_REMOVE(&src_w->z_index, src_wp, zentry); diff --git a/cmd-kill-pane.c b/cmd-kill-pane.c index f634a7cd7..bd842cbc3 100644 --- a/cmd-kill-pane.c +++ b/cmd-kill-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-kill-pane.c,v 1.34 2026/06/09 21:22:22 nicm Exp $ */ +/* $OpenBSD: cmd-kill-pane.c,v 1.35 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -34,6 +34,7 @@ static int cmd_kill_pane_filter(struct cmdq_item *, const struct cmd_entry cmd_kill_pane_entry = { .name = "kill-pane", + .description = "Destroy one or more panes.", .alias = "killp", .args = { "af:t:", 0, 0, NULL }, diff --git a/cmd-kill-server.c b/cmd-kill-server.c index 8b70be3d8..d0d94e782 100644 --- a/cmd-kill-server.c +++ b/cmd-kill-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-kill-server.c,v 1.20 2021/08/21 10:22:39 nicm Exp $ */ +/* $OpenBSD: cmd-kill-server.c,v 1.21 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_kill_server_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_kill_server_entry = { .name = "kill-server", + .description = "Stop the tmux server.", .alias = NULL, .args = { "", 0, 0, NULL }, @@ -42,6 +43,7 @@ const struct cmd_entry cmd_kill_server_entry = { const struct cmd_entry cmd_start_server_entry = { .name = "start-server", + .description = "Start the tmux server.", .alias = "start", .args = { "", 0, 0, NULL }, diff --git a/cmd-kill-session.c b/cmd-kill-session.c index 7edd2a30a..031066c8a 100644 --- a/cmd-kill-session.c +++ b/cmd-kill-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-kill-session.c,v 1.31 2026/06/09 12:57:40 nicm Exp $ */ +/* $OpenBSD: cmd-kill-session.c,v 1.32 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -36,6 +36,7 @@ static int cmd_kill_session_filter(struct cmdq_item *, const struct cmd_entry cmd_kill_session_entry = { .name = "kill-session", + .description = "Destroy one or more sessions.", .alias = NULL, .args = { "aCgf:t:", 0, 0, NULL }, diff --git a/cmd-kill-window.c b/cmd-kill-window.c index e7a6ec32e..242c7b4f1 100644 --- a/cmd-kill-window.c +++ b/cmd-kill-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-kill-window.c,v 1.30 2026/06/09 12:57:40 nicm Exp $ */ +/* $OpenBSD: cmd-kill-window.c,v 1.31 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -33,6 +33,7 @@ static int cmd_kill_window_filter(struct cmdq_item *, const struct cmd_entry cmd_kill_window_entry = { .name = "kill-window", + .description = "Destroy one or more windows.", .alias = "killw", .args = { "af:t:", 0, 0, NULL }, @@ -46,6 +47,7 @@ const struct cmd_entry cmd_kill_window_entry = { const struct cmd_entry cmd_unlink_window_entry = { .name = "unlink-window", + .description = "Unlink a window from a session.", .alias = "unlinkw", .args = { "kt:", 0, 0, NULL }, diff --git a/cmd-list-buffers.c b/cmd-list-buffers.c index 82ee58947..cbb7a2429 100644 --- a/cmd-list-buffers.c +++ b/cmd-list-buffers.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-buffers.c,v 1.41 2026/02/27 08:25:12 nicm Exp $ */ +/* $OpenBSD: cmd-list-buffers.c,v 1.42 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -34,6 +34,7 @@ static enum cmd_retval cmd_list_buffers_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_list_buffers_entry = { .name = "list-buffers", + .description = "List paste buffers.", .alias = "lsb", .args = { "F:f:O:r", 0, 0, NULL }, diff --git a/cmd-list-clients.c b/cmd-list-clients.c index 77f3912ca..aef752d58 100644 --- a/cmd-list-clients.c +++ b/cmd-list-clients.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-clients.c,v 1.42 2026/02/27 08:25:12 nicm Exp $ */ +/* $OpenBSD: cmd-list-clients.c,v 1.43 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -39,6 +39,7 @@ static enum cmd_retval cmd_list_clients_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_list_clients_entry = { .name = "list-clients", + .description = "List connected clients.", .alias = "lsc", .args = { "F:f:O:rt:", 0, 0, NULL }, diff --git a/cmd-list-commands.c b/cmd-list-commands.c index 5e9c4430c..b68386013 100644 --- a/cmd-list-commands.c +++ b/cmd-list-commands.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-commands.c,v 1.15 2026/02/24 08:22:13 nicm Exp $ */ +/* $OpenBSD: cmd-list-commands.c,v 1.16 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,20 +31,43 @@ "#{command_list_name}" \ "#{?command_list_alias, (#{command_list_alias}),} " \ "#{command_list_usage}" +#define LIST_COMMANDS_HELP_TEMPLATE \ + "#{p|#{command_list_name_width}:command_list_name} " \ + "#{command_list_description}" +#define LIST_COMMANDS_VERBOSE_TEMPLATE \ + LIST_COMMANDS_TEMPLATE "\n #{command_list_description}" static enum cmd_retval cmd_list_commands(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_list_commands_entry = { .name = "list-commands", + .description = "List commands or show command syntax.", .alias = "lscm", - .args = { "F:", 0, 1, NULL }, - .usage = "[-F format] [command]", + .args = { "F:h", 0, 1, NULL }, + .usage = "[-h] [-F format] [command]", .flags = CMD_STARTSERVER|CMD_AFTERHOOK, .exec = cmd_list_commands }; +static u_int +cmd_list_commands_get_width(const struct cmd_entry *only) +{ + const struct cmd_entry **entryp; + u_int width, largest = 0; + + if (only != NULL) + return (utf8_cstrwidth(only->name)); + + for (entryp = cmd_table; *entryp != NULL; entryp++) { + width = utf8_cstrwidth((*entryp)->name); + if (width > largest) + largest = width; + } + return (largest); +} + static void cmd_list_single_command(const struct cmd_entry *entry, struct format_tree *ft, const char *template, struct cmdq_item *item) @@ -63,6 +86,11 @@ cmd_list_single_command(const struct cmd_entry *entry, struct format_tree *ft, else s = ""; format_add(ft, "command_list_usage", "%s", s); + if (entry->description != NULL) + s = entry->description; + else + s = ""; + format_add(ft, "command_list_description", "%s", s); line = format_expand(ft, template); if (*line != '\0') @@ -75,33 +103,44 @@ cmd_list_commands(struct cmd *self, struct cmdq_item *item) { struct args *args = cmd_get_args(self); const struct cmd_entry **entryp; - const struct cmd_entry *entry; + const struct cmd_entry *entry = NULL; struct format_tree *ft; const char *template, *command; char *cause; + int help; + u_int width; - if ((template = args_get(args, 'F')) == NULL) - template = LIST_COMMANDS_TEMPLATE; - - ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); - format_defaults(ft, NULL, NULL, NULL, NULL); + if ((template = args_get(args, 'F')) == NULL) { + help = args_has(args, 'h'); + if (help > 1) + template = LIST_COMMANDS_VERBOSE_TEMPLATE; + else if (help == 1) + template = LIST_COMMANDS_HELP_TEMPLATE; + else + template = LIST_COMMANDS_TEMPLATE; + } command = args_string(args, 0); - if (command == NULL) { - for (entryp = cmd_table; *entryp != NULL; entryp++) - cmd_list_single_command(*entryp, ft, template, item); - } else { + if (command != NULL) { entry = cmd_find(command, &cause); - if (entry != NULL) - cmd_list_single_command(entry, ft, template, item); - else { + if (entry == NULL) { cmdq_error(item, "%s", cause); free(cause); - format_free(ft); return (CMD_RETURN_ERROR); } } + ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); + format_defaults(ft, NULL, NULL, NULL, NULL); + width = cmd_list_commands_get_width(entry); + format_add(ft, "command_list_name_width", "%u", width); + + if (entry == NULL) { + for (entryp = cmd_table; *entryp != NULL; entryp++) + cmd_list_single_command(*entryp, ft, template, item); + } else + cmd_list_single_command(entry, ft, template, item); + format_free(ft); return (CMD_RETURN_NORMAL); } diff --git a/cmd-list-keys.c b/cmd-list-keys.c index e8617c2e3..ccaa4a0f9 100644 --- a/cmd-list-keys.c +++ b/cmd-list-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-keys.c,v 1.78 2026/07/01 13:12:17 nicm Exp $ */ +/* $OpenBSD: cmd-list-keys.c,v 1.79 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -42,6 +42,7 @@ static enum cmd_retval cmd_list_keys_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_list_keys_entry = { .name = "list-keys", + .description = "List key bindings.", .alias = "lsk", .args = { "1aF:NO:P:rT:", 0, 1, NULL }, diff --git a/cmd-list-panes.c b/cmd-list-panes.c index 5bc996c2f..412679c4d 100644 --- a/cmd-list-panes.c +++ b/cmd-list-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-panes.c,v 1.41 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: cmd-list-panes.c,v 1.42 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -36,6 +36,7 @@ static void cmd_list_panes_window(struct cmd *, struct session *, const struct cmd_entry cmd_list_panes_entry = { .name = "list-panes", + .description = "List panes and their state.", .alias = "lsp", .args = { "aF:f:O:rst:", 0, 0, NULL }, diff --git a/cmd-list-sessions.c b/cmd-list-sessions.c index 30f2178ad..87e7106ed 100644 --- a/cmd-list-sessions.c +++ b/cmd-list-sessions.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-sessions.c,v 1.37 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: cmd-list-sessions.c,v 1.38 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -40,6 +40,7 @@ static enum cmd_retval cmd_list_sessions_exec(struct cmd *, const struct cmd_entry cmd_list_sessions_entry = { .name = "list-sessions", + .description = "List sessions and their state.", .alias = "ls", .args = { "F:f:O:r", 0, 0, NULL }, diff --git a/cmd-list-windows.c b/cmd-list-windows.c index 2cbccc1b8..1f49a1f17 100644 --- a/cmd-list-windows.c +++ b/cmd-list-windows.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-list-windows.c,v 1.51 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: cmd-list-windows.c,v 1.52 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -43,6 +43,7 @@ static enum cmd_retval cmd_list_windows_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_list_windows_entry = { .name = "list-windows", + .description = "List windows and their state.", .alias = "lsw", .args = { "aF:f:O:rt:", 0, 0, NULL }, diff --git a/cmd-load-buffer.c b/cmd-load-buffer.c index df35b82c1..96cc4e62e 100644 --- a/cmd-load-buffer.c +++ b/cmd-load-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-load-buffer.c,v 1.66 2025/10/28 07:32:26 nicm Exp $ */ +/* $OpenBSD: cmd-load-buffer.c,v 1.68 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -35,6 +35,7 @@ static enum cmd_retval cmd_load_buffer_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_load_buffer_entry = { .name = "load-buffer", + .description = "Load a file into a paste buffer.", .alias = "loadb", .args = { "b:t:w", 1, 1, NULL }, @@ -78,9 +79,9 @@ cmd_load_buffer_done(__unused struct client *c, const char *path, int error, tc->session != NULL && (~tc->flags & CLIENT_DEAD)) tty_set_selection(&tc->tty, "", copy, bsize); - if (tc != NULL) - server_client_unref(tc); } + if (tc != NULL) + server_client_unref(tc); cmdq_continue(item); free(cdata->name); diff --git a/cmd-lock-server.c b/cmd-lock-server.c index 1feac03f8..ce7c69078 100644 --- a/cmd-lock-server.c +++ b/cmd-lock-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-lock-server.c,v 1.31 2021/08/21 10:22:39 nicm Exp $ */ +/* $OpenBSD: cmd-lock-server.c,v 1.32 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -28,6 +28,7 @@ static enum cmd_retval cmd_lock_server_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_lock_server_entry = { .name = "lock-server", + .description = "Lock every client.", .alias = "lock", .args = { "", 0, 0, NULL }, @@ -39,6 +40,7 @@ const struct cmd_entry cmd_lock_server_entry = { const struct cmd_entry cmd_lock_session_entry = { .name = "lock-session", + .description = "Lock every client attached to a session.", .alias = "locks", .args = { "t:", 0, 0, NULL }, @@ -52,6 +54,7 @@ const struct cmd_entry cmd_lock_session_entry = { const struct cmd_entry cmd_lock_client_entry = { .name = "lock-client", + .description = "Lock a client.", .alias = "lockc", .args = { "t:", 0, 0, NULL }, diff --git a/cmd-move-window.c b/cmd-move-window.c index 1cd4f10a8..a8f1383c9 100644 --- a/cmd-move-window.c +++ b/cmd-move-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-move-window.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ +/* $OpenBSD: cmd-move-window.c,v 1.35 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -30,6 +30,7 @@ static enum cmd_retval cmd_move_window_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_move_window_entry = { .name = "move-window", + .description = "Move a window between sessions.", .alias = "movew", .args = { "abdkrs:t:", 0, 0, NULL }, @@ -44,6 +45,7 @@ const struct cmd_entry cmd_move_window_entry = { const struct cmd_entry cmd_link_window_entry = { .name = "link-window", + .description = "Link a window into another session.", .alias = "linkw", .args = { "abdks:t:", 0, 0, NULL }, diff --git a/cmd-new-session.c b/cmd-new-session.c index 56d4f9779..7f7643bfe 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-new-session.c,v 1.152 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-new-session.c,v 1.153 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -37,6 +37,7 @@ static enum cmd_retval cmd_new_session_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_new_session_entry = { .name = "new-session", + .description = "Create a new session.", .alias = "new", .args = { "Ac:dDe:EF:f:n:Ps:t:x:Xy:", 0, -1, NULL }, @@ -53,6 +54,7 @@ const struct cmd_entry cmd_new_session_entry = { const struct cmd_entry cmd_has_session_entry = { .name = "has-session", + .description = "Check whether a session exists.", .alias = "has", .args = { "t:", 0, 0, NULL }, diff --git a/cmd-new-window.c b/cmd-new-window.c index 6ec55fe64..60000a67d 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-new-window.c,v 1.104 2026/09/03 21:04:11 nicm Exp $ */ +/* $OpenBSD: cmd-new-window.c,v 1.105 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -36,6 +36,7 @@ static enum cmd_retval cmd_new_window_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_new_window_entry = { .name = "new-window", + .description = "Create a window.", .alias = "neww", .args = { "abc:de:EF:kn:PSt:", 0, -1, NULL }, diff --git a/cmd-parse.y b/cmd-parse.y index a6541c930..95159e86c 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-parse.y,v 1.60 2026/09/08 10:20:08 nicm Exp $ */ +/* $OpenBSD: cmd-parse.y,v 1.63 2026/10/08 06:44:16 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -41,6 +41,7 @@ static char *yylex_format(void); struct cmd_parse_scope { int flag; + int taken; TAILQ_ENTRY (cmd_parse_scope) entry; }; @@ -68,6 +69,22 @@ struct cmd_parse_command { }; TAILQ_HEAD(cmd_parse_commands, cmd_parse_command); +enum cmd_parse_value_type { + CMD_PARSE_VALUE_STRING, + CMD_PARSE_VALUE_ARGUMENT, + CMD_PARSE_VALUE_ARGUMENTS, + CMD_PARSE_VALUE_COMMAND, + CMD_PARSE_VALUE_COMMANDS +}; + +struct cmd_parse_owned_value { + enum cmd_parse_value_type type; + void *value; + + TAILQ_ENTRY(cmd_parse_owned_value) entry; +}; +TAILQ_HEAD(cmd_parse_owned_values, cmd_parse_owned_value); + struct cmd_parse_state { FILE *f; @@ -86,10 +103,16 @@ struct cmd_parse_state { struct cmd_parse_scope *scope; TAILQ_HEAD(, cmd_parse_scope) stack; + + struct cmd_parse_owned_values values; }; static struct cmd_parse_state parse_state; static char *cmd_parse_get_error(const char *, u_int, const char *); +static void *cmd_parse_own(enum cmd_parse_value_type, void *); +static struct cmd_parse_commands *cmd_parse_own_commands(void); +static void *cmd_parse_take(void *); +static void cmd_parse_drop(void *); static void cmd_parse_free_command(struct cmd_parse_command *); static struct cmd_parse_commands *cmd_parse_new_commands(void); static void cmd_parse_free_commands(struct cmd_parse_commands *); @@ -138,7 +161,7 @@ lines : /* empty */ { struct cmd_parse_state *ps = &parse_state; - ps->commands = $1; + ps->commands = cmd_parse_take($1); } statements : statement '\n' @@ -149,18 +172,16 @@ statements : statement '\n' { $$ = $1; TAILQ_CONCAT($$, $2, entry); - free($2); + cmd_parse_drop($2); } statement : /* empty */ { - $$ = xmalloc (sizeof *$$); - TAILQ_INIT($$); + $$ = cmd_parse_own_commands(); } | hidden_assignment { - $$ = xmalloc (sizeof *$$); - TAILQ_INIT($$); + $$ = cmd_parse_own_commands(); } | condition { @@ -169,8 +190,8 @@ statement : /* empty */ if (ps->scope == NULL || ps->scope->flag) $$ = $1; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($1); } } | commands @@ -180,8 +201,8 @@ statement : /* empty */ if (ps->scope == NULL || ps->scope->flag) $$ = $1; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($1); } } @@ -203,6 +224,7 @@ expanded : format struct cmd_find_state *fsp; struct cmd_find_state fs; int flags = FORMAT_NOJOBS; + char *expanded; if (cmd_find_valid_state(&pi->fs)) fsp = &pi->fs; @@ -213,9 +235,10 @@ expanded : format ft = format_create(c, pi->item, FORMAT_NONE, flags); format_defaults(ft, c, fsp->s, fsp->wl, fsp->wp); - $$ = format_expand(ft, $1); + expanded = format_expand(ft, $1); + $$ = cmd_parse_own(CMD_PARSE_VALUE_STRING, expanded); format_free(ft); - free($1); + cmd_parse_drop($1); } optional_assignment : /* empty */ @@ -240,7 +263,7 @@ assignment : EQUALS } if ((~flags & CMD_PARSE_PARSEONLY) && flag) environ_put(global_environ, $1, 0); - free($1); + cmd_parse_drop($1); } hidden_assignment : HIDDEN EQUALS @@ -262,7 +285,7 @@ hidden_assignment : HIDDEN EQUALS } if ((~flags & CMD_PARSE_PARSEONLY) && flag) environ_put(global_environ, $2, ENVIRON_HIDDEN); - free($2); + cmd_parse_drop($2); } if_open : IF expanded @@ -272,7 +295,8 @@ if_open : IF expanded scope = xmalloc(sizeof *scope); $$ = scope->flag = format_true($2); - free($2); + scope->taken = scope->flag; + cmd_parse_drop($2); if (ps->scope != NULL) TAILQ_INSERT_HEAD(&ps->stack, ps->scope, entry); @@ -285,7 +309,8 @@ if_else : ELSE struct cmd_parse_scope *scope; scope = xmalloc(sizeof *scope); - scope->flag = !ps->scope->flag; + scope->flag = !ps->scope->taken; + scope->taken = 1; free(ps->scope); ps->scope = scope; @@ -297,8 +322,10 @@ if_elif : ELIF expanded struct cmd_parse_scope *scope; scope = xmalloc(sizeof *scope); - $$ = scope->flag = format_true($2); - free($2); + scope->flag = !ps->scope->taken && format_true($2); + scope->taken = ps->scope->taken || scope->flag; + $$ = scope->flag; + cmd_parse_drop($2); free(ps->scope); ps->scope = scope; @@ -319,48 +346,48 @@ condition : if_open '\n' statements if_close if ($1) $$ = $3; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($3); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($3); } } | if_open '\n' statements if_else '\n' statements if_close { if ($1) { $$ = $3; - cmd_parse_free_commands($6); + cmd_parse_drop($6); } else { $$ = $6; - cmd_parse_free_commands($3); + cmd_parse_drop($3); } } | if_open '\n' statements elif if_close { if ($1) { $$ = $3; - cmd_parse_free_commands($4.commands); + cmd_parse_drop($4.commands); } else if ($4.flag) { $$ = $4.commands; - cmd_parse_free_commands($3); + cmd_parse_drop($3); } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($3); + cmd_parse_drop($4.commands); } } | if_open '\n' statements elif if_else '\n' statements if_close { if ($1) { $$ = $3; - cmd_parse_free_commands($4.commands); - cmd_parse_free_commands($7); + cmd_parse_drop($4.commands); + cmd_parse_drop($7); } else if ($4.flag) { $$ = $4.commands; - cmd_parse_free_commands($3); - cmd_parse_free_commands($7); + cmd_parse_drop($3); + cmd_parse_drop($7); } else { $$ = $7; - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); + cmd_parse_drop($3); + cmd_parse_drop($4.commands); } } @@ -371,8 +398,8 @@ elif : if_elif '\n' statements $$.commands = $3; } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($3); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($3); } } | if_elif '\n' statements elif @@ -380,16 +407,16 @@ elif : if_elif '\n' statements if ($1) { $$.flag = 1; $$.commands = $3; - cmd_parse_free_commands($4.commands); + cmd_parse_drop($4.commands); } else if ($4.flag) { $$.flag = 1; $$.commands = $4.commands; - cmd_parse_free_commands($3); + cmd_parse_drop($3); } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($3); - cmd_parse_free_commands($4.commands); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($3); + cmd_parse_drop($4.commands); } } @@ -397,12 +424,13 @@ commands : command { struct cmd_parse_state *ps = &parse_state; - $$ = cmd_parse_new_commands(); + $$ = cmd_parse_own_commands(); if (!TAILQ_EMPTY(&$1->arguments) && - (ps->scope == NULL || ps->scope->flag)) + (ps->scope == NULL || ps->scope->flag)) { + cmd_parse_take($1); TAILQ_INSERT_TAIL($$, $1, entry); - else - cmd_parse_free_command($1); + } else + cmd_parse_drop($1); } | commands ';' { @@ -412,21 +440,19 @@ commands : command { $$ = $1; TAILQ_CONCAT($$, $3, entry); - free($3); + cmd_parse_drop($3); } | commands ';' command { struct cmd_parse_state *ps = &parse_state; + $$ = $1; if (!TAILQ_EMPTY(&$3->arguments) && (ps->scope == NULL || ps->scope->flag)) { - $$ = $1; + cmd_parse_take($3); TAILQ_INSERT_TAIL($$, $3, entry); - } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($1); - cmd_parse_free_command($3); - } + } else + cmd_parse_drop($3); } | condition1 { @@ -440,6 +466,7 @@ command : assignment $$ = xcalloc(1, sizeof *$$); $$->line = ps->input->line; TAILQ_INIT(&$$->arguments); + cmd_parse_own(CMD_PARSE_VALUE_COMMAND, $$); } | optional_assignment TOKEN { @@ -452,8 +479,9 @@ command : assignment arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; - arg->string = $2; + arg->string = cmd_parse_take($2); TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); + cmd_parse_own(CMD_PARSE_VALUE_COMMAND, $$); } | optional_assignment TOKEN arguments { @@ -465,12 +493,13 @@ command : assignment TAILQ_INIT(&$$->arguments); TAILQ_CONCAT(&$$->arguments, $3, entry); - free($3); + cmd_parse_drop($3); arg = xcalloc(1, sizeof *arg); arg->type = CMD_PARSE_STRING; - arg->string = $2; + arg->string = cmd_parse_take($2); TAILQ_INSERT_HEAD(&$$->arguments, arg, entry); + cmd_parse_own(CMD_PARSE_VALUE_COMMAND, $$); } condition1 : if_open commands if_close @@ -478,48 +507,48 @@ condition1 : if_open commands if_close if ($1) $$ = $2; else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($2); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($2); } } | if_open commands if_else commands if_close { if ($1) { $$ = $2; - cmd_parse_free_commands($4); + cmd_parse_drop($4); } else { $$ = $4; - cmd_parse_free_commands($2); + cmd_parse_drop($2); } } | if_open commands elif1 if_close { if ($1) { $$ = $2; - cmd_parse_free_commands($3.commands); + cmd_parse_drop($3.commands); } else if ($3.flag) { $$ = $3.commands; - cmd_parse_free_commands($2); + cmd_parse_drop($2); } else { - $$ = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); + $$ = cmd_parse_own_commands(); + cmd_parse_drop($2); + cmd_parse_drop($3.commands); } } | if_open commands elif1 if_else commands if_close { if ($1) { $$ = $2; - cmd_parse_free_commands($3.commands); - cmd_parse_free_commands($5); + cmd_parse_drop($3.commands); + cmd_parse_drop($5); } else if ($3.flag) { $$ = $3.commands; - cmd_parse_free_commands($2); - cmd_parse_free_commands($5); + cmd_parse_drop($2); + cmd_parse_drop($5); } else { $$ = $5; - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); + cmd_parse_drop($2); + cmd_parse_drop($3.commands); } } @@ -530,8 +559,8 @@ elif1 : if_elif commands $$.commands = $2; } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($2); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($2); } } | if_elif commands elif1 @@ -539,16 +568,16 @@ elif1 : if_elif commands if ($1) { $$.flag = 1; $$.commands = $2; - cmd_parse_free_commands($3.commands); + cmd_parse_drop($3.commands); } else if ($3.flag) { $$.flag = 1; $$.commands = $3.commands; - cmd_parse_free_commands($2); + cmd_parse_drop($2); } else { $$.flag = 0; - $$.commands = cmd_parse_new_commands(); - cmd_parse_free_commands($2); - cmd_parse_free_commands($3.commands); + $$.commands = cmd_parse_own_commands(); + cmd_parse_drop($2); + cmd_parse_drop($3.commands); } } @@ -557,10 +586,13 @@ arguments : argument $$ = xcalloc(1, sizeof *$$); TAILQ_INIT($$); + cmd_parse_take($1); TAILQ_INSERT_HEAD($$, $1, entry); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENTS, $$); } | argument arguments { + cmd_parse_take($1); TAILQ_INSERT_HEAD($2, $1, entry); $$ = $2; } @@ -569,19 +601,22 @@ argument : TOKEN { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_STRING; - $$->string = $1; + $$->string = cmd_parse_take($1); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENT, $$); } | EQUALS { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_STRING; - $$->string = $1; + $$->string = cmd_parse_take($1); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENT, $$); } | '{' argument_statements { $$ = xcalloc(1, sizeof *$$); $$->type = CMD_PARSE_COMMANDS; - $$->commands = $2; + $$->commands = cmd_parse_take($2); + cmd_parse_own(CMD_PARSE_VALUE_ARGUMENT, $$); } argument_statements : statement '}' @@ -592,7 +627,7 @@ argument_statements : statement '}' { $$ = $1; TAILQ_CONCAT($$, $2, entry); - free($2); + cmd_parse_drop($2); } %% @@ -681,22 +716,116 @@ cmd_parse_free_commands(struct cmd_parse_commands *cmds) free(cmds); } +static void * +cmd_parse_own(enum cmd_parse_value_type type, void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + v = xmalloc(sizeof *v); + v->type = type; + v->value = value; + TAILQ_INSERT_TAIL(&ps->values, v, entry); + return (value); +} + +static struct cmd_parse_commands * +cmd_parse_own_commands(void) +{ + struct cmd_parse_commands *cmds; + + cmds = cmd_parse_new_commands(); + return (cmd_parse_own(CMD_PARSE_VALUE_COMMANDS, cmds)); +} + +static struct cmd_parse_owned_value * +cmd_parse_find_value(void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + TAILQ_FOREACH_REVERSE(v, &ps->values, cmd_parse_owned_values, entry) { + if (v->value == value) + return (v); + } + fatalx("parser value not owned"); +} + +static void * +cmd_parse_take(void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + v = cmd_parse_find_value(value); + TAILQ_REMOVE(&ps->values, v, entry); + free(v); + return (value); +} + +static void +cmd_parse_free_value(enum cmd_parse_value_type type, void *value) +{ + switch (type) { + case CMD_PARSE_VALUE_STRING: + free(value); + break; + case CMD_PARSE_VALUE_ARGUMENT: + cmd_parse_free_argument(value); + break; + case CMD_PARSE_VALUE_ARGUMENTS: + cmd_parse_free_arguments(value); + free(value); + break; + case CMD_PARSE_VALUE_COMMAND: + cmd_parse_free_command(value); + break; + case CMD_PARSE_VALUE_COMMANDS: + cmd_parse_free_commands(value); + break; + } +} + +static void +cmd_parse_drop(void *value) +{ + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_owned_value *v; + + v = cmd_parse_find_value(value); + TAILQ_REMOVE(&ps->values, v, entry); + cmd_parse_free_value(v->type, v->value); + free(v); +} + static struct cmd_parse_commands * cmd_parse_run_parser(char **cause) { - struct cmd_parse_state *ps = &parse_state; - struct cmd_parse_scope *scope, *scope1; - int retval; + struct cmd_parse_state *ps = &parse_state; + struct cmd_parse_scope *scope, *scope1; + struct cmd_parse_owned_value *v, *v1; + int retval; ps->commands = NULL; TAILQ_INIT(&ps->stack); + TAILQ_INIT(&ps->values); retval = yyparse(); TAILQ_FOREACH_SAFE(scope, &ps->stack, entry, scope1) { TAILQ_REMOVE(&ps->stack, scope, entry); free(scope); } + free(ps->scope); + ps->scope = NULL; + + TAILQ_FOREACH_SAFE(v, &ps->values, entry, v1) { + TAILQ_REMOVE(&ps->values, v, entry); + cmd_parse_free_value(v->type, v->value); + free(v); + } if (retval != 0) { + if (ps->commands != NULL) + cmd_parse_free_commands(ps->commands); *cause = ps->error; return (NULL); } @@ -1327,9 +1456,10 @@ yylex(void) */ next = yylex_getc(); if (condition && next == '{') { - yylval.token = yylex_format(); - if (yylval.token == NULL) + token = yylval.token = yylex_format(); + if (token == NULL) return (ERROR); + cmd_parse_own(CMD_PARSE_VALUE_STRING, token); return (FORMAT); } while (next != '\n' && next != EOF) @@ -1346,35 +1476,37 @@ yylex(void) * % is a condition unless it is all % or all numbers, * then it is a token. */ - yylval.token = yylex_get_word('%'); - for (cp = yylval.token; *cp != '\0'; cp++) { + token = yylval.token = yylex_get_word('%'); + for (cp = token; *cp != '\0'; cp++) { if (*cp != '%' && !isdigit((u_char)*cp)) break; } - if (*cp == '\0') + if (*cp == '\0') { + cmd_parse_own(CMD_PARSE_VALUE_STRING, token); return (TOKEN); + } ps->condition = 1; - if (strcmp(yylval.token, "%hidden") == 0) { - free(yylval.token); + if (strcmp(token, "%hidden") == 0) { + free(token); return (HIDDEN); } - if (strcmp(yylval.token, "%if") == 0) { - free(yylval.token); + if (strcmp(token, "%if") == 0) { + free(token); return (IF); } - if (strcmp(yylval.token, "%else") == 0) { - free(yylval.token); + if (strcmp(token, "%else") == 0) { + free(token); return (ELSE); } - if (strcmp(yylval.token, "%elif") == 0) { - free(yylval.token); + if (strcmp(token, "%elif") == 0) { + free(token); return (ELIF); } - if (strcmp(yylval.token, "%endif") == 0) { - free(yylval.token); + if (strcmp(token, "%endif") == 0) { + free(token); return (ENDIF); } - free(yylval.token); + free(token); return (ERROR); } @@ -1384,7 +1516,7 @@ yylex(void) token = yylex_token(ch); if (token == NULL) return (ERROR); - yylval.token = token; + yylval.token = cmd_parse_own(CMD_PARSE_VALUE_STRING, token); if (strchr(token, '=') != NULL && yylex_is_var(*token, 1)) { for (cp = token + 1; *cp != '='; cp++) { diff --git a/cmd-paste-buffer.c b/cmd-paste-buffer.c index feed94403..2fa0f986c 100644 --- a/cmd-paste-buffer.c +++ b/cmd-paste-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-paste-buffer.c,v 1.44 2026/03/04 07:19:32 tb Exp $ */ +/* $OpenBSD: cmd-paste-buffer.c,v 1.45 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_paste_buffer_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_paste_buffer_entry = { .name = "paste-buffer", + .description = "Paste a buffer into a pane.", .alias = "pasteb", .args = { "db:prSs:t:", 0, 0, NULL }, diff --git a/cmd-pipe-pane.c b/cmd-pipe-pane.c index 1041c2199..1272fc88e 100644 --- a/cmd-pipe-pane.c +++ b/cmd-pipe-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-pipe-pane.c,v 1.64 2026/07/27 14:25:46 nicm Exp $ */ +/* $OpenBSD: cmd-pipe-pane.c,v 1.65 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -41,6 +41,7 @@ static void cmd_pipe_pane_error_callback(struct bufferevent *, short, void *); const struct cmd_entry cmd_pipe_pane_entry = { .name = "pipe-pane", + .description = "Pipe pane input or output to a command.", .alias = "pipep", .args = { "IOot:", 0, 1, NULL }, diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 11b43bf22..cd631e914 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-refresh-client.c,v 1.55 2026/07/17 08:37:29 nicm Exp $ */ +/* $OpenBSD: cmd-refresh-client.c,v 1.56 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_refresh_client_exec(struct cmd *, const struct cmd_entry cmd_refresh_client_entry = { .name = "refresh-client", + .description = "Refresh or resize a client.", .alias = "refresh", .args = { "A:B:cC:Df:r:F:lLRSt:U", 0, 1, NULL }, diff --git a/cmd-rename-session.c b/cmd-rename-session.c index 35719c564..d11bccc12 100644 --- a/cmd-rename-session.c +++ b/cmd-rename-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-rename-session.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-rename-session.c,v 1.40 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_rename_session_exec(struct cmd *, const struct cmd_entry cmd_rename_session_entry = { .name = "rename-session", + .description = "Rename a session.", .alias = "rename", .args = { "t:", 1, 1, NULL }, diff --git a/cmd-rename-window.c b/cmd-rename-window.c index f55f8a18f..2cb7bf265 100644 --- a/cmd-rename-window.c +++ b/cmd-rename-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-rename-window.c,v 1.30 2026/06/29 18:17:28 nicm Exp $ */ +/* $OpenBSD: cmd-rename-window.c,v 1.31 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_rename_window_exec(struct cmd *, const struct cmd_entry cmd_rename_window_entry = { .name = "rename-window", + .description = "Rename a window.", .alias = "renamew", .args = { "t:", 1, 1, NULL }, diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index c644dd783..9096563f9 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-resize-pane.c,v 1.69 2026/09/21 10:33:16 nicm Exp $ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.71 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -39,6 +39,7 @@ static void cmd_resize_pane_mouse_resize_tiled(struct client *, const struct cmd_entry cmd_resize_pane_entry = { .name = "resize-pane", + .description = "Resize or zoom a pane.", .alias = "resizep", .args = { "D::L::MR::Tt:U::x:y:Z", 0, 1, NULL }, @@ -237,8 +238,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, struct window_pane *wp; struct layout_cell *lc; int y, ly, x, lx, sx, sy, new_sx, new_sy; - int left, right; - int new_xoff, new_yoff, resizes = 0; + int left, right, resizes = 0; + int new_xoff, new_yoff, old_xoff, old_yoff; wp = cmd_mouse_pane(m, NULL, &wl); if (wp == NULL) { @@ -249,6 +250,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, lc = wp->layout_cell; sx = wp->sx; sy = wp->sy; + old_xoff = wp->xoff; + old_yoff = wp->yoff; left = wp->xoff - 1; right = wp->xoff + sx; if (window_pane_scrollbar_reserve(wp) && @@ -348,7 +351,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, } if (resizes != 0) { layout_fix_panes(w, NULL); - server_redraw_window(w); + window_redraw_floating_pane(wp, old_xoff, old_yoff, sx, sy); server_redraw_window_borders(w); } } diff --git a/cmd-resize-window.c b/cmd-resize-window.c index a89750607..6b5b5522a 100644 --- a/cmd-resize-window.c +++ b/cmd-resize-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-resize-window.c,v 1.10 2023/06/30 13:19:32 nicm Exp $ */ +/* $OpenBSD: cmd-resize-window.c,v 1.11 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2018 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_resize_window_exec(struct cmd *, const struct cmd_entry cmd_resize_window_entry = { .name = "resize-window", + .description = "Resize a window.", .alias = "resizew", .args = { "aADLRt:Ux:y:", 0, 1, NULL }, diff --git a/cmd-respawn-pane.c b/cmd-respawn-pane.c index 66a5a0be4..bc7ff4e7c 100644 --- a/cmd-respawn-pane.c +++ b/cmd-respawn-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-respawn-pane.c,v 1.40 2026/07/03 16:09:49 nicm Exp $ */ +/* $OpenBSD: cmd-respawn-pane.c,v 1.41 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_respawn_pane_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_respawn_pane_entry = { .name = "respawn-pane", + .description = "Restart a pane's command.", .alias = "respawnp", .args = { "c:e:Ekt:", 0, -1, NULL }, diff --git a/cmd-respawn-window.c b/cmd-respawn-window.c index e10469649..246b0de0d 100644 --- a/cmd-respawn-window.c +++ b/cmd-respawn-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-respawn-window.c,v 1.51 2026/07/03 16:09:49 nicm Exp $ */ +/* $OpenBSD: cmd-respawn-window.c,v 1.52 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_respawn_window_exec(struct cmd *, const struct cmd_entry cmd_respawn_window_entry = { .name = "respawn-window", + .description = "Restart every pane in a window.", .alias = "respawnw", .args = { "c:e:Ekt:", 0, -1, NULL }, diff --git a/cmd-rotate-window.c b/cmd-rotate-window.c index d62fd0fa4..27d17ee3b 100644 --- a/cmd-rotate-window.c +++ b/cmd-rotate-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-rotate-window.c,v 1.34 2026/06/22 08:47:45 nicm Exp $ */ +/* $OpenBSD: cmd-rotate-window.c,v 1.35 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -29,6 +29,7 @@ static enum cmd_retval cmd_rotate_window_exec(struct cmd *, const struct cmd_entry cmd_rotate_window_entry = { .name = "rotate-window", + .description = "Rotate panes in a window.", .alias = "rotatew", .args = { "Dt:UZ", 0, 0, NULL }, diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 4834cd9f7..029a76155 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-run-shell.c,v 1.95 2026/09/20 07:59:55 nicm Exp $ */ +/* $OpenBSD: cmd-run-shell.c,v 1.96 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -42,6 +42,7 @@ static void cmd_run_shell_print(struct job *, const char *); const struct cmd_entry cmd_run_shell_entry = { .name = "run-shell", + .description = "Run a shell command.", .alias = "run", .args = { "bd:Ct:Es:c:", 0, -1, cmd_run_shell_args_parse }, diff --git a/cmd-save-buffer.c b/cmd-save-buffer.c index 6dd844cdd..861153a29 100644 --- a/cmd-save-buffer.c +++ b/cmd-save-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-save-buffer.c,v 1.57 2025/10/28 07:32:26 nicm Exp $ */ +/* $OpenBSD: cmd-save-buffer.c,v 1.58 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha @@ -35,6 +35,7 @@ static enum cmd_retval cmd_save_buffer_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_save_buffer_entry = { .name = "save-buffer", + .description = "Save a paste buffer to a file.", .alias = "saveb", .args = { "ab:", 1, 1, NULL }, @@ -46,6 +47,7 @@ const struct cmd_entry cmd_save_buffer_entry = { const struct cmd_entry cmd_show_buffer_entry = { .name = "show-buffer", + .description = "Print a paste buffer.", .alias = "showb", .args = { "b:", 0, 0, NULL }, diff --git a/cmd-select-layout.c b/cmd-select-layout.c index 95f60e6ca..9e9e02e5d 100644 --- a/cmd-select-layout.c +++ b/cmd-select-layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-layout.c,v 1.45 2026/09/09 07:03:39 nicm Exp $ */ +/* $OpenBSD: cmd-select-layout.c,v 1.46 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_select_layout_exec(struct cmd *, const struct cmd_entry cmd_select_layout_entry = { .name = "select-layout", + .description = "Choose or apply a layout.", .alias = "selectl", .args = { "Enopt:", 0, 1, NULL }, @@ -44,6 +45,7 @@ const struct cmd_entry cmd_select_layout_entry = { const struct cmd_entry cmd_next_layout_entry = { .name = "next-layout", + .description = "Select the next layout.", .alias = "nextl", .args = { "t:", 0, 0, NULL }, @@ -57,6 +59,7 @@ const struct cmd_entry cmd_next_layout_entry = { const struct cmd_entry cmd_previous_layout_entry = { .name = "previous-layout", + .description = "Select the previous layout.", .alias = "prevl", .args = { "t:", 0, 0, NULL }, diff --git a/cmd-select-pane.c b/cmd-select-pane.c index e23ca0adc..c51e4ea31 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: cmd-select-pane.c,v 1.79 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_select_pane_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_select_pane_entry = { .name = "select-pane", + .description = "Select or mark a pane.", .alias = "selectp", .args = { "DdegLlMmP:RT:t:UZ", 0, 0, NULL }, /* -P and -g deprecated */ @@ -44,6 +45,7 @@ const struct cmd_entry cmd_select_pane_entry = { const struct cmd_entry cmd_last_pane_entry = { .name = "last-pane", + .description = "Select the previously active pane.", .alias = "lastp", .args = { "det:Z", 0, 0, NULL }, diff --git a/cmd-select-window.c b/cmd-select-window.c index 32b4821bf..1e0c89a87 100644 --- a/cmd-select-window.c +++ b/cmd-select-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-select-window.c,v 1.30 2021/08/21 10:22:39 nicm Exp $ */ +/* $OpenBSD: cmd-select-window.c,v 1.31 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_select_window_exec(struct cmd *, const struct cmd_entry cmd_select_window_entry = { .name = "select-window", + .description = "Select a window.", .alias = "selectw", .args = { "lnpTt:", 0, 0, NULL }, @@ -44,6 +45,7 @@ const struct cmd_entry cmd_select_window_entry = { const struct cmd_entry cmd_next_window_entry = { .name = "next-window", + .description = "Select the next window.", .alias = "next", .args = { "at:", 0, 0, NULL }, @@ -57,6 +59,7 @@ const struct cmd_entry cmd_next_window_entry = { const struct cmd_entry cmd_previous_window_entry = { .name = "previous-window", + .description = "Select the previous window.", .alias = "prev", .args = { "at:", 0, 0, NULL }, @@ -70,6 +73,7 @@ const struct cmd_entry cmd_previous_window_entry = { const struct cmd_entry cmd_last_window_entry = { .name = "last-window", + .description = "Select the previously active window.", .alias = "last", .args = { "t:", 0, 0, NULL }, diff --git a/cmd-send-keys.c b/cmd-send-keys.c index 14ca26de5..0d737d696 100644 --- a/cmd-send-keys.c +++ b/cmd-send-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-send-keys.c,v 1.81 2026/06/11 19:13:34 nicm Exp $ */ +/* $OpenBSD: cmd-send-keys.c,v 1.82 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_send_keys_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_send_keys_entry = { .name = "send-keys", + .description = "Send keys to a pane or client.", .alias = "send", .args = { "c:FHKlMN:Rt:X", 0, -1, NULL }, @@ -46,6 +47,7 @@ const struct cmd_entry cmd_send_keys_entry = { const struct cmd_entry cmd_send_prefix_entry = { .name = "send-prefix", + .description = "Send the prefix key to a pane.", .alias = NULL, .args = { "2t:", 0, 0, NULL }, diff --git a/cmd-server-access.c b/cmd-server-access.c index e85a278a8..1f07c7214 100644 --- a/cmd-server-access.c +++ b/cmd-server-access.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-server-access.c,v 1.6 2026/06/09 12:58:40 nicm Exp $ */ +/* $OpenBSD: cmd-server-access.c,v 1.7 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2021 Dallas Lyons @@ -36,6 +36,7 @@ static enum cmd_retval cmd_server_access_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_server_access_entry = { .name = "server-access", + .description = "Manage access to the tmux server.", .alias = NULL, .args = { "adglrw", 0, 1, NULL }, diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index 3e540f706..e57a8ae6c 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.39 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,7 @@ static enum cmd_retval cmd_set_buffer_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_set_buffer_entry = { .name = "set-buffer", + .description = "Create or modify a paste buffer.", .alias = "setb", .args = { "ab:t:n:w", 0, 1, NULL }, @@ -43,6 +44,7 @@ const struct cmd_entry cmd_set_buffer_entry = { const struct cmd_entry cmd_delete_buffer_entry = { .name = "delete-buffer", + .description = "Delete a paste buffer.", .alias = "deleteb", .args = { "b:", 0, 0, NULL }, diff --git a/cmd-set-environment.c b/cmd-set-environment.c index 9a6552cb5..d15ccdec5 100644 --- a/cmd-set-environment.c +++ b/cmd-set-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ +/* $OpenBSD: cmd-set-environment.c,v 1.30 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_set_environment_exec(struct cmd *, const struct cmd_entry cmd_set_environment_entry = { .name = "set-environment", + .description = "Set or remove an environment variable.", .alias = "setenv", .args = { "Fhgrt:u", 1, 2, NULL }, diff --git a/cmd-set-option.c b/cmd-set-option.c index 335a54760..3db645e24 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-option.c,v 1.146 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: cmd-set-option.c,v 1.147 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -38,6 +38,7 @@ static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, const struct cmd_entry cmd_set_option_entry = { .name = "set-option", + .description = "Set a server, session, or pane option.", .alias = "set", .args = { "aFgopqst:uUw", 1, 2, cmd_set_option_args_parse }, @@ -51,6 +52,7 @@ const struct cmd_entry cmd_set_option_entry = { const struct cmd_entry cmd_set_window_option_entry = { .name = "set-window-option", + .description = "Set a window option.", .alias = "setw", .args = { "aFgoqt:u", 1, 2, cmd_set_option_args_parse }, @@ -64,6 +66,7 @@ const struct cmd_entry cmd_set_window_option_entry = { const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", + .description = "Set or remove a hook.", .alias = NULL, .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, diff --git a/cmd-show-environment.c b/cmd-show-environment.c index 692f09c32..d7757e0df 100644 --- a/cmd-show-environment.c +++ b/cmd-show-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ +/* $OpenBSD: cmd-show-environment.c,v 1.30 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -36,6 +36,7 @@ static void cmd_show_environment_print(struct cmd *, struct cmdq_item *, const struct cmd_entry cmd_show_environment_entry = { .name = "show-environment", + .description = "Show environment variables.", .alias = "showenv", .args = { "hgst:", 0, 1, NULL }, diff --git a/cmd-show-messages.c b/cmd-show-messages.c index bdb433714..1d26d3f09 100644 --- a/cmd-show-messages.c +++ b/cmd-show-messages.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-messages.c,v 1.37 2025/11/18 08:37:54 nicm Exp $ */ +/* $OpenBSD: cmd-show-messages.c,v 1.38 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -36,6 +36,7 @@ static enum cmd_retval cmd_show_messages_exec(struct cmd *, const struct cmd_entry cmd_show_messages_entry = { .name = "show-messages", + .description = "Show server and client messages.", .alias = "showmsgs", .args = { "JTt:", 0, 0, NULL }, diff --git a/cmd-show-options.c b/cmd-show-options.c index cba601ad8..45c325531 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-options.c,v 1.76 2026/07/27 19:15:58 nicm Exp $ */ +/* $OpenBSD: cmd-show-options.c,v 1.77 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,7 @@ static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *, const struct cmd_entry cmd_show_options_entry = { .name = "show-options", + .description = "Show server, session, or pane options.", .alias = "show", .args = { "AgF:Hpqst:vw", 0, 1, NULL }, @@ -62,6 +63,7 @@ const struct cmd_entry cmd_show_options_entry = { const struct cmd_entry cmd_show_window_options_entry = { .name = "show-window-options", + .description = "Show window options.", .alias = "showw", .args = { "F:gvt:", 0, 1, NULL }, @@ -75,6 +77,7 @@ const struct cmd_entry cmd_show_window_options_entry = { const struct cmd_entry cmd_show_hooks_entry = { .name = "show-hooks", + .description = "Show hooks.", .alias = NULL, .args = { "BF:gpt:w", 0, 1, NULL }, diff --git a/cmd-show-prompt-history.c b/cmd-show-prompt-history.c index 5b6af76fb..ecf1045c0 100644 --- a/cmd-show-prompt-history.c +++ b/cmd-show-prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-show-prompt-history.c,v 1.5 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: cmd-show-prompt-history.c,v 1.6 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2021 Anindya Mukherjee @@ -29,6 +29,7 @@ static enum cmd_retval cmd_show_prompt_history_exec(struct cmd *, const struct cmd_entry cmd_show_prompt_history_entry = { .name = "show-prompt-history", + .description = "Show interactive prompt history.", .alias = "showphist", .args = { "T:", 0, 0, NULL }, @@ -40,6 +41,7 @@ const struct cmd_entry cmd_show_prompt_history_entry = { const struct cmd_entry cmd_clear_prompt_history_entry = { .name = "clear-prompt-history", + .description = "Clear interactive prompt history.", .alias = "clearphist", .args = { "T:", 0, 0, NULL }, diff --git a/cmd-source-file.c b/cmd-source-file.c index 771b03663..b509d7595 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */ +/* $OpenBSD: cmd-source-file.c,v 1.64 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha @@ -37,6 +37,7 @@ static enum cmd_retval cmd_source_file_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_source_file_entry = { .name = "source-file", + .description = "Load commands from a configuration file.", .alias = "source", .args = { "t:Fnqv", 1, -1, NULL }, diff --git a/cmd-split-window.c b/cmd-split-window.c index 641792e2c..2db840b6b 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.151 2026/09/10 11:02:18 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.153 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -38,6 +38,7 @@ static void cmd_split_window_mouse_resize(struct client *, const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", + .description = "Create a floating pane.", .alias = "newp", .args = { "AbB:Cc:Dde:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, @@ -57,6 +58,7 @@ const struct cmd_entry cmd_new_pane_entry = { const struct cmd_entry cmd_split_window_entry = { .name = "split-window", + .description = "Create a new pane by splitting a window.", .alias = "splitw", .args = { "bB:c:de:EfF:hIkl:m:p:PR:s:S:t:T:vWZ", 0, -1, NULL }, @@ -362,6 +364,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) enum pane_lines lines; u_int sx, sy; int x, y, xoff, yoff, border; + int oxoff, oyoff, osx, osy; if (c->tty.mouse_last_pane == -1) return; @@ -417,8 +420,14 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) if (sy < PANE_MINIMUM) sy = PANE_MINIMUM; + oxoff = wp->xoff; + oyoff = wp->yoff; + osx = wp->sx; + osy = wp->sy; + layout_set_size(lc, sx, sy, xoff, yoff); layout_fix_panes(w, NULL); - server_redraw_window(w); + + window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy); server_redraw_window_borders(w); } diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index 04e1cc7a9..76d2928f2 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-swap-pane.c,v 1.55 2026/07/15 13:02:33 nicm Exp $ */ +/* $OpenBSD: cmd-swap-pane.c,v 1.57 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -30,6 +30,7 @@ static enum cmd_retval cmd_swap_pane_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_swap_pane_entry = { .name = "swap-pane", + .description = "Swap two panes.", .alias = "swapp", .args = { "dDs:t:UZ", 0, 0, NULL }, @@ -88,7 +89,7 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'D')) { if (window_pane_is_floating(dst_wp)) { cmdq_error(item, "cannot swap down on floating pane"); - return (CMD_RETURN_ERROR); + goto error; } src_w = dst_w; src_wp = TAILQ_NEXT(dst_wp, entry); @@ -100,7 +101,7 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) } else if (args_has(args, 'U')) { if (window_pane_is_floating(dst_wp)) { cmdq_error(item, "cannot swap up on floating pane"); - return (CMD_RETURN_ERROR); + goto error; } src_w = dst_w; src_wp = TAILQ_PREV(dst_wp, window_panes, entry); @@ -202,4 +203,9 @@ out: if (src_w != dst_w && window_pop_zoom(dst_w)) server_redraw_window(dst_w); return (CMD_RETURN_NORMAL); + +error: + if (window_pop_zoom(dst_w)) + server_redraw_window(dst_w); + return (CMD_RETURN_ERROR); } diff --git a/cmd-swap-window.c b/cmd-swap-window.c index 0141e6a57..f5f5bdd16 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-swap-window.c,v 1.30 2026/07/27 08:03:01 nicm Exp $ */ +/* $OpenBSD: cmd-swap-window.c,v 1.31 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -30,6 +30,7 @@ static enum cmd_retval cmd_swap_window_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_swap_window_entry = { .name = "swap-window", + .description = "Swap two windows.", .alias = "swapw", .args = { "ds:t:", 0, 0, NULL }, diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 44ea43cbc..19a04cf39 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */ +/* $OpenBSD: cmd-switch-client.c,v 1.76 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -33,6 +33,7 @@ static enum cmd_retval cmd_switch_client_exec(struct cmd *, const struct cmd_entry cmd_switch_client_entry = { .name = "switch-client", + .description = "Switch a client to another session.", .alias = "switchc", .args = { "c:EFlnO:pt:rT:Z", 0, 0, NULL }, diff --git a/cmd-unbind-key.c b/cmd-unbind-key.c index f537fb576..63663c6c7 100644 --- a/cmd-unbind-key.c +++ b/cmd-unbind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-unbind-key.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ +/* $OpenBSD: cmd-unbind-key.c,v 1.35 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -30,6 +30,7 @@ static enum cmd_retval cmd_unbind_key_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_unbind_key_entry = { .name = "unbind-key", + .description = "Remove key bindings.", .alias = "unbind", .args = { "anqT:", 0, 1, NULL }, diff --git a/cmd-wait-for.c b/cmd-wait-for.c index 7326c7519..77c4bd6eb 100644 --- a/cmd-wait-for.c +++ b/cmd-wait-for.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-wait-for.c,v 1.24 2026/09/22 06:46:50 nicm Exp $ */ +/* $OpenBSD: cmd-wait-for.c,v 1.25 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -32,6 +32,7 @@ static enum cmd_retval cmd_wait_for_exec(struct cmd *, struct cmdq_item *); const struct cmd_entry cmd_wait_for_entry = { .name = "wait-for", + .description = "Wait for or signal named channels and events.", .alias = "wait", .args = { "EF:LSUlvw:", 1, 1, NULL }, diff --git a/format.c b/format.c index e3af653fc..a0deedf1f 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.421 2026/09/28 16:52:55 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.425 2026/10/05 10:59:37 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -88,9 +88,12 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) /* Maximum pad and trim width. */ #define FORMAT_MAX_WIDTH 10000 -/* Maximum repeat size. */ +/* Maximum repeat count. */ #define FORMAT_MAX_REPEAT 10000 +/* Maximum repeat result size in bytes. */ +#define FORMAT_MAX_REPEAT_SIZE 65536 + /* Maximum precision. */ #define FORMAT_MAX_PRECISION 100 @@ -1676,7 +1679,7 @@ format_cb_client_last_session(struct format_tree *ft) static void * format_cb_client_name(struct format_tree *ft) { - if (ft->c != NULL) + if (ft->c != NULL && ft->c->name != NULL) return (xstrdup(ft->c->name)); return (NULL); } @@ -1739,7 +1742,7 @@ format_cb_client_termfeatures(struct format_tree *ft) static void * format_cb_client_termname(struct format_tree *ft) { - if (ft->c != NULL) + if (ft->c != NULL && ft->c->term_name != NULL) return (xstrdup(ft->c->term_name)); return (NULL); } @@ -1760,7 +1763,7 @@ format_cb_client_termtype(struct format_tree *ft) static void * format_cb_client_tty(struct format_tree *ft) { - if (ft->c != NULL) + if (ft->c != NULL && ft->c->ttyname != NULL) return (xstrdup(ft->c->ttyname)); return (NULL); } @@ -4565,8 +4568,8 @@ format_pretty_time(time_t t, int seconds) now = t; age = now - t; - localtime_r(&now, &now_tm); - localtime_r(&t, &tm); + if (localtime_r(&now, &now_tm) == NULL || localtime_r(&t, &tm) == NULL) + return (xstrdup("")); /* Last 24 hours. */ if (age < 24 * 3600) { @@ -4737,10 +4740,12 @@ found: found = format_pretty_time(t, 0); else { if (time_format != NULL) { - localtime_r(&t, &tm); + if (localtime_r(&t, &tm) == NULL) + return (NULL); format_strftime(s, sizeof s, time_format, &tm); } else { - ctime_r(&t, s); + if (ctime_r(&t, s) == NULL) + return (NULL); s[strcspn(s, "\n")] = '\0'; } found = xstrdup(s); @@ -4871,6 +4876,9 @@ format_skip1(struct format_expand_state *es, const char *s, const char *end) int brackets = 0; u_int check = 0; + if (es != NULL && !format_check_time(es, NULL)) + return (NULL); + for (; *s != '\0'; s++) { if (es != NULL && !format_check_time(es, &check)) return (NULL); @@ -6472,7 +6480,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, value = xstrdup(""); else { n = strlen(left); - if (n != 0 && nrep > (SIZE_MAX - 1) / n) { + if (n != 0 && nrep > FORMAT_MAX_REPEAT_SIZE / n) { format_log(es, "repeat is too long: %s", copy); value = xstrdup(""); } else { diff --git a/grid-reader.c b/grid-reader.c index 4cf47b8cb..b192e7a7b 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid-reader.c,v 1.11 2026/07/29 17:42:56 nicm Exp $ */ +/* $OpenBSD: grid-reader.c,v 1.12 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee @@ -436,3 +436,208 @@ grid_reader_cursor_back_to_indentation(struct grid_reader *gr) gr->cx = oldx; gr->cy = oldy; } + +/* Get the end of the last used line. */ +static void +grid_reader_output_end(struct grid *gd, u_int *x, u_int *y) +{ + u_int last = gd->hsize + gd->sy - 1; + + while (last > 0 && grid_get_line(gd, last)->cellused == 0) + last--; + *x = grid_get_line(gd, last)->cellused; + *y = last; +} + +/* Find the most recent complete output at or before the cursor. */ +static int +grid_reader_previous_output_range(struct grid_reader *gr, u_int *sx, + u_int *sy, u_int *ex, u_int *ey) +{ + struct grid *gd = gr->gd; + struct grid_line *gl; + struct osc133_data *od; + u_int cursor_x = gr->cx, cursor_y = gr->cy; + u_int start_x, start_y, y, total; + int found = 0, have_prompt = 0; + int pending = 0; + int has_start, has_end, end_first; + int cleared; + + total = gd->hsize + gd->sy; + + /* + * Scan from the top because markers arrive in their natural order, C + * then D. A backward scan would meet D first and need extra state to + * find its C. The last complete output found is the one wanted. + */ + for (y = 0; y < total && y <= cursor_y; y++) { + gl = grid_get_line(gd, y); + od = &gl->osc133_data; + + /* On the cursor's line, ignore markers after the cursor. */ + has_start = has_end = end_first = 0; + if (gl->flags & GRID_LINE_START_OUTPUT) { + if (y != cursor_y || od->out_start_col <= cursor_x) + has_start = 1; + } + if (gl->flags & GRID_LINE_END_OUTPUT) { + if (y != cursor_y || od->out_end_col <= cursor_x) + has_end = 1; + } + + /* An end before the start ends the previous output. */ + if (has_start && has_end && od->out_end_col < od->out_start_col) + end_first = 1; + + /* A C marker starts an output that is pending until its D. */ + if (has_start && !end_first) { + start_x = od->out_start_col; + start_y = y; + pending = 1; + } + + /* The output may have cleared its C marker from the screen. */ + if (!pending && !have_prompt && + (gl->flags & GRID_LINE_END_OUTPUT)) { + cleared = 1; + if ((gl->flags & GRID_LINE_START_PROMPT) && + od->out_end_col > od->prompt_col) + cleared = 0; + if (cleared) { + start_x = start_y = 0; + pending = 1; + } + } + + /* A D marker completes the pending output. */ + if (pending && has_end) { + *sx = start_x; + *sy = start_y; + *ex = od->out_end_col; + *ey = y; + found = 1; + pending = 0; + } + if (has_start && end_first) { + start_x = od->out_start_col; + start_y = y; + pending = 1; + } + + /* A prompt drops an unfinished output. */ + if (gl->flags & GRID_LINE_START_PROMPT) { + if (!has_start || od->out_start_col < od->prompt_col) + pending = 0; + have_prompt = 1; + } + } + return (found); +} + +/* + * Find the output range for the command at the cursor, from its C marker to + * its D marker. If the prompt has no output yet, use the previous output. + * Returns 0 if there is no usable range. + */ +int +grid_reader_output_range(struct grid_reader *gr, u_int *sx, u_int *sy, + u_int *ex, u_int *ey) +{ + struct grid *gd = gr->gd; + struct grid_line *gl; + struct osc133_data *od; + u_int cursor_x = gr->cx, cursor_y = gr->cy; + u_int prompt_x = 0, prompt_y = UINT_MAX; + u_int y, total; + int found_start = 0, found_end = 0; + int next_prompt, in_range, end_ok; + int found; + + /* Find the last prompt at or before the cursor. */ + total = gd->hsize + gd->sy; + for (y = 0; y < total; y++) { + gl = grid_get_line(gd, y); + od = &gl->osc133_data; + if (~gl->flags & GRID_LINE_START_PROMPT) + continue; + if (y > cursor_y) + break; + if (y == cursor_y && od->prompt_col > cursor_x) + break; + prompt_y = y; + prompt_x = od->prompt_col; + } + + /* With no prompt, its A marker may have left history, so start at 0. */ + if (prompt_y == UINT_MAX) + y = 0; + else + y = prompt_y; + + /* Walk down from the prompt to its C and D, up to the next prompt. */ + for (; y < total; y++) { + gl = grid_get_line(gd, y); + od = &gl->osc133_data; + next_prompt = 0; + if (y != prompt_y && (gl->flags & GRID_LINE_START_PROMPT)) + next_prompt = 1; + + /* Output before the next prompt on its line is ours. */ + if (y == prompt_y) + in_range = (od->out_start_col >= prompt_x); + else if (next_prompt) + in_range = (od->out_start_col < od->prompt_col); + else + in_range = 1; + if (gl->flags & GRID_LINE_START_OUTPUT && in_range) { + *sx = od->out_start_col; + *sy = y; + found_start = 1; + } + + /* Both A and C may have left history while D remains. */ + if (!found_start && prompt_y == UINT_MAX && + (gl->flags & GRID_LINE_END_OUTPUT)) { + if (!next_prompt || od->out_end_col <= od->prompt_col) { + *sx = *sy = 0; + found_start = 1; + } + } + + /* An output may end on the same line or the next prompt's. */ + if (found_start && (gl->flags & GRID_LINE_END_OUTPUT)) { + end_ok = 1; + if (y == prompt_y && od->out_end_col < prompt_x) + end_ok = 0; + if (y == *sy && od->out_end_col < *sx) + end_ok = 0; + if (end_ok) { + *ex = od->out_end_col; + *ey = y; + found_end = 1; + break; + } + } + if (next_prompt) + break; + } + + /* The cursor is on a prompt with no output yet. */ + if (!found_start) { + found = grid_reader_previous_output_range(gr, sx, sy, ex, ey); + return (found); + } + + /* + * Without a D marker the command is still running, so its output runs + * to the last used line. If the next prompt came first, it has no + * usable end. + */ + if (!found_end) { + if (y != total) + return (0); + grid_reader_output_end(gd, ex, ey); + } + return (1); +} diff --git a/grid.c b/grid.c index 424f5bea3..28a82cb2c 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */ +/* $OpenBSD: grid.c,v 1.159 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1347,12 +1347,60 @@ grid_reflow_move(struct grid *gd, struct grid_line *from) return (to); } +/* Move the OSC 133 markers from one line to another. */ +static void +grid_reflow_move_osc133(struct grid_line *to, struct grid_line *from, + u_int to_col, u_int count, int last) +{ + struct osc133_data *src = &from->osc133_data; + struct osc133_data *dst = &to->osc133_data; + u_short *src_col[] = { + &src->prompt_col, + &src->cmd_col, + &src->out_start_col, + &src->out_end_col + }; + u_short *dst_col[] = { + &dst->prompt_col, + &dst->cmd_col, + &dst->out_start_col, + &dst->out_end_col + }; + u_int flags[] = { + GRID_LINE_START_PROMPT|GRID_LINE_SECOND_PROMPT, + GRID_LINE_START_COMMAND, + GRID_LINE_START_OUTPUT, + GRID_LINE_END_OUTPUT + }; + u_int i, flag; + + if ((from->flags & GRID_LINE_OSC133_FLAGS) == 0) + return; + for (i = 0; i < nitems(flags); i++) { + flag = from->flags & flags[i]; + if (flag == 0) + continue; + if (!last && *src_col[i] >= count) { + *src_col[i] -= count; + continue; + } + *dst_col[i] = to_col + *src_col[i]; + to->flags |= flag; + from->flags &= ~flag; + *src_col[i] = 0; + if (flag == GRID_LINE_END_OUTPUT) { + dst->exit_status = src->exit_status; + src->exit_status = 0; + } + } +} + /* Join line below onto this one. */ static void grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, u_int width, int already) { - struct grid_line *gl, *from = NULL; + struct grid_line *gl, *from = NULL, *next; struct grid_cell gc; u_int lines, left, i, to, line, want = 0; u_int at; @@ -1383,13 +1431,20 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, break; line = yy + 1 + lines; - /* If the next line is empty, skip it. */ + /* Consume empty lines, including any markers. */ if (~gd->linedata[line].flags & GRID_LINE_WRAPPED) wrapped = 0; if (gd->linedata[line].cellused == 0) { + next = &gd->linedata[line]; + if (!wrapped && + (next->flags & GRID_LINE_OSC133_FLAGS) == 0) + break; + from = next; + want = 0; + grid_reflow_move_osc133(gl, from, at, 0, 1); + lines++; if (!wrapped) break; - lines++; continue; } @@ -1416,6 +1471,8 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy, grid_set_cell(target, at, to, &gc); at++; } + grid_reflow_move_osc133(gl, from, at - want, want, + want == from->cellused); lines++; /* @@ -1486,6 +1543,14 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, line = target->sy + 1; first = grid_reflow_add(target, lines); + /* The first line keeps only the markers within its first at cells. */ + memcpy(first, gl, sizeof *first); + first->cellsize = first->cellused = at; + first->flags &= ~GRID_LINE_OSC133_FLAGS; + first->flags |= GRID_LINE_WRAPPED; + memset(&first->osc133_data, 0, sizeof first->osc133_data); + grid_reflow_move_osc133(first, gl, 0, at, 0); + /* Copy sections from the original line. */ width = 0; xx = 0; @@ -1493,6 +1558,8 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, grid_get_cell1(gl, i, &gc); if (width + gc.data.width > sx) { target->linedata[line].flags |= GRID_LINE_WRAPPED; + grid_reflow_move_osc133(&target->linedata[line], gl, 0, + xx, 0); line++; width = 0; @@ -1504,11 +1571,7 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, } if (flags & GRID_LINE_WRAPPED) target->linedata[line].flags |= GRID_LINE_WRAPPED; - - /* Move the remainder of the original line. */ - gl->cellsize = gl->cellused = at; - gl->flags |= GRID_LINE_WRAPPED; - memcpy(first, gl, sizeof *first); + grid_reflow_move_osc133(&target->linedata[line], gl, 0, xx, 1); grid_reflow_dead(gl); /* Adjust the scroll position. */ diff --git a/input.c b/input.c index ededa8f84..140f03388 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.272 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.273 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3313,7 +3313,6 @@ input_osc_133(struct input_ctx *ictx, const char *p) case 'A': case 'N': if (gl != NULL) { - memset(&gl->osc133_data, 0, sizeof gl->osc133_data); gl->osc133_data.prompt_col = s->cx; gl->flags |= GRID_LINE_START_PROMPT; } diff --git a/key-bindings.c b/key-bindings.c index 786017407..d8ba0cceb 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.193 2026/09/21 20:45:48 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.194 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -53,6 +53,7 @@ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ " ''" \ " '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \ + " '#{?#{==:#{pane_mode},copy-mode},Edit,}' 'e' {if -F '#{selection_present}' {send -X open-selection} {send -X open-output}}" \ " '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \ " ''" \ " '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \ @@ -575,6 +576,7 @@ key_bindings_init(void) "bind -Tcopy-mode C-l { send -X recentre-top-bottom }", "bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }", "bind -Tcopy-mode C-n { send -X cursor-down }", + "bind -Tcopy-mode C-o { send -X select-output }", "bind -Tcopy-mode C-p { send -X cursor-up }", "bind -Tcopy-mode C-r { command-prompt -P -T search -ip'(search up)' -I'#{pane_search_string}' { send -X search-backward-incremental -- '%%' } }", "bind -Tcopy-mode C-s { command-prompt -P -T search -ip'(search down)' -I'#{pane_search_string}' { send -X search-forward-incremental -- '%%' } }", @@ -592,6 +594,7 @@ key_bindings_init(void) "bind -Tcopy-mode R { send -X rectangle-toggle }", "bind -Tcopy-mode T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }", "bind -Tcopy-mode X { send -X set-mark }", + "bind -Tcopy-mode e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }", "bind -Tcopy-mode f { command-prompt -P -1p'(jump forward)' { send -X jump-forward -- '%%' } }", "bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode n { send -X search-again }", @@ -687,6 +690,8 @@ key_bindings_init(void) "bind -Tcopy-mode-vi K { send -X scroll-up }", "bind -Tcopy-mode-vi L { send -X bottom-line }", "bind -Tcopy-mode-vi M { send -X middle-line }", + "bind -Tcopy-mode-vi M-e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }", + "bind -Tcopy-mode-vi M-o { send -X select-output }", "bind -Tcopy-mode-vi N { send -X search-reverse }", "bind -Tcopy-mode-vi P { send -X toggle-position }", "bind -Tcopy-mode-vi T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }", diff --git a/layout.c b/layout.c index 74c3c29bb..92dad4946 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.101 2026/09/20 08:42:46 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.102 2026/10/02 12:48:52 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -469,7 +469,6 @@ layout_fix_panes(struct window *w, struct window_pane *skip) sx = PANE_MINIMUM; else sx = sx - sb_w - sb_pad; - wp->flags |= PANE_REDRAWSCROLLBAR; } window_pane_resize(wp, sx, sy); @@ -477,8 +476,11 @@ layout_fix_panes(struct window *w, struct window_pane *skip) if (wp->xoff != old_xoff || wp->yoff != old_yoff || wp->sx != old_sx || - wp->sy != old_sy) + wp->sy != old_sy) { + if (window_pane_scrollbar_reserve(wp)) + wp->flags |= PANE_REDRAWSCROLLBAR; changed = 1; + } } if (changed) redraw_invalidate_scene(w); diff --git a/options-table.c b/options-table.c index d2a7b8270..09d1dabf3 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.247 2026/09/21 10:22:31 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.252 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -183,7 +183,8 @@ static const char *options_table_copy_mode_line_numbers_list[] = { "#[pop-default]" \ "#[norange default]" #define OPTIONS_TABLE_STATUS_FORMAT2 \ - "#[align=left]#{R: ,#{n:#{session_name}}}P: " \ + "#[align=left]" \ + "#{R: ,#{e|-:#{w;T;=/#{status-left-length}:status-left},3}}P: " \ "#[norange default]" \ "#[list=on align=#{status-justify}]" \ "#[list=left-marker]<#[list=right-marker]>#[list=on]" \ @@ -194,7 +195,8 @@ static const char *options_table_copy_mode_line_numbers_list[] = { "#[push-default]" \ "#{T:window-pane-status-format}" \ "#[pop-default]" \ - "#[norange list=on default] " \ + "#[norange list=on default]" \ + "#{?loop_last_flag,,#{E:window-status-separator}}" \ "," \ "#[range=pane|#{pane_id} list=focus " \ "#{?#{!=:#{E:pane-status-current-style},default}," \ @@ -205,10 +207,12 @@ static const char *options_table_copy_mode_line_numbers_list[] = { "#[push-default]" \ "#{T:window-pane-current-status-format}" \ "#[pop-default]" \ - "#[norange list=on default] " \ + "#[norange list=on default]" \ + "#{?loop_last_flag,,#{E:window-status-separator}}" \ "}" #define OPTIONS_TABLE_STATUS_FORMAT3 \ - "#[align=left]#{R: ,#{n:#{session_name}}}S: " \ + "#[align=left]" \ + "#{R: ,#{e|-:#{w;T;=/#{status-left-length}:status-left},3}}S: " \ "#[norange default]" \ "#[list=on align=#{status-justify}]" \ "#[list=left-marker]<#[list=right-marker]>#[list=on]" \ @@ -217,7 +221,7 @@ static const char *options_table_copy_mode_line_numbers_list[] = { "#{E:session-status-style}" \ "]" \ "#[push-default]" \ - "#S#{session_alert}" \ + "#S#{q/h:session_alert}" \ "#[pop-default]" \ "#[norange list=on default] " \ "," \ @@ -228,7 +232,7 @@ static const char *options_table_copy_mode_line_numbers_list[] = { "}" \ "]" \ "#[push-default]" \ - "#S*#{session_alert}" \ + "#S*#{q/h:session_alert}" \ "#[pop-default]" \ "#[norange list=on default] " \ "}" @@ -325,7 +329,8 @@ const struct options_table_entry options_table[] = { "server-info=show-messages -JT," "info=show-messages -JT," "choose-window=choose-tree -w," - "choose-session=choose-tree -s", + "choose-session=choose-tree -s," + "help=list-commands -h", .separator = ",", .text = "Array of command aliases. " "Each entry is an alias and a command separated by '='." @@ -739,6 +744,15 @@ const struct options_table_entry options_table[] = { "'User0', 'User1' and so on." }, +#ifdef HAVE_UTEMPTER + { .name = "utmp", + .type = OPTIONS_TABLE_FLAG, + .scope = OPTIONS_TABLE_SERVER, + .default_num = 1, + .text = "Whether to maintain utmp records." + }, +#endif + { .name = "variation-selector-always-wide", .type = OPTIONS_TABLE_FLAG, .scope = OPTIONS_TABLE_SERVER, @@ -1410,8 +1424,12 @@ const struct options_table_entry options_table[] = { "#{t/p:top_line_time}#{?#{e|>:#{top_line_time},0}, ,}" "[#{copy_position}/#{copy_position_limit}]" "#{?search_timed_out, (timed out)," - "#{?search_count, (#{search_count}" - "#{?search_count_partial,+,} results),}}", + "#{?search_count_present, (" + "#{?#{==:#{search_count},0},0 matches," + "#{?search_count_partial,#{search_count}+ matches," + "#{?#{==:#{search_count},1},1 of 1 match," + "#{?search_count_current,#{search_count_current} of " + "#{search_count} matches,#{search_count} matches}}}}),}}", .text = "Format of the position indicator in copy mode." }, @@ -1773,6 +1791,14 @@ const struct options_table_entry options_table[] = { .text = "Default style of the active pane." }, + { .name = "window-default-command", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW, + .default_str = "", + .text = "Default command to run in new panes. If set, " + "overrides 'default-command'." + }, + { .name = "window-pane-current-status-format", .type = OPTIONS_TABLE_STRING, .scope = OPTIONS_TABLE_WINDOW, diff --git a/options.c b/options.c index 4e5d57eaa..843d067cb 100644 --- a/options.c +++ b/options.c @@ -1410,6 +1410,14 @@ options_push_changes(const char *name) tty_keys_build(&loop->tty); } } + if (strcmp(name, "utmp") == 0) { + RB_FOREACH(wp, window_pane_tree, &all_window_panes) { + if (options_get_number(global_options, name)) + window_pane_utmp_add(wp); + else + window_pane_utmp_remove(wp); + } + } if (strcmp(name, "status") == 0 || strcmp(name, "status-interval") == 0) status_timer_start_all(); diff --git a/proc.c b/proc.c index bfe1fbe26..de9e8264a 100644 --- a/proc.c +++ b/proc.c @@ -32,6 +32,7 @@ #endif #ifdef HAVE_JEMALLOC +#define JEMALLOC_MANGLE #include #endif diff --git a/regress/alternate-screen-resize.sh b/regress/alternate-screen-resize.sh new file mode 100644 index 000000000..c9016336a --- /dev/null +++ b/regress/alternate-screen-resize.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# A pending pane resize must still reach the pty when the application enters +# the alternate screen before the resize timer fires. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest$$ -f/dev/null" + +TMP=$(mktemp) +TMP2=$(mktemp) +trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 + +$TMUX new -d -x 80 -y 24 \ + "read x; printf '\033[?1049h'; read x; stty size >$TMP; sleep 100" \ + $TMP2 +cmp -s "$TMP" "$TMP2" || exit 1 + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/attach-session-redraw.sh b/regress/attach-session-redraw.sh new file mode 100755 index 000000000..985461499 --- /dev/null +++ b/regress/attach-session-redraw.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +# Selecting a pane with attach-session from an already attached client must +# redraw pane contents when window-style and window-active-style differ. + +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 +INNER="$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lattach-redraw-outer-$$ -f/dev/null" +BEFORE=$DIR/before +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 + +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" +} + +LEFT=$($INNER new-session -dPF '#{pane_id}' -s inner -x 40 -y 8 \ + "printf 'LEFT'; 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 default-terminal screen || exit 1 +$INNER split-window -h -t "$LEFT" "printf 'RIGHT'; exec sleep 100" || exit 1 +$INNER set-option -w -t "$LEFT" window-style bg=red || exit 1 +$INNER set-option -w -t "$LEFT" window-active-style bg=blue || exit 1 +$INNER bind-key -n x attach-session -t "$LEFT" || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +sleep 1 +$OUTER send-keys -t outer:0.0 x || exit 1 +sleep 1 +[ "$($INNER display-message -p -t inner '#{pane_id}')" = "$LEFT" ] || + fail "attach-session did not select the target pane" +$OUTER capture-pane -pe -t outer:0.0 >"$BEFORE" || exit 1 + +# A forced redraw produces the correct scene. It must be identical to the +# scene drawn immediately by attach-session. +$INNER refresh-client -t "$CLIENT" || exit 1 +sleep 1 +$OUTER capture-pane -pe -t outer:0.0 >"$AFTER" || exit 1 +cmp -s "$BEFORE" "$AFTER" || + fail "attach-session left stale active/inactive pane styles" + +exit 0 diff --git a/regress/cfg-client-free.sh b/regress/cfg-client-free.sh new file mode 100644 index 000000000..9069163f6 --- /dev/null +++ b/regress/cfg-client-free.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +# a client lost while the configuration is loading is freed + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMPDIR=$(mktemp -d) || exit 1 +TMUX="$TEST_TMUX -S$TMPDIR/tmux.sock" + +cleanup() +{ + touch $TMPDIR/go + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup 0 1 15 + +# Poll until a command succeeds. +wait_for() +{ + i=0 + while ! "$@"; do + [ $i -eq 100 ] && exit 1 + i=$((i + 1)) + sleep 0.1 + done +} + +# The run-shell holds the configuration until the go file exists. +cat <$TMPDIR/conf +new-session -d -s keep +run-shell 'touch $TMPDIR/started; i=0; while [ ! -f $TMPDIR/go ] && [ \$i -lt 100 ]; do sleep 0.1; i=\$((i + 1)); done' +EOF + +cd $TMPDIR || exit 1 +$TMUX -v -f$TMPDIR/conf new-session -d -s first : [cfg_done/" not in contents, contents + finally: + go.touch() + server.terminate() + try: + server.wait(timeout=3) + except subprocess.TimeoutExpired: + server.kill() + server.wait() +PY diff --git a/regress/cfg-load-once.sh b/regress/cfg-load-once.sh new file mode 100644 index 000000000..b9f4289e1 --- /dev/null +++ b/regress/cfg-load-once.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# the configuration is loaded once if the client that started it is lost, and +# the next client still waits for it to finish + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMPDIR=$(mktemp -d) || exit 1 +TMUX="$TEST_TMUX -S$TMPDIR/tmux.sock" + +cleanup() +{ + touch $TMPDIR/go + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup 0 1 15 + +# Poll until a command succeeds. +wait_for() +{ + i=0 + while ! "$@"; do + [ $i -eq 100 ] && exit 1 + i=$((i + 1)) + sleep 0.1 + done +} + +# The run-shell holds the configuration until the go file exists. +cat <$TMPDIR/conf +set -ga @runs x +new-session -d -s keep +run-shell 'touch $TMPDIR/started; i=0; while [ ! -f $TMPDIR/go ] && [ \$i -lt 100 ]; do sleep 0.1; i=\$((i + 1)); done' +set -g @late 1 +EOF + +cd $TMPDIR || exit 1 +$TMUX -v -f$TMPDIR/conf new-session -d -s first $TMPDIR/out & +pid=$! +wait_for grep -q "name is client-$pid\$" $log +touch $TMPDIR/go +wait $pid || exit 1 + +[ "$(cat $TMPDIR/out)" = "x 1" ] || exit 1 + +exit 0 diff --git a/regress/check-names.sh b/regress/check-names.sh index be8ad617b..8ab68f497 100644 --- a/regress/check-names.sh +++ b/regress/check-names.sh @@ -5,6 +5,15 @@ PATH=/bin:/usr/bin TERM=screen +# The pane's shell must not be the user's own interactive shell: a custom +# PS1/PROMPT_COMMAND that sets the terminal title (as many do, for tmux/xterm +# TERM types) would redraw over the titles this test sets and checks on +# every prompt, regardless of how long it waits first. +shell= +if command -v bash >/dev/null 2>&1; then + shell='bash --noprofile --norc +o history' +fi + [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null @@ -30,7 +39,7 @@ must_equal() [ "$got" = "$want" ] || fail "got '$got', expected '$want'" } -$TMUX new-session -d -x 80 -y 24 || exit 1 +$TMUX new-session -d -x 80 -y 24 -- $shell || exit 1 $TMUX set-option -qg allow-set-title on || exit 1 $TMUX set-option -qg allow-rename on || exit 1 $TMUX set-option -qg automatic-rename off || exit 1 diff --git a/regress/cmd-parse-assignment.sh b/regress/cmd-parse-assignment.sh new file mode 100644 index 000000000..53ec4b9f0 --- /dev/null +++ b/regress/cmd-parse-assignment.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +# A bare assignment in a command sequence must not discard earlier commands. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +CONF=$(mktemp) +trap '$TMUX kill-server 2>/dev/null; rm -f "$CONF"' 0 +trap 'exit 1' 1 2 15 +$TMUX new-session -d || exit 1 + +check() +{ + name=$1 + expected=$2 + $TMUX set -g @trace '' || exit 1 + $TMUX set-environment -gu PARSE_ASSIGN || exit 1 + printf '%s\n' "$3" >"$CONF" + $TMUX source-file "$CONF" || exit 1 + trace=$($TMUX show-options -gqv @trace) || exit 1 + assignment=$($TMUX show-environment -g PARSE_ASSIGN 2>/dev/null) + out="$trace:$assignment" + if [ "$out" != "$expected" ]; then + echo "$name: expected '$expected', got '$out'" + cat "$CONF" + exit 1 + fi +} + +check trailing 'ab:PARSE_ASSIGN=1' \ + 'set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1' +check middle 'ab:PARSE_ASSIGN=1' \ + 'set -ag @trace a; PARSE_ASSIGN=1; set -ag @trace b' +check leading 'ab:PARSE_ASSIGN=1' \ + 'PARSE_ASSIGN=1; set -ag @trace a; set -ag @trace b' +check repeated 'ab:PARSE_ASSIGN=2' \ + 'set -ag @trace a; PARSE_ASSIGN=1; set -ag @trace b; PARSE_ASSIGN=2' +check semicolon 'a:PARSE_ASSIGN=1' \ + 'set -ag @trace a; PARSE_ASSIGN=1;' +check standalone ':PARSE_ASSIGN=1' 'PARSE_ASSIGN=1' +check braces 'ab:PARSE_ASSIGN=1' \ + 'if-shell -F 1 { set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1 }' +check active 'ab:PARSE_ASSIGN=1' \ + '%if 1 set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1 %endif' +check inactive ':' \ + '%if 0 set -ag @trace a; set -ag @trace b; PARSE_ASSIGN=1 %endif' + +# An assignment must not hide an invalid command earlier in the sequence. +$TMUX set -g @trace '' || exit 1 +printf '%s\n' \ + 'set -ag @trace a; nonexistent-command; PARSE_ASSIGN=1' >"$CONF" +if out=$($TMUX source-file "$CONF" 2>&1); then + echo 'Invalid command before assignment was ignored' + exit 1 +fi +if [ "$out" != "$CONF:1: unknown command: nonexistent-command" ]; then + echo "Unexpected parse error: '$out'" + exit 1 +fi +trace=$($TMUX show-options -gqv @trace) || exit 1 +if [ -n "$trace" ]; then + echo 'Commands executed despite a parse error' + exit 1 +fi + +exit 0 diff --git a/regress/cmd-parse-condition.sh b/regress/cmd-parse-condition.sh new file mode 100644 index 000000000..c5487cc2f --- /dev/null +++ b/regress/cmd-parse-condition.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +# Assignments and commands must use the first matching conditional branch. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +CONF=$(mktemp) +trap '$TMUX kill-server 2>/dev/null; rm -f "$CONF"' 0 +trap 'exit 1' 1 2 15 +$TMUX new-session -d || exit 1 + +branch() +{ + printf '%%%s %s%s' "$1" "$2" "$sep" + printf '%sPARSE_BRANCH=%s%sset -g @branch %s%s' \ + "$prefix" "$3" "$sep" "$3" "$sep" +} + +# Cover no, one and two elif branches, with and without else, all condition +# values, and nesting inside both an active and an inactive outer branch. +for mode in normal hidden inline; do + prefix= + sep=' +' + [ "$mode" = hidden ] && prefix='%hidden ' + [ "$mode" = inline ] && sep=' ' + for elifs in 0 1 2; do + bits=0 + while [ "$bits" -lt "$((1 << (elifs + 1)))" ]; do + for otherwise in 0 1; do + for outer in 0 1; do + expected=unchanged + { + printf '%s\n' 'PARSE_BRANCH=unchanged' \ + 'set -g @branch unchanged' + printf '%%if %s\n' "$outer" + n=0 + while [ "$n" -le "$elifs" ]; do + condition=$(((bits >> n) & 1)) + directive=elif + [ "$n" -eq 0 ] && directive=if + branch "$directive" "$condition" "$n" + if [ "$condition" -eq 1 ] && + [ "$expected" = unchanged ]; then + expected=$n + fi + n=$((n + 1)) + done + if [ "$otherwise" -eq 1 ]; then + branch else '' else + [ "$expected" = unchanged ] && expected=else + fi + printf '%s\n' '%endif' '%endif' \ + 'set -g @value "$PARSE_BRANCH"' + } >"$CONF" + [ "$outer" -eq 0 ] && expected=unchanged + $TMUX source-file "$CONF" || exit 1 + out=$($TMUX display-message -p '#{@value}:#{@branch}') + if [ "$out" != "$expected:$expected" ]; then + echo "$mode, $elifs elifs, bits=$bits, else=$otherwise, outer=$outer" + echo "Expected $expected:$expected, got $out" + cat "$CONF" + exit 1 + fi + done + done + bits=$((bits + 1)) + done + done +done + +exit 0 diff --git a/regress/copy-mode-no-client-scroll.sh b/regress/copy-mode-no-client-scroll.sh new file mode 100644 index 000000000..5dc8577f9 --- /dev/null +++ b/regress/copy-mode-no-client-scroll.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# copy-mode -S can be called from a hook without an attached client. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) || exit 1 +TMUX_TMPDIR=$OUT +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT +trap 'exit 1' 1 2 3 15 + +$TMUX new-session -d -s test 'sleep 300' || fail "new-session failed" +$TMUX set -g remain-on-exit on || fail "setting remain-on-exit failed" +$TMUX set-hook -g pane-died 'copy-mode -S' || + fail "setting pane-died hook failed" +$TMUX respawn-pane -k -t test:0 'exit 0' || fail "respawn-pane failed" + +i=0 +while [ "$i" -lt 50 ]; do + $TMUX has-session -t test 2>/dev/null || \ + fail "server died in copy-mode -S" + [ "$($TMUX display-message -p -t test:0 '#{pane_dead}' 2>/dev/null)" = 1 ] && \ + break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "pane-died hook was not triggered" +sleep 0.5 +$TMUX has-session -t test || fail "server died after copy-mode -S" + +exit 0 diff --git a/regress/copy-mode-search-count.sh b/regress/copy-mode-search-count.sh new file mode 100755 index 000000000..842bf4e2e --- /dev/null +++ b/regress/copy-mode-search-count.sh @@ -0,0 +1,161 @@ +#!/bin/sh + +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 + +cleanup() +{ + $TMUX kill-server 2>/dev/null +} +trap cleanup 0 +trap 'exit 1' 1 2 3 15 + +$TMUX new -d -x40 -y10 \ + "printf 'zero needle\none needle\ntwo needle\n'; cat" || exit 1 +$TMUX set -g window-size manual || exit 1 + +check_count() +{ + [ "$($TMUX display-message -p \ + '#{search_count_current}/#{search_count}')" = "$1" ] || exit 1 +} + +check_unsearched() +{ + check_count / + [ -z "$($TMUX display-message -p '#{search_count_present}')" ] || + exit 1 + case "$($TMUX display-message -p \ + '#{E:copy-mode-position-format}')" in + *match*|*result*) exit 1 ;; + esac +} + +for keys in vi emacs; do + $TMUX set -g mode-keys "$keys" || exit 1 + $TMUX copy-mode || exit 1 + check_unsearched + $TMUX send-keys -X scroll-up || exit 1 + check_unsearched + $TMUX resize-window -x40 -y10 || exit 1 + check_unsearched + $TMUX send-keys -X history-top || exit 1 + $TMUX send-keys -X start-of-line || exit 1 + + $TMUX send-keys -X search-forward-text needle || exit 1 + check_count 1/3 + $TMUX send-keys -X search-again || exit 1 + check_count 2/3 + $TMUX resize-window -x30 -y8 || exit 1 + check_count 2/3 + $TMUX send-keys -X search-again || exit 1 + check_count 3/3 + $TMUX send-keys -X search-again || exit 1 + check_count 1/3 + $TMUX send-keys -X search-reverse || exit 1 + if [ "$keys" = vi ]; then + reverse_index=3 + else + reverse_index=1 + fi + check_count "$reverse_index/3" + case "$($TMUX display-message -p \ + '#{E:copy-mode-position-format}')" in + *"($reverse_index of 3 matches)") ;; + *) exit 1 ;; + esac + + $TMUX send-keys -X cancel || exit 1 + $TMUX copy-mode || exit 1 + check_unsearched + $TMUX send-keys -X history-bottom || exit 1 + $TMUX send-keys -X search-backward-text needle || exit 1 + check_count 3/3 + $TMUX send-keys -X cancel || exit 1 + + # Moving onto the second match before searching again skips to the third. + $TMUX copy-mode || exit 1 + $TMUX send-keys -X history-top || exit 1 + $TMUX send-keys -X start-of-line || exit 1 + $TMUX send-keys -X search-forward-text needle || exit 1 + $TMUX send-keys -X cursor-down || exit 1 + $TMUX send-keys -X search-again || exit 1 + check_count 3/3 + $TMUX send-keys -X cancel || exit 1 +done + +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -X search-forward-text absent || exit 1 +check_count /0 +case "$($TMUX display-message -p '#{E:copy-mode-position-format}')" in + *'(0 matches)') ;; + *) exit 1 ;; +esac +$TMUX send-keys -X search-forward-text one || exit 1 +check_count 1/1 +case "$($TMUX display-message -p '#{E:copy-mode-position-format}')" in + *'(1 of 1 match)') ;; + *) exit 1 ;; +esac +$TMUX send-keys -X cancel || exit 1 + +# Backward searches can find an overlapping start inside a marked match. +# The index must refer to the match after the cursor moves to its beginning. +$TMUX new-window -n adjacent "printf 'aaaa'; cat" || exit 1 +for keys in vi emacs; do + $TMUX set -g mode-keys "$keys" || exit 1 + $TMUX copy-mode || exit 1 + check_unsearched + $TMUX send-keys -X search-backward-incremental -- '=aa' || exit 1 + check_count 2/2 + $TMUX send-keys -X search-backward-incremental -- '-aa' || exit 1 + check_count 1/2 + case "$($TMUX display-message -p \ + '#{E:copy-mode-position-format}')" in + *'(1 of 2 matches)') ;; + *) exit 1 ;; + esac + $TMUX send-keys -X search-backward-incremental -- '-aa' || exit 1 + check_count 2/2 + $TMUX send-keys -X search-backward-incremental -- '-aa' || exit 1 + check_count 1/2 + $TMUX send-keys -X search-backward-incremental -- '=' || exit 1 + check_unsearched + $TMUX send-keys -X cancel || exit 1 + + $TMUX copy-mode || exit 1 + check_unsearched + $TMUX send-keys -X search-backward-text aa || exit 1 + check_count 2/2 + $TMUX send-keys -X search-again || exit 1 + check_count 1/2 + $TMUX send-keys -X search-again || exit 1 + check_count 2/2 + $TMUX send-keys -X cancel || exit 1 +done + +# Scrolling into real history must not create a search count. +$TMUX new-window -n scroll \ + 'i=0; while [ $i -lt 80 ]; do echo "line $i"; i=$((i + 1)); done; cat' || + exit 1 +$TMUX copy-mode -u || exit 1 +[ "$($TMUX display-message -p '#{scroll_position}')" -gt 0 ] || exit 1 +check_unsearched +$TMUX send-keys -X history-top || exit 1 +check_unsearched +$TMUX resize-window -x40 -y10 || exit 1 +check_unsearched +$TMUX send-keys -X cancel || exit 1 + +format=$($TMUX show -gv copy-mode-position-format) || exit 1 +case "$format" in + *'#{search_count_current} of '*'#{search_count} matches'*) ;; + *) exit 1 ;; +esac + +exit 0 diff --git a/regress/copy-mode-selection-mode.sh b/regress/copy-mode-selection-mode.sh index bcd070d59..27dc71b91 100644 --- a/regress/copy-mode-selection-mode.sh +++ b/regress/copy-mode-selection-mode.sh @@ -78,4 +78,59 @@ expected=$(printf 'alpha\nbeta\ngamma') [ "$($TMUX show-buffer)" = "$expected" ] || fail "reversed line-mode selection did not keep complete lines" +# Word mode must initialize its reset boundaries from the existing selection, +# rather than extending it back to the origin on the next cursor movement. +for mode in emacs vi; do + $TMUX send-keys -X cancel || exit 1 + $TMUX set-window-option -g mode-keys "$mode" || exit 1 + $TMUX copy-mode || exit 1 + $TMUX send-keys -X history-top || exit 1 + $TMUX send-keys -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-right || exit 1 + $TMUX send-keys -X selection-mode word || exit 1 + $TMUX send-keys -X copy-selection-no-clear -C || exit 1 + [ "$($TMUX show-buffer)" = beta ] || + fail "$mode: word-mode selection did not expand to the current word" + $TMUX send-keys -X cursor-right || exit 1 + $TMUX send-keys -X copy-selection-no-clear -C || exit 1 + expected=$(printf 'beta\ngamma') + [ "$($TMUX show-buffer)" = "$expected" ] || + fail "$mode: word-mode movement reset the selection to the origin" + + # Reversing the character selection before changing mode must keep the + # fixed end and its containing word when the cursor moves further back. + $TMUX send-keys -X cancel || exit 1 + $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 -X selection-mode word || exit 1 + $TMUX send-keys -X cursor-left || exit 1 + $TMUX send-keys -X copy-selection-no-clear -C || exit 1 + expected=$(printf 'alpha\nbeta\ngamma') + [ "$($TMUX show-buffer)" = "$expected" ] || + fail "$mode: reversed word-mode selection lost its fixed end" + + # A stopped selection should expand to words without moving its cursor. + $TMUX send-keys -X cancel || exit 1 + $TMUX copy-mode || exit 1 + $TMUX send-keys -X history-top || exit 1 + $TMUX send-keys -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-right || exit 1 + $TMUX send-keys -X stop-selection || exit 1 + before=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}') + $TMUX send-keys -X selection-mode word || exit 1 + [ "$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}')" = \ + "$before" ] || fail "$mode: word-mode switch moved a stopped cursor" + $TMUX send-keys -X copy-selection-no-clear -C || exit 1 + [ "$($TMUX show-buffer)" = beta ] || + fail "$mode: stopped word-mode selection did not contain the whole word" +done + exit 0 diff --git a/regress/copy-mode-selection-scroll.sh b/regress/copy-mode-selection-scroll.sh index 7e7aee9ce..cb1152b49 100644 --- a/regress/copy-mode-selection-scroll.sh +++ b/regress/copy-mode-selection-scroll.sh @@ -110,4 +110,92 @@ wheel 64 5 5 $TMUX send-keys -X copy-selection-no-clear || exit 1 expect_buffer "$extended_start" +# Perform a mouse drag across a sequence of col,row coordinates (1-based). +# SGR mouse format: \033[/dev/null + # Keep the button held briefly so auto-scrolling can run, and + # capture copy-mode state before mouse release exits copy mode. + sleep 0.5 + scroll_pos=$($TMUX display -p '#{scroll_position}') + cursor_y=$($TMUX display -p '#{copy_cursor_y}') + # Mouse button 1 up (release) at the final coordinate. + seq=$(printf '\033[<0;%s;%sm' "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 +} + +# Turn off status line so all 10 pane rows are visible in the outer client, +# and write a 24-char line on the bottom row (row 10, longer than the 18-char +# line 79 above it). +$TMUX set -g status off || exit 1 +$TMUX send-keys -X cancel || exit 1 +$TMUX send-keys -l "line 80 xxxxxxxxxxxxxxxx" || exit 1 +sleep 0.2 + +for mode in emacs vi; do + $TMUX setw -g mode-keys "$mode" || exit 1 + + # In emacs mode the selection end column is exclusive, so selecting all + # characters of an 18- or 24-char line requires placing the cursor one + # column past the last character; in vi mode the end column is inclusive. + if [ "$mode" = emacs ]; then + ecol18=19 + ecol24=25 + else + ecol18=18 + ecol24=24 + fi + + # Drag along the bottom row (row 10) past the end of the line and back + # when already at the bottom of history; the selection must stay on + # line 80 instead of jumping up to line 79. + $TMUX copy-mode || exit 1 + drag 1,10 "$ecol24,10" "$((ecol24 + 1)),10" "$ecol24,10" + expect_buffer "line 80 xxxxxxxxxxxxxxxx" + + # Scroll up 1 line so line 80 is off-screen below, then drag from row 9 + # to the bottom row (row 10) to auto-scroll down and select lines 78..80. + $TMUX copy-mode || exit 1 + $TMUX send-keys -X scroll-up || exit 1 + drag 1,9 23,10 "$ecol24,10" + expect_buffer "$(printf 'line 78 xxxxxxxxxx\nline 79 xxxxxxxxxx\nline 80 xxxxxxxxxxxxxxxx')" + + # Scroll 1 line below the top of history so line 00 is off-screen above, + # then drag from row 2 to the top row (row 1) to auto-scroll up and + # select lines 00..02. + $TMUX copy-mode || exit 1 + $TMUX send-keys -X history-top || exit 1 + $TMUX send-keys -X scroll-down || exit 1 + drag "$ecol18,2" 1,1 + expect_buffer "$(printf 'line 00 xxxxxxxxxx\nline 01 xxxxxxxxxx\nline 02 xxxxxxxxxx')" + + # Scroll up 5 lines, then drag onto the bottom row and keep the button + # held to auto-scroll all the way to the bottom. + $TMUX copy-mode || exit 1 + $TMUX send-keys -N5 -X scroll-up || exit 1 + drag 1,9 5,10 + [ "$scroll_pos" = 0 ] || fail "$mode: drag auto-scroll stopped at $scroll_pos" + + # At the top of history, drag onto the top row and keep the button + # held: the cursor must stay on the top row. + $TMUX copy-mode || exit 1 + $TMUX send-keys -X history-top || exit 1 + drag 5,2 5,1 + [ "$cursor_y" = 0 ] || fail "$mode: cursor left the top row (row $cursor_y)" +done + exit 0 diff --git a/regress/copy-mode-word-redraw.sh b/regress/copy-mode-word-redraw.sh new file mode 100644 index 000000000..b4825395e --- /dev/null +++ b/regress/copy-mode-word-redraw.sh @@ -0,0 +1,149 @@ +#!/bin/sh + +# Word selection can extend beyond the cursor's row. Compare what an attached +# client drew with a full copy-mode repaint after extending and shrinking it. + +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 -Lword-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lword-outer-$$ -f/dev/null" +DIR=$(mktemp -d) || exit 1 +exit_status=0 + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 +trap 'exit 1' 1 2 3 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +check_buffer() +{ + $INNER send-keys -X copy-selection-no-clear || exit 1 + $INNER save-buffer "$DIR/copied" || exit 1 + cmp -s "$DIR/expected" "$DIR/copied" || fail 'unexpected copied text' +} + +check_redraw() +{ + label=$1 + sleep 0.2 + $OUTER capture-pane -ep -t outer:0.0 >"$DIR/before" || exit 1 + $INNER send-keys -X toggle-position || exit 1 + $INNER send-keys -X toggle-position || exit 1 + sleep 0.2 + $OUTER capture-pane -ep -t outer:0.0 >"$DIR/after" || exit 1 + if ! cmp -s "$DIR/before" "$DIR/after"; then + diff -u "$DIR/before" "$DIR/after" >&2 + echo "$label: stale word-selection highlight" >&2 + exit_status=1 + fi +} + +$INNER new-session -d -s inner -x20 -y8 '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 mode-keys emacs || exit 1 +$INNER set-option -g copy-mode-position-format '' || exit 1 +$INNER set-option -g copy-mode-selection-style 'fg=white,bg=red' || exit 1 +$OUTER new-session -d -s outer -x20 -y8 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +sleep 0.5 + +# A wrapped word ending on the penultimate row. In emacs mode end-of-line +# moves onto the exclusive end and extends the selection into the blank row +# below; start-of-line then removes that row from the selection. +awk 'BEGIN { print "PREFIX"; for (i = 0; i < 115; i++) printf "a"; print "" }' \ + >"$DIR/text" || exit 1 +$INNER new-window -t inner:1 "cat '$DIR/text'; exec sleep 100" || exit 1 +$INNER resize-window -x20 -y8 || exit 1 +sleep 0.2 +$INNER copy-mode || exit 1 +$INNER send-keys -X history-top || exit 1 +$INNER send-keys -X cursor-down || exit 1 +$INNER send-keys -X select-word || exit 1 +$INNER send-keys -X end-of-line || exit 1 +[ "$($INNER display-message -p '#{selection_end_y}')" = 7 ] || + fail 'selection did not extend into the blank row' +check_redraw 'extend onto blank row' +awk 'BEGIN { for (i = 0; i < 115; i++) printf "a"; print "" }' \ + >"$DIR/expected" || exit 1 +check_buffer +$INNER send-keys -X start-of-line || exit 1 +[ "$($INNER display-message -p '#{selection_end_y}')" = 6 ] || + fail 'selection did not shrink away from the blank row' +check_redraw 'shrink away from blank row' +awk 'BEGIN { for (i = 0; i < 115; i++) printf "a" }' \ + >"$DIR/expected" || exit 1 +check_buffer + +# The selected next word can itself wrap across several rows even though the +# cursor only moved within one row. All of those rows must be repainted. +awk 'BEGIN { print "PREFIX"; print "one"; + for (i = 0; i < 65; i++) printf "b"; print ""; print "END" }' \ + >"$DIR/text" || exit 1 +$INNER new-window -t inner:2 "cat '$DIR/text'; exec sleep 100" || exit 1 +$INNER resize-window -x20 -y8 || exit 1 +sleep 0.2 +$INNER copy-mode || exit 1 +$INNER send-keys -X history-top || exit 1 +$INNER send-keys -X cursor-down || exit 1 +$INNER send-keys -X select-word || exit 1 +$INNER send-keys -X end-of-line || exit 1 +[ "$($INNER display-message -p '#{selection_end_y}')" = 5 ] || + fail 'selection did not extend over the wrapped next word' +check_redraw 'extend over a wrapped next word' +awk 'BEGIN { print "one"; for (i = 0; i < 65; i++) printf "b" }' \ + >"$DIR/expected" || exit 1 +check_buffer +$INNER send-keys -X start-of-line || exit 1 +check_redraw 'shrink away from a wrapped next word' +printf 'one' >"$DIR/expected" || exit 1 +check_buffer + +# Drag a word selection onto the bottom row and back while there is still +# history below the viewport. A redraw must not read the next backing row +# and paint it onto the last visible row. +awk 'BEGIN { for (i = 0; i < 30; i++) printf "line%02d abc def\n", i }' \ + >"$DIR/text" || exit 1 +$INNER new-window -t inner:3 "cat '$DIR/text'; exec sleep 100" || exit 1 +$INNER resize-window -x20 -y8 || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER unbind-key -Tcopy-mode MouseDragEnd1Pane || exit 1 +sleep 0.2 +$INNER copy-mode || exit 1 +$INNER send-keys -X history-top || exit 1 +$INNER send-keys -X cursor-down || exit 1 +$INNER send-keys -X select-word || exit 1 +$OUTER send-keys -l "$(printf '\033[<0;3;2M')" || exit 1 +sleep 0.1 +$OUTER send-keys -l "$(printf '\033[<32;8;7M')" || exit 1 +sleep 0.1 +$INNER send-keys -X selection-mode word || exit 1 +$OUTER send-keys -l "$(printf '\033[<32;8;8M')" || exit 1 +sleep 0.1 +# Leave the edge to cancel the auto-scroll timer before comparing. +$OUTER send-keys -l "$(printf '\033[<32;8;7M')" || exit 1 +sleep 0.1 +$OUTER send-keys -l "$(printf '\033[<0;8;7m')" || exit 1 +sleep 0.1 +[ "$($INNER display-message -p '#{selection_mode}')" = word ] || + fail 'mouse drag did not retain word selection mode' +[ "$($INNER display-message -p '#{scroll_position}')" -gt 0 ] || + fail 'mouse drag reached the bottom of history' +check_redraw 'drag word selection at bottom of viewport' + +exit "$exit_status" diff --git a/regress/display-message-client.sh b/regress/display-message-client.sh new file mode 100755 index 000000000..c0f52c257 --- /dev/null +++ b/regress/display-message-client.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +# display-message -c must take the client formats from the given client, even +# when it is attached to a different session from the target pane. + +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 one -x40 -y10 'exec sleep 100' || exit 1 +$INNER new-session -d -s two -x40 -y10 'exec sleep 100' || exit 1 +$INNER set-option -g status off || exit 1 + +# Each client has a distinct TERM so that client_termname identifies it. +attach() +{ + echo "env -i PATH=/bin:/usr/bin TERM=$1 LC_ALL=C.UTF-8 $TEST_TMUX -LtestI$$ -f/dev/null attach-session -t $2" +} +$OUTER new-session -d -s outer -x40 -y10 "$(attach screen one)" || exit 1 +$OUTER new-window -d "$(attach xterm two)" || exit 1 +$OUTER set-option -g status off || exit 1 + +i=0 +while [ "$i" -lt 50 ]; do + n=$($INNER list-clients -F '#{client_name}' 2>/dev/null|wc -l) + [ "$n" -eq 2 ] && break + sleep 0.1 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "clients did not attach" + +c1=$($INNER list-clients -F '#{client_name}' -f '#{==:#{client_termname},screen}') +c2=$($INNER list-clients -F '#{client_name}' -f '#{==:#{client_termname},xterm}') +[ -n "$c1" ] && [ -n "$c2" ] || fail "could not find both clients" + +# The target is in session two, but the client is the one in session one. +term=$($INNER display-message -c "$c1" -t two: -p '#{client_termname}') +[ "$term" = screen ] || fail "client_termname was '$term', not screen" + +# The session formats still come from the target. +session=$($INNER display-message -c "$c1" -t two: -p '#{session_name}') +[ "$session" = two ] || fail "session_name was '$session', not two" + +term=$($INNER display-message -c "$c2" -t one: -p '#{client_termname}') +[ "$term" = xterm ] || fail "client_termname was '$term', not xterm" + +exit 0 diff --git a/regress/floating-pane-border-clients.sh b/regress/floating-pane-border-clients.sh new file mode 100755 index 000000000..917f3b6c1 --- /dev/null +++ b/regress/floating-pane-border-clients.sh @@ -0,0 +1,135 @@ +#!/bin/sh + +# Damage-only redraws must evaluate both active and inactive border styles +# for each client, even though the cached border cells belong to the pane. + +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 +INNER="$TEST_TMUX -Lborder-style-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lborder-style-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +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_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + count=$($INNER list-clients 2>/dev/null | wc -l) + [ "$count" -eq 2 ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "two inner clients did not attach" +} + +wait_for_marker() +{ + target=$1 + marker=$2 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1 + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client in $target did not receive $marker" +} + +cat >"$DIR/emitter.pl" <<'PERL' +use strict; +use warnings; + +$| = 1; +for my $phase (1 .. 2) { + while (!-e "$ENV{TRIGGER}-$phase") { + select undef, undef, undef, 0.01; + } + # Change an unused palette entry and acknowledge it in the pane body. + print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase"; +} +sleep 100; +PERL + +$INNER new-session -d -s inner -x 60 -y 20 \ + "TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1 +$INNER set -g status off || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g automatic-rename off || exit 1 +$INNER set -g status-interval 0 || exit 1 +$INNER set -g pane-border-lines simple || exit 1 +$INNER set -g pane-border-status top || exit 1 +$INNER set -g pane-border-format 'CLIENT=#{client_name}' || exit 1 +FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \ + 'sleep 100') || exit 1 + +$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1 +$OUTER set -g status off || exit 1 +$OUTER set -g window-size manual || exit 1 +$OUTER set -g default-terminal screen || exit 1 +LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1 +RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1 +for target in "$LEFT" "$RIGHT"; do + $OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" || + exit 1 +done +wait_for_clients + +NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1 +NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1 +STYLE="fg=#{?#{==:#{client_name},$NAME1},red,blue}" +$INNER set -g pane-border-style "$STYLE" || exit 1 +$INNER set -g pane-active-border-style "$STYLE" || exit 1 + +# Capture just the floating pane's rows, excluding the acknowledgement in +# the tiled pane. Exercise each cache by changing the floating pane's focus. +phase=1 +while [ "$phase" -le 2 ]; do + if [ "$phase" -eq 2 ]; then + $INNER select-pane -t "$FLOAT" || exit 1 + fi + $INNER refresh-client -t "$NAME1" || exit 1 + $INNER refresh-client -t "$NAME2" || exit 1 + sleep 0.5 + for target in "$LEFT" "$RIGHT"; do + $OUTER capture-pane -pe -S 5 -E 10 -t "$target" \ + >"$DIR/before-$target" || exit 1 + done + RED=$(printf '\033[31m') + BLUE=$(printf '\033[34m') + grep -Fq "$RED" "$DIR/before-$LEFT" || fail "missing red border" + grep -Fq "$BLUE" "$DIR/before-$RIGHT" || fail "missing blue border" + + : >"$DIR/trigger-$phase" + wait_for_marker "$LEFT" "DAMAGE$phase" + wait_for_marker "$RIGHT" "DAMAGE$phase" + sleep 0.2 + for target in "$LEFT" "$RIGHT"; do + $OUTER capture-pane -pe -S 5 -E 10 -t "$target" \ + >"$CAPTURE" || exit 1 + diff -u "$DIR/before-$target" "$CAPTURE" || + fail "phase $phase changed $target's border style" + done + phase=$((phase + 1)) +done + +exit 0 diff --git a/regress/floating-pane-drag-composite.sh b/regress/floating-pane-drag-composite.sh new file mode 100755 index 000000000..f1fb37faa --- /dev/null +++ b/regress/floating-pane-drag-composite.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +# Each drag motion must draw the floating pane content once. + +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 +cd "$DIR" || exit 1 +INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + cd / + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.15 +} + +$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; 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-option -g status-interval 0 || exit 1 +$INNER set-option -g automatic-rename off || exit 1 +FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \ + "printf 'DRAGMARK'; exec sleep 100") || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 +sleep 0.5 + +XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') +GRABCOL=$((XOFF + 3)) +BORDERROW=$YOFF + +# Begin capture after focus changes from the initial mouse press have settled. +mouse 0 "$GRABCOL" "$BORDERROW" M +$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1 +$INNER refresh-client || exit 1 +sleep 0.5 +grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content" +grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row" +offset=$(wc -c <"$DIR/output") + +i=0 +steps=6 +while [ "$i" -lt "$steps" ]; do + GRABCOL=$((GRABCOL + 1)) + mouse 32 "$GRABCOL" "$BORDERROW" M + i=$((i + 1)) +done +mouse 0 "$GRABCOL" "$BORDERROW" m +sleep 0.3 + +NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns" +tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output" +n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output") +[ "$n" -eq "$steps" ] || + fail "$steps drag motions wrote the floating pane content $n times" + +exit 0 diff --git a/regress/floating-pane-drag-ghost.sh b/regress/floating-pane-drag-ghost.sh new file mode 100755 index 000000000..abc20633d --- /dev/null +++ b/regress/floating-pane-drag-ghost.sh @@ -0,0 +1,108 @@ +#!/bin/sh + +# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating() +# (cmd-resize-pane.c) reported damage for just a dragged floating pane's +# content rectangle, not the one-cell border frame drawn around it (see the +# "floating" case in screen-redraw.c, which draws that frame at +# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own +# content area). Damage scoped to only the content area left the frame's +# previous position undrawn as the pane moved, so dragging it left a trail +# of un-erased border frames behind - visible as several "corners" stacked +# up rather than just the pane's current one. +# +# This bug has nothing to do with images - it reproduces with a plain +# floating pane and no image support required. + +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" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} + +# drag STARTCOL STARTROW ENDCOL ENDROW +# +# Write a plain (unmodified) SGR button-1 press, drag update and release at +# 1-based positions to the outer pane holding the inner client - this +# matches the default MouseDown1Border/MouseDrag1Border bindings used to +# move or resize a floating pane by its border. +drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +TMP=$(mktemp) +trap "cleanup; rm -f $TMP" 0 1 15 + +$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1 +$TMUX set -g mouse on +$TMUX set -g default-command 'sh -c "sleep 100"' + +FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) || + fail "new-pane -X -Y failed" +FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}') +FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}') +FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}') + +$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +# Sanity check: exactly one floating pane, so exactly one top-left corner, +# before dragging anything. +$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" +n=$(grep -o '┌' $TMP | wc -l) +[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n" + +# Drag the floating pane by its top border (row FTOP-1, some column within +# its width) down several rows in a few separate steps, then release. A +# single drag() call already does press/motion/release, so call it several +# times in a row to simulate a multi-step real drag. +GRABCOL=$((FLEFT + FWIDTH / 2)) +STARTROW=$FTOP +i=0 +while [ $i -lt 6 ]; do + newrow=$((STARTROW + i + 1)) + drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow + i=$((i + 1)) +done + +$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed" + +# Exactly one top-left corner should remain - the pane's current position. +# This is expected to fail before the fix: multiple corners (a trail of +# un-erased frames) would remain from the intermediate drag positions. +n=$(grep -o '┌' $TMP | wc -l) +[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)" + +exit 0 diff --git a/regress/floating-pane-drag-sb-strip.sh b/regress/floating-pane-drag-sb-strip.sh new file mode 100755 index 000000000..fbdebbc82 --- /dev/null +++ b/regress/floating-pane-drag-sb-strip.sh @@ -0,0 +1,160 @@ +#!/bin/sh + +# Regression test: dragging a floating pane across another pane's ordinary +# content must not redraw that other pane's scrollbar, unless the drag +# actually crosses the scrollbar's own strip. +# +# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag +# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the +# floating pane's old or new rectangle, rather than just its narrow +# scrollbar strip - so dragging a floating pane back and forth over an +# ordinary tiled pane's content (never touching its scrollbar) still +# needlessly redrew that pane's scrollbar on every motion step. See +# tmux-image-redraw-known-bugs.md for the full write-up. +# +# This is checked by giving the non-dragged pane a distinctive scrollbar +# colour and counting how many times its SGR code appears in the client's +# raw output while the floating pane is dragged vertically over that pane's +# body, well clear of its scrollbar column: with the fix, it should never +# reappear after the initial draw. + +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" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} + +# drag STARTCOL STARTROW ENDCOL ENDROW +drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.5 +} + +cleanup + +TMP=$(mktemp) +trap "cleanup; rm -f $TMP" 0 1 15 + +$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1 +$TMUX set -g status-right '' || fail "set status-right failed" +$TMUX set -g mouse on || fail "set mouse failed" +$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed" +$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed" + +$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed" + +PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}') +LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1) +[ -n "$LEFT" ] || fail "could not identify left pane" + +# A distinctive scrollbar colour for the non-dragged (left) pane only. +$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' || + fail "set pane-scrollbars-style failed" + +ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}') +ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}') +AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}') +AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}') +[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)" + +# A small floating pane placed well inside the left pane's content area, +# clear of its (right-hand) scrollbar column by several columns. +FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \ + -X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed" +FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}') +FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}') +FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}') +[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] || + fail "sanity: floating pane too close to the scrollbar column" + +# Start the outer session with a plain shell, then start capturing before +# triggering the attach - starting the attach as the outer pane's initial +# command would mean pipe-pane only starts after the attach-driven initial +# redraw (which draws the scrollbars) has already happened, missing it. +$TMUX2 new-session -d -x 60 -y 20 || exit 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." +$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed" +$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed" +$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed" +sleep 1 + +# Sanity check: the distinctive scrollbar colour reaches the client at all. +grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client" +: >$TMP + +# Drag the floating pane straight up and down by its top border, staying at +# a fixed column the whole time - this never crosses the left pane's +# scrollbar strip, only its ordinary content. +GRABCOL=$((FLEFT + FWIDTH / 2)) +row=$FTOP +i=0 +while [ $i -lt 6 ]; do + newrow=$((row + 1)) + drag $GRABCOL $row $GRABCOL $newrow + row=$newrow + i=$((i + 1)) +done +i=0 +while [ $i -lt 6 ]; do + newrow=$((row - 1)) + drag $GRABCOL $row $GRABCOL $newrow + row=$newrow + i=$((i + 1)) +done + +# The scrollbar colour should never reappear - its geometry never changed, +# and the drag never crossed its column. This is expected to fail before +# the fix - see the header comment. +n=$(grep -ac '48;5;201' $TMP) +[ "$n" -eq 0 ] || + fail "left pane's scrollbar was redrawn $n times while dragging over its body only" + +# Cross the scrollbar and return, comparing each damaged redraw with a full +# refresh to check that covering and uncovering its cells leaves no artifacts. +offset=$((ALEFT + AWIDTH - FLEFT - FWIDTH / 2)) +for delta in "$offset" "$((-offset))"; do + oldleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}') + newcol=$((GRABCOL + delta)) + drag "$GRABCOL" "$row" "$newcol" "$row" + GRABCOL=$newcol + newleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}') + [ "$newleft" -eq "$((oldleft + delta))" ] || + fail "floating pane did not move across the scrollbar as expected" + before=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))") + $TMUX refresh-client || fail "refresh-client failed" + sleep 0.5 + after=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))") + [ "$before" = "$after" ] || + fail "scrollbar crossing differs from a full redraw" +done + +exit 0 diff --git a/regress/floating-pane-drag-sync.sh b/regress/floating-pane-drag-sync.sh new file mode 100755 index 000000000..bd63d5adf --- /dev/null +++ b/regress/floating-pane-drag-sync.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +# Synchronized drag output must not cause a full redraw of untouched rows. + +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 +cd "$DIR" || exit 1 +INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + cd / + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.15 +} + +$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; 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-option -g status-interval 0 || exit 1 +$INNER set-option -g automatic-rename off || exit 1 +FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \ + "printf 'DRAGMARK'; exec sleep 100") || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$INNER set-option -as terminal-features ',screen-256color:sync' || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 +sleep 0.5 + +XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') +GRABCOL=$((XOFF + 3)) +BORDERROW=$YOFF + +# Begin capture after focus changes from the initial mouse press have settled. +mouse 0 "$GRABCOL" "$BORDERROW" M +$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1 +$INNER refresh-client || exit 1 +sleep 0.5 +grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content" +grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row" +offset=$(wc -c <"$DIR/output") + +i=0 +steps=6 +while [ "$i" -lt "$steps" ]; do + GRABCOL=$((GRABCOL + 1)) + mouse 32 "$GRABCOL" "$BORDERROW" M + i=$((i + 1)) +done +mouse 0 "$GRABCOL" "$BORDERROW" m +sleep 0.3 + +NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns" +tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output" +n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output") +[ "$n" -ge "$steps" ] || fail "drag output did not reach the client" +perl -0777 -ne 'exit(/\e\[\?2026h/ ? 0 : 1)' "$DIR/drag-output" || + fail "drag did not use synchronized output" +if grep -aq OUTSIDE "$DIR/drag-output"; then + fail "synchronized drag redrew an untouched row" +fi + +exit 0 diff --git a/regress/floating-pane-drag-wide-character.sh b/regress/floating-pane-drag-wide-character.sh new file mode 100755 index 000000000..cc6854efd --- /dev/null +++ b/regress/floating-pane-drag-wide-character.sh @@ -0,0 +1,200 @@ +#!/bin/sh + +# Damage at a floating pane's vacated edge must always redraw a complete +# grid character. redraw_damage_grow_span_clip() (screen-redraw.c) widens a +# damage rectangle's left edge by one cell whenever it isn't already at the +# span's own edge, to pull in a wide character's base half when the edge +# lands on its padding half - but it did this unconditionally, with no +# check of which half it was actually touching. When the edge instead +# already lands cleanly on a fresh character's base cell, growing left +# walks into the *previous*, unrelated character's padding cell and blanks +# it (tty_draw_line() treats any leading padding cell as proof its own +# range starts mid-character). +# +# This is a general damage-composition bug, not specific to any one kind of +# pane, but the exact column parity needs to be deterministic to actually +# catch it (a lucky parity draws fine). This constructs it by creating the +# floating pane, checking its real position (the border-framing offset +# added to -X is not something to hand-compute), and recreating it one +# column over if necessary until the vacated rectangle's left edge lands on +# a base cell. + +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 +INNER="$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lwidechar-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +BASE=$DIR/base +CAPTURE=$DIR/capture +FLOAT= + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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 + 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_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +slice_columns() +{ + # Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of + # $1, decoding UTF-8 - the pane's own new position (well clear of + # this range) must not affect the comparison, so this only looks at + # the narrow strip actually vacated, not the whole line. capture-pane + # text has one decoded character per double-width cell pair (every + # character here is width 2), so terminal columns are converted to + # character indices by halving before slicing. + perl -CSD -e ' + my ($row1, $row2, $col1, $col2, $file) = @ARGV; + open my $fh, "<:encoding(UTF-8)", $file or die $!; + my @lines = <$fh>; + my $c1 = int($col1 / 2); + my $c2 = int(($col2 + 1) / 2); + for my $n ($row1 .. $row2) { + my $line = $lines[$n - 1]; + $line =~ s/\R\z//; + print substr($line, $c1, $c2 - $c1), "\n"; + } + ' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1" +} + +wait_old_rows_restored() +{ + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + slice_columns "$BASE" >"$DIR/want" + slice_columns "$CAPTURE" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "wide characters under the floating pane's vacated edge were not restored" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +binmode STDOUT, ':encoding(UTF-8)'; +$| = 1; +for my $row (1 .. 10) { + print "\e[$row;1H", chr(0x754c) x 20; +} +sleep 100; +PERL + +$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || 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-option -g pane-scrollbars off || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +wait_outer_has '界界界' +$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1 + +# Create the floating pane, then check its actual resulting position. Try +# adjacent starting columns until the vacated rectangle's left edge +# (xoff - 1) lands on an even (base-cell) column - the odd case is the one +# every earlier manual test happened to land on by chance. +startx=5 +tries=0 +while [ "$tries" -lt 2 ]; do + [ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null + FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 12 -y 3 -X "$startx" \ + -Y 5 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1 + sleep 0.2 + XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') + YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') + oldleft=$((XOFF - 1)) + if [ $((oldleft % 2)) -eq 0 ]; then + break + fi + startx=$((startx + 1)) + tries=$((tries + 1)) +done +[ $(((XOFF - 1) % 2)) -eq 0 ] || fail "could not find bad-parity starting column" + +wait_outer_has FLOATMARK + +ROW1=$((YOFF + 1)) +ROW2=$((YOFF + 3)) +COL1=$((XOFF - 4)) +COL2=$((XOFF + 4)) + +# Grab the pane's top border a couple of columns in (avoiding the corner +# cells) and drag it well clear of its old rectangle. +GRABCOL=$((XOFF + 3)) +BORDERROW=$((YOFF)) + +seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW") +$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1 +sleep 0.1 +seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 15))" "$BORDERROW") +$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1 +sleep 0.1 +seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 15))" "$BORDERROW") +$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1 +sleep 0.1 + +NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)" + +wait_old_rows_restored + +exit 0 diff --git a/regress/floating-pane-drag-wide-right.sh b/regress/floating-pane-drag-wide-right.sh new file mode 100755 index 000000000..495126a50 --- /dev/null +++ b/regress/floating-pane-drag-wide-right.sh @@ -0,0 +1,206 @@ +#!/bin/sh + +# Mirror of floating-pane-drag-wide-character.sh for the *right* edge of a +# damage rectangle. redraw_damage_grow_span_clip() (screen-redraw.c) widens +# a damage rectangle's right edge by one cell whenever it isn't already at +# the span's own edge, to pull in a wide character's base half when the +# edge lands on its padding half - but it did this unconditionally, with no +# check of which half it was actually touching. When the edge instead +# already lands cleanly on a fresh character's base cell (a character fully +# outside the range), growing right pulls in just that base cell - +# tty_draw_line() sees it can't fit that character's full width in the +# remaining range (tty_draw_line_get_empty()'s gc->data.width > nx check) +# and clears it, exactly as the left-edge bug cleared a neighbouring +# character's padding half. +# +# As with the left-edge test, the exact column parity needs to be +# deterministic to actually catch it. This constructs it by creating the +# floating pane, checking its real position, and recreating it one column +# over if necessary until the vacated rectangle's right edge lands on a +# base cell. + +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 +INNER="$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lwidecharr-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +BASE=$DIR/base +CAPTURE=$DIR/capture +FLOAT= +PANEWIDTH=12 + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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 + 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_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +slice_columns() +{ + # Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of + # $1, decoding UTF-8 - the pane's own new position (well clear of + # this range) must not affect the comparison, so this only looks at + # the narrow strip actually vacated, not the whole line. capture-pane + # text has one decoded character per double-width cell pair (every + # character here is width 2), so terminal columns are converted to + # character indices by halving before slicing. + perl -CSD -e ' + my ($row1, $row2, $col1, $col2, $file) = @ARGV; + open my $fh, "<:encoding(UTF-8)", $file or die $!; + my @lines = <$fh>; + my $c1 = int($col1 / 2); + my $c2 = int(($col2 + 1) / 2); + for my $n ($row1 .. $row2) { + my $line = $lines[$n - 1]; + $line =~ s/\R\z//; + print substr($line, $c1, $c2 - $c1), "\n"; + } + ' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1" +} + +wait_old_rows_restored() +{ + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + slice_columns "$BASE" >"$DIR/want" + slice_columns "$CAPTURE" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "wide characters under the floating pane's vacated right edge were not restored" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +binmode STDOUT, ':encoding(UTF-8)'; +$| = 1; +for my $row (1 .. 10) { + print "\e[$row;1H", chr(0x754c) x 20; +} +sleep 100; +PERL + +$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || 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-option -g pane-scrollbars off || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +wait_outer_has '界界界' +$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1 + +# Create the floating pane, then check its actual resulting position. Try +# adjacent starting columns until the vacated rectangle's right edge +# (xoff + PANEWIDTH + 1, the border-grown exclusive end - see +# window_pane_damage_floating(), window.c) lands on an even (base-cell) +# column. +startx=5 +tries=0 +while [ "$tries" -lt 2 ]; do + [ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null + FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x "$PANEWIDTH" -y 3 \ + -X "$startx" -Y 5 \ + 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1 + sleep 0.2 + XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') + YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') + oldright=$((XOFF + PANEWIDTH + 1)) + if [ $((oldright % 2)) -eq 0 ]; then + break + fi + startx=$((startx + 1)) + tries=$((tries + 1)) +done +[ $(((XOFF + PANEWIDTH + 1) % 2)) -eq 0 ] || + fail "could not find bad-parity starting column" + +wait_outer_has FLOATMARK + +ROW1=$((YOFF + 1)) +ROW2=$((YOFF + 3)) +COL1=$((XOFF + PANEWIDTH - 2)) +COL2=$((XOFF + PANEWIDTH + 2)) + +# Grab the pane's top border a couple of columns in (avoiding the corner +# cells) and drag it well clear of its old rectangle - far enough right +# that its new position starts past the checked columns above (which sit +# just past the pane's *original* right edge). +GRABCOL=$((XOFF + 3)) +BORDERROW=$((YOFF)) + +seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW") +$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1 +sleep 0.1 +seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 20))" "$BORDERROW") +$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1 +sleep 0.1 +seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 20))" "$BORDERROW") +$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1 +sleep 0.1 + +NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)" + +wait_old_rows_restored + +exit 0 diff --git a/regress/floating-pane-offscreen-alt.sh b/regress/floating-pane-offscreen-alt.sh new file mode 100755 index 000000000..97dbbe68e --- /dev/null +++ b/regress/floating-pane-offscreen-alt.sh @@ -0,0 +1,102 @@ +#!/bin/sh + +# Returning from the alternate screen must redraw the visible part of a +# floating pane clipped at the left edge, the top edge, or both. + +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 +INNER="$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Loffscreen-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +for my $row (1 .. 5) { + print "\e[$row;1H", 'A' x 15; +} +while (!-e "$ENV{TRIGGER}-alternate") { + select undef, undef, undef, 0.01; +} +print "\e[?1049h"; +for my $row (1 .. 5) { + print "\e[$row;1H", 'B' x 15; +} +while (!-e "$ENV{TRIGGER}-restore") { + select undef, undef, undef, 0.01; +} +print "\e[?1049l"; +sleep 100; +PERL + +$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || 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 -x 40 -y 10 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +for position in left top both; do + case "$position" in + left) x=-5; y=2 ;; + top) x=5; y=-2 ;; + both) x=-5; y=-2 ;; + esac + FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X "$x" -Y "$y" \ + "TRIGGER='$DIR/$position' perl '$EMITTER'") || exit 1 + [ "$($INNER display-message -p -t "$FLOAT" '#{pane_left},#{pane_top}')" = "$((x + 1)),$((y + 1))" ] || + fail "$position: floating pane has unexpected position" + wait_outer_has AAAAA + cp "$CAPTURE" "$DIR/primary" + : >"$DIR/$position-alternate" + wait_outer_has BBBBB + : >"$DIR/$position-restore" + wait_outer_has AAAAA + cmp -s "$DIR/primary" "$CAPTURE" || + fail "$position: primary screen was not completely restored" + $INNER kill-pane -t "$FLOAT" || exit 1 +done + +exit 0 diff --git a/regress/floating-pane-prompt-wide-character.sh b/regress/floating-pane-prompt-wide-character.sh new file mode 100755 index 000000000..cdd892a54 --- /dev/null +++ b/regress/floating-pane-prompt-wide-character.sh @@ -0,0 +1,179 @@ +#!/bin/sh + +# A damage rectangle's clip range is grown to avoid splitting a wide +# character, but redraw_damage_grow_span_clip() (screen-redraw.c) only ever +# checks the span's own pane *content* grid (wp->screen) for that. For a +# REDRAW_SPAN_PANE span, that same range is then also handed to +# redraw_damage_draw_pane_prompt() to recompose the pane's separately +# rendered prompt (wp->prompt, e.g. from "command-prompt -P") over the +# damaged sub-range - but the prompt is drawn into its own, freshly +# allocated one-line screen, unrelated to the pane's content grid, so a +# range grown (or left ungrown) against the content is not necessarily +# grown correctly for the prompt's own wide characters. +# +# This is invisible when the pane's own content is plain ASCII (as here): +# redraw_damage_grow_span_clip() never finds anything to grow against, so +# the raw, ungrown geometric range is passed straight through to the +# prompt - and if that range's edge lands mid-character in the *prompt's* +# grid, tty_draw_line() clears the character it cuts through +# (tty_draw_line_get_empty()'s gc->data.width > nx check, for a trailing +# base cell with no room left for its padding half). +# +# The trigger is a palette change (OSC 4) in a tiled pane that is one half +# of a vertical split running the full height of the window - occluded +# under the floating pane, but still geometrically triggering a redraw of +# its own rectangle. Positioned so the split boundary falls inside the +# floating pane's own CJK prompt, this reproduces exactly Codex's report: +# "a floating pane containing a CJK prompt across a tiled-pane boundary - +# a palette update in the tiled pane blanks a prompt character." + +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 +cd "$DIR" || exit 1 +INNER="$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lpromptwide-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +CAPTURE=$DIR/capture +FLOAT= + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + cd / + rm -rf "$DIR" +} +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_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_prompt_row_intact() +{ + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + line=$(sed -n "${PROMPTROW}p" "$CAPTURE") + case $line in + *"$PROMPTTEXT"*) return 0 ;; + esac + sleep 0.1 + i=$((i + 1)) + done + fail "the CJK prompt was not intact after the palette-triggered damage - got: $line" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +my $line = ; +print "\e]4;1;rgb:11/22/33\e\\"; +sleep 100; +PERL + +$INNER new-session -d -s inner -x 60 -y 12 "perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +LEFT=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1 + +# Split so the boundary between the two tiled panes falls at column 17 - +# used below to pick a floating-pane column that lands the boundary +# mid-character inside the prompt. +RIGHT=$($INNER split-window -t inner -h -l 43 -PF '#{pane_id}' \ + 'sleep 100') || exit 1 +RX=$($INNER display-message -p -t "$RIGHT" '#{pane_left}') || exit 1 + +# Create the floating pane, then check its actual resulting position (the +# border-framing offset added to -X is not something to hand-compute). Try +# adjacent starting columns until the split boundary lands on an odd +# (padding-half) column of the prompt's own numbering, and within the +# prompt's 12-column width. +startx=10 +tries=0 +while [ "$tries" -lt 4 ]; do + [ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null + FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 24 -y 5 -X "$startx" \ + -Y 2 "sh -c 'i=0; while [ \$i -lt 10 ]; do \ +printf AAAAAAAAAAAAAAAAAAAAAA\\\\n; i=\$((i+1)); done; sleep 100'") || + exit 1 + sleep 0.2 + X1=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') + Y1=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') + H1=$($INNER display-message -p -t "$FLOAT" '#{pane_height}') + local=$((RX - X1 - 1)) + if [ "$local" -ge 1 ] && [ "$local" -le 11 ] && + [ $((local % 2)) -eq 1 ]; then + break + fi + startx=$((startx + 1)) + tries=$((tries + 1)) +done +local=$((RX - X1 - 1)) +[ "$local" -ge 1 ] && [ "$local" -le 11 ] && [ $((local % 2)) -eq 1 ] || + fail "could not find bad-parity starting column" + +$OUTER new-session -d -s outer -x 60 -y 12 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +wait_outer_has AAAAAAAAAAAAAAAAAAAAAA +CLIENT=$($INNER list-clients -F '#{client_name}') || exit 1 + +$INNER select-pane -t "$FLOAT" || exit 1 +PROMPTTEXT=$(printf '\344\270\255' | perl -CSD -ne 'print $_ x 6') +$INNER command-prompt -b -P -t "$CLIENT" -p "$PROMPTTEXT" \ + 'display-message -- %1' || exit 1 +wait_outer_has "$PROMPTTEXT" + +PROMPTROW=$((Y1 + H1)) + +# Trigger the damage: unblock the emitter so it fires the palette change in +# the left tiled pane, which is occluded under (but geometrically overlaps) +# the floating pane's prompt row. +$INNER send-keys -t "$LEFT" Enter || exit 1 + +wait_prompt_row_intact + +exit 0 diff --git a/regress/floating-pane-status-cross-client.sh b/regress/floating-pane-status-cross-client.sh new file mode 100755 index 000000000..fadb5e5bf --- /dev/null +++ b/regress/floating-pane-status-cross-client.sh @@ -0,0 +1,151 @@ +#!/bin/sh + +# Damage redraws must show each client's own pane status, even though the +# cached status screen is shared between clients. + +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 +INNER="$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lstatuscc-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + count=$($INNER list-clients 2>/dev/null | wc -l) + [ "$count" -eq 2 ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "two inner clients did not attach" +} + +wait_for_marker() +{ + target=$1 + marker=$2 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1 + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client in $target did not receive $marker" +} + +cat >"$DIR/emitter.pl" <<'PERL' +use strict; +use warnings; + +$| = 1; +for my $phase (1 .. 4) { + while (!-e "$ENV{TRIGGER}-$phase") { + select undef, undef, undef, 0.01; + } + # Change an unused palette entry and acknowledge it in the pane body. + print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase"; +} +sleep 100; +PERL + +$INNER new-session -d -s inner -x 60 -y 20 \ + "TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1 +$INNER set -g status off || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g automatic-rename off || exit 1 +$INNER set -g status-interval 0 || exit 1 +$INNER set -g pane-border-lines simple || exit 1 +$INNER set -g pane-border-status top || exit 1 +$INNER set -g pane-border-format 'CLIENT=<#{client_name}>' || exit 1 +FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \ + 'sleep 100') || exit 1 + +$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1 +$OUTER set -g status off || exit 1 +$OUTER set -g window-size manual || exit 1 +$OUTER set -g default-terminal screen || exit 1 +LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1 +RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1 +for target in "$LEFT" "$RIGHT"; do + $OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" || + exit 1 +done +wait_for_clients + +NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1 +NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1 + +# Disable periodic status updates above and trigger damage without a command +# that also requests a status redraw. Each client must keep its own title. +$INNER refresh-client -t "$NAME1" || exit 1 +$INNER refresh-client -t "$NAME2" || exit 1 +sleep 0.5 +for phase in 0 1 2; do + if [ "$phase" -ne 0 ]; then + : >"$DIR/trigger-$phase" + wait_for_marker "$LEFT" "DAMAGE$phase" + wait_for_marker "$RIGHT" "DAMAGE$phase" + sleep 0.2 + fi + for target in "$LEFT" "$RIGHT"; do + if [ "$target" = "$LEFT" ]; then + name=$NAME1 + other=$NAME2 + else + name=$NAME2 + other=$NAME1 + fi + $OUTER capture-pane -p -S 5 -E 10 -t "$target" \ + >"$CAPTURE" || exit 1 + grep -Fq "CLIENT=<$name>" "$CAPTURE" || + fail "phase $phase: missing $name's pane status" + if grep -Fq "CLIENT=<$other>" "$CAPTURE"; then + fail "phase $phase: $name received $other's pane status" + fi + done +done + +# Leave just one client so a cache keyed only by client would remain stale. +$OUTER respawn-pane -k -t "$RIGHT" 'sleep 100' || exit 1 +i=0 +while [ "$($INNER list-clients | wc -l)" -ne 1 ]; do + [ "$i" -lt 50 ] || fail "second client did not detach" + sleep 0.1 + i=$((i + 1)) +done +$INNER set-environment -g TEST_STATUS_VALUE initial || exit 1 +$INNER set -g pane-border-format 'VALUE=#{TEST_STATUS_VALUE}' || exit 1 +$INNER refresh-client -t "$NAME1" || exit 1 +wait_for_marker "$LEFT" VALUE=initial +for phase in 3 4; do + # Changing the environment does not itself request a status redraw. + $INNER set-environment -g TEST_STATUS_VALUE "phase$phase" || exit 1 + : >"$DIR/trigger-$phase" + wait_for_marker "$LEFT" "DAMAGE$phase" + wait_for_marker "$LEFT" "VALUE=phase$phase" +done + +exit 0 diff --git a/regress/floating-pane-status-format.sh b/regress/floating-pane-status-format.sh new file mode 100755 index 000000000..9974b7515 --- /dev/null +++ b/regress/floating-pane-status-format.sh @@ -0,0 +1,102 @@ +#!/bin/sh + +# Resizing a floating pane must refresh session status formats which depend on +# its geometry, not only the pane scene and borders. + +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 +INNER="$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lfloating-status-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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 + 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_outer_has_status() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + tail -1 "$CAPTURE" | grep -q "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client status did not show $marker" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +$INNER new-session -d -s inner -x 50 -y 12 'sleep 100' || exit 1 +FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 10 -y 5 -X 5 -Y 3 \ + 'sleep 100') || fail "could not create floating pane" +$INNER set-option -g window-size manual || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER set-option -g status on || exit 1 +$INNER set-option -g status-position bottom || exit 1 +$INNER set-option -g status-left 'WIDTH=#{pane_width}' || exit 1 +$INNER set-option -g status-right '' || exit 1 +$INNER set-option -g status-interval 0 || exit 1 + +$OUTER new-session -d -s outer -x 50 -y 12 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +OLD_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}') +wait_outer_has_status "WIDTH=$OLD_WIDTH" + +RIGHT=$($INNER display-message -p -t "$FLOAT" '#{pane_right}') +TOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') +X=$((RIGHT + 2)) +Y=$((TOP + 2)) + +# Grab the right frame and enlarge the floating pane. +mouse 0 "$X" "$Y" M +mouse 32 "$((X + 1))" "$Y" M +mouse 32 "$((X + 8))" "$Y" M +mouse 0 "$((X + 8))" "$Y" m + +NEW_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}') +[ "$NEW_WIDTH" -ne "$OLD_WIDTH" ] || fail "floating pane was not resized" +wait_outer_has_status "WIDTH=$NEW_WIDTH" + +exit 0 diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 475feb524..3fdfb59bd 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -245,6 +245,19 @@ test_format "#{R:a,3}" "aaa" test_format "#{R:ab,2}" "abab" # A long repeat exercises output-buffer growth during expansion. test_format "#{n:#{R:x,300}}" "300" +# Limit the result size as well as the repeat count, including nested repeats. +test_format "#{n:#{R:abcdefgh,8192}}" "65536" +test_format "#{R:abcdefgh,8193}" "" +test_format "#{R:,10000}" "" +test_format "#{n:#{R:#{R:x,256},256}}" "65536" +test_format "#{R:#{R:x,256},257}" "" +test_format "#{R:#{R:abcdefgh,8192}x,1}" "" +test_format "#{n:#{R:中文中,7281}}" "65529" +test_format "#{R:中文中,7282}" "" +test_format "#{n:#{R:x,10000}}" "10000" +test_format "#{R:x,10001}" "" +# Fuzzy matching must not receive an enormous string from nested repeats. +test_format "#{m/p:a,#{R:#{R:#{R:a,353},38},90}}" "" # --- Width, padding and truncation --------------------------------------- diff --git a/regress/format-no-current-window.sh b/regress/format-no-current-window.sh index 07bec45cc..d8f19b0fe 100644 --- a/regress/format-no-current-window.sh +++ b/regress/format-no-current-window.sh @@ -26,7 +26,8 @@ FMT="$FMT #{window_active_clients} #{window_active_clients_list}" $TMUX kill-server 2>/dev/null # No session: the callbacks have nothing to read and must return empty. -out=$($TMUX start\; display-message -p "$FMT") || fail "display with no session" +out=$($TMUX start\; set -s exit-empty off \; display-message -p "$FMT") || + fail "display with no session" [ "$out" = " " ] || fail "no session expanded to '$out'" $TMUX new-session -d -s src -n one -- sleep 30 || fail "new-session src" diff --git a/regress/format-strings.sh b/regress/format-strings.sh old mode 100644 new mode 100755 index c06c340e3..90ca60f88 --- a/regress/format-strings.sh +++ b/regress/format-strings.sh @@ -176,7 +176,7 @@ test_conditional_with_pane_in_mode "#{?pane_in_mode,abc,#{?#{==:#{session_name}, test_conditional_with_session_name "#{?pane_in_mode,abc,#{?#{==:#{session_name},Summer},ABC,XYZ}}" "ABC" "XYZ" # Some fancy stackings -test_conditional_with_pane_in_mode "#{?#{==:#{?pane_in_mode,#{session_name},#(echo Spring)},Summer},abc,xyz}" "abc" "xyz" +test_conditional_with_pane_in_mode "#{?#{==:#{?pane_in_mode,#{session_name},#{@cold}},Summer},abc,xyz}" "abc" "xyz" diff --git a/regress/format-time-range.sh b/regress/format-time-range.sh new file mode 100644 index 000000000..7a8e059a4 --- /dev/null +++ b/regress/format-time-range.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +# time modifiers with a value outside the range the system can convert + +PATH=/bin:/usr/bin +TERM=screen +TZ=UTC +export TZ + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null" 0 1 15 + +$TMUX new -d +$TMUX set -g @ok 1700000000 +$TMUX set -g @huge 3093527980800 +$TMUX set -g @bad 9223372036854775807 + +# A convertible time still formats, so an empty result below means the value +# was rejected and not that the modifier stopped working. +$TMUX display -p '[#{t:@ok}]' >$TMP +(echo "[Tue Nov 14 22:13:20 2023]"|cmp -s - $TMP) || exit 1 +$TMUX display -p '[#{t/f/zz:@ok}]' >$TMP +(echo "[zz]"|cmp -s - $TMP) || exit 1 +$TMUX display -p '[#{t/p:@ok}]' >$TMP +(echo "[Nov23]"|cmp -s - $TMP) || exit 1 + +# A year with more digits than ctime has room for must not be printed cut +# short, but other modifiers can still format it. +$TMUX display -p '[#{t:@huge}]' >$TMP +grep -Eqx '\[\]|\[Sat Jan 1 00:00:00 100000\]' $TMP || exit 1 +$TMUX display -p '[#{t/f/zz:@huge}]' >$TMP +(echo "[zz]"|cmp -s - $TMP) || exit 1 + +$TMUX display -p '[#{t:@bad}]' >$TMP +(echo "[]"|cmp -s - $TMP) || exit 1 +$TMUX display -p '[#{t/p:@bad}]' >$TMP +(echo "[]"|cmp -s - $TMP) || exit 1 +$TMUX display -p '[#{t/f/zz:@bad}]' >$TMP +(echo "[]"|cmp -s - $TMP) || exit 1 + +exit 0 diff --git a/regress/input-nowrap-collect.sh b/regress/input-nowrap-collect.sh new file mode 100644 index 000000000..93496fb68 --- /dev/null +++ b/regress/input-nowrap-collect.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +. ./input-common.inc + +# The shortest collected prefix must leave the final cell at the right edge. +start_pane narrow 2 3 '\033[?7lABC' +check_capture narrow 'AC' +check_cursor narrow '1,0' +check_flags narrow '- AC' + +# REP may fill the final cell, but subsequent text still overwrites it. +start_pane repeat 5 3 '\033[?7lA\033[9bYZ' +check_capture repeat 'AAAAZ' +check_cursor repeat '4,0' +check_flags repeat '- AAAAZ' + +# Style changes must flush collected cells without moving the final cursor. +start_pane styled 5 3 '\033[?7l\033[31mAB\033[32mCDE\033[34mZ' +check_capture styled 'ABCDZ' +check_cursor styled '4,0' +check_raw_matches styled \ + 'C 0,0 data=\(1,1,A\).*fg=red\[1\] ' \ + 'C 0,2 data=\(1,1,C\).*fg=green\[2\] ' \ + 'C 0,4 data=\(1,1,Z\).*fg=blue\[4\] ' + +# Overwriting a wide character's padding must clear the old leading cell. +start_pane padding 8 3 '\033[?7lA\343\201\202B\r\033[2CX' +check_capture padding 'A XB' +check_cursor padding '3,0' +check_raw_matches padding \ + 'C 0,1 data=\(1,1, \) flags=NONE\[0\]' \ + 'C 0,2 data=\(1,1,X\) flags=NONE\[0\]' + +# Insert mode must shift existing cells even when autowrap is disabled. +start_pane insert 5 3 '\033[?7lABCD\r\033[C\033[4hXY\033[4l' +check_capture insert 'AXYBC' +check_cursor insert '3,0' + +# Re-enabling wrap after a collected prefix must retain the wrap boundary. +start_pane toggle 5 3 '\033[?7lABCD\033[?7hEF' +check_capture toggle 'ABCDE +F' +check_cursor toggle '1,1' +check_flags toggle 'W ABCDE +- F' + +exit $exit_status diff --git a/regress/input-requests.sh b/regress/input-requests.sh index 094ce2a8a..1f403ac06 100644 --- a/regress/input-requests.sh +++ b/regress/input-requests.sh @@ -1,5 +1,10 @@ #!/bin/sh +# Verify palette and clipboard payload delivery with both OSC terminators, +# fragmented replies and a separate Escape before the reply. Invalid payloads +# must leave the request pending for a subsequent valid reply. Check that +# foreground and background reports update the colours queried by a pane. + PATH=/bin:/usr/bin TERM=screen @@ -8,6 +13,7 @@ TERM=screen python3 - "$TEST_TMUX" <<'PY' import os import select +import shlex import signal import subprocess import sys @@ -49,22 +55,30 @@ def read_until(fd, needle, timeout=5): raise RuntimeError("did not see terminal request %r in %r" % (needle, data)) -def wait_file(path, timeout=5): +def wait_file(path, length, timeout=5): end = time.time() + timeout while time.time() < end: try: with open(path, "rb") as f: data = f.read() - if data: + if len(data) >= length: return data except FileNotFoundError: pass time.sleep(0.05) return b"" -def respawn(command): +def respawn(command, output): + with open(output, "wb"): + pass run("respawn-window", "-k", "-t", "requests:0", command) - time.sleep(0.2) + +def wait_pane(expected, context): + end = time.monotonic() + 5 + while run("capture-pane", "-p").stdout.strip() != expected: + assert time.monotonic() < end, (context, "unexpected pane bytes", + run("capture-pane", "-p").stdout.strip()) + time.sleep(.01) def cleanup(pid=None): if pid is not None: @@ -73,6 +87,8 @@ def cleanup(pid=None): except ProcessLookupError: pass run("kill-server", check=False) + if pid is not None: + os.waitpid(pid, 0) run("kill-server", check=False) run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests", @@ -82,37 +98,97 @@ pid, fd = attach() try: time.sleep(0.5) - with tempfile.NamedTemporaryFile(delete=False) as f: - palette_out = f.name - respawn("stty raw -echo min 1 time 50; " - "printf '\\033]4;99;?\\033\\\\'; " - "dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" % - palette_out) - read_until(fd, b"\033]4;99;?\033\\") - os.write(fd, b"\033]4;99;rgb:0101/0202/0303\033\\") - got = wait_file(palette_out) - expected = b"^[]4;99;rgb:0101/0202/0303^[\\" - if got != expected: - raise AssertionError("palette reply: expected %r got %r" % - (expected, got)) - + run("set-option", "-g", "assume-paste-time", "0") + run("set-option", "-s", "escape-time", "1000") + run("set-option", "-g", "@seen", "") + run("bind-key", "-n", "Escape", "set-option", "-gF", "@seen", + "#{@seen}E") run("set-option", "-s", "set-clipboard", "on") run("set-option", "-s", "get-clipboard", "request") - with tempfile.NamedTemporaryFile(delete=False) as f: - clip_out = f.name - respawn("stty raw -echo min 1 time 50; " - "printf '\\033]52;c;?\\033\\\\'; " - "dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" % - clip_out) - data = read_until(fd, b"]52;") - if b"?" not in data: - raise RuntimeError("clipboard request missing query in %r" % data) - os.write(fd, b"\033]52;c;UmVxdWVzdA==\033\\") - got = wait_file(clip_out) - expected = b"^[]52;c;UmVxdWVzdA==^[\\" - if got != expected: - raise AssertionError("clipboard reply: expected %r got %r" % - (expected, got)) + expected_escapes = b"" + replies = [ + ("palette", "\\033]4;99;?\\033\\\\", b"\033]4;99;?\033\\", + b"\033]4;99;rgb:0101/0202/0303", b"\033]4;99;invalid"), + ("clipboard", "\\033]52;c;?\\033\\\\", b"]52;", + b"\033]52;c;UmVxdWVzdA==", b"\033]52;c;!!!!"), + ] + with tempfile.TemporaryDirectory() as directory: + output = os.path.join(directory, "reply") + for name, query, needle, payload, invalid in replies: + for prefixed in (False, True): + for split in (False, True): + for terminator in (b"\007", b"\033\\"): + context = (name, prefixed, split, terminator) + # Replies to the pane use its request's ST terminator, + # regardless of the terminal reply's terminator. + expected = payload + b"\033\\" + respawn("stty raw -echo min 1 time 50; " + "printf '%s'; dd bs=1 count=%d 2>/dev/null >%s; " + "exec cat -v" % + (query, len(expected), shlex.quote(output)), output) + data = read_until(fd, needle) + assert b"?" in data, (context, "query missing", data) + + os.write(fd, invalid + terminator) + time.sleep(.05) + with open(output, "rb") as f: + assert f.read() == b"", (context, "invalid payload delivered") + + wire = (b"\033" if prefixed else b"") + payload + terminator + if split: + os.write(fd, wire[:-1]) + time.sleep(.05) + with open(output, "rb") as f: + assert f.read() == b"", (context, "partial payload delivered") + os.write(fd, wire[-1:]) + else: + os.write(fd, wire) + got = wait_file(output, len(expected)) + assert got == expected, (context, expected, got) + if prefixed: + expected_escapes += b"E" + seen = run("show-option", "-gv", "@seen").stdout.strip() + assert seen == expected_escapes, (context, "Escape count", seen) + + # Check that decoding did not deliver the reply twice + # or leak raw bytes after delivering the payload. + os.write(fd, b"Z") + wait_pane(b"Z", context) + + colour_value = 1 + for colour in (10, 11): + for prefixed in (False, True): + for terminator in (b"\007", b"\033\\"): + context = ("colour", colour, prefixed, terminator) + # A different colour each time prevents an earlier + # successful report from masking a later ignored one. + payload = b"\033]%d;rgb:%04x/0202/0303" % ( + colour, colour_value * 257) + colour_value += 1 + wire = (b"\033" if prefixed else b"") + payload + terminator + if prefixed: + os.write(fd, wire[:-1]) + time.sleep(.05) + assert run("capture-pane", "-p").stdout.strip() == b"Z", ( + context, "partial reply leaked") + os.write(fd, wire[-1:] + b"X") + expected_escapes += b"E" + else: + os.write(fd, wire + b"X") + wait_pane(b"ZX", context) + seen = run("show-option", "-gv", "@seen").stdout.strip() + assert seen == expected_escapes, (context, "Escape count", seen) + + expected = payload + b"\033\\" + respawn("stty raw -echo min 1 time 50; " + "printf '\\033]%d;?\\033\\\\'; " + "dd bs=1 count=%d 2>/dev/null >%s; exec cat -v" % + (colour, len(expected), shlex.quote(output)), output) + got = wait_file(output, len(expected)) + assert got == expected, (context, "colour not applied", got) + os.write(fd, b"Z") + wait_pane(b"Z", context) finally: cleanup(pid) + os.close(fd) PY diff --git a/regress/join-pane-modal.sh b/regress/join-pane-modal.sh new file mode 100644 index 000000000..823709da3 --- /dev/null +++ b/regress/join-pane-modal.sh @@ -0,0 +1,80 @@ +#!/bin/sh + +# Moving a pane within a window must preserve a modal pane's return target. + +PATH=/bin:/usr/bin +TERM=screen +export TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup EXIT + +fail() +{ + echo "$*" >&2 + exit 1 +} + +check_ok() +{ + $TMUX "$@" || fail "command failed: $*" +} + +check_active() +{ + active=$($TMUX display-message -p -t "$1" '#{pane_id}') + [ "$active" = "$2" ] || + fail "active pane is $active, expected $2" +} + +check_ok new-session -d -s modal -x 120 -y 40 'cat' +p0=$($TMUX display-message -p -t modal:0 '#{pane_id}') +p1=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') || + fail "first split failed" +p2=$($TMUX split-window -PF '#{pane_id}' -t "$p1" 'cat') || + fail "second split failed" + +# Both commands, with and without -d, must restore the moved pane after +# the modal closes. The modal must keep focus during the move. +for command in join-pane move-pane; do + for detached in '' -d; do + check_ok select-pane -t "$p2" + modal=$($TMUX new-pane -OdPF '#{pane_id}' -t "$p0" \ + -x 80 -y 10 'cat') || fail "new modal failed" + check_active modal:0 "$modal" + check_ok "$command" $detached -h -s "$p2" -t "$p0" + check_active modal:0 "$modal" + check_ok kill-pane -t "$modal" + check_active modal:0 "$p2" + done +done + +# Moving a different pane must also preserve the return target. +modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \ + -x 80 -y 10 'cat') || fail "new modal failed" +check_ok join-pane -h -s "$p1" -t "$p0" +check_active modal:0 "$modal" +check_ok kill-pane -t "$modal" +check_active modal:0 "$p2" + +# Moving the return target to another window must still forget it, so +# closing the modal selects a pane remaining in its own window. +check_ok new-window -d -t modal:1 'cat' +modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \ + -x 80 -y 10 'cat') || fail "new modal failed" +check_ok join-pane -d -h -s "$p2" -t modal:1 +check_active modal:0 "$modal" +check_ok kill-pane -t "$modal" +active=$($TMUX display-message -p -t modal:0 '#{pane_id}') +case "$active" in +"$p0"|"$p1") ;; +*) fail "modal returned to a pane outside its window: $active" ;; +esac +window=$($TMUX display-message -p -t "$p2" '#{window_index}') +[ "$window" = 1 ] || fail "pane did not move to the other window" diff --git a/regress/lifecycle-deferred.sh b/regress/lifecycle-deferred.sh index d2669ff46..a978eb0d2 100644 --- a/regress/lifecycle-deferred.sh +++ b/regress/lifecycle-deferred.sh @@ -189,15 +189,15 @@ while [ "$i" -le 20 ]; do idx=$((50 + i)) run_tmux new-session -d -s "$s" -n base 'sleep 1000' >/dev/null - run_tmux split-window -t "$s:base" 'sleep 1000' >/dev/null - run_tmux respawn-pane -k -t "$s:base.1" 'sleep 1000' >/dev/null + run_tmux split-window -E -t "$s:base" >/dev/null + run_tmux respawn-pane -k -E -t "$s:base.1" >/dev/null run_tmux kill-pane -t "$s:base.1" >/dev/null - run_tmux new-window -t "$s" -n second 'sleep 1000' >/dev/null + run_tmux new-window -E -t "$s" -n second >/dev/null run_tmux link-window -s "$s:second" -t "life:$idx" >/dev/null run_tmux unlink-window -t "life:$idx" >/dev/null - run_tmux new-window -t "$s" -n single 'sleep 1000' >/dev/null + run_tmux new-window -E -t "$s" -n single >/dev/null run_tmux kill-pane -t "$s:single.0" >/dev/null run_tmux kill-window -t "$s:base" >/dev/null diff --git a/regress/menu-live-output.sh b/regress/menu-live-output.sh new file mode 100644 index 000000000..3e127ea32 --- /dev/null +++ b/regress/menu-live-output.sh @@ -0,0 +1,164 @@ +#!/bin/sh + +# Inspect an attached client's terminal while panes write behind a menu. +# Incremental output must agree with the scene renderer, including floating +# panes and a client viewport smaller than the window. + +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 -d) + +cleanup() { + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -f "$TMP/output" "$TMP/float" "$TMP/actual" "$TMP/expected" + rmdir "$TMP" +} +trap cleanup 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +# A full scene redraw is the reference for each incremental update. Check an +# uncovered marker before refreshing, so a frozen pane cannot pass the test. +check_update() { + printf '%b' "$2" >&3 + printf '\033[1;1H\033[2K%s' "$1" >&3 + sleep 1 + $TMUX capturep -p >"$TMP/actual" || exit 1 + grep -q "^$1" "$TMP/actual" || fail "$1: pane stopped updating" + $TMUX capturep -pe >"$TMP/actual" || exit 1 + $TMUX2 refresh-client || exit 1 + sleep 1 + $TMUX capturep -pe >"$TMP/expected" || exit 1 + if ! cmp -s "$TMP/actual" "$TMP/expected"; then + diff -u "$TMP/expected" "$TMP/actual" >&2 + fail "$1: incremental output differs from scene redraw" + fi +} + +menu() { + $TMUX2 display-menu -b simple -T live -C 0 "$@" \ + alpha a "" beta b "" || exit 1 + sleep 1 + $TMUX capturep -p >"$TMP/actual" || exit 1 + grep -q alpha "$TMP/actual" || fail "menu did not appear" +} + +mkfifo "$TMP/output" "$TMP/float" || exit 1 +$TMUX2 new -d -x40 -y14 "exec cat '$TMP/output'" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g window-size manual || exit 1 +$TMUX2 resizew -x40 -y14 || exit 1 +$TMUX2 set -as terminal-features ',screen:clipboard' || exit 1 +$TMUX2 set -g set-clipboard on || exit 1 +exec 3>"$TMP/output" + +$TMUX new -d -x40 -y14 "$TMUX2 attach" || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX resizew -x40 -y14 || exit 1 +$TMUX set -g set-clipboard on || exit 1 +sleep 1 + +menu -x6 -y8 +check_update text '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789' +check_update clear-line '\033[6;1H\033[2K' +check_update clear-end-line '\033[6;1H\033[K' +check_update clear-start-line '\033[6;40H\033[1K' +check_update erase-character '\033[6;1H\033[40X' +check_update clear-screen '\033[2J' +check_update clear-end-screen '\033[3;1H\033[J' +check_update clear-start-screen '\033[10;40H\033[1J' +check_update scroll '\033[14;1Hone\r\ntwo\r\nthree\r\nfour\r\n' +check_update reverse-scroll '\033[1;1H\033M' +check_update insert-character '\033[6;1Habcdefghijklmnopqrstuvwxyz\033[6;1H\033[3@' +check_update delete-character '\033[6;1Habcdefghijklmnopqrstuvwxyz\033[6;1H\033[3P' +check_update insert-line '\033[5;1H\033[2L' +check_update delete-line '\033[5;1H\033[2M' +check_update wide-text '\033[6;6H\347\225\214\347\225\214\347\225\214' +check_update overwrite-wide '\033[6;5H\347\225\214\033[6;6Hx' +check_update insert-mode '\033[6;1H\033[4habc\033[4l' +check_update sync '\033[?2026h\033[6;1Habcdefghijklmnopqrstuvwxyz\033[?2026l' +check_update sync-scroll '\033[?2026h\033[14;1Hone\r\ntwo\r\nthree\r\n\033[?2026l' + +# Text on the right of the menu crosses the client viewport edge. A terminal +# fallback which redraws the entire pane line would overwrite the menu. +$TMUX2 resizew -x60 -y14 || exit 1 +sleep 1 +check_update viewport '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwx' +$TMUX2 resizew -x40 -y14 || exit 1 + +# Window coordinates must still be used when the status line is above it. +$TMUX2 set -g status on || exit 1 +$TMUX2 set -g status-position top || exit 1 +$TMUX2 set -g status-format[0] '' || exit 1 +$TMUX2 resizew -y13 || exit 1 +sleep 1 +check_update top-status '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789' +$TMUX2 set -g status off || exit 1 +$TMUX2 resizew -y14 || exit 1 + +# Both the menu and floating panes obscure the base pane. The menu also +# obscures incremental output from the floating pane itself. +FLOAT=$($TMUX2 new-pane -dPF '#{pane_id}' -x24 -y8 -X4 -Y2 \ + "exec cat '$TMP/float'") || exit 1 +exec 4>"$TMP/float" +sleep 1 +check_update behind-float '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789' +printf '\033[4;1HABCDEFGHIJKLMNOPQRSTUVWX' >&4 +check_update float-text '' +printf '\033[4;1H\033[4hxyz\033[4l' >&4 +check_update float-insert '' +printf '\033[8;1Hone\r\ntwo\r\nthree\r\n' >&4 +check_update float-scroll '' +$TMUX2 kill-pane -t "$FLOAT" || exit 1 + +# Without a menu, the same whole-line and insert-mode paths must continue to +# respect floating panes. This also exercises split visible ranges. +$TMUX send Escape || exit 1 +FLOAT=$($TMUX2 new-pane -dPF '#{pane_id}' -x12 -y6 -X6 -Y3 \ + 'exec sleep 100') || exit 1 +$TMUX2 resizew -x60 -y14 || exit 1 +sleep 1 +check_update float-viewport '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwx' +check_update float-insert-mode '\033[6;1H\033[4habc\033[4l' +$TMUX2 kill-pane -t "$FLOAT" || exit 1 +$TMUX2 resizew -x40 -y14 || exit 1 + +# A menu covering an entire line leaves an empty visible range. Borderless +# menus and menus larger than the window must use their actual screen size. +menu -b none -x0 -y8 +check_update borderless '\033[7;1Habcdefghijklmnopqrstuvwxyz0123456789' +$TMUX send Escape || exit 1 +$TMUX2 display-menu -b simple -T live -C 0 -x0 -y8 \ + 'This menu is wider than the whole window' a '' beta b '' || exit 1 +sleep 1 +check_update full-width '\033[6;1Habcdefghijklmnopqrstuvwxyz0123456789' + +# Nonvisual output is still delivered while a menu is open. +printf '\033]52;c;bWVudS1jbGlwYm9hcmQ=\007' >&3 +sleep 1 +[ "$($TMUX show-buffer 2>/dev/null)" = menu-clipboard ] || + fail "menu blocked the clipboard update" + +# The pane grid must keep updating under the menu, both for capture-pane and +# for the redraw when the menu closes. +printf '\033[2J\033[6;7Hlatest content' >&3 +sleep 1 +$TMUX2 capturep -p -t%0 >"$TMP/actual" || exit 1 +grep -q 'latest content' "$TMP/actual" || fail "underlying pane is stale" +$TMUX send Escape || exit 1 +sleep 1 +$TMUX capturep -p >"$TMP/actual" || exit 1 +grep -q 'latest content' "$TMP/actual" || fail "menu close left stale content" + +exit 0 diff --git a/regress/new-pane-mouse-status.sh b/regress/new-pane-mouse-status.sh new file mode 100755 index 000000000..314476aaf --- /dev/null +++ b/regress/new-pane-mouse-status.sh @@ -0,0 +1,143 @@ +#!/bin/sh + +# A Ctrl-drag starting on the status line must not create a floating pane. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Lmouse-status-inner-$$ -f/dev/null" +TMUX2="$TEST_TMUX -Lmouse-status-outer-$$ -f/dev/null" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +wait_status() +{ + i=0 + while [ "$i" -lt 100 ]; do + screen=$($TMUX2 capture-pane -p -t outer:0.0) || exit 1 + if printf '%s\n' "$screen" | grep -q 'WINDOW WINDOW WINDOW' && + printf '%s\n' "$screen" | grep -q 'PANE PANE PANE PANE' && + printf '%s\n' "$screen" | grep -q 'CONTROL CONTROL CONTROL'; then + return + fi + sleep 0.05 + i=$((i + 1)) + done + fail "status lines were not redrawn" +} + +wait_option() +{ + option=$1 + i=0 + while [ "$i" -lt 100 ]; do + got=$($TMUX show -gv "$option" 2>/dev/null) + [ "$got" = "$2" ] && return + sleep 0.05 + i=$((i + 1)) + done + fail "got $option '$got', expected '$2'" +} + +mouse() +{ + seq=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $TMUX2 send-keys -t outer:0.0 -l "$seq" || fail "send mouse failed" + sleep 0.2 +} + +check_drag() +{ + start=$1 + end=$2 + sync=$3 + + if [ -n "$sync" ]; then + $TMUX set -g @drag-end '' || exit 1 + fi + + # First move along the status line, then into the pane. The first + # movement establishes the drag origin without dispatching new-pane. + mouse 16 10 "$start" M + mouse 48 11 "$start" M + mouse 48 16 "$end" M + mouse 48 18 "$end" M + mouse 16 18 "$end" m + + if [ -n "$sync" ]; then + wait_option @drag-end done + fi + + panes=$($TMUX list-panes -F '#{pane_id}') || fail "server exited" + [ "$panes" = "$BASE" ] || fail "status-line drag created a pane: $panes" +} + +$TMUX new-session -d -s inner -x 60 -y 20 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g default-command 'sleep 100' || exit 1 +BASE=$($TMUX list-panes -F '#{pane_id}') || exit 1 +$TMUX2 new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +check_drag 20 15 + +$TMUX set -g status 2 || exit 1 +sleep 0.5 +check_drag 20 14 + +$TMUX set -g status-position top || exit 1 +sleep 0.5 +check_drag 2 8 + +$TMUX set -g status on || exit 1 +sleep 0.5 +check_drag 1 7 + +# Check drags starting on window, pane and control ranges. +$TMUX set -g status 3 || exit 1 +$TMUX set -g 'status-format[0]' '#[range=window|0]WINDOW WINDOW WINDOW#[norange]' || exit 1 +$TMUX set -g 'status-format[1]' "#[range=pane|$BASE]PANE PANE PANE PANE#[norange]" || exit 1 +$TMUX set -g 'status-format[2]' '#[range=control|0]CONTROL CONTROL CONTROL#[norange]' || exit 1 +$TMUX bind -n C-MouseDragEnd1Pane set -g @drag-end done || exit 1 +wait_status + +row=1 +for range in window pane control; do + check_drag "$row" 9 sync + row=$((row + 1)) +done + +# A new drag inside the pane must still use the default Pane binding. +$TMUX set -g status on || exit 1 +sleep 0.5 +mouse 16 3 4 M +mouse 48 10 8 M +mouse 16 10 8 m +panes=$($TMUX list-panes -F '#{pane_floating_flag}') || fail "server exited" +[ "$panes" = "$(printf '0\n1')" ] || fail "pane drag did not create a pane" + +# Starting on the status line must not dispatch Empty bindings either. +id=$($TMUX list-panes -F '#{pane_id}' | tail -n 1) +$TMUX kill-pane -t "$id" || exit 1 +$TMUX break-pane -W -s "$BASE" || exit 1 +$TMUX resize-pane -t "$BASE" -x 10 -y 4 || exit 1 +$TMUX move-pane -t "$BASE" -P top-left || exit 1 +sleep 0.5 +check_drag 1 10 + +exit 0 diff --git a/regress/prompt-words-history.sh b/regress/prompt-words-history.sh index 6aaa5689c..e3a6882dd 100644 --- a/regress/prompt-words-history.sh +++ b/regress/prompt-words-history.sh @@ -131,6 +131,14 @@ printf '%s\n' "$captured" | grep -Fq 'show-buffer' || printf '%s\n' "$captured" | grep -Fq 'show-environment' || fail "ambiguous completion list was incomplete" $OUTER send-keys Escape || exit 1 +# Wait until Escape has closed the prompt before sending another Meta key. +# Otherwise the two can be parsed together as a single escape sequence. +i=0 +while capture | grep -Fq '(word)'; do + [ "$i" -lt 50 ] || fail "completion prompt did not close" + sleep 0.1 + i=$((i + 1)) +done # Add entries to both history rings through real prompts. bind_prompt 'history-command' diff --git a/regress/redraw-damage-accumulate.sh b/regress/redraw-damage-accumulate.sh new file mode 100644 index 000000000..3577cf603 --- /dev/null +++ b/regress/redraw-damage-accumulate.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +# Many wrapped rows crossing a panned viewport produce disjoint damage. +# Every row must survive the rectangle-count limit and subsequent merging. + +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 +INNER="$TEST_TMUX -Laccumulate-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Laccumulate-outer-$$ -f/dev/null" +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 +fail() +{ + echo "$*" >&2 + exit 1 +} +wait_marker() +{ + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$DIR/capture" || exit 1 + grep -q "$1" "$DIR/capture" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client did not receive $1" +} +cat >"$DIR/emitter.pl" <<'PERL' +use strict; +use warnings; +$| = 1; +for my $row (1 .. 80) { + printf "\e[%d;1HROW%02d-%s", $row, $row, 'abcdefghij' x 6; +} +print "\e[1;1H"; +while (!-e $ENV{TRIGGER}) { + select undef, undef, undef, 0.01; +} +my $output = ''; +# Each wrapped second row needs a separate damage rectangle. Keep the +# batch below the PTY read size so all 18 arrive in the same input pass. +for my $region (0 .. 17) { + my $top = 1 + $region * 4; + $output .= "\e[$top;1H" . ('A' x 80) . ('B' x 24) . + sprintf('DAMAGE%02d', $region) . ('B' x 48); +} +$output .= "\e[80;21HDONE"; +syswrite STDOUT, $output; +while (!-e "$ENV{TRIGGER}-merge") { + select undef, undef, undef, 0.01; +} +# Returning from the alternate screen requests a full-pane rectangle. A +# later wrapped-row rectangle overlaps it and must not shrink that damage. +$output = "\e[?1049h\e[40;21HALT-VISIBLE\e[?1049l"; +$output .= "\e[1;1H" . ('C' x 80) . ('D' x 24) . 'MERGE00' . ('D' x 49); +$output .= "\e[80;21HMERGED"; +syswrite STDOUT, $output; +sleep 100; +PERL +$INNER new-session -d -s inner -x 80 -y 80 \ + "TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1 +$INNER set -g status off || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g automatic-rename off || exit 1 +$INNER set -g status-interval 0 || exit 1 +$INNER set -as terminal-features ',screen:sync' || exit 1 +$INNER set -g pane-border-lines simple || exit 1 +$OUTER new-session -d -s outer -x 40 -y 80 'sleep 100' || exit 1 +$OUTER set -g status off || exit 1 +$OUTER set -g window-size manual || exit 1 +$OUTER set -g default-terminal screen || exit 1 +$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1 +wait_marker ROW80 +CLIENT=$($OUTER display -p -t outer:0.0 '#{pane_tty}') || exit 1 +$INNER refresh-client -t "$CLIENT" -R 20 || exit 1 +# Keep the emitter inactive so its writes use a synchronized frame. Otherwise +# its own queued output can force a full redraw and hide lost rectangles. +$INNER new-pane -x 6 -y 3 -X 65 -Y 74 'sleep 100' || exit 1 +sleep 0.2 +: >"$DIR/trigger" +wait_marker DONE +sleep 0.2 +$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1 +region=0 +while [ "$region" -lt 18 ]; do + marker=$(printf 'DAMAGE%02d' "$region") + sed -n "$((2 + region * 4))p" "$DIR/before" | grep -q "$marker" || + fail "missing damage for region $region" + region=$((region + 1)) +done +$INNER refresh-client -t "$CLIENT" || exit 1 +sleep 0.2 +$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1 +diff -u "$DIR/before" "$DIR/after" || fail "accumulated redraw differs from full redraw" +: >"$DIR/trigger-merge" +wait_marker MERGED +sleep 0.2 +$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1 +sed -n '2p' "$DIR/before" | grep -q MERGE00 || fail "merged damage lost the wrapped row" +$INNER refresh-client -t "$CLIENT" || exit 1 +sleep 0.2 +$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1 +diff -u "$DIR/before" "$DIR/after" || fail "merged redraw differs from full redraw" +exit 0 diff --git a/regress/redraw-damage-blocked-client.sh b/regress/redraw-damage-blocked-client.sh new file mode 100644 index 000000000..ca835036e --- /dev/null +++ b/regress/redraw-damage-blocked-client.sh @@ -0,0 +1,140 @@ +#!/bin/sh + +# One terminal stops reading while another consumes floating-pane damage. +# The slow client must catch up after the window's shared damage is cleared. + +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 +INNER="$TEST_TMUX -Lblocked-inner-$$ -f/dev/null" +FAST="$TEST_TMUX -Lblocked-fast-$$ -f/dev/null" +SLOW="$TEST_TMUX -Lblocked-slow-$$ -f/dev/null" +STOPPED= +cleanup() +{ + [ -n "$STOPPED" ] && kill -CONT "$STOPPED" 2>/dev/null + $FAST kill-server 2>/dev/null + $SLOW kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 +fail() +{ + echo "$*" >&2 + exit 1 +} +wait_marker() +{ + terminal=$1 + marker=$2 + i=0 + while [ "$i" -lt 100 ]; do + $terminal capture-pane -p >"$DIR/capture" || exit 1 + grep -q "$marker" "$DIR/capture" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client did not receive $marker" +} +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $FAST send-keys -l "$sequence" || exit 1 + sleep 0.2 +} +cat >"$DIR/emitter.pl" <<'PERL' +use strict; +use warnings; +$| = 1; +print 'READY'; +while (!-e "$ENV{TRIGGER}-flood") { + select undef, undef, undef, 0.01; +} +my $frame = 0; +while (!-e "$ENV{TRIGGER}-stop") { + my $output = ''; + for my $row (1 .. 20) { + $output .= "\e[$row;1H" . join('', map { chr(33 + ($_ + $frame) % 80) } 0 .. 77); + } + print $output; + $frame++; + select undef, undef, undef, 0.01; +} +for my $row (1 .. 20) { + printf "\e[%d;1HFINAL%02d-%s", $row, $row, '0123456789' x 7; +} +print "\e[20;1HDONE"; +sleep 100; +PERL +$INNER new-session -d -s inner -x 240 -y 120 \ + "TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1 +$INNER set -g status off || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g automatic-rename off || exit 1 +$INNER set -g status-interval 0 || exit 1 +$INNER set -g mouse on || exit 1 +$INNER set -g pane-border-lines simple || exit 1 +FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 \ + 'printf FLOAT; exec sleep 100') || exit 1 +for terminal in "$FAST" "$SLOW"; do + $terminal new-session -d -x 240 -y 120 'sleep 100' || exit 1 + $terminal set -g status off || exit 1 + $terminal set -g window-size manual || exit 1 + $terminal set -g default-terminal screen || exit 1 + $terminal respawn-pane -k "$INNER attach -t inner" || exit 1 + wait_marker "$terminal" READY +done +FASTCLIENT=$($FAST display -p '#{pane_tty}') || exit 1 +SLOWCLIENT=$($SLOW display -p '#{pane_tty}') || exit 1 +SLOWPID=$($SLOW display -p '#{pid}') || exit 1 +before=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1 +slowbefore=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1 +# The large terminal keeps this backlog below the discard threshold, so +# automatic recovery from discarded output cannot mask lost damage. +# Stop only our outer server, leaving its inner client attached to a PTY +# whose master is no longer read. This creates real terminal backpressure. +STOPPED=$SLOWPID +kill -STOP "$SLOWPID" || exit 1 +: >"$DIR/trigger-flood" +i=0 +while :; do + written=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1 + fastwritten=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1 + # More than a PTY can buffer has been queued for the stopped terminal, + # while the other terminal is still receiving the same output. + [ "$written" -gt "$((slowbefore + 65536))" ] && + [ "$fastwritten" -gt "$((before + 65536))" ] && break + [ "$i" -lt 100 ] || fail "slow client did not become blocked" + sleep 0.1 + i=$((i + 1)) +done +: >"$DIR/trigger-stop" +wait_marker "$FAST" DONE +[ "$($INNER display -p -c "$FASTCLIENT" '#{client_discarded}')" -eq 0 ] || + fail "fast client also became blocked" +mouse 0 12 6 M +mouse 32 42 6 M +mouse 0 42 6 m +[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 36 ] || fail "pane did not move" +$FAST capture-pane -p >"$DIR/fast-before" || exit 1 +# Ensure the fast client already restored the vacated frame. +sed -n '6p' "$DIR/fast-before" | grep -q '^FINAL06-0123456789' || + fail "fast client did not restore old footprint" +kill -CONT "$SLOWPID" || exit 1 +STOPPED= +wait_marker "$SLOW" DONE +# Allow queued terminal output and deferred redraws to drain. +sleep 0.5 +[ "$($INNER display -p -c "$SLOWCLIENT" '#{client_discarded}')" -eq 0 ] || + fail "discard recovery could mask lost deferred damage" +$SLOW capture-pane -p >"$DIR/slow-before" || exit 1 +diff -u "$DIR/fast-before" "$DIR/slow-before" || fail "slow client did not catch up" +$INNER refresh-client -t "$FASTCLIENT" || exit 1 +sleep 0.2 +$FAST capture-pane -p >"$DIR/after" || exit 1 +diff -u "$DIR/fast-before" "$DIR/after" || fail "damage redraw differed from full redraw" +exit 0 diff --git a/regress/redraw-damage-only.sh b/regress/redraw-damage-only.sh new file mode 100755 index 000000000..a01824019 --- /dev/null +++ b/regress/redraw-damage-only.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# Check a redraw callback which has no accompanying client redraw flags. A +# wrapped row crossing a panned viewport cannot use the direct tty path. + +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 +INNER="$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Ldamage-only-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +TRIGGER=$DIR/trigger +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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 + 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_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_inner_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t inner:0.0 2>/dev/null | + grep -q "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner pane did not contain $marker" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +for my $row (1 .. 12) { + print "\e[$row;1H", 'o' x 79; +} +print "\e[1;1H"; +while (!-e $ENV{TRIGGER}) { + select undef, undef, undef, 0.01; +} + +my $second = ('B' x 24) . 'DAMAGE-ONLY' . ('B' x 45); +print "\e[5;1H", ('A' x 80), $second; +sleep 100; +PERL + +$INNER new-session -d -s inner -x 80 -y 12 \ + "TRIGGER='$TRIGGER' perl '$EMITTER'" || 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 -x 40 -y 12 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +$INNER refresh-client -t "$CLIENT" -R 20 || exit 1 +wait_outer_has oooooooooo + +: >"$TRIGGER" +wait_inner_has DAMAGE-ONLY +wait_outer_has DAMAGE-ONLY + +exit 0 diff --git a/regress/redraw-damage-viewports.sh b/regress/redraw-damage-viewports.sh new file mode 100644 index 000000000..2cfe1cffc --- /dev/null +++ b/regress/redraw-damage-viewports.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +# Moving and interactively resizing floating panes must restore both clients, +# including a smaller client panned horizontally and vertically. + +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 +INNER="$TEST_TMUX -Lviewports-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lviewports-outer-$$ -f/dev/null" + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 +fail() +{ + echo "$*" >&2 + exit 1 +} +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t wide:0.0 -l "$sequence" || exit 1 + sleep 0.2 +} +assert_scene() +{ + # Compare what each terminal actually received with a fresh full redraw. + # Capture both first: refreshing one client must not repair the other. + for target in wide small; do + $OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-before" || exit 1 + done + $INNER refresh-client -t "$WIDE" || exit 1 + $INNER refresh-client -t "$SMALL" || exit 1 + sleep 0.2 + for target in wide small; do + $OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-after" || exit 1 + diff -u "$DIR/$target-before" "$DIR/$target-after" || + fail "$1: $target client differed from a full redraw" + done +} + +cat >"$DIR/background.pl" <<'PERL' +$| = 1; +for my $row (1 .. 24) { + printf "\e[%d;1HROW%02d-", $row, $row; + print '0123456789' x 7; +} +sleep 100; +PERL +$INNER new-session -d -s inner -x 80 -y 24 "perl '$DIR/background.pl'" || exit 1 +$INNER set -g status off || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g automatic-rename off || exit 1 +$INNER set -g status-interval 0 || exit 1 +$INNER set -g mouse on || exit 1 +$INNER set -g default-command 'sleep 100' || exit 1 +$INNER set -g pane-border-lines simple || exit 1 +$OUTER new-session -d -s wide -x 80 -y 24 'sleep 100' || exit 1 +$OUTER set -g status off || exit 1 +$OUTER set -g window-size manual || exit 1 +$OUTER set -g default-terminal screen || exit 1 +$OUTER new-session -d -s small -x 40 -y 12 'sleep 100' || exit 1 +for target in wide small; do + $OUTER respawn-pane -k -t "$target:0.0" "$INNER attach -t inner" || exit 1 +done +i=0 +while [ "$($INNER list-clients | wc -l)" -ne 2 ]; do + [ "$i" -lt 50 ] || fail "two clients did not attach" + sleep 0.1 + i=$((i + 1)) +done +WIDE=$($OUTER display -p -t wide:0.0 '#{pane_tty}') || exit 1 +SMALL=$($OUTER display -p -t small:0.0 '#{pane_tty}') || exit 1 +$INNER refresh-client -t "$SMALL" -R 20 || exit 1 +$INNER refresh-client -t "$SMALL" -D 6 || exit 1 +FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 16 -y 6 -X 25 -Y 9) || exit 1 +sleep 0.3 +assert_scene initial +# Confirm that the two clients really have different viewports. +[ "$(head -1 "$DIR/wide-before" | cut -c1-6)" = ROW01- ] || fail "wrong wide viewport" +[ "$(head -1 "$DIR/small-before" | cut -c1-6)" != ROW01- ] || fail "small client was not panned" + +# Meta-drag the body using move-pane -M, including a partially clipped position. +mouse 8 30 12 M +mouse 40 48 15 M +[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 44 ] || fail "Meta-drag did not move pane" +assert_scene move-right +mouse 40 28 10 M +assert_scene move-back +mouse 8 28 10 m + +# Ctrl-drag creates a new floating pane, then changes its size while held. +mouse 16 52 14 M +mouse 48 75 23 M +NEW=$($INNER display -p '#{pane_id}') || exit 1 +[ "$NEW" != "$FLOAT" ] || fail "Ctrl-drag did not create a pane" +[ "$($INNER display -p -t "$NEW" '#{pane_floating_flag}')" -eq 1 ] || fail "new pane is not floating" +assert_scene create +mouse 48 64 19 M +[ "$($INNER display -p -t "$NEW" '#{pane_width}')" -eq 11 ] || fail "Ctrl-drag did not shrink pane" +assert_scene shrink +mouse 16 64 19 m +assert_scene release +exit 0 diff --git a/regress/redraw-multiclient.sh b/regress/redraw-multiclient.sh new file mode 100755 index 000000000..cdcbe01ff --- /dev/null +++ b/regress/redraw-multiclient.sh @@ -0,0 +1,186 @@ +#!/bin/sh + +# Redraw a moved floating pane on both attached clients viewing the same +# window. Window redraw work must not be consumed by only one client. +# +# Uses ASCII pane borders (rather than the default UTF-8 box-drawing) because +# this test nests a real tmux client inside another tmux's pane to get a +# genuine terminal to capture from; that nested-tmux relay has been observed +# to mis-render a cell that previously held a multi-byte UTF-8 border +# character being overwritten later by plain content, on the outer instance's +# own interpretation, independent of anything the inner tmux sends. That is a +# nested-test-harness artifact, not a real tmux bug - confirmed by replaying +# the exact same drag sequence against a real terminal (xterm), where it +# never reproduces. ASCII borders avoid the artifact entirely. + +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 +INNER="$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lredraw-multi-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + count=$($INNER list-clients 2>/dev/null | wc -l) + [ "$count" -eq 2 ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "two inner clients did not attach" +} + +wait_outer_has() +{ + target=$1 + marker=$2 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t "$target" >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer pane $target did not show $marker" +} + +wait_float_left() +{ + comparison=$1 + limit=$2 + i=0 + while [ "$i" -lt 50 ]; do + left=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') + if [ "$comparison" = gt ] && [ "$left" -gt "$limit" ]; then + return 0 + fi + if [ "$comparison" = lt ] && [ "$left" -lt "$limit" ]; then + return 0 + fi + sleep 0.1 + i=$((i + 1)) + done + fail "floating pane did not move" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$2" "$3" "$4" "$5") + $OUTER send-keys -t "$1" -l "$sequence" || exit 1 + sleep 0.1 +} + +drag_float() +{ + target=$1 + startcol=$2 + startrow=$3 + endcol=$4 + mouse "$target" 0 "$startcol" "$startrow" M + mouse "$target" 32 "$endcol" "$startrow" M + mouse "$target" 0 "$endcol" "$startrow" m +} + +assert_scene() +{ + target=$1 + base=$2 + firstcol=$3 + lastcol=$4 + + $OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1 + + # With ASCII (simple) borders every corner and junction is the same + # '+', so one rectangular floating pane always draws exactly 4 of + # them; more means a stale frame was left behind somewhere. + corners=$(grep -o '+' "$CAPTURE" | wc -l) + [ "$corners" -eq 4 ] || + fail "outer pane $target had $corners floating frames" + + sed -n '6,11p' "$base" | cut -c"$firstcol-$lastcol" >"$DIR/want" + sed -n '6,11p' "$CAPTURE" | cut -c"$firstcol-$lastcol" >"$DIR/got" + cmp -s "$DIR/want" "$DIR/got" || + fail "outer pane $target did not restore the old floating area" +} + +C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; exec sleep 100'" + +$INNER new-session -d -s inner -x 60 -y 20 "$C" || 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-option -g default-command 'sleep 100' || exit 1 +$INNER set-option -g pane-border-lines simple || exit 1 + +$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen || exit 1 +$OUTER split-window -h -t outer:0.0 'sleep 100' || exit 1 + +PANES=$($OUTER list-panes -t outer:0 -F '#{pane_id} #{pane_left}') +LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1) +RIGHT=$(echo "$PANES" | sort -k2 -n | tail -1 | cut -d' ' -f1) +[ -n "$LEFT" ] && [ -n "$RIGHT" ] || fail "could not find outer panes" + +$OUTER respawn-pane -k -t "$LEFT" \ + "$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 +$OUTER respawn-pane -k -t "$RIGHT" \ + "$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_clients +wait_outer_has "$LEFT" BG-ROW-19 +wait_outer_has "$RIGHT" BG-ROW-19 +$OUTER capture-pane -p -t "$LEFT" >"$DIR/base-left" || exit 1 +$OUTER capture-pane -p -t "$RIGHT" >"$DIR/base-right" || exit 1 + +FLOAT=$($INNER new-pane -dPF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) || + fail "could not create floating pane" +wait_outer_has "$LEFT" '+' +wait_outer_has "$RIGHT" '+' + +FTOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') +FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +FWIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}') +GRABCOL=$((FLEFT + FWIDTH / 2 + 1)) + +# Move right through one client and require both clients to restore the old +# left-hand footprint. +drag_float "$LEFT" "$GRABCOL" "$FTOP" $((GRABCOL + 30)) +wait_float_left gt 30 +assert_scene "$LEFT" "$DIR/base-left" 1 20 +assert_scene "$RIGHT" "$DIR/base-right" 1 20 + +# Move back through the other client and check the old right-hand footprint. +FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') +GRABCOL=$((FLEFT + FWIDTH / 2 + 1)) +drag_float "$RIGHT" "$GRABCOL" "$FTOP" $((GRABCOL - 30)) +wait_float_left lt 10 +assert_scene "$LEFT" "$DIR/base-left" 35 60 +assert_scene "$RIGHT" "$DIR/base-right" 35 60 + +exit 0 diff --git a/regress/redraw-screen-write.sh b/regress/redraw-screen-write.sh new file mode 100755 index 000000000..0d007f609 --- /dev/null +++ b/regress/redraw-screen-write.sh @@ -0,0 +1,208 @@ +#!/bin/sh + +# Check that full and region screen-write fallbacks update an attached client, +# not only tmux's internal pane grid. + +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 +EMITTER=$DIR/emitter.pl +CAPTURE=$DIR/capture +INNER= +OUTER= +N=0 + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + [ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null + [ -n "$INNER" ] && $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_outer_lacks() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" || return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client still showed $marker" +} + +wait_inner_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t inner:0.0 2>/dev/null | + grep -q "$marker" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner pane did not contain $marker" +} + +wait_inner_lacks() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER capture-pane -p -t inner:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" || return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner pane still contained $marker" +} + +setup() +{ + mode=$1 + [ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null + [ -n "$INNER" ] && $INNER kill-server 2>/dev/null + N=$((N + 1)) + INNER="$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null" + OUTER="$TEST_TMUX -Lredraw-write-outer-$$-$N -f/dev/null" + + $INNER new-session -d -s inner -x 40 -y 12 \ + "MODE=$mode READY='$DIR/ready-$N' TRIGGER='$DIR/trigger-$N' perl '$EMITTER'" || + 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 -x 40 -y 12 'sleep 100' || exit 1 + $OUTER set-option -g status off || exit 1 + $OUTER set-option -g window-size manual || exit 1 + $OUTER set-option -g default-terminal screen || exit 1 + $OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null attach-session -t inner" || + exit 1 +} + +trigger() +{ + : >"$DIR/trigger-$N-${1:-1}" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +my $mode = $ENV{MODE}; +my $ready = $ENV{READY}; +my $trigger = $ENV{TRIGGER}; + +sub fill_screen { + my ($prefix) = @_; + print "\e[2J\e[H"; + for my $row (0 .. 11) { + printf "\e[%d;1H%s-ROW-%02d", $row + 1, $prefix, $row; + } +} + +if ($mode eq 'ris') { + fill_screen('RIS'); +} elsif ($mode eq 'alternate') { + fill_screen('BASE'); +} elsif ($mode eq 'scroll') { + fill_screen('SCROLL'); +} else { + die "unknown mode $mode\n"; +} + +open my $fh, '>', $ready or die "$ready: $!\n"; +close $fh; +while (!-e "$trigger-1") { + select undef, undef, undef, 0.01; +} + +if ($mode eq 'ris') { + print "\ec"; +} elsif ($mode eq 'alternate') { + print "\e[?1049h"; + fill_screen('ALT'); + while (!-e "$trigger-2") { + select undef, undef, undef, 0.01; + } + print "\e[?1049l"; +} else { + print "\e[12;1H\r\nSCROLL-NEW"; +} +sleep 100; +PERL + +# RIS clears the complete screen. The source pane and attached client must both +# lose every old row. +setup ris +wait_outer_has RIS-ROW-11 +trigger +wait_inner_lacks RIS-ROW +wait_outer_lacks RIS-ROW + +# Leaving the alternate screen restores every row of the base screen. +setup alternate +wait_outer_has BASE-ROW-11 +trigger +wait_inner_has ALT-ROW-11 +wait_outer_has ALT-ROW-11 +wait_outer_lacks BASE-ROW +trigger 2 +wait_inner_has BASE-ROW-11 +wait_outer_has BASE-ROW-11 +wait_outer_lacks ALT-ROW + +# Scrolling a pane which is narrower than the terminal redraws its complete +# region. Check the physical client row by row after the source grid shifts. +setup scroll +$INNER split-window -h -t inner:0 'sleep 100' || exit 1 +wait_outer_has SCROLL-ROW-11 +trigger +wait_inner_has SCROLL-NEW +wait_outer_has SCROLL-NEW +row=1 +while [ "$row" -le 11 ]; do + expected=$(printf 'SCROLL-ROW-%02d' "$row") + actual=$(sed -n "${row}p" "$CAPTURE") + case "$actual" in + "$expected"*) ;; + *) fail "outer row $row was not redrawn as $expected" ;; + esac + row=$((row + 1)) +done +actual=$(sed -n '12p' "$CAPTURE") +case "$actual" in +SCROLL-NEW*) ;; +*) fail "outer bottom row was not redrawn as SCROLL-NEW" ;; +esac + +exit 0 diff --git a/regress/respawn-pane-empty.sh b/regress/respawn-pane-empty.sh new file mode 100644 index 000000000..2821a9534 --- /dev/null +++ b/regress/respawn-pane-empty.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# respawn-pane -k -E leaves an empty pane, which must not be treated as exited +# when the process it replaced is reaped + +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 +sleep 1 + +# The only pane of the only session: the server must not exit. +$TMUX new -d 'sleep 1000' || exit 1 +$TMUX respawn-pane -k -E || exit 1 +sleep 1 +$TMUX has-session -t0 2>/dev/null || exit 1 +$TMUX kill-server 2>/dev/null +sleep 1 + +# With remain-on-exit the pane must not be dead or keep the old tty. +$TMUX new -d 'sleep 1000' \; set -g remain-on-exit on || exit 1 +$TMUX respawn-pane -k -E || exit 1 +sleep 1 +[ "$($TMUX display -p '#{pane_dead}:#{pane_tty}')" = "0:" ] || exit 1 +$TMUX kill-server 2>/dev/null + +exit 0 diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh old mode 100644 new mode 100755 index c3b65a970..aeecff05b --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -30,21 +30,47 @@ fail() { } compare() { - sleep 1 - $TMUX capturep -p $2 >$TMP || exit 1 if [ -n "$GENERATE" ]; then + sleep 1 + $TMUX capturep -p $2 >$TMP || exit 1 cp $TMP "$RESULTS/$1.result" || exit 1 echo "generated $1" - else - cmp -s $TMP "$RESULTS/$1.result" || \ - fail "scene $1 differs from $RESULTS/$1.result" + return fi + + tries=0 + while [ "$tries" -lt 100 ]; do + $TMUX capturep -p $2 >$TMP || exit 1 + cmp -s $TMP "$RESULTS/$1.result" && return + sleep 0.1 + tries=$((tries + 1)) + done + fail "scene $1 differs from $RESULTS/$1.result" +} + +wait_for_pids() { + for pid in $1; do + tries=0 + while kill -0 "$pid" 2>/dev/null; do + tries=$((tries + 1)) + [ "$tries" -lt 100 ] || \ + fail "timed out waiting for pane $pid to exit" + sleep 0.05 + done + done } # new_scene : fresh inner window of the given window size. new_scene() { - $TMUX2 neww -d "sh -c 'printf base; exec sleep 100'" || exit 1 - $TMUX2 selectw -t:\$ || exit 1 + if [ -n "$window" ]; then + pids=$($TMUX2 list-panes -t"$window" -F '#{pane_pid}') || exit 1 + $TMUX2 killw -t"$window" || exit 1 + wait_for_pids "$pids" + fi + + window=$($TMUX2 neww -dP -F '#{window_id}' \ + "sh -c 'printf base; exec sleep 100'") || exit 1 + $TMUX2 selectw -t"$window" || exit 1 $TMUX2 resizew -x$1 -y$2 || exit 1 } @@ -74,6 +100,7 @@ $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 +window= $TMUX2 set -g status off || exit 1 $TMUX2 set -g window-size manual || exit 1 $TMUX2 set -g pane-border-format " #{pane_title} " || exit 1 diff --git a/regress/swap-pane-zoom-error.sh b/regress/swap-pane-zoom-error.sh new file mode 100644 index 000000000..0d85f72f1 --- /dev/null +++ b/regress/swap-pane-zoom-error.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# swap-pane refusing a floating pane must leave the zoom alone + +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 + +TMP=$(mktemp) +trap "rm -f $TMP; $TMUX kill-server 2>/dev/null" 0 1 15 + +$TMUX new -d +$TMUX splitw -d +$TMUX new-pane -d + +echo $($TMUX lsp -F'#{pane_id}:#{pane_floating_flag}') >$TMP +(echo "%0:0 %1:0 %2:1"|cmp -s - $TMP) || exit 1 + +$TMUX resize-pane -Z -t %0 +[ "$($TMUX display -p '#{window_zoomed_flag}')" = 1 ] || exit 1 + +$TMUX swap-pane -Z -D -t %2 +[ "$?" = 0 ] && exit 1 +[ "$($TMUX display -p '#{window_zoomed_flag}')" = 1 ] || exit 1 + +$TMUX swap-pane -Z -U -t %2 +[ "$?" = 0 ] && exit 1 +[ "$($TMUX display -p '#{window_zoomed_flag}')" = 1 ] || exit 1 + +# A swap that is allowed still happens. +$TMUX swap-pane -D -t %0 || exit 1 +echo $($TMUX lsp -F'#{pane_id}') >$TMP +(echo "%1 %0 %2"|cmp -s - $TMP) || exit 1 + +$TMUX kill-server 2>/dev/null + +exit 0 diff --git a/regress/switch-client-redraw.sh b/regress/switch-client-redraw.sh new file mode 100755 index 000000000..a03c382dd --- /dev/null +++ b/regress/switch-client-redraw.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Switching windows in the same session must redraw the attached client. The +# session object is shared, so its current winlink cannot be compared after it +# has already been changed. + +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 +INNER="$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lswitch-redraw-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&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 + 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_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +$INNER new-session -d -s inner -x 40 -y 8 \ + "printf '\033[2J\033[HA-WINDOW'; exec sleep 100" || exit 1 +$INNER new-window -d -t inner:1 \ + "printf '\033[2J\033[HB-WINDOW'; exec sleep 100" || 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 -x 40 -y 8 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null attach-session -t inner:0" || + exit 1 + +wait_for_client +wait_outer_has A-WINDOW + +$INNER switch-client -c "$CLIENT" -t inner:1.0 || exit 1 +[ "$($INNER display-message -p -t inner '#{window_index}')" -eq 1 ] || + fail "server did not select window 1" +wait_outer_has B-WINDOW + +exit 0 diff --git a/regress/switch-mode.sh b/regress/switch-mode.sh index 5f3fe8c12..bfb87dc2b 100644 --- a/regress/switch-mode.sh +++ b/regress/switch-mode.sh @@ -63,6 +63,22 @@ wait_count() fail "found $count '$marker' rows, expected $want" } +wait_first() +{ + marker=$1 + want=$2 + i=0 + while [ "$i" -lt 50 ]; do + got=$(capture | grep -F "$marker" | head -1) + case "$got" in + "$want"*) return 0 ;; + esac + sleep 0.1 + i=$((i + 1)) + done + fail "first '$marker' row is '$got', expected prefix '$want'" +} + wait_format() { target=$1 @@ -150,6 +166,22 @@ case "$picked" in *) fail "window selection produced '$picked', expected '=bravo:0.'" ;; esac +# -O selects the sort order and -r reverses it. By name the aaa window is +# first, by index main is, and by name reversed pick-two is. +$INNER new-window -d -t alpha: -n aaa 'exec sleep 100' || exit 1 +$INNER switch-mode -w -t alpha:main.0 -F 'ORDER #{window_name}' -O name || exit 1 +wait_first 'ORDER ' 'ORDER aaa' +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{pane_in_mode}' 0 +$INNER switch-mode -w -t alpha:main.0 -F 'ORDER #{window_name}' -O index || exit 1 +wait_first 'ORDER ' 'ORDER last' +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{pane_in_mode}' 0 +$INNER switch-mode -w -t alpha:main.0 -F 'ORDER #{window_name}' -O name -r || exit 1 +wait_first 'ORDER ' 'ORDER pick-two' +$INNER send-keys -t alpha:main.0 Escape || exit 1 +wait_format alpha:main.0 '#{pane_in_mode}' 0 + # 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 diff --git a/regress/tty-clamp-area-status-top.sh b/regress/tty-clamp-area-status-top.sh new file mode 100755 index 000000000..901d0d760 --- /dev/null +++ b/regress/tty-clamp-area-status-top.sh @@ -0,0 +1,127 @@ +#!/bin/sh + +# Clearing an area of a pane that is only partly visible in a client smaller +# than the window must account for status lines at the top. The area is +# clamped in window coordinates but drawn in terminal coordinates, which are +# shifted down by the status lines. +# +# Phase 1: the viewport is at the top, so the bottom of the cleared area is +# hidden; the last rows on the terminal must still be cleared. +# Phase 2: the viewport is at the bottom, so the top of the cleared area is +# hidden; this used to fail with "tty_clamp_area: y too big". + +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 +INNER="$TEST_TMUX -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" +CAPTURE=$DIR/capture + +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 + count=$($INNER list-clients 2>/dev/null | wc -l) + [ "$count" -eq 1 ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_for_marker() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $INNER has-session 2>/dev/null || fail "inner server exited" + $OUTER capture-pane -p -t outer >"$CAPTURE" || exit 1 + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "client did not receive $marker" +} + +# Fill all 20 rows, then clear from the top and print a marker on a row that +# is visible in the current viewport. +cat >"$DIR/emitter.pl" <<'PERL' +use strict; +use warnings; + +sub wait_for { + my ($file) = @_; + select undef, undef, undef, 0.01 while !-e $file; +} + +$| = 1; +for my $phase (1 .. 2) { + wait_for("$ENV{TRIGGER}-fill-$phase"); + print "\e[H", join("\r\n", ("FILL") x 20); + wait_for("$ENV{TRIGGER}-clear-$phase"); + my $row = $phase == 1 ? 1 : 20; + print "\e[H\e[J\e[$row;1HCLEARED$phase"; +} +sleep 100; +PERL + +$INNER new-session -d -s inner -x 40 -y 20 \ + "TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1 +$INNER set -g window-size manual || exit 1 +$INNER set -g status 2 || exit 1 +$INNER set -g status-position top || exit 1 +$INNER set -g status-interval 0 || exit 1 +$INNER set -g automatic-rename off || exit 1 + +# The client is as large as the window, so with two status lines only 18 +# window rows are visible. +$OUTER new-session -d -s outer -x 40 -y 20 'sleep 100' || exit 1 +$OUTER set -g status off || exit 1 +$OUTER set -g window-size manual || exit 1 +$OUTER set -g default-terminal screen || exit 1 +$OUTER respawn-pane -k -t outer "$INNER attach-session -t inner" || exit 1 +wait_for_client +NAME=$($OUTER display-message -p -t outer '#{pane_tty}') || exit 1 + +for phase in 1 2; do + if [ "$phase" -eq 1 ]; then + PAN=-U + else + PAN=-D + fi + : >"$DIR/trigger-fill-$phase" + sleep 0.2 + $INNER refresh-client -t "$NAME" $PAN 100 || exit 1 + sleep 0.5 + + : >"$DIR/trigger-clear-$phase" + wait_for_marker "CLEARED$phase" + sleep 0.2 + $INNER has-session 2>/dev/null || fail "inner server exited" + $OUTER capture-pane -p -t outer >"$CAPTURE" || exit 1 + if grep -q FILL "$CAPTURE"; then + cat "$CAPTURE" >&2 + fail "phase $phase left rows uncleared" + fi +done + +exit 0 diff --git a/regress/tty-keys-extended-escape.sh b/regress/tty-keys-extended-escape.sh new file mode 100644 index 000000000..5ebdf1b6c --- /dev/null +++ b/regress/tty-keys-extended-escape.sh @@ -0,0 +1,97 @@ +#!/bin/sh + +# An Escape followed by an extended key must remain a separate key. The +# sequence's leading Escape must not be consumed as the second half of +# M-Escape. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -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 +trap 'exit 1' 1 2 15 + +$INNER new-session -d -s inner -x80 -y10 "stty raw -echo; exec cat -v" || + exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g assume-paste-time 0 || exit 1 +$INNER set-option -s escape-time 1000 || exit 1 +$INNER set-option -s extended-keys on || exit 1 +$INNER unbind-key -a -T root || exit 1 +$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g assume-paste-time 0 || exit 1 + +i=0 +while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do + [ "$i" -lt 100 ] || fail 'inner client did not attach' + sleep 0.05 + i=$((i + 1)) +done + +bind_key() +{ + $INNER bind-key -n "$1" send-keys -l "$2" || exit 1 +} +bind_key Escape E +bind_key M-Escape A +bind_key C-a C +bind_key M-C-a M +bind_key M-Up U +bind_key M-DC D + +expected_output= + +send_raw() +{ + # Hex arguments avoid command separators in a partial sequence. + bytes=$(printf '%s' "$1" | od -An -v -tx1) + $OUTER send-keys -H $bytes || exit 1 +} + +assert_events() +{ + name=$1 + expected_output=$expected_output$2 + send_raw "$3" + if [ "$#" -eq 4 ]; then + sleep 0.05 + send_raw "$4" + fi + i=0 + while [ "$i" -lt 100 ]; do + actual=$($INNER capture-pane -p | tr -d '\n') + [ "$actual" = "$expected_output" ] && return 0 + sleep 0.05 + i=$((i + 1)) + done + fail "$name: got '$actual', expected '$expected_output'" +} + +assert_events 'CSI u' C "$(printf '\033[97;5u')" +assert_events 'Escape and CSI u' EC "$(printf '\033\033[97;5u')" +assert_events 'Escape and xterm' EC "$(printf '\033\033[27;5;97~')" +assert_events 'Escape and CSI u with Meta' EM "$(printf '\033\033[97;7u')" +assert_events 'split CSI u' EC "$(printf '\033\033[97;')" '5u' +assert_events 'split xterm' EC "$(printf '\033\033[27;5')" ';97~' +assert_events 'CSI u split after two Escapes' EC "$(printf '\033\033')" '[97;5u' +assert_events 'xterm split after two Escapes' EC "$(printf '\033\033')" '[27;5;97~' +assert_events 'M-Escape' A "$(printf '\033\033')" +assert_events 'M-Up' U "$(printf '\033\033[A')" +assert_events 'M-DC' D "$(printf '\033\033[3~')" + +exit 0 diff --git a/regress/tty-keys-focus-escape.sh b/regress/tty-keys-focus-escape.sh new file mode 100644 index 000000000..8d6c3d831 --- /dev/null +++ b/regress/tty-keys-focus-escape.sh @@ -0,0 +1,143 @@ +#!/bin/sh + +# An Escape followed by a focus report must remain a separate key. The +# report's leading Escape must not be consumed as the second half of M-Escape, +# and the focus change must still reach the client and the pane. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -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 +trap 'exit 1' 1 2 15 + +$INNER new-session -d -s inner -x80 -y10 \ + "printf '\033[?1004h'; stty raw -echo; exec cat -v" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g assume-paste-time 0 || exit 1 +$INNER set-option -g focus-events on || exit 1 +$INNER set-option -s escape-time 1000 || exit 1 +$INNER unbind-key -a -T root || exit 1 +$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g assume-paste-time 0 || exit 1 + +i=0 +while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do + [ "$i" -lt 100 ] || fail 'inner client did not attach' + sleep 0.05 + i=$((i + 1)) +done + +bind_key() +{ + $INNER bind-key -n "$1" send-keys -l "$2" || exit 1 +} +bind_key Escape E +bind_key M-Escape A +bind_key M-Up U + +send_raw() +{ + # Hex arguments avoid command separators in a partial sequence. + bytes=$(printf '%s' "$1" | od -An -v -tx1) + $OUTER send-keys -H $bytes || exit 1 +} + +capture() +{ + $INNER capture-pane -p | tr -d '\n' +} + +focused() +{ + case $($INNER list-clients -F '#{client_flags}') in + *focused*) + echo yes + ;; + *) + echo no + ;; + esac +} + +# Start unfocused, whatever the state after attaching, once the pane has +# enabled focus reporting. +i=0 +while :; do + send_raw "$(printf '\033[I\033[O')" + sleep 0.05 + case $(capture) in + *'^[[O') + [ "$(focused)" = no ] && break + ;; + esac + [ "$i" -lt 100 ] || fail 'pane did not receive focus reports' + i=$((i + 1)) +done +expected_output=$(capture) + +# A focus report is forwarded to the pane directly, while the Escape binding +# runs from the command queue, so the pane may see them in either order. The +# second expected output, if not empty, is that other order. +assert_events() +{ + name=$1 + expected=$expected_output$2 + if [ -n "$3" ]; then + alternative=$expected_output$3 + else + alternative=$expected + fi + expected_focus=$4 + send_raw "$5" + if [ "$#" -eq 6 ]; then + sleep 0.05 + send_raw "$6" + fi + i=0 + while [ "$i" -lt 100 ]; do + actual=$(capture) + actual_focus=$(focused) + if [ "$actual" = "$expected" ] || + [ "$actual" = "$alternative" ]; then + if [ "$actual_focus" = "$expected_focus" ]; then + expected_output=$actual + return 0 + fi + fi + sleep 0.05 + i=$((i + 1)) + done + fail "$name: got '$actual' focused=$actual_focus," \ + "expected '$expected' focused=$expected_focus" +} + +assert_events 'FocusIn' '^[[I' '' yes "$(printf '\033[I')" +assert_events 'FocusOut' '^[[O' '' no "$(printf '\033[O')" +assert_events 'Escape and FocusIn' 'E^[[I' '^[[IE' yes \ + "$(printf '\033\033[I')" +assert_events 'Escape and FocusOut' 'E^[[O' '^[[OE' no \ + "$(printf '\033\033[O')" +assert_events 'split FocusIn' 'E^[[I' '^[[IE' yes \ + "$(printf '\033\033[')" 'I' +assert_events 'FocusOut split after two Escapes' 'E^[[O' '^[[OE' no \ + "$(printf '\033\033')" '[O' +assert_events 'M-Escape' A '' no "$(printf '\033\033')" +assert_events 'M-Up' U '' no "$(printf '\033\033[A')" + +exit 0 diff --git a/regress/tty-keys-mouse-escape.sh b/regress/tty-keys-mouse-escape.sh new file mode 100644 index 000000000..569fa6642 --- /dev/null +++ b/regress/tty-keys-mouse-escape.sh @@ -0,0 +1,98 @@ +#!/bin/sh + +# An Escape followed by a mouse report must remain a separate key. The +# report's leading Escape must not be consumed as the second half of M-Escape. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -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 +trap 'exit 1' 1 2 15 + +$INNER new-session -d -s inner -x80 -y10 \ + "stty raw -echo; printf '\033[?1003h\033[?1006h'; exec cat -v" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g mouse on || exit 1 +$INNER set-option -g assume-paste-time 0 || exit 1 +$INNER set-option -s escape-time 1000 || exit 1 +$INNER unbind-key -a -T root || exit 1 +$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g assume-paste-time 0 || exit 1 + +i=0 +while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do + [ "$i" -lt 100 ] || fail 'inner client did not attach' + sleep 0.05 + i=$((i + 1)) +done + +bind_key() +{ + $INNER bind-key -n "$1" send-keys -l "$2" || exit 1 +} +bind_key Escape E +bind_key M-Escape A +bind_key M-a B +bind_key M-Up U + +expected_output= + +send_raw() +{ + # Hex arguments avoid command separators in a partial mouse report. + bytes=$(printf '%s' "$1" | od -An -v -tx1) + $OUTER send-keys -H $bytes || exit 1 +} + +assert_events() +{ + name=$1 + expected_output=$expected_output$2 + send_raw "$3" + if [ "$#" -eq 4 ]; then + sleep 0.05 + send_raw "$4" + fi + i=0 + while [ "$i" -lt 100 ]; do + actual=$($INNER capture-pane -p | tr -d '\n') + [ "$actual" = "$expected_output" ] && return 0 + sleep 0.05 + i=$((i + 1)) + done + fail "$name: got '$actual', expected '$expected_output'" +} + +assert_events 'SGR motion' 'E^[[<35;3;3M' "$(printf '\033\033[<35;3;3M')" +assert_events 'SGR press' 'E^[[<0;3;3M' "$(printf '\033\033[<0;3;3M')" +assert_events 'SGR release' 'E^[[<0;3;3m' "$(printf '\033\033[<0;3;3m')" +assert_events 'SGR wheel' 'E^[[<64;3;3M' "$(printf '\033\033[<64;3;3M')" +assert_events 'X10 motion' 'E^[[MC##' "$(printf '\033\033[MC##')" +assert_events 'X10 press' 'E^[[M ##' "$(printf '\033\033[M ##')" +assert_events 'split SGR report' 'E^[[<35;3;3M' "$(printf '\033\033[<35;')" '3;3M' +assert_events 'split mouse prefix' 'E^[[<35;3;3M' "$(printf '\033\033[')" '<35;3;3M' +assert_events 'split after two Escapes' 'E^[[<35;3;3M' "$(printf '\033\033')" '[<35;3;3M' +assert_events 'invalid SGR coordinates' E "$(printf '\033\033[<35;0;3M')" +assert_events 'discarded wheel release' E "$(printf '\033\033[<64;3;3m')" +assert_events 'M-Escape' A "$(printf '\033\033')" +assert_events 'M-Escape and bracket' 'A[' "$(printf '\033\033[')" +assert_events 'M-a' B "$(printf '\033a')" +assert_events 'M-Up' U "$(printf '\033\033[A')" + +exit 0 diff --git a/regress/tty-keys-paste.sh b/regress/tty-keys-paste.sh new file mode 100644 index 000000000..694230095 --- /dev/null +++ b/regress/tty-keys-paste.sh @@ -0,0 +1,87 @@ +#!/bin/sh + +# Paste state must change only when the start or end key completes. A separate +# Escape before paste start uses its binding, pasted keys bypass bindings, and +# the first key after paste end uses its binding again. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -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 +trap 'exit 1' 1 2 15 + +$INNER new-session -d -s inner -x80 -y10 \ + "stty raw -echo; printf '\033[?2004h'; exec cat -v" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g assume-paste-time 0 || exit 1 +$INNER set-option -s escape-time 20 || exit 1 +$INNER unbind-key -a -T root || exit 1 +$INNER bind-key -n Escape send-keys -l E || exit 1 +$INNER bind-key -n a send-keys -l A || exit 1 +$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g assume-paste-time 0 || exit 1 + +i=0 +while [ -z "$($INNER list-clients -F '#{client_name}')" ] || + [ "$($INNER display-message -p '#{bracket_paste_flag}')" != 1 ]; do + [ "$i" -lt 100 ] || fail 'client did not attach or enable bracketed paste' + sleep 0.05 + i=$((i + 1)) +done + +send_raw() +{ + bytes=$(printf '%s' "$1" | od -An -v -tx1) + $OUTER send-keys -H $bytes || exit 1 +} + +assert_output() +{ + i=0 + while [ "$i" -lt 100 ]; do + actual=$($INNER capture-pane -p | tr -d '\n') + [ "$actual" = "$1" ] && return 0 + sleep 0.05 + i=$((i + 1)) + done + fail "got '$actual', expected '$1'" +} + +send_raw "$(printf '\033\033[200~a\033[201~a')" +expected='E^[[200~a^[[201~A' +assert_output "$expected" + +# A partial paste end gets more time than escape-time. The final key must +# still be outside the paste when the rest arrives after that time. +send_raw "$(printf '\033[200~a\033[20')" +sleep 0.05 +send_raw '1~a' +expected=$expected'^[[200~a^[[201~A' +assert_output "$expected" + +# Split the start immediately before its final byte, including an extra +# Escape. Neither the Escape binding nor paste mode may be lost. +$INNER set-option -s escape-time 1000 || exit 1 +send_raw "$(printf '\033\033[200')" +sleep 0.05 +send_raw "$(printf '~a\033[201~a')" +expected=$expected'E^[[200~a^[[201~A' +assert_output "$expected" + +exit 0 diff --git a/regress/tty-keys-replies.sh b/regress/tty-keys-replies.sh new file mode 100755 index 000000000..e67717c7a --- /dev/null +++ b/regress/tty-keys-replies.sh @@ -0,0 +1,152 @@ +#!/bin/sh + +# Test terminal reply state changes, including replies fragmented at every +# byte and replies preceded by a separate Escape. Use a PTY directly so that +# an outer tmux cannot answer startup queries before the test does. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 - "$TEST_TMUX" <<'PY' +import fcntl +import os +import select +import struct +import subprocess +import sys +import termios +import time + +tmux = sys.argv[1] +server = [tmux, "", "-f/dev/null"] + +def run(*args, check=True): + p = subprocess.run(server + list(args), capture_output=True) + if check and p.returncode != 0: + raise RuntimeError("%r: %s" % (args, p.stderr.decode())) + return p.stdout.decode().strip() + +def state(): + return run("list-clients", "-F", + "#{client_termfeatures}|#{client_termtype}|" + "#{client_width}x#{client_height}|" + "#{client_cell_width}x#{client_cell_height}") + +def matches(name, value): + features, kind, size, cells = value.split("|") + features = set(features.split(",")) + if name == "DA": + return {"sixel", "margins", "rectfill", "clipboard"} <= features + if name == "DA2": + return "ignorefkeys" in features + if name == "XDA": + return kind == "ReplyTest" + if name == "sync": + return "sync" in features + if name == "characters": + return size == "61x17" + if name == "pixels": + return cells == "10x20" + +reports = [ + ("DA", b"\033[?65;4;21;28;52c"), + ("DA2", b"\033[>85;1;0c"), + ("XDA", b"\033P>|ReplyTest\033\\"), + ("sync", b"\033[?2026;2$y"), + ("characters", b"\033[8;17;61t"), + ("pixels", b"\033[4;480;800t"), +] +cases = 0 +for name, report in reports: + for prefixed in (False, True): + deliveries = ["whole", "split", "bytewise"] + if name == "sync" and not prefixed: + deliveries += ["invalid", "expired"] + for delivery in deliveries: + context = (name, prefixed, delivery) + pid = fd = None + # Each case needs fresh one-shot startup reply flags. A new + # socket also avoids racing shutdown of the previous server. + server[1] = "-LtestReplies%d-%d" % (os.getpid(), cases) + try: + run("new-session", "-d", "-s", "replies", "-x", "80", + "-y", "24", "stty raw -echo; exec cat -v") + run("set-option", "-g", "status", "off") + run("set-option", "-g", "assume-paste-time", "0") + if delivery == "expired": + run("set-option", "-s", "escape-time", "100") + else: + run("set-option", "-s", "escape-time", "60000") + run("set-option", "-g", "@seen", "") + run("bind-key", "-n", "Escape", "set-option", "-gF", + "@seen", "#{@seen}E") + pid, fd = os.forkpty() + if pid == 0: + os.environ["TERM"] = "xterm-256color" + fcntl.ioctl(0, termios.TIOCSWINSZ, + struct.pack("HHHH", 24, 80, 0, 0)) + os.execv(tmux, server + ["attach-session", "-t", "replies"]) + output = b"" + deadline = time.monotonic() + 5 + while b"\033[?2026$p" not in output: + assert time.monotonic() < deadline, (context, "missing query") + if select.select([fd], [], [], .01)[0]: + output += os.read(fd, 65536) + baseline = state() + assert not matches(name, baseline), (context, baseline) + wire = (b"\033" if prefixed else b"") + report + if delivery == "invalid": + # A near match must fall back to ordinary key parsing. + wire = b"\033[?2026;5$y" + if delivery in ("whole", "invalid"): + chunks = [wire] + elif delivery in ("split", "expired"): + chunks = [wire[:-1], wire[-1:]] + else: + chunks = [wire[i:i+1] for i in range(len(wire))] + for part in chunks[:-1]: + os.write(fd, part) + time.sleep(.01) + if delivery != "bytewise": + actual = state() + assert actual == baseline, (context, "premature effect", actual) + assert run("capture-pane", "-p") == "", (context, "partial leak") + expected = "Z" + if delivery == "expired": + # Allow the partial reply's timer to expire. The pending + # query extends it to at least 500ms; wait for fallback + # rather than assuming a fixed expiration time. + expected = wire[:-1].replace(b"\033", b"^[").decode() + deadline = time.monotonic() + 5 + while run("capture-pane", "-p") != expected: + assert time.monotonic() < deadline, (context, "timer stalled") + if select.select([fd], [], [], .01)[0]: + os.read(fd, 65536) + if delivery in ("invalid", "expired"): + expected = wire.replace(b"\033", b"^[").decode() + "Z" + # A trailing key confirms that the entire reply has been + # processed, including cases with no extra Escape. + os.write(fd, chunks[-1] + b"Z") + deadline = time.monotonic() + 5 + while run("capture-pane", "-p") != expected: + assert time.monotonic() < deadline, (context, + "reply leaked or input stalled", run("capture-pane", "-p")) + if select.select([fd], [], [], .01)[0]: + os.read(fd, 65536) + actual = state() + if delivery in ("invalid", "expired"): + assert actual == baseline, (context, "unexpected effect", actual) + else: + assert matches(name, actual), (context, "missing effect", actual) + assert run("show-option", "-gv", "@seen") == ( + "E" if prefixed else ""), (context, "Escape count") + cases += 1 + finally: + run("kill-server", check=False) + if fd is not None: + os.close(fd) + if pid is not None: + os.waitpid(pid, 0) +PY diff --git a/regress/tty-keys-reply-escape.sh b/regress/tty-keys-reply-escape.sh new file mode 100644 index 000000000..41516f431 --- /dev/null +++ b/regress/tty-keys-reply-escape.sh @@ -0,0 +1,127 @@ +#!/bin/sh + +# An Escape followed by a terminal reply must remain a separate key. The +# reply's leading Escape must not be consumed as the second half of a Meta +# key, which would send the rest of the reply to the pane as text. A reply +# split before the end of a known key must wait for the rest. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -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 +trap 'exit 1' 1 2 15 + +$INNER new-session -d -s inner -x80 -y10 "stty raw -echo; exec cat -v" || + exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g assume-paste-time 0 || exit 1 +$INNER set-option -s escape-time 1000 || exit 1 +$INNER unbind-key -a -T root || exit 1 +$OUTER new-session -d -s outer -x80 -y10 "$INNER attach -t inner" || exit 1 +$OUTER set-option -g assume-paste-time 0 || exit 1 + +i=0 +while [ -z "$($INNER list-clients -F '#{client_name}')" ]; do + [ "$i" -lt 100 ] || fail 'inner client did not attach' + sleep 0.05 + i=$((i + 1)) +done + +bind_key() +{ + $INNER bind-key -n "$1" send-keys -l "$2" || exit 1 +} +bind_key Escape E +bind_key M-Escape A +bind_key M-] B +bind_key M-O P +bind_key M-Up U + +send_raw() +{ + # Hex arguments avoid command separators in a partial sequence. + bytes=$(printf '%s' "$1" | od -An -v -tx1) + $OUTER send-keys -H $bytes || exit 1 +} + +capture() +{ + $INNER capture-pane -p | tr -d '\n' +} + +theme() +{ + $INNER list-clients -F '#{client_theme}' +} + +expected_output= +assert_keys() +{ + name=$1 + expected=$expected_output$2 + expected_theme=$3 + send_raw "$4" + if [ "$#" -eq 5 ]; then + sleep 0.05 + send_raw "$5" + fi + i=0 + while [ "$i" -lt 100 ]; do + actual=$(capture) + actual_theme=$(theme) + if [ "$actual" = "$expected" ] && + [ "$actual_theme" = "$expected_theme" ]; then + expected_output=$actual + return 0 + fi + sleep 0.05 + i=$((i + 1)) + done + fail "$name: got '$actual' theme=$actual_theme," \ + "expected '$expected' theme=$expected_theme" +} + +assert_keys 'Escape and dark theme report' E dark \ + "$(printf '\033\033[?997;1n')" +assert_keys 'Escape and light theme report' E light \ + "$(printf '\033\033[?997;2n')" +assert_keys 'split theme report' '' dark \ + "$(printf '\033[?997')" ';1n' +assert_keys 'Escape and light theme report, split' E light \ + "$(printf '\033\033[?99')" '7;2n' +assert_keys 'theme report split after two Escapes' E dark \ + "$(printf '\033\033')" '[?997;1n' +assert_keys 'Escape and light theme report again' E light \ + "$(printf '\033\033[?997;2n')" +assert_keys 'Escape and background colour' E light \ + "$(printf '\033\033]11;rgb:ffff/ffff/ffff\033\\')" +assert_keys 'Escape and foreground colour (BEL)' E light \ + "$(printf '\033\033]10;rgb:0000/0000/0000\007')" +assert_keys 'split background colour' E light \ + "$(printf '\033\033]11;rgb:ff')" "$(printf 'ff/ffff/ffff\033\\')" +assert_keys 'Escape and palette colour' E light \ + "$(printf '\033\033]4;1;rgb:ffff/0000/0000\033\\')" +assert_keys 'Escape and clipboard' E light \ + "$(printf '\033\033]52;c;dGVzdA==\007')" +assert_keys 'M-Escape' A light "$(printf '\033\033')" +assert_keys 'M-]' B light "$(printf '\033]')" +assert_keys 'M-O' P light "$(printf '\033O')" +assert_keys 'M-Up' U light "$(printf '\033\033[A')" + +exit 0 diff --git a/regress/tty-margins-sb.sh b/regress/tty-margins-sb.sh new file mode 100755 index 000000000..6f573af92 --- /dev/null +++ b/regress/tty-margins-sb.sh @@ -0,0 +1,96 @@ +#!/bin/sh + +# With left/right margins, a pane narrowed by a scrollbar can scroll without +# retransmitting its existing rows. Both paths must produce the same screen. + +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 + +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 + +cat >"$DIR/emitter.pl" <<'PERL' +use strict; +use warnings; +$| = 1; +print join("\r\n", map { "KEEP0$_" } 1 .. 6); +while (!-e $ENV{TRIGGER}) { + select undef, undef, undef, 0.01; +} +print "\r\nNEWROW"; +sleep 100; +PERL + +j=0 +for margins in on off; do + INNER="$TEST_TMUX -LA$j$$ -f/dev/null" + OUTER="$TEST_TMUX -LB$j$$ -f/dev/null" + + $INNER new-session -d -s inner -x 40 -y 6 \ + "TRIGGER='$DIR/trigger-$margins' perl '$DIR/emitter.pl'" || exit 1 + $INNER set -g status off || exit 1 + $INNER set -g status-interval 0 || exit 1 + $INNER set -g automatic-rename off || exit 1 + $INNER set -g window-size manual || exit 1 + $INNER set -g pane-scrollbars on || exit 1 + if [ "$margins" = on ]; then + $INNER set -as terminal-features ',screen-256color:margins' || exit 1 + fi + + $OUTER new-session -d -s outer -x 40 -y 6 'sleep 100' || exit 1 + $OUTER set -g status off || exit 1 + $OUTER set -g window-size manual || exit 1 + $OUTER set -g default-terminal screen-256color || exit 1 + $OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1 + sleep 0.5 + $OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output-$margins'" || exit 1 + $INNER refresh-client || exit 1 + sleep 0.5 + grep -aq KEEP02 "$DIR/output-$margins" || fail "initial rows not captured" + offset=$(wc -c <"$DIR/output-$margins") + : >"$DIR/trigger-$margins" + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$DIR/screen-$margins" || exit 1 + grep -q NEWROW "$DIR/screen-$margins" && break + sleep 0.1 + i=$((i + 1)) + done + [ "$i" -lt 50 ] || fail "$margins: scroll did not reach the terminal" + sleep 0.2 + tail -c +"$((offset + 1))" "$DIR/output-$margins" >"$DIR/scroll-$margins" + grep -aq NEWROW "$DIR/scroll-$margins" || fail "scroll output not captured" + if [ "$margins" = on ]; then + if grep -aq KEEP02 "$DIR/scroll-$margins"; then + fail "scroll with margins retransmitted an existing row" + fi + else + grep -aq KEEP02 "$DIR/scroll-$margins" || + fail "scroll without margins did not exercise the redraw fallback" + fi + printf 'KEEP02\nKEEP03\nKEEP04\nKEEP05\nKEEP06\nNEWROW\n' >"$DIR/expected" + cmp -s "$DIR/expected" "$DIR/screen-$margins" || + fail "$margins: terminal did not contain the expected scrolled rows" + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + j=$((j + 1)) +done + +exit 0 diff --git a/regress/window-copy-sb-drag-focus-style.sh b/regress/window-copy-sb-drag-focus-style.sh new file mode 100755 index 000000000..77f478955 --- /dev/null +++ b/regress/window-copy-sb-drag-focus-style.sh @@ -0,0 +1,123 @@ +#!/bin/sh + +# Dragging an inactive pane's scrollbar slider changes which pane is +# active (window_copy_scroll(), window-copy.c). Every other place in the +# codebase that changes the active pane pairs it with +# window_redraw_active_switch() (or falls back to a full window redraw) so +# the old/new active panes' window-active-style/window-style colours get +# repainted - window_copy_scroll() was the one caller that did neither, so +# a scrollbar-slider drag changed focus but left both panes' body colours +# stale. This used to be masked by window_set_active_pane() itself doing +# an unconditional full redraw on every active-pane change, until that was +# narrowed to borders/status-only for the common case. +# +# This creates two tiled panes with clearly distinguishable +# window-active-style/window-style backgrounds, puts the *inactive* one in +# copy mode, drags its scrollbar slider, and checks - via an attached +# client's own received bytes, since window-style is applied during +# redraw composition rather than stored in the grid, so capture-pane +# alone would not reflect it - that the newly active pane's marker text +# immediately shows the active-style colour, not the stale one. + +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 +INNER="$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lsbfocus-outer-$$ -f/dev/null" +CAPTURE=$DIR/capture +ESC=$(printf '\033') + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat -A "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +get_mark_color() +{ + $OUTER capture-pane -e -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + LC_ALL=C grep -a "MARK1" "$CAPTURE" | + LC_ALL=C grep -aoE "${ESC}\[4[12]m" | tail -1 +} + +wait_mark_color() +{ + want=$1 + i=0 + while [ "$i" -lt 50 ]; do + [ "$(get_mark_color)" = "$want" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "MARK1's pane never showed the expected background (wanted '$want', got '$(get_mark_color)')" +} + +mouse() +{ + sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1 + sleep 0.1 +} + +$INNER new-session -d -s inner -x 40 -y 10 '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-option -g pane-scrollbars on || exit 1 +$INNER setw pane-scrollbars-position right || exit 1 +$INNER setw pane-scrollbars-style 'width=1,pad=0' || exit 1 +$INNER set-option -g window-active-style 'bg=red' || exit 1 +$INNER set-option -g window-style 'bg=green' || exit 1 +$INNER split-window -h -t inner -d 'printf MARK1; sleep 100' || exit 1 + +PANE1=$($INNER list-panes -t inner -F '#{pane_id}' | sed -n 2p) +[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 0 ] || + fail "sanity: MARK1's pane is already active before the drag" + +# Put the inactive pane in copy mode without switching focus (copy-mode -t +# targets a pane without making it active). +$INNER copy-mode -t "$PANE1" || exit 1 + +$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_mark_color "${ESC}[42m" + +XOFF=$($INNER display-message -p -t "$PANE1" '#{pane_left}') +YOFF=$($INNER display-message -p -t "$PANE1" '#{pane_top}') +SX=$($INNER display-message -p -t "$PANE1" '#{pane_width}') +SBCOL=$((XOFF + SX + 1)) +SBROW=$((YOFF + 1)) + +# With no scrollback beyond the pane's own content, the slider fills the +# whole scrollbar track, so any point on it (here, its very first row) is +# on the slider. +mouse 0 "$SBCOL" "$SBROW" M +mouse 32 "$SBCOL" "$((SBROW + 1))" M +mouse 0 "$SBCOL" "$((SBROW + 1))" m + +[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 1 ] || + fail "sanity: dragging the scrollbar slider did not change focus" + +wait_mark_color "${ESC}[41m" + +exit 0 diff --git a/screen-redraw.c b/screen-redraw.c index 01cdd1177..e4fb93281 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-redraw.c,v 1.162 2026/09/24 11:19:39 nicm Exp $ */ +/* $OpenBSD: screen-redraw.c,v 1.163 2026/10/02 12:48:52 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -204,14 +204,36 @@ struct redraw_scene { u_int oy; }; +/* A damaged rectangle in a window. */ +struct redraw_damage { + u_int x; + u_int y; + u_int sx; + u_int sy; + + TAILQ_ENTRY(redraw_damage) entry; +}; + +/* + * If there are more damage rectangles than this, they are collapsed into + * one. + */ +#define REDRAW_DAMAGE_MAX 16 + /* Cell for building the scene. */ struct redraw_build_cell { struct redraw_span_data data; }; - static struct redraw_build_cell *redraw_cells; static size_t redraw_ncells; +/* + * We can reuse the same pane status lines during one damage redraw, but when + * we enter a new one, the client or format variables may have changed, so we + * need to make them again. The generation is increased so this happens. + */ +static u_int redraw_status_generation; + /* Context for building the scene. */ struct redraw_build_ctx { struct client *c; @@ -1054,6 +1076,13 @@ redraw_free_scene(struct redraw_scene *scene) free(scene); } +/* Does a client's cached scene show this window? */ +int +redraw_client_has_window(struct client *c, struct window *w) +{ + return (c->redraw_scene != NULL && c->redraw_scene->w == w); +} + /* Mark a window's cached redraw scenes as out of date. */ void redraw_invalidate_scene(struct window *w) @@ -1061,6 +1090,104 @@ redraw_invalidate_scene(struct window *w) w->redraw_scene_generation++; } +/* Free all pending damage for a window. */ +void +redraw_free_damage(struct window *w) +{ + struct redraw_damage *rd, *rd1; + + TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) { + TAILQ_REMOVE(&w->damage, rd, entry); + free(rd); + } + w->damage_count = 0; +} + +/* Collapse all pending damage for a window into one rectangle. */ +static void +redraw_collapse_damage(struct window *w) +{ + struct redraw_damage *rd, *rd1, *first; + u_int x0, y0, x1, y1; + + first = TAILQ_FIRST(&w->damage); + if (first == NULL) + return; + x0 = first->x; + y0 = first->y; + x1 = first->x + first->sx; + y1 = first->y + first->sy; + + TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) { + if (rd->x < x0) + x0 = rd->x; + if (rd->y < y0) + y0 = rd->y; + if (rd->x + rd->sx > x1) + x1 = rd->x + rd->sx; + if (rd->y + rd->sy > y1) + y1 = rd->y + rd->sy; + if (rd != first) { + TAILQ_REMOVE(&w->damage, rd, entry); + free(rd); + } + } + + first->x = x0; + first->y = y0; + first->sx = x1 - x0; + first->sy = y1 - y0; + w->damage_count = 1; +} + +/* Record window damage, merging nearby rectangles and limiting the count. */ +void +redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy) +{ + struct redraw_damage *rd; + u_int x0, y0, x1, y1, area, union_area; + + if (x >= w->sx || y >= w->sy) + return; + if (x + sx > w->sx) + sx = w->sx - x; + if (y + sy > w->sy) + sy = w->sy - y; + if (sx == 0 || sy == 0) + return; + + TAILQ_FOREACH(rd, &w->damage, entry) { + if (x > rd->x + rd->sx || rd->x > x + sx || + y > rd->y + rd->sy || rd->y > y + sy) + continue; + + x0 = (x < rd->x) ? x : rd->x; + y0 = (y < rd->y) ? y : rd->y; + x1 = (x + sx > rd->x + rd->sx) ? x + sx : rd->x + rd->sx; + y1 = (y + sy > rd->y + rd->sy) ? y + sy : rd->y + rd->sy; + + area = sx * sy + rd->sx * rd->sy; + union_area = (x1 - x0) * (y1 - y0); + if (union_area > 2 * area) + continue; + + rd->x = x0; + rd->y = y0; + rd->sx = x1 - x0; + rd->sy = y1 - y0; + return; + } + + rd = xcalloc(1, sizeof *rd); + rd->x = x; + rd->y = y; + rd->sx = sx; + rd->sy = sy; + TAILQ_INSERT_TAIL(&w->damage, rd, entry); + if (++w->damage_count > REDRAW_DAMAGE_MAX) + redraw_collapse_damage(w); +} + /* Mark all cached redraw scenes as out of date. */ void redraw_invalidate_all_scenes(void) @@ -1603,6 +1730,24 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx, dctx->flags |= REDRAW_ISOLATES; } +/* Build a pane prompt. */ +static void +redraw_make_pane_prompt(struct window_pane *wp, struct screen *screen) +{ + struct screen_write_ctx ctx; + struct prompt_draw_data pdd; + + screen_init(screen, wp->sx, 1, 0); + screen_write_start(&ctx, screen); + pdd.ctx = &ctx; + pdd.cursor_x = &wp->prompt_cx; + pdd.area_x = 0; + pdd.area_width = wp->sx; + pdd.prompt_line = 0; + prompt_draw(wp->prompt, &pdd); + screen_write_stop(&ctx); +} + /* Draw a pane's prompt over its content. */ static void redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) @@ -1611,8 +1756,6 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) struct client *c = scene->c; struct tty *tty = &c->tty; struct screen screen; - struct screen_write_ctx ctx; - struct prompt_draw_data pdd; int ox = scene->ox, oy = scene->oy; int sx = scene->sx, sy = scene->sy; int line, cy, px, offset, width, wy; @@ -1645,16 +1788,7 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) if (px + width > sx) width = sx - px; - screen_init(&screen, wp->sx, 1, 0); - screen_write_start(&ctx, &screen); - pdd.ctx = &ctx; - pdd.cursor_x = &wp->prompt_cx; - pdd.area_x = 0; - pdd.area_width = wp->sx; - pdd.prompt_line = 0; - prompt_draw(wp->prompt, &pdd); - screen_write_stop(&ctx); - + redraw_make_pane_prompt(wp, &screen); tty_draw_line(tty, &screen, offset, 0, width, px, cy, NULL); screen_free(&screen); } @@ -1684,7 +1818,9 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) redraw = status_prompt_redraw(c); else redraw = status_redraw(c); - if (!redraw && !REDRAW_IS_ALL(flags)) { + if (!redraw && + (~c->flags & CLIENT_REDRAWSTATUSALWAYS) && + !REDRAW_IS_ALL(flags)) { flags &= ~REDRAW_STATUS; if (flags == 0) return; @@ -1842,7 +1978,7 @@ redraw_screen(struct client *c) else { if (c->flags & CLIENT_REDRAWBORDERS) flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS); - if (c->flags & CLIENT_REDRAWSTATUS) + if (c->flags & (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS)) flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS); if (c->flags & CLIENT_REDRAWMENU) flags |= REDRAW_MENU; @@ -1868,3 +2004,138 @@ redraw_pane_scrollbar(struct client *c, struct window_pane *wp) { redraw_draw(c, wp, REDRAW_PANE_SCROLLBAR); } + +/* Rebuild damaged pane status. */ +static void +redraw_damage_refresh_status(struct redraw_draw_ctx *dctx, + struct window_pane *wp) +{ + struct redraw_span *first; + u_int g = wp->status_generation, width; + + if ((wp->flags & PANE_NEWSTATUS) && g == redraw_status_generation) + return; + width = redraw_pane_status_width(dctx, wp, &first); + if (width != 0) { + window_make_pane_status(wp, dctx->scene->c, width, first); + wp->flags |= PANE_NEWSTATUS; + wp->status_generation = redraw_status_generation; + } +} + +/* Draw a pane's prompt over a damaged span. */ +static void +redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx, + struct redraw_span *span, u_int y) +{ + struct redraw_scene *scene = dctx->scene; + struct window_pane *wp = span->data.p.wp; + struct tty *tty = &scene->c->tty; + struct screen screen; + u_int px = span->data.p.px, width, prompt_y; + + if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0) + return; + if (dctx->flags & REDRAW_STATUS_TOP) + prompt_y = 0; + else + prompt_y = wp->sy - 1; + if (span->data.p.py != prompt_y) + return; + redraw_make_pane_prompt(wp, &screen); + if (px < screen_size_x(&screen)) { + width = span->width; + if (width > screen_size_x(&screen) - px) + width = screen_size_x(&screen) - px; + tty_draw_line(tty, &screen, px, 0, width, span->x, y, NULL); + } + screen_free(&screen); +} + +/* Draw the spans intersecting a damaged rectangle. */ +static void +redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y, + u_int sx, u_int sy) +{ + struct redraw_scene *scene = dctx->scene; + struct redraw_line *line; + struct redraw_spans *spans; + struct redraw_span *span; + u_int cy, yy, type; + + if (x >= scene->sx || y >= scene->sy) + return; + if (x + sx > scene->sx) + sx = scene->sx - x; + if (y + sy > scene->sy) + sy = scene->sy - y; + if (sx == 0 || sy == 0) + return; + + for (yy = y; yy < y + sy; yy++) { + line = &scene->lines[yy]; + if (dctx->flags & REDRAW_STATUS_TOP) + cy = dctx->status_lines + yy; + else + cy = yy; + for (type = 0; type < REDRAW_SPAN_TYPES; type++) { + spans = &line->spans[type]; + TAILQ_FOREACH(span, spans, entry) { + if (span->x >= x + sx) + continue; + if (span->x + span->width <= x) + continue; + if (type == REDRAW_SPAN_STATUS) { + redraw_damage_refresh_status(dctx, + span->data.st.wp); + } + redraw_draw_span(dctx, span, cy); + if (type == REDRAW_SPAN_PANE) { + redraw_damage_draw_pane_prompt(dctx, + span, cy); + } + } + } + } +} + +/* Draw pending window damage on this client. */ +void +redraw_client_damage(struct client *c) +{ + struct window *w = c->session->curw->window; + struct window_pane *wp; + struct redraw_scene *scene; + struct redraw_draw_ctx dctx; + struct redraw_damage *rd; + u_int ox, oy, sx, sy, x0, y0, x1, y1; + + if (TAILQ_EMPTY(&w->damage)) + return; + redraw_status_generation++; + + scene = redraw_get_scene(c); + if (scene == NULL) + return; + redraw_set_draw_context(&dctx, scene); + redraw_get_window_offset(c, &ox, &oy, &sx, &sy); + + TAILQ_FOREACH(wp, &w->panes, entry) { + wp->border_gc_set = 0; + wp->active_border_gc_set = 0; + } + + tty_sync_start(&c->tty); + tty_update_mode(&c->tty, c->tty.mode & ~CURSOR_MODES, NULL); + + TAILQ_FOREACH(rd, &w->damage, entry) { + x0 = (rd->x > ox) ? rd->x : ox; + y0 = (rd->y > oy) ? rd->y : oy; + x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx; + y1 = (rd->y + rd->sy < oy + sy) ? rd->y + rd->sy : oy + sy; + if (x0 < x1 && y0 < y1) { + redraw_draw_damage_rectangle(&dctx, x0 - ox, y0 - oy, + x1 - x0, y1 - y0); + } + } +} diff --git a/screen-write.c b/screen-write.c index 04ef06bc5..388749319 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD: screen-write.c,v 1.294 2026/09/28 10:10:16 nicm Exp $ */ +/* $OpenBSD: screen-write.c,v 1.299 2026/10/05 08:41:23 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -122,14 +122,26 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy) evtimer_add(&w->offset_timer, &tv); } -/* Do a full redraw. */ +/* Redraw lines. */ static void -screen_write_redraw_cb(const struct tty_ctx *ttyctx) +screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny) { struct window_pane *wp = ttyctx->arg; + int x0, y0, x1, y1; - if (wp != NULL) - wp->flags |= PANE_REDRAW; + if (wp == NULL) + return; + + x0 = wp->xoff; + y0 = wp->yoff + (int)py; + x1 = x0 + (int)wp->sx; + y1 = y0 + (int)ny; + if (x0 < 0) + x0 = 0; + if (y0 < 0) + y0 = 0; + if (x1 > x0 && y1 > y0) + redraw_damage_window(wp->window, x0, y0, x1 - x0, y1 - y0); } /* Update context for client. */ @@ -177,11 +189,12 @@ screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c) return (1); } -/* Return 1 if there is a floating window pane overlapping this pane. */ +/* Return 1 if a menu or floating pane overlaps this pane. */ static int screen_write_pane_is_obscured(struct screen_write_ctx *ctx) { struct window_pane *wp = ctx->wp; + struct menu_data *md; if (ctx->wp == NULL) return (0); @@ -192,6 +205,16 @@ screen_write_pane_is_obscured(struct screen_write_ctx *ctx) } ctx->flags |= SCREEN_WRITE_CHECKED_IF_OBSCURED; + md = wp->window->menu; + if (md != NULL && + (int)menu_x(md) < wp->xoff + (int)wp->sx && + (int)(menu_x(md) + menu_width(md)) > wp->xoff && + (int)menu_y(md) < wp->yoff + (int)wp->sy && + (int)(menu_y(md) + menu_height(md)) > wp->yoff) { + ctx->flags |= SCREEN_WRITE_OBSCURED; + return (1); + } + if (ctx->wp->xoff < 0 || ctx->wp->yoff < 0 || ctx->wp->xoff + ctx->wp->sx > ctx->wp->window->sx || @@ -1377,11 +1400,11 @@ screen_write_sync_scroll_dirty(struct screen_write_ctx *ctx) /* Redraw the scrolled lines for a client which cannot scroll them. */ static void -screen_write_sync_redraw_cb(const struct tty_ctx *ttyctx) +screen_write_sync_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny) { struct window_pane *wp = ttyctx->arg; - bit_nset(wp->sync_dirty, wp->sync_rupper, wp->sync_rlower); + bit_nset(wp->sync_dirty, py, py + ny - 1); } /* Send the deferred scroll to the client. */ @@ -2273,7 +2296,7 @@ screen_write_fullredraw(struct screen_write_ctx *ctx) screen_write_initctx(ctx, &ttyctx, 1, 0); if (ttyctx.redraw_cb != NULL) - ttyctx.redraw_cb(&ttyctx); + ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy); } /* Trim collected items. */ @@ -2511,13 +2534,18 @@ screen_write_collect_flush_line(struct screen_write_ctx *ctx, u_int y) ttyctx.n = w_length; tty_write(tty_cmd_clearcharacter, &ttyctx); } else { - screen_write_initctx(ctx, &ttyctx, 0, 0); - ttyctx.cell = &ci->gc; - if (ci->wrapped) - ttyctx.flags |= TTY_CTX_WRAPPED; - ttyctx.data.data = cl->data + w_start; - ttyctx.data.size = w_length; - tty_write(tty_cmd_cells, &ttyctx); + screen_write_initctx(ctx, &ttyctx, 0, 1); + if (ttyctx.flags & TTY_CTX_PANE_OBSCURED) { + ttyctx.n = w_length; + tty_write(tty_cmd_redrawline, &ttyctx); + } else { + ttyctx.cell = &ci->gc; + if (ci->wrapped) + ttyctx.flags |= TTY_CTX_WRAPPED; + ttyctx.data.data = cl->data + w_start; + ttyctx.data.size = w_length; + tty_write(tty_cmd_cells, &ttyctx); + } } items++; written = 1; @@ -2745,6 +2773,10 @@ screen_write_collect_add(struct screen_write_ctx *ctx, * Don't need to check that the attributes and whatnot are still the * same - input_parse will end the collection when anything that isn't * a plain character is encountered. + * + * Without wrapping, collect up to but not including the last column. + * Leave that cell to screen_write_cell so repeated writes overwrite it + * and the cursor remains at the right edge. */ collect = 1; @@ -2754,7 +2786,8 @@ screen_write_collect_add(struct screen_write_ctx *ctx, collect = 0; else if (gc->attr & GRID_ATTR_CHARSET) collect = 0; - else if (~s->mode & MODE_WRAP) + else if ((~s->mode & MODE_WRAP) && + s->cx + ctx->item->used >= sx - 1) collect = 0; else if (s->mode & MODE_INSERT) collect = 0; @@ -2920,6 +2953,11 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) /* Create space for character in insert mode. */ if (s->mode & MODE_INSERT) { screen_write_collect_flush(ctx, 0, __func__); + if (wp != NULL && screen_write_pane_is_obscured(ctx)) { + if (screen_write_should_draw_line(ctx, s->cy)) + screen_write_redraw_line(ctx, &ttyctx, s->cy); + return; + } ttyctx.n = width; if (screen_write_should_draw_line(ctx, s->cy)) tty_write(tty_cmd_insertcharacter, &ttyctx); @@ -3269,6 +3307,7 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, { struct tty_ctx ttyctx; struct window_pane *wp = ctx->wp; + int pending; if (wp != NULL && !options_get_number(wp->options, "alternate-screen")) return; @@ -3278,11 +3317,12 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, return; if (wp != NULL) { + pending = !TAILQ_EMPTY(&wp->resize_queue); window_pane_clear_resizes(wp, NULL); if (event_initialized(&wp->resize_timer)) evtimer_del(&wp->resize_timer); layout_fix_panes(wp->window, NULL); - if (!TAILQ_EMPTY(&wp->resize_queue)) { + if (pending || !TAILQ_EMPTY(&wp->resize_queue)) { window_pane_send_resize(wp, wp->sx, wp->sy); window_pane_clear_resizes(wp, NULL); } @@ -3291,7 +3331,7 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, screen_write_initctx(ctx, &ttyctx, 1, 0); if (ttyctx.redraw_cb != NULL) - ttyctx.redraw_cb(&ttyctx); + ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy); } /* Turn alternate screen off. */ @@ -3316,5 +3356,5 @@ screen_write_alternateoff(struct screen_write_ctx *ctx, struct grid_cell *gc, screen_write_initctx(ctx, &ttyctx, 1, 0); if (ttyctx.redraw_cb != NULL) - ttyctx.redraw_cb(&ttyctx); + ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy); } diff --git a/server-client.c b/server-client.c index a454524f1..5bf179523 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.515 2026/09/28 11:25:51 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.519 2026/10/09 10:16:02 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -356,7 +356,13 @@ server_client_set_session(struct client *c, struct session *s) tty_update_client_offset(c); status_timer_start(c); server_client_fire_session_changed(c, old); - server_redraw_client(c); + + /* + * Redraw if the session or displayed window changed. Use the + * cached scene because the session's current window is already set. + */ + if (old != s || !redraw_client_has_window(c, s->curw->window)) + server_redraw_client(c); } server_check_unattached(); @@ -369,8 +375,7 @@ server_client_lost(struct client *c) { struct client_file *cf, *cf1; - if (cfg_client == c) - cfg_client = NULL; + cfg_client_lost(c); c->flags |= CLIENT_DEAD; status_prompt_clear(c); @@ -394,6 +399,9 @@ server_client_lost(struct client *c) if (c->name != NULL && (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL))) events_fire_client("client-closed", c); + c->session = NULL; + c->last_session = NULL; + if (c->flags & CLIENT_CONTROL) control_stop(c); if (c->flags & CLIENT_TERMINAL) @@ -786,9 +794,14 @@ server_client_check_mouse(struct client *c, struct key_event *event) x = m->x, y = m->y, b = m->b; if (x == m->lx && y == m->ly) return (KEYC_UNKNOWN); + if (c->tty.mouse_drag_status) { + x = c->tty.mouse_drag_x; + y = c->tty.mouse_drag_y; + } log_debug("drag update at %u,%u", x, y); } else { x = m->lx, y = m->ly, b = m->lb; + c->tty.mouse_drag_status = 0; log_debug("drag start at %u,%u", x, y); } } else if (MOUSE_WHEEL(m->b)) { @@ -846,6 +859,8 @@ have_event: if (m->statusat != -1 && y >= (u_int)m->statusat && y < m->statusat + m->statuslines) { + if (type == KEYC_TYPE_MOUSEDRAG && c->tty.mouse_drag_flag == 0) + c->tty.mouse_drag_status = 1; sr = status_get_range(c, x, y - m->statusat); if (sr == NULL) { loc = KEYC_MOUSE_LOCATION_STATUS_DEFAULT; @@ -1077,17 +1092,24 @@ have_event: * where the user grabbed. */ if (c->tty.mouse_drag_flag == 0) { - c->tty.mouse_drag_x = px; - c->tty.mouse_drag_y = py; + if (c->tty.mouse_drag_status) { + c->tty.mouse_drag_x = x; + c->tty.mouse_drag_y = y; + } else { + c->tty.mouse_drag_x = px; + c->tty.mouse_drag_y = py; + } } c->tty.mouse_drag_flag = MOUSE_BUTTONS(b) + 1; - /* Only change pane if not already dragging a pane border. */ - if (lwp == NULL) { + /* Change the pane if the drag did not start on status line. */ + if (!c->tty.mouse_drag_status && lwp == NULL) { lwp = wp = window_get_active_at(w, px, py); if (wp != NULL) c->tty.mouse_last_pane = wp->id; } + + /* Change scrollbar position if needed. */ if (c->tty.mouse_scrolling_flag == 0 && loc == KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER) { c->tty.mouse_scrolling_flag = 1; @@ -1349,10 +1371,11 @@ server_client_key_callback(struct cmdq_item *item, void *data) m->key = key; /* - * Mouse drag is in progress, so fire the callback (now that - * the mouse event is valid). + * Synchronize direct drag output with the later damage redraw + * before invoking the drag callback. */ if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) { + tty_sync_start(&c->tty); c->tty.mouse_drag_update(c, m); goto out; } @@ -1779,8 +1802,8 @@ server_client_loop(void) } /* - * Any windows will have been redrawn as part of clients, so clear - * their flags now. + * Clear window redraw state after processing all clients. Deferred + * redraws are preserved in client flags. */ RB_FOREACH(w, windows, &windows) { TAILQ_FOREACH(wp, &w->panes, entry) { @@ -1791,6 +1814,8 @@ server_client_loop(void) wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR| PANE_ACTIVITY); } + redraw_free_damage(w); + check_window_name(w); } @@ -2337,6 +2362,8 @@ server_client_any_pane_redraw(struct client *c) if (c->flags & CLIENT_REDRAWWINDOW) return (1); + if (!TAILQ_EMPTY(&w->damage)) + return (1); TAILQ_FOREACH(wp, &w->panes, entry) { if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR)) return (1); @@ -2353,6 +2380,7 @@ server_client_check_redraw(struct client *c) struct window *w = s->curw->window; struct window_pane *wp; int needed, tflags, mode = tty->mode; + int damaged = !TAILQ_EMPTY(&w->damage); struct timeval tv = { .tv_usec = 1000 }; static struct event ev; size_t n; @@ -2378,12 +2406,12 @@ server_client_check_redraw(struct client *c) return; } - /* - * If there is outstanding data, defer the redraw until it has been - * consumed. We can just add a timer to get out of the event loop and - * end up back here. - */ + /* Ignore output queued within the current synchronized frame. */ n = EVBUFFER_LENGTH(tty->out); + if ((tty->flags & TTY_SYNCING) && n > tty->sync_offset) + n = tty->sync_offset; + + /* Defer until output drains, preserving damage in client flags. */ if (n != 0 || (tty->flags & TTY_BLOCK)) { if (n != 0) log_debug("%s: redraw deferred (%zu left)", c->name, n); @@ -2395,6 +2423,10 @@ server_client_check_redraw(struct client *c) log_debug("redraw timer started"); evtimer_add(&ev, &tv); } + if (damaged) { + c->flags |= CLIENT_REDRAWWINDOW; + return; + } TAILQ_FOREACH(wp, &w->panes, entry) { if (wp->flags & PANE_REDRAW) { c->flags |= CLIENT_REDRAWWINDOW; @@ -2428,6 +2460,10 @@ server_client_check_redraw(struct client *c) redraw_pane_scrollbar(c, wp); } } + + /* Draw damage here if no client redraw flags will handle it. */ + if (damaged && (c->flags & CLIENT_ALLREDRAWFLAGS) == 0) + redraw_client_damage(c); } /* @@ -2441,6 +2477,7 @@ server_client_check_redraw(struct client *c) } server_client_set_progress_bar(c); redraw_screen(c); + redraw_client_damage(c); } /* Put the tty back how it was. */ diff --git a/server-fn.c b/server-fn.c index e9db5a887..45809ee96 100644 --- a/server-fn.c +++ b/server-fn.c @@ -363,10 +363,7 @@ server_destroy_pane(struct window_pane *wp, int notify) u_int sy = screen_size_y(&wp->base); if (wp->fd != -1) { -#ifdef HAVE_UTEMPTER - utempter_remove_record(wp->fd); - kill(getpid(), SIGCHLD); -#endif + window_pane_utmp_remove(wp); bufferevent_free(wp->event); wp->event = NULL; close(wp->fd); diff --git a/server.c b/server.c index 27ff80c29..be4427954 100644 --- a/server.c +++ b/server.c @@ -448,6 +448,10 @@ server_signal(int sig) server_child_signal(); break; case SIGUSR1: +#ifdef HAVE_SYSTEMD + if (systemd_activated()) + break; +#endif event_del(&server_ev_accept); fd = server_create_socket(server_client_flags, NULL); if (fd != -1) { diff --git a/spawn.c b/spawn.c index 801288a99..63300e4fb 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.53 2026/09/21 12:43:36 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.57 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -322,6 +322,7 @@ spawn_pane(struct spawn_context *sc, char **cause) sc->wp0->event = NULL; } if (sc->wp0->fd != -1) { + window_pane_utmp_remove(sc->wp0); close(sc->wp0->fd); sc->wp0->fd = -1; } @@ -377,7 +378,9 @@ spawn_pane(struct spawn_context *sc, char **cause) * directory. */ if (sc->argc == 0 && (~sc->flags & SPAWN_RESPAWN)) { - cmd = options_get_string(s->options, "default-command"); + cmd = options_get_string(w->options, "window-default-command"); + if (cmd == NULL || *cmd == '\0') + cmd = options_get_string(s->options, "default-command"); if (cmd != NULL && *cmd != '\0') { argc = 1; argv = (char **)&cmd; @@ -458,6 +461,8 @@ spawn_pane(struct spawn_context *sc, char **cause) /* If the command is empty, don't fork a child process. */ if (sc->flags & SPAWN_EMPTY) { new_wp->flags |= PANE_EMPTY; + new_wp->pid = 0; + *new_wp->tty = '\0'; new_wp->base.mode &= ~MODE_CURSOR; new_wp->base.mode |= MODE_CRLF; goto complete; @@ -465,14 +470,14 @@ spawn_pane(struct spawn_context *sc, char **cause) new_wp->flags &= ~PANE_EMPTY; /* Store current working directory and change to new one. */ - if (getcwd(path, sizeof path) != NULL) { - if (chdir(new_wp->cwd) == 0) - actual_cwd = new_wp->cwd; - else if (home != NULL && chdir(home) == 0) - actual_cwd = home; - else if (chdir("/") == 0) - actual_cwd = "/"; - } + if (getcwd(path, sizeof path) == NULL) + strlcpy(path, "/", sizeof path); + if (chdir(new_wp->cwd) == 0) + actual_cwd = new_wp->cwd; + else if (home != NULL && chdir(home) == 0) + actual_cwd = home; + else if (chdir("/") == 0) + actual_cwd = "/"; /* Fork the new process. */ new_wp->pid = fdforkpty(ptm_fd, &new_wp->fd, new_wp->tty, NULL, &ws); @@ -575,16 +580,8 @@ spawn_pane(struct spawn_context *sc, char **cause) _exit(1); complete: -#ifdef HAVE_UTEMPTER - if (~new_wp->flags & PANE_EMPTY) { - xasprintf(&cp, "tmux(%lu).%%%u", (long)getpid(), new_wp->id); - utempter_add_record(new_wp->fd, cp); - kill(getpid(), SIGCHLD); - free(cp); - } -#endif - new_wp->flags &= ~PANE_EXITED; + window_pane_utmp_add(new_wp); sigprocmask(SIG_SETMASK, &oldset, NULL); window_pane_set_event(new_wp); @@ -701,7 +698,7 @@ spawn_editor_finish(struct window_pane *wp) struct spawn_editor_state * spawn_editor(struct client *c, const char *buf, size_t len, - spawn_finish_edit_cb cb, void *arg) + const char *editor, spawn_finish_edit_cb cb, void *arg) { struct spawn_editor_state *es; struct spawn_context sc = { 0 }; @@ -715,13 +712,13 @@ spawn_editor(struct client *c, const char *buf, size_t len, FILE *f; char *cmd, *cause = NULL; char path[] = _PATH_TMP "tmux.XXXXXXXX"; - const char *editor; int fd; if (w->modal != NULL) return (NULL); - editor = options_get_string(global_options, "editor"); + if (editor == NULL) + editor = options_get_string(global_options, "editor"); fd = mkstemp(path); if (fd == -1) return (NULL); diff --git a/tmux.1 b/tmux.1 index db16793f1..59c4b8b46 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1174 2026/09/25 13:44:49 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1182 2026/10/09 13:12:14 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: September 25 2026 $ +.Dd $Mdocdate: October 9 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -386,10 +386,11 @@ Change to the pane above, below, to the left, or to the right of the current pane. .It M\-1 to M\-7 Arrange panes in one of the seven preset layouts: -even\-horizontal, even\-vertical, -main\-horizontal, main\-horizontal\-mirrored, -main\-vertical, main\-vertical\-mirrored, -or tiled. +.Cm even\-horizontal , even\-vertical , +.Cm main\-horizontal , main\-horizontal\-mirrored , +.Cm main\-vertical , main\-vertical\-mirrored , +or +.Cm tiled . .It Space Arrange the current window in the next preset layout. .It M\-n @@ -1120,22 +1121,22 @@ detaching the client, typically causing it to exit. sets a comma-separated list of client flags. The flags are: .Bl -tag -width Ds -.It ignore\-size +.It Cm ignore\-size the client does not affect the size of other clients -.It new\-layouts +.It Cm new\-layouts use the new layout string format -.It no\-detach\-on\-destroy +.It Cm no\-detach\-on\-destroy do not detach the client when the session it is attached to is destroyed if there are any other sessions -.It no\-output +.It Cm no\-output the client does not receive pane output in control mode -.It pause\-after=seconds +.It Cm pause\-after Ns = Ns Ar seconds output is paused once the pane is .Ar seconds behind in control mode -.It read\-only +.It Cm read\-only the client is read-only -.It wait\-exit +.It Cm wait\-exit wait for an empty line input before exiting in control mode .El .Pp @@ -1145,7 +1146,7 @@ turns a flag off if the client is already attached. .Fl r is an alias for .Fl f -.Ar read\-only,ignore\-size . +.Cm read\-only , Ns Cm ignore\-size . When a client is read-only, only keys bound to the .Ic detach\-client or @@ -1270,6 +1271,7 @@ If is specified, list only clients connected to that session. .Tg lscm .It Xo Ic list\-commands +.Op Fl h .Op Fl F Ar format .Op Ar command .Xc @@ -1278,6 +1280,11 @@ List the syntax of .Ar command or - if omitted - of all commands supported by .Nm . +With +.Fl h , +show command names and a short description. +.Fl h +twice shows syntax as well. .Tg ls .It Xo Ic list\-sessions .Op Fl r @@ -1521,7 +1528,7 @@ If .Ql continue , .Nm will return to sending output to the pane if it was paused (manually or with the -.Ar pause\-after +.Cm pause\-after flag). If .Ql pause , @@ -1764,9 +1771,9 @@ to specify the field to sort on: one of (time). .Fl r toggles the client -.Ic read\-only +.Cm read\-only and -.Ic ignore\-size +.Cm ignore\-size flags (see the .Ic attach\-session command). @@ -2012,6 +2019,25 @@ Same as .Ic copy\-pipe but also exit copy mode. .It Xo +.Ic copy\-pipe\-output +.Op Fl aCP +.Op Ar command +.Op Ar prefix +.Xc +Copy the current OSC 133 command output, pipe it to +.Ar command , +and clear the selection. +.Ar prefix +is used to name the new paste buffer. +.It Xo +.Ic copy\-output +.Op Fl aCP +.Op Ar prefix +.Xc +Copy the current OSC 133 command output and clear the selection. +.Ar prefix +is used to name the new paste buffer. +.It Xo .Ic copy\-selection .Op Fl CP .Op Ar prefix @@ -2230,6 +2256,31 @@ but treat all non-whitespace characters as part of a word. .Xc Switch at which end of the selection the cursor sits. .It Xo +.Ic open\-output +.Op Fl a +.Op Ar command +(vi: M\-e) +(emacs: e) +.Xc +Open the current OSC 133 command output in an editor. +If +.Ar command +is given, it is used as the editor command; otherwise, the +.Ic editor +option is used. +.It Xo +.Ic open\-selection +.Op Ar command +(vi: M\-e) +(emacs: e) +.Xc +Open the current selection in an editor. +If +.Ar command +is given, it is used as the editor command; otherwise, the +.Ic editor +option is used. +.It Xo .Ic page\-down (vi: C\-f) (emacs: PageDown) @@ -2270,6 +2321,21 @@ Same as .Ic pipe but also exit copy mode. .It Xo +.Ic pipe\-output +.Op Fl a +.Op Ar command +.Xc +Pipe the current OSC 133 command output to +.Ar command +and clear the selection. +.It Xo +.Ic pipe\-selection +.Op Ar command +.Xc +Pipe the selected text to +.Ar command +and clear the selection. +.It Xo .Ic previous\-matching\-bracket (emacs: M\-C\-b) .Xc @@ -2352,7 +2418,7 @@ Scroll down. If .Ic mode\-keys is -.Ic vi , +.Cm vi , the cursor is fixed relative to the text. .It Xo .Ic scroll\-down\-and\-cancel @@ -2399,7 +2465,7 @@ Scroll up. If .Ic mode\-keys is -.Ic vi , +.Cm vi , the cursor is fixed relative to the text. .It Xo .Ic search\-again @@ -2459,6 +2525,13 @@ Search forward for the specified plain text. Repeat the last search in the reverse direction (forward becomes backward and backward becomes forward). .It Xo +.Ic select\-output +.Op Fl a +(vi: M\-o) +(emacs: C\-o) +.Xc +Select the current OSC 133 command output. +.It Xo .Ic select\-line (vi: V) .Xc @@ -2668,31 +2741,31 @@ as normal. .Pp The following layouts are supported: .Bl -tag -width Ds -.It Ic even\-horizontal +.It Cm even\-horizontal Panes are spread out evenly from left to right across the window. -.It Ic even\-vertical +.It Cm even\-vertical Panes are spread evenly from top to bottom. -.It Ic main\-horizontal +.It Cm main\-horizontal A large (main) pane is shown at the top of the window and the remaining panes are spread from left to right in the leftover space at the bottom. Use the .Em main\-pane\-height window option to specify the height of the top pane. -.It Ic main\-horizontal\-mirrored +.It Cm main\-horizontal\-mirrored The same as -.Ic main\-horizontal +.Cm main\-horizontal but mirrored so the main pane is at the bottom of the window. -.It Ic main\-vertical +.It Cm main\-vertical A large (main) pane is shown on the left of the window and the remaining panes are spread from top to bottom in the leftover space on the right. Use the .Em main\-pane\-width window option to specify the width of the left pane. -.It Ic main\-vertical\-mirrored +.It Cm main\-vertical\-mirrored The same as -.Ic main\-vertical +.Cm main\-vertical but mirrored so the main pane is on the right of the window. -.It Ic tiled +.It Cm tiled Panes are spread out as evenly as possible over the window in both rows and columns. .El @@ -3132,8 +3205,9 @@ The command works only if at least one client is attached. .It Xo .Ic switch\-mode -.Op Fl kswZ +.Op Fl krsZw .Op Fl F Ar format +.Op Fl O Ar sort\-order .Op Fl t Ar target\-pane .Op Ar command .Xc @@ -3146,6 +3220,15 @@ matching and only matching items are shown, sorted by how well they match. lists sessions (the default) and .Fl w lists windows. +.Fl O +specifies the initial sort order within matches of equal quality: one of +.Ql index , +.Ql name , +or +.Ql activity +(time). +.Fl r +reverses the sort order. .Fl Z zooms the pane. The following keys may be used in switch mode: @@ -3620,6 +3703,8 @@ is the command to execute. If .Ar shell\-command is not specified, the value of the +.Ic window\-default\-command +or .Ic default\-command option is used. .Fl c @@ -4557,13 +4642,13 @@ or .Pc . Values of .Em flag Ns -type options may be one of: -.Ic 1 , -.Ic on , -.Ic yes , -.Ic 0 , -.Ic off , +.Cm 1 , +.Cm on , +.Cm yes , +.Cm 0 , +.Cm off , or -.Ic no ; +.Cm no ; for possible .Em choice values, see the respective option; for @@ -4680,6 +4765,9 @@ flag unsets an option, so a session inherits the option from the global options (or with .Fl g , restores a global option to the default). +For an array option given with a key, +.Fl u +removes only that item. .Fl U unsets an option (like .Fl u ) @@ -4779,7 +4867,7 @@ 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 +.Op Cm on | off .Xc When enabled (the default), the client terminal switches to the alternate screen and clears it when a client is attached. @@ -4788,7 +4876,7 @@ When disabled, 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 +.Ar name Ns = Ns Ar value .Xc This is an array of custom aliases for commands. If an unknown command matches @@ -4825,12 +4913,12 @@ Give the command to pipe to if the .Ic copy\-pipe copy mode command is used without arguments. .It Xo Ic theme -.Op Ic detect | terminal | light | dark +.Op Cm detect | terminal | light | dark .Xc Whether tmux should use the detected client theme -.Pq Ic detect , +.Pq Cm detect , use terminal ANSI colours -.Pq Ic terminal , +.Pq Cm terminal , or force the light or dark theme. .It Xo Ic dark\-theme\-black , .Ic dark\-theme\-white , @@ -4855,17 +4943,17 @@ or force the light or dark theme. .Xc Set the theme colours. These are the named colours -.Ic themeblack , -.Ic themewhite , -.Ic themelightgrey , -.Ic themedarkgrey , -.Ic themegreen , -.Ic themeyellow , -.Ic themered , -.Ic themeblue , -.Ic themecyan +.Cm THemeblack , +.Cm themewhite , +.Cm themelightgrey , +.Cm themedarkgrey , +.Cm themegreen , +.Cm themeyellow , +.Cm themered , +.Cm themeblue , +.Cm themecyan and -.Ic thememagenta +.Cm thememagenta which may be used in styles and elsewhere a .Ar colour is accepted. @@ -4902,16 +4990,16 @@ Set the command used when .Nm runs an editor. .It Xo Ic exit\-empty -.Op Ic on | off +.Op Cm on | off .Xc If enabled (the default), the server will exit when there are no active sessions. .It Xo Ic exit\-unattached -.Op Ic on | off +.Op Cm on | off .Xc If enabled, the server will exit when there are no attached clients. .It Xo Ic extended\-keys -.Op Ic on | off | always +.Op Cm on | off | always .Xc Controls how modified keys are reported. When enabled, @@ -4937,12 +5025,12 @@ its disambiguate and report-all enhancements are supported. For .Ic modifyOtherKeys , when set to -.Ic on , +.Cm on , the program inside the pane can request one of two modes: mode 1 which changes the sequence for only keys which lack an existing well-known representation; or mode 2 which changes the sequence for all keys. When set to -.Ic always , +.Cm always , modes 1 and 2 can still be requested by applications, but mode 1 will be forced instead of the standard mode. This applies only to applications inside panes; it does not force the Kitty @@ -4950,7 +5038,7 @@ keyboard protocol or change what .Nm requests from the terminal. When set to -.Ic off , +.Cm off , this feature is disabled and only standard keys are reported. .Pp .Nm @@ -4971,7 +5059,7 @@ and .Ic client_key_mode variables. .It Xo Ic extended\-keys\-format -.Op Ic csi\-u | xterm | kitty +.Op Cm csi\-u | xterm | kitty .Xc Selects the format for reporting modified keys to programs which request .Ic modifyOtherKeys . @@ -4982,18 +5070,18 @@ resource. For example, C\-S\-a will be reported as .Ql \[ha][[27;6;65\[ti] when set to -.Ic xterm , +.Cm xterm , and as .Ql \[ha][[65;6u when set to -.Ic csi\-u . -.Ic kitty +.Cm csi\-u . +.Cm kitty is accepted for compatibility and is the same as -.Ic csi\-u ; +.Cm csi\-u ; programs which request the Kitty keyboard protocol receive it whatever this option is set to. .It Xo Ic focus\-events -.Op Ic on | off +.Op Cm on | off .Xc When enabled, focus events are requested from the terminal if supported and passed through to applications running in @@ -5001,32 +5089,32 @@ passed through to applications running in Attached clients should be detached and attached again after changing this option. .It Xo Ic focus\-follows\-mouse -.Op Ic on | off +.Op Cm on | off .Xc When enabled and .Ic mouse is on, moving the mouse into a pane selects it. .It Xo Ic get\-clipboard -.Op Ic both | request | buffer | off +.Op Cm both | request | buffer | off .Xc Controls the behaviour when an application requests the clipboard from .Nm . .Pp If -.Ic off , +.Cm off , the request is ignored; if -.Ic buffer , +.Cm buffer , .Nm responds with the newest paste buffer; -.Ic request +.Cm request causes .Nm to request the clipboard from the most recently used client (if possible) and send the reply (if any) back to the application; -.Ic both +.Cm both is the same as -.Ic request +.Cm request but also creates a paste buffer. .Pp See also the @@ -5046,7 +5134,7 @@ each client. Set the number of history items to save in the history file for each type of command prompt. .It Xo Ic set\-clipboard -.Op Ic on | external | off +.Op Cm on | external | off .Xc Attempt to set the terminal clipboard content using the .Xr xterm 1 @@ -5059,19 +5147,19 @@ description (see the section). .Pp If set to -.Ic on , +.Cm on , .Nm will both accept the escape sequence to create a buffer and attempt to set the terminal clipboard. If set to -.Ic external , +.Cm external , .Nm will attempt to set the terminal clipboard but ignore attempts by applications to set .Nm buffers. If -.Ic off , +.Cm off , .Nm will neither accept the clipboard escape sequence nor attempt to set the clipboard. @@ -5213,9 +5301,19 @@ For example: set \-s user\-keys[0] "\ee[5;30012\[ti]" bind User0 resize\-pane \-L 3 .Ed -.It Xo Ic variation\-selector\-always\-wide +.It Xo Ic utmp .Op Ic on | off .Xc +Maintain utmp records for panes using +.Xr utempter 3 . +This option is unavailable if +.Nm +was built without +.Xr utempter 3 +support. +.It Xo Ic variation\-selector\-always\-wide +.Op Cm on | off +.Xc Always treat Unicode variation selector 16 as marking a wide character. This is a feature of some terminals as part of their Unicode 14 support. .El @@ -5223,23 +5321,23 @@ This is a feature of some terminals as part of their Unicode 14 support. Available session options are: .Bl -tag -width Ds .It Xo Ic activity\-action -.Op Ic any | none | current | other +.Op Cm any | none | current | other .Xc Set action on window activity when .Ic monitor\-activity is on. -.Ic any +.Cm any means activity in any window linked to a session causes a bell or message (depending on .Ic visual\-activity ) in the current window of that session, -.Ic none +.Cm none means all activity is ignored (equivalent to .Ic monitor\-activity being off), -.Ic current +.Cm current means only activity in windows other than the current window are ignored and -.Ic other +.Cm other means activity in the current window is ignored but not those in other windows. .It Ic assume\-paste\-time Ar milliseconds If keys are entered faster than one in @@ -5253,7 +5351,7 @@ Set the base index from which an unused index should be searched when a new window is created. The default is zero. .It Xo Ic bell\-action -.Op Ic any | none | current | other +.Op Cm any | none | current | other .Xc Set action on a bell in a window when .Ic monitor\-bell @@ -5288,7 +5386,7 @@ or This option should be configured when .Nm is used as a login shell. -.It Ic default\-size Ar XxY +.It Ic default\-size Ar X Ns x Ns Ar Y Set the default size of new windows when the .Ic window\-size option is set to manual or when a session is created with @@ -5299,41 +5397,48 @@ The value is the width and height separated by an character. The default is 80x24. .It Xo Ic destroy\-unattached -.Op Ic off | on | keep\-last | keep\-group +.Op Cm off | on | keep\-last | keep\-group .Xc If -.Ic on , +.Cm on , destroy the session after the last client has detached. If -.Ic off +.Cm off (the default), leave the session orphaned. If -.Ic keep\-last , +.Cm keep\-last , destroy the session only if it is in a group and has other sessions in that group. If -.Ic keep\-group , +.Cm keep\-group , destroy the session unless it is in a group and is the only session in that group. +A session without attached clients is destroyed or kept as follows: +.Bl -column "in group, NOT last" "keep" "destroy" "keep\-last" "keep\-group" +.It Sy "Session" Ta Cm off Ta Cm on Ta Cm keep\-last Ta Cm keep\-group +.It Li "NOT in group" Ta keep Ta destroy Ta keep Ta destroy +.It Li "in group, NOT last" Ta keep Ta destroy Ta destroy Ta destroy +.It Li "last in group" Ta keep Ta destroy Ta keep Ta keep +.El .It Xo Ic detach\-on\-destroy -.Op Ic off | on | no\-detached | previous | next +.Op Cm off | on | no\-detached | previous | next .Xc If -.Ic on +.Cm on (the default), the client is detached when the session it is attached to is destroyed. If -.Ic off , +.Cm off , the client is switched to the most recently active of the remaining sessions. If -.Ic no\-detached , +.Cm no\-detached , the client is detached only if there are no detached sessions; if detached sessions exist, the client is switched to the most recently active. If -.Ic previous +.Cm previous or -.Ic next , +.Cm next , the client is switched to the previous or next session in alphabetical order. .It Ic display\-time Ar time Set the amount of time for which status line messages and other on-screen @@ -5397,14 +5502,14 @@ section on how to specify Set the type of characters used for drawing menu borders. .Ar type may be one of -.Ic single , -.Ic rounded , -.Ic double , -.Ic heavy , -.Ic simple , -.Ic padded , +.Cm single , +.Cm rounded , +.Cm double , +.Cm heavy , +.Cm simple , +.Cm padded , or -.Ic none . +.Cm none . .It Ic message\-command\-style Ar style Set status line message command style. This is used for the command prompt with @@ -5433,7 +5538,7 @@ The default uses a conditional to select between and .Ic message\-command\-style . .It Xo Ic message\-line -.Op Ic 0 | 1 | 2 | 3 | 4 +.Op Cm 0 | 1 | 2 | 3 | 4 .Xc Set line on which status line messages and the command prompt are shown. .It Ic message\-style Ar style @@ -5462,7 +5567,7 @@ see the .Sx STYLES section. .It Xo Ic mouse -.Op Ic on | off +.Op Cm on | off .Xc If on, .Nm @@ -5511,7 +5616,7 @@ See the .Ic cursor\-style options for available styles. .It Xo Ic renumber\-windows -.Op Ic on | off +.Op Cm on | off .Xc If on, when a window is closed in a session, automatically renumber the other windows in numerical order. @@ -5535,7 +5640,7 @@ See also the .Ic initial\-repeat\-time option. .It Xo Ic set\-titles -.Op Ic on | off +.Op Cm on | off .Xc Attempt to set the client terminal title using the .Em tsl @@ -5556,7 +5661,7 @@ Formats are expanded, see the .Sx FORMATS section. .It Xo Ic silence\-action -.Op Ic any | none | current | other +.Op Cm any | none | current | other .Xc Set action on window silence when .Ic monitor\-silence @@ -5564,22 +5669,31 @@ is on. The values are the same as those for .Ic activity\-action . .It Xo Ic status -.Op Ic off | on | 2 | 3 | 4 | 5 +.Op Cm off | on | 2 | 3 | 4 | 5 .Xc Show or hide the status line or specify its size. Using -.Ic on +.Cm on gives a status line one row in height; -.Ic 2 , -.Ic 3 , -.Ic 4 +.Cm 2 , +.Cm 3 , +.Cm 4 or -.Ic 5 +.Cm 5 more rows. .It Ic status\-format[] Ar format Specify the format to be used for each line of the status line. -The default builds the top status line from the various individual status -options below. +The default first status line is built from the various individual status +options below, the second shows the panes in the current window and the third +the sessions. +A default line may be extended rather than replaced by saving its default value +in a user option and expanding that in the new value, for example: +.Bd -literal -offset indent +set \-g status 2 +set \-gu status\-format # start with default status line +set \-gF @old_status_format1 "#{status\-format[1]}" +set \-g status\-format[1] "#{E:@old_status_format1}#[nolist align=right]#{pane_current_path}" +.Ed .It Ic status\-interval Ar interval Update the status line every .Ar interval @@ -5587,17 +5701,19 @@ seconds. By default, updates will occur every 15 seconds. A setting of zero disables redrawing at interval. .It Xo Ic status\-justify -.Op Ic left | centre | right | absolute\-centre +.Op Cm left | centre | right | absolute\-centre .Xc Set the position of the window list in the status line: left, centre or right. centre puts the window list in the relative centre of the available free space; absolute\-centre uses the centre of the entire horizontal space. .It Xo Ic status\-keys -.Op Ic vi | emacs +.Op Cm vi | emacs .Xc Use vi or emacs-style key bindings in the status line, for example at the command prompt. -The default is emacs, unless the +The default is +.Cm emacs , +unless the .Ev VISUAL or .Ev EDITOR @@ -5641,7 +5757,7 @@ see the .Sx STYLES section. .It Xo Ic status\-position -.Op Ic top | bottom +.Op Cm top | bottom .Xc Set the position of the status line. .It Ic status\-right Ar string @@ -5685,7 +5801,7 @@ was given to the .Ic set\-environment command). .It Xo Ic visual\-activity -.Op Ic on | off | both +.Op Cm on | off | both .Xc If on, display a message instead of sending a bell when activity occurs in a window for which the @@ -5693,7 +5809,7 @@ window for which the window option is enabled. If set to both, a bell and a message are produced. .It Xo Ic visual\-bell -.Op Ic on | off | both +.Op Cm on | off | both .Xc If on, a message is shown on a bell in a window for which the .Ic monitor\-bell @@ -5704,7 +5820,7 @@ Also see the .Ic bell\-action option. .It Xo Ic visual\-silence -.Op Ic on | off | both +.Op Cm on | off | both .Xc If .Ic monitor\-silence @@ -5726,7 +5842,7 @@ Available window options are: .Pp .Bl -tag -width Ds -compact .It Xo Ic aggressive\-resize -.Op Ic on | off +.Op Cm on | off .Xc Aggressively resize the chosen window. This means that @@ -5742,7 +5858,7 @@ session; this option is good for full-screen programs which support and poor for interactive programs such as shells. .Pp .It Xo Ic automatic\-rename -.Op Ic on | off +.Op Cm on | off .Xc Control automatic window renaming. When this setting is enabled, @@ -5773,7 +5889,7 @@ option is enabled. Set clock colour. .Pp .It Xo Ic clock\-mode\-style -.Op Ic 12 | 24 | 12\-with\-seconds | 24\-with\-seconds +.Op Cm 12 | 24 | 12\-with\-seconds | 24\-with\-seconds .Xc Set clock hour format. .Pp @@ -5820,11 +5936,11 @@ set. .It Ic main\-pane\-height Ar height .It Ic main\-pane\-width Ar width Set the width or height of the main (left or top) pane in the -.Ic main\-horizontal , -.Ic main\-horizontal\-mirrored , -.Ic main\-vertical , +.Cm main\-horizontal , +.Cm main\-horizontal\-mirrored , +.Cm main\-vertical , or -.Ic main\-vertical\-mirrored +.Cm main\-vertical\-mirrored layouts. If suffixed by .Ql % , @@ -5898,40 +6014,42 @@ see the section. .Pp .It Xo Ic copy\-mode\-line\-numbers -.Op Ic off | default | absolute | relative | hybrid +.Op Cm off | default | absolute | relative | hybrid .Xc Show line numbers in copy mode. -.Ic off +.Cm off hides line numbers, -.Ic default +.Cm default shows line numbers matching the copy mode position indicator and .Ic goto\-line , -.Ic absolute +.Cm absolute shows absolute line numbers, -.Ic relative +.Cm relative shows line numbers relative to the cursor, and -.Ic hybrid +.Cm hybrid shows the current line number as absolute and other line numbers as relative. With -.Ic off +.Cm off or -.Ic default , +.Cm default , the position indicator and .Ic goto\-line use the same numbering; with -.Ic absolute , -.Ic relative +.Cm absolute , +.Cm relative and -.Ic hybrid , +.Cm hybrid , they use absolute line numbers. .Pp .It Xo Ic mode\-keys -.Op Ic vi | emacs +.Op Cm vi | emacs .Xc Use vi or emacs-style key bindings in copy mode. -The default is emacs, unless +The default is +.Cm emacs , +unless .Ev VISUAL or .Ev EDITOR @@ -5947,22 +6065,22 @@ see the section. .Pp .It Xo Ic monitor\-activity -.Op Ic on | off +.Op Cm on | off .Xc Monitor for activity in the window. Windows with activity are highlighted in the status line. .Pp .It Xo Ic monitor\-bell -.Op Ic on | off +.Op Cm on | off .Xc Monitor for a bell in the window. Windows with a bell are highlighted in the status line. .Pp .It Xo Ic monitor\-silence -.Op Ic interval +.Op Ar interval .Xc Monitor for silence (no activity) in the window within -.Ic interval +.Ar interval seconds. Windows that have been silent for the interval are highlighted in the status line. @@ -5970,9 +6088,9 @@ An interval of zero disables the monitoring. .Pp .It Ic other\-pane\-height Ar height Set the height of the other panes (not the main pane) in the -.Ic main\-horizontal +.Cm main\-horizontal and -.Ic main\-horizontal\-mirrored +.Cm main\-horizontal\-mirrored layouts. If this option is set to 0 (the default), it will have no effect. If both the @@ -5989,9 +6107,9 @@ this is a percentage of the window size. Like .Ic other\-pane\-height , but set the width of other panes in the -.Ic main\-vertical +.Cm main\-vertical and -.Ic main\-vertical\-mirrored +.Cm main\-vertical\-mirrored layouts. .Pp .It Ic pane\-active\-border\-style Ar style @@ -6012,7 +6130,7 @@ but set the starting index for pane numbers. Set the text shown in pane border status lines. .Pp .It Xo Ic pane\-border\-indicators -.Op Ic off | colour | arrows | both +.Op Cm off | colour | arrows | both .Xc Indicate active pane by colouring only half of the border in windows with exactly two panes, by displaying arrow markers, by drawing both or neither. @@ -6021,38 +6139,39 @@ exactly two panes, by displaying arrow markers, by drawing both or neither. Set the type of characters used for drawing pane borders. .Ar type may be one of: -.Bl -tag -width Ds -.It single +.Pp +.Bl -tag -width rounded -compact +.It Cm single single lines using ACS or UTF\-8 characters -.It double +.It Cm double double lines using UTF\-8 characters -.It heavy +.It Cm heavy heavy lines using UTF\-8 characters -.It rounded +.It Cm rounded single lines with rounded corners using UTF\-8 characters -.It simple +.It Cm simple simple ASCII characters -.It number +.It Cm number the pane number -.It spaces +.It Cm spaces space characters -.It none +.It Cm none no border for floating panes .El .Pp -.Ql double +.Cm double and -.Ql heavy +.Cm heavy will fall back to standard ACS line drawing when UTF\-8 is not supported. .Pp .It Xo Ic pane\-border\-status -.Op Ic off | top | bottom | top\-floating | bottom\-floating +.Op Cm off | top | bottom | top\-floating | bottom\-floating .Xc Turn pane border status lines off or set their position. If set to -.Ic top\-floating +.Cm top\-floating or -.Ic bottom\-floating , +.Cm bottom\-floating , status lines are shown only on floating panes. .Pp .It Ic pane\-border\-style Ar style @@ -6065,7 +6184,7 @@ section. Attributes are ignored. .Pp .It Xo Ic pane\-scrollbars -.Op Ic off | modal | on | auto\-hide +.Op Cm off | modal | on | auto\-hide .Xc When enabled, a character based scrollbar appears on the left or right of each pane. @@ -6074,27 +6193,27 @@ A filled section of the scrollbar, known as the represents the position and size of the visible part of the pane content. .Pp If set to -.Ic on +.Cm on the scrollbar is visible all the time and the pane is narrowed by the width of the scrollbar. If set to -.Ic modal +.Cm modal the scrollbar only appears when the pane is in copy mode or view mode and auto-hides when not in use. If set to -.Ic auto\-hide +.Cm auto\-hide the scrollbar is available all the time but auto-hides when not in use. With -.Ic modal +.Cm modal and -.Ic auto\-hide , +.Cm auto\-hide , the scrollbar overlays the pane and does not narrow or reflow the pane. .Pp See also .Ic pane\-scrollbars\-style . .Pp .It Xo Ic pane\-scrollbars\-position -.Op Ic left | right +.Op Cm left | right .Xc Sets which side of the pane to display pane scrollbars on. .Pp @@ -6118,9 +6237,9 @@ Other attributes are ignored. Set the time in milliseconds before scrollbars are hidden when .Ic pane\-scrollbars is -.Ic modal +.Cm modal or -.Ic auto\-hide . +.Cm auto\-hide . .Pp .It Ic pane\-status\-current\-style Ar style Set status line style for the currently active pane. @@ -6156,7 +6275,7 @@ section. .Pp .It Ic tiled\-layout\-max\-columns Ar number Set the maximum number of columns in the -.Ic tiled +.Cm tiled layout. A value of 0 (the default) means no limit. When a limit is set, panes are arranged to not exceed this number of columns, @@ -6189,6 +6308,11 @@ see the .Sx STYLES section. .Pp +.It Ic window\-default\-command Ar shell\-command +Per-window version of the +.Ic default\-command +option. +.Pp .It Ic window\-status\-activity\-style Ar style Set status line style for windows with an activity alert. For how to specify @@ -6273,7 +6397,7 @@ command and the option. .Pp .It Xo Ic wrap\-search -.Op Ic on | off +.Op Cm on | off .Xc If this option is set, searches will wrap around the end of the pane contents. The default is on. @@ -6283,32 +6407,32 @@ Available pane options are: .Pp .Bl -tag -width Ds -compact .It Xo Ic allow\-passthrough -.Op Ic on | off | all +.Op Cm on | off | all .Xc Allow programs in the pane to bypass .Nm using a terminal escape sequence (\eePtmux;...\ee\e\e). If set to -.Ic on , +.Cm on , passthrough sequences will be allowed only if the pane is visible. If set to -.Ic all , +.Cm all , they will be allowed even if the pane is invisible. .Pp .It Xo Ic allow\-rename -.Op Ic on | off +.Op Cm on | off .Xc Allow programs in the pane to change the window name using a terminal escape sequence (\eek...\ee\e\e). .Pp .It Xo Ic allow\-set\-title -.Op Ic on | off +.Op Cm on | off .Xc Allow programs in the pane to change the title using the terminal escape sequences (\ee]2;...\ee\e\e or \ee]0;...\ee\e\e). .Pp .It Xo Ic alternate\-screen -.Op Ic on | off +.Op Cm on | off .Xc This option configures whether programs running inside the pane may use the terminal alternate screen feature, which allows the @@ -6327,13 +6451,13 @@ Set the colour of the cursor. .It Ic cursor\-style Ar style Set the style of the cursor. Available styles are: -.Ic default , -.Ic blinking\-block , -.Ic block , -.Ic blinking\-underline , -.Ic underline , -.Ic blinking\-bar , -.Ic bar . +.Cm default , +.Cm blinking\-block , +.Cm block , +.Cm blinking\-underline , +.Cm underline , +.Cm blinking\-bar , +.Cm bar . .Pp .It Ic pane\-colours[] Ar colour The default colour palette. @@ -6343,18 +6467,18 @@ uses when the colour with that index is requested. The index may be from zero to 255. .Pp .It Xo Ic remain\-on\-exit -.Op Ic on | off | failed | key | failed\-key +.Op Cm on | off | failed | key | failed\-key .Xc A pane with this flag set is not destroyed when the program running in it exits. If set to -.Ic failed , +.Cm failed , then only when the program exit status is not zero. If set to -.Ic key , +.Cm key , the pane stays open and closes when a key is pressed. If set to -.Ic failed\-key , +.Cm failed\-key , the pane stays open and closes when a key is pressed only if the program exit status is not zero. The pane may be reactivated with the @@ -6367,7 +6491,7 @@ Set the text shown at the bottom of exited panes when is enabled. .Pp .It Xo Ic scroll\-on\-clear -.Op Ic on | off +.Op Cm on | off .Xc When the entire screen is cleared and this option is on, scroll the contents of the screen into history before clearing it. @@ -6382,7 +6506,7 @@ see the section. .Pp .It Xo Ic synchronize\-panes -.Op Ic on | off +.Op Cm on | off .Xc Duplicate input to all other panes in the same window where this option is also on (only for panes that are not in any mode). @@ -6455,7 +6579,8 @@ itself. They are named with an .Ql after\- prefix. -For example, the following command adds a hook to select the even\-vertical +For example, the following command adds a hook to select the +.Cm even\-vertical layout after every .Ic split\-window : .Bd -literal -offset indent @@ -7334,7 +7459,9 @@ The following variables are available, where appropriate: .It Li "client_written" Ta "" Ta "Bytes written to client" .It Li "command" Ta "" Ta "Name of command in use, if any" .It Li "command_list_alias" Ta "" Ta "Command alias if listing commands" +.It Li "command_list_description" Ta "" Ta "Command description if listing commands" .It Li "command_list_name" Ta "" Ta "Command name if listing commands" +.It Li "command_list_name_width" Ta "" Ta "Maximum command name width in list" .It Li "command_list_usage" Ta "" Ta "Command usage if listing commands" .It Li "config_files" Ta "" Ta "List of configuration files loaded" .It Li "cursor_blinking" Ta "" Ta "1 if the cursor is blinking" @@ -7491,7 +7618,9 @@ The following variables are available, where appropriate: .It Li "scroll_region_lower" Ta "" Ta "Bottom of scroll region in pane" .It Li "scroll_region_upper" Ta "" Ta "Top of scroll region in pane" .It Li "search_count" Ta "" Ta "Count of search results" +.It Li "search_count_current" Ta "" Ta "Index of the current search result" .It Li "search_count_partial" Ta "" Ta "1 if search count is partial count" +.It Li "search_count_present" Ta "" Ta "1 if search count is available" .It Li "search_match" Ta "" Ta "Search match if any" .It Li "search_present" Ta "" Ta "1 if search started in copy mode" .It Li "selection_active" Ta "" Ta "1 if selection started and changes with the cursor in copy mode" @@ -8213,14 +8342,14 @@ command should be omitted. sets the type of characters used for drawing menu borders. .Ar border\-lines may be one of -.Ic single , -.Ic rounded , -.Ic double , -.Ic heavy , -.Ic simple , -.Ic padded , +.Cm single , +.Cm rounded , +.Cm double , +.Cm heavy , +.Cm simple , +.Cm padded , or -.Ic none . +.Cm none . .Pp .Fl H sets the style for the selected menu item (see @@ -8246,14 +8375,14 @@ and give the position of the menu. Both may be a row or column number, or one of the following special values: .Bl -column "XXXXX" "XXXX" -offset indent -.It Sy "Value" Ta Sy "Flag" Ta Sy "Meaning" -.It Li "C" Ta "Both" Ta "The centre of the terminal" -.It Li "R" Ta Fl x Ta "The right side of the terminal" -.It Li "P" Ta "Both" Ta "The bottom left of the pane" -.It Li "M" Ta "Both" Ta "The mouse position" -.It Li "L" Ta "Both" Ta "The position of the last menu" -.It Li "W" Ta "Both" Ta "The window position on the status line" -.It Li "S" Ta Fl y Ta "The line above or below the status line" +.It Sy Value Ta Sy Flag Ta Sy Meaning +.It Cm C Ta both Ta "The centre of the terminal" +.It Cm R Ta Fl x Ta "The right side of the terminal" +.It Cm P Ta both Ta "The bottom left of the pane" +.It Cm M Ta both Ta "The mouse position" +.It Cm L Ta both Ta "The position of the last menu" +.It Cm W Ta both Ta "The window position on the status line" +.It Cm S Ta Fl y Ta "The line above or below the status line" .El .Pp Or a format, which is expanded with the following additional variables. @@ -9020,7 +9149,7 @@ which is named An error has happened in a configuration file. .It Ic %continue Ar pane\-id The pane has been continued after being paused (if the -.Ar pause\-after +.Cm pause\-after flag is set, see .Ic refresh\-client .Fl A ) . @@ -9036,7 +9165,7 @@ describes why the client exited. New form of .Ic %output sent when the -.Ar pause\-after +.Cm pause\-after flag is set. .Ar age is the time in milliseconds for which tmux had buffered the output before it @@ -9058,7 +9187,7 @@ The new layout is and the window's visible layout is .Ar window\-visible\-layout . If the -.Ar new\-layouts +.Cm new\-layouts flag is set, both layout fields use the new format string; see .Ic refresh\-client .Fl f . @@ -9086,7 +9215,7 @@ Paste buffer has been deleted. .It Ic %pause Ar pane\-id The pane has been paused (if the -.Ar pause\-after +.Cm pause\-after flag is set). .It Ic %session\-changed Ar session\-id Ar name The client is now attached to the session with ID diff --git a/tmux.h b/tmux.h index 6acdf1dc0..d10497800 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1449 2026/09/28 10:10:16 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1455 2026/10/09 13:12:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -69,6 +69,7 @@ struct options_array_item; struct options_entry; struct prompt; struct window_pane_prompt; +struct redraw_damage; struct redraw_scene; struct redraw_span; struct screen_write_citem; @@ -483,6 +484,17 @@ enum { KEYC_REPORT_DARK_THEME, KEYC_REPORT_LIGHT_THEME, + /* Terminal replies, handled in tty-keys.c and never fired. */ + KEYC_REPORT_CLIPBOARD, + KEYC_REPORT_KITTY, + KEYC_REPORT_SYNC, + KEYC_REPORT_DA, + KEYC_REPORT_DA2, + KEYC_REPORT_XDA, + KEYC_REPORT_COLOURS, + KEYC_REPORT_PALETTE, + KEYC_REPORT_WINSZ, + /* Mouse state. */ KEYC_MOUSE, /* unclassified mouse event */ KEYC_DRAGGING, /* dragging in progress */ @@ -1434,6 +1446,7 @@ struct window_pane { #define PANE_CAPTUREALLKEYS 0x100000 #define PANE_FLOATOVERZOOM 0x200000 #define PANE_CLOSEONCANCEL 0x400000 +#define PANE_UTMP 0x800000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1496,6 +1509,7 @@ struct window_pane { struct screen base; struct screen status_screen; + u_int status_generation; TAILQ_HEAD(, window_mode_entry) modes; @@ -1526,6 +1540,7 @@ struct window_pane { TAILQ_HEAD(window_panes, window_pane); TAILQ_HEAD(window_panes_zindex, window_pane); RB_HEAD(window_pane_tree, window_pane); +TAILQ_HEAD(redraw_damages, redraw_damage); /* Window structure. */ struct window { @@ -1569,6 +1584,9 @@ struct window { uint64_t redraw_scene_generation; + struct redraw_damages damage; + u_int damage_count; + struct menu_data *menu; u_int menu_last_px; u_int menu_last_py; @@ -1895,6 +1913,8 @@ struct tty { struct event timer; size_t discarded; + size_t sync_offset; + struct termios tio; struct grid_cell cell; @@ -1930,6 +1950,7 @@ struct tty { u_int mouse_last_y; u_int mouse_last_b; int mouse_drag_flag; + int mouse_drag_status; u_int mouse_drag_x; u_int mouse_drag_y; int mouse_scrolling_flag; @@ -1945,7 +1966,7 @@ struct tty { }; /* Terminal command context. */ -typedef void (*tty_ctx_redraw_cb)(const struct tty_ctx *); +typedef void (*tty_ctx_redraw_cb)(const struct tty_ctx *, u_int, u_int); typedef int (*tty_ctx_set_client_cb)(struct tty_ctx *, struct client *); struct tty_ctx { struct screen *s; @@ -2149,6 +2170,7 @@ struct cmd_entry_flag { /* Command definition. */ struct cmd_entry { const char *name; + const char *description; const char *alias; struct args_parse args; @@ -2705,6 +2727,7 @@ extern char **cfg_files; extern u_int cfg_nfiles; extern int cfg_quiet; void start_cfg(void); +void cfg_client_lost(struct client *); int load_cfg(const char *, struct client *, struct cmdq_item *, struct cmd_find_state *, int, struct cmdq_item **); int load_cfg_from_buffer(const void *, size_t, const char *, @@ -3139,7 +3162,7 @@ int tty_keys_colours(struct tty *, const char *, size_t, size_t *, int tty_keys_kitty(struct tty *, const char *, size_t, size_t *, key_code *); int tty_keys_kitty_query(struct tty *, const char *, size_t, - size_t *); + size_t *, int); /* arguments.c */ void args_set(struct args *, u_char, struct args_value *, int); @@ -3624,6 +3647,8 @@ void grid_reader_start(struct grid_reader *, struct grid *, u_int, u_int); void grid_reader_get_cursor(struct grid_reader *, u_int *, u_int *); u_int grid_reader_line_length(struct grid_reader *); int grid_reader_in_set(struct grid_reader *, const char *); +int grid_reader_output_range(struct grid_reader *, u_int *, u_int *, + u_int *, u_int *); void grid_reader_cursor_right(struct grid_reader *, int, int, int); void grid_reader_cursor_left(struct grid_reader *, int); void grid_reader_cursor_down(struct grid_reader *); @@ -3749,8 +3774,12 @@ void redraw_screen(struct client *); void redraw_pane(struct client *, struct window_pane *); void redraw_pane_scrollbar(struct client *, struct window_pane *); void redraw_free_scene(struct redraw_scene *); +int redraw_client_has_window(struct client *, struct window *); void redraw_invalidate_scene(struct window *); void redraw_invalidate_all_scenes(void); +void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int); +void redraw_free_damage(struct window *); +void redraw_client_damage(struct client *); int redraw_get_status_border_cell_type(struct redraw_span **, u_int); /* screen.c */ @@ -3810,6 +3839,8 @@ struct window *window_find_by_id(u_int); void window_update_activity(struct window *); struct window *window_create(u_int, u_int, u_int, u_int); void window_pane_set_event(struct window_pane *); +void window_pane_utmp_add(struct window_pane *); +void window_pane_utmp_remove(struct window_pane *); void window_pane_wait_finish(struct window_pane *); struct window_pane *window_get_active_at(struct window *, u_int, u_int); struct window_pane *window_find_string(struct window *, const char *); @@ -3927,6 +3958,8 @@ struct style_range *window_pane_status_get_range(struct window_pane *, u_int, u_int); int window_pane_is_floating(struct window_pane *); int window_pane_is_floating_with_hidden(struct window_pane *); +void window_redraw_floating_pane(struct window_pane *, int, int, + int, int); /* window-border.c */ void window_set_fill_cells(struct window *); @@ -4307,7 +4340,7 @@ struct winlink *spawn_window(struct spawn_context *, char **); struct window_pane *spawn_pane(struct spawn_context *, char **); typedef void (*spawn_finish_edit_cb)(char *, size_t, void *); struct spawn_editor_state *spawn_editor(struct client *, const char *, size_t, - spawn_finish_edit_cb, void *); + const char *, spawn_finish_edit_cb, void *); void spawn_cancel_editor(struct spawn_editor_state *); pid_t spawn_get_editor_pid(struct spawn_editor_state *); void spawn_editor_finish(struct window_pane *); diff --git a/tty-keys.c b/tty-keys.c index 13c1c3471..6765dc6e6 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty-keys.c,v 1.215 2026/09/25 10:37:05 nicm Exp $ */ +/* $OpenBSD: tty-keys.c,v 1.216 2026/10/06 17:49:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -37,6 +37,20 @@ * into a ternary tree. */ +/* + * A key or terminal reply parsed from the input buffer. A reply is parsed + * without side effects and applied only once it is complete and not part of + * a longer key. + */ +struct tty_key_parse { + key_code key; + size_t size; + struct mouse_event m; + + int flags; +#define TTY_KEY_PARSE_NO_META 0x1 +}; + static void tty_keys_add1(struct tty_key **, const char *, key_code); static void tty_keys_add(struct tty *, const char *, key_code); static void tty_keys_free1(struct tty_key *); @@ -44,23 +58,45 @@ static struct tty_key *tty_keys_find1(struct tty_key *, const char *, size_t, size_t *); static struct tty_key *tty_keys_find(struct tty *, const char *, size_t, size_t *); -static int tty_keys_next1(struct tty *, const char *, size_t, key_code *, - size_t *, int); static void tty_keys_callback(int, short, void *); static int tty_keys_extended_key(struct tty *, const char *, size_t, size_t *, key_code *); static int tty_keys_mouse(struct tty *, const char *, size_t, size_t *, struct mouse_event *); static int tty_keys_clipboard(struct tty *, const char *, size_t, - size_t *); + size_t *, int); static int tty_keys_device_attributes(struct tty *, const char *, size_t, - size_t *); + size_t *, int); static int tty_keys_device_attributes2(struct tty *, const char *, size_t, - size_t *); + size_t *, int); 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 *); + size_t, size_t *, int); +static int tty_keys_sync(struct tty *, const char *, size_t, size_t *, + int); +static int tty_keys_colours1(struct tty *, const char *, size_t, size_t *, + int); +static int tty_keys_palette(struct tty *, const char *, size_t, size_t *, + int); +static int tty_keys_winsz(struct tty *, const char *, size_t, size_t *, + int); +static int tty_keys_next1(struct tty *, const char *, size_t, + struct tty_key_parse *, int); + +/* Terminal replies, in the order they are checked. */ +static const struct { + key_code key; + int (*fn)(struct tty *, const char *, size_t, size_t *, int); +} tty_keys_replies[] = { + { KEYC_REPORT_CLIPBOARD, tty_keys_clipboard }, + { KEYC_REPORT_KITTY, tty_keys_kitty_query }, + { KEYC_REPORT_SYNC, tty_keys_sync }, + { KEYC_REPORT_DA, tty_keys_device_attributes }, + { KEYC_REPORT_DA2, tty_keys_device_attributes2 }, + { KEYC_REPORT_XDA, tty_keys_extended_device_attributes }, + { KEYC_REPORT_COLOURS, tty_keys_colours1 }, + { KEYC_REPORT_PALETTE, tty_keys_palette }, + { KEYC_REPORT_WINSZ, tty_keys_winsz } +}; /* A key tree entry. */ struct tty_key { @@ -75,8 +111,8 @@ struct tty_key { /* Default raw keys. */ struct tty_default_key_raw { - const char *string; - key_code key; + const char *string; + key_code key; }; static const struct tty_default_key_raw tty_default_raw_keys[] = { /* Application escape. */ @@ -614,10 +650,57 @@ tty_keys_partial_paste_end(const char *buf, size_t len) return (memcmp(buf, paste_end, len) == 0); } +/* + * Look for a terminal reply, mouse event or extended key. These sequences + * carry their own modifiers, so none can follow an Escape as a Meta prefix. + */ +static int +tty_keys_reply(struct tty *tty, const char *buf, size_t len, + struct tty_key_parse *kp) +{ + u_int i; + int n; + + for (i = 0; i < nitems(tty_keys_replies); i++) { + n = tty_keys_replies[i].fn(tty, buf, len, &kp->size, 0); + if (n == 0) + kp->key = tty_keys_replies[i].key; + if (n != -1) + return (n); + } + + switch (tty_keys_mouse(tty, buf, len, &kp->size, &kp->m)) { + case 0: /* yes */ + kp->key = KEYC_MOUSE; + return (0); + case -1: /* no, or not valid */ + break; + case -2: /* yes, but we don't care */ + kp->key = KEYC_UNKNOWN; + return (0); + case 1: /* partial */ + return (1); + } + + switch (tty_keys_kitty(tty, buf, len, &kp->size, &kp->key)) { + case 0: /* yes */ + return (0); + case -1: /* no, or not valid */ + break; + case -2: /* yes, but not representable by tmux */ + kp->key = KEYC_UNKNOWN; + return (0); + case 1: /* partial */ + return (1); + } + + return (tty_keys_extended_key(tty, buf, len, &kp->size, &kp->key)); +} + /* Look up part of the next key. */ static int -tty_keys_next1(struct tty *tty, const char *buf, size_t len, key_code *key, - size_t *size, int expired) +tty_keys_next1(struct tty *tty, const char *buf, size_t len, + struct tty_key_parse *kp, int expired) { struct client *c = tty->client; struct tty_key *tk, *tk1; @@ -625,12 +708,27 @@ tty_keys_next1(struct tty *tty, const char *buf, size_t len, key_code *key, enum utf8_state more; utf8_char uc; u_int i; + key_code key; log_debug("%s: next key is %zu (%.*s) (expired=%d)", c->name, len, (int)len, buf, expired); + memset(kp, 0, sizeof *kp); + + /* Is this a terminal reply, mouse event or extended key? */ + switch (tty_keys_reply(tty, buf, len, kp)) { + case 0: /* yes */ + kp->flags |= TTY_KEY_PARSE_NO_META; + return (0); + case -1: /* no, or not valid */ + break; + case 1: /* partial */ + if (!expired) + return (1); + break; + } /* Is this a known key? */ - tk = tty_keys_find(tty, buf, len, size); + tk = tty_keys_find(tty, buf, len, &kp->size); if (tk != NULL && tk->key != KEYC_UNKNOWN) { tk1 = tk; do @@ -638,18 +736,29 @@ tty_keys_next1(struct tty *tty, const char *buf, size_t len, key_code *key, while ((tk1 = tk1->next) != NULL); if (tk->next != NULL && !expired) return (1); - *key = tk->key; - if ((*key & KEYC_MASK_KEY) == KEYC_PASTE_START) - tty->flags |= TTY_BRACKETPASTE; - else if ((*key & KEYC_MASK_KEY) == KEYC_PASTE_END) - tty->flags &= ~TTY_BRACKETPASTE; + key = kp->key = tk->key; + + /* + * A key with an implied Meta already includes its Escape, and + * focus and theme reports never have a Meta modifier. + */ + if ((key & KEYC_IMPLIED_META) || + key == KEYC_FOCUS_IN || + key == KEYC_FOCUS_OUT || + key == KEYC_REPORT_DARK_THEME || + key == KEYC_REPORT_LIGHT_THEME) + kp->flags |= TTY_KEY_PARSE_NO_META; return (0); } + /* Is this the start of a known key? */ + if (tk != NULL && !expired) + return (1); + /* Is this valid UTF-8? */ more = utf8_open(&ud, (u_char)*buf); if (more == UTF8_MORE) { - *size = ud.size; + kp->size = ud.size; if (len < ud.size) { if (!expired) return (1); @@ -662,84 +771,38 @@ tty_keys_next1(struct tty *tty, const char *buf, size_t len, key_code *key, if (utf8_from_data(&ud, &uc) != UTF8_DONE) return (-1); - *key = uc; + kp->key = uc; log_debug("%s: UTF-8 key %.*s %#llx", c->name, (int)ud.size, - ud.data, *key); + ud.data, kp->key); return (0); } return (-1); } -/* Process window size change escape sequences. */ -static int -tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size) +/* Apply a complete terminal reply. */ +static void +tty_keys_apply(struct tty *tty, const char *buf, struct tty_key_parse *kp) { - struct client *c = tty->client; - size_t end; - char tmp[64]; - u_int sx, sy, xpixel, ypixel, char_x, char_y; + size_t size; + u_int i; - *size = 0; - - /* If we did not request this, ignore it. */ - if (!(tty->flags & TTY_WINSIZEQUERY)) - return (-1); - - /* First two bytes are always \033[. */ - if (buf[0] != '\033') - return (-1); - if (len == 1) - return (1); - if (buf[1] != '[') - return (-1); - if (len == 2) - return (1); - - /* - * Stop at either 't' or anything that isn't a - * number or ';'. - */ - for (end = 2; end < len && end != sizeof tmp; end++) { - if (buf[end] == 't') - break; - if (!isdigit((u_char)buf[end]) && buf[end] != ';') - break; + if (kp->key == KEYC_MOUSE) { + tty->mouse_last_x = kp->m.x; + tty->mouse_last_y = kp->m.y; + tty->mouse_last_b = kp->m.b; + return; } - if (end == len) - return (1); - if (end == sizeof tmp || buf[end] != 't') - return (-1); - - /* Copy to the buffer. */ - memcpy(tmp, buf + 2, end - 2); - tmp[end - 2] = '\0'; - - /* Try to parse the window size sequence. */ - if (sscanf(tmp, "8;%u;%u", &sy, &sx) == 2) { - /* Window size in characters. */ - tty_set_size(tty, sx, sy, tty->xpixel, tty->ypixel); - - *size = end + 1; - return (0); - } else if (sscanf(tmp, "4;%u;%u", &ypixel, &xpixel) == 2) { - /* Window size in pixels. */ - char_x = (xpixel && tty->sx) ? xpixel / tty->sx : 0; - char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0; - tty_set_size(tty, tty->sx, tty->sy, char_x, char_y); - tty_invalidate(tty); - - tty->flags &= ~TTY_WINSIZEQUERY; - *size = end + 1; - return (0); + for (i = 0; i < nitems(tty_keys_replies); i++) { + if (tty_keys_replies[i].key == kp->key) { + tty_keys_replies[i].fn(tty, buf, kp->size, &size, 1); + kp->key = KEYC_UNKNOWN; + return; + } } - - log_debug("%s: unrecognized window size sequence: %s", c->name, tmp); - return (-1); } - /* Process at least one key in the buffer. Return 0 if no keys present. */ int tty_keys_next(struct tty *tty) @@ -749,9 +812,9 @@ tty_keys_next(struct tty *tty) const char *buf; size_t len, size; cc_t bspace; - int delay, expired = 0, n, bg = tty->bg; + int delay, expired = 0, n; key_code key, onlykey; - struct mouse_event m = { 0 }; + struct tty_key_parse kp; struct key_event *event; /* Get key buffer. */ @@ -771,150 +834,9 @@ tty_keys_next(struct tty *tty) return (1); } - /* Is this a clipboard response? */ - switch (tty_keys_clipboard(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 Kitty keyboard protocol response? */ - switch (tty_keys_kitty_query(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 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 */ - key = KEYC_UNKNOWN; - goto complete_key; - case -1: /* no, or not valid */ - break; - case 1: /* partial */ - goto partial_key; - } - - /* Is this a secondary device attributes response? */ - switch (tty_keys_device_attributes2(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 an extended device attributes response? */ - switch (tty_keys_extended_device_attributes(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 colours response? */ - switch (tty_keys_colours(tty, buf, len, &size, &tty->fg, &tty->bg)) { - case 0: /* yes */ - key = KEYC_UNKNOWN; - if (tty->bg != bg) - server_client_update_theme_colours(c); - session_theme_changed(c->session); - goto complete_key; - case -1: /* no, or not valid */ - break; - case 1: /* partial */ - if (tty->bg != bg) - server_client_update_theme_colours(c); - session_theme_changed(c->session); - goto partial_key; - } - - /* Is this a palette response? */ - switch (tty_keys_palette(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 mouse key press? */ - switch (tty_keys_mouse(tty, buf, len, &size, &m)) { - case 0: /* yes */ - key = KEYC_MOUSE; - goto complete_key; - case -1: /* no, or not valid */ - break; - case -2: /* yes, but we don't care. */ - key = KEYC_MOUSE; - goto discard_key; - case 1: /* partial */ - goto partial_key; - } - - /* Is this a Kitty keyboard protocol key press? */ - switch (tty_keys_kitty(tty, buf, len, &size, &key)) { - case 0: /* yes */ - goto complete_key; - case -1: /* no, or not valid */ - break; - case -2: /* yes, but not representable by tmux */ - goto discard_key; - case 1: /* partial */ - goto partial_key; - } - - /* Is this an extended key press? */ - switch (tty_keys_extended_key(tty, buf, len, &size, &key)) { - case 0: /* yes */ - goto complete_key; - case -1: /* no, or not valid */ - break; - case 1: /* partial */ - goto partial_key; - } - - /* Check for window size query */ - switch (tty_keys_winsz(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; - } - first_key: /* Try to lookup complete key. */ - n = tty_keys_next1(tty, buf, len, &key, &size, expired); + n = tty_keys_next1(tty, buf, len, &kp, expired); if (n == 0) /* found */ goto complete_key; if (n == 1) @@ -926,22 +848,25 @@ first_key: */ if (*buf == '\033' && len > 1) { /* Look for a key without the escape. */ - n = tty_keys_next1(tty, buf + 1, len - 1, &key, &size, expired); + n = tty_keys_next1(tty, buf + 1, len - 1, &kp, expired); if (n == 0) { /* found */ - if (key & KEYC_IMPLIED_META) { + if (kp.flags & TTY_KEY_PARSE_NO_META) { /* * We want the escape key as well as the xterm * key, because the xterm sequence implicitly * includes the escape (so if we see * \033\033[1;3D we know it is an Escape - * followed by M-Left, not just M-Left). + * followed by M-Left, not just M-Left). The + * same applies to terminal replies, mouse + * events and extended keys. */ - key = '\033'; - size = 1; + memset(&kp, 0, sizeof kp); + kp.key = '\033'; + kp.size = 1; goto complete_key; } - key |= KEYC_META; - size++; + kp.key |= KEYC_META; + kp.size++; goto complete_key; } if (n == 1) /* partial */ @@ -997,6 +922,9 @@ first_key: key = onlykey | KEYC_CTRL | (key & KEYC_META); } + memset(&kp, 0, sizeof kp); + kp.key = key; + kp.size = size; goto complete_key; partial_key: @@ -1043,32 +971,42 @@ partial_key: return (0); complete_key: - log_debug("%s: complete key %.*s %#llx", c->name, (int)size, buf, key); + log_debug("%s: complete key %.*s %#llx", c->name, (int)kp.size, buf, + kp.key); /* Remove key timer. */ if (event_initialized(&tty->key_timer)) evtimer_del(&tty->key_timer); tty->flags &= ~TTY_TIMER; + /* Apply terminal replies and mouse state. */ + tty_keys_apply(tty, buf, &kp); + + /* Check for bracketed paste. */ + if ((kp.key & KEYC_MASK_KEY) == KEYC_PASTE_START) + tty->flags |= TTY_BRACKETPASTE; + else if ((kp.key & KEYC_MASK_KEY) == KEYC_PASTE_END) + tty->flags &= ~TTY_BRACKETPASTE; + /* Check for focus events. */ - if (key == KEYC_FOCUS_OUT) { + if (kp.key == KEYC_FOCUS_OUT) { c->flags &= ~CLIENT_FOCUSED; window_update_focus(c->session->curw->window); events_fire_client("client-focus-out", c); - } else if (key == KEYC_FOCUS_IN) { + } else if (kp.key == KEYC_FOCUS_IN) { c->flags |= CLIENT_FOCUSED; events_fire_client("client-focus-in", c); window_update_focus(c->session->curw->window); } /* Fire the key. */ - if (key != KEYC_UNKNOWN) { + if (kp.key != KEYC_UNKNOWN) { event = xcalloc(1, sizeof *event); - event->key = key; - memcpy(&event->m, &m, sizeof event->m); + event->key = kp.key; + memcpy(&event->m, &kp.m, sizeof event->m); - event->buf = xmalloc(size); - event->len = size; + event->buf = xmalloc(kp.size); + event->len = kp.size; memcpy (event->buf, buf, event->len); if (!server_client_handle_key(c, event)) { @@ -1078,15 +1016,7 @@ complete_key: } /* Remove data from buffer. */ - evbuffer_drain(tty->in, size); - - return (1); - -discard_key: - log_debug("%s: discard key %.*s %#llx", c->name, (int)size, buf, key); - - /* Remove data from buffer. */ - evbuffer_drain(tty->in, size); + evbuffer_drain(tty->in, kp.size); return (1); } @@ -1353,7 +1283,7 @@ tty_keys_mouse(struct tty *tty, const char *buf, size_t len, size_t *size, } else return (-1); - /* Fill mouse event. */ + /* Fill mouse event. The last mouse state is updated when applied. */ m->lx = tty->mouse_last_x; m->x = x; m->ly = tty->mouse_last_y; @@ -1363,11 +1293,6 @@ tty_keys_mouse(struct tty *tty, const char *buf, size_t len, size_t *size, m->sgr_type = sgr_type; m->sgr_b = sgr_b; - /* Update last mouse state. */ - tty->mouse_last_x = x; - tty->mouse_last_y = y; - tty->mouse_last_b = b; - return (0); } @@ -1376,7 +1301,8 @@ tty_keys_mouse(struct tty *tty, const char *buf, size_t len, size_t *size, * partial. */ static int -tty_keys_clipboard(struct tty *tty, const char *buf, size_t len, size_t *size) +tty_keys_clipboard(struct tty *tty, const char *buf, size_t len, size_t *size, + int apply) { struct client *c = tty->client; size_t end, terminator = 0, needed; @@ -1422,6 +1348,8 @@ tty_keys_clipboard(struct tty *tty, const char *buf, size_t len, size_t *size) if (end == len) return (1); *size = end + 1; + if (!apply) + return (0); /* Skip the initial part. */ buf += 5; @@ -1491,7 +1419,7 @@ tty_keys_clipboard(struct tty *tty, const char *buf, size_t len, size_t *size) */ static int tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, - size_t *size) + size_t *size, int apply) { struct client *c = tty->client; u_int i, n = 0; @@ -1529,6 +1457,8 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, return (-1); tmp[i] = '\0'; *size = 4 + i; + if (!apply) + return (0); /* Convert all arguments to numbers. */ cp = tmp; @@ -1575,7 +1505,8 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len, * failure, 1 for partial. */ static int -tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size) +tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size, + int apply) { struct client *c = tty->client; static const char prefix[] = "\033[?2026;"; @@ -1607,6 +1538,8 @@ tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size) if (buf[i++] != 'y') return (-1); *size = i; + if (!apply) + return (0); if (status == 1 || status == 2 || status == 3) { tty_parse_client_features(c, "sync", ","); @@ -1624,7 +1557,7 @@ tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size) */ static int tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, - size_t *size) + size_t *size, int apply) { struct client *c = tty->client; u_int i, n = 0; @@ -1662,6 +1595,8 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, return (-1); tmp[i] = '\0'; *size = 4 + i; + if (!apply) + return (0); /* Convert all arguments to numbers. */ cp = tmp; @@ -1703,7 +1638,7 @@ tty_keys_device_attributes2(struct tty *tty, const char *buf, size_t len, */ static int tty_keys_extended_device_attributes(struct tty *tty, const char *buf, - size_t len, size_t *size) + size_t len, size_t *size, int apply) { struct client *c = tty->client; u_int i; @@ -1742,7 +1677,7 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, if (i == (sizeof tmp) - 1) return (-1); *size = 5 + i; - if (i == 0) + if (i == 0 || !apply) return (0); tmp[i - 1] = '\0'; @@ -1776,7 +1711,7 @@ tty_keys_extended_device_attributes(struct tty *tty, const char *buf, /* * Handle foreground or background input. Returns 0 for success, -1 for - * failure, 1 for partial. + * failure, 1 for partial. If fg is NULL, only check the reply. */ int tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, @@ -1824,7 +1759,7 @@ tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, if (i == (sizeof tmp) - 1) return (-1); *size = 6 + i; - if (i == 0) + if (i == 0 || fg == NULL) return (0); if (tmp[i - 1] == '\033') tmp[i - 1] = '\0'; @@ -1852,9 +1787,30 @@ tty_keys_colours(struct tty *tty, const char *buf, size_t len, size_t *size, return (0); } +/* Handle foreground or background input from the terminal. */ +static int +tty_keys_colours1(struct tty *tty, const char *buf, size_t len, size_t *size, + int apply) +{ + struct client *c = tty->client; + int bg = tty->bg, n; + + if (!apply) + return (tty_keys_colours(tty, buf, len, size, NULL, NULL)); + + n = tty_keys_colours(tty, buf, len, size, &tty->fg, &tty->bg); + if (n == 0) { + if (tty->bg != bg) + server_client_update_theme_colours(c); + session_theme_changed(c->session); + } + return (n); +} + /* Handle OSC 4 palette colour responses. */ static int -tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) +tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size, + int apply) { struct client *c = tty->client; u_int i; @@ -1908,6 +1864,8 @@ tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) return (-1); if (idx < 0 || idx > 255) return (-1); + if (!apply) + return (0); /* Work out the colour. */ pd.c = colour_parseX11(endptr + 1); @@ -1918,3 +1876,76 @@ tty_keys_palette(struct tty *tty, const char *buf, size_t len, size_t *size) return (0); } + +/* Process window size change escape sequences. */ +static int +tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size, + int apply) +{ + struct client *c = tty->client; + size_t end; + char tmp[64]; + u_int sx, sy, xpixel, ypixel, char_x, char_y; + + *size = 0; + + /* If we did not request this, ignore it. */ + if (!(tty->flags & TTY_WINSIZEQUERY)) + return (-1); + + /* First two bytes are always \033[. */ + if (buf[0] != '\033') + return (-1); + if (len == 1) + return (1); + if (buf[1] != '[') + return (-1); + if (len == 2) + return (1); + + /* + * Stop at either 't' or anything that isn't a + * number or ';'. + */ + for (end = 2; end < len && end != sizeof tmp; end++) { + if (buf[end] == 't') + break; + if (!isdigit((u_char)buf[end]) && buf[end] != ';') + break; + } + if (end == len) + return (1); + if (end == sizeof tmp || buf[end] != 't') + return (-1); + + /* Copy to the buffer. */ + memcpy(tmp, buf + 2, end - 2); + tmp[end - 2] = '\0'; + + /* Try to parse the window size sequence. */ + if (sscanf(tmp, "8;%u;%u", &sy, &sx) == 2) { + *size = end + 1; + if (!apply) + return (0); + + /* Window size in characters. */ + tty_set_size(tty, sx, sy, tty->xpixel, tty->ypixel); + return (0); + } else if (sscanf(tmp, "4;%u;%u", &ypixel, &xpixel) == 2) { + *size = end + 1; + if (!apply) + return (0); + + /* Window size in pixels. */ + char_x = (xpixel && tty->sx) ? xpixel / tty->sx : 0; + char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0; + tty_set_size(tty, tty->sx, tty->sy, char_x, char_y); + tty_invalidate(tty); + + tty->flags &= ~TTY_WINSIZEQUERY; + return (0); + } + + log_debug("%s: unrecognized window size sequence: %s", c->name, tmp); + return (-1); +} diff --git a/tty-kitty.c b/tty-kitty.c index 3a4c756ec..552c51f17 100644 --- a/tty-kitty.c +++ b/tty-kitty.c @@ -252,7 +252,7 @@ tty_kitty_function(u_int number, char final) int tty_keys_kitty_query(struct tty *tty, const char *buf, size_t len, - size_t *size) + size_t *size, int apply) { struct client *c = tty->client; size_t i; @@ -286,6 +286,9 @@ tty_keys_kitty_query(struct tty *tty, const char *buf, size_t len, return (-1); *size = i + 1; + if (!apply) + return (0); + tty->kitty_keys = flags; tty->flags |= TTY_HAVEKKB; log_debug("%s: received Kitty keyboard flags %u", c->name, flags); diff --git a/tty.c b/tty.c index fbde2ce5c..e1347fd08 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD: tty.c,v 1.482 2026/09/22 06:58:06 nicm Exp $ */ +/* $OpenBSD: tty.c,v 1.485 2026/10/07 07:34:59 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1147,7 +1147,7 @@ tty_redraw_region(struct tty *tty, const struct tty_ctx *ctx) */ if (tty_large_region(tty, ctx) || ctx->flags & TTY_CTX_PANE_OBSCURED) { log_debug("%s: %s large region redraw", __func__, c->name); - ctx->redraw_cb(ctx); + ctx->redraw_cb(ctx, ctx->orupper, ctx->orlower - ctx->orupper + 1); return; } @@ -1313,6 +1313,10 @@ tty_clamp_area(struct tty *tty, const struct tty_ctx *ctx, u_int px, u_int py, if (*rx > nx) fatalx("%s: x too big, %u > %u", __func__, *rx, nx); + /* + * yoff is in window coordinates; ctx->yoff - ctx->ryoff is the number + * of status lines above the window on the terminal. + */ if (yoff >= ctx->woy && yoff + ny <= ctx->woy + ctx->wsy) { /* All visible. */ *j = 0; @@ -1320,19 +1324,19 @@ tty_clamp_area(struct tty *tty, const struct tty_ctx *ctx, u_int px, u_int py, *ry = ny; } else if (yoff < ctx->woy && yoff + ny > ctx->woy + ctx->wsy) { /* Both top and bottom not visible. */ - *j = ctx->woy; - *y = 0; + *j = ctx->woy - yoff; + *y = ctx->yoff - ctx->ryoff; *ry = ctx->wsy; } else if (yoff < ctx->woy) { /* Top not visible. */ - *j = ctx->woy - (ctx->yoff + py); - *y = 0; + *j = ctx->woy - yoff; + *y = ctx->yoff - ctx->ryoff; *ry = ny - *j; } else { /* Bottom not visible. */ *j = 0; *y = (ctx->yoff + py) - ctx->woy; - *ry = ctx->wsy - *y; + *ry = ctx->woy + ctx->wsy - yoff; } if (*ry > ny) fatalx("%s: y too big, %u > %u", __func__, *ry, ny); @@ -1549,6 +1553,7 @@ tty_sync_start(struct tty *tty) if (tty->flags & TTY_SYNCING) return; tty->flags |= TTY_SYNCING; + tty->sync_offset = EVBUFFER_LENGTH(tty->out); if (tty_term_has(tty->term, TTYC_SYNC)) { log_debug("%s sync start", tty->client->name); @@ -1955,7 +1960,7 @@ tty_cmd_alignmenttest(struct tty *tty, const struct tty_ctx *ctx) u_int i, j; if (ctx->flags & TTY_CTX_WINDOW_BIGGER) { - ctx->redraw_cb(ctx); + ctx->redraw_cb(ctx, 0, ctx->sy); return; } @@ -2014,7 +2019,7 @@ tty_cmd_cells(struct tty *tty, const struct tty_ctx *ctx) tty->cy == tty->rlower) tty_draw_pane(tty, ctx, ctx->ocy); else - ctx->redraw_cb(ctx); + ctx->redraw_cb(ctx, ctx->ocy, 1); return; } @@ -2860,7 +2865,7 @@ tty_colours_fg(struct tty *tty, const struct grid_cell *gc) * reset because some terminals do not clear bright correctly. */ if (tty->cell.fg >= 90 && - tty->cell.bg <= 97 && + tty->cell.fg <= 97 && (gc->fg < 90 || gc->fg > 97)) tty_reset(tty); diff --git a/utf8-combined.c b/utf8-combined.c index 02abc3518..78a34074d 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8-combined.c,v 1.10 2026/06/09 11:49:36 nicm Exp $ */ +/* $OpenBSD: utf8-combined.c,v 1.11 2026/10/05 08:32:24 nicm Exp $ */ /* * Copyright (c) 2023 Nicholas Marriott @@ -121,6 +121,12 @@ utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) /* Emoji skin tone modifiers. */ switch (a) { + case 0x0261D: + case 0x026F9: + case 0x0270A: + case 0x0270B: + case 0x0270C: + case 0x0270D: case 0x1F44B: case 0x1F44C: case 0x1F44D: diff --git a/utf8.c b/utf8.c index a00c1bd6d..71918083b 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD: utf8.c,v 1.73 2026/09/20 08:11:00 nicm Exp $ */ +/* $OpenBSD: utf8.c,v 1.74 2026/10/05 08:32:24 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -51,12 +51,8 @@ static struct utf8_width_cache utf8_width_cache = RB_INITIALIZER(utf8_width_cache); static struct utf8_width_item utf8_default_width_cache[] = { - { .wc = 0x0261D, .width = 2 }, - { .wc = 0x026F9, .width = 2 }, { .wc = 0x0270A, .width = 2 }, { .wc = 0x0270B, .width = 2 }, - { .wc = 0x0270C, .width = 2 }, - { .wc = 0x0270D, .width = 2 }, { .wc = 0x1F1E6, .width = 1 }, { .wc = 0x1F1E7, .width = 1 }, { .wc = 0x1F1E8, .width = 1 }, diff --git a/window-buffer.c b/window-buffer.c index bc25de66e..7f05a5673 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */ +/* $OpenBSD: window-buffer.c,v 1.53 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -606,7 +606,8 @@ window_buffer_start_edit(struct window_buffer_modedata *data, ed->name = xstrdup(paste_buffer_name(pb)); ed->pb = pb; - ed->editor = spawn_editor(c, buf, len, window_buffer_edit_close_cb, ed); + ed->editor = spawn_editor(c, buf, len, NULL, + window_buffer_edit_close_cb, ed); if (ed->editor == NULL) window_buffer_finish_edit(ed); else { diff --git a/window-copy.c b/window-copy.c index fd42e91bd..3d40ffe24 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.431 2026/09/21 10:43:37 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.437 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -87,6 +87,10 @@ static int window_copy_last_regex(struct grid *, u_int, u_int, u_int, int); static int window_copy_search_mark_at(struct window_copy_mode_data *, u_int, u_int, u_int *); +static int window_copy_grow_searchpos(struct window_copy_mode_data *, + u_int, u_int, u_int); +static void window_copy_search_set_index(struct window_copy_mode_data *, + u_int, u_int); static char *window_copy_stringify(struct grid *, u_int, u_int, u_int, char *, u_int *); static void window_copy_cstrtocellpos(struct grid *, u_int, u_int *, @@ -117,9 +121,17 @@ static int window_copy_set_selection(struct window_mode_entry *, int, int); static int window_copy_update_selection(struct window_mode_entry *, int, int); static void window_copy_synchronize_cursor(struct window_mode_entry *, int); +static int window_copy_get_output_range(struct window_mode_entry *, int, + u_int *, u_int *, u_int *, u_int *); static void *window_copy_get_selection(struct window_mode_entry *, size_t *); +static void *window_copy_get_output(struct window_mode_entry *, size_t *, + int); +static void *window_copy_get_grid_range(struct window_mode_entry *, u_int, + u_int, u_int, u_int, size_t *); static void window_copy_copy_buffer(struct window_mode_entry *, const char *, void *, size_t, int, int); +static void window_copy_pipe_buffer(struct session *, const char *, + void *, size_t); static void window_copy_pipe(struct window_mode_entry *, struct session *, const char *); static void window_copy_copy_pipe(struct window_mode_entry *, @@ -129,6 +141,8 @@ static void window_copy_copy_selection(struct window_mode_entry *, const char *, int, int); static void window_copy_append_selection(struct window_mode_entry *); static void window_copy_clear_selection(struct window_mode_entry *); +static u_int window_copy_copy_line_length(struct window_mode_entry *, + u_int, int *); static void window_copy_copy_line(struct window_mode_entry *, char **, size_t *, u_int, u_int, u_int); static int window_copy_in_set(struct window_mode_entry *, u_int, u_int, @@ -341,6 +355,10 @@ struct window_copy_mode_data { char *searchstr; u_char *searchmark; int searchcount; + int searchindex; + u_int *searchpos; + u_int searchposcount; + u_int searchpossize; int searchmore; int searchall; int searchx; @@ -581,6 +599,8 @@ window_copy_common_init(struct window_mode_entry *wme) data->searchstr = NULL; } data->searchx = data->searchy = data->searcho = -1; + data->searchcount = -1; + data->searchindex = -1; data->searchall = 1; data->jumptype = WINDOW_COPY_OFF; @@ -682,6 +702,7 @@ window_copy_free(struct window_mode_entry *wme) evtimer_del(&data->refresh_timer); free(data->searchmark); + free(data->searchpos); free(data->searchstr); free(data->jumpchar); @@ -768,6 +789,7 @@ window_copy_scroll(struct window_pane *wp, int sl_mpos, u_int my, struct window_mode_entry *wme = TAILQ_FIRST(&wp->modes); if (wme != NULL) { + window_redraw_active_switch(wp->window, wp); window_set_active_pane(wp->window, wp, 0); window_copy_scroll1(wme, wp, sl_mpos, my, tty_oy, scroll_exit); } @@ -1143,7 +1165,9 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) format_add(ft, "selection_active", "1"); else format_add(ft, "selection_active", "0"); - if (data->endselx != data->selx || data->endsely != data->sely) + if (data->modekeys == MODEKEY_VI || + data->endselx != data->selx || + data->endsely != data->sely) format_add(ft, "selection_present", "1"); else format_add(ft, "selection_present", "0"); @@ -1167,8 +1191,12 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) format_add(ft, "search_present", "%d", data->searchmark != NULL); format_add(ft, "search_timed_out", "%d", data->timeout); if (data->searchcount != -1) { + format_add(ft, "search_count_present", "1"); format_add(ft, "search_count", "%d", data->searchcount); format_add(ft, "search_count_partial", "%d", data->searchmore); + if (data->searchindex != -1) + format_add(ft, "search_count_current", "%d", + data->searchindex); } format_add_cb(ft, "search_match", window_copy_search_match_cb); @@ -1193,9 +1221,11 @@ window_copy_size_changed(struct window_mode_entry *wme) struct screen *s = &data->screen; struct screen_write_ctx ctx; int search = (data->searchmark != NULL); + int searchindex = data->searchindex; window_copy_clear_selection(wme); window_copy_clear_marks(wme); + data->searchindex = searchindex; screen_write_start(&ctx, s); window_copy_write_lines(wme, &ctx, 0, screen_size_y(s)); @@ -1587,6 +1617,84 @@ window_copy_cmd_copy_selection_and_cancel(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_CANCEL); } +static enum window_copy_cmd_action +window_copy_cmd_select_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + struct grid_reader gr; + void *buf; + size_t len; + u_int sx, sy, ex, ey, last; + int all = args_has(cs->wargs, 'a'); + int wrapped; + + if (!window_copy_get_output_range(wme, all, &sx, &sy, &ex, &ey)) + return (WINDOW_COPY_CMD_NOTHING); + + /* An empty output selects nothing. */ + buf = window_copy_get_grid_range(wme, sx, sy, ex, ey, &len); + if (buf == NULL) + return (WINDOW_COPY_CMD_NOTHING); + free(buf); + + window_copy_clear_selection(wme); + data->lineflag = LINE_SEL_NONE; + data->rectflag = 0; + data->selflag = SEL_CHAR; + window_copy_scroll_to(wme, sx, sy, 1); + window_copy_start_selection(wme); + + /* The end is inclusive in vi mode, so step back onto the last cell. */ + if (data->modekeys == MODEKEY_VI) { + last = window_copy_copy_line_length(wme, ey, NULL); + if (ex > last) + ex = last; + if (ex == 0 && ey > 0) { + ey--; + ex = window_copy_copy_line_length(wme, ey, &wrapped); + if (wrapped && ex > 0) + ex--; + } else { + grid_reader_start(&gr, data->backing->grid, ex, ey); + grid_reader_cursor_left(&gr, 1); + grid_reader_get_cursor(&gr, &ex, &ey); + } + } + window_copy_scroll_to(wme, ex, ey, 1); + return (WINDOW_COPY_CMD_REDRAW); +} + +static enum window_copy_cmd_action +window_copy_cmd_copy_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct client *c = cs->c; + struct session *s = cs->s; + struct winlink *wl = cs->wl; + struct window_pane *wp = wme->wp; + void *buf; + char *prefix = NULL; + const char *arg0 = args_string(cs->wargs, 0); + size_t len; + int all = args_has(cs->wargs, 'a'); + int set_paste = !args_has(cs->wargs, 'P'); + int set_clip = !args_has(cs->wargs, 'C'); + + if (arg0 != NULL) + prefix = format_single(NULL, arg0, c, s, wl, wp); + if (s != NULL) { + buf = window_copy_get_output(wme, &len, all); + if (buf != NULL) { + window_copy_copy_buffer(wme, prefix, buf, len, + set_paste, set_clip); + } + } + free(prefix); + window_copy_clear_selection(wme); + return (WINDOW_COPY_CMD_REDRAW); +} + static enum window_copy_cmd_action window_copy_cmd_cursor_down(struct window_copy_cmd_state *cs) { @@ -2216,68 +2324,80 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) 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); + else + return (WINDOW_COPY_CMD_MOVE); + if (data->selflag == SEL_CHAR || 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); + /* + * Set up the reset positions normally initialized by select-word or + * select-line, including when changing the mode of an existing selection. + */ + if (data->cursordrag == CURSORDRAG_SEL) { + fx = data->endselx; + fy = data->endsely; + } else { + fx = data->selx; + fy = data->sely; } - return (WINDOW_COPY_CMD_MOVE); + + 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); + if (data->selflag == SEL_WORD) + grid_reader_cursor_previous_word(&gr, data->separators, 0, 1); + else + grid_reader_cursor_start_of_line(&gr, 1); + grid_reader_get_cursor(&gr, &sx, &sy); + grid_reader_start(&gr, data->backing->grid, ex, ey); + if (data->selflag == SEL_WORD) { + grid_reader_cursor_next_word_end(&gr, data->separators); + if (options_get_number(wme->wp->window->options, "mode-keys") == + MODEKEY_VI) + grid_reader_cursor_left(&gr, 1); + } else + 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; + if (data->selflag == SEL_LINE) + x = window_copy_cursor_limit(wme, ey, 0); + else + x = ex; + window_copy_scroll_to(wme, x, ey, 1); + } + } + if (data->selflag == SEL_WORD) + window_copy_set_selection(wme, 0, 1); + else if (data->cursordrag == CURSORDRAG_NONE) + window_copy_set_selection(wme, 0, 0); + return (WINDOW_COPY_CMD_REDRAW); } static enum window_copy_cmd_action @@ -2742,6 +2862,127 @@ window_copy_cmd_pipe_and_cancel(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_CANCEL); } +static enum window_copy_cmd_action +window_copy_cmd_copy_pipe_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct client *c = cs->c; + struct session *s = cs->s; + struct winlink *wl = cs->wl; + struct window_pane *wp = wme->wp; + void *buf; + char *command = NULL, *prefix = NULL; + const char *arg0 = args_string(cs->wargs, 0); + const char *arg1 = args_string(cs->wargs, 1); + size_t len; + int all = args_has(cs->wargs, 'a'); + int set_paste = !args_has(cs->wargs, 'P'); + int set_clip = !args_has(cs->wargs, 'C'); + + if (arg1 != NULL) + prefix = format_single(NULL, arg1, c, s, wl, wp); + if (s != NULL && arg0 != NULL && *arg0 != '\0') + command = format_single(NULL, arg0, c, s, wl, wp); + if (s != NULL) { + buf = window_copy_get_output(wme, &len, all); + if (buf != NULL) { + window_copy_pipe_buffer(s, command, buf, len); + window_copy_copy_buffer(wme, prefix, buf, len, + set_paste, set_clip); + } + } + free(command); + free(prefix); + window_copy_clear_selection(wme); + return (WINDOW_COPY_CMD_REDRAW); +} + +static enum window_copy_cmd_action +window_copy_cmd_pipe_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct client *c = cs->c; + struct session *s = cs->s; + struct winlink *wl = cs->wl; + struct window_pane *wp = wme->wp; + void *buf; + char *command = NULL; + const char *arg0 = args_string(cs->wargs, 0); + size_t len; + int all = args_has(cs->wargs, 'a'); + + if (s != NULL && arg0 != NULL && *arg0 != '\0') + command = format_single(NULL, arg0, c, s, wl, wp); + if (s != NULL) { + buf = window_copy_get_output(wme, &len, all); + if (buf != NULL) { + window_copy_pipe_buffer(s, command, buf, len); + free(buf); + } + } + free(command); + window_copy_clear_selection(wme); + return (WINDOW_COPY_CMD_REDRAW); +} + +static void +window_copy_open_done(char *buf, __unused size_t len, __unused void *arg) +{ + free(buf); +} + +static void +window_copy_open_buffer(struct window_copy_cmd_state *cs, void *buf, + size_t len) +{ + struct window_mode_entry *wme = cs->wme; + struct window_pane *wp = wme->wp; + char *editor = NULL; + const char *arg0 = args_string(cs->wargs, 0); + + if (arg0 != NULL && *arg0 != '\0') + editor = format_single(NULL, arg0, cs->c, cs->s, cs->wl, wp); + spawn_editor(cs->c, buf, len, editor, window_copy_open_done, NULL); + free(editor); +} + +static enum window_copy_cmd_action +window_copy_cmd_open_selection(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; + void *buf; + size_t len; + + if (cs->c == NULL) + return (WINDOW_COPY_CMD_NOTHING); + if (data->screen.sel == NULL && data->lineflag == LINE_SEL_NONE) + return (WINDOW_COPY_CMD_NOTHING); + buf = window_copy_get_selection(wme, &len); + if (buf != NULL) { + window_copy_open_buffer(cs, buf, len); + free(buf); + } + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_open_output(struct window_copy_cmd_state *cs) +{ + struct window_mode_entry *wme = cs->wme; + void *buf; + size_t len; + + if (cs->c == NULL) + return (WINDOW_COPY_CMD_NOTHING); + buf = window_copy_get_output(wme, &len, args_has(cs->wargs, 'a')); + if (buf != NULL) { + window_copy_open_buffer(cs, buf, len); + free(buf); + } + return (WINDOW_COPY_CMD_NOTHING); +} + static enum window_copy_cmd_action window_copy_cmd_goto_line(struct window_copy_cmd_state *cs) { @@ -2970,20 +3211,16 @@ window_copy_cmd_search_backward_incremental(struct window_copy_cmd_state *cs) data->searchregex = 0; free(data->searchstr); data->searchstr = xstrdup(arg0); - if (!window_copy_search_up(wme, 0)) { - window_copy_clear_marks(wme); + if (!window_copy_search_up(wme, 0)) return (WINDOW_COPY_CMD_REDRAW); - } break; case '+': data->searchtype = WINDOW_COPY_SEARCHDOWN; data->searchregex = 0; free(data->searchstr); data->searchstr = xstrdup(arg0); - if (!window_copy_search_down(wme, 0)) { - window_copy_clear_marks(wme); + if (!window_copy_search_down(wme, 0)) return (WINDOW_COPY_CMD_REDRAW); - } break; } return (action); @@ -3027,20 +3264,16 @@ window_copy_cmd_search_forward_incremental(struct window_copy_cmd_state *cs) data->searchregex = 0; free(data->searchstr); data->searchstr = xstrdup(arg0); - if (!window_copy_search_down(wme, 0)) { - window_copy_clear_marks(wme); + if (!window_copy_search_down(wme, 0)) return (WINDOW_COPY_CMD_REDRAW); - } break; case '-': data->searchtype = WINDOW_COPY_SEARCHUP; data->searchregex = 0; free(data->searchstr); data->searchstr = xstrdup(arg0); - if (!window_copy_search_up(wme, 0)) { - window_copy_clear_marks(wme); + if (!window_copy_search_up(wme, 0)) return (WINDOW_COPY_CMD_REDRAW); - } } return (action); } @@ -3404,6 +3637,18 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_copy_pipe_and_cancel }, + { .command = "copy-pipe-output", + .args = { "aCP", 0, 2, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_copy_pipe_output + }, + { .command = "copy-output", + .args = { "aCP", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_copy_output + }, { .command = "copy-selection-no-clear", .args = { "CP", 0, 1, NULL }, .flags = 0, @@ -3626,6 +3871,18 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_EMACS_ONLY, .f = window_copy_cmd_other_end }, + { .command = "open-output", + .args = { "a", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_open_output + }, + { .command = "open-selection", + .args = { "", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_open_selection + }, { .command = "page-down", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -3662,6 +3919,18 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_pipe_and_cancel }, + { .command = "pipe-output", + .args = { "a", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_pipe_output + }, + { .command = "pipe-selection", + .args = { "", 0, 1, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_pipe + }, { .command = "previous-matching-bracket", .args = { "", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -3839,6 +4108,12 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_search_reverse }, + { .command = "select-output", + .args = { "a", 0, 0, NULL }, + .flags = 0, + .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, + .f = window_copy_cmd_select_output + }, { .command = "select-line", .args = { "", 0, 0, NULL }, .flags = 0, @@ -4685,6 +4960,7 @@ window_copy_search(struct window_mode_entry *wme, int direction, int regex) return (0); if (data->searchall || wp->searchstr == NULL || + data->searchpos == NULL || wp->searchregex != regex) { visible_only = 0; data->searchall = 0; @@ -4762,8 +5038,8 @@ window_copy_search(struct window_mode_entry *wme, int direction, int regex) fx = data->cx; fy = screen_hsize(data->backing) - data->oy + data->cy; } - if (direction) { + window_copy_search_set_index(data, fx, fy); /* * When in Emacs mode, position the cursor just after * the mark. @@ -4797,8 +5073,12 @@ window_copy_search(struct window_mode_entry *wme, int direction, int regex) window_copy_move_left(s, &fx, &fy, 0); } } + fx = data->cx; + fy = screen_hsize(data->backing) - data->oy + data->cy; + window_copy_search_set_index(data, fx, fy); } - } + } else if (!visible_only) + window_copy_search_marks(wme, &ss, regex, 0); window_copy_redraw_screen(wme); screen_free(&ss); @@ -4841,6 +5121,51 @@ window_copy_clip_width(u_int width, u_int b, u_int sx, u_int sy) return ((b + width > sx * sy) ? (sx * sy) - b : width); } +static int +window_copy_grow_searchpos(struct window_copy_mode_data *data, u_int px, + u_int py, u_int sx) +{ + u_int pos; + + if (py > (UINT_MAX - px) / sx) { + free(data->searchpos); + data->searchpos = NULL; + data->searchposcount = 0; + data->searchpossize = 0; + return (0); + } + if (data->searchposcount == data->searchpossize) { + data->searchpossize *= 2; + data->searchpos = xreallocarray(data->searchpos, + data->searchpossize, sizeof *data->searchpos); + } + pos = py * sx + px; + data->searchpos[data->searchposcount++] = pos; + return (1); +} + +static void +window_copy_search_set_index(struct window_copy_mode_data *data, u_int px, + u_int py) +{ + u_int position, left = 0, right = data->searchposcount, middle; + u_int sx = data->backing->grid->sx; + + data->searchindex = -1; + if (py > (UINT_MAX - px) / sx) + return; + position = py * sx + px; + while (left < right) { + middle = left + ((right - left) / 2); + if (data->searchpos[middle] < position) + left = middle + 1; + else + right = middle; + } + if (left < data->searchposcount && data->searchpos[left] == position) + data->searchindex = left + 1; +} + static u_int window_copy_search_mark_match(struct window_copy_mode_data *data, u_int px, u_int py, u_int width, int regex) @@ -4881,7 +5206,8 @@ window_copy_search_marks(struct window_mode_entry *wme, struct screen *ssp, struct screen_write_ctx ctx; struct grid *gd = s->grid; struct grid_cell gc; - int found, cis, stopped = 0; + int found, cis, stopped = 0, oldindex; + int trackpos = 0; int cflags = REG_EXTENDED; u_int px, py, nfound = 0, width; u_int ssize = 1, start, end, sx = gd->sx; @@ -4926,6 +5252,14 @@ window_copy_search_marks(struct window_mode_entry *wme, struct screen *ssp, start = 0; end = gd->hsize + sy; stop = get_timer() + WINDOW_COPY_SEARCH_ALL_TIMEOUT; + oldindex = data->searchindex; + data->searchindex = -1; + free(data->searchpos); + data->searchpos = xreallocarray(NULL, 64, + sizeof *data->searchpos); + data->searchposcount = 0; + data->searchpossize = 64; + trackpos = 1; } again: @@ -4951,6 +5285,9 @@ again: break; } nfound++; + if (!visible_only && trackpos) + trackpos = window_copy_grow_searchpos(data, px, py, + sx); px += window_copy_search_mark_match(data, px, py, width, regex); } @@ -4979,6 +5316,14 @@ again: if (!visible_only) { if (stopped) { + data->searchindex = -1; + if (trackpos) { + /* Keep this non-NULL to avoid another full search. */ + data->searchpos = xreallocarray(data->searchpos, 1, + sizeof *data->searchpos); + data->searchpossize = 1; + } + data->searchposcount = 0; if (nfound > 1000) data->searchcount = 1000; else if (nfound > 100) @@ -4991,6 +5336,9 @@ again: } else { data->searchcount = nfound; data->searchmore = 0; + if (data->searchindex == -1 && oldindex > 0 && + oldindex <= data->searchcount) + data->searchindex = oldindex; } } @@ -5008,7 +5356,12 @@ window_copy_clear_marks(struct window_mode_entry *wme) struct window_copy_mode_data *data = wme->data; data->searchcount = -1; + data->searchindex = -1; data->searchmore = 0; + free(data->searchpos); + data->searchpos = NULL; + data->searchposcount = 0; + data->searchpossize = 0; free(data->searchmark); data->searchmark = NULL; @@ -5507,7 +5860,6 @@ static void window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y) { struct window_copy_mode_data *data = wme->data; - struct grid *gd = data->backing->grid; u_int new_y, start, end; new_y = data->cy; @@ -5519,15 +5871,6 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y) end = old_y; } - /* - * In word selection mode the first word on the line below the cursor - * might be selected, so add this line to the redraw area. - */ - if (data->selflag == SEL_WORD) { - /* Last grid line in data coordinates. */ - if (end < gd->sy + data->oy - 1) - end++; - } window_copy_redraw_lines(wme, start, end - start + 1); } @@ -5864,10 +6207,14 @@ window_copy_update_selection(struct window_mode_entry *wme, int may_redraw, { struct window_copy_mode_data *data = wme->data; struct screen *s = &data->screen; + int changed; if (s->sel == NULL && data->lineflag == LINE_SEL_NONE) return (0); - return (window_copy_set_selection(wme, may_redraw, no_reset)); + changed = window_copy_set_selection(wme, may_redraw, no_reset); + if (data->selflag == SEL_WORD && may_redraw) + window_copy_redraw_screen(wme); + return (changed); } /* @@ -5960,9 +6307,11 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, window_copy_redraw_lines(wme, cy, sy - cy + 1); } else { if (endsy < cy) { - window_copy_redraw_lines(wme, endsy, cy - endsy + 1); + window_copy_redraw_lines(wme, endsy, + cy - endsy + 1); } else { - window_copy_redraw_lines(wme, cy, endsy - cy + 1); + window_copy_redraw_lines(wme, cy, + endsy - cy + 1); } } } @@ -5970,6 +6319,78 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, return (1); } +/* Get the range of the current output, or the whole buffer with all. */ +static int +window_copy_get_output_range(struct window_mode_entry *wme, int all, + u_int *sx, u_int *sy, u_int *ex, u_int *ey) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + struct grid_reader gr; + u_int total, cursor_y; + int found; + + if (!all) { + cursor_y = screen_hsize(data->backing) + data->cy - data->oy; + grid_reader_start(&gr, gd, data->cx, cursor_y); + found = grid_reader_output_range(&gr, sx, sy, ex, ey); + return (found); + } + total = gd->hsize + gd->sy; + *sx = *sy = 0; + *ey = total - 1; + *ex = grid_get_line(gd, *ey)->cellused; + return (1); +} + +/* Get current output. */ +static void * +window_copy_get_output(struct window_mode_entry *wme, size_t *len, int all) +{ + void *buf; + u_int sx, sy, ex, ey; + + if (!window_copy_get_output_range(wme, all, &sx, &sy, &ex, &ey)) { + *len = 0; + return (NULL); + } + buf = window_copy_get_grid_range(wme, sx, sy, ex, ey, len); + return (buf); +} + +/* Get the text from sx,sy up to ex,ey, or NULL if there is none. */ +static void * +window_copy_get_grid_range(struct window_mode_entry *wme, u_int sx, u_int sy, + u_int ex, u_int ey, size_t *len) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + char *buf; + size_t off; + u_int i, first, last; + + buf = xmalloc(1); + off = 0; + for (i = sy; i <= ey; i++) { + first = 0; + last = gd->sx; + if (i == sy) + first = sx; + if (i == ey) + last = ex; + window_copy_copy_line(wme, &buf, &off, i, first, last); + } + if (off != 0 && buf[off - 1] == '\n') + off--; + if (off == 0) { + free(buf); + *len = 0; + return (NULL); + } + *len = off; + return (buf); +} + static void * window_copy_get_selection(struct window_mode_entry *wme, size_t *len) { @@ -5980,7 +6401,7 @@ window_copy_get_selection(struct window_mode_entry *wme, size_t *len) size_t off; u_int i, xx, yy, sx, sy, ex, ey, ey_last; u_int firstsx, lastex, restex, restsx, selx; - int keys; + int keys, wrapped; if (data->screen.sel == NULL && data->lineflag == LINE_SEL_NONE) { buf = window_copy_match_at_cursor(data); @@ -6013,7 +6434,7 @@ window_copy_get_selection(struct window_mode_entry *wme, size_t *len) } /* Trim ex to end of line. */ - ey_last = window_copy_find_length(wme, ey); + ey_last = window_copy_copy_line_length(wme, ey, &wrapped); if (ex > ey_last) ex = ey_last; @@ -6084,8 +6505,7 @@ window_copy_get_selection(struct window_mode_entry *wme, size_t *len) } /* Remove final \n (unless at end in vi mode). */ if (keys == MODEKEY_EMACS || lastex <= ey_last) { - if (~grid_get_line(data->backing->grid, ey)->flags & - GRID_LINE_WRAPPED || lastex != ey_last) + if (!wrapped || lastex != ey_last) off -= 1; } *len = off; @@ -6126,18 +6546,28 @@ window_copy_pipe_run(struct window_mode_entry *wme, struct session *s, const char *cmd, size_t *len) { void *buf; - struct job *job; buf = window_copy_get_selection(wme, len); + window_copy_pipe_buffer(s, cmd, buf, *len); + return (buf); +} + +static void +window_copy_pipe_buffer(struct session *s, const char *cmd, void *buf, + size_t len) +{ + struct job *job; + + if (buf == NULL) + return; if (cmd == NULL || *cmd == '\0') cmd = options_get_string(global_options, "copy-command"); if (cmd != NULL && *cmd != '\0') { job = job_run(cmd, 0, NULL, NULL, s, NULL, NULL, NULL, NULL, NULL, JOB_NOWAIT, -1, -1); if (job != NULL) - bufferevent_write(job_get_event(job), buf, *len); + bufferevent_write(job_get_event(job), buf, len); } - return (buf); } static void @@ -6213,6 +6643,29 @@ window_copy_append_selection(struct window_mode_entry *wme) free(bufname); } +/* Get the length for copying, preserving spaces on wrapped lines. */ +static u_int +window_copy_copy_line_length(struct window_mode_entry *wme, u_int sy, + int *wrapped) +{ + struct window_copy_mode_data *data = wme->data; + struct grid *gd = data->backing->grid; + struct grid_line *gl = grid_get_line(gd, sy); + u_int length; + int line_wrapped; + + line_wrapped = 0; + if ((gl->flags & GRID_LINE_WRAPPED) && gl->cellsize <= gd->sx) + line_wrapped = 1; + if (wrapped != NULL) + *wrapped = line_wrapped; + if (line_wrapped) + length = gl->cellsize; + else + length = window_copy_find_length(wme, sy); + return (length); +} + static void window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, u_int sy, u_int sx, u_int ex) @@ -6220,27 +6673,15 @@ window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, struct window_copy_mode_data *data = wme->data; struct grid *gd = data->backing->grid; struct grid_cell gc; - struct grid_line *gl; struct utf8_data ud; - u_int i, xx, wrapped = 0; + u_int i, xx; const char *s; + int wrapped; if (sx > ex) return; - /* - * Work out if the line was wrapped at the screen edge and all of it is - * on screen. - */ - gl = grid_get_line(gd, sy); - if (gl->flags & GRID_LINE_WRAPPED && gl->cellsize <= gd->sx) - wrapped = 1; - - /* If the line was wrapped, don't strip spaces (use the full length). */ - if (wrapped) - xx = gl->cellsize; - else - xx = window_copy_find_length(wme, sy); + xx = window_copy_copy_line_length(wme, sy, &wrapped); if (ex > xx) ex = xx; if (sx > xx) @@ -6505,7 +6946,9 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) if (data->lineflag == LINE_SEL_LEFT_RIGHT && oy == data->sely) window_copy_other_end(wme); - if (scroll_only && options_get_number(oo, "mode-keys") == MODEKEY_VI) { + /* In vi mode, keep cursor on the same line unless dragging. */ + if (scroll_only && data->cursordrag == CURSORDRAG_NONE && + options_get_number(oo, "mode-keys") == MODEKEY_VI) { if (data->cy < screen_size_y(s) - 1) window_copy_update_cursor(wme, data->cx, data->cy + 1); } @@ -6585,7 +7028,9 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) if (data->lineflag == LINE_SEL_RIGHT_LEFT && oy == data->endsely) window_copy_other_end(wme); - if (scroll_only && options_get_number(oo, "mode-keys") == MODEKEY_VI) { + /* In vi mode, keep cursor on the same line unless dragging. */ + if (scroll_only && data->cursordrag == CURSORDRAG_NONE && + options_get_number(oo, "mode-keys") == MODEKEY_VI) { if (data->cy > 0) window_copy_update_cursor(wme, data->cx, data->cy - 1); } @@ -7249,13 +7694,16 @@ static void window_copy_acquire_cursor_up(struct window_mode_entry *wme, u_int hsize, u_int oy, u_int oldy, u_int px, u_int py) { + struct window_copy_mode_data *data = wme->data; u_int cy, yy, ny, nd; yy = hsize - oy; if (py < yy) { ny = yy - py; cy = 0; - nd = 1; + nd = oldy + ny + 1; + if (nd > screen_size_y(&data->screen)) + nd = screen_size_y(&data->screen); } else { ny = 0; cy = py - yy; @@ -7281,8 +7729,8 @@ window_copy_acquire_cursor_down(struct window_mode_entry *wme, u_int hsize, yy = sy - 1; if (cy > yy) { ny = cy - yy; - oldy = yy; - nd = 1; + oldy = oldy > ny ? oldy - ny : 0; + nd = sy - oldy; } else { ny = 0; nd = cy - oldy + 1; diff --git a/window-customize.c b/window-customize.c index 1bf311ff2..b8ec4f140 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.36 2026/07/28 10:35:31 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.37 2026/10/08 07:50:05 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -2011,13 +2011,15 @@ window_customize_edit_close_cb(char *buf, size_t len, void *arg) switch (ed->edit_type) { case WINDOW_CUSTOMIZE_EDIT_OPTION: if (window_customize_option_editable(data, item) && - window_customize_set_option_value(item, value, &cause) != 0) { + window_customize_set_option_value(item, value, + &cause) != 0) { free(cause); goto out; } break; case WINDOW_CUSTOMIZE_EDIT_KEY_COMMAND: - if (window_customize_set_command_value(item, value, &cause) != 0) { + if (window_customize_set_command_value(item, value, + &cause) != 0) { free(cause); goto out; } @@ -2104,8 +2106,8 @@ window_customize_start_edit(struct window_customize_modedata *data, buf = "\n"; len = 1; } - ed->editor = spawn_editor(c, buf, len, window_customize_edit_close_cb, - ed); + ed->editor = spawn_editor(c, buf, len, NULL, + window_customize_edit_close_cb, ed); free(value); if (ed->editor == NULL) window_customize_finish_edit(ed); diff --git a/window-switch.c b/window-switch.c index 1ff1007bb..24b3851ac 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-switch.c,v 1.4 2026/09/01 21:03:55 nicm Exp $ */ +/* $OpenBSD: window-switch.c,v 1.5 2026/10/07 13:13:49 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -88,6 +88,8 @@ struct window_switch_modedata { char *command; enum window_switch_type type; + enum sort_order order; + int reversed; char *filter; struct prompt *prompt; u_int prompt_cx; @@ -191,8 +193,9 @@ window_switch_build(struct window_switch_modedata *data) struct winlink **wl; struct sort_criteria sort_crit; - sort_crit.order = SORT_NAME; - sort_crit.reversed = 0; + sort_crit.order = data->order; + sort_crit.reversed = data->reversed; + sort_crit.order_seq = NULL; for (i = 0; i < data->item_size; i++) window_switch_free_item(data->item_list[i]); @@ -346,6 +349,18 @@ window_switch_init(struct window_mode_entry *wme, wme->data = data = xcalloc(1, sizeof *data); data->wp = wp; + data->order = SORT_NAME; + data->reversed = 0; + if (args != NULL) { + if (args_has(args, 'O')) { + data->order = sort_order_from_string(args_get(args, 'O')); + if (data->order == SORT_END) + data->order = SORT_NAME; + } + if (args_has(args, 'r')) + data->reversed = 1; + } + if (args_has(args, 'w')) data->type = WINDOW_SWITCH_TYPE_WINDOW; else diff --git a/window-visible.c b/window-visible.c index a49bfa2ef..a1ee90307 100644 --- a/window-visible.c +++ b/window-visible.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-visible.c,v 1.5 2026/07/19 17:25:38 nicm Exp $ */ +/* $OpenBSD: window-visible.c,v 1.6 2026/10/02 15:20:41 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -25,7 +25,7 @@ /* * Check if a single character is within a visible range (not obscured by a - * floating pane). + * menu or floating pane). */ int window_position_is_visible(struct visible_ranges *r, u_int px) @@ -43,9 +43,45 @@ window_position_is_visible(struct visible_ranges *r, u_int px) return (0); } +/* Remove width cells starting at px from the visible ranges. */ +static void +window_visible_ranges_remove(struct visible_ranges *r, int px, u_int width) +{ + struct visible_range *ri; + u_int i; + int start, end, right = px + width; + + if (width == 0) + return; + + for (i = 0; i < r->used; i++) { + ri = &r->ranges[i]; + start = ri->px; + end = start + ri->nx; + if (ri->nx == 0 || right <= start || px >= end) + continue; + if (px <= start) { + /* Remove the left part or the whole range. */ + ri->px = right < end ? right : end; + ri->nx = end - ri->px; + } else { + /* Keep the left part, and split off any right part. */ + ri->nx = px - start; + if (right < end) { + server_client_ensure_ranges(r, r->used + 1); + memmove(&r->ranges[i + 2], &r->ranges[i + 1], + (r->used - i - 1) * sizeof *r->ranges); + r->ranges[i + 1].px = right; + r->ranges[i + 1].nx = end - right; + r->used++; + } + } + } +} + /* * Construct ranges array for the line at starting at px,py of width cells of - * base_wp that are unobsructed. All ranges are in window coordinates. + * base_wp that are unobstructed. All ranges are in window coordinates. */ struct visible_ranges * window_visible_ranges(struct window_pane *base_wp, int px, int py, u_int width, @@ -53,11 +89,10 @@ window_visible_ranges(struct window_pane *base_wp, int px, int py, u_int width, { struct window_pane *wp; struct window *w; - struct visible_range *ri; + struct menu_data *md; static struct visible_ranges sr = { NULL, 0, 0 }; int found_self, sb_w, sb_pos; - int lb, rb, tb, bb, sx, ex, no_border; - u_int i, s; + int lb, rb, tb, bb, no_border; if (py < 0 || width == 0) goto empty; @@ -95,6 +130,13 @@ window_visible_ranges(struct window_pane *base_wp, int px, int py, u_int width, r->used = 1; } + /* The menu is above every pane in the window. */ + md = w->menu; + if (md != NULL && + (u_int)py >= menu_y(md) && + (u_int)py - menu_y(md) < menu_height(md)) { + window_visible_ranges_remove(r, menu_x(md), menu_width(md)); + } found_self = 0; TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) { @@ -129,89 +171,36 @@ window_visible_ranges(struct window_pane *base_wp, int px, int py, u_int width, else sb_w = sb_pos = 0; - for (i = 0; i < r->used; i++) { - ri = &r->ranges[i]; - if (ri->nx == 0) - continue; - if (no_border) { - lb = wp->xoff; - rb = wp->xoff + (int)wp->sx - 1; - } else if (sb_pos == PANE_SCROLLBARS_LEFT) { - if (wp->xoff > sb_w) - lb = wp->xoff - 1 - sb_w; - else - lb = 0; - } else { /* PANE_SCROLLBARS_RIGHT or none. */ - if (wp->xoff > 0) - lb = wp->xoff - 1; - else - lb = 0; - } - if (!no_border) { - if (sb_pos == PANE_SCROLLBARS_LEFT) - rb = wp->xoff + (int)wp->sx; - else /* PANE_SCROLLBARS_RIGHT or none. */ - rb = wp->xoff + (int)wp->sx + sb_w; - } - if (lb < 0) + if (no_border) { + lb = wp->xoff; + rb = wp->xoff + (int)wp->sx - 1; + } else if (sb_pos == PANE_SCROLLBARS_LEFT) { + if (wp->xoff > sb_w) + lb = wp->xoff - 1 - sb_w; + else + lb = 0; + } else { /* PANE_SCROLLBARS_RIGHT or none. */ + if (wp->xoff > 0) + lb = wp->xoff - 1; + else lb = 0; - if (rb < 0) - continue; - if (no_border && rb >= (int)w->sx) - rb = w->sx - 1; - else if (!no_border && rb > (int)w->sx) - rb = w->sx - 1; - if (lb > rb) - continue; - - sx = ri->px; - ex = sx + ri->nx - 1; - if (lb > sx && lb <= ex && rb > ex) { - /* - * If the left edge of floating pane falls - * inside this range and right edge covers up - * to right of range, then shrink left edge of - * range. - */ - ri->nx = lb - sx; - } else if (rb >= sx && rb <= ex && lb <= sx) { - /* - * Else if the right edge of floating pane falls - * inside of this range and left edge covers - * the left of range, then move px forward to - * right edge of pane. - */ - ri->nx = ex - rb; - ri->px = rb + 1; - } else if (lb > sx && rb <= ex) { - /* - * Else if pane fully inside range then split - * into 2 ranges. - */ - server_client_ensure_ranges(r, r->used + 1); - for (s = r->used; s > i; s--) { - memcpy(&r->ranges[s], &r->ranges[s - 1], - sizeof *r->ranges); - } - ri = &r->ranges[i]; - r->ranges[i + 1].px = rb + 1; - r->ranges[i + 1].nx = ex - rb; - /* ri->px was copied, unchanged. */ - ri->nx = lb - sx; - r->used++; - } else if (lb <= sx && rb > ex) { - /* - * If floating pane completely covers this range - * then delete it (make it 0 length). - */ - ri->nx = 0; - } else { - /* - * The range is already obscured, do - * nothing. - */ - } } + if (!no_border) { + if (sb_pos == PANE_SCROLLBARS_LEFT) + rb = wp->xoff + (int)wp->sx; + else /* PANE_SCROLLBARS_RIGHT or none. */ + rb = wp->xoff + (int)wp->sx + sb_w; + } + if (lb < 0) + lb = 0; + if (rb < 0) + continue; + if (no_border && rb >= (int)w->sx) + rb = w->sx - 1; + else if (!no_border && rb > (int)w->sx) + rb = w->sx - 1; + if (lb <= rb) + window_visible_ranges_remove(r, lb, rb - lb + 1); } return (r); diff --git a/window.c b/window.c index 515c2ffc6..ddc49a2a5 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.383 2026/09/28 10:42:01 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.385 2026/10/08 07:06:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -423,6 +423,7 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel) TAILQ_INIT(&w->panes); TAILQ_INIT(&w->z_index); TAILQ_INIT(&w->last_panes); + TAILQ_INIT(&w->damage); w->active = NULL; w->lastlayout = -1; @@ -476,6 +477,7 @@ window_destroy(struct window *w) menu_destroy(w); window_destroy_panes(w); + redraw_free_damage(w); if (event_initialized(&w->name_event)) evtimer_del(&w->name_event); @@ -754,6 +756,7 @@ int window_set_active_pane(struct window *w, struct window_pane *wp, int notify) { struct window_pane *lastwp; + int unzoomed; log_debug("%s: pane %%%u", __func__, wp->id); @@ -761,7 +764,8 @@ 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) && !window_pane_is_visible(wp)) + unzoomed = (w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp); + if (unzoomed) window_unzoom(w, 1); lastwp = w->active; @@ -778,7 +782,19 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) } tty_update_window_offset(w); - server_redraw_window(w); + + /* + * Unzooming changes every pane's geometry and needs a full window + * redraw. Otherwise, only the previous and new active pane's border + * and status appearance changed, so avoid redrawing unaffected pane + * content. + */ + if (unzoomed) + server_redraw_window(w); + else { + server_redraw_window_borders(w); + server_status_window(w); + } if (notify) window_fire_pane_changed(w, w->active, lastwp); @@ -1563,6 +1579,46 @@ window_pane_scrollbar_redraw_visibility(struct window_pane *wp) server_redraw_window(wp->window); } +#ifdef HAVE_UTEMPTER +void +window_pane_utmp_add(struct window_pane *wp) +{ + char *cp; + + if (wp->fd == -1 || + (wp->flags & (PANE_EMPTY|PANE_EXITED|PANE_UTMP)) || + !options_get_number(global_options, "utmp")) + return; + xasprintf(&cp, "tmux(%lu).%%%u", (long)getpid(), wp->id); + utempter_add_record(wp->fd, cp); + kill(getpid(), SIGCHLD); + wp->flags |= PANE_UTMP; + free(cp); +} +#else +void +window_pane_utmp_add(__unused struct window_pane *wp) +{ +} +#endif + +#ifdef HAVE_UTEMPTER +void +window_pane_utmp_remove(struct window_pane *wp) +{ + if (~wp->flags & PANE_UTMP) + return; + utempter_remove_record(wp->fd); + kill(getpid(), SIGCHLD); + wp->flags &= ~PANE_UTMP; +} +#else +void +window_pane_utmp_remove(__unused struct window_pane *wp) +{ +} +#endif + static void window_pane_destroy(struct window_pane *wp) { @@ -1576,13 +1632,12 @@ window_pane_destroy(struct window_pane *wp) window_pane_free_modes(wp); screen_write_sync_clear_dirty(wp); - if (wp->fd != -1) { -#ifdef HAVE_UTEMPTER - utempter_remove_record(wp->fd); - kill(getpid(), SIGCHLD); -#endif + if (wp->event != NULL) { bufferevent_free(wp->event); wp->event = NULL; + } + if (wp->fd != -1) { + window_pane_utmp_remove(wp); close(wp->fd); wp->fd = -1; } @@ -2971,3 +3026,40 @@ window_pane_is_floating_with_hidden(struct window_pane *wp) return (0); return (1); } + +/* Report damage for a floating pane, including its border and scrollbar. */ +static void +window_damage_floating_pane(struct window_pane *wp, int xoff, int yoff, + int sx, int sy) +{ + struct window *w = wp->window; + int x0, x1, y0, y1, sb_left = 0, sb_right = 0; + struct style *sb_sy = &wp->scrollbar_style; + + if (window_pane_scrollbar_reserve(wp)) { + if (w->sb_pos == PANE_SCROLLBARS_LEFT) + sb_left = sb_sy->width + sb_sy->pad; + else + sb_right = sb_sy->width + sb_sy->pad; + } + x0 = xoff - 1 - sb_left; + x1 = xoff + sx + sb_right; + y0 = yoff - 1; + y1 = yoff + sy; + if (x0 < 0) + x0 = 0; + if (y0 < 0) + y0 = 0; + if (x1 >= x0 && y1 >= y0) + redraw_damage_window(w, x0, y0, x1 - x0 + 1U, y1 - y0 + 1U); +} + +/* Report damage for a floating pane's old and new areas. */ +void +window_redraw_floating_pane(struct window_pane *wp, int oxoff, int oyoff, + int osx, int osy) +{ + window_damage_floating_pane(wp, oxoff, oyoff, osx, osy); + window_damage_floating_pane(wp, wp->xoff, wp->yoff, wp->sx, wp->sy); + server_status_window(wp->window); +}