diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 883ee720e..8f6633624 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -25,12 +25,15 @@ jobs: - name: ubuntu-24.04-x64 runner: ubuntu-24.04 make: make - # - name: ubuntu-24.04-arm64 - # runner: ubuntu-24.04-arm - # make: make - # - name: macos-26-arm64 - # runner: macos-26 - # make: gmake + configure: --enable-utf8proc --enable-asan + #- name: ubuntu-24.04-arm64 + # runner: ubuntu-24.04-arm + # make: make + # configure: --enable-utf8proc --enable-asan + - name: macos-26-arm64 + runner: macos-26 + make: gmake + configure: --enable-utf8proc --enable-asan steps: - name: checkout @@ -65,11 +68,19 @@ jobs: - name: build run: | - sh autogen.sh - ./configure --enable-utf8proc + sh autogen.sh + ./configure ${{ matrix.configure }} ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" - name: test run: | cd regress ${{ matrix.make }} + + - name: logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: regress-logs-${{ matrix.name }} + path: regress/logs/*.log + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 46e3ad595..31ca7add0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ configure core etc/ fuzz/*-fuzzer +regress/logs/ tags tmux tmux.1.* diff --git a/Makefile.am b/Makefile.am index ebb681700..2887925bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -13,7 +13,8 @@ AM_CPPFLAGS += @XOPEN_DEFINES@ \ -DTMUX_VERSION='"@VERSION@"' \ -DTMUX_CONF='"$(sysconfdir)/tmux.conf:~/.tmux.conf:$$XDG_CONFIG_HOME/tmux/tmux.conf:~/.config/tmux/tmux.conf"' \ -DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \ - -DTMUX_TERM='"@DEFAULT_TERM@"' + -DTMUX_TERM='"@DEFAULT_TERM@"' \ + -DTMUX_MOUSE=1 # Additional object files. LDADD = $(LIBOBJS) @@ -42,7 +43,8 @@ AM_CPPFLAGS += -DDEBUG endif AM_CPPFLAGS += -iquote. if IS_ASAN -AM_CFLAGS += -fsanitize=address +AM_CPPFLAGS += -DASAN +AM_CFLAGS += -fsanitize=address -fno-omit-frame-pointer AM_LDFLAGS += -fsanitize=address endif endif @@ -160,13 +162,16 @@ dist_tmux_SOURCES = \ control-notify.c \ control.c \ environ.c \ + events-payload.c \ + events.c \ file.c \ - format.c \ format-draw.c \ + format.c \ fuzzy.c \ grid-reader.c \ grid-view.c \ grid.c \ + hooks.c \ hyperlinks.c \ input-keys.c \ input.c \ @@ -179,15 +184,15 @@ dist_tmux_SOURCES = \ log.c \ menu.c \ mode-tree.c \ + monitor.c \ names.c \ - notify.c \ options-table.c \ options.c \ paste.c \ popup.c \ proc.c \ - prompt.c \ prompt-history.c \ + prompt.c \ regsub.c \ resize.c \ screen-redraw.c \ @@ -202,9 +207,9 @@ dist_tmux_SOURCES = \ spawn.c \ status.c \ style.c \ + tmux-protocol.h \ tmux.c \ tmux.h \ - tmux-protocol.h \ tty-acs.c \ tty-draw.c \ tty-features.c \ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..56f11f8ae --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +Please report security issues to nicholas.marriott@gmail.com. diff --git a/alerts.c b/alerts.c index 81c41fa6a..ef4ae4061 100644 --- a/alerts.c +++ b/alerts.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: alerts.c,v 1.35 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -205,7 +205,7 @@ alerts_check_bell(struct window *w) } if (!alerts_action_applies(wl, "bell-action")) continue; - notify_winlink("alert-bell", wl); + events_fire_winlink("alert-bell", wl); if (s->flags & SESSION_ALERTED) continue; @@ -241,7 +241,7 @@ alerts_check_activity(struct window *w) } if (!alerts_action_applies(wl, "activity-action")) continue; - notify_winlink("alert-activity", wl); + events_fire_winlink("alert-activity", wl); if (s->flags & SESSION_ALERTED) continue; @@ -277,7 +277,7 @@ alerts_check_silence(struct window *w) } if (!alerts_action_applies(wl, "silence-action")) continue; - notify_winlink("alert-silence", wl); + events_fire_winlink("alert-silence", wl); if (s->flags & SESSION_ALERTED) continue; diff --git a/arguments.c b/arguments.c index 72e16d511..a69aca8c1 100644 --- a/arguments.c +++ b/arguments.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/attributes.c b/attributes.c index b0c9ef518..d853b8db0 100644 --- a/attributes.c +++ b/attributes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: attributes.c,v 1.13 2026/07/02 08:51:05 nicm Exp $ */ /* * Copyright (c) 2009 Joshua Elsasser diff --git a/cfg.c b/cfg.c index 68d2d406e..ad9cdb0f0 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cfg.c,v 1.89 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/client.c b/client.c index b0f0b55e6..ab14e1ade 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -257,9 +257,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, pid_t ppid; enum msgtype msg; struct termios tio, saved_tio; - size_t size, linesize = 0; - ssize_t linelen; - char *line = NULL, **caps = NULL, *cause; + size_t size; + char **caps = NULL, *cause; u_int ncaps = 0; /* Set up the initial command. */ @@ -410,11 +409,6 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, client_exec(client_execshell, client_execcmd); } - /* Restore streams to blocking. */ - setblocking(STDIN_FILENO, 1); - setblocking(STDOUT_FILENO, 1); - setblocking(STDERR_FILENO, 1); - /* Print the exit message, if any, and exit. */ if (client_attached) { if (client_exitreason != CLIENT_EXIT_NONE) @@ -429,15 +423,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, else printf("%%exit\n"); fflush(stdout); - if (client_flags & CLIENT_CONTROL_WAITEXIT) { - setvbuf(stdin, NULL, _IOLBF, 0); - for (;;) { - linelen = getline(&line, &linesize, stdin); - if (linelen <= 1) - break; - } - free(line); - } + if (client_flags & CLIENT_CONTROL_WAITEXIT) + control_wait_exit(STDIN_FILENO); if (client_flags & CLIENT_CONTROLCONTROL) { printf("\033\\"); fflush(stdout); @@ -445,6 +432,12 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, } } else if (client_exitreason != CLIENT_EXIT_NONE) fprintf(stderr, "%s\n", client_exit_message()); + + /* Restore the streams to blocking. */ + setblocking(STDIN_FILENO, 1); + setblocking(STDOUT_FILENO, 1); + setblocking(STDERR_FILENO, 1); + return (client_exitval); } diff --git a/cmd-attach-session.c b/cmd-attach-session.c index 32b1e0cbd..050c7ca38 100644 --- a/cmd-attach-session.c +++ b/cmd-attach-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-attach-session.c,v 1.91 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -163,7 +163,7 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag, if (~c->flags & CLIENT_CONTROL) proc_send(c->peer, MSG_READY, -1, NULL, 0); - notify_client("client-attached", c); + events_fire_client("client-attached", c); c->flags |= CLIENT_ATTACHED; } diff --git a/cmd-bind-key.c b/cmd-bind-key.c index a846488a4..3ad498111 100644 --- a/cmd-bind-key.c +++ b/cmd-bind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-bind-key.c,v 1.47 2025/04/09 07:03:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 7eb371c25..8d85c57b2 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-break-pane.c,v 1.72 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -51,10 +51,9 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, struct window *w, struct window_pane *wp) { struct layout_cell *lc = wp->layout_cell; - u_int sx = lc->saved_sx, sy = lc->saved_sy; - int ox = lc->saved_xoff, oy = lc->saved_yoff; char *cause = NULL; enum pane_lines lines = window_get_pane_lines(w); + struct layout_geometry *fg = &lc->fg; if (window_pane_is_floating(wp)) { cmdq_error(item, "pane is already floating"); @@ -65,14 +64,13 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, return (CMD_RETURN_ERROR); } - if (layout_floating_args_parse(item, args, lines, w, &sx, &sy, &ox, &oy, - &cause) != 0) { + if (layout_floating_args_parse(item, args, lines, w, fg, &cause) != 0) { cmdq_error(item, "failed to float pane: %s", cause); free(cause); return (CMD_RETURN_ERROR); } layout_remove_tile(w, lc); - layout_set_size(lc, sx, sy, ox, oy); + layout_set_size(lc, fg->sx, fg->sy, fg->xoff, fg->yoff); lc->flags |= LAYOUT_CELL_FLOATING; TAILQ_REMOVE(&w->z_index, wp, zentry); @@ -82,7 +80,7 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, window_set_active_pane(w, wp, 1); layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -101,7 +99,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) struct session *dst_s = target->s; struct window_pane *wp = source->wp; struct window *w = wl->window; - char *newname, *cause, *cp; + char *cause, *cp; int idx = target->idx, before; const char *template, *name = args_get(args, 'n'); @@ -153,6 +151,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) layout_close_pane(wp); w = wp->window = window_create(w->sx, w->sy, w->xpixel, w->ypixel); + window_add_ref(w, __func__); options_set_parent(wp->options, w->options); wp->flags |= (PANE_STYLECHANGED|PANE_THEMECHANGED); TAILQ_INSERT_HEAD(&w->panes, wp, entry); @@ -160,12 +159,11 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->active = wp; w->latest = tc; - if (name == NULL) { - newname = default_window_name(w); - window_set_name(w, newname, 0); - free(newname); - } else { - window_set_name(w, name, 0); + free(w->name); + if (name == NULL) + w->name = default_window_name(w); + else { + w->name = clean_name(name, 0); options_set_number(w->options, "automatic-rename", 0); } @@ -176,6 +174,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) if (idx == -1) idx = -1 - options_get_number(dst_s->options, "base-index"); wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ + window_remove_ref(w, __func__); if (!args_has(args, 'd')) { session_select(dst_s, wl->idx); cmd_find_from_session(current, dst_s, 0); diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index c3729cc45..a17ae6243 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-capture-pane.c,v 1.67 2026/07/02 10:34:14 nicm Exp $ */ /* * Copyright (c) 2009 Jonathan Alvarado diff --git a/cmd-choose-tree.c b/cmd-choose-tree.c index 4a6bd152b..f9ce7d3b7 100644 --- a/cmd-choose-tree.c +++ b/cmd-choose-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-choose-tree.c,v 1.57 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2012 Thomas Adam diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index 6012b6f80..ffed25ef2 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-confirm-before.c b/cmd-confirm-before.c index 8a247c012..109abf83d 100644 --- a/cmd-confirm-before.c +++ b/cmd-confirm-before.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-confirm-before.c,v 1.61 2026/06/24 10:55:39 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index fce6d3fbd..051cb58a2 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-copy-mode.c,v 1.53 2026/07/08 08:07:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -30,8 +30,8 @@ const struct cmd_entry cmd_copy_mode_entry = { .name = "copy-mode", .alias = NULL, - .args = { "deHMqSs:t:u", 0, 0, NULL }, - .usage = "[-deHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE, + .args = { "dekHMqSs:t:u", 0, 0, NULL }, + .usage = "[-dekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE, .source = { 's', CMD_FIND_PANE, 0 }, .target = { 't', CMD_FIND_PANE, 0 }, diff --git a/cmd-detach-client.c b/cmd-detach-client.c index b5a4cf1c1..82ce9a00b 100644 --- a/cmd-detach-client.c +++ b/cmd-detach-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-detach-client.c,v 1.39 2026/05/22 15:22:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-display-menu.c b/cmd-display-menu.c index e1766fc2e..ba1bcf4ae 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-menu.c,v 1.51 2026/07/07 09:45:09 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -126,6 +126,10 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, format_add(ft, "popup_mouse_y", "%u", event->m.y); } + /* Position of the previous menu, for -x/-y L. */ + format_add(ft, "popup_last_x", "%u", tc->menu_last_px); + format_add(ft, "popup_last_y", "%u", tc->menu_last_py + h); + /* * If there are any status lines, add this window position and the * status line position. @@ -238,6 +242,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, xp = "#{popup_pane_left}"; else if (strcmp(xp, "M") == 0) xp = "#{popup_mouse_centre_x}"; + else if (strcmp(xp, "L") == 0) + xp = "#{popup_last_x}"; else if (strcmp(xp, "W") == 0) xp = "#{popup_window_status_line_x}"; p = format_expand(ft, xp); @@ -258,6 +264,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item, yp = "#{popup_pane_bottom}"; else if (strcmp(yp, "M") == 0) yp = "#{popup_mouse_top}"; + else if (strcmp(yp, "L") == 0) + yp = "#{popup_last_y}"; else if (strcmp(yp, "S") == 0) yp = "#{popup_status_line_y}"; else if (strcmp(yp, "W") == 0) diff --git a/cmd-display-message.c b/cmd-display-message.c index 3138d9223..f786006c3 100644 --- a/cmd-display-message.c +++ b/cmd-display-message.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-message.c,v 1.65 2026/02/23 08:46:57 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-display-panes.c b/cmd-display-panes.c index baa855333..01ccc101f 100644 --- a/cmd-display-panes.c +++ b/cmd-display-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-panes.c,v 1.56 2026/06/25 16:32:42 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-find-window.c b/cmd-find-window.c index 560998370..28a8949cd 100644 --- a/cmd-find-window.c +++ b/cmd-find-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-find-window.c,v 1.56 2023/12/27 20:42:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-find.c b/cmd-find.c index 555e5f1f0..f4c8245c3 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-find.c,v 1.86 2026/07/02 08:47:25 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott diff --git a/cmd-if-shell.c b/cmd-if-shell.c index 9aae9745a..f3ce9ba57 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 672a3c84d..bbee45b96 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-join-pane.c,v 1.69 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -71,8 +71,9 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, struct window *w = wl->window; struct layout_cell *lc = wp->layout_cell; struct window_pane *owp; - int wx = w->sx, wy = w->sy, px = lc->sx; - int py = lc->sy, xoff = lc->xoff, yoff = lc->yoff; + int wx = w->sx, wy = w->sy; + int px = lc->g.sx, py = lc->g.sy; + int xoff = lc->g.xoff, yoff = lc->g.yoff; int border = 1; if (window_pane_get_pane_lines(wp) == PANE_LINES_NONE) @@ -180,12 +181,12 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, return (CMD_RETURN_ERROR); } - if (xoff != lc->xoff || yoff != lc->yoff) { - lc->xoff = xoff; - lc->yoff = yoff; + if (xoff != lc->g.xoff || yoff != lc->g.yoff) { + lc->g.xoff = xoff; + lc->g.yoff = yoff; layout_fix_panes(w, NULL); } - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -200,7 +201,7 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args, const char *errstr, *argval; const char flags[] = { 'U', 'D', 'L', 'R' }; char *cause = NULL, flag; - int xoff = lc->xoff, yoff = lc->yoff, adjust; + int xoff = lc->g.xoff, yoff = lc->g.yoff, adjust; u_int i; enum pane_lines lines = window_pane_get_pane_lines(wp); @@ -251,11 +252,11 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args, xoff += adjust; } - if (xoff != lc->xoff || yoff != lc->yoff) { - lc->xoff = xoff; - lc->yoff = yoff; + if (xoff != lc->g.xoff || yoff != lc->g.yoff) { + lc->g.xoff = xoff; + lc->g.yoff = yoff; layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -319,8 +320,8 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m) ly = m->statusat - 1; if (x != lx || y != ly) { - lc->xoff += x - lx; - lc->yoff += y - ly; + lc->g.xoff += x - lx; + lc->g.yoff += y - ly; layout_fix_panes(w, NULL); server_redraw_window(w); server_redraw_window_borders(w); @@ -357,7 +358,46 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl, else TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); + server_redraw_window(w); + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w, + struct window_pane *wp) +{ + struct layout_cell *lc = wp->layout_cell; + + if (!window_pane_is_floating(wp)) { + cmdq_error(item, "pane is not floating"); + return (CMD_RETURN_ERROR); + } + if (w->flags & WINDOW_ZOOMED) { + cmdq_error(item, "can't tile a pane while window is zoomed"); + return (CMD_RETURN_ERROR); + } + + lc->fg.sx = lc->g.sx; + lc->fg.sy = lc->g.sy; + lc->fg.xoff = lc->g.xoff; + lc->fg.yoff = lc->g.yoff; + + if (layout_insert_tile(w, lc) != 0) { + cmdq_error(item, "no space for a new pane"); + return (CMD_RETURN_ERROR); + } + lc->flags &= ~LAYOUT_CELL_FLOATING; + + TAILQ_REMOVE(&w->z_index, wp, zentry); + TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); + + if (!args_has(args, 'd')) + window_set_active_pane(w, wp, 1); + layout_fix_offsets(w); + layout_fix_panes(w, NULL); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -412,6 +452,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) server_unzoom_window(src_w); if (src_wp == dst_wp) { + if (window_pane_is_floating(src_wp)) + return (cmd_join_pane_tile(item, args, src_w, src_wp)); cmdq_error(item, "source and target panes must be different"); return (CMD_RETURN_ERROR); } @@ -459,8 +501,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) if (window_count_panes(src_w, 1) == 0) server_kill_window(src_w, 1); else - notify_window("window-layout-changed", src_w); - notify_window("window-layout-changed", dst_w); + events_fire_window("window-layout-changed", src_w); + events_fire_window("window-layout-changed", dst_w); return (CMD_RETURN_NORMAL); } diff --git a/cmd-kill-pane.c b/cmd-kill-pane.c index 836551f01..f634a7cd7 100644 --- a/cmd-kill-pane.c +++ b/cmd-kill-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-pane.c,v 1.34 2026/06/09 21:22:22 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-kill-server.c b/cmd-kill-server.c index 7bb79e3d4..8b70be3d8 100644 --- a/cmd-kill-server.c +++ b/cmd-kill-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-server.c,v 1.20 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-kill-session.c b/cmd-kill-session.c index 4bf36d785..7edd2a30a 100644 --- a/cmd-kill-session.c +++ b/cmd-kill-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-session.c,v 1.31 2026/06/09 12:57:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-kill-window.c b/cmd-kill-window.c index 0253605f1..e7a6ec32e 100644 --- a/cmd-kill-window.c +++ b/cmd-kill-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-window.c,v 1.30 2026/06/09 12:57:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-buffers.c b/cmd-list-buffers.c index 347c69341..82ee58947 100644 --- a/cmd-list-buffers.c +++ b/cmd-list-buffers.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-buffers.c,v 1.41 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-clients.c b/cmd-list-clients.c index ae235d83c..77f3912ca 100644 --- a/cmd-list-clients.c +++ b/cmd-list-clients.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-clients.c,v 1.42 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-commands.c b/cmd-list-commands.c index 0b0f912d8..5e9c4430c 100644 --- a/cmd-list-commands.c +++ b/cmd-list-commands.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-commands.c,v 1.15 2026/02/24 08:22:13 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-keys.c b/cmd-list-keys.c index 2222ea241..79d5499fd 100644 --- a/cmd-list-keys.c +++ b/cmd-list-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-keys.c,v 1.78 2026/07/01 13:12:17 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-panes.c b/cmd-list-panes.c index 094ca850c..54c8db7d3 100644 --- a/cmd-list-panes.c +++ b/cmd-list-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-panes.c,v 1.40 2026/06/01 14:01:09 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-list-sessions.c b/cmd-list-sessions.c index 79a1ab620..07a622979 100644 --- a/cmd-list-sessions.c +++ b/cmd-list-sessions.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-sessions.c,v 1.36 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-windows.c b/cmd-list-windows.c index 927976398..40f8c2db5 100644 --- a/cmd-list-windows.c +++ b/cmd-list-windows.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-windows.c,v 1.50 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-load-buffer.c b/cmd-load-buffer.c index 82ac2fd47..df35b82c1 100644 --- a/cmd-load-buffer.c +++ b/cmd-load-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-load-buffer.c,v 1.66 2025/10/28 07:32:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-lock-server.c b/cmd-lock-server.c index bd61dcffd..1feac03f8 100644 --- a/cmd-lock-server.c +++ b/cmd-lock-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-lock-server.c,v 1.31 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-move-window.c b/cmd-move-window.c index 4b90e70f8..1cd4f10a8 100644 --- a/cmd-move-window.c +++ b/cmd-move-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-move-window.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-new-session.c b/cmd-new-session.c index ba02c72d4..56d4f9779 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-new-session.c,v 1.152 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -325,7 +325,7 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) session_group_synchronize_to(s); session_select(s, RB_MIN(winlinks, &s->windows)->idx); } - notify_session("session-created", s); + events_fire_session("session-created", s); /* * Set the client to the new session. If a command client exists, it is diff --git a/cmd-new-window.c b/cmd-new-window.c index bb6113fd6..d1e2ab4e5 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-new-window.c,v 1.103 2026/07/08 08:07:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -38,8 +38,8 @@ const struct cmd_entry cmd_new_window_entry = { .name = "new-window", .alias = "neww", - .args = { "abc:de:F:kn:PSt:", 0, -1, NULL }, - .usage = "[-abdkPS] [-c start-directory] [-e environment] [-F format] " + .args = { "abc:de:EF:kn:PSt:", 0, -1, NULL }, + .usage = "[-abdEkPS] [-c start-directory] [-e environment] [-F format] " "[-n window-name] " CMD_TARGET_WINDOW_USAGE " [shell-command [argument ...]]", @@ -60,12 +60,19 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) struct client *tc = cmdq_get_target_client(item); struct session *s = target->s; struct winlink *wl = target->wl, *new_wl = NULL; - int idx = target->idx, before; + int idx = target->idx, before, count = args_count(args); char *cause = NULL, *cp, *expanded, *wname = NULL; const char *template, *name; struct cmd_find_state fs; struct args_value *av; + if (args_has(args, 'E') && + count != 0 && + (count != 1 || *args_string(args, 0) != '\0')) { + cmdq_error(item, "command cannot be given for empty pane"); + return (CMD_RETURN_ERROR); + } + /* * If -S and -n are given and -t is not and a single window with this * name already exists, select it. @@ -134,6 +141,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = 0; + if (args_has(args, 'E') || (count == 1 && *args_string(args, 0) == '\0')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'd')) sc.flags |= SPAWN_DETACHED; if (args_has(args, 'k')) diff --git a/cmd-parse.y b/cmd-parse.y index a03da825f..37955ed2d 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/cmd-paste-buffer.c b/cmd-paste-buffer.c index 7233b7d27..feed94403 100644 --- a/cmd-paste-buffer.c +++ b/cmd-paste-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-paste-buffer.c,v 1.44 2026/03/04 07:19:32 tb Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-pipe-pane.c b/cmd-pipe-pane.c index 844b8b853..d96d6c7d2 100644 --- a/cmd-pipe-pane.c +++ b/cmd-pipe-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-pipe-pane.c,v 1.63 2026/04/28 08:47:55 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-queue.c b/cmd-queue.c index 9eee5473a..411563101 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-queue.c,v 1.120 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -350,74 +350,53 @@ cmdq_insert_after(struct cmdq_item *after, struct cmdq_item *item) /* Insert a hook. */ void -cmdq_insert_hook(struct session *s, struct cmdq_item *item, +cmdq_insert_hook(__unused struct session *s, struct cmdq_item *item, struct cmd_find_state *current, const char *fmt, ...) { - struct cmdq_state *state = item->state; struct cmd *cmd = item->cmd; struct args *args = cmd_get_args(cmd); struct args_entry *ae; struct args_value *av; - struct options *oo; + struct event_payload *ep; va_list ap; char *name, tmp[32], flag, *arguments; u_int i; const char *value; - struct cmdq_item *new_item; - struct cmdq_state *new_state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_parse_tree *tree; if (item->state->flags & CMDQ_STATE_NOHOOKS) return; - if (s == NULL) - oo = global_s_options; - else - oo = s->options; va_start(ap, fmt); xvasprintf(&name, fmt, ap); va_end(ap); - o = options_get(oo, name); - if (o == NULL) { - free(name); - return; - } - log_debug("running hook %s (parent %p)", name, item); - - /* - * The hooks get a new state because they should not update the current - * target or formats for any subsequent commands. - */ - new_state = cmdq_new_state(current, &state->event, CMDQ_STATE_NOHOOKS); - cmdq_add_format(new_state, "hook", "%s", name); + ep = event_payload_create(); + if (current != NULL) + event_payload_set_target(ep, current); + event_payload_set_pointer(ep, "_cmdq_item", item, NULL, NULL); arguments = args_print(args); - cmdq_add_format(new_state, "hook_arguments", "%s", arguments); + event_payload_set_string(ep, "arguments", "%s", arguments); free(arguments); for (i = 0; i < args_count(args); i++) { - xsnprintf(tmp, sizeof tmp, "hook_argument_%d", i); - cmdq_add_format(new_state, tmp, "%s", args_string(args, i)); + xsnprintf(tmp, sizeof tmp, "argument_%u", i); + event_payload_set_string(ep, tmp, "%s", args_string(args, i)); } flag = args_first(args, &ae); while (flag != 0) { value = args_get(args, flag); - if (value == NULL) { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); - cmdq_add_format(new_state, tmp, "1"); - } else { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); - cmdq_add_format(new_state, tmp, "%s", value); - } + xsnprintf(tmp, sizeof tmp, "flag_%c", flag); + if (value == NULL) + event_payload_set_string(ep, tmp, "1"); + else + event_payload_set_string(ep, tmp, "%s", value); i = 0; av = args_first_value(args, flag); while (av != NULL) { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c_%d", flag, i); - cmdq_add_format(new_state, tmp, "%s", av->string); + xsnprintf(tmp, sizeof tmp, "flag_%c_%u", flag, i); + event_payload_set_string(ep, tmp, "%s", av->string); i++; av = args_next_value(av); } @@ -425,20 +404,7 @@ cmdq_insert_hook(struct session *s, struct cmdq_item *item, flag = args_next(&ae); } - a = options_array_first(o); - while (a != NULL) { - tree = options_array_item_value(a)->cmd; - if (tree != NULL) { - new_item = cmd_invoke_get(tree, new_state, NULL); - if (item != NULL) - item = cmdq_insert_after(item, new_item); - else - item = cmdq_append(NULL, new_item); - } - a = options_array_next(a); - } - - cmdq_free_state(new_state); + events_fire(name, ep); free(name); } diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 13fcbb1dd..da2a27976 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-refresh-client.c,v 1.54 2026/07/05 08:24:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -46,36 +46,17 @@ const struct cmd_entry cmd_refresh_client_entry = { static void cmd_refresh_client_update_subscription(struct client *tc, const char *value) { - char *copy, *split, *name, *what; - enum control_sub_type subtype; - int subid = -1; + char *name, *format; + enum monitor_type type; + int id; - copy = name = xstrdup(value); - if ((split = strchr(copy, ':')) == NULL) { - control_remove_sub(tc, copy); - goto out; + if (monitor_parse(value, &name, &type, &id, &format) != 0) { + control_remove_sub(tc, value); + return; } - *split++ = '\0'; - - what = split; - if ((split = strchr(what, ':')) == NULL) - goto out; - *split++ = '\0'; - - if (strcmp(what, "%*") == 0) - subtype = CONTROL_SUB_ALL_PANES; - else if (sscanf(what, "%%%d", &subid) == 1 && subid >= 0) - subtype = CONTROL_SUB_PANE; - else if (strcmp(what, "@*") == 0) - subtype = CONTROL_SUB_ALL_WINDOWS; - else if (sscanf(what, "@%d", &subid) == 1 && subid >= 0) - subtype = CONTROL_SUB_WINDOW; - else - subtype = CONTROL_SUB_SESSION; - control_add_sub(tc, name, subtype, subid, split); - -out: - free(copy); + control_add_sub(tc, name, type, id, format); + free(name); + free(format); } static enum cmd_retval diff --git a/cmd-rename-session.c b/cmd-rename-session.c index b399483b8..35719c564 100644 --- a/cmd-rename-session.c +++ b/cmd-rename-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rename-session.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,8 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) struct args *args = cmd_get_args(self); struct cmd_find_state *target = cmdq_get_target(item); struct session *s = target->s; + struct event_payload *ep; + struct cmd_find_state fs; char *newname, *tmp; tmp = format_single_from_target(item, args_string(args, 0)); @@ -69,13 +71,20 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } + ep = event_payload_create(); + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", s); + event_payload_set_string(ep, "old_name", "%s", s->name); + event_payload_set_string(ep, "new_name", "%s", newname); + RB_REMOVE(sessions, &sessions, s); free(s->name); s->name = newname; RB_INSERT(sessions, &sessions, s); server_status_session(s); - notify_session("session-renamed", s); + events_fire("session-renamed", ep); return (CMD_RETURN_NORMAL); } diff --git a/cmd-rename-window.c b/cmd-rename-window.c index 8ab9b658a..f55f8a18f 100644 --- a/cmd-rename-window.c +++ b/cmd-rename-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rename-window.c,v 1.30 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 348923528..01ddf424a 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -182,7 +182,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) if (lc->parent != NULL) layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -270,10 +270,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, if ((lx == left || lx == left + 1) && ly == wp->yoff - 1) { /* Top left corner. */ - new_sx = lc->sx + (lx - x); + new_sx = lc->g.sx + (lx - x); if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = lc->sy + (ly - y); + new_sy = lc->g.sy + (ly - y); if (new_sy < PANE_MINIMUM) new_sy = PANE_MINIMUM; new_xoff = x + 1; /* because mouse is on border at xoff - 1 */ @@ -283,65 +283,65 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c, } else if ((lx == right + 1 || lx == right) && ly == wp->yoff - 1) { /* Top right corner. */ - new_sx = x - lc->xoff; + new_sx = x - lc->g.xoff; if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = lc->sy + (ly - y); + new_sy = lc->g.sy + (ly - y); if (new_sy < PANE_MINIMUM) new_sy = PANE_MINIMUM; new_yoff = y + 1; - layout_set_size(lc, new_sx, new_sy, lc->xoff, new_yoff); + layout_set_size(lc, new_sx, new_sy, lc->g.xoff, new_yoff); resizes++; } else if ((lx == left || lx == left + 1) && ly == wp->yoff + sy) { /* Bottom left corner. */ - new_sx = lc->sx + (lx - x); + new_sx = lc->g.sx + (lx - x); if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = y - lc->yoff; + new_sy = y - lc->g.yoff; if (new_sy < PANE_MINIMUM) return; new_xoff = x + 1; - layout_set_size(lc, new_sx, new_sy, new_xoff, lc->yoff); + layout_set_size(lc, new_sx, new_sy, new_xoff, lc->g.yoff); resizes++; } else if ((lx == right + 1 || lx == right) && ly == wp->yoff + sy) { /* Bottom right corner. */ - new_sx = x - lc->xoff; + new_sx = x - lc->g.xoff; if (new_sx < PANE_MINIMUM) new_sx = PANE_MINIMUM; - new_sy = y - lc->yoff; + new_sy = y - lc->g.yoff; if (new_sy < PANE_MINIMUM) new_sy = PANE_MINIMUM; - layout_set_size(lc, new_sx, new_sy, lc->xoff, lc->yoff); + layout_set_size(lc, new_sx, new_sy, lc->g.xoff, lc->g.yoff); resizes++; } else if (lx == right) { /* Right border. */ - new_sx = x - lc->xoff; + new_sx = x - lc->g.xoff; if (new_sx < PANE_MINIMUM) return; - layout_set_size(lc, new_sx, lc->sy, lc->xoff, lc->yoff); + layout_set_size(lc, new_sx, lc->g.sy, lc->g.xoff, lc->g.yoff); resizes++; } else if (lx == left) { /* Left border. */ - new_sx = lc->sx + (lx - x); + new_sx = lc->g.sx + (lx - x); if (new_sx < PANE_MINIMUM) return; new_xoff = x + 1; - layout_set_size(lc, new_sx, lc->sy, new_xoff, lc->yoff); + layout_set_size(lc, new_sx, lc->g.sy, new_xoff, lc->g.yoff); resizes++; } else if (ly == wp->yoff + sy) { /* Bottom border. */ - new_sy = y - lc->yoff; + new_sy = y - lc->g.yoff; if (new_sy < PANE_MINIMUM) return; - layout_set_size(lc, lc->sx, new_sy, lc->xoff, lc->yoff); + layout_set_size(lc, lc->g.sx, new_sy, lc->g.xoff, lc->g.yoff); resizes++; } else if (ly == wp->yoff - 1) { /* Top border (move instead of resize). */ - new_xoff = lc->xoff + (x - lx); + new_xoff = lc->g.xoff + (x - lx); new_yoff = y + 1; - layout_set_size(lc, lc->sx, lc->sy, new_xoff, new_yoff); + layout_set_size(lc, lc->g.sx, lc->g.sy, new_xoff, new_yoff); resizes++; } if (resizes != 0) { diff --git a/cmd-resize-window.c b/cmd-resize-window.c index c420451cf..a89750607 100644 --- a/cmd-resize-window.c +++ b/cmd-resize-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-resize-window.c,v 1.10 2023/06/30 13:19:32 nicm Exp $ */ /* * Copyright (c) 2018 Nicholas Marriott diff --git a/cmd-respawn-pane.c b/cmd-respawn-pane.c index 1e1dd4c6c..66a5a0be4 100644 --- a/cmd-respawn-pane.c +++ b/cmd-respawn-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-respawn-pane.c,v 1.40 2026/07/03 16:09:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -34,8 +34,8 @@ const struct cmd_entry cmd_respawn_pane_entry = { .name = "respawn-pane", .alias = "respawnp", - .args = { "c:e:kt:", 0, -1, NULL }, - .usage = "[-k] [-c start-directory] [-e environment] " + .args = { "c:e:Ekt:", 0, -1, NULL }, + .usage = "[-Ek] [-c start-directory] [-e environment] " CMD_TARGET_PANE_USAGE " [shell-command [argument ...]]", .target = { 't', CMD_FIND_PANE, 0 }, @@ -75,6 +75,8 @@ cmd_respawn_pane_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = SPAWN_RESPAWN; + if (args_has(args, 'E')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'k')) sc.flags |= SPAWN_KILL; diff --git a/cmd-respawn-window.c b/cmd-respawn-window.c index e1eae0afc..e10469649 100644 --- a/cmd-respawn-window.c +++ b/cmd-respawn-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-respawn-window.c,v 1.51 2026/07/03 16:09:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -34,8 +34,8 @@ const struct cmd_entry cmd_respawn_window_entry = { .name = "respawn-window", .alias = "respawnw", - .args = { "c:e:kt:", 0, -1, NULL }, - .usage = "[-k] [-c start-directory] [-e environment] " + .args = { "c:e:Ekt:", 0, -1, NULL }, + .usage = "[-Ek] [-c start-directory] [-e environment] " CMD_TARGET_WINDOW_USAGE " [shell-command [argument ...]]", .target = { 't', CMD_FIND_WINDOW, 0 }, @@ -74,6 +74,8 @@ cmd_respawn_window_exec(struct cmd *self, struct cmdq_item *item) sc.cwd = args_get(args, 'c'); sc.flags = SPAWN_RESPAWN; + if (args_has(args, 'E')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'k')) sc.flags |= SPAWN_KILL; diff --git a/cmd-rotate-window.c b/cmd-rotate-window.c index c7459a9ae..d62fd0fa4 100644 --- a/cmd-rotate-window.c +++ b/cmd-rotate-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rotate-window.c,v 1.34 2026/06/22 08:47:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 16c98ae9a..4d62aa69a 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-run-shell.c,v 1.91 2026/06/01 08:27:37 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-save-buffer.c b/cmd-save-buffer.c index a6d57979b..6dd844cdd 100644 --- a/cmd-save-buffer.c +++ b/cmd-save-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-save-buffer.c,v 1.57 2025/10/28 07:32:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-select-layout.c b/cmd-select-layout.c index 417797472..741dc63f4 100644 --- a/cmd-select-layout.c +++ b/cmd-select-layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-layout.c,v 1.43 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -139,7 +139,7 @@ changed: free(oldlayout); recalculate_sizes(); server_redraw_window(w); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (CMD_RETURN_NORMAL); error: diff --git a/cmd-select-pane.c b/cmd-select-pane.c index d146ea6cf..91a36cddb 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-pane.c,v 1.76 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -80,6 +80,69 @@ cmd_select_pane_redraw(struct window *w) } } +static enum cmd_retval +cmd_select_pane_marked_pane(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct event_payload *ep; + struct cmd_find_state fs; + struct winlink *wl = target->wl; + struct window_pane *wp = target->wp, *lwp = NULL, *mwp; + struct session *s = target->s; + + if (args_has(args, 'm') && !window_pane_is_visible(wp)) + return (CMD_RETURN_NORMAL); + if (server_check_marked()) + lwp = marked_pane.wp; + + if (args_has(args, 'M') || server_is_marked(s, wl, wp)) + server_clear_marked(); + else + server_set_marked(s, wl, wp); + mwp = marked_pane.wp; + + ep = event_payload_create(); + if (mwp != NULL) + cmd_find_from_pane(&fs, mwp, 0); + else if (lwp != NULL) + cmd_find_from_pane(&fs, lwp, 0); + else + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (mwp != NULL) { + event_payload_set_pane(ep, "pane", mwp); + event_payload_set_pane(ep, "new_pane", mwp); + event_payload_set_window(ep, "window", mwp->window); + } else if (lwp != NULL) { + event_payload_set_pane(ep, "pane", lwp); + event_payload_set_window(ep, "window", lwp->window); + } else { + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + } + if (lwp != NULL) + event_payload_set_pane(ep, "old_pane", lwp); + event_payload_set_int(ep, "marked", mwp != NULL); + events_fire("marked-pane-changed", ep); + + if (lwp != NULL) { + lwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|PANE_THEMECHANGED); + server_redraw_window_borders(lwp->window); + server_status_window(lwp->window); + } + if (mwp != NULL) { + mwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|PANE_THEMECHANGED); + server_redraw_window_borders(mwp->window); + server_status_window(mwp->window); + } + if (window_pane_is_floating(wp)) { + window_redraw_active_switch(wp->window, wp); + window_set_active_pane(wp->window, wp, 1); + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -88,10 +151,12 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state *current = cmdq_get_current(item); struct cmd_find_state *target = cmdq_get_target(item); struct client *c = cmdq_get_client(item); + struct event_payload *ep; + struct cmd_find_state fs; struct winlink *wl = target->wl; struct window *w = wl->window; struct session *s = target->s; - struct window_pane *wp = target->wp, *activewp, *lastwp, *markedwp; + struct window_pane *wp = target->wp, *activewp, *lastwp; struct options *oo = wp->options; char *title; const char *style; @@ -134,38 +199,8 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } - if (args_has(args, 'm') || args_has(args, 'M')) { - if (args_has(args, 'm') && !window_pane_is_visible(wp)) - return (CMD_RETURN_NORMAL); - if (server_check_marked()) - lastwp = marked_pane.wp; - else - lastwp = NULL; - - if (args_has(args, 'M') || server_is_marked(s, wl, wp)) - server_clear_marked(); - else - server_set_marked(s, wl, wp); - markedwp = marked_pane.wp; - - if (lastwp != NULL) { - lastwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED| - PANE_THEMECHANGED); - server_redraw_window_borders(lastwp->window); - server_status_window(lastwp->window); - } - if (markedwp != NULL) { - markedwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED| - PANE_THEMECHANGED); - server_redraw_window_borders(markedwp->window); - server_status_window(markedwp->window); - } - if (window_pane_is_floating(wp)) { - window_redraw_active_switch(w, wp); - window_set_active_pane(w, wp, 1); - } - return (CMD_RETURN_NORMAL); - } + if (args_has(args, 'm') || args_has(args, 'M')) + return (cmd_select_pane_marked_pane(self, item)); style = args_get(args, 'P'); if (style != NULL) { @@ -218,7 +253,13 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); if (screen_set_title(&wp->base, title, 0)) { - notify_pane("pane-title-changed", wp); + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); server_redraw_window_borders(wp->window); server_status_window(wp->window); } diff --git a/cmd-select-window.c b/cmd-select-window.c index 4aca3e60e..32b4821bf 100644 --- a/cmd-select-window.c +++ b/cmd-select-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-window.c,v 1.30 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-send-keys.c b/cmd-send-keys.c index 6be3f2ead..06043bddb 100644 --- a/cmd-send-keys.c +++ b/cmd-send-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-send-keys.c,v 1.81 2026/06/11 19:13:34 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-server-access.c b/cmd-server-access.c index 895e69417..e85a278a8 100644 --- a/cmd-server-access.c +++ b/cmd-server-access.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-server-access.c,v 1.6 2026/06/09 12:58:40 nicm Exp $ */ /* * Copyright (c) 2021 Dallas Lyons diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index eef2978e2..6e640fec6 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-set-environment.c b/cmd-set-environment.c index 78c32452b..9a6552cb5 100644 --- a/cmd-set-environment.c +++ b/cmd-set-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-set-option.c b/cmd-set-option.c index fbe32cfae..335a54760 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-option.c,v 1.146 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,10 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *, u_int, char **); static enum cmd_retval cmd_set_option_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_set_hook_event_exec(struct cmd *, + struct cmdq_item *); +static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, + struct args *, int); const struct cmd_entry cmd_set_option_entry = { .name = "set-option", @@ -62,8 +66,9 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpRt:uw", 1, 2, cmd_set_option_args_parse }, - .usage = "[-agpRuw] " CMD_TARGET_PANE_USAGE " hook [command]", + .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERTuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -72,14 +77,152 @@ const struct cmd_entry cmd_set_hook_entry = { }; static enum args_parse_type -cmd_set_option_args_parse(__unused struct args *args, u_int idx, +cmd_set_option_args_parse(struct args *args, u_int idx, __unused char **cause) { + if (args_has(args, 'B')) + return (ARGS_PARSE_COMMANDS_OR_STRING); if (idx == 1) return (ARGS_PARSE_COMMANDS_OR_STRING); return (ARGS_PARSE_STRING); } +static enum cmd_retval +cmd_set_hook_event_exec(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct event_payload *ep; + struct client *c; + char *argument; + + if (args_count(args) == 0) { + cmdq_error(item, "missing argument"); + return (CMD_RETURN_ERROR); + } + if (args_count(args) != 1) { + cmdq_error(item, "too many arguments"); + return (CMD_RETURN_ERROR); + } + + argument = format_single_from_target(item, args_string(args, 0)); + if (*argument != '@') { + cmdq_error(item, "event name must start with @"); + free(argument); + return (CMD_RETURN_ERROR); + } + + ep = event_payload_create(); + event_payload_set_target(ep, target); + c = cmdq_get_client(item); + if (c != NULL) + event_payload_set_client(ep, "client", c); + if (target->s != NULL) + event_payload_set_session(ep, "session", target->s); + if (target->w != NULL) + event_payload_set_window(ep, "window", target->w); + if (target->wl != NULL) + event_payload_set_int(ep, "window_index", target->wl->idx); + else if (target->idx != -1) + event_payload_set_int(ep, "window_index", target->idx); + if (target->wp != NULL) + event_payload_set_pane(ep, "pane", target->wp); + events_fire(argument, ep); + free(argument); + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) +{ + struct cmd_find_state *target = cmdq_get_target(item), fs; + struct options *oo; + struct options_entry *o; + struct session *s = NULL; + char *cause = NULL, *name = NULL, *format = NULL; + char *expanded = NULL, *newvalue = NULL; + const char *value, *old; + enum monitor_type type; + int id, scope, flags = 0; + + if (args_count(args) > 1) { + cmdq_error(item, "too many arguments"); + return (CMD_RETURN_ERROR); + } + + value = args_get(args, 'B'); + if (args_has(args, 'u')) { + if (monitor_parse(value, &name, &type, &id, &format) != 0) + name = xstrdup(value); + free(format); + format = NULL; + } else { + if (monitor_parse(value, &name, &type, &id, &format) != 0) { + cmdq_error(item, "invalid subscription: %s", value); + return (CMD_RETURN_ERROR); + } + } + + if (*name != '@') { + cmdq_error(item, "monitor hook name must start with @"); + goto fail; + } + + scope = options_scope_from_name(args, window, name, target, &oo, + &cause); + if (scope == OPTIONS_TABLE_NONE) { + cmdq_error(item, "%s", cause); + free(cause); + goto fail; + } + cmd_find_copy_state(&fs, target); + + if (args_has(args, 'u')) { + hooks_monitor_remove(oo, name); + goto out; + } + + if (args_count(args) != 0) { + value = args_string(args, 0); + if (args_has(args, 'F')) { + expanded = format_single_from_target(item, value); + value = expanded; + } + o = options_get_only(oo, name); + if (!args_has(args, 'o') || o == NULL) { + if (args_has(args, 'a') && o != NULL) { + old = options_get_string(oo, name); + xasprintf(&newvalue, "%s%s", old, value); + value = newvalue; + } + options_set_string(oo, name, 0, "%s", value); + options_push_changes(name); + } + } + + if (oo != global_options && + oo != global_s_options && + oo != global_w_options) + s = target->s; + if (args_has(args, 'T')) + flags |= MONITOR_NOTIFY_TRUE; + hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); + +out: + free(newvalue); + free(expanded); + free(name); + free(format); + return (CMD_RETURN_NORMAL); + +fail: + free(newvalue); + free(expanded); + free(name); + free(format); + return (CMD_RETURN_ERROR); +} + static enum cmd_retval cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) { @@ -89,26 +232,34 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) struct window_pane *loop; struct options *oo; struct options_entry *parent, *o, *po; - char *name, *argument, *expanded = NULL; - char *cause; + char *name, *argument, *cause; + char *expanded = NULL, *array_key = NULL; const char *value; - int window, idx, already, error, ambiguous; + int window, already, error, ambiguous; int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); + if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'E')) + return (cmd_set_hook_event_exec(self, item)); + if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B')) + return (cmd_set_hook_monitor_exec(item, args, window)); + if (args_count(args) == 0) { + cmdq_error(item, "missing argument"); + return (CMD_RETURN_ERROR); + } /* Expand argument. */ argument = format_single_from_target(item, args_string(args, 0)); /* If set-hook -R, fire the hook straight away. */ if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'R')) { - notify_hook(item, argument); + hooks_run(item, argument); free(argument); return (CMD_RETURN_NORMAL); } - /* Parse option name and index. */ - name = options_match(argument, &idx, &ambiguous); + /* Parse option name and array key. */ + name = options_match(argument, &array_key, &ambiguous); if (name == NULL) { if (args_has(args, 'q')) goto out; @@ -140,21 +291,23 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) o = options_get_only(oo, name); parent = options_get(oo, name); - /* Check that array options and indexes match up. */ - if (idx != -1 && (*name == '@' || !options_is_array(parent))) { + /* Check that array options and keys match up. */ + if (array_key != NULL && (*name == '@' || !options_is_array(parent))) { cmdq_error(item, "not an array: %s", argument); goto fail; } /* With -o, check this option is not already set. */ if (!args_has(args, 'u') && args_has(args, 'o')) { - if (idx == -1) + if (array_key == NULL) already = (o != NULL); else { if (o == NULL) already = 0; + else if (options_array_get(o, array_key) != NULL) + already = 1; else - already = (options_array_get(o, idx) != NULL); + already = 0; } if (already) { if (args_has(args, 'q')) @@ -170,7 +323,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) po = options_get_only(loop->options, name); if (po == NULL) continue; - if (options_remove_or_default(po, idx, &cause) != 0) { + if (options_remove_or_default(po, array_key, + &cause) != 0) { cmdq_error(item, "%s", cause); free(cause); goto fail; @@ -180,7 +334,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'u') || args_has(args, 'U')) { if (o == NULL) goto out; - if (options_remove_or_default(o, idx, &cause) != 0) { + if (options_remove_or_default(o, array_key, &cause) != 0) { cmdq_error(item, "%s", cause); free(cause); goto fail; @@ -191,7 +345,9 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) goto fail; } options_set_string(oo, name, append, "%s", value); - } else if (idx == -1 && !options_is_array(parent)) { + if (cmd_get_entry(self) == &cmd_set_hook_entry) + hooks_add_event(name); + } else if (array_key == NULL && !options_is_array(parent)) { error = options_from_string(oo, options_table_entry(parent), options_table_entry(parent)->name, value, args_has(args, 'a'), &cause); @@ -207,7 +363,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) } if (o == NULL) o = options_empty(oo, options_table_entry(parent)); - if (idx == -1) { + if (array_key == NULL) { if (!append) options_array_clear(o); if (options_array_assign(o, value, &cause) != 0) { @@ -215,7 +371,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) free(cause); goto fail; } - } else if (options_array_set(o, idx, value, append, + } else if (options_array_set(o, array_key, value, append, &cause) != 0) { cmdq_error(item, "%s", cause); free(cause); @@ -229,11 +385,13 @@ out: free(argument); free(expanded); free(name); + free(array_key); return (CMD_RETURN_NORMAL); fail: free(argument); free(expanded); free(name); + free(array_key); return (CMD_RETURN_ERROR); } diff --git a/cmd-show-environment.c b/cmd-show-environment.c index 3440c33e4..692f09c32 100644 --- a/cmd-show-environment.c +++ b/cmd-show-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-show-messages.c b/cmd-show-messages.c index aa02c203d..bdb433714 100644 --- a/cmd-show-messages.c +++ b/cmd-show-messages.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-messages.c,v 1.37 2025/11/18 08:37:54 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-show-options.c b/cmd-show-options.c index 56f8178f9..94017a6be 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-options.c,v 1.73 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -30,7 +30,11 @@ static enum cmd_retval cmd_show_options_exec(struct cmd *, struct cmdq_item *); static void cmd_show_options_print(struct cmd *, struct cmdq_item *, - struct options_entry *, int, int); + struct options_entry *, const char *, int); +static void cmd_show_hooks_print_monitor(struct cmdq_item *, + struct options_entry *); +static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *, + int, struct options *); static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *, int, struct options *); @@ -64,8 +68,8 @@ const struct cmd_entry cmd_show_hooks_entry = { .name = "show-hooks", .alias = NULL, - .args = { "gpt:w", 0, 1, NULL }, - .usage = "[-gpw] " CMD_TARGET_PANE_USAGE " [hook]", + .args = { "Bgpt:w", 0, 1, NULL }, + .usage = "[-Bgpw] " CMD_TARGET_PANE_USAGE " [hook]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -80,7 +84,8 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state *target = cmdq_get_target(item); struct options *oo; char *argument, *name = NULL, *cause; - int window, idx, ambiguous, parent, scope; + char *array_key = NULL; + int window, ambiguous, parent, scope; struct options_entry *o; window = (cmd_get_entry(self) == &cmd_show_window_options_entry); @@ -95,11 +100,14 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) free(cause); return (CMD_RETURN_ERROR); } + if (cmd_get_entry(self) == &cmd_show_hooks_entry && + args_has(args, 'B')) + return (cmd_show_hooks_monitor(self, item, scope, oo)); return (cmd_show_options_all(self, item, scope, oo)); } argument = format_single_from_target(item, args_string(args, 0)); - name = options_match(argument, &idx, &ambiguous); + name = options_match(argument, &array_key, &ambiguous); if (name == NULL) { if (args_has(args, 'q')) goto out; @@ -124,8 +132,13 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) parent = 1; } else parent = 0; - if (o != NULL) - cmd_show_options_print(self, item, o, idx, parent); + if (o != NULL) { + if (cmd_get_entry(self) == &cmd_show_hooks_entry && + args_has(args, 'B')) + cmd_show_hooks_print_monitor(item, o); + else + cmd_show_options_print(self, item, o, array_key, parent); + } else if (*name == '@') { if (args_has(args, 'q')) goto out; @@ -135,26 +148,28 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item) out: free(name); + free(array_key); free(argument); return (CMD_RETURN_NORMAL); fail: free(name); + free(array_key); free(argument); return (CMD_RETURN_ERROR); } static void cmd_show_options_print(struct cmd *self, struct cmdq_item *item, - struct options_entry *o, int idx, int parent) + struct options_entry *o, const char *array_key, int parent) { struct args *args = cmd_get_args(self); struct options_array_item *a; const char *name = options_name(o); char *value, *tmp = NULL, *escaped; - if (idx != -1) { - xasprintf(&tmp, "%s[%d]", name, idx); + if (array_key != NULL) { + xasprintf(&tmp, "%s[%s]", name, array_key); name = tmp; } else { if (options_is_array(o)) { @@ -165,8 +180,8 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, return; } while (a != NULL) { - idx = options_array_item_index(a); - cmd_show_options_print(self, item, o, idx, + array_key = options_array_item_key(a); + cmd_show_options_print(self, item, o, array_key, parent); a = options_array_next(a); } @@ -174,7 +189,7 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, } } - value = options_to_string(o, idx, 0); + value = options_to_string(o, array_key, 0); if (args_has(args, 'v')) cmdq_print(item, "%s", value); else if (options_is_string(o)) { @@ -195,6 +210,33 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item, free(tmp); } +static void +cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o) +{ + char *value; + + value = hooks_monitor_to_string(o); + if (value == NULL) + return; + cmdq_print(item, "%s", value); + free(value); +} + +/* Show all hook monitors. */ +static enum cmd_retval +cmd_show_hooks_monitor(__unused struct cmd *self, struct cmdq_item *item, + __unused int scope, struct options *oo) +{ + struct options_entry *o; + + o = options_first(oo); + while (o != NULL) { + cmd_show_hooks_print_monitor(item, o); + o = options_next(o); + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, struct options *oo) @@ -203,15 +245,14 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, const struct options_table_entry *oe; struct options_entry *o; struct options_array_item *a; - const char *name; - u_int idx; + const char *name, *array_key; int parent; if (cmd_get_entry(self) != &cmd_show_hooks_entry) { o = options_first(oo); while (o != NULL) { if (options_table_entry(o) == NULL) - cmd_show_options_print(self, item, o, -1, 0); + cmd_show_options_print(self, item, o, NULL, 0); o = options_next(o); } } @@ -238,7 +279,7 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, parent = 0; if (!options_is_array(o)) - cmd_show_options_print(self, item, o, -1, parent); + cmd_show_options_print(self, item, o, NULL, parent); else if ((a = options_array_first(o)) == NULL) { if (!args_has(args, 'v')) { name = options_name(o); @@ -249,8 +290,8 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope, } } else { while (a != NULL) { - idx = options_array_item_index(a); - cmd_show_options_print(self, item, o, idx, + array_key = options_array_item_key(a); + cmd_show_options_print(self, item, o, array_key, parent); a = options_array_next(a); } diff --git a/cmd-show-prompt-history.c b/cmd-show-prompt-history.c index 741d22588..5b6af76fb 100644 --- a/cmd-show-prompt-history.c +++ b/cmd-show-prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-prompt-history.c,v 1.5 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2021 Anindya Mukherjee diff --git a/cmd-source-file.c b/cmd-source-file.c index cc1c04069..5f86eb207 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha diff --git a/cmd-split-window.c b/cmd-split-window.c index 604618b80..6c657fab5 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-split-window.c,v 1.141 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -81,8 +81,9 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct session *s = target->s; struct winlink *wl = target->wl; struct window *w = wl->window; - struct window_pane *wp = target->wp, *new_wp; + struct window_pane *wp = target->wp, *new_wp = NULL; struct layout_cell *lc = NULL; + struct event_payload *ep; struct cmd_find_state fs; int input, empty, is_floating, flags = 0; const char *template, *style, *value; @@ -104,7 +105,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) flags |= SPAWN_FULLSIZE; input = args_has(args, 'I'); - if (input) + if (input || (count == 1 && *args_string(args, 0) == '\0')) empty = 1; else empty = args_has(args, 'E'); @@ -167,6 +168,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if ((new_wp = spawn_pane(&sc, &cause)) == NULL) { cmdq_error(item, "create pane failed: %s", cause); free(cause); + /* + * spawn_pane has already torn the half-built pane down (its + * fork-failure path removes the pane and destroys the layout + * cell), so new_wp is NULL and there is nothing for fail to do. + */ goto fail; } @@ -212,17 +218,19 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); screen_set_title(&new_wp->base, title, 0); - notify_pane("pane-title-changed", new_wp); + ep = event_payload_create(); + cmd_find_from_pane(&fs, new_wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", new_wp); + event_payload_set_window(ep, "window", new_wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); free(title); } if (input) { switch (window_pane_start_input(new_wp, item, &cause)) { case -1: - server_client_remove_pane(new_wp); - if (!is_floating) - layout_close_pane(new_wp); - window_remove_pane(wp->window, new_wp); cmdq_error(item, "%s", cause); free(cause); goto fail; @@ -269,10 +277,20 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); fail: + /* + * If the pane was spawned before we failed, tear it down here; this + * also destroys its layout cell. spawn_pane's own failure path has + * already done this, so new_wp is NULL in that case. + */ + if (new_wp != NULL) { + server_client_remove_pane(new_wp); + if (!is_floating) + layout_close_pane(new_wp); + window_remove_pane(wp->window, new_wp); + } if (sc.argv != NULL) cmd_free_argv(sc.argc, sc.argv); environ_free(sc.environ); - layout_destroy_cell(w, lc, &w->layout_root); return (CMD_RETURN_ERROR); diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index f7519a0dd..da7ea3706 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-swap-pane.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -42,6 +42,22 @@ const struct cmd_entry cmd_swap_pane_entry = { .exec = cmd_swap_pane_exec }; +static struct window_pane * +cmd_swap_pane_next_tiled_pane(struct window_pane *wp) +{ + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) + wp = TAILQ_NEXT(wp, entry); + return (wp); +} + +static struct window_pane * +cmd_swap_pane_prev_tiled_pane(struct window_pane *wp) +{ + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) + wp = TAILQ_PREV(wp, window_panes, entry); + return (wp); +} + static enum cmd_retval cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -62,15 +78,29 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) server_redraw_window(dst_w); 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); + } src_w = dst_w; src_wp = TAILQ_NEXT(dst_wp, entry); - if (src_wp == NULL) + src_wp = cmd_swap_pane_next_tiled_pane(src_wp); + if (src_wp == NULL) { src_wp = TAILQ_FIRST(&dst_w->panes); + src_wp = cmd_swap_pane_next_tiled_pane(src_wp); + } } 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); + } src_w = dst_w; src_wp = TAILQ_PREV(dst_wp, window_panes, entry); - if (src_wp == NULL) + src_wp = cmd_swap_pane_prev_tiled_pane(src_wp); + if (src_wp == NULL) { src_wp = TAILQ_LAST(&dst_w->panes, window_panes); + src_wp = cmd_swap_pane_prev_tiled_pane(src_wp); + } } if (src_w != dst_w && window_push_zoom(src_w, 0, args_has(args, 'Z'))) @@ -79,12 +109,6 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) if (src_wp == dst_wp) goto out; - if (window_pane_is_floating(src_wp) || - window_pane_is_floating(dst_wp)) { - cmdq_error(item, "cannot swap floating panes"); - return (CMD_RETURN_ERROR); - } - server_client_remove_pane(src_wp); server_client_remove_pane(dst_wp); @@ -156,9 +180,9 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) redraw_invalidate_scene(dst_w); server_redraw_window(dst_w); - notify_window("window-layout-changed", src_w); + events_fire_window("window-layout-changed", src_w); if (src_w != dst_w) - notify_window("window-layout-changed", dst_w); + events_fire_window("window-layout-changed", dst_w); out: if (window_pop_zoom(src_w)) diff --git a/cmd-swap-window.c b/cmd-swap-window.c index d31353982..980da0395 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-swap-window.c,v 1.29 2025/10/30 13:52:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 9865ac52c..177107b47 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-unbind-key.c b/cmd-unbind-key.c index 6d91d7cca..f537fb576 100644 --- a/cmd-unbind-key.c +++ b/cmd-unbind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-unbind-key.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-wait-for.c b/cmd-wait-for.c index 8a6aa259e..0e4130fcd 100644 --- a/cmd-wait-for.c +++ b/cmd-wait-for.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-wait-for.c,v 1.23 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -34,8 +34,8 @@ const struct cmd_entry cmd_wait_for_entry = { .name = "wait-for", .alias = "wait", - .args = { "LSU", 1, 1, NULL }, - .usage = "[-L|-S|-U] channel", + .args = { "EF:LSUlvw:", 1, 1, NULL }, + .usage = "[-ELSUlv] [-F format] [-w waiter] name", .flags = 0, .exec = cmd_wait_for_exec @@ -46,6 +46,17 @@ struct wait_item { TAILQ_ENTRY(wait_item) entry; }; +struct wait_event_item { + struct cmdq_item *item; + struct events_sink *sink; + char *name; + char *filter; + int verbose; + TAILQ_ENTRY(wait_event_item) entry; +}; +static TAILQ_HEAD(, wait_event_item) wait_event_items = + TAILQ_HEAD_INITIALIZER(wait_event_items); + struct wait_channel { const char *name; int locked; @@ -76,9 +87,28 @@ static enum cmd_retval cmd_wait_for_lock(struct cmdq_item *, const char *, struct wait_channel *); static enum cmd_retval cmd_wait_for_unlock(struct cmdq_item *, const char *, struct wait_channel *); +static enum cmd_retval cmd_wait_for_event(struct cmdq_item *, const char *, + struct args *); +static void cmd_wait_for_event_cb(const char *, + struct event_payload *, void *); +static void cmd_wait_for_event_free(struct wait_event_item *); +static enum cmd_retval cmd_wait_for_event_list(struct cmdq_item *, + const char *); +static enum cmd_retval cmd_wait_for_event_wake(struct cmdq_item *, + const char *, struct args *); +static enum cmd_retval cmd_wait_for_list(struct cmdq_item *, + struct wait_channel *); +static enum cmd_retval cmd_wait_for_wake(struct cmdq_item *, const char *, + struct args *, struct wait_channel *); static struct wait_channel *cmd_wait_for_add(const char *); static void cmd_wait_for_remove(struct wait_channel *); +static void cmd_wait_for_remove_empty( + struct wait_channel *); +static const char *cmd_wait_for_item_client_name( + struct cmdq_item *); +static const char *cmd_wait_for_client_name( + struct wait_event_item *); static struct wait_channel * cmd_wait_for_add(const char *name) @@ -117,16 +147,53 @@ cmd_wait_for_remove(struct wait_channel *wc) free(wc); } +static void +cmd_wait_for_remove_empty(struct wait_channel *wc) +{ + if (wc->locked || wc->woken) + return; + if (!TAILQ_EMPTY(&wc->waiters) || !TAILQ_EMPTY(&wc->lockers)) + return; + + log_debug("remove empty wait channel %s", wc->name); + + RB_REMOVE(wait_channels, &wait_channels, wc); + + free((void *)wc->name); + free(wc); +} + +static const char * +cmd_wait_for_item_client_name(struct cmdq_item *item) +{ + struct client *c = cmdq_get_client(item); + + if (c == NULL || c->name == NULL) + return (""); + return (c->name); +} + +static const char * +cmd_wait_for_client_name(struct wait_event_item *wei) +{ + return (cmd_wait_for_item_client_name(wei->item)); +} + static enum cmd_retval cmd_wait_for_exec(struct cmd *self, struct cmdq_item *item) { - struct args *args = cmd_get_args(self); + struct args *args = cmd_get_args(self); const char *name = args_string(args, 0); - struct wait_channel *wc, find; + struct wait_channel *wc, find = { .name = name }; + + if (args_has(args, 'E')) + return (cmd_wait_for_event(item, name, args)); - find.name = name; wc = RB_FIND(wait_channels, &wait_channels, &find); - + if (args_has(args, 'l')) + return (cmd_wait_for_list(item, wc)); + if (args_has(args, 'w')) + return (cmd_wait_for_wake(item, name, args, wc)); if (args_has(args, 'S')) return (cmd_wait_for_signal(item, name, wc)); if (args_has(args, 'L')) @@ -136,6 +203,179 @@ cmd_wait_for_exec(struct cmd *self, struct cmdq_item *item) return (cmd_wait_for_wait(item, name, wc)); } +static void +cmd_wait_for_event_print(struct wait_event_item *wei, struct event_payload *ep) +{ + struct event_payload_item *epi; + const char *key; + char *value; + + epi = event_payload_first(ep); + while (epi != NULL) { + key = event_payload_item_name(epi); + if (*key != '_') { + value = event_payload_item_print(epi); + cmdq_print(wei->item, "%s=%s", key, value); + free(value); + } + epi = event_payload_next(epi); + } +} + +static void +cmd_wait_for_event_cb(__unused const char *name, struct event_payload *ep, + void *item_data) +{ + struct wait_event_item *wei = item_data; + struct format_tree *ft; + char *expanded; + int flag; + + if (wei->verbose) + cmd_wait_for_event_print(wei, ep); + + if (wei->filter != NULL) { + ft = format_create(cmdq_get_client(wei->item), wei->item, + FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, NULL); + expanded = format_expand(ft, wei->filter); + flag = format_true(expanded); + free(expanded); + format_free(ft); + + if (!flag) + return; + } + + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); +} + +static void +cmd_wait_for_event_free(struct wait_event_item *wei) +{ + events_remove_sink(wei->sink); + free(wei->name); + free(wei->filter); + free(wei); +} + +static enum cmd_retval +cmd_wait_for_event(struct cmdq_item *item, const char *name, struct args *args) +{ + struct wait_event_item *wei; + const char *filter = args_get(args, 'F'); + + if (!hooks_valid_event_name(name)) { + cmdq_error(item, "invalid event: %s", name); + return (CMD_RETURN_ERROR); + } + if (args_has(args, 'l')) + return (cmd_wait_for_event_list(item, name)); + if (args_has(args, 'w')) + return (cmd_wait_for_event_wake(item, name, args)); + + if (cmdq_get_client(item) == NULL) { + cmdq_error(item, "not able to wait"); + return (CMD_RETURN_ERROR); + } + + wei = xcalloc(1, sizeof *wei); + wei->item = item; + wei->name = xstrdup(name); + wei->filter = (filter != NULL ? xstrdup(filter) : NULL); + wei->verbose = args_has(args, 'v'); + wei->sink = events_add_sink(name, cmd_wait_for_event_cb, wei); + TAILQ_INSERT_TAIL(&wait_event_items, wei, entry); + + return (CMD_RETURN_WAIT); +} + +static enum cmd_retval +cmd_wait_for_event_list(struct cmdq_item *item, const char *name) +{ + struct wait_event_item *wei; + + TAILQ_FOREACH(wei, &wait_event_items, entry) { + if (strcmp(wei->name, name) == 0) + cmdq_print(item, "%s", cmd_wait_for_client_name(wei)); + } + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_wait_for_event_wake(struct cmdq_item *item, const char *name, + struct args *args) +{ + struct wait_event_item *wei, *wei1; + const char *client_name = args_get(args, 'w'); + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + if (strcmp(wei->name, name) != 0) + continue; + if (strcmp(cmd_wait_for_client_name(wei), client_name) != 0) + continue; + + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + return (CMD_RETURN_NORMAL); + } + + cmdq_error(item, "waiter %s not found", client_name); + return (CMD_RETURN_ERROR); +} + +static enum cmd_retval +cmd_wait_for_list(struct cmdq_item *item, struct wait_channel *wc) +{ + struct wait_item *wi; + + if (wc == NULL) + return (CMD_RETURN_NORMAL); + + TAILQ_FOREACH(wi, &wc->waiters, entry) + cmdq_print(item, "%s", cmd_wait_for_item_client_name(wi->item)); + TAILQ_FOREACH(wi, &wc->lockers, entry) + cmdq_print(item, "%s", cmd_wait_for_item_client_name(wi->item)); + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_wait_for_wake(__unused struct cmdq_item *item, const char *name, + struct args *args, struct wait_channel *wc) +{ + struct wait_item *wi, *wi1; + const char *client_name = args_get(args, 'w'); + + if (wc != NULL) { + TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { + name = cmd_wait_for_item_client_name(wi->item); + if (strcmp(name, client_name) != 0) + continue; + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->waiters, wi, entry); + free(wi); + cmd_wait_for_remove_empty(wc); + return (CMD_RETURN_NORMAL); + } + TAILQ_FOREACH_SAFE(wi, &wc->lockers, entry, wi1) { + name = cmd_wait_for_item_client_name(wi->item); + if (strcmp(name, client_name) != 0) + continue; + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->lockers, wi, entry); + free(wi); + cmd_wait_for_remove_empty(wc); + return (CMD_RETURN_NORMAL); + } + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_wait_for_signal(__unused struct cmdq_item *item, const char *name, struct wait_channel *wc) @@ -245,6 +485,13 @@ cmd_wait_for_flush(void) { struct wait_channel *wc, *wc1; struct wait_item *wi, *wi1; + struct wait_event_item *wei, *wei1; + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + } RB_FOREACH_SAFE(wc, wait_channels, &wait_channels, wc1) { TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { diff --git a/cmd.c b/cmd.c index 6565c36a4..331177bbb 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd.c,v 1.186 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -646,7 +646,7 @@ cmd_template_replace(const char *template, const char *s, int idx) char ch, *buf; const char *ptr, *cp, quote[] = "\"\\$;~"; int replaced, quoted; - size_t len; + size_t len, slen; if (strchr(template, '%') == NULL) return (xstrdup(template)); @@ -671,7 +671,10 @@ cmd_template_replace(const char *template, const char *s, int idx) if (quoted) ptr++; - buf = xrealloc(buf, len + (strlen(s) * 3) + 1); + slen = strlen(s); + if (slen > SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) + fatalx("argument too long"); + buf = xrealloc(buf, len + (slen * 3) + 1); for (cp = s; *cp != '\0'; cp++) { if (quoted && strchr(quote, *cp) != NULL) buf[len++] = '\\'; @@ -680,6 +683,8 @@ cmd_template_replace(const char *template, const char *s, int idx) buf[len] = '\0'; continue; } + if (len > SIZE_MAX - 2) + fatalx("argument too long"); buf = xrealloc(buf, len + 2); buf[len++] = ch; buf[len] = '\0'; diff --git a/colour.c b/colour.c index b37882a88..411d77921 100644 --- a/colour.c +++ b/colour.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: colour.c,v 1.35 2026/07/06 14:29:10 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -1294,7 +1294,8 @@ colour_palette_from_option(struct colour_palette *p, struct options *oo) { struct options_entry *o; struct options_array_item *a; - u_int i, n; + union options_value *ov; + u_int i; int c; if (p == NULL) @@ -1312,12 +1313,11 @@ colour_palette_from_option(struct colour_palette *p, struct options *oo) p->default_palette = xcalloc(256, sizeof *p->default_palette); for (i = 0; i < 256; i++) p->default_palette[i] = -1; - while (a != NULL) { - n = options_array_item_index(a); - if (n < 256) { - c = options_array_item_value(a)->number; - p->default_palette[n] = c; + for (i = 0; i < 256; i++) { + ov = options_array_getv(o, "%u", i); + if (ov != NULL) { + c = ov->number; + p->default_palette[i] = c; } - a = options_array_next(a); } } diff --git a/compat/getline.c b/compat/getline.c index 90437ccb9..feabdd43c 100644 --- a/compat/getline.c +++ b/compat/getline.c @@ -74,8 +74,14 @@ getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp) } if (ptr + 2 >= eptr) { char *nbuf; - size_t nbufsiz = *bufsiz * 2; + size_t nbufsiz; ssize_t d = ptr - *buf; + + if (*bufsiz > SIZE_MAX / 2) { + errno = EOVERFLOW; + return -1; + } + nbufsiz = *bufsiz * 2; if ((nbuf = realloc(*buf, nbufsiz)) == NULL) return -1; *buf = nbuf; diff --git a/configure.ac b/configure.ac index e2a5f169c..c118800b8 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ # configure.ac -AC_INIT([tmux], next-3.7) +AC_INIT([tmux], next-3.8) AC_PREREQ([2.60]) AC_CONFIG_AUX_DIR(etc) @@ -78,7 +78,7 @@ AC_ARG_ENABLE( asan, AS_HELP_STRING(--enable-asan, enable ASAN build flags), , - enable_asan=no + [case "x$host_os" in *darwin*) enable_asan=yes;; esac] ) AM_CONDITIONAL(IS_ASAN, test "x$enable_asan" = xyes) diff --git a/control-notify.c b/control-notify.c index 021f9884a..6fec1683f 100644 --- a/control-notify.c +++ b/control-notify.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -23,31 +23,56 @@ #include "tmux.h" +/* Should this client be sent events? */ #define CONTROL_SHOULD_NOTIFY_CLIENT(c) \ - ((c) != NULL && ((c)->flags & CLIENT_CONTROL) && \ + ((c) != NULL && \ + ((c)->flags & CLIENT_CONTROL) && \ + (~(c)->flags & CLIENT_EXIT) && \ (c)->control_state != NULL) -void -control_notify_pane_mode_changed(int pane) +/* Notify control clients that pane mode changed. */ +static void +control_pane_mode_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; + struct window_pane *wp; + struct client *c; + char *value; - TAILQ_FOREACH(c, &clients, entry) { - if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) - continue; - - control_write(c, "%%pane-mode-changed %%%u", pane); + wp = event_payload_get_pane(ep, "pane"); + if (wp != NULL) { + TAILQ_FOREACH(c, &clients, entry) { + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_write(c, "%%pane-mode-changed %%%u", wp->id); + } + return; } + + value = event_payload_print(ep, "pane"); + if (value == NULL) + return; + TAILQ_FOREACH(c, &clients, entry) { + if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) + control_write(c, "%%pane-mode-changed %s", value); + } + free(value); } -void -control_notify_window_layout_changed(struct window *w) +/* Notify control clients that window layout changed. */ +static void +control_window_layout_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; - struct session *s; - struct winlink *wl; - const char *template; - char *cp; + struct client *c; + struct session *s; + struct winlink *wl; + struct window *w = event_payload_get_window(ep, "window"); + const char *template; + char *cp; + + if (w == NULL) + return; template = "%layout-change #{window_id} #{window_layout} " "#{window_visible_layout} #{window_raw_flags}"; @@ -72,12 +97,15 @@ control_notify_window_layout_changed(struct window *w) free(cp); } -void -control_notify_window_pane_changed(struct window *w) +/* Notify control clients that window pane changed. */ +static void +control_window_pane_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { struct client *c; + struct window *w = event_payload_get_window(ep, "window"); - if (w->active == NULL) + if (w == NULL || w->active == NULL) return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) @@ -88,12 +116,17 @@ control_notify_window_pane_changed(struct window *w) } } -void -control_notify_window_unlinked(__unused struct session *s, struct window *w) +/* Notify control clients that a window was unlinked. */ +static void +control_window_unlinked_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -106,12 +139,17 @@ control_notify_window_unlinked(__unused struct session *s, struct window *w) } } -void -control_notify_window_linked(__unused struct session *s, struct window *w) +/* Notify control clients that a window was linked. */ +static void +control_window_linked_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -124,12 +162,17 @@ control_notify_window_linked(__unused struct session *s, struct window *w) } } -void -control_notify_window_renamed(struct window *w) +/* Notify control clients that a window was renamed. */ +static void +control_window_renamed_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -145,13 +188,16 @@ control_notify_window_renamed(struct window *w) } } -void -control_notify_client_session_changed(struct client *cc) +/* Notify control clients that a client changed session. */ +static void +control_client_session_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; - struct session *s; + struct client *cc = event_payload_get_client(ep, "client"); + struct client *c; + struct session *s; - if (cc->session == NULL) + if (cc == NULL || cc->session == NULL) return; s = cc->session; @@ -169,22 +215,32 @@ control_notify_client_session_changed(struct client *cc) } } -void -control_notify_client_detached(struct client *cc) +/* Notify control clients that a client detached. */ +static void +control_client_detached_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; + struct client *cc = event_payload_get_client(ep, "client"); + struct client *c; + if (cc == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) control_write(c, "%%client-detached %s", cc->name); } } -void -control_notify_session_renamed(struct session *s) +/* Notify control clients that a session was renamed. */ +static void +control_session_renamed_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; + struct session *s = event_payload_get_session(ep, "session"); + struct client *c; + if (s == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; @@ -193,8 +249,10 @@ control_notify_session_renamed(struct session *s) } } -void -control_notify_session_created(__unused struct session *s) +/* Notify control clients that sessions changed. */ +static void +control_session_created_cb(__unused const char *name, + __unused struct event_payload *ep, __unused void *sink_data) { struct client *c; @@ -206,8 +264,10 @@ control_notify_session_created(__unused struct session *s) } } -void -control_notify_session_closed(__unused struct session *s) +/* Notify control clients that sessions changed. */ +static void +control_session_closed_cb(__unused const char *name, + __unused struct event_payload *ep, __unused void *sink_data) { struct client *c; @@ -219,9 +279,12 @@ control_notify_session_closed(__unused struct session *s) } } -void -control_notify_session_window_changed(struct session *s) +/* Notify control clients that the current window changed. */ +static void +control_session_window_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + struct session *s = event_payload_get_session(ep, "session"); struct client *c; /* @@ -229,7 +292,7 @@ control_notify_session_window_changed(struct session *s) * session has been destroyed (which sets curw to NULL) but is kept * alive by the notification's reference. Skip the notification. */ - if (s->curw == NULL) + if (s == NULL || s->curw == NULL) return; TAILQ_FOREACH(c, &clients, entry) { @@ -241,28 +304,68 @@ control_notify_session_window_changed(struct session *s) } } -void -control_notify_paste_buffer_changed(const char *name) +/* Notify control clients that a paste buffer changed. */ +static void +control_paste_buffer_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + const char *pbname = event_payload_get_string(ep, "paste_buffer"); struct client *c; + if (pbname == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%paste-buffer-changed %s", name); + control_write(c, "%%paste-buffer-changed %s", pbname); } } -void -control_notify_paste_buffer_deleted(const char *name) +/* Notify control clients that a paste buffer was deleted. */ +static void +control_paste_buffer_deleted_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + const char *pbname = event_payload_get_string(ep, "paste_buffer"); struct client *c; + if (pbname == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%paste-buffer-deleted %s", name); + control_write(c, "%%paste-buffer-deleted %s", pbname); } } + +/* Build control event sinks. */ +void +control_build_events(void) +{ + /* Control event sink callbacks. */ + static struct { + const char *name; + events_cb cb; + } events[] = { + { "pane-mode-changed", control_pane_mode_changed_cb }, + { "window-layout-changed", control_window_layout_changed_cb }, + { "window-pane-changed", control_window_pane_changed_cb }, + { "window-unlinked", control_window_unlinked_cb }, + { "window-linked", control_window_linked_cb }, + { "window-renamed", control_window_renamed_cb }, + { "client-session-changed", control_client_session_changed_cb }, + { "client-detached", control_client_detached_cb }, + { "session-renamed", control_session_renamed_cb }, + { "session-created", control_session_created_cb }, + { "session-closed", control_session_closed_cb }, + { "session-window-changed", control_session_window_changed_cb }, + { "paste-buffer-changed", control_paste_buffer_changed_cb }, + { "paste-buffer-deleted", control_paste_buffer_deleted_cb } + }; + u_int i; + + for (i = 0; i < nitems(events); i++) + events_add_sink(events[i].name, events[i].cb, NULL); +} diff --git a/control.c b/control.c index 36f6148d1..35d876ed4 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control.c,v 1.61 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -19,6 +19,9 @@ #include +#include +#include +#include #include #include #include @@ -75,42 +78,6 @@ struct control_pane { }; RB_HEAD(control_panes, control_pane); -/* Subscription pane. */ -struct control_sub_pane { - u_int pane; - u_int idx; - char *last; - - RB_ENTRY(control_sub_pane) entry; -}; -RB_HEAD(control_sub_panes, control_sub_pane); - -/* Subscription window. */ -struct control_sub_window { - u_int window; - u_int idx; - char *last; - - RB_ENTRY(control_sub_window) entry; -}; -RB_HEAD(control_sub_windows, control_sub_window); - -/* Control client subscription. */ -struct control_sub { - char *name; - char *format; - - enum control_sub_type type; - u_int id; - - char *last; - struct control_sub_panes panes; - struct control_sub_windows windows; - - RB_ENTRY(control_sub) entry; -}; -RB_HEAD(control_subs, control_sub); - /* Control client state. */ struct control_state { struct control_panes panes; @@ -123,8 +90,7 @@ struct control_state { struct bufferevent *read_event; struct bufferevent *write_event; - struct control_subs subs; - struct event subs_timer; + struct monitor_set *subs; }; /* Low and high watermarks. */ @@ -154,75 +120,6 @@ control_pane_cmp(struct control_pane *cp1, struct control_pane *cp2) } RB_GENERATE_STATIC(control_panes, control_pane, entry, control_pane_cmp); -/* Compare client subs. */ -static int -control_sub_cmp(struct control_sub *csub1, struct control_sub *csub2) -{ - return (strcmp(csub1->name, csub2->name)); -} -RB_GENERATE_STATIC(control_subs, control_sub, entry, control_sub_cmp); - -/* Compare client subscription panes. */ -static int -control_sub_pane_cmp(struct control_sub_pane *csp1, - struct control_sub_pane *csp2) -{ - if (csp1->pane < csp2->pane) - return (-1); - if (csp1->pane > csp2->pane) - return (1); - if (csp1->idx < csp2->idx) - return (-1); - if (csp1->idx > csp2->idx) - return (1); - return (0); -} -RB_GENERATE_STATIC(control_sub_panes, control_sub_pane, entry, - control_sub_pane_cmp); - -/* Compare client subscription windows. */ -static int -control_sub_window_cmp(struct control_sub_window *csw1, - struct control_sub_window *csw2) -{ - if (csw1->window < csw2->window) - return (-1); - if (csw1->window > csw2->window) - return (1); - if (csw1->idx < csw2->idx) - return (-1); - if (csw1->idx > csw2->idx) - return (1); - return (0); -} -RB_GENERATE_STATIC(control_sub_windows, control_sub_window, entry, - control_sub_window_cmp); - -/* Free a subscription. */ -static void -control_free_sub(struct control_state *cs, struct control_sub *csub) -{ - struct control_sub_pane *csp, *csp1; - struct control_sub_window *csw, *csw1; - - RB_FOREACH_SAFE(csp, control_sub_panes, &csub->panes, csp1) { - RB_REMOVE(control_sub_panes, &csub->panes, csp); - free(csp->last); - free(csp); - } - RB_FOREACH_SAFE(csw, control_sub_windows, &csub->windows, csw1) { - RB_REMOVE(control_sub_windows, &csub->windows, csw); - free(csw->last); - free(csw); - } - free(csub->last); - - RB_REMOVE(control_subs, &cs->subs, csub); - free(csub->name); - free(csub->format); - free(csub); -} - /* Free a block. */ static void control_free_block(struct control_state *cs, struct control_block *cb) @@ -481,7 +378,7 @@ control_write_output(struct client *c, struct window_pane *wp) if (winlink_find_by_window(&c->session->windows, wp->window) == NULL) return; - if (c->flags & CONTROL_IGNORE_FLAGS) { + if (c->flags & (CONTROL_IGNORE_FLAGS|CLIENT_EXIT)) { cp = control_get_pane(c, wp); if (cp != NULL) goto ignore; @@ -597,6 +494,53 @@ control_all_done(struct client *c) return (EVBUFFER_LENGTH(cs->write_event->output) == 0); } +/* + * Wait for the terminal to send an empty line or close, used by a control + * client after printing %exit so a wrapping terminal (such as iTerm2) can + * finish reading. + */ +void +control_wait_exit(int fd) +{ + struct pollfd pfd; + struct evbuffer *evb; + char *line; + int n; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + + for (;;) { + line = evbuffer_readln(evb, NULL, EVBUFFER_EOL_LF); + if (line != NULL) { + if (*line == '\0') { /* empty line, stop */ + free(line); + break; + } + free(line); + continue; /* drain buffered lines first */ + } + + memset(&pfd, 0, sizeof pfd); + pfd.fd = fd; + pfd.events = POLLIN; + if (poll(&pfd, 1, INFTIM) == -1) { + if (errno == EINTR) + continue; + break; + } + + n = evbuffer_read(evb, fd, -1); + if (n == 0) + break; + if (n == -1 && errno != EAGAIN && errno != EINTR) + break; + } + + evbuffer_free(evb); +} + /* Flush all blocks until output. */ static void control_flush_all_blocks(struct client *c) @@ -772,6 +716,30 @@ control_write_callback(__unused struct bufferevent *bufev, void *data) bufferevent_disable(cs->write_event, EV_WRITE); } +/* Write a subscription change. */ +static void +control_sub_change(struct monitor_change *change, __unused void *data) +{ + struct client *c = change->c; + struct session *s = change->s; + struct winlink *wl = change->wl; + struct window_pane *wp = change->wp; + struct window *w; + + if (wp != NULL) { + w = wp->window; + control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s", + change->name, s->id, w->id, wl->idx, wp->id, change->value); + } else if (wl != NULL) { + w = wl->window; + control_write(c, "%%subscription-changed %s $%u @%u %u - : %s", + change->name, s->id, w->id, wl->idx, change->value); + } else { + control_write(c, "%%subscription-changed %s $%u - - - : %s", + change->name, s->id, change->value); + } +} + /* Initialize for control mode. */ void control_start(struct client *c) @@ -789,7 +757,7 @@ control_start(struct client *c) RB_INIT(&cs->panes); TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->all_blocks); - RB_INIT(&cs->subs); + cs->subs = monitor_create_client(c, control_sub_change, NULL); cs->read_event = bufferevent_new(c->fd, control_read_callback, control_write_callback, control_error_callback, c); @@ -838,20 +806,16 @@ control_stop(struct client *c) { struct control_state *cs = c->control_state; struct control_block *cb, *cb1; - struct control_sub *csub, *csub1; if (cs == NULL) return; + monitor_destroy(cs->subs); + if (~c->flags & CLIENT_CONTROLCONTROL) bufferevent_free(cs->write_event); bufferevent_free(cs->read_event); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) - control_free_sub(cs, csub); - if (evtimer_initialized(&cs->subs_timer)) - evtimer_del(&cs->subs_timer); - control_reset_offsets(c); TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) control_free_block(cs, cb); @@ -860,313 +824,14 @@ control_stop(struct client *c) free(cs); } -/* Check session subscription. */ -static void -control_check_subs_session(struct client *c, struct control_sub *csub, - struct format_tree *ft) -{ - struct session *s = c->session; - char *value; - - value = format_expand(ft, csub->format); - - if (csub->last != NULL && strcmp(value, csub->last) == 0) { - free(value); - return; - } - control_write(c, - "%%subscription-changed %s $%u - - - : %s", - csub->name, s->id, value); - free(csub->last); - csub->last = value; -} - -/* Check pane subscription. */ -static void -control_check_subs_pane(struct client *c, struct control_sub *csub) -{ - struct session *s = c->session; - struct window_pane *wp; - struct window *w; - struct winlink *wl; - struct format_tree *ft; - char *value; - struct control_sub_pane *csp, find; - - wp = window_pane_find_by_id(csub->id); - if (wp == NULL || wp->fd == -1) - return; - w = wp->window; - - TAILQ_FOREACH(wl, &w->winlinks, wentry) { - if (wl->session != s) - continue; - - ft = format_create_defaults(NULL, c, s, wl, wp); - value = format_expand(ft, csub->format); - format_free(ft); - - find.pane = wp->id; - find.idx = wl->idx; - - csp = RB_FIND(control_sub_panes, &csub->panes, &find); - if (csp == NULL) { - csp = xcalloc(1, sizeof *csp); - csp->pane = wp->id; - csp->idx = wl->idx; - RB_INSERT(control_sub_panes, &csub->panes, csp); - } - - if (csp->last != NULL && strcmp(value, csp->last) == 0) { - free(value); - continue; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u %%%u : %s", - csub->name, s->id, w->id, wl->idx, wp->id, value); - free(csp->last); - csp->last = value; - } -} - -/* Check all-panes subscription for a pane. */ -static void -control_check_subs_all_panes_one(struct client *c, struct control_sub *csub, - struct format_tree *ft, struct winlink *wl, struct window_pane *wp) -{ - struct session *s = c->session; - struct window *w = wl->window; - char *value; - struct control_sub_pane *csp, find; - - value = format_expand(ft, csub->format); - - find.pane = wp->id; - find.idx = wl->idx; - - csp = RB_FIND(control_sub_panes, &csub->panes, &find); - if (csp == NULL) { - csp = xcalloc(1, sizeof *csp); - csp->pane = wp->id; - csp->idx = wl->idx; - RB_INSERT(control_sub_panes, &csub->panes, csp); - } - - if (csp->last != NULL && strcmp(value, csp->last) == 0) { - free(value); - return; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u %%%u : %s", - csub->name, s->id, w->id, wl->idx, wp->id, value); - free(csp->last); - csp->last = value; -} - -/* Check window subscription. */ -static void -control_check_subs_window(struct client *c, struct control_sub *csub) -{ - struct session *s = c->session; - struct window *w; - struct winlink *wl; - struct format_tree *ft; - char *value; - struct control_sub_window *csw, find; - - w = window_find_by_id(csub->id); - if (w == NULL) - return; - - TAILQ_FOREACH(wl, &w->winlinks, wentry) { - if (wl->session != s) - continue; - - ft = format_create_defaults(NULL, c, s, wl, NULL); - value = format_expand(ft, csub->format); - format_free(ft); - - find.window = w->id; - find.idx = wl->idx; - - csw = RB_FIND(control_sub_windows, &csub->windows, &find); - if (csw == NULL) { - csw = xcalloc(1, sizeof *csw); - csw->window = w->id; - csw->idx = wl->idx; - RB_INSERT(control_sub_windows, &csub->windows, csw); - } - - if (csw->last != NULL && strcmp(value, csw->last) == 0) { - free(value); - continue; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u - : %s", - csub->name, s->id, w->id, wl->idx, value); - free(csw->last); - csw->last = value; - } -} - -/* Check all-windows subscription for a window. */ -static void -control_check_subs_all_windows_one(struct client *c, struct control_sub *csub, - struct format_tree *ft, struct winlink *wl) -{ - struct session *s = c->session; - struct window *w = wl->window; - char *value; - struct control_sub_window *csw, find; - - value = format_expand(ft, csub->format); - - find.window = w->id; - find.idx = wl->idx; - - csw = RB_FIND(control_sub_windows, &csub->windows, &find); - if (csw == NULL) { - csw = xcalloc(1, sizeof *csw); - csw->window = w->id; - csw->idx = wl->idx; - RB_INSERT(control_sub_windows, &csub->windows, csw); - } - - if (csw->last != NULL && strcmp(value, csw->last) == 0) { - free(value); - return; - } - control_write(c, - "%%subscription-changed %s $%u @%u %u - : %s", - csub->name, s->id, w->id, wl->idx, value); - free(csw->last); - csw->last = value; -} - -/* Check subscriptions timer. */ -static void -control_check_subs_timer(__unused int fd, __unused short events, void *data) -{ - struct client *c = data; - struct control_state *cs = c->control_state; - struct control_sub *csub, *csub1; - struct session *s = c->session; - struct format_tree *ft; - struct winlink *wl; - struct window_pane *wp; - struct timeval tv = { .tv_sec = 1 }; - int have_session = 0, have_all_panes = 0; - int have_all_windows = 0; - - log_debug("%s: timer fired", __func__); - evtimer_add(&cs->subs_timer, &tv); - - if (s == NULL) - return; - - /* Find which subscription types are present. */ - RB_FOREACH(csub, control_subs, &cs->subs) { - switch (csub->type) { - case CONTROL_SUB_SESSION: - have_session = 1; - break; - case CONTROL_SUB_ALL_PANES: - have_all_panes = 1; - break; - case CONTROL_SUB_ALL_WINDOWS: - have_all_windows = 1; - break; - default: - break; - } - } - - /* Check session subscriptions. */ - if (have_session) { - ft = format_create_defaults(NULL, c, s, NULL, NULL); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { - if (csub->type == CONTROL_SUB_SESSION) - control_check_subs_session(c, csub, ft); - } - format_free(ft); - } - - /* Check pane and window subscriptions. */ - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) { - switch (csub->type) { - case CONTROL_SUB_PANE: - control_check_subs_pane(c, csub); - break; - case CONTROL_SUB_WINDOW: - control_check_subs_window(c, csub); - break; - case CONTROL_SUB_SESSION: - case CONTROL_SUB_ALL_PANES: - case CONTROL_SUB_ALL_WINDOWS: - break; - } - } - - /* Check all-panes subscriptions. */ - if (have_all_panes) { - RB_FOREACH(wl, winlinks, &s->windows) { - TAILQ_FOREACH(wp, &wl->window->panes, entry) { - ft = format_create_defaults(NULL, c, s, wl, wp); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, - csub1) { - if (csub->type != CONTROL_SUB_ALL_PANES) - continue; - control_check_subs_all_panes_one(c, - csub, ft, wl, wp); - } - format_free(ft); - } - } - } - - /* Check all-windows subscriptions. */ - if (have_all_windows) { - RB_FOREACH(wl, winlinks, &s->windows) { - ft = format_create_defaults(NULL, c, s, wl, NULL); - RB_FOREACH_SAFE(csub, control_subs, &cs->subs, - csub1) { - if (csub->type != CONTROL_SUB_ALL_WINDOWS) - continue; - control_check_subs_all_windows_one(c, csub, ft, - wl); - } - format_free(ft); - } - } -} - /* Add a subscription. */ void -control_add_sub(struct client *c, const char *name, enum control_sub_type type, +control_add_sub(struct client *c, const char *name, enum monitor_type type, int id, const char *format) { struct control_state *cs = c->control_state; - struct control_sub *csub, find; - struct timeval tv = { .tv_sec = 1 }; - find.name = (char *)name; - if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL) - control_free_sub(cs, csub); - - csub = xcalloc(1, sizeof *csub); - csub->name = xstrdup(name); - csub->type = type; - csub->id = id; - csub->format = xstrdup(format); - RB_INSERT(control_subs, &cs->subs, csub); - - RB_INIT(&csub->panes); - RB_INIT(&csub->windows); - - if (!evtimer_initialized(&cs->subs_timer)) - evtimer_set(&cs->subs_timer, control_check_subs_timer, c); - if (!evtimer_pending(&cs->subs_timer, NULL)) - evtimer_add(&cs->subs_timer, &tv); + monitor_add(cs->subs, name, type, id, format, MONITOR_NOTIFY_INITIAL); } /* Remove a subscription. */ @@ -1174,11 +839,6 @@ void control_remove_sub(struct client *c, const char *name) { struct control_state *cs = c->control_state; - struct control_sub *csub, find; - find.name = (char *)name; - if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL) - control_free_sub(cs, csub); - if (RB_EMPTY(&cs->subs)) - evtimer_del(&cs->subs_timer); + monitor_remove(cs->subs, name); } diff --git a/environ.c b/environ.c index 19f9c327e..f2a60e258 100644 --- a/environ.c +++ b/environ.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: environ.c,v 1.31 2026/06/13 20:07:30 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/events-payload.c b/events-payload.c new file mode 100644 index 000000000..444836cf3 --- /dev/null +++ b/events-payload.c @@ -0,0 +1,672 @@ +/* $OpenBSD: events-payload.c,v 1.1 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include + +#include "tmux.h" + +/* Event payload item. */ +struct event_payload_item { + char *name; + enum event_payload_type type; + + union { + char *string; + time_t time; + int number; + u_int unsigned_number; + struct client *client; + struct session *session; + struct window *window; + struct window_pane *pane; + struct { + void *ptr; + event_payload_free_cb free_cb; + event_payload_print_cb print_cb; + } pointer; + }; + + RB_ENTRY(event_payload_item) entry; +}; + +RB_HEAD(event_payload_tree, event_payload_item); + +struct event_payload { + struct event_payload_tree items; + struct cmd_find_state target; +}; + +static int +event_payload_cmp(struct event_payload_item *epi1, + struct event_payload_item *epi2) +{ + return (strcmp(epi1->name, epi2->name)); +} +RB_GENERATE_STATIC(event_payload_tree, event_payload_item, entry, + event_payload_cmp); + +/* Find an item. */ +static struct event_payload_item * +event_payload_find(struct event_payload *ep, const char *name) +{ + struct event_payload_item find = { .name = (char *)name }; + + return (RB_FIND(event_payload_tree, &ep->items, &find)); +} + +/* Free the target in a payload. */ +static void +event_payload_free_target(struct event_payload *ep) +{ + struct cmd_find_state *target = &ep->target; + + if (target->s != NULL) + session_remove_ref(target->s, __func__); + if (target->w != NULL) + window_remove_ref(target->w, __func__); + if (target->wp != NULL) + window_pane_remove_ref(target->wp, __func__); + + cmd_find_clear_state(target, 0); +} + +/* Free the value in an item. */ +static void +event_payload_free_value(struct event_payload_item *epi) +{ + switch (epi->type) { + case EVENT_PAYLOAD_STRING: + free(epi->string); + break; + case EVENT_PAYLOAD_CLIENT: + server_client_unref(epi->client); + break; + case EVENT_PAYLOAD_SESSION: + session_remove_ref(epi->session, __func__); + break; + case EVENT_PAYLOAD_WINDOW: + window_remove_ref(epi->window, __func__); + break; + case EVENT_PAYLOAD_PANE: + window_pane_remove_ref(epi->pane, __func__); + break; + case EVENT_PAYLOAD_POINTER: + if (epi->pointer.free_cb != NULL) + epi->pointer.free_cb(epi->pointer.ptr); + break; + case EVENT_PAYLOAD_INT: + case EVENT_PAYLOAD_UINT: + case EVENT_PAYLOAD_TIME: + break; + } +} + +/* Set an item. */ +static void +event_payload_set_item(struct event_payload *ep, const char *name, + struct event_payload_item *new) +{ + struct event_payload_item *old; + + new->name = xstrdup(name); + old = RB_INSERT(event_payload_tree, &ep->items, new); + if (old != NULL) { + RB_REMOVE(event_payload_tree, &ep->items, old); + event_payload_free_value(old); + free(old->name); + free(old); + RB_INSERT(event_payload_tree, &ep->items, new); + } +} + +/* Create an event payload. */ +struct event_payload * +event_payload_create(void) +{ + struct event_payload *ep; + + ep = xcalloc(1, sizeof *ep); + RB_INIT(&ep->items); + cmd_find_clear_state(&ep->target, 0); + return (ep); +} + +/* Free an event payload. */ +void +event_payload_free(struct event_payload *ep) +{ + struct event_payload_item *epi, *epi1; + + if (ep != NULL) { + RB_FOREACH_SAFE(epi, event_payload_tree, &ep->items, epi1) { + RB_REMOVE(event_payload_tree, &ep->items, epi); + event_payload_free_value(epi); + free(epi->name); + free(epi); + } + event_payload_free_target(ep); + free(ep); + } +} + +/* Set the target. */ +void +event_payload_set_target(struct event_payload *ep, struct cmd_find_state *fs) +{ + struct cmd_find_state *target = &ep->target; + + event_payload_free_target(ep); + + if (fs->s != NULL) { + session_add_ref(fs->s, __func__); + target->s = fs->s; + } + if (fs->wl != NULL) { + target->idx = fs->wl->idx; + if (target->s == NULL) { + session_add_ref(fs->wl->session, __func__); + target->s = fs->wl->session; + } + } else + target->idx = -1; + if (fs->w != NULL) { + window_add_ref(fs->w, __func__); + target->w = fs->w; + } else if (fs->wl != NULL) { + window_add_ref(fs->wl->window, __func__); + target->w = fs->wl->window; + } + if (fs->wp != NULL) { + window_pane_add_ref(fs->wp, __func__); + target->wp = fs->wp; + } +} + +/* Get the target. */ +int +event_payload_get_target(struct event_payload *ep, struct cmd_find_state *fs) +{ + struct cmd_find_state *t = &ep->target; + struct winlink *wl = NULL; + int flags = fs->flags; + + if (t->idx != -1 && + t->s != NULL && + t->w != NULL && + session_alive(t->s)) { + wl = winlink_find_by_index(&t->s->windows, t->idx); + if (wl != NULL && wl->window != t->w) + wl = NULL; + } + + cmd_find_clear_state(fs, flags); + fs->s = t->s; + fs->w = t->w; + fs->wp = t->wp; + fs->wl = wl; + fs->idx = (wl != NULL ? wl->idx : -1); + if (cmd_find_valid_state(fs)) + return (1); + + if (wl != NULL && + t->wp != NULL && + window_has_pane(wl->window, t->wp)) { + cmd_find_from_winlink_pane(fs, wl, t->wp, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (t->wp != NULL && + cmd_find_from_pane(fs, t->wp, flags) == 0 && + cmd_find_valid_state(fs)) + return (1); + + if (wl != NULL) { + cmd_find_from_winlink(fs, wl, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (t->s != NULL && + t->w != NULL && + session_alive(t->s) && + cmd_find_from_session_window(fs, t->s, t->w, flags) == 0 && + cmd_find_valid_state(fs)) + return (1); + + if (t->s != NULL && session_alive(t->s)) { + cmd_find_from_session(fs, t->s, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (cmd_find_from_nothing(fs, flags) == 0) + return (1); + + cmd_find_clear_state(fs, flags); + return (0); +} + +/* Set a string item. */ +void +event_payload_set_string(struct event_payload *ep, const char *name, + const char *fmt, ...) +{ + struct event_payload_item *epi; + va_list ap; + + va_start(ap, fmt); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_STRING; + xvasprintf(&epi->string, fmt, ap); + event_payload_set_item(ep, name, epi); + + va_end(ap); +} + +/* Set a time item. */ +void +event_payload_set_time(struct event_payload *ep, const char *name, + time_t value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_TIME; + epi->time = value; + event_payload_set_item(ep, name, epi); +} + +/* Set a number item. */ +void +event_payload_set_int(struct event_payload *ep, const char *name, int value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_INT; + epi->number = value; + event_payload_set_item(ep, name, epi); +} + +/* Set an unsigned number item. */ +void +event_payload_set_uint(struct event_payload *ep, const char *name, u_int value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_UINT; + epi->unsigned_number = value; + event_payload_set_item(ep, name, epi); +} + +/* Set a client item. */ +void +event_payload_set_client(struct event_payload *ep, const char *name, + struct client *c) +{ + struct event_payload_item *epi; + + c->references++; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_CLIENT; + epi->client = c; + event_payload_set_item(ep, name, epi); +} + +/* Set a session item. */ +void +event_payload_set_session(struct event_payload *ep, const char *name, + struct session *s) +{ + struct event_payload_item *epi; + + session_add_ref(s, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_SESSION; + epi->session = s; + event_payload_set_item(ep, name, epi); +} + +/* Set a window item. */ +void +event_payload_set_window(struct event_payload *ep, const char *name, + struct window *w) +{ + struct event_payload_item *epi; + + window_add_ref(w, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_WINDOW; + epi->window = w; + event_payload_set_item(ep, name, epi); +} + +/* Set a pane item. */ +void +event_payload_set_pane(struct event_payload *ep, const char *name, + struct window_pane *wp) +{ + struct event_payload_item *epi; + + window_pane_add_ref(wp, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_PANE; + epi->pane = wp; + event_payload_set_item(ep, name, epi); +} + +/* Set a pointer item. */ +void +event_payload_set_pointer(struct event_payload *ep, const char *name, + void *ptr, event_payload_free_cb free_cb, event_payload_print_cb print_cb) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_POINTER; + epi->pointer.ptr = ptr; + epi->pointer.free_cb = free_cb; + epi->pointer.print_cb = print_cb; + event_payload_set_item(ep, name, epi); +} + +/* Get a string item. */ +const char * +event_payload_get_string(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_STRING) + return (NULL); + return (epi->string); +} + +/* Print a payload item. */ +static void +event_payload_add_item(struct event_payload_item *epi, struct evbuffer *evb) +{ + switch (epi->type) { + case EVENT_PAYLOAD_STRING: + evbuffer_add_printf(evb, "%s", epi->string); + break; + case EVENT_PAYLOAD_TIME: + evbuffer_add_printf(evb, "%lld", (long long)epi->time); + break; + case EVENT_PAYLOAD_INT: + evbuffer_add_printf(evb, "%d", epi->number); + break; + case EVENT_PAYLOAD_UINT: + evbuffer_add_printf(evb, "%u", epi->unsigned_number); + break; + case EVENT_PAYLOAD_CLIENT: + evbuffer_add_printf(evb, "%s", epi->client->name); + break; + case EVENT_PAYLOAD_SESSION: + evbuffer_add_printf(evb, "$%u", epi->session->id); + break; + case EVENT_PAYLOAD_WINDOW: + evbuffer_add_printf(evb, "@%u", epi->window->id); + break; + case EVENT_PAYLOAD_PANE: + evbuffer_add_printf(evb, "%%%u", epi->pane->id); + break; + case EVENT_PAYLOAD_POINTER: + if (epi->pointer.print_cb != NULL) + epi->pointer.print_cb(epi->pointer.ptr, evb); + else + evbuffer_add_printf(evb, "%p", epi->pointer.ptr); + break; + } +} + +/* Print a payload item. */ +char * +event_payload_item_print(struct event_payload_item *epi) +{ + struct evbuffer *evb; + char *value = NULL; + size_t size; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + event_payload_add_item(epi, evb); + if ((size = EVBUFFER_LENGTH(evb)) != 0) + value = xmemdup(EVBUFFER_DATA(evb), size); + else + value = xstrdup(""); + evbuffer_free(evb); + return (value); +} + +/* Print an item value. */ +char * +event_payload_print(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL) + return (NULL); + return (event_payload_item_print(epi)); +} + +/* Add payload items as formats. */ +void +event_payload_add_formats(struct event_payload *ep, struct format_tree *ft, + const char *prefix) +{ + struct event_payload_item *epi; + char *name, *value; + const char *key; + + if (prefix == NULL) + prefix = ""; + + RB_FOREACH(epi, event_payload_tree, &ep->items) { + key = epi->name; + if (*key == '_') + continue; + + value = event_payload_item_print(epi); + xasprintf(&name, "%s%s", prefix, key); + format_add(ft, name, "%s", value); + free(name); + free(value); + + if (epi->type == EVENT_PAYLOAD_SESSION) { + xasprintf(&name, "%s%s_name", prefix, key); + format_add(ft, name, "%s", epi->session->name); + free(name); + } else if (epi->type == EVENT_PAYLOAD_WINDOW) { + xasprintf(&name, "%s%s_name", prefix, key); + format_add(ft, name, "%s", epi->window->name); + free(name); + } + } +} + +/* Get the first payload item. */ +struct event_payload_item * +event_payload_first(struct event_payload *ep) +{ + return (RB_MIN(event_payload_tree, &ep->items)); +} + +/* Get the next payload item. */ +struct event_payload_item * +event_payload_next(struct event_payload_item *epi) +{ + return (RB_NEXT(event_payload_tree, , epi)); +} + +/* Get a payload item name. */ +const char * +event_payload_item_name(struct event_payload_item *epi) +{ + return (epi->name); +} + +/* Get a payload item type. */ +enum event_payload_type +event_payload_item_type(struct event_payload_item *epi) +{ + return (epi->type); +} + +/* Log a payload. */ +void +event_payload_log(struct event_payload *ep, const char *fmt, ...) +{ + struct event_payload_item *epi; + struct evbuffer *evb; + va_list ap; + char *prefix; + + va_start(ap, fmt); + xvasprintf(&prefix, fmt, ap); + va_end(ap); + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + if (ep != NULL) { + RB_FOREACH(epi, event_payload_tree, &ep->items) { + if (EVBUFFER_LENGTH(evb) != 0) + evbuffer_add_printf(evb, ", "); + evbuffer_add_printf(evb, "%s=", epi->name); + event_payload_add_item(epi, evb); + } + } + log_debug("%s%.*s", prefix, (int)EVBUFFER_LENGTH(evb), + (char *)EVBUFFER_DATA(evb)); + evbuffer_free(evb); + free(prefix); +} + +/* Get a time item. */ +time_t +event_payload_get_time(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_TIME) + return (0); + return (epi->time); +} + +/* Get a number item. */ +int +event_payload_get_int(struct event_payload *ep, const char *name, int *value) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_INT) + return (-1); + *value = epi->number; + return (0); +} + +/* Get an unsigned number item. */ +int +event_payload_get_uint(struct event_payload *ep, const char *name, u_int *value) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_UINT) + return (-1); + *value = epi->unsigned_number; + return (0); +} + +/* Get a client item. */ +struct client * +event_payload_get_client(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_CLIENT) + return (NULL); + return (epi->client); +} + +/* Get a session item. */ +struct session * +event_payload_get_session(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_SESSION) + return (NULL); + return (epi->session); +} + +/* Get a window item. */ +struct window * +event_payload_get_window(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_WINDOW) + return (NULL); + return (epi->window); +} + +/* Get a pane item. */ +struct window_pane * +event_payload_get_pane(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_PANE) + return (NULL); + return (epi->pane); +} + +/* Get a pointer item. */ +void * +event_payload_get_pointer(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_POINTER) + return (NULL); + return (epi->pointer.ptr); +} diff --git a/events.c b/events.c new file mode 100644 index 000000000..8d6e3eed1 --- /dev/null +++ b/events.c @@ -0,0 +1,199 @@ +/* $OpenBSD: events.c,v 1.1 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Event sink. */ +struct events_sink { + char *name; + events_cb cb; + void *data; + int dead; + u_int generation; + + TAILQ_ENTRY(events_sink) entry; +}; + +TAILQ_HEAD(events_sinks, events_sink); +static struct events_sinks events_sinks = TAILQ_HEAD_INITIALIZER(events_sinks); + +static u_int events_dispatching; +static u_int events_generation; + +/* Free an event sink. */ +static void +events_free_sink(struct events_sink *es) +{ + TAILQ_REMOVE(&events_sinks, es, entry); + free(es->name); + free(es); +} + +/* Free dead event sinks. */ +static void +events_free_dead(void) +{ + struct events_sink *es, *es1; + + TAILQ_FOREACH_SAFE(es, &events_sinks, entry, es1) { + if (es->dead) + events_free_sink(es); + } +} + +/* Add an event sink. */ +struct events_sink * +events_add_sink(const char *name, events_cb cb, void *data) +{ + struct events_sink *es; + + es = xcalloc(1, sizeof *es); + es->name = xstrdup(name); + es->cb = cb; + es->data = data; + es->generation = ++events_generation; + + TAILQ_INSERT_TAIL(&events_sinks, es, entry); + return (es); +} + +/* Remove an event sink. */ +void +events_remove_sink(struct events_sink *es) +{ + if (es != NULL && !es->dead) { + if (events_dispatching != 0) + es->dead = 1; + else + events_free_sink(es); + } +} + +/* Fire an event. */ +void +events_fire(const char *name, struct event_payload *ep) +{ + struct events_sink *es; + u_int generation = events_generation; + + event_payload_set_string(ep, "event", "%s", name); + + if (log_get_level() != 0) + event_payload_log(ep, "%s: %s: ", __func__, name); + + events_dispatching++; + TAILQ_FOREACH(es, &events_sinks, entry) { + if (es->dead || es->generation > generation) + continue; + if (strcmp(es->name, name) == 0) + es->cb(name, ep, es->data); + } + if (--events_dispatching == 0) + events_free_dead(); + event_payload_free(ep); +} + +/* Fire a client event. */ +void +events_fire_client(const char *name, struct client *c) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_client(&fs, c, 0); + event_payload_set_target(ep, &fs); + event_payload_set_client(ep, "client", c); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.w != NULL) + event_payload_set_window(ep, "window", fs.w); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + else if (fs.idx != -1) + event_payload_set_int(ep, "window_index", fs.idx); + if (fs.wp != NULL) + event_payload_set_pane(ep, "pane", fs.wp); + events_fire(name, ep); +} + +/* Fire a session event. */ +void +events_fire_session(const char *name, struct session *s) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + if (session_alive(s)) { + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + } + event_payload_set_session(ep, "session", s); + events_fire(name, ep); +} + +/* Fire a window event. */ +void +events_fire_window(const char *name, struct window *w) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + events_fire(name, ep); +} + +/* Fire a pane event. */ +void +events_fire_pane(const char *name, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + events_fire(name, ep); +} + +/* Fire a winlink event. */ +void +events_fire_winlink(const char *name, struct winlink *wl) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_winlink(&fs, wl, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + events_fire(name, ep); +} diff --git a/file.c b/file.c index 0e10b37a4..2eed63403 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: file.c,v 1.20 2026/05/17 10:54:01 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/format-draw.c b/format-draw.c index 47bf192e6..ea1da421a 100644 --- a/format-draw.c +++ b/format-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format-draw.c,v 1.33 2026/06/29 17:08:52 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/format.c b/format.c index 88ffaa0b8..be4a5ba23 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format.c,v 1.403 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -42,6 +42,7 @@ struct format_expand_state; static char *format_job_get(struct format_expand_state *, const char *); +static char *format_quote_shell_single(const char *); static char *format_expand1(struct format_expand_state *, const char *); static int format_replace(struct format_expand_state *, const char *, size_t, char **, size_t *, size_t *); @@ -123,6 +124,10 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_CLIENT_ENVIRON 0x4000000 #define FORMAT_COLOUR_ESC_FG 0x8000000 #define FORMAT_COLOUR_ESC_BG 0x10000000 +#define FORMAT_QUOTE_SHELL_SQ 0x20000000 +#define FORMAT_OPTIONS 0x40000000 +#define FORMAT_ENVIRON 0x80000000ULL +#define FORMAT_DIFFERENCE 0x100000000ULL /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -878,6 +883,37 @@ format_cb_start_command(struct format_tree *ft) return (cmd_stringify_argv(wp->argc, wp->argv)); } +/* Callback for pane_start_command_list. */ +static void * +format_cb_start_command_list(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + char *buf = NULL, *s; + size_t len = 0; + int i; + + if (wp == NULL) + return (NULL); + if (wp->argc == 0) + return (xstrdup("")); + + for (i = 0; i < wp->argc; i++) { + s = format_quote_shell_single(wp->argv[i]); + + len += strlen(s) + 1; + buf = xrealloc(buf, len); + + if (i == 0) + *buf = '\0'; + else + strlcat(buf, " ", len); + strlcat(buf, s, len); + + free(s); + } + return (buf); +} + /* Callback for pane_start_path. */ static void * format_cb_start_path(struct format_tree *ft) @@ -2151,6 +2187,106 @@ format_cb_pane_dead_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_last_output_time. */ +static void * +format_cb_pane_last_output_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_output_time != 0) { + tv.tv_sec = wp->last_output_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_last_prompt_time. */ +static void * +format_cb_pane_last_prompt_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_prompt_time != 0) { + tv.tv_sec = wp->last_prompt_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_start_time. */ +static void * +format_cb_pane_command_start_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_start_time != 0) { + tv.tv_sec = wp->cmd_start_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_end_time. */ +static void * +format_cb_pane_command_end_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_end_time != 0) { + tv.tv_sec = wp->cmd_end_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_running. */ +static void * +format_cb_pane_command_running(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%d", !!(wp->flags & PANE_CMDRUNNING))); + return (NULL); +} + +/* Callback for pane_command_duration. */ +static void * +format_cb_pane_command_duration(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + time_t end; + + if (wp == NULL || wp->cmd_start_time == 0) + return (NULL); + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = wp->cmd_end_time; + if (end < wp->cmd_start_time) + end = wp->cmd_start_time; + return (format_printf("%lld", (long long)(end - wp->cmd_start_time))); +} + +/* Callback for pane_command_status. */ +static void * +format_cb_pane_command_status(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL && wp->cmd_status != -1) + return (format_printf("%d", wp->cmd_status)); + return (NULL); +} + /* Callback for pane_format. */ static void * format_cb_pane_format(struct format_tree *ft) @@ -2306,7 +2442,7 @@ format_cb_pane_path(struct format_tree *ft) static void * format_cb_pane_pid(struct format_tree *ft) { - if (ft->wp != NULL) + if (ft->wp != NULL && ft->wp->fd != -1) return (format_printf("%ld", (long)ft->wp->pid)); return (NULL); } @@ -2861,6 +2997,19 @@ format_cb_window_height(struct format_tree *ft) return (NULL); } +/* Callback for window_manual_height. */ +static void * +format_cb_window_manual_height(struct format_tree *ft) +{ + struct window *w = ft->w; + + if (w == NULL) + return (NULL); + if (options_get_number(w->options, "window-size") != WINDOW_SIZE_MANUAL) + return (xstrdup("")); + return (format_printf("%u", w->manual_sy)); +} + /* Callback for window_id. */ static void * format_cb_window_id(struct format_tree *ft) @@ -3041,6 +3190,19 @@ format_cb_window_width(struct format_tree *ft) return (NULL); } +/* Callback for window_manual_width. */ +static void * +format_cb_window_manual_width(struct format_tree *ft) +{ + struct window *w = ft->w; + + if (w == NULL) + return (NULL); + if (options_get_number(w->options, "window-size") != WINDOW_SIZE_MANUAL) + return (xstrdup("")); + return (format_printf("%u", w->manual_sx)); +} + /* Callback for window_zoomed_flag. */ static void * format_cb_window_zoomed_flag(struct format_tree *ft) @@ -3441,6 +3603,21 @@ static const struct format_table_entry format_table[] = { { "pane_bottom", FORMAT_TABLE_STRING, format_cb_pane_bottom }, + { "pane_command_duration", FORMAT_TABLE_STRING, + format_cb_pane_command_duration + }, + { "pane_command_end_time", FORMAT_TABLE_TIME, + format_cb_pane_command_end_time + }, + { "pane_command_running", FORMAT_TABLE_STRING, + format_cb_pane_command_running + }, + { "pane_command_start_time", FORMAT_TABLE_TIME, + format_cb_pane_command_start_time + }, + { "pane_command_status", FORMAT_TABLE_STRING, + format_cb_pane_command_status + }, { "pane_current_command", FORMAT_TABLE_STRING, format_cb_current_command }, @@ -3492,6 +3669,12 @@ static const struct format_table_entry format_table[] = { { "pane_last", FORMAT_TABLE_STRING, format_cb_pane_last }, + { "pane_last_output_time", FORMAT_TABLE_TIME, + format_cb_pane_last_output_time + }, + { "pane_last_prompt_time", FORMAT_TABLE_TIME, + format_cb_pane_last_prompt_time + }, { "pane_left", FORMAT_TABLE_STRING, format_cb_pane_left }, @@ -3531,6 +3714,9 @@ static const struct format_table_entry format_table[] = { { "pane_start_command", FORMAT_TABLE_STRING, format_cb_start_command }, + { "pane_start_command_list", FORMAT_TABLE_STRING, + format_cb_start_command_list + }, { "pane_start_path", FORMAT_TABLE_STRING, format_cb_start_path }, @@ -3747,6 +3933,12 @@ static const struct format_table_entry format_table[] = { { "window_linked_sessions_list", FORMAT_TABLE_STRING, format_cb_window_linked_sessions_list }, + { "window_manual_height", FORMAT_TABLE_STRING, + format_cb_window_manual_height + }, + { "window_manual_width", FORMAT_TABLE_STRING, + format_cb_window_manual_width + }, { "window_marked_flag", FORMAT_TABLE_STRING, format_cb_window_marked_flag }, @@ -4027,6 +4219,29 @@ format_quote_shell(const char *s) return (out); } +/* Quote string with POSIX shell single quotes. */ +static char * +format_quote_shell_single(const char *s) +{ + const char *cp; + char *out, *at; + + at = out = xmalloc(strlen(s) * 4 + 3); + *at++ = '\''; + for (cp = s; *cp != '\0'; cp++) { + if (*cp == '\'') { + *at++ = '\''; + *at++ = '\\'; + *at++ = '\''; + *at++ = '\''; + } else + *at++ = *cp; + } + *at++ = '\''; + *at = '\0'; + return (out); +} + /* Quote #s in string. */ static char * format_quote_style(const char *s) @@ -4128,9 +4343,20 @@ format_relative_time(time_t t) return (xstrdup(out)); } +/* Make a time difference in seconds. */ +static char * +format_time_difference(time_t t) +{ + time_t now = time(NULL); + char *out; + + xasprintf(&out, "%ld", (long)now - (long)t); + return (out); +} + /* Find a format entry. */ static char * -format_find(struct format_tree *ft, const char *key, int modifiers, +format_find(struct format_tree *ft, const char *key, uint64_t modifiers, const char *time_format) { const struct format_table_entry *fte; @@ -4138,25 +4364,26 @@ format_find(struct format_tree *ft, const char *key, int modifiers, struct format_entry *fe, fe_find; struct environ_entry *envent; struct options_entry *o; - int idx; char *found = NULL, *saved, s[512]; + char *array_key = NULL; const char *errstr; time_t t = 0; struct tm tm; - o = options_parse_get(global_options, key, &idx, 0); + o = options_parse_get(global_options, key, &array_key, 0); if (o == NULL && ft->wp != NULL) - o = options_parse_get(ft->wp->options, key, &idx, 0); + o = options_parse_get(ft->wp->options, key, &array_key, 0); if (o == NULL && ft->w != NULL) - o = options_parse_get(ft->w->options, key, &idx, 0); + o = options_parse_get(ft->w->options, key, &array_key, 0); if (o == NULL) - o = options_parse_get(global_w_options, key, &idx, 0); + o = options_parse_get(global_w_options, key, &array_key, 0); if (o == NULL && ft->s != NULL) - o = options_parse_get(ft->s->options, key, &idx, 0); + o = options_parse_get(ft->s->options, key, &array_key, 0); if (o == NULL) - o = options_parse_get(global_s_options, key, &idx, 0); + o = options_parse_get(global_s_options, key, &array_key, 0); if (o != NULL) { - found = options_to_string(o, idx, 1); + found = options_to_string(o, array_key, 1); + free(array_key); goto found; } @@ -4211,6 +4438,8 @@ found: return (NULL); if (modifiers & FORMAT_RELATIVE) found = format_relative_time(t); + else if (modifiers & FORMAT_DIFFERENCE) + found = format_time_difference(t); else if (modifiers & FORMAT_PRETTY) found = format_pretty_time(t, 0); else { @@ -4245,6 +4474,11 @@ found: found = format_quote_shell(saved); free(saved); } + if (modifiers & FORMAT_QUOTE_SHELL_SQ) { + saved = found; + found = format_quote_shell_single(saved); + free(saved); + } if (modifiers & FORMAT_QUOTE_STYLE) { saved = found; found = format_quote_style(saved); @@ -4455,7 +4689,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, /* * Modifiers are a ; separated list of the forms: - * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,R,<,> + * l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,<,> * =a * =/a * =/a/ @@ -4474,7 +4708,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, break; /* Check single character modifiers with no arguments. */ - if (strchr("labdnwETSWPL!<>", cp[0]) != NULL && + if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL && format_is_end(cp[1])) { format_add_modifier(&list, count, cp, 1, NULL, 0); cp++; @@ -4496,7 +4730,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, } /* Now try single character with arguments. */ - if (strchr("ImCLNPSst=pReqWc", cp[0]) == NULL) + if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL) break; c = cp[0]; @@ -4773,11 +5007,14 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt) else use = all; nft = format_create(c, item, FORMAT_NONE, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); + format_defaults(nft, ft->c, s, NULL, NULL); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, use); format_free(next.ft); @@ -4820,9 +5057,9 @@ format_window_name(struct format_expand_state *es, const char *fmt) return (xstrdup("0")); } -/* Add neighbor window variables to the format tree. */ +/* Add neighbour window variables to the format tree. */ static void -format_add_window_neighbor(struct format_tree *nft, struct winlink *wl, +format_add_window_neighbour(struct format_tree *nft, struct winlink *wl, struct session *s, const char *prefix) { struct options_entry *o; @@ -4842,7 +5079,7 @@ format_add_window_neighbor(struct format_tree *nft, struct winlink *wl, oname = options_name(o); if (*oname == '@') { xasprintf(&prefixed, "%s_%s", prefix, oname); - oval = options_to_string(o, -1, 1); + oval = options_to_string(o, NULL, 1); format_add(nft, prefixed, "%s", oval); free(oval); free(prefixed); @@ -4855,20 +5092,21 @@ format_add_window_neighbor(struct format_tree *nft, struct winlink *wl, static char * format_loop_windows(struct format_expand_state *es, const char *fmt) { - struct sort_criteria *sc = &sort_crit; - struct format_tree *ft = es->ft; - struct client *c = ft->client; - struct cmdq_item *item = ft->item; - struct format_tree *nft; - struct format_expand_state next; - char *all, *active, *use, *expanded, *value; - struct evbuffer *buffer; - size_t size; - struct winlink *wl, **l; - struct window *w; - int i, n; + struct sort_criteria *sc = &sort_crit; + struct format_tree *ft = es->ft; + struct client *c = ft->client; + struct session *s = ft->s; + struct cmdq_item *item = ft->item; + struct format_tree *nft; + struct format_expand_state next; + char *all, *active, *use, *expanded, *value; + struct evbuffer *buffer; + size_t size; + struct winlink *wl, **l; + struct window *w; + int i, n; - if (ft->s == NULL) { + if (s == NULL) { format_log(es, "window loop but no session"); return (NULL); } @@ -4882,33 +5120,38 @@ format_loop_windows(struct format_expand_state *es, const char *fmt) if (buffer == NULL) fatalx("out of memory"); - l = sort_get_winlinks_session(ft->s, &n, sc); + l = sort_get_winlinks_session(s, &n, sc); for (i = 0; i < n; i++) { wl = l[i]; w = wl->window; format_log(es, "window loop: %u @%u", wl->idx, w->id); - if (active != NULL && wl == ft->s->curw) + if (active != NULL && wl == s->curw) use = active; else use = all; - nft = format_create(c, item, FORMAT_WINDOW|w->id, - ft->flags); + nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); - format_defaults(nft, ft->c, ft->s, wl, NULL); - /* Add neighbor window data to the format tree. */ - format_add(nft, "window_after_active", "%d", - i > 0 && l[i - 1] == ft->s->curw); - format_add(nft, "window_before_active", "%d", - i + 1 < n && l[i + 1] == ft->s->curw); + /* Add neighbour window data to the format tree. */ + if (i > 0 && l[i - 1] == s->curw) + format_add(nft, "window_after_active", "1"); + else + format_add(nft, "window_after_active", "0"); + if (i + 1 < n && l[i + 1] == s->curw) + format_add(nft, "window_before_active", "1"); + else + format_add(nft, "window_before_active", "0"); if (i + 1 < n) - format_add_window_neighbor(nft, l[i + 1], ft->s, "next"); + format_add_window_neighbour(nft, l[i + 1], s, "next"); if (i > 0) - format_add_window_neighbor(nft, l[i - 1], ft->s, "prev"); + format_add_window_neighbour(nft, l[i - 1], s, "prev"); + format_defaults(nft, ft->c, s, wl, NULL); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, use); format_free(nft); @@ -4965,13 +5208,15 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) use = active; else use = all; - nft = format_create(c, item, FORMAT_PANE|wp->id, - ft->flags); + nft = format_create(c, item, FORMAT_PANE|wp->id, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); + format_defaults(nft, ft->c, ft->s, ft->wl, wp); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, use); format_free(nft); @@ -4990,6 +5235,271 @@ format_loop_panes(struct format_expand_state *es, const char *fmt) return (value); } +/* Add an option to an options loop. */ +static void +format_loop_add_option(struct format_expand_state *es, const char *fmt, + struct evbuffer *buffer, struct options_entry *o, u_int n, u_int i) +{ + struct format_tree *ft = es->ft, *nft; + struct format_expand_state next; + const struct options_table_entry *oe = options_table_entry(o); + const char *name = options_name(o); + char *expanded, *s; + int is_array = options_is_array(o); + + format_log(es, "option loop: %s", name); + nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags); + + format_add(nft, "option_name", "%s", name); + s = options_to_string(o, NULL, 0); + format_add(nft, "option_value", "%s", s); + free(s); + + format_add(nft, "option_is_array", "%d", is_array); + format_add(nft, "option_array_key", "%s", ""); + format_add(nft, "option_array_index", "%s", ""); + format_add(nft, "option_array_first", "%d", is_array); + format_add(nft, "option_array_last", "%d", is_array); + format_add(nft, "option_array_count", "%u", n); + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + format_add(nft, "option_is_hook", "1"); + else + format_add(nft, "option_is_hook", "0"); + format_add(nft, "option_is_user", "%d", oe == NULL); + + if (options_next(o) == NULL) + format_add(nft, "loop_last_flag", "1"); + else + format_add(nft, "loop_last_flag", "0"); + format_add(nft, "loop_index", "%u", i); + + format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp); + format_copy_state(&next, es, 0); + next.ft = nft; + + expanded = format_expand1(&next, fmt); + format_free(nft); + evbuffer_add(buffer, expanded, strlen(expanded)); + free(expanded); +} + +/* Add an array option item to an options loop. */ +static void +format_loop_add_array_item(struct format_expand_state *es, const char *fmt, + struct evbuffer *buffer, struct options_entry *o, + struct options_array_item *a, int n, u_int i) +{ + struct format_tree *ft = es->ft, *nft; + struct format_expand_state next; + const struct options_table_entry *oe = options_table_entry(o); + const char *name = options_name(o); + const char *array_key; + char *expanded, *s; + + array_key = options_array_item_key(a); + format_log(es, "option loop: %s[%s]", name, array_key); + nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags); + + format_add(nft, "option_name", "%s", name); + s = options_to_string(o, array_key, 0); + format_add(nft, "option_value", "%s", s); + free(s); + + format_add(nft, "option_is_array", "1"); + format_add(nft, "option_array_key", "%s", array_key); + format_add(nft, "option_array_index", "%s", array_key); + if (a == options_array_first(o)) + format_add(nft, "option_array_first", "1"); + else + format_add(nft, "option_array_first", "0"); + if (options_array_next(a) == NULL) + format_add(nft, "option_array_last", "1"); + else + format_add(nft, "option_array_last", "0"); + format_add(nft, "option_array_count", "%u", n); + + if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) + format_add(nft, "option_is_hook", "1"); + else + format_add(nft, "option_is_hook", "0"); + format_add(nft, "option_is_user", "%d", oe == NULL); + + if (options_array_next(a) == NULL && options_next(o) == NULL) + format_add(nft, "loop_last_flag", "1"); + else + format_add(nft, "loop_last_flag", "0"); + format_add(nft, "loop_index", "%u", i); + + format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp); + format_copy_state(&next, es, 0); + next.ft = nft; + + expanded = format_expand1(&next, fmt); + format_free(nft); + evbuffer_add(buffer, expanded, strlen(expanded)); + free(expanded); +} + +/* Loop over options. */ +static char * +format_loop_options(struct format_expand_state *es, const char *fmt, + const char *flags) +{ + struct format_tree *ft = es->ft; + struct options *oo = NULL; + struct options_entry *o; + struct options_array_item *a; + char *value; + struct evbuffer *buffer; + size_t size; + u_int i = 0, n; + int global = 0; + + if (flags == NULL || *flags == '\0') + flags = "s"; + if (strchr(flags, 'v') != NULL) + oo = global_options; + else { + if (strchr(flags, 'g') != NULL) + global = 1; + if (strchr(flags, 'w') != NULL) { + if (global) + oo = global_w_options; + else if (ft->w != NULL) + oo = ft->w->options; + } else if (strchr(flags, 's') != NULL) { + if (global) + oo = global_s_options; + else if (ft->s != NULL) + oo = ft->s->options; + } else if (strchr(flags, 'p') != NULL) { + if (global) + /* invalid */; + else if (ft->wp != NULL) + oo = ft->wp->options; + } else if (global) + oo = global_s_options; + } + if (oo == NULL) + return (xstrdup("")); + + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); + + o = options_first(oo); + while (o != NULL) { + + n = 0; + if (options_is_array(o)) { + a = options_array_first(o); + while (a != NULL) { + n++; + a = options_array_next(a); + } + } + + if (!options_is_array(o) || n == 0) { + format_loop_add_option(es, fmt, buffer, o, n, i); + i++; + o = options_next(o); + continue; + } + + a = options_array_first(o); + while (a != NULL) { + format_loop_add_array_item(es, fmt, buffer, o, a, n, i); + i++; + a = options_array_next(a); + } + o = options_next(o); + } + + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); + return (value); +} + +/* Loop over an environment. */ +static char * +format_loop_environ(struct format_expand_state *es, const char *fmt, + const char *flags) +{ + struct format_tree *ft = es->ft, *nft; + struct client *c = ft->client; + struct cmdq_item *item = ft->item; + struct format_expand_state next; + struct environ *env = NULL; + struct environ_entry *envent; + char *expanded, *value; + struct evbuffer *buffer; + size_t size; + u_int i = 0; + + if (flags == NULL || *flags == '\0' || strcmp(flags, "s") == 0) { + if (ft->s != NULL) + env = ft->s->environ; + } else if (strcmp(flags, "g") == 0) + env = global_environ; + else if (strcmp(flags, "c") == 0) { + if (ft->client != NULL) + env = ft->client->environ; + } + if (env == NULL) + return (xstrdup("")); + + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); + + envent = environ_first(env); + while (envent != NULL) { + format_log(es, "environment loop: %s", envent->name); + nft = format_create(c, item, FORMAT_NONE, ft->flags); + + format_add(nft, "environ_name", "%s", envent->name); + if (envent->value == NULL) + format_add(nft, "environ_value", "%s", ""); + else + format_add(nft, "environ_value", "%s", envent->value); + + if (envent->flags & ENVIRON_HIDDEN) + format_add(nft, "environ_hidden", "1"); + else + format_add(nft, "environ_hidden", "0"); + format_add(nft, "environ_removed", "%d", envent->value == NULL); + + if (environ_next(envent) == NULL) + format_add(nft, "loop_last_flag", "1"); + else + format_add(nft, "loop_last_flag", "0"); + format_add(nft, "loop_index", "%u", i); + + format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp); + format_copy_state(&next, es, 0); + next.ft = nft; + + expanded = format_expand1(&next, fmt); + format_free(nft); + evbuffer_add(buffer, expanded, strlen(expanded)); + free(expanded); + + i++; + envent = environ_next(envent); + } + + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); + return (value); +} + /* Loop over clients. */ static char * format_loop_clients(struct format_expand_state *es, const char *fmt) @@ -5014,14 +5524,16 @@ format_loop_clients(struct format_expand_state *es, const char *fmt) c = l[i]; format_log(es, "client loop: %s", c->name); nft = format_create(c, item, 0, ft->flags); + format_add(nft, "loop_index", "%d", i); format_add(nft, "loop_last_flag", "%d", i == n - 1); + format_defaults(nft, c, ft->s, ft->wl, ft->wp); format_copy_state(&next, es, 0); next.ft = nft; + expanded = format_expand1(&next, fmt); format_free(nft); - evbuffer_add(buffer, expanded, strlen(expanded)); free(expanded); } @@ -5191,12 +5703,14 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, char *copy0, *condition, *found, *new; char *value, *left, *right; size_t valuelen; - int modifiers = 0, limit = 0, width = 0; + uint64_t modifiers = 0; + int limit = 0, width = 0; int j, c; struct format_modifier *list, *cmp = NULL, *search = NULL; struct format_modifier **sub = NULL, *mexp = NULL, *fm; struct format_modifier *bool_op_n = NULL; u_int i, count, nsub = 0, nrep, check = 0; + const char *loop_flags = ""; struct format_expand_state next; struct environ_entry *envent; @@ -5305,6 +5819,8 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, modifiers |= FORMAT_PRETTY; else if (strchr(fm->argv[0], 'r') != NULL) modifiers |= FORMAT_RELATIVE; + else if (strchr(fm->argv[0], 'd') != NULL) + modifiers |= FORMAT_DIFFERENCE; else if (fm->argc >= 2 && strchr(fm->argv[0], 'f') != NULL) { free(time_format); @@ -5315,6 +5831,8 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, case 'q': if (fm->argc < 1) modifiers |= FORMAT_QUOTE_SHELL; + else if (strchr(fm->argv[0], 's') != NULL) + modifiers |= FORMAT_QUOTE_SHELL_SQ; else if (strchr(fm->argv[0], 'e') != NULL || strchr(fm->argv[0], 'h') != NULL) modifiers |= FORMAT_QUOTE_STYLE; @@ -5381,11 +5899,27 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, sc->reversed = 0; break; } + if (strchr(fm->argv[0], 'i') != NULL) + sc->order = SORT_INDEX; + else if (strchr(fm->argv[0], 'z') != NULL) + sc->order = SORT_Z; + else + sc->order = SORT_CREATION; if (strchr(fm->argv[0], 'r') != NULL) sc->reversed = 1; else sc->reversed = 0; break; + case 'O': + modifiers |= FORMAT_OPTIONS; + if (fm->argc == 1) + loop_flags = fm->argv[0]; + break; + case 'V': + modifiers |= FORMAT_ENVIRON; + if (fm->argc == 1) + loop_flags = fm->argv[0]; + break; case 'L': modifiers |= FORMAT_CLIENTS; if (fm->argc < 1) { @@ -5521,6 +6055,14 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, value = format_loop_clients(es, copy); if (value == NULL) goto fail; + } else if (modifiers & FORMAT_OPTIONS) { + value = format_loop_options(es, copy, loop_flags); + if (value == NULL) + goto fail; + } else if (modifiers & FORMAT_ENVIRON) { + value = format_loop_environ(es, copy, loop_flags); + if (value == NULL) + goto fail; } else if (modifiers & FORMAT_WINDOW_NAME) { value = format_window_name(es, copy); if (value == NULL) diff --git a/fuzzy.c b/fuzzy.c index 00fc90687..83d34715c 100644 --- a/fuzzy.c +++ b/fuzzy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: fuzzy.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/grid-reader.c b/grid-reader.c index 421c25181..9b40def6c 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee diff --git a/grid-view.c b/grid-view.c index 655ec9e46..86317526f 100644 --- a/grid-view.c +++ b/grid-view.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/grid.c b/grid.c index bc7abd4ed..d753e03f1 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid.c,v 1.153 2026/07/02 08:51:05 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -60,21 +60,6 @@ static const struct grid_cell_entry grid_cleared_entry = { }; #ifdef __APPLE__ -static void -grid_check_lines(struct grid *gd) -{ - u_int i, j; - - for (i = 0; i < gd->hsize + gd->sy; i++) { - for (j = i + 1; j < gd->hsize + gd->sy; j++) { - if (gd->linedata[i].celldata != NULL) - assert(gd->linedata[i].celldata != gd->linedata[j].celldata); - if (gd->linedata[i].extddata != NULL) - assert(gd->linedata[i].extddata != gd->linedata[j].extddata); - } - } -} - void grid_check_is_clear(struct grid *gd) { @@ -104,11 +89,6 @@ grid_check_is_clear(struct grid *gd) } } #else -static void -grid_check_lines(__unused struct grid *gd) -{ -} - void grid_check_is_clear(__unused struct grid *gd) { @@ -386,6 +366,9 @@ grid_create(u_int sx, u_int sy, u_int hlimit) if (gd->sy != 0) gd->linedata = xcalloc(gd->sy, sizeof *gd->linedata); +#ifdef __APPLE__ + assert(gd->hsize == 0); +#endif grid_check_is_clear(gd); return (gd); } @@ -506,8 +489,6 @@ grid_scroll_history(struct grid *gd, u_int bg) gd->linedata[gd->hsize].time = current_time; gd->hsize++; gd->scroll_added++; - - grid_check_lines(gd); } /* Clear the history. */ @@ -557,8 +538,6 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg) gd->hscrolled++; gd->hsize++; gd->scroll_added++; - - grid_check_lines(gd); } /* Expand line to fit to cell. */ @@ -820,8 +799,6 @@ grid_move_lines(struct grid *gd, u_int dy, u_int py, u_int ny, u_int bg) } if (py != 0 && (py < dy || py >= dy + ny)) gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED; - - grid_check_lines(gd); } /* Move a group of cells. */ @@ -1311,8 +1288,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, sy++; dy++; } - - grid_check_lines(dst); } /* Mark line as dead. */ @@ -1609,8 +1584,6 @@ grid_reflow(struct grid *gd, u_int sx) gd->linedata = target->linedata; free(target); gd->scroll_generation++; - - grid_check_lines(gd); } /* Convert to position based on wrapped lines. */ diff --git a/hooks.c b/hooks.c new file mode 100644 index 000000000..c0d0d5018 --- /dev/null +++ b/hooks.c @@ -0,0 +1,458 @@ +/* $OpenBSD: hooks.c,v 1.13 2026/07/10 15:20:06 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2012 George Nachman + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Hook monitor state owned by an option entry. */ +struct hook_monitor { + struct options *oo; + + struct monitor_set *set; + struct events_sink *sink; + struct cmd_find_state fs; + + enum monitor_type type; + int id; + char *format; +}; + +/* Hook command data built from an event payload. */ +struct hooks_data { + const char *name; + struct cmd_find_state fs; + struct format_tree *formats; + struct options *oo; + struct client *client; + int expand; +}; + +/* Hook event sink registered for a notify event name. */ +struct hooks_event { + char *name; + struct events_sink *sink; + TAILQ_ENTRY(hooks_event) entry; +}; +TAILQ_HEAD(hooks_events, hooks_event); +static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events); + +/* Insert one hook command list. */ +static struct cmdq_item * +hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd, + struct cmd_parse_tree *tree, struct cmdq_state *state) +{ + struct cmdq_item *new_item; + char *s; + + if (tree == NULL) + return (item); + if (log_get_level() != 0) { + s = cmd_parse_print(tree, 0); + log_debug("%s: hook %s is: %s", __func__, hd->name, s); + free(s); + } + new_item = cmd_invoke_get(tree, state, NULL); + if (item != NULL) + return (cmdq_insert_after(item, new_item)); + return (cmdq_append(NULL, new_item)); +} + +/* Parse a hook command. */ +static struct cmd_parse_tree * +hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs, + const char *value, char **cause) +{ + struct cmd_parse_tree *tree; + struct format_tree *ft; + char *expanded; + + if (!hd->expand) + return (cmd_parse_from_string(value, NULL, cause)); + + ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp); + if (hd->formats != NULL) + format_merge(ft, hd->formats); + expanded = format_expand(ft, value); + format_free(ft); + + tree = cmd_parse_from_string(expanded, NULL, cause); + free(expanded); + return (tree); +} + +/* Insert commands for a hook. */ +static void +hooks_insert(struct cmdq_item *item, struct hooks_data *hd) +{ + struct cmd_find_state fs; + struct options *oo; + struct cmdq_state *state; + struct options_entry *o; + struct options_array_item *a; + struct cmd_parse_tree *tree; + const char *value; + char *cause = NULL; + + log_debug("%s: inserting hook %s", __func__, hd->name); + + cmd_find_clear_state(&fs, 0); + if (cmd_find_empty_state(&hd->fs) || !cmd_find_valid_state(&hd->fs)) + cmd_find_from_nothing(&fs, 0); + else + cmd_find_copy_state(&fs, &hd->fs); + + if (hd->oo != NULL) { + oo = hd->oo; + o = options_get_only(oo, hd->name); + } else { + if (fs.s == NULL) + oo = global_s_options; + else + oo = fs.s->options; + o = options_get(oo, hd->name); + if (o == NULL && fs.wp != NULL) { + oo = fs.wp->options; + o = options_get(oo, hd->name); + } + if (o == NULL && fs.wl != NULL) { + oo = fs.wl->window->options; + o = options_get(oo, hd->name); + } + } + if (o == NULL) { + log_debug("%s: hook %s not found", __func__, hd->name); + return; + } + + if (item == NULL) + state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); + else { + state = cmdq_new_state(&fs, cmdq_get_event(item), + CMDQ_STATE_NOHOOKS); + } + cmdq_add_formats(state, hd->formats); + + if (*hd->name == '@') { + value = options_get_string(oo, hd->name); + cause = NULL; + tree = hooks_parse(hd, &fs, value, &cause); + if (tree == NULL) { + log_debug("%s: can't parse hook %s: %s", __func__, + hd->name, cause); + free(cause); + } else { + hooks_insert_one(item, hd, tree, state); + cmd_parse_free(tree); + } + } else { + a = options_array_first(o); + while (a != NULL) { + if (hd->expand) { + value = options_array_item_value(a)->string; + cause = NULL; + tree = hooks_parse(hd, &fs, value, &cause); + if (tree == NULL) { + if (cause != NULL) + cmdq_error(item, "%s", cause); + free(cause); + } else { + item = hooks_insert_one(item, hd, tree, + state); + cmd_parse_free(tree); + } + } else { + tree = options_array_item_value(a)->cmd; + item = hooks_insert_one(item, hd, tree, + state); + } + a = options_array_next(a); + } + } + + cmdq_free_state(state); +} + +/* Insert commands for a hook event. */ +static void +hooks_insert_event(struct cmdq_item *item, const char *name, + struct event_payload *ep, struct options *oo, int expand) +{ + struct hooks_data hd; + struct format_tree *ft; + struct client *c; + + if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + return; + + c = event_payload_get_client(ep, "client"); + ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, "hook_"); + format_add(ft, "hook", "%s", name); + format_log_debug(ft, __func__); + + memset(&hd, 0, sizeof hd); + hd.name = name; + cmd_find_clear_state(&hd.fs, 0); + event_payload_get_target(ep, &hd.fs); + hd.formats = ft; + hd.oo = oo; + hd.client = c; + hd.expand = expand; + + hooks_insert(item, &hd); + format_free(ft); +} + +/* Handle an event for hooks. */ +static void +hooks_event_cb(const char *name, struct event_payload *ep, + __unused void *sink_data) +{ + struct cmdq_item *item; + + if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) + return; + + item = event_payload_get_pointer(ep, "_cmdq_item"); + if (item != NULL) { + hooks_insert_event(item, name, ep, NULL, 0); + return; + } + + item = cmdq_running(NULL); + if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + hooks_insert_event(NULL, name, ep, NULL, 0); +} + +/* Add a hook event sink. */ +void +hooks_add_event(const char *name) +{ + struct hooks_event *he; + + TAILQ_FOREACH(he, &hooks_events, entry) { + if (strcmp(he->name, name) == 0) + return; + } + + he = xcalloc(1, sizeof *he); + he->name = xstrdup(name); + he->sink = events_add_sink(name, hooks_event_cb, NULL); + TAILQ_INSERT_TAIL(&hooks_events, he, entry); +} + +/* Return if an event name can be fired through the hooks path. */ +int +hooks_valid_event_name(const char *name) +{ + const struct options_table_entry *oe; + + if (*name == '@') + return (1); + oe = options_search(name); + return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); +} + +/* Add hook event sinks for all built-in hooks. */ +void +hooks_build_events(void) +{ + const struct options_table_entry *oe; + + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->flags & OPTIONS_TABLE_IS_HOOK) + hooks_add_event(oe->name); + } +} + +/* Run a hook immediately. */ +void +hooks_run(struct cmdq_item *item, const char *name) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct hooks_data hd = { 0 }; + + hd.name = name; + cmd_find_copy_state(&hd.fs, target); + hd.client = cmdq_get_client(item); + + hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); + format_add(hd.formats, "hook", "%s", name); + format_log_debug(hd.formats, __func__); + + hooks_insert(item, &hd); + format_free(hd.formats); +} + +/* Free a hook monitor. */ +void +hooks_monitor_free(void *data) +{ + struct hook_monitor *hm = data; + + events_remove_sink(hm->sink); + monitor_destroy(hm->set); + free(hm->format); + free(hm); +} + +/* Remove a hook monitor. */ +void +hooks_monitor_remove(struct options *oo, const char *name) +{ + struct options_entry *o; + struct hook_monitor *hm; + + o = options_get_only(oo, name); + if (o == NULL) + return; + + hm = options_get_monitor_data(o); + if (hm != NULL) { + options_set_monitor_data(o, NULL); + hooks_monitor_free(hm); + } +} + +/* Handle a hook monitor event. */ +static void +hooks_monitor_hook_cb(const char *name, struct event_payload *ep, + void *sink_data) +{ + struct hook_monitor *hm = sink_data; + + if (event_payload_get_pointer(ep, "_hook_monitor") == hm) + hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); +} + +/* Fire a hook monitor event. */ +static void +hooks_monitor_cb(struct monitor_change *change, void *data) +{ + struct hook_monitor *hm = data; + struct event_payload *ep; + struct winlink *wl = change->wl; + struct window_pane *wp = change->wp; + struct cmd_find_state fs; + + ep = event_payload_create(); + event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); + + cmd_find_clear_state(&fs, 0); + if (wl != NULL && wp != NULL && wp->window == wl->window) + cmd_find_from_winlink_pane(&fs, wl, wp, 0); + else if (wl != NULL) + cmd_find_from_winlink(&fs, wl, 0); + else if (wp != NULL) + cmd_find_from_pane(&fs, wp, 0); + else if (change->s != NULL) + cmd_find_from_session(&fs, change->s, 0); + else + cmd_find_copy_state(&fs, &hm->fs); + event_payload_set_target(ep, &fs); + + if (change->value != NULL) + event_payload_set_string(ep, "value", "%s", change->value); + else + event_payload_set_string(ep, "value", "%s", ""); + if (change->last != NULL) + event_payload_set_string(ep, "last", "%s", change->last); + else + event_payload_set_string(ep, "last", "%s", ""); + + if (change->c != NULL) + event_payload_set_client(ep, "client", change->c); + if (change->s != NULL) + event_payload_set_session(ep, "session", change->s); + if (wl != NULL) { + if (change->s == NULL) + event_payload_set_session(ep, "session", + wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + } + if (wp != NULL) { + event_payload_set_pane(ep, "pane", wp); + if (wl == NULL) + event_payload_set_window(ep, "window", wp->window); + } + + events_fire(change->name, ep); +} + +/* Add a hook monitor. */ +void +hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, + const char *name, enum monitor_type type, int id, const char *format, + int flags, struct cmd_find_state *fs, struct session *s) +{ + struct options_entry *o; + struct hook_monitor *hm; + + hooks_monitor_remove(oo, name); + o = options_get_only(oo, name); + if (o == NULL) + o = options_set_string(oo, name, 0, "%s", ""); + + hm = xcalloc(1, sizeof *hm); + hm->oo = oo; + cmd_find_copy_state(&hm->fs, fs); + hm->type = type; + hm->id = id; + hm->format = xstrdup(format); + hm->set = monitor_create_session(s, hooks_monitor_cb, hm); + hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); + options_set_monitor_data(o, hm); + monitor_add(hm->set, name, type, id, format, flags); +} + +/* Convert a hook monitor to its value. */ +char * +hooks_monitor_to_string(struct options_entry *o) +{ + struct hook_monitor *hm = options_get_monitor_data(o); + const char *name = options_name(o); + char *s; + + if (hm == NULL) + return (NULL); + + switch (hm->type) { + case MONITOR_SESSION: + xasprintf(&s, "%s::%s", name, hm->format); + break; + case MONITOR_PANE: + xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_PANES: + xasprintf(&s, "%s:%%*:%s", name, hm->format); + break; + case MONITOR_WINDOW: + xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_WINDOWS: + xasprintf(&s, "%s:@*:%s", name, hm->format); + break; + } + return (s); +} diff --git a/hyperlinks.c b/hyperlinks.c index 1ea55f3b1..27af5c4f0 100644 --- a/hyperlinks.c +++ b/hyperlinks.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: hyperlinks.c,v 1.5 2026/06/29 16:44:06 nicm Exp $ */ /* * Copyright (c) 2021 Will diff --git a/input-keys.c b/input-keys.c index 39692232c..4cade4e53 100644 --- a/input-keys.c +++ b/input-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input-keys.c,v 1.114 2026/06/15 21:47:01 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/input.c b/input.c index 5f7f9d132..0ccef3b11 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input.c,v 1.266 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -174,6 +174,8 @@ static void input_osc_110(struct input_ctx *, const char *); static void input_osc_111(struct input_ctx *, const char *); static void input_osc_112(struct input_ctx *, const char *); static void input_osc_133(struct input_ctx *, const char *); +static void input_fire_pane_title_changed(struct window_pane *, + const char *); /* Transition entry/exit handlers. */ static void input_clear(struct input_ctx *); @@ -210,6 +212,21 @@ static int input_end_bel(struct input_ctx *); /* Command table comparison function. */ static int input_table_compare(const void *, const void *); +static void +input_fire_pane_title_changed(struct window_pane *wp, const char *title) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); +} + /* Command table entry. */ struct input_table_entry { int ch; @@ -1017,6 +1034,8 @@ input_parse_pane(struct window_pane *wp) size_t new_size; new_data = window_pane_get_new_data(wp, &wp->offset, &new_size); + if (new_size != 0) + wp->last_output_time = time(NULL); input_parse_buffer(wp, new_data, new_size); window_pane_update_used_data(wp, &wp->offset, new_size); } @@ -2180,7 +2199,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - notify_pane("pane-title-changed", wp); + events_fire_pane("pane-title-changed", wp); server_redraw_window_borders(w); server_status_window(w); break; @@ -2700,7 +2719,7 @@ input_exit_osc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, p, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, p); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2778,7 +2797,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, ictx->input_buf, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, ictx->input_buf); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2818,7 +2837,7 @@ input_exit_rename(struct input_ctx *ictx) if (ictx->input_len == 0) { o = options_get_only(w->options, "automatic-rename"); if (o != NULL) - options_remove_or_default(o, -1, NULL); + options_remove_or_default(o, NULL, NULL); if (!options_get_number(w->options, "automatic-rename")) window_set_name(w, "", 1); } else { @@ -3146,7 +3165,8 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 1, 12, 0, c, ictx->input_end); + input_osc_colour_reply(ictx, 1, 12, 0, c, + ictx->input_end); } return; } @@ -3166,24 +3186,92 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Fire an OSC 133 command event. */ +static void +input_fire_command_event(struct window_pane *wp, const char *name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + time_t tstart = wp->cmd_start_time, end; + time_t tend = wp->cmd_end_time; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_pane(ep, "pane", wp); + + if (wp->cmd_status != -1) + event_payload_set_int(ep, "command_status", wp->cmd_status); + if (tstart != 0) + event_payload_set_time(ep, "command_start_time", tstart); + if (tend != 0) + event_payload_set_time(ep, "command_end_time", tend); + + if (tstart != 0) { + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = tend; + if (end < tstart) + end = tstart; + end -= tstart; + event_payload_set_uint(ep, "command_duration", end); + } + + events_fire(name, ep); +} + /* Handle the OSC 133 sequence. */ static void input_osc_133(struct input_ctx *ictx, const char *p) { + struct window_pane *wp = ictx->wp; struct grid *gd = ictx->ctx.s->grid; u_int line = ictx->ctx.s->cy + gd->hsize; - struct grid_line *gl; + struct grid_line *gl = NULL; + const char *errstr; + int status; - if (line > gd->hsize + gd->sy - 1) - return; - gl = grid_get_line(gd, line); + if (line <= gd->hsize + gd->sy - 1) + gl = grid_get_line(gd, line); switch (*p) { case 'A': - gl->flags |= GRID_LINE_START_PROMPT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_PROMPT; + if (wp != NULL) { + wp->last_prompt_time = time(NULL); + events_fire_pane("pane-shell-prompt", wp); + } break; case 'C': - gl->flags |= GRID_LINE_START_OUTPUT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_OUTPUT; + if (wp != NULL) { + wp->cmd_start_time = time(NULL); + wp->cmd_end_time = 0; + wp->flags |= PANE_CMDRUNNING; + wp->cmd_status = -1; + input_fire_command_event(wp, "pane-command-started"); + } + break; + case 'D': + if (wp != NULL) { + wp->cmd_end_time = time(NULL); + wp->flags &= ~PANE_CMDRUNNING; + wp->cmd_status = -1; + if (p[1] == ';' && p[2] != '\0') { + status = strtonum(p + 2, 0, INT_MAX, &errstr); + if (errstr == NULL) + wp->cmd_status = status; + } + input_fire_command_event(wp, "pane-command-finished"); + } break; } } @@ -3289,7 +3377,7 @@ input_osc_52(struct input_ctx *ictx, const char *p) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, clip, out, outlen); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); paste_add(NULL, out, outlen); } } diff --git a/job.c b/job.c index 72dbec95c..d665ede9f 100644 --- a/job.c +++ b/job.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: job.c,v 1.75 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -147,7 +147,7 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, else if (chdir("/") == 0) environ_set(env, "PWD", 0, "/"); else - fatal("chdir failed"); + _exit(1); } environ_push(env); @@ -155,21 +155,21 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, if (~flags & JOB_PTY) { if (dup2(out[1], STDIN_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDIN_FILENO; if (dup2(out[1], STDOUT_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDOUT_FILENO; if (flags & JOB_SHOWSTDERR) { if (dup2(out[1], STDERR_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDERR_FILENO; } else { nullfd = open(_PATH_DEVNULL, O_RDWR); if (nullfd == -1) - fatal("open failed"); + _exit(1); if (dup2(nullfd, STDERR_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); if (nullfd != STDERR_FILENO) close(nullfd); } @@ -183,11 +183,11 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, if (flags & JOB_DEFAULTSHELL) setenv("SHELL", shell, 1); execl(shell, argv0, "-c", cmd, (char *)NULL); - fatal("execl failed"); + _exit(1); } else { argvp = cmd_copy_argv(argc, argv); execvp(argvp[0], argvp); - fatal("execvp failed"); + _exit(1); } } diff --git a/key-bindings.c b/key-bindings.c index 4a9155c22..6b56f7a80 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: key-bindings.c,v 1.186 2026/07/07 12:30:36 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -59,17 +59,45 @@ " '#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}' 'C-h' {copy-mode -q; send-keys -l -- \"#{q:mouse_hyperlink}\"}" \ " '#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}' 'h' {copy-mode -q; set-buffer -- \"#{q:mouse_hyperlink}\"}" \ " ''" \ + " '#{?#{#{pane_floating_flag}},Move,}' '' {display-menu -xL -yL -T '#[align=centre]Move' " DEFAULT_MOVE_MENU " }" \ + " '#{?#{#{pane_floating_flag}},Move & Resize,}' '' {display-menu -xL -yL -T '#[align=centre]Move & Resize' " DEFAULT_MOVE_RESIZE_MENU " }" \ + " '#{?#{#{pane_floating_flag}},Tile,}' 't' { join-pane }" \ + " '#{?#{!:#{pane_floating_flag}},Float,}' 'f' { break-pane -W }" \ " '#{?#{!:#{pane_floating_flag}},Horizontal Split,}' 'h' {split-window -h}" \ " '#{?#{!:#{pane_floating_flag}},Vertical Split,}' 'v' {split-window -v}" \ " ''" \ " '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' 'u' {swap-pane -U}" \ " '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' 'd' {swap-pane -D}" \ - " '#{?#{!:#{pane_floating_flag}},#{?pane_marked_set,,-}Swap Marked,}' 's' {swap-pane}" \ + " '#{?pane_marked_set,,-}Swap Marked' 's' {swap-pane}" \ " ''" \ " 'Kill' 'X' {kill-pane}" \ " 'Respawn' 'R' {respawn-pane -k}" \ " '#{?pane_marked,Unmark,Mark}' 'm' {select-pane -m}" \ " '#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}' 'z' {resize-pane -Z}" +#define DEFAULT_MOVE_MENU \ + " 'Centre' 'c' {move-pane -P centre}" \ + " ''" \ + " 'Top Left' '1' {move-pane -P top-left}" \ + " 'Top Right' '2' {move-pane -P top-right}" \ + " 'Bottom Left' '3' {move-pane -P bottom-left}" \ + " 'Bottom Right' '4' {move-pane -P bottom-right}" \ + " ''" \ + " 'Top' 't' {move-pane -P top-centre}" \ + " 'Bottom' 'b' {move-pane -P bottom-centre}" \ + " 'Left' 'l' {move-pane -P centre-left}" \ + " 'Right' 'r' {move-pane -P centre-right}" +#define DEFAULT_MOVE_RESIZE_MENU \ + " 'Fill' '0' {resize-pane -x100% -y100%; move-pane -P top-left}" \ + " ''" \ + " 'Top Left' '1' {resize-pane -x50% -y50%; move-pane -P top-left}" \ + " 'Top Right' '2' {resize-pane -x50% -y50%; move-pane -P top-right}" \ + " 'Bottom Left' '3' {resize-pane -x50% -y50%; move-pane -P bottom-left}" \ + " 'Bottom Right' '4' {resize-pane -x50% -y50%; move-pane -P bottom-right}" \ + " ''" \ + " 'Top' 't' {resize-pane -x100% -y50%; move-pane -P top-centre}" \ + " 'Bottom' 'b' {resize-pane -x100% -y50%; move-pane -P bottom-centre}" \ + " 'Left' 'l' {resize-pane -x50% -y100%; move-pane -P centre-left}" \ + " 'Right' 'r' {resize-pane -x50% -y100%; move-pane -P centre-right}" static int key_bindings_cmp(struct key_binding *, struct key_binding *); RB_GENERATE_STATIC(key_bindings, key_binding, entry, key_bindings_cmp); @@ -358,13 +386,14 @@ key_bindings_init(void) "bind -N 'Rename current session' '$' { command-prompt -I'#S' { rename-session -- '%%' } }", "bind -N 'Split window horizontally' % { split-window -h }", "bind -N 'Kill current window' & { confirm-before -p\"kill-window #W? (y/n)\" kill-window }", - "bind -N 'Prompt for window index to select' \"'\" { command-prompt -T window-target -pindex { select-window -t ':%%' } }", + "bind -N 'Prompt for window index to select' \"'\" { command-prompt -pindex { select-window -t ':%%' } }", "bind -N 'New floating pane' * { new-pane }", + "bind -N 'Toggle pane between floating and tiled' @ { if -F '#{pane_floating_flag}' { join-pane } { break-pane -W } }", "bind -N 'Switch to previous client' ( { switch-client -p }", "bind -N 'Switch to next client' ) { switch-client -n }", "bind -N 'Rename current window' , { command-prompt -I'#W' { rename-window -- '%%' } }", "bind -N 'Delete the most recent paste buffer' - { delete-buffer }", - "bind -N 'Move the current window' . { command-prompt -T target { move-window -t '%%' } }", + "bind -N 'Move the current window' . { command-prompt { move-window -t '%%' } }", "bind -N 'Describe key binding' '/' { command-prompt -kpkey { list-keys -1N '%%' } }", "bind -N 'Select window 0' 0 { select-window -t:=0 }", "bind -N 'Select window 1' 1 { select-window -t:=1 }", @@ -405,10 +434,8 @@ key_bindings_init(void) "bind -N 'Choose a window from a list' w { choose-tree -Zw }", "bind -N 'Kill the active pane' x { confirm-before -p\"kill-pane #P? (y/n)\" kill-pane }", "bind -N 'Zoom the active pane' z { resize-pane -Z }", - "bind -N 'Move pane to top-left corner' '{' { resize-pane -x50% -y50%; move-pane -P top-left }", - "bind -N 'Move pane to top-right corner' '}' { resize-pane -x50% -y50%; move-pane -P top-right }", - "bind -N 'Move pane to bottom-left corner' 'M-{' { resize-pane -x50% -y50%; move-pane -P bottom-left }", - "bind -N 'Move pane to bottom-right corner' 'M-}' { resize-pane -x50% -y50%; move-pane -P bottom-right }", + "bind -N 'Swap the active pane with the pane above' '{' { swap-pane -U }", + "bind -N 'Swap the active pane with the pane below' '}' { swap-pane -D }", "bind -N 'Show messages' '~' { show-messages }", "bind -N 'Enter copy mode and scroll up' PPage { copy-mode -u }", "bind -N 'Select the pane above the active pane' -r Up { select-pane -U }", @@ -439,6 +466,28 @@ key_bindings_init(void) "bind -N 'Resize the pane left' -r C-Left if -F '#{?floating_pane_flag}' { resizep -R-1 } { resize-pane -L }", "bind -N 'Resize the pane right' -r C-Right { resize-pane -R }", + /* Floating pane movement. */ + "bind -N 'Move a floating pane' g { switch-client -Tmove }", + "bind -Tmove -N 'Move pane to top-left corner' 1 { move-pane -P top-left }", + "bind -Tmove -N 'Move pane to top-right corner' 2 { move-pane -P top-right }", + "bind -Tmove -N 'Move pane to bottom-left corner' 3 { move-pane -P bottom-left }", + "bind -Tmove -N 'Move pane to bottom-right corner' 4 { move-pane -P bottom-right }", + "bind -Tmove -N 'Move pane to top-left corner and resize' M-1 { resize-pane -x50% -y50%; move-pane -P top-left }", + "bind -Tmove -N 'Move pane to top-right corner and resize' M-2 { resize-pane -x50% -y50%; move-pane -P top-right }", + "bind -Tmove -N 'Move pane to bottom-left corner and resize' M-3 { resize-pane -x50% -y50%; move-pane -P bottom-left }", + "bind -Tmove -N 'Move pane to bottom-right corner and resize' M-4 { resize-pane -x50% -y50%; move-pane -P bottom-right }", + "bind -Tmove -N 'Move pane to top' 'Up' { move-pane -P top-centre }", + "bind -Tmove -N 'Move pane to bottom' 'Down' { move-pane -P bottom-centre }", + "bind -Tmove -N 'Move pane to left' 'Left' { move-pane -P centre-left }", + "bind -Tmove -N 'Move pane to right' 'Right' { move-pane -P centre-right }", + "bind -Tmove -N 'Move pane to top and resize' 'M-Up' { resizep -x100% -y50%; move-pane -P top-centre }", + "bind -Tmove -N 'Move pane to bottom and resize' 'M-Down' { resizep -x100% -y50%; move-pane -P bottom-centre }", + "bind -Tmove -N 'Move pane to left and resize' 'M-Left' { resizep -x50% -y100%; move-pane -P centre-left }", + "bind -Tmove -N 'Move pane to right and resize' 'M-Right' { resizep -x50% -y100%; move-pane -P centre-right }", + "bind -Tmove -N 'Move pane to fill the window' 0 { resize-pane -x100% -y100%; move-pane -P top-left }", + "bind -Tmove -N 'Display move menu' , { if -F '#{pane_floating_flag}' { display-menu -xP -yP -T '#[align=centre]Move' " DEFAULT_MOVE_MENU " } }", + "bind -Tmove -N 'Display move and resize menu' . { if -F '#{pane_floating_flag}' { display-menu -xP -yP -T '#[align=centre]Move & Resize' " DEFAULT_MOVE_RESIZE_MENU " } }", + /* Menu keys */ "bind -N 'Display window menu' < { display-menu -xW -yW -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_WINDOW_MENU " }", "bind -N 'Display pane menu' > { display-menu -xP -yP -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " }", @@ -477,6 +526,7 @@ key_bindings_init(void) /* Mouse button 1 down on default pane-border-format */ "bind -n MouseDown1Control9 { display-menu -t= -xM -yM -O -T 'Kill pane #{pane_index}?' 'Yes' 'y' { kill-pane -t= } 'No' 'n' {}}", "bind -n MouseDown1Control8 { resize-pane -Z }", + "bind -n MouseDown1Control7 { if -Ft= '#{pane_floating_flag}' { join-pane } { break-pane -W } }", /* Mouse wheel down on status line. */ "bind -n WheelDownStatus { next-window }", diff --git a/key-string.c b/key-string.c index 3df9da4b9..ac7a1bf89 100644 --- a/key-string.c +++ b/key-string.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: key-string.c,v 1.77 2026/03/31 11:46:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/layout-custom.c b/layout-custom.c index c5a348ac0..9df0448ff 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout-custom.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -101,10 +101,10 @@ layout_append(struct layout_cell *lc, char *buf, size_t len) return (0); if (lc->wp != NULL) { tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d,%u", - lc->sx, lc->sy, lc->xoff, lc->yoff, lc->wp->id); + lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff, lc->wp->id); } else { tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d", - lc->sx, lc->sy, lc->xoff, lc->yoff); + lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); } if (tmplen > (sizeof tmp) - 1) return (-1); @@ -145,24 +145,24 @@ layout_check(struct layout_cell *lc) break; case LAYOUT_LEFTRIGHT: TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->sy != lc->sy) + if (lcchild->g.sy != lc->g.sy) return (0); if (!layout_check(lcchild)) return (0); - n += lcchild->sx + 1; + n += lcchild->g.sx + 1; } - if (n - 1 != lc->sx) + if (n - 1 != lc->g.sx) return (0); break; case LAYOUT_TOPBOTTOM: TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if (lcchild->sx != lc->sx) + if (lcchild->g.sx != lc->g.sx) return (0); if (!layout_check(lcchild)) return (0); - n += lcchild->sy + 1; + n += lcchild->g.sy + 1; } - if (n - 1 != lc->sy) + if (n - 1 != lc->g.sy) return (0); break; } @@ -237,21 +237,21 @@ layout_parse(struct window *w, const char *layout, char **cause) break; case LAYOUT_LEFTRIGHT: TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) { - sy = lcchild->sy + 1; - sx += lcchild->sx + 1; + sy = lcchild->g.sy + 1; + sx += lcchild->g.sx + 1; } break; case LAYOUT_TOPBOTTOM: TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) { - sx = lcchild->sx + 1; - sy += lcchild->sy + 1; + sx = lcchild->g.sx + 1; + sy += lcchild->g.sy + 1; } break; } if (tiled_lc->type != LAYOUT_WINDOWPANE && - (tiled_lc->sx != sx || tiled_lc->sy != sy)) { + (tiled_lc->g.sx != sx || tiled_lc->g.sy != sy)) { layout_print_cell(tiled_lc, __func__, 0); - tiled_lc->sx = sx - 1; tiled_lc->sy = sy - 1; + tiled_lc->g.sx = sx - 1; tiled_lc->g.sy = sy - 1; } /* Check the new layout. */ @@ -262,7 +262,7 @@ layout_parse(struct window *w, const char *layout, char **cause) /* Resize window to the layout size. */ if (sx != 0 && sy != 0) - window_resize(w, tiled_lc->sx, tiled_lc->sy, -1, -1); + window_resize(w, tiled_lc->g.sx, tiled_lc->g.sy, -1, -1); /* Destroy the old layout and swap to the new. */ layout_free_cell(w->layout_root, 0); @@ -286,7 +286,7 @@ layout_parse(struct window *w, const char *layout, char **cause) recalculate_sizes(); layout_print_cell(tiled_lc, __func__, 0); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (0); @@ -358,10 +358,10 @@ layout_construct_cell(struct layout_cell *lcparent, const char **layout) } lc = layout_create_cell(lcparent); - lc->sx = sx; - lc->sy = sy; - lc->xoff = xoff; - lc->yoff = yoff; + lc->g.sx = sx; + lc->g.sy = sy; + lc->g.xoff = xoff; + lc->g.yoff = yoff; return (lc); } diff --git a/layout-set.c b/layout-set.c index cf78585c2..7e944d32f 100644 --- a/layout-set.c +++ b/layout-set.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout-set.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -170,8 +170,8 @@ layout_set_even(struct window *w, enum layout_type type) TAILQ_INSERT_TAIL(&lcroot->cells, lcchild, entry); lcchild->parent = lcroot; if (layout_cell_is_tiled(lcchild)) { - lcchild->sx = w->sx; - lcchild->sy = w->sy; + lcchild->g.sx = w->sx; + lcchild->g.sy = w->sy; } } @@ -182,8 +182,8 @@ layout_set_even(struct window *w, enum layout_type type) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); - notify_window("window-layout-changed", w); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -291,8 +291,8 @@ layout_set_main_h(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); - notify_window("window-layout-changed", w); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -388,8 +388,8 @@ layout_set_main_h_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); - notify_window("window-layout-changed", w); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -485,8 +485,8 @@ layout_set_main_v(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); - notify_window("window-layout-changed", w); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -583,8 +583,8 @@ layout_set_main_v_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); - notify_window("window-layout-changed", w); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -705,7 +705,7 @@ layout_set_tiled(struct window *w) layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lcroot->sx, lcroot->sy, -1, -1); - notify_window("window-layout-changed", w); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } diff --git a/layout.c b/layout.c index 2a605d6cc..177d2f4cf 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout.c,v 1.93 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -56,32 +56,29 @@ static int layout_set_size_check(struct window *, struct layout_cell *, static void layout_resize_child_cells(struct window *, struct layout_cell *); +/* Initializes cell geometry to sentinel values. */ +static void +layout_geometry_init(struct layout_geometry *lg) +{ + lg->sx = UINT_MAX; + lg->sy = UINT_MAX; + lg->xoff = INT_MAX; + lg->yoff = INT_MAX; +} + /* Create a new layout cell. */ struct layout_cell * layout_create_cell(struct layout_cell *lcparent) { struct layout_cell *lc; - lc = xmalloc(sizeof *lc); + lc = xcalloc(1, sizeof *lc); lc->type = LAYOUT_WINDOWPANE; - lc->flags = 0; lc->parent = lcparent; - TAILQ_INIT(&lc->cells); - lc->sx = UINT_MAX; - lc->sy = UINT_MAX; - - lc->xoff = INT_MAX; - lc->yoff = INT_MAX; - - lc->saved_sx = UINT_MAX; - lc->saved_sy = UINT_MAX; - - lc->saved_xoff = INT_MAX; - lc->saved_yoff = INT_MAX; - - lc->wp = NULL; + layout_geometry_init(&lc->g); + layout_geometry_init(&lc->fg); return (lc); } @@ -144,8 +141,8 @@ layout_print_cell(struct layout_cell *lc, const char *hdr, u_int n) break; } log_debug("%s:%*s%p type %s [parent %p] wp=%p [%d,%d %ux%u]", hdr, n, - " ", lc, type, lc->parent, lc->wp, lc->xoff, lc->yoff, lc->sx, - lc->sy); + " ", lc, type, lc->parent, lc->wp, lc->g.xoff, lc->g.yoff, lc->g.sx, + lc->g.sy); switch (lc->type) { case LAYOUT_LEFTRIGHT: case LAYOUT_TOPBOTTOM: @@ -164,10 +161,10 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y) struct layout_cell *lcchild, *last = NULL; TAILQ_FOREACH(lcchild, &lc->cells, entry) { - if ((int)x >= lcchild->xoff && - (int)x < lcchild->xoff + (int)lcchild->sx && - (int)y >= lcchild->yoff && - (int)y < lcchild->yoff + (int)lcchild->sy) { + if ((int)x >= lcchild->g.xoff && + (int)x < lcchild->g.xoff + (int)lcchild->g.sx && + (int)y >= lcchild->g.yoff && + (int)y < lcchild->g.yoff + (int)lcchild->g.sy) { /* Inside the cell - recurse. */ return (layout_search_by_border(lcchild, x, y)); } @@ -179,13 +176,13 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y) switch (lc->type) { case LAYOUT_LEFTRIGHT: - if ((int)x < lcchild->xoff && - (int)x >= last->xoff + (int)last->sx) + if ((int)x < lcchild->g.xoff && + (int)x >= last->g.xoff + (int)last->g.sx) return (last); break; case LAYOUT_TOPBOTTOM: - if ((int)y < lcchild->yoff && - (int)y >= last->yoff + (int)last->sy) + if ((int)y < lcchild->g.yoff && + (int)y >= last->g.yoff + (int)last->g.sy) return (last); break; case LAYOUT_WINDOWPANE: @@ -202,11 +199,11 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y) void layout_set_size(struct layout_cell *lc, u_int sx, u_int sy, int xoff, int yoff) { - lc->sx = sx; - lc->sy = sy; + lc->g.sx = sx; + lc->g.sy = sy; - lc->xoff = xoff; - lc->yoff = yoff; + lc->g.xoff = xoff; + lc->g.yoff = yoff; } /* Make a cell a leaf cell. */ @@ -331,28 +328,28 @@ layout_fix_offsets1(struct layout_cell *lc) int xoff, yoff; if (lc->type == LAYOUT_LEFTRIGHT) { - xoff = lc->xoff; + xoff = lc->g.xoff; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (!layout_cell_is_tiled(lcchild) && !layout_cell_has_tiled_child(lcchild)) continue; - lcchild->xoff = xoff; - lcchild->yoff = lc->yoff; + lcchild->g.xoff = xoff; + lcchild->g.yoff = lc->g.yoff; if (lcchild->type != LAYOUT_WINDOWPANE) layout_fix_offsets1(lcchild); - xoff += lcchild->sx + 1; + xoff += lcchild->g.sx + 1; } } else { - yoff = lc->yoff; + yoff = lc->g.yoff; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (!layout_cell_is_tiled(lcchild) && !layout_cell_has_tiled_child(lcchild)) continue; - lcchild->xoff = lc->xoff; - lcchild->yoff = yoff; + lcchild->g.xoff = lc->g.xoff; + lcchild->g.yoff = yoff; if (lcchild->type != LAYOUT_WINDOWPANE) layout_fix_offsets1(lcchild); - yoff += lcchild->sy + 1; + yoff += lcchild->g.sy + 1; } } } @@ -367,8 +364,8 @@ layout_fix_offsets(struct window *w) if (lc->flags & LAYOUT_CELL_FLOATING) return; - lc->xoff = 0; - lc->yoff = 0; + lc->g.xoff = 0; + lc->g.yoff = 0; layout_fix_offsets1(lc); } @@ -452,16 +449,17 @@ layout_fix_panes(struct window *w, struct window_pane *skip) old_sx = wp->sx; old_sy = wp->sy; - wp->xoff = lc->xoff; - wp->yoff = lc->yoff; - sx = lc->sx; - sy = lc->sy; + wp->xoff = lc->g.xoff; + wp->yoff = lc->g.yoff; + sx = lc->g.sx; + sy = lc->g.sy; if (!window_pane_is_floating(wp) && layout_add_horizontal_border(w, lc, status)) { if (status == PANE_STATUS_TOP) wp->yoff++; - sy--; + if (sy > 1) + sy--; } if (window_pane_scrollbar_reserve(wp)) { @@ -535,14 +533,14 @@ layout_resize_check(struct window *w, struct layout_cell *lc, if (lc->type == LAYOUT_WINDOWPANE) { /* Space available in this cell only. */ if (type == LAYOUT_LEFTRIGHT) { - available = lc->sx; + available = lc->g.sx; if (w->sb == PANE_SCROLLBARS_ALWAYS) minimum = PANE_MINIMUM + sb_style->width + sb_style->pad; else minimum = PANE_MINIMUM; } else { - available = lc->sy; + available = lc->g.sy; if (layout_add_horizontal_border(w, lc, status)) minimum = PANE_MINIMUM + 1; else @@ -580,12 +578,13 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, enum layout_type type, int change) { struct layout_cell *lcchild; + int changed; /* Adjust the cell size. */ if (type == LAYOUT_LEFTRIGHT) - lc->sx += change; + lc->g.sx += change; else - lc->sy += change; + lc->g.sy += change; /* If this is a leaf cell, that is all that is necessary. */ if (type == LAYOUT_WINDOWPANE) @@ -613,6 +612,7 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, * until no further change is possible. */ while (change != 0) { + changed = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (change == 0) break; @@ -622,13 +622,17 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc, if (change > 0) { layout_resize_adjust(w, lcchild, type, 1); change--; + changed = 1; continue; } if (layout_resize_check(w, lcchild, type) > 0) { layout_resize_adjust(w, lcchild, type, -1); change++; + changed = 1; } } + if (!changed) + break; } } @@ -640,9 +644,9 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc, int change; if (type == LAYOUT_LEFTRIGHT) - change = size - lc->sx; + change = size - lc->g.sx; else - change = size - lc->sy; + change = size - lc->g.sy; layout_resize_adjust(w, lc, type, change); } @@ -716,9 +720,9 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, lcother = layout_cell_get_neighbour(lc); if (lcother != NULL) { if (lcparent->type == LAYOUT_LEFTRIGHT) - change = lc->sx + 1; + change = lc->g.sx + 1; else - change = lc->sy + 1; + change = lc->g.sy + 1; layout_resize_adjust(w, lcother, lcparent->type, change); } else layout_remove_tile(w, lcparent); @@ -739,8 +743,8 @@ out: lc->parent = lcparent->parent; if (lc->parent == NULL) { if (layout_cell_is_tiled(lc)) { - lc->xoff = 0; - lc->yoff = 0; + lc->g.xoff = 0; + lc->g.yoff = 0; } *lcroot = lc; } else @@ -791,29 +795,29 @@ layout_resize(struct window *w, u_int sx, u_int sy) */ if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING)) return; - xchange = sx - lc->sx; + xchange = sx - lc->g.sx; xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT); if (xchange < 0 && xchange < -xlimit) xchange = -xlimit; if (xlimit == 0) { - if (sx <= lc->sx) /* lc->sx is minimum possible */ + if (sx <= lc->g.sx) /* lc->g.sx is minimum possible */ xchange = 0; else - xchange = sx - lc->sx; + xchange = sx - lc->g.sx; } if (xchange != 0) layout_resize_adjust(w, lc, LAYOUT_LEFTRIGHT, xchange); /* Adjust vertically in a similar fashion. */ - ychange = sy - lc->sy; + ychange = sy - lc->g.sy; ylimit = layout_resize_check(w, lc, LAYOUT_TOPBOTTOM); if (ychange < 0 && ychange < -ylimit) ychange = -ylimit; if (ylimit == 0) { - if (sy <= lc->sy) /* lc->sy is minimum possible */ + if (sy <= lc->g.sy) /* lc->g.sy is minimum possible */ ychange = 0; else - ychange = sy - lc->sy; + ychange = sy - lc->g.sy; } if (ychange != 0) layout_resize_adjust(w, lc, LAYOUT_TOPBOTTOM, ychange); @@ -844,9 +848,9 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type, /* Work out the size adjustment. */ if (type == LAYOUT_LEFTRIGHT) - size = lc->sx; + size = lc->g.sx; else - size = lc->sy; + size = lc->g.sy; if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) change = size - new_size; else @@ -877,13 +881,13 @@ layout_resize_floating_pane_to(struct window_pane *wp, enum layout_type type, } if (type == LAYOUT_TOPBOTTOM) { - if (lc->sy == size) + if (lc->g.sy == size) return (0); - lc->sy = size; + lc->g.sy = size; } else { - if (lc->sx == size) + if (lc->g.sx == size) return (0); - lc->sx = size; + lc->g.sx = size; } redraw_invalidate_scene(wp->window); return (0); @@ -905,23 +909,23 @@ layout_resize_floating_pane(struct window_pane *wp, enum layout_type type, return (0); if (type == LAYOUT_TOPBOTTOM) { - size = lc->sy + change; + size = lc->g.sy + change; if (size < PANE_MINIMUM || size > PANE_MAXIMUM) { *cause = xstrdup("change is too big or too small"); return (-1); } - lc->sy = size; + lc->g.sy = size; if (opposite) - lc->yoff -= change; + lc->g.yoff -= change; } else { - size = lc->sx + change; + size = lc->g.sx + change; if (size < PANE_MINIMUM || size > PANE_MAXIMUM) { *cause = xstrdup("change is too big or too small"); return (-1); } - lc->sx = size; + lc->g.sx = size; if (opposite) - lc->xoff -= change; + lc->g.xoff -= change; } redraw_invalidate_scene(wp->window); return (0); @@ -953,7 +957,7 @@ layout_resize_layout(struct window *w, struct layout_cell *lc, /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); } /* Resize a single pane within the layout. */ @@ -1087,13 +1091,13 @@ layout_new_pane_size(struct window *w, u_int previous, struct layout_cell *lc, */ min = (PANE_MINIMUM + 1) * (count_left - 1); if (type == LAYOUT_LEFTRIGHT) { - if (lc->sx - available > min) - min = lc->sx - available; - new_size = (lc->sx * size) / previous; + if (lc->g.sx - available > min) + min = lc->g.sx - available; + new_size = (lc->g.sx * size) / previous; } else { - if (lc->sy - available > min) - min = lc->sy - available; - new_size = (lc->sy * size) / previous; + if (lc->g.sy - available > min) + min = lc->g.sy - available; + new_size = (lc->g.sy * size) / previous; } /* Check against the maximum and minimum size. */ @@ -1129,9 +1133,9 @@ layout_set_size_check(struct window *w, struct layout_cell *lc, return (0); if (type == LAYOUT_LEFTRIGHT) - previous = lc->sx; + previous = lc->g.sx; else - previous = lc->sy; + previous = lc->g.sy; idx = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { @@ -1167,32 +1171,32 @@ static void layout_resize_child_cells(struct window *w, struct layout_cell *lc) { struct layout_cell *lcchild; - u_int previous, available, count, idx; + u_int prev, available, count, idx; if (lc->type == LAYOUT_WINDOWPANE) return; /* What is the current size used? */ count = 0; - previous = 0; + prev = 0; TAILQ_FOREACH(lcchild, &lc->cells, entry) { if (!layout_cell_is_tiled(lcchild) && !layout_cell_has_tiled_child(lcchild)) continue; count++; if (lc->type == LAYOUT_LEFTRIGHT) - previous += lcchild->sx; + prev += lcchild->g.sx; else if (lc->type == LAYOUT_TOPBOTTOM) - previous += lcchild->sy; + prev += lcchild->g.sy; } - previous += (count - 1); + prev += (count - 1); /* And how much is available? */ available = 0; if (lc->type == LAYOUT_LEFTRIGHT) - available = lc->sx; + available = lc->g.sx; else if (lc->type == LAYOUT_TOPBOTTOM) - available = lc->sy; + available = lc->g.sy; /* Resize children into the new size. */ idx = 0; @@ -1201,20 +1205,20 @@ layout_resize_child_cells(struct window *w, struct layout_cell *lc) !layout_cell_has_tiled_child(lcchild)) continue; if (lc->type == LAYOUT_TOPBOTTOM) { - lcchild->sx = lc->sx; - lcchild->xoff = lc->xoff; + lcchild->g.sx = lc->g.sx; + lcchild->g.xoff = lc->g.xoff; } else { - lcchild->sx = layout_new_pane_size(w, previous, lcchild, - lc->type, lc->sx, count - idx, available); - available -= (lcchild->sx + 1); + lcchild->g.sx = layout_new_pane_size(w, prev, lcchild, + lc->type, lc->g.sx, count - idx, available); + available -= (lcchild->g.sx + 1); } if (lc->type == LAYOUT_LEFTRIGHT) { - lcchild->sy = lc->sy; - lcchild->yoff = lc->yoff; + lcchild->g.sy = lc->g.sy; + lcchild->g.yoff = lc->g.yoff; } else { - lcchild->sy = layout_new_pane_size(w, previous, lcchild, - lc->type, lc->sy, count - idx, available); - available -= (lcchild->sy + 1); + lcchild->g.sy = layout_new_pane_size(w, prev, lcchild, + lc->type, lc->g.sy, count - idx, available); + available -= (lcchild->g.sy + 1); } layout_resize_child_cells(w, lcchild); idx++; @@ -1234,7 +1238,7 @@ layout_replace_with_node(struct window *w, struct layout_cell *lc, lcparent = layout_create_cell(lc->parent); layout_make_node(lcparent, type); - layout_set_size(lcparent, lc->sx, lc->sy, lc->xoff, lc->yoff); + layout_set_size(lcparent, lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff); if (lc->parent == NULL) w->layout_root = lcparent; else @@ -1253,7 +1257,7 @@ layout_split_check_space(struct window_pane *wp, struct layout_cell *lc, enum layout_type type) { struct style *sb_style = &wp->scrollbar_style; - u_int minimum, sx = lc->sx, sy = lc->sy; + u_int minimum, sx = lc->g.sx, sy = lc->g.sy; int status; if (lc->flags & LAYOUT_CELL_FLOATING) @@ -1292,7 +1296,7 @@ layout_split_sizes(struct layout_cell *lc, int size, int before, enum layout_type type, u_int *size1, u_int *size2, u_int *saved_size) { u_int s1, s2, ss; - u_int sx = lc->sx, sy = lc->sy; + u_int sx = lc->g.sx, sy = lc->g.sy; if (type == LAYOUT_LEFTRIGHT) ss = sx; @@ -1306,7 +1310,7 @@ layout_split_sizes(struct layout_cell *lc, int size, int before, s2 = size; if (s2 < PANE_MINIMUM) s2 = PANE_MINIMUM; - else if (s2 > sx - 2) + else if (s2 > ss - 2) s2 = ss - 2; s1 = ss - 1 - s2; @@ -1339,10 +1343,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, lc = wp->layout_cell; /* Copy the old cell size. */ - sx = lc->sx; - sy = lc->sy; - xoff = lc->xoff; - yoff = lc->yoff; + sx = lc->g.sx; + sy = lc->g.sy; + xoff = lc->g.xoff; + yoff = lc->g.yoff; /* Check there is enough space for the two new panes. */ if (!layout_split_check_space(wp, lc, type)) @@ -1383,13 +1387,13 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, * must be resized before inserting the new cell. */ if (lc->type == LAYOUT_LEFTRIGHT) { - lc->sx = new_size; + lc->g.sx = new_size; layout_resize_child_cells(wp->window, lc); - lc->sx = saved_size; + lc->g.sx = saved_size; } else if (lc->type == LAYOUT_TOPBOTTOM) { - lc->sy = new_size; + lc->g.sy = new_size; layout_resize_child_cells(wp->window, lc); - lc->sy = saved_size; + lc->g.sy = saved_size; } resize_first = 1; @@ -1433,10 +1437,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, */ if (!resize_first && type == LAYOUT_LEFTRIGHT) { layout_set_size(lc1, size1, sy, xoff, yoff); - layout_set_size(lc2, size2, sy, xoff + lc1->sx + 1, yoff); + layout_set_size(lc2, size2, sy, xoff + lc1->g.sx + 1, yoff); } else if (!resize_first && type == LAYOUT_TOPBOTTOM) { layout_set_size(lc1, sx, size1, xoff, yoff); - layout_set_size(lc2, sx, size2, xoff, yoff + lc1->sy + 1); + layout_set_size(lc2, sx, size2, xoff, yoff + lc1->g.sy + 1); } if (full_size) { if (!resize_first) @@ -1453,8 +1457,8 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size, * layout_assign_pane before much else happens! */ struct layout_cell * -layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx, - u_int sy, int ox, int oy) +layout_floating_pane(struct window *w, struct window_pane *wp, + struct layout_geometry *lg) { struct layout_cell *lc, *lcnew, *lcparent; @@ -1466,16 +1470,16 @@ layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx, if (lcparent == NULL) { /* - * Adding a pane to a root that isn't node. Must create and - * insert a new root. - */ + * Adding a pane to a root that isn't a node. Must create and + * insert a new root. + */ lcparent = layout_replace_with_node(w, lc, LAYOUT_TOPBOTTOM); } lcnew = layout_create_cell(lcparent); TAILQ_INSERT_AFTER(&lcparent->cells, lc, lcnew, entry); lcnew->flags |= LAYOUT_CELL_FLOATING; - layout_set_size(lcnew, sx, sy, ox, oy); + layout_set_size(lcnew, lg->sx, lg->sy, lg->xoff, lg->yoff); return (lcnew); } @@ -1498,7 +1502,7 @@ layout_close_pane(struct window_pane *wp) layout_fix_offsets(w); layout_fix_panes(w, NULL); } - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); } /* Spread out cells inside a parent cell. */ @@ -1518,12 +1522,12 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) status = window_get_pane_status(w); if (parent->type == LAYOUT_LEFTRIGHT) - size = parent->sx; + size = parent->g.sx; else if (parent->type == LAYOUT_TOPBOTTOM) { if (layout_add_horizontal_border(w, parent, status)) - size = parent->sy - 1; + size = parent->g.sy - 1; else - size = parent->sy; + size = parent->g.sy; } else return (0); if (size < number - 1) @@ -1544,7 +1548,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) continue; change = 0; if (parent->type == LAYOUT_LEFTRIGHT) { - change = each - (int)lc->sx; + change = each - (int)lc->g.sx; if (remainder > 0) { change++; remainder--; @@ -1559,7 +1563,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) this++; remainder--; } - change = this - (int)lc->sy; + change = this - (int)lc->g.sy; layout_resize_adjust(w, lc, LAYOUT_TOPBOTTOM, change); } if (change != 0) @@ -1656,29 +1660,29 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args, char **cause) { struct layout_cell *lcnew; - u_int sx = UINT_MAX, sy = UINT_MAX; - int ox = INT_MAX, oy = INT_MAX; + struct layout_geometry fg; - if (layout_floating_args_parse(item, args, lines, w, &sx, &sy, &ox, &oy, - cause) != 0) + layout_geometry_init(&fg); + if (layout_floating_args_parse(item, args, lines, w, &fg, cause) != 0) return (NULL); - lcnew = layout_floating_pane(w, wp, sx, sy, ox, oy); + window_push_zoom(wp->window, 1, args_has(args, 'Z')); + lcnew = layout_floating_pane(w, wp, &fg); return (lcnew); } int layout_floating_args_parse(struct cmdq_item *item, struct args *args, - enum pane_lines lines, struct window *w, u_int *sxp, u_int *syp, int *oxp, - int *oyp, char **cause) + enum pane_lines lines, struct window *w, struct layout_geometry *lg, + char **cause) { int sx, sy, ox, oy; char *error = NULL; - sx = *sxp == UINT_MAX ? w->sx / 2 : *sxp; - sy = *syp == UINT_MAX ? w->sy / 4 : *syp; - ox = *oxp == INT_MAX ? INT_MAX : *oxp; - oy = *oyp == INT_MAX ? INT_MAX : *oyp; + sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx; + sy = lg->sy == UINT_MAX ? w->sy / 4 : lg->sy; + ox = lg->xoff; + oy = lg->yoff; if (args_has(args, 'x')) { sx = args_percentage_and_expand(args, 'x', 0, PANE_MAXIMUM, @@ -1730,7 +1734,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, ox = 4; } w->last_new_pane_x = ox; - } else + } else if (args_has(args, 'X')) if (lines != PANE_LINES_NONE) ox += 1; if (oy == INT_MAX) { @@ -1742,7 +1746,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, oy = 2; } w->last_new_pane_y = oy; - } else + } else if (args_has(args, 'Y')) if (lines != PANE_LINES_NONE) oy += 1; @@ -1755,10 +1759,10 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, return (-1); } - *sxp = sx; - *syp = sy; - *oxp = ox; - *oyp = oy; + lg->sx = sx; + lg->sy = sy; + lg->xoff = ox; + lg->yoff = oy; return (0); } @@ -1774,7 +1778,7 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) int change; if (lc->flags & LAYOUT_CELL_FLOATING) - return (0); + return (-1); lcneighbour = layout_cell_get_neighbour(lc); if (lcneighbour == NULL) { @@ -1787,9 +1791,9 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) * neighbour. */ if (type == LAYOUT_TOPBOTTOM) - change = lc->sy + 1; + change = lc->g.sy + 1; else - change = lc->sx + 1; + change = lc->g.sx + 1; layout_resize_adjust(w, lcneighbour, type, change); } @@ -1799,7 +1803,7 @@ layout_remove_tile(struct window *w, struct layout_cell *lc) */ if (lc->parent != NULL) layout_set_size(lc, 0, 0, 0, 0); - return (1); + return (0); } /* @@ -1816,14 +1820,14 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) if (lc == NULL) fatalx("layout cell cannot be null when tiling"); - lcparent = lc->parent; - if (lc->flags & LAYOUT_CELL_FLOATING) - return (1); + if (layout_cell_is_tiled(lc)) + return (-1); + lcparent = lc->parent; if (lcparent == NULL) { /* Only pane in the layout. */ layout_set_size(lc, w->sx, w->sy, 0, 0); - return (1); + return (0); } type = lcparent->type; @@ -1835,9 +1839,9 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) */ layout_insert_tile(w, lcparent); if (type == LAYOUT_LEFTRIGHT) - size1 = lcparent->sx; + size1 = lcparent->g.sx; else - size1 = lcparent->sy; + size1 = lcparent->g.sy; layout_resize_set_size(w, lc, type, size1); } else { /* @@ -1846,7 +1850,7 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) */ lctiled = layout_cell_get_first_tiled(lcneighbour); if (!layout_split_check_space(lctiled->wp, lcneighbour, type)) - return (0); + return (-1); layout_split_sizes(lcneighbour, -1, 0, type, &size1, &size2, &saved_size); layout_resize_set_size(w, lc, type, size1); @@ -1855,13 +1859,13 @@ layout_insert_tile(struct window *w, struct layout_cell *lc) /* Setting opposite of the 'split' size to that of the parent. */ if (lcparent->type == LAYOUT_LEFTRIGHT) { - size1 = lcparent->sy; + size1 = lcparent->g.sy; type = LAYOUT_TOPBOTTOM; } else { - size1 = lcparent->sx; + size1 = lcparent->g.sx; type = LAYOUT_LEFTRIGHT; } layout_resize_set_size(w, lc, type, size1); - return (1); + return (0); } diff --git a/log.c b/log.c index 0e0d1d1a1..966675679 100644 --- a/log.c +++ b/log.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: log.c,v 1.33 2026/05/17 16:02:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/menu.c b/menu.c index bff833cbd..666d59df1 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: menu.c,v 1.68 2026/07/07 09:45:09 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -313,6 +313,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) struct mouse_event *m = &event->m; u_int i; int count = menu->count, old = md->choice; + int move; const char *name = NULL; const struct menu_item *item; struct cmd_parse_tree *tree; @@ -321,6 +322,14 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) char *error; if (KEYC_IS_MOUSE(event->key)) { + /* + * A mouse move with no button held reports as a release, so + * treat it as highlight-only: it must never select or close the + * menu, otherwise a menu opened without a button already down + * (such as a submenu opened from another menu) would vanish as + * soon as the mouse moved over it. + */ + move = MOUSE_DRAG(m->b) && MOUSE_RELEASE(m->b); if (md->flags & MENU_NOMOUSE) { if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1) return (1); @@ -331,7 +340,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) m->y < md->py + 1 || m->y > md->py + 1 + count - 1) { if (~md->flags & MENU_STAYOPEN) { - if (MOUSE_RELEASE(m->b)) + if (!move && MOUSE_RELEASE(m->b)) return (1); } else { if (!MOUSE_RELEASE(m->b) && @@ -346,7 +355,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event) return (0); } if (~md->flags & MENU_STAYOPEN) { - if (MOUSE_RELEASE(m->b)) + if (!move && MOUSE_RELEASE(m->b)) goto chosen; } else { if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b)) @@ -562,6 +571,10 @@ menu_prepare(struct menu *menu, int flags, int starting_choice, if (py + menu->count + 2 > c->tty.sy) py = c->tty.sy - menu->count - 2; + /* Remember where this menu is so -x/-y L can reuse the position. */ + c->menu_last_px = px; + c->menu_last_py = py; + if (lines == BOX_LINES_DEFAULT) lines = options_get_number(o, "menu-border-lines"); diff --git a/mode-tree.c b/mode-tree.c index b26901141..ac44fec24 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: mode-tree.c,v 1.99 2026/07/06 12:08:52 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -37,8 +37,7 @@ enum mode_tree_preview { }; #define MODE_TREE_PREFIX_STYLE \ - "#{?mode_tree_selected,#[default]#[noacs]," \ - "#[fg=themelightgrey]#[bg=default]#[noacs]}" + "#[fg=themelightgrey]#[bg=default]#[noacs]" #define MODE_TREE_PREFIX_FORMAT \ MODE_TREE_PREFIX_STYLE \ @@ -835,6 +834,11 @@ mode_tree_draw(struct mode_tree_data *mtd) if (mtd->line_size == 0) return; + w = mtd->width; + h = mtd->height; + if (w == 0 || h == 0) + return; + memcpy(&gc0, &grid_default_cell, sizeof gc0); memcpy(&gc, &grid_default_cell, sizeof gc); style_apply(&gc, oo, "tree-mode-selection-style", NULL); @@ -844,9 +848,6 @@ mode_tree_draw(struct mode_tree_data *mtd) dfg = gc.fg; dfg0 = gc0.fg; - w = mtd->width; - h = mtd->height; - screen_write_start(&ctx, s); screen_write_clearscreen(&ctx, 8); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); @@ -950,16 +951,16 @@ mode_tree_draw(struct mode_tree_data *mtd) } } else { screen_write_clearendofline(&ctx, gc.bg); - format_draw(&ctx, &gc, prefix_width, prefix, NULL, 0); + format_draw(&ctx, &gc, prefix_width, prefix, NULL, 1); if (left != 0) { screen_write_cursormove(&ctx, prefix_width, i - mtd->offset, 0); - format_draw(&ctx, &gc, left, text, NULL, 0); + format_draw(&ctx, &gc, left, text, NULL, 1); if (mti->text != NULL && width < w) { screen_write_cursormove(&ctx, width, i - mtd->offset, 0); format_draw(&ctx, &gc, w - width, - mti->text, NULL, 0); + mti->text, NULL, 1); } } } diff --git a/monitor.c b/monitor.c new file mode 100644 index 000000000..d6338e878 --- /dev/null +++ b/monitor.c @@ -0,0 +1,661 @@ +/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#include "tmux.h" + +/* Subscription pane. */ +struct monitor_pane { + u_int pane; + u_int idx; + char *last; + u_int generation; + + RB_ENTRY(monitor_pane) entry; +}; +RB_HEAD(monitor_panes, monitor_pane); + +/* Subscription window. */ +struct monitor_window { + u_int window; + u_int idx; + char *last; + u_int generation; + + RB_ENTRY(monitor_window) entry; +}; +RB_HEAD(monitor_windows, monitor_window); + +/* Subscription. */ +struct monitor_item { + char *name; + char *format; + + enum monitor_type type; + u_int id; + int flags; + + char *last; + struct monitor_panes panes; + struct monitor_windows windows; + + RB_ENTRY(monitor_item) entry; +}; +RB_HEAD(monitor_items, monitor_item); + +/* Monitored subscription set. */ +struct monitor_set { + struct client *client; + struct session *session; + monitor_cb cb; + void *data; + + struct monitor_items items; + struct event timer; + u_int generation; +}; + +static void monitor_timer(__unused int, __unused short, void *); + +/* Get the session for this monitor set. */ +static struct session * +monitor_get_session(struct monitor_set *ms) +{ + struct session *s; + + if (ms->client != NULL) + return (ms->client->session); + s = ms->session; + if (s == NULL) + return (RB_MIN(sessions, &sessions)); + if (session_find_by_id(s->id) != s) + return (NULL); + return (s); +} + +/* Create a format tree for a subscription. */ +static struct format_tree * +monitor_create_formats(struct client *c, struct session *s, struct winlink *wl, + struct window_pane *wp) +{ + struct format_tree *ft; + + ft = format_create(NULL, NULL, 0, FORMAT_NOJOBS); + format_defaults(ft, c, s, wl, wp); + return (ft); +} + +/* Compare subscriptions. */ +static int +monitor_item_cmp(struct monitor_item *m1, struct monitor_item *m2) +{ + return (strcmp(m1->name, m2->name)); +} +RB_GENERATE_STATIC(monitor_items, monitor_item, entry, monitor_item_cmp); + +/* Compare subscription panes. */ +static int +monitor_pane_cmp(struct monitor_pane *mp1, struct monitor_pane *mp2) +{ + if (mp1->pane < mp2->pane) + return (-1); + if (mp1->pane > mp2->pane) + return (1); + if (mp1->idx < mp2->idx) + return (-1); + if (mp1->idx > mp2->idx) + return (1); + return (0); +} +RB_GENERATE_STATIC(monitor_panes, monitor_pane, entry, monitor_pane_cmp); + +/* Compare subscription windows. */ +static int +monitor_window_cmp(struct monitor_window *mw1, struct monitor_window *mw2) +{ + if (mw1->window < mw2->window) + return (-1); + if (mw1->window > mw2->window) + return (1); + if (mw1->idx < mw2->idx) + return (-1); + if (mw1->idx > mw2->idx) + return (1); + return (0); +} +RB_GENERATE_STATIC(monitor_windows, monitor_window, entry, monitor_window_cmp); + +/* Free a subscription. */ +static void +monitor_free_item(struct monitor_set *ms, struct monitor_item *me) +{ + struct monitor_pane *mp, *mp1; + struct monitor_window *mw, *mw1; + + RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) { + RB_REMOVE(monitor_panes, &me->panes, mp); + free(mp->last); + free(mp); + } + RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) { + RB_REMOVE(monitor_windows, &me->windows, mw); + free(mw->last); + free(mw); + } + free(me->last); + + RB_REMOVE(monitor_items, &ms->items, me); + free(me->name); + free(me->format); + free(me); +} + +/* Report a changed value. */ +static void +monitor_report(struct monitor_set *ms, struct monitor_item *me, + struct session *s, struct winlink *wl, struct window_pane *wp, + const char *value, const char *last) +{ + struct monitor_change change = { 0 }; + + log_debug("%s: %s changed to %s", __func__, me->name, value); + + change.name = me->name; + change.value = value; + change.last = last; + change.c = ms->client; + change.s = s; + change.wl = wl; + change.wp = wp; + ms->cb(&change, ms->data); +} + +/* Check a value against its last value and report if changed. */ +static void +monitor_check_value(struct monitor_set *ms, struct monitor_item *me, + struct session *s, struct winlink *wl, struct window_pane *wp, + char *value, char **last) +{ + if (*last == NULL) { + *last = value; + if ((me->flags & MONITOR_NOTIFY_INITIAL) && + ((~me->flags & MONITOR_NOTIFY_TRUE) || + format_true(value))) + monitor_report(ms, me, s, wl, wp, value, NULL); + return; + } + + if (strcmp(value, *last) == 0) { + free(value); + return; + } + + if ((~me->flags & MONITOR_NOTIFY_TRUE) || format_true(value)) + monitor_report(ms, me, s, wl, wp, value, *last); + free(*last); + *last = value; +} + +/* Check session subscription. */ +static void +monitor_check_session(struct monitor_set *ms, struct monitor_item *me, + struct format_tree *ft) +{ + struct session *s = monitor_get_session(ms); + char *value; + + value = format_expand(ft, me->format); + + monitor_check_value(ms, me, s, NULL, NULL, value, &me->last); +} + +/* Check pane subscription. */ +static void +monitor_check_pane(struct monitor_set *ms, struct monitor_item *me) +{ + struct client *c = ms->client; + struct session *s = monitor_get_session(ms); + struct window_pane *wp; + struct window *w; + struct winlink *wl; + struct format_tree *ft; + char *value; + struct monitor_pane *mp, find; + + wp = window_pane_find_by_id(me->id); + if (wp == NULL || wp->fd == -1) + return; + w = wp->window; + + TAILQ_FOREACH(wl, &w->winlinks, wentry) { + if (wl->session != s) + continue; + + ft = monitor_create_formats(c, s, wl, wp); + value = format_expand(ft, me->format); + format_free(ft); + + find.pane = wp->id; + find.idx = wl->idx; + mp = RB_FIND(monitor_panes, &me->panes, &find); + if (mp == NULL) { + mp = xcalloc(1, sizeof *mp); + mp->pane = wp->id; + mp->idx = wl->idx; + RB_INSERT(monitor_panes, &me->panes, mp); + } + + monitor_check_value(ms, me, s, wl, wp, value, &mp->last); + } +} + +/* Check one all-panes subscription. */ +static void +monitor_check_all_panes_one(struct monitor_set *ms, struct monitor_item *me, + struct format_tree *ft, struct winlink *wl, struct window_pane *wp) +{ + struct session *s = monitor_get_session(ms); + char *value; + struct monitor_pane *mp, find; + + value = format_expand(ft, me->format); + + find.pane = wp->id; + find.idx = wl->idx; + mp = RB_FIND(monitor_panes, &me->panes, &find); + if (mp == NULL) { + mp = xcalloc(1, sizeof *mp); + mp->pane = wp->id; + mp->idx = wl->idx; + RB_INSERT(monitor_panes, &me->panes, mp); + } + mp->generation = ms->generation; + + monitor_check_value(ms, me, s, wl, wp, value, &mp->last); +} + +/* Remove all-panes entries not seen during the current scan. */ +static void +monitor_sweep_all_panes(struct monitor_item *me, u_int generation) +{ + struct monitor_pane *mp, *mp1; + + RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) { + if (mp->generation == generation) + continue; + RB_REMOVE(monitor_panes, &me->panes, mp); + free(mp->last); + free(mp); + } +} + +/* Check window subscription. */ +static void +monitor_check_window(struct monitor_set *ms, struct monitor_item *me) +{ + struct client *c = ms->client; + struct session *s = monitor_get_session(ms); + struct window *w; + struct winlink *wl; + struct format_tree *ft; + char *value; + struct monitor_window *mw, find; + + w = window_find_by_id(me->id); + if (w == NULL) + return; + + TAILQ_FOREACH(wl, &w->winlinks, wentry) { + if (wl->session != s) + continue; + + ft = monitor_create_formats(c, s, wl, NULL); + value = format_expand(ft, me->format); + format_free(ft); + + find.window = w->id; + find.idx = wl->idx; + mw = RB_FIND(monitor_windows, &me->windows, &find); + if (mw == NULL) { + mw = xcalloc(1, sizeof *mw); + mw->window = w->id; + mw->idx = wl->idx; + RB_INSERT(monitor_windows, &me->windows, mw); + } + + monitor_check_value(ms, me, s, wl, NULL, value, &mw->last); + } +} + +/* Check one all-windows subscription. */ +static void +monitor_check_all_windows_one(struct monitor_set *ms, struct monitor_item *me, + struct format_tree *ft, struct winlink *wl) +{ + struct session *s = monitor_get_session(ms); + struct window *w = wl->window; + char *value; + struct monitor_window *mw, find; + + value = format_expand(ft, me->format); + + find.window = w->id; + find.idx = wl->idx; + mw = RB_FIND(monitor_windows, &me->windows, &find); + if (mw == NULL) { + mw = xcalloc(1, sizeof *mw); + mw->window = w->id; + mw->idx = wl->idx; + RB_INSERT(monitor_windows, &me->windows, mw); + } + mw->generation = ms->generation; + + monitor_check_value(ms, me, s, wl, NULL, value, &mw->last); +} + +/* Remove all-windows entries not seen during the current scan. */ +static void +monitor_sweep_all_windows(struct monitor_item *me, u_int generation) +{ + struct monitor_window *mw, *mw1; + + RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) { + if (mw->generation == generation) + continue; + RB_REMOVE(monitor_windows, &me->windows, mw); + free(mw->last); + free(mw); + } +} + +/* Check session subscriptions. */ +static void +monitor_check_sessions(struct monitor_set *ms) +{ + struct client *c = ms->client; + struct session *s = monitor_get_session(ms); + struct monitor_item *me, *me1; + struct format_tree *ft; + + ft = monitor_create_formats(c, s, NULL, NULL); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type == MONITOR_SESSION) + monitor_check_session(ms, me, ft); + } + format_free(ft); +} + +/* Check pane and window subscriptions. */ +static void +monitor_check_panes_windows(struct monitor_set *ms) +{ + struct monitor_item *me, *me1; + + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + switch (me->type) { + case MONITOR_PANE: + monitor_check_pane(ms, me); + break; + case MONITOR_WINDOW: + monitor_check_window(ms, me); + break; + case MONITOR_SESSION: + case MONITOR_ALL_PANES: + case MONITOR_ALL_WINDOWS: + break; + } + } +} + +/* Check all-panes subscriptions. */ +static void +monitor_check_all_panes(struct monitor_set *ms) +{ + struct client *c = ms->client; + struct session *s = monitor_get_session(ms); + struct monitor_item *me, *me1; + struct window_pane *wp; + struct format_tree *ft; + struct winlink *wl; + + if (++ms->generation == 0) + ms->generation = 1; + RB_FOREACH(wl, winlinks, &s->windows) { + TAILQ_FOREACH(wp, &wl->window->panes, entry) { + ft = monitor_create_formats(c, s, wl, wp); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type != MONITOR_ALL_PANES) + continue; + monitor_check_all_panes_one(ms, me, ft, wl, wp); + } + format_free(ft); + } + } + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type == MONITOR_ALL_PANES) + monitor_sweep_all_panes(me, ms->generation); + } +} + +/* Check all-windows subscriptions. */ +static void +monitor_check_all_windows(struct monitor_set *ms) +{ + struct client *c = ms->client; + struct session *s = monitor_get_session(ms); + struct monitor_item *me, *me1; + struct format_tree *ft; + struct winlink *wl; + + if (++ms->generation == 0) + ms->generation = 1; + RB_FOREACH(wl, winlinks, &s->windows) { + ft = monitor_create_formats(c, s, wl, NULL); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type != MONITOR_ALL_WINDOWS) + continue; + monitor_check_all_windows_one(ms, me, ft, wl); + } + format_free(ft); + } + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) { + if (me->type == MONITOR_ALL_WINDOWS) + monitor_sweep_all_windows(me, ms->generation); + } +} + +/* Check subscriptions. */ +static void +monitor_timer(__unused int fd, __unused short events, void *data) +{ + struct monitor_set *ms = data; + struct monitor_item *me; + struct timeval tv = { .tv_sec = 1 }; + int have_session = 0, have_all_panes = 0; + int have_all_windows = 0; + + log_debug("%s: timer fired", __func__); + evtimer_add(&ms->timer, &tv); + + if (monitor_get_session(ms) == NULL) + return; + + RB_FOREACH(me, monitor_items, &ms->items) { + switch (me->type) { + case MONITOR_SESSION: + have_session = 1; + break; + case MONITOR_ALL_PANES: + have_all_panes = 1; + break; + case MONITOR_ALL_WINDOWS: + have_all_windows = 1; + break; + case MONITOR_PANE: + case MONITOR_WINDOW: + break; + } + } + + if (have_session) + monitor_check_sessions(ms); + monitor_check_panes_windows(ms); + if (have_all_panes) + monitor_check_all_panes(ms); + if (have_all_windows) + monitor_check_all_windows(ms); +} + +/* Create a monitor set. */ +static struct monitor_set * +monitor_create(monitor_cb cb, void *data) +{ + struct monitor_set *ms; + + ms = xcalloc(1, sizeof *ms); + ms->cb = cb; + ms->data = data; + RB_INIT(&ms->items); + return (ms); +} + +/* Create a client monitor set. */ +struct monitor_set * +monitor_create_client(struct client *c, monitor_cb cb, void *data) +{ + struct monitor_set *ms; + + ms = monitor_create(cb, data); + ms->client = c; + return (ms); +} + +/* Create a monitor set for a session. */ +struct monitor_set * +monitor_create_session(struct session *s, monitor_cb cb, void *data) +{ + struct monitor_set *ms; + + ms = monitor_create(cb, data); + ms->session = s; + if (s != NULL) + session_add_ref(s, __func__); + return (ms); +} + +/* Destroy a monitor set. */ +void +monitor_destroy(struct monitor_set *ms) +{ + struct monitor_item *me, *me1; + + if (ms != NULL) { + if (evtimer_initialized(&ms->timer)) + evtimer_del(&ms->timer); + RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) + monitor_free_item(ms, me); + if (ms->session != NULL) + session_remove_ref(ms->session, __func__); + free(ms); + } +} + +/* Parse a subscription. */ +int +monitor_parse(const char *value, char **name, enum monitor_type *type, int *id, + char **format) +{ + char *copy, *what, *split; + + copy = xstrdup(value); + *id = -1; + + what = strchr(copy, ':'); + if (what == NULL) + goto fail; + *what++ = '\0'; + + split = strchr(what, ':'); + if (split == NULL) + goto fail; + *split++ = '\0'; + + if (strcmp(what, "%*") == 0) + *type = MONITOR_ALL_PANES; + else if (sscanf(what, "%%%d", id) == 1 && *id >= 0) + *type = MONITOR_PANE; + else if (strcmp(what, "@*") == 0) + *type = MONITOR_ALL_WINDOWS; + else if (sscanf(what, "@%d", id) == 1 && *id >= 0) + *type = MONITOR_WINDOW; + else + *type = MONITOR_SESSION; + *name = xstrdup(copy); + *format = xstrdup(split); + + free(copy); + return (0); + +fail: + free(copy); + return (-1); +} + +/* Add a subscription. */ +void +monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, + int id, const char *format, int flags) +{ + struct monitor_item *me, find = { .name = (char *)name }; + struct timeval tv = { .tv_sec = 1 }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL) + monitor_free_item(ms, me); + + me = xcalloc(1, sizeof *me); + me->name = xstrdup(name); + me->format = xstrdup(format); + me->type = type; + me->id = id; + me->flags = flags; + RB_INIT(&me->panes); + RB_INIT(&me->windows); + RB_INSERT(monitor_items, &ms->items, me); + + if (!evtimer_initialized(&ms->timer)) + evtimer_set(&ms->timer, monitor_timer, ms); + if (!evtimer_pending(&ms->timer, NULL)) + evtimer_add(&ms->timer, &tv); +} + +/* Remove a subscription. */ +void +monitor_remove(struct monitor_set *ms, const char *name) +{ + struct monitor_item *me, find = { .name = (char *)name }; + + if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL) + monitor_free_item(ms, me); + if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer)) + evtimer_del(&ms->timer); +} diff --git a/names.c b/names.c index cee310366..aa7e6006e 100644 --- a/names.c +++ b/names.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: names.c,v 1.48 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/notify.c b/notify.c deleted file mode 100644 index 755850525..000000000 --- a/notify.c +++ /dev/null @@ -1,325 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2012 George Nachman - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include -#include - -#include "tmux.h" - -struct notify_entry { - const char *name; - struct cmd_find_state fs; - struct format_tree *formats; - - struct client *client; - struct session *session; - struct window *window; - int pane; - const char *pbname; -}; - -static struct cmdq_item * -notify_insert_one_hook(struct cmdq_item *item, struct notify_entry *ne, - struct cmd_parse_tree *tree, struct cmdq_state *state) -{ - struct cmdq_item *new_item; - char *s; - - if (tree == NULL) - return (item); - if (log_get_level() != 0) { - s = cmd_parse_print(tree, 0); - log_debug("%s: hook %s is: %s", __func__, ne->name, s); - free(s); - } - new_item = cmd_invoke_get(tree, state, NULL); - return (cmdq_insert_after(item, new_item)); -} - -static void -notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) -{ - struct cmd_find_state fs; - struct options *oo; - struct cmdq_state *state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_parse_tree *tree; - const char *value; - char *cause; - - log_debug("%s: inserting hook %s", __func__, ne->name); - - cmd_find_clear_state(&fs, 0); - if (cmd_find_empty_state(&ne->fs) || !cmd_find_valid_state(&ne->fs)) - cmd_find_from_nothing(&fs, 0); - else - cmd_find_copy_state(&fs, &ne->fs); - - if (fs.s == NULL) - oo = global_s_options; - else - oo = fs.s->options; - o = options_get(oo, ne->name); - if (o == NULL && fs.wp != NULL) { - oo = fs.wp->options; - o = options_get(oo, ne->name); - } - if (o == NULL && fs.wl != NULL) { - oo = fs.wl->window->options; - o = options_get(oo, ne->name); - } - if (o == NULL) { - log_debug("%s: hook %s not found", __func__, ne->name); - return; - } - - state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); - cmdq_add_formats(state, ne->formats); - - if (*ne->name == '@') { - value = options_get_string(oo, ne->name); - tree = cmd_parse_from_string(value, NULL, &cause); - if (tree == NULL) { - log_debug("%s: can't parse hook %s: %s", __func__, - ne->name, cause); - free(cause); - } else { - item = notify_insert_one_hook(item, ne, tree, state); - cmd_parse_free(tree); - } - } else { - a = options_array_first(o); - while (a != NULL) { - tree = options_array_item_value(a)->cmd; - item = notify_insert_one_hook(item, ne, tree, state); - a = options_array_next(a); - } - } - - cmdq_free_state(state); -} - -static enum cmd_retval -notify_callback(struct cmdq_item *item, void *data) -{ - struct notify_entry *ne = data; - - log_debug("%s: %s", __func__, ne->name); - - if (strcmp(ne->name, "pane-mode-changed") == 0) - control_notify_pane_mode_changed(ne->pane); - if (strcmp(ne->name, "window-layout-changed") == 0) - control_notify_window_layout_changed(ne->window); - if (strcmp(ne->name, "window-pane-changed") == 0) - control_notify_window_pane_changed(ne->window); - if (strcmp(ne->name, "window-unlinked") == 0) - control_notify_window_unlinked(ne->session, ne->window); - if (strcmp(ne->name, "window-linked") == 0) - control_notify_window_linked(ne->session, ne->window); - if (strcmp(ne->name, "window-renamed") == 0) - control_notify_window_renamed(ne->window); - if (strcmp(ne->name, "client-session-changed") == 0) - control_notify_client_session_changed(ne->client); - if (strcmp(ne->name, "client-detached") == 0) - control_notify_client_detached(ne->client); - if (strcmp(ne->name, "session-renamed") == 0) - control_notify_session_renamed(ne->session); - if (strcmp(ne->name, "session-created") == 0) - control_notify_session_created(ne->session); - if (strcmp(ne->name, "session-closed") == 0) - control_notify_session_closed(ne->session); - if (strcmp(ne->name, "session-window-changed") == 0) - control_notify_session_window_changed(ne->session); - if (strcmp(ne->name, "paste-buffer-changed") == 0) - control_notify_paste_buffer_changed(ne->pbname); - if (strcmp(ne->name, "paste-buffer-deleted") == 0) - control_notify_paste_buffer_deleted(ne->pbname); - - notify_insert_hook(item, ne); - - if (ne->client != NULL) - server_client_unref(ne->client); - if (ne->session != NULL) - session_remove_ref(ne->session, __func__); - if (ne->window != NULL) - window_remove_ref(ne->window, __func__); - - if (ne->fs.s != NULL) - session_remove_ref(ne->fs.s, __func__); - - format_free(ne->formats); - free((void *)ne->name); - free((void *)ne->pbname); - free(ne); - - return (CMD_RETURN_NORMAL); -} - -static void -notify_add(const char *name, struct cmd_find_state *fs, struct client *c, - struct session *s, struct window *w, struct window_pane *wp, - const char *pbname) -{ - struct notify_entry *ne; - struct cmdq_item *item; - - item = cmdq_running(NULL); - if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - return; - - ne = xcalloc(1, sizeof *ne); - ne->name = xstrdup(name); - - ne->client = c; - ne->session = s; - ne->window = w; - ne->pane = (wp != NULL ? (int)wp->id : -1); - ne->pbname = (pbname != NULL ? xstrdup(pbname) : NULL); - - ne->formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(ne->formats, "hook", "%s", name); - if (c != NULL) - format_add(ne->formats, "hook_client", "%s", c->name); - if (s != NULL) { - format_add(ne->formats, "hook_session", "$%u", s->id); - format_add(ne->formats, "hook_session_name", "%s", s->name); - } - if (w != NULL) { - format_add(ne->formats, "hook_window", "@%u", w->id); - format_add(ne->formats, "hook_window_name", "%s", w->name); - } - if (wp != NULL) { - format_add(ne->formats, "hook_pane", "%%%d", wp->id); - format_add(ne->formats, "hook_window", "@%u", wp->window->id); - format_add(ne->formats, "hook_window_name", "%s", - wp->window->name); - } - format_log_debug(ne->formats, __func__); - - if (c != NULL) - c->references++; - if (s != NULL) - session_add_ref(s, __func__); - if (w != NULL) - window_add_ref(w, __func__); - - cmd_find_copy_state(&ne->fs, fs); - if (ne->fs.s != NULL) /* cmd_find_valid_state needs session */ - session_add_ref(ne->fs.s, __func__); - - cmdq_append(NULL, cmdq_get_callback(notify_callback, ne)); -} - -void -notify_hook(struct cmdq_item *item, const char *name) -{ - struct cmd_find_state *target = cmdq_get_target(item); - struct notify_entry ne; - - memset(&ne, 0, sizeof ne); - - ne.name = name; - cmd_find_copy_state(&ne.fs, target); - - ne.client = cmdq_get_client(item); - ne.session = target->s; - ne.window = target->w; - ne.pane = (target->wp != NULL ? (int)target->wp->id : -1); - - ne.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(ne.formats, "hook", "%s", name); - format_log_debug(ne.formats, __func__); - - notify_insert_hook(item, &ne); - format_free(ne.formats); -} - -void -notify_client(const char *name, struct client *c) -{ - struct cmd_find_state fs; - - cmd_find_from_client(&fs, c, 0); - notify_add(name, &fs, c, NULL, NULL, NULL, NULL); -} - -void -notify_session(const char *name, struct session *s) -{ - struct cmd_find_state fs; - - if (session_alive(s)) - cmd_find_from_session(&fs, s, 0); - else - cmd_find_from_nothing(&fs, 0); - notify_add(name, &fs, NULL, s, NULL, NULL, NULL); -} - -void -notify_winlink(const char *name, struct winlink *wl) -{ - struct cmd_find_state fs; - - cmd_find_from_winlink(&fs, wl, 0); - notify_add(name, &fs, NULL, wl->session, wl->window, NULL, NULL); -} - -void -notify_session_window(const char *name, struct session *s, struct window *w) -{ - struct cmd_find_state fs; - - cmd_find_from_session_window(&fs, s, w, 0); - notify_add(name, &fs, NULL, s, w, NULL, NULL); -} - -void -notify_window(const char *name, struct window *w) -{ - struct cmd_find_state fs; - - cmd_find_from_window(&fs, w, 0); - notify_add(name, &fs, NULL, NULL, w, NULL, NULL); -} - -void -notify_pane(const char *name, struct window_pane *wp) -{ - struct cmd_find_state fs; - - cmd_find_from_pane(&fs, wp, 0); - notify_add(name, &fs, NULL, NULL, NULL, wp, NULL); -} - -void -notify_paste_buffer(const char *pbname, int deleted) -{ - struct cmd_find_state fs; - - cmd_find_clear_state(&fs, 0); - if (deleted) { - notify_add("paste-buffer-deleted", &fs, NULL, NULL, NULL, NULL, - pbname); - } else { - notify_add("paste-buffer-changed", &fs, NULL, NULL, NULL, NULL, - pbname); - } -} diff --git a/options-table.c b/options-table.c index ef047888e..55f169683 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options-table.c,v 1.233 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -955,7 +955,7 @@ const struct options_table_entry options_table[] = { { .name = "mouse", .type = OPTIONS_TABLE_FLAG, .scope = OPTIONS_TABLE_SESSION, - .default_num = 0, + .default_num = TMUX_MOUSE, .text = "Whether the mouse is recognised and mouse key bindings are " "executed. " "Applications inside panes can use the mouse even when 'off'." @@ -1538,6 +1538,9 @@ const struct options_table_entry options_table[] = { "\"#{pane_title}\"" "#{?#{mouse}," "#[align=right]" + "#[range=control|7][" + "#{?#{pane_floating_flag},t,f}" + "]#[norange]" "#[range=control|8][" "#{?#{window_zoomed_flag},u,z}" "]#[norange]" @@ -1929,12 +1932,22 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-light-theme", ""), OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), + OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), + OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-in", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-out", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), + OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), @@ -1945,6 +1958,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-renamed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-resized", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", ""), OPTIONS_TABLE_HOOK("window-unlinked", ""), { .name = NULL } diff --git a/options.c b/options.c index d0d689809..19bea23b1 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options.c,v 1.90 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -32,18 +32,69 @@ */ struct options_array_item { - u_int index; + char *key; union options_value value; RB_ENTRY(options_array_item) entry; }; + +static int +options_array_key_to_number(const char *key, u_int *idx) +{ + const char *errstr; + long long n; + + if (*key == '\0') + return (-1); + for (const char *cp = key; *cp != '\0'; cp++) { + if (!isdigit((u_char)*cp)) + return (0); + } + + n = strtonum(key, 0, UINT_MAX, &errstr); + if (errstr != NULL) + return (-1); + if (idx != NULL) + *idx = n; + return (1); +} + +static char * +options_array_correct_key(const char *key) +{ + u_int idx; + int numeric; + char *out; + + numeric = options_array_key_to_number(key, &idx); + if (numeric == -1) + return (NULL); + if (numeric == 1) { + xasprintf(&out, "%u", idx); + return (out); + } + return (xstrdup(key)); +} + static int options_array_cmp(struct options_array_item *a1, struct options_array_item *a2) { - if (a1->index < a2->index) + u_int i1, i2; + int n1, n2; + + n1 = options_array_key_to_number(a1->key, &i1); + n2 = options_array_key_to_number(a2->key, &i2); + if (n1 && n2) { + if (i1 < i2) + return (-1); + if (i1 > i2) + return (1); + return (0); + } + if (n1) return (-1); - if (a1->index > a2->index) + if (n2) return (1); - return (0); + return (strcmp(a1->key, a2->key)); } RB_GENERATE_STATIC(options_array, options_array_item, entry, options_array_cmp); @@ -56,6 +107,7 @@ struct options_entry { int cached; struct style style; + void *monitor_data; RB_ENTRY(options_entry) entry; }; @@ -259,6 +311,7 @@ options_default(struct options *oo, const struct options_table_entry *oe) { struct options_entry *o; union options_value *ov; + char key[32]; char *error = NULL; u_int i; @@ -270,8 +323,10 @@ options_default(struct options *oo, const struct options_table_entry *oe) options_array_assign(o, oe->default_str, NULL); return (o); } - for (i = 0; oe->default_arr[i] != NULL; i++) - options_array_set(o, i, oe->default_arr[i], 0, NULL); + for (i = 0; oe->default_arr[i] != NULL; i++) { + xsnprintf(key, sizeof key, "%u", i); + options_array_set(o, key, oe->default_arr[i], 0, NULL); + } return (o); } @@ -348,6 +403,8 @@ options_remove(struct options_entry *o) options_array_clear(o); else options_value_free(o, &o->value); + if (o->monitor_data != NULL) + hooks_monitor_free(o->monitor_data); RB_REMOVE(options_tree, &oo->tree, o); free((void *)o->name); free(o); @@ -365,6 +422,18 @@ options_owner(struct options_entry *o) return (o->owner); } +void * +options_get_monitor_data(struct options_entry *o) +{ + return (o->monitor_data); +} + +void +options_set_monitor_data(struct options_entry *o, void *data) +{ + o->monitor_data = data; +} + const struct options_table_entry * options_table_entry(struct options_entry *o) { @@ -372,21 +441,21 @@ options_table_entry(struct options_entry *o) } static struct options_array_item * -options_array_item(struct options_entry *o, u_int idx) +options_array_item(struct options_entry *o, const char *key) { struct options_array_item a; - a.index = idx; + a.key = (char *)key; return (RB_FIND(options_array, &o->value.array, &a)); } static struct options_array_item * -options_array_new(struct options_entry *o, u_int idx) +options_array_new(struct options_entry *o, const char *key) { struct options_array_item *a; a = xcalloc(1, sizeof *a); - a->index = idx; + a->key = xstrdup(key); RB_INSERT(options_array, &o->value.array, a); return (a); } @@ -396,6 +465,7 @@ options_array_free(struct options_entry *o, struct options_array_item *a) { options_value_free(o, &a->value); RB_REMOVE(options_array, &o->value.array, a); + free(a->key); free(a); } @@ -412,24 +482,45 @@ options_array_clear(struct options_entry *o) } union options_value * -options_array_get(struct options_entry *o, u_int idx) +options_array_get(struct options_entry *o, const char *key) { struct options_array_item *a; + char *new_key; if (!OPTIONS_IS_ARRAY(o)) return (NULL); - a = options_array_item(o, idx); + new_key = options_array_correct_key(key); + if (new_key == NULL) + return (NULL); + a = options_array_item(o, new_key); + free(new_key); if (a == NULL) return (NULL); return (&a->value); } +union options_value * +options_array_getv(struct options_entry *o, const char *fmt, ...) +{ + union options_value *ov; + va_list ap; + char *key; + + va_start(ap, fmt); + xvasprintf(&key, fmt, ap); + va_end(ap); + + ov = options_array_get(o, key); + free(key); + return (ov); +} + int -options_array_set(struct options_entry *o, u_int idx, const char *value, +options_array_set(struct options_entry *o, const char *key, const char *value, int append, char **cause) { struct options_array_item *a; - char *new; + char *new, *new_key; long long number; struct cmd_parse_tree *tree; char *error = NULL; @@ -440,10 +531,18 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, return (-1); } + new_key = options_array_correct_key(key); + if (new_key == NULL) { + if (cause != NULL) + xasprintf(cause, "bad array key: %s", key); + return (-1); + } + if (value == NULL) { - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a != NULL) options_array_free(o, a); + free(new_key); return (0); } @@ -454,48 +553,54 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, *cause = error; else free(error); + free(new_key); return (-1); } - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a == NULL) - a = options_array_new(o, idx); + a = options_array_new(o, new_key); else options_value_free(o, &a->value); a->value.cmd = tree; + free(new_key); return (0); } if (OPTIONS_IS_STRING(o)) { - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a != NULL && append) xasprintf(&new, "%s%s", a->value.string, value); else new = xstrdup(value); if (a == NULL) - a = options_array_new(o, idx); + a = options_array_new(o, new_key); else options_value_free(o, &a->value); a->value.string = new; + free(new_key); return (0); } if (o->tableentry->type == OPTIONS_TABLE_COLOUR) { if ((number = colour_fromstring(value)) == -1) { xasprintf(cause, "bad colour: %s", value); + free(new_key); return (-1); } - a = options_array_item(o, idx); + a = options_array_item(o, new_key); if (a == NULL) - a = options_array_new(o, idx); + a = options_array_new(o, new_key); else options_value_free(o, &a->value); a->value.number = number; + free(new_key); return (0); } if (cause != NULL) *cause = xstrdup("wrong array type"); + free(new_key); return (-1); } @@ -504,6 +609,7 @@ options_array_assign(struct options_entry *o, const char *s, char **cause) { const char *separator; char *copy, *next, *string; + char key[32]; u_int i; separator = o->tableentry->separator; @@ -513,10 +619,11 @@ options_array_assign(struct options_entry *o, const char *s, char **cause) if (*s == '\0') return (0); for (i = 0; i < UINT_MAX; i++) { - if (options_array_item(o, i) == NULL) + if (options_array_getv(o, "%u", i) == NULL) break; } - return (options_array_set(o, i, s, 0, cause)); + xsnprintf(key, sizeof key, "%u", i); + return (options_array_set(o, key, s, 0, cause)); } if (*s == '\0') @@ -526,12 +633,13 @@ options_array_assign(struct options_entry *o, const char *s, char **cause) if (*next == '\0') continue; for (i = 0; i < UINT_MAX; i++) { - if (options_array_item(o, i) == NULL) + if (options_array_getv(o, "%u", i) == NULL) break; } if (i == UINT_MAX) break; - if (options_array_set(o, i, next, 0, cause) != 0) { + xsnprintf(key, sizeof key, "%u", i); + if (options_array_set(o, key, next, 0, cause) != 0) { free(copy); return (-1); } @@ -551,13 +659,13 @@ options_array_first(struct options_entry *o) struct options_array_item * options_array_next(struct options_array_item *a) { - return (RB_NEXT(options_array, &o->value.array, a)); + return (RB_NEXT(options_array, , a)); } -u_int -options_array_item_index(struct options_array_item *a) +const char * +options_array_item_key(struct options_array_item *a) { - return (a->index); + return (a->key); } union options_value * @@ -579,15 +687,16 @@ options_is_string(struct options_entry *o) } char * -options_to_string(struct options_entry *o, int idx, int numeric) +options_to_string(struct options_entry *o, const char *key, int numeric) { struct options_array_item *a; char *result = NULL; char *last = NULL; char *next; + char *new_key; if (OPTIONS_IS_ARRAY(o)) { - if (idx == -1) { + if (key == NULL) { RB_FOREACH(a, options_array, &o->value.array) { next = options_value_to_string(o, &a->value, numeric); @@ -604,7 +713,11 @@ options_to_string(struct options_entry *o, int idx, int numeric) return (xstrdup("")); return (result); } - a = options_array_item(o, idx); + new_key = options_array_correct_key(key); + if (new_key == NULL) + return (xstrdup("")); + a = options_array_item(o, new_key); + free(new_key); if (a == NULL) return (xstrdup("")); return (options_value_to_string(o, &a->value, numeric)); @@ -613,37 +726,41 @@ options_to_string(struct options_entry *o, int idx, int numeric) } char * -options_parse(const char *name, int *idx) +options_parse(const char *name, char **key) { - char *copy, *cp, *end; + char *copy, *cp, *end, *raw, *new_key; if (*name == '\0') return (NULL); + *key = NULL; copy = xstrdup(name); if ((cp = strchr(copy, '[')) == NULL) { - *idx = -1; return (copy); } end = strchr(cp + 1, ']'); - if (end == NULL || end[1] != '\0' || !isdigit((u_char)end[-1])) { + if (end == NULL || end[1] != '\0' || end == cp + 1) { free(copy); return (NULL); } - if (sscanf(cp, "[%d]", idx) != 1 || *idx < 0) { + raw = xstrndup(cp + 1, end - (cp + 1)); + new_key = options_array_correct_key(raw); + free(raw); + if (new_key == NULL) { free(copy); return (NULL); } + *key = new_key; *cp = '\0'; return (copy); } struct options_entry * -options_parse_get(struct options *oo, const char *s, int *idx, int only) +options_parse_get(struct options *oo, const char *s, char **key, int only) { struct options_entry *o; char *name; - name = options_parse(s, idx); + name = options_parse(s, key); if (name == NULL) return (NULL); if (only) @@ -651,6 +768,10 @@ options_parse_get(struct options *oo, const char *s, int *idx, int only) else o = options_get(oo, name); free(name); + if (o == NULL) { + free(*key); + *key = NULL; + } return (o); } @@ -667,14 +788,14 @@ options_search(const char *name) } char * -options_match(const char *s, int *idx, int *ambiguous) +options_match(const char *s, char **key, int *ambiguous) { const struct options_table_entry *oe, *found; char *parsed; const char *name; size_t namelen; - parsed = options_parse(s, idx); + parsed = options_parse(s, key); if (parsed == NULL) return (NULL); if (*parsed == '@') { @@ -695,6 +816,8 @@ options_match(const char *s, int *idx, int *ambiguous) if (found != NULL) { *ambiguous = 1; free(parsed); + free(*key); + *key = NULL; return (NULL); } found = oe; @@ -703,19 +826,21 @@ options_match(const char *s, int *idx, int *ambiguous) free(parsed); if (found == NULL) { *ambiguous = 0; + free(*key); + *key = NULL; return (NULL); } return (xstrdup(found->name)); } struct options_entry * -options_match_get(struct options *oo, const char *s, int *idx, int only, +options_match_get(struct options *oo, const char *s, char **key, int only, int *ambiguous) { char *name; struct options_entry *o; - name = options_match(s, idx, ambiguous); + name = options_match(s, key, ambiguous); if (name == NULL) return (NULL); *ambiguous = 0; @@ -724,6 +849,10 @@ options_match_get(struct options *oo, const char *s, int *idx, int only, else o = options_get(oo, name); free(name); + if (o == NULL) { + free(*key); + *key = NULL; + } return (o); } @@ -1314,11 +1443,12 @@ options_push_changes(const char *name) } int -options_remove_or_default(struct options_entry *o, int idx, char **cause) +options_remove_or_default(struct options_entry *o, const char *key, + char **cause) { struct options *oo = o->owner; - if (idx == -1) { + if (key == NULL) { if (o->tableentry != NULL && (oo == global_options || oo == global_s_options || @@ -1326,7 +1456,7 @@ options_remove_or_default(struct options_entry *o, int idx, char **cause) options_default(oo, o->tableentry); else options_remove(o); - } else if (options_array_set(o, idx, NULL, 0, cause) != 0) + } else if (options_array_set(o, key, NULL, 0, cause) != 0) return (-1); return (0); } diff --git a/osdep-openbsd.c b/osdep-openbsd.c index b4c8fc56b..fbcd329b9 100644 --- a/osdep-openbsd.c +++ b/osdep-openbsd.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: procname.c,v 1.20 2022/02/22 17:35:01 deraadt Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/paste.c b/paste.c index 5ee80afd1..15e8cccc1 100644 --- a/paste.c +++ b/paste.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: paste.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -43,6 +43,16 @@ static int paste_cmp_times(const struct paste_buffer *, const struct paste_buffer *); RB_GENERATE_STATIC(paste_time_tree, paste_buffer, time_entry, paste_cmp_times); +static void +paste_fire_event(const char *name, const char *pbname) +{ + struct event_payload *ep; + + ep = event_payload_create(); + event_payload_set_string(ep, "paste_buffer", "%s", pbname); + events_fire(name, ep); +} + static int paste_cmp_names(const struct paste_buffer *a, const struct paste_buffer *b) { @@ -137,7 +147,7 @@ paste_get_name(const char *name) void paste_free(struct paste_buffer *pb) { - notify_paste_buffer(pb->name, 1); + paste_fire_event("paste-buffer-deleted", pb->name); RB_REMOVE(paste_name_tree, &paste_by_name, pb); RB_REMOVE(paste_time_tree, &paste_by_time, pb); @@ -195,7 +205,7 @@ paste_add(const char *prefix, char *data, size_t size) RB_INSERT(paste_name_tree, &paste_by_name, pb); RB_INSERT(paste_time_tree, &paste_by_time, pb); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); } /* Rename a paste buffer. */ @@ -253,8 +263,8 @@ paste_rename(const char *oldname, const char *newname, char **cause) RB_INSERT(paste_name_tree, &paste_by_name, pb); - notify_paste_buffer(oldname, 1); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-deleted", oldname); + paste_fire_event("paste-buffer-changed", pb->name); return (0); } @@ -311,7 +321,7 @@ paste_set(char *data, size_t size, const char *name, char **cause) RB_INSERT(paste_name_tree, &paste_by_name, pb); RB_INSERT(paste_time_tree, &paste_by_time, pb); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); return (0); } @@ -324,7 +334,7 @@ paste_replace(struct paste_buffer *pb, char *data, size_t size) pb->data = data; pb->size = size; - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); } /* Convert start of buffer into a nice string. */ diff --git a/popup.c b/popup.c index 26b89c613..fad0eb498 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: popup.c,v 1.75 2026/07/09 07:32:58 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -244,7 +244,7 @@ popup_check_cb(struct client* c, void *data, u_int px, u_int py, u_int nx) */ for (i = 0; i < mr->used; i++) { server_client_overlay_range(pd->px, pd->py, pd->sx, - pd->sy, r->ranges[i].px, py, r->ranges[i].nx, + pd->sy, mr->ranges[i].px, py, mr->ranges[i].nx, &pd->or[i]); } diff --git a/proc.c b/proc.c index b3a0afb25..7da1b430c 100644 --- a/proc.c +++ b/proc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott diff --git a/prompt-history.c b/prompt-history.c index ac99bdb95..ad1ab4047 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: prompt-history.c,v 1.1 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/prompt.c b/prompt.c index 1ab447ebe..6187a7a12 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: prompt.c,v 1.4 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/regress/Makefile b/regress/Makefile index 1a9ba55cb..7a4387bd3 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -1,19 +1,29 @@ TESTS!= echo *.sh +LOGDIR=logs -.PHONY: all $(TESTS) -.NOTPARALLEL: all $(TESTS) +.PHONY: all +.NOTPARALLEL: all all: - @failed=0; failures=; \ + @mkdir -p "$(LOGDIR)"; \ + rm -f "$(LOGDIR)"/*.log; \ + failed=0; failures=; \ for test in $(TESTS); do \ + base=$${test##*/}; \ + log="$(LOGDIR)/$${base%.sh}.log"; \ + rm -f "$$log"; \ printf '%-40s ' "$$test"; \ start=$$(date +%s); \ - if sh "$$test" >/dev/null 2>&1; then \ + env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \ + sh -x "$$test" >"$$log" 2>&1; \ + if [ $$? -eq 0 ]; then \ end=$$(date +%s); \ + rm -f "$$log"; \ echo "PASS ($$((end - start))s)"; \ else \ end=$$(date +%s); \ echo "FAIL ($$((end - start))s)"; \ + echo " log: $$log"; \ failed=1; \ failures="$$failures $$test"; \ fi; \ @@ -25,9 +35,7 @@ all: for test in $$failures; do \ echo " $$test"; \ done; \ + else \ + rmdir "$(LOGDIR)" 2>/dev/null || true; \ fi; \ exit $$failed - -$(TESTS): - sh $@ - sleep 1 diff --git a/regress/alerts.sh b/regress/alerts.sh new file mode 100644 index 000000000..cfb84c711 --- /dev/null +++ b/regress/alerts.sh @@ -0,0 +1,291 @@ +#!/bin/sh + +# monitor-activity, monitor-bell and monitor-silence: both the +# alert-activity, alert-bell and alert-silence hooks and the winlink alert +# flags (window_activity_flag, window_bell_flag, window_silence_flag and +# the #, !, ~ characters in window_flags). The sessions are detached so +# alert flags are set even on the current window; the *-action options +# still decide whether the hooks fire. Panes run cat: activity is +# generated by the tty echo of send-keys and a bell by sending a BEL and +# newline for cat to write back. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_for_fmt() +{ + target=$1 + fmt=$2 + expected=$3 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $fmt for $target to be '$expected' but got '$value'" +} + +assert_fmt_unchanged() +{ + target=$1 + fmt=$2 + expected=$3 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $fmt for $target to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +flags_have() +{ + target=$1 + char=$2 + + flags=$($TMUX display -pt "$target" '#{window_flags}') || + fail "display window_flags failed" + case "$flags" in + *"$char"*) ;; + *) fail "expected '$char' in window_flags for $target: '$flags'" ;; + esac +} + +flags_lack() +{ + target=$1 + char=$2 + + flags=$($TMUX display -pt "$target" '#{window_flags}') || + fail "display window_flags failed" + case "$flags" in + *"$char"*) fail "unexpected '$char' in window_flags for $target: '$flags'" ;; + esac +} + +bell() +{ + $TMUX send-keys -t "$1" -H 07 0a || fail "send-keys bell failed" +} + +activity() +{ + $TMUX send-keys -t "$1" -l x || fail "send-keys activity failed" +} + +$TMUX new -d -s mon -n w0 cat || fail "new-session mon failed" + +$TMUX set -g @log '' || fail "set @log failed" +$TMUX set-hook -g alert-bell \ + 'set -gF @log "#{@log}|alert-bell:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook alert-bell failed" +$TMUX set-hook -g alert-activity \ + 'set -gF @log "#{@log}|alert-activity:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook alert-activity failed" +$TMUX set-hook -g alert-silence \ + 'set -gF @log "#{@log}|alert-silence:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook alert-silence failed" + +# A bell in a non-current window (monitor-bell defaults to on) fires +# alert-bell and sets the bell flag, shown as ! in window_flags. +$TMUX neww -d -t mon: -n bellw cat || fail "new-window bellw failed" +assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0 +flags_lack mon:bellw '!' +bell mon:bellw +wait_for @log '|alert-bell:mon:bellw' +wait_for_fmt mon:bellw '#{window_bell_flag}' 1 +flags_have mon:bellw '!' + +# Bells are not deduplicated: a second bell fires the hook again even +# though the flag is still set. +bell mon:bellw +wait_for @log '|alert-bell:mon:bellw|alert-bell:mon:bellw' + +# Selecting the window clears the alert flags. +$TMUX selectw -t mon:bellw || fail "select-window bellw failed" +wait_for_fmt mon:bellw '#{window_bell_flag}' 0 +flags_lack mon:bellw '!' +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + +# A bell in the current window of a detached session still sets the flag +# and bell-action any (the default) fires the hook for the current window. +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:w0 +wait_for @log '|alert-bell:mon:w0' +wait_for_fmt mon:w0 '#{window_bell_flag}' 1 +$TMUX selectw -t mon:bellw || fail "select-window bellw failed" +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" +wait_for_fmt mon:w0 '#{window_bell_flag}' 0 + +# bell-action none: the flag is still set but the hook does not fire. +$TMUX set -t mon bell-action none || fail "set bell-action none failed" +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:bellw +wait_for_fmt mon:bellw '#{window_bell_flag}' 1 +assert_unchanged @log '' +$TMUX set -ut mon bell-action || fail "unset bell-action failed" +$TMUX selectw -t mon:bellw || fail "select-window bellw failed" +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + +# monitor-bell off: neither the flag nor the hook. +$TMUX set -wt mon:bellw monitor-bell off || fail "set monitor-bell failed" +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:bellw +assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0 +assert_unchanged @log '' + +# monitor-activity defaults to off: output sets neither flag nor hook. +$TMUX neww -d -t mon: -n actw cat || fail "new-window actw failed" +$TMUX set -g @log '' || fail "reset @log failed" +activity mon:actw +assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0 +assert_unchanged @log '' + +# With monitor-activity on, output fires alert-activity and sets the +# activity flag, shown as # in window_flags. +$TMUX set -wt mon:actw monitor-activity on || + fail "set monitor-activity failed" +activity mon:actw +wait_for @log '|alert-activity:mon:actw' +wait_for_fmt mon:actw '#{window_activity_flag}' 1 +flags_have mon:actw '#' + +# While the flag is set further activity does not fire the hook again. +$TMUX set -g @log '' || fail "reset @log failed" +activity mon:actw +assert_unchanged @log '' + +# On a detached session selecting the window does not clear the activity +# flag: session_set_current treats the selection itself as activity and +# the flag is raised again at once because the session is not attached. +# No hook fires as activity-action other does not apply to the current +# window. +$TMUX selectw -t mon:actw || fail "select-window actw failed" +assert_fmt_unchanged mon:actw '#{window_activity_flag}' 1 +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" +assert_fmt_unchanged mon:actw '#{window_activity_flag}' 1 +assert_unchanged @log '' + +# With a client attached, selecting the window does clear the activity +# flag and it stays clear for the current window. +mkfifo "$OUT/fifo" || fail "mkfifo failed" +$TMUX -C attach -t mon <"$OUT/fifo" >"$OUT/control.out" 2>&1 & +exec 3>"$OUT/fifo" +wait_for_fmt mon: '#{session_attached}' 1 +$TMUX selectw -t mon:actw || fail "select-window actw failed" +wait_for_fmt mon:actw '#{window_activity_flag}' 0 +flags_lack mon:actw '#' +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" +exec 3>&- +wait_for_fmt mon: '#{session_attached}' 0 + +# Once the flag is clear the alert is re-armed. +activity mon:actw +wait_for @log '|alert-activity:mon:actw' + +# Activity in the current window of a detached session sets the flag but +# activity-action other (the default) does not fire the hook. +$TMUX set -wt mon:w0 monitor-activity on || + fail "set monitor-activity w0 failed" +$TMUX set -g @log '' || fail "reset @log failed" +activity mon:w0 +wait_for_fmt mon:w0 '#{window_activity_flag}' 1 +assert_unchanged @log '' +$TMUX set -wut mon:w0 monitor-activity || + fail "unset monitor-activity w0 failed" + +# monitor-silence: a quiet window fires alert-silence after the interval +# and sets the silence flag, shown as ~ in window_flags. The hook fires +# only once while the flag remains set even though the timer keeps +# running. +$TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed" +assert_fmt_unchanged mon:silw '#{window_silence_flag}' 0 +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX set -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed" +wait_for @log '|alert-silence:mon:silw' +wait_for_fmt mon:silw '#{window_silence_flag}' 1 +flags_have mon:silw '~' +assert_unchanged @log '|alert-silence:mon:silw' +$TMUX set -wt mon:silw monitor-silence 0 || + fail "reset monitor-silence failed" +$TMUX selectw -t mon:silw || fail "select-window silw failed" +wait_for_fmt mon:silw '#{window_silence_flag}' 0 +flags_lack mon:silw '~' +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + +# A window linked into two sessions: the hook fires once per winlink and +# the flag is set in both sessions. Selecting the window in one session +# clears the flags on every winlink. +$TMUX new -d -s mon2 || fail "new-session mon2 failed" +$TMUX neww -d -t mon: -n shw cat || fail "new-window shw failed" +$TMUX link-window -d -s mon:shw -t mon2:5 || fail "link-window failed" +$TMUX set -g @log '' || fail "reset @log failed" +bell mon:shw +wait_for @log '|alert-bell:mon:shw|alert-bell:mon2:shw' +wait_for_fmt mon:shw '#{window_bell_flag}' 1 +wait_for_fmt mon2:5 '#{window_bell_flag}' 1 +$TMUX selectw -t mon2:5 || fail "select-window mon2:5 failed" +wait_for_fmt mon2:5 '#{window_bell_flag}' 0 +wait_for_fmt mon:shw '#{window_bell_flag}' 0 + +exit 0 diff --git a/regress/am-terminal.sh b/regress/am-terminal.sh index bf1cabfe7..fae0e502d 100644 --- a/regress/am-terminal.sh +++ b/regress/am-terminal.sh @@ -4,9 +4,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX2 kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/border-arrows.sh b/regress/border-arrows.sh index ea571b05a..a67171bf5 100644 --- a/regress/border-arrows.sh +++ b/regress/border-arrows.sh @@ -11,9 +11,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX_OUTER="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX_OUTER="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX_OUTER kill-server 2>/dev/null trap "$TMUX kill-server 2>/dev/null; $TMUX_OUTER kill-server 2>/dev/null" 0 1 15 diff --git a/regress/buffers.sh b/regress/buffers.sh new file mode 100644 index 000000000..fdacacb5b --- /dev/null +++ b/regress/buffers.sh @@ -0,0 +1,287 @@ +#!/bin/sh + +# Tests of paste buffer command semantics, as implemented in cmd-set-buffer.c +# (set-buffer and delete-buffer), cmd-paste-buffer.c, cmd-load-buffer.c, +# cmd-save-buffer.c (save-buffer and show-buffer), cmd-list-buffers.c and +# paste.c. +# +# This exercises: +# - set-buffer creating automatic buffers (buffer0, buffer1, ... with the +# most recent first), -b creating/replacing a named buffer, -a appending, +# -n renaming and the error paths (no data, unknown buffer); +# - show-buffer for the top and for named buffers; +# - delete-buffer for the top and named buffers, and when nothing exists; +# - list-buffers -F custom formats and -f filters; +# - paste-buffer into a pane: newline-to-CR translation by default, -r raw, +# -s custom separator, -d delete-after-paste, unknown buffer error; +# - the buffer-limit option evicting the oldest automatic buffers but not +# named buffers; +# - load-buffer/save-buffer round trips including control characters and +# UTF-8, save-buffer -a appending and errors for missing files/buffers. + +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) +TMP2=$(mktemp) +trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_buffers $expected +# +# Compare the buffer list (as "name=content ...", most recent first) with +# $expected. +check_buffers() +{ + out=$(echo $($TMUX list-buffers -F \ + '#{buffer_name}=#{buffer_sample}')) + if [ "$out" != "$1" ]; then + echo "Buffer list wrong." + echo "Expected: '$1'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_show $args $expected +# +# Compare show-buffer output with $expected. +check_show() +{ + out=$($TMUX show-buffer $1 2>&1) + if [ "$out" != "$2" ]; then + echo "show-buffer $1 wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +check_ok new-session -d -s B -x 80 -y 24 + +# --------------------------------------------------------------------------- +# set-buffer, show-buffer, delete-buffer, list-buffers. + +# Automatic buffers stack with the most recent first. +check_ok set-buffer one +check_ok set-buffer two +check_buffers 'buffer1=two buffer0=one' +check_show '' 'two' +check_show '-b buffer0' 'one' + +# -a only appends to a buffer named with -b: without -b it creates a new +# automatic buffer. Empty data is silently ignored. +check_ok set-buffer -a '!' +check_buffers 'buffer2=! buffer1=two buffer0=one' +check_ok delete-buffer +check_ok set-buffer '' +check_buffers 'buffer1=two buffer0=one' + +# -b names a buffer explicitly; setting it again replaces the content; +# -a appends to it. +check_ok set-buffer -b named abc +check_buffers 'named=abc buffer1=two buffer0=one' +check_ok set-buffer -b named xyz +check_ok set-buffer -a -b named 123 +check_buffers 'named=xyz123 buffer1=two buffer0=one' +check_show '-b named' 'xyz123' + +# -n renames; renaming to a bad source is an error, as is no data at all. +check_ok set-buffer -b named -n other +check_buffers 'other=xyz123 buffer1=two buffer0=one' +check_fail 'unknown buffer: nosuch' set-buffer -b nosuch -n foo +check_fail 'no data specified' set-buffer -b other +check_fail 'no buffer nosuch' show-buffer -b nosuch + +# list-buffers -f filters. +out=$($TMUX list-buffers -f '#{==:#{buffer_name},other}' -F '#{buffer_name}') +if [ "$out" != "other" ]; then + echo "list-buffers -f wrong: '$out'" + exit 1 +fi + +# delete-buffer -b removes one buffer; without -b the most recent automatic +# buffer goes - named buffers are not candidates for the top, for +# show-buffer and delete-buffer alike. +check_ok delete-buffer -b buffer1 +check_buffers 'other=xyz123 buffer0=one' +check_fail 'unknown buffer: buffer1' delete-buffer -b buffer1 +check_ok delete-buffer +check_buffers 'other=xyz123' +check_fail 'no buffers' show-buffer +check_fail 'no buffer' delete-buffer +check_ok delete-buffer -b other +check_fail 'no buffers' show-buffer + +# --------------------------------------------------------------------------- +# buffer-limit. + +# Only automatic buffers count against buffer-limit and the oldest are +# evicted; named buffers survive. (Automatic buffer numbers keep counting +# up over the life of the server, so compare content only.) +check_ok set-option -g buffer-limit 3 +check_ok set-buffer -b keepme precious +check_ok set-buffer a1 +check_ok set-buffer a2 +check_ok set-buffer a3 +check_ok set-buffer a4 +out=$(echo $($TMUX list-buffers -F '#{buffer_sample}')) +if [ "$out" != 'a4 a3 a2 precious' ]; then + echo "buffer-limit eviction wrong: '$out'" + exit 1 +fi +check_ok set-option -g buffer-limit 50 +check_ok delete-buffer -b keepme +check_ok delete-buffer; check_ok delete-buffer; check_ok delete-buffer + +# --------------------------------------------------------------------------- +# paste-buffer. + +# Paste into a raw, echo-free pane running cat -v so control characters are +# visible; a fresh window per paste keeps assertions simple. + +# paste_line $bufdata $pasteargs $expected +# +# Set a buffer, paste it into a fresh cat -v pane and compare the first +# screen line with $expected. +paste_line() +{ + $TMUX kill-window -t B:9 2>/dev/null + check_ok new-window -d -t B:9 'stty raw -echo && exec cat -v' + i=0 + while [ "$($TMUX display-message -p -t B:9.0 \ + '#{pane_current_command}')" != "cat" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "cat did not start."; exit 1; } + sleep 0.1 + done + check_ok set-buffer -b paste "$1" + check_ok paste-buffer $2 -b paste -t B:9.0 + i=0 + while out=$($TMUX capture-pane -p -t B:9.0 | sed -n 1p) && \ + [ "$out" != "$3" ]; do + i=$((i + 1)) + if [ $i -gt 50 ]; then + echo "Paste of '$1' ($2) wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi + sleep 0.1 + done +} + +# By default linefeeds are replaced with carriage returns (shown as ^M by +# cat -v); -r pastes raw and -s sets an explicit separator. +paste_line 'one +two' '' 'one^Mtwo' +paste_line 'one +two' '-r' 'one' +paste_line 'one +two' '-s |' 'one|two' +paste_line 'one +two' '-s XX' 'oneXXtwo' + +# -d deletes the buffer after pasting. +paste_line 'gone' '-d' 'gone' +check_fail 'no buffer paste' show-buffer -b paste + +# Unknown buffer is an error. +check_fail 'no buffer nosuch' paste-buffer -b nosuch -t B:9.0 +check_ok kill-window -t B:9 + +# --------------------------------------------------------------------------- +# load-buffer and save-buffer. + +# Round trip a file with control characters and UTF-8 through load-buffer +# and save-buffer. +printf 'line1\tx\033[31m\001\002\303\251\n' >"$TMP" +check_ok load-buffer -b file "$TMP" +check_ok save-buffer -b file "$TMP2" +if ! cmp -s "$TMP" "$TMP2"; then + echo "load-buffer/save-buffer round trip differs." + exit 1 +fi + +# save-buffer -a appends. +check_ok save-buffer -a -b file "$TMP2" +cat "$TMP" "$TMP" >"$TMP".x +if ! cmp -s "$TMP".x "$TMP2"; then + rm -f "$TMP".x + echo "save-buffer -a did not append." + exit 1 +fi +rm -f "$TMP".x + +# show-buffer prints the loaded content (text form). +check_ok delete-buffer -b file + +# load-buffer of a missing file and save-buffer of a missing buffer or to a +# bad path are errors. +check_fail "No such file or directory: $TMP.nosuch" \ + load-buffer -b x "$TMP.nosuch" +check_fail 'no buffer nosuch' save-buffer -b nosuch "$TMP2" +check_ok set-buffer -b sb data +out=$($TMUX save-buffer -b sb /nonexistent/dir/file 2>&1) +if [ $? -eq 0 ]; then + echo "save-buffer to bad path succeeded." + exit 1 +fi + +# save-buffer - writes to stdout and load-buffer - reads from stdin. +out=$($TMUX save-buffer -b sb -) +if [ "$out" != "data" ]; then + echo "save-buffer - wrong: '$out'" + exit 1 +fi +check_ok delete-buffer -b sb +printf 'from stdin' | $TMUX load-buffer -b stdinbuf - +check_show '-b stdinbuf' 'from stdin' +check_ok delete-buffer -b stdinbuf + +assert_alive + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/capture-pane-hyperlink.sh b/regress/capture-pane-hyperlink.sh index 80092e7f2..ec89c6c5d 100644 --- a/regress/capture-pane-hyperlink.sh +++ b/regress/capture-pane-hyperlink.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMP=$(mktemp) TMP2=$(mktemp) trap "rm -f $TMP $TMP2" 0 1 15 diff --git a/regress/capture-pane-sgr0.sh b/regress/capture-pane-sgr0.sh index 7c9c32315..1e73e7191 100644 --- a/regress/capture-pane-sgr0.sh +++ b/regress/capture-pane-sgr0.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/check-names.sh b/regress/check-names.sh index 886299f2d..be8ad617b 100644 --- a/regress/check-names.sh +++ b/regress/check-names.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null fail() diff --git a/regress/choose-buffer.sh b/regress/choose-buffer.sh new file mode 100644 index 000000000..ca9a26758 --- /dev/null +++ b/regress/choose-buffer.sh @@ -0,0 +1,271 @@ +#!/bin/sh + +# Tests of buffer mode (window-buffer.c) as driven by choose-buffer: that the +# -f filter removes buffers that do not match (by name and by content), that +# a filter matching nothing falls back to showing everything, that -O and -r +# change the sort order, that d deletes the selected buffer and C-t and D +# delete all tagged buffers, and that Enter runs the default command +# (paste-buffer) with the selected buffer. +# +# The list is drawn on a mode screen which capture-pane does not show, so a +# second server provides a client: an inner "tmux attach" runs inside a pane +# of the second server, and that pane is captured to read what the inner +# client rendered. Each choose-buffer call uses a distinct -F marker so a +# capture can be tied to the call it belongs to. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + exit 1 +} + +# capture the screen rendered by the inner client +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_for $marker +# +# Wait (up to ~10s) until the rendered screen contains $marker, so the +# capture is known to show the mode instance under test. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +# wait_gone $marker +# +# Wait (up to ~10s) until the rendered screen no longer contains $marker. +wait_gone() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' to disappear" +} + +# wait_count $marker $n +# +# Wait (up to ~10s) until exactly $n rendered lines contain $marker. The +# matching capture is left in CAPTURED. +wait_count() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1") + [ "$c" -eq "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $2 lines of '$1' (have $c)" +} + +# wait_buffers $n +# +# Wait (up to ~10s) until the test server has exactly $n paste buffers. +wait_buffers() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-buffers -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.5 + i=$((i + 1)) + done + fail "expected $1 buffers, have $c" +} + +# wait_clients $n +# +# Wait (up to ~10s) until the test server has exactly $n clients attached. +wait_clients() +{ + i=0 + while [ "$i" -lt 10 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 1 + i=$((i + 1)) + done + return 1 +} + +# wait_mode $target $state +# +# Wait (up to ~10s) until a pane enters or leaves mode. +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +exit_mode() +{ + marker=$1 + shift + + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" + wait_mode aaa:0 0 + wait_gone "$marker" +} + +$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1 + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 +wait_clients 1 || fail "no client attached to test server" + +# Two named buffers with distinct contents; bufa is created first. +$TMUX set-buffer -b bufa "hello buffer" || exit 1 +$TMUX set-buffer -b bufz "other buffer" || exit 1 + +# --- filter by buffer name --------------------------------------------------- +$TMUX choose-buffer -t aaa:0 -F 'B1' -f '#{==:#{buffer_name},bufa}' || exit 1 +wait_count ': B1' 1 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'bufa: B1' || \ + fail "bufa missing when it matches" +printf '%s\n' "$out" | grep -F -q 'bufz: B1' && \ + fail "bufz shown but does not match" +[ "$(printf '%s\n' "$out" | grep -F -c ': B1')" -eq 1 ] || \ + fail "expected 1 buffer" +exit_mode ': B1' q + +# --- filter by buffer content ------------------------------------------------ +$TMUX choose-buffer -t aaa:0 -F 'B2' -f '#{m:*hello*,#{buffer_sample}}' || \ + exit 1 +wait_count ': B2' 1 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'bufa: B2' || \ + fail "bufa missing when content matches" +printf '%s\n' "$out" | grep -F -q 'bufz: B2' && \ + fail "bufz shown but content not matched" +exit_mode ': B2' q + +# --- no filter shows both buffers --------------------------------------------- +$TMUX choose-buffer -t aaa:0 -F 'B3' || exit 1 +wait_count ': B3' 2 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'bufa: B3' || \ + fail "bufa missing with no filter" +printf '%s\n' "$out" | grep -F -q 'bufz: B3' || \ + fail "bufz missing with no filter" +[ "$(printf '%s\n' "$out" | grep -F -c ': B3')" -eq 2 ] || \ + fail "expected 2 buffers" +exit_mode ': B3' q + +# --- filter matching nothing --------------------------------------------------- +# +# Everything is shown and the filter indicator reports no matches. +$TMUX choose-buffer -t aaa:0 -F 'B4' -f '#{==:#{buffer_name},nosuch}' || \ + exit 1 +wait_count ': B4' 2 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'bufa: B4' || \ + fail "bufa missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'bufz: B4' || \ + fail "bufz missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'no matches' || \ + fail "no matches indicator missing" +exit_mode ': B4' q + +# --- sort orders --------------------------------------------------------------- +# +# By name bufa sorts first and -r reverses. +$TMUX choose-buffer -t aaa:0 -F 'B5' -O name || exit 1 +wait_count ': B5' 2 +printf '%s\n' "$CAPTURED" | grep -F ': B5' | head -1 | \ + grep -F -q 'bufa: B5' || \ + fail "bufa not first with -O name" +exit_mode ': B5' q + +$TMUX choose-buffer -t aaa:0 -F 'B6' -O name -r || exit 1 +wait_count ': B6' 2 +printf '%s\n' "$CAPTURED" | grep -F ': B6' | head -1 | \ + grep -F -q 'bufz: B6' || \ + fail "bufz not first with -O name -r" +exit_mode ': B6' q + +# --- d deletes the selected buffer -------------------------------------------- +# +# The filter leaves only bufz listed and selected; d deletes it. +$TMUX choose-buffer -t aaa:0 -F 'G1' -f '#{==:#{buffer_name},bufz}' || exit 1 +wait_for 'bufz: G1' +$TMUX send-keys -t aaa:0 d +wait_buffers 1 +$TMUX list-buffers -F '#{buffer_name}' | grep -F -q 'bufa' || \ + fail "wrong buffer deleted" +exit_mode ': G1' q + +# --- C-t tags all buffers and D deletes the tagged ------------------------------ +$TMUX set-buffer -b bufz "other buffer" || exit 1 +$TMUX set-buffer -b bufb "third buffer" || exit 1 +$TMUX choose-buffer -t aaa:0 -F 'G2' || exit 1 +wait_count ': G2' 3 +$TMUX send-keys -t aaa:0 C-t D +wait_buffers 0 +wait_mode aaa:0 0 +wait_gone ': G2' + +# --- Enter runs the default command (paste-buffer) ------------------------------ +# +# The only buffer is listed and selected; Enter leaves the mode and pastes it +# into the shell in the pane, where it appears on the screen. +$TMUX set-buffer -b bufa "hello buffer" || exit 1 +$TMUX choose-buffer -t aaa:0 -F 'G3' || exit 1 +wait_count ': G3' 1 +exit_mode ': G3' Enter +i=0 +while [ "$i" -lt 50 ]; do + $TMUX capture-pane -p -t aaa:0 | grep -F -q 'hello buffer' && \ + break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "buffer not pasted into pane" + +exit 0 diff --git a/regress/choose-client.sh b/regress/choose-client.sh new file mode 100644 index 000000000..d325f5335 --- /dev/null +++ b/regress/choose-client.sh @@ -0,0 +1,218 @@ +#!/bin/sh + +# Tests of client mode (window-client.c) as driven by choose-client: that the +# -f filter removes clients that do not match, that a filter matching nothing +# falls back to showing everything, and that Enter runs the default command +# (detach-client) on the selected client. Sort orders are not tested here +# because clients are named after their ttys, which are not predictable. +# +# The list is drawn on a mode screen which capture-pane does not show, so a +# second server provides the clients: two inner "tmux attach" commands run in +# panes of the second server, and the pane holding the client the mode is +# displayed on is captured. Each choose-client call uses a distinct -F marker +# so a capture can be tied to the call it belongs to. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + exit 1 +} + +# capture the screen rendered by the inner client attached to aaa +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_for $marker +# +# Wait (up to ~10s) until the rendered screen contains $marker, so the +# capture is known to show the mode instance under test. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +# wait_gone $marker +# +# Wait (up to ~10s) until the rendered screen no longer contains $marker. +wait_gone() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' to disappear" +} + +# wait_count $marker $n +# +# Wait (up to ~10s) until exactly $n rendered lines contain $marker. The +# matching capture is left in CAPTURED. +wait_count() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1") + [ "$c" -eq "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $2 lines of '$1' (have $c)" +} + +# wait_clients $n +# +# Wait (up to ~10s) until the test server has exactly $n clients attached. +wait_clients() +{ + i=0 + while [ "$i" -lt 10 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 1 + i=$((i + 1)) + done + return 1 +} + +# wait_mode $target $state +# +# Wait (up to ~10s) until a pane enters or leaves mode. +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +exit_mode() +{ + marker=$1 + shift + + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" + wait_mode aaa:0 0 + wait_gone "$marker" +} + +# One client attached to each of two sessions; the mode is displayed on the +# client attached to aaa (in window 0 of the outer server) and the filters +# tell the clients apart by their attached session. +$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1 +$TMUX new-session -d -s bbb -x 80 -y 24 'cat' || exit 1 + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 +$TMUX2 new-window -d -t out: "$TMUX attach -t bbb" || exit 1 +wait_clients 2 || fail "expected two clients attached to test server" + +# --- filter keeping only the aaa client ------------------------------------- +$TMUX choose-client -t aaa:0 -F 'C1=#{client_session}' \ + -f '#{==:#{client_session},aaa}' || exit 1 +wait_count 'C1=' 1 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'C1=aaa' || \ + fail "aaa client missing when it matches" +printf '%s\n' "$out" | grep -F -q 'C1=bbb' && \ + fail "bbb client shown but does not match" +[ "$(printf '%s\n' "$out" | grep -F -c 'C1=')" -eq 1 ] || \ + fail "expected 1 client" +exit_mode 'C1=' q + +# --- filter keeping only the bbb client ------------------------------------- +$TMUX choose-client -t aaa:0 -F 'C2=#{client_session}' \ + -f '#{==:#{client_session},bbb}' || exit 1 +wait_count 'C2=' 1 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'C2=bbb' || \ + fail "bbb client missing when it matches" +printf '%s\n' "$out" | grep -F -q 'C2=aaa' && \ + fail "aaa client shown but does not match" +[ "$(printf '%s\n' "$out" | grep -F -c 'C2=')" -eq 1 ] || \ + fail "expected 1 client" +exit_mode 'C2=' q + +# --- no filter shows both clients ------------------------------------------- +$TMUX choose-client -t aaa:0 -F 'C3=#{client_session}' || exit 1 +wait_count 'C3=' 2 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'C3=aaa' || \ + fail "aaa client missing with no filter" +printf '%s\n' "$out" | grep -F -q 'C3=bbb' || \ + fail "bbb client missing with no filter" +[ "$(printf '%s\n' "$out" | grep -F -c 'C3=')" -eq 2 ] || \ + fail "expected 2 clients" +exit_mode 'C3=' q + +# --- filter matching nothing ------------------------------------------------ +# +# Everything is shown and the filter indicator reports no matches. +$TMUX choose-client -t aaa:0 -F 'C4=#{client_session}' \ + -f '#{==:#{client_session},nosuch}' || exit 1 +wait_count 'C4=' 2 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'C4=aaa' || \ + fail "aaa client missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'C4=bbb' || \ + fail "bbb client missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'no matches' || \ + fail "no matches indicator missing" +exit_mode 'C4=' q + +# --- Enter runs the default command (detach-client) -------------------------- +# +# The filter leaves only the bbb client listed and selected; Enter detaches +# it, leaving only the aaa client attached. +$TMUX choose-client -t aaa:0 -F 'G1=#{client_session}' \ + -f '#{==:#{client_session},bbb}' || exit 1 +wait_for 'G1=bbb' +$TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed" +wait_clients 1 || fail "bbb client did not detach" +wait_mode aaa:0 0 +wait_gone 'G1=' +[ "$($TMUX list-clients -F '#{client_session}')" = "aaa" ] || \ + fail "wrong client detached" + +exit 0 diff --git a/regress/choose-tree.sh b/regress/choose-tree.sh new file mode 100644 index 000000000..132890730 --- /dev/null +++ b/regress/choose-tree.sh @@ -0,0 +1,293 @@ +#!/bin/sh + +# Tests of tree mode (window-tree.c) as driven by choose-tree. +# +# Filtering: the -f filter is applied per pane and removes panes, windows and +# sessions with no matching panes (a window with more than one pane and no +# matching panes must disappear - GitHub issue 5326); -h keeps a window +# listed when its only matching pane is the pane the tree is drawn in; a +# filter matching nothing falls back to showing everything. +# +# Sorting: -O and -r change the sort order. +# +# Keys: h and l collapse and expand; f prompts for a filter and c clears it; +# g goes to the top; Enter runs the default command (switch-client); x kills +# the current item after a confirmation prompt. +# +# The tree is drawn on a mode screen which capture-pane does not show, so - as +# in environ-update.sh - a second server provides a client: an inner "tmux +# attach" runs inside a pane of the second server, and that pane is captured +# to read what the inner client rendered. Each choose-tree call uses a +# distinct -F marker so a capture can be tied to the call it belongs to. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + exit 1 +} + +# capture the screen rendered by the inner client +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_for $marker +# +# Wait (up to ~10s) until the rendered screen contains $marker, so the +# capture is known to show the mode instance under test. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +# wait_count $row-marker $n +# +# Wait (up to ~10s) until exactly $n rendered list rows contain $row-marker. +wait_count() +{ + i=0 + while [ "$i" -lt 50 ]; do + CAPTURED=$(capture) + c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1") + [ "$c" -eq "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $2 lines of '$1' (have $c)" +} + +# wait_clients $n +# +# Wait (up to ~10s) until the test server has exactly $n clients attached. +wait_clients() +{ + i=0 + while [ "$i" -lt 10 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 1 + i=$((i + 1)) + done + return 1 +} + +# wait_mode $target $state +# +# Wait (up to ~10s) until a pane enters or leaves mode. +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +exit_mode() +{ + $TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed" + wait_mode aaa:0 0 +} + +# Session zzz is created first, so it sorts first by index, and has a +# two-pane window 0 and a single-pane window 1. Session aaa has one window +# with one pane and is where the tree is displayed. With everything expanded +# and no filter the tree is nine lines: +# +# 0 zzz 1 window 0 2 pane 0 3 pane 1 4 window 1 5 pane 0 +# 6 aaa 7 window 0 8 pane 0 +$TMUX new-session -d -s zzz -x 80 -y 24 'cat' || exit 1 +$TMUX split-window -t zzz:0 'cat' || exit 1 +$TMUX new-window -t zzz 'cat' || exit 1 +$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1 + +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1 +wait_clients 1 || fail "no client attached to test server" + +# --- filter keeping only aaa ------------------------------------------------ +# +# zzz must disappear entirely: its single-pane window 1 and - the GitHub 5326 +# regression - its two-pane window 0. aaa contributes exactly three lines +# (session, window, pane). +$TMUX choose-tree -t aaa:0 -F 'F1' -f '#{==:#{session_name},aaa}' || exit 1 +wait_count ': F1' 3 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'aaa: F1' || \ + fail "aaa missing when filter matches it" +printf '%s\n' "$out" | grep -F -q 'zzz: F1' && \ + fail "zzz shown but no pane matches" +exit_mode q + +# --- filter keeping only zzz ------------------------------------------------ +# +# zzz contributes six lines (session, two windows, three panes); aaa must +# disappear. +$TMUX choose-tree -t aaa:0 -F 'F2' -f '#{==:#{session_name},zzz}' || exit 1 +wait_count ': F2' 6 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'zzz: F2' || \ + fail "zzz missing when filter matches it" +printf '%s\n' "$out" | grep -F -q 'aaa: F2' && \ + fail "aaa shown but no pane matches" +exit_mode q + +# --- filter matching a single pane ------------------------------------------ +# +# Only pane 1 of zzz:0 matches, so the tree is exactly session zzz, window 0 +# and that pane; zzz:1 and all of aaa must disappear. +$TMUX choose-tree -t aaa:0 -F 'F3' -f '#{==:#{pane_index},1}' || exit 1 +wait_count ': F3' 3 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'zzz: F3' || \ + fail "zzz missing when its pane matches" +printf '%s\n' "$out" | grep -F -q 'aaa: F3' && \ + fail "aaa shown but no pane matches" +printf '%s\n' "$out" | grep -F -q '1: F3' || \ + fail "matching pane missing" +exit_mode q + +# --- filter matching nothing ------------------------------------------------ +# +# Everything is shown and the filter indicator reports no matches. +$TMUX choose-tree -t aaa:0 -F 'F4' -f '#{==:#{session_name},nosuch}' || exit 1 +wait_count ': F4' 9 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'aaa: F4' || \ + fail "aaa missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'zzz: F4' || \ + fail "zzz missing with no-match filter" +printf '%s\n' "$out" | grep -F -q 'no matches' || \ + fail "no matches indicator missing" +exit_mode q + +# --- -h with the tree pane as the only match -------------------------------- +# +# With -h the pane the tree is drawn in is hidden, but it still counts as a +# match, so session and window aaa stay listed: two lines, no pane line. +$TMUX choose-tree -h -t aaa:0 -F 'F5' -f '#{==:#{session_name},aaa}' || \ + exit 1 +wait_count ': F5' 2 +printf '%s\n' "$CAPTURED" | grep -F -q 'aaa: F5' || \ + fail "aaa missing with -h" +exit_mode q + +# --- sort orders ------------------------------------------------------------ +# +# By index zzz (created first) sorts first, by name aaa does, and -r reverses. +$TMUX choose-tree -t aaa:0 -F 'F6' -O index || exit 1 +wait_count ': F6' 9 +printf '%s\n' "$CAPTURED" | grep -F ': F6' | head -1 | \ + grep -F -q 'zzz: F6' || \ + fail "zzz not first with -O index" +exit_mode q + +$TMUX choose-tree -t aaa:0 -F 'F7' -O name || exit 1 +wait_count ': F7' 9 +printf '%s\n' "$CAPTURED" | grep -F ': F7' | head -1 | \ + grep -F -q 'aaa: F7' || \ + fail "aaa not first with -O name" +exit_mode q + +$TMUX choose-tree -t aaa:0 -F 'F8' -O name -r || exit 1 +wait_count ': F8' 9 +printf '%s\n' "$CAPTURED" | grep -F ': F8' | head -1 | \ + grep -F -q 'zzz: F8' || \ + fail "zzz not first with -O name -r" +exit_mode q + +# --- collapse and expand with h and l ----------------------------------------- +# +# g moves to the top (session zzz); h collapses it, hiding its five children; +# l expands it again. +$TMUX choose-tree -t aaa:0 -F 'G1' -O index || exit 1 +wait_count ': G1' 9 +$TMUX send-keys -t aaa:0 g h || fail "send-keys collapse failed" +wait_count ': G1' 4 +$TMUX send-keys -t aaa:0 l || fail "send-keys expand failed" +wait_count ': G1' 9 +exit_mode q + +# --- filter entered at the prompt with f, cleared with c ---------------------- +$TMUX choose-tree -t aaa:0 -F 'G2' -O index || exit 1 +wait_count ': G2' 9 +$TMUX send-keys -t aaa:0 f || fail "send-keys f failed" +$TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}' || \ + fail "send-keys filter failed" +$TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed" +wait_count ': G2' 3 +out=$CAPTURED +printf '%s\n' "$out" | grep -F -q 'aaa: G2' || \ + fail "aaa missing with prompt filter" +printf '%s\n' "$out" | grep -F -q 'zzz: G2' && \ + fail "zzz shown with prompt filter" +$TMUX send-keys -t aaa:0 c || fail "send-keys c failed" +wait_count ': G2' 9 +exit_mode q + +# --- Enter runs the default command (switch-client) ---------------------------- +# +# g selects session zzz and Enter switches the client to it. +$TMUX choose-tree -t aaa:0 -F 'G3' -O index || exit 1 +wait_count ': G3' 9 +$TMUX send-keys -t aaa:0 g Enter || fail "send-keys Enter failed" +i=0 +while [ "$i" -lt 50 ]; do + [ "$($TMUX list-clients -F '#{client_session}')" = "zzz" ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "client did not switch to zzz" +$TMUX switch-client -c "$($TMUX list-clients -F '#{client_name}')" -t aaa || \ + exit 1 +wait_mode aaa:0 0 + +# --- x kills the current item after confirmation ------------------------------- +# +# g and four times j select window 1 of zzz; x asks for confirmation and y +# kills it, leaving zzz with one window and the tree with seven lines. +$TMUX choose-tree -t aaa:0 -F 'G4' -O index || exit 1 +wait_count ': G4' 9 +$TMUX send-keys -t aaa:0 g j j j j x || fail "send-keys x failed" +wait_for 'Kill window 1' +$TMUX send-keys -t aaa:0 y || fail "send-keys y failed" +wait_count ': G4' 7 +[ "$($TMUX list-windows -t zzz -F x | grep -c x)" -eq 1 ] || \ + fail "window 1 of zzz not killed" +exit_mode q + +exit 0 diff --git a/regress/combine-test.sh b/regress/combine-test.sh index a4d4f3681..ac9f800b0 100644 --- a/regress/combine-test.sh +++ b/regress/combine-test.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/command-alias.sh b/regress/command-alias.sh index 632e959a2..aae434cc0 100644 --- a/regress/command-alias.sh +++ b/regress/command-alias.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX new-session -d -sfoo || exit 1 diff --git a/regress/command-order.sh b/regress/command-order.sh index 775768a6a..cddd9b29e 100644 --- a/regress/command-order.sh +++ b/regress/command-order.sh @@ -4,11 +4,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap 'rm -f "$TMP"; $TMUX kill-server 2>/dev/null' 0 1 15 cat <$TMP new -sfoo -nfoo0; neww -nfoo1; neww -nfoo2 @@ -27,6 +26,7 @@ foo,foo1 foo,foo2 EOF +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" cat <$TMP new -sfoo -nfoo0 neww -nfoo1 diff --git a/regress/conf-syntax.sh b/regress/conf-syntax.sh index 49b9710b3..562ac76ad 100644 --- a/regress/conf-syntax.sh +++ b/regress/conf-syntax.sh @@ -4,10 +4,13 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX= +n=0 +trap '[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null' 0 1 15 for i in conf/*.conf; do + n=$((n + 1)) + TMUX="$TEST_TMUX -LtestA$$-$n -f/dev/null" $TMUX -f/dev/null start \; source -n $i || exit 1 done diff --git a/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf b/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf index c3a4da4b3..676a001a1 100644 --- a/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf +++ b/regress/conf/2eae5d47049c1f6d0bef3db4e171aed7.conf @@ -35,7 +35,7 @@ bind -n C-h run "(tmux display-message -p '#{pane_current_command}' | grep -iq v bind -n C-j run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-j) || tmux select-pane -D" bind -n C-k run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-k) || tmux select-pane -U" bind -n C-l run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-l) || tmux select-pane -R" -bind -n C-\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l" +bind -n C-\\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l" # C-l is taken oer by vim style pane navigation bind C-l send-keys 'C-l' diff --git a/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf b/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf index 0c41caa49..b24bab51b 100644 --- a/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf +++ b/regress/conf/ad21dbb0893240563ddfdd954b9903a1.conf @@ -183,7 +183,7 @@ bind t swap-window -t 1 # swap the current window's position with window # 1, unbind & # unbind default binding for `split-window -h` bind - split-window -v -c '#{pane_current_path}' # vertical split bind _ split-window -v -c '#{pane_current_path}' -f # full vertical split (v2.3+) -bind \ split-window -h -c '#{pane_current_path}' # horizontal split +bind \\ split-window -h -c '#{pane_current_path}' # horizontal split bind | split-window -h -c '#{pane_current_path}' -f # full horizontal split (v2.3+) # https://www.reddit.com/r/tmux/comments/3paqoi/tmux_21_has_been_released/cw5wy00 bind w switch-client -Tsplit_wind diff --git a/regress/control-client-exit.sh b/regress/control-client-exit.sh new file mode 100644 index 000000000..ac08db5cc --- /dev/null +++ b/regress/control-client-exit.sh @@ -0,0 +1,111 @@ +#!/bin/sh + +# Once a control client is marked for exit (by detach-client here, or by the +# "too far behind" eviction in control_check_age), the server must stop +# queueing new output and notifications for it. The exit handshake only waits +# for already-queued blocks to drain (control_all_done), so anything queued +# after that point is written to a client that will never read it, and can hold +# the handshake - and the pane buffer it pins - open. This checks that a +# notification generated while the client is exiting never reaches its output. +# +# To see this in seconds rather than waiting out CONTROL_MAXIMUM_AGE (300s) the +# control client's output goes down a fifo that a helper drains only slowly. +# The slow drain keeps a backlog stuck in the server so the exit handshake +# cannot complete; while it is stuck we detach the client (which sets the exit +# flag) and rename the window to a marker. Draining the fifo must then not turn +# up the marker as a %window-renamed line. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +DIR=$(mktemp -d) +FIFO=$DIR/fifo +OUT=$DIR/out +MARKER=exitmarker +READER= + +mkfifo "$FIFO" || exit 1 +: >"$OUT" + +cleanup() { + [ -n "$READER" ] && kill "$READER" 2>/dev/null + $TMUX kill-server 2>/dev/null + exec 8<&- 2>/dev/null + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +# A detached session whose pane floods printable output forever. +$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1 +$TMUX setw -g automatic-rename off || exit 1 + +# Attach a control client: output to the fifo, stdin held open by sleep so it +# stays attached while we drive the session from outside. Open the read end +# after the client has opened the write end. +( sleep 30 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 & +exec 8<"$FIFO" + +n=0 +while [ $n -lt 50 ]; do + $TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break + sleep 0.1 + n=$((n + 1)) +done +$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . || + { echo "control client did not attach"; exit 1; } + +# Drain the control stream slowly (~2.5KB/s). The pane floods far faster, so a +# backlog stays stuck in the server and the fifo stays full, which is what +# keeps the exit handshake from completing below. +( while :; do + dd bs=256 count=1 <&8 >>"$OUT" 2>/dev/null || exit 0 + sleep 0.1 +done ) & +READER=$! + +# Wait until the client is demonstrably backed up: the output only keeps +# growing at the slow-drain rate while the flood is outrunning the reader. +n=0 +while [ $n -lt 100 ]; do + [ "$(wc -c <"$OUT")" -ge 8000 ] && break + sleep 0.1 + n=$((n + 1)) +done +[ "$(wc -c <"$OUT")" -ge 8000 ] || { echo "client output did not back up"; exit 1; } + +# Detach the control client: this sets CLIENT_EXIT immediately. The stuck +# backlog holds the window between CLIENT_EXIT and the exit handshake open. +$TMUX detach-client -s rt + +# While the client is exiting, rename the window to the marker. Vanilla queues +# these %window-renamed notifications for the exiting client; the fix drops them. +i=0 +while [ $i -lt 5 ]; do + $TMUX rename-window -t rt:0 "$MARKER$i" + i=$((i + 1)) +done +sleep 0.5 + +# Drain fast (bounded) so the backlog, and any notification queued behind it, +# reaches OUT. A watchdog keeps this from blocking. +kill "$READER" 2>/dev/null +READER= +( dd bs=4096 count=128 <&8 >>"$OUT" 2>/dev/null ) & +DRAIN=$! +( sleep 3; kill "$DRAIN" 2>/dev/null ) & +WATCHDOG=$! +wait "$DRAIN" 2>/dev/null +kill "$WATCHDOG" 2>/dev/null + +# The marker must never appear as a notification to the exiting client. +if grep -q "%window-renamed .*$MARKER" "$OUT"; then + echo "notification queued to exiting control client:" + grep "%window-renamed .*$MARKER" "$OUT" + exit 1 +fi + +exit 0 diff --git a/regress/control-client-sanity.sh b/regress/control-client-sanity.sh index f6c36e1bd..32695e7c0 100644 --- a/regress/control-client-sanity.sh +++ b/regress/control-client-sanity.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/control-client-size.sh b/regress/control-client-size.sh index 7d5e1dc27..3099b873f 100644 --- a/regress/control-client-size.sh +++ b/regress/control-client-size.sh @@ -8,12 +8,11 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) OUT=$(mktemp) -trap "rm -f $TMP $OUT" 0 1 15 +trap 'rm -f "$TMP" "$OUT"; $TMUX kill-server 2>/dev/null' 0 1 15 $TMUX -f/dev/null new -d || exit 1 sleep 1 @@ -26,6 +25,7 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":80 24\n:100 50\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" $TMUX -f/dev/null new -d || exit 1 sleep 1 cat <$TMP @@ -37,6 +37,7 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":80 24\n:80 24\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-3 -f/dev/null" cat <$TMP ls -F':#{window_width} #{window_height}' refresh -C 80,24 diff --git a/regress/control-client-wait-exit.sh b/regress/control-client-wait-exit.sh new file mode 100644 index 000000000..63739b284 --- /dev/null +++ b/regress/control-client-wait-exit.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# A control client with the wait-exit flag lingers after %exit until its input +# sends an empty line or closes. The terminating empty line must be honoured +# even when several lines arrive in a single read (as from a pipe or a raw +# terminal): reading input with stdio would pull them all into the FILE buffer +# and leave the empty line where poll never sees it, hanging the client. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest" +$TMUX kill-server 2>/dev/null + +FIFO=$(mktemp -u) +OUT=$(mktemp) +mkfifo "$FIFO" || exit 1 +trap "$TMUX kill-server 2>/dev/null; rm -f $FIFO $OUT" 0 1 15 + +# Start a control client that reads its input from the fifo. Keep the write +# end open (fd 3) so the client never sees EOF: it must exit on the empty +# line, not on the pipe closing. +$TMUX -f/dev/null -C new -s wait-exit <"$FIFO" >"$OUT" 2>&1 & +CLIENT=$! +exec 3>"$FIFO" +sleep 1 + +# Ask to linger after exit, then detach so the client prints %exit and enters +# the wait-exit loop. +printf 'refresh-client -f wait-exit\n' >&3 +sleep 1 +$TMUX detach-client -s wait-exit + +# Wait for the client to print %exit and enter the wait-exit loop. +i=0 +while [ $i -lt 5 ]; do + grep -q '^%exit' "$OUT" && break + sleep 1 + i=$((i + 1)) +done +grep -q '^%exit' "$OUT" || exit 1 + +# Deliver the terminating empty line in one write, after a non-empty line, so +# a single read returns "a\n" and "\n" together. The empty line must still end +# the wait. +printf 'a\n\n' >&3 + +# The client should exit promptly. If it is still alive after the timeout the +# empty line was lost in a buffer and the client has hung. +i=0 +while [ $i -lt 10 ]; do + kill -0 $CLIENT 2>/dev/null || break + sleep 1 + i=$((i + 1)) +done +if kill -0 $CLIENT 2>/dev/null; then + kill $CLIENT 2>/dev/null + exit 1 +fi +exec 3>&- + +exit 0 diff --git a/regress/control-notify-events.sh b/regress/control-notify-events.sh new file mode 100644 index 000000000..ab696f5e8 --- /dev/null +++ b/regress/control-notify-events.sh @@ -0,0 +1,102 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +PID= + +cleanup() +{ + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +wait_for() +{ + pattern=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing: $pattern" + cat "$OUT" + return 1 +} + +wait_for_count() +{ + pattern=$1 + expected=$2 + timeout=${3:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + count=$(grep -F -- "$pattern" "$OUT" 2>/dev/null | wc -l) + if [ "$count" -ge "$expected" ]; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing count $expected for: $pattern" + cat "$OUT" + return 1 +} + +send() +{ + printf '%s\n' "$*" >&3 +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s notify -x 80 -y 24 || exit 1 +wid=$($TMUX display-message -p -t notify:0 '#{window_id}') || exit 1 +pane=$($TMUX display-message -p -t notify:0.0 '#{pane_id}') || exit 1 + +mkfifo "$IN" +: >"$OUT" +$TMUX -C attach-session -t notify <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +send 'display-message -p ready' +wait_for 'ready' 3 || exit 1 + +send 'rename-window notify-renamed' +wait_for "%window-renamed $wid notify-renamed" || exit 1 + +send 'split-window -h' +wait_for "%layout-change $wid " || exit 1 + +send 'select-pane -t:.0' +wait_for "%window-pane-changed $wid $pane" || exit 1 + +send 'copy-mode -t:.0' +wait_for "%pane-mode-changed $pane" || exit 1 + +sessions_changed=$(grep -F -- '%sessions-changed' "$OUT" 2>/dev/null | \ + wc -l) +send 'new-session -d -s notify-extra' +wait_for_count '%sessions-changed' $((sessions_changed + 1)) || exit 1 + +send 'kill-session -t notify-extra' +wait_for_count '%sessions-changed' $((sessions_changed + 2)) || exit 1 + +exit 0 diff --git a/regress/control-subscriptions.sh b/regress/control-subscriptions.sh new file mode 100644 index 000000000..8aebb2ea0 --- /dev/null +++ b/regress/control-subscriptions.sh @@ -0,0 +1,172 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +PID= + +cleanup() +{ + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +wait_for() +{ + pattern=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing: $pattern" + cat "$OUT" + return 1 +} + +reject_for() +{ + pattern=$1 + timeout=${2:-3} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + echo "unexpected: $pattern" + cat "$OUT" + return 1 + fi + sleep 1 + i=$((i + 1)) + done + return 0 +} + +wait_tmux() +{ + target=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if $TMUX display-message -p -t "$target" '#{window_id}' \ + >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + return 1 +} + +window_fields() +{ + $TMUX display-message -p -t "$1" '#{window_id} #{window_index}' +} + +pane_fields() +{ + $TMUX display-message -p -t "$1" '#{window_id} #{window_index} #{pane_id}' +} + +send() +{ + printf '%s\n' "$*" >&3 +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s subs -x 80 -y 24 || exit 1 +sid=$($TMUX display-message -p -t subs '#{session_id}') + +mkfifo "$IN" +: >"$OUT" +$TMUX -C attach-session -t subs <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +send 'display-message -p ready' +wait_for 'ready' 3 || exit 1 + +send "refresh-client -B 'sw::#{session_windows}'" +wait_for "%subscription-changed sw $sid - - - : 1" || exit 1 + +send 'new-window' +wait_for "%subscription-changed sw $sid - - - : 2" || exit 1 + +send 'refresh-client -B sw' +send 'new-window' +reject_for "%subscription-changed sw $sid - - - : 3" || exit 1 + +send 'new-window -n pane-test' +wait_tmux subs:pane-test || exit 1 +set -- $(pane_fields subs:pane-test) +wid=$1 +widx=$2 +pane=$3 + +send "refresh-client -B 'ap:%*:#{pane_id}'" +wait_for "%subscription-changed ap $sid $wid $widx $pane : $pane" || exit 1 + +send 'split-window -t subs:pane-test' +i=0 +while [ "$i" -lt 6 ]; do + [ "$($TMUX list-panes -t subs:pane-test | wc -l)" -eq 2 ] && break + sleep 1 + i=$((i + 1)) +done +[ "$i" -lt 6 ] || exit 1 +newpane=$($TMUX list-panes -t subs:pane-test -F '#{pane_id}' | tail -n 1) +wait_for "%subscription-changed ap $sid $wid $widx $newpane : $newpane" || + exit 1 + +$TMUX new-session -d -s other || exit 1 +$TMUX link-window -s subs:pane-test -t other:1 || exit 1 + +send "refresh-client -B 'sp:$newpane:#{pane_id}:#{window_id}'" +wait_for "%subscription-changed sp $sid $wid $widx $newpane : $newpane:$wid" || + exit 1 + +send "refresh-client -B 'cw:$wid:#{window_id}:#{window_index}'" +wait_for "%subscription-changed cw $sid $wid $widx - : $wid:$widx" || + exit 1 + +send "refresh-client -B 'aw:@*:#{window_id}'" +send 'new-window -n window-test' +wait_tmux subs:window-test || exit 1 +set -- $(window_fields subs:window-test) +awid=$1 +awidx=$2 +wait_for "%subscription-changed aw $sid $awid $awidx - : $awid" || exit 1 + +send "refresh-client -B 'dup::#{session_windows}'" +wcount=$($TMUX display-message -p -t subs '#{session_windows}') +wait_for "%subscription-changed dup $sid - - - : $wcount" || exit 1 +send "refresh-client -B 'dup::#{session_name}'" +wait_for "%subscription-changed dup $sid - - - : subs" || exit 1 +send 'new-window -n dup-test' +wait_tmux subs:dup-test || exit 1 +wcount=$($TMUX display-message -p -t subs '#{session_windows}') +reject_for "%subscription-changed dup $sid - - - : $wcount" || exit 1 + +send "refresh-client -B 'missing-pane:%999999:#{pane_id}'" +send "refresh-client -B 'missing-window:@999999:#{window_id}'" +reject_for '%subscription-changed missing-pane' || exit 1 +reject_for '%subscription-changed missing-window' || exit 1 + +exit 0 diff --git a/regress/copy-mode-scroll-exit.sh b/regress/copy-mode-scroll-exit.sh new file mode 100644 index 000000000..0f9e2d335 --- /dev/null +++ b/regress/copy-mode-scroll-exit.sh @@ -0,0 +1,37 @@ +#!/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 \ + 'i=0; while [ $i -lt 80 ]; do echo "line $i"; i=$((i + 1)); done; cat' || + exit 1 +$TMUX set -g window-size manual || exit 1 + +$TMUX copy-mode -e || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -X start-of-line || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -X cursor-down || exit 1 + +[ "$($TMUX display-message -p '#{selection_present}')" = "1" ] || exit 1 +$TMUX send-keys -N200 -X scroll-down || exit 1 +[ "$($TMUX display-message -p '#{pane_in_mode} #{scroll_position}')" = "1 0" ] || + exit 1 + +$TMUX send-keys -X clear-selection || exit 1 +$TMUX send-keys -X scroll-down || exit 1 +[ "$($TMUX display-message -p '#{pane_in_mode}')" = "0" ] || exit 1 + +exit 0 diff --git a/regress/copy-mode-test-emacs.sh b/regress/copy-mode-test-emacs.sh index a5b69c675..3e9cbf50b 100644 --- a/regress/copy-mode-test-emacs.sh +++ b/regress/copy-mode-test-emacs.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX new -d -x40 -y10 \ diff --git a/regress/copy-mode-test-vi.sh b/regress/copy-mode-test-vi.sh index 2d6bfc287..71810b006 100644 --- a/regress/copy-mode-test-vi.sh +++ b/regress/copy-mode-test-vi.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -f/dev/null -Ltest" +TMUX="$TEST_TMUX -f/dev/null -LtestA$$" $TMUX kill-server 2>/dev/null $TMUX new -d -x40 -y10 \ diff --git a/regress/cursor-test1.sh b/regress/cursor-test1.sh index 73a336be7..b098cdc15 100644 --- a/regress/cursor-test1.sh +++ b/regress/cursor-test1.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/cursor-test2.sh b/regress/cursor-test2.sh index 3de80fdac..8e9e7ec80 100644 --- a/regress/cursor-test2.sh +++ b/regress/cursor-test2.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/cursor-test3.sh b/regress/cursor-test3.sh index e4881c79a..213ed83ba 100644 --- a/regress/cursor-test3.sh +++ b/regress/cursor-test3.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/cursor-test4.sh b/regress/cursor-test4.sh index 8f4dfa5c3..0eb5f0c9a 100644 --- a/regress/cursor-test4.sh +++ b/regress/cursor-test4.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/decrqm-sync.sh b/regress/decrqm-sync.sh index 7f8c3f61d..f4daf36a9 100644 --- a/regress/decrqm-sync.sh +++ b/regress/decrqm-sync.sh @@ -9,7 +9,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 1 diff --git a/regress/environ-update.sh b/regress/environ-update.sh index 59f7d74c5..44e4e72c4 100644 --- a/regress/environ-update.sh +++ b/regress/environ-update.sh @@ -17,9 +17,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # A second server on its own socket hosts the pane that runs the inner client. -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/environ.sh b/regress/environ.sh index 244ec367d..2d5c0c031 100644 --- a/regress/environ.sh +++ b/regress/environ.sh @@ -19,7 +19,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_value $args $expected @@ -151,7 +151,7 @@ cat > "$CONF" </dev/null $CTMUX new-session -d -s c -x 80 -y 24 || { rm -f "$CONF"; exit 1; } out=$($CTMUX show-environment -g CFGVAR 2>&1) diff --git a/regress/floating-pane-geometry.sh b/regress/floating-pane-geometry.sh index 7bd033ff6..7e80e8942 100644 --- a/regress/floating-pane-geometry.sh +++ b/regress/floating-pane-geometry.sh @@ -15,7 +15,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null fail() diff --git a/regress/format-fuzzy.sh b/regress/format-fuzzy.sh index 31a5b60cb..00bc28bd4 100644 --- a/regress/format-fuzzy.sh +++ b/regress/format-fuzzy.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # test_format $format $expected_result test_format() diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index 131b6527a..58c02af7c 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -12,10 +12,12 @@ PATH=/bin:/usr/bin TERM=screen TZ=UTC -export TZ +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TZ LANG LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" ESC=$(printf '\033') @@ -98,6 +100,11 @@ $TMUX set -g @host 'myhost' || exit 1 $TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC $TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q: $TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e: +$TMUX set -g @sq "a'b" || exit 1 # a single quote for q/s: +$TMUX set -g @sub 'abABab' || exit 1 +$TMUX set -g @slash 'foo/bar foo/' || exit 1 +$TMUX set -g @nl "$(printf 'a\nb')" || exit 1 +q_s_nl=$(printf "'a\nb'") # --- Comparisons and matching -------------------------------------------- @@ -151,6 +158,10 @@ test_format "#{!!:non-empty}" "1" # q: escapes shell special characters with a backslash. test_format "#{q:@sp}" 'a\ b\$c' +# q/s quotes with POSIX shell single quotes. +test_format "#{q/s:@sp}" "'a b\$c'" +test_format "#{q/s:@sq}" "'a'\\''b'" +test_format "#{q/s:@nl}" "$q_s_nl" # q/e and q/h escape "#" for the format/style parser by doubling it. test_format "#{q/e:@hash}" 'a##b' test_format "#{q/h:@hash}" 'a##b' @@ -280,6 +291,15 @@ if [ -z "$($TMUX display-message -p '#{t/r:@ts}')" ]; then echo "Format test failed for '#{t/r:@ts}': empty result" exit 1 fi +# t/d: difference from the current time in seconds. +diff=$($TMUX display-message -p '#{t/d:@ts}') +case "$diff" in +[0-9]*) ;; +*) + echo "Format test failed for '#{t/d:@ts}': expected positive value, got '$diff'" + exit 1 + ;; +esac # t/f: custom strftime format applied to the variable's time. Tested in a # format_expand context (list-windows -F), where a single strftime specifier is @@ -315,6 +335,14 @@ done # A time in the future has no relative form. $TMUX set -g @future "$((now + 100000))" test_format "#{t/r:@future}" "" +diff=$($TMUX display-message -p '#{t/d:@future}') +case "$diff" in +-[0-9]*) ;; +*) + echo "Format test failed for '#{t/d:@future}': expected negative value, got '$diff'" + exit 1 + ;; +esac # --- Content search (C) -------------------------------------------------- @@ -417,9 +445,23 @@ test_format "#{=/3/#,:@s}" "abc," # escaped comma in the marker # The truncation marker is itself expanded as a format. test_format "#{=/3/#{l:>}:@s}" "abc>" -# Substitution flags: a third argument of "i" is case-insensitive; an invalid -# regular expression leaves the text unchanged. +# Substitution, including regular expressions, back references, different +# delimiters, empty matches and the "i" case-insensitive flag. +test_format "#{s/z/X/:@s}" "abcdefghij" +test_format "#{s/[bd]/X/:@s}" "aXcXefghij" test_format "#{s/A/X/i:@s}" "Xbcdefghij" +test_format "#{s/a(.)/\\1x/i:@sub}" "bxBxbx" +test_format "#{s/(.)(.)/\\2\\1/:@s}" "badcfehgji" +test_format "#{s|foo/|bar/|:@slash}" "bar/bar bar/" +test_format "#{s/^abc/ABC/:@s}" "ABCdefghij" +test_format "#{s/^(.)(.)/\\2\\1/:@s}" "bacdefghij" +test_format "#{s/^x*//:@s}" "abcdefghij" +test_format "#{s/^/X/:@s}" "Xabcdefghij" +test_format "#{s/^x*/X/:@s}" "Xabcdefghij" +test_format "#{s/$/X/:@s}" "abcdefghijX" +test_format "#{s/x*//:@s}" "abcdefghij" +test_format "#{s/x*/X/:@s}" "aXbXcXdXeXfXgXhXiXjX" +# An invalid regular expression leaves the text unchanged. test_format "#{s/[/X/:@s}" "abcdefghij" @@ -517,11 +559,17 @@ $TMUX split-window -h -t zeta:charlie $TMUX split-window -h -t zeta:charlie test_format "#{P:#{pane_index}}" "012" "zeta:charlie" test_format "#{P/r:#{pane_index}}" "210" "zeta:charlie" -# A pane-sort argument is accepted; for panes only the r (reverse) suffix has an -# effect, so these all keep the count and exercise the argument branch. +# Pane sort accepts i (pane-list order) and z (z-order). Other sort letters +# fall back to the default creation order; r reverses whichever order is used. test_format "#{P/i:x}" "xxx" "zeta:charlie" +test_format "#{P/i:#{pane_index}}" "012" "zeta:charlie" +test_format "#{P/z:x}" "xxx" "zeta:charlie" test_format "#{P/n:x}" "xxx" "zeta:charlie" test_format "#{P/t:x}" "xxx" "zeta:charlie" +$TMUX new-pane -d -t zeta:charlie -x 20 -y 10 -X 1 -Y 1 +test_format "#{P/i:#{pane_index}}" "0123" "zeta:charlie" +test_format "#{P/z:#{pane_index}}" "3012" "zeta:charlie" +test_format "#{P/zr:#{pane_index}}" "0123" "zeta:charlie" # Verbose expansion of the loops, to exercise their logging paths. $TMUX display-message -v -p "#{S:#{session_name}}" >/dev/null 2>&1 diff --git a/regress/format-mouse.sh b/regress/format-mouse.sh index 8032f8a57..9a443d19c 100644 --- a/regress/format-mouse.sh +++ b/regress/format-mouse.sh @@ -18,8 +18,8 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" cleanup() { diff --git a/regress/format-render-contexts.sh b/regress/format-render-contexts.sh new file mode 100644 index 000000000..2cceff2e3 --- /dev/null +++ b/regress/format-render-contexts.sh @@ -0,0 +1,314 @@ +#!/bin/sh +# Exercise format and style rendering in live contexts. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export PATH TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +LIMIT=20000 + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +run_cmd() +{ + if command -v timeout >/dev/null 2>&1; then + timeout 10 "$@" + else + "$@" + fi +} + +bounded() +{ + name=$1 + text=$2 + + n=$(printf '%s' "$text" | wc -c) + [ "$n" -le "$LIMIT" ] || fail "$name too large: $n bytes" +} + +tmux_run() +{ + name=$1 + shift + + out=$( + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX "$@" 2>&1 + else + $TMUX "$@" 2>&1 + fi + ) + rc=$? + bounded "$name" "$out" + [ "$rc" -eq 0 ] || fail "$name failed: $out" + printf '%s' "$out" +} + +capture() +{ + out=$( + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX2 capture-pane -p -t out:0 2>/dev/null + else + $TMUX2 capture-pane -p -t out:0 2>/dev/null + fi + ) + rc=$? + bounded "capture-pane" "$out" + [ "$rc" -eq 0 ] || fail "capture-pane failed" + printf '%s\n' "$out" +} + +capture_esc() +{ + out=$( + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX2 capture-pane -pe -t out:0 \ + 2>/dev/null + else + $TMUX2 capture-pane -pe -t out:0 2>/dev/null + fi + ) + rc=$? + bounded "capture-pane -e" "$out" + [ "$rc" -eq 0 ] || fail "capture-pane -e failed" + printf '%s\n' "$out" +} + +assert_alive() +{ + tmux_run "server alive ($1)" display-message -p alive >/dev/null +} + +wait_for() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + if capture | grep -F "$marker" >/dev/null 2>&1; then + sleep 0.1 + return 0 + fi + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for $marker" +} + +render_status_options() +{ + label=$1 + fmt=$2 + + tmux_run "$label status-left" \ + set-option -g status-left "SL$label: $fmt" >/dev/null + tmux_run "$label clear status-right" \ + set-option -g status-right '' >/dev/null + tmux_run "$label status-format left" \ + set-option -g status-format[0] '#{E:status-left}' >/dev/null + tmux_run "$label refresh status-left" refresh-client -S >/dev/null + wait_for "SL$label:" + + tmux_run "$label clear status-left" \ + set-option -g status-left '' >/dev/null + tmux_run "$label status-right" \ + set-option -g status-right "SR$label: $fmt" >/dev/null + tmux_run "$label status-format right" \ + set-option -g status-format[0] \ + '#[align=right]#{E:status-right}' >/dev/null + tmux_run "$label refresh status-right" refresh-client -S >/dev/null + wait_for "SR$label:" + assert_alive "$label status-left/right" +} + +render_status_format() +{ + label=$1 + fmt=$2 + + tmux_run "$label status-format" \ + set-option -g status-format[0] "SF$label: $fmt" >/dev/null + tmux_run "$label refresh status-format" refresh-client -S >/dev/null + wait_for "SF$label:" + assert_alive "$label status-format" +} + +render_message() +{ + label=$1 + fmt=$2 + + tmux_run "$label display-message" \ + display-message -t fmt:0 -d 1000 "DM$label: $fmt" >/dev/null + wait_for "DM$label:" + assert_alive "$label display-message" +} + +render_choose_tree() +{ + label=$1 + fmt=$2 + + tmux_run "$label choose-tree" \ + choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null + wait_for "CT$label:" + tmux_run "$label quit choose-tree" send-keys -t fmt:0 q >/dev/null + assert_alive "$label choose-tree" +} + +render_customize() +{ + label=$1 + fmt=$2 + + tmux_run "$label customize-mode" \ + customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null + sleep 0.5 + out=$(capture) + bounded "$label customize-mode capture" "$out" + tmux_run "$label quit customize-mode" send-keys -t fmt:0 q >/dev/null + assert_alive "$label customize-mode" +} + +render_list_output() +{ + label=$1 + fmt=$2 + + tmux_run "$label bind-key" \ + bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null + out=$(tmux_run "$label list-keys" list-keys -T root F12) + bounded "$label list-keys" "$out" + printf '%s' "$out" | grep -F "LK$label:" >/dev/null 2>&1 || + fail "$label list-keys missing rendered command" + + tmux_run "$label set show option" \ + set-option -g "@format_render_$label" "SO$label: $fmt" >/dev/null + out=$(tmux_run "$label show-options" show-options -g "@format_render_$label") + bounded "$label show-options" "$out" + printf '%s' "$out" | grep -F "SO$label:" >/dev/null 2>&1 || + fail "$label show-options missing option" + + assert_alive "$label list output" +} + +run_corpus() +{ + label=$1 + fmt=$2 + + render_status_options "$label" "$fmt" + render_status_format "$label" "$fmt" + render_message "$label" "$fmt" + render_choose_tree "$label" "$fmt" + render_customize "$label" "$fmt" + render_list_output "$label" "$fmt" +} + +check_sgr_sanity() +{ + esc=$(printf '\033') + + tmux_run "set message style fill" \ + set-option -g message-style \ + "fg=#080808,bg=#ffff00,fill=#ffff00,bold" >/dev/null + tmux_run "show filled message" \ + display-message -t fmt:0 -d 1000 \ + '#[bg=blue,italics] hello, #[fg=#080808,bg=default] world!' \ + >/dev/null + wait_for "hello, world!" + + out=$(capture_esc) + printf '%s' "$out" | grep -F 'world!' >/dev/null 2>&1 || + fail "message text missing from SGR capture" + printf '%s' "$out" | grep "$esc" >/dev/null 2>&1 || + fail "SGR capture has no escapes" + + world_sgr=$( + printf '%s\n' "$out" | + awk -v esc="$esc" ' + /world!/ { + i = index($0, "world!") + pre = substr($0, 1, i - 1) + n = split(pre, parts, esc "\\[") + print parts[n] + exit + }' + ) + + case "$world_sgr" in + *'48;'*) ;; + *) fail "world segment missing explicit background SGR: $world_sgr" ;; + esac + case "$world_sgr" in + *'49'*) + fail "world segment used default background instead of message fill: $world_sgr" + ;; + esac + + assert_alive "message SGR sanity" +} + +wide_fmt() +{ + printf 'wide: \0316\0225\0316\0273\0316\0273\0316\0267\0316\0275\0316\0271\0316\0272\0316\0254 \0344\0270\0255\0346\0226\0207 #{=12:\0344\0270\0255\0346\0226\0207abc}' +} + +long_style_fmt() +{ + printf '%s' '#[fg=colour1,bg=colour2,us=colour3,acs,bright,dim,underscore,blink,reverse,hidden,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,range=user|aaaaaaaaaaaaaaaa,align=absolute-centre,list=on,fill=colour200,width=4294967295,pad=4294967295]OVERLONG-STYLE#[default]' +} + +trap cleanup 0 1 15 +cleanup + +tmux_run "new inner session" \ + new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null +tmux_run "new inner window" \ + new-window -t fmt -n second "exec sleep 1000" >/dev/null +tmux_run "select first window" select-window -t fmt:0 >/dev/null +tmux_run "status interval" set-option -g status-interval 1 >/dev/null +tmux_run "base status" set-option -g status on >/dev/null +tmux_run "nested option" \ + set-option -g @nested 'NESTED-#{session_name}-#{window_index}' >/dev/null + +run_cmd $TMUX2 new-session -d -s out -x 100 -y 30 "$TMUX attach -t fmt" \ + >/dev/null 2>&1 || fail "failed to start outer client" + +i=0 +while [ "$i" -lt 50 ]; do + c=$(tmux_run "wait clients" list-clients -F x | grep -c x) + [ "$c" -eq 1 ] && break + sleep 0.2 + i=$((i + 1)) +done +[ "$i" -lt 50 ] || fail "inner client did not attach" + +run_corpus A 'plain #{session_name}:#{window_index} #{@nested}' +run_corpus B "$(wide_fmt)" +run_corpus C '#[fg=colour10,bg=colour17,bold,italics]styled #{pane_current_command}#[default]' +run_corpus D '#[push-default]#[fg=red,bg=blue]push #{?pane_active,active,inactive}#[pop-default] after' +run_corpus E "$(long_style_fmt)" + +check_sgr_sanity + +cleanup +exit 0 diff --git a/regress/format-strings.sh b/regress/format-strings.sh index bae66ab00..877e18fec 100644 --- a/regress/format-strings.sh +++ b/regress/format-strings.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # test_format $format $expected_result test_format() diff --git a/regress/format-variables.sh b/regress/format-variables.sh index dccc0e390..449712e34 100644 --- a/regress/format-variables.sh +++ b/regress/format-variables.sh @@ -15,10 +15,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" # A second server on its own socket provides a real terminal (an inner client # attached inside one of its panes) so client terminal variables are populated. -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" # Every variable name in format_table[]. Kept as a plain word list so it can be # iterated with normal shell word splitting. @@ -133,6 +133,7 @@ pane_pipe_pid pane_right pane_search_string pane_start_command +pane_start_command_list pane_start_path pane_synchronized pane_tabs @@ -295,7 +296,7 @@ sleep 1 # write end of a FIFO open, so client_* variables have a client to read. rm -f "$FIFO" mkfifo "$FIFO" || exit 1 -sleep 30 >"$FIFO" & +while :; do sleep 1; done >"$FIFO" & HOLD=$! $TMUX -C attach -t cov <"$FIFO" >/dev/null 2>&1 & CC=$! @@ -343,6 +344,8 @@ test_var pane_at_top "1" -t "$TGT" test_var pane_at_left "1" -t "$TGT" test_var last_window_index "1" -t "$TGT" test_var pid "$($TMUX display-message -p '#{pid}')" -t "$TGT" +test_var pane_start_command "cat" -t "$TGT" +test_var pane_start_command_list "'cat'" -t "$TGT" # The grouped session is reported as such. test_var session_grouped "1" -t "cov:" @@ -384,5 +387,29 @@ esac [ -n "$($TMUX display-message -p '#{t/r:start_time}')" ] || fail "Empty #{t/r:start_time}." +# pane_start_command_list quotes each argv word for sh, so evaluating the +# expansion reconstructs the original argv exactly - including words with +# quotes, spaces, newlines and empty words. sh -c ignores the extra words +# (they become positional parameters), so the pane stays alive. -u stops +# the server sanitizing the newline away when printing to a non-UTF-8 +# client (the test runs without a locale in the environment). +$TMUX new-session -d -s quot -x 80 -y 24 -- sh -c 'sleep 100' arg0 \ + "it's a 'test'" 'two words' '' 'new +line' || fail "Failed to create quoting test session." +LIST=$($TMUX -u display-message -t 'quot:0.0' -p '#{pane_start_command_list}') +eval "set -- $LIST" +GOT=$(for a; do printf '<%s>' "$a"; done) +EXP=$(for a in sh -c 'sleep 100' arg0 "it's a 'test'" 'two words' '' 'new +line'; do printf '<%s>' "$a"; done) +if [ "$GOT" != "$EXP" ]; then + echo "pane_start_command_list did not round-trip." + echo "Expected: $EXP" + echo "But got: $GOT" + fail "Expansion was: $LIST" +fi +# A pane started with the default shell has an empty start command. +$TMUX new-window -d -t 'quot:' || fail "Failed to create shell window." +test_var pane_start_command_list "" -t "quot:1.0" + cleanup exit 0 diff --git a/regress/has-session-return.sh b/regress/has-session-return.sh index a7d76ad85..8b259ac76 100644 --- a/regress/has-session-return.sh +++ b/regress/has-session-return.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX -f/dev/null has -tfoo /dev/null && exit 1 diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh new file mode 100644 index 000000000..3c1ba7909 --- /dev/null +++ b/regress/hooks-after.sh @@ -0,0 +1,146 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +# An after hook fires with the command arguments as formats. +$TMUX set -g @after 0 || fail "set @after failed" +$TMUX set-hook -g after-rename-window \ + 'set -gF @after "#{hook}|#{hook_arguments}|#{hook_argument_0}|#{hook_flag_t}"' || + fail "set-hook -g after-rename-window failed" +$TMUX rename-window -t one:0 first || fail "rename-window first failed" +wait_for @after 'after-rename-window|-t one:0 first|first|one:0' + +# An appended hook command runs after the first. +$TMUX set -g @after2 0 || fail "set @after2 failed" +$TMUX set-hook -ga after-rename-window \ + 'set -gF @after2 "#{@after}+2"' || + fail "set-hook -ga after-rename-window failed" +$TMUX rename-window -t one:0 second || fail "rename-window second failed" +wait_for @after 'after-rename-window|-t one:0 second|second|one:0' +wait_for @after2 'after-rename-window|-t one:0 second|second|one:0+2' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu failed" + +# A hook command is inserted after the command that fired it, before the next +# command in the same command list. +$TMUX set -g @order '' || fail "set @order failed" +$TMUX set-hook -g after-rename-window \ + 'set -gF @order "#{@order}H"' || + fail "set-hook -g after-rename-window order failed" +$TMUX rename-window -t one:0 ordered \; set -gF @order '#{@order}N' || + fail "rename-window order failed" +wait_for @order 'HN' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu order failed" + +# Repeated flag values are available as numbered hook flag formats. +$TMUX set -g @flags 0 || fail "set @flags failed" +$TMUX set-hook -g after-split-window \ + 'set -gF @flags "#{hook_arguments}|#{hook_argument_0}|#{hook_flag_t}|#{hook_flag_e}|#{hook_flag_e_0}|#{hook_flag_e_1}"' || + fail "set-hook -g after-split-window failed" +$TMUX split-window -d -t one:0 -e A=1 -e B=2 'sleep 60' || + fail "split-window flags failed" +wait_for @flags '-d -e A=1 -e B=2 -t one:0 "sleep 60"|sleep 60|one:0|B=2|A=1|B=2' +$TMUX set-hook -gu after-split-window || + fail "set-hook -gu after-split-window failed" + +# A session after hook only fires for commands targeting that session and +# the hook commands run with the command target as current state. +$TMUX set -g @safter 0 || fail "set @safter failed" +$TMUX set-hook -t two after-rename-window \ + 'set -gF @safter "#{hook}:#{session_name}"' || + fail "set-hook -t two after-rename-window failed" +$TMUX rename-window -t one:0 third || fail "rename-window third failed" +assert_unchanged @safter 0 +$TMUX rename-window -t two:0 fourth || fail "rename-window fourth failed" +wait_for @safter 'after-rename-window:two' +$TMUX set-hook -u -t two after-rename-window || + fail "set-hook -u -t two failed" + +# The command-error hook fires when a command fails. +$TMUX set -g @error 0 || fail "set @error failed" +$TMUX set-hook -g command-error 'set -gF @error "#{hook}"' || + fail "set-hook -g command-error failed" +if $TMUX rename-window -t nosuchsession:0 x 2>/dev/null; then + fail "rename-window to missing session succeeded" +fi +wait_for @error 'command-error' +$TMUX set-hook -gu command-error || fail "set-hook -gu command-error failed" + +# Commands run from a hook do not fire their own after hooks. +$TMUX set -g @copy 0 || fail "set @copy failed" +$TMUX set-hook -g after-copy-mode 'set -gF @copy "#{hook}"' || + fail "set-hook -g after-copy-mode failed" +$TMUX copy-mode -t one:0 || fail "copy-mode failed" +wait_for @copy 'after-copy-mode' +$TMUX send-keys -t one:0 -X cancel || fail "cancel failed" +$TMUX set -g @copy 0 || fail "reset @copy failed" +$TMUX set -g @ran 0 || fail "set @ran failed" +$TMUX set-hook -g after-rename-window 'copy-mode -t one:0' || + fail "set-hook -g after-rename-window nested failed" +$TMUX set-hook -ga after-rename-window 'set -g @ran 1' || + fail "set-hook -ga after-rename-window nested failed" +$TMUX rename-window -t one:0 fifth || fail "rename-window fifth failed" +wait_for @ran 1 +mode=$($TMUX display -pt one:0 '#{pane_in_mode}') || + fail "display pane_in_mode failed" +[ "$mode" = 1 ] || fail "hook did not enter copy mode" +assert_unchanged @copy 0 + +exit 0 diff --git a/regress/hooks-lifecycle.sh b/regress/hooks-lifecycle.sh new file mode 100644 index 000000000..3ec748ff4 --- /dev/null +++ b/regress/hooks-lifecycle.sh @@ -0,0 +1,176 @@ +#!/bin/sh + +# End-of-life hooks: pane-exited, pane-died, window-unlinked and +# session-closed when the pane, window or session they refer to is being +# or has been destroyed. Each hook appends to @log so the order hooks fire +# in is checked as well as the hook formats for the dead objects. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s main || fail "new-session main failed" + +$TMUX set -g @log '' || fail "set @log failed" +$TMUX set-hook -g pane-exited \ + 'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' || + fail "set-hook pane-exited failed" +$TMUX set-hook -g window-unlinked \ + 'set -gF @log "#{@log}|window-unlinked:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-unlinked failed" +$TMUX set-hook -g session-closed \ + 'set -gF @log "#{@log}|session-closed:#{hook_session_name}"' || + fail "set-hook session-closed failed" + +# The only pane of the only window of a session exits: pane-exited, then +# window-unlinked, then session-closed, each seeing the dead object in the +# hook formats. +pane=$($TMUX new -d -s doomed -n dwin -P -F '#{pane_id}' 'true') || + fail "new-session doomed failed" +wait_for @log \ + "|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed" +assert_unchanged @log \ + "|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed" + +# The dead pane, window and session cannot be used as targets but the +# server survives. +if $TMUX select-pane -t "$pane" 2>/dev/null; then + fail "dead pane still a valid target" +fi +if $TMUX list-windows -t doomed >/dev/null 2>&1; then + fail "dead session still a valid target" +fi +$TMUX list-panes -sat main >/dev/null || fail "list-panes failed" +$TMUX has -t main || fail "server died after pane exit chain" + +# A pane-exited hook command can run after the pane has been removed. It +# should not retain only the event payload's temporary target references. +$TMUX set -g @queued-pane-exited 0 || fail "set @queued-pane-exited failed" +$TMUX set-hook -g pane-exited \ + 'display-message -p "queued #{hook_pane}" ; set -g @queued-pane-exited 1' || + fail "set-hook queued pane-exited failed" +pane=$($TMUX new -d -s queued -n qwin -P -F '#{pane_id}' 'true') || + fail "new-session queued failed" +wait_for @queued-pane-exited 1 +$TMUX has -t main || fail "server died after queued pane-exited hook" +$TMUX set-hook -g pane-exited \ + 'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' || + fail "restore pane-exited hook failed" + +# kill-window on the last window: window-unlinked then session-closed but +# no pane-exited for the panes in the killed window. +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX new -d -s doomed2 -n dwin2 || fail "new-session doomed2 failed" +$TMUX splitw -d -t doomed2:0 || fail "split-window doomed2 failed" +$TMUX kill-window -t doomed2:0 || fail "kill-window failed" +wait_for @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2' +assert_unchanged @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2' +$TMUX has -t main || fail "server died after kill-window chain" + +# kill-session: session-closed fires first, then window-unlinked for its +# windows. A window linked into another session survives. +$TMUX new -d -s shareA -n shared || fail "new-session shareA failed" +$TMUX new -d -s shareB -n bwin || fail "new-session shareB failed" +$TMUX link-window -s shareA:shared -t shareB:7 || fail "link-window failed" +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX kill-session -t shareA || fail "kill-session shareA failed" +wait_for @log '|session-closed:shareA|window-unlinked:shareA:shared' +name=$($TMUX display -pt shareB:7 '#{window_name}') || + fail "shared window did not survive" +[ "$name" = shared ] || fail "expected window shared but got $name" + +# Killing the surviving session destroys the shared window for real while +# the session is being destroyed. +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX kill-window -t shareB:0 || fail "kill-window bwin failed" +wait_for @log '|window-unlinked:shareB:bwin' +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX kill-session -t shareB || fail "kill-session shareB failed" +wait_for @log '|session-closed:shareB|window-unlinked:shareB:shared' +$TMUX has -t main || fail "server died after kill-session chain" + +# A pane-died hook can kill its own dead pane (the hook runs with the dead +# pane as current target). The kill-pane runs without hooks so pane-exited +# does not fire. +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX new -d -s roe -n rwin || fail "new-session roe failed" +$TMUX set -wt roe:0 remain-on-exit on || fail "set remain-on-exit failed" +$TMUX set-hook -g pane-died \ + 'set -gF @log "#{@log}|pane-died:#{hook_pane}" ; kill-pane' || + fail "set-hook pane-died failed" +pane=$($TMUX splitw -d -t roe:0 -P -F '#{pane_id}' 'true') || + fail "split-window roe failed" +wait_for @log "|pane-died:$pane" +assert_unchanged @log "|pane-died:$pane" +if $TMUX select-pane -t "$pane" 2>/dev/null; then + fail "pane-died hook did not kill its pane" +fi +$TMUX list-panes -t roe:0 >/dev/null || fail "surviving pane broken" +$TMUX has -t roe || fail "session roe died" +$TMUX set-hook -gu pane-died || fail "unset pane-died failed" +$TMUX kill-session -t roe || fail "kill-session roe failed" + +# Killing the last session with end-of-life hooks still set: the server +# runs the hooks and exits cleanly. +$TMUX kill-session -t main || fail "kill-session main failed" +i=0 +while $TMUX has 2>/dev/null; do + i=$((i + 1)) + [ $i -lt 30 ] || fail "server still running after last session killed" + sleep 0.2 +done + +exit 0 diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh new file mode 100644 index 000000000..37d1cd651 --- /dev/null +++ b/regress/hooks-notify.sh @@ -0,0 +1,292 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +$TMUX new -d -s main || fail "new-session main failed" + +# session-created, session-renamed, session-closed. +$TMUX set -g @s 0 || fail "set @s failed" +$TMUX set-hook -g session-created \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-created failed" +$TMUX new -d -s tmp || fail "new-session tmp failed" +wait_for @s 'session-created:tmp' +$TMUX set-hook -gu session-created || fail "unset session-created failed" + +$TMUX set-hook -g session-renamed \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-renamed failed" +$TMUX rename-session -t tmp tmp2 || fail "rename-session failed" +wait_for @s 'session-renamed:tmp2' +$TMUX set-hook -gu session-renamed || fail "unset session-renamed failed" + +$TMUX set-hook -g session-closed \ + 'set -gF @s "#{hook}:#{hook_session_name}"' || + fail "set-hook session-closed failed" +$TMUX kill-session -t tmp2 || fail "kill-session failed" +wait_for @s 'session-closed:tmp2' +$TMUX set-hook -gu session-closed || fail "unset session-closed failed" + +# window-linked and window-unlinked from new-window and kill-window. +$TMUX set -g @w 0 || fail "set @w failed" +$TMUX set-hook -g window-linked \ + 'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-linked failed" +$TMUX set-hook -g window-unlinked \ + 'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' || + fail "set-hook window-unlinked failed" +$TMUX neww -d -t main: -n mywin || fail "new-window failed" +wait_for @w 'window-linked:main:mywin' + +# window-linked and window-unlinked from link-window and unlink-window. +$TMUX new -d -s other || fail "new-session other failed" +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX link-window -s main:mywin -t other:5 || fail "link-window failed" +wait_for @w 'window-linked:other:mywin' +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX unlink-window -t other:5 || fail "unlink-window failed" +wait_for @w 'window-unlinked:other:mywin' +$TMUX set -g @w 0 || fail "reset @w failed" +$TMUX kill-window -t main:mywin || fail "kill-window failed" +wait_for @w 'window-unlinked:main:mywin' +$TMUX set-hook -gu window-linked || fail "unset window-linked failed" +$TMUX set-hook -gu window-unlinked || fail "unset window-unlinked failed" + +# window-renamed sees the window id. +window=$($TMUX display -pt main:0 '#{window_id}') || + fail "display window_id failed" +$TMUX set -g @r 0 || fail "set @r failed" +$TMUX set-hook -g window-renamed \ + 'set -gF @r "#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-renamed failed" +$TMUX rename-window -t main:0 renamed || fail "rename-window failed" +wait_for @r "window-renamed:$window:renamed" +$TMUX set-hook -gu window-renamed || fail "unset window-renamed failed" + +# window-layout-changed from split-window. +$TMUX set -g @l 0 || fail "set @l failed" +$TMUX set-hook -g window-layout-changed \ + 'set -gF @l "#{hook}:#{hook_window}"' || + fail "set-hook window-layout-changed failed" +$TMUX splitw -d -t main:0 || fail "split-window failed" +wait_for @l "window-layout-changed:$window" +$TMUX set-hook -gu window-layout-changed || + fail "unset window-layout-changed failed" + +# window-pane-changed from select-pane. +$TMUX set -g @p 0 || fail "set @p failed" +$TMUX set-hook -g window-pane-changed \ + 'set -gF @p "#{hook}:#{hook_window}"' || + fail "set-hook window-pane-changed failed" +$TMUX selectp -t main:0.1 || fail "select-pane failed" +wait_for @p "window-pane-changed:$window" +$TMUX set-hook -gu window-pane-changed || + fail "unset window-pane-changed failed" + +# session-window-changed from select-window. +$TMUX neww -d -t main: -n w2 || fail "new-window w2 failed" +$TMUX set -g @c 0 || fail "set @c failed" +$TMUX set-hook -g session-window-changed \ + 'set -gF @c "#{hook}:#{hook_session_name}"' || + fail "set-hook session-window-changed failed" +$TMUX selectw -t main:w2 || fail "select-window failed" +wait_for @c 'session-window-changed:main' +$TMUX set-hook -gu session-window-changed || + fail "unset session-window-changed failed" + +# pane-mode-changed from entering and leaving copy mode. +$TMUX set -g @m 0 || fail "set @m failed" +pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display pane_id failed" +$TMUX set-hook -g pane-mode-changed \ + 'set -gF @m "#{hook}:#{hook_pane}:#{pane_in_mode}"' || + fail "set-hook pane-mode-changed failed" +$TMUX copy-mode -t main:0.0 || fail "copy-mode failed" +wait_for @m "pane-mode-changed:$pane:1" +$TMUX send-keys -t main:0.0 -X cancel || fail "cancel failed" +wait_for @m "pane-mode-changed:$pane:0" +$TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed" + +# pane-exited when a pane's command exits with remain-on-exit off. +$TMUX set -g @x 0 || fail "set @x failed" +$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' || + fail "set-hook pane-exited failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || + fail "split-window true failed" +wait_for @x "pane-exited:$pane" +$TMUX set-hook -gu pane-exited || fail "unset pane-exited failed" + +# pane-died when a pane's command exits with remain-on-exit on. +$TMUX set -g remain-on-exit on || fail "set remain-on-exit failed" +$TMUX set -g @d 0 || fail "set @d failed" +$TMUX set-hook -g pane-died 'set -gF @d "#{hook}:#{hook_pane}"' || + fail "set-hook pane-died failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || + fail "split-window remain-on-exit failed" +wait_for @d "pane-died:$pane" +$TMUX set-hook -gu pane-died || fail "unset pane-died failed" +$TMUX killp -t "$pane" || fail "kill-pane failed" +$TMUX set -g remain-on-exit off || fail "reset remain-on-exit failed" + +# pane-title-changed when a pane sets its title. +$TMUX set -g @t 0 || fail "set @t failed" +$TMUX set-hook -g pane-title-changed \ + 'set -gF @t "#{hook}:#{hook_pane}:#{pane_title}"' || + fail "set-hook pane-title-changed failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ + 'printf "\033]2;mytitle\007"; sleep 30') || + fail "split-window title failed" +wait_for @t "pane-title-changed:$pane:mytitle" +$TMUX set-hook -gu pane-title-changed || + fail "unset pane-title-changed failed" +$TMUX killp -t "$pane" || fail "kill-pane title failed" + +# pane-created payload from a command pane, an empty pane, and a respawn. +$TMUX set -g @pc 0 || fail "set @pc failed" +$TMUX set-hook -g pane-created \ + 'set -gF @pc "#{hook}:#{hook_pane}:#{hook_pane_command}:#{hook_created_empty}:#{hook_created_respawn}"' || + fail "set-hook pane-created failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-created command failed" +wait_for @pc "pane-created:$pane:\"sleep 30\":0:0" +$TMUX killp -t "$pane" || fail "kill-pane pane-created command failed" + +$TMUX set -g @pc 0 || fail "reset @pc failed" +empty_command=$($TMUX show -gqv default-shell) || + fail "show default-shell failed" +pane=$($TMUX splitw -d -E -t main:0 -P -F '#{pane_id}') || + fail "split-window pane-created empty failed" +wait_for @pc "pane-created:$pane:$empty_command:1:0" + +$TMUX set -g @pc 0 || fail "reset @pc respawn failed" +$TMUX respawnp -k -t "$pane" 'sleep 30' || fail "respawn-pane failed" +wait_for @pc "pane-created:$pane:\"sleep 30\":0:1" +$TMUX killp -t "$pane" || fail "kill-pane pane-created respawn failed" +$TMUX set-hook -gu pane-created || fail "unset pane-created failed" + +# pane-resized carries old and new dimensions. +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-resized failed" +old_size=$($TMUX display -pt "$pane" '#{pane_width},#{pane_height}') || + fail "display old pane size failed" +$TMUX set -g @pr 0 || fail "set @pr failed" +$TMUX set-hook -g pane-resized \ + 'set -gF @pr "#{hook}:#{hook_pane}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook pane-resized failed" +$TMUX resizep -t "$pane" -x 20 -y 10 || fail "resize-pane failed" +new_size=$($TMUX display -pt "$pane" '#{pane_width},#{pane_height}') || + fail "display new pane size failed" +wait_for @pr "pane-resized:$pane:$old_size->$new_size" +$TMUX set-hook -gu pane-resized || fail "unset pane-resized failed" +$TMUX killp -t "$pane" || fail "kill-pane pane-resized failed" + +# pane-mode-entered and pane-mode-exited expose split transition payloads. +$TMUX set -g @me 0 || fail "set @me failed" +$TMUX set -g @mx 0 || fail "set @mx failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-mode failed" +$TMUX set-hook -g pane-mode-entered \ + 'set -gF @me "#{hook}:#{hook_pane}:#{hook_mode_entered}:#{hook_current_mode}:#{hook_previous_mode}"' || + fail "set-hook pane-mode-entered failed" +$TMUX set-hook -g pane-mode-exited \ + 'set -gF @mx "#{hook}:#{hook_pane}:#{hook_mode_entered}:#{hook_current_mode}:#{hook_previous_mode}"' || + fail "set-hook pane-mode-exited failed" +$TMUX copy-mode -t "$pane" || fail "copy-mode split event failed" +wait_for @me "pane-mode-entered:$pane:1:copy-mode:" +$TMUX send-keys -t "$pane" -X cancel || fail "cancel split mode failed" +wait_for @mx "pane-mode-exited:$pane:0::copy-mode" +$TMUX set-hook -gu pane-mode-entered || fail "unset pane-mode-entered failed" +$TMUX set-hook -gu pane-mode-exited || fail "unset pane-mode-exited failed" +$TMUX killp -t "$pane" || fail "kill-pane pane-mode failed" + +# marked-pane-changed carries the new/old marked pane and marked flag. +$TMUX set -g @mk 0 || fail "set @mk failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window marked-pane failed" +$TMUX set-hook -g marked-pane-changed \ + 'set -gF @mk "#{hook}:#{hook_marked}:#{hook_pane}:#{hook_new_pane}:#{hook_old_pane}"' || + fail "set-hook marked-pane-changed failed" +$TMUX selectp -t "$pane" -m || fail "mark pane failed" +wait_for @mk "marked-pane-changed:1:$pane:$pane:" +$TMUX selectp -t "$pane" -m || fail "unmark pane failed" +wait_for @mk "marked-pane-changed:0:$pane::$pane" +$TMUX set-hook -gu marked-pane-changed || + fail "unset marked-pane-changed failed" +$TMUX killp -t "$pane" || fail "kill-pane marked-pane failed" + +# window-zoomed and window-unzoomed fire on resize-pane -Z. +$TMUX set -g @zoom 0 || fail "set @zoom failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window zoom failed" +window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display zoom window failed" +$TMUX set-hook -g window-zoomed \ + 'set -gF @zoom "#{hook}:#{hook_window}"' || + fail "set-hook window-zoomed failed" +$TMUX set-hook -g window-unzoomed \ + 'set -gF @zoom "#{hook}:#{hook_window}"' || + fail "set-hook window-unzoomed failed" +$TMUX resizep -Z -t "$pane" || fail "zoom pane failed" +wait_for @zoom "window-zoomed:$window" +$TMUX resizep -Z -t "$pane" || fail "unzoom pane failed" +wait_for @zoom "window-unzoomed:$window" +$TMUX set-hook -gu window-zoomed || fail "unset window-zoomed failed" +$TMUX set-hook -gu window-unzoomed || fail "unset window-unzoomed failed" +$TMUX killp -t "$pane" || fail "kill-pane zoom failed" + +# client-attached and client-detached using a control client. +$TMUX set -g @a 0 || fail "set @a failed" +$TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || + fail "set-hook client-attached failed" +$TMUX set-hook -g client-detached 'set -gF @a "#{hook}"' || + fail "set-hook client-detached failed" +mkfifo "$OUT/fifo" || fail "mkfifo failed" +$TMUX -C attach -t main <"$OUT/fifo" >"$OUT/control.out" 2>&1 & +exec 3>"$OUT/fifo" +wait_for @a 'client-attached' +exec 3>&- +wait_for @a 'client-detached' +$TMUX set-hook -gu client-attached || fail "unset client-attached failed" +$TMUX set-hook -gu client-detached || fail "unset client-detached failed" + +exit 0 diff --git a/regress/hooks.sh b/regress/hooks.sh new file mode 100644 index 000000000..e1d75bf67 --- /dev/null +++ b/regress/hooks.sh @@ -0,0 +1,155 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 10 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session one failed" + +# A global hook fires and sees the hook formats. +$TMUX set -g @created 0 || fail "set @created failed" +$TMUX set-hook -g session-created \ + 'set -gF @created "#{hook}:#{hook_session_name}"' || + fail "set-hook -g session-created failed" +$TMUX new -d -s two || fail "new-session two failed" +wait_for @created 'session-created:two' + +# Hooks are arrays: an appended command runs after the first. +$TMUX set -g @second 0 || fail "set @second failed" +$TMUX set-hook -ga session-created \ + 'set -gF @second "#{@created}+second"' || + fail "set-hook -ga session-created failed" +$TMUX new -d -s three || fail "new-session three failed" +wait_for @created 'session-created:three' +wait_for @second 'session-created:three+second' + +# show-hooks lists both commands. +shown=$($TMUX show-hooks -g session-created) || + fail "show-hooks -g failed" +[ "$(echo "$shown" | wc -l)" -eq 2 ] || + fail "unexpected show-hooks output: $shown" +echo "$shown" | grep -q '^session-created\[0\]' || + fail "missing first array item: $shown" +echo "$shown" | grep -q '^session-created\[1\]' || + fail "missing second array item: $shown" + +# Unsetting removes the whole hook. +$TMUX set-hook -gu session-created || fail "set-hook -gu failed" +shown=$($TMUX show-hooks -g session-created) || + fail "show-hooks -g after unset failed" +if echo "$shown" | grep -q '\['; then + fail "show-hooks showed removed hook: $shown" +fi +$TMUX set -g @created 0 || fail "reset @created failed" +$TMUX new -d -s four || fail "new-session four failed" +assert_unchanged @created 0 + +# An unknown hook name is rejected. +if $TMUX set-hook -g no-such-hook 'display x' 2>/dev/null; then + fail "unknown hook name was accepted" +fi + +# A session hook only fires for events in that session. +$TMUX set -g @renamed 0 || fail "set @renamed failed" +$TMUX set-hook -t one window-renamed \ + 'set -gF @renamed "#{hook}:#{hook_window_name}"' || + fail "set-hook -t one window-renamed failed" +$TMUX rename-window -t two:0 twoname || fail "rename-window two failed" +assert_unchanged @renamed 0 +$TMUX rename-window -t one:0 onename || fail "rename-window one failed" +wait_for @renamed 'window-renamed:onename' +$TMUX set-hook -u -t one window-renamed || fail "set-hook -u failed" + +# A pane hook only fires for events in that pane. +pane2=$($TMUX splitw -d -t one:0 -P -F '#{pane_id}') || + fail "split-window failed" +$TMUX set -g @mode 0 || fail "set @mode failed" +$TMUX set-hook -p -t "$pane2" pane-mode-changed \ + 'set -gF @mode "#{hook}:#{hook_pane}"' || + fail "set-hook -p pane-mode-changed failed" +$TMUX copy-mode -t one:0.0 || fail "copy-mode pane 0 failed" +assert_unchanged @mode 0 +$TMUX copy-mode -t "$pane2" || fail "copy-mode pane 1 failed" +wait_for @mode "pane-mode-changed:$pane2" +$TMUX send-keys -t "$pane2" -X cancel || fail "cancel failed" +$TMUX set-hook -pu -t "$pane2" pane-mode-changed || + fail "set-hook -pu failed" + +# A window hook only fires for events in that window. +$TMUX set -g @wmode 0 || fail "set @wmode failed" +$TMUX set-hook -w -t two:0 pane-mode-changed \ + 'set -gF @wmode "#{hook}:#{hook_window_name}"' || + fail "set-hook -w pane-mode-changed failed" +$TMUX copy-mode -t three:0.0 || fail "copy-mode three failed" +assert_unchanged @wmode 0 +$TMUX copy-mode -t two:0.0 || fail "copy-mode two failed" +wait_for @wmode 'pane-mode-changed:twoname' +$TMUX set-hook -wu -t two:0 pane-mode-changed || + fail "set-hook -wu failed" + +# Commands run from a hook do not fire hooks again. +$TMUX set -g @renames '' || fail "set @renames failed" +$TMUX set-hook -g window-renamed \ + 'set -gF @renames "#{@renames}x" ; rename-window -t three:0 inner' || + fail "set-hook -g window-renamed failed" +$TMUX rename-window -t three:0 outer || fail "rename-window outer failed" +wait_for @renames 'x' +assert_unchanged @renames 'x' +name=$($TMUX display -pt three:0 '#{window_name}') || + fail "display window_name failed" +[ "$name" = inner ] || fail "expected window name inner but got $name" + +exit 0 diff --git a/regress/if-shell-TERM.sh b/regress/if-shell-TERM.sh index 21846fd10..d1db52f45 100644 --- a/regress/if-shell-TERM.sh +++ b/regress/if-shell-TERM.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/if-shell-error.sh b/regress/if-shell-error.sh index 2eab8f7ca..3b2dac753 100644 --- a/regress/if-shell-error.sh +++ b/regress/if-shell-error.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/if-shell-nested.sh b/regress/if-shell-nested.sh index 434775c43..e10be41c6 100644 --- a/regress/if-shell-nested.sh +++ b/regress/if-shell-nested.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/input-common.inc b/regress/input-common.inc index 96eec1f09..100a7be0f 100644 --- a/regress/input-common.inc +++ b/regress/input-common.inc @@ -2,7 +2,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMP=$(mktemp) EXP=$(mktemp) diff --git a/regress/input-keys.sh b/regress/input-keys.sh index e2fc41f54..cb771d19a 100644 --- a/regress/input-keys.sh +++ b/regress/input-keys.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 1 $TMUX -f/dev/null new -x20 -y2 -d \; set -g escape-time 0 || exit 1 diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh new file mode 100644 index 000000000..767f08bec --- /dev/null +++ b/regress/input-reflow-stress.sh @@ -0,0 +1,494 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export PATH TERM LANG 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 "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1 +EXP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1 +exit_status=0 + +WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80" +LIVE_WIDTHS="68 24 80" +HEIGHT=24 +HISTORY_LIMIT=220 +HISTORY_BOUND=12000 +JOINED_BOUND=500000 +RAW_BOUND=5000000 +COPY_BOUND=20000 +LIVE_BOUND=50000 + +cleanup() +{ + rm -f "$TMP" "$EXP" + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +trap cleanup 0 1 15 + +record_fail() +{ + echo "FAIL: $1" >&2 + exit_status=1 +} + +u8() +{ + printf "$1" +} + +wide_a() +{ + u8 '\0343\0201\0202' +} + +wide_b() +{ + u8 '\0347\0225\0214' +} + +combining_acute() +{ + u8 '\0314\0201' +} + +zero_width_space() +{ + u8 '\0342\0200\0213' +} + +zero_width_joiner() +{ + u8 '\0342\0200\0215' +} + +replacement() +{ + u8 '\0357\0277\0275' +} + +assert_alive() +{ + $TMUX display-message -p -t stress: alive >/dev/null 2>&1 || + record_fail "server exited after $1" +} + +make_orphan_payload() +{ + tag=$1 + + printf 'OP%s-01|ECH-half|left' "$tag" + wide_b + printf 'right' + printf '\033[6D\033[1X\n' + + printf 'OP%s-02|EL-inside|left' "$tag" + wide_b + printf 'right' + printf '\033[6D\033[K\n' + + printf 'OP%s-03|overwrite-leading|left' "$tag" + wide_b + printf 'right' + printf '\rOP%s-03|overwrite-leading|left!\n' "$tag" + + printf 'OP%s-04|right-edge|' "$tag" + i=0 + while [ "$i" -lt 60 ]; do + printf '.' + i=$((i + 1)) + done + wide_b + printf 'Z' + printf '\033[2D\033[1X\n' + + printf 'OP%s-05|ERASED|left' "$tag" + wide_b + printf 'right\r\033[2KOP%s-05|ERASED|left clear\n' "$tag" + + printf 'SENT-%s|ORPHAN-PHASE|complete\n' "$tag" +} + +make_payload() +{ + i=0 + while [ "$i" -lt 16 ]; do + printf 'ASCII%02d|abcdefghijklmnopqrstuvwxyz\n' "$i" + + printf 'WIDE%02d|' "$i" + wide_a + printf '|' + wide_b + printf '|tail\n' + + printf 'COMB%02d|e' "$i" + combining_acute + printf '|zero' + zero_width_space + zero_width_joiner + printf '|done\n' + + printf 'STYLE%02d|\033[1;31mred\033[0m|\033[4munder\033[0m\n' \ + "$i" + + printf 'LINK%02d|\033]8;;https://example.invalid/%d\007link%d\033]8;;\007|end\n' \ + "$i" "$i" "$i" + + printf 'WRAP%02d|%064d\n' "$i" "$i" + + printf 'CRBS%02d|abcdef\rCRBS%02d|XYZ\n' "$i" "$i" + printf 'BS%02d|abc\bZ\n' "$i" + i=$((i + 1)) + done + + i=0 + while [ "$i" -lt 6 ]; do + printf 'L%04d|stable|abcdefghijklmnopqrstuvwxyz\n' "$i" + i=$((i + 1)) + done + + printf 'SENT-A|SURVIVES|plain logical line\n' + printf 'SENT-B|SURVIVES|wide-free after erases\n' + make_orphan_payload PRE + printf 'SENT-C|SURVIVES|tail logical line\n' +} + +make_alternate_payload() +{ + printf '\033[?1049h' + printf 'ALT-SENT|alternate screen|' + wide_a + printf '\nALT-SENT|resize target\n' +} + +exit_alternate_payload() +{ + printf '\033[?1049l' +} + +load_and_paste() +{ + buffer=$1 + shift + + "$@" >"$EXP" + $TMUX load-buffer -b "$buffer" "$EXP" || exit 1 + $TMUX paste-buffer -d -b "$buffer" -t stress:0.0 || exit 1 + sleep 0.3 +} + +capture_joined() +{ + $TMUX capture-pane -pNJ -t stress: -S -5000 -E - >"$TMP" +} + +assert_joined_sane() +{ + label=$1 + + capture_joined || { + record_fail "joined capture failed after $label" + return + } + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$JOINED_BOUND" ]; then + record_fail "joined capture too large after $label: $bytes" + fi + + if grep -q "$(replacement)" "$TMP"; then + record_fail "replacement character in joined capture after $label" + fi + + out=$( + awk -v label="$label" ' + { + line = $0 + seen_on_line = 0 + while (match(line, /L[0-9][0-9][0-9][0-9]\|/)) { + id = substr(line, RSTART + 1, 4) + 0 + seen_on_line++ + seen[id]++ + ids[++count] = id + found = 1 + last = id + line = substr(line, RSTART + RLENGTH) + } + if (seen_on_line > 1) + printf("fused IDs after %s: %s\n", label, $0) + } + END { + if (!found) { + printf("no line IDs after %s\n", label) + exit + } + for (i = 2; i <= count; i++) { + if (ids[i] < ids[i - 1]) + drops++ + } + if (drops > 1) + printf("IDs out of order after %s\n", label) + for (id in seen) { + if (seen[id] > 2) + printf("duplicate ID after %s: L%04d\n", label, id) + } + }' "$TMP" + ) + [ -z "$out" ] || record_fail "$out" + + for marker in \ + 'SENT-A|SURVIVES|plain logical line' \ + 'SENT-B|SURVIVES|wide-free after erases' \ + 'SENT-C|SURVIVES|tail logical line' + do + grep -F "$marker" "$TMP" >/dev/null 2>&1 || + record_fail "missing sentinel after $label: $marker" + done + + if grep '^OP.*-05|ERASED|.*' "$TMP" | grep -q "$(wide_b)"; then + record_fail "erased wide character visible after $label" + fi +} + +assert_final_logical_text() +{ + label=$1 + + capture_joined || { + record_fail "joined capture failed after $label" + return + } + + for marker in \ + 'SENT-A|SURVIVES|plain logical line' \ + 'SENT-B|SURVIVES|wide-free after erases' \ + 'SENT-C|SURVIVES|tail logical line' \ + 'OPPRE-03|overwrite-leading|left!' \ + 'OPPOST-03|overwrite-leading|left!' \ + 'SENT-POST|ORPHAN-PHASE|complete' + do + grep -F "$marker" "$TMP" >/dev/null 2>&1 || + record_fail "missing expected logical text after $label: $marker" + done +} + +assert_raw_sane() +{ + label=$1 + + $TMUX capture-pane -pR -t stress: >"$TMP" || + record_fail "raw capture failed after $label" + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$RAW_BOUND" ]; then + record_fail "raw capture too large after $label: $bytes" + fi + + width=$($TMUX display-message -p -t stress: '#{pane_width}' 2>/dev/null) + case "$width" in + ''|*[!0-9]*) width=0 ;; + esac + + out=$( + awk -v label="$label" -v width="$width" ' + { + sub(/^[ ]+/, "") + if ($1 != "C") + next + + coord = $2 + sub(/^[0-9]*,/, "", coord) + sub(/[^0-9].*$/, "", coord) + col = coord + 0 + if (width > 0 && (col < 0 || col >= width)) + printf("cell column outside width after %s: width %d: %s\n", label, width, $0) + + data = $0 + if (data !~ /data=\(/) { + printf("cell without data after %s: %s\n", label, $0) + next + } + sub(/^.*data=\(/, "", data) + split(data, parts, ",") + cell_width = parts[1] + 0 + padding = ($0 ~ /flags=[^ ]*PADDING/) + if (!padding && cell_width == 0) + printf("visible zero-width cell after %s: %s\n", label, $0) + }' "$TMP" + ) + [ -z "$out" ] || record_fail "$out" +} + +assert_history_sane() +{ + label=$1 + size=$($TMUX display-message -p -t stress: '#{history_size}' 2>/dev/null) + case "$size" in + ''|*[!0-9]*) + record_fail "bad history size after $label: $size" + ;; + *) + if [ "$size" -gt "$HISTORY_BOUND" ]; then + record_fail "history too large after $label: $size" + fi + ;; + esac +} + +assert_copy_mode_sane() +{ + label=$1 + + $TMUX capture-pane -pM -S -5000 -E - -t stress: >"$TMP" || + record_fail "copy-mode capture failed after $label" + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$COPY_BOUND" ]; then + record_fail "copy-mode capture too large after $label: $bytes" + fi + + if grep -q "$(replacement)" "$TMP"; then + record_fail "replacement character in copy-mode after $label" + fi + + if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then + record_fail "no line ID in copy-mode after $label" + fi +} + +run_resize_checks() +{ + for width in $WIDTHS; do + $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1 + sleep 0.1 + assert_alive "resize to $width" + assert_joined_sane "resize to $width" + assert_raw_sane "resize to $width" + assert_history_sane "resize to $width" + done +} + +wait_outer_contains() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null && + grep -F "$marker" "$TMP" >/dev/null 2>&1 && + return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +assert_live_client_redraw() +{ + $TMUX set-option -t stress: status on >/dev/null || exit 1 + $TMUX set-option -t stress: status-interval 1 >/dev/null || exit 1 + + $TMUX2 kill-server 2>/dev/null + $TMUX2 new-session -d -s out -x 100 -y 12 "$TMUX attach -t stress" || + exit 1 + + i=0 + while [ "$i" -lt 50 ]; do + clients=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$clients" -ge 1 ] && break + sleep 0.2 + i=$((i + 1)) + done + [ "$i" -lt 50 ] || { + record_fail "nested client did not attach" + return + } + + for width in $LIVE_WIDTHS; do + $TMUX set-option -t stress: status-left "REDRAW-$width " >/dev/null || + exit 1 + $TMUX2 resize-window -t out: -x "$width" -y 12 || exit 1 + sleep 0.2 + done + + wait_outer_contains 'REDRAW-80' || + record_fail "outer capture missing final redraw marker" + + $TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null || + record_fail "outer capture failed" + + bytes=$(wc -c <"$TMP") + if [ "$bytes" -gt "$LIVE_BOUND" ]; then + record_fail "outer capture too large: $bytes" + fi + + if grep -q "$(replacement)" "$TMP"; then + record_fail "replacement character in outer capture" + fi + + grep -F 'SENT-POST|ORPHAN-PHASE|complete' "$TMP" >/dev/null 2>&1 || + record_fail "outer capture missing expected sentinel" + + grep -F 'REDRAW-24' "$TMP" >/dev/null 2>&1 && + record_fail "outer capture contains stale width marker" + + $TMUX2 kill-server 2>/dev/null + $TMUX set-option -t stress: status off >/dev/null || exit 1 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null +sleep 0.1 + +$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1 +$TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1 +$TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'stty -echo; exec cat' || exit 1 +$TMUX kill-session -t test-setup || exit 1 +sleep 0.3 + +load_and_paste stress-data make_payload +assert_joined_sane "initial payload" +assert_raw_sane "initial payload" +assert_history_sane "initial payload" + +$TMUX resize-window -t stress: -x 40 -y "$HEIGHT" || exit 1 +sleep 0.1 +load_and_paste stress-orphan-post make_orphan_payload POST +assert_joined_sane "post-resize orphan payload" +assert_raw_sane "post-resize orphan payload" + +run_resize_checks +assert_final_logical_text "return to original width" + +$TMUX copy-mode -H -t stress: || exit 1 +$TMUX send-keys -t stress: -X history-top +sleep 0.1 +assert_alive "copy-mode" +assert_copy_mode_sane "copy-mode history-top" +$TMUX send-keys -t stress: -X cancel +sleep 0.1 + +assert_live_client_redraw + +load_and_paste stress-alt make_alternate_payload +for width in 30 12 80; do + $TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1 + sleep 0.1 + assert_alive "alternate resize to $width" + assert_raw_sane "alternate resize to $width" +done +load_and_paste stress-alt-exit exit_alternate_payload +sleep 0.3 +assert_alive "alternate screen exit" +assert_joined_sane "alternate screen exit" +assert_raw_sane "alternate screen exit" +assert_final_logical_text "alternate screen exit" + +cleanup +exit $exit_status diff --git a/regress/input-replies.sh b/regress/input-replies.sh index 9baed827d..5fc77f843 100644 --- a/regress/input-replies.sh +++ b/regress/input-replies.sh @@ -4,10 +4,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -sleep 0.1 +sleep 0.5 TMP=$(mktemp) EXP=$(mktemp) @@ -18,7 +18,7 @@ $TMUX new-session -d -x 80 -y 24 -s replies \; \ $TMUX set-option -s set-clipboard on || exit 1 $TMUX set-option -s get-clipboard buffer || exit 1 printf Hello | $TMUX load-buffer - -sleep 0.3 +sleep 0.5 exit_status=0 @@ -53,7 +53,7 @@ query_timeout() $TMUX respawn-window -k -t replies:0 \ "stty raw -echo min 0 time 5; printf '$setup'; printf '$seq'; sleep 0.1; dd bs=1 count=128 2>/dev/null | cat -v >$TMP" - sleep 0.7 + sleep 1 printf "%s" "$expected" >"$EXP" cmp "$TMP" "$EXP" || fail "$name" } @@ -62,26 +62,26 @@ query "dsr-ok" '^[[0n' '\033[5n' 4 '' query "dsr-cursor" '^[[1;1R' '\033[6n' 6 '' query "da-primary" '^[[?1;2c' '\033[c' 7 '' query "da-secondary" '^[[>84;0;0c' '\033[>c' 10 '' -query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 8 '' -query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 8 '\033[4h' -query "decrqm-cursor-keys-reset" '^[[?1;2$y' '\033[?1$p' 9 '' -query "decrqm-cursor-keys-set" '^[[?1;1$y' '\033[?1$p' 9 '\033[?1h' -query "decrqm-columns" '^[[?3;4$y' '\033[?3$p' 9 '' -query "decrqm-origin-reset" '^[[?6;2$y' '\033[?6$p' 9 '' -query "decrqm-origin-set" '^[[?6;1$y' '\033[?6$p' 9 '\033[?6h' -query "decrqm-wrap-set" '^[[?7;1$y' '\033[?7$p' 9 '' -query "decrqm-wrap-reset" '^[[?7;2$y' '\033[?7$p' 9 '\033[?7l' -query "decrqm-cursor-visible-set" '^[[?25;1$y' '\033[?25$p' 10 '' -query "decrqm-cursor-visible-reset" '^[[?25;2$y' '\033[?25$p' 10 '\033[?25l' -query "decrqm-mouse-standard-set" '^[[?1000;1$y' '\033[?1000$p' 12 '\033[?1000h' -query "decrqm-mouse-button-set" '^[[?1002;1$y' '\033[?1002$p' 12 '\033[?1002h' -query "decrqm-mouse-all-set" '^[[?1003;1$y' '\033[?1003$p' 12 '\033[?1003h' -query "decrqm-focus-set" '^[[?1004;1$y' '\033[?1004$p' 12 '\033[?1004h' -query "decrqm-mouse-utf8-set" '^[[?1005;1$y' '\033[?1005$p' 12 '\033[?1005h' -query "decrqm-mouse-sgr-set" '^[[?1006;1$y' '\033[?1006$p' 12 '\033[?1006h' -query "decrqm-bracket-paste-set" '^[[?2004;1$y' '\033[?2004$p' 12 '\033[?2004h' -query "decrqm-theme-updates-set" '^[[?2031;1$y' '\033[?2031$p' 12 '\033[?2031h' -query "decrqss-cursor-style" '^[P1$r q0 q' '\033P$q q\033\\' 10 '' +query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 7 '' +query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 7 '\033[4h' +query "decrqm-cursor-keys-reset" '^[[?1;2$y' '\033[?1$p' 8 '' +query "decrqm-cursor-keys-set" '^[[?1;1$y' '\033[?1$p' 8 '\033[?1h' +query "decrqm-columns" '^[[?3;4$y' '\033[?3$p' 8 '' +query "decrqm-origin-reset" '^[[?6;2$y' '\033[?6$p' 8 '' +query "decrqm-origin-set" '^[[?6;1$y' '\033[?6$p' 8 '\033[?6h' +query "decrqm-wrap-set" '^[[?7;1$y' '\033[?7$p' 8 '' +query "decrqm-wrap-reset" '^[[?7;2$y' '\033[?7$p' 8 '\033[?7l' +query "decrqm-cursor-visible-set" '^[[?25;1$y' '\033[?25$p' 9 '' +query "decrqm-cursor-visible-reset" '^[[?25;2$y' '\033[?25$p' 9 '\033[?25l' +query "decrqm-mouse-standard-set" '^[[?1000;1$y' '\033[?1000$p' 11 '\033[?1000h' +query "decrqm-mouse-button-set" '^[[?1002;1$y' '\033[?1002$p' 11 '\033[?1002h' +query "decrqm-mouse-all-set" '^[[?1003;1$y' '\033[?1003$p' 11 '\033[?1003h' +query "decrqm-focus-set" '^[[?1004;1$y' '\033[?1004$p' 11 '\033[?1004h' +query "decrqm-mouse-utf8-set" '^[[?1005;1$y' '\033[?1005$p' 11 '\033[?1005h' +query "decrqm-mouse-sgr-set" '^[[?1006;1$y' '\033[?1006$p' 11 '\033[?1006h' +query "decrqm-bracket-paste-set" '^[[?2004;1$y' '\033[?2004$p' 11 '\033[?2004h' +query "decrqm-theme-updates-set" '^[[?2031;1$y' '\033[?2031$p' 11 '\033[?2031h' +query "decrqss-cursor-style" '^[P1$r q0 q^[\' '\033P$q q\033\\' 12 '' query_timeout "osc-10-query" '^[]10;rgb:ffff/0000/0000^G' '\033]10;?\007' '\033]10;red\007' query_timeout "osc-11-query" '^[]11;rgb:0000/0000/ffff^G' '\033]11;?\007' '\033]11;blue\007' diff --git a/regress/input-requests.sh b/regress/input-requests.sh index d9186c673..094ce2a8a 100644 --- a/regress/input-requests.sh +++ b/regress/input-requests.sh @@ -15,7 +15,8 @@ import tempfile import time tmux = sys.argv[1] -server = [tmux, "-Ltest", "-f/dev/null"] +label = "testA%d" % os.getpid() +server = [tmux, "-L" + label, "-f/dev/null"] def run(*args, check=True): return subprocess.run(server + list(args), check=check, @@ -25,7 +26,7 @@ def attach(): pid, fd = os.forkpty() if pid == 0: os.environ["TERM"] = "xterm-256color" - os.execl(tmux, tmux, "-Ltest", "-f/dev/null", "attach-session", + os.execl(tmux, tmux, "-L" + label, "-f/dev/null", "attach-session", "-t", "requests") os.set_blocking(fd, False) return pid, fd @@ -85,7 +86,7 @@ try: palette_out = f.name respawn("stty raw -echo min 1 time 50; " "printf '\\033]4;99;?\\033\\\\'; " - "dd bs=1 count=64 2>/dev/null | cat -v >%s; sleep 1" % + "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\\") @@ -101,7 +102,7 @@ try: clip_out = f.name respawn("stty raw -echo min 1 time 50; " "printf '\\033]52;c;?\\033\\\\'; " - "dd bs=1 count=64 2>/dev/null | cat -v >%s; sleep 1" % + "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: diff --git a/regress/kill-session-process-exit.sh b/regress/kill-session-process-exit.sh index 04617ca2c..405449be2 100644 --- a/regress/kill-session-process-exit.sh +++ b/regress/kill-session-process-exit.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null sleep 1 diff --git a/regress/lifecycle-deferred.sh b/regress/lifecycle-deferred.sh new file mode 100644 index 000000000..d2669ff46 --- /dev/null +++ b/regress/lifecycle-deferred.sh @@ -0,0 +1,216 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +TMPDIR=$(mktemp -d) +IN="$TMPDIR/in" +OUT="$TMPDIR/out" +CONTROL_PID= + +cleanup() +{ + exec 3>&- 2>/dev/null + [ -n "$CONTROL_PID" ] && kill "$CONTROL_PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMPDIR" +} + +fail() +{ + echo "$1" >&2 + [ -s "$OUT" ] && sed -n '1,120p' "$OUT" >&2 + cleanup + exit 1 +} + +run_tmux() +{ + out= + if command -v timeout >/dev/null 2>&1; then + out=$(timeout 10 $TMUX "$@" 2>&1) + else + out=$($TMUX "$@" 2>&1) + fi + rc=$? + [ "$rc" -eq 0 ] || fail "tmux $* failed ($rc): $out" + printf '%s' "$out" +} + +send_control() +{ + printf '%s\n' "$1" >&3 || fail "failed to write control command: $1" +} + +wait_clients() +{ + want=$1 + i=0 + + while [ "$i" -lt 50 ]; do + have=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$have" -eq "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +wait_format() +{ + target=$1 + format=$2 + want=$3 + i=0 + + while [ "$i" -lt 50 ]; do + have=$($TMUX display-message -p -t "$target" "$format" 2>/dev/null) + [ "$have" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + return 1 +} + +assert_alive() +{ + run_tmux has-session -t life >/dev/null + fields=$(run_tmux display-message -p -t life \ + '#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}') + case "$fields" in + life:@*:%*:*) ;; + *) fail "bad current fields after $1: $fields" ;; + esac +} + +check_control_output() +{ + sleep 1 + + if grep -E '(^%error |server exited|lost server|\(null\)|no current)' \ + "$OUT" >/dev/null 2>&1; then + fail "control client reported an error or invalid object" + fi + + awk ' + $1 == "%session-window-changed" { + if (NF != 3 || $2 !~ /^\$[0-9]+$/ || $3 !~ /^@[0-9]+$/) + bad = 1 + } + $1 == "%subscription-changed" { + colon = 0 + for (i = 2; i <= NF; i++) + if ($i == ":") + colon = 1 + if (NF < 7 || !colon) + bad = 1 + } + END { exit bad } + ' "$OUT" || fail "control client received a malformed notification" +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +run_tmux new-session -d -s life -n prompt -x 80 -y 24 'sleep 1000' \ + >/dev/null +run_tmux set-option -g detach-on-destroy off >/dev/null +run_tmux new-window -t life -n tree 'sleep 1000' >/dev/null +run_tmux new-window -t life -n work 'sleep 1000' >/dev/null +run_tmux select-window -t life:prompt >/dev/null + +run_tmux set-hook -g after-new-window \ + 'display-message -p "hook-new #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g after-split-window \ + 'display-message -p "hook-split #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g after-kill-pane \ + 'display-message -p "hook-kill #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g pane-exited \ + 'display-message -p "hook-exit #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g window-layout-changed \ + 'display-message -p "hook-layout #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux set-hook -g session-window-changed \ + 'display-message -p "hook-current #{session_name}:#{window_id}:#{pane_id}"' \ + >/dev/null +run_tmux bind-key -n M-p command-prompt -P -p '(life)' \ + "set -g @lifecycle-prompt '%% #{session_name}:#{window_id}:#{pane_id}'" \ + >/dev/null + +$TMUX2 new-session -d -s outer -n prompt -x 80 -y 24 "$TMUX attach -t life" \ + || fail "failed to start first attached client" +$TMUX2 new-window -t outer -n tree "$TMUX attach -t life" \ + || fail "failed to start second attached client" +wait_clients 2 || fail "normal clients did not attach" + +$TMUX2 send-keys -t outer:prompt M-p || fail "failed to open command prompt" +sleep 1 +run_tmux choose-tree -t life:tree.0 \ + -F 'tree #{session_name}:#{window_id}:#{pane_id}' >/dev/null +wait_format life:tree.0 '#{pane_mode}' tree-mode || \ + fail "choose-tree did not enter tree-mode" + +mkfifo "$IN" || fail "failed to create control fifo" +(cat "$IN" | $TMUX -C attach -t life >"$OUT" 2>&1) & +CONTROL_PID=$! +exec 3>"$IN" +wait_clients 3 || fail "control client did not attach" + +CONTROL_CLIENT=$($TMUX list-clients -F '#{client_name} #{client_control_mode}' | + awk '$2 == 1 { print $1; exit }') +[ -n "$CONTROL_CLIENT" ] || fail "missing control client" + +send_control "refresh-client -B 'all:%*:#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}'" +send_control "refresh-client -B 'windows:@*:#{session_name}:#{window_id}:#{window_index}:#{window_panes}'" +sleep 1 + +run_tmux kill-pane -t life:prompt.0 >/dev/null +run_tmux kill-window -t life:tree >/dev/null +assert_alive "killing prompt and tree panes" + +i=1 +while [ "$i" -le 20 ]; do + # Keep this sequence fixed: failures should reproduce on the same pass. + s=ld$i + ctl=ctl$i + 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 kill-pane -t "$s:base.1" >/dev/null + + run_tmux new-window -t "$s" -n second 'sleep 1000' >/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 kill-pane -t "$s:single.0" >/dev/null + run_tmux kill-window -t "$s:base" >/dev/null + + run_tmux new-session -d -s "$ctl" -n ctl 'sleep 1000' >/dev/null + run_tmux switch-client -c "$CONTROL_CLIENT" -t "$ctl" >/dev/null + run_tmux kill-session -t "$ctl" >/dev/null + + run_tmux kill-session -t "$s" >/dev/null + assert_alive "iteration $i" + + i=$((i + 1)) +done + +check_control_output +cleanup +exit 0 diff --git a/regress/mode-kill.sh b/regress/mode-kill.sh new file mode 100755 index 000000000..a3e0d09b9 --- /dev/null +++ b/regress/mode-kill.sh @@ -0,0 +1,171 @@ +#!/bin/sh + +# Tests of the -k flag on the mode-entering commands (cmd-copy-mode.c and +# cmd-choose-tree.c). With -k the pane is killed when the mode is exited: this +# is stored on the mode entry in window_pane_set_mode() and acted on in +# window_pane_reset_mode() (window.c). It is exercised here for: +# +# - copy-mode -k (window-copy.c); +# - choose-tree -k (window-tree.c); +# - choose-buffer -k (window-buffer.c). +# +# choose-tree and choose-buffer share cmd_choose_tree_exec(), which also backs +# choose-client and customize-mode, so those are not repeated. +# +# Each mode is entered in the active pane of a two-pane window: exiting with +# -k must remove that pane and leave the other. copy-mode is left with the +# server-side "-X cancel" and needs no client, so those tests run first. The +# tree modes only act on a key once the client has drawn the mode, so - as in +# choose-tree.sh - a second server then provides a client (an inner "tmux +# attach") and its pane is captured to wait until the mode has rendered before +# the exit key is sent. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + exit 1 +} + +# capture the screen rendered by the inner client. +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +# wait_clients $n: wait until the test server has exactly $n clients. +wait_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 clients, have $c" +} + +# wait_mode $target $state: wait until a pane enters (1) or leaves (0) mode. +wait_mode() +{ + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$1" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$2" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $1 mode state is '$got', expected '$2'" +} + +# wait_for $marker: wait until the rendered screen contains $marker. +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + capture | grep -F -q "$1" && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1' to render" +} + +# pane_gone $pane-id: true if the pane no longer exists. (display-message -t +# on a missing pane id falls back to a default target and succeeds, so the +# pane list is searched instead.) +pane_gone() +{ + ! $TMUX list-panes -s -t m -F '#{pane_id}' 2>/dev/null | \ + grep -q -x "$1" +} + +# open_window: make a fresh two-pane window and leave the new (active) pane +# as $active and the other as $other. +open_window() +{ + $TMUX new-window -t m: -n w 'cat' || fail "new-window failed" + $TMUX split-window -t m:w 'cat' || fail "split-window failed" + other=$($TMUX display-message -p -t m:w.0 '#{pane_id}') + active=$($TMUX display-message -p -t m:w '#{pane_id}') +} + +# check_killed $label: wait for the active pane to be killed, leaving only the +# other pane, then drop the window. +check_killed() +{ + i=0 + while [ "$i" -lt 50 ]; do + pane_gone "$active" && break + sleep 0.2 + i=$((i + 1)) + done + pane_gone "$active" || fail "$1: pane not killed on exit" + panes=$($TMUX list-panes -t m:w -F '#{pane_id}' | tr '\n' ' ') + [ "$panes" = "$other " ] || \ + fail "$1: expected only $other left, have $panes" + $TMUX kill-window -t m:w 2>/dev/null +} + +# Session m; window 0 keeps a live pane so the session (and later the client) +# survives each test killing a pane. +$TMUX new-session -d -s m -x 80 -y 24 'cat' || exit 1 + +# --- copy-mode -k kills the pane, plain copy-mode does not ------------------- +# +# copy-mode is exited with the server-side "-X cancel", so no client is needed +# and none is attached yet. +open_window +$TMUX copy-mode -k -t m:w || fail "copy-mode -k failed" +wait_mode "$active" 1 +$TMUX send-keys -t m:w -X cancel || fail "copy cancel failed" +check_killed 'copy-mode -k' + +open_window +$TMUX copy-mode -t m:w || fail "copy-mode failed" +wait_mode "$active" 1 +$TMUX send-keys -t m:w -X cancel || fail "copy cancel failed" +wait_mode "$active" 0 +pane_gone "$active" && fail 'copy-mode: pane killed without -k' +$TMUX kill-window -t m:w 2>/dev/null + +# --- choose-tree -k and choose-buffer -k kill the pane ---------------------- +# +# These need the client to draw the mode before a key acts, so attach one now. +# A paste buffer is needed for choose-buffer to have something to show, and a +# distinct -F marker per call is waited for in the capture so the exit key is +# only sent once the mode is drawn. +$TMUX set-buffer 'mode-kill buffer' || exit 1 +$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t m" || exit 1 +wait_clients 1 +open_window +$TMUX choose-tree -k -F 'TREEMARK' -t m:w || fail "choose-tree -k failed" +wait_for 'TREEMARK' +$TMUX send-keys -t m:w q || fail "choose-tree exit failed" +check_killed 'choose-tree -k' + +open_window +$TMUX choose-buffer -k -F 'BUFMARK' -t m:w || fail "choose-buffer -k failed" +wait_for 'BUFMARK' +$TMUX send-keys -t m:w q || fail "choose-buffer exit failed" +check_killed 'choose-buffer -k' + +exit 0 diff --git a/regress/mode-mutation.sh b/regress/mode-mutation.sh new file mode 100644 index 000000000..96927e995 --- /dev/null +++ b/regress/mode-mutation.sh @@ -0,0 +1,307 @@ +#!/bin/sh + +# Exercise modes while their backing objects are changed from outside the +# client displaying the mode. This catches stale selection indexes and pointers +# after a mode list is shrunk or rebuilt. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR + +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup_servers() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null + sleep 0.5 +} + +cleanup() +{ + cleanup_servers + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +capture() +{ + $TMUX2 capture-pane -p -t out:0 2>/dev/null +} + +assert_alive() +{ + $TMUX display-message -p 'alive' >/dev/null 2>&1 || \ + fail "$1: server exited" +} + +wait_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 clients, have $c" +} + +wait_for() +{ + i=0 + while [ "$i" -lt 50 ]; do + capture | grep -F -q "$1" && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "timed out waiting for '$1'" +} + +wait_mode() +{ + t=$1 + want=$2 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane $t mode state is $got, expected $want" +} + +repeat_key() +{ + t=$1 + key=$2 + count=$3 + + i=0 + while [ "$i" -lt "$count" ]; do + $TMUX send-keys -t "$t" "$key" || \ + fail "failed to send $key to $t" + i=$((i + 1)) + done +} + +start_client() +{ + s=$1 + cmd=${2:-cat} + + cleanup_servers + $TMUX new-session -d -s "$s" -n main -x 80 -y 24 "$cmd" || \ + fail "$s: new-session failed" + $TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t $s" || \ + fail "$s: outer client failed" + wait_clients 1 +} + +new_outer_client() +{ + s=$1 + + $TMUX2 new-window -d -t out: "$TMUX attach -t $s" || \ + fail "$s: outer client failed" +} + +client_for_session() +{ + $TMUX list-clients -F '#{client_name} #{client_session}' | + awk -v s="$1" '$2 == s { print $1; exit }' +} + +test_choose_tree() +{ + start_client tree-a + $TMUX new-session -d -s tree-b -n one 'cat' || fail "tree-b failed" + $TMUX new-window -d -t tree-b -n two 'cat' || fail "tree-b:1 failed" + $TMUX new-session -d -s tree-c -n one 'cat' || fail "tree-c failed" + $TMUX new-session -d -s tree-d -n one 'cat' || fail "tree-d failed" + $TMUX split-window -d -t tree-a:0 'cat' || fail "tree split failed" + + $TMUX choose-tree -t tree-a:0 -O index -F 'MT #{session_name}:#{window_index}.#{pane_index}' || \ + fail "choose-tree failed" + wait_for 'MT ' + repeat_key tree-a:0 j 40 + + $TMUX kill-session -t tree-d || fail "tree kill-session failed" + $TMUX kill-session -t tree-c || fail "tree kill-session failed" + $TMUX rename-session -t tree-b tree-renamed || fail "tree rename failed" + $TMUX rename-window -t tree-renamed:0 renamed || fail "tree rename-window failed" + $TMUX kill-window -t tree-renamed:1 || fail "tree kill-window failed" + side=$($TMUX split-window -d -P -F '#{pane_id}' -t tree-a:0 'cat') || \ + fail "tree side split failed" + $TMUX break-pane -d -s "$side" || fail "tree break-pane failed" + $TMUX join-pane -d -s "$side" -t tree-a:0.0 || \ + fail "tree join-pane failed" + i=0 + while [ "$i" -lt 12 ]; do + $TMUX new-window -d -t tree-a -n "new$i" 'cat' || \ + fail "tree new-window failed" + i=$((i + 1)) + done + + assert_alive "choose-tree mutation" + $TMUX send-keys -t tree-a:0 k j l h Enter || \ + fail "choose-tree keys failed" + wait_mode tree-a:0 0 + assert_alive "choose-tree exit" +} + +test_choose_buffer() +{ + start_client buffer-a + + i=0 + while [ "$i" -lt 30 ]; do + $TMUX set-buffer -b "mbuf$i" "buffer mutation $i" || \ + fail "set-buffer failed" + i=$((i + 1)) + done + + $TMUX choose-buffer -t buffer-a:0 -F 'MB #{buffer_name}' || \ + fail "choose-buffer failed" + wait_for 'MB ' + repeat_key buffer-a:0 j 40 + + i=8 + while [ "$i" -lt 30 ]; do + $TMUX delete-buffer -b "mbuf$i" || fail "delete-buffer failed" + i=$((i + 1)) + done + i=30 + while [ "$i" -lt 50 ]; do + $TMUX set-buffer -b "mbuf$i" "new buffer mutation $i" || \ + fail "new set-buffer failed" + i=$((i + 1)) + done + + assert_alive "choose-buffer mutation" + $TMUX send-keys -t buffer-a:0 k j Enter || \ + fail "choose-buffer keys failed" + wait_mode buffer-a:0 0 + assert_alive "choose-buffer exit" +} + +test_choose_client() +{ + start_client client-a + $TMUX new-session -d -s client-b -n main 'cat' || fail "client-b failed" + $TMUX new-session -d -s client-c -n main 'cat' || fail "client-c failed" + new_outer_client client-b + new_outer_client client-c + wait_clients 3 + + $TMUX choose-client -t client-a:0 -F 'MC #{client_session}' || \ + fail "choose-client failed" + wait_for 'MC ' + repeat_key client-a:0 j 20 + + c=$(client_for_session client-c) + [ -n "$c" ] || fail "client-c client not found" + $TMUX detach-client -t "$c" || fail "detach client-c failed" + c=$(client_for_session client-b) + [ -n "$c" ] || fail "client-b client not found" + $TMUX detach-client -t "$c" || fail "detach client-b failed" + + $TMUX new-session -d -s client-d -n main 'cat' || fail "client-d failed" + new_outer_client client-d + wait_clients 2 + + assert_alive "choose-client mutation" + $TMUX send-keys -t client-a:0 k j Enter || \ + fail "choose-client keys failed" + wait_mode client-a:0 0 + assert_alive "choose-client exit" +} + +test_customize_mode() +{ + start_client option-a + + i=0 + while [ "$i" -lt 30 ]; do + $TMUX set-option -g "@mode_mut_$i" "$i" || \ + fail "set option failed" + i=$((i + 1)) + done + + $TMUX customize-mode -t option-a:0 -F 'MO #{option_name}=#{option_value}' || \ + fail "customize-mode failed" + wait_mode option-a:0 1 + repeat_key option-a:0 j 80 + + i=10 + while [ "$i" -lt 30 ]; do + $TMUX set-option -gu "@mode_mut_$i" || fail "unset option failed" + i=$((i + 1)) + done + i=30 + while [ "$i" -lt 55 ]; do + $TMUX set-option -g "@mode_mut_$i" "$i" || \ + fail "new option failed" + i=$((i + 1)) + done + $TMUX set-option -g status-left 'mutated' || fail "status-left failed" + $TMUX rename-session -t option-a option-renamed || fail "option rename failed" + + assert_alive "customize-mode mutation" + $TMUX send-keys -t option-renamed:0 k j C-d C-u q || \ + fail "customize-mode keys failed" + wait_mode option-renamed:0 0 + assert_alive "customize-mode exit" +} + +test_copy_mode() +{ + start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat' + $TMUX set-window-option -g mode-keys vi || fail "mode-keys failed" + $TMUX split-window -d -t copy-a:0 'cat' || fail "copy split failed" + + $TMUX copy-mode -t copy-a:0 || fail "copy-mode failed" + wait_mode copy-a:0 1 + repeat_key copy-a:0 k 20 + + $TMUX rename-window -t copy-a:0 renamed || fail "copy rename-window failed" + side=$($TMUX split-window -d -P -F '#{pane_id}' -t copy-a:renamed 'cat') || \ + fail "copy side split failed" + $TMUX break-pane -d -s "$side" || fail "copy break-pane failed" + $TMUX join-pane -d -s "$side" -t copy-a:renamed.0 || \ + fail "copy join-pane failed" + $TMUX kill-pane -t copy-a:renamed.1 || fail "copy kill-pane failed" + $TMUX new-window -d -t copy-a -n extra 'cat' || fail "copy new-window failed" + $TMUX kill-window -t copy-a:extra || fail "copy kill-window failed" + $TMUX rename-session -t copy-a copy-renamed || fail "copy rename failed" + + assert_alive "copy-mode mutation" + $TMUX send-keys -t copy-renamed:renamed.0 j k C-d C-u q || \ + fail "copy-mode keys failed" + wait_mode copy-renamed:renamed.0 0 + assert_alive "copy-mode exit" +} + +cleanup_servers +test_choose_tree +test_choose_buffer +test_choose_client +test_customize_mode +test_copy_mode +cleanup +exit 0 diff --git a/regress/new-session-base-index.sh b/regress/new-session-base-index.sh index bb6dd5945..89fdb4de5 100644 --- a/regress/new-session-base-index.sh +++ b/regress/new-session-base-index.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-session-command.sh b/regress/new-session-command.sh index b2fc91db7..520c91e4f 100644 --- a/regress/new-session-command.sh +++ b/regress/new-session-command.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-session-environment.sh b/regress/new-session-environment.sh index e5404e67d..fc935687f 100644 --- a/regress/new-session-environment.sh +++ b/regress/new-session-environment.sh @@ -5,7 +5,7 @@ PATH=/bin:/usr/bin [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TERM=$($TMUX start \; show -gv default-terminal) diff --git a/regress/new-session-no-client.sh b/regress/new-session-no-client.sh index 84fddaecb..2f3da5abe 100644 --- a/regress/new-session-no-client.sh +++ b/regress/new-session-no-client.sh @@ -8,7 +8,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/new-session-size.sh b/regress/new-session-size.sh index 029bd6eed..422ac69fb 100644 --- a/regress/new-session-size.sh +++ b/regress/new-session-size.sh @@ -6,11 +6,10 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -$TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap 'rm -f "$TMP"; $TMUX kill-server 2>/dev/null' 0 1 15 $TMUX -f/dev/null new -d $TMP printf "80 24\n"|cmp -s $TMP - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" $TMUX -f/dev/null new -d -x 100 -y 50 $TMP diff --git a/regress/new-window-command.sh b/regress/new-window-command.sh index 183491159..21a2661e8 100644 --- a/regress/new-window-command.sh +++ b/regress/new-window-command.sh @@ -6,7 +6,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/options-array.sh b/regress/options-array.sh index 3b18ce977..fbaff6ae2 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -3,12 +3,14 @@ # Tests of array options in the options engine (options_array_* in options.c # and the array handling in cmd-set-option.c / cmd-show-options.c). # -# Array options are indexed by integer. This exercises: setting a whole array -# from a separator-delimited string; per-index set with option[N]; -a append -# (which lands at the next free index); show ordering by ascending index and -# preservation of gaps; per-index unset with -u; show -v of a single index and -# of a missing index; and per-option separators (user-keys splits only on -# comma, update-environment on space or comma). +# Array options are keyed by string, with numeric-looking keys kept compatible +# with the old numeric forms. This exercises: setting a whole array from a +# separator-delimited string; per-key set with option[key]; -a append (which +# lands at the next free numeric key); show ordering by numeric keys first in +# ascending order followed by string keys in strcmp order; preservation of gaps; +# per-key unset with -u; show -v of a single key and of a missing key; and +# per-option separators (user-keys splits only on comma, update-environment on +# space or comma). # # update-environment (session), status-format (session), user-keys (server) # and command-alias (server) are used as representative array options. @@ -20,7 +22,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null check_value() @@ -34,6 +36,21 @@ check_value() fi } +check_wait_value() +{ + i=0 + while [ "$i" -lt 30 ]; do + out=$($TMUX show $1 2>&1) + [ "$out" = "$2" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + echo "show $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 +} + # check_array $args $expected # # Compare the full (multi-line) show output for an array option with a @@ -87,41 +104,59 @@ $TMUX new-session -d -s main -x 80 -y 24 || exit 1 # --- whole-array assignment splits on the separator ----------------------- # # update-environment has the default " ," separator, so a single string value -# is split into consecutive indices starting at 0. +# is split into consecutive numeric keys starting at 0. check_ok set -g update-environment "AAA BBB,CCC" check_array "-g update-environment" "update-environment[0] AAA update-environment[1] BBB update-environment[2] CCC" -# --- -a append goes to the next free index -------------------------------- +# --- -a append goes to the next free numeric key -------------------------- check_ok set -ga update-environment "DDD" check_array "-g update-environment" "update-environment[0] AAA update-environment[1] BBB update-environment[2] CCC update-environment[3] DDD" -# --- per-index unset leaves a gap; show preserves order and gaps ---------- +# --- per-key unset leaves a gap; show preserves order and gaps ------------ check_ok set -gu update-environment[1] check_array "-g update-environment" "update-environment[0] AAA update-environment[2] CCC update-environment[3] DDD" -# show -v of an existing index returns its value; a missing index is empty. +# show -v of an existing key returns its value; a missing key is empty. check_value "-gv update-environment[0]" "AAA" check_value "-gv update-environment[1]" "" +check_ok set -g update-environment[notify] "EEE" +check_ok set -ga update-environment "FFF" +check_array "-g update-environment" "update-environment[0] AAA +update-environment[1] FFF +update-environment[2] CCC +update-environment[3] DDD +update-environment[notify] EEE" -# --- explicit indexed set, including out-of-order and gaps ---------------- +# --- explicit keyed set, including out-of-order and gaps ------------------ # # status-format is a session array; assigning an empty string first clears its -# multi-index default, then set specific indices out of order and confirm show -# sorts by ascending index and keeps the gap at [1]. +# multi-index default, then set specific keys out of order and confirm show +# sorts by ascending numeric key and keeps the gap at [1]. check_ok set -g status-format "" check_array "-g status-format" "status-format" check_ok set -g status-format[5] "five" check_ok set -g status-format[0] "zero" check_ok set -g status-format[2] "two" +check_ok set -g status-format[01] "one" +check_ok set -g status-format[zoom] "zoom" +check_ok set -g status-format[foo-bar] "foo-bar" +check_ok set -g status-format[xterm-256color] "xterm" check_array "-g status-format" "status-format[0] zero +status-format[1] one status-format[2] two -status-format[5] five" +status-format[5] five +status-format[foo-bar] foo-bar +status-format[xterm-256color] xterm +status-format[zoom] zoom" +check_value "-gv status-format[01]" "one" +check_ok set -gu status-format[zoom] +check_value "-gv status-format[zoom]" "" # --- comma-only separator (user-keys) ------------------------------------- # @@ -133,11 +168,48 @@ user-keys[1] "Two Three"' # --- command-type array (a hook) ------------------------------------------ # -# Hooks are command arrays: an indexed value is parsed as a command when set +# Hooks are command arrays: a keyed value is parsed as a command when set # and re-printed from the parsed command list; a syntax error is reported. check_ok set -g alert-bell[0] "display-message hi" check_value "-gv alert-bell[0]" "display-message hi" check_fail "syntax error" set -g alert-bell[0] "if -x {" +check_ok set-hook -g window-renamed[notify] "display-message renamed" +check_value "-gv window-renamed[notify]" "display-message renamed" +check_ok set-hook -gu window-renamed[notify] +check_value "-gv window-renamed[notify]" "" +check_ok set -g @hook_first 0 +check_ok set -g @hook_second 0 +check_ok set-hook -g window-renamed[first] "set -g @hook_first 1" +check_ok set-hook -g window-renamed[second] "set -g @hook_second 1" +shown=$($TMUX show-hooks -g window-renamed) || { + echo "show-hooks -g window-renamed failed" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[first\]' || { + echo "missing first hook key: $shown" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[second\]' || { + echo "missing second hook key: $shown" + exit 1 +} +check_ok rename-window -t main:0 array-hooks +check_wait_value "-gqv @hook_first" "1" +check_wait_value "-gqv @hook_second" "1" +check_ok set-hook -gu window-renamed[first] +shown=$($TMUX show-hooks -g window-renamed) || { + echo "show-hooks -g window-renamed after unset failed" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[first\]' && { + echo "first hook key was not unset: $shown" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[second\]' || { + echo "second hook key did not remain: $shown" + exit 1 +} +check_ok set-hook -gu window-renamed[second] # --- colour-type array ---------------------------------------------------- # @@ -146,11 +218,11 @@ check_ok set -w pane-colours[0] red check_value "-wv pane-colours[0]" "red" check_fail "bad colour: xxxyyy" set -w pane-colours[1] xxxyyy -# --- -o refuses to overwrite an already-set index ------------------------- +# --- -o refuses to overwrite an already-set key --------------------------- check_ok set -g command-alias[9] "x=list-keys" check_fail "already set: command-alias[9]" set -go command-alias[9] "y=list-keys" -# --- non-array option rejects index syntax -------------------------------- +# --- non-array option rejects key syntax ---------------------------------- # # status-left is a plain string; indexing it is an error. check_fail "not an array: status-left[0]" set -g status-left[0] "x" diff --git a/regress/options-scope.sh b/regress/options-scope.sh index bbc7168aa..ac6d62f77 100644 --- a/regress/options-scope.sh +++ b/regress/options-scope.sh @@ -17,7 +17,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check_value $args $expected diff --git a/regress/options-values.sh b/regress/options-values.sh index 28acd2b47..75da52e01 100644 --- a/regress/options-values.sh +++ b/regress/options-values.sh @@ -17,7 +17,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null check_value() diff --git a/regress/osc-11colours.sh b/regress/osc-11colours.sh index 2fba76d82..5cdc1be41 100644 --- a/regress/osc-11colours.sh +++ b/regress/osc-11colours.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null $TMUX new -d diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh new file mode 100644 index 000000000..29c79e2e2 --- /dev/null +++ b/regress/pane-ops.sh @@ -0,0 +1,640 @@ +#!/bin/sh + +# Tests of pane management command semantics (not parsing), as implemented in +# cmd-split-window.c, cmd-break-pane.c, cmd-join-pane.c (join-pane and +# move-pane), cmd-swap-pane.c, cmd-kill-pane.c, cmd-respawn-pane.c, +# cmd-respawn-window.c, cmd-resize-pane.c and cmd-select-pane.c. +# +# This exercises: +# - split-window -h/-v with -l in cells and percent, -b placing the new pane +# before (left/top of) the target and -f spanning the full window size; +# - break-pane moving a pane into a new window (-d, -n name, -a after, -P -F +# printing the new location); +# - join-pane moving a window's only pane into another window (destroying the +# source window), -b before, -l size, and the identical-panes error; +# - move-pane as an alias for join-pane; +# - swap-pane -U/-D/-s/-t, -d keeping the active pane, and the marked pane +# (select-pane -m/-M) as the default swap source; +# - kill-pane, kill-pane -a keeping only the target; +# - respawn-pane/respawn-window refusing a live pane without -k, working on a +# dead pane (remain-on-exit) and killing with -k; +# - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z +# zoom/unzoom (including implicit unzoom on split). +# +# window-ops.sh covers window-level commands and buffers.sh paste buffers. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_fmt $target $format $expected +# +# Expand a format in a target's context and compare with $expected. +check_fmt() +{ + out=$($TMUX display-message -p -t "$1" "$2" 2>&1) + if [ "$out" != "$3" ]; then + echo "Format '$2' for '$1' wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_panes $target $expected +# +# Compare the pane list of a window (as "index:id ...", in index order) with +# $expected. +check_panes() +{ + out=$(echo $($TMUX list-panes -t "$1" -F '#{pane_index}:#{pane_id}')) + if [ "$out" != "$2" ]; then + echo "Pane list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# split-window geometry. + +check_ok new-session -d -s P -x 80 -y 24 -n main +p0=$($TMUX display-message -p -t P:0.0 '#{pane_id}') + +# Horizontal split with -l in cells: new pane gets exactly that width and the +# old pane the rest minus the separator line. +check_ok split-window -d -h -l 20 -t "$p0" +p1=$($TMUX display-message -p -t P:0.1 '#{pane_id}') +check_fmt "$p1" '#{pane_width}x#{pane_height}' '20x24' +check_fmt "$p0" '#{pane_width}x#{pane_height}' '59x24' + +# Vertical split with a percentage of the pane being split. +check_ok split-window -d -v -l 25% -t "$p0" +p2=$($TMUX display-message -p -t P:0.1 '#{pane_id}') +check_fmt "$p2" '#{pane_width}x#{pane_height}' '59x6' +check_fmt "$p0" '#{pane_width}x#{pane_height}' '59x17' + +# -b puts the new pane to the left of the target; -f makes it span the full +# window height. +check_ok split-window -d -h -b -f -l 10 -t "$p0" +p3=$($TMUX display-message -p -t P:0.0 '#{pane_id}') +check_fmt "$p3" '#{pane_width}x#{pane_height}' '10x24' +check_fmt "$p3" '#{pane_left},#{pane_top}' '0,0' +check_fmt "$p0" '#{pane_width}x#{pane_height}' '50x17' +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# The new pane becomes active unless -d is given. +check_ok select-pane -t "$p0" +check_ok split-window -d -v -t "$p0" +check_fmt 'P:0' '#{pane_id}' "$p0" +p4=$($TMUX display-message -p -t P:0.2 '#{pane_id}') +check_ok split-window -v -t "$p4" +p5=$($TMUX display-message -p -t 'P:0' '#{pane_id}') +check_ok kill-pane -t "$p5" +check_ok kill-pane -t "$p4" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# --------------------------------------------------------------------------- +# break-pane and join-pane. + +# break-pane moves a pane to a new window; -P -F prints where it went and -n +# names the new window. +out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \ + -s "$p1" -t P:) +if [ "$out" != "1:$p1" ]; then + echo "break-pane -P output wrong: '$out'" + exit 1 +fi +check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1' +check_fmt 'P:0' '#{window_panes}' '3' + +# join-pane -v moves it back (the source window, left empty, is destroyed). +check_ok join-pane -d -v -s P:broken.0 -t "$p2" +check_fmt 'P:0' '#{window_panes}' '4' +if $TMUX has-session -t P:broken 2>/dev/null; then + echo "Window 'broken' still exists after join-pane." + exit 1 +fi + +# The joined pane is below the target (-v, no -b). +top=$($TMUX display-message -p -t "$p2" '#{pane_bottom}') +joined=$($TMUX display-message -p -t "$p1" '#{pane_top}') +if [ "$joined" -le "$top" ]; then + echo "Joined pane is not below target ($joined <= $top)." + exit 1 +fi + +# join-pane -h -b puts the source to the left of the target; -l sets size. +check_ok break-pane -d -n broken -s "$p1" -t P: +check_ok join-pane -d -h -b -l 30 -s P:broken.0 -t "$p2" +check_fmt "$p1" '#{pane_width}' '30' +l1=$($TMUX display-message -p -t "$p1" '#{pane_left}') +l2=$($TMUX display-message -p -t "$p2" '#{pane_left}') +if [ "$l1" -ge "$l2" ]; then + echo "Joined pane is not left of target ($l1 >= $l2)." + exit 1 +fi + +# Joining a pane to itself is an error. +check_fail 'source and target panes must be different' \ + join-pane -d -s "$p0" -t "$p0" + +# break-pane to an occupied window index or with an invalid (non-UTF-8) name +# is an error. +check_fail 'index in use: 0' break-pane -d -s "$p1" -t P:0 +check_fail "invalid window name: $(printf 'a\377b')" \ + break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P: + +# join-pane can move a pane from one window to another without destroying +# the source window if other panes remain. (On this branch move-pane is +# reserved for floating panes, covered by floating-pane-geometry.sh.) +check_ok new-window -d -t P:5 -n other +check_ok join-pane -d -s "$p1" -t P:5.0 +check_fmt 'P:5' '#{window_panes}' '2' +check_fmt 'P:0' '#{window_panes}' '3' +check_ok join-pane -d -v -s "$p1" -t "$p2" +check_fmt 'P:0' '#{window_panes}' '4' +check_fmt 'P:5' '#{window_panes}' '1' + +# --------------------------------------------------------------------------- +# swap-pane. + +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# -s/-t swap two panes. +check_ok swap-pane -d -s "$p3" -t "$p1" +check_panes P:0 "0:$p1 1:$p0 2:$p2 3:$p3" +check_ok swap-pane -d -s "$p3" -t "$p1" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# -U swaps the target pane with the previous pane, -D with the next; without +# -s the target is the active pane. +check_ok swap-pane -d -U -t "$p0" +check_panes P:0 "0:$p0 1:$p3 2:$p2 3:$p1" +check_ok swap-pane -d -D -t "$p0" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# Without -d the target pane becomes the active pane (it arrives at the +# source pane's position). +check_ok select-pane -t "$p0" +check_ok swap-pane -s "$p0" -t "$p2" +check_fmt 'P:0' '#{pane_id}' "$p2" +check_panes P:0 "0:$p3 1:$p2 2:$p0 3:$p1" +check_ok swap-pane -s "$p2" -t "$p0" +check_fmt 'P:0' '#{pane_id}' "$p0" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# With a marked pane and no -s, the marked pane is the swap source. +check_ok select-pane -m -t "$p3" +check_fmt "$p3" '#{pane_marked}' '1' +check_ok swap-pane -d -t "$p1" +check_panes P:0 "0:$p1 1:$p0 2:$p2 3:$p3" +check_ok swap-pane -d -t "$p1" +check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1" + +# select-pane -M clears the mark. +check_ok select-pane -M +check_fmt "$p3" '#{pane_marked}' '0' +check_fmt 'P:0' '#{pane_marked_set}' '0' + +# --------------------------------------------------------------------------- +# resize-pane and zoom. + +# Absolute -x on a horizontal split and percentage. +check_ok resize-pane -t "$p3" -x 20 +check_fmt "$p3" '#{pane_width}' '20' +check_ok resize-pane -t "$p3" -x 25% +check_fmt "$p3" '#{pane_width}' '20' +check_ok resize-pane -t "$p3" -x 10 +check_fmt "$p3" '#{pane_width}' '10' + +# Relative adjustments: -R grows a left pane, -L shrinks it back; a count may +# be given. +check_ok resize-pane -t "$p3" -R +check_fmt "$p3" '#{pane_width}' '11' +check_ok resize-pane -t "$p3" -L +check_fmt "$p3" '#{pane_width}' '10' +check_ok resize-pane -t "$p3" -R 5 +check_fmt "$p3" '#{pane_width}' '15' +check_ok resize-pane -t "$p3" -L 5 +check_fmt "$p3" '#{pane_width}' '10' + +# -y on a vertical split. +check_ok resize-pane -t "$p2" -y 10 +check_fmt "$p2" '#{pane_height}' '10' + +# p2 is the bottom pane, so its bottom border cannot move down: -D instead +# grows it by taking lines from the pane above and -U gives them back. +check_ok resize-pane -t "$p2" -D 2 +check_fmt "$p2" '#{pane_height}' '12' +check_ok resize-pane -t "$p2" -U 2 +check_fmt "$p2" '#{pane_height}' '10' + +# Bad adjustment, width and height are errors. +check_fail 'adjustment invalid' resize-pane -t "$p2" -U nonsense +check_fail 'width invalid' resize-pane -t "$p2" -x nonsense +check_fail 'height invalid' resize-pane -t "$p2" -y nonsense + +# -Z zooms: the pane temporarily fills the window and the flags show it. +check_ok resize-pane -Z -t "$p0" +check_fmt "$p0" '#{window_zoomed_flag}:#{pane_width}x#{pane_height}' \ + '1:80x24' + +# Zoom is transparent to pane commands on other panes, and -Z again unzooms. +check_ok resize-pane -Z -t "$p0" +check_fmt "$p0" '#{window_zoomed_flag}' '0' + +# Splitting while zoomed unzooms first. +check_ok resize-pane -Z -t "$p0" +check_fmt 'P:0' '#{window_zoomed_flag}' '1' +check_ok split-window -d -v -t "$p0" +check_fmt 'P:0' '#{window_zoomed_flag}' '0' +check_fmt 'P:0' '#{window_panes}' '5' +p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}') +check_ok kill-pane -t "$p6" + +# --------------------------------------------------------------------------- +# kill-pane. + +check_fmt 'P:0' '#{window_panes}' '4' +check_ok kill-pane -t "$p3" +check_panes P:0 "0:$p0 1:$p2 2:$p1" + +# -a kills every pane except the target. +check_ok kill-pane -a -t "$p0" +check_panes P:0 "0:$p0" + +# Killing the last pane in a window kills the window. +check_ok new-window -d -t P:7 -n goner +check_ok kill-pane -t P:7.0 +if $TMUX has-session -t P:goner 2>/dev/null; then + echo "Window 'goner' still exists after killing its only pane." + exit 1 +fi + +# --------------------------------------------------------------------------- +# respawn-pane and respawn-window. + +# Respawning a pane whose process is alive fails without -k. +check_fail "respawn pane failed: pane P:0.0 still active" \ + respawn-pane -t P:0.0 +check_fail "respawn window failed: window P:0 still active" \ + respawn-window -t P:0 + +# With remain-on-exit a pane whose command exited stays as a dead pane and +# may be respawned without -k. +check_ok set-option -g remain-on-exit on +check_ok new-window -d -t P:8 -n dead 'true' +i=0 +while [ "$($TMUX display-message -p -t P:8.0 '#{pane_dead}')" != "1" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "Pane did not die."; exit 1; } + sleep 0.1 +done +check_ok respawn-pane -t P:8.0 'sleep 100' +check_fmt 'P:8.0' '#{pane_dead}' '0' + +# -k kills the live process and respawns. +check_ok respawn-pane -k -t P:8.0 'sleep 200' +check_fmt 'P:8.0' '#{pane_dead}' '0' + +# respawn-window -k replaces the whole window (all panes) with one pane. +check_ok split-window -d -t P:8 +check_fmt 'P:8' '#{window_panes}' '2' +check_ok respawn-window -k -t P:8 'sleep 300' +check_fmt 'P:8' '#{window_panes}' '1' +check_fmt 'P:8.0' '#{pane_dead}' '0' +check_ok set-option -g remain-on-exit off + +# --------------------------------------------------------------------------- +# select-pane. + +# A 2x2-ish arrangement: q0 on top, q1 bottom-left, q2 bottom-right. +check_ok new-window -d -t P:2 -n sel +q0=$($TMUX display-message -p -t P:2.0 '#{pane_id}') +check_ok split-window -d -v -t "$q0" +q1=$($TMUX display-message -p -t P:2.1 '#{pane_id}') +check_ok split-window -d -h -t "$q1" +q2=$($TMUX display-message -p -t P:2.2 '#{pane_id}') + +# Directional selection: -D, -R and -U move by pane position. +check_ok select-pane -t "$q0" +check_ok select-pane -D -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q1" +check_ok select-pane -R -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q2" +check_ok select-pane -U -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q0" + +# -l returns to the previously active pane; a window that never had another +# active pane has no last pane. +check_ok select-pane -l -t P:2 +check_fmt 'P:2' '#{pane_id}' "$q2" +check_fail 'no last pane' select-pane -l -t P:8.0 + +# -d disables input to a pane, -e enables it again and -T sets the title. +check_ok select-pane -d -t "$q0" +check_fmt "$q0" '#{pane_input_off}' '1' +check_ok select-pane -e -t "$q0" +check_fmt "$q0" '#{pane_input_off}' '0' +check_ok select-pane -T mytitle -t "$q0" +check_fmt "$q0" '#{pane_title}' 'mytitle' +check_ok kill-window -t P:2 + +# --------------------------------------------------------------------------- +# more split-window variants. + +check_ok new-window -d -t P:2 -n splits + +# new-window -E creates an empty initial pane, running no command. +check_ok new-window -d -E -t P:9 -n empty +check_fmt 'P:9.0' '#{pane_dead}' '0' +check_fmt 'P:9.0' '#{pane_pid}' '' +check_fail 'command cannot be given for empty pane' \ + new-window -d -E -t P:10 -n empty 'true' + +# An empty string as the sole argument is equivalent to -E: the pane is +# created empty, running no command. +check_ok new-window -d -t P:12 -n empty-str '' +check_fmt 'P:12.0' '#{pane_dead}' '0' +check_fmt 'P:12.0' '#{pane_pid}' '' +check_ok kill-window -t P:12 + +# A missing command (rather than an empty one) runs the default command, so +# the pane is not empty and has a process. +check_ok new-window -d -t P:12 -n default-cmd +check_fmt 'P:12.0' '#{?pane_pid,live,empty}' 'live' +check_ok kill-window -t P:12 + +# respawn-pane -E stores the command and cwd without starting it. +tmp=${TMPDIR:-/tmp}/tmux-pane-ops-empty-$$ +rm -f "$tmp" +check_ok new-window -d -E -t P:10 -n empty-respawn +check_ok respawn-pane -E -c /tmp -t P:10.0 "pwd > $tmp" +if [ -e "$tmp" ]; then + echo "respawn-pane -E started command unexpectedly" + exit 1 +fi +check_ok respawn-pane -t P:10.0 +i=0 +while [ ! -e "$tmp" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && echo "respawn-pane did not start stored command" && \ + exit 1 + sleep 0.1 +done +if [ "$(cat "$tmp")" != "/tmp" ]; then + echo "respawn-pane did not use stored cwd" + exit 1 +fi +rm -f "$tmp" +check_ok kill-window -t P:9 + +# respawn-window -E stores the command and cwd without starting it. +tmp=${TMPDIR:-/tmp}/tmux-window-ops-empty-$$ +rm -f "$tmp" +check_ok new-window -d -E -t P:11 -n empty-respawn-window +check_ok respawn-window -E -c /tmp -t P:11 "pwd > $tmp" +check_fmt 'P:11.0' '#{pane_pid}' '' +if [ -e "$tmp" ]; then + echo "respawn-window -E started command unexpectedly" + exit 1 +fi +check_ok respawn-window -t P:11 +i=0 +while [ ! -e "$tmp" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && echo "respawn-window did not start stored command" && \ + exit 1 + sleep 0.1 +done +if [ "$(cat "$tmp")" != "/tmp" ]; then + echo "respawn-window did not use stored cwd" + exit 1 +fi +rm -f "$tmp" + +# -E splits with an empty pane, running no command; giving one is an error. +check_ok split-window -d -E -t P:2.0 +check_fmt 'P:2' '#{window_panes}' '2' +check_fail 'command cannot be given for empty pane' \ + split-window -d -E -t P:2.0 'sleep 5' + +# An empty string as the sole argument splits with an empty pane, like -E. +eid=$($TMUX split-window -d -P -F '#{pane_id}' -t P:2.0 '') +check_fmt "$eid" '#{pane_dead}' '0' +check_fmt "$eid" '#{pane_pid}' '' +check_ok kill-pane -t "$eid" +check_fmt 'P:2' '#{window_panes}' '2' + +# -e adds to the new pane's environment. +eid=$($TMUX split-window -d -P -F '#{pane_id}' -e GREETING=hello -t P:2.0 \ + 'echo $GREETING; exec cat') +i=0 +while out=$($TMUX capture-pane -p -t "$eid" | sed -n 1p) && \ + [ "$out" != "hello" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "split-window -e wrong: '$out'"; exit 1; } + sleep 0.1 +done + +# A bad -l size is an error. +check_fail 'invalid tiled geometry invalid' \ + split-window -d -v -l invalid -t P:2.0 + +# -Z zooms the new pane. +check_ok split-window -d -Z -t P:2.0 +check_fmt 'P:2' '#{window_zoomed_flag}' '1' +check_ok kill-window -t P:2 + +# --------------------------------------------------------------------------- +# more swap-pane: wrapping, cross-window, self and zoomed swaps. + +check_ok new-window -d -t P:3 -n swaps +check_ok split-window -d -v -t P:3.0 +check_ok split-window -d -v -t P:3.0 +r0=$($TMUX display-message -p -t P:3.0 '#{pane_id}') +r1=$($TMUX display-message -p -t P:3.1 '#{pane_id}') +r2=$($TMUX display-message -p -t P:3.2 '#{pane_id}') +o0=$($TMUX display-message -p -t P:5.0 '#{pane_id}') + +# -D on the last pane and -U on the first wrap around to the other end. +check_ok swap-pane -d -D -t "$r2" +check_panes P:3 "0:$r2 1:$r1 2:$r0" +check_ok swap-pane -d -s "$r0" -t "$r2" +check_ok swap-pane -d -U -t "$r0" +check_panes P:3 "0:$r2 1:$r1 2:$r0" +check_ok swap-pane -d -s "$r0" -t "$r2" +check_panes P:3 "0:$r0 1:$r1 2:$r2" + +# Swapping a pane with itself quietly does nothing. +check_ok swap-pane -d -s "$r1" -t "$r1" +check_panes P:3 "0:$r0 1:$r1 2:$r2" + +# Panes can be swapped between different windows. +check_ok swap-pane -d -s "$o0" -t "$r1" +check_panes P:3 "0:$r0 1:$o0 2:$r2" +check_panes P:5 "0:$r1" +check_ok swap-pane -d -s "$r1" -t "$o0" +check_panes P:3 "0:$r0 1:$r1 2:$r2" +check_panes P:5 "0:$o0" + +# -Z keeps the window zoomed across the swap. +check_ok resize-pane -Z -t "$r0" +check_ok swap-pane -d -Z -s "$r0" -t "$r1" +check_fmt 'P:3' '#{window_zoomed_flag}' '1' +check_ok resize-pane -Z -t P:3 +check_panes P:3 "0:$r1 1:$r0 2:$r2" + +# kill-pane -a -f only kills other panes matching the filter. +check_ok kill-pane -a -f '#{==:#{pane_id},'"$r2"'}' -t "$r1" +check_panes P:3 "0:$r1 1:$r0" +check_ok kill-window -t P:3 + +# --------------------------------------------------------------------------- +# split-window -I and -s. + +check_ok new-window -d -t P:3 -n splits2 + +# -I fills the new (empty) pane from standard input. +printf 'stdin-stuff' | $TMUX split-window -d -I -t P:3.0 +if [ $? -ne 0 ]; then + echo "split-window -I failed." + exit 1 +fi +i=0 +while out=$($TMUX capture-pane -p -t P:3.1 | sed -n 1p) && \ + [ "$out" != "stdin-stuff" ]; do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "split-window -I wrong: '$out'"; exit 1; } + sleep 0.1 +done + +# -s sets the new pane's window-style. +sid=$($TMUX split-window -d -P -F '#{pane_id}' -s 'bg=red' -t P:3.0) +out=$($TMUX show-options -v -p -t "$sid" window-style) +if [ "$out" != "bg=red" ]; then + echo "split-window -s style wrong: '$out'" + exit 1 +fi +check_ok kill-window -t P:3 + +# --------------------------------------------------------------------------- +# more break-pane: -a insertion, selection and single-pane windows. + +# check_windows $session $expected +# +# Compare the window list of a session (as "index:name ...") with $expected. +check_windows() +{ + out=$(echo $($TMUX list-windows -t "$1" -F \ + '#{window_index}:#{window_name}')) + if [ "$out" != "$2" ]; then + echo "Window list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_ok new-session -d -s Q -x 80 -y 24 -n q0 + +# -a breaks into a new window inserted after the target index, shuffling the +# following windows up. +check_ok new-window -d -t Q:1 -n q1 +check_ok split-window -d -t Q:1 +check_ok break-pane -d -a -s Q:1.1 -n qa -t Q:0 +check_windows Q '0:q0 1:qa 2:q1' + +# Without -d the new window is selected. +check_ok split-window -d -t Q:2 +check_ok select-window -t Q:0 +check_ok break-pane -s Q:2.1 -n qcur -t Q: +check_fmt 'Q:' '#{window_name}' 'qcur' +check_windows Q '0:q0 1:qa 2:q1 3:qcur' + +# Breaking the only pane of a window relinks the window at a new index; -n +# still renames it. +check_ok new-window -d -t Q:5 -n qsolo +out=$($TMUX break-pane -d -P -F '#{window_name}' -s Q:5.0 -n qmoved -t Q:) +if [ "$out" != "qmoved" ]; then + echo "single-pane break-pane output wrong: '$out'" + exit 1 +fi +if $TMUX has-session -t Q:qsolo 2>/dev/null; then + echo "Window 'qsolo' still exists after single-pane break-pane." + exit 1 +fi +check_ok has-session -t Q:qmoved +check_ok kill-session -t Q + +# --------------------------------------------------------------------------- +# resize-pane -T. + +# -T trims the history: lines below the cursor position are removed and the +# cursor moves to the bottom. seq writes 100 lines (leaving 77 in history on +# a 24-line screen) and the escape sequence puts the cursor on line 5. +check_ok new-window -d -t P:2 'seq 1 100; printf "\033[5;1H"; exec cat' +i=0 +while [ "$($TMUX display-message -p -t P:2.0 '#{history_size}')" != "77" ] +do + i=$((i + 1)) + [ $i -gt 50 ] && { echo "History did not fill."; exit 1; } + sleep 0.1 +done +check_fmt 'P:2.0' '#{cursor_y}' '4' +check_ok resize-pane -T -t P:2.0 +check_fmt 'P:2.0' '#{history_size}' '58' +check_fmt 'P:2.0' '#{cursor_y}' '23' +check_ok kill-window -t P:2 + +assert_alive + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/prompt-keys.sh b/regress/prompt-keys.sh index 8c17581b1..6fde51b7b 100644 --- a/regress/prompt-keys.sh +++ b/regress/prompt-keys.sh @@ -7,8 +7,8 @@ LANG=C.UTF-8 export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -OUT="$TEST_TMUX -Ltest -f/dev/null" # outer (host for the client) -IN="$TEST_TMUX -Ltest2 -f/dev/null" # inner (under test) +OUT="$TEST_TMUX -LtestA$$ -f/dev/null" # outer (host for the client) +IN="$TEST_TMUX -LtestB$$ -f/dev/null" # inner (under test) $OUT kill-server 2>/dev/null $IN kill-server 2>/dev/null diff --git a/regress/prompt-mechanics.sh b/regress/prompt-mechanics.sh index 6f3e5294e..9e57d7a55 100644 --- a/regress/prompt-mechanics.sh +++ b/regress/prompt-mechanics.sh @@ -20,8 +20,8 @@ LANG=C.UTF-8 export TERM LC_ALL LANG [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -OUT="$TEST_TMUX -Ltest -f/dev/null" # outer (host for the client) -IN="$TEST_TMUX -Ltest2 -f/dev/null" # inner (under test) +OUT="$TEST_TMUX -LtestA$$ -f/dev/null" # outer (host for the client) +IN="$TEST_TMUX -LtestB$$ -f/dev/null" # inner (under test) $OUT kill-server 2>/dev/null $IN kill-server 2>/dev/null @@ -49,6 +49,19 @@ status_line() { got() { $IN show -gv @r } +wait_opt() { + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($IN show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} reset() { $IN set -g @r "SENTINEL" || exit 1 } @@ -85,6 +98,12 @@ $IN bind -n M-j command-prompt -P -I hello -p '(pre)' "set -g @r '%%'" || e $IN bind -n M-m command-prompt -p 'first,second' "set -g @r '%1/%2'" || exit 1 $IN bind -n M-c command-prompt -p '(cmd)' "set -g @r '%%'" || exit 1 $IN bind -n M-h command-prompt -T search -p '(srch)' "set -g @r '%%'" || exit 1 +$IN set-hook -g pane-prompt-opened \ + 'set -gF @prompt_open "#{hook}:#{hook_pane}:#{hook_prompt_type}"' || + exit 1 +$IN set-hook -g pane-prompt-closed \ + 'set -gF @prompt_close "#{hook}:#{hook_pane}:#{hook_prompt_type}"' || + exit 1 # --- Outer session: attach the inner one inside its pane. ------------------- $OUT new -d -x80 -y24 || exit 1 @@ -111,14 +130,17 @@ settle # --- 1b. window.c: drawn over the pane, not on the status line. --- reset +pane=$($IN display-message -p '#{pane_id}') || exit 1 $OUT send-keys M-p || exit 1 settle +wait_opt @prompt_open "pane-prompt-opened:$pane:command" capture | grep -qF '(pane)' || fail "pane prompt not drawn in the pane" status_line | grep -qF '(pane)' && \ fail "pane prompt drawn on the status line, not over the pane" $OUT send-keys -l "deep" || exit 1 $OUT send-keys Enter || exit 1 settle +wait_opt @prompt_close "pane-prompt-closed:$pane:command" [ "$(got)" = "deep" ] || fail "pane prompt accept recovered '$(got)', wanted 'deep'" # --- 1c. mode-tree.c: search prompt drawn in the pane. --- diff --git a/regress/run-shell-output.sh b/regress/run-shell-output.sh index 3b44504c2..926a520c9 100644 --- a/regress/run-shell-output.sh +++ b/regress/run-shell-output.sh @@ -7,7 +7,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/screen-redraw-bidi.sh b/regress/screen-redraw-bidi.sh index e4e962950..aa8d21627 100644 --- a/regress/screen-redraw-bidi.sh +++ b/regress/screen-redraw-bidi.sh @@ -17,8 +17,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) @@ -42,6 +42,25 @@ compare() { fi } +wait_for_pane() { + i=0 + while [ $i -lt 50 ]; do + case "$($TMUX2 capturep -p -t "$1")" in + *"$2"*) return ;; + esac + i=$((i + 1)) + sleep 0.1 + done + fail "pane $1 did not contain: $2" +} + +attach_scene() { + # Start with a fresh outer screen and attach only after the complete + # scene exists, so it receives one initial redraw. + $TMUX respawnp -k "$TMUX2 attach" || exit 1 + sleep 1 +} + new_scene() { $TMUX2 neww -d "sh -c 'printf \"BIDI-LEFT 12345\nBIDI-RIGHT 67890\"; exec sleep 100'" || exit 1 $TMUX2 selectw -t:\$ || exit 1 @@ -63,17 +82,19 @@ $TMUX new -d -x40 -y12 || exit 1 $TMUX set -g status off || exit 1 $TMUX set -g window-size manual || exit 1 $TMUX set -g default-terminal "tmux-256color" || exit 1 -$TMUX send -l "$TMUX2 attach" || exit 1 -$TMUX send Enter || exit 1 -sleep 1 # Single pane: content is wrapped in isolates. new_scene +wait_for_pane ':.0' 'BIDI-RIGHT 67890' +attach_scene compare bidi-single # Two panes: borders and both panes are isolated. new_scene $TMUX2 splitw -h "$C" || exit 1 +wait_for_pane ':.0' 'BIDI-RIGHT 67890' +wait_for_pane ':.1' 'BIDI-PANE 67890' +attach_scene compare bidi-split exit 0 diff --git a/regress/screen-redraw-cache.sh b/regress/screen-redraw-cache.sh index 27c98e3b4..0e0abf9db 100644 --- a/regress/screen-redraw-cache.sh +++ b/regress/screen-redraw-cache.sh @@ -18,8 +18,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-floating.sh b/regress/screen-redraw-floating.sh index 82c36d364..67d213699 100644 --- a/regress/screen-redraw-floating.sh +++ b/regress/screen-redraw-floating.sh @@ -16,8 +16,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-indicators.sh b/regress/screen-redraw-indicators.sh index 60bf652b2..28a1dee6c 100644 --- a/regress/screen-redraw-indicators.sh +++ b/regress/screen-redraw-indicators.sh @@ -18,8 +18,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-outside.sh b/regress/screen-redraw-outside.sh index 53a2a72b1..3eeb94938 100644 --- a/regress/screen-redraw-outside.sh +++ b/regress/screen-redraw-outside.sh @@ -18,8 +18,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-popups.sh b/regress/screen-redraw-popups.sh index 1eb7e5897..26e0f7175 100644 --- a/regress/screen-redraw-popups.sh +++ b/regress/screen-redraw-popups.sh @@ -17,13 +17,19 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX= +TMUX2= +SETUP=0 RESULTS=screen-redraw-results TMP=$(mktemp) -trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ - 0 1 15 + +cleanup() { + rm -f "$TMP" + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null +} +trap cleanup 0 1 15 fail() { echo "$*" >&2 @@ -46,8 +52,11 @@ C="sh -c 'i=0; while [ \$i -lt 13 ]; do printf \"POP%02d abcdefghij\n\" \$i; i=\ # setup: fresh inner window attached inside a fresh outer pane, 40x14. setup() { - $TMUX kill-server 2>/dev/null - $TMUX2 kill-server 2>/dev/null + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null + SETUP=$((SETUP + 1)) + TMUX="$TEST_TMUX -LtestA$$-$SETUP -f/dev/null" + TMUX2="$TEST_TMUX -LtestB$$-$SETUP -f/dev/null" $TMUX2 new -d -x40 -y14 "$C" || exit 1 $TMUX2 set -g status off || exit 1 $TMUX2 set -g window-size manual || exit 1 diff --git a/regress/screen-redraw-scrollbars.sh b/regress/screen-redraw-scrollbars.sh index f8298b5e1..914fe542c 100644 --- a/regress/screen-redraw-scrollbars.sh +++ b/regress/screen-redraw-scrollbars.sh @@ -16,8 +16,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-status.sh b/regress/screen-redraw-status.sh index 12ca20bb1..46cb5fcad 100644 --- a/regress/screen-redraw-status.sh +++ b/regress/screen-redraw-status.sh @@ -20,8 +20,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/screen-redraw-tiled.sh b/regress/screen-redraw-tiled.sh index 94353f5f8..de7c57aff 100644 --- a/regress/screen-redraw-tiled.sh +++ b/regress/screen-redraw-tiled.sh @@ -24,8 +24,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) @@ -49,16 +49,17 @@ compare() { fi } +C="sh -c 'exec sleep 1000'" + # Fresh inner window of fixed size, with one command running. new_scene() { $TMUX2 selectw -t:0 || exit 1 $TMUX2 kill-window -a 2>/dev/null - $TMUX2 neww -d "sh -c 'exec sleep 100'" || exit 1 + $TMUX2 neww -d "$C" || exit 1 $TMUX2 selectw -t:\$ || exit 1 $TMUX2 resizew -x40 -y14 || exit 1 } -C="sh -c 'exec sleep 100'" # Layouts. Each produces one kind of junction. Splits at fixed window size are # deterministic. diff --git a/regress/screen-redraw-window-style.sh b/regress/screen-redraw-window-style.sh index 1d2b2b281..69ac824f2 100644 --- a/regress/screen-redraw-window-style.sh +++ b/regress/screen-redraw-window-style.sh @@ -13,8 +13,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" RESULTS=screen-redraw-results TMP=$(mktemp) diff --git a/regress/session-group-resize.sh b/regress/session-group-resize.sh index 28e7a85ed..f2b634a43 100644 --- a/regress/session-group-resize.sh +++ b/regress/session-group-resize.sh @@ -10,7 +10,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null TMP1=$(mktemp) diff --git a/regress/session-ops.sh b/regress/session-ops.sh new file mode 100644 index 000000000..7fc2b65cb --- /dev/null +++ b/regress/session-ops.sh @@ -0,0 +1,347 @@ +#!/bin/sh + +# Tests of session management command semantics, as implemented in +# cmd-new-session.c, cmd-rename-session.c, cmd-kill-session.c and +# cmd-has-session.c, plus grouped sessions (new-session -t). +# +# This exercises: +# - new-session naming: explicit -s, invalid and duplicate names, automatic +# numeric names, -n naming the initial window and -A attaching to (here: +# not duplicating) an existing session; +# - session_id/session_name/session_windows formats and has-session; +# - rename-session, including duplicate and invalid names, and that the +# session keeps its id when renamed; +# - kill-session, kill-session -a (all but target), the "-f only valid with +# -a" guard, and that killing the last session stops the server; +# - grouped sessions: new-session -t shares the window list (a window made +# in one session appears in the other; killed windows disappear), current +# windows are tracked independently and destroying one grouped session +# leaves the windows in the other. +# +# session-group-resize.sh covers sizing of grouped sessions. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@" &1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_fmt $target $format $expected +# +# Expand a format in a target's context and compare with $expected. +check_fmt() +{ + out=$($TMUX display-message -p -t "$1" "$2" 2>&1) + if [ "$out" != "$3" ]; then + echo "Format '$2' for '$1' wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_sessions $expected +# +# Compare the session list (as "name name ...", sorted by name) with +# $expected. +check_sessions() +{ + out=$(echo $($TMUX list-sessions -F '#{session_name}' | LC_ALL=C sort)) + if [ "$out" != "$1" ]; then + echo "Session list wrong." + echo "Expected: '$1'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_windows $session $expected +# +# Compare the window list of a session (as "index:name ...") with $expected. +check_windows() +{ + out=$(echo $($TMUX list-windows -t "$1" -F \ + '#{window_index}:#{window_name}')) + if [ "$out" != "$2" ]; then + echo "Window list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +# wait_client_session $expected +# +# Wait for the only test client to be attached to $expected. +wait_client_session() +{ + expected=$1 + i=0 + + while [ $i -lt 30 ]; do + out=$($TMUX list-clients -F '#{client_session}' 2>/dev/null || + true) + if [ "$out" = "$expected" ]; then + return + fi + i=$((i + 1)) + sleep 0.1 + done + + echo "Client session wrong." + echo "Expected: '$expected'" + echo "But got: '$out'" + exit 1 +} + +# attach_control_client $session +# +# Attach a control client to $session and keep its input open on fd 9. +attach_control_client() +{ + fifo=$(mktemp -u) + + mkfifo "$fifo" || exit 1 + $TMUX -C attach-session -t "$1" <"$fifo" >/dev/null 2>&1 & + control_pid=$! + exec 9>"$fifo" + rm -f "$fifo" + wait_client_session "$1" +} + +# check_attached_destroy $mode +# +# Run an attached tmux inside a pane and destroy its only session. +check_attached_destroy() +{ + mode=$1 + outer=DODouter$mode + outdir=$(mktemp -d) || exit 1 + script=$outdir/inner.sh + rcfile=$outdir/rc + + cat >"$script" <<-EOF + #!/bin/sh + "$TEST_TMUX" -LtestInner$$-$mode -f/dev/null new \\; \ + set -g detach-on-destroy $mode \\; send exit Enter + printf '%s\n' \$? >"$rcfile" + EOF + chmod +x "$script" || exit 1 + + check_ok new-session -d -s "$outer" -x 80 -y 24 + check_ok send-keys -t "$outer:0.0" "sh $script" Enter + + i=0 + while [ $i -lt 50 ]; do + [ -f "$rcfile" ] && break + i=$((i + 1)) + sleep 0.1 + done + + pane=$($TMUX capture-pane -pt "$outer:0.0" -S -) + if [ ! -f "$rcfile" ]; then + echo "Inner tmux did not exit." + echo "$pane" + $TMUX kill-session -t "$outer" 2>/dev/null || true + rm -rf "$outdir" + exit 1 + fi + rc=$(cat "$rcfile") + if [ "$rc" != 0 ]; then + echo "Inner tmux exited with status $rc." + echo "$pane" + $TMUX kill-session -t "$outer" 2>/dev/null || true + rm -rf "$outdir" + exit 1 + fi + case "$pane" in + *"server exited unexpectedly"*) + echo "Inner tmux server crashed." + echo "$pane" + $TMUX kill-session -t "$outer" 2>/dev/null || true + rm -rf "$outdir" + exit 1 + ;; + esac + + check_ok kill-session -t "$outer" + rm -rf "$outdir" +} + +# --------------------------------------------------------------------------- +# new-session and has-session. + +check_ok new-session -d -s S1 -x 80 -y 24 -n first +check_fmt 'S1:' '#{session_name}:#{window_name}:#{session_windows}' \ + 'S1:first:1' + +# A duplicate name is an error. Only invalid UTF-8 is rejected as a name: +# colons, periods and even an empty string are allowed (such sessions can +# only be targeted by id). +check_fail 'duplicate session: S1' new-session -d -s S1 +badname=$(printf 'a\377b') +check_fail "invalid session name: $badname" new-session -d -s "$badname" +oddid=$($TMUX new-session -d -s 'a:b.c' -x 80 -y 24 -P -F '#{session_id}') +check_fmt "$oddid" '#{session_name}' 'a:b.c' +check_ok kill-session -t "$oddid" +emptyid=$($TMUX new-session -d -s '' -x 80 -y 24 -P -F '#{session_id}') +check_fmt "$emptyid" '#{session_name}' '' +check_ok kill-session -t "$emptyid" + +# Without -s, sessions get a numeric name matching their id counter. +autoname=$($TMUX new-session -d -x 80 -y 24 -P -F '#{session_name}') +autoid=$($TMUX display-message -p -t "=$autoname:" '#{session_id}') +if [ "\$$autoname" != "$autoid" ]; then + echo "Automatic session name '$autoname' does not match id '$autoid'." + exit 1 +fi +check_ok has-session -t "=$autoname" +check_ok rename-session -t "=$autoname" S2 +check_sessions 'S1 S2' + +# -A creates the session only if it does not exist; if it does, -A means +# attach, which a detached client without a terminal cannot do. +check_ok new-session -d -A -s S3 +check_sessions 'S1 S2 S3' +check_fail 'open terminal failed: not a terminal' new-session -d -A -s S3 +check_sessions 'S1 S2 S3' +check_ok kill-session -t S3 + +# has-session fails for a missing session. +check_fail "can't find session: nosuch" has-session -t nosuch + +# --------------------------------------------------------------------------- +# rename-session. + +# The id survives a rename and the old name is gone. +id=$($TMUX display-message -p -t S2: '#{session_id}') +check_ok rename-session -t S2 newname +check_sessions 'S1 newname' +check_fmt "$id" '#{session_name}' 'newname' +check_fail "can't find session: S2" has-session -t S2 + +# Renaming to an existing or invalid name is an error. +check_fail 'duplicate session: S1' rename-session -t newname S1 +check_fail "invalid session name: $badname" rename-session -t newname \ + "$badname" +check_ok rename-session -t newname S2 + +# --------------------------------------------------------------------------- +# grouped sessions (new-session -t). + +check_ok new-session -d -s G1 -x 80 -y 24 -n shared +check_ok new-session -d -s G2 -t G1 +check_fmt 'G1:' '#{session_grouped}:#{session_group_size}' '1:2' +check_fmt 'G2:' '#{session_grouped}:#{session_group_list}' '1:G1,G2' + +# The window list is shared: windows created or killed in one session +# appear and disappear in the other. +check_ok new-window -d -t G2: -n added +check_windows G1 '0:shared 1:added' +check_windows G2 '0:shared 1:added' +check_ok kill-window -t G1:added +check_windows G2 '0:shared' + +# The current window is tracked per session. +check_ok new-window -d -t G2:1 -n other +check_ok select-window -t G1:0 +check_ok select-window -t G2:1 +check_fmt 'G1:' '#{window_name}' 'shared' +check_fmt 'G2:' '#{window_name}' 'other' + +# Killing one grouped session leaves the windows in the other (the group +# itself survives with a single member). +check_ok kill-session -t G2 +check_windows G1 '0:shared 1:other' +check_fmt 'G1:' '#{session_grouped}:#{session_group_size}' '1:1' +check_ok kill-window -t G1:other + +# --------------------------------------------------------------------------- +# kill-session. + +check_sessions 'G1 S1 S2' +check_fail '-f only valid with -a' kill-session -f 'x' -t S1 + +# -C only clears alerts; the session survives. +check_ok kill-session -C -t S2 +check_ok has-session -t S2 + +# detach-on-destroy previous and next move attached clients in +# alphabetical order and must not crash when a session is destroyed. +check_ok new-session -d -s DODa -x 80 -y 24 +check_ok new-session -d -s DODb -x 80 -y 24 +check_ok new-session -d -s DODc -x 80 -y 24 +check_ok set-option -t DODb detach-on-destroy previous +attach_control_client DODb +check_ok kill-session -t DODb +wait_client_session DODa +exec 9>&- +wait "$control_pid" 2>/dev/null || true +check_ok kill-session -t DODa +check_ok kill-session -t DODc + +check_ok new-session -d -s DODa -x 80 -y 24 +check_ok new-session -d -s DODb -x 80 -y 24 +check_ok new-session -d -s DODc -x 80 -y 24 +check_ok set-option -t DODb detach-on-destroy next +attach_control_client DODb +check_ok kill-session -t DODb +wait_client_session DODc +exec 9>&- +wait "$control_pid" 2>/dev/null || true +check_ok kill-session -t DODa +check_ok kill-session -t DODc + +# With only one session, previous and next have no replacement session; the +# attached client must exit cleanly rather than being moved to the dying +# session. +check_attached_destroy previous +check_attached_destroy next + +# -a kills every other session. +check_ok kill-session -a -t S1 +check_sessions 'S1' + +# Killing the last session stops the server. +check_ok kill-session -t S1 +if $TMUX has-session -t S1 2>/dev/null; then + echo "Server still up after killing the last session." + exit 1 +fi + +exit 0 diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh new file mode 100644 index 000000000..0144884dc --- /dev/null +++ b/regress/set-hook-B.sh @@ -0,0 +1,160 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 15 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || \ + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +$TMUX new -d -s one || fail "new-session failed" + +$TMUX set -g @seen 0 || fail "set @seen failed" +$TMUX set-hook -g -B '@session-name::#{session_name}' \ + 'set -g @seen "#{hook}:#{hook_value}"' || + fail "set-hook -B failed" +shown=$($TMUX show-hooks -g -B @session-name) || + fail "show-hooks -B failed" +[ "$shown" = '@session-name::#{session_name}' ] || + fail "unexpected show-hooks -B output: $shown" +assert_unchanged @seen 0 + +$TMUX rename-session two || fail "rename-session two failed" +wait_for @seen '@session-name:two' + +$TMUX set -g @seen-last 0 || fail "set @seen-last failed" +$TMUX set-hook -g -B '@session-name::#{session_name}' \ + 'set -g @seen-last "#{hook_last}->#{hook_value}"' || + fail "set-hook -B replacement failed" +assert_unchanged @seen-last 0 +$TMUX rename-session one || fail "rename-session one failed" +wait_for @seen-last 'two->one' + +$TMUX set-hook -gu -B @session-name || fail "set-hook -gu -B failed" +shown=$($TMUX show-hooks -g -B @session-name) || + fail "show-hooks -B after remove failed" +[ -z "$shown" ] || fail "show-hooks -B showed removed monitor: $shown" +last=$($TMUX show -gqv @seen-last) +$TMUX rename-session three || fail "rename-session three failed" +assert_unchanged @seen-last "$last" + +$TMUX set -gu @value || fail "unset @value failed" +$TMUX set -g @empty-seen 0 || fail "set @empty-seen failed" +$TMUX set-hook -g -B '@empty::#{@value}' \ + 'set -g @empty-seen "#{hook_last}->#{hook_value}"' || + fail "set-hook -B empty failed" +assert_unchanged @empty-seen 0 +$TMUX set -g @value changed || fail "set @value failed" +wait_for @empty-seen '->changed' + +if $TMUX set-hook -g -B 'bad::#{session_name}' 'display-message x' \ + >"$OUT/bad.out" 2>"$OUT/bad.err"; then + fail "non-@ monitor hook name was accepted" +fi + +session=$($TMUX display -p '#{session_id}') +window=$($TMUX display -p '#{window_id}') +pane=$($TMUX display -p '#{pane_id}') +pane_number=${pane#%} + +$TMUX set -gu @pane-value || fail "unset @pane-value failed" +$TMUX set -g @pane-seen 0 || fail "set @pane-seen failed" +$TMUX set-hook -g -B "@pane:%$pane_number:#{pane_width}" \ + 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || + fail "set-hook -B pane selector failed" +assert_unchanged @pane-seen 0 +$TMUX set-hook -g -B "@pane:%$pane_number:#{@pane-value}" \ + 'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' || + fail "set-hook -B pane replacement failed" +assert_unchanged @pane-seen 0 +$TMUX set -g @pane-value changed || fail "set @pane-value failed" +wait_for @pane-seen "$session:$window:0:$pane:changed" + +$TMUX set -g @exact-value one || fail "set @exact-value failed" +$TMUX set -g @exact-seen 0 || fail "set @exact-seen failed" +$TMUX set -gw @foo 'set -g @exact-seen inherited' || + fail "set global @foo failed" +$TMUX set-hook -w -B '@foo::#{@exact-value}' || + fail "set-hook -B exact scope monitor failed" +assert_unchanged @exact-seen 0 +$TMUX set -g @exact-value two || fail "set @exact-value two failed" +assert_unchanged @exact-seen 0 +$TMUX set-hook -w -B '@foo::#{@exact-value}' \ + 'set -g @exact-seen "#{hook_value}"' || + fail "set-hook -B exact scope command failed" +assert_unchanged @exact-seen 0 +$TMUX set -g @exact-value three || fail "set @exact-value three failed" +wait_for @exact-seen three + +target_pane=$($TMUX splitw -P -F '#{pane_id}') || + fail "split-window failed" +$TMUX set -g @target-pane 0 || fail "set @target-pane failed" +$TMUX set-hook -g -B '@target:%*:#{@target-value}' \ + 'set -g @target-pane "#{pane_id}"' || + fail "set-hook -B target pane failed" +assert_unchanged @target-pane 0 +$TMUX set -pt "$target_pane" @target-value changed || + fail "set pane @target-value failed" +wait_for @target-pane "$target_pane" + +$TMUX new -d -s zzz-survivor || fail "new survivor session failed" +$TMUX set -g @global-after-destroy 0 || + fail "set @global-after-destroy failed" +$TMUX set-hook -g -t three -B '@global-after-destroy-session::#{session_name}' \ + 'set -g @global-after-destroy "#{hook_last}->#{hook_value}"' || + fail "set-hook -B global after destroy failed" +assert_unchanged @global-after-destroy 0 +$TMUX kill-session -t three || fail "kill destroyed monitor session failed" +wait_for @global-after-destroy 'three->zzz-survivor' + +exit 0 diff --git a/regress/set-hook-E.sh b/regress/set-hook-E.sh new file mode 100755 index 000000000..f8d30d05e --- /dev/null +++ b/regress/set-hook-E.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_channel() +{ + channel=$1 + + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX wait-for "$channel" || + fail "wait-for $channel timed out" + return + fi + + $TMUX wait-for "$channel" & + pid=$! + i=0 + while kill -0 "$pid" 2>/dev/null; do + [ $i -lt 50 ] || { + kill "$pid" 2>/dev/null || true + fail "wait-for $channel timed out" + } + i=$((i + 1)) + sleep 0.2 + done + wait "$pid" || fail "wait-for $channel failed" +} + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + count=${3:-15} + i=0 + + while [ $i -lt "$count" ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_list() +{ + name=$1 + i=0 + + while [ $i -lt 50 ]; do + value=$($TMUX wait-for -E -l "$name" 2>/dev/null || true) + if [ -n "$value" ]; then + printf '%s\n' "$value" | sed -n '1p' + return + fi + i=$((i + 1)) + sleep 0.2 + done + fail "wait-for -E -l $name found no waiters" +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +$TMUX set -g @event_seen 0 || fail "set @event_seen failed" +$TMUX wait-for -E @manual-event \; set -g @event_seen 1 \; \ + wait-for -S she-event & +event_pid=$! +wait_list @manual-event >/dev/null + +$TMUX set-hook -E @manual-event || fail "set-hook -E @manual-event failed" +wait_channel she-event +wait "$event_pid" || fail "wait-for -E @manual-event failed" +wait_for @event_seen 1 + +$TMUX set-hook -E @no-sink || fail "set-hook -E @no-sink failed" + +pane=$($TMUX display -pt two:0.0 '#{pane_id}') || + fail "display-message pane failed" +$TMUX set -g @hook_seen 0 || fail "set @hook_seen failed" +$TMUX set-hook -g @manual-hook \ + 'set -gF @hook_seen "#{hook}:#{session_name}:#{window_index}:#{pane_id}"' || + fail "set-hook @manual-hook failed" + +$TMUX set-hook -E -t two:0.0 @manual-hook || + fail "set-hook -E @manual-hook failed" +wait_for @hook_seen "@manual-hook:two:0:$pane" + +$TMUX set -g @r_hook 0 || fail "set @r_hook failed" +$TMUX set -g @r_event 0 || fail "set @r_event failed" +$TMUX set-hook -g @manual-r 'set -g @r_hook 1' || + fail "set-hook @manual-r failed" +$TMUX wait-for -E @manual-r \; set -g @r_event 1 \; wait-for -S she-r & +r_pid=$! +r_client=$(wait_list @manual-r) + +$TMUX set-hook -R @manual-r || fail "set-hook -R @manual-r failed" +wait_for @r_hook 1 +assert_unchanged @r_event 0 5 + +$TMUX wait-for -E -w "$r_client" @manual-r || + fail "wait-for -E -w @manual-r failed" +wait_channel she-r +wait "$r_pid" || fail "wait-for -E @manual-r failed" + +if $TMUX set-hook -E window-renamed 2>/dev/null; then + fail "set-hook -E window-renamed succeeded" +fi + +exit 0 diff --git a/regress/set-hook-R.sh b/regress/set-hook-R.sh new file mode 100644 index 000000000..49af68efc --- /dev/null +++ b/regress/set-hook-R.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +pane=$($TMUX display -pt two:0.0 '#{pane_id}') || + fail "display-message pane failed" + +$TMUX set -g @seen 0 || fail "set @seen failed" +$TMUX set-hook -g @manual \ + 'set -gF @seen "#{hook}:#{session_name}:#{window_index}:#{pane_id}"' || + fail "set-hook @manual failed" + +[ "$($TMUX show -gqv @seen)" = 0 ] || + fail "hook ran before set-hook -R" + +$TMUX set-hook -g -R -t two:0.0 @manual || + fail "set-hook -R @manual failed" +wait_for @seen "@manual:two:0:$pane" + +exit 0 diff --git a/regress/style-trim.sh b/regress/style-trim.sh index 5b03482d9..f42e0ffff 100644 --- a/regress/style-trim.sh +++ b/regress/style-trim.sh @@ -17,9 +17,9 @@ if command -v bash >/dev/null 2>&1; then fi [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX2 kill-server 2>/dev/null $TMUX2 -f/dev/null new -d "$TMUX -f/dev/null new -- $shell" diff --git a/regress/targets-panes.sh b/regress/targets-panes.sh index db353403b..9ea936173 100644 --- a/regress/targets-panes.sh +++ b/regress/targets-panes.sh @@ -27,7 +27,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check $target $expected [format] diff --git a/regress/targets.sh b/regress/targets.sh index 346a86736..75ee3dbb8 100644 --- a/regress/targets.sh +++ b/regress/targets.sh @@ -29,7 +29,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null # check $target $expected [format] diff --git a/regress/theme-report.sh b/regress/theme-report.sh new file mode 100755 index 000000000..4c42244b4 --- /dev/null +++ b/regress/theme-report.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# Test that the theme reported to a pane (mode 2031 / DSR 996) follows the +# terminal's reported theme, not a guess from the background colour. +# +# An inner client is attached inside an outer tmux pane. The outer pane has a +# light background, so the outer server answers the inner client's DSR 996 with +# light. The inner server is given a dark window background, so a guess from the +# background colour would say dark. A pane in the inner server then queries DSR +# 996: the answer must be light (2), following the client's reported theme, not +# dark (1) from the background. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +INNER="$TEST_TMUX -LtestI$$ -f/dev/null" +OUTER="$TEST_TMUX -LtestO$$ -f/dev/null" +$INNER kill-server 2>/dev/null +$OUTER kill-server 2>/dev/null + +TMP=$(mktemp) +trap "rm -f $TMP; $INNER kill-server 2>/dev/null; $OUTER kill-server 2>/dev/null" \ + 0 1 15 + +# Inner server, dark background, keep panes alive across the query respawn. +$INNER new-session -d -x80 -y24 || exit 1 +$INNER set -g remain-on-exit on +$INNER set -g window-style 'bg=black' + +# Outer server with a light background, running the inner client attached so the +# inner client has a real terminal that reports a theme. +$OUTER new-session -d -x80 -y24 || exit 1 +$OUTER set -g window-style 'bg=white' +$OUTER new-window "$INNER attach" || exit 1 +sleep 2 + +# Query DSR 996 from an inner pane and capture the CSI ? 997 ; Ps n reply. +$INNER respawnw -k -t:0 -- sh -c " + exec 2>/dev/null + stty raw -echo + printf '\033[?996n' + dd bs=1 count=9 2>/dev/null | cat -v > $TMP + sleep 1 +" +sleep 2 + +actual=$(cat "$TMP") +expected='^[[?997;2n' # 2 = light (from the terminal), not 1 = dark (from bg) + +if [ "$actual" = "$expected" ]; then + [ -n "$VERBOSE" ] && echo "[PASS] reported terminal theme ($actual)" + exit 0 +fi + +echo "[FAIL] expected '$expected' (light, from terminal), got '$actual'" +exit 1 diff --git a/regress/tty-draw-line.sh b/regress/tty-draw-line.sh index b57de08cc..59331aa0d 100644 --- a/regress/tty-draw-line.sh +++ b/regress/tty-draw-line.sh @@ -10,8 +10,8 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" fail() { echo "$*" >&2 diff --git a/regress/tty-keys.sh b/regress/tty-keys.sh index 849f6f336..1b300b995 100644 --- a/regress/tty-keys.sh +++ b/regress/tty-keys.sh @@ -4,9 +4,9 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" $TMUX kill-server 2>/dev/null -TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" $TMUX2 kill-server 2>/dev/null TMP=$(mktemp) diff --git a/regress/utf8-test.sh b/regress/utf8-test.sh index b3cf41bd0..1836cc459 100644 --- a/regress/utf8-test.sh +++ b/regress/utf8-test.sh @@ -4,7 +4,7 @@ PATH=/bin:/usr/bin TERM=screen [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -Ltest -f/dev/null" +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMP=$(mktemp) trap "rm -f $TMP" 0 1 15 $TMUX kill-server 2>/dev/null diff --git a/regress/wait-for-E.sh b/regress/wait-for-E.sh new file mode 100644 index 000000000..f58437a34 --- /dev/null +++ b/regress/wait-for-E.sh @@ -0,0 +1,206 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +LANG=C.UTF-8 +export TERM LC_ALL LANG + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_channel() +{ + channel=$1 + + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX wait-for "$channel" || + fail "wait-for $channel timed out" + return + fi + + $TMUX wait-for "$channel" & + pid=$! + i=0 + while kill -0 "$pid" 2>/dev/null; do + [ $i -lt 50 ] || { + kill "$pid" 2>/dev/null || true + fail "wait-for $channel timed out" + } + i=$((i + 1)) + sleep 0.2 + done + wait "$pid" || fail "wait-for $channel failed" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + count=${3:-15} + i=0 + + while [ $i -lt "$count" ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_list() +{ + event=$1 + name=$2 + i=0 + + while [ $i -lt 50 ]; do + if [ "$event" = 1 ]; then + value=$($TMUX wait-for -E -l "$name" 2>/dev/null || true) + else + value=$($TMUX wait-for -l "$name" 2>/dev/null || true) + fi + if [ -n "$value" ]; then + printf '%s\n' "$value" | sed -n '1p' + return + fi + i=$((i + 1)) + sleep 0.2 + done + fail "wait-for -l $name found no waiters" +} + +$TMUX new -d -s wf || fail "new-session failed" + +$TMUX wait-for wf-list & +list_pid=$! +client=$(wait_list 0 wf-list) +$TMUX wait-for -w "$client" wf-list || fail "wait-for -w wf-list failed" +wait "$list_pid" || fail "wait-for -w did not wake channel waiter" + +$TMUX set -g @forced 0 || fail "set @forced failed" +$TMUX wait-for -E @forced-event \; set -g @forced 1 & +forced_pid=$! +client=$(wait_list 1 @forced-event) +$TMUX wait-for -E -w "$client" @forced-event || + fail "wait-for -E -w @forced-event failed" +wait "$forced_pid" || fail "wait-for -E -w did not wake event waiter" +[ "$($TMUX show -gqv @forced)" = 1 ] || + fail "wait-for -E -w did not continue event waiter" + +if $TMUX wait-for -E foobar 2>/dev/null; then + fail "wait-for -E accepted invalid event" +fi + +$TMUX wait-for -E @not-yet-fired & +not_yet_pid=$! +client=$(wait_list 1 @not-yet-fired) +$TMUX wait-for -E -w "$client" @not-yet-fired || + fail "wait-for -E -w @not-yet-fired failed" +wait "$not_yet_pid" || fail "wait-for -E @not-yet-fired failed" + +$TMUX set -g @wf_value 0 || fail "set @wf_value failed" +$TMUX set-hook -g -B '@wf::#{@wf_value}' 'wait-for -S wf-hook' || + fail "set-hook -B failed" + +$TMUX wait-for -E @wf \; wait-for -S wf-event & +event_pid=$! + +# Let the monitor take its first sample so the next change is reported. +sleep 1.5 + +$TMUX set -g @wf_value 1 || fail "set @wf_value 1 failed" + +wait_channel wf-event +wait_channel wf-hook +wait "$event_pid" || fail "wait-for -E command failed" + +$TMUX set -g @late 0 || fail "set @late failed" +$TMUX wait-for -E @wf \; set -g @late 1 \; wait-for -S wf-late & +late_pid=$! +assert_unchanged @late 0 5 + +$TMUX set -g @wf_value 2 || fail "set @wf_value 2 failed" +wait_channel wf-late +wait "$late_pid" || fail "late wait-for -E command failed" + +$TMUX set -g @filtered 0 || fail "set @filtered failed" +$TMUX wait-for -E -F '#{==:#{value},3}' @wf \; set -g @filtered 1 \; \ + wait-for -S wf-filtered & +filtered_pid=$! +assert_unchanged @filtered 0 5 + +$TMUX set -g @wf_value unmatched || fail "set @wf_value unmatched failed" +assert_unchanged @filtered 0 5 + +$TMUX set -g @wf_value 3 || fail "set @wf_value 3 failed" +wait_channel wf-filtered +wait "$filtered_pid" || fail "filtered wait-for -E command failed" + +verbose_file="$OUT/verbose" +$TMUX wait-for -E -v @wf \; wait-for -S wf-verbose >"$verbose_file" & +verbose_pid=$! + +sleep 0.5 +$TMUX set -g @wf_value 4 || fail "set @wf_value 4 failed" +wait_channel wf-verbose +wait "$verbose_pid" || fail "verbose wait-for -E command failed" +grep '^event=@wf$' "$verbose_file" >/dev/null || + fail "verbose wait-for -E did not print event payload" +grep '^value=4$' "$verbose_file" >/dev/null || + fail "verbose wait-for -E did not print value payload" +grep '^_hook_monitor=' "$verbose_file" >/dev/null && + fail "verbose wait-for -E printed private payload" + +$TMUX new -d -s wf2 || fail "new-session wf2 failed" + +$TMUX wait-for -E window-renamed \; wait-for -S wf-renamed & +renamed_pid=$! + +sleep 0.5 +$TMUX rename-window -t wf2:0 renamed || fail "rename-window failed" +wait_channel wf-renamed +wait "$renamed_pid" || fail "wait-for -E window-renamed failed" + +$TMUX set-hook -g window-renamed 'wait-for -S wf-hook-renamed' || + fail "set-hook window-renamed failed" +$TMUX rename-window -t wf2:0 renamed-again || + fail "rename-window renamed-again failed" +wait_channel wf-hook-renamed + +$TMUX set -g @builtin_filtered 0 || fail "set @builtin_filtered failed" +target=$($TMUX splitw -d -t wf2:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window target failed" +$TMUX wait-for -E -F "#{==:#{pane},$target}" pane-exited \; \ + set -g @builtin_filtered 1 \; wait-for -S wf-builtin-filtered & +builtin_filtered_pid=$! +assert_unchanged @builtin_filtered 0 5 + +$TMUX splitw -d -t wf2:0 'true' || fail "split-window nonmatching failed" +assert_unchanged @builtin_filtered 0 5 + +$TMUX send-keys -t "$target" C-c || fail "send C-c to target failed" +wait_channel wf-builtin-filtered +wait "$builtin_filtered_pid" || + fail "filtered wait-for -E pane-exited command failed" + +exit 0 diff --git a/regress/window-ops.sh b/regress/window-ops.sh new file mode 100644 index 000000000..bdb1b5fb9 --- /dev/null +++ b/regress/window-ops.sh @@ -0,0 +1,406 @@ +#!/bin/sh + +# Tests of window management command semantics (not parsing), as implemented +# in cmd-new-window.c, cmd-move-window.c (move-window and link-window), +# cmd-unlink-window.c, cmd-swap-window.c, cmd-rotate-window.c, +# cmd-kill-window.c and cmd-select-window.c. +# +# This exercises: +# - new-window placement: next free index, explicit index, index in use with +# and without -k, -a (after) and -b (before) insertion with shuffling, and +# -S selecting an existing window by name instead of creating; +# - move-window to a free index, to an occupied index with and without -k, +# -a insertion and -r renumbering (including base-index); +# - renumber-windows closing gaps; +# - link-window sharing a window between two sessions (window_linked and +# window_linked_sessions), unlink-window removing one link and refusing to +# unlink the last link without -k; +# - swap-window within and between sessions, -d keeping the active window, +# and the grouped-sessions error; +# - rotate-window -U/-D rotating pane positions; +# - kill-window switching to the last (previously current) window, kill-window +# -a killing all other windows and the "-f only valid with -a" guard. +# +# pane-ops.sh covers pane-level commands and buffers.sh covers paste buffers. + +PATH=/bin:/usr/bin +TERM=screen +LANG=C.UTF-8 +LC_ALL=C.UTF-8 +export TERM LANG LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_ok $cmd... +# +# Run a command and require that it succeeds. +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +# +# Run a command and require that it fails with the given error message. +check_fail() +{ + exp="$1" + shift + out=$($TMUX "$@" 2>&1) + if [ $? -eq 0 ]; then + echo "Command succeeded (expected failure): $*" + exit 1 + fi + if [ "$out" != "$exp" ]; then + echo "Wrong error for: $*" + echo "Expected: '$exp'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_windows $session $expected +# +# Compare the window list of a session (as "index:name index:name ...", in +# index order) with $expected. +check_windows() +{ + out=$(echo $($TMUX list-windows -t "$1" -F \ + '#{window_index}:#{window_name}')) + if [ "$out" != "$2" ]; then + echo "Window list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_fmt $target $format $expected +# +# Expand a format in a target's context and compare with $expected. +check_fmt() +{ + out=$($TMUX display-message -p -t "$1" "$2" 2>&1) + if [ "$out" != "$3" ]; then + echo "Format '$2' for '$1' wrong." + echo "Expected: '$3'" + echo "But got: '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +# --------------------------------------------------------------------------- +# new-window placement. + +check_ok new-session -d -s W -x 80 -y 24 -n w0 + +# Next free index. +check_ok new-window -d -t W: -n w1 +check_ok new-window -d -t W: -n w2 +check_windows W '0:w0 1:w1 2:w2' + +# Explicit index, then the next new window fills the first free index, not +# one past the highest. +check_ok new-window -d -t W:9 -n w9 +check_ok new-window -d -t W: -n w3 +check_windows W '0:w0 1:w1 2:w2 3:w3 9:w9' + +# Occupied index fails without -k and replaces with -k. +check_fail 'create window failed: index 9 in use' \ + new-window -d -t W:9 -n dup +check_ok new-window -d -k -t W:9 -n w9k +check_windows W '0:w0 1:w1 2:w2 3:w3 9:w9k' + +# -a inserts after the target, shuffling the following windows up. +check_ok new-window -d -a -t W:1 -n wA +check_windows W '0:w0 1:w1 2:wA 3:w2 4:w3 9:w9k' + +# -b inserts before the target, shuffling the target and followers up. +check_ok new-window -d -b -t W:0 -n wB +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' + +# -S selects an existing window with the same name instead of creating (with +# -d it would not switch, so no -d here). +check_ok select-window -t W:0 +check_ok new-window -S -t W: -n w3 +check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k' +check_fmt 'W:' '#{window_index}:#{window_name}' '5:w3' + +# Clean up to a known arrangement. +check_ok kill-window -t W:wB +check_ok kill-window -t W:wA +check_ok kill-window -t W:w9k +check_ok move-window -r -t W: +check_windows W '0:w0 1:w1 2:w2 3:w3' + +# --------------------------------------------------------------------------- +# move-window. + +# To a free index. +check_ok move-window -d -s W:2 -t W:7 +check_windows W '0:w0 1:w1 3:w3 7:w2' + +# To an occupied index, without and with -k. +check_fail 'index in use: 7' move-window -d -s W:3 -t W:7 +check_ok move-window -d -k -s W:3 -t W:7 +check_windows W '0:w0 1:w1 7:w3' + +# -a inserts after the target and shuffles. +check_ok move-window -d -a -s W:7 -t W:0 +check_windows W '0:w0 1:w3 2:w1' + +# -r renumbers in order, respecting base-index. +check_ok move-window -d -s W:2 -t W:8 +check_ok set-option -t W base-index 5 +check_ok move-window -r -t W: +check_windows W '5:w0 6:w3 7:w1' +check_ok set-option -t W base-index 0 +check_ok move-window -r -t W: +check_windows W '0:w0 1:w3 2:w1' + +# With the renumber-windows option on, killing a window renumbers the rest +# automatically. +check_ok set-option -t W renumber-windows on +check_ok new-window -d -t W:9 -n wtmp +check_windows W '0:w0 1:w3 2:w1 9:wtmp' +check_ok kill-window -t W:1 +check_windows W '0:w0 1:w1 2:wtmp' +check_ok kill-window -t W:2 +check_ok set-option -t W renumber-windows off +check_ok new-window -d -t W:2 -n w3 +check_windows W '0:w0 1:w1 2:w3' + +# Without -s, the current window of the client/session moves. +check_ok select-window -t W:2 +check_ok move-window -d -t W:6 +check_windows W '0:w0 1:w1 6:w3' +check_ok move-window -r -t W: +check_windows W '0:w0 1:w1 2:w3' + +# --------------------------------------------------------------------------- +# link-window and unlink-window. + +check_ok new-session -d -s L -x 80 -y 24 -n l0 + +# Link a window from W into L and check it is shared. +check_ok link-window -d -s W:w1 -t L:5 +check_windows L '0:l0 5:w1' +check_fmt 'W:w1' '#{window_linked}' '1' +check_fmt 'L:5' '#{window_linked_sessions}' '2' + +# The linked window is the same window: renaming in one session shows in the +# other. +check_ok rename-window -t L:5 shared +check_windows W '0:w0 1:shared 2:w3' +check_ok rename-window -t W:1 w1 + +# Linking again to an occupied index fails without -k. +check_fail 'index in use: 0' link-window -d -s W:w3 -t L:0 + +# Unlink removes one link; the window survives in the other session. +check_ok unlink-window -t L:5 +check_windows L '0:l0' +check_windows W '0:w0 1:w1 2:w3' +check_fmt 'W:w1' '#{window_linked}' '0' + +# Unlinking a window linked to only one session needs -k. +check_fail 'window only linked to one session' unlink-window -t W:w3 +check_ok unlink-window -k -t W:w3 +check_windows W '0:w0 1:w1' + +# --------------------------------------------------------------------------- +# swap-window. + +check_ok new-window -d -t W:2 -n w2 +check_ok new-window -d -t W:3 -n w3 + +# Swap within a session: indices are exchanged. +check_ok swap-window -d -s W:0 -t W:3 +check_windows W '0:w3 1:w1 2:w2 3:w0' +check_ok swap-window -d -s W:0 -t W:3 +check_windows W '0:w0 1:w1 2:w2 3:w3' + +# Without -d the current index does not change, so the window that arrives +# there becomes current; with -d the swapped windows are selected, so the +# source window stays current at its new index. +check_ok select-window -t W:0 +check_ok swap-window -s W:0 -t W:3 +check_fmt 'W:' '#{window_index}:#{window_name}' '0:w3' +check_ok swap-window -s W:3 -t W:0 +check_fmt 'W:' '#{window_index}:#{window_name}' '0:w0' +check_ok swap-window -d -s W:0 -t W:3 +check_fmt 'W:' '#{window_index}:#{window_name}' '3:w0' +check_ok swap-window -d -s W:3 -t W:0 +check_fmt 'W:' '#{window_index}:#{window_name}' '0:w0' + +# Swap between two different sessions. +check_ok swap-window -d -s W:w2 -t L:l0 +check_windows W '0:w0 1:w1 2:l0 3:w3' +check_windows L '0:w2' +check_ok swap-window -d -s W:2 -t L:0 +check_windows W '0:w0 1:w1 2:w2 3:w3' +check_windows L '0:l0' + +# Swapping between two sessions in the same group is an error. +check_ok new-session -d -s WG -t W +check_fail "can't move window, sessions are grouped" \ + swap-window -d -s W:0 -t WG:1 +check_ok kill-session -t WG +check_windows W '0:w0 1:w1 2:w2 3:w3' + +# --------------------------------------------------------------------------- +# rotate-window. + +check_ok new-session -d -s R -x 80 -y 24 +check_ok split-window -d -t R:0 +check_ok split-window -d -t R:0 +p0=$($TMUX display-message -p -t R:0.0 '#{pane_id}') +p1=$($TMUX display-message -p -t R:0.1 '#{pane_id}') +p2=$($TMUX display-message -p -t R:0.2 '#{pane_id}') + +# check_panes $target $expected +# +# Compare the pane list of a window (as "index:id ...") with $expected. +check_panes() +{ + out=$(echo $($TMUX list-panes -t "$1" -F \ + '#{pane_index}:#{pane_id}')) + if [ "$out" != "$2" ]; then + echo "Pane list of '$1' wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_panes R:0 "0:$p0 1:$p1 2:$p2" + +# -U rotates panes up (each pane moves to the previous position); -D rotates +# down. -U then -D restores the original order. +check_ok rotate-window -U -t R:0 +check_panes R:0 "0:$p1 1:$p2 2:$p0" +check_ok rotate-window -D -t R:0 +check_panes R:0 "0:$p0 1:$p1 2:$p2" +check_ok rotate-window -D -t R:0 +check_panes R:0 "0:$p2 1:$p0 2:$p1" +check_ok rotate-window -U -t R:0 + +# The active position is preserved across rotation: the pane that arrives at +# the active position becomes the active pane. +check_ok select-pane -t R:0.0 +check_ok rotate-window -U -t R:0 +check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1" +check_ok rotate-window -D -t R:0 +check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0" + +# --------------------------------------------------------------------------- +# kill-window. + +# Killing the current window switches to the last (previously current) +# window. +check_ok select-window -t W:1 +check_ok select-window -t W:3 +check_fmt 'W:' '#{window_index}' '3' +check_ok kill-window -t W:3 +check_fmt 'W:' '#{window_index}' '1' +check_windows W '0:w0 1:w1 2:w2' + +# -f is only valid with -a. +check_fail '-f only valid with -a' kill-window -f 'x' -t W:0 + +# -a kills every window except the target. +check_ok kill-window -a -t W:w1 +check_windows W '1:w1' + +# --------------------------------------------------------------------------- +# select-window, next-window, previous-window. + +# -P prints where the new window went; an invalid (non-UTF-8) name is an +# error. +out=$($TMUX new-window -d -t W:0 -n wa -P -F '#{window_index}:#{window_name}') +if [ "$out" != "0:wa" ]; then + echo "new-window -P output wrong: '$out'" + exit 1 +fi +check_fail "invalid window name: $(printf 'a\377b')" \ + new-window -d -t W: -n "$(printf 'a\377b')" +check_ok new-window -d -t W:2 -n wc +check_windows W '0:wa 1:w1 2:wc' + +# -n and -p select the next and previous window, wrapping at the ends. +check_ok select-window -t W:0 +check_ok select-window -n -t W: +check_fmt 'W:' '#{window_index}' '1' +check_ok select-window -n -t W: +check_fmt 'W:' '#{window_index}' '2' +check_ok select-window -n -t W: +check_fmt 'W:' '#{window_index}' '0' +check_ok select-window -p -t W: +check_fmt 'W:' '#{window_index}' '2' + +# next-window and previous-window are the same code. +check_ok next-window -t W: +check_fmt 'W:' '#{window_index}' '0' +check_ok previous-window -t W: +check_fmt 'W:' '#{window_index}' '2' + +# -l selects the previously current window and select-window -T on the +# already-current window does the same. +check_ok select-window -t W:1 +check_ok select-window -l -t W: +check_fmt 'W:' '#{window_index}' '2' +check_ok select-window -T -t W:2 +check_fmt 'W:' '#{window_index}' '1' +check_ok select-window -T -t W:2 +check_fmt 'W:' '#{window_index}' '2' + +# With -a, next-window looks for a window with an alert and fails if there +# is none; a fresh session has no last window. +check_fail 'no next window' next-window -a -t W: +check_fail 'no previous window' previous-window -a -t W: +check_ok new-session -d -s F -x 80 -y 24 +check_fail 'no last window' select-window -l -t F: +check_fail 'no last window' select-window -T -t F:0 +check_ok kill-session -t F + +# --------------------------------------------------------------------------- +# more kill-window -a: no-op, filters and multiply-linked windows. + +# -a with a single window in the session does nothing. +check_ok kill-window -a -t L:0 +check_windows L '0:l0' + +# -a -f only kills other windows matching the filter. +check_ok kill-window -a -f '#{==:#{window_name},wc}' -t W:0 +check_windows W '0:wa 1:w1' + +# If the current window is linked into the session more than once, -a kills +# it too - taking the whole session with it here. +check_ok new-session -d -s D -x 80 -y 24 -n d0 +check_ok link-window -d -s D:0 -t D:5 +check_ok new-window -d -t D:1 -n dx +check_ok select-window -t D:0 +check_ok kill-window -a -t D:0 +if $TMUX has-session -t D 2>/dev/null; then + echo "Session D survived kill-window -a on multiply-linked window." + exit 1 +fi + +check_fmt 'R:0' '#{window_panes}' '3' +assert_alive + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regsub.c b/regsub.c index 62d750d8a..c49c72bb9 100644 --- a/regsub.c +++ b/regsub.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: regsub.c,v 1.10 2026/07/08 11:04:51 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -90,6 +90,15 @@ regsub(const char *pattern, const char *with, const char *text, int flags) */ regsub_copy(&buf, &len, text, last, m[0].rm_so + start); + /* For anchored patterns, replace the first match only. */ + if (*pattern == '^') { + regsub_expand(&buf, &len, with, text + start, m, + nitems(m)); + last = start + m[0].rm_eo; + regsub_copy(&buf, &len, text, last, end); + break; + } + /* * If the last match was empty and this one isn't (it is either * later or has matched text), expand this match. If it is @@ -109,12 +118,6 @@ regsub(const char *pattern, const char *with, const char *text, int flags) start += m[0].rm_eo + 1; empty = 1; } - - /* Stop now if anchored to start. */ - if (*pattern == '^') { - regsub_copy(&buf, &len, text, start, end); - break; - } } buf[len] = '\0'; diff --git a/resize.c b/resize.c index 6cbc938dd..c8257cec0 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: resize.c,v 1.56 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -46,13 +46,13 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) layout_resize(w, sx, sy); /* Resize the window, it can be no smaller than the layout. */ - if (sx < w->layout_root->sx) - sx = w->layout_root->sx; - if (sy < w->layout_root->sy) - sy = w->layout_root->sy; + if (sx < w->layout_root->g.sx) + sx = w->layout_root->g.sx; + if (sy < w->layout_root->g.sy) + sy = w->layout_root->g.sy; window_resize(w, sx, sy, xpixel, ypixel); log_debug("%s: @%u resized to %ux%u; layout %ux%u", __func__, w->id, - sx, sy, w->layout_root->sx, w->layout_root->sy); + sx, sy, w->layout_root->g.sx, w->layout_root->g.sy); /* Restore the window zoom state. */ if (zoomed) @@ -60,8 +60,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) tty_update_window_offset(w); server_redraw_window(w); - notify_window("window-layout-changed", w); - notify_window("window-resized", w); + events_fire_window("window-layout-changed", w); + events_fire_window("window-resized", w); w->flags &= ~WINDOW_RESIZE; } diff --git a/screen-redraw.c b/screen-redraw.c index 21499d426..fd67041f7 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen-redraw.c,v 1.150 2026/07/09 07:32:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -1588,8 +1588,10 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) struct screen *sl; struct redraw_scene *scene; struct window_pane *loop; - u_int width, i, y, lines; + u_int width, i, y, lines, j; struct redraw_span *first; + struct visible_ranges *r; + struct visible_range *rr; int redraw; if (c->flags & CLIENT_SUSPENDED) @@ -1667,7 +1669,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) } } tty_sync_start(tty); - tty_update_mode(tty, 0, NULL); + tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL); if (wp != NULL) redraw_draw_pane_lines(&dctx, wp, flags); @@ -1694,8 +1696,16 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) else y = c->tty.sy - lines; sl = c->status.active; - for (i = 0; i < lines; i++) - tty_draw_line(tty, sl, 0, i, UINT_MAX, 0, y + i, NULL); + for (i = 0; i < lines; i++) { + r = tty_check_overlay_range(tty, 0, y + i, tty->sx); + for (j = 0; j < r->used; j++) { + rr = &r->ranges[j]; + if (rr->nx == 0) + continue; + tty_draw_line(tty, sl, rr->px, i, rr->nx, + rr->px, y + i, NULL); + } + } } if (c->overlay_draw != NULL && (flags & REDRAW_OVERLAY)) c->overlay_draw(c, c->overlay_data); diff --git a/screen-write.c b/screen-write.c index 403e7f12c..be0ac3e53 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen-write.c,v 1.283 2026/07/09 07:35:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2096,7 +2096,7 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_collect_clear(ctx, 0, sy); - if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy)) + if (!screen_write_should_draw_lines(ctx, 0, sy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) { tty_write(tty_cmd_clearscreen, &ttyctx); @@ -2418,6 +2418,10 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only, if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP))) goto discard; if (s->mode & MODE_SYNC) { + if (ctx->scrolled != 0) { + screen_write_should_draw_lines(ctx, s->rupper, + s->rlower + 1 - s->rupper); + } for (y = 0; y < screen_size_y(s); y++) { cl = &s->write_list[y]; if (!TAILQ_EMPTY(&cl->items)) @@ -3127,7 +3131,13 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc, if (wp != NULL) { 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)) { + window_pane_send_resize(wp, wp->sx, wp->sy); + window_pane_clear_resizes(wp, NULL); + } server_redraw_window_borders(wp->window); } diff --git a/screen.c b/screen.c index f3fdfcbd4..6b539b382 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen.c,v 1.105 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -99,12 +99,12 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit) s->write_list = NULL; s->hyperlinks = NULL; - screen_reinit(s); + screen_reinit(s, 1); } /* Reinitialise screen. */ void -screen_reinit(struct screen *s) +screen_reinit(struct screen *s, int check) { s->cx = 0; s->cy = 0; @@ -123,7 +123,8 @@ screen_reinit(struct screen *s) s->saved_cy = UINT_MAX; screen_reset_tabs(s); - grid_check_is_clear(s->grid); + if (check) + grid_check_is_clear(s->grid); grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8); screen_clear_selection(s); @@ -151,6 +152,10 @@ screen_reset_hyperlinks(struct screen *s) void screen_free(struct screen *s) { +#ifdef ENABLE_SIXEL + struct image *im; +#endif + free(s->sel); free(s->tabs); free(s->path); @@ -168,6 +173,14 @@ screen_free(struct screen *s) screen_free_titles(s); #ifdef ENABLE_SIXEL + /* + * Images saved when entering the alternate screen stay linked in the + * global list; move them back so they are freed and unlinked here, or + * a later eviction would write through this freed screen. + */ + TAILQ_CONCAT(&s->images, &s->saved_images, entry); + TAILQ_FOREACH(im, &s->images, entry) + im->list = &s->images; image_free_all(s); #endif } diff --git a/server-acl.c b/server-acl.c index db95a3c6b..03e758af5 100644 --- a/server-acl.c +++ b/server-acl.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-acl.c,v 1.3 2026/06/08 21:38:19 nicm Exp $ */ /* * Copyright (c) 2021 Holland Schutte, Jayson Morberg diff --git a/server-client.c b/server-client.c index e6ece92f7..2865133a7 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-client.c,v 1.486 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -429,7 +429,7 @@ server_client_set_session(struct client *c, struct session *s) alerts_check_session(s); tty_update_client_offset(c); status_timer_start(c); - notify_client("client-session-changed", c); + events_fire_client("client-session-changed", c); server_redraw_client(c); } @@ -464,7 +464,7 @@ server_client_lost(struct client *c) if (c->flags & CLIENT_ATTACHED) { server_client_attached_lost(c); - notify_client("client-detached", c); + events_fire_client("client-detached", c); } if (c->flags & CLIENT_CONTROL) @@ -1193,6 +1193,8 @@ server_client_update_theme_colours(struct client *c) format_defaults(ft, c, NULL, NULL, NULL); theme = c->theme; + if (theme == THEME_UNKNOWN) + theme = colour_totheme(c->tty.bg); if (option == 2) theme = THEME_LIGHT; else if (option == 3) @@ -1282,7 +1284,7 @@ server_client_update_latest(struct client *c) if (options_get_number(w->options, "window-size") == WINDOW_SIZE_LATEST) recalculate_size(w, 0); - notify_client("client-active", c); + events_fire_client("client-active", c); } /* Get repeat time. */ @@ -2466,7 +2468,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) c->overlay_resize(c, c->overlay_data); server_redraw_client(c); if (c->session != NULL) - notify_client("client-resized", c); + events_fire_client("client-resized", c); break; case MSG_EXITING: if (datalen != 0) @@ -3061,10 +3063,10 @@ server_client_report_theme(struct client *c, enum client_theme theme) if (theme == THEME_LIGHT) { c->theme = THEME_LIGHT; - notify_client("client-light-theme", c); + events_fire_client("client-light-theme", c); } else { c->theme = THEME_DARK; - notify_client("client-dark-theme", c); + events_fire_client("client-dark-theme", c); } /* diff --git a/server-fn.c b/server-fn.c index 16cda7565..9bf48b162 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-fn.c,v 1.147 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -29,6 +29,31 @@ #include "tmux.h" static void server_destroy_session_group(struct session *); +static void server_fire_pane_exit(const char *, struct window_pane *); + +static void +server_fire_pane_exit(const char *name, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + int status = wp->status; + const char *signame; + + if (WIFSIGNALED(status)) + signame = sig2name(WTERMSIG(status)); + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + if (WIFEXITED(status)) + event_payload_set_int(ep, "exit_status", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + event_payload_set_string(ep, "exit_signal", "%s", signame); + event_payload_set_int(ep, "exit_success", status == 0); + events_fire(name, ep); +} void server_redraw_client(struct client *c) @@ -202,6 +227,7 @@ server_kill_window(struct window *w, int renumber) struct session *s, *s1; struct winlink *wl; + window_add_ref(w, __func__); RB_FOREACH_SAFE(s, sessions, &sessions, s1) { if (!session_has(s, w)) continue; @@ -219,6 +245,7 @@ server_kill_window(struct window *w, int renumber) server_renumber_session(s); } recalculate_sizes(); + window_remove_ref(w, __func__); } void @@ -272,8 +299,7 @@ server_link_window(struct session *src, struct winlink *srcwl, * Can't use session_detach as it will destroy session * if this makes it empty. */ - notify_session_window("window-unlinked", dst, - dstwl->window); + events_fire_winlink("window-unlinked", dstwl); dstwl->flags &= ~WINLINK_ALERTFLAGS; winlink_stack_remove(&dst->lastw, dstwl); winlink_remove(&dst->windows, dstwl); @@ -351,7 +377,7 @@ server_destroy_pane(struct window_pane *wp, int notify) gettimeofday(&wp->dead_time, NULL); if (notify) - notify_pane("pane-died", wp); + server_fire_pane_exit("pane-died", wp); s = options_get_string(wp->options, "remain-on-exit-format"); if (*s != '\0') { @@ -374,7 +400,7 @@ server_destroy_pane(struct window_pane *wp, int notify) } if (notify) - notify_pane("pane-exited", wp); + server_fire_pane_exit("pane-exited", wp); server_unzoom_window(w); server_client_remove_pane(wp); @@ -438,6 +464,7 @@ server_destroy_session(struct session *s) { struct client *c; struct session *s_new = NULL, *cs_new = NULL, *use_s; + struct sort_criteria sort_crit = { .order = SORT_NAME }; int detach_on_destroy; detach_on_destroy = options_get_number(s->options, "detach-on-destroy"); @@ -446,9 +473,11 @@ server_destroy_session(struct session *s) else if (detach_on_destroy == 2) s_new = server_find_session(s, server_newer_detached_session); else if (detach_on_destroy == 3) - s_new = session_previous_session(s, NULL); + s_new = session_previous_session(s, &sort_crit); else if (detach_on_destroy == 4) - s_new = session_next_session(s, NULL); + s_new = session_next_session(s, &sort_crit); + if (s_new == s) + s_new = NULL; /* * If no suitable new session was found above, then look for any diff --git a/server.c b/server.c index 7780ff519..4142272ae 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -214,6 +214,8 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, TAILQ_INIT(&clients); RB_INIT(&sessions); key_bindings_init(); + control_build_events(); + hooks_build_events(); TAILQ_INIT(&message_log); gettimeofday(&start_time, NULL); diff --git a/session.c b/session.c index 445897e2a..da30f586b 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: session.c,v 1.105 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -206,7 +206,7 @@ session_destroy(struct session *s, int notify, const char *from) RB_REMOVE(sessions, &sessions, s); if (notify) - notify_session("session-closed", s); + events_fire_session("session-closed", s); free(s->tio); @@ -219,7 +219,7 @@ session_destroy(struct session *s, int notify, const char *from) winlink_stack_remove(&s->lastw, TAILQ_FIRST(&s->lastw)); while (!RB_EMPTY(&s->windows)) { wl = RB_ROOT(&s->windows); - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_remove(&s->windows, wl); } @@ -330,7 +330,7 @@ session_attach(struct session *s, struct window *w, int idx, char **cause) } wl->session = s; winlink_set_window(wl, w); - notify_session_window("window-linked", s, w); + events_fire_winlink("window-linked", wl); session_group_synchronize_from(s); return (wl); @@ -346,7 +346,7 @@ session_detach(struct session *s, struct winlink *wl) session_next(s, 0); wl->flags &= ~WINLINK_ALERTFLAGS; - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_stack_remove(&s->lastw, wl); winlink_remove(&s->windows, wl); @@ -493,7 +493,7 @@ session_set_current(struct session *s, struct winlink *wl) winlink_clear_flags(wl); window_update_activity(wl->window); tty_update_window_offset(wl->window); - notify_session("session-window-changed", s); + events_fire_session("session-window-changed", s); return (0); } @@ -657,7 +657,7 @@ session_group_synchronize1(struct session *target, struct session *s) wl2 = winlink_add(&s->windows, wl->idx); wl2->session = s; winlink_set_window(wl2, wl->window); - notify_session_window("window-linked", s, wl2->window); + events_fire_winlink("window-linked", wl2); wl2->flags |= wl->flags & WINLINK_ALERTFLAGS; } @@ -683,7 +683,7 @@ session_group_synchronize1(struct session *target, struct session *s) wl = RB_ROOT(&old_windows); wl2 = winlink_find_by_window_id(&s->windows, wl->window->id); if (wl2 == NULL) - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_remove(&old_windows, wl); } } diff --git a/sort.c b/sort.c index c416b8425..31f6e82f1 100644 --- a/sort.c +++ b/sort.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ /* * Copyright (c) 2026 Dane Jensen diff --git a/spawn.c b/spawn.c index 91010d775..37798f44f 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: spawn.c,v 1.47 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -71,6 +71,45 @@ spawn_log(const char *from, struct spawn_context *sc) log_debug("%s: s=$%u %s idx=%d", from, s->id, tmp, sc->idx); } +static void +spawn_fire_pane_created(struct spawn_context *sc, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + char *cmd = NULL; + const char *cwd = wp->cwd; + + ep = event_payload_create(); + cmd_find_from_winlink_pane(&fs, sc->wl, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", sc->s); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_int(ep, "window_index", sc->wl->idx); + event_payload_set_pane(ep, "pane", wp); + + if (wp->argc != 0) + cmd = cmd_stringify_argv(wp->argc, wp->argv); + if (cmd != NULL && *cmd != '\0') + event_payload_set_string(ep, "pane_command", "%s", cmd); + else if (wp->shell != NULL) + event_payload_set_string(ep, "pane_command", "%s", wp->shell); + free(cmd); + + if (cwd != NULL) + event_payload_set_string(ep, "pane_current_path", "%s", cwd); + + if (sc->flags & SPAWN_EMPTY) + event_payload_set_int(ep, "created_empty", 1); + else + event_payload_set_int(ep, "created_empty", 0); + if (sc->flags & SPAWN_RESPAWN) + event_payload_set_int(ep, "created_respawn", 1); + else + event_payload_set_int(ep, "created_respawn", 0); + + events_fire("pane-created", ep); +} + struct winlink * spawn_window(struct spawn_context *sc, char **cause) { @@ -131,7 +170,7 @@ spawn_window(struct spawn_context *sc, char **cause) * if this makes it empty. */ wl->flags &= ~WINLINK_ALERTFLAGS; - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_stack_remove(&s->lastw, wl); winlink_remove(&s->windows, wl); @@ -177,7 +216,7 @@ spawn_window(struct spawn_context *sc, char **cause) /* Set the name of the new window. */ if (~sc->flags & SPAWN_RESPAWN) { free(w->name); - if (sc->name == NULL || *sc->name == '\0') + if (sc->name == NULL) w->name = default_window_name(w); else { w->name = xstrdup(sc->name); @@ -191,7 +230,7 @@ spawn_window(struct spawn_context *sc, char **cause) /* Fire notification if new window. */ if (~sc->flags & SPAWN_RESPAWN) - notify_session_window("window-linked", s, w); + events_fire_winlink("window-linked", sc->wl); session_group_synchronize_from(s); return (sc->wl); @@ -264,14 +303,20 @@ spawn_pane(struct spawn_context *sc, char **cause) free(cwd); return (NULL); } - if (sc->wp0->fd != -1) { + if (sc->wp0->event != NULL) { bufferevent_free(sc->wp0->event); + sc->wp0->event = NULL; + } + if (sc->wp0->fd != -1) { close(sc->wp0->fd); + sc->wp0->fd = -1; } window_pane_reset_mode_all(sc->wp0); - screen_reinit(&sc->wp0->base); - input_free(sc->wp0->ictx); - sc->wp0->ictx = NULL; + screen_reinit(&sc->wp0->base, 0); + if (sc->wp0->ictx != NULL) { + input_free(sc->wp0->ictx); + sc->wp0->ictx = NULL; + } new_wp = sc->wp0; new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN); } else { @@ -387,6 +432,7 @@ spawn_pane(struct spawn_context *sc, char **cause) new_wp->base.mode |= MODE_CRLF; goto complete; } + new_wp->flags &= ~PANE_EMPTY; /* Store current working directory and change to new one. */ if (getcwd(path, sizeof path) != NULL) { @@ -512,8 +558,8 @@ complete: sigprocmask(SIG_SETMASK, &oldset, NULL); window_pane_set_event(new_wp); - environ_free(child); + spawn_fire_pane_created(sc, new_wp); if (sc->flags & SPAWN_RESPAWN) return (new_wp); @@ -524,7 +570,7 @@ complete: window_set_active_pane(w, new_wp, 1); } if (~sc->flags & SPAWN_NONOTIFY) - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (new_wp); } @@ -625,13 +671,13 @@ spawn_editor(struct client *c, const char *buf, size_t len, struct window *w = wl->window; struct window_pane *wp; struct layout_cell *lc; + struct layout_geometry lg; struct environ *env; FILE *f; char *cmd, *cause = NULL; char path[] = _PATH_TMP "tmux.XXXXXXXX"; const char *editor; int fd; - u_int px, py, sx, sy; editor = options_get_string(global_options, "editor"); fd = mkstemp(path); @@ -655,12 +701,12 @@ spawn_editor(struct client *c, const char *buf, size_t len, es->cb = cb; es->arg = arg; - sx = w->sx * 9 / 10; - sy = w->sy * 9 / 10; - px = w->sx / 2 - sx / 2; - py = w->sy / 2 - sy / 2; + lg.sx = w->sx * 9 / 10; + lg.sy = w->sy * 9 / 10; + lg.xoff = w->sx / 2 - lg.sx / 2; + lg.yoff = w->sy / 2 - lg.sy / 2; window_push_zoom(w, 1, 0); - lc = layout_floating_pane(w, NULL, sx, sy, px, py); + lc = layout_floating_pane(w, NULL, &lg); if (lc == NULL) { spawn_editor_free(es); return (NULL); diff --git a/status.c b/status.c index bb0c3bef9..2f3c8f11e 100644 --- a/status.c +++ b/status.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: status.c,v 1.273 2026/07/06 14:29:10 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -274,7 +274,7 @@ status_redraw(struct client *c) for (i = 0; i < lines; i++) { screen_write_cursormove(&ctx, 0, i, 0); - ov = options_array_get(o, i); + ov = options_array_getv(o, "%u", i); if (ov == NULL) { for (n = 0; n < width; n++) screen_write_putc(&ctx, &gc, ' '); @@ -488,7 +488,7 @@ status_message_redraw(struct client *c) status_message_area(c, &ax, &aw); ft = format_create_defaults(NULL, c, NULL, NULL, NULL); - memcpy(&gc, &grid_default_cell, sizeof gc); + style_apply(&gc, s->options, "message-style", ft); /* * Set #{message} in the format tree. If styles should be ignored in diff --git a/style.c b/style.c index 2ca5e37af..55d08fe75 100644 --- a/style.c +++ b/style.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: style.c,v 1.47 2026/06/29 17:08:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/tmux-protocol.h b/tmux-protocol.h index 3cf00c094..d823ce7e2 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ /* * Copyright (c) 2021 Nicholas Marriott diff --git a/tmux.1 b/tmux.1 index cd6e7a9d5..9f921c91a 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD$ +.\" $OpenBSD: tmux.1,v 1.1132 2026/07/10 15:20:06 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$ +.Dd $Mdocdate: July 10 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -365,13 +365,13 @@ Toggle zoom state of the current pane. .It Tab Choose a new window and session by fuzzy matching. .It { -Move floating pane to top-left corner. +Swap the current pane with the previous pane. .It } -Move floating pane to top-right corner. -.It M-{ -Move floating pane to bottom-left corner. -.It M-} -Move floating pane to bottom-right corner. +Swap the current pane with the next pane. +.It g 1, 2, 3, 4 +.It g Up, Down, Left, Right +Move floating pane to corners or edges. +Meta resizes the pane as well. .It \[ti] Show previous messages from .Nm , @@ -2342,7 +2342,8 @@ but also exit copy mode if the cursor reaches the bottom. .It Xo .Ic scroll\-exit\-on .Xc -Turn on exiting copy mode when scrolling to the end of the buffer. +Turn on exiting copy mode when scrolling to the end of the buffer, unless a +selection is present. .It Xo .Ic scroll\-exit\-off .Xc @@ -2587,7 +2588,7 @@ The synopsis for the command is: .Bl -tag -width Ds .It Xo Ic copy\-mode -.Op Fl deHMqSu +.Op Fl dekHMqSu .Op Fl s Ar src\-pane .Op Fl t Ar target\-pane .Xc @@ -2619,6 +2620,7 @@ instead of .Fl e specifies that scrolling to the bottom of the history (to the visible screen) should exit copy mode. +This will not happen if a selection is present. While in copy mode, pressing a key other than those used for scrolling will disable this behaviour. This is intended to allow fast scrolling through a pane's history, for @@ -2627,6 +2629,9 @@ example with: bind PageUp copy\-mode \-eu bind PageDown copy\-mode \-ed .Ed +.Pp +.Fl k +kills the pane when the mode is exited. .El .Pp A number of preset arrangements of panes are available, these are called @@ -3249,6 +3254,15 @@ is omitted and a marked pane is present (see .Ic select\-pane .Fl m ) , the marked pane is used rather than the current pane. +.Pp +If +.Ar src\-pane +is floating and +.Ar dst\-pane +is either unspecified or equal to +.Ar src\-pane , +.Ar src\-pane +is returned to its previous position in the layout. .Tg killp .It Xo Ic kill\-pane .Op Fl a @@ -3516,7 +3530,7 @@ the option. .Tg neww .It Xo Ic new\-window -.Op Fl abdkPS +.Op Fl abdEkPS .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl F Ar format @@ -3540,6 +3554,9 @@ is the new window location. If .Fl d is given, the session does not make the new window the current window. +If +.Fl E +is given, the initial pane is created without a running command. .Ar target\-window represents the window to be created; if the target already exists an error is shown, unless the @@ -3839,7 +3856,7 @@ This command will automatically set to manual in the window options. .Tg respawnp .It Xo Ic respawn\-pane -.Op Fl k +.Op Fl \&Ek .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl t Ar target\-pane @@ -3856,6 +3873,9 @@ executed. The pane must be already inactive, unless .Fl k is given, in which case any existing command is killed. +If +.Fl E +is given, the pane is left without a running command. .Fl c specifies a new working directory for the pane. The @@ -3865,7 +3885,7 @@ option has the same meaning as for the command. .Tg respawnw .It Xo Ic respawn\-window -.Op Fl k +.Op Fl \&Ek .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl t Ar target\-window @@ -3882,6 +3902,9 @@ executed. The window must be already inactive, unless .Fl k is given, in which case any existing command is killed. +If +.Fl E +is given, the window is left with one pane and without a running command. .Fl c specifies a new working directory for the window. The @@ -4560,6 +4583,10 @@ If .Fl g is given, the global session or window option is set. .Pp +Array options may be set by giving a key in square brackets after the option +name, for example +.Ql command-alias[zoom] . +.Pp .Fl F expands formats in the option value. The @@ -4671,7 +4698,7 @@ it is replaced with .Ar value . For example, after: .Pp -.Dl set \-s command\-alias[100] zoom=\[aq]resize\-pane \-Z\[aq] +.Dl set \-s command\-alias[zoom] zoom=\[aq]resize\-pane \-Z\[aq] .Pp Using: .Pp @@ -6272,11 +6299,11 @@ or .Fl H . The following two commands are equivalent: .Bd -literal -offset indent. -set\-hook \-g pane\-mode\-changed[42] \[aq]set \-g status\-left\-style bg=red\[aq] -set\-option \-g pane\-mode\-changed[42] \[aq]set \-g status\-left\-style bg=red\[aq] +set\-hook \-g pane\-mode\-changed[style] \[aq]display\-message changed\[aq] +set\-option \-g pane\-mode\-changed[style] \[aq]display\-message changed\[aq] .Ed .Pp -Setting a hook without specifying an array index clears the hook and sets the +Setting a hook without specifying an array key clears the hook and sets the first member of the array. .Pp A command's after @@ -6300,11 +6327,7 @@ For example, this could be used to write to a log file: set\-hook \-g command\-error "run\-shell \\"echo 'a tmux command failed' >>/tmp/log\\"" .Ed .Pp -All the notifications listed in the -.Sx CONTROL MODE -section are hooks (without any arguments), except -.Ic %exit . -The following additional hooks are available: +The following hooks are available in addition to after hooks: .Bl -tag -width "XXXXXXXXXXXXXXXXXXXXXX" .It alert\-activity Run when a window has activity. @@ -6323,11 +6346,11 @@ Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. .It client\-detached -Run when a client is detached +Run when a client is detached. .It client\-focus\-in -Run when focus enters a client +Run when focus enters a client. .It client\-focus\-out -Run when focus exits a client +Run when focus exits a client. .It client\-resized Run when a client is resized. .It client\-session\-changed @@ -6338,6 +6361,21 @@ Run when a client switches to a light theme. Run when a client switches to a dark theme. .It command\-error Run when a command fails. +.It marked\-pane\-changed +Run when the marked pane is set or cleared. +.It pane\-command\-finished +Run when an OSC 133 command finishes in a pane. +The event payload includes +.Ql hook_command_start_time , +.Ql hook_command_end_time , +.Ql hook_command_duration +and +.Ql hook_command_status +if present. +.It pane\-command\-started +Run when an OSC 133 command starts in a pane. +.It pane\-created +Run when a pane is created or respawned. .It pane\-died Run when the program running in a pane exits, but .Ic remain\-on\-exit @@ -6352,20 +6390,40 @@ option is on. Run when the focus exits a pane, if the .Ic focus\-events option is on. +.It pane\-mode\-changed +Run when a pane changes mode. +.It pane\-mode\-entered +Run when a pane enters a mode. +.It pane\-mode\-exited +Run when a pane exits a mode. +.It pane\-prompt\-closed +Run when a prompt in a pane is closed. +.It pane\-prompt\-opened +Run when a prompt is opened in a pane. +.It pane\-resized +Run when a pane is resized. .It pane\-set\-clipboard Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. -.It session\-created -Run when a new session created. +.It pane\-shell\-prompt +Run when an OSC 133 shell prompt starts in a pane. +.It pane\-title\-changed +Run when a pane title is changed. .It session\-closed -Run when a session closed. +Run when a session is closed. +.It session\-created +Run when a new session is created. .It session\-renamed Run when a session is renamed. +.It session\-window\-changed +Run when a session changes its active window. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked Run when a window is linked into a session. +.It window\-pane\-changed +Run when a window changes its active pane. .It window\-renamed Run when a window is renamed. .It window\-resized @@ -6373,6 +6431,10 @@ Run when a window is resized. This may be after the .Ar client\-resized hook is run. +.It window\-unzoomed +Run when a window is unzoomed. +.It window\-zoomed +Run when a window is zoomed. .It window\-unlinked Run when a window is unlinked from a session. .El @@ -6380,12 +6442,15 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpRuw +.Op Fl agpERTuw +.Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command .Xc Without +.Fl E +or .Fl R , sets (or with .Fl u @@ -6397,18 +6462,66 @@ The flags are the same as for .Ic set\-option . .Pp With +.Fl B , +.Ar name:what:format +uses the same subscription syntax as +.Ic refresh\-client +.Fl B : +.Ar name +is the hook to run, +.Ar what +selects the session, pane, all panes, window, or all windows, and +.Ar format +is expanded once a second. +With +.Fl T , +the hook is run only when +.Ar format +is true. +For monitor hooks, +.Ar name +must begin with +.Ql @ . +If +.Ar command +is given, it is stored as the +.Ql @ +hook command; otherwise only the monitor is created or replaced. +Note that monitor hooks are not inherited, the hook is only run from +the scope where it is created. +With +.Fl u , +the subscription named by +.Fl B +is removed. +.Pp +With +.Fl E , +fire the user event named +.Ar hook\-name . +.Ar hook\-name +must begin with +.Ql @ . +.Pp +With .Fl R , run .Ar hook\-name immediately. .It Xo Ic show\-hooks -.Op Fl gpw +.Op Fl Bgpw .Op Fl t Ar target\-pane .Op Ar hook .Xc Shows hooks. The flags are the same as for .Ic show\-options . +.Pp +With +.Fl B , +shows the subscriptions installed with +.Em set\-hook +.Fl B . .El .Sh MOUSE SUPPORT If the @@ -6756,10 +6869,14 @@ Adding will use shorter but less accurate time format for times in the past. .Ql r .Pq Ql t/r -will show the time relative to the current time, for example -.Ql \1m +will give the time relative to the current time, for example +.Ql \&1m or .Ql 2m23s . +.Ql d +.Pq Ql t/d +will give the difference from the time to current time in seconds (future +times are negative). A custom format may be given using an .Ql f suffix (note that @@ -6815,7 +6932,9 @@ or with .Ql a escape .Nm -command arguments. +command arguments; with +.Ql s +use single quotes. .Ql E:\& will expand the format twice, for example .Ql #{E:status\-left} @@ -6849,7 +6968,13 @@ to sort in reverse order. .Ql /r\& can also be used with .Ql P:\& -to reverse the sort order by pane index. +to reverse the sort order; by default panes are sorted by creation order. +.Ql P:\& +can also take +.Ql /i\& +to sort by pane index or +.Ql /z\& +to sort by z-index. For example, .Ql S/nr:\& to sort sessions by name in reverse order. @@ -6875,6 +7000,33 @@ prefix, for example a user option on the next window is available as .Ql next_@color . .Pp +.Ql O:\& +will loop over each option; +array options are looped once for each array item. +.Ql O:\& +may be given a flag to choose the options table: +.Bl -column "Flag" "Table" -offset indent +.It Sy "Flag" Ta Sy "Table" +.It Li "s" Ta "session" +.It Li "w" Ta "window" +.It Li "p" Ta "pane" +.It Li "v" Ta "server" +.El +.Pp +.Ql g +chooses global options. +The default is +.Ql s . +.Ql V:\& +will loop over each environment variable. +Its flags are: +.Bl -column "Flag" "Environment" -offset indent +.It Sy "Flag" Ta Sy "Environment" +.It Li "s" Ta "session environment" +.It Li "g" Ta "global environment" +.It Li "c" Ta "client environment" +.El +.Pp .Ql N:\& checks if a window (without any suffix or with the .Ql w @@ -7011,10 +7163,13 @@ The following variables are available, where appropriate: .It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any" +.It Li "hook_last" Ta "" Ta "Previous value for a monitor hook" .It Li "hook_pane" Ta "" Ta "ID of pane where hook was run, if any" .It Li "hook_session" Ta "" Ta "ID of session where hook was run, if any" .It Li "hook_session_name" Ta "" Ta "Name of session where hook was run, if any" +.It Li "hook_value" Ta "" Ta "New value for a monitor hook" .It Li "hook_window" Ta "" Ta "ID of window where hook was run, if any" +.It Li "hook_window_index" Ta "" Ta "Index of window where hook was run, if any" .It Li "hook_window_name" Ta "" Ta "Name of window where hook was run, if any" .It Li "host" Ta "#H" Ta "Hostname of local host" .It Li "host_short" Ta "#h" Ta "Hostname of local host (no domain name)" @@ -7058,6 +7213,11 @@ The following variables are available, where appropriate: .It Li "pane_at_top" Ta "" Ta "1 if pane is at the top of window" .It Li "pane_bg" Ta "" Ta "Pane background colour" .It Li "pane_bottom" Ta "" Ta "Bottom of pane" +.It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" +.It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" +.It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" +.It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" +.It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" .It Li "pane_current_command" Ta "" Ta "Current command if available" .It Li "pane_current_path" Ta "" Ta "Current path if available" .It Li "pane_dead" Ta "" Ta "1 if pane is dead" @@ -7075,6 +7235,8 @@ The following variables are available, where appropriate: .It Li "pane_input_off" Ta "" Ta "1 if input to pane is disabled" .It Li "pane_key_mode" Ta "" Ta "Extended key reporting mode in this pane" .It Li "pane_last" Ta "" Ta "1 if last pane" +.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output" +.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began" .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" @@ -7088,6 +7250,7 @@ The following variables are available, where appropriate: .It Li "pane_right" Ta "" Ta "Right of pane" .It Li "pane_search_string" Ta "" Ta "Last search string in copy mode" .It Li "pane_start_command" Ta "" Ta "Command pane started with" +.It Li "pane_start_command_list" Ta "" Ta "Command pane started with, quoted" .It Li "pane_start_path" Ta "" Ta "Path pane started with" .It Li "pane_synchronized" Ta "" Ta "1 if pane is synchronized" .It Li "pane_tabs" Ta "" Ta "Pane tab positions" @@ -7173,6 +7336,8 @@ The following variables are available, where appropriate: .It Li "window_linked" Ta "" Ta "1 if window is linked across sessions" .It Li "window_linked_sessions" Ta "" Ta "Number of sessions this window is linked to" .It Li "window_linked_sessions_list" Ta "" Ta "List of sessions this window is linked to" +.It Li "window_manual_height" Ta "" Ta "Manual height of window, if set" +.It Li "window_manual_width" Ta "" Ta "Manual width of window, if set" .It Li "window_marked_flag" Ta "" Ta "1 if window contains the marked pane" .It Li "window_name" Ta "#W" Ta "Name of window" .It Li "window_offset_x" Ta "" Ta "X offset into window if larger than client" @@ -7724,10 +7889,7 @@ This affects what completions are offered when is pressed. Available types are: .Ql command , -.Ql search , -.Ql target -and -.Ql window\-target . +.Ql search . .Pp The following keys have a special meaning in the command prompt, depending on the value of the @@ -7862,6 +8024,7 @@ Both may be a row or column number, or one of the following special values: .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" .El @@ -7872,6 +8035,8 @@ Or a format, which is expanded including the following additional variables: .It Li "popup_centre_x" Ta "Centered in the client" .It Li "popup_centre_y" Ta "Centered in the client" .It Li "popup_height" Ta "Height of menu or popup" +.It Li "popup_last_x" Ta "Left of the last menu" +.It Li "popup_last_y" Ta "Bottom of the last menu" .It Li "popup_mouse_bottom" Ta "Bottom of at the mouse" .It Li "popup_mouse_centre_x" Ta "Horizontal centre at the mouse" .It Li "popup_mouse_centre_y" Ta "Vertical centre at the mouse" @@ -8456,8 +8621,10 @@ or the current pane if omitted) after the command finishes. If the command fails, the exit status is also displayed. .Tg wait .It Xo Ic wait\-for -.Op Fl L | S | U -.Ar channel +.Op Fl ELSUlv +.Op Fl F Ar format +.Op Fl w Ar waiter +.Ar name .Xc .D1 Pq alias: Ic wait When used without options, prevents the client from exiting until woken using @@ -8470,6 +8637,41 @@ is used, the channel is locked and any clients that try to lock the same channel are made to wait until the channel is unlocked with .Ic wait\-for .Fl U . +.Pp +With +.Fl E , +.Nm +waits for the next event with +.Ar name . +Events include hook and notification names, and user +.Ql @ +events generated by +.Ic set-hook +.Fl E +or +.Ic set-hook +.Fl B . +If +.Fl F +is given, +.Ar format +must also be true. +If +.Fl v +is given, event payload keys are printed (whether or not +.Ar format +is true). +.Pp +.Fl l +list the waiters for +.Ar name +and +.Fl w +wakes +.Ar waiter +on +.Ar name +immediately. .El .Sh EXIT MESSAGES When a diff --git a/tmux.c b/tmux.c index 132c39217..2942d3788 100644 --- a/tmux.c +++ b/tmux.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.c,v 1.221 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,24 @@ static char *make_label(const char *, char **); static int areshell(const char *); static const char *getshell(void); +#ifdef ASAN +__attribute__((used)) const char *__asan_default_options(void); +__attribute__((used)) const char * +__asan_default_options(void) +{ + return ( + "abort_on_error=1:" + "halt_on_error=1:" + "detect_leaks=0:" + "detect_stack_use_after_return=1:" + "strict_string_checks=1:" + "check_initialization_order=1:" + "log_path=/tmp/tmux-asan:" + "log_exe_name=1" + ); +} +#endif + static __dead void usage(int status) { diff --git a/tmux.h b/tmux.h index c0f2a1c6c..3dd80b724 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.h,v 1.1396 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -52,6 +52,9 @@ struct cmdq_state; struct cmds; struct control_state; struct environ; +struct event_payload; +struct event_payload_item; +struct events_sink; struct format_job_tree; struct format_tree; struct hyperlinks_uri; @@ -102,6 +105,9 @@ struct winlink; #ifndef TMUX_LOCK_CMD #define TMUX_LOCK_CMD "lock -np" #endif +#ifndef TMUX_MOUSE +#define TMUX_MOUSE 0 +#endif /* Minimum and maximum layout cell size, NOT including border lines. */ #define PANE_MINIMUM 1 @@ -1265,6 +1271,7 @@ struct visible_ranges { /* Child window structure. */ struct window_pane { u_int id; + int references; u_int active_point; struct window *window; @@ -1296,6 +1303,8 @@ struct window_pane { #define PANE_THEMECHANGED 0x2000 #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 +#define PANE_DESTROYED 0x10000 +#define PANE_CMDRUNNING 0x20000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1318,6 +1327,12 @@ struct window_pane { struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct spawn_editor_state *editor; + time_t last_output_time; + time_t last_prompt_time; + time_t cmd_start_time; + time_t cmd_end_time; + int cmd_status; + int fd; struct bufferevent *event; @@ -1503,32 +1518,31 @@ enum layout_type { LAYOUT_WINDOWPANE }; +/* Layout cell sizes and position. */ +struct layout_geometry { + u_int sx; + u_int sy; + int xoff; + int yoff; +}; + /* Layout cells queue. */ TAILQ_HEAD(layout_cells, layout_cell); /* Layout cell. */ struct layout_cell { - enum layout_type type; + enum layout_type type; + int flags; #define LAYOUT_CELL_FLOATING 0x1 - int flags; - struct layout_cell *parent; + struct layout_cell *parent; - u_int sx; - u_int sy; + struct layout_geometry g; + struct layout_geometry fg; /* saved floating pane */ - int xoff; - int yoff; - - u_int saved_sx; - u_int saved_sy; - - int saved_xoff; - int saved_yoff; - - struct window_pane *wp; - struct layout_cells cells; + struct window_pane *wp; + struct layout_cells cells; TAILQ_ENTRY(layout_cell) entry; }; @@ -2281,6 +2295,9 @@ struct client { void *overlay_data; struct event overlay_timer; + u_int menu_last_px; + u_int menu_last_py; + struct client_files files; u_int source_file_depth; @@ -2291,14 +2308,44 @@ struct client { }; TAILQ_HEAD(clients, client); -/* Control mode subscription type. */ -enum control_sub_type { - CONTROL_SUB_SESSION, - CONTROL_SUB_PANE, - CONTROL_SUB_ALL_PANES, - CONTROL_SUB_WINDOW, - CONTROL_SUB_ALL_WINDOWS +/* Monitor. */ +enum monitor_type { + MONITOR_SESSION, + MONITOR_PANE, + MONITOR_ALL_PANES, + MONITOR_WINDOW, + MONITOR_ALL_WINDOWS }; +#define MONITOR_NOTIFY_INITIAL 0x1 +#define MONITOR_NOTIFY_TRUE 0x2 +struct monitor_change { + const char *name; + const char *value; + const char *last; + + struct client *c; + struct session *s; + struct winlink *wl; + struct window_pane *wp; +}; +typedef void (*monitor_cb)(struct monitor_change *, void *); + +/* Event payload type. */ +enum event_payload_type { + EVENT_PAYLOAD_STRING, + EVENT_PAYLOAD_TIME, + EVENT_PAYLOAD_INT, + EVENT_PAYLOAD_UINT, + EVENT_PAYLOAD_CLIENT, + EVENT_PAYLOAD_SESSION, + EVENT_PAYLOAD_WINDOW, + EVENT_PAYLOAD_PANE, + EVENT_PAYLOAD_POINTER +}; + +/* Event payload callbacks. */ +typedef void (*event_payload_free_cb)(void *); +typedef void (*event_payload_print_cb)(void *, struct evbuffer *); /* Key binding and key table. */ struct key_binding { @@ -2609,23 +2656,78 @@ char *format_grid_hyperlink(struct grid *, u_int, u_int, struct screen *); char *format_grid_line(struct grid *, u_int); -/* format-draw.c */ -void format_draw(struct screen_write_ctx *, - const struct grid_cell *, u_int, const char *, - struct style_ranges *, int); -u_int format_width(const char *); -char *format_trim_left(const char *, u_int); -char *format_trim_right(const char *, u_int); +/* events-payload.c */ +struct event_payload *event_payload_create(void); +void event_payload_free(struct event_payload *); +void printflike(2, 3) event_payload_log(struct event_payload *, const char *, + ...); +char *event_payload_item_print(struct event_payload_item *); +void event_payload_set_target(struct event_payload *, + struct cmd_find_state *); +int event_payload_get_target(struct event_payload *, + struct cmd_find_state *); +void printflike(3, 4) event_payload_set_string(struct event_payload *, + const char *, const char *, ...); +void event_payload_set_time(struct event_payload *, const char *, time_t); +void event_payload_set_int(struct event_payload *, const char *, int); +void event_payload_set_uint(struct event_payload *, const char *, u_int); +void event_payload_set_client(struct event_payload *, const char *, + struct client *); +void event_payload_set_session(struct event_payload *, const char *, + struct session *); +void event_payload_set_window(struct event_payload *, const char *, + struct window *); +void event_payload_set_pane(struct event_payload *, const char *, + struct window_pane *); +void event_payload_set_pointer(struct event_payload *, const char *, void *, + event_payload_free_cb, event_payload_print_cb); +const char *event_payload_get_string(struct event_payload *, const char *); +char *event_payload_print(struct event_payload *, const char *); +void event_payload_add_formats(struct event_payload *, + struct format_tree *, const char *); +struct event_payload_item *event_payload_first(struct event_payload *); +struct event_payload_item *event_payload_next(struct event_payload_item *); +const char *event_payload_item_name(struct event_payload_item *); +enum event_payload_type event_payload_item_type(struct event_payload_item *); +time_t event_payload_get_time(struct event_payload *, const char *); +int event_payload_get_int(struct event_payload *, const char *, int *); +int event_payload_get_uint(struct event_payload *, const char *, u_int *); +struct client *event_payload_get_client(struct event_payload *, const char *); +struct session *event_payload_get_session(struct event_payload *, const char *); +struct window *event_payload_get_window(struct event_payload *, const char *); +struct window_pane *event_payload_get_pane(struct event_payload *, + const char *); +void *event_payload_get_pointer(struct event_payload *, const char *); -/* notify.c */ -void notify_hook(struct cmdq_item *, const char *); -void notify_client(const char *, struct client *); -void notify_session(const char *, struct session *); -void notify_winlink(const char *, struct winlink *); -void notify_session_window(const char *, struct session *, struct window *); -void notify_window(const char *, struct window *); -void notify_pane(const char *, struct window_pane *); -void notify_paste_buffer(const char *, int); +/* events.c */ +typedef void (*events_cb)(const char *, struct event_payload *, void *); +struct events_sink *events_add_sink(const char *, events_cb, void *); +void events_remove_sink(struct events_sink *); +void events_fire(const char *, struct event_payload *); +void events_fire_client(const char *, struct client *); +void events_fire_session(const char *, struct session *); +void events_fire_window(const char *, struct window *); +void events_fire_pane(const char *, struct window_pane *); +void events_fire_winlink(const char *, struct winlink *); + +/* format-draw.c */ +void format_draw(struct screen_write_ctx *, const struct grid_cell *, + u_int, const char *, struct style_ranges *, int); +u_int format_width(const char *); +char *format_trim_left(const char *, u_int); +char *format_trim_right(const char *, u_int); + +/* hooks.c */ +void hooks_add_event(const char *); +int hooks_valid_event_name(const char *); +void hooks_build_events(void); +void hooks_run(struct cmdq_item *, const char *); +void hooks_monitor_add(struct cmdq_item *, struct options *, + const char *, enum monitor_type, int, const char *, + int, struct cmd_find_state *, struct session *); +void hooks_monitor_remove(struct options *, const char *); +void hooks_monitor_free(void *); +char *hooks_monitor_to_string(struct options_entry *); /* options.c */ struct options *options_create(struct options *); @@ -2641,29 +2743,33 @@ struct options_entry *options_default(struct options *, char *options_default_to_string(const struct options_table_entry *); const char *options_name(struct options_entry *); struct options *options_owner(struct options_entry *); +void *options_get_monitor_data(struct options_entry *); +void options_set_monitor_data(struct options_entry *, void *); const struct options_table_entry *options_table_entry(struct options_entry *); struct options_entry *options_get_only(struct options *, const char *); struct options_entry *options_get(struct options *, const char *); void options_array_clear(struct options_entry *); -union options_value *options_array_get(struct options_entry *, u_int); -int options_array_set(struct options_entry *, u_int, const char *, - int, char **); +union options_value *options_array_get(struct options_entry *, const char *); +union options_value * printflike(2, 3) options_array_getv( + struct options_entry *, const char *, ...); +int options_array_set(struct options_entry *, const char *, + const char *, int, char **); int options_array_assign(struct options_entry *, const char *, char **); struct options_array_item *options_array_first(struct options_entry *); struct options_array_item *options_array_next(struct options_array_item *); -u_int options_array_item_index(struct options_array_item *); +const char *options_array_item_key(struct options_array_item *); union options_value *options_array_item_value(struct options_array_item *); int options_is_array(struct options_entry *); int options_is_string(struct options_entry *); -char *options_to_string(struct options_entry *, int, int); -char *options_parse(const char *, int *); -struct options_entry *options_parse_get(struct options *, const char *, int *, - int); +char *options_to_string(struct options_entry *, const char *, int); +char *options_parse(const char *, char **); +struct options_entry *options_parse_get(struct options *, const char *, + char **, int); const struct options_table_entry *options_search(const char *); -char *options_match(const char *, int *, int *); -struct options_entry *options_match_get(struct options *, const char *, int *, - int, int *); +char *options_match(const char *, char **, int *); +struct options_entry *options_match_get(struct options *, const char *, + char **, int, int *); const char *options_get_string(struct options *, const char *); long long options_get_number(struct options *, const char *); struct cmd_parse_tree *options_get_command(struct options *, const char *); @@ -2686,7 +2792,7 @@ int options_from_string(struct options *, int options_find_choice(const struct options_table_entry *, const char *, char **); void options_push_changes(const char *); -int options_remove_or_default(struct options_entry *, int, +int options_remove_or_default(struct options_entry *, const char *, char **); /* options-table.c */ @@ -3453,7 +3559,7 @@ int redraw_get_status_border_cell_type(struct redraw_span **, u_int); /* screen.c */ void screen_init(struct screen *, u_int, u_int, u_int); -void screen_reinit(struct screen *); +void screen_reinit(struct screen *, int); void screen_free(struct screen *); void screen_reset_tabs(struct screen *); void screen_reset_hyperlinks(struct screen *); @@ -3581,6 +3687,8 @@ void window_pane_stack_remove(struct window_panes *, void window_set_name(struct window *, const char *, int); void window_add_ref(struct window *, const char *); void window_remove_ref(struct window *, const char *); +void window_pane_add_ref(struct window_pane *, const char *); +void window_pane_remove_ref(struct window_pane *, const char *); void winlink_clear_flags(struct winlink *); int winlink_shuffle_up(struct session *, struct winlink *, int); int window_pane_start_input(struct window_pane *, @@ -3641,7 +3749,7 @@ void layout_destroy_cell(struct window *, struct layout_cell *, void layout_resize_layout(struct window *, struct layout_cell *, enum layout_type, int, int); struct layout_cell *layout_search_by_border(struct layout_cell *, u_int, u_int); -void layout_set_size(struct layout_cell *, u_int, u_int, int, int); +void layout_set_size(struct layout_cell *, u_int, u_int, int, int); void layout_make_leaf(struct layout_cell *, struct window_pane *); void layout_make_node(struct layout_cell *, enum layout_type); void layout_fix_zindexes(struct window *, struct layout_cell *); @@ -3675,7 +3783,7 @@ struct layout_cell *layout_replace_with_node(struct window *, struct layout_cell *layout_split_pane(struct window_pane *, enum layout_type, int, int); struct layout_cell *layout_floating_pane(struct window *, struct window_pane *, - u_int, u_int, int, int); + struct layout_geometry *); void layout_close_pane(struct window_pane *); int layout_spread_cell(struct window *, struct layout_cell *); void layout_spread_out(struct window_pane *); @@ -3683,10 +3791,10 @@ struct layout_cell *layout_get_tiled_cell(struct cmdq_item *, struct args *, struct window *, struct window_pane *, int, char **); struct layout_cell *layout_get_floating_cell(struct cmdq_item *, struct args *, enum pane_lines, struct window *, struct window_pane *, - char **cause); + char **); int layout_floating_args_parse(struct cmdq_item *, struct args *, - enum pane_lines, struct window *, u_int *, u_int *, int *, - int *, char **); + enum pane_lines, struct window *, struct layout_geometry *, + char **); int layout_remove_tile(struct window *, struct layout_cell *); int layout_insert_tile(struct window *, struct layout_cell *); @@ -3795,11 +3903,22 @@ void check_window_name(struct window *); char *default_window_name(struct window *); char *parse_window_name(const char *); +/* monitor.c */ +struct monitor_set *monitor_create_client(struct client *, monitor_cb, void *); +struct monitor_set *monitor_create_session(struct session *, monitor_cb, void *); +void monitor_destroy(struct monitor_set *); +int monitor_parse(const char *, char **, enum monitor_type *, int *, + char **); +void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, + const char *, int); +void monitor_remove(struct monitor_set *, const char *); + /* control.c */ void control_discard(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); +void control_wait_exit(int); void control_set_pane_on(struct client *, struct window_pane *); void control_set_pane_off(struct client *, struct window_pane *); void control_continue_pane(struct client *, struct window_pane *); @@ -3810,25 +3929,12 @@ void control_reset_offsets(struct client *); void printflike(2, 3) control_write(struct client *, const char *, ...); void control_write_output(struct client *, struct window_pane *); int control_all_done(struct client *); -void control_add_sub(struct client *, const char *, enum control_sub_type, - int, const char *); +void control_add_sub(struct client *, const char *, enum monitor_type, int, + const char *); void control_remove_sub(struct client *, const char *); /* control-notify.c */ -void control_notify_pane_mode_changed(int); -void control_notify_window_layout_changed(struct window *); -void control_notify_window_pane_changed(struct window *); -void control_notify_window_unlinked(struct session *, struct window *); -void control_notify_window_linked(struct session *, struct window *); -void control_notify_window_renamed(struct window *); -void control_notify_client_session_changed(struct client *); -void control_notify_client_detached(struct client *); -void control_notify_session_renamed(struct session *); -void control_notify_session_created(struct session *); -void control_notify_session_closed(struct session *); -void control_notify_session_window_changed(struct session *); -void control_notify_paste_buffer_changed(const char *); -void control_notify_paste_buffer_deleted(const char *); +void control_build_events(void); /* session.c */ extern struct sessions sessions; diff --git a/tty-acs.c b/tty-acs.c index 3dab31b6f..eedb79c2f 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-acs.c,v 1.13 2023/08/08 07:19:48 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/tty-draw.c b/tty-draw.c index 825d81e49..d47be66bc 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-draw.c,v 1.14 2026/06/19 10:38:29 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/tty-features.c b/tty-features.c index b1f8032d6..656100ba9 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/tty-keys.c b/tty-keys.c index f48fb1b71..61e541165 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-keys.c,v 1.210 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -495,7 +495,6 @@ tty_keys_build(struct tty *tty) u_int i, j; const char *s; struct options_entry *o; - struct options_array_item *a; union options_value *ov; char copy[16]; key_code key; @@ -532,12 +531,10 @@ tty_keys_build(struct tty *tty) o = options_get(global_options, "user-keys"); if (o != NULL) { - a = options_array_first(o); - while (a != NULL) { - i = options_array_item_index(a); - ov = options_array_item_value(a); - tty_keys_add(tty, ov->string, KEYC_USER + i); - a = options_array_next(a); + for (i = 0; i <= KEYC_NUSER; i++) { + ov = options_array_getv(o, "%u", i); + if (ov != NULL) + tty_keys_add(tty, ov->string, KEYC_USER + i); } } } @@ -751,7 +748,7 @@ tty_keys_next(struct tty *tty) const char *buf; size_t len, size; cc_t bspace; - int delay, expired = 0, n; + int delay, expired = 0, n, bg = tty->bg; key_code key, onlykey; struct mouse_event m = { 0 }; struct key_event *event; @@ -811,11 +808,15 @@ tty_keys_next(struct tty *tty) 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; } @@ -1007,10 +1008,10 @@ complete_key: if (key == KEYC_FOCUS_OUT) { c->flags &= ~CLIENT_FOCUSED; window_update_focus(c->session->curw->window); - notify_client("client-focus-out", c); + events_fire_client("client-focus-out", c); } else if (key == KEYC_FOCUS_IN) { c->flags |= CLIENT_FOCUSED; - notify_client("client-focus-in", c); + events_fire_client("client-focus-in", c); window_update_focus(c->session->curw->window); } diff --git a/tty-term.c b/tty-term.c index c248aa840..164b9f977 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/tty.c b/tty.c index 03dabb335..a0d212732 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty.c,v 1.476 2026/06/26 11:36:22 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/utf8-combined.c b/utf8-combined.c index 923342b43..02abc3518 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: utf8-combined.c,v 1.10 2026/06/09 11:49:36 nicm Exp $ */ /* * Copyright (c) 2023 Nicholas Marriott diff --git a/utf8.c b/utf8.c index e57100fd2..887d2b0f9 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/window-border.c b/window-border.c index 5420f4b97..0419015f4 100644 --- a/window-border.c +++ b/window-border.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-border.c,v 1.1 2026/06/22 08:47:46 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-buffer.c b/window-buffer.c index 639cb5ede..3e0d3df62 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-buffer.c,v 1.49 2026/06/26 08:10:49 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-client.c b/window-client.c index a780222e3..64e38cb81 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-client.c,v 1.46 2026/07/04 22:09:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -164,6 +164,7 @@ const struct window_mode window_client_mode = { struct window_client_itemdata { struct client *c; + char *ttyname; }; struct window_client_modedata { @@ -204,6 +205,7 @@ static void window_client_free_item(struct window_client_itemdata *item) { server_client_unref(item->c); + free(item->ttyname); free(item); } @@ -231,6 +233,7 @@ window_client_build(void *modedata, struct sort_criteria *sort_crit, item = window_client_add_item(data); item->c = l[i]; + item->ttyname = xstrdup(l[i]->ttyname); l[i]->references++; } @@ -552,7 +555,7 @@ window_client_key(struct window_mode_entry *wme, struct client *c, break; case '\r': item = mode_tree_get_current(mtd); - mode_tree_run_command(c, NULL, data->command, item->c->ttyname); + mode_tree_run_command(c, NULL, data->command, item->ttyname); finished = 1; break; } diff --git a/window-clock.c b/window-clock.c index ace160a8b..9853b2abb 100644 --- a/window-clock.c +++ b/window-clock.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-clock.c,v 1.34 2026/06/25 16:32:42 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/window-copy.c b/window-copy.c index aedbba99b..997cb8888 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-copy.c,v 1.413 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,8 +49,8 @@ static void window_copy_next_paragraph(struct window_mode_entry *); static void window_copy_previous_paragraph(struct window_mode_entry *); static void window_copy_redraw_selection(struct window_mode_entry *, u_int); static void window_copy_redraw_lines(struct window_mode_entry *, u_int, - u_int); -static void window_copy_redraw_screen(struct window_mode_entry *); + u_int, int); +static void window_copy_redraw_screen(struct window_mode_entry *, int); static void window_copy_do_refresh(struct window_mode_entry *, int); static void window_copy_refresh_timer(int, short, void *); static void window_copy_refresh_arm(struct window_mode_entry *); @@ -66,7 +66,7 @@ static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int, static u_int window_copy_cursor_unoffset(struct window_mode_entry *, u_int, u_int); static void window_copy_write_line(struct window_mode_entry *, - struct screen_write_ctx *, u_int); + struct screen_write_ctx *, u_int, int); static void window_copy_write_lines(struct window_mode_entry *, struct screen_write_ctx *, u_int, u_int); static char *window_copy_match_at_cursor(struct window_copy_mode_data *); @@ -212,6 +212,7 @@ enum { enum window_copy_cmd_action { WINDOW_COPY_CMD_NOTHING, + WINDOW_COPY_CMD_MOVE, WINDOW_COPY_CMD_REDRAW, WINDOW_COPY_CMD_CANCEL, }; @@ -628,7 +629,7 @@ window_copy_init(struct window_mode_entry *wme, screen_write_start(&ctx, &data->screen); for (i = 0; i < screen_size_y(&data->screen); i++) - window_copy_write_line(wme, &ctx, i); + window_copy_write_line(wme, &ctx, i, 1); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(&data->screen)), data->cy, 0); screen_write_stop(&ctx); @@ -742,10 +743,10 @@ window_copy_vadd(struct window_pane *wp, int parse, const char *fmt, va_list ap) * (If there's any history at all, it has changed.) */ if (screen_hsize(data->backing)) - window_copy_redraw_lines(wme, 0, 1); + window_copy_redraw_lines(wme, 0, 1, 1); /* Write the new lines. */ - window_copy_redraw_lines(wme, old_cy, backing->cy - old_cy + 1); + window_copy_redraw_lines(wme, old_cy, backing->cy - old_cy + 1, 1); screen_write_stop(&ctx); } @@ -854,7 +855,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, window_copy_cursor_end_of_line(wme); } - if (scroll_exit && data->oy == 0) { + if (scroll_exit && data->oy == 0 && data->screen.sel == NULL) { window_pane_reset_mode(wp); return; } @@ -863,7 +864,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); window_pane_scrollbar_show(wp, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } void @@ -917,7 +918,7 @@ window_copy_pageup1(struct window_mode_entry *wme, int half_page) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); window_pane_scrollbar_show(wme->wp, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } void @@ -972,13 +973,13 @@ window_copy_pagedown1(struct window_mode_entry *wme, int half_page, window_copy_cursor_end_of_line(wme); } - if (scroll_exit && data->oy == 0) + if (scroll_exit && data->oy == 0 && data->screen.sel == NULL) return (1); if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); window_copy_update_selection(wme, 1, 0); window_pane_scrollbar_show(wme->wp, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); return (0); } @@ -1223,7 +1224,7 @@ window_copy_resize(struct window_mode_entry *wme, u_int sx, u_int sy) } window_copy_size_changed(wme); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static const char * @@ -1292,7 +1293,7 @@ window_copy_cmd_back_to_indentation(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_back_to_indentation(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1305,7 +1306,7 @@ window_copy_cmd_begin_selection(struct window_copy_cmd_state *cs) if (m != NULL) { window_copy_start_drag(c, m); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } data->lineflag = LINE_SEL_NONE; @@ -1323,7 +1324,7 @@ window_copy_cmd_stop_selection(struct window_copy_cmd_state *cs) data->cursordrag = CURSORDRAG_NONE; data->lineflag = LINE_SEL_NONE; data->selflag = SEL_CHAR; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1579,7 +1580,7 @@ window_copy_cmd_cursor_down(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_down(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1594,7 +1595,7 @@ window_copy_cmd_cursor_down_and_cancel(struct window_copy_cmd_state *cs) window_copy_cursor_down(wme, 0); if (cy == data->cy && data->oy == 0) return (WINDOW_COPY_CMD_CANCEL); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1605,7 +1606,7 @@ window_copy_cmd_cursor_left(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_left(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1619,7 +1620,7 @@ window_copy_cmd_cursor_right(struct window_copy_cmd_state *cs) window_copy_cursor_right(wme, data->screen.sel != NULL && data->rectflag); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } /* Scroll line containing the cursor to the given position. */ @@ -1687,7 +1688,7 @@ window_copy_cmd_scroll_to_mouse(struct window_copy_cmd_state *cs) tty_window_offset(&c->tty, &tty_ox, &tty_oy, &tty_sx, &tty_sy); window_copy_scroll(wp, c->tty.mouse_slider_mpos, m->y, tty_oy, scroll_exit); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } /* Scroll line containing the cursor to the top. */ @@ -1705,7 +1706,7 @@ window_copy_cmd_cursor_up(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_up(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1736,7 +1737,7 @@ window_copy_cmd_end_of_line(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_end_of_line(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1750,7 +1751,7 @@ window_copy_cmd_halfpage_down(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 1, data->scroll_exit)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1764,7 +1765,7 @@ window_copy_cmd_halfpage_down_and_cancel(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 1, 1)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1775,7 +1776,7 @@ window_copy_cmd_halfpage_up(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_pageup1(wme, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1860,7 +1861,7 @@ window_copy_cmd_jump_again(struct window_copy_cmd_state *cs) window_copy_cursor_jump_to_back(wme); break; } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1888,7 +1889,7 @@ window_copy_cmd_jump_reverse(struct window_copy_cmd_state *cs) window_copy_cursor_jump_to(wme); break; } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -1987,7 +1988,7 @@ window_copy_cmd_previous_matching_bracket(struct window_copy_cmd_state *cs) window_copy_scroll_to(wme, px, py, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2108,7 +2109,7 @@ window_copy_cmd_next_matching_bracket(struct window_copy_cmd_state *cs) window_copy_scroll_to(wme, px, py, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2119,7 +2120,7 @@ window_copy_cmd_next_paragraph(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_next_paragraph(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2130,7 +2131,7 @@ window_copy_cmd_next_space(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word(wme, ""); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2141,7 +2142,7 @@ window_copy_cmd_next_space_end(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word_end(wme, "", 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2155,7 +2156,7 @@ window_copy_cmd_next_word(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word(wme, separators); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2169,7 +2170,7 @@ window_copy_cmd_next_word_end(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_next_word_end(wme, separators, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2182,7 +2183,7 @@ window_copy_cmd_other_end(struct window_copy_cmd_state *cs) data->selflag = SEL_CHAR; if ((np % 2) != 0) window_copy_other_end(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2200,7 +2201,7 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs) data->selflag = SEL_WORD; } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) data->selflag = SEL_LINE; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2214,7 +2215,7 @@ window_copy_cmd_page_down(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 0, data->scroll_exit)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2227,7 +2228,7 @@ window_copy_cmd_page_down_and_cancel(struct window_copy_cmd_state *cs) if (window_copy_pagedown1(wme, 0, 1)) return (WINDOW_COPY_CMD_CANCEL); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2238,7 +2239,7 @@ window_copy_cmd_page_up(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_pageup1(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2249,7 +2250,7 @@ window_copy_cmd_previous_paragraph(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_previous_paragraph(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2260,7 +2261,7 @@ window_copy_cmd_previous_space(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_previous_word(wme, "", 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2274,7 +2275,7 @@ window_copy_cmd_previous_word(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_previous_word(wme, separators, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2286,7 +2287,7 @@ window_copy_cmd_rectangle_on(struct window_copy_cmd_state *cs) data->lineflag = LINE_SEL_NONE; window_copy_rectangle_set(wme, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2298,7 +2299,7 @@ window_copy_cmd_rectangle_off(struct window_copy_cmd_state *cs) data->lineflag = LINE_SEL_NONE; window_copy_rectangle_set(wme, 0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2310,7 +2311,7 @@ window_copy_cmd_rectangle_toggle(struct window_copy_cmd_state *cs) data->lineflag = LINE_SEL_NONE; window_copy_rectangle_set(wme, !data->rectflag); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2320,7 +2321,7 @@ window_copy_cmd_scroll_exit_on(struct window_copy_cmd_state *cs) data->scroll_exit = 1; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2330,7 +2331,7 @@ window_copy_cmd_scroll_exit_off(struct window_copy_cmd_state *cs) data->scroll_exit = 0; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2340,7 +2341,7 @@ window_copy_cmd_scroll_exit_toggle(struct window_copy_cmd_state *cs) data->scroll_exit = !data->scroll_exit; - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2350,11 +2351,21 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + /* + * If at the bottom and scroll_exit is active with no selection, + * cancel/exit copy-mode. Otherwise nothing can change, so return + * WINDOW_COPY_CMD_NOTHING. + */ + if (data->oy == 0) { + if (data->scroll_exit && data->screen.sel == NULL) + return (WINDOW_COPY_CMD_CANCEL); + return (WINDOW_COPY_CMD_NOTHING); + } for (; np != 0; np--) window_copy_cursor_down(wme, 1); - if (data->scroll_exit && data->oy == 0) + if (data->scroll_exit && data->oy == 0 && data->screen.sel == NULL) return (WINDOW_COPY_CMD_CANCEL); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2368,18 +2379,25 @@ window_copy_cmd_scroll_down_and_cancel(struct window_copy_cmd_state *cs) window_copy_cursor_down(wme, 1); if (data->oy == 0) return (WINDOW_COPY_CMD_CANCEL); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action window_copy_cmd_scroll_up(struct window_copy_cmd_state *cs) { struct window_mode_entry *wme = cs->wme; + struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + /* + * If at the top, nothing can change, so return WINDOW_COPY_CMD_NOTHING + * and do not repaint anything. + */ + if (data->oy == screen_hsize(data->backing)) + return (WINDOW_COPY_CMD_NOTHING); for (; np != 0; np--) window_copy_cursor_up(wme, 1); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2396,7 +2414,7 @@ window_copy_cmd_search_again(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_down(wme, data->searchregex); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2413,7 +2431,7 @@ window_copy_cmd_search_reverse(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_up(wme, data->searchregex); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2481,7 +2499,7 @@ window_copy_cmd_select_word(struct window_copy_cmd_state *cs) else { window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 1)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } data->endselrx = data->cx; data->endselry = screen_hsize(data->backing) + data->cy - data->oy; @@ -2511,7 +2529,7 @@ window_copy_cmd_start_of_line(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_start_of_line(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2590,7 +2608,7 @@ window_copy_cmd_pipe_no_clear(struct window_copy_cmd_state *cs) window_copy_pipe(wme, s, command); free(command); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2621,7 +2639,7 @@ window_copy_cmd_goto_line(struct window_copy_cmd_state *cs) if (*arg0 != '\0') window_copy_goto_line(wme, arg0); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2639,7 +2657,7 @@ window_copy_cmd_jump_backward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump_back(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2657,7 +2675,7 @@ window_copy_cmd_jump_forward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2675,7 +2693,7 @@ window_copy_cmd_jump_to_backward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump_to_back(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2693,7 +2711,7 @@ window_copy_cmd_jump_to_forward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_cursor_jump_to(wme); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2702,7 +2720,7 @@ window_copy_cmd_jump_to_mark(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_jump_to_mark(wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2711,7 +2729,7 @@ window_copy_cmd_next_prompt(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_prompt(wme, 1, args_has(cs->wargs, 'o')); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2720,7 +2738,7 @@ window_copy_cmd_previous_prompt(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; window_copy_cursor_prompt(wme, 0, args_has(cs->wargs, 'o')); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2731,7 +2749,7 @@ window_copy_cmd_search_backward(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHUP; @@ -2740,7 +2758,7 @@ window_copy_cmd_search_backward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_up(wme, 1); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2751,7 +2769,7 @@ window_copy_cmd_search_backward_text(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHUP; @@ -2760,7 +2778,7 @@ window_copy_cmd_search_backward_text(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_up(wme, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2771,7 +2789,7 @@ window_copy_cmd_search_forward(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHDOWN; @@ -2780,7 +2798,7 @@ window_copy_cmd_search_forward(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_down(wme, 1); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2791,7 +2809,7 @@ window_copy_cmd_search_forward_text(struct window_copy_cmd_state *cs) u_int np = wme->prefix; if (!window_copy_expand_search_string(cs)) - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); if (data->searchstr != NULL) { data->searchtype = WINDOW_COPY_SEARCHDOWN; @@ -2800,7 +2818,7 @@ window_copy_cmd_search_forward_text(struct window_copy_cmd_state *cs) for (; np != 0; np--) window_copy_search_down(wme, 0); } - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -2811,7 +2829,7 @@ window_copy_cmd_search_backward_incremental(struct window_copy_cmd_state *cs) const char *arg0 = args_string(cs->wargs, 0); const char *ss = data->searchstr; char prefix; - enum window_copy_cmd_action action = WINDOW_COPY_CMD_NOTHING; + enum window_copy_cmd_action action = WINDOW_COPY_CMD_MOVE; data->timeout = 0; @@ -2868,7 +2886,7 @@ window_copy_cmd_search_forward_incremental(struct window_copy_cmd_state *cs) const char *arg0 = args_string(cs->wargs, 0); const char *ss = data->searchstr; char prefix; - enum window_copy_cmd_action action = WINDOW_COPY_CMD_NOTHING; + enum window_copy_cmd_action action = WINDOW_COPY_CMD_MOVE; data->timeout = 0; @@ -2989,7 +3007,7 @@ window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) follow = (data->oy == 0 && data->cy == screen_size_y(&data->screen) - 1); window_copy_do_refresh(wme, follow); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); /* The timer runs outside key handling, so force a repaint. */ wp->flags |= PANE_REDRAW; wp->flags &= ~PANE_UNSEENCHANGES; @@ -3026,14 +3044,14 @@ static enum window_copy_cmd_action window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) { window_copy_refresh_start(cs->wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action window_copy_cmd_refresh_off(struct window_copy_cmd_state *cs) { window_copy_refresh_stop(cs->wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -3045,7 +3063,7 @@ window_copy_cmd_refresh_toggle(struct window_copy_cmd_state *cs) window_copy_refresh_stop(cs->wme); else window_copy_refresh_start(cs->wme); - return (WINDOW_COPY_CMD_NOTHING); + return (WINDOW_COPY_CMD_MOVE); } static enum window_copy_cmd_action @@ -3716,7 +3734,7 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, cs.s = s; cs.wl = wl; - action = WINDOW_COPY_CMD_NOTHING; + action = WINDOW_COPY_CMD_MOVE; for (i = 0; i < nitems(window_copy_cmd_table); i++) { if (strcmp(window_copy_cmd_table[i].command, command) == 0) { flags = window_copy_cmd_table[i].flags; @@ -3755,7 +3773,7 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, window_copy_clear_marks(wme); data->searchx = data->searchy = -1; } - if (action == WINDOW_COPY_CMD_NOTHING) + if (action == WINDOW_COPY_CMD_MOVE) action = WINDOW_COPY_CMD_REDRAW; } wme->prefix = 1; @@ -3763,15 +3781,13 @@ window_copy_command(struct window_mode_entry *wme, struct client *c, if (action == WINDOW_COPY_CMD_CANCEL) window_pane_reset_mode(wp); else if (action == WINDOW_COPY_CMD_REDRAW) - window_copy_redraw_screen(wme); - else if (action == WINDOW_COPY_CMD_NOTHING) { + window_copy_redraw_screen(wme, 1); + else if (action == WINDOW_COPY_CMD_MOVE) { /* - * Nothing is not actually nothing - most commands at least - * move the cursor (what would be the point of a command that - * literally does nothing?) and in that case we need to redraw - * the first line to update the indicator. + * If the cursor moves or a similar trivial change is made, + * only redraw the first line to update the indicator. */ - window_copy_redraw_lines(wme, 0, 1); + window_copy_redraw_lines(wme, 0, 1, 0); } } @@ -3808,7 +3824,7 @@ window_copy_scroll_to(struct window_mode_entry *wme, u_int px, u_int py, if (data->oy != old_oy) window_pane_scrollbar_show(wme->wp, 1); if (!no_redraw) - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static int @@ -4596,7 +4612,7 @@ window_copy_search(struct window_mode_entry *wme, int direction, int regex) } } } - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); screen_free(&ss); return (found); @@ -4849,7 +4865,7 @@ window_copy_goto_line(struct window_mode_entry *wme, const char *linestr) } window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -5119,7 +5135,7 @@ window_copy_set_line_numbers(struct window_pane *wp, int enabled) if (data == NULL || data->line_numbers == enabled) return; data->line_numbers = enabled; - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } int @@ -5141,7 +5157,7 @@ window_copy_get_current_offset(struct window_pane *wp, u_int *offset, static void window_copy_write_line(struct window_mode_entry *wme, - struct screen_write_ctx *ctx, u_int py) + struct screen_write_ctx *ctx, u_int py, int clear) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5166,7 +5182,8 @@ window_copy_write_line(struct window_mode_entry *wme, content_sx = sx; screen_write_cursormove(ctx, 0, py, 0); - screen_write_clearline(ctx, 8); + if (clear) + screen_write_clearline(ctx, 8); ft = format_create_defaults(NULL, NULL, NULL, NULL, wp); @@ -5237,7 +5254,7 @@ window_copy_write_lines(struct window_mode_entry *wme, u_int yy; for (yy = py; yy < py + ny; yy++) - window_copy_write_line(wme, ctx, yy); + window_copy_write_line(wme, ctx, yy, 1); } static void @@ -5265,11 +5282,12 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y) if (end < gd->sy + data->oy - 1) end++; } - window_copy_redraw_lines(wme, start, end - start + 1); + window_copy_redraw_lines(wme, start, end - start + 1, 0); } static void -window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) +window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny, + int clear) { struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; @@ -5280,7 +5298,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) if (window_copy_line_number_width(wme) != 0) { screen_write_start(&ctx, &data->screen); for (i = py; i < py + ny; i++) - window_copy_write_line(wme, &ctx, i); + window_copy_write_line(wme, &ctx, i, clear); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -5294,7 +5312,7 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) else screen_write_start_pane(&ctx, wp, NULL); for (i = py; i < py + ny; i++) - window_copy_write_line(wme, &ctx, i); + window_copy_write_line(wme, &ctx, i, clear); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -5304,11 +5322,11 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) } static void -window_copy_redraw_screen(struct window_mode_entry *wme) +window_copy_redraw_screen(struct window_mode_entry *wme, int clear) { struct window_copy_mode_data *data = wme->data; - window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen)); + window_copy_redraw_lines(wme, 0, screen_size_y(&data->screen), clear); } static void @@ -5318,7 +5336,7 @@ window_copy_style_changed(struct window_mode_entry *wme) if (data->screen.sel != NULL) window_copy_set_selection(wme, 0, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -5439,7 +5457,7 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) if (s->sel != NULL || data->lineflag != LINE_SEL_NONE || old_cy != data->cy) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } if (width >= screen_size_x(s)) @@ -5447,7 +5465,7 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) else content_sx = screen_size_x(s) - width; if (old_cx >= content_sx || data->cx >= content_sx) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } screen_write_start_pane(&ctx, wp, NULL); @@ -5458,9 +5476,9 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy) return; } if (old_cx == screen_size_x(s)) - window_copy_redraw_lines(wme, old_cy, 1); + window_copy_redraw_lines(wme, old_cy, 1, 1); if (data->cx == screen_size_x(s)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); else { screen_write_start_pane(&ctx, wp, NULL); screen_write_cursormove(&ctx, @@ -5587,16 +5605,16 @@ window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, cy = data->cy; if (data->cursordrag == CURSORDRAG_ENDSEL) { if (sy < cy) - window_copy_redraw_lines(wme, sy, cy - sy + 1); + window_copy_redraw_lines(wme, sy, cy - sy + 1, 1); else - window_copy_redraw_lines(wme, cy, sy - cy + 1); + window_copy_redraw_lines(wme, cy, sy - cy + 1, 1); } else { if (endsy < cy) { window_copy_redraw_lines(wme, endsy, - cy - endsy + 1); + cy - endsy + 1, 1); } else { window_copy_redraw_lines(wme, cy, - endsy - cy + 1); + endsy - cy + 1, 1); } } } @@ -5746,7 +5764,7 @@ window_copy_copy_buffer(struct window_mode_entry *wme, const char *prefix, screen_write_setselection(&ctx, "", buf, len); screen_write_stop(&ctx); wp->flags |= redraw; - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); } if (set_paste) @@ -5831,7 +5849,7 @@ window_copy_append_selection(struct window_mode_entry *wme) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, "", buf, len); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); } pb = paste_get_top(&bufname); @@ -6076,7 +6094,7 @@ window_copy_other_end(struct window_mode_entry *wme) data->cx = hsize; window_copy_update_selection(wme, 1, 1); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -6153,9 +6171,9 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) window_copy_scroll_down(wme, 1); if (scroll_only) { if (data->cy == screen_size_y(s) - 1) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 0); else - window_copy_redraw_lines(wme, data->cy, 2); + window_copy_redraw_lines(wme, data->cy, 2, 0); } } else { if (norectsel) { @@ -6165,9 +6183,9 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) window_copy_update_cursor(wme, data->cx, data->cy - 1); if (window_copy_update_selection(wme, 1, 0)) { if (data->cy == screen_size_y(s) - 1) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); else - window_copy_redraw_lines(wme, data->cy, 2); + window_copy_redraw_lines(wme, data->cy, 2, 1); } } @@ -6179,7 +6197,7 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) { window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6192,13 +6210,13 @@ window_copy_cursor_up(struct window_mode_entry *wme, int scroll_only) px = window_copy_find_length(wme, py); window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } else if (data->lineflag == LINE_SEL_RIGHT_LEFT) { window_copy_update_cursor(wme, 0, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6232,7 +6250,7 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) data->cx = data->lastcx; window_copy_scroll_up(wme, 1); if (scroll_only && data->cy > 0) - window_copy_redraw_lines(wme, data->cy - 1, 2); + window_copy_redraw_lines(wme, data->cy - 1, 2, 0); } else { if (norectsel) { window_copy_update_cursor(wme, data->lastcx, @@ -6240,7 +6258,7 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) } else window_copy_update_cursor(wme, data->cx, data->cy + 1); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy - 1, 2); + window_copy_redraw_lines(wme, data->cy - 1, 2, 1); } if (norectsel) { @@ -6251,7 +6269,7 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) { window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6264,13 +6282,13 @@ window_copy_cursor_down(struct window_mode_entry *wme, int scroll_only) px = window_copy_find_length(wme, py); window_copy_update_cursor(wme, px, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } else if (data->lineflag == LINE_SEL_RIGHT_LEFT) { window_copy_update_cursor(wme, 0, data->cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, data->cy, 1); + window_copy_redraw_lines(wme, data->cy, 1, 1); } } @@ -6540,7 +6558,7 @@ window_copy_cursor_prompt(struct window_mode_entry *wme, int direction, } window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -6564,21 +6582,22 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } screen_write_start(&ctx, &data->screen); screen_write_cursormove(&ctx, 0, 0, 0); screen_write_deleteline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, screen_size_y(s) - ny, ny); - window_copy_write_line(wme, &ctx, 0); + window_copy_write_line(wme, &ctx, 0, 0); if (screen_size_y(s) > 1) - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); if (screen_size_y(s) > 3) - window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); + window_copy_write_line(wme, &ctx, + screen_size_y(s) - 2, 0); if (s->sel != NULL && screen_size_y(s) > ny) { window_copy_write_line(wme, &ctx, - screen_size_y(s) - ny - 1); + screen_size_y(s) - ny - 1, 0); } screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), @@ -6595,13 +6614,14 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) screen_write_cursormove(&ctx, 0, 0, 0); screen_write_deleteline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, screen_size_y(s) - ny, ny); - window_copy_write_line(wme, &ctx, 0); + window_copy_write_line(wme, &ctx, 0, 0); if (screen_size_y(s) > 1) - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); if (screen_size_y(s) > 3) - window_copy_write_line(wme, &ctx, screen_size_y(s) - 2); + window_copy_write_line(wme, &ctx, screen_size_y(s) - 2, 0); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1); + window_copy_write_line(wme, &ctx, screen_size_y(s) - ny - 1, + 0); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -6633,7 +6653,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 0); return; } screen_write_start(&ctx, &data->screen); @@ -6641,9 +6661,9 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) screen_write_insertline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, 0, ny); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, ny); + window_copy_write_line(wme, &ctx, ny, 0); else if (ny == 1) - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); @@ -6660,9 +6680,9 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) screen_write_insertline(&ctx, ny, 8); window_copy_write_lines(wme, &ctx, 0, ny); if (s->sel != NULL && screen_size_y(s) > ny) - window_copy_write_line(wme, &ctx, ny); + window_copy_write_line(wme, &ctx, ny, 0); else if (ny == 1) /* nuke position */ - window_copy_write_line(wme, &ctx, 1); + window_copy_write_line(wme, &ctx, 1, 0); screen_write_cursormove(&ctx, window_copy_cursor_offset(wme, data->cx, screen_size_x(s)), data->cy, 0); screen_write_stop(&ctx); @@ -6683,7 +6703,7 @@ window_copy_rectangle_set(struct window_mode_entry *wme, int rectflag) window_copy_update_cursor(wme, px, data->cy); window_copy_update_selection(wme, 1, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } static void @@ -6761,7 +6781,7 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) break; } - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); window_copy_drag_update(c, m); } @@ -6856,7 +6876,7 @@ window_copy_jump_to_mark(struct window_mode_entry *wme) data->my = tmy; data->showmark = 1; window_copy_update_selection(wme, 0, 0); - window_copy_redraw_screen(wme); + window_copy_redraw_screen(wme, 1); } /* Scroll up if the cursor went off the visible screen. */ @@ -6882,7 +6902,7 @@ window_copy_acquire_cursor_up(struct window_mode_entry *wme, u_int hsize, } window_copy_update_cursor(wme, px, cy); if (window_copy_update_selection(wme, 1, 0)) - window_copy_redraw_lines(wme, cy, nd); + window_copy_redraw_lines(wme, cy, nd, 1); } /* Scroll down if the cursor went off the visible screen. */ @@ -6911,5 +6931,5 @@ window_copy_acquire_cursor_down(struct window_mode_entry *wme, u_int hsize, else window_copy_update_cursor(wme, px, cy); if (window_copy_update_selection(wme, 1, no_reset)) - window_copy_redraw_lines(wme, oldy, nd); + window_copy_redraw_lines(wme, oldy, nd, 1); } diff --git a/window-customize.c b/window-customize.c index 01f1eeff4..aecc5ac46 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-customize.c,v 1.30 2026/07/06 14:40:57 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -82,15 +82,15 @@ enum window_customize_change { }; struct window_customize_itemdata { - struct window_customize_modedata *data; - enum window_customize_scope scope; + struct window_customize_modedata *data; + enum window_customize_scope scope; - char *table; - key_code key; + char *table; + key_code key; - struct options *oo; - char *name; - int idx; + struct options *oo; + char *name; + char *array_key; }; struct window_customize_modedata { @@ -111,15 +111,17 @@ struct window_customize_modedata { }; static uint64_t -window_customize_get_tag(struct options_entry *o, int idx, +window_customize_get_tag(struct options_entry *o, struct options_array_item *a, const struct options_table_entry *oe) { uint64_t offset; + if (a != NULL) + return ((uint64_t)(uintptr_t)a); if (oe == NULL) return ((uint64_t)o); offset = ((char *)oe - (char *)options_table) / sizeof *options_table; - return ((2ULL << 62)|(offset << 32)|((idx + 1) << 1)|1); + return ((2ULL << 62)|(offset << 32)|1); } static struct options * @@ -224,6 +226,7 @@ window_customize_free_item(struct window_customize_itemdata *item) { free(item->table); free(item->name); + free(item->array_key); free(item); } @@ -237,26 +240,26 @@ window_customize_build_array(struct window_customize_modedata *data, struct window_customize_itemdata *item; struct options_array_item *ai; char *name, *value, *text; - u_int idx; uint64_t tag; + const char *array_key; ai = options_array_first(o); while (ai != NULL) { - idx = options_array_item_index(ai); + array_key = options_array_item_key(ai); - xasprintf(&name, "%s[%u]", options_name(o), idx); + xasprintf(&name, "%s[%s]", options_name(o), array_key); format_add(ft, "option_name", "%s", name); - value = options_to_string(o, idx, 0); + value = options_to_string(o, array_key, 0); format_add(ft, "option_value", "%s", value); item = window_customize_add_item(data); item->scope = scope; item->oo = oo; item->name = xstrdup(options_name(o)); - item->idx = idx; + item->array_key = xstrdup(array_key); text = format_expand(ft, data->format); - tag = window_customize_get_tag(o, idx, oe); + tag = window_customize_get_tag(o, ai, oe); mode_tree_add(data->data, top, item, tag, name, text, -1); free(text); @@ -307,7 +310,7 @@ window_customize_build_option(struct window_customize_modedata *data, format_add(ft, "option_unit", "%s", ""); if (!array) { - value = options_to_string(o, -1, 0); + value = options_to_string(o, NULL, 0); format_add(ft, "option_value", "%s", value); free(value); } @@ -324,13 +327,12 @@ window_customize_build_option(struct window_customize_modedata *data, item->oo = oo; item->scope = scope; item->name = xstrdup(name); - item->idx = -1; if (array) text = NULL; else text = format_expand(ft, data->format); - tag = window_customize_get_tag(o, -1, oe); + tag = window_customize_get_tag(o, NULL, oe); top = mode_tree_add(data->data, top, item, tag, name, text, 0); free(text); @@ -483,7 +485,6 @@ window_customize_build_keys(struct window_customize_modedata *data, item->table = xstrdup(kt->name); item->key = bd->key; item->name = xstrdup(key_string_lookup_key(item->key, 0)); - item->idx = -1; expanded = format_expand(ft, data->format); child = mode_tree_add(data->data, top, item, (uint64_t)bd, @@ -654,12 +655,12 @@ window_customize_draw_option(struct window_customize_modedata *data, { struct screen *s = ctx->s; u_int cx = s->cx, cy = s->cy; - int idx; struct options_entry *o, *parent; struct options *go, *wo; const struct options_table_entry *oe; struct grid_cell gc; const char **choice, *text, *name; + const char *array_key; const char *space = "", *unit = ""; char *value = NULL, *expanded; char *default_value = NULL; @@ -670,7 +671,7 @@ window_customize_draw_option(struct window_customize_modedata *data, if (!window_customize_check_item(data, item, &fs)) return; name = item->name; - idx = item->idx; + array_key = item->array_key; o = options_get(item->oo, name); if (o == NULL) @@ -709,25 +710,25 @@ window_customize_draw_option(struct window_customize_modedata *data, &grid_default_cell, "This is a %s option.", text)) goto out; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (idx != -1) { + if (array_key != NULL) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, - "This is an array option, index %u.", idx)) + "This is an array option, key %s.", array_key)) goto out; } else { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "This is an array option.")) goto out; } - if (idx == -1) + if (array_key == NULL) goto out; } screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */ if (s->cy >= cy + sy - 1) goto out; - value = options_to_string(o, idx, 0); - if (oe != NULL && idx == -1) { + value = options_to_string(o, array_key, 0); + if (oe != NULL && array_key == NULL) { default_value = options_default_to_string(oe); if (strcmp(default_value, value) == 0) { free(default_value); @@ -821,7 +822,7 @@ window_customize_draw_option(struct window_customize_modedata *data, if (wo != NULL && options_owner(o) != wo) { parent = options_get_only(wo, name); if (parent != NULL) { - value = options_to_string(parent, -1 , 0); + value = options_to_string(parent, NULL, 0); if (!screen_write_text(ctx, s->cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "Window value (from window %u): %s%s%s", fs.wl->idx, @@ -832,7 +833,7 @@ window_customize_draw_option(struct window_customize_modedata *data, if (go != NULL && options_owner(o) != go) { parent = options_get_only(go, name); if (parent != NULL) { - value = options_to_string(parent, -1 , 0); + value = options_to_string(parent, NULL, 0); if (!screen_write_text(ctx, s->cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "Global value: %s%s%s", value, space, unit)) @@ -897,6 +898,8 @@ static const char* window_customize_help_lines[] = { "#[fg=themelightgrey]" " U #[#{E:tree-mode-border-style},acs]x#[default] Unset tagged %1s", "#[fg=themelightgrey]" + " a #[#{E:tree-mode-border-style},acs]x#[default] Change array key", + "#[fg=themelightgrey]" " f #[#{E:tree-mode-border-style},acs]x#[default] Enter a filter", "#[fg=themelightgrey]" " v #[#{E:tree-mode-border-style},acs]x#[default] Toggle information", @@ -1008,8 +1011,10 @@ window_customize_set_option_callback(struct client *c, void *itemdata, const struct options_table_entry *oe; struct options *oo = item->oo; const char *name = item->name; + const char *array_key = item->array_key; char *cause; - int idx = item->idx; + u_int idx; + char keybuf[32]; if (s == NULL || *s == '\0' || data->dead) return (PROMPT_CLOSE); @@ -1021,13 +1026,15 @@ window_customize_set_option_callback(struct client *c, void *itemdata, oe = options_table_entry(o); if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (idx == -1) { + if (array_key == NULL) { for (idx = 0; idx < INT_MAX; idx++) { - if (options_array_get(o, idx) == NULL) + if (options_array_getv(o, "%u", idx) == NULL) break; } + xsnprintf(keybuf, sizeof keybuf, "%u", idx); + array_key = keybuf; } - if (options_array_set(o, idx, s, 0, &cause) != 0) + if (options_array_set(o, array_key, s, 0, &cause) != 0) goto fail; } else { if (options_from_string(oo, oe, name, s, 0, &cause) != 0) @@ -1057,10 +1064,11 @@ window_customize_set_option(struct client *c, const struct options_table_entry *oe; struct options *oo; struct window_customize_itemdata *new_item; - int flag, idx = item->idx; + int flag; enum window_customize_scope scope = WINDOW_CUSTOMIZE_NONE; u_int choice; const char *name = item->name, *space = ""; + const char *array_key = item->array_key; char *prompt, *value, *text; struct cmd_find_state fs; @@ -1143,25 +1151,26 @@ window_customize_set_option(struct client *c, else if (scope != WINDOW_CUSTOMIZE_SERVER) space = ", global"; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (idx == -1) { + if (array_key == NULL) { xasprintf(&prompt, "(%s[+]%s%s) ", name, space, text); } else { - xasprintf(&prompt, "(%s[%d]%s%s) ", name, idx, - space, text); + xasprintf(&prompt, "(%s[%s]%s%s) ", name, + array_key, space, text); } } else xasprintf(&prompt, "(%s%s%s) ", name, space, text); free(text); - value = options_to_string(o, idx, 0); + value = options_to_string(o, array_key, 0); new_item = xcalloc(1, sizeof *new_item); new_item->data = data; new_item->scope = scope; new_item->oo = oo; new_item->name = xstrdup(name); - new_item->idx = idx; + if (array_key != NULL) + new_item->array_key = xstrdup(array_key); data->references++; mode_tree_set_prompt(data->data, c, prompt, value, @@ -1174,6 +1183,84 @@ window_customize_set_option(struct client *c, } } +static enum prompt_result +window_customize_set_array_key_callback(struct client *c, void *itemdata, + const char *s, __unused enum prompt_key_result key) +{ + struct window_customize_itemdata *item = itemdata; + struct window_customize_modedata *data; + struct options_entry *o; + const char *name, *array_key; + char *value, *cause; + + if (item == NULL) + return (PROMPT_CLOSE); + data = item->data; + if (s == NULL || *s == '\0' || data->dead) + return (PROMPT_CLOSE); + name = item->name; + array_key = item->array_key; + if (array_key == NULL || !window_customize_check_item(data, item, NULL)) + return (PROMPT_CLOSE); + + o = options_get(item->oo, name); + if (o == NULL) + return (PROMPT_CLOSE); + if (options_array_get(o, s) != NULL) + return (PROMPT_CLOSE); + + value = options_to_string(o, array_key, 0); + if (options_array_set(o, s, value, 0, &cause) != 0) + goto fail; + free(value); + + options_array_set(o, array_key, NULL, 0, NULL); + options_push_changes(item->name); + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + return (PROMPT_CLOSE); + +fail: + free(value); + *cause = toupper((u_char)*cause); + status_message_set(c, -1, 1, 0, 0, "%s", cause); + free(cause); + return (PROMPT_CLOSE); +} + +static void +window_customize_set_array_key(struct client *c, + struct window_customize_modedata *data, + struct window_customize_itemdata *item) +{ + struct window_customize_itemdata *new_item; + char *prompt; + + if (item == NULL || + item->array_key == NULL || + !window_customize_check_item(data, item, NULL)) + return; + + xasprintf(&prompt, "(%s[%s]) ", item->name, item->array_key); + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->scope = item->scope; + new_item->oo = item->oo; + new_item->name = xstrdup(item->name); + new_item->array_key = xstrdup(item->array_key); + + data->references++; + mode_tree_set_prompt(data->data, c, prompt, item->array_key, + PROMPT_TYPE_COMMAND, PROMPT_NOFORMAT, + window_customize_set_array_key_callback, + window_customize_free_item_callback, new_item); + + free(prompt); +} + static void window_customize_unset_option(struct window_customize_modedata *data, struct window_customize_itemdata *item) @@ -1186,9 +1273,10 @@ window_customize_unset_option(struct window_customize_modedata *data, o = options_get(item->oo, item->name); if (o == NULL) return; - if (item->idx != -1 && item == mode_tree_get_current(data->data)) + if (item->array_key != NULL && + item == mode_tree_get_current(data->data)) mode_tree_up(data->data, 0); - options_remove_or_default(o, item->idx, NULL); + options_remove_or_default(o, item->array_key, NULL); } static void @@ -1200,14 +1288,14 @@ window_customize_reset_option(struct window_customize_modedata *data, if (item == NULL || !window_customize_check_item(data, item, NULL)) return; - if (item->idx != -1) + if (item->array_key != NULL) return; oo = item->oo; while (oo != NULL) { o = options_get_only(item->oo, item->name); if (o != NULL) - options_remove_or_default(o, -1, NULL); + options_remove_or_default(o, NULL, NULL); oo = options_get_parent(oo); } } @@ -1448,7 +1536,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, struct window_pane *wp = wme->wp; struct window_customize_modedata *data = wme->data; struct window_customize_itemdata *item, *new_item; - int finished, idx; + int finished; char *prompt; u_int tagged; @@ -1458,6 +1546,11 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, item = new_item; switch (key) { + case 'a': + if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + break; + window_customize_set_array_key(c, data, item); + break; case '\r': case 's': if (item == NULL) @@ -1486,7 +1579,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, mode_tree_build(data->data); break; case 'd': - if (item == NULL || item->idx != -1) + if (item == NULL || item->array_key != NULL) break; xasprintf(&prompt, "Reset %s to default? ", item->name); data->references++; @@ -1515,10 +1608,10 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, case 'u': if (item == NULL) break; - idx = item->idx; - if (idx != -1) - xasprintf(&prompt, "Unset %s[%d]? ", item->name, idx); - else + if (item->array_key != NULL) { + xasprintf(&prompt, "Unset %s[%s]? ", item->name, + item->array_key); + } else xasprintf(&prompt, "Unset %s? ", item->name); data->references++; data->change = WINDOW_CUSTOMIZE_UNSET; diff --git a/window-switch.c b/window-switch.c index 8157175e1..f3de15411 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-switch.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-tree.c b/window-tree.c index 878d050b8..35b7e68e0 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-tree.c,v 1.93 2026/07/04 22:09:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott @@ -326,10 +326,11 @@ window_tree_build_window(struct session *s, struct winlink *wl, struct window_tree_itemdata *item; struct mode_tree_item *mti; char *name, *text; - struct window_pane *wp, **l; - u_int n, i; + struct window_pane **l; + u_int n, i, found; int expanded; struct format_tree *ft; + uint64_t tag = FORMAT_NONE; item = window_tree_add_item(data); item->type = WINDOW_TREE_WINDOW; @@ -337,7 +338,9 @@ window_tree_build_window(struct session *s, struct winlink *wl, item->winlink = wl->idx; item->pane = -1; - ft = format_create(NULL, NULL, FORMAT_PANE|wl->window->active->id, 0); + if (wl->window != NULL && wl->window->active != NULL) + tag = FORMAT_PANE|wl->window->active->id; + ft = format_create(NULL, NULL, tag, 0); format_defaults(ft, NULL, s, wl, NULL); text = format_expand(ft, data->format); xasprintf(&name, "%u", wl->idx); @@ -354,29 +357,23 @@ window_tree_build_window(struct session *s, struct winlink *wl, free(name); mode_tree_align(mti, 1); - if ((wp = TAILQ_FIRST(&wl->window->panes)) == NULL) - goto empty; - if (TAILQ_NEXT(wp, entry) == NULL) { - if (!window_tree_filter_pane(s, wl, wp, filter)) - goto empty; - } - l = sort_get_panes_window(wl->window, &n, sort_crit); - if (n == 0) - goto empty; + found = 0; for (i = 0; i < n; i++) { + if (!window_tree_filter_pane(s, wl, l[i], filter)) + continue; + found++; if (data->hide_preview_this_pane && l[i] == data->wp) continue; - if (window_tree_filter_pane(s, wl, l[i], filter)) - window_tree_build_pane(s, wl, l[i], modedata, mti); + window_tree_build_pane(s, wl, l[i], modedata, mti); + } + if (found == 0) { + window_tree_free_item(item); + data->item_size--; + mode_tree_remove(data->data, mti); + return (0); } return (1); - -empty: - window_tree_free_item(item); - data->item_size--; - mode_tree_remove(data->data, mti); - return (0); } static void @@ -391,6 +388,7 @@ window_tree_build_session(struct session *s, void *modedata, u_int n, i, empty; int expanded; struct format_tree *ft; + uint64_t tag = FORMAT_NONE; item = window_tree_add_item(data); item->type = WINDOW_TREE_SESSION; @@ -398,7 +396,9 @@ window_tree_build_session(struct session *s, void *modedata, item->winlink = -1; item->pane = -1; - ft = format_create(NULL, NULL, FORMAT_PANE|wl->window->active->id, 0); + if (wl != NULL && wl->window != NULL && wl->window->active != NULL) + tag = FORMAT_PANE|wl->window->active->id; + ft = format_create(NULL, NULL, tag, 0); format_defaults(ft, NULL, s, NULL, NULL); text = format_expand(ft, data->format); format_free(ft); diff --git a/window-visible.c b/window-visible.c index f46967ec7..360e4fc88 100644 --- a/window-visible.c +++ b/window-visible.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-visible.c,v 1.4 2026/06/29 19:03:34 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/window.c b/window.c index 1fb012389..8b26c7d20 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window.c,v 1.360 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -71,9 +71,10 @@ struct window_pane_input_data { static struct window_pane *window_pane_create(struct window *, u_int, u_int, u_int); static void window_pane_destroy(struct window_pane *); +static void window_pane_free(struct window_pane *); static void window_pane_scrollbar_timer(int, short, void *); -static void window_pane_full_size_offset(struct window_pane *wp, - int *xoff, int *yoff, u_int *sx, u_int *sy); +static void window_pane_full_size_offset(struct window_pane *, int *, int *, + u_int *, u_int *); RB_GENERATE(windows, window, entry, window_cmp); RB_GENERATE(winlinks, winlink, entry, winlink_cmp); @@ -85,6 +86,7 @@ struct window_pane_prompt { status_prompt_input_cb inputcb; prompt_free_cb freecb; void *data; + enum prompt_type type; }; int @@ -93,6 +95,78 @@ window_cmp(struct window *w1, struct window *w2) return (w1->id - w2->id); } +static void +window_fire_renamed(struct window *w, const char *old_name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_string(ep, "old_name", "%s", old_name); + event_payload_set_string(ep, "new_name", "%s", w->name); + events_fire("window-renamed", ep); +} + +static void +window_fire_pane_changed(struct window *w, struct window_pane *wp, + struct window_pane *lastwp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_pane(ep, "new_pane", wp); + if (lastwp != NULL) + event_payload_set_pane(ep, "old_pane", lastwp); + events_fire("window-pane-changed", ep); +} + +static void +window_fire_pane_mode_changed(const char *name, struct window_pane *wp, + const char *previous, const char *current, int entered) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + + if (current != NULL) + event_payload_set_string(ep, "current_mode", "%s", current); + if (previous != NULL) + event_payload_set_string(ep, "previous_mode", "%s", previous); + event_payload_set_int(ep, "mode_entered", entered); + + events_fire(name, ep); +} + +static void +window_fire_pane_prompt(const char *name, struct window_pane *wp, + enum prompt_type type) +{ + struct event_payload *ep; + struct cmd_find_state fs; + const char *type_string = prompt_type_string(type); + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "prompt_type", "%s", type_string); + events_fire(name, ep); +} + int winlink_cmp(struct winlink *wl1, struct winlink *wl2) { @@ -331,6 +405,8 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel) w->ypixel = ypixel; w->options = options_create(global_w_options); + w->sb = options_get_number(w->options, "pane-scrollbars"); + w->sb_pos = options_get_number(w->options, "pane-scrollbars-position"); w->references = 0; TAILQ_INIT(&w->winlinks); @@ -419,16 +495,37 @@ window_remove_ref(struct window *w, const char *from) window_destroy(w); } +void +window_pane_add_ref(struct window_pane *wp, const char *from) +{ + wp->references++; + log_debug("%s: %%%u %s, now %d", __func__, wp->id, from, + wp->references); +} + +void +window_pane_remove_ref(struct window_pane *wp, const char *from) +{ + wp->references--; + log_debug("%s: %%%u %s, now %d", __func__, wp->id, from, + wp->references); + + if (wp->references == 0) + window_pane_free(wp); +} + void window_set_name(struct window *w, const char *new_name, int untrusted) { - char *name; + char *last, *name; name = clean_name(new_name, untrusted); if (name != NULL) { + last = xstrdup(w->name); free(w->name); w->name = name; - notify_window("window-renamed", w); + window_fire_renamed(w, last); + free(last); } } @@ -538,13 +635,13 @@ window_pane_update_focus(struct window_pane *wp) log_debug("%s: %%%u focus out", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[O", 3); - notify_pane("pane-focus-out", wp); + events_fire_pane("pane-focus-out", wp); wp->flags &= ~PANE_FOCUSED; } else if (focused && (~wp->flags & PANE_FOCUSED)) { log_debug("%s: %%%u focus in", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[I", 3); - notify_pane("pane-focus-in", wp); + events_fire_pane("pane-focus-in", wp); wp->flags |= PANE_FOCUSED; } else log_debug("%s: %%%u focus unchanged", __func__, wp->id); @@ -580,7 +677,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) server_redraw_window(w); if (notify) - notify_window("window-pane-changed", w); + window_fire_pane_changed(w, w->active, lastwp); return (1); } @@ -770,7 +867,8 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); w->flags |= WINDOW_ZOOMED; - notify_window("window-layout-changed", w); + events_fire_window("window-zoomed", w); + events_fire_window("window-layout-changed", w); redraw_invalidate_scene(w); return (0); @@ -796,8 +894,10 @@ window_unzoom(struct window *w, int notify) } layout_fix_panes(w, NULL); - if (notify) - notify_window("window-layout-changed", w); + if (notify) { + events_fire_window("window-unzoomed", w); + events_fire_window("window-layout-changed", w); + } redraw_invalidate_scene(w); return (0); @@ -879,7 +979,7 @@ window_lost_pane(struct window *w, struct window_pane *wp) if (w->active != NULL) { window_pane_stack_remove(&w->last_panes, w->active); w->active->flags |= PANE_CHANGED; - notify_window("window-pane-changed", w); + events_fire_window("window-pane-changed", w); window_update_focus(w); } } @@ -1080,9 +1180,11 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) char host[HOST_NAME_MAX + 1]; wp = xcalloc(1, sizeof *wp); + wp->references = 1; wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; + wp->cmd_status = -1; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp); @@ -1207,11 +1309,12 @@ window_pane_destroy(struct window_pane *wp) window_pane_wait_finish(wp); spawn_editor_finish(wp); + RB_REMOVE(window_pane_tree, &all_window_panes, wp); + wp->flags |= PANE_DESTROYED; window_pane_clear_prompt(wp); window_pane_free_modes(wp); screen_write_clear_dirty(wp); - free(wp->searchstr); if (wp->fd != -1) { #ifdef HAVE_UTEMPTER @@ -1219,18 +1322,20 @@ window_pane_destroy(struct window_pane *wp) kill(getpid(), SIGCHLD); #endif bufferevent_free(wp->event); + wp->event = NULL; close(wp->fd); + wp->fd = -1; } - if (wp->ictx != NULL) + if (wp->ictx != NULL) { input_free(wp->ictx); - - screen_free(&wp->status_screen); - - screen_free(&wp->base); + wp->ictx = NULL; + } if (wp->pipe_fd != -1) { bufferevent_free(wp->pipe_event); + wp->pipe_event = NULL; close(wp->pipe_fd); + wp->pipe_fd = -1; } if (event_initialized(&wp->resize_timer)) @@ -1241,7 +1346,18 @@ window_pane_destroy(struct window_pane *wp) event_del(&wp->sb_auto_timer); window_pane_clear_resizes(wp, NULL); - RB_REMOVE(window_pane_tree, &all_window_panes, wp); + window_pane_remove_ref(wp, __func__); +} + +static void +window_pane_free(struct window_pane *wp) +{ + log_debug("pane %%%u freed (%d references)", wp->id, wp->references); + + free(wp->searchstr); + + screen_free(&wp->status_screen); + screen_free(&wp->base); options_free(wp->options); free((void *)wp->cwd); @@ -1326,6 +1442,8 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) { struct window_mode_entry *wme; struct window_pane_resize *r; + struct event_payload *ep; + struct cmd_find_state fs; if (sx == wp->sx && sy == wp->sy) return; @@ -1348,6 +1466,17 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) wme->mode->resize(wme, sx, sy); + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_uint(ep, "width", sx); + event_payload_set_uint(ep, "height", sy); + event_payload_set_uint(ep, "old_width", r->osx); + event_payload_set_uint(ep, "old_height", r->osy); + events_fire("pane-resized", ep); } int @@ -1357,9 +1486,13 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, { struct window_mode_entry *wme; struct window *w = wp->window; + const char *name = mode->name, *p = NULL; - if (!TAILQ_EMPTY(&wp->modes) && TAILQ_FIRST(&wp->modes)->mode == mode) - return (1); + if (!TAILQ_EMPTY(&wp->modes)) { + if (TAILQ_FIRST(&wp->modes)->mode == mode) + return (1); + p = TAILQ_FIRST(&wp->modes)->mode->name; + } TAILQ_FOREACH(wme, &wp->modes, entry) { if (wme->mode == mode) @@ -1385,7 +1518,9 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-entered", wp, p, name, 1); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 1); return (0); } @@ -1396,11 +1531,13 @@ window_pane_reset_mode(struct window_pane *wp) struct window_mode_entry *wme, *next; struct window *w = wp->window; int kill; + const char *name, *p; if (TAILQ_EMPTY(&wp->modes)) return; wme = TAILQ_FIRST(&wp->modes); + p = wme->mode->name; kill = wme->kill; TAILQ_REMOVE(&wp->modes, wme, entry); wme->mode->free(wme); @@ -1417,13 +1554,16 @@ window_pane_reset_mode(struct window_pane *wp) if (next->mode->resize != NULL) next->mode->resize(next, wp->sx, wp->sy); } + name = (next == NULL ? NULL : next->mode->name); wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); layout_fix_panes(w, NULL); server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-exited", wp, p, name, 0); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 0); if (kill) server_kill_pane(wp); @@ -1486,6 +1626,7 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wpp->inputcb = inputcb; wpp->freecb = freecb; wpp->data = data; + wpp->type = type; memset(&pd, 0, sizeof pd); prompt_set_options(&pd, s); @@ -1503,19 +1644,28 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wp->flags |= PANE_REDRAW; prompt_incremental_start(wp->prompt); + window_fire_pane_prompt("pane-prompt-opened", wp, type); } /* Close a pane prompt. */ void window_pane_clear_prompt(struct window_pane *wp) { - struct prompt *prompt = wp->prompt; + struct prompt *prompt = wp->prompt; + struct window_pane_prompt *wpp = wp->prompt_data; + enum prompt_type type = PROMPT_TYPE_INVALID; - if (prompt == NULL) - return; - wp->prompt = NULL; - prompt_free(prompt); - wp->flags |= PANE_REDRAW; + if (prompt != NULL) { + if (wpp != NULL) + type = wpp->type; + + wp->prompt = NULL; + prompt_free(prompt); + wp->flags |= PANE_REDRAW; + + if (~wp->flags & PANE_DESTROYED) + window_fire_pane_prompt("pane-prompt-closed", wp, type); + } } /* Does this pane have an open prompt? */ @@ -2386,7 +2536,6 @@ window_pane_get_theme(struct window_pane *wp) { struct window *w; struct client *loop; - enum client_theme theme; int found_light = 0, found_dark = 0; if (wp == NULL) @@ -2394,14 +2543,9 @@ window_pane_get_theme(struct window_pane *wp) w = wp->window; /* - * Derive theme from pane background color, if it's not the default - * colour. + * Prefer a theme reported by an attached client with mode 2031 or DSR + * 996: the terminal knows its own light or dark mode. */ - theme = colour_totheme(window_pane_get_bg(wp)); - if (theme != THEME_UNKNOWN) - return (theme); - - /* Try to find a client that has a theme. */ TAILQ_FOREACH(loop, &clients, entry) { if (loop->flags & CLIENT_UNATTACHEDFLAGS) continue; @@ -2418,12 +2562,16 @@ window_pane_get_theme(struct window_pane *wp) break; } } - if (found_dark && !found_light) return (THEME_DARK); if (found_light && !found_dark) return (THEME_LIGHT); - return (THEME_UNKNOWN); + + /* + * Otherwise guess from the pane background colour, for terminals which + * do not report a theme themselves. + */ + return (colour_totheme(window_pane_get_bg(wp))); } void diff --git a/xmalloc.c b/xmalloc.c index 14e805ed9..c6a2f6f13 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: xmalloc.c,v 1.15 2026/07/12 20:16:20 nicm Exp $ */ /* * Author: Tatu Ylonen @@ -32,8 +32,7 @@ xmalloc(size_t size) fatalx("xmalloc: zero size"); ptr = malloc(size); if (ptr == NULL) - fatalx("xmalloc: allocating %zu bytes: %s", - size, strerror(errno)); + fatal("xmalloc: allocating %zu bytes", size); return ptr; } @@ -44,10 +43,11 @@ xcalloc(size_t nmemb, size_t size) if (size == 0 || nmemb == 0) fatalx("xcalloc: zero size"); + if (SIZE_MAX / nmemb < size) + fatalx("xcalloc: nmemb * size > SIZE_MAX"); ptr = calloc(nmemb, size); if (ptr == NULL) - fatalx("xcalloc: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xcalloc: allocating %zu bytes", size * nmemb); return ptr; } @@ -66,8 +66,7 @@ xreallocarray(void *ptr, size_t nmemb, size_t size) fatalx("xreallocarray: zero size"); new_ptr = reallocarray(ptr, nmemb, size); if (new_ptr == NULL) - fatalx("xreallocarray: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xreallocarray: allocating %zu bytes", size * nmemb); return new_ptr; } @@ -80,8 +79,7 @@ xrecallocarray(void *ptr, size_t oldnmemb, size_t nmemb, size_t size) fatalx("xrecallocarray: zero size"); new_ptr = recallocarray(ptr, oldnmemb, nmemb, size); if (new_ptr == NULL) - fatalx("xrecallocarray: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xrecallocarray: allocating %zu bytes", size * nmemb); return new_ptr; } @@ -91,7 +89,7 @@ xstrdup(const char *str) char *cp; if ((cp = strdup(str)) == NULL) - fatalx("xstrdup: %s", strerror(errno)); + fatal("xstrdup"); return cp; } @@ -101,7 +99,7 @@ xstrndup(const char *str, size_t maxlen) char *cp; if ((cp = strndup(str, maxlen)) == NULL) - fatalx("xstrndup: %s", strerror(errno)); + fatal("xstrndup"); return cp; } @@ -126,7 +124,6 @@ xasprintf(char **ret, const char *fmt, ...) va_start(ap, fmt); i = xvasprintf(ret, fmt, ap); va_end(ap); - return i; } @@ -136,10 +133,8 @@ xvasprintf(char **ret, const char *fmt, va_list ap) int i; i = vasprintf(ret, fmt, ap); - - if (i == -1) - fatalx("xasprintf: %s", strerror(errno)); - + if (i == -1 || *ret == NULL) + fatal("xvasprintf"); return i; } @@ -152,7 +147,6 @@ xsnprintf(char *str, size_t len, const char *fmt, ...) va_start(ap, fmt); i = xvsnprintf(str, len, fmt, ap); va_end(ap); - return i; } @@ -163,11 +157,8 @@ xvsnprintf(char *str, size_t len, const char *fmt, va_list ap) if (len > INT_MAX) fatalx("xsnprintf: len > INT_MAX"); - i = vsnprintf(str, len, fmt, ap); - if (i < 0 || i >= (int)len) fatalx("xsnprintf: overflow"); - return i; } diff --git a/xmalloc.h b/xmalloc.h index a87f053bf..f64d3f734 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: xmalloc.h,v 1.5 2026/06/18 10:56:22 nicm Exp $ */ /* * Author: Tatu Ylonen