Merge branch 'master' into command_parser

This commit is contained in:
Nicholas Marriott
2026-07-02 22:55:04 +01:00
61 changed files with 4195 additions and 369 deletions

75
.github/workflows/regress.yml vendored Normal file
View File

@@ -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 }}

View File

@@ -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.

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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]",

95
grid.c
View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
wp = TAILQ_NEXT(wp, entry);
while (wp != NULL && window_pane_is_floating(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);
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);
}

View File

@@ -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);
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);
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,6 +1511,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent)
number = 0;
TAILQ_FOREACH (lc, &parent->cells, entry)
if (layout_cell_is_tiled(lc))
number++;
if (number <= 1)
return (0);
@@ -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;

View File

@@ -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

Binary file not shown.

View File

@@ -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'

126
regress/environ-update.sh Normal file
View File

@@ -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

187
regress/environ.sh Normal file
View File

@@ -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" <<EOF
CFGVAR=fromconfig
%hidden CFGHID=hiddencfg
EOF
CTMUX="$TEST_TMUX -Ltest2 -f$CONF"
$CTMUX kill-server 2>/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

565
regress/format-modifiers.sh Normal file
View File

@@ -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

143
regress/format-mouse.sh Normal file
View File

@@ -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

388
regress/format-variables.sh Normal file
View File

@@ -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

176
regress/input-common.inc Normal file
View File

@@ -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
}

39
regress/input-cursor.sh Normal file
View File

@@ -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

54
regress/input-edit.sh Normal file
View File

@@ -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

View File

@@ -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 '<27>A<EFBFBD>B'
exit $exit_status

15
regress/input-modes.sh Normal file
View File

@@ -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

37
regress/input-osc.sh Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

47
regress/input-raw-edit.sh Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

39
regress/input-raw-sgr.sh Normal file
View File

@@ -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

View File

@@ -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

28
regress/input-raw-wrap.sh Normal file
View File

@@ -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

94
regress/input-replies.sh Normal file
View File

@@ -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

117
regress/input-requests.sh Normal file
View File

@@ -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

73
regress/input-scroll.sh Normal file
View File

@@ -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

26
regress/input-sgr.sh Normal file
View File

@@ -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

45
regress/input-unicode.sh Normal file
View File

@@ -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

161
regress/options-array.sh Normal file
View File

@@ -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

208
regress/options-scope.sh Normal file
View File

@@ -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

193
regress/options-values.sh Normal file
View File

@@ -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

View File

@@ -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"

View File

@@ -1,12 +1,12 @@
base 
 
 
 ┌──────────────┐ 
 │OVERSB  │ 
 │  │ 
 │  │ 
 │  │ 
 └──────────────┘ 
 ┌──────────────┐ 
 │OVERSB  │ 
 │  │ 
 │  │ 
 │  │ 
 └──────────────┘ 
 
 
 

View File

@@ -1,12 +1,12 @@
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│
│

View File

@@ -1,12 +1,12 @@
── 0:left ──────────┬── 1:right ────────
 │
│
│
│
│
│
│
│
│
│
│
── 0:left ──────────┬── 1:right ────────
 │
│
│
│
│
│
│
│
│
│
│

View File

@@ -4,7 +4,7 @@
────────────────────────────────────────
────────────────────────────────────────

View File

@@ -1,12 +1,12 @@
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │

View File

@@ -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 
 

View File

@@ -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

View File

@@ -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 

View File

@@ -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
 │

0
regress/session-group-resize.sh Executable file → Normal file
View File

150
regress/targets-panes.sh Normal file
View File

@@ -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

226
regress/targets.sh Normal file
View File

@@ -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

View File

@@ -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 = "<22>" |
2.3.4 U-0010FFFF = f4 8f bf bf = "􏿿" |
2.3.5 U-00110000 = f4 90 80 80 = "<22>" |
|
3 Malformed sequences |
|

View File

@@ -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);
}
}
}

View File

@@ -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,8 +1739,14 @@ 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;
@@ -1686,7 +1758,8 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
screen_write_initctx(ctx, &ttyctx, 1, 1);
ttyctx.bg = bg;
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_reverseindex, &ttyctx);
@@ -1694,8 +1767,6 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
}
screen_write_redraw_pane(ctx, &ttyctx);
} else if (s->cy > 0)
screen_write_set_cursor(ctx, -1, s->cy - 1);
}
/* Set scroll region. */
@@ -1745,7 +1816,12 @@ 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);
@@ -1754,11 +1830,10 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
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);
}
/* 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);

View File

@@ -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);

4
tmux.1
View File

@@ -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

12
tmux.h
View File

@@ -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);

View File

@@ -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) {