diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml new file mode 100644 index 000000000..883ee720e --- /dev/null +++ b/.github/workflows/regress.yml @@ -0,0 +1,75 @@ +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 + # - name: ubuntu-24.04-arm64 + # runner: ubuntu-24.04-arm + # make: make + # - name: macos-26-arm64 + # runner: macos-26 + # make: gmake + + 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 --enable-utf8proc + ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)" + + - name: test + run: | + cd regress + ${{ matrix.make }} 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/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-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-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..672a3c84d 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]", diff --git a/grid.c b/grid.c index 63b3b9d3a..bc7abd4ed 100644 --- a/grid.c +++ b/grid.c @@ -1712,3 +1712,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/layout-custom.c b/layout-custom.c index 0fc3f5d16..c5a348ac0 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -265,7 +265,7 @@ layout_parse(struct window *w, const char *layout, char **cause) window_resize(w, tiled_lc->sx, tiled_lc->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); } @@ -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..cf78585c2 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->sx = w->sx; + lcchild->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->sx, lcroot->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->sx, lcroot->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->sx, lcroot->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->sx, lcroot->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->sx, lcroot->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->sx, lcroot->sy, -1, -1); notify_window("window-layout-changed", w); server_redraw_window(w); } diff --git a/layout.c b/layout.c index c38197b44..2a605d6cc 100644 --- a/layout.c +++ b/layout.c @@ -88,20 +88,24 @@ layout_create_cell(struct layout_cell *lcparent) /* 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: @@ -255,7 +259,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; @@ -699,13 +703,13 @@ 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; } @@ -721,7 +725,7 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc, /* Remove this from the parent's list. */ TAILQ_REMOVE(&lcparent->cells, lc, entry); - layout_free_cell(lc); + layout_free_cell(lc, 0); out: /* @@ -742,7 +746,7 @@ out: } else TAILQ_REPLACE(&lc->parent->cells, lcparent, lc, entry); - layout_free_cell(lcparent); + layout_free_cell(lcparent, 0); } } @@ -760,9 +764,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. */ @@ -1507,7 +1511,8 @@ 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); @@ -1535,6 +1540,8 @@ 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; diff --git a/regress/Makefile b/regress/Makefile index e6c3619fc..1a9ba55cb 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -3,8 +3,31 @@ TESTS!= echo *.sh .PHONY: all $(TESTS) .NOTPARALLEL: all $(TESTS) -all: $(TESTS) +all: + @failed=0; failures=; \ + for test in $(TESTS); do \ + printf '%-40s ' "$$test"; \ + start=$$(date +%s); \ + if sh "$$test" >/dev/null 2>&1; then \ + end=$$(date +%s); \ + echo "PASS ($$((end - start))s)"; \ + else \ + end=$$(date +%s); \ + echo "FAIL ($$((end - start))s)"; \ + 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; \ + fi; \ + exit $$failed $(TESTS): - sh $*.sh + sh $@ sleep 1 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/check-names.sh b/regress/check-names.sh index 21e185ac1..886299f2d 100644 --- a/regress/check-names.sh +++ b/regress/check-names.sh @@ -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/environ-update.sh b/regress/environ-update.sh new file mode 100644 index 000000000..59f7d74c5 --- /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 -Ltest -f/dev/null" +# A second server on its own socket hosts the pane that runs the inner client. +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +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..244ec367d --- /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 -Ltest -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/format-modifiers.sh b/regress/format-modifiers.sh new file mode 100644 index 000000000..131b6527a --- /dev/null +++ b/regress/format-modifiers.sh @@ -0,0 +1,565 @@ +#!/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 +export TZ + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -Ltest -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: + + +# --- 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/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" +# A pane-sort argument is accepted; for panes only the r (reverse) suffix has an +# effect, so these all keep the count and exercise the argument branch. +test_format "#{P/i:x}" "xxx" "zeta:charlie" +test_format "#{P/n:x}" "xxx" "zeta:charlie" +test_format "#{P/t:x}" "xxx" "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..8032f8a57 --- /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 -Ltest -f/dev/null" +TMUX2="$TEST_TMUX -Ltest2 -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-variables.sh b/regress/format-variables.sh new file mode 100644 index 000000000..dccc0e390 --- /dev/null +++ b/regress/format-variables.sh @@ -0,0 +1,388 @@ +#!/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 -Ltest -f/dev/null" +# A second server on its own socket provides a real terminal (an inner client +# attached inside one of its panes) so client terminal variables are populated. +TMUX2="$TEST_TMUX -Ltest2 -f/dev/null" + +# 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_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" + +# 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}." + +cleanup +exit 0 diff --git a/regress/input-common.inc b/regress/input-common.inc new file mode 100644 index 000000000..96eec1f09 --- /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 -Ltest -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-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-replies.sh b/regress/input-replies.sh new file mode 100644 index 000000000..9baed827d --- /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 -Ltest -f/dev/null" + +$TMUX kill-server 2>/dev/null +sleep 0.1 + +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.3 + +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 0.7 + 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' 8 '' +query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 8 '\033[4h' +query "decrqm-cursor-keys-reset" '^[[?1;2$y' '\033[?1$p' 9 '' +query "decrqm-cursor-keys-set" '^[[?1;1$y' '\033[?1$p' 9 '\033[?1h' +query "decrqm-columns" '^[[?3;4$y' '\033[?3$p' 9 '' +query "decrqm-origin-reset" '^[[?6;2$y' '\033[?6$p' 9 '' +query "decrqm-origin-set" '^[[?6;1$y' '\033[?6$p' 9 '\033[?6h' +query "decrqm-wrap-set" '^[[?7;1$y' '\033[?7$p' 9 '' +query "decrqm-wrap-reset" '^[[?7;2$y' '\033[?7$p' 9 '\033[?7l' +query "decrqm-cursor-visible-set" '^[[?25;1$y' '\033[?25$p' 10 '' +query "decrqm-cursor-visible-reset" '^[[?25;2$y' '\033[?25$p' 10 '\033[?25l' +query "decrqm-mouse-standard-set" '^[[?1000;1$y' '\033[?1000$p' 12 '\033[?1000h' +query "decrqm-mouse-button-set" '^[[?1002;1$y' '\033[?1002$p' 12 '\033[?1002h' +query "decrqm-mouse-all-set" '^[[?1003;1$y' '\033[?1003$p' 12 '\033[?1003h' +query "decrqm-focus-set" '^[[?1004;1$y' '\033[?1004$p' 12 '\033[?1004h' +query "decrqm-mouse-utf8-set" '^[[?1005;1$y' '\033[?1005$p' 12 '\033[?1005h' +query "decrqm-mouse-sgr-set" '^[[?1006;1$y' '\033[?1006$p' 12 '\033[?1006h' +query "decrqm-bracket-paste-set" '^[[?2004;1$y' '\033[?2004$p' 12 '\033[?2004h' +query "decrqm-theme-updates-set" '^[[?2031;1$y' '\033[?2031$p' 12 '\033[?2031h' +query "decrqss-cursor-style" '^[P1$r q0 q' '\033P$q q\033\\' 10 '' + +query_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..d9186c673 --- /dev/null +++ b/regress/input-requests.sh @@ -0,0 +1,117 @@ +#!/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] +server = [tmux, "-Ltest", "-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, "-Ltest", "-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=64 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=64 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/options-array.sh b/regress/options-array.sh new file mode 100644 index 000000000..3b18ce977 --- /dev/null +++ b/regress/options-array.sh @@ -0,0 +1,161 @@ +#!/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 indexed by integer. This exercises: setting a whole array +# from a separator-delimited string; per-index set with option[N]; -a append +# (which lands at the next free index); show ordering by ascending index and +# preservation of gaps; per-index unset with -u; show -v of a single index and +# of a missing index; and per-option separators (user-keys splits only on +# comma, update-environment on space or comma). +# +# 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 -Ltest -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 indices starting at 0. +check_ok set -g update-environment "AAA BBB,CCC" +check_array "-g update-environment" "update-environment[0] AAA +update-environment[1] BBB +update-environment[2] CCC" + +# --- -a append goes to the next free index -------------------------------- +check_ok set -ga update-environment "DDD" +check_array "-g update-environment" "update-environment[0] AAA +update-environment[1] BBB +update-environment[2] CCC +update-environment[3] DDD" + +# --- per-index unset leaves a gap; show preserves order and gaps ---------- +check_ok set -gu update-environment[1] +check_array "-g update-environment" "update-environment[0] AAA +update-environment[2] CCC +update-environment[3] DDD" +# show -v of an existing index returns its value; a missing index is empty. +check_value "-gv update-environment[0]" "AAA" +check_value "-gv update-environment[1]" "" + +# --- explicit indexed set, including out-of-order and gaps ---------------- +# +# status-format is a session array; assigning an empty string first clears its +# multi-index default, then set specific indices out of order and confirm show +# sorts by ascending index and keeps the gap at [1]. +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_array "-g status-format" "status-format[0] zero +status-format[2] two +status-format[5] five" + +# --- 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: an indexed 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 {" + +# --- 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 index ------------------------- +check_ok set -g command-alias[9] "x=list-keys" +check_fail "already set: command-alias[9]" set -go command-alias[9] "y=list-keys" + +# --- non-array option rejects index syntax -------------------------------- +# +# 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..bbc7168aa --- /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 -Ltest -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..28acd2b47 --- /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 -Ltest -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/prompt-keys.sh b/regress/prompt-keys.sh index 7fa7a673b..8c17581b1 100644 --- a/regress/prompt-keys.sh +++ b/regress/prompt-keys.sh @@ -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/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/session-group-resize.sh b/regress/session-group-resize.sh old mode 100755 new mode 100644 diff --git a/regress/targets-panes.sh b/regress/targets-panes.sh new file mode 100644 index 000000000..db353403b --- /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 -Ltest -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..346a86736 --- /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 -Ltest -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/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/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 cdeb2e611..403e7f12c 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,20 +215,48 @@ screen_write_pane_is_obscured(struct screen_write_ctx *ctx) return (0); } -/* Should we draw to TTY for this screen? */ +/* Should we draw to the TTY? */ static int -screen_write_should_draw(struct screen_write_ctx *ctx) +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 (s->mode & MODE_SYNC) - return (0); 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, @@ -1010,7 +1039,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); } } @@ -1042,7 +1071,7 @@ screen_write_stop_sync(struct window_pane *wp) 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); } @@ -1197,9 +1226,6 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx, struct visible_ranges *r; struct visible_range *ri; - if (!screen_write_should_draw(ctx)) - return; - r = window_visible_ranges(wp, xoff, yoff + yy, sx, NULL); for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; @@ -1238,6 +1264,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) @@ -1280,7 +1344,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) screen_write_initctx(ctx, &ttyctx, 1, 1); - if (!screen_write_should_draw(ctx)) + 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); @@ -1321,7 +1385,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 (!screen_write_should_draw(ctx)) + 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); @@ -1362,7 +1426,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 (!screen_write_should_draw(ctx)) + 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); @@ -1403,7 +1467,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 (!screen_write_should_draw(ctx)) + 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); @@ -1444,7 +1508,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 (!screen_write_should_draw(ctx)) + 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); @@ -1471,7 +1535,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 (!screen_write_should_draw(ctx)) + 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); @@ -1488,7 +1552,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; @@ -1512,7 +1576,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 (!screen_write_should_draw(ctx)) + 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); @@ -1523,8 +1588,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; @@ -1539,7 +1605,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 (!screen_write_should_draw(ctx)) + 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); @@ -1673,29 +1739,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 (!screen_write_should_draw(ctx)) - 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. */ @@ -1745,20 +1816,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. */ @@ -1798,7 +1873,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; @@ -1819,7 +1894,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 (!screen_write_should_draw(ctx)) + 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); @@ -1872,7 +1948,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 (!screen_write_should_draw(ctx)) + 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); @@ -1947,7 +2023,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 (!screen_write_should_draw(ctx)) + 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); @@ -2020,7 +2096,7 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg) screen_write_collect_clear(ctx, 0, sy); - if (!screen_write_should_draw(ctx)) + if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy)) return; if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) { tty_write(tty_cmd_clearscreen, &ttyctx); @@ -2334,12 +2410,21 @@ 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 (!screen_write_should_draw(ctx)) + if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP))) goto discard; + if (s->mode & MODE_SYNC) { + 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)) @@ -2677,12 +2762,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 (screen_write_should_draw(ctx)) + if (screen_write_should_draw_line(ctx, s->cy)) tty_write(tty_cmd_insertcharacter, &ttyctx); } /* If not writing, done now. */ - if (skip || !screen_write_should_draw(ctx)) + if (skip || !screen_write_should_draw_line(ctx, s->cy)) return; /* Do a full line redraw if needed. */ @@ -2702,7 +2787,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 (screen_write_should_draw(ctx)) + if (screen_write_should_draw_line(ctx, s->cy)) tty_write(tty_cmd_cell, &ttyctx); return; } @@ -2712,7 +2797,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 (!screen_write_should_draw(ctx)) + if (!screen_write_should_draw_line(ctx, s->cy)) return; for (i = 0; i < r->used; i++) { ri = &r->ranges[i]; @@ -2855,7 +2940,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 (screen_write_should_draw(ctx)) + if (screen_write_should_draw_line(ctx, cy)) tty_write(tty_cmd_cell, &ttyctx); screen_write_set_cursor(ctx, cx, cy); diff --git a/spawn.c b/spawn.c index 59e107292..91010d775 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); diff --git a/tmux.1 b/tmux.1 index 3df357e1a..cd6e7a9d5 100644 --- a/tmux.1 +++ b/tmux.1 @@ -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 diff --git a/tmux.h b/tmux.h index af1b59793..c0f2a1c6c 100644 --- a/tmux.h +++ b/tmux.h @@ -1297,6 +1297,9 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 + bitstr_t *sync_dirty; + u_int sync_dirty_size; + u_int sb_slider_y; u_int sb_slider_h; int sb_auto_visible; @@ -3285,6 +3288,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); @@ -3393,6 +3399,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); @@ -3627,7 +3634,7 @@ 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 **); @@ -3638,6 +3645,7 @@ 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 *, @@ -3646,7 +3654,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); diff --git a/window.c b/window.c index 341aefd4e..1fb012389 100644 --- a/window.c +++ b/window.c @@ -357,8 +357,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); @@ -785,7 +785,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; @@ -1210,6 +1210,7 @@ window_pane_destroy(struct window_pane *wp) window_pane_clear_prompt(wp); window_pane_free_modes(wp); + screen_write_clear_dirty(wp); free(wp->searchstr); if (wp->fd != -1) {