diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml new file mode 100644 index 000000000..96cee88b4 --- /dev/null +++ b/.github/workflows/regress.yml @@ -0,0 +1,87 @@ +name: 'Run Tests' + +on: + workflow_dispatch: + schedule: + - cron: '33 3 * * *' + +permissions: + contents: read + +concurrency: + group: tmux-tests + cancel-in-progress: true + +jobs: + regress: + name: ${{ matrix.name }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + include: + - name: ubuntu-24.04-x64 + runner: ubuntu-24.04 + make: make + 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 + + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + autoconf \ + automake \ + bison \ + build-essential \ + libevent-dev \ + libncurses-dev \ + libutf8proc-dev \ + pkg-config + + - name: dependencies + if: runner.os == 'macOS' + run: | + brew install \ + autoconf \ + automake \ + bison \ + libevent \ + make \ + ncurses \ + utf8proc \ + pkg-config + + - name: build + run: | + sh autogen.sh + ./configure ${{ matrix.configure }} + ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" + + - name: test + run: | + cd regress + export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" + ${{ 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/CHANGES b/CHANGES index 172a8d533..7b1a2a54d 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,8 @@ -CHANGES FROM 3.7 to 3.7a +CHANGES FROM 3.7a TO 3.7b + +* Fix so that the end of a synchronized update again triggers a redraw. + +CHANGES FROM 3.7 TO 3.7a * Fix crash in break-pane when no name is provided. diff --git a/Makefile.am b/Makefile.am index eeb24f821..d985e510d 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) @@ -178,6 +179,7 @@ dist_tmux_SOURCES = \ log.c \ menu.c \ mode-tree.c \ + monitor.c \ names.c \ notify.c \ options-table.c \ diff --git a/attributes.c b/attributes.c index 8eaa8897b..b0c9ef518 100644 --- a/attributes.c +++ b/attributes.c @@ -45,7 +45,7 @@ attributes_tostring(int attr) (attr & GRID_ATTR_UNDERSCORE_3) ? "curly-underscore," : "", (attr & GRID_ATTR_UNDERSCORE_4) ? "dotted-underscore," : "", (attr & GRID_ATTR_UNDERSCORE_5) ? "dashed-underscore," : "", - (attr & GRID_ATTR_OVERLINE) ? "overline," : "", + (attr & GRID_ATTR_OVERLINE) ? "overline," : "", (attr & GRID_ATTR_NOATTR) ? "noattr," : ""); if (len > 0) buf[len - 1] = '\0'; diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 7eb371c25..3891534a0 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -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); diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index 7820f3ce7..c3729cc45 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -42,8 +42,8 @@ const struct cmd_entry cmd_capture_pane_entry = { .name = "capture-pane", .alias = "capturep", - .args = { "ab:CeE:FHJLMNpPqS:Tt:", 0, 0, NULL }, - .usage = "[-aCeFHJLMNpPqT] " CMD_BUFFER_USAGE " [-E end-line] " + .args = { "ab:CeE:FHJLMNpPqRS:Tt:", 0, 0, NULL }, + .usage = "[-aCeFHJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] " "[-S start-line] " CMD_TARGET_PANE_USAGE, .target = { 't', CMD_FIND_PANE, 0 }, @@ -75,6 +75,96 @@ cmd_capture_pane_append(char *buf, size_t *len, const char *line, return (buf); } +static char * +cmd_capture_pane_cell(struct screen *s, u_int xx, u_int yy) +{ + struct grid *gd = s->grid; + struct hyperlinks *hl = s->hyperlinks; + struct grid_cell gc; + char *line, *data, *link, *linkid, *f, *b, *u; + char c[UTF8_SIZE + 1]; + const char *uri, *iid; + u_int flags; + + grid_get_cell(gd, xx, yy, &gc); + + memcpy(c, gc.data.data, gc.data.size); + c[gc.data.size] = '\0'; + utf8_stravis(&data, c, VIS_OCTAL|VIS_CSTYLE|VIS_TAB|VIS_NL); + + if (gc.link != 0 && hyperlinks_get(hl, gc.link, &uri, &iid, NULL)) { + xasprintf(&link, "%s", uri); + if (iid != NULL && *iid != '\0') + xasprintf(&linkid, "%s", iid); + else + xasprintf(&linkid, "NONE"); + } else { + xasprintf(&link, "NONE"); + xasprintf(&linkid, "NONE"); + } + + flags = gc.flags; + if (gc.fg & COLOUR_FLAG_256) + flags |= GRID_FLAG_FG256; + if (gc.bg & COLOUR_FLAG_256) + flags |= GRID_FLAG_BG256; + + xasprintf(&f, "%s[%x]", colour_tostring(gc.fg), gc.fg); + xasprintf(&b, "%s[%x]", colour_tostring(gc.bg), gc.bg); + xasprintf(&u, "%s[%x]", colour_tostring(gc.us), gc.us); + + xasprintf(&line, "\t\tC %u,%u data=(%u,%u,%s) flags=%s[%x] " + "attr=%s[%x] fg=%s bg=%s us=%s link=%s linkid=%s\n", + yy, xx, gc.data.width, gc.data.size, data, + grid_cell_flags_string(flags), flags, + grid_cell_attr_string(gc.attr), gc.attr, f, b, u, link, linkid); + + free(f); + free(b); + free(u); + free(link); + free(linkid); + free(data); + return (line); +} + +static char * +cmd_capture_pane_grid(struct window_pane *wp, size_t *len) +{ + struct screen *s = &wp->base; + struct grid *gd = s->grid; + struct grid_line *gl; + char *buf = xstrdup(""), *line; + char p[11]; + u_int yy, xx, total = gd->hsize + gd->sy; + + xasprintf(&line, "G %ux%u (%u/%u)\n", gd->sx, gd->sy, gd->hsize, + gd->hlimit); + buf = cmd_capture_pane_append(buf, len, line, strlen(line)); + free(line); + + for (yy = 0; yy < total; yy++) { + gl = grid_get_line(gd, yy); + if (yy < gd->hsize) + snprintf(p, sizeof p, "-"); + else + snprintf(p, sizeof p, "%u", yy - gd->hsize); + xasprintf(&line, "\tL %u (%s) flags=%s[%x] %u/%u\n", yy, + p, grid_line_flags_string(gl->flags), gl->flags, + gl->cellused, gl->cellsize); + buf = cmd_capture_pane_append(buf, len, line, strlen(line)); + free(line); + + for (xx = 0; xx < gd->sx; xx++) { + line = cmd_capture_pane_cell(s, xx, yy); + buf = cmd_capture_pane_append(buf, len, line, + strlen(line)); + free(line); + } + } + return (buf); +} + static char * cmd_capture_pane_pending(struct args *args, struct window_pane *wp, size_t *len) @@ -323,7 +413,9 @@ cmd_capture_pane_exec(struct cmd *self, struct cmdq_item *item) } len = 0; - if (args_has(args, 'P') && !args_has(args, 'H')) + if (args_has(args, 'R')) + buf = cmd_capture_pane_grid(wp, &len); + else if (args_has(args, 'P') && !args_has(args, 'H')) buf = cmd_capture_pane_pending(args, wp, &len); else buf = cmd_capture_pane_history(args, item, wp, &len); diff --git a/cmd-display-menu.c b/cmd-display-menu.c index e1766fc2e..22d4bd503 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -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-find.c b/cmd-find.c index d2f902516..555e5f1f0 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -1011,7 +1011,7 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item, strcmp(target, "{active}") == 0 || strcmp(target, "{current}") == 0) { c = cmdq_get_client(item); - if (c == NULL) { + if (c == NULL || c->session == NULL) { cmdq_error(item, "no current client"); goto error; } diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 17d110745..314fc396f 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -52,7 +52,7 @@ const struct cmd_entry cmd_move_pane_entry = { .name = "move-pane", .alias = "movep", - .args = { "bdfhMvl:L::P:R::s:t:U::X:Y:z:", 0, 0, NULL }, + .args = { "bdD::fhMvl:L::P:R::s:t:U::X:Y:z:", 0, 0, NULL }, .usage = "[-bdfhMv] [-D lines] [-l size] [-L columns] [-P position] " "[-R columns] " CMD_SRCDST_PANE_USAGE " [-U lines] " "[-X x-position] [-Y y-position] [-z z-index]", @@ -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,9 +181,9 @@ 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); @@ -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,9 +252,9 @@ 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); 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); @@ -363,6 +364,45 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl, 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); + notify_window("window-layout-changed", w); + server_redraw_window(w); + + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -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); } diff --git a/cmd-list-keys.c b/cmd-list-keys.c index abb51b2c8..7aa38c61e 100644 --- a/cmd-list-keys.c +++ b/cmd-list-keys.c @@ -232,7 +232,7 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item) n = 1; ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); - format_defaults(ft, NULL, NULL, NULL, NULL); + format_defaults(ft, tc, NULL, NULL, NULL); format_add(ft, "notes_only", "%d", notes_only); format_add(ft, "key_has_repeat", "%d", key_bindings_has_repeat(l, n)); format_add(ft, "key_string_width", "%u", cmd_list_keys_get_width(l, n)); diff --git a/cmd-new-window.c b/cmd-new-window.c index bb6113fd6..6e6d70ccc 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -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')) + sc.flags |= SPAWN_EMPTY; if (args_has(args, 'd')) sc.flags |= SPAWN_DETACHED; if (args_has(args, 'k')) diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 13fcbb1dd..6f22da83e 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -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-resize-pane.c b/cmd-resize-pane.c index 348923528..3ce805115 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -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-respawn-pane.c b/cmd-respawn-pane.c index 1e1dd4c6c..bf058a267 100644 --- a/cmd-respawn-pane.c +++ b/cmd-respawn-pane.c @@ -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..e67757e1b 100644 --- a/cmd-respawn-window.c +++ b/cmd-respawn-window.c @@ -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-set-option.c b/cmd-set-option.c index fbe32cfae..1b6de1e86 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -31,6 +31,8 @@ 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_monitor_exec(struct cmdq_item *, + struct args *, int); const struct cmd_entry cmd_set_option_entry = { .name = "set-option", @@ -62,8 +64,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 = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -72,14 +75,100 @@ 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_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; + char *cause = NULL, *name = NULL, *format = NULL; + char *expanded = NULL, *newvalue = NULL; + const char *value, *old; + enum monitor_type type; + int id, scope; + + 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')) { + notify_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); + } + } + + notify_monitor_add(item, oo, name, type, id, format, &fs, target->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,13 +178,19 @@ 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, '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)); @@ -107,8 +202,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) 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 +235,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 +267,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 +278,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 +289,7 @@ 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)) { + } 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 +305,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 +313,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 +327,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-options.c b/cmd-show-options.c index 56f8178f9..cd15c80d8 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -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 = notify_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-split-window.c b/cmd-split-window.c index 604618b80..4963d40f0 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -81,7 +81,7 @@ 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 cmd_find_state fs; int input, empty, is_floating, flags = 0; @@ -167,6 +167,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; } @@ -219,10 +224,6 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) 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 +270,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..7f5160e65 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -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); diff --git a/colour.c b/colour.c index 0db080fc4..f563fd5aa 100644 --- a/colour.c +++ b/colour.c @@ -290,6 +290,58 @@ colour_tostring(int c) return ("invalid"); } +/* Convert colour to an SGR escape sequence. */ +const char * +colour_toescape(struct client *c, int colour, int bg) +{ + static char s[32]; + u_char r, g, b; + int n, flags = (TERM_256COLOURS|TERM_RGBCOLOURS); + u_int o = (bg ? 40 : 30); + + if (c != NULL && (c->tty.flags & TTY_OPENED) && c->tty.term != NULL) + flags = c->tty.term->flags; + + if (colour & COLOUR_FLAG_THEME) { + n = colour & 0xff; + if (c != NULL && (u_int)n < COLOUR_THEME_COUNT) + colour = c->theme_colours[n]; + else + colour = colour_theme_terminal_colour(n); + } + + if (colour == 8 || colour == 9) { + xsnprintf(s, sizeof s, "\033[%dm", o + 9); + return (s); + } + + if ((~flags & TERM_RGBCOLOURS) & (colour & COLOUR_FLAG_RGB)) { + colour_split_rgb(colour, &r, &g, &b); + colour = colour_find_rgb(r, g, b); + } + if ((~flags & TERM_256COLOURS) & (colour & COLOUR_FLAG_256)) + colour = colour_256to16(colour); + + if (colour & COLOUR_FLAG_RGB) { + colour_split_rgb(colour, &r, &g, &b); + xsnprintf(s, sizeof s, "\033[%d;2;%u;%u;%um", o + 8, r, g, b); + return (s); + } + if (colour & COLOUR_FLAG_256) { + xsnprintf(s, sizeof s, "\033[%d;5;%um", o + 8, colour & 0xff); + return (s); + } + if (colour >= 0 && colour <= 7) { + xsnprintf(s, sizeof s, "\033[%dm", colour + o); + return (s); + } + if (colour >= 90 && colour <= 97) { + xsnprintf(s, sizeof s, "\033[%dm", colour + o - 30); + return (s); + } + return (NULL); +} + /* Convert background colour to theme. */ enum client_theme colour_totheme(int c) @@ -1242,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) @@ -1260,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/configure.ac b/configure.ac index e2a5f169c..6abb2d9d5 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) diff --git a/control.c b/control.c index b88072d36..5886b54f5 100644 --- a/control.c +++ b/control.c @@ -75,42 +75,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 +87,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 +117,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) @@ -766,6 +660,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) @@ -783,7 +701,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); @@ -832,20 +750,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); @@ -854,313 +768,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. */ @@ -1168,11 +783,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/format.c b/format.c index 8d8dfec94..8953f65d2 100644 --- a/format.c +++ b/format.c @@ -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 *); @@ -121,6 +122,11 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2) #define FORMAT_CLIENT_TERMCAP 0x1000000 #define FORMAT_CLIENT_TERMFEAT 0x2000000 #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 /* Limit on recursion. */ #define FORMAT_LOOP_LIMIT 100 @@ -876,6 +882,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) @@ -2304,7 +2341,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); } @@ -2859,6 +2896,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) @@ -3039,6 +3089,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) @@ -3529,6 +3592,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 }, @@ -3745,6 +3811,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 }, @@ -4025,6 +4097,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,7 +4223,7 @@ format_relative_time(time_t t) /* 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; @@ -4136,25 +4231,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; } @@ -4243,6 +4339,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); @@ -4453,7 +4554,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/ @@ -4472,7 +4573,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, break; /* Check single character modifiers with no arguments. */ - if (strchr("labcdnwETSWPL!<>", cp[0]) != NULL && + if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL && format_is_end(cp[1])) { format_add_modifier(&list, count, cp, 1, NULL, 0); cp++; @@ -4494,7 +4595,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s, } /* Now try single character with arguments. */ - if (strchr("ImCLNPSst=pReqW", cp[0]) == NULL) + if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL) break; c = cp[0]; @@ -4771,11 +4872,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); @@ -4818,9 +4922,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; @@ -4840,7 +4944,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); @@ -4853,20 +4957,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); } @@ -4880,33 +4985,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); @@ -4963,13 +5073,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); @@ -4988,6 +5100,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) @@ -5012,14 +5389,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); } @@ -5189,12 +5568,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; @@ -5272,6 +5653,12 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, break; case 'c': modifiers |= FORMAT_COLOUR; + if (fm->argc < 1) + break; + if (strchr(fm->argv[0], 'f') != NULL) + modifiers |= FORMAT_COLOUR_ESC_FG; + if (strchr(fm->argv[0], 'b') != NULL) + modifiers |= FORMAT_COLOUR_ESC_BG; break; case 'd': modifiers |= FORMAT_DIRNAME; @@ -5307,6 +5694,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; @@ -5373,11 +5762,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) { @@ -5470,11 +5875,28 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen, /* Is this a colour? */ if (modifiers & FORMAT_COLOUR) { new = format_expand1(es, copy); - c = colour_fromstring(new); - if (c == -1 || (c = colour_force_rgb(c)) == -1) - value = xstrdup(""); - else - xasprintf(&value, "%06x", c & 0xffffff); + if (modifiers & (FORMAT_COLOUR_ESC_FG|FORMAT_COLOUR_ESC_BG)) { + if (strcasecmp(new, "none") == 0) + value = xstrdup("\033[0m"); + else if ((c = colour_fromstring(new)) == -1) + value = xstrdup(""); + else { + if (modifiers & FORMAT_COLOUR_ESC_BG) + cp = colour_toescape(ft->c, c, 1); + else + cp = colour_toescape(ft->c, c, 0); + if (cp == NULL) + value = xstrdup(""); + else + value = xstrdup(cp); + } + } else { + c = colour_fromstring(new); + if (c == -1 || (c = colour_force_rgb(c)) == -1) + value = xstrdup(""); + else + xasprintf(&value, "%06x", c & 0xffffff); + } free(new); goto done; } @@ -5496,6 +5918,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/grid.c b/grid.c index 28680cbdd..d03352f08 100644 --- a/grid.c +++ b/grid.c @@ -60,23 +60,37 @@ static const struct grid_cell_entry grid_cleared_entry = { }; #ifdef __APPLE__ -static void -grid_check_lines(struct grid *gd) +void +grid_check_is_clear(struct grid *gd) { - u_int i, j; + struct grid_line *gl; + u_int yy, ny; - 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); - } + assert(gd != NULL); + + if (gd->sy == 0) { + assert(gd->linedata == NULL); + return; + } + + assert(gd->linedata != NULL); + + ny = gd->hsize + gd->sy; + for (yy = 0; yy < ny; yy++) { + gl = &gd->linedata[yy]; + + assert(gl->celldata == NULL); + assert(gl->cellused == 0); + assert(gl->cellsize == 0); + assert(gl->extddata == NULL); + assert(gl->extdsize == 0); + assert(gl->flags == 0); + assert(gl->time == 0); } } #else -static void -grid_check_lines(__unused struct grid *gd) +void +grid_check_is_clear(__unused struct grid *gd) { } #endif @@ -341,28 +355,18 @@ grid_create(u_int sx, u_int sy, u_int hlimit) { struct grid *gd; - gd = xmalloc(sizeof *gd); + gd = xcalloc(1, sizeof *gd); gd->sx = sx; gd->sy = sy; if (hlimit != 0) gd->flags = GRID_HISTORY; - else - gd->flags = 0; - - gd->hscrolled = 0; - gd->hsize = 0; gd->hlimit = hlimit; - gd->scroll_added = 0; - gd->scroll_collected = 0; - gd->scroll_generation = 0; - if (gd->sy != 0) gd->linedata = xcalloc(gd->sy, sizeof *gd->linedata); - else - gd->linedata = NULL; + grid_check_is_clear(gd); return (gd); } @@ -482,8 +486,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. */ @@ -533,8 +535,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. */ @@ -796,8 +796,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. */ @@ -1287,8 +1285,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. */ @@ -1585,8 +1581,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. */ @@ -1688,3 +1682,98 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) return (0); return (utf8_cstrhas(set, &gc.data)); } + +/* Line flags to string. */ +const char * +grid_line_flags_string(int flags) +{ + static char s[128]; + + *s = '\0'; + if (flags & GRID_LINE_WRAPPED) + strlcat(s, "WRAPPED,", sizeof s); + if (flags & GRID_LINE_EXTENDED) + strlcat(s, "EXTENDED,", sizeof s); + if (flags & GRID_LINE_DEAD) + strlcat(s, "DEAD,", sizeof s); + if (flags & GRID_LINE_START_PROMPT) + strlcat(s, "START_PROMPT,", sizeof s); + if (flags & GRID_LINE_START_OUTPUT) + strlcat(s, "START_OUTPUT,", sizeof s); + if (flags & GRID_LINE_HYPERLINK) + strlcat(s, "HYPERLINK,", sizeof s); + if (*s == '\0') + return ("NONE"); + s[strlen(s) - 1] = '\0'; + return (s); +} + +/* Cell flags to string. */ +const char * +grid_cell_flags_string(int flags) +{ + static char s[128]; + + *s = '\0'; + if (flags & GRID_FLAG_FG256) + strlcat(s, "FG256,", sizeof s); + if (flags & GRID_FLAG_BG256) + strlcat(s, "BG256,", sizeof s); + if (flags & GRID_FLAG_PADDING) + strlcat(s, "PADDING,", sizeof s); + if (flags & GRID_FLAG_EXTENDED) + strlcat(s, "EXTENDED,", sizeof s); + if (flags & GRID_FLAG_SELECTED) + strlcat(s, "SELECTED,", sizeof s); + if (flags & GRID_FLAG_CLEARED) + strlcat(s, "CLEARED,", sizeof s); + if (flags & GRID_FLAG_TAB) + strlcat(s, "TAB,", sizeof s); + if (flags & GRID_FLAG_NOPALETTE) + strlcat(s, "NOPALETTE,", sizeof s); + if (*s == '\0') + return ("NONE"); + s[strlen(s) - 1] = '\0'; + return (s); +} + +/* Cell attributes to string. */ +const char * +grid_cell_attr_string(int attr) +{ + static char s[256]; + + *s = '\0'; + if (attr & GRID_ATTR_CHARSET) + strlcat(s, "CHARSET,", sizeof s); + if (attr & GRID_ATTR_BRIGHT) + strlcat(s, "BRIGHT,", sizeof s); + if (attr & GRID_ATTR_DIM) + strlcat(s, "DIM,", sizeof s); + if (attr & GRID_ATTR_UNDERSCORE) + strlcat(s, "UNDERSCORE,", sizeof s); + if (attr & GRID_ATTR_BLINK) + strlcat(s, "BLINK,", sizeof s); + if (attr & GRID_ATTR_REVERSE) + strlcat(s, "REVERSE,", sizeof s); + if (attr & GRID_ATTR_HIDDEN) + strlcat(s, "HIDDEN,", sizeof s); + if (attr & GRID_ATTR_ITALICS) + strlcat(s, "ITALICS,", sizeof s); + if (attr & GRID_ATTR_STRIKETHROUGH) + strlcat(s, "STRIKETHROUGH,", sizeof s); + if (attr & GRID_ATTR_UNDERSCORE_2) + strlcat(s, "UNDERSCORE_2,", sizeof s); + if (attr & GRID_ATTR_UNDERSCORE_3) + strlcat(s, "UNDERSCORE_3,", sizeof s); + if (attr & GRID_ATTR_UNDERSCORE_4) + strlcat(s, "UNDERSCORE_4,", sizeof s); + if (attr & GRID_ATTR_UNDERSCORE_5) + strlcat(s, "UNDERSCORE_5,", sizeof s); + if (attr & GRID_ATTR_OVERLINE) + strlcat(s, "OVERLINE,", sizeof s); + if (*s == '\0') + return ("NONE"); + s[strlen(s) - 1] = '\0'; + return (s); +} diff --git a/input.c b/input.c index 5f7f9d132..a85b555f8 100644 --- a/input.c +++ b/input.c @@ -2818,7 +2818,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 { diff --git a/key-bindings.c b/key-bindings.c index e5a58a94e..c73fd7c33 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -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); @@ -360,13 +388,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 }", @@ -407,10 +436,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 }", @@ -441,6 +468,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 " }", @@ -479,6 +528,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/layout-custom.c b/layout-custom.c index 0fc3f5d16..65fb3780b 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -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,10 +262,10 @@ 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); + layout_free_cell(w->layout_root, 0); w->layout_root = tiled_lc; /* Assign the panes into the cells. */ @@ -291,7 +291,7 @@ layout_parse(struct window *w, const char *layout, char **cause) return (0); fail: - layout_free_cell(tiled_lc); + layout_free_cell(tiled_lc, 0); return (-1); } @@ -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); } @@ -423,6 +423,6 @@ layout_construct(struct layout_cell *lcparent, const char **layout, return (0); fail: - layout_free_cell(*lc); + layout_free_cell(*lc, 0); return (-1); } diff --git a/layout-set.c b/layout-set.c index 157133904..2f164b01c 100644 --- a/layout-set.c +++ b/layout-set.c @@ -124,12 +124,12 @@ layout_set_previous(struct window *w) } static struct window_pane * -layout_first_tiled(struct window *w) +layout_set_first_tiled(struct window *w) { struct window_pane *wp; TAILQ_FOREACH(wp, &w->panes, entry) { - if (!window_pane_is_floating(wp)) + if (wp->layout_cell && layout_cell_is_tiled(wp->layout_cell)) return (wp); } return (NULL); @@ -139,19 +139,15 @@ static void layout_set_even(struct window *w, enum layout_type type) { struct window_pane *wp; - struct layout_cell *lc, *lcnew; + struct layout_cell *lcroot, *lcchild; u_int n, sx, sy; layout_print_cell(w->layout_root, __func__, 1); - /* Get number of panes. */ n = window_count_panes(w, 0); if (n <= 1) return; - /* Free the old root and construct a new. */ - layout_free(w); - lc = w->layout_root = layout_create_cell(NULL); if (type == LAYOUT_LEFTRIGHT) { sx = (n * (PANE_MINIMUM + 1)) - 1; if (sx < w->sx) @@ -163,30 +159,30 @@ layout_set_even(struct window *w, enum layout_type type) sy = w->sy; sx = w->sx; } - layout_set_size(lc, sx, sy, 0, 0); - layout_make_node(lc, type); - /* Build new leaf cells. */ + layout_free(w, 1); + lcroot = w->layout_root = layout_create_cell(NULL); + layout_set_size(lcroot, sx, sy, 0, 0); + layout_make_node(lcroot, type); + TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_is_floating(wp)) - continue; - lcnew = layout_create_cell(lc); - layout_make_leaf(lcnew, wp); - lcnew->sx = w->sx; - lcnew->sy = w->sy; - TAILQ_INSERT_TAIL(&lc->cells, lcnew, entry); + lcchild = wp->layout_cell; + TAILQ_INSERT_TAIL(&lcroot->cells, lcchild, entry); + lcchild->parent = lcroot; + if (layout_cell_is_tiled(lcchild)) { + lcchild->g.sx = w->sx; + lcchild->g.sy = w->sy; + } } - /* Spread out cells. */ - layout_spread_cell(w, lc); + layout_spread_cell(w, lcroot); - /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lc->sx, lc->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -206,15 +202,14 @@ layout_set_even_v(struct window *w) static void layout_set_main_h(struct window *w) { - struct window_pane *wp; - struct layout_cell *lc, *lcmain, *lcother, *lcchild; + struct window_pane *wp, *wpmain; + struct layout_cell *lcroot, *lcmain, *lcother, *lcchild; u_int n, mainh, otherh, sx, sy; char *cause; const char *s; layout_print_cell(w->layout_root, __func__, 1); - /* Get number of panes. */ n = window_count_panes(w, 0); if (n <= 1) return; @@ -255,52 +250,48 @@ layout_set_main_h(struct window *w) if (sx < w->sx) sx = w->sx; - /* Free old tree and create a new root. */ - layout_free(w); - lc = w->layout_root = layout_create_cell(NULL); - layout_set_size(lc, sx, mainh + otherh + 1, 0, 0); - layout_make_node(lc, LAYOUT_TOPBOTTOM); + layout_free(w, 1); + lcroot = w->layout_root = layout_create_cell(NULL); + layout_set_size(lcroot, sx, mainh + otherh + 1, 0, 0); + layout_make_node(lcroot, LAYOUT_TOPBOTTOM); - /* Create the main pane. */ - lcmain = layout_create_cell(lc); + wpmain = layout_set_first_tiled(w); + lcmain = wpmain->layout_cell; + lcmain->parent = lcroot; layout_set_size(lcmain, sx, mainh, 0, 0); - layout_make_leaf(lcmain, layout_first_tiled(w)); - TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry); + TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry); - /* Create the other pane. */ - lcother = layout_create_cell(lc); - layout_set_size(lcother, sx, otherh, 0, 0); if (n == 1) { - wp = TAILQ_NEXT(layout_first_tiled(w), entry); - while (wp != NULL && window_pane_is_floating(wp)) + wp = TAILQ_NEXT(wpmain, entry); + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) wp = TAILQ_NEXT(wp, entry); - layout_make_leaf(lcother, wp); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry); + wp->layout_cell->parent = lcroot; } else { + lcother = layout_create_cell(lcroot); + layout_set_size(lcother, sx, otherh, 0, 0); layout_make_node(lcother, LAYOUT_LEFTRIGHT); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + TAILQ_INSERT_TAIL(&lcroot->cells, lcother, entry); - /* Add the remaining panes as children. */ TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_is_floating(wp)) + if (wp == wpmain) continue; - if (wp == layout_first_tiled(w)) - continue; - lcchild = layout_create_cell(lcother); - layout_set_size(lcchild, PANE_MINIMUM, otherh, 0, 0); - layout_make_leaf(lcchild, wp); + lcchild = wp->layout_cell; TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry); + lcchild->parent = lcother; + if (layout_cell_is_tiled(lcchild)) + layout_set_size(lcchild, PANE_MINIMUM, otherh, + 0, 0); } layout_spread_cell(w, lcother); } - /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lc->sx, lc->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -308,15 +299,14 @@ layout_set_main_h(struct window *w) static void layout_set_main_h_mirrored(struct window *w) { - struct window_pane *wp; - struct layout_cell *lc, *lcmain, *lcother, *lcchild; + struct window_pane *wp, *wpmain; + struct layout_cell *lcroot, *lcmain, *lcother, *lcchild; u_int n, mainh, otherh, sx, sy; char *cause; const char *s; layout_print_cell(w->layout_root, __func__, 1); - /* Get number of panes. */ n = window_count_panes(w, 0); if (n <= 1) return; @@ -357,52 +347,48 @@ layout_set_main_h_mirrored(struct window *w) if (sx < w->sx) sx = w->sx; - /* Free old tree and create a new root. */ - layout_free(w); - lc = w->layout_root = layout_create_cell(NULL); - layout_set_size(lc, sx, mainh + otherh + 1, 0, 0); - layout_make_node(lc, LAYOUT_TOPBOTTOM); + layout_free(w, 1); + lcroot = w->layout_root = layout_create_cell(NULL); + layout_set_size(lcroot, sx, mainh + otherh + 1, 0, 0); + layout_make_node(lcroot, LAYOUT_TOPBOTTOM); + + wpmain = layout_set_first_tiled(w); + lcmain = wpmain->layout_cell; + lcmain->parent = lcroot; + layout_set_size(lcmain, sx, mainh, 0, 0); + TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry); - /* Create the other pane. */ - lcother = layout_create_cell(lc); - layout_set_size(lcother, sx, otherh, 0, 0); if (n == 1) { - wp = TAILQ_NEXT(layout_first_tiled(w), entry); - while (wp != NULL && window_pane_is_floating(wp)) + wp = TAILQ_NEXT(wpmain, entry); + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) wp = TAILQ_NEXT(wp, entry); - layout_make_leaf(lcother, wp); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry); + wp->layout_cell->parent = lcroot; } else { + lcother = layout_create_cell(lcroot); + layout_set_size(lcother, sx, otherh, 0, 0); layout_make_node(lcother, LAYOUT_LEFTRIGHT); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + TAILQ_INSERT_HEAD(&lcroot->cells, lcother, entry); - /* Add the remaining panes as children. */ TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_is_floating(wp)) + if (wp == wpmain) continue; - if (wp == layout_first_tiled(w)) - continue; - lcchild = layout_create_cell(lcother); - layout_set_size(lcchild, PANE_MINIMUM, otherh, 0, 0); - layout_make_leaf(lcchild, wp); + lcchild = wp->layout_cell; TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry); + lcchild->parent = lcother; + if (layout_cell_is_tiled(lcchild)) + layout_set_size(lcchild, PANE_MINIMUM, otherh, + 0, 0); } layout_spread_cell(w, lcother); } - /* Create the main pane. */ - lcmain = layout_create_cell(lc); - layout_set_size(lcmain, sx, mainh, 0, 0); - layout_make_leaf(lcmain, layout_first_tiled(w)); - TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry); - - /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lc->sx, lc->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -410,21 +396,20 @@ layout_set_main_h_mirrored(struct window *w) static void layout_set_main_v(struct window *w) { - struct window_pane *wp; - struct layout_cell *lc, *lcmain, *lcother, *lcchild; + struct window_pane *wp, *wpmain; + struct layout_cell *lcroot, *lcmain, *lcother, *lcchild; u_int n, mainw, otherw, sx, sy; char *cause; const char *s; layout_print_cell(w->layout_root, __func__, 1); - /* Get number of panes. */ n = window_count_panes(w, 0); if (n <= 1) return; n--; /* take off main pane */ - /* Find available width - take off one line for the border. */ + /* Find available width - take off one column for the border. */ sx = w->sx - 1; /* Get the main pane width. */ @@ -459,52 +444,48 @@ layout_set_main_v(struct window *w) if (sy < w->sy) sy = w->sy; - /* Free old tree and create a new root. */ - layout_free(w); - lc = w->layout_root = layout_create_cell(NULL); - layout_set_size(lc, mainw + otherw + 1, sy, 0, 0); - layout_make_node(lc, LAYOUT_LEFTRIGHT); + layout_free(w, 1); + lcroot = w->layout_root = layout_create_cell(NULL); + layout_set_size(lcroot, mainw + otherw + 1, sy, 0, 0); + layout_make_node(lcroot, LAYOUT_LEFTRIGHT); - /* Create the main pane. */ - lcmain = layout_create_cell(lc); + wpmain = layout_set_first_tiled(w); + lcmain = wpmain->layout_cell; + lcmain->parent = lcroot; layout_set_size(lcmain, mainw, sy, 0, 0); - layout_make_leaf(lcmain, layout_first_tiled(w)); - TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry); + TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry); - /* Create the other pane. */ - lcother = layout_create_cell(lc); - layout_set_size(lcother, otherw, sy, 0, 0); if (n == 1) { - wp = TAILQ_NEXT(layout_first_tiled(w), entry); - while (wp != NULL && window_pane_is_floating(wp)) + wp = TAILQ_NEXT(wpmain, entry); + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) wp = TAILQ_NEXT(wp, entry); - layout_make_leaf(lcother, wp); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry); + wp->layout_cell->parent = lcroot; } else { + lcother = layout_create_cell(lcroot); layout_make_node(lcother, LAYOUT_TOPBOTTOM); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + layout_set_size(lcother, otherw, sy, 0, 0); + TAILQ_INSERT_TAIL(&lcroot->cells, lcother, entry); - /* Add the remaining panes as children. */ TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_is_floating(wp)) + if (wp == wpmain) continue; - if (wp == layout_first_tiled(w)) - continue; - lcchild = layout_create_cell(lcother); - layout_set_size(lcchild, otherw, PANE_MINIMUM, 0, 0); - layout_make_leaf(lcchild, wp); + lcchild = wp->layout_cell; TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry); + lcchild->parent = lcother; + if (layout_cell_is_tiled(lcchild)) + layout_set_size(lcchild, otherw, PANE_MINIMUM, + 0, 0); } layout_spread_cell(w, lcother); } - /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lc->sx, lc->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } @@ -512,8 +493,8 @@ layout_set_main_v(struct window *w) static void layout_set_main_v_mirrored(struct window *w) { - struct window_pane *wp; - struct layout_cell *lc, *lcmain, *lcother, *lcchild; + struct window_pane *wp, *wpmain; + struct layout_cell *lcroot, *lcmain, *lcother, *lcchild; u_int n, mainw, otherw, sx, sy; char *cause; const char *s; @@ -526,7 +507,7 @@ layout_set_main_v_mirrored(struct window *w) return; n--; /* take off main pane */ - /* Find available width - take off one line for the border. */ + /* Find available width - take off one column for the border. */ sx = w->sx - 1; /* Get the main pane width. */ @@ -561,62 +542,58 @@ layout_set_main_v_mirrored(struct window *w) if (sy < w->sy) sy = w->sy; - /* Free old tree and create a new root. */ - layout_free(w); - lc = w->layout_root = layout_create_cell(NULL); - layout_set_size(lc, mainw + otherw + 1, sy, 0, 0); - layout_make_node(lc, LAYOUT_LEFTRIGHT); + layout_free(w, 1); + lcroot = w->layout_root = layout_create_cell(NULL); + layout_set_size(lcroot, mainw + otherw + 1, sy, 0, 0); + layout_make_node(lcroot, LAYOUT_LEFTRIGHT); + + wpmain = layout_set_first_tiled(w); + lcmain = wpmain->layout_cell; + lcmain->parent = lcroot; + layout_set_size(lcmain, mainw, sy, 0, 0); + TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry); - /* Create the other pane. */ - lcother = layout_create_cell(lc); - layout_set_size(lcother, otherw, sy, 0, 0); if (n == 1) { - wp = TAILQ_NEXT(layout_first_tiled(w), entry); - while (wp != NULL && window_pane_is_floating(wp)) + wp = TAILQ_NEXT(wpmain, entry); + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) wp = TAILQ_NEXT(wp, entry); - layout_make_leaf(lcother, wp); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry); + wp->layout_cell->parent = lcroot; } else { + lcother = layout_create_cell(lcroot); layout_make_node(lcother, LAYOUT_TOPBOTTOM); - TAILQ_INSERT_TAIL(&lc->cells, lcother, entry); + layout_set_size(lcother, otherw, sy, 0, 0); + TAILQ_INSERT_HEAD(&lcroot->cells, lcother, entry); - /* Add the remaining panes as children. */ TAILQ_FOREACH(wp, &w->panes, entry) { - if (window_pane_is_floating(wp)) + if (wp == wpmain) continue; - if (wp == layout_first_tiled(w)) - continue; - lcchild = layout_create_cell(lcother); - layout_set_size(lcchild, otherw, PANE_MINIMUM, 0, 0); - layout_make_leaf(lcchild, wp); + lcchild = wp->layout_cell; TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry); + lcchild->parent = lcother; + if (layout_cell_is_tiled(lcchild)) + layout_set_size(lcchild, otherw, PANE_MINIMUM, + 0, 0); } layout_spread_cell(w, lcother); } - /* Create the main pane. */ - lcmain = layout_create_cell(lc); - layout_set_size(lcmain, mainw, sy, 0, 0); - layout_make_leaf(lcmain, layout_first_tiled(w)); - TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry); - - /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lc->sx, lc->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } -void +static void layout_set_tiled(struct window *w) { struct options *oo = w->options; struct window_pane *wp; - struct layout_cell *lc, *lcrow, *lcchild; + struct layout_cell *lcroot, *lcrow, *lcchild; u_int n, width, height, used, sx, sy; u_int i, j, columns, rows, max_columns; @@ -647,56 +624,59 @@ layout_set_tiled(struct window *w) if (height < PANE_MINIMUM) height = PANE_MINIMUM; - /* Free old tree and create a new root. */ - layout_free(w); - lc = w->layout_root = layout_create_cell(NULL); sx = ((width + 1) * columns) - 1; if (sx < w->sx) sx = w->sx; sy = ((height + 1) * rows) - 1; if (sy < w->sy) sy = w->sy; - layout_set_size(lc, sx, sy, 0, 0); - layout_make_node(lc, LAYOUT_TOPBOTTOM); - /* Create a grid of the cells, skipping any floating panes. */ + layout_free(w, 1); + lcroot = w->layout_root = layout_create_cell(NULL); + layout_set_size(lcroot, sx, sy, 0, 0); + layout_make_node(lcroot, LAYOUT_TOPBOTTOM); + + /* Create a grid of the tiled cells. */ wp = TAILQ_FIRST(&w->panes); - while (wp != NULL && window_pane_is_floating(wp)) - wp = TAILQ_NEXT(wp, entry); for (j = 0; j < rows; j++) { + while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell)) + wp = TAILQ_NEXT(wp, entry); /* If this is the last cell, all done. */ if (wp == NULL) break; - /* Create the new row. */ - lcrow = layout_create_cell(lc); - layout_set_size(lcrow, w->sx, height, 0, 0); - TAILQ_INSERT_TAIL(&lc->cells, lcrow, entry); + lcchild = wp->layout_cell; /* If only one column, just use the row directly. */ if (n - (j * columns) == 1 || columns == 1) { - layout_make_leaf(lcrow, wp); + lcchild->parent = lcroot; + TAILQ_INSERT_TAIL(&lcroot->cells, lcchild, entry); + layout_set_size(lcchild, w->sx, height, 0, 0); wp = TAILQ_NEXT(wp, entry); - while (wp != NULL && window_pane_is_floating(wp)) - wp = TAILQ_NEXT(wp, entry); continue; } - /* Add in the columns. */ + /* Create the new row. */ + lcrow = layout_create_cell(lcroot); layout_make_node(lcrow, LAYOUT_LEFTRIGHT); + layout_set_size(lcrow, w->sx, height, 0, 0); + TAILQ_INSERT_TAIL(&lcroot->cells, lcrow, entry); + + /* Add in the columns. */ for (i = 0; i < columns; i++) { /* Create and add a pane cell. */ - lcchild = layout_create_cell(lcrow); - layout_set_size(lcchild, width, height, 0, 0); - layout_make_leaf(lcchild, wp); + lcchild->parent = lcrow; TAILQ_INSERT_TAIL(&lcrow->cells, lcchild, entry); + layout_set_size(lcchild, width, height, 0, 0); /* Move to the next non-floating cell. */ wp = TAILQ_NEXT(wp, entry); - while (wp != NULL && window_pane_is_floating(wp)) + while (wp != NULL && + !layout_cell_is_tiled(wp->layout_cell)) wp = TAILQ_NEXT(wp, entry); if (wp == NULL) break; + lcchild = wp->layout_cell; } /* @@ -713,21 +693,19 @@ layout_set_tiled(struct window *w) w->sx - used); } - /* Adjust the last row height to fit if necessary. */ used = (rows * height) + rows - 1; if (w->sy > used) { - lcrow = TAILQ_LAST(&lc->cells, layout_cells); + lcrow = TAILQ_LAST(&lcroot->cells, layout_cells); layout_resize_adjust(w, lcrow, LAYOUT_TOPBOTTOM, w->sy - used); } - /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); layout_print_cell(w->layout_root, __func__, 1); - window_resize(w, lc->sx, lc->sy, -1, -1); + window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } diff --git a/layout.c b/layout.c index c38197b44..170f3c5fb 100644 --- a/layout.c +++ b/layout.c @@ -56,52 +56,53 @@ 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); } /* Free a layout cell. */ void -layout_free_cell(struct layout_cell *lc) +layout_free_cell(struct layout_cell *lc, int only_nodes) { - struct layout_cell *lcchild; + struct layout_cell *lcchild, *lcnext; - if (lc == NULL) + if (lc == NULL || (only_nodes && lc->type == LAYOUT_WINDOWPANE)) return; switch (lc->type) { case LAYOUT_LEFTRIGHT: case LAYOUT_TOPBOTTOM: - while (!TAILQ_EMPTY(&lc->cells)) { - lcchild = TAILQ_FIRST(&lc->cells); - TAILQ_REMOVE(&lc->cells, lcchild, entry); - layout_free_cell(lcchild); + lcchild = TAILQ_FIRST(&lc->cells); + while (lcchild != NULL) { + lcnext = TAILQ_NEXT(lcchild, entry); + if (!only_nodes || lcchild->type != LAYOUT_WINDOWPANE) { + TAILQ_REMOVE(&lc->cells, lcchild, entry); + layout_free_cell(lcchild, only_nodes); + } + lcchild = lcnext; } break; case LAYOUT_WINDOWPANE: @@ -140,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: @@ -160,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)); } @@ -175,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: @@ -198,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. */ @@ -255,7 +256,7 @@ layout_fix_zindexes(struct window *w, struct layout_cell *lc) } } -static int +int layout_cell_is_tiled(struct layout_cell *lc) { int is_leaf = lc->type == LAYOUT_WINDOWPANE; @@ -327,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; } } } @@ -363,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); } @@ -448,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)) { @@ -531,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 @@ -576,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) @@ -609,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; @@ -618,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; } } @@ -636,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); } @@ -699,29 +707,29 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, if (lcparent == NULL) { if (lc->wp != NULL) *lcroot = NULL; - layout_free_cell(lc); + layout_free_cell(lc, 0); return; } if (!layout_cell_is_tiled(lc)) { TAILQ_REMOVE(&lcparent->cells, lc, entry); - layout_free_cell(lc); + layout_free_cell(lc, 0); goto out; } 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); /* Remove this from the parent's list. */ TAILQ_REMOVE(&lcparent->cells, lc, entry); - layout_free_cell(lc); + layout_free_cell(lc, 0); out: /* @@ -735,14 +743,14 @@ 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 TAILQ_REPLACE(&lc->parent->cells, lcparent, lc, entry); - layout_free_cell(lcparent); + layout_free_cell(lcparent, 0); } } @@ -760,9 +768,9 @@ layout_init(struct window *w, struct window_pane *wp) /* Free layout for pane. */ void -layout_free(struct window *w) +layout_free(struct window *w, int only_nodes) { - layout_free_cell(w->layout_root); + layout_free_cell(w->layout_root, only_nodes); } /* Resize the entire layout after window resize. */ @@ -787,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); @@ -840,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 @@ -873,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); @@ -901,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); @@ -1083,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. */ @@ -1125,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) { @@ -1163,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; @@ -1197,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++; @@ -1230,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 @@ -1249,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) @@ -1288,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; @@ -1335,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)) @@ -1379,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; @@ -1429,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) @@ -1449,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; @@ -1462,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); } @@ -1507,18 +1515,19 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) number = 0; TAILQ_FOREACH (lc, &parent->cells, entry) - number++; + if (layout_cell_is_tiled(lc)) + number++; if (number <= 1) return (0); 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) @@ -1535,9 +1544,11 @@ layout_spread_cell(struct window *w, struct layout_cell *parent) changed = 0; TAILQ_FOREACH (lc, &parent->cells, entry) { + if (!layout_cell_is_tiled(lc)) + continue; change = 0; if (parent->type == LAYOUT_LEFTRIGHT) { - change = each - (int)lc->sx; + change = each - (int)lc->g.sx; if (remainder > 0) { change++; remainder--; @@ -1552,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) @@ -1649,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, @@ -1723,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) { @@ -1735,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; @@ -1748,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); } @@ -1767,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) { @@ -1780,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); } @@ -1792,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); } /* @@ -1809,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; @@ -1828,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 { /* @@ -1839,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); @@ -1848,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/menu.c b/menu.c index 756bb5693..257452fd7 100644 --- a/menu.c +++ b/menu.c @@ -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 cmdq_state *state; @@ -320,6 +321,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); @@ -330,7 +339,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) && @@ -345,7 +354,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)) @@ -558,6 +567,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 3a126129c..59d9e4fa9 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -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..6d1a747fd --- /dev/null +++ b/monitor.c @@ -0,0 +1,658 @@ +/* $OpenBSD$ */ + +/* + * 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; + u_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) + monitor_report(ms, me, s, wl, wp, value, NULL); + return; + } + + if (strcmp(value, *last) == 0) { + free(value); + return; + } + + 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, u_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/notify.c b/notify.c index 8605601cf..07656ccff 100644 --- a/notify.c +++ b/notify.c @@ -27,12 +27,25 @@ struct notify_entry { const char *name; struct cmd_find_state fs; struct format_tree *formats; + struct options *oo; struct client *client; struct session *session; struct window *window; int pane; const char *pbname; + int expand; +}; + +struct notify_monitor { + struct options *oo; + + struct monitor_set *set; + struct cmd_find_state fs; + + enum monitor_type type; + int id; + char *format; }; static struct cmdq_item * @@ -50,7 +63,31 @@ notify_insert_one_hook(struct cmdq_item *item, struct notify_entry *ne, free(s); } new_item = cmdq_get_command(cmdlist, state); - return (cmdq_insert_after(item, new_item)); + if (item != NULL) + return (cmdq_insert_after(item, new_item)); + return (cmdq_append(NULL, new_item)); +} + +static struct cmd_parse_result * +notify_parse_hook(struct notify_entry *ne, struct cmd_find_state *fs, + const char *value) +{ + struct cmd_parse_result *pr; + struct format_tree *ft; + char *expanded; + + if (!ne->expand) + return (cmd_parse_from_string(value, NULL)); + + ft = format_create_defaults(NULL, ne->client, fs->s, fs->wl, fs->wp); + if (ne->formats != NULL) + format_merge(ft, ne->formats); + expanded = format_expand(ft, value); + format_free(ft); + + pr = cmd_parse_from_string(expanded, NULL); + free(expanded); + return (pr); } static void @@ -73,18 +110,23 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) 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; + if (ne->oo != NULL) { + oo = ne->oo; + o = options_get_only(oo, ne->name); + } else { + 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); @@ -96,7 +138,7 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) if (*ne->name == '@') { value = options_get_string(oo, ne->name); - pr = cmd_parse_from_string(value, NULL); + pr = notify_parse_hook(ne, &fs, value); switch (pr->status) { case CMD_PARSE_ERROR: log_debug("%s: can't parse hook %s: %s", __func__, @@ -110,8 +152,24 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) } else { a = options_array_first(o); while (a != NULL) { - cmdlist = options_array_item_value(a)->cmdlist; - item = notify_insert_one_hook(item, ne, cmdlist, state); + if (ne->expand) { + value = options_array_item_value(a)->string; + pr = notify_parse_hook(ne, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + if (pr->error != NULL) + cmdq_error(item, "%s", pr->error); + break; + case CMD_PARSE_SUCCESS: + item = notify_insert_one_hook(item, ne, + pr->cmdlist, state); + break; + } + } else { + cmdlist = options_array_item_value(a)->cmdlist; + item = notify_insert_one_hook(item, ne, cmdlist, + state); + } a = options_array_next(a); } } @@ -175,6 +233,136 @@ notify_callback(struct cmdq_item *item, void *data) return (CMD_RETURN_NORMAL); } +void +notify_monitor_free(void *data) +{ + struct notify_monitor *nhm = data; + + monitor_destroy(nhm->set); + free(nhm->format); + free(nhm); +} + +void +notify_monitor_remove(struct options *oo, const char *name) +{ + struct options_entry *o; + struct notify_monitor *nhm; + + o = options_get_only(oo, name); + if (o == NULL) + return; + + nhm = options_get_monitor_data(o); + if (nhm != NULL) { + options_set_monitor_data(o, NULL); + notify_monitor_free(nhm); + } +} + +static void +notify_monitor_cb(struct monitor_change *change, void *data) +{ + struct notify_monitor *nhm = data; + struct notify_entry ne; + struct cmdq_item *item; + struct window *w; + + item = cmdq_running(NULL); + if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + return; + + memset(&ne, 0, sizeof ne); + ne.name = change->name; + ne.oo = nhm->oo; + ne.client = change->c; + ne.expand = 1; + if (change->wp != NULL && change->wl != NULL) + cmd_find_from_winlink_pane(&ne.fs, change->wl, change->wp, 0); + else if (change->wl != NULL) + cmd_find_from_winlink(&ne.fs, change->wl, 0); + else if (change->s != NULL) + cmd_find_from_session(&ne.fs, change->s, 0); + else + cmd_find_copy_state(&ne.fs, &nhm->fs); + ne.formats = format_create(change->c, item, FORMAT_NONE, FORMAT_NOJOBS); + format_add(ne.formats, "hook", "%s", change->name); + format_add(ne.formats, "hook_value", "%s", change->value); + format_add(ne.formats, "hook_last", "%s", + change->last == NULL ? "" : change->last); + if (change->s != NULL) { + format_add(ne.formats, "hook_session", "$%u", change->s->id); + format_add(ne.formats, "hook_session_name", "%s", change->s->name); + } + if (change->wl != NULL) { + w = change->wl->window; + format_add(ne.formats, "hook_window", "@%u", w->id); + format_add(ne.formats, "hook_window_name", "%s", w->name); + format_add(ne.formats, "hook_window_index", "%d", change->wl->idx); + } + if (change->wp != NULL) { + format_add(ne.formats, "hook_pane", "%%%u", change->wp->id); + } + + notify_insert_hook(item, &ne); + format_free(ne.formats); +} + +void +notify_monitor_add(__unused struct cmdq_item *item, struct options *oo, + const char *name, enum monitor_type type, int id, const char *format, + struct cmd_find_state *fs, struct session *s) +{ + struct options_entry *o; + struct notify_monitor *nhm; + + notify_monitor_remove(oo, name); + o = options_get_only(oo, name); + if (o == NULL) + o = options_set_string(oo, name, 0, "%s", ""); + + nhm = xcalloc(1, sizeof *nhm); + nhm->oo = oo; + cmd_find_copy_state(&nhm->fs, fs); + nhm->type = type; + nhm->id = id; + nhm->format = xstrdup(format); + nhm->set = monitor_create_session(s, notify_monitor_cb, nhm); + options_set_monitor_data(o, nhm); + monitor_add(nhm->set, name, type, id, format, 0); +} + +/* Convert a hook monitor to its value. */ +char * +notify_monitor_to_string(struct options_entry *o) +{ + struct notify_monitor *nhm = options_get_monitor_data(o); + const char *name = options_name(o); + char *s; + + if (nhm == NULL) + return (NULL); + + switch (nhm->type) { + case MONITOR_SESSION: + xasprintf(&s, "%s::%s", name, nhm->format); + break; + case MONITOR_PANE: + xasprintf(&s, "%s:%%%d:%s", name, nhm->id, nhm->format); + break; + case MONITOR_ALL_PANES: + xasprintf(&s, "%s:%%*:%s", name, nhm->format); + break; + case MONITOR_WINDOW: + xasprintf(&s, "%s:@%d:%s", name, nhm->id, nhm->format); + break; + case MONITOR_ALL_WINDOWS: + xasprintf(&s, "%s:@*:%s", name, nhm->format); + break; + } + return (s); +} + 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, diff --git a/options-table.c b/options-table.c index ef047888e..afcfea18a 100644 --- a/options-table.c +++ b/options-table.c @@ -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]" diff --git a/options.c b/options.c index e34320869..3b81ed57d 100644 --- a/options.c +++ b/options.c @@ -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]; u_int i; struct cmd_parse_result *pr; @@ -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); } @@ -354,6 +409,8 @@ options_remove(struct options_entry *o) options_array_clear(o); else options_value_free(o, &o->value); + if (o->monitor_data != NULL) + notify_monitor_free(o->monitor_data); RB_REMOVE(options_tree, &oo->tree, o); free((void *)o->name); free(o); @@ -371,6 +428,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) { @@ -378,21 +447,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); } @@ -402,6 +471,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); } @@ -418,24 +488,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; struct cmd_parse_result *pr; long long number; @@ -445,10 +536,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); } @@ -460,50 +559,56 @@ options_array_set(struct options_entry *o, u_int idx, const char *value, *cause = pr->error; else free(pr->error); + free(new_key); return (-1); case CMD_PARSE_SUCCESS: break; } - 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.cmdlist = pr->cmdlist; + 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); } @@ -512,6 +617,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; @@ -521,10 +627,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') @@ -534,12 +641,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); } @@ -559,13 +667,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 * @@ -587,15 +695,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); @@ -612,7 +721,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)); @@ -621,37 +734,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) @@ -659,6 +776,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); } @@ -675,14 +796,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 == '@') { @@ -703,6 +824,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; @@ -711,19 +834,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; @@ -732,6 +857,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); } @@ -1325,11 +1454,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 || @@ -1337,7 +1467,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/regress/Makefile b/regress/Makefile index e6c3619fc..6370c77c9 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -1,10 +1,42 @@ TESTS!= echo *.sh +LOGDIR=logs -.PHONY: all $(TESTS) -.NOTPARALLEL: all $(TESTS) +.PHONY: all +.NOTPARALLEL: all -all: $(TESTS) - -$(TESTS): - sh $*.sh - sleep 1 +all: + @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); \ + ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \ + env -i LC_CTYPE=C.UTF-8 ASAN_OPTIONS="$$ASAN_OPTIONS" \ + 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; \ + sleep 1; \ + done; \ + if [ "$$failed" -ne 0 ]; then \ + echo; \ + echo "failures:"; \ + for test in $$failures; do \ + echo " $$test"; \ + done; \ + else \ + rmdir "$(LOGDIR)" 2>/dev/null || true; \ + fi; \ + exit $$failed diff --git a/regress/UTF-8-test.txt b/regress/UTF-8-test.txt index a5b5d50e6..ab230089d 100644 Binary files a/regress/UTF-8-test.txt and b/regress/UTF-8-test.txt differ 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 21e185ac1..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() @@ -35,68 +35,59 @@ $TMUX set-option -qg allow-set-title on || exit 1 $TMUX set-option -qg allow-rename on || exit 1 $TMUX set-option -qg automatic-rename off || exit 1 -# Commands reject ':' and '.' for sessions and windows, but allow '#'. -$TMUX rename-session 'session#ok' || fail "session name with # rejected" -must_equal "$($TMUX display-message -p '#{session_name}')" 'session#ok' -must_fail $TMUX rename-session 'session:bad' -must_fail $TMUX rename-session 'session.bad' +# Commands allow empty names, ':', '.', '#' and '#('. +$TMUX rename-session '' || fail "empty session name rejected" +must_equal "$($TMUX display-message -p '#{session_name}')" '' +$TMUX rename-session 'session:.##(ok)' || \ + fail "session name with : . or #( rejected" +must_equal "$($TMUX display-message -p '#{session_name}')" 'session:.#(ok)' -$TMUX rename-window 'window#ok' || fail "window name with # rejected" -must_equal "$($TMUX display-message -p '#{window_name}')" 'window#ok' -must_fail $TMUX rename-window 'window:bad' -must_fail $TMUX rename-window 'window.bad' +$TMUX rename-window '' || fail "empty window name rejected" +must_equal "$($TMUX display-message -p '#{window_name}')" '' +$TMUX rename-window 'window:.##(ok)' || \ + fail "window name with : . or #( rejected" +must_equal "$($TMUX display-message -p '#{window_name}')" 'window:.#(ok)' -$TMUX set-option -q @name 'format#ok' || exit 1 +$TMUX set-option -q @name 'format:.#(ok)' || exit 1 $TMUX rename-session '#{@name}' || fail "format in session name not expanded" -must_equal "$($TMUX display-message -p '#{session_name}')" 'format#ok' +must_equal "$($TMUX display-message -p '#{session_name}')" 'format:.#(ok)' $TMUX rename-window '#{@name}' || fail "format in window name not expanded" -must_equal "$($TMUX display-message -p '#{window_name}')" 'format#ok' -must_fail $TMUX rename-session '#{session_name}:bad' -must_fail $TMUX rename-window '#{window_name}.bad' +must_equal "$($TMUX display-message -p '#{window_name}')" 'format:.#(ok)' +$TMUX set-option -q @name 'format:.#(ok)' || exit 1 pid=$($TMUX display-message -p '#{pid}') created=$($TMUX new-session -dP -F '#{session_id}:#{window_id}' \ - -s 'new-session#ok' -n 'new-window#ok') || \ - fail "new-session name with # rejected" + -s 'new-session:.##(ok)' -n 'new-window:.##(ok)') || \ + fail "new-session name with : . or #( rejected" created_session=${created%:*} created_window=${created#*:} must_equal "$($TMUX display-message -pt "$created_session" '#{session_name}')" \ - 'new-session#ok' + 'new-session:.#(ok)' must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \ - 'new-window#ok' + 'new-window:.#(ok)' $TMUX kill-session -t "$created_session" -must_fail $TMUX new-session -d -s 'new-session:bad' -must_fail $TMUX new-session -d -s 'new-session.bad' -must_fail $TMUX new-session -d -n 'new-window:bad' -must_fail $TMUX new-session -d -n 'new-window.bad' - created_window=$($TMUX new-window -dP -F '#{window_id}' \ - -n 'created-window#ok') || \ - fail "new-window name with # rejected" + -n 'created-window:.##(ok)') || \ + fail "new-window name with : . or #( rejected" must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \ - 'created-window#ok' -must_fail $TMUX new-window -d -n 'created-window:bad' -must_fail $TMUX new-window -d -n 'created-window.bad' + 'created-window:.#(ok)' created=$($TMUX new-session -dP -F '#{session_id}:#{window_id}' \ - -s 'new-session-#{pid}' -n 'new-window-#{pid}') || \ + -s 'new-session-#{pid}:.##(ok)' -n 'new-window-#{pid}:.##(ok)') || \ fail "format in new-session name not expanded" created_session=${created%:*} created_window=${created#*:} must_equal "$($TMUX display-message -pt "$created_session" '#{session_name}')" \ - "new-session-$pid" + "new-session-$pid:.#(ok)" must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \ - "new-window-$pid" + "new-window-$pid:.#(ok)" $TMUX kill-session -t "$created_session" created_window=$($TMUX new-window -dP -F '#{window_id}' -n '#{@name}') || \ fail "format in new-window name not expanded" must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \ - 'format#ok' -must_fail $TMUX new-session -d -s 'new-session-#{pid}:bad' -must_fail $TMUX new-session -d -n 'new-window-#{pid}.bad' -must_fail $TMUX new-window -d -n '#{window_name}:bad' + 'format:.#(ok)' # Invalid UTF-8 is never allowed for command names. invalid=$(printf '\302') @@ -124,9 +115,9 @@ must_equal "$($TMUX list-buffers -F '#{buffer_name}')" 'buffer#:.ok' # Window names from escape sequences allow '#' except in '#('. $TMUX send-keys "printf '\\033kescape#:.ok\\033\\\\'" Enter || exit 1 sleep 1 -must_equal "$($TMUX display-message -p '#{window_name}')" 'escape#__ok' +must_equal "$($TMUX display-message -p '#{window_name}')" 'escape#:.ok' -# Titles from escape sequences reject only '#'. +# Titles from escape sequences allow '#' except in '#('. $TMUX send-keys "printf '\\033]2;escape#:.ok\\007'" Enter || exit 1 sleep 1 must_equal "$($TMUX display-message -p '#{pane_title}')" 'escape#:.ok' 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..9c957246a 100644 --- a/regress/command-order.sh +++ b/regress/command-order.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/conf-syntax.sh b/regress/conf-syntax.sh index 49b9710b3..4dbc384b3 100644 --- a/regress/conf-syntax.sh +++ b/regress/conf-syntax.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 for i in conf/*.conf; do 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-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..e6742608a 100644 --- a/regress/control-client-size.sh +++ b/regress/control-client-size.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/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-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 new file mode 100644 index 000000000..44e4e72c4 --- /dev/null +++ b/regress/environ-update.sh @@ -0,0 +1,126 @@ +#!/bin/sh + +# Tests of update-environment handling (environ_update() in environ.c), which +# runs when a client attaches to a session: for each pattern in the session's +# update-environment option, a matching variable in the attaching client's +# environment is copied into the session environment, and a pattern that +# matches nothing clears that name in the session (a NULL-valued entry). +# +# This needs a real attached client with a controllable environment, so - as in +# format-variables.sh - a second server provides one: an inner "tmux attach" +# runs inside a pane of the second server, and the variables to import are set +# in that inner command's own environment. +# +# environ.sh covers the set-environment/show-environment commands themselves. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +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 -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +# check_value $var $expected +# +# Compare show-environment of $var on the session with $expected. +check_value() +{ + out=$($TMUX show-environment -t main "$1" 2>&1) + if [ "$out" != "$2" ]; then + echo "show-environment $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + cleanup + exit 1 + fi +} + +# 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 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# The session imports MYVAR and ABSENTVAR by exact name and anything matching +# the glob TEST_*; nothing else is imported. +$TMUX set -g update-environment "MYVAR ABSENTVAR TEST_*" || exit 1 + +# Seed the session so the effect of attaching is visible: MYVAR will be +# overwritten by the client's value and ABSENTVAR will be cleared. +$TMUX set-environment -t main MYVAR oldvalue || exit 1 +$TMUX set-environment -t main ABSENTVAR pre-existing || exit 1 + +# --- attach a client whose environment carries the imported variables ------ +# +# MYVAR and TEST_GLOB are present in the inner client's environment; ABSENTVAR +# is deliberately absent; OTHER is present but not named by update-environment. +$TMUX2 new-session -d -x 90 -y 30 \ + "MYVAR=fromclient TEST_GLOB=globval OTHER=nope $TMUX attach -t main" \ + || fail "could not start inner client" +wait_clients 1 || fail "no client attached to test server" + +# MYVAR matched by name and present in the client -> imported (overwrites). +check_value MYVAR "MYVAR=fromclient" +# TEST_GLOB matched by the TEST_* glob and present -> imported. +check_value TEST_GLOB "TEST_GLOB=globval" +# ABSENTVAR named but not in the client environment -> cleared (NULL value, +# printed as -NAME). +check_value ABSENTVAR "-ABSENTVAR" +# OTHER is in the client environment but not named by update-environment, so it +# is not imported at all. +out=$($TMUX show-environment -t main OTHER 2>&1) +[ "$out" = "unknown variable: OTHER" ] || \ + fail "OTHER was imported but should not have been: '$out'" + +# --- -E disables the update-environment import ----------------------------- +# +# Detach the client (kill its host server), reset the session variables, then +# reattach with -E: the session values must be left untouched. +$TMUX2 kill-server 2>/dev/null +wait_clients 0 || fail "client did not detach" + +$TMUX set-environment -t main MYVAR oldvalue2 || exit 1 +$TMUX set-environment -t main ABSENTVAR pre2 || exit 1 + +$TMUX2 new-session -d -x 90 -y 30 \ + "MYVAR=fromclientE $TMUX attach -E -t main" \ + || fail "could not start inner -E client" +wait_clients 1 || fail "no -E client attached to test server" + +# With -E neither variable is touched by the attach. +check_value MYVAR "MYVAR=oldvalue2" +check_value ABSENTVAR "ABSENTVAR=pre2" + +if [ "$($TMUX display-message -p alive)" != "alive" ]; then + fail "server died after update-environment tests" +fi + +cleanup +exit 0 diff --git a/regress/environ.sh b/regress/environ.sh new file mode 100644 index 000000000..2d5c0c031 --- /dev/null +++ b/regress/environ.sh @@ -0,0 +1,187 @@ +#!/bin/sh + +# Tests of the environment engine (environ.c) and its two commands, +# set-environment/setenv (cmd-set-environment.c) and show-environment/showenv +# (cmd-show-environment.c). +# +# The environment is a red-black tree of name/value entries held at two +# scopes: the global environment and each session's environment. An entry +# may be marked hidden (ENVIRON_HIDDEN) or "cleared" (a NULL value, which +# masks an inherited variable rather than removing the entry). This +# exercises: set and show at global and session scope; the shell (-s) output +# form and its escaping of $ ` " and \; hidden variables (-h) and their +# filtering; -r cleared entries printed as -NAME / "unset NAME;"; -u removal; +# -F expansion of the value at set time; the plain "NAME=value" and "%hidden" +# config-file assignment forms (environ_put); and the full set of argument and +# target errors from both commands. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_value $args $expected +# +# Run show-environment with $args and compare the single-line output. +check_value() +{ + out=$($TMUX show-environment $1 2>&1) + if [ "$out" != "$2" ]; then + echo "show-environment $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $cmd... +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 +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# --- set and show at session scope ---------------------------------------- +check_ok set-environment FOO bar +check_value "FOO" "FOO=bar" +# setenv is an alias for set-environment; showenv for show-environment. +check_ok setenv FOO2 bar2 +out=$($TMUX showenv FOO2 2>&1) +[ "$out" = "FOO2=bar2" ] || { echo "setenv/showenv alias failed: '$out'"; exit 1; } + +# --- set and show at global scope ----------------------------------------- +# +# The global scope is separate from the session scope: a session variable is +# not visible in the global environment. +check_ok set-environment -g GVAR gval +check_value "-g GVAR" "GVAR=gval" +check_fail "unknown variable: FOO" show-environment -g FOO + +# --- overwrite replaces the value ----------------------------------------- +check_ok set-environment FOO baz +check_value "FOO" "FOO=baz" + +# --- shell (-s) output form and escaping ---------------------------------- +# +# With -s the value is printed as a shell assignment with export, and the +# characters $ ` " and \ are backslash-escaped (POSIX double-quote rules). +check_ok set-environment ESC 'a$b`c"d\e' +check_value "-s ESC" 'ESC="a\$b\`c\"d\\e"; export ESC;' + +# --- -F expands the value as a format at set time ------------------------- +# +# With a resolvable target the value is expanded once when set; the stored +# value is the result, not the format. +check_ok set-environment -t main -F EXP '#{session_name}' +check_value "EXP" "EXP=main" + +# --- hidden variables (-h) ------------------------------------------------ +# +# set-environment -h marks a variable hidden. show-environment hides it by +# default and only prints it when -h is given; conversely a normal variable is +# omitted when -h is given. +check_ok set-environment -h SECRET s3cr +check_value "SECRET" "" +check_value "-h SECRET" "SECRET=s3cr" +check_value "-h FOO" "" + +# --- -r clears a variable (NULL value, masks inheritance) ----------------- +# +# A cleared entry still exists but has no value: normal form prints "-NAME" +# and shell form prints "unset NAME;". +check_ok set-environment -r FOO +check_value "FOO" "-FOO" +check_value "-s FOO" "unset FOO;" + +# --- -u removes a variable entirely --------------------------------------- +check_ok set-environment -u FOO +check_fail "unknown variable: FOO" show-environment FOO + +# --- show with no name lists every (non-hidden) variable ------------------ +check_ok set-environment -g LISTA 1 +check_ok set-environment -g LISTB 2 +check_ok set-environment -gh LISTHID 3 +out=$($TMUX show-environment -g 2>&1) +echo "$out" | grep -q '^LISTA=1$' || { echo "list missing LISTA"; exit 1; } +echo "$out" | grep -q '^LISTB=2$' || { echo "list missing LISTB"; exit 1; } +# A hidden variable is not listed without -h. +echo "$out" | grep -q '^LISTHID' && { echo "list showed hidden var without -h"; exit 1; } +# With -h only hidden variables are listed. +$TMUX show-environment -gh 2>&1 | grep -q '^LISTHID=3$' || \ + { echo "list -h missing LISTHID"; exit 1; } + +# --- config-file assignment forms (environ_put) --------------------------- +# +# A bare NAME=value line in a config file sets a global variable; a "%hidden" +# NAME=value line sets a hidden one. Start a second server from such a config +# and read the values back. +CONF=$(mktemp) +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) +[ "$out" = "CFGVAR=fromconfig" ] || \ + { echo "config assignment failed: '$out'"; $CTMUX kill-server; rm -f "$CONF"; exit 1; } +out=$($CTMUX show-environment -gh CFGHID 2>&1) +[ "$out" = "CFGHID=hiddencfg" ] || \ + { echo "config %hidden failed: '$out'"; $CTMUX kill-server; rm -f "$CONF"; exit 1; } +# The %hidden variable is hidden from a plain show. +out=$($CTMUX show-environment -g CFGHID 2>&1) +[ "$out" = "" ] || \ + { echo "config %hidden not hidden: '$out'"; $CTMUX kill-server; rm -f "$CONF"; exit 1; } +$CTMUX kill-server 2>/dev/null +rm -f "$CONF" + +# --- set-environment argument errors -------------------------------------- +check_fail "empty variable name" set-environment "" x +check_fail "variable name contains =" set-environment "A=B" x +check_fail "can't specify a value with -u" set-environment -u FOO val +check_fail "can't specify a value with -r" set-environment -r FOO val +check_fail "no value specified" set-environment NOVAL + +# --- show-environment errors ---------------------------------------------- +check_fail "unknown variable: MISSING" show-environment MISSING + +# --- unresolvable target errors ------------------------------------------- +check_fail "no such session: nosuch" show-environment -t nosuch FOO +check_fail "no such session: nosuch" set-environment -t nosuch FOO bar + +assert_alive "after environ tests" + +$TMUX kill-server 2>/dev/null +exit 0 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 new file mode 100644 index 000000000..b6340b622 --- /dev/null +++ b/regress/format-modifiers.sh @@ -0,0 +1,580 @@ +#!/bin/sh + +# Tests of format modifiers as described in tmux(1) FORMATS. +# +# This complements format-strings.sh (which covers escapes, conditionals, +# boolean operators and the l: literal modifier). Here we exercise the +# remaining modifiers: comparisons/matching (m, C, <, >, ==, ...), numeric +# operations (e|op|), width/padding/truncation (=, p, n, w, a, R), basename +# and dirname (b, d), time conversion (t), loops (S, W, P), colour (c) and +# modifier nesting/limits. + +PATH=/bin:/usr/bin +TERM=screen +TZ=UTC +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 -LtestA$$ -f/dev/null" + +ESC=$(printf '\033') + +# test_format $format $expected [$target] +# +# Expand $format with display-message and compare with $expected. If $target +# is given it is passed to display-message with -t. +test_format() +{ + fmt="$1" + exp="$2" + target="$3" + + if [ -n "$target" ]; then + out=$($TMUX display-message -t "$target" -p "$fmt") + else + out=$($TMUX display-message -p "$fmt") + fi + + if [ "$out" != "$exp" ]; then + echo "Format test failed for '$fmt'." + echo "Expected: '$exp'" + echo "But got '$out'" + exit 1 + fi +} + +# test_expand $format $expected +# +# Expand $format in a plain format_expand context (list-windows -F on the +# single-window "tf" session) rather than the format_expand_time context of +# display-message. This matters for t/f: display-message runs the whole format +# through strftime(3), so a strftime specifier there must be doubled (%%H); in a +# format_expand context a single specifier (%H) is applied directly to the +# variable's time. +test_expand() +{ + fmt="$1" + exp="$2" + + out=$($TMUX list-windows -t tf -F "$fmt") + + if [ "$out" != "$exp" ]; then + echo "Format test failed for '$fmt'." + echo "Expected: '$exp'" + echo "But got '$out'" + exit 1 + fi +} + +# assert_alive +# +# Check that the server is still responding (used after operations that could +# in principle crash it, such as recursion and division by zero). +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server did not survive: $1" + exit 1 + fi +} + +$TMUX kill-server 2>/dev/null +sleep 0.1 +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# Single-window session used by test_expand for format_expand-context tests. +$TMUX new-session -d -s tf || exit 1 + +# User options used as inputs. Modifiers operate on variable names, so plain +# literals must be provided via options (or a nested #{l:...}). They are set +# globally (-g) so they are visible from every session, including the "tf" +# session used by test_expand. +$TMUX set -g @s 'abcdefghij' || exit 1 +$TMUX set -g @path '/usr/local/bin/foo' || exit 1 +$TMUX set -g @name 'window-name' || exit 1 +$TMUX set -g @greek 'αβγ' || exit 1 # 6 bytes, 3 columns wide +$TMUX set -g @cjk '中文' || exit 1 # 6 bytes, 4 columns wide +$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 @nl "$(printf 'a\nb')" || exit 1 +q_s_nl=$(printf "'a\nb'") + + +# --- Comparisons and matching -------------------------------------------- + +# m: glob match, first argument is the pattern. +test_format "#{m:*foo*,barfoobar}" "1" +test_format "#{m:*foo*,barbar}" "0" +test_format "#{m:abc,abc}" "1" +# m/i: ignore case. +test_format "#{m/i:*FOO*,barfoobar}" "1" +test_format "#{m/i:*FOO*,barbar}" "0" +# m/r: regular expression. +test_format "#{m/r:^[0-9]+\$,12345}" "1" +test_format "#{m/r:^[0-9]+\$,12a45}" "0" +# m/ri: regular expression, ignore case. +test_format "#{m/ri:^ab+\$,ABBB}" "1" +test_format "#{m/ri:^ab+\$,ACCC}" "0" +# m/z: fuzzy match, returns a boolean. +test_format "#{m/z:foo,foobar}" "1" +test_format "#{m/z:xyz,foobar}" "0" +# m/p: fuzzy match, returns the matched (0-based) column positions. +test_format "#{m/p:ac,abc}" "0,2" +test_format "#{m/p:xyz,abc}" "" +# Fuzzy match against empty text. +test_format "#{m/p:x,}" "" +test_format "#{m/z:x,}" "0" + +# String comparisons. +test_format "#{==:#{@host},myhost}" "1" +test_format "#{==:#{@host},other}" "0" +test_format "#{!=:abc,xyz}" "1" +test_format "#{!=:abc,abc}" "0" +test_format "#{<:3,5}" "1" +test_format "#{<:5,3}" "0" +test_format "#{>:5,3}" "1" +test_format "#{>:3,5}" "0" +test_format "#{<=:5,5}" "1" +test_format "#{<=:6,5}" "0" +test_format "#{>=:5,5}" "1" +test_format "#{>=:4,5}" "0" + +# Negation and canonical boolean. +test_format "#{!:0}" "1" +test_format "#{!:1}" "0" +test_format "#{!!:}" "0" +test_format "#{!!:0}" "0" +test_format "#{!!:non-empty}" "1" + + +# --- Quoting (q) --------------------------------------------------------- + +# 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' +# q/a quotes the value as a single shell argument. +test_format "#{q/a:@sp}" '"a b\$c"' + + +# --- Name existence (N) -------------------------------------------------- + +# N/w is true if a window with the (expanded) name exists in the session, N/s +# if a session with that name exists. The default (no argument) is /w. +$TMUX rename-window -t main:0 knownwin +test_format "#{N/s:main}" "1" +test_format "#{N/s:nosuchsession}" "0" +test_format "#{N/w:knownwin}" "1" "main:" +test_format "#{N/w:nosuchwindow}" "0" "main:" +test_format "#{N:nosuchwindow}" "0" "main:" + + +# --- Numeric operations (e) ---------------------------------------------- + +# Integer operators. +test_format "#{e|+|:2,3}" "5" +test_format "#{e|-|:10,4}" "6" +test_format "#{e|-|:2,5}" "-3" +test_format "#{e|*|:6,7}" "42" +test_format "#{e|/|:20,4}" "5" +# Modulus - both spellings (% must be doubled as it is a strftime specifier). +test_format "#{e|m|:7,3}" "1" +test_format "#{e|%%|:7,3}" "1" + +# Numeric comparison operators. +test_format "#{e|==|:5,5}" "1" +test_format "#{e|!=|:5,5}" "0" +test_format "#{e|<|:2,5}" "1" +test_format "#{e|>|:9,2}" "1" +test_format "#{e|<=|:5,5}" "1" +test_format "#{e|>=|:5,5}" "1" + +# Floating point with a decimal-place count. +test_format "#{e|*|f|4:5.5,3}" "16.5000" +test_format "#{e|/|f|3:1,3}" "0.333" +test_format "#{e|/|f|2:10,3}" "3.33" +# Default number of decimal places for float is two. +test_format "#{e|*|f:2.5,2}" "5.00" + +# Division by zero must not crash the server (result is unspecified). +$TMUX display-message -p "#{e|/|:5,0}" >/dev/null 2>&1 +$TMUX display-message -p "#{e|/|f:5,0}" >/dev/null 2>&1 +assert_alive "division by zero" + + +# --- ASCII and repeat ---------------------------------------------------- + +# a: numeric value to its ASCII character. +test_format "#{a:98}" "b" +test_format "#{a:65}" "A" +# a: out-of-range or non-numeric input yields an empty string. +test_format "#{a:200}" "" +test_format "#{a:notanumber}" "" +# R: repeat first argument second-argument times. +test_format "#{R:a,3}" "aaa" +test_format "#{R:ab,2}" "abab" +# A long repeat exercises output-buffer growth during expansion. +test_format "#{n:#{R:x,300}}" "300" + + +# --- Width, padding and truncation --------------------------------------- + +# =N truncates from the start, =-N from the end. +test_format "#{=5:@s}" "abcde" +test_format "#{=-5:@s}" "fghij" +# = with no width, or a non-numeric width, does not truncate. +test_format "#{=:@s}" "abcdefghij" +test_format "#{=/x:@s}" "abcdefghij" +# A marker is appended/prepended only when trimming actually occurs. +test_format "#{=/5/...:@s}" "abcde..." +test_format "#{=/5/...:@name}" "windo..." +test_format "#{=/20/...:@s}" "abcdefghij" +# Truncation is display-width (UTF-8) aware: a wide (2-column) character is only +# included if it fits entirely within the limit. +test_format "#{=3:@greek}" "αβγ" +test_format "#{=2:@greek}" "αβ" +test_format "#{=2:@cjk}" "中" +test_format "#{=1:@cjk}" "" +# Markers with wide characters: the marker is added when trimming occurs, and a +# limit that splits a wide character drops it entirely. +test_format "#{=/2/x:@cjk}" "中x" +test_format "#{=/1/x:@cjk}" "x" + +# p pads to a width: a positive width left-aligns (pads on the right), a +# negative width right-aligns (pads on the left). +test_format "#{p12:@name}" "window-name " +test_format "#{p-12:@name}" " window-name" +# No padding once the value already meets the width. +test_format "#{p3:@name}" "window-name" +# p with no width does nothing. +test_format "#{p:@name}" "window-name" +# Padding is display-width aware: @cjk is 4 columns wide, so p6/p-6 add two +# spaces (not four). +test_format "#{p6:@cjk}" "中文 " +test_format "#{p-6:@cjk}" " 中文" + +# n is byte length, w is display width. +test_format "#{n:@s}" "10" +test_format "#{w:@s}" "10" +test_format "#{n:@greek}" "6" +test_format "#{w:@greek}" "3" +test_format "#{n:@cjk}" "6" +test_format "#{w:@cjk}" "4" + + +# --- basename and dirname ------------------------------------------------ + +test_format "#{b:@path}" "foo" +test_format "#{d:@path}" "/usr/local/bin" + + +# --- Time conversion ----------------------------------------------------- + +# t: converts an integer time to a ctime(3) string. +test_format "#{t:@ts}" "Sun Sep 9 01:46:40 2001" +# t/p: shorter format for times in the past. +test_format "#{t/p:@ts}" "Sep01" +# t/r: relative time depends on the current time, just check it is non-empty. +if [ -z "$($TMUX display-message -p '#{t/r:@ts}')" ]; then + echo "Format test failed for '#{t/r:@ts}': empty result" + exit 1 +fi + +# 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 +# applied directly. (In display-message, which additionally expands the format +# through strftime, these would need to be doubled - %%Y etc.) The colon in the +# format is escaped as '#:' because it is otherwise the modifier separator. +test_expand "#{t/f/%Y:@ts}" "2001" +test_expand "#{t/f/%Y-%m-%d:@ts}" "2001-09-09" +test_expand "#{t/f/%H#:%M#:%S:@ts}" "01:46:40" +# An escaped comma in the custom format is unescaped before strftime. +test_expand "#{t/f/%Y#,end:@ts}" "2001,end" + +# T: expands its argument and then runs the result through strftime with the +# current time. A value with no strftime specifier is returned unchanged. +test_format "#{T:@ts}" "1000000000" + +# t/p (pretty) and t/r (relative) format times by age relative to now, with a +# different branch per age band. Build options a known number of seconds in the +# past and check each yields a non-empty result (the exact text depends on the +# wall clock, so only non-emptiness is asserted). +now=$(date +%s) +for age in 30 300 4000 90000 200000 3000000 40000000; do + $TMUX set -g @age "$((now - age))" + if [ -z "$($TMUX display-message -p '#{t/r:@age}')" ]; then + echo "Empty #{t/r:@age} for age ${age}s" + exit 1 + fi + if [ -z "$($TMUX display-message -p '#{t/p:@age}')" ]; then + echo "Empty #{t/p:@age} for age ${age}s" + exit 1 + fi +done +# A time in the future has no relative form. +$TMUX set -g @future "$((now + 100000))" +test_format "#{t/r:@future}" "" + + +# --- Content search (C) -------------------------------------------------- + +# Use a window running cat so the content is deterministic (no shell prompt). +$TMUX new-session -d -s search -x 80 -y 10 'cat' +sleep 1 +$TMUX send-keys -t search: 'Zebra_Marker_42' Enter +sleep 1 +# C: returns the (1-based) line number of a match or 0 if not found. +test_format "#{C:Zebra_Marker_42}" "1" "search:" +test_format "#{C:Absent_String_999}" "0" "search:" +test_format "#{C/r:Zebra_.*_42}" "1" "search:" +test_format "#{C/i:zebra_marker_42}" "1" "search:" +$TMUX kill-session -t search 2>/dev/null + + +# --- Colour (c) ---------------------------------------------------------- + +# c: converts a colour to its six-digit hexadecimal RGB value. +test_format "#{c:red}" "800000" +test_format "#{c:colour4}" "000080" +test_format "#{c:#7f7f7f}" "7f7f7f" +# c/f and c/b produce the SGR escape sequence for fg/bg respectively. +test_format "#{c/f:red}" "${ESC}[31m" +test_format "#{c/b:red}" "${ESC}[41m" +test_format "#{c/b:colour4}" "${ESC}[48;5;4m" +# "none" gives a reset; an unknown colour gives an empty string. +test_format "#{c/f:none}" "${ESC}[0m" +test_format "#{c:notacolour}" "" +test_format "#{c/f:notacolour}" "" + + +# --- Nesting and limits -------------------------------------------------- + +# Modifier chaining: inner b: then outer truncation/padding/length. +test_format "#{=5:#{b:@path}}" "foo" +test_format "#{=2:#{b:@path}}" "fo" +test_format "#{p6:#{b:@path}}" "foo " +test_format "#{n:#{b:@path}}" "3" + +# Nested l: literal expanded then truncated. +test_format "#{=5:#{l:abcdefghij}}" "abcde" + +# Deeper nesting: basename -> pad to 10 -> truncate to 5. +test_format "#{=5:#{p10:#{b:@path}}}" "foo " +# A substitution applied to a nested basename. +test_format "#{s/o/O/:#{b:@path}}" "fOO" + +# Unbounded self-recursion must hit the loop limit rather than crash. +$TMUX set @rec '#{E:@rec}' +$TMUX display-message -p '#{E:@rec}' >/dev/null 2>&1 +assert_alive "recursive expansion" + + +# --- Missing, malformed and limit inputs --------------------------------- + +# An undefined variable expands to empty; modifiers on it behave sensibly. +test_format "#{@undefined}" "" +test_format "#{=5:@undefined}" "" +test_format "#{b:@undefined}" "" +test_format "#{n:@undefined}" "0" + +# Malformed numeric expressions expand to empty rather than erroring out. +test_format "#{e|+|:notanumber,2}" "" # invalid left operand +test_format "#{e|+|:2,notanumber}" "" # invalid right operand +test_format "#{e|badop|:1,2}" "" # unknown operator +test_format "#{e|+|f|x:1,2}" "" # invalid precision +test_format "#{e|+|:1}" "" # too few operands +test_format "#{e|+|f|2|extra:1,2}" "" # too many arguments (limit is 3) + +# Repeat with a non-numeric or zero count yields an empty string. +test_format "#{R:a,notanumber}" "" +test_format "#{R:a,0}" "" + +# Comparisons with too few arguments expand to empty. +test_format "#{==:a}" "" +test_format "#{<:a}" "" + +# A substitution with fewer than two arguments is a no-op. +test_format "#{s/onlyone:@s}" "abcdefghij" + +# A non-numeric width for = or p is treated as no width (no change). +test_format "#{=/x:@s}" "abcdefghij" +test_format "#{p/x:@s}" "abcdefghij" + +# The I (client terminal) modifier with no attached client is empty; this also +# exercises its argument parsing (/c termcap, /f feature, default). The +# non-empty terminal cases are covered with a real client in format-variables.sh. +test_format "#{I/c:RGB}" "" +test_format "#{I/f:overline}" "" +test_format "#{I:x}" "" + + +# --- Escaping inside modifiers ------------------------------------------- + +# A "," or "#" inside a modifier argument is escaped with "#". +test_format "#{s/#,/-/:#{l:a,b,c}}" "a-b-c" # escaped comma in the pattern +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. +test_format "#{s/A/X/i:@s}" "Xbcdefghij" +test_format "#{s/[/X/:@s}" "abcdefghij" + + +# --- Unicode in modifier arguments --------------------------------------- + +# Wide (CJK) and emoji text: matching, substitution, repeat and markers all +# operate on characters, and n/w report bytes/columns. +$TMUX set -g @emoji '😀😀' || exit 1 # 8 bytes, 4 columns +test_format "#{m:*中*,#{@cjk}}" "1" +test_format "#{s/文/X/:@cjk}" "中X" +test_format "#{R:中,3}" "中中中" +test_format "#{=/1/中:@s}" "a中" +test_format "#{w:@emoji}" "4" +test_format "#{n:@emoji}" "8" +test_format "#{=2:@emoji}" "😀" + + +# --- Server messages (show-messages) ------------------------------------- + +# show-messages formats each logged message (this exercises the message-time +# formatting path); just check the server survives producing it. +$TMUX show-messages >/dev/null 2>&1 +assert_alive "show-messages" + + +# --- Verbose expansion (logging) ----------------------------------------- + +# display-message -v turns on format logging, so re-expanding a representative +# set of formats with -v exercises the logging code paths. Only survival is +# checked; the log text itself is not asserted. +for f in \ + '#{=3:@s}' \ + '#{e|+|:2,3}' \ + '#{e|*|f|2:2.5,2}' \ + '#{m:*a*,abc}' \ + '#{<:3,5}' \ + '#{s/a/X/:@s}' \ + '#{b:@path}' \ + '#{t:@ts}' \ + '#{p6:@name}' \ + '#{=3:#{b:@path}}'; do + $TMUX display-message -v -p "$f" >/dev/null 2>&1 +done +assert_alive "verbose expansion" + + +# --- Loops and sorting (S, W, P, L) -------------------------------------- +# +# These need a fully controlled server so the set of sessions, windows and +# panes (and their order) is known, so start from a clean server. This must be +# the last section as it discards the setup above. +$TMUX kill-server 2>/dev/null +sleep 0.1 + +# Sessions, created in this order, so session ids (and hence creation order) +# are zeta=$0, alpha=$1, mike=$2. +$TMUX new-session -d -s zeta -x 80 -y 24 || exit 1 +$TMUX new-session -d -s alpha || exit 1 +$TMUX new-session -d -s mike || exit 1 +$TMUX set -g automatic-rename off + +# S loops over every session. The default order is by session id (SORT_INDEX), +# /i is the same, /n is by name, and the r suffix reverses. +test_format "#{S:#{session_name} }" "zeta alpha mike " +test_format "#{S/i:#{session_name} }" "zeta alpha mike " +test_format "#{S/n:#{session_name} }" "alpha mike zeta " +test_format "#{S/nr:#{session_name} }" "zeta mike alpha " +test_format "#{S/ir:#{session_name} }" "mike alpha zeta " +# /t sorts by activity time; the exact order is timing-dependent, so just check +# every session is still iterated (this exercises the activity-sort branch). +test_format "#{S/t:x}" "xxx" +# An unrecognised sort letter falls back to the default order; /r on its own +# reverses that default (this covers the fall-through branch). +test_format "#{S/r:#{session_name} }" "mike alpha zeta " + +# Windows in session zeta: window 0 renamed charlie, then alpha at 1, bravo at +# 2. The default order is by index (SORT_ORDER), /n is by name, r reverses. +$TMUX rename-window -t zeta:0 charlie +$TMUX new-window -d -t zeta:1 -n alpha +$TMUX new-window -d -t zeta:2 -n bravo +test_format "#{W:#{window_name} }" "charlie alpha bravo " "zeta:" +test_format "#{W/n:#{window_name} }" "alpha bravo charlie " "zeta:" +test_format "#{W/nr:#{window_name} }" "charlie bravo alpha " "zeta:" +test_format "#{W/ir:#{window_index}}" "210" "zeta:" +# /i (by index) and /t (by activity); /i matches the default order here. +test_format "#{W/i:#{window_name} }" "charlie alpha bravo " "zeta:" +test_format "#{W/t:x}" "xxx" "zeta:" +# An unrecognised sort letter falls back to the default order; /r reverses it. +test_format "#{W/r:#{window_name} }" "bravo alpha charlie " "zeta:" + +# Panes in window zeta:charlie. Splitting the active (newest) pane each time +# makes pane index match creation order (0,1,2 left to right). The default +# order is by creation (SORT_CREATION), r reverses. +$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" +# 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 +$TMUX display-message -v -t zeta: -p "#{W:#{window_name}}" >/dev/null 2>&1 +$TMUX display-message -v -t zeta:charlie -p "#{P:#{pane_index}}" >/dev/null 2>&1 +assert_alive "verbose loop expansion" + +# L loops over attached clients. Attach two control-mode clients, each held +# open by a background process keeping a FIFO's write end open. +FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1" +FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2" +rm -f "$FIFO1" "$FIFO2" +mkfifo "$FIFO1" "$FIFO2" || exit 1 +# Hold the write ends open so the control clients stay attached. +sleep 30 >"$FIFO1" & +HOLD1=$! +sleep 30 >"$FIFO2" & +HOLD2=$! +$TMUX -C attach -t zeta <"$FIFO1" >/dev/null 2>&1 & +CC1=$! +$TMUX -C attach -t alpha <"$FIFO2" >/dev/null 2>&1 & +CC2=$! +sleep 1 +# Two clients attached: L emits one item per client. +test_format "#{L:x}" "xx" +# The client sort orders (default, index, name, activity, reversed) are all +# accepted; assert only the count so the test does not depend on client names or +# timing. +test_format "#{L/i:x}" "xx" +test_format "#{L/n:x}" "xx" +test_format "#{L/t:x}" "xx" +test_format "#{L/nr:x}" "xx" +test_format "#{L/r:x}" "xx" +# Now detach one and confirm the count drops to one. +kill $HOLD2 2>/dev/null +sleep 1 +test_format "#{L:x}" "x" +kill $HOLD1 $CC1 $CC2 2>/dev/null +rm -f "$FIFO1" "$FIFO2" + +exit 0 diff --git a/regress/format-mouse.sh b/regress/format-mouse.sh new file mode 100644 index 000000000..9a443d19c --- /dev/null +++ b/regress/format-mouse.sh @@ -0,0 +1,143 @@ +#!/bin/sh + +# Tests of the mouse format variables (mouse_x, mouse_y, mouse_word, +# mouse_line, ...). These are only populated while a mouse key binding is being +# dispatched, so the test drives a real mouse event: +# +# - an inner client is attached inside a pane of a second ("outer") tmux +# server, giving the inner server a genuine terminal; +# - mouse mode is on and a MouseDown1Pane binding records the mouse format +# variables into an option; +# - an SGR mouse sequence is written to the outer pane, so the inner client +# receives it as a real mouse click. +# +# This exercises the mouse callbacks and the grid word/line lookup code that +# display-message cannot otherwise reach. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +# click COL ROW +# +# Write an SGR mouse press then release (button 0) at 1-based COL/ROW to the +# outer pane holding the inner client. +click() +{ + col="$1" + row="$2" + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' "$col" "$row" "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +# Inner session with a single pane running cat, so its content is exactly what +# we send it. +$TMUX new-session -d -s cov -x 80 -y 24 'cat' || exit 1 +$TMUX set -g mouse on +sleep 1 +$TMUX send-keys -t cov:0.0 'alpha beta gamma' Enter +sleep 1 + +# Record every pane mouse variable when the pane is clicked. +$TMUX bind -n MouseDown1Pane run-shell \ + "$TMUX set -g @m 'x=#{mouse_x} y=#{mouse_y} word=#{mouse_word} line=#{mouse_line} pane=#{mouse_pane} hl=[#{mouse_hyperlink}]'" + +# Attach a real client inside an outer tmux pane. Clicks all target the first +# row, which lines up with the inner client regardless of the outer status line. +$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t cov" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." + +# Click column 3, row 1: over the first word ("alpha") of the first line. +click 3 1 + +M=$($TMUX show -gv @m 2>/dev/null) +[ -n "$M" ] || fail "Mouse binding did not fire (no @m)." + +# mouse_x is 0-based column (SGR column 3 -> x 2); mouse_y is 0-based row 0. +case "$M" in +*"x=2 "*) ;; +*) fail "Unexpected mouse_x in: $M" ;; +esac +case "$M" in +*"y=0 "*) ;; +*) fail "Unexpected mouse_y in: $M" ;; +esac +# mouse_word is the word under the cursor, mouse_line the whole line. +case "$M" in +*"word=alpha "*) ;; +*) fail "Unexpected mouse_word in: $M" ;; +esac +case "$M" in +*"line=alpha beta gamma "*) ;; +*) fail "Unexpected mouse_line in: $M" ;; +esac + +# A click in a different column selects a different word. +click 8 1 +M=$($TMUX show -gv @m 2>/dev/null) +case "$M" in +*"word=beta "*) ;; +*) fail "Unexpected mouse_word for second click in: $M" ;; +esac + +# The same variables have a separate path when the pane is in a mode (the word +# and line come from the mode, not the live grid). A binding in the copy-mode +# key table fires while copy mode is active. +$TMUX bind -T copy-mode MouseDown1Pane run-shell \ + "$TMUX set -g @cm 'x=#{mouse_x} word=#{mouse_word} line=#{mouse_line}'" +$TMUX copy-mode -t cov:0.0 +sleep 1 +click 8 1 +CM=$($TMUX show -gv @cm 2>/dev/null) +case "$CM" in +*"word=beta"*) ;; +*) fail "Unexpected copy-mode mouse_word in: $CM" ;; +esac +$TMUX send-keys -t cov:0.0 -X cancel +sleep 1 + +# Hyperlinks: a new window whose pane emits an OSC 8 hyperlink over the text +# "LINKED". Clicking it reports the target URL via mouse_hyperlink (this drives +# the grid hyperlink lookup). The emitter is written to a small script to keep +# the escape sequence readable. +LINKSH="${TMPDIR:-/tmp}/fmt-mouse-link-$$.sh" +cat >"$LINKSH" <<'EOF' +#!/bin/sh +printf '\033]8;;http://example.com\033\\LINKED\033]8;;\033\\\n' +exec cat +EOF +chmod +x "$LINKSH" +$TMUX neww -t cov: -n link "$LINKSH" +sleep 1 +$TMUX select-window -t cov:link +sleep 1 +click 3 1 +M=$($TMUX show -gv @m 2>/dev/null) +rm -f "$LINKSH" +case "$M" in +*"hl=[http://example.com]"*) ;; +*) fail "Unexpected mouse_hyperlink in: $M" ;; +esac + +cleanup +exit 0 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 new file mode 100644 index 000000000..9131c2cf4 --- /dev/null +++ b/regress/format-variables.sh @@ -0,0 +1,415 @@ +#!/bin/sh + +# Tests that every format variable listed in tmux(1) (the format_table in +# format.c) can be expanded without crashing the server, and checks the value +# of a stable subset. +# +# The main point is coverage and crash-safety: each variable is expanded in a +# rich context - a real attached client (from a nested tmux), a control-mode +# client, a grouped session, two windows with a bell alert, a window with two +# panes running cat, a paste buffer and options - so the per-variable callbacks +# actually run. format-modifiers.sh covers the modifier machinery; this covers +# the variable callbacks. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +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 -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. +NAMES=" +active_window_index +alternate_on +alternate_saved_x +alternate_saved_y +bracket_paste_flag +buffer_created +buffer_full +buffer_mode_format +buffer_name +buffer_sample +buffer_size +client_activity +client_cell_height +client_cell_width +client_colours +client_control_mode +client_created +client_discarded +client_flags +client_height +client_key_table +client_last_session +client_mode_format +client_name +client_pid +client_prefix +client_readonly +client_session +client_termfeatures +client_termname +client_termtype +client_theme +client_tty +client_uid +client_user +client_utf8 +client_width +client_written +config_files +cursor_blinking +cursor_character +cursor_colour +cursor_flag +cursor_shape +cursor_very_visible +cursor_x +cursor_y +history_all_bytes +history_bytes +history_limit +history_size +host +host_short +insert_flag +keypad_cursor_flag +keypad_flag +last_window_index +mouse_all_flag +mouse_any_flag +mouse_button_flag +mouse_hyperlink +mouse_line +mouse_pane +mouse_sgr_flag +mouse_standard_flag +mouse_status_line +mouse_status_range +mouse_utf8_flag +mouse_word +mouse_x +mouse_y +next_session_id +origin_flag +pane_active +pane_at_bottom +pane_at_left +pane_at_right +pane_at_top +pane_bg +pane_bottom +pane_current_command +pane_current_path +pane_dead +pane_dead_signal +pane_dead_status +pane_dead_time +pane_fg +pane_flags +pane_floating_flag +pane_format +pane_height +pane_id +pane_in_mode +pane_index +pane_input_off +pane_key_mode +pane_last +pane_left +pane_marked +pane_marked_set +pane_mode +pane_path +pane_pb_progress +pane_pb_state +pane_pid +pane_pipe +pane_pipe_pid +pane_right +pane_search_string +pane_start_command +pane_start_command_list +pane_start_path +pane_synchronized +pane_tabs +pane_title +pane_top +pane_tty +pane_unseen_changes +pane_width +pane_x +pane_y +pane_z +pane_zoomed_flag +pid +scroll_region_lower +scroll_region_upper +server_sessions +session_active +session_activity +session_activity_flag +session_alert +session_alerts +session_attached +session_attached_list +session_bell_flag +session_created +session_format +session_group +session_group_attached +session_group_attached_list +session_group_list +session_group_many_attached +session_group_size +session_grouped +session_id +session_last_attached +session_many_attached +session_marked +session_name +session_path +session_silence_flag +session_stack +session_windows +sixel_support +socket_path +start_time +synchronized_output_flag +tree_mode_format +uid +user +version +window_active +window_active_clients +window_active_clients_list +window_active_sessions +window_active_sessions_list +window_activity +window_activity_flag +window_bell_flag +window_bigger +window_cell_height +window_cell_width +window_end_flag +window_flags +window_format +window_height +window_id +window_index +window_last_flag +window_layout +window_linked +window_linked_sessions +window_linked_sessions_list +window_marked_flag +window_name +window_offset_x +window_offset_y +window_panes +window_raw_flags +window_silence_flag +window_stack_index +window_start_flag +window_visible_layout +window_width +window_zoomed_flag +wrap_flag +" + +# test_var $name $expected [$extra_args...] +# +# Expand a single #{name} and compare against $expected. Any extra arguments +# are passed straight to display-message (e.g. -c or -t). +test_var() +{ + name="$1" + exp="$2" + shift 2 + + out=$($TMUX display-message "$@" -p "#{$name}") + if [ "$out" != "$exp" ]; then + echo "Variable test failed for '#{$name}'." + echo "Expected: '$exp'" + echo "But got '$out'" + exit 1 + fi +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server did not survive: $1" + exit 1 + fi +} + +FIFO="${TMPDIR:-/tmp}/fmt-vars-$$" +HOLD="" +CC="" + +cleanup() +{ + [ -n "$HOLD" ] && kill $HOLD 2>/dev/null + [ -n "$CC" ] && kill $CC 2>/dev/null + rm -f "$FIFO" + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# A session "cov" with a window "win0" holding two panes running cat, plus a +# second window, an option and a paste buffer. +$TMUX new-session -d -s cov -x 80 -y 24 -n win0 'cat' || exit 1 +$TMUX set -g automatic-rename off +$TMUX set -g monitor-bell on +$TMUX set -g monitor-activity on +$TMUX split-window -t cov:win0 -d 'cat' || exit 1 +$TMUX new-window -d -t cov:1 -n win1 'cat' || exit 1 +$TMUX set -g @opt 'optionvalue' || exit 1 +$TMUX set-buffer -b buf0 'somebuffer' || exit 1 + +# A second session grouped with cov, so the session_group_* variables have real +# data to report. +$TMUX new-session -d -s cov2 -t cov || exit 1 + +sleep 1 +$TMUX send-keys -t cov:win0.0 'some pane content' Enter +# Ring the bell in the non-current window so a bell alert is raised on the +# session (this populates session_alert/session_alerts and window_bell_flag). +$TMUX send-keys -t cov:win1.0 C-g +sleep 1 + +# Attach a control-mode client, held open by a background process keeping the +# write end of a FIFO open, so client_* variables have a client to read. +rm -f "$FIFO" +mkfifo "$FIFO" || exit 1 +sleep 30 >"$FIFO" & +HOLD=$! +$TMUX -C attach -t cov <"$FIFO" >/dev/null 2>&1 & +CC=$! + +# Attach a real client too: an inner tmux running inside a pane of the second +# server gets a genuine terminal, which populates the terminal-dependent client +# variables (client_termname, cursor_shape, the I modifier, ...). +$TMUX2 new-session -d -x 90 -y 30 "$TMUX attach -t cov" || exit 1 +sleep 1 + +# The real (terminal) client, identified by not being in control mode. +RC=$($TMUX list-clients -F '#{client_control_mode} #{client_name}' | + awk '$1==0 { print $2; exit }') +# The control client. +CLIENT=$($TMUX list-clients -F '#{client_control_mode} #{client_name}' | + awk '$1==1 { print $2; exit }') +[ -n "$RC" ] || fail "No real client attached." +[ -n "$CLIENT" ] || fail "No control client attached." + +# Expand every variable at once, with the real terminal client and a target +# pane in context, and confirm the server survives. This runs every callback. +FMT="" +for n in $NAMES; do + FMT="$FMT#{$n}" +done +$TMUX display-message -c "$RC" -t cov:win0.0 -p "$FMT" >/dev/null 2>&1 +assert_alive "expanding all variables together" + +# Expand each variable on its own too, so a crash can be pinned to one name. +for n in $NAMES; do + $TMUX display-message -c "$RC" -t cov:win0.0 -p "#{$n}" >/dev/null 2>&1 + assert_alive "expanding #{$n}" +done + +# Deterministic checks on stable variables (targeting pane 0 of window 0). +TGT="cov:win0.0" +test_var session_name "cov" -t "$TGT" +test_var window_name "win0" -t "$TGT" +test_var window_index "0" -t "$TGT" +test_var window_panes "2" -t "$TGT" +test_var session_windows "2" -t "$TGT" +test_var pane_index "0" -t "$TGT" +test_var pane_in_mode "0" -t "$TGT" +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:" +test_var session_group_size "2" -t "cov:" + +# list-buffers -F formats each paste buffer (this fills in the paste-buffer +# format defaults). +if [ "$($TMUX list-buffers -F '#{buffer_name}=#{buffer_sample}')" != \ + "buf0=somebuffer" ]; then + fail "Unexpected list-buffers format output." +fi + +# Version reported by the variable matches tmux -V. +VER=$($TMUX -V | sed 's/^tmux //') +test_var version "$VER" -t "$TGT" + +# Client variables from each kind of client. +test_var client_name "$CLIENT" -c "$CLIENT" +test_var client_control_mode "1" -c "$CLIENT" +test_var client_control_mode "0" -c "$RC" +test_var socket_path "$($TMUX display-message -p '#{socket_path}')" -c "$CLIENT" +# The real client has a terminal, so termcap/feature/environ queries work. +test_var "I/e:TERM" "$($TMUX display-message -c "$RC" -p '#{client_termname}')" \ + -c "$RC" +# Termcap and feature queries against a real terminal return a boolean. +case "$($TMUX display-message -c "$RC" -p '#{I/c:colors}')" in +0|1) ;; +*) fail "Unexpected #{I/c:colors} for real client." ;; +esac +case "$($TMUX display-message -c "$RC" -p '#{I/f:256}')" in +0|1) ;; +*) fail "Unexpected #{I/f:256} for real client." ;; +esac + +# Time variables through the pretty and relative modifiers: start_time is the +# recent server start, exercising the "last 24 hours" and "just now" paths. +[ -n "$($TMUX display-message -p '#{t/p:start_time}')" ] || + fail "Empty #{t/p:start_time}." +[ -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..37934db8d --- /dev/null +++ b/regress/hooks-after.sh @@ -0,0 +1,124 @@ +#!/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_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|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|second|one:0' +wait_for @after2 'after-rename-window|second|one:0+2' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu 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..e90f7ba3a --- /dev/null +++ b/regress/hooks-lifecycle.sh @@ -0,0 +1,162 @@ +#!/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" + +# 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..41916248c --- /dev/null +++ b/regress/hooks-notify.sh @@ -0,0 +1,199 @@ +#!/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" + +# 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 new file mode 100644 index 000000000..100a7be0f --- /dev/null +++ b/regress/input-common.inc @@ -0,0 +1,176 @@ +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +TMP=$(mktemp) +EXP=$(mktemp) +trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15 + +exit_status=0 + +fail() +{ + echo "FAIL: $1" + diff -u "$EXP" "$TMP" + exit_status=1 +} + +start_pane() +{ + start_pane_hlimit "$1" "$2" "$3" "$4" 0 +} + +start_pane_history() +{ + start_pane_hlimit "$1" "$2" "$3" "$4" 2000 +} + +start_pane_hlimit() +{ + name=$1 + sx=$2 + sy=$3 + seq=$4 + hlimit=$5 + + $TMUX 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 "$hlimit" || exit 1 + $TMUX new-session -d -x "$sx" -y "$sy" -s "$name" \ + "printf '$seq'; sleep 2" || exit 1 + $TMUX kill-session -t test-setup + sleep 0.3 +} + +start_cmd() +{ + name=$1 + sx=$2 + sy=$3 + cmd=$4 + + $TMUX kill-server 2>/dev/null + sleep 0.1 + $TMUX new-session -d -x "$sx" -y "$sy" -s "$name" "$cmd" || exit 1 + sleep 0.3 +} + +normalize_capture() +{ + sed 's/[ ]*$//' | + awk '{ line[NR] = $0; if ($0 != "") last = NR } + END { for (i = 1; i <= last; i++) print line[i] }' +} + +capture_grid() +{ + $TMUX capture-pane -pN -t "$1:" -S 0 -E - | normalize_capture +} + +check_capture() +{ + name=$1 + expected=$2 + + capture_grid "$name" >"$TMP" + printf "%s\n" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name" +} + +check_cursor() +{ + name=$1 + expected=$2 + + actual=$($TMUX display-message -p -t "$name:" '#{cursor_x},#{cursor_y}') + if [ "$actual" != "$expected" ]; then + printf "%s\n" "$expected" >"$EXP" + printf "%s\n" "$actual" >"$TMP" + fail "$name cursor" + fi +} + +check_flags() +{ + name=$1 + expected=$2 + + $TMUX capture-pane -pNF -t "$name:" -S 0 -E - | + normalize_capture | + awk '$0 != "-"' >"$TMP" + printf "%s\n" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name flags" +} + +check_joined() +{ + name=$1 + expected=$2 + + $TMUX capture-pane -pNJ -t "$name:" -S 0 -E - | + normalize_capture >"$TMP" + printf "%s\n" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name joined" +} + +capture_raw() +{ + $TMUX capture-pane -pR -t "$1:" +} + +capture_raw_used() +{ + capture_raw "$1" | + awk '/^(G| L)/ || /^ C/ && $3 !~ /^data=\(1,1, \)$/' +} + +check_raw() +{ + name=$1 + expected=$2 + + capture_raw "$name" >"$TMP" + printf "%s\n" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name raw" +} + +check_raw_used() +{ + name=$1 + expected=$2 + + capture_raw_used "$name" >"$TMP" + printf "%s\n" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name raw used" +} + +check_raw_has() +{ + name=$1 + shift + + capture_raw "$name" >"$TMP" + for expected in "$@"; do + if ! grep -Fqx "$expected" "$TMP"; then + printf "%s\n" "$expected" >"$EXP" + fail "$name raw missing" + fi + done +} + +check_raw_matches() +{ + name=$1 + shift + + capture_raw "$name" >"$TMP" + for expected in "$@"; do + if ! grep -Eq "$expected" "$TMP"; then + printf "%s\n" "$expected" >"$EXP" + fail "$name raw missing" + fi + done +} diff --git a/regress/input-cursor.sh b/regress/input-cursor.sh new file mode 100644 index 000000000..51a825fc1 --- /dev/null +++ b/regress/input-cursor.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane cursor 10 3 'ABCDE\r\033[2Cxy\033[1D!\033[4GZ\n' +check_capture cursor 'ABxZE' +check_cursor cursor '0,1' + +start_pane saverc 10 3 'abc\0337\033[2;5HXY\0338Z\n' +check_capture saverc 'abcZ + XY' +check_cursor saverc '0,1' + +start_pane hvp 10 4 'A\033[3dB\033[5GC\033[2;2fD\n' +check_capture hvp 'A + D + B C' +check_cursor hvp '0,2' + +start_pane cursorlines 8 4 'A\033[2BB\033[1FC\033[1AD\n' +check_capture cursorlines 'AD +C + B' +check_cursor cursorlines '0,1' + +start_pane tabs 12 3 'a\tb\n' +check_capture tabs 'a b' +check_cursor tabs '0,1' + +start_pane tabclear 12 3 '\033H\ta\033[3g\r\tb\n' +check_capture tabclear ' a b' +check_cursor tabclear '0,1' + +start_pane cbt 16 3 '0123456789\r\033[10C\033[Zx\n' +check_capture cbt '01234567x9' +check_cursor cbt '0,1' + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/input-edit.sh b/regress/input-edit.sh new file mode 100644 index 000000000..3c8ad3cef --- /dev/null +++ b/regress/input-edit.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane dch 10 3 'abcdef\r\033[3C\033[2PXY\n' +check_capture dch 'abcXY' + +start_pane ich 10 3 'abcdef\r\033[3C\033[2@XY\n' +check_capture ich 'abcXYdef' + +start_pane erase 10 3 'abcdef\r\033[3C\033[KZ\n' +check_capture erase 'abcZ' + +start_pane el1 10 3 'abcdef\r\033[3C\033[1KZ\n' +check_capture el1 ' Zef' + +start_pane ech 10 3 'abcdef\r\033[3C\033[2XX\n' +check_capture ech 'abcX f' + +start_pane ed 10 3 'one\ntwo\033[2;2H\033[JX\n' +check_capture ed 'one +tX' + +start_pane ed1 10 3 'one\ntwo\033[2;2H\033[1JX\n' +check_capture ed1 ' + Xo' + +start_pane ed2 10 3 'one\ntwo\033[2JZ\n' +check_capture ed2 ' + Z' + +start_pane il 8 4 '111\n222\n333\033[2;1H\033[LAAA\n' +check_capture il '111 +AAA +222 +333' + +start_pane dl 8 4 '111\n222\n333\033[2;1H\033[MZZZ\n' +check_capture dl '111 +ZZZ' + +start_pane irm 10 3 'abcdef\r\033[4h\033[3CXY\033[4lZ\n' +check_capture irm 'abcXYZef' + +start_pane rep 10 3 'A\033[4bB\n' +check_capture rep 'AAAAAB' + +start_pane decaln 6 3 '\033#8' +check_capture decaln 'EEEEEE +EEEEEE +EEEEEE' + +$TMUX kill-server 2>/dev/null +exit $exit_status 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-malformed.sh b/regress/input-malformed.sh new file mode 100644 index 000000000..55a7e38c0 --- /dev/null +++ b/regress/input-malformed.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +. ./input-common.inc + +start_cmd csi-param-discard 8 3 \ + "perl -e 'print qq{\e[}, q{1} x 80, qq{\030OK}'; sleep 2" +check_capture csi-param-discard 'OK' + +start_cmd csi-interm-discard 8 3 \ + "perl -e 'print qq{\e[ \030OK}'; sleep 2" +check_capture csi-interm-discard 'OK' + +start_cmd osc-discard 8 3 \ + "perl -e 'print qq{\e]2;}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2" +check_capture osc-discard 'OK' + +start_cmd apc-discard 8 3 \ + "perl -e 'print qq{\e_}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2" +check_capture apc-discard 'OK' + +start_pane unknown-csi 8 3 '\033[?9999zOK' +check_capture unknown-csi 'OK' + +start_pane unknown-osc 8 3 '\033]999;bad\aOK' +check_capture unknown-osc 'OK' + +start_pane malformed-osc 8 3 '\033]8;id=a:id=b;http://bad\aX\033]8;id=no-separator\aY\033]9;4;5;200\a\033]9;4;z\a\033]10;notacolour\a\033]11;notacolour\a\033]12;notacolour\a\033]4;999;red\a\033]104;999\a\033]52bad\a\033]52;c;@@@\aOK' +check_capture malformed-osc 'XYOK' +check_raw_matches malformed-osc \ + 'C 0,0 data=\(1,1,X\).* link=NONE linkid=NONE' \ + 'C 0,1 data=\(1,1,Y\).* link=NONE linkid=NONE' \ + 'C 0,2 data=\(1,1,O\).* link=NONE linkid=NONE' \ + 'C 0,3 data=\(1,1,K\).* link=NONE linkid=NONE' + +start_pane malformed-dcs 8 3 '\033P$qBAD\033\\OK' +check_capture malformed-dcs 'OK^[P0$r +^[\' + +start_pane malformed-utf8 8 3 '\360\200\200\200A\355\240\200B' +check_capture malformed-utf8 '�A�B' + +exit $exit_status diff --git a/regress/input-modes.sh b/regress/input-modes.sh new file mode 100644 index 000000000..9bbe44429 --- /dev/null +++ b/regress/input-modes.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n' +check_capture alternate 'MAINZ' + +start_pane osc133 10 4 '\033]133;A\007prompt\n\033]133;C\007output\n' +check_capture osc133 'prompt +output' +check_flags osc133 'P prompt +O output' + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/input-osc.sh b/regress/input-osc.sh new file mode 100644 index 000000000..19b9d400f --- /dev/null +++ b/regress/input-osc.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane hyperlink 20 3 '\033]8;id=1;https://example.com\033\\link\033]8;;\033\\ plain\n' +check_capture hyperlink 'link plain' +check_flags hyperlink 'HX link plain' +$TMUX capture-pane -peH -t hyperlink: -S 0 -E - >/dev/null || exit 1 + +start_pane palette 20 3 '\033]4;1;rgb:11/22/33;2;red\007\033]104;1;2\007X\n' +check_capture palette 'X' + +start_pane osc-colours 20 3 '\033]10;rgb:11/22/33\007\033]11;rgb:44/55/66\007\033]12;rgb:77/88/99\007\033]110\007\033]111\007\033]112\007X\n' +check_capture osc-colours 'X' + +start_pane progress 20 3 '\033]9;4;1;25\007\033]9;4;0\007\033]9;4;5;200\007X\n' +check_capture progress 'X' + +start_pane rename 20 3 '\033krenamed\033\\X\n' +check_capture rename 'X' + +start_pane apc-title 20 3 '\033_test-title\033\\X\n' +check_capture apc-title 'X' + +$TMUX kill-server 2>/dev/null +sleep 0.1 +$TMUX new-session -d -x 20 -y 3 -s osc52 "sleep 2" || exit 1 +$TMUX set-option -s set-clipboard on || exit 1 +$TMUX respawn-pane -k -t osc52: \ + "printf '\033]52;c;SGVsbG8=\007'; sleep 2" || exit 1 +sleep 0.3 +$TMUX save-buffer -b buffer0 - >"$TMP" +printf "Hello" >"$EXP" +cmp "$TMP" "$EXP" || fail "osc52" + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/input-raw-controls.sh b/regress/input-raw-controls.sh new file mode 100644 index 000000000..ef67fe39c --- /dev/null +++ b/regress/input-raw-controls.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane bs 8 3 'abc\bd' +check_capture bs 'abd' +check_raw_matches bs \ + 'C 0,0 data=\(1,1,a\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1,b\) flags=NONE\[0\]' \ + 'C 0,2 data=\(1,1,d\) flags=NONE\[0\]' + +start_pane nel 8 3 'A\033EB' +check_capture nel 'A +B' +check_raw_matches nel \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 1,0 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane tabstops 16 3 '\033H1\t2\033[3g\r\t3' +check_raw_matches tabstops \ + 'C 0,0 data=\(1,1,1\) flags=NONE\[0\]' \ + 'C 0,8 data=\(1,1,2\) flags=NONE\[0\]' \ + 'C 0,15 data=\(1,1,3\) flags=NONE\[0\]' + +start_pane decaln 6 3 '\033#8' +check_raw_matches decaln \ + 'C 0,0 data=\(1,1,E\) flags=NONE\[0\]' \ + 'C 1,5 data=\(1,1,E\) flags=NONE\[0\]' \ + 'C 2,5 data=\(1,1,E\) flags=NONE\[0\]' + +start_pane charset 8 3 '\033(0qxl\033(BZ' +check_raw_matches charset \ + 'C 0,0 data=\(1,1,q\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \ + 'C 0,1 data=\(1,1,x\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \ + 'C 0,2 data=\(1,1,l\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \ + 'C 0,3 data=\(1,1,Z\) flags=NONE\[0\] attr=NONE\[0\]' + +start_pane g1charset 8 3 '\033)0\016q\017Z' +check_raw_matches g1charset \ + 'C 0,0 data=\(1,1,q\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \ + 'C 0,1 data=\(1,1,Z\) flags=NONE\[0\] attr=NONE\[0\]' + +start_pane csisave 8 3 '\033[3;3HS\033[s\033[1;1HA\033[uR' +check_raw_matches csisave \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 2,2 data=\(1,1,S\) flags=NONE\[0\]' \ + 'C 2,3 data=\(1,1,R\) flags=NONE\[0\]' + +start_pane alternate 8 3 'main\033[?1049halt\033[?1049lback' +check_capture alternate 'mainback' +check_raw_matches alternate \ + 'C 0,0 data=\(1,1,m\) flags=NONE\[0\]' \ + 'C 0,4 data=\(1,1,b\) flags=NONE\[0\]' \ + 'C 0,7 data=\(1,1,k\) flags=NONE\[0\]' + +start_pane sync 8 3 '\033P=1signored\033\\A\033P=2s\033\\B' +check_raw_matches sync \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane private 8 3 '\033[?25lA\033[?25hB\033[?1000hC\033[?1000lD' +check_raw_matches private \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1,B\) flags=NONE\[0\]' \ + 'C 0,2 data=\(1,1,C\) flags=NONE\[0\]' \ + 'C 0,3 data=\(1,1,D\) flags=NONE\[0\]' + +start_pane ris 8 3 'A\033cB' +check_capture ris 'B' +check_raw_matches ris \ + 'C 0,0 data=\(1,1,B\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\]' + +start_pane keypad 8 3 '\033=A\033>B' +check_raw_matches keypad \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane cursorstyle 8 3 '\033[5 qA\033[0 qB' +check_raw_matches cursorstyle \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1,B\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-cursor.sh b/regress/input-raw-cursor.sh new file mode 100644 index 000000000..4cba03eed --- /dev/null +++ b/regress/input-raw-cursor.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane absolute 8 4 'A\033[3;5HB\033[2GC\033[2D!' +check_capture absolute 'A + +!C B' +check_cursor absolute '1,2' +check_raw_matches absolute \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 2,4 data=\(1,1,B\) flags=NONE\[0\]' \ + 'C 2,0 data=\(1,1,!\) flags=NONE\[0\]' \ + 'C 2,1 data=\(1,1,C\) flags=NONE\[0\]' + +start_pane savecursor 8 4 '\033[4;4HS\0337\033[1;1HA\0338R' +check_capture savecursor 'A + + + SR' +check_cursor savecursor '5,3' +check_raw_matches savecursor \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 3,3 data=\(1,1,S\) flags=NONE\[0\]' \ + 'C 3,4 data=\(1,1,R\) flags=NONE\[0\]' + +start_pane origin 8 5 '\033[2;4r\033[?6h\033[1;1HO\033[3;1HP\033[?6lQ' +check_raw_matches origin \ + 'C 1,0 data=\(1,1,O\) flags=NONE\[0\]' \ + 'C 3,0 data=\(1,1,P\) flags=NONE\[0\]' \ + 'C 0,0 data=\(1,1,Q\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-edit.sh b/regress/input-raw-edit.sh new file mode 100644 index 000000000..6d78cc3c4 --- /dev/null +++ b/regress/input-raw-edit.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane erasechars 8 3 'ABCDEFGH\r\033[3C\033[2X' +check_capture erasechars 'ABC FGH' +check_raw_matches erasechars \ + 'C 0,3 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,4 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,5 data=\(1,1,F\) flags=NONE\[0\]' + +start_pane deletechars 8 3 'ABCDEFGH\r\033[3C\033[3P' +check_capture deletechars 'ABCGH' +check_raw_matches deletechars \ + 'C 0,3 data=\(1,1,G\) flags=NONE\[0\]' \ + 'C 0,4 data=\(1,1,H\) flags=NONE\[0\]' \ + 'C 0,5 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' + +start_pane insertchars 8 3 'ABCDEF\r\033[3C\033[2@xy' +check_capture insertchars 'ABCxyDEF' +check_raw_matches insertchars \ + 'C 0,3 data=\(1,1,x\) flags=NONE\[0\]' \ + 'C 0,4 data=\(1,1,y\) flags=NONE\[0\]' \ + 'C 0,5 data=\(1,1,D\) flags=NONE\[0\]' + +start_pane eraseline 8 3 'ABCDEFGH\r\033[4C\033[K' +check_capture eraseline 'ABCD' +check_raw_matches eraseline \ + 'C 0,4 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,7 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' + +start_pane erasescreen 8 3 '1111111\033[2;1H2222222\033[H\033[JZ' +check_capture erasescreen 'Z' +check_raw_matches erasescreen \ + '^G 8x3 \(0/0\)$' \ + 'C [0-9]+,0 data=\(1,1,Z\) flags=NONE\[0\]' \ + 'C [0-9]+,1 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' + +start_pane tabs 12 3 'A\tB\033[2g\r\033[IC' +check_raw_matches tabs \ + 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ + 'C 0,1 data=\(7,7, \) flags=TAB\[[0-9a-f]+\]' \ + 'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\]' \ + 'C 0,8 data=\(1,1,B\) flags=NONE\[0\]' \ + 'C 0,0 data=\(1,1,C\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-history.sh b/regress/input-raw-history.sh new file mode 100644 index 000000000..d3c7d260c --- /dev/null +++ b/regress/input-raw-history.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane_hlimit trim 6 3 'one\ntwo\nthree\nfour\nfive\nsix' 2 +check_raw_matches trim \ + '^G 6x3 \(2/2\)$' \ + 'L 0 \(-\) flags=NONE\[0\]' \ + 'L 1 \(-\) flags=NONE\[0\]' + +$TMUX clear-history -t trim: +check_raw_matches trim \ + '^G 6x3 \(0/2\)$' \ + 'C 0,0 data=\(1,1,f\) flags=NONE\[0\]' \ + 'C 2,0 data=\(1,1,s\) flags=NONE\[0\]' + +start_pane_hlimit edhistory 6 3 'one\ntwo\nthree\033[H\033[JZ' 5 +check_raw_matches edhistory \ + '^G 6x3 \([1-9][0-9]*/5\)$' \ + 'L [0-9]+ \(-\) flags=NONE\[0\]' \ + 'C [0-9]+,0 data=\(1,1,Z\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-reflow.sh b/regress/input-raw-reflow.sh new file mode 100644 index 000000000..2e1d6c188 --- /dev/null +++ b/regress/input-raw-reflow.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane_history reflow 8 4 'abcdefgh\nijklmnop\nqrstuvwx\nyz' +$TMUX resize-window -t reflow: -x 4 -y 4 +sleep 0.2 +check_raw_matches reflow \ + '^G 4x4 \([0-9]+/2000\)$' \ + 'L [0-9]+ \([0-9-]+\) flags=WRAPPED\[[0-9a-f]+\]' \ + 'C [0-9]+,0 data=\(1,1,a\) flags=NONE\[0\]' \ + 'C [0-9]+,3 data=\(1,1,d\) flags=NONE\[0\]' + +$TMUX resize-window -t reflow: -x 12 -y 4 +sleep 0.2 +check_raw_matches reflow \ + '^G 12x4 \([0-9]+/2000\)$' \ + 'C [0-9]+,0 data=\(1,1,a\) flags=NONE\[0\]' \ + 'C [0-9]+,7 data=\(1,1,h\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-scroll.sh b/regress/input-raw-scroll.sh new file mode 100644 index 000000000..8fb2a6f50 --- /dev/null +++ b/regress/input-raw-scroll.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane_history history 6 3 'one\ntwo\nthree\nfour\nfive' +check_raw_matches history \ + '^G 6x3 \([1-9][0-9]*/2000\)$' \ + 'L [0-9]+ \(-\) flags=NONE\[0\]' \ + 'C [0-9]+,0 data=\(1,1,o\) flags=NONE\[0\]' + +start_pane_history index 6 4 'A\nB\nC\033[2;3r\033[2;1HX\033D\033DY' +check_raw_matches index \ + 'C [0-9]+,0 data=\(1,1,X\) flags=NONE\[0\]' \ + 'C [0-9]+,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C [0-9]+,1 data=\(1,1,Y\) flags=NONE\[0\]' + +start_pane reverse 6 4 'A\nB\nC\033[2;3r\033[2;1H\033MY' +check_raw_matches reverse \ + 'C 1,0 data=\(1,1,Y\) flags=NONE\[0\]' \ + 'C 2,0 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane insertline 6 4 'A\nB\nC\033[2;3r\033[2;1H\033[LY' +check_raw_matches insertline \ + 'C 1,0 data=\(1,1,Y\) flags=NONE\[0\]' \ + 'C 2,0 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane deleteline 6 4 'A\nB\nC\033[2;3r\033[2;1H\033[MY' +check_raw_matches deleteline \ + 'C 1,0 data=\(1,1,Y\) flags=NONE\[0\]' \ + 'C 2,0 data=\(1,1, \) flags=NONE\[0\]' + +start_pane region-edge 6 4 'top\033[2;3rmid\033[2;1H\033D\033Mbot' +check_raw_matches region-edge \ + 'C 0,0 data=\(1,1,m\) flags=NONE\[0\]' \ + 'C 1,0 data=\(1,1,b\) flags=NONE\[0\]' \ + 'C 2,0 data=\(1,1, \) flags=NONE\[0\]' \ + 'C 3,0 data=\(1,1, \) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-sgr.sh b/regress/input-raw-sgr.sh new file mode 100644 index 000000000..c9a0b5fba --- /dev/null +++ b/regress/input-raw-sgr.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane attrs 16 3 '\033[1mB\033[2mD\033[3mI\033[4mU\033[5mK\033[7mR\033[8mH\033[9mS\033[53mO' +check_raw_matches attrs \ + 'C 0,0 data=\(1,1,B\) flags=NONE\[0\] attr=BRIGHT\[[0-9a-f]+\]' \ + 'C 0,1 data=\(1,1,D\) flags=NONE\[0\] attr=BRIGHT,DIM\[[0-9a-f]+\]' \ + 'C 0,2 data=\(1,1,I\) flags=NONE\[0\] attr=BRIGHT,DIM,ITALICS\[[0-9a-f]+\]' \ + 'C 0,3 data=\(1,1,U\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,ITALICS\[[0-9a-f]+\]' \ + 'C 0,5 data=\(1,1,R\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,BLINK,REVERSE,ITALICS\[[0-9a-f]+\]' \ + 'C 0,7 data=\(1,1,S\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,BLINK,REVERSE,HIDDEN,ITALICS,STRIKETHROUGH\[[0-9a-f]+\]' \ + 'C 0,8 data=\(1,1,O\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,BLINK,REVERSE,HIDDEN,ITALICS,STRIKETHROUGH,OVERLINE\[[0-9a-f]+\]' + +start_pane colours 12 3 '\033[38;5;196;48;5;17mX\033[58;5;45mY' +check_raw_matches colours \ + 'C 0,0 data=\(1,1,X\) flags=FG256,BG256\[[0-9a-f]+\] attr=NONE\[0\] fg=colour196\[10000c4\] bg=colour17\[1000011\]' \ + 'C 0,1 data=\(1,1,Y\) flags=FG256,BG256\[[0-9a-f]+\] attr=NONE\[0\] fg=colour196\[10000c4\] bg=colour17\[1000011\] us=colour45\[100002d\]' + +start_pane bce 8 3 '\033[44mA\033[K' +check_raw_matches bce \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\] attr=NONE\[0\].* bg=blue\[4\]' \ + 'C 0,1 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\].* bg=blue\[4\]' \ + 'C 0,7 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\].* bg=blue\[4\]' + +start_pane underlines 12 3 '\033[4:2m2\033[4:3m3\033[4:4m4\033[4:5m5' +check_raw_matches underlines \ + 'C 0,0 data=\(1,1,2\) flags=NONE\[0\] attr=UNDERSCORE_2\[[0-9a-f]+\]' \ + 'C 0,1 data=\(1,1,3\) flags=NONE\[0\] attr=UNDERSCORE_3\[[0-9a-f]+\]' \ + 'C 0,2 data=\(1,1,4\) flags=NONE\[0\] attr=UNDERSCORE_4\[[0-9a-f]+\]' \ + 'C 0,3 data=\(1,1,5\) flags=NONE\[0\] attr=UNDERSCORE_5\[[0-9a-f]+\]' + +start_pane hyperlink 12 3 '\033]8;id=id1;https://example.com/a\033\\A\033]8;;\033\\B' +check_raw_matches hyperlink \ + 'L 0 \(0\) flags=EXTENDED,HYPERLINK\[[0-9a-f]+\]' \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\].* link=https://example.com/a linkid=id1' \ + 'C 0,1 data=\(1,1,B\) flags=NONE\[0\].* link=NONE linkid=NONE' + +exit $exit_status diff --git a/regress/input-raw-unicode.sh b/regress/input-raw-unicode.sh new file mode 100644 index 000000000..76e2f5512 --- /dev/null +++ b/regress/input-raw-unicode.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane wide 8 3 'A\343\201\202B' +check_capture wide 'AあB' +check_raw_matches wide \ + 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ + 'C 0,1 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,3 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane combining 8 3 'e\314\201x' +check_raw_matches combining \ + 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ + 'C 0,0 data=\(1,[0-9]+,.*\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,x\) flags=NONE\[0\]' + +start_pane emoji 10 3 '\360\237\230\200Z' +check_raw_matches emoji \ + 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ + 'C 0,0 data=\(2,4,😀\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,2 data=\(1,1,Z\) flags=NONE\[0\]' + +start_pane flag 10 3 '\360\237\207\254\360\237\207\247!' +check_raw_matches flag \ + 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ + 'C 0,0 data=\(2,8,🇬🇧\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,2 data=\(1,1,!\) flags=NONE\[0\]' + +start_pane variation 10 3 '*\357\270\217!' +check_raw_matches variation \ + 'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \ + 'C 0,0 data=\(2,[0-9]+,.*\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,2 data=\(1,1,!\) flags=NONE\[0\]' + +start_pane invalid 10 3 '\377A' +check_raw_matches invalid \ + 'C 0,0 data=\(1,3,.*\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,A\) flags=NONE\[0\]' + +start_pane trunc2 10 3 '\303A' +check_raw_matches trunc2 \ + 'C 0,0 data=\(1,3,.*\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,A\) flags=NONE\[0\]' + +start_pane trunc3 10 3 '\342\202A' +check_raw_matches trunc3 \ + 'C 0,0 data=\(1,3,.*\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,1 data=\(1,1,A\) flags=NONE\[0\]' + +start_pane overwrite-wide-left 10 3 'A\343\201\202B\r\033[1CX' +check_raw_matches overwrite-wide-left \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1,X\) flags=NONE\[0\]' \ + 'C 0,2 data=\(1,1, \) flags=NONE\[0\]' \ + 'C 0,3 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane overwrite-wide-pad 10 3 'A\343\201\202B\r\033[2CX' +check_raw_matches overwrite-wide-pad \ + 'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \ + 'C 0,1 data=\(1,1, \) flags=NONE\[0\]' \ + 'C 0,2 data=\(1,1,X\) flags=NONE\[0\]' \ + 'C 0,3 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane overwrite-wide-with-wide 10 3 'A\343\201\202B\r\033[1C\347\225\214' +check_raw_matches overwrite-wide-with-wide \ + 'C 0,1 data=\(2,3,界\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 0,3 data=\(1,1,B\) flags=NONE\[0\]' + +start_pane wide-right-edge 4 3 'ABC\343\201\202Z' +check_raw_matches wide-right-edge \ + 'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \ + 'C 1,0 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \ + 'C 1,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \ + 'C 1,2 data=\(1,1,Z\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-raw-wrap.sh b/regress/input-raw-wrap.sh new file mode 100644 index 000000000..7ae2634d6 --- /dev/null +++ b/regress/input-raw-wrap.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane wrap 5 3 'ABCDEZ' +check_capture wrap 'ABCDE +Z' +check_raw_matches wrap \ + '^G 5x3 \(0/0\)$' \ + 'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \ + 'C 0,4 data=\(1,1,E\) flags=NONE\[0\]' \ + 'C 1,0 data=\(1,1,Z\) flags=NONE\[0\]' + +start_pane nowrap 5 3 '\033[?7lABCDEZ' +check_capture nowrap 'ABCDZ' +check_raw_matches nowrap \ + '^G 5x3 \(0/0\)$' \ + 'L 0 \(0\) flags=NONE\[0\]' \ + 'C 0,4 data=\(1,1,Z\) flags=NONE\[0\]' + +start_pane pending 5 3 'ABCD\r\033[4CZ' +check_capture pending 'ABCDZ' +check_cursor pending '5,0' +check_raw_matches pending \ + 'L 0 \(0\) flags=NONE\[0\]' \ + 'C 0,4 data=\(1,1,Z\) flags=NONE\[0\]' + +exit $exit_status diff --git a/regress/input-reflow-stress.sh b/regress/input-reflow-stress.sh new file mode 100644 index 000000000..926e8cdd0 --- /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 '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 new file mode 100644 index 000000000..5fc77f843 --- /dev/null +++ b/regress/input-replies.sh @@ -0,0 +1,94 @@ +#!/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 +sleep 0.5 + +TMP=$(mktemp) +EXP=$(mktemp) +trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15 + +$TMUX new-session -d -x 80 -y 24 -s replies \; \ + set-window-option -t replies:0 remain-on-exit on || exit 1 +$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.5 + +exit_status=0 + +fail() +{ + echo "FAIL: $1" + diff -u "$EXP" "$TMP" + exit_status=1 +} + +query() +{ + name=$1 + expected=$2 + seq=$3 + count=$4 + setup=$5 + + $TMUX respawn-window -k -t replies:0 \ + "stty raw -echo min 1 time 20; printf '$setup'; printf '$seq'; dd bs=1 count=$count 2>/dev/null | cat -v >$TMP" + sleep 0.5 + printf "%s" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name" +} + +query_timeout() +{ + name=$1 + expected=$2 + seq=$3 + setup=$4 + + $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 1 + printf "%s" "$expected" >"$EXP" + cmp "$TMP" "$EXP" || fail "$name" +} + +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' 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' +query_timeout "osc-12-query" '^[]12;rgb:0000/ffff/0000^G' '\033]12;?\007' '\033]12;green\007' +query_timeout "osc-4-query" '^[]4;1;rgb:ffff/0000/0000^G' '\033]4;1;?\007' '\033]4;1;red\007' +query_timeout "osc-104-reset-query" '' '\033]4;1;?\007' '\033]4;1;red\007\033]104;1\007' +query_timeout "osc-52-query" '^[]52;c;SGVsbG8=^G' '\033]52;c;?\007' '' + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/input-requests.sh b/regress/input-requests.sh new file mode 100644 index 000000000..094ce2a8a --- /dev/null +++ b/regress/input-requests.sh @@ -0,0 +1,118 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 - "$TEST_TMUX" <<'PY' +import os +import select +import signal +import subprocess +import sys +import tempfile +import time + +tmux = sys.argv[1] +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, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +def attach(): + pid, fd = os.forkpty() + if pid == 0: + os.environ["TERM"] = "xterm-256color" + os.execl(tmux, tmux, "-L" + label, "-f/dev/null", "attach-session", + "-t", "requests") + os.set_blocking(fd, False) + return pid, fd + +def read_until(fd, needle, timeout=5): + end = time.time() + timeout + data = b"" + while time.time() < end: + r, _, _ = select.select([fd], [], [], 0.05) + if fd in r: + try: + chunk = os.read(fd, 4096) + except BlockingIOError: + chunk = b"" + if chunk == b"": + continue + data += chunk + if needle in data: + return data + raise RuntimeError("did not see terminal request %r in %r" % + (needle, data)) + +def wait_file(path, timeout=5): + end = time.time() + timeout + while time.time() < end: + try: + with open(path, "rb") as f: + data = f.read() + if data: + return data + except FileNotFoundError: + pass + time.sleep(0.05) + return b"" + +def respawn(command): + run("respawn-window", "-k", "-t", "requests:0", command) + time.sleep(0.2) + +def cleanup(pid=None): + if pid is not None: + try: + os.kill(pid, signal.SIGHUP) + except ProcessLookupError: + pass + run("kill-server", check=False) + +run("kill-server", check=False) +run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests", + "sleep 60") + +pid, fd = attach() +try: + time.sleep(0.5) + + with tempfile.NamedTemporaryFile(delete=False) as f: + palette_out = f.name + respawn("stty raw -echo min 1 time 50; " + "printf '\\033]4;99;?\\033\\\\'; " + "dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" % + palette_out) + read_until(fd, b"\033]4;99;?\033\\") + os.write(fd, b"\033]4;99;rgb:0101/0202/0303\033\\") + got = wait_file(palette_out) + expected = b"^[]4;99;rgb:0101/0202/0303^[\\" + if got != expected: + raise AssertionError("palette reply: expected %r got %r" % + (expected, got)) + + run("set-option", "-s", "set-clipboard", "on") + run("set-option", "-s", "get-clipboard", "request") + with tempfile.NamedTemporaryFile(delete=False) as f: + clip_out = f.name + respawn("stty raw -echo min 1 time 50; " + "printf '\\033]52;c;?\\033\\\\'; " + "dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" % + clip_out) + data = read_until(fd, b"]52;") + if b"?" not in data: + raise RuntimeError("clipboard request missing query in %r" % data) + os.write(fd, b"\033]52;c;UmVxdWVzdA==\033\\") + got = wait_file(clip_out) + expected = b"^[]52;c;UmVxdWVzdA==^[\\" + if got != expected: + raise AssertionError("clipboard reply: expected %r got %r" % + (expected, got)) +finally: + cleanup(pid) +PY diff --git a/regress/input-scroll.sh b/regress/input-scroll.sh new file mode 100644 index 000000000..6ae48e96d --- /dev/null +++ b/regress/input-scroll.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane wrap 5 3 'abcdeF' +check_capture wrap 'abcde +F' +check_cursor wrap '1,1' +check_flags wrap 'W abcde +- F' +check_joined wrap 'abcdeF' + +start_pane wraplast 5 3 'abcd\033[5GZQ' +check_capture wraplast 'abcdZ +Q' +check_cursor wraplast '1,1' + +start_pane nowrap 5 3 '\033[?7labcdeF' +check_capture nowrap 'abcdF' +check_cursor nowrap '4,0' + +start_pane origin 6 4 '111111\n222222\n333333\n444444\033[2;3r\033[?6h\033[1;1HAA\033[?6l\033[r' +check_capture origin '111111 +AA2222 +333333 +444444' + +start_pane scrollup 5 4 '11111\n22222\n33333\n44444\033[2;3r\033[3;1HAAAAA\nBBBBB\033[r' +check_capture scrollup '11111 +AAAAA +BBBBB +44444' + +start_pane scrolldown 5 4 '11111\n22222\n33333\n44444\033[2;3r\033[2;1H\033[TZZZZZ\033[r' +check_capture scrolldown '11111 +ZZZZZ +22222 +44444' + +start_pane ri 5 4 '11111\n22222\n33333\n44444\033[2;3r\033[2;1H\033MZZZZZ\033[r' +check_capture ri '11111 +ZZZZZ +22222 +44444' + +start_pane nel 5 3 'AA\033EBC\n' +check_capture nel 'AA +BC' + +$TMUX kill-server 2>/dev/null +sleep 0.1 +$TMUX new-session -d -x 5 -y 3 -s history \; \ + set-option -g history-limit 3 \; \ + respawn-pane -k "printf '01\n02\n03\n04\n05\n06'; sleep 2" || exit 1 +sleep 0.3 +$TMUX capture-pane -pN -t history: -S - -E - | normalize_capture >"$TMP" +printf "%s\n" '01 +02 +03 +04 +05 +06' >"$EXP" +cmp "$TMP" "$EXP" || fail "history limit" + +$TMUX clear-history -t history: +$TMUX capture-pane -pN -t history: -S - -E - | normalize_capture >"$TMP" +printf "%s\n" '04 +05 +06' >"$EXP" +cmp "$TMP" "$EXP" || fail "clear-history" + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/input-sgr.sh b/regress/input-sgr.sh new file mode 100644 index 000000000..435a0a9ab --- /dev/null +++ b/regress/input-sgr.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane sgr-basic 20 3 '\033[1;2;3;4;5;7;8;9mA\033[22;23;24;25;27;28;29mB\n' +check_capture sgr-basic 'AB' +$TMUX capture-pane -peN -t sgr-basic: -S 0 -E - >/dev/null || exit 1 + +start_pane sgr-colour 20 3 '\033[31;42mA\033[38;5;196;48;5;22mB\033[38;2;1;2;3;48;2;4;5;6mC\033[39;49mD\n' +check_capture sgr-colour 'ABCD' +$TMUX capture-pane -peN -t sgr-colour: -S 0 -E - >/dev/null || exit 1 + +start_pane sgr-underline 20 3 '\033[4:1mA\033[4:2mB\033[4:3mC\033[4:4mD\033[4:5mE\033[4:0mF\n' +check_capture sgr-underline 'ABCDEF' +$TMUX capture-pane -peN -t sgr-underline: -S 0 -E - >/dev/null || exit 1 + +start_pane sgr-uscolour 20 3 '\033[58;5;45;4mA\033[58:2::10:20:30mB\033[59mC\n' +check_capture sgr-uscolour 'ABC' +$TMUX capture-pane -peN -t sgr-uscolour: -S 0 -E - >/dev/null || exit 1 + +start_pane sgr-reset 20 3 '\033[90;100mA\033[0mB\033[91;101mC\033[39;49mD\n' +check_capture sgr-reset 'ABCD' +$TMUX capture-pane -peN -t sgr-reset: -S 0 -E - >/dev/null || exit 1 + +$TMUX kill-server 2>/dev/null +exit $exit_status diff --git a/regress/input-unicode.sh b/regress/input-unicode.sh new file mode 100644 index 000000000..ca74a82e8 --- /dev/null +++ b/regress/input-unicode.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +. ./input-common.inc + +start_pane wide 10 3 '\343\201\202B\rX\n' +check_capture wide 'X B' +check_flags wide 'X X B' + +start_pane widepad 10 3 'A\343\201\202B\r\033[2CX\n' +check_capture widepad 'A XB' +check_flags widepad 'X A XB' + +start_pane wideedge 5 3 'abc\343\201\202Z\n' +check_capture wideedge 'abcあ +Z' +check_cursor wideedge '0,2' +check_joined wideedge 'abcあZ' + +start_pane wideeol 5 3 'abcd\343\201\202Z\n' +check_capture wideeol 'abcd +あZ' +check_cursor wideeol '0,2' + +start_pane combine 10 3 'e\314\201\n' +check_capture combine 'é' +check_cursor combine '0,1' + +start_pane combinewide 10 3 '\343\201\202\314\201X\n' +check_capture combinewide 'あ́X' +check_cursor combinewide '0,1' + +start_pane variation 10 3 '\342\234\224\357\270\217X\n' +check_capture variation '✔️X' +check_cursor variation '0,1' + +start_pane flag 10 3 '\360\237\207\254\360\237\207\247X\n' +check_capture flag '🇬🇧X' +check_cursor flag '0,1' + +start_pane combining-left 10 3 '\314\201A\n' +check_capture combining-left 'A' +check_cursor combining-left '0,1' + +$TMUX kill-server 2>/dev/null +exit $exit_status 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-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..6a35abbbd 100644 --- a/regress/new-session-size.sh +++ b/regress/new-session-size.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-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 new file mode 100644 index 000000000..d9751c94c --- /dev/null +++ b/regress/options-array.sh @@ -0,0 +1,185 @@ +#!/bin/sh + +# 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 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. +# +# options-scope.sh covers scoping/inheritance and options-values.sh covers +# value validation. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +check_value() +{ + out=$($TMUX show $1 2>&1) + if [ "$out" != "$2" ]; then + echo "show $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +# check_array $args $expected +# +# Compare the full (multi-line) show output for an array option with a +# newline-separated $expected string. +check_array() +{ + out=$($TMUX show $1 2>&1) + if [ "$out" != "$(printf '%s' "$2")" ]; then + echo "show $1 (array) failed." + echo "Expected:"; printf '%s\n' "$2" + echo "But got:"; printf '%s\n' "$out" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +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 +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# --- whole-array assignment splits on the separator ----------------------- +# +# update-environment has the default " ," separator, so a single string value +# 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 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-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 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 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 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[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) ------------------------------------- +# +# user-keys splits only on comma, so an embedded space stays within one entry +# (and show quotes a value containing a space). +check_ok set -g user-keys "One,Two Three" +check_array "-g user-keys" 'user-keys[0] One +user-keys[1] "Two Three"' + +# --- command-type array (a hook) ------------------------------------------ +# +# 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]" "" + +# --- colour-type array ---------------------------------------------------- +# +# pane-colours is a colour array; an indexed value is validated as a colour. +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 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 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" + +assert_alive "after options-array tests" + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/options-scope.sh b/regress/options-scope.sh new file mode 100644 index 000000000..ac6d62f77 --- /dev/null +++ b/regress/options-scope.sh @@ -0,0 +1,208 @@ +#!/bin/sh + +# Tests of the options engine scoping and inheritance, as described in the +# OPTIONS section of tmux(1) and implemented in options.c, cmd-set-option.c and +# cmd-show-options.c. +# +# This exercises: global vs session vs window vs pane precedence; -u to remove +# an option (revealing the inherited value); -gu to restore a global option to +# its compiled default; scope inference from the option name (-w/-p and the +# set-window-option alias); show -v (which does NOT walk parents) versus show -A +# (which does, marking inherited values with a trailing *); unknown/ambiguous +# option errors and -q suppression; and user options (@foo) at every scope. +# +# options-values.sh covers value validation and options-array.sh covers arrays. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check_value $args $expected +# +# Run show-option with $args and compare the single-line output with $expected. +check_value() +{ + out=$($TMUX show $1 2>&1) + if [ "$out" != "$2" ]; then + echo "show $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +# 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 +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# --- global vs session precedence ----------------------------------------- +# +# status-left is a session option. A value set at the session scope shadows +# the global one; show -v at each scope reports that scope's own value. +check_ok set -g status-left "GLOBAL" +check_ok set status-left "SESSION" +check_value "-v status-left" "SESSION" +check_value "-gv status-left" "GLOBAL" + +# show -v does NOT inherit: -u removes the session entry, after which the +# session-scope show -v is empty even though the global value still exists. +check_ok set -u status-left +check_value "-v status-left" "" +check_value "-gv status-left" "GLOBAL" + +# show -A walks the parent scopes and marks an inherited value with a "*". +out=$($TMUX show -A 2>/dev/null | grep '^status-left\*') +if [ "$out" != "status-left* GLOBAL" ]; then + echo "show -A did not mark inherited status-left." + echo "But got: '$out'" + exit 1 +fi + +# --- -gu restores the compiled default ------------------------------------ +# +# Removing a global option with -u restores its built-in default rather than +# deleting it; status-left's default is the format "[#{session_name}] ". +check_ok set -g status-left "GLOBAL2" +check_value "-gv status-left" "GLOBAL2" +check_ok set -gu status-left +check_value "-gv status-left" "[#{session_name}] " + +# --- scope inference from the option name --------------------------------- +# +# mode-keys is a window option, so a bare set-option infers the window scope; +# set-window-option (setw) is an explicit alias for the same thing, and -g w +# targets the global window options. +check_ok set mode-keys vi +check_value "-wv mode-keys" "vi" +check_ok setw mode-keys emacs +check_value "-wv mode-keys" "emacs" +check_ok set -gw mode-keys vi +check_value "-gwv mode-keys" "vi" + +# cursor-colour is a window-and-pane option. A pane-scope value overrides a +# window-scope one for that pane. +check_ok set -w cursor-colour blue +check_ok set -p cursor-colour red +check_value "-pv cursor-colour" "red" +out=$($TMUX show -Ap 2>/dev/null | grep '^cursor-colour ') +if [ "$out" != "cursor-colour red" ]; then + echo "pane cursor-colour did not override window value." + echo "But got: '$out'" + exit 1 +fi + +# --- -U unsets a window option and clears pane copies ---------------------- +# +# When a window option also has per-pane copies, -u on the window scope leaves +# those pane copies in place; -U additionally removes the option from every +# pane in the window, so all panes fall back to inheritance. +$TMUX split-window -t main || exit 1 +panes=$($TMUX list-panes -t main -F '#{pane_id}') +set -- $panes +pa=$1 +pb=$2 +check_ok set -p -t "$pa" cursor-colour red +check_ok set -p -t "$pb" cursor-colour blue +check_ok set -w -t main cursor-colour green +check_value "-pv -t $pa cursor-colour" "red" +check_value "-pv -t $pb cursor-colour" "blue" +check_value "-wv -t main cursor-colour" "green" +check_ok set -Uw -t main cursor-colour +check_value "-pv -t $pa cursor-colour" "" +check_value "-pv -t $pb cursor-colour" "" +check_value "-wv -t main cursor-colour" "" + +# --- unknown, ambiguous and -q -------------------------------------------- +check_fail "invalid option: no-such-option" set -g no-such-option x +check_fail "ambiguous option: status-l" set -g status-l x +# A unique prefix resolves to the full option name. +check_ok set -g status-inte 5 +check_value "-gv status-interval" "5" +# -q suppresses the error and exits successfully. +check_ok set -gq no-such-option x +check_ok show -gqv no-such-option + +# --- errors from unresolvable targets ------------------------------------- +# +# A -t target that does not resolve produces a scope-specific error from +# options_scope_from_name()/options_scope_from_flags(). +check_fail "no such session: nosuch" show -t nosuch status-left +check_fail "no such window: nosuch" show -w -t nosuch mode-keys +check_fail "no such pane: nosuch" set -p -t nosuch cursor-colour red + +# --- show with no option name lists every option -------------------------- +# +# show without a specific option walks the whole table (cmd_show_options_all). +# Hooks are hidden unless -H is given. +$TMUX set -g @listme "here" || exit 1 +if ! $TMUX show -g | grep -q '^@listme here$'; then + echo "show -g did not list @listme." + exit 1 +fi +# alert-bell is a hook: only shown with -H. +if $TMUX show -g | grep -q '^alert-bell'; then + echo "show -g listed a hook without -H." + exit 1 +fi +if ! $TMUX show -gH | grep -q '^alert-bell'; then + echo "show -gH did not list the alert-bell hook." + exit 1 +fi + +# --- user options at every scope ------------------------------------------ +# +# @-prefixed user options can be created freely at any scope and do not +# inherit type checking. +check_ok set -g @u "global-user" +check_ok set @u "session-user" +check_ok set -w @u "window-user" +check_ok set -p @u "pane-user" +check_value "-gv @u" "global-user" +check_value "-v @u" "session-user" +check_value "-wv @u" "window-user" +check_value "-pv @u" "pane-user" + +assert_alive "after options-scope tests" + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/options-values.sh b/regress/options-values.sh new file mode 100644 index 000000000..75da52e01 --- /dev/null +++ b/regress/options-values.sh @@ -0,0 +1,193 @@ +#!/bin/sh + +# Tests of options engine value validation, as implemented by +# options_from_string() and friends in options.c. +# +# Each option table entry has a type (string, number, key, colour, flag, +# choice, command) with type-specific parsing and validation. This exercises: +# number range limits; choice options rejecting unknown values; flag options +# toggling with no value and rejecting garbage; colour and key options +# rejecting invalid input; string append with -a; -F expansion at set time; +# and -o refusing to overwrite an option that is already set. +# +# options-scope.sh covers scoping/inheritance and options-array.sh covers +# arrays. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +check_value() +{ + out=$($TMUX show $1 2>&1) + if [ "$out" != "$2" ]; then + echo "show $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +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 +} + +assert_alive() +{ + if [ "$($TMUX display-message -p alive)" != "alive" ]; then + echo "Server died: $1" + exit 1 + fi +} + +$TMUX new-session -d -s main -x 80 -y 24 || exit 1 + +# --- number options ------------------------------------------------------- +# +# display-time is a number with a minimum of 0; a negative value and a +# non-numeric value are both rejected via strtonum(3). +check_ok set -g display-time 4000 +check_value "-gv display-time" "4000" +check_fail "value is too small: -5" set -g display-time -5 +check_fail "value is invalid: abc" set -g display-time abc +# A missing value is rejected for a non-flag, non-choice option. +check_fail "empty value" set -g display-time + +# --- choice options ------------------------------------------------------- +# +# status-keys accepts only its listed choices (vi/emacs); anything else is an +# "unknown value" error and the option keeps its previous value. +check_ok set -g status-keys vi +check_value "-gv status-keys" "vi" +check_fail "unknown value: bogus" set -g status-keys bogus +check_value "-gv status-keys" "vi" + +# --- flag options --------------------------------------------------------- +# +# focus-events is an on/off flag. Setting with no value toggles it; explicit +# on/off/yes/no/1/0 are accepted (case-insensitively); anything else fails. +check_ok set -g focus-events off +check_value "-gv focus-events" "off" +check_ok set -g focus-events # toggle +check_value "-gv focus-events" "on" +check_ok set -g focus-events # toggle back +check_value "-gv focus-events" "off" +check_ok set -g focus-events yes +check_value "-gv focus-events" "on" +check_ok set -g focus-events NO +check_value "-gv focus-events" "off" +check_fail "bad value: maybe" set -g focus-events maybe + +# --- colour options ------------------------------------------------------- +# +# status-bg is a colour; named colours, numbers and #rrggbb are accepted, +# garbage is rejected. +check_ok set -g status-bg red +check_value "-gv status-bg" "red" +check_ok set -g status-bg colour123 +check_value "-gv status-bg" "colour123" +check_ok set -g status-bg "#00ff00" +check_value "-gv status-bg" "#00ff00" +check_fail "bad colour: xxxyyy" set -g status-bg xxxyyy + +# --- style options -------------------------------------------------------- +# +# status-style is a style string, validated when set; a bogus style keyword is +# rejected and the old value is retained. +check_ok set -g status-style "fg=red,bg=black" +check_value "-gv status-style" "fg=red,bg=black" +check_fail "invalid style: bg=xxxyyy" set -g status-style "bg=xxxyyy" +check_value "-gv status-style" "fg=red,bg=black" + +# --- key options ---------------------------------------------------------- +# +# prefix is a key; a valid key name is stored in canonical form, a bad one is +# rejected. +check_ok set -g prefix C-a +check_value "-gv prefix" "C-a" +check_fail "bad key: boguskey" set -g prefix boguskey + +# --- string options with extra validation --------------------------------- +# +# default-shell is a string but is checked to be an executable shell; a bogus +# path is rejected and the old value kept. +old=$($TMUX show -gv default-shell) +check_fail "not a suitable shell: /not/a/shell" set -g default-shell /not/a/shell +check_value "-gv default-shell" "$old" + +# --- user options require a value ------------------------------------------ +# +# A user option set with no value at all is an error. +check_fail "empty value" set -g @novalue + +# --- command options ------------------------------------------------------ +# +# default-client-command is a command option: the value is parsed as a tmux +# command when set and re-printed from the parsed command list. A syntax +# error is reported and the option is left unchanged. +check_ok set -g default-client-command "new-window" +check_value "-gv default-client-command" "new-window" +check_fail "syntax error" set -g default-client-command "if -x {" +check_value "-gv default-client-command" "new-window" + +# --- renamed option aliases ----------------------------------------------- +# +# Historical option names are mapped to their current spelling, so setting +# cursor-color updates cursor-colour. +check_ok set -w cursor-color red +check_value "-wv cursor-colour" "red" + +# --- string append (-a) --------------------------------------------------- +# +# -a appends to the current string value rather than replacing it. +check_ok set -g @str "foo" +check_ok set -ga @str "bar" +check_value "-gv @str" "foobar" + +# --- -F expands at set time ----------------------------------------------- +# +# With -F the value is expanded as a format once, at set time; without -F it is +# stored literally. +check_ok set -gF @expanded "#{session_name}" +check_value "-gv @expanded" "main" +check_ok set -g @literal "#{session_name}" +check_value "-gv @literal" "#{session_name}" + +# --- -o refuses to overwrite ---------------------------------------------- +# +# -o makes set-option fail if the option is already set, leaving it unchanged; +# it succeeds for an option that is not yet set. +check_ok set -g @once "first" +check_fail "already set: @once" set -go @once "second" +check_value "-gv @once" "first" +check_ok set -go @fresh "value" +check_value "-gv @fresh" "value" + +assert_alive "after options-values tests" + +$TMUX kill-server 2>/dev/null +exit 0 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..762bea200 --- /dev/null +++ b/regress/pane-ops.sh @@ -0,0 +1,620 @@ +#!/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' + +# 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' + +# -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 7fa7a673b..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 @@ -131,11 +131,14 @@ $IN send-keys -l "Z" || exit 1 settle search_is "hello Z" "C-w did not kill a word" -# C-a then C-k kills the whole line. +# C-a then C-k kills the whole line. The mode prompt no longer fills the rest +# of the row, so insert a marker to distinguish prompt input from tree content +# that may remain visible after the prompt. $IN send-keys C-a || exit 1 $IN send-keys C-k || exit 1 +$IN send-keys -l "X" || exit 1 settle -search_row | grep -q '(search) [^ ]' && fail "C-a C-k did not clear the line" +search_is "X" "C-a C-k did not clear the line" # --- 3. Editing kept the prompt open the whole time. --- in_tree_mode || fail "editing keys closed the mode" diff --git a/regress/prompt-mechanics.sh b/regress/prompt-mechanics.sh index 6f3e5294e..55bd17a69 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 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..ee9c2fa16 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) 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..8ad3ba2eb 100644 --- a/regress/screen-redraw-popups.sh +++ b/regress/screen-redraw-popups.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) diff --git a/regress/screen-redraw-results/floating-over-scrollbar.result b/regress/screen-redraw-results/floating-over-scrollbar.result index 5e85eff82..d618c0a60 100644 --- a/regress/screen-redraw-results/floating-over-scrollbar.result +++ b/regress/screen-redraw-results/floating-over-scrollbar.result @@ -1,12 +1,12 @@ base      - ┌──────────────┐  - │OVERSB  │  - │  │  - │  │  - │  │  - └──────────────┘  + ┌──────────────┐  + │OVERSB  │  + │  │  + │  │  + │  │  + └──────────────┘        diff --git a/regress/screen-redraw-results/marked-pane-lr.result b/regress/screen-redraw-results/marked-pane-lr.result index 4f3d1af11..53c9aff44 100644 --- a/regress/screen-redraw-results/marked-pane-lr.result +++ b/regress/screen-redraw-results/marked-pane-lr.result @@ -1,12 +1,12 @@ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ diff --git a/regress/screen-redraw-results/marked-pane-status.result b/regress/screen-redraw-results/marked-pane-status.result index ff99a0bc2..502f4cc70 100644 --- a/regress/screen-redraw-results/marked-pane-status.result +++ b/regress/screen-redraw-results/marked-pane-status.result @@ -1,12 +1,12 @@ -── 0:left ──────────┬── 1:right ──────── - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ +── 0:left ──────────┬── 1:right ──────── + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ diff --git a/regress/screen-redraw-results/marked-pane-tb.result b/regress/screen-redraw-results/marked-pane-tb.result index 23fe88924..c16b2a177 100644 --- a/regress/screen-redraw-results/marked-pane-tb.result +++ b/regress/screen-redraw-results/marked-pane-tb.result @@ -4,7 +4,7 @@ -──────────────────────────────────────── +──────────────────────────────────────── diff --git a/regress/screen-redraw-results/marked-pane-three.result b/regress/screen-redraw-results/marked-pane-three.result index f7b776713..e199f5415 100644 --- a/regress/screen-redraw-results/marked-pane-three.result +++ b/regress/screen-redraw-results/marked-pane-three.result @@ -1,12 +1,12 @@ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ + │ │ diff --git a/regress/screen-redraw-results/scrollbar-floating.result b/regress/screen-redraw-results/scrollbar-floating.result index 51542ad0d..81c9dfbae 100644 --- a/regress/screen-redraw-results/scrollbar-floating.result +++ b/regress/screen-redraw-results/scrollbar-floating.result @@ -1,12 +1,12 @@ SB00 abcdefghij  SB01 abcdefghij  SB02 abcdefghij  -SB03 abc┌──────────────────┐  -SB04 abc│FLOAT02 abcdef  │  -SB05 abc│FLOAT03 abcdef  │  -SB06 abc│FLOAT04 abcdef  │  -SB07 abc│  │  -SB08 abc└──────────────────┘  +SB03 abc┌──────────────────┐  +SB04 abc│FLOAT02 abcdef  │  +SB05 abc│FLOAT03 abcdef  │  +SB06 abc│FLOAT04 abcdef  │  +SB07 abc│  │  +SB08 abc└──────────────────┘  SB09 abcdefghij  SB10 abcdefghij    diff --git a/regress/screen-redraw-results/scrollbar-split-left.result b/regress/screen-redraw-results/scrollbar-split-left.result index 9d602a4d6..5ac964ab2 100644 --- a/regress/screen-redraw-results/scrollbar-split-left.result +++ b/regress/screen-redraw-results/scrollbar-split-left.result @@ -1,10 +1,10 @@ - SB00 abcdefghij │ SBL00 abcdefghij - SB01 abcdefghij │ SBL01 abcdefghij - SB02 abcdefghij │ SBL02 abcdefghij - SB03 abcdefghij │ SBL03 abcdefghij - SB04 abcdefghij │ SBL04 abcdefghij - SB05 abcdefghij │ SBL05 abcdefghij - SB06 abcdefghij │ SBL06 abcdefghij + SB00 abcdefghij │ SBL00 abcdefghij + SB01 abcdefghij │ SBL01 abcdefghij + SB02 abcdefghij │ SBL02 abcdefghij + SB03 abcdefghij │ SBL03 abcdefghij + SB04 abcdefghij │ SBL04 abcdefghij + SB05 abcdefghij │ SBL05 abcdefghij + SB06 abcdefghij │ SBL06 abcdefghij  SB07 abcdefghij │ SBL07 abcdefghij  SB08 abcdefghij │ SBL08 abcdefghij  SB09 abcdefghij │ SBL09 abcdefghij diff --git a/regress/screen-redraw-results/scrollbar-split-right.result b/regress/screen-redraw-results/scrollbar-split-right.result index ab1d465ef..7946089cd 100644 --- a/regress/screen-redraw-results/scrollbar-split-right.result +++ b/regress/screen-redraw-results/scrollbar-split-right.result @@ -1,10 +1,10 @@ -SB00 abcdefghij  │SBR00 abcdefghij  -SB01 abcdefghij  │SBR01 abcdefghij  -SB02 abcdefghij  │SBR02 abcdefghij  -SB03 abcdefghij  │SBR03 abcdefghij  -SB04 abcdefghij  │SBR04 abcdefghij  -SB05 abcdefghij  │SBR05 abcdefghij  -SB06 abcdefghij  │SBR06 abcdefghij  +SB00 abcdefghij  │SBR00 abcdefghij  +SB01 abcdefghij  │SBR01 abcdefghij  +SB02 abcdefghij  │SBR02 abcdefghij  +SB03 abcdefghij  │SBR03 abcdefghij  +SB04 abcdefghij  │SBR04 abcdefghij  +SB05 abcdefghij  │SBR05 abcdefghij  +SB06 abcdefghij  │SBR06 abcdefghij  SB07 abcdefghij  │SBR07 abcdefghij  SB08 abcdefghij  │SBR08 abcdefghij  SB09 abcdefghij  │SBR09 abcdefghij  diff --git a/regress/screen-redraw-results/window-style-active.result b/regress/screen-redraw-results/window-style-active.result index 6210820aa..aa110d5ed 100644 --- a/regress/screen-redraw-results/window-style-active.result +++ b/regress/screen-redraw-results/window-style-active.result @@ -1,8 +1,8 @@ -STYLE00 abcdefghij │STYLE00 abcdefghij -STYLE01 abcdefghij │STYLE01 abcdefghij -STYLE02 abcdefghij │STYLE02 abcdefghij -STYLE03 abcdefghij │STYLE03 abcdefghij -STYLE04 abcdefghij │STYLE04 abcdefghij +STYLE00 abcdefghij │STYLE00 abcdefghij +STYLE01 abcdefghij │STYLE01 abcdefghij +STYLE02 abcdefghij │STYLE02 abcdefghij +STYLE03 abcdefghij │STYLE03 abcdefghij +STYLE04 abcdefghij │STYLE04 abcdefghij STYLE05 abcdefghij │STYLE05 abcdefghij STYLE06 abcdefghij │STYLE06 abcdefghij  │ 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..3b32478f0 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) 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 old mode 100755 new mode 100644 index 28e7a85ed..f2b634a43 --- 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..08c5dfbae --- /dev/null +++ b/regress/session-ops.sh @@ -0,0 +1,217 @@ +#!/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 +} + +# --------------------------------------------------------------------------- +# 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 + +# -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 100755 index 000000000..4947af70c --- /dev/null +++ b/regress/set-hook-B.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_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" + +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 new file mode 100644 index 000000000..9ea936173 --- /dev/null +++ b/regress/targets-panes.sh @@ -0,0 +1,150 @@ +#!/bin/sh + +# Tests of pane target resolution in cmd-find.c. +# +# Building on targets.sh (session/window resolution), this exercises the pane +# half of cmd_find_target() in a known 2x2 split: +# +# - pane ids (%n), pane indices, and the +/- offset and ! last-pane tokens; +# - positional tokens {top-left}/{top-right}/{bottom-left}/{bottom-right} +# and {top}/{bottom}/{left}/{right}; +# - directional tokens {up-of}/{down-of}/{left-of}/{right-of} relative to +# the active pane; +# - the ".pane" and "sess:win.pane" combined forms; +# - the marked pane, reached with ~ / {marked} and cleared with -M; +# - and the error paths: a pane id in the wrong window, a directional token +# with no neighbour, and an unset marked pane. +# +# The 2x2 split is created in a fixed order so pane ids are deterministic: +# +# +--------+--------+ +# | %0 | %1 | top-left = %0 top-right = %1 +# +--------+--------+ bottom-left = %2 bottom-right = %3 +# | %2 | %3 | +# +--------+--------+ + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +$TMUX kill-server 2>/dev/null + +# check $target $expected [format] +# +# The default format is the pane id. +check() +{ + fmt=${3:-'#{pane_id}'} + out=$($TMUX display-message -p -t "$1" "$fmt" 2>&1) + if [ "$out" != "$2" ]; then + echo "target '$1' resolved wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +check_fail() +{ + out=$($TMUX has-session -t "$2" 2>&1) + if [ $? -eq 0 ]; then + echo "target '$2' resolved (expected failure)." + exit 1 + fi + if [ "$out" != "$1" ]; then + echo "Wrong error for target '$2'." + echo "Expected: '$1'" + 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 +} + +# --- fixture: a 2x2 split plus a single-pane window ----------------------- +check_ok new-session -d -s p -x 80 -y 24 +check_ok split-window -h -t p:0 # %0 left, %1 right +check_ok split-window -v -t p:0.%0 # split left: %0 top, %2 bottom +check_ok split-window -v -t p:0.%1 # split right: %1 top, %3 bottom +check_ok new-window -d -t p: -n solo # a second, single-pane window + +# --- pane ids, index, offsets --------------------------------------------- +check "p:0.%3" "%3" # exact pane id +check "p:0.3" "%3" # pane by index +check "p:0.%1" "%1" # sess:win.pane form +check ".%1" "%1" # .pane form (current window) +# "sess:.pane" (empty window part) resolves the pane in the session's current +# window. Make window 0 current first. +check_ok select-window -t p:0 +check "p:.%1" "%1" +check "p:.{top-left}" "%0" +# Offsets are relative to the active pane; make %0 active first. +check_ok select-pane -t p:0.%0 +check "p:0.+" "1" '#{pane_index}' # next pane +check "p:0.-" "3" '#{pane_index}' # previous pane (wraps) + +# --- last pane (!) -------------------------------------------------------- +check_ok select-pane -t p:0.%2 +check_ok select-pane -t p:0.%0 # now the last pane is %2 +check "p:0.!" "%2" + +# --- positional tokens (absolute geometry) -------------------------------- +check "p:0.{top-left}" "%0" +check "p:0.{top-right}" "%1" +check "p:0.{bottom-left}" "%2" +check "p:0.{bottom-right}" "%3" +check "p:0.{top}" "%0" # leftmost of the top row +check "p:0.{bottom}" "%2" # leftmost of the bottom row +check "p:0.{left}" "%0" # top of the left column +check "p:0.{right}" "%1" # top of the right column + +# --- directional tokens (relative to the active pane) --------------------- +# +# From the top-left pane the real neighbours are below and to the right. +check_ok select-pane -t p:0.%0 +check "p:0.{down-of}" "%2" +check "p:0.{right-of}" "%1" +# From the bottom-right pane the real neighbours are above and to the left. +check_ok select-pane -t p:0.%3 +check "p:0.{up-of}" "%1" +check "p:0.{left-of}" "%2" + +# --- pane error paths ----------------------------------------------------- +check_fail "can't find pane: %0" "p:solo.%0" # pane id, wrong window +check_fail "can't find pane: {up-of}" "p:solo.{up-of}" # no neighbour +check_fail "can't find pane: 9" "p:0.9" # no such index + +# --- marked pane ---------------------------------------------------------- +# +# ~ / {marked} resolve to the marked pane from anywhere; -M clears it. +check_fail "no marked target" "~" # nothing marked yet +check_ok select-pane -m -t p:0.%1 +check "~" "%1" +check "{marked}" "%1" +# The mark is global: it resolves even with a different current window. +check_ok select-window -t p:solo +check "~" "%1" +check_ok select-window -t p:0 +check_ok select-pane -M # clear the mark +check_fail "no marked target" "~" +check_fail "no marked target" "{marked}" + +assert_alive "after pane target tests" + +$TMUX kill-server 2>/dev/null +exit 0 diff --git a/regress/targets.sh b/regress/targets.sh new file mode 100644 index 000000000..75ee3dbb8 --- /dev/null +++ b/regress/targets.sh @@ -0,0 +1,226 @@ +#!/bin/sh + +# Tests of target (session and window) resolution in cmd-find.c. +# +# A target string like "session:window.pane" is parsed by cmd_find_target() +# and resolved to a concrete session/window/pane. This exercises the session +# and window halves of that machinery: +# +# - session and window ids ($n, @n) and names; +# - exact (=name), prefix and fnmatch matching, and the ambiguous/missing +# error paths for each; +# - the combined "sess:", "sess:win", ":win" and "sess:win.pane" forms and +# the empty (current) target; +# - the offset and special window tokens (^ $ ! + - and their {start}, +# {end}, {last}, {next}, {previous} spellings), including +N/-N with +# wrap-around; +# - the special whole-target tokens {active}/@/{current} and {mouse}/=; +# - the CMD_FIND_WINDOW_INDEX "can't specify pane here" guard; and +# - -s versus -t resolution on link-window/move-window. +# +# Positive cases are asserted with display-message -p -t (which renders the +# resolved target); error cases with has-session -t, which resolves strictly +# and prints the cmd-find error text. +# +# Pane resolution (directional/positional tokens, marked pane) is covered by +# targets-panes.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 + +# check $target $expected [format] +# +# Resolve $target and compare the rendered value. The default format is the +# window index; pass a third argument to override. +check() +{ + fmt=${3:-'#{window_index}'} + out=$($TMUX display-message -p -t "$1" "$fmt" 2>&1) + if [ "$out" != "$2" ]; then + echo "target '$1' resolved wrong." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 + fi +} + +check_ok() +{ + if ! $TMUX "$@"; then + echo "Command failed (expected success): $*" + exit 1 + fi +} + +# check_fail $expected_error $target +# +# has-session resolves the target strictly and prints the cmd-find error. +check_fail() +{ + out=$($TMUX has-session -t "$2" 2>&1) + if [ $? -eq 0 ]; then + echo "target '$2' resolved (expected failure)." + exit 1 + fi + if [ "$out" != "$1" ]; then + echo "Wrong error for target '$2'." + echo "Expected: '$1'" + 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 +} + +# --- fixture -------------------------------------------------------------- +# +# Session alpha with four named windows (0 editor, 1 editing, 2 shell, +# 3 logs); "editor"/"editing" share a prefix for the ambiguity tests. Two +# grp* sessions share a prefix for the session ambiguity tests. +check_ok new-session -d -s alpha -x 80 -y 24 +check_ok rename-window -t alpha:0 editor +check_ok new-window -d -t alpha: -n editing +check_ok new-window -d -t alpha: -n shell +check_ok new-window -d -t alpha: -n logs +check_ok new-session -d -s beta -x 80 -y 24 +check_ok new-window -d -t beta: -n bw1 +check_ok new-session -d -s grp1 -x 80 -y 24 +check_ok new-session -d -s grp2 -x 80 -y 24 + +# Give alpha a last-window (2) with the current window left at 0. +check_ok select-window -t alpha:2 +check_ok select-window -t alpha:0 + +# --- session ids and names ------------------------------------------------ +sid=$($TMUX display-message -p -t alpha: '#{session_id}') +check "$sid:" "alpha" '#{session_name}' +check "=alpha:" "alpha" '#{session_name}' # exact +check "alpha:" "alpha" '#{session_name}' # full name +check "al:" "alpha" '#{session_name}' # prefix +check "al*:" "alpha" '#{session_name}' # fnmatch + +# --- session error paths -------------------------------------------------- +check_fail "can't find session: grp" "grp:" # ambiguous prefix +check_fail "can't find session: grp*" "grp*:" # ambiguous fnmatch +check_fail "can't find session: al" "=al:" # exact-only, no such session +check_fail "can't find session: nosuch" "nosuch:" + +# --- window ids and names ------------------------------------------------- +wid=$($TMUX display-message -p -t alpha:editing '#{window_id}') +# A bare @id (no session) resolves both window and session. +check "$wid" "alpha:1" '#{session_name}:#{window_index}' +check "alpha:shell" "2" # exact name +check "alpha:edito" "0" # prefix +check "alpha:=editor" "0" # exact match flag +check "alpha:sh*" "2" # fnmatch +check "alpha:1" "1" # index + +# A window id qualified by a session resolves within that session; a window id +# belonging to a different session is rejected. +w2=$($TMUX display-message -p -t alpha:shell '#{window_id}') +check "alpha:$w2" "2" +bw=$($TMUX display-message -p -t beta: '#{window_id}') +check_fail "can't find window: $bw" "alpha:$bw" # window id, wrong session + +# --- window error paths --------------------------------------------------- +check_fail "can't find window: edit" "alpha:edit" # ambiguous prefix +check_fail "can't find window: e*" "alpha:e*" # ambiguous fnmatch +check_fail "can't find window: nope" "alpha:nope" # missing +check_fail "can't find window: @999" "@999" # missing window id + +# --- offset and special window tokens ------------------------------------- +# +# alpha's current window is 0; offsets wrap around the four windows. +check "alpha:^" "0" # start +check "alpha:\$" "3" # end +check "alpha:+" "1" # next +check "alpha:-" "3" # previous (wraps) +check "alpha:+2" "2" +check "alpha:-2" "2" # wraps +check "alpha:{start}" "0" +check "alpha:{end}" "3" +check "alpha:{next}" "1" +check "alpha:{previous}" "3" +check "alpha:!" "2" # last window +check "alpha:{last}" "2" + +# --- combined and empty forms --------------------------------------------- +# +# Empty targets use the current pane from TMUX_PANE when there is no client. +# This keeps the test independent of the best-session fallback. +check_ok select-window -t alpha:0 +pane=$($TMUX display-message -p -t alpha:0 '#{pane_id}') +TMUX_PANE=$pane check "" "alpha" '#{session_name}' # empty target is current +TMUX_PANE=$pane check "" "alpha:0" '#{session_name}:#{window_index}' +TMUX_PANE=$pane check ":shell" "alpha:2" '#{session_name}:#{window_index}' +check "alpha:shell.0" "alpha:2" '#{session_name}:#{window_index}' +TMUX_PANE=$pane check "alpha:.0" "alpha:0" '#{session_name}:#{window_index}' # empty window part + +# --- bare-name fallbacks -------------------------------------------------- +# +# A bare pane target that is not a pane falls back to a window, then to a +# session, using the current session (alpha). +TMUX_PANE=$pane check "editor" "0" '#{window_index}' # bare window name +check "beta" "beta" '#{session_name}' # bare session name + +# --- whole-target special tokens ------------------------------------------ +# +# {active}/@/{current} need a client with a session; with only a detached +# command client they must error cleanly (regression: this used to crash the +# server via a NULL session dereference). {mouse}/= need a mouse event. +check_fail "no current client" "{active}" +check_fail "no current client" "@" +check_fail "no current client" "{current}" +check_fail "no mouse target" "{mouse}" +check_fail "no mouse target" "=" +assert_alive "after whole-target special tokens" + +# --- CMD_FIND_WINDOW_INDEX rejects a pane part ---------------------------- +out=$($TMUX new-window -d -t 'alpha:1.%0' 2>&1) +[ $? -ne 0 ] || { echo "new-window with pane target succeeded"; exit 1; } +[ "$out" = "can't specify pane here" ] || \ + { echo "wrong pane-here error: '$out'"; exit 1; } + +# --- window index targets: offsets resolve to an index -------------------- +# +# new-window's -t is a window index (CMD_FIND_WINDOW_INDEX); an offset from +# the current window (0) picks the numeric index rather than an existing +# window, so "+6" creates window 6. +check_ok select-window -t alpha:0 +check_ok new-window -d -t 'alpha:+6' -n offwin +check "alpha:6" "offwin" '#{window_name}' +check_ok kill-window -t alpha:6 + +# --- -s versus -t resolution ---------------------------------------------- +# +# link-window takes a source window (-s) and a destination index (-t); each +# side is resolved independently by cmd-find. +check_ok new-session -d -s src -x 80 -y 24 +check_ok new-window -d -t src: -n payload +check_ok link-window -s src:payload -t alpha:9 +check "alpha:9" "payload" '#{window_name}' +# move-window relocates it; the old index must be gone. +check_ok move-window -s alpha:9 -t alpha:5 +check "alpha:5" "payload" '#{window_name}' +check_fail "can't find window: 9" "alpha:9" + +# --- default state with no client ----------------------------------------- +# +# run-shell with no target and no attached client has cmd-find build the +# current state from nothing (the best session). +check_ok run-shell 'true' + +assert_alive "after target tests" + +$TMUX kill-server 2>/dev/null +exit 0 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.result b/regress/utf8-test.result index e700cb17f..67ed5cb52 100644 --- a/regress/utf8-test.result +++ b/regress/utf8-test.result @@ -90,7 +90,6 @@ You should see the Greek word 'kosme': "κόσμε" 2.3.2 U-0000E000 = ee 80 80 = "" | 2.3.3 U-0000FFFD = ef bf bd = "�" | 2.3.4 U-0010FFFF = f4 8f bf bf = "􏿿" | -2.3.5 U-00110000 = f4 90 80 80 = "�" | | 3 Malformed sequences | | 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/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/resize.c b/resize.c index 6cbc938dd..19a940d61 100644 --- a/resize.c +++ b/resize.c @@ -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) diff --git a/screen-redraw.c b/screen-redraw.c index 1cd314bb7..21499d426 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1655,12 +1655,14 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) if (wp != NULL) { if (wp->base.mode & MODE_SYNC) screen_write_stop_sync(wp); + screen_write_clear_dirty(wp); } else { TAILQ_FOREACH(loop, &scene->w->panes, entry) { if (!window_pane_is_visible(loop)) continue; if (loop->base.mode & MODE_SYNC) screen_write_stop_sync(loop); + screen_write_clear_dirty(loop); } } } diff --git a/screen-write.c b/screen-write.c index a9dbe4501..c198ea226 100644 --- a/screen-write.c +++ b/screen-write.c @@ -38,6 +38,7 @@ static int screen_write_overwrite(struct screen_write_ctx *, struct grid_cell *, u_int); static int screen_write_combine(struct screen_write_ctx *, const struct grid_cell *); +static void screen_write_flush_dirty(struct window_pane *); struct screen_write_citem { u_int x; @@ -214,6 +215,48 @@ screen_write_pane_is_obscured(struct screen_write_ctx *ctx) return (0); } +/* Should we draw to the TTY? */ +static int +screen_write_should_draw_lines(struct screen_write_ctx *ctx, u_int y, u_int ny) +{ + struct window_pane *wp = ctx->wp; + struct screen *s = ctx->s; + u_int sy = screen_size_y(s); + bitstr_t *bs; + + if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP))) + return (0); + if (s->mode & MODE_SYNC) { + if (wp != NULL && y < sy && ny != 0) { + bs = wp->sync_dirty; + if (ny > sy - y) + ny = sy - y; + if (bs == NULL || wp->sync_dirty_size != sy) { + if (bs != NULL && wp->sync_dirty_size != sy) { + y = 0; + ny = sy; + } + free(bs); + + bs = wp->sync_dirty = bit_alloc(sy); + if (bs == NULL) + fatal("bit_alloc failed"); + wp->sync_dirty_size = sy; + } + bit_nset(bs, y, y + ny - 1); + } + return (0); + } + return (1); +} + +/* Should we draw this line to the TTY? */ +static int +screen_write_should_draw_line(struct screen_write_ctx *ctx, u_int y) +{ + return (screen_write_should_draw_lines(ctx, y, 1)); +} + /* Set up context for TTY command. */ static void screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, @@ -979,7 +1022,7 @@ screen_write_sync_callback(__unused int fd, __unused short events, void *arg) if (wp->base.mode & MODE_SYNC) { wp->base.mode &= ~MODE_SYNC; - wp->flags |= PANE_REDRAW; + screen_write_flush_dirty(wp); } } @@ -1004,14 +1047,14 @@ screen_write_start_sync(struct window_pane *wp) void screen_write_stop_sync(struct window_pane *wp) { - if (wp == NULL) + if (wp == NULL || (~wp->base.mode & MODE_SYNC)) return; if (event_initialized(&wp->sync_timer)) evtimer_del(&wp->sync_timer); wp->base.mode &= ~MODE_SYNC; - wp->flags |= PANE_REDRAW; + screen_write_flush_dirty(wp); log_debug("%s: %%%u stopped sync mode", __func__, wp->id); } @@ -1166,9 +1209,6 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, struct visible_ranges *r; struct visible_range *ri; - if (s->mode & MODE_SYNC) - return; - r = window_visible_ranges(wp, xoff, yoff + yy, sx, NULL); for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; @@ -1207,6 +1247,44 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, } } +/* Redraw dirty lines. */ +static void +screen_write_flush_dirty(struct window_pane *wp) +{ + struct screen_write_ctx ctx; + struct tty_ctx ttyctx; + struct screen *s = &wp->base; + u_int y, sy = screen_size_y(s), lines = 0; + + if (wp->sync_dirty == NULL) + return; + + screen_write_start_pane(&ctx, wp, s); + screen_write_initctx(&ctx, &ttyctx, 1, 1); + + for (y = 0; y < sy; y++) { + if (bit_test(wp->sync_dirty, y)) { + screen_write_redraw_line(&ctx, &ttyctx, y); + lines++; + } + } + log_debug("%s: %%%u had %u dirty lines", __func__, wp->id, lines); + + screen_write_stop(&ctx); + screen_write_clear_dirty(wp); +} + +/* Clear any dirty lines. */ +void +screen_write_clear_dirty(struct window_pane *wp) +{ + if (wp != NULL && wp->sync_dirty != NULL) { + free(wp->sync_dirty); + wp->sync_dirty = NULL; + wp->sync_dirty_size = 0; + } +} + /* Redraw all visible cells in a pane. */ static void screen_write_redraw_pane(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx) @@ -1249,7 +1327,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) screen_write_initctx(ctx, &ttyctx, 1, 1); - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, 0, screen_size_y(s))) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_alignmenttest, &ttyctx); @@ -1290,7 +1368,7 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = nx; - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_line(ctx, s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_insertcharacter, &ttyctx); @@ -1331,7 +1409,7 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = nx; - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_line(ctx, s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_deletecharacter, &ttyctx); @@ -1372,7 +1450,7 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = nx; - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_line(ctx, s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_clearcharacter, &ttyctx); @@ -1413,7 +1491,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = ny; - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_insertline, &ttyctx); @@ -1440,7 +1518,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = ny; - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, s->cy, s->rlower + 1 - s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_insertline, &ttyctx); @@ -1457,7 +1535,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) struct screen *s = ctx->s; struct grid *gd = s->grid; struct tty_ctx ttyctx; - u_int sy = screen_size_y(s); + u_int sy = screen_size_y(s), ry; if (ny == 0) ny = 1; @@ -1481,7 +1559,8 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = ny; - if (s->mode & MODE_SYNC) + ry = s->rlower + 1 - s->rupper; + if (!screen_write_should_draw_lines(ctx, s->rupper, ry)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_deleteline, &ttyctx); @@ -1492,8 +1571,9 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) return; } - if (ny > s->rlower + 1 - s->cy) - ny = s->rlower + 1 - s->cy; + ry = s->rlower + 1 - s->cy; + if (ny > ry) + ny = ry; if (ny == 0) return; @@ -1508,7 +1588,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = ny; - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, s->cy, ry)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_deleteline, &ttyctx); @@ -1642,29 +1722,34 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg) { struct screen *s = ctx->s; struct tty_ctx ttyctx; + u_int ry; + + if (s->cy != s->rupper) { + if (s->cy > 0) + screen_write_set_cursor(ctx, -1, s->cy - 1); + return; + } - if (s->cy == s->rupper) { #ifdef ENABLE_SIXEL - if (image_free_all(s) && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; + if (image_free_all(s) && ctx->wp != NULL) + ctx->wp->flags |= PANE_REDRAW; #endif - grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg); - screen_write_collect_flush(ctx, 0, __func__); + grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg); + screen_write_collect_flush(ctx, 0, __func__); - screen_write_initctx(ctx, &ttyctx, 1, 1); - ttyctx.bg = bg; + screen_write_initctx(ctx, &ttyctx, 1, 1); + ttyctx.bg = bg; - if (s->mode & MODE_SYNC) - return; - if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { - tty_write(tty_cmd_reverseindex, &ttyctx); - return; - } + ry = s->rlower + 1 - s->rupper; + if (!screen_write_should_draw_lines(ctx, s->rupper, ry)) + return; + if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { + tty_write(tty_cmd_reverseindex, &ttyctx); + return; + } - screen_write_redraw_pane(ctx, &ttyctx); - } else if (s->cy > 0) - screen_write_set_cursor(ctx, -1, s->cy - 1); + screen_write_redraw_pane(ctx, &ttyctx); } /* Set scroll region. */ @@ -1714,20 +1799,24 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg) ctx->bg = bg; } - if (s->cy == s->rlower) { + if (s->cy != s->rlower) { + if (s->cy < screen_size_y(s) - 1) + screen_write_set_cursor(ctx, -1, s->cy + 1); + return; + } + #ifdef ENABLE_SIXEL - if (rlower == screen_size_y(s) - 1) - redraw = image_scroll_up(s, 1); - else - redraw = image_check_line(s, rupper, rlower - rupper); - if (redraw && ctx->wp != NULL) - ctx->wp->flags |= PANE_REDRAW; + if (rlower == screen_size_y(s) - 1) + redraw = image_scroll_up(s, 1); + else + redraw = image_check_line(s, rupper, rlower - rupper); + if (redraw && ctx->wp != NULL) + ctx->wp->flags |= PANE_REDRAW; #endif - grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); - screen_write_collect_scroll(ctx, bg); - ctx->scrolled++; - } else if (s->cy < screen_size_y(s) - 1) - screen_write_set_cursor(ctx, -1, s->cy + 1); + + grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); + screen_write_collect_scroll(ctx, bg); + ctx->scrolled++; } /* Scroll up. */ @@ -1767,7 +1856,7 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg) struct screen *s = ctx->s; struct grid *gd = s->grid; struct tty_ctx ttyctx; - u_int i; + u_int i, ry; screen_write_initctx(ctx, &ttyctx, 1, 1); ttyctx.bg = bg; @@ -1788,7 +1877,8 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg) screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = lines; - if (s->mode & MODE_SYNC) + ry = s->rlower + 1 - s->rupper; + if (!screen_write_should_draw_lines(ctx, s->rupper, ry)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) { tty_write(tty_cmd_scrolldown, &ttyctx); @@ -1841,7 +1931,7 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_collect_clear(ctx, s->cy + 1, sy - (s->cy + 1)); screen_write_collect_flush(ctx, 0, __func__); - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) { tty_write(tty_cmd_clearendofscreen, &ttyctx); @@ -1916,7 +2006,7 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_collect_clear(ctx, 0, s->cy); screen_write_collect_flush(ctx, 0, __func__); - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, 0, s->cy + 1)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) { tty_write(tty_cmd_clearstartofscreen, &ttyctx); @@ -1989,7 +2079,7 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_collect_clear(ctx, 0, sy); - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_lines(ctx, 0, sy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) { tty_write(tty_cmd_clearscreen, &ttyctx); @@ -2303,12 +2393,25 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only, const char *from) { struct screen *s = ctx->s; + struct window_pane *wp = ctx->wp; u_int y, cx, cy, items = 0; struct screen_write_citem *ci, *tmp; struct screen_write_cline *cl; - if (s->mode & MODE_SYNC) + 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)) + screen_write_should_draw_line(ctx, y); + } + goto discard; + } if (ctx->scrolled != 0) { if (!screen_write_collect_flush_scrolled(ctx)) @@ -2646,12 +2749,12 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) if (s->mode & MODE_INSERT) { screen_write_collect_flush(ctx, 0, __func__); ttyctx.n = width; - if (~s->mode & MODE_SYNC) + if (screen_write_should_draw_line(ctx, s->cy)) tty_write(tty_cmd_insertcharacter, &ttyctx); } /* If not writing, done now. */ - if (skip || s->mode & MODE_SYNC) + if (skip || !screen_write_should_draw_line(ctx, s->cy)) return; /* Do a full line redraw if needed. */ @@ -2671,7 +2774,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) for (i = 0, vis = 0; i < r->used; i++) vis += r->ranges[i].nx; if (vis >= width) { - if (~s->mode & MODE_SYNC) + if (screen_write_should_draw_line(ctx, s->cy)) tty_write(tty_cmd_cell, &ttyctx); return; } @@ -2681,7 +2784,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) * spaces in the visible regions. */ utf8_set(&tmp_gc.data, ' '); - if (s->mode & MODE_SYNC) + if (!screen_write_should_draw_line(ctx, s->cy)) return; for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; @@ -2824,7 +2927,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) ttyctx.cell = &last; if (force_wide) ttyctx.flags |= TTY_CTX_CELL_INVALIDATE; - if (~s->mode & MODE_SYNC) + if (screen_write_should_draw_line(ctx, cy)) tty_write(tty_cmd_cell, &ttyctx); screen_write_set_cursor(ctx, cx, cy); diff --git a/screen.c b/screen.c index 8e513758e..62a079270 100644 --- a/screen.c +++ b/screen.c @@ -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); - + if (check) + grid_check_is_clear(s->grid); grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8); screen_clear_selection(s); diff --git a/server-client.c b/server-client.c index f170396ec..52c3c7f2d 100644 --- a/server-client.c +++ b/server-client.c @@ -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) @@ -2048,19 +2050,18 @@ server_client_reset_state(struct client *c) if (!window_position_is_visible(r, cx)) cursor = 0; - if (window_pane_scrollbar_overlay_visible(wp)) { - sb_w = wp->scrollbar_style.width; - if (sb_w > wp->sx) - sb_w = wp->sx; - if (sb_w != 0 && - w->sb_pos == - PANE_SCROLLBARS_LEFT) { - if (s->cx < sb_w) + if (window_pane_scrollbar_overlay_visible(wp)) { + sb_w = wp->scrollbar_style.width; + if (sb_w > wp->sx) + sb_w = wp->sx; + if (sb_w != 0 && + w->sb_pos == PANE_SCROLLBARS_LEFT) { + if (s->cx < sb_w) + cursor = 0; + } else if (sb_w != 0 && + s->cx >= wp->sx - sb_w) cursor = 0; - } else if (sb_w != 0 && - s->cx >= wp->sx - sb_w) - cursor = 0; - } + } if (status_at_line(c) == 0) cy += status_line_size(c); @@ -2089,10 +2090,10 @@ server_client_reset_state(struct client *c) mode |= MODE_MOUSE_ALL; } } - if (options_get_number(oo, "focus-follows-mouse") || - w->sb == PANE_SCROLLBARS_MODAL || - w->sb == PANE_SCROLLBARS_AUTOHIDE) - mode |= MODE_MOUSE_ALL; + if (options_get_number(oo, "focus-follows-mouse") || + w->sb == PANE_SCROLLBARS_MODAL || + w->sb == PANE_SCROLLBARS_AUTOHIDE) + mode |= MODE_MOUSE_ALL; else if (~mode & MODE_MOUSE_ALL) mode |= MODE_MOUSE_BUTTON; } diff --git a/spawn.c b/spawn.c index 59e107292..e0cac4bc1 100644 --- a/spawn.c +++ b/spawn.c @@ -104,7 +104,7 @@ spawn_window(struct spawn_context *sc, char **cause) sc->wp0 = TAILQ_FIRST(&w->panes); TAILQ_REMOVE(&w->panes, sc->wp0, entry); - layout_free(w); + layout_free(w, 0); window_destroy_panes(w); TAILQ_INSERT_HEAD(&w->panes, sc->wp0, entry); @@ -264,14 +264,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 +393,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) { @@ -625,13 +632,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 +662,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..8f7c209e1 100644 --- a/status.c +++ b/status.c @@ -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/tmux.1 b/tmux.1 index 054464527..2b36cb7cd 100644 --- a/tmux.1 +++ b/tmux.1 @@ -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 , @@ -2770,7 +2770,7 @@ The pane must not already be floating or hidden, and the window must not be zoomed. .Tg capturep .It Xo Ic capture\-pane -.Op Fl aeFHLpPqCJMN +.Op Fl aeFHLpPRqCJMN .Op Fl b Ar buffer\-name .Op Fl E Ar end\-line .Op Fl S Ar start\-line @@ -2830,6 +2830,8 @@ With .Fl H , only hyperlinks in the specified lines are captured. Multiple hyperlinks on the same line are separated by spaces. +.Fl R +dumps the internal grid data for diagnostics. .Pp .Fl S and @@ -3247,6 +3249,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 @@ -3514,7 +3525,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 @@ -3538,6 +3549,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 @@ -3837,7 +3851,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 @@ -3854,6 +3868,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 @@ -3863,7 +3880,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 @@ -3880,6 +3897,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 @@ -4556,6 +4576,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 @@ -4667,7 +4691,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 @@ -6268,11 +6292,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 @@ -6377,6 +6401,7 @@ Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook .Op Fl agpRuw +.Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command @@ -6393,18 +6418,53 @@ 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. +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 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 @@ -6696,6 +6756,12 @@ results in replaces a .Nm colour by its six-digit hexadecimal RGB value. +If an argument of +.Ql f +or +.Ql b +is given, it will instead produce the SGR escape sequence to set the foreground +or background colour respectively. .Pp A limit may be placed on the length of the resultant string by prefixing it by an @@ -6747,7 +6813,7 @@ 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 +.Ql \&1m or .Ql 2m23s . A custom format may be given using an @@ -6805,7 +6871,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} @@ -6839,7 +6907,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. @@ -6865,6 +6939,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 @@ -7001,10 +7102,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)" @@ -7078,6 +7182,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" @@ -7163,6 +7268,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" @@ -7714,10 +7821,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 @@ -7852,6 +7956,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 @@ -7862,6 +7967,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" diff --git a/tmux.h b/tmux.h index 863a41eca..db46e6fb1 100644 --- a/tmux.h +++ b/tmux.h @@ -99,6 +99,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 @@ -1262,6 +1265,7 @@ struct visible_ranges { /* Child window structure. */ struct window_pane { u_int id; + int references; u_int active_point; struct window *window; @@ -1293,6 +1297,10 @@ struct window_pane { #define PANE_THEMECHANGED 0x2000 #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 +#define PANE_DESTROYED 0x10000 + + bitstr_t *sync_dirty; + u_int sync_dirty_size; u_int sb_slider_y; u_int sb_slider_h; @@ -1497,32 +1505,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; }; @@ -2302,6 +2309,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; @@ -2312,14 +2322,26 @@ 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 +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 *); /* Key binding and key table. */ struct key_binding { @@ -2640,6 +2662,12 @@ char *format_trim_right(const char *, u_int); /* notify.c */ void notify_hook(struct cmdq_item *, const char *); +void notify_monitor_add(struct cmdq_item *, struct options *, + const char *, enum monitor_type, int, const char *, + struct cmd_find_state *, struct session *); +void notify_monitor_remove(struct options *, const char *); +void notify_monitor_free(void *); +char *notify_monitor_to_string(struct options_entry *); void notify_client(const char *, struct client *); void notify_session(const char *, struct session *); void notify_winlink(const char *, struct winlink *); @@ -2662,29 +2690,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_list *options_get_command(struct options *, const char *); @@ -2707,7 +2739,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 */ @@ -3299,6 +3331,7 @@ void colour_split_rgb(int, u_char *, u_char *, u_char *); int colour_force_rgb(int); int colour_dim(int, u_int); const char *colour_tostring(int); +const char *colour_toescape(struct client *, int, int); enum client_theme colour_totheme(int); int colour_fromstring(const char *); const char *colour_theme_option(u_int, enum client_theme); @@ -3323,6 +3356,7 @@ bitstr_t *fuzzy_match(const char *, const char *, u_int, u_int *); /* grid.c */ extern const struct grid_cell grid_default_cell; +void grid_check_is_clear(struct grid *); void grid_empty_line(struct grid *, u_int, u_int); void grid_set_tab(struct grid_cell *, u_int); int grid_cells_equal(const struct grid_cell *, const struct grid_cell *); @@ -3332,6 +3366,9 @@ struct grid *grid_create(u_int, u_int, u_int); void grid_destroy(struct grid *); void grid_free_lines(struct grid *, u_int, u_int); int grid_compare(struct grid *, struct grid *); +const char *grid_line_flags_string(int); +const char *grid_cell_flags_string(int); +const char *grid_cell_attr_string(int); void grid_collect_history(struct grid *, int); void grid_remove_history(struct grid *, u_int ); void grid_scroll_history(struct grid *, u_int); @@ -3440,6 +3477,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int); void screen_write_mode_clear(struct screen_write_ctx *, int); void screen_write_start_sync(struct window_pane *); void screen_write_stop_sync(struct window_pane *); +void screen_write_clear_dirty(struct window_pane *); void screen_write_cursorup(struct screen_write_ctx *, u_int); void screen_write_cursordown(struct screen_write_ctx *, u_int); void screen_write_cursorright(struct screen_write_ctx *, u_int); @@ -3493,7 +3531,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 *); @@ -3621,6 +3659,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 *, @@ -3674,17 +3714,18 @@ struct visible_ranges *window_visible_ranges(struct window_pane *, int, int, /* layout.c */ u_int layout_count_cells(struct layout_cell *); struct layout_cell *layout_create_cell(struct layout_cell *); -void layout_free_cell(struct layout_cell *); +void layout_free_cell(struct layout_cell *, int); void layout_print_cell(struct layout_cell *, const char *, u_int); void layout_destroy_cell(struct window *, struct layout_cell *, 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 *); +int layout_cell_is_tiled(struct layout_cell *); void layout_fix_offsets(struct window *); void layout_fix_panes(struct window *, struct window_pane *); void layout_resize_adjust(struct window *, struct layout_cell *, @@ -3693,7 +3734,7 @@ void layout_resize_set_size(struct window *, struct layout_cell *, enum layout_type, u_int); struct layout_cell *layout_cell_get_neighbour(struct layout_cell *); void layout_init(struct window *, struct window_pane *); -void layout_free(struct window *); +void layout_free(struct window *, int); void layout_resize(struct window *, u_int, u_int); void layout_resize_pane(struct window_pane *, enum layout_type, int, int); @@ -3714,7 +3755,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 *); @@ -3722,10 +3763,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 *); @@ -3834,6 +3875,16 @@ 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 *, u_int); +void monitor_remove(struct monitor_set *, const char *); + /* control.c */ void control_discard(struct client *); void control_start(struct client *); @@ -3849,8 +3900,8 @@ 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 */ diff --git a/tty-features.c b/tty-features.c index 6e168758f..b1f8032d6 100644 --- a/tty-features.c +++ b/tty-features.c @@ -589,6 +589,7 @@ tty_default_features(int *feat, const char *name, u_int version) "cstyle," "extkeys," "focus," + "overline," "hyperlinks," "osc7," "sync," diff --git a/tty-keys.c b/tty-keys.c index f48fb1b71..d6a2610ea 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -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; } diff --git a/window-client.c b/window-client.c index a780222e3..5a8c71862 100644 --- a/window-client.c +++ b/window-client.c @@ -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-customize.c b/window-customize.c index 32caae9a3..09da37f2e 100644 --- a/window-customize.c +++ b/window-customize.c @@ -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, @@ -653,12 +654,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; @@ -669,7 +670,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) @@ -708,25 +709,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); @@ -820,7 +821,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, @@ -831,7 +832,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)) @@ -896,6 +897,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", @@ -1007,8 +1010,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); @@ -1020,13 +1025,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) @@ -1056,10 +1063,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; @@ -1142,25 +1150,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, @@ -1173,6 +1182,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) @@ -1185,9 +1272,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 @@ -1199,14 +1287,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); } } @@ -1452,7 +1540,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; @@ -1462,6 +1550,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) @@ -1490,7 +1583,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++; @@ -1519,10 +1612,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-tree.c b/window-tree.c index 878d050b8..98898e01a 100644 --- a/window-tree.c +++ b/window-tree.c @@ -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.c b/window.c index 341aefd4e..1b2563b5e 100644 --- a/window.c +++ b/window.c @@ -71,6 +71,7 @@ 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); @@ -331,6 +332,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); @@ -357,8 +360,8 @@ window_destroy(struct window *w) window_unzoom(w, 0); RB_REMOVE(windows, &windows, w); - layout_free_cell(w->layout_root); - layout_free_cell(w->saved_layout_root); + layout_free_cell(w->layout_root, 0); + layout_free_cell(w->saved_layout_root, 0); free(w->old_layout); window_destroy_panes(w); @@ -419,6 +422,25 @@ 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) { @@ -785,7 +807,7 @@ window_unzoom(struct window *w, int notify) return (-1); w->flags &= ~WINDOW_ZOOMED; - layout_free(w); + layout_free(w, 0); w->layout_root = w->saved_layout_root; w->saved_layout_root = NULL; @@ -1080,6 +1102,7 @@ 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; @@ -1208,9 +1231,11 @@ window_pane_destroy(struct window_pane *wp) spawn_editor_finish(wp); window_pane_clear_prompt(wp); + RB_REMOVE(window_pane_tree, &all_window_panes, wp); + wp->flags |= PANE_DESTROYED; window_pane_free_modes(wp); - free(wp->searchstr); + screen_write_clear_dirty(wp); if (wp->fd != -1) { #ifdef HAVE_UTEMPTER @@ -1218,18 +1243,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)) @@ -1240,7 +1267,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);