Merge branch 'master' into 4772-pane-scrollbars-on-causes-alt-screen-to-flicker

This commit is contained in:
Michael Grant
2026-07-08 21:07:29 +01:00
166 changed files with 12051 additions and 1514 deletions

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

@@ -0,0 +1,87 @@
name: 'Run Tests'
on:
workflow_dispatch:
schedule:
- cron: '33 3 * * *'
permissions:
contents: read
concurrency:
group: tmux-tests
cancel-in-progress: true
jobs:
regress:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- name: ubuntu-24.04-x64
runner: ubuntu-24.04
make: make
configure: --enable-utf8proc --enable-asan
#- name: ubuntu-24.04-arm64
# runner: ubuntu-24.04-arm
# make: make
# configure: --enable-utf8proc --enable-asan
- name: macos-26-arm64
runner: macos-26
make: gmake
configure: --enable-utf8proc
steps:
- name: checkout
uses: actions/checkout@v4
- name: dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
autoconf \
automake \
bison \
build-essential \
libevent-dev \
libncurses-dev \
libutf8proc-dev \
pkg-config
- name: dependencies
if: runner.os == 'macOS'
run: |
brew install \
autoconf \
automake \
bison \
libevent \
make \
ncurses \
utf8proc \
pkg-config
- name: build
run: |
sh autogen.sh
./configure ${{ matrix.configure }}
${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: test
run: |
cd regress
export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0"
${{ matrix.make }}
- name: logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: regress-logs-${{ matrix.name }}
path: regress/logs/*.log
if-no-files-found: ignore

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ configure
core
etc/
fuzz/*-fuzzer
regress/logs/
tags
tmux
tmux.1.*

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

@@ -13,7 +13,8 @@ AM_CPPFLAGS += @XOPEN_DEFINES@ \
-DTMUX_VERSION='"@VERSION@"' \
-DTMUX_CONF='"$(sysconfdir)/tmux.conf:~/.tmux.conf:$$XDG_CONFIG_HOME/tmux/tmux.conf:~/.config/tmux/tmux.conf"' \
-DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \
-DTMUX_TERM='"@DEFAULT_TERM@"'
-DTMUX_TERM='"@DEFAULT_TERM@"' \
-DTMUX_MOUSE=1
# Additional object files.
LDADD = $(LIBOBJS)
@@ -178,6 +179,7 @@ dist_tmux_SOURCES = \
log.c \
menu.c \
mode-tree.c \
monitor.c \
names.c \
notify.c \
options-table.c \

View File

@@ -45,7 +45,7 @@ attributes_tostring(int attr)
(attr & GRID_ATTR_UNDERSCORE_3) ? "curly-underscore," : "",
(attr & GRID_ATTR_UNDERSCORE_4) ? "dotted-underscore," : "",
(attr & GRID_ATTR_UNDERSCORE_5) ? "dashed-underscore," : "",
(attr & GRID_ATTR_OVERLINE) ? "overline," : "",
(attr & GRID_ATTR_OVERLINE) ? "overline," : "",
(attr & GRID_ATTR_NOATTR) ? "noattr," : "");
if (len > 0)
buf[len - 1] = '\0';

View File

@@ -51,10 +51,9 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args,
struct window *w, struct window_pane *wp)
{
struct layout_cell *lc = wp->layout_cell;
u_int sx = lc->saved_sx, sy = lc->saved_sy;
int ox = lc->saved_xoff, oy = lc->saved_yoff;
char *cause = NULL;
enum pane_lines lines = window_get_pane_lines(w);
struct layout_geometry *fg = &lc->fg;
if (window_pane_is_floating(wp)) {
cmdq_error(item, "pane is already floating");
@@ -65,14 +64,13 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args,
return (CMD_RETURN_ERROR);
}
if (layout_floating_args_parse(item, args, lines, w, &sx, &sy, &ox, &oy,
&cause) != 0) {
if (layout_floating_args_parse(item, args, lines, w, fg, &cause) != 0) {
cmdq_error(item, "failed to float pane: %s", cause);
free(cause);
return (CMD_RETURN_ERROR);
}
layout_remove_tile(w, lc);
layout_set_size(lc, sx, sy, ox, oy);
layout_set_size(lc, fg->sx, fg->sy, fg->xoff, fg->yoff);
lc->flags |= LAYOUT_CELL_FLOATING;
TAILQ_REMOVE(&w->z_index, wp, zentry);

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

@@ -126,6 +126,10 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item,
format_add(ft, "popup_mouse_y", "%u", event->m.y);
}
/* Position of the previous menu, for -x/-y L. */
format_add(ft, "popup_last_x", "%u", tc->menu_last_px);
format_add(ft, "popup_last_y", "%u", tc->menu_last_py + h);
/*
* If there are any status lines, add this window position and the
* status line position.
@@ -238,6 +242,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item,
xp = "#{popup_pane_left}";
else if (strcmp(xp, "M") == 0)
xp = "#{popup_mouse_centre_x}";
else if (strcmp(xp, "L") == 0)
xp = "#{popup_last_x}";
else if (strcmp(xp, "W") == 0)
xp = "#{popup_window_status_line_x}";
p = format_expand(ft, xp);
@@ -258,6 +264,8 @@ cmd_display_menu_get_pos(struct client *tc, struct cmdq_item *item,
yp = "#{popup_pane_bottom}";
else if (strcmp(yp, "M") == 0)
yp = "#{popup_mouse_top}";
else if (strcmp(yp, "L") == 0)
yp = "#{popup_last_y}";
else if (strcmp(yp, "S") == 0)
yp = "#{popup_status_line_y}";
else if (strcmp(yp, "W") == 0)

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]",
@@ -71,8 +71,9 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
struct window *w = wl->window;
struct layout_cell *lc = wp->layout_cell;
struct window_pane *owp;
int wx = w->sx, wy = w->sy, px = lc->sx;
int py = lc->sy, xoff = lc->xoff, yoff = lc->yoff;
int wx = w->sx, wy = w->sy;
int px = lc->g.sx, py = lc->g.sy;
int xoff = lc->g.xoff, yoff = lc->g.yoff;
int border = 1;
if (window_pane_get_pane_lines(wp) == PANE_LINES_NONE)
@@ -180,9 +181,9 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
return (CMD_RETURN_ERROR);
}
if (xoff != lc->xoff || yoff != lc->yoff) {
lc->xoff = xoff;
lc->yoff = yoff;
if (xoff != lc->g.xoff || yoff != lc->g.yoff) {
lc->g.xoff = xoff;
lc->g.yoff = yoff;
layout_fix_panes(w, NULL);
}
notify_window("window-layout-changed", w);
@@ -200,7 +201,7 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args,
const char *errstr, *argval;
const char flags[] = { 'U', 'D', 'L', 'R' };
char *cause = NULL, flag;
int xoff = lc->xoff, yoff = lc->yoff, adjust;
int xoff = lc->g.xoff, yoff = lc->g.yoff, adjust;
u_int i;
enum pane_lines lines = window_pane_get_pane_lines(wp);
@@ -251,9 +252,9 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args,
xoff += adjust;
}
if (xoff != lc->xoff || yoff != lc->yoff) {
lc->xoff = xoff;
lc->yoff = yoff;
if (xoff != lc->g.xoff || yoff != lc->g.yoff) {
lc->g.xoff = xoff;
lc->g.yoff = yoff;
layout_fix_panes(w, NULL);
notify_window("window-layout-changed", w);
server_redraw_window(w);
@@ -319,8 +320,8 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1;
if (x != lx || y != ly) {
lc->xoff += x - lx;
lc->yoff += y - ly;
lc->g.xoff += x - lx;
lc->g.yoff += y - ly;
layout_fix_panes(w, NULL);
server_redraw_window(w);
server_redraw_window_borders(w);
@@ -363,6 +364,45 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl,
return (CMD_RETURN_NORMAL);
}
static enum cmd_retval
cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w,
struct window_pane *wp)
{
struct layout_cell *lc = wp->layout_cell;
if (!window_pane_is_floating(wp)) {
cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR);
}
if (w->flags & WINDOW_ZOOMED) {
cmdq_error(item, "can't tile a pane while window is zoomed");
return (CMD_RETURN_ERROR);
}
lc->fg.sx = lc->g.sx;
lc->fg.sy = lc->g.sy;
lc->fg.xoff = lc->g.xoff;
lc->fg.yoff = lc->g.yoff;
if (layout_insert_tile(w, lc) != 0) {
cmdq_error(item, "no space for a new pane");
return (CMD_RETURN_ERROR);
}
lc->flags &= ~LAYOUT_CELL_FLOATING;
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_TAIL(&w->z_index, wp, zentry);
if (!args_has(args, 'd'))
window_set_active_pane(w, wp, 1);
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
notify_window("window-layout-changed", w);
server_redraw_window(w);
return (CMD_RETURN_NORMAL);
}
static enum cmd_retval
cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
{
@@ -412,6 +452,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
server_unzoom_window(src_w);
if (src_wp == dst_wp) {
if (window_pane_is_floating(src_wp))
return (cmd_join_pane_tile(item, args, src_w, src_wp));
cmdq_error(item, "source and target panes must be different");
return (CMD_RETURN_ERROR);
}

View File

@@ -232,7 +232,7 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item)
n = 1;
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_defaults(ft, NULL, NULL, NULL, NULL);
format_defaults(ft, tc, NULL, NULL, NULL);
format_add(ft, "notes_only", "%d", notes_only);
format_add(ft, "key_has_repeat", "%d", key_bindings_has_repeat(l, n));
format_add(ft, "key_string_width", "%u", cmd_list_keys_get_width(l, n));

View File

@@ -38,8 +38,8 @@ const struct cmd_entry cmd_new_window_entry = {
.name = "new-window",
.alias = "neww",
.args = { "abc:de:F:kn:PSt:", 0, -1, NULL },
.usage = "[-abdkPS] [-c start-directory] [-e environment] [-F format] "
.args = { "abc:de:EF:kn:PSt:", 0, -1, NULL },
.usage = "[-abdEkPS] [-c start-directory] [-e environment] [-F format] "
"[-n window-name] " CMD_TARGET_WINDOW_USAGE
" [shell-command [argument ...]]",
@@ -60,12 +60,19 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct client *tc = cmdq_get_target_client(item);
struct session *s = target->s;
struct winlink *wl = target->wl, *new_wl = NULL;
int idx = target->idx, before;
int idx = target->idx, before, count = args_count(args);
char *cause = NULL, *cp, *expanded, *wname = NULL;
const char *template, *name;
struct cmd_find_state fs;
struct args_value *av;
if (args_has(args, 'E') &&
count != 0 &&
(count != 1 || *args_string(args, 0) != '\0')) {
cmdq_error(item, "command cannot be given for empty pane");
return (CMD_RETURN_ERROR);
}
/*
* If -S and -n are given and -t is not and a single window with this
* name already exists, select it.
@@ -134,6 +141,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
sc.cwd = args_get(args, 'c');
sc.flags = 0;
if (args_has(args, 'E'))
sc.flags |= SPAWN_EMPTY;
if (args_has(args, 'd'))
sc.flags |= SPAWN_DETACHED;
if (args_has(args, 'k'))

View File

@@ -46,36 +46,17 @@ const struct cmd_entry cmd_refresh_client_entry = {
static void
cmd_refresh_client_update_subscription(struct client *tc, const char *value)
{
char *copy, *split, *name, *what;
enum control_sub_type subtype;
int subid = -1;
char *name, *format;
enum monitor_type type;
int id;
copy = name = xstrdup(value);
if ((split = strchr(copy, ':')) == NULL) {
control_remove_sub(tc, copy);
goto out;
if (monitor_parse(value, &name, &type, &id, &format) != 0) {
control_remove_sub(tc, value);
return;
}
*split++ = '\0';
what = split;
if ((split = strchr(what, ':')) == NULL)
goto out;
*split++ = '\0';
if (strcmp(what, "%*") == 0)
subtype = CONTROL_SUB_ALL_PANES;
else if (sscanf(what, "%%%d", &subid) == 1 && subid >= 0)
subtype = CONTROL_SUB_PANE;
else if (strcmp(what, "@*") == 0)
subtype = CONTROL_SUB_ALL_WINDOWS;
else if (sscanf(what, "@%d", &subid) == 1 && subid >= 0)
subtype = CONTROL_SUB_WINDOW;
else
subtype = CONTROL_SUB_SESSION;
control_add_sub(tc, name, subtype, subid, split);
out:
free(copy);
control_add_sub(tc, name, type, id, format);
free(name);
free(format);
}
static enum cmd_retval

View File

@@ -270,10 +270,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
if ((lx == left || lx == left + 1) && ly == wp->yoff - 1) {
/* Top left corner. */
new_sx = lc->sx + (lx - x);
new_sx = lc->g.sx + (lx - x);
if (new_sx < PANE_MINIMUM)
new_sx = PANE_MINIMUM;
new_sy = lc->sy + (ly - y);
new_sy = lc->g.sy + (ly - y);
if (new_sy < PANE_MINIMUM)
new_sy = PANE_MINIMUM;
new_xoff = x + 1; /* because mouse is on border at xoff - 1 */
@@ -283,65 +283,65 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
} else if ((lx == right + 1 || lx == right) &&
ly == wp->yoff - 1) {
/* Top right corner. */
new_sx = x - lc->xoff;
new_sx = x - lc->g.xoff;
if (new_sx < PANE_MINIMUM)
new_sx = PANE_MINIMUM;
new_sy = lc->sy + (ly - y);
new_sy = lc->g.sy + (ly - y);
if (new_sy < PANE_MINIMUM)
new_sy = PANE_MINIMUM;
new_yoff = y + 1;
layout_set_size(lc, new_sx, new_sy, lc->xoff, new_yoff);
layout_set_size(lc, new_sx, new_sy, lc->g.xoff, new_yoff);
resizes++;
} else if ((lx == left || lx == left + 1) &&
ly == wp->yoff + sy) {
/* Bottom left corner. */
new_sx = lc->sx + (lx - x);
new_sx = lc->g.sx + (lx - x);
if (new_sx < PANE_MINIMUM)
new_sx = PANE_MINIMUM;
new_sy = y - lc->yoff;
new_sy = y - lc->g.yoff;
if (new_sy < PANE_MINIMUM)
return;
new_xoff = x + 1;
layout_set_size(lc, new_sx, new_sy, new_xoff, lc->yoff);
layout_set_size(lc, new_sx, new_sy, new_xoff, lc->g.yoff);
resizes++;
} else if ((lx == right + 1 || lx == right) &&
ly == wp->yoff + sy) {
/* Bottom right corner. */
new_sx = x - lc->xoff;
new_sx = x - lc->g.xoff;
if (new_sx < PANE_MINIMUM)
new_sx = PANE_MINIMUM;
new_sy = y - lc->yoff;
new_sy = y - lc->g.yoff;
if (new_sy < PANE_MINIMUM)
new_sy = PANE_MINIMUM;
layout_set_size(lc, new_sx, new_sy, lc->xoff, lc->yoff);
layout_set_size(lc, new_sx, new_sy, lc->g.xoff, lc->g.yoff);
resizes++;
} else if (lx == right) {
/* Right border. */
new_sx = x - lc->xoff;
new_sx = x - lc->g.xoff;
if (new_sx < PANE_MINIMUM)
return;
layout_set_size(lc, new_sx, lc->sy, lc->xoff, lc->yoff);
layout_set_size(lc, new_sx, lc->g.sy, lc->g.xoff, lc->g.yoff);
resizes++;
} else if (lx == left) {
/* Left border. */
new_sx = lc->sx + (lx - x);
new_sx = lc->g.sx + (lx - x);
if (new_sx < PANE_MINIMUM)
return;
new_xoff = x + 1;
layout_set_size(lc, new_sx, lc->sy, new_xoff, lc->yoff);
layout_set_size(lc, new_sx, lc->g.sy, new_xoff, lc->g.yoff);
resizes++;
} else if (ly == wp->yoff + sy) {
/* Bottom border. */
new_sy = y - lc->yoff;
new_sy = y - lc->g.yoff;
if (new_sy < PANE_MINIMUM)
return;
layout_set_size(lc, lc->sx, new_sy, lc->xoff, lc->yoff);
layout_set_size(lc, lc->g.sx, new_sy, lc->g.xoff, lc->g.yoff);
resizes++;
} else if (ly == wp->yoff - 1) {
/* Top border (move instead of resize). */
new_xoff = lc->xoff + (x - lx);
new_xoff = lc->g.xoff + (x - lx);
new_yoff = y + 1;
layout_set_size(lc, lc->sx, lc->sy, new_xoff, new_yoff);
layout_set_size(lc, lc->g.sx, lc->g.sy, new_xoff, new_yoff);
resizes++;
}
if (resizes != 0) {

View File

@@ -34,8 +34,8 @@ const struct cmd_entry cmd_respawn_pane_entry = {
.name = "respawn-pane",
.alias = "respawnp",
.args = { "c:e:kt:", 0, -1, NULL },
.usage = "[-k] [-c start-directory] [-e environment] "
.args = { "c:e:Ekt:", 0, -1, NULL },
.usage = "[-Ek] [-c start-directory] [-e environment] "
CMD_TARGET_PANE_USAGE " [shell-command [argument ...]]",
.target = { 't', CMD_FIND_PANE, 0 },
@@ -75,6 +75,8 @@ cmd_respawn_pane_exec(struct cmd *self, struct cmdq_item *item)
sc.cwd = args_get(args, 'c');
sc.flags = SPAWN_RESPAWN;
if (args_has(args, 'E'))
sc.flags |= SPAWN_EMPTY;
if (args_has(args, 'k'))
sc.flags |= SPAWN_KILL;

View File

@@ -34,8 +34,8 @@ const struct cmd_entry cmd_respawn_window_entry = {
.name = "respawn-window",
.alias = "respawnw",
.args = { "c:e:kt:", 0, -1, NULL },
.usage = "[-k] [-c start-directory] [-e environment] "
.args = { "c:e:Ekt:", 0, -1, NULL },
.usage = "[-Ek] [-c start-directory] [-e environment] "
CMD_TARGET_WINDOW_USAGE " [shell-command [argument ...]]",
.target = { 't', CMD_FIND_WINDOW, 0 },
@@ -74,6 +74,8 @@ cmd_respawn_window_exec(struct cmd *self, struct cmdq_item *item)
sc.cwd = args_get(args, 'c');
sc.flags = SPAWN_RESPAWN;
if (args_has(args, 'E'))
sc.flags |= SPAWN_EMPTY;
if (args_has(args, 'k'))
sc.flags |= SPAWN_KILL;

View File

@@ -31,6 +31,8 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *,
u_int, char **);
static enum cmd_retval cmd_set_option_exec(struct cmd *,
struct cmdq_item *);
static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *,
struct args *, int);
const struct cmd_entry cmd_set_option_entry = {
.name = "set-option",
@@ -62,8 +64,9 @@ const struct cmd_entry cmd_set_hook_entry = {
.name = "set-hook",
.alias = NULL,
.args = { "agpRt:uw", 1, 2, cmd_set_option_args_parse },
.usage = "[-agpRuw] " CMD_TARGET_PANE_USAGE " hook [command]",
.args = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse },
.usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " "
"[hook] [command]",
.target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL },
@@ -72,14 +75,100 @@ const struct cmd_entry cmd_set_hook_entry = {
};
static enum args_parse_type
cmd_set_option_args_parse(__unused struct args *args, u_int idx,
cmd_set_option_args_parse(struct args *args, u_int idx,
__unused char **cause)
{
if (args_has(args, 'B'))
return (ARGS_PARSE_COMMANDS_OR_STRING);
if (idx == 1)
return (ARGS_PARSE_COMMANDS_OR_STRING);
return (ARGS_PARSE_STRING);
}
static enum cmd_retval
cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window)
{
struct cmd_find_state *target = cmdq_get_target(item), fs;
struct options *oo;
struct options_entry *o;
char *cause = NULL, *name = NULL, *format = NULL;
char *expanded = NULL, *newvalue = NULL;
const char *value, *old;
enum monitor_type type;
int id, scope;
if (args_count(args) > 1) {
cmdq_error(item, "too many arguments");
return (CMD_RETURN_ERROR);
}
value = args_get(args, 'B');
if (args_has(args, 'u')) {
if (monitor_parse(value, &name, &type, &id, &format) != 0)
name = xstrdup(value);
free(format);
format = NULL;
} else {
if (monitor_parse(value, &name, &type, &id, &format) != 0) {
cmdq_error(item, "invalid subscription: %s", value);
return (CMD_RETURN_ERROR);
}
}
if (*name != '@') {
cmdq_error(item, "monitor hook name must start with @");
goto fail;
}
scope = options_scope_from_name(args, window, name, target, &oo,
&cause);
if (scope == OPTIONS_TABLE_NONE) {
cmdq_error(item, "%s", cause);
free(cause);
goto fail;
}
cmd_find_copy_state(&fs, target);
if (args_has(args, 'u')) {
notify_monitor_remove(oo, name);
goto out;
}
if (args_count(args) != 0) {
value = args_string(args, 0);
if (args_has(args, 'F')) {
expanded = format_single_from_target(item, value);
value = expanded;
}
o = options_get_only(oo, name);
if (!args_has(args, 'o') || o == NULL) {
if (args_has(args, 'a') && o != NULL) {
old = options_get_string(oo, name);
xasprintf(&newvalue, "%s%s", old, value);
value = newvalue;
}
options_set_string(oo, name, 0, "%s", value);
options_push_changes(name);
}
}
notify_monitor_add(item, oo, name, type, id, format, &fs, target->s);
out:
free(newvalue);
free(expanded);
free(name);
free(format);
return (CMD_RETURN_NORMAL);
fail:
free(newvalue);
free(expanded);
free(name);
free(format);
return (CMD_RETURN_ERROR);
}
static enum cmd_retval
cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
{
@@ -89,13 +178,19 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
struct window_pane *loop;
struct options *oo;
struct options_entry *parent, *o, *po;
char *name, *argument, *expanded = NULL;
char *cause;
char *name, *argument, *cause;
char *expanded = NULL, *array_key = NULL;
const char *value;
int window, idx, already, error, ambiguous;
int window, already, error, ambiguous;
int scope;
window = (cmd_get_entry(self) == &cmd_set_window_option_entry);
if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B'))
return (cmd_set_hook_monitor_exec(item, args, window));
if (args_count(args) == 0) {
cmdq_error(item, "missing argument");
return (CMD_RETURN_ERROR);
}
/* Expand argument. */
argument = format_single_from_target(item, args_string(args, 0));
@@ -107,8 +202,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
}
/* Parse option name and index. */
name = options_match(argument, &idx, &ambiguous);
/* Parse option name and array key. */
name = options_match(argument, &array_key, &ambiguous);
if (name == NULL) {
if (args_has(args, 'q'))
goto out;
@@ -140,21 +235,23 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
o = options_get_only(oo, name);
parent = options_get(oo, name);
/* Check that array options and indexes match up. */
if (idx != -1 && (*name == '@' || !options_is_array(parent))) {
/* Check that array options and keys match up. */
if (array_key != NULL && (*name == '@' || !options_is_array(parent))) {
cmdq_error(item, "not an array: %s", argument);
goto fail;
}
/* With -o, check this option is not already set. */
if (!args_has(args, 'u') && args_has(args, 'o')) {
if (idx == -1)
if (array_key == NULL)
already = (o != NULL);
else {
if (o == NULL)
already = 0;
else if (options_array_get(o, array_key) != NULL)
already = 1;
else
already = (options_array_get(o, idx) != NULL);
already = 0;
}
if (already) {
if (args_has(args, 'q'))
@@ -170,7 +267,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
po = options_get_only(loop->options, name);
if (po == NULL)
continue;
if (options_remove_or_default(po, idx, &cause) != 0) {
if (options_remove_or_default(po, array_key,
&cause) != 0) {
cmdq_error(item, "%s", cause);
free(cause);
goto fail;
@@ -180,7 +278,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'u') || args_has(args, 'U')) {
if (o == NULL)
goto out;
if (options_remove_or_default(o, idx, &cause) != 0) {
if (options_remove_or_default(o, array_key, &cause) != 0) {
cmdq_error(item, "%s", cause);
free(cause);
goto fail;
@@ -191,7 +289,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
goto fail;
}
options_set_string(oo, name, append, "%s", value);
} else if (idx == -1 && !options_is_array(parent)) {
} else if (array_key == NULL && !options_is_array(parent)) {
error = options_from_string(oo, options_table_entry(parent),
options_table_entry(parent)->name, value,
args_has(args, 'a'), &cause);
@@ -207,7 +305,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
}
if (o == NULL)
o = options_empty(oo, options_table_entry(parent));
if (idx == -1) {
if (array_key == NULL) {
if (!append)
options_array_clear(o);
if (options_array_assign(o, value, &cause) != 0) {
@@ -215,7 +313,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
free(cause);
goto fail;
}
} else if (options_array_set(o, idx, value, append,
} else if (options_array_set(o, array_key, value, append,
&cause) != 0) {
cmdq_error(item, "%s", cause);
free(cause);
@@ -229,11 +327,13 @@ out:
free(argument);
free(expanded);
free(name);
free(array_key);
return (CMD_RETURN_NORMAL);
fail:
free(argument);
free(expanded);
free(name);
free(array_key);
return (CMD_RETURN_ERROR);
}

View File

@@ -30,7 +30,11 @@
static enum cmd_retval cmd_show_options_exec(struct cmd *, struct cmdq_item *);
static void cmd_show_options_print(struct cmd *, struct cmdq_item *,
struct options_entry *, int, int);
struct options_entry *, const char *, int);
static void cmd_show_hooks_print_monitor(struct cmdq_item *,
struct options_entry *);
static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *,
int, struct options *);
static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *,
int, struct options *);
@@ -64,8 +68,8 @@ const struct cmd_entry cmd_show_hooks_entry = {
.name = "show-hooks",
.alias = NULL,
.args = { "gpt:w", 0, 1, NULL },
.usage = "[-gpw] " CMD_TARGET_PANE_USAGE " [hook]",
.args = { "Bgpt:w", 0, 1, NULL },
.usage = "[-Bgpw] " CMD_TARGET_PANE_USAGE " [hook]",
.target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL },
@@ -80,7 +84,8 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item);
struct options *oo;
char *argument, *name = NULL, *cause;
int window, idx, ambiguous, parent, scope;
char *array_key = NULL;
int window, ambiguous, parent, scope;
struct options_entry *o;
window = (cmd_get_entry(self) == &cmd_show_window_options_entry);
@@ -95,11 +100,14 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
free(cause);
return (CMD_RETURN_ERROR);
}
if (cmd_get_entry(self) == &cmd_show_hooks_entry &&
args_has(args, 'B'))
return (cmd_show_hooks_monitor(self, item, scope, oo));
return (cmd_show_options_all(self, item, scope, oo));
}
argument = format_single_from_target(item, args_string(args, 0));
name = options_match(argument, &idx, &ambiguous);
name = options_match(argument, &array_key, &ambiguous);
if (name == NULL) {
if (args_has(args, 'q'))
goto out;
@@ -124,8 +132,13 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
parent = 1;
} else
parent = 0;
if (o != NULL)
cmd_show_options_print(self, item, o, idx, parent);
if (o != NULL) {
if (cmd_get_entry(self) == &cmd_show_hooks_entry &&
args_has(args, 'B'))
cmd_show_hooks_print_monitor(item, o);
else
cmd_show_options_print(self, item, o, array_key, parent);
}
else if (*name == '@') {
if (args_has(args, 'q'))
goto out;
@@ -135,26 +148,28 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
out:
free(name);
free(array_key);
free(argument);
return (CMD_RETURN_NORMAL);
fail:
free(name);
free(array_key);
free(argument);
return (CMD_RETURN_ERROR);
}
static void
cmd_show_options_print(struct cmd *self, struct cmdq_item *item,
struct options_entry *o, int idx, int parent)
struct options_entry *o, const char *array_key, int parent)
{
struct args *args = cmd_get_args(self);
struct options_array_item *a;
const char *name = options_name(o);
char *value, *tmp = NULL, *escaped;
if (idx != -1) {
xasprintf(&tmp, "%s[%d]", name, idx);
if (array_key != NULL) {
xasprintf(&tmp, "%s[%s]", name, array_key);
name = tmp;
} else {
if (options_is_array(o)) {
@@ -165,8 +180,8 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item,
return;
}
while (a != NULL) {
idx = options_array_item_index(a);
cmd_show_options_print(self, item, o, idx,
array_key = options_array_item_key(a);
cmd_show_options_print(self, item, o, array_key,
parent);
a = options_array_next(a);
}
@@ -174,7 +189,7 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item,
}
}
value = options_to_string(o, idx, 0);
value = options_to_string(o, array_key, 0);
if (args_has(args, 'v'))
cmdq_print(item, "%s", value);
else if (options_is_string(o)) {
@@ -195,6 +210,33 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item,
free(tmp);
}
static void
cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o)
{
char *value;
value = notify_monitor_to_string(o);
if (value == NULL)
return;
cmdq_print(item, "%s", value);
free(value);
}
/* Show all hook monitors. */
static enum cmd_retval
cmd_show_hooks_monitor(__unused struct cmd *self, struct cmdq_item *item,
__unused int scope, struct options *oo)
{
struct options_entry *o;
o = options_first(oo);
while (o != NULL) {
cmd_show_hooks_print_monitor(item, o);
o = options_next(o);
}
return (CMD_RETURN_NORMAL);
}
static enum cmd_retval
cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope,
struct options *oo)
@@ -203,15 +245,14 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope,
const struct options_table_entry *oe;
struct options_entry *o;
struct options_array_item *a;
const char *name;
u_int idx;
const char *name, *array_key;
int parent;
if (cmd_get_entry(self) != &cmd_show_hooks_entry) {
o = options_first(oo);
while (o != NULL) {
if (options_table_entry(o) == NULL)
cmd_show_options_print(self, item, o, -1, 0);
cmd_show_options_print(self, item, o, NULL, 0);
o = options_next(o);
}
}
@@ -238,7 +279,7 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope,
parent = 0;
if (!options_is_array(o))
cmd_show_options_print(self, item, o, -1, parent);
cmd_show_options_print(self, item, o, NULL, parent);
else if ((a = options_array_first(o)) == NULL) {
if (!args_has(args, 'v')) {
name = options_name(o);
@@ -249,8 +290,8 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope,
}
} else {
while (a != NULL) {
idx = options_array_item_index(a);
cmd_show_options_print(self, item, o, idx,
array_key = options_array_item_key(a);
cmd_show_options_print(self, item, o, array_key,
parent);
a = options_array_next(a);
}

View File

@@ -81,7 +81,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
struct session *s = target->s;
struct winlink *wl = target->wl;
struct window *w = wl->window;
struct window_pane *wp = target->wp, *new_wp;
struct window_pane *wp = target->wp, *new_wp = NULL;
struct layout_cell *lc = NULL;
struct cmd_find_state fs;
int input, empty, is_floating, flags = 0;
@@ -167,6 +167,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if ((new_wp = spawn_pane(&sc, &cause)) == NULL) {
cmdq_error(item, "create pane failed: %s", cause);
free(cause);
/*
* spawn_pane has already torn the half-built pane down (its
* fork-failure path removes the pane and destroys the layout
* cell), so new_wp is NULL and there is nothing for fail to do.
*/
goto fail;
}
@@ -219,10 +224,6 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (input) {
switch (window_pane_start_input(new_wp, item, &cause)) {
case -1:
server_client_remove_pane(new_wp);
if (!is_floating)
layout_close_pane(new_wp);
window_remove_pane(wp->window, new_wp);
cmdq_error(item, "%s", cause);
free(cause);
goto fail;
@@ -269,10 +270,20 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
fail:
/*
* If the pane was spawned before we failed, tear it down here; this
* also destroys its layout cell. spawn_pane's own failure path has
* already done this, so new_wp is NULL in that case.
*/
if (new_wp != NULL) {
server_client_remove_pane(new_wp);
if (!is_floating)
layout_close_pane(new_wp);
window_remove_pane(wp->window, new_wp);
}
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
layout_destroy_cell(w, lc, &w->layout_root);
return (CMD_RETURN_ERROR);

View File

@@ -42,6 +42,22 @@ const struct cmd_entry cmd_swap_pane_entry = {
.exec = cmd_swap_pane_exec
};
static struct window_pane *
cmd_swap_pane_next_tiled_pane(struct window_pane *wp)
{
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
return (wp);
}
static struct window_pane *
cmd_swap_pane_prev_tiled_pane(struct window_pane *wp)
{
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_PREV(wp, window_panes, entry);
return (wp);
}
static enum cmd_retval
cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item)
{
@@ -62,15 +78,29 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window(dst_w);
if (args_has(args, 'D')) {
if (window_pane_is_floating(dst_wp)) {
cmdq_error(item, "cannot swap down on floating pane");
return (CMD_RETURN_ERROR);
}
src_w = dst_w;
src_wp = TAILQ_NEXT(dst_wp, entry);
if (src_wp == NULL)
src_wp = cmd_swap_pane_next_tiled_pane(src_wp);
if (src_wp == NULL) {
src_wp = TAILQ_FIRST(&dst_w->panes);
src_wp = cmd_swap_pane_next_tiled_pane(src_wp);
}
} else if (args_has(args, 'U')) {
if (window_pane_is_floating(dst_wp)) {
cmdq_error(item, "cannot swap up on floating pane");
return (CMD_RETURN_ERROR);
}
src_w = dst_w;
src_wp = TAILQ_PREV(dst_wp, window_panes, entry);
if (src_wp == NULL)
src_wp = cmd_swap_pane_prev_tiled_pane(src_wp);
if (src_wp == NULL) {
src_wp = TAILQ_LAST(&dst_w->panes, window_panes);
src_wp = cmd_swap_pane_prev_tiled_pane(src_wp);
}
}
if (src_w != dst_w && window_push_zoom(src_w, 0, args_has(args, 'Z')))
@@ -79,12 +109,6 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item)
if (src_wp == dst_wp)
goto out;
if (window_pane_is_floating(src_wp) ||
window_pane_is_floating(dst_wp)) {
cmdq_error(item, "cannot swap floating panes");
return (CMD_RETURN_ERROR);
}
server_client_remove_pane(src_wp);
server_client_remove_pane(dst_wp);

View File

@@ -290,6 +290,58 @@ colour_tostring(int c)
return ("invalid");
}
/* Convert colour to an SGR escape sequence. */
const char *
colour_toescape(struct client *c, int colour, int bg)
{
static char s[32];
u_char r, g, b;
int n, flags = (TERM_256COLOURS|TERM_RGBCOLOURS);
u_int o = (bg ? 40 : 30);
if (c != NULL && (c->tty.flags & TTY_OPENED) && c->tty.term != NULL)
flags = c->tty.term->flags;
if (colour & COLOUR_FLAG_THEME) {
n = colour & 0xff;
if (c != NULL && (u_int)n < COLOUR_THEME_COUNT)
colour = c->theme_colours[n];
else
colour = colour_theme_terminal_colour(n);
}
if (colour == 8 || colour == 9) {
xsnprintf(s, sizeof s, "\033[%dm", o + 9);
return (s);
}
if ((~flags & TERM_RGBCOLOURS) & (colour & COLOUR_FLAG_RGB)) {
colour_split_rgb(colour, &r, &g, &b);
colour = colour_find_rgb(r, g, b);
}
if ((~flags & TERM_256COLOURS) & (colour & COLOUR_FLAG_256))
colour = colour_256to16(colour);
if (colour & COLOUR_FLAG_RGB) {
colour_split_rgb(colour, &r, &g, &b);
xsnprintf(s, sizeof s, "\033[%d;2;%u;%u;%um", o + 8, r, g, b);
return (s);
}
if (colour & COLOUR_FLAG_256) {
xsnprintf(s, sizeof s, "\033[%d;5;%um", o + 8, colour & 0xff);
return (s);
}
if (colour >= 0 && colour <= 7) {
xsnprintf(s, sizeof s, "\033[%dm", colour + o);
return (s);
}
if (colour >= 90 && colour <= 97) {
xsnprintf(s, sizeof s, "\033[%dm", colour + o - 30);
return (s);
}
return (NULL);
}
/* Convert background colour to theme. */
enum client_theme
colour_totheme(int c)
@@ -1242,7 +1294,8 @@ colour_palette_from_option(struct colour_palette *p, struct options *oo)
{
struct options_entry *o;
struct options_array_item *a;
u_int i, n;
union options_value *ov;
u_int i;
int c;
if (p == NULL)
@@ -1260,12 +1313,11 @@ colour_palette_from_option(struct colour_palette *p, struct options *oo)
p->default_palette = xcalloc(256, sizeof *p->default_palette);
for (i = 0; i < 256; i++)
p->default_palette[i] = -1;
while (a != NULL) {
n = options_array_item_index(a);
if (n < 256) {
c = options_array_item_value(a)->number;
p->default_palette[n] = c;
for (i = 0; i < 256; i++) {
ov = options_array_getv(o, "%u", i);
if (ov != NULL) {
c = ov->number;
p->default_palette[i] = c;
}
a = options_array_next(a);
}
}

View File

@@ -1,6 +1,6 @@
# configure.ac
AC_INIT([tmux], next-3.7)
AC_INIT([tmux], next-3.8)
AC_PREREQ([2.60])
AC_CONFIG_AUX_DIR(etc)

452
control.c
View File

@@ -75,42 +75,6 @@ struct control_pane {
};
RB_HEAD(control_panes, control_pane);
/* Subscription pane. */
struct control_sub_pane {
u_int pane;
u_int idx;
char *last;
RB_ENTRY(control_sub_pane) entry;
};
RB_HEAD(control_sub_panes, control_sub_pane);
/* Subscription window. */
struct control_sub_window {
u_int window;
u_int idx;
char *last;
RB_ENTRY(control_sub_window) entry;
};
RB_HEAD(control_sub_windows, control_sub_window);
/* Control client subscription. */
struct control_sub {
char *name;
char *format;
enum control_sub_type type;
u_int id;
char *last;
struct control_sub_panes panes;
struct control_sub_windows windows;
RB_ENTRY(control_sub) entry;
};
RB_HEAD(control_subs, control_sub);
/* Control client state. */
struct control_state {
struct control_panes panes;
@@ -123,8 +87,7 @@ struct control_state {
struct bufferevent *read_event;
struct bufferevent *write_event;
struct control_subs subs;
struct event subs_timer;
struct monitor_set *subs;
};
/* Low and high watermarks. */
@@ -154,75 +117,6 @@ control_pane_cmp(struct control_pane *cp1, struct control_pane *cp2)
}
RB_GENERATE_STATIC(control_panes, control_pane, entry, control_pane_cmp);
/* Compare client subs. */
static int
control_sub_cmp(struct control_sub *csub1, struct control_sub *csub2)
{
return (strcmp(csub1->name, csub2->name));
}
RB_GENERATE_STATIC(control_subs, control_sub, entry, control_sub_cmp);
/* Compare client subscription panes. */
static int
control_sub_pane_cmp(struct control_sub_pane *csp1,
struct control_sub_pane *csp2)
{
if (csp1->pane < csp2->pane)
return (-1);
if (csp1->pane > csp2->pane)
return (1);
if (csp1->idx < csp2->idx)
return (-1);
if (csp1->idx > csp2->idx)
return (1);
return (0);
}
RB_GENERATE_STATIC(control_sub_panes, control_sub_pane, entry,
control_sub_pane_cmp);
/* Compare client subscription windows. */
static int
control_sub_window_cmp(struct control_sub_window *csw1,
struct control_sub_window *csw2)
{
if (csw1->window < csw2->window)
return (-1);
if (csw1->window > csw2->window)
return (1);
if (csw1->idx < csw2->idx)
return (-1);
if (csw1->idx > csw2->idx)
return (1);
return (0);
}
RB_GENERATE_STATIC(control_sub_windows, control_sub_window, entry,
control_sub_window_cmp);
/* Free a subscription. */
static void
control_free_sub(struct control_state *cs, struct control_sub *csub)
{
struct control_sub_pane *csp, *csp1;
struct control_sub_window *csw, *csw1;
RB_FOREACH_SAFE(csp, control_sub_panes, &csub->panes, csp1) {
RB_REMOVE(control_sub_panes, &csub->panes, csp);
free(csp->last);
free(csp);
}
RB_FOREACH_SAFE(csw, control_sub_windows, &csub->windows, csw1) {
RB_REMOVE(control_sub_windows, &csub->windows, csw);
free(csw->last);
free(csw);
}
free(csub->last);
RB_REMOVE(control_subs, &cs->subs, csub);
free(csub->name);
free(csub->format);
free(csub);
}
/* Free a block. */
static void
control_free_block(struct control_state *cs, struct control_block *cb)
@@ -766,6 +660,30 @@ control_write_callback(__unused struct bufferevent *bufev, void *data)
bufferevent_disable(cs->write_event, EV_WRITE);
}
/* Write a subscription change. */
static void
control_sub_change(struct monitor_change *change, __unused void *data)
{
struct client *c = change->c;
struct session *s = change->s;
struct winlink *wl = change->wl;
struct window_pane *wp = change->wp;
struct window *w;
if (wp != NULL) {
w = wp->window;
control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s",
change->name, s->id, w->id, wl->idx, wp->id, change->value);
} else if (wl != NULL) {
w = wl->window;
control_write(c, "%%subscription-changed %s $%u @%u %u - : %s",
change->name, s->id, w->id, wl->idx, change->value);
} else {
control_write(c, "%%subscription-changed %s $%u - - - : %s",
change->name, s->id, change->value);
}
}
/* Initialize for control mode. */
void
control_start(struct client *c)
@@ -783,7 +701,7 @@ control_start(struct client *c)
RB_INIT(&cs->panes);
TAILQ_INIT(&cs->pending_list);
TAILQ_INIT(&cs->all_blocks);
RB_INIT(&cs->subs);
cs->subs = monitor_create_client(c, control_sub_change, NULL);
cs->read_event = bufferevent_new(c->fd, control_read_callback,
control_write_callback, control_error_callback, c);
@@ -832,20 +750,16 @@ control_stop(struct client *c)
{
struct control_state *cs = c->control_state;
struct control_block *cb, *cb1;
struct control_sub *csub, *csub1;
if (cs == NULL)
return;
monitor_destroy(cs->subs);
if (~c->flags & CLIENT_CONTROLCONTROL)
bufferevent_free(cs->write_event);
bufferevent_free(cs->read_event);
RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1)
control_free_sub(cs, csub);
if (evtimer_initialized(&cs->subs_timer))
evtimer_del(&cs->subs_timer);
control_reset_offsets(c);
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb);
@@ -854,313 +768,14 @@ control_stop(struct client *c)
free(cs);
}
/* Check session subscription. */
static void
control_check_subs_session(struct client *c, struct control_sub *csub,
struct format_tree *ft)
{
struct session *s = c->session;
char *value;
value = format_expand(ft, csub->format);
if (csub->last != NULL && strcmp(value, csub->last) == 0) {
free(value);
return;
}
control_write(c,
"%%subscription-changed %s $%u - - - : %s",
csub->name, s->id, value);
free(csub->last);
csub->last = value;
}
/* Check pane subscription. */
static void
control_check_subs_pane(struct client *c, struct control_sub *csub)
{
struct session *s = c->session;
struct window_pane *wp;
struct window *w;
struct winlink *wl;
struct format_tree *ft;
char *value;
struct control_sub_pane *csp, find;
wp = window_pane_find_by_id(csub->id);
if (wp == NULL || wp->fd == -1)
return;
w = wp->window;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session != s)
continue;
ft = format_create_defaults(NULL, c, s, wl, wp);
value = format_expand(ft, csub->format);
format_free(ft);
find.pane = wp->id;
find.idx = wl->idx;
csp = RB_FIND(control_sub_panes, &csub->panes, &find);
if (csp == NULL) {
csp = xcalloc(1, sizeof *csp);
csp->pane = wp->id;
csp->idx = wl->idx;
RB_INSERT(control_sub_panes, &csub->panes, csp);
}
if (csp->last != NULL && strcmp(value, csp->last) == 0) {
free(value);
continue;
}
control_write(c,
"%%subscription-changed %s $%u @%u %u %%%u : %s",
csub->name, s->id, w->id, wl->idx, wp->id, value);
free(csp->last);
csp->last = value;
}
}
/* Check all-panes subscription for a pane. */
static void
control_check_subs_all_panes_one(struct client *c, struct control_sub *csub,
struct format_tree *ft, struct winlink *wl, struct window_pane *wp)
{
struct session *s = c->session;
struct window *w = wl->window;
char *value;
struct control_sub_pane *csp, find;
value = format_expand(ft, csub->format);
find.pane = wp->id;
find.idx = wl->idx;
csp = RB_FIND(control_sub_panes, &csub->panes, &find);
if (csp == NULL) {
csp = xcalloc(1, sizeof *csp);
csp->pane = wp->id;
csp->idx = wl->idx;
RB_INSERT(control_sub_panes, &csub->panes, csp);
}
if (csp->last != NULL && strcmp(value, csp->last) == 0) {
free(value);
return;
}
control_write(c,
"%%subscription-changed %s $%u @%u %u %%%u : %s",
csub->name, s->id, w->id, wl->idx, wp->id, value);
free(csp->last);
csp->last = value;
}
/* Check window subscription. */
static void
control_check_subs_window(struct client *c, struct control_sub *csub)
{
struct session *s = c->session;
struct window *w;
struct winlink *wl;
struct format_tree *ft;
char *value;
struct control_sub_window *csw, find;
w = window_find_by_id(csub->id);
if (w == NULL)
return;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session != s)
continue;
ft = format_create_defaults(NULL, c, s, wl, NULL);
value = format_expand(ft, csub->format);
format_free(ft);
find.window = w->id;
find.idx = wl->idx;
csw = RB_FIND(control_sub_windows, &csub->windows, &find);
if (csw == NULL) {
csw = xcalloc(1, sizeof *csw);
csw->window = w->id;
csw->idx = wl->idx;
RB_INSERT(control_sub_windows, &csub->windows, csw);
}
if (csw->last != NULL && strcmp(value, csw->last) == 0) {
free(value);
continue;
}
control_write(c,
"%%subscription-changed %s $%u @%u %u - : %s",
csub->name, s->id, w->id, wl->idx, value);
free(csw->last);
csw->last = value;
}
}
/* Check all-windows subscription for a window. */
static void
control_check_subs_all_windows_one(struct client *c, struct control_sub *csub,
struct format_tree *ft, struct winlink *wl)
{
struct session *s = c->session;
struct window *w = wl->window;
char *value;
struct control_sub_window *csw, find;
value = format_expand(ft, csub->format);
find.window = w->id;
find.idx = wl->idx;
csw = RB_FIND(control_sub_windows, &csub->windows, &find);
if (csw == NULL) {
csw = xcalloc(1, sizeof *csw);
csw->window = w->id;
csw->idx = wl->idx;
RB_INSERT(control_sub_windows, &csub->windows, csw);
}
if (csw->last != NULL && strcmp(value, csw->last) == 0) {
free(value);
return;
}
control_write(c,
"%%subscription-changed %s $%u @%u %u - : %s",
csub->name, s->id, w->id, wl->idx, value);
free(csw->last);
csw->last = value;
}
/* Check subscriptions timer. */
static void
control_check_subs_timer(__unused int fd, __unused short events, void *data)
{
struct client *c = data;
struct control_state *cs = c->control_state;
struct control_sub *csub, *csub1;
struct session *s = c->session;
struct format_tree *ft;
struct winlink *wl;
struct window_pane *wp;
struct timeval tv = { .tv_sec = 1 };
int have_session = 0, have_all_panes = 0;
int have_all_windows = 0;
log_debug("%s: timer fired", __func__);
evtimer_add(&cs->subs_timer, &tv);
if (s == NULL)
return;
/* Find which subscription types are present. */
RB_FOREACH(csub, control_subs, &cs->subs) {
switch (csub->type) {
case CONTROL_SUB_SESSION:
have_session = 1;
break;
case CONTROL_SUB_ALL_PANES:
have_all_panes = 1;
break;
case CONTROL_SUB_ALL_WINDOWS:
have_all_windows = 1;
break;
default:
break;
}
}
/* Check session subscriptions. */
if (have_session) {
ft = format_create_defaults(NULL, c, s, NULL, NULL);
RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) {
if (csub->type == CONTROL_SUB_SESSION)
control_check_subs_session(c, csub, ft);
}
format_free(ft);
}
/* Check pane and window subscriptions. */
RB_FOREACH_SAFE(csub, control_subs, &cs->subs, csub1) {
switch (csub->type) {
case CONTROL_SUB_PANE:
control_check_subs_pane(c, csub);
break;
case CONTROL_SUB_WINDOW:
control_check_subs_window(c, csub);
break;
case CONTROL_SUB_SESSION:
case CONTROL_SUB_ALL_PANES:
case CONTROL_SUB_ALL_WINDOWS:
break;
}
}
/* Check all-panes subscriptions. */
if (have_all_panes) {
RB_FOREACH(wl, winlinks, &s->windows) {
TAILQ_FOREACH(wp, &wl->window->panes, entry) {
ft = format_create_defaults(NULL, c, s, wl, wp);
RB_FOREACH_SAFE(csub, control_subs, &cs->subs,
csub1) {
if (csub->type != CONTROL_SUB_ALL_PANES)
continue;
control_check_subs_all_panes_one(c,
csub, ft, wl, wp);
}
format_free(ft);
}
}
}
/* Check all-windows subscriptions. */
if (have_all_windows) {
RB_FOREACH(wl, winlinks, &s->windows) {
ft = format_create_defaults(NULL, c, s, wl, NULL);
RB_FOREACH_SAFE(csub, control_subs, &cs->subs,
csub1) {
if (csub->type != CONTROL_SUB_ALL_WINDOWS)
continue;
control_check_subs_all_windows_one(c, csub, ft,
wl);
}
format_free(ft);
}
}
}
/* Add a subscription. */
void
control_add_sub(struct client *c, const char *name, enum control_sub_type type,
control_add_sub(struct client *c, const char *name, enum monitor_type type,
int id, const char *format)
{
struct control_state *cs = c->control_state;
struct control_sub *csub, find;
struct timeval tv = { .tv_sec = 1 };
find.name = (char *)name;
if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL)
control_free_sub(cs, csub);
csub = xcalloc(1, sizeof *csub);
csub->name = xstrdup(name);
csub->type = type;
csub->id = id;
csub->format = xstrdup(format);
RB_INSERT(control_subs, &cs->subs, csub);
RB_INIT(&csub->panes);
RB_INIT(&csub->windows);
if (!evtimer_initialized(&cs->subs_timer))
evtimer_set(&cs->subs_timer, control_check_subs_timer, c);
if (!evtimer_pending(&cs->subs_timer, NULL))
evtimer_add(&cs->subs_timer, &tv);
monitor_add(cs->subs, name, type, id, format, MONITOR_NOTIFY_INITIAL);
}
/* Remove a subscription. */
@@ -1168,11 +783,6 @@ void
control_remove_sub(struct client *c, const char *name)
{
struct control_state *cs = c->control_state;
struct control_sub *csub, find;
find.name = (char *)name;
if ((csub = RB_FIND(control_subs, &cs->subs, &find)) != NULL)
control_free_sub(cs, csub);
if (RB_EMPTY(&cs->subs))
evtimer_del(&cs->subs_timer);
monitor_remove(cs->subs, name);
}

530
format.c
View File

@@ -42,6 +42,7 @@
struct format_expand_state;
static char *format_job_get(struct format_expand_state *, const char *);
static char *format_quote_shell_single(const char *);
static char *format_expand1(struct format_expand_state *, const char *);
static int format_replace(struct format_expand_state *, const char *,
size_t, char **, size_t *, size_t *);
@@ -121,6 +122,11 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2)
#define FORMAT_CLIENT_TERMCAP 0x1000000
#define FORMAT_CLIENT_TERMFEAT 0x2000000
#define FORMAT_CLIENT_ENVIRON 0x4000000
#define FORMAT_COLOUR_ESC_FG 0x8000000
#define FORMAT_COLOUR_ESC_BG 0x10000000
#define FORMAT_QUOTE_SHELL_SQ 0x20000000
#define FORMAT_OPTIONS 0x40000000
#define FORMAT_ENVIRON 0x80000000ULL
/* Limit on recursion. */
#define FORMAT_LOOP_LIMIT 100
@@ -876,6 +882,37 @@ format_cb_start_command(struct format_tree *ft)
return (cmd_stringify_argv(wp->argc, wp->argv));
}
/* Callback for pane_start_command_list. */
static void *
format_cb_start_command_list(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
char *buf = NULL, *s;
size_t len = 0;
int i;
if (wp == NULL)
return (NULL);
if (wp->argc == 0)
return (xstrdup(""));
for (i = 0; i < wp->argc; i++) {
s = format_quote_shell_single(wp->argv[i]);
len += strlen(s) + 1;
buf = xrealloc(buf, len);
if (i == 0)
*buf = '\0';
else
strlcat(buf, " ", len);
strlcat(buf, s, len);
free(s);
}
return (buf);
}
/* Callback for pane_start_path. */
static void *
format_cb_start_path(struct format_tree *ft)
@@ -2304,7 +2341,7 @@ format_cb_pane_path(struct format_tree *ft)
static void *
format_cb_pane_pid(struct format_tree *ft)
{
if (ft->wp != NULL)
if (ft->wp != NULL && ft->wp->fd != -1)
return (format_printf("%ld", (long)ft->wp->pid));
return (NULL);
}
@@ -2859,6 +2896,19 @@ format_cb_window_height(struct format_tree *ft)
return (NULL);
}
/* Callback for window_manual_height. */
static void *
format_cb_window_manual_height(struct format_tree *ft)
{
struct window *w = ft->w;
if (w == NULL)
return (NULL);
if (options_get_number(w->options, "window-size") != WINDOW_SIZE_MANUAL)
return (xstrdup(""));
return (format_printf("%u", w->manual_sy));
}
/* Callback for window_id. */
static void *
format_cb_window_id(struct format_tree *ft)
@@ -3039,6 +3089,19 @@ format_cb_window_width(struct format_tree *ft)
return (NULL);
}
/* Callback for window_manual_width. */
static void *
format_cb_window_manual_width(struct format_tree *ft)
{
struct window *w = ft->w;
if (w == NULL)
return (NULL);
if (options_get_number(w->options, "window-size") != WINDOW_SIZE_MANUAL)
return (xstrdup(""));
return (format_printf("%u", w->manual_sx));
}
/* Callback for window_zoomed_flag. */
static void *
format_cb_window_zoomed_flag(struct format_tree *ft)
@@ -3529,6 +3592,9 @@ static const struct format_table_entry format_table[] = {
{ "pane_start_command", FORMAT_TABLE_STRING,
format_cb_start_command
},
{ "pane_start_command_list", FORMAT_TABLE_STRING,
format_cb_start_command_list
},
{ "pane_start_path", FORMAT_TABLE_STRING,
format_cb_start_path
},
@@ -3745,6 +3811,12 @@ static const struct format_table_entry format_table[] = {
{ "window_linked_sessions_list", FORMAT_TABLE_STRING,
format_cb_window_linked_sessions_list
},
{ "window_manual_height", FORMAT_TABLE_STRING,
format_cb_window_manual_height
},
{ "window_manual_width", FORMAT_TABLE_STRING,
format_cb_window_manual_width
},
{ "window_marked_flag", FORMAT_TABLE_STRING,
format_cb_window_marked_flag
},
@@ -4025,6 +4097,29 @@ format_quote_shell(const char *s)
return (out);
}
/* Quote string with POSIX shell single quotes. */
static char *
format_quote_shell_single(const char *s)
{
const char *cp;
char *out, *at;
at = out = xmalloc(strlen(s) * 4 + 3);
*at++ = '\'';
for (cp = s; *cp != '\0'; cp++) {
if (*cp == '\'') {
*at++ = '\'';
*at++ = '\\';
*at++ = '\'';
*at++ = '\'';
} else
*at++ = *cp;
}
*at++ = '\'';
*at = '\0';
return (out);
}
/* Quote #s in string. */
static char *
format_quote_style(const char *s)
@@ -4128,7 +4223,7 @@ format_relative_time(time_t t)
/* Find a format entry. */
static char *
format_find(struct format_tree *ft, const char *key, int modifiers,
format_find(struct format_tree *ft, const char *key, uint64_t modifiers,
const char *time_format)
{
const struct format_table_entry *fte;
@@ -4136,25 +4231,26 @@ format_find(struct format_tree *ft, const char *key, int modifiers,
struct format_entry *fe, fe_find;
struct environ_entry *envent;
struct options_entry *o;
int idx;
char *found = NULL, *saved, s[512];
char *array_key = NULL;
const char *errstr;
time_t t = 0;
struct tm tm;
o = options_parse_get(global_options, key, &idx, 0);
o = options_parse_get(global_options, key, &array_key, 0);
if (o == NULL && ft->wp != NULL)
o = options_parse_get(ft->wp->options, key, &idx, 0);
o = options_parse_get(ft->wp->options, key, &array_key, 0);
if (o == NULL && ft->w != NULL)
o = options_parse_get(ft->w->options, key, &idx, 0);
o = options_parse_get(ft->w->options, key, &array_key, 0);
if (o == NULL)
o = options_parse_get(global_w_options, key, &idx, 0);
o = options_parse_get(global_w_options, key, &array_key, 0);
if (o == NULL && ft->s != NULL)
o = options_parse_get(ft->s->options, key, &idx, 0);
o = options_parse_get(ft->s->options, key, &array_key, 0);
if (o == NULL)
o = options_parse_get(global_s_options, key, &idx, 0);
o = options_parse_get(global_s_options, key, &array_key, 0);
if (o != NULL) {
found = options_to_string(o, idx, 1);
found = options_to_string(o, array_key, 1);
free(array_key);
goto found;
}
@@ -4243,6 +4339,11 @@ found:
found = format_quote_shell(saved);
free(saved);
}
if (modifiers & FORMAT_QUOTE_SHELL_SQ) {
saved = found;
found = format_quote_shell_single(saved);
free(saved);
}
if (modifiers & FORMAT_QUOTE_STYLE) {
saved = found;
found = format_quote_style(saved);
@@ -4453,7 +4554,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s,
/*
* Modifiers are a ; separated list of the forms:
* l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,R,<,>
* l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,<,>
* =a
* =/a
* =/a/
@@ -4472,7 +4573,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s,
break;
/* Check single character modifiers with no arguments. */
if (strchr("labcdnwETSWPL!<>", cp[0]) != NULL &&
if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL &&
format_is_end(cp[1])) {
format_add_modifier(&list, count, cp, 1, NULL, 0);
cp++;
@@ -4494,7 +4595,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s,
}
/* Now try single character with arguments. */
if (strchr("ImCLNPSst=pReqW", cp[0]) == NULL)
if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL)
break;
c = cp[0];
@@ -4771,11 +4872,14 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt)
else
use = all;
nft = format_create(c, item, FORMAT_NONE, ft->flags);
format_add(nft, "loop_index", "%d", i);
format_add(nft, "loop_last_flag", "%d", i == n - 1);
format_defaults(nft, ft->c, s, NULL, NULL);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, use);
format_free(next.ft);
@@ -4818,9 +4922,9 @@ format_window_name(struct format_expand_state *es, const char *fmt)
return (xstrdup("0"));
}
/* Add neighbor window variables to the format tree. */
/* Add neighbour window variables to the format tree. */
static void
format_add_window_neighbor(struct format_tree *nft, struct winlink *wl,
format_add_window_neighbour(struct format_tree *nft, struct winlink *wl,
struct session *s, const char *prefix)
{
struct options_entry *o;
@@ -4840,7 +4944,7 @@ format_add_window_neighbor(struct format_tree *nft, struct winlink *wl,
oname = options_name(o);
if (*oname == '@') {
xasprintf(&prefixed, "%s_%s", prefix, oname);
oval = options_to_string(o, -1, 1);
oval = options_to_string(o, NULL, 1);
format_add(nft, prefixed, "%s", oval);
free(oval);
free(prefixed);
@@ -4853,20 +4957,21 @@ format_add_window_neighbor(struct format_tree *nft, struct winlink *wl,
static char *
format_loop_windows(struct format_expand_state *es, const char *fmt)
{
struct sort_criteria *sc = &sort_crit;
struct format_tree *ft = es->ft;
struct client *c = ft->client;
struct cmdq_item *item = ft->item;
struct format_tree *nft;
struct format_expand_state next;
char *all, *active, *use, *expanded, *value;
struct evbuffer *buffer;
size_t size;
struct winlink *wl, **l;
struct window *w;
int i, n;
struct sort_criteria *sc = &sort_crit;
struct format_tree *ft = es->ft;
struct client *c = ft->client;
struct session *s = ft->s;
struct cmdq_item *item = ft->item;
struct format_tree *nft;
struct format_expand_state next;
char *all, *active, *use, *expanded, *value;
struct evbuffer *buffer;
size_t size;
struct winlink *wl, **l;
struct window *w;
int i, n;
if (ft->s == NULL) {
if (s == NULL) {
format_log(es, "window loop but no session");
return (NULL);
}
@@ -4880,33 +4985,38 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
if (buffer == NULL)
fatalx("out of memory");
l = sort_get_winlinks_session(ft->s, &n, sc);
l = sort_get_winlinks_session(s, &n, sc);
for (i = 0; i < n; i++) {
wl = l[i];
w = wl->window;
format_log(es, "window loop: %u @%u", wl->idx, w->id);
if (active != NULL && wl == ft->s->curw)
if (active != NULL && wl == s->curw)
use = active;
else
use = all;
nft = format_create(c, item, FORMAT_WINDOW|w->id,
ft->flags);
nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags);
format_add(nft, "loop_index", "%d", i);
format_add(nft, "loop_last_flag", "%d", i == n - 1);
format_defaults(nft, ft->c, ft->s, wl, NULL);
/* Add neighbor window data to the format tree. */
format_add(nft, "window_after_active", "%d",
i > 0 && l[i - 1] == ft->s->curw);
format_add(nft, "window_before_active", "%d",
i + 1 < n && l[i + 1] == ft->s->curw);
/* Add neighbour window data to the format tree. */
if (i > 0 && l[i - 1] == s->curw)
format_add(nft, "window_after_active", "1");
else
format_add(nft, "window_after_active", "0");
if (i + 1 < n && l[i + 1] == s->curw)
format_add(nft, "window_before_active", "1");
else
format_add(nft, "window_before_active", "0");
if (i + 1 < n)
format_add_window_neighbor(nft, l[i + 1], ft->s, "next");
format_add_window_neighbour(nft, l[i + 1], s, "next");
if (i > 0)
format_add_window_neighbor(nft, l[i - 1], ft->s, "prev");
format_add_window_neighbour(nft, l[i - 1], s, "prev");
format_defaults(nft, ft->c, s, wl, NULL);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, use);
format_free(nft);
@@ -4963,13 +5073,15 @@ format_loop_panes(struct format_expand_state *es, const char *fmt)
use = active;
else
use = all;
nft = format_create(c, item, FORMAT_PANE|wp->id,
ft->flags);
nft = format_create(c, item, FORMAT_PANE|wp->id, ft->flags);
format_add(nft, "loop_index", "%d", i);
format_add(nft, "loop_last_flag", "%d", i == n - 1);
format_defaults(nft, ft->c, ft->s, ft->wl, wp);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, use);
format_free(nft);
@@ -4988,6 +5100,271 @@ format_loop_panes(struct format_expand_state *es, const char *fmt)
return (value);
}
/* Add an option to an options loop. */
static void
format_loop_add_option(struct format_expand_state *es, const char *fmt,
struct evbuffer *buffer, struct options_entry *o, u_int n, u_int i)
{
struct format_tree *ft = es->ft, *nft;
struct format_expand_state next;
const struct options_table_entry *oe = options_table_entry(o);
const char *name = options_name(o);
char *expanded, *s;
int is_array = options_is_array(o);
format_log(es, "option loop: %s", name);
nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags);
format_add(nft, "option_name", "%s", name);
s = options_to_string(o, NULL, 0);
format_add(nft, "option_value", "%s", s);
free(s);
format_add(nft, "option_is_array", "%d", is_array);
format_add(nft, "option_array_key", "%s", "");
format_add(nft, "option_array_index", "%s", "");
format_add(nft, "option_array_first", "%d", is_array);
format_add(nft, "option_array_last", "%d", is_array);
format_add(nft, "option_array_count", "%u", n);
if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK))
format_add(nft, "option_is_hook", "1");
else
format_add(nft, "option_is_hook", "0");
format_add(nft, "option_is_user", "%d", oe == NULL);
if (options_next(o) == NULL)
format_add(nft, "loop_last_flag", "1");
else
format_add(nft, "loop_last_flag", "0");
format_add(nft, "loop_index", "%u", i);
format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, fmt);
format_free(nft);
evbuffer_add(buffer, expanded, strlen(expanded));
free(expanded);
}
/* Add an array option item to an options loop. */
static void
format_loop_add_array_item(struct format_expand_state *es, const char *fmt,
struct evbuffer *buffer, struct options_entry *o,
struct options_array_item *a, int n, u_int i)
{
struct format_tree *ft = es->ft, *nft;
struct format_expand_state next;
const struct options_table_entry *oe = options_table_entry(o);
const char *name = options_name(o);
const char *array_key;
char *expanded, *s;
array_key = options_array_item_key(a);
format_log(es, "option loop: %s[%s]", name, array_key);
nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags);
format_add(nft, "option_name", "%s", name);
s = options_to_string(o, array_key, 0);
format_add(nft, "option_value", "%s", s);
free(s);
format_add(nft, "option_is_array", "1");
format_add(nft, "option_array_key", "%s", array_key);
format_add(nft, "option_array_index", "%s", array_key);
if (a == options_array_first(o))
format_add(nft, "option_array_first", "1");
else
format_add(nft, "option_array_first", "0");
if (options_array_next(a) == NULL)
format_add(nft, "option_array_last", "1");
else
format_add(nft, "option_array_last", "0");
format_add(nft, "option_array_count", "%u", n);
if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK))
format_add(nft, "option_is_hook", "1");
else
format_add(nft, "option_is_hook", "0");
format_add(nft, "option_is_user", "%d", oe == NULL);
if (options_array_next(a) == NULL && options_next(o) == NULL)
format_add(nft, "loop_last_flag", "1");
else
format_add(nft, "loop_last_flag", "0");
format_add(nft, "loop_index", "%u", i);
format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, fmt);
format_free(nft);
evbuffer_add(buffer, expanded, strlen(expanded));
free(expanded);
}
/* Loop over options. */
static char *
format_loop_options(struct format_expand_state *es, const char *fmt,
const char *flags)
{
struct format_tree *ft = es->ft;
struct options *oo = NULL;
struct options_entry *o;
struct options_array_item *a;
char *value;
struct evbuffer *buffer;
size_t size;
u_int i = 0, n;
int global = 0;
if (flags == NULL || *flags == '\0')
flags = "s";
if (strchr(flags, 'v') != NULL)
oo = global_options;
else {
if (strchr(flags, 'g') != NULL)
global = 1;
if (strchr(flags, 'w') != NULL) {
if (global)
oo = global_w_options;
else if (ft->w != NULL)
oo = ft->w->options;
} else if (strchr(flags, 's') != NULL) {
if (global)
oo = global_s_options;
else if (ft->s != NULL)
oo = ft->s->options;
} else if (strchr(flags, 'p') != NULL) {
if (global)
/* invalid */;
else if (ft->wp != NULL)
oo = ft->wp->options;
} else if (global)
oo = global_s_options;
}
if (oo == NULL)
return (xstrdup(""));
buffer = evbuffer_new();
if (buffer == NULL)
fatalx("out of memory");
o = options_first(oo);
while (o != NULL) {
n = 0;
if (options_is_array(o)) {
a = options_array_first(o);
while (a != NULL) {
n++;
a = options_array_next(a);
}
}
if (!options_is_array(o) || n == 0) {
format_loop_add_option(es, fmt, buffer, o, n, i);
i++;
o = options_next(o);
continue;
}
a = options_array_first(o);
while (a != NULL) {
format_loop_add_array_item(es, fmt, buffer, o, a, n, i);
i++;
a = options_array_next(a);
}
o = options_next(o);
}
if ((size = EVBUFFER_LENGTH(buffer)) != 0)
value = xmemdup(EVBUFFER_DATA(buffer), size);
else
value = xstrdup("");
evbuffer_free(buffer);
return (value);
}
/* Loop over an environment. */
static char *
format_loop_environ(struct format_expand_state *es, const char *fmt,
const char *flags)
{
struct format_tree *ft = es->ft, *nft;
struct client *c = ft->client;
struct cmdq_item *item = ft->item;
struct format_expand_state next;
struct environ *env = NULL;
struct environ_entry *envent;
char *expanded, *value;
struct evbuffer *buffer;
size_t size;
u_int i = 0;
if (flags == NULL || *flags == '\0' || strcmp(flags, "s") == 0) {
if (ft->s != NULL)
env = ft->s->environ;
} else if (strcmp(flags, "g") == 0)
env = global_environ;
else if (strcmp(flags, "c") == 0) {
if (ft->client != NULL)
env = ft->client->environ;
}
if (env == NULL)
return (xstrdup(""));
buffer = evbuffer_new();
if (buffer == NULL)
fatalx("out of memory");
envent = environ_first(env);
while (envent != NULL) {
format_log(es, "environment loop: %s", envent->name);
nft = format_create(c, item, FORMAT_NONE, ft->flags);
format_add(nft, "environ_name", "%s", envent->name);
if (envent->value == NULL)
format_add(nft, "environ_value", "%s", "");
else
format_add(nft, "environ_value", "%s", envent->value);
if (envent->flags & ENVIRON_HIDDEN)
format_add(nft, "environ_hidden", "1");
else
format_add(nft, "environ_hidden", "0");
format_add(nft, "environ_removed", "%d", envent->value == NULL);
if (environ_next(envent) == NULL)
format_add(nft, "loop_last_flag", "1");
else
format_add(nft, "loop_last_flag", "0");
format_add(nft, "loop_index", "%u", i);
format_defaults(nft, ft->c, ft->s, ft->wl, ft->wp);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, fmt);
format_free(nft);
evbuffer_add(buffer, expanded, strlen(expanded));
free(expanded);
i++;
envent = environ_next(envent);
}
if ((size = EVBUFFER_LENGTH(buffer)) != 0)
value = xmemdup(EVBUFFER_DATA(buffer), size);
else
value = xstrdup("");
evbuffer_free(buffer);
return (value);
}
/* Loop over clients. */
static char *
format_loop_clients(struct format_expand_state *es, const char *fmt)
@@ -5012,14 +5389,16 @@ format_loop_clients(struct format_expand_state *es, const char *fmt)
c = l[i];
format_log(es, "client loop: %s", c->name);
nft = format_create(c, item, 0, ft->flags);
format_add(nft, "loop_index", "%d", i);
format_add(nft, "loop_last_flag", "%d", i == n - 1);
format_defaults(nft, c, ft->s, ft->wl, ft->wp);
format_copy_state(&next, es, 0);
next.ft = nft;
expanded = format_expand1(&next, fmt);
format_free(nft);
evbuffer_add(buffer, expanded, strlen(expanded));
free(expanded);
}
@@ -5189,12 +5568,14 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
char *copy0, *condition, *found, *new;
char *value, *left, *right;
size_t valuelen;
int modifiers = 0, limit = 0, width = 0;
uint64_t modifiers = 0;
int limit = 0, width = 0;
int j, c;
struct format_modifier *list, *cmp = NULL, *search = NULL;
struct format_modifier **sub = NULL, *mexp = NULL, *fm;
struct format_modifier *bool_op_n = NULL;
u_int i, count, nsub = 0, nrep, check = 0;
const char *loop_flags = "";
struct format_expand_state next;
struct environ_entry *envent;
@@ -5272,6 +5653,12 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
break;
case 'c':
modifiers |= FORMAT_COLOUR;
if (fm->argc < 1)
break;
if (strchr(fm->argv[0], 'f') != NULL)
modifiers |= FORMAT_COLOUR_ESC_FG;
if (strchr(fm->argv[0], 'b') != NULL)
modifiers |= FORMAT_COLOUR_ESC_BG;
break;
case 'd':
modifiers |= FORMAT_DIRNAME;
@@ -5307,6 +5694,8 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
case 'q':
if (fm->argc < 1)
modifiers |= FORMAT_QUOTE_SHELL;
else if (strchr(fm->argv[0], 's') != NULL)
modifiers |= FORMAT_QUOTE_SHELL_SQ;
else if (strchr(fm->argv[0], 'e') != NULL ||
strchr(fm->argv[0], 'h') != NULL)
modifiers |= FORMAT_QUOTE_STYLE;
@@ -5373,11 +5762,27 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
sc->reversed = 0;
break;
}
if (strchr(fm->argv[0], 'i') != NULL)
sc->order = SORT_INDEX;
else if (strchr(fm->argv[0], 'z') != NULL)
sc->order = SORT_Z;
else
sc->order = SORT_CREATION;
if (strchr(fm->argv[0], 'r') != NULL)
sc->reversed = 1;
else
sc->reversed = 0;
break;
case 'O':
modifiers |= FORMAT_OPTIONS;
if (fm->argc == 1)
loop_flags = fm->argv[0];
break;
case 'V':
modifiers |= FORMAT_ENVIRON;
if (fm->argc == 1)
loop_flags = fm->argv[0];
break;
case 'L':
modifiers |= FORMAT_CLIENTS;
if (fm->argc < 1) {
@@ -5470,11 +5875,28 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
/* Is this a colour? */
if (modifiers & FORMAT_COLOUR) {
new = format_expand1(es, copy);
c = colour_fromstring(new);
if (c == -1 || (c = colour_force_rgb(c)) == -1)
value = xstrdup("");
else
xasprintf(&value, "%06x", c & 0xffffff);
if (modifiers & (FORMAT_COLOUR_ESC_FG|FORMAT_COLOUR_ESC_BG)) {
if (strcasecmp(new, "none") == 0)
value = xstrdup("\033[0m");
else if ((c = colour_fromstring(new)) == -1)
value = xstrdup("");
else {
if (modifiers & FORMAT_COLOUR_ESC_BG)
cp = colour_toescape(ft->c, c, 1);
else
cp = colour_toescape(ft->c, c, 0);
if (cp == NULL)
value = xstrdup("");
else
value = xstrdup(cp);
}
} else {
c = colour_fromstring(new);
if (c == -1 || (c = colour_force_rgb(c)) == -1)
value = xstrdup("");
else
xasprintf(&value, "%06x", c & 0xffffff);
}
free(new);
goto done;
}
@@ -5496,6 +5918,14 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
value = format_loop_clients(es, copy);
if (value == NULL)
goto fail;
} else if (modifiers & FORMAT_OPTIONS) {
value = format_loop_options(es, copy, loop_flags);
if (value == NULL)
goto fail;
} else if (modifiers & FORMAT_ENVIRON) {
value = format_loop_environ(es, copy, loop_flags);
if (value == NULL)
goto fail;
} else if (modifiers & FORMAT_WINDOW_NAME) {
value = format_window_name(es, copy);
if (value == NULL)

157
grid.c
View File

@@ -60,23 +60,37 @@ static const struct grid_cell_entry grid_cleared_entry = {
};
#ifdef __APPLE__
static void
grid_check_lines(struct grid *gd)
void
grid_check_is_clear(struct grid *gd)
{
u_int i, j;
struct grid_line *gl;
u_int yy, ny;
for (i = 0; i < gd->hsize + gd->sy; i++) {
for (j = i + 1; j < gd->hsize + gd->sy; j++) {
if (gd->linedata[i].celldata != NULL)
assert(gd->linedata[i].celldata != gd->linedata[j].celldata);
if (gd->linedata[i].extddata != NULL)
assert(gd->linedata[i].extddata != gd->linedata[j].extddata);
}
assert(gd != NULL);
if (gd->sy == 0) {
assert(gd->linedata == NULL);
return;
}
assert(gd->linedata != NULL);
ny = gd->hsize + gd->sy;
for (yy = 0; yy < ny; yy++) {
gl = &gd->linedata[yy];
assert(gl->celldata == NULL);
assert(gl->cellused == 0);
assert(gl->cellsize == 0);
assert(gl->extddata == NULL);
assert(gl->extdsize == 0);
assert(gl->flags == 0);
assert(gl->time == 0);
}
}
#else
static void
grid_check_lines(__unused struct grid *gd)
void
grid_check_is_clear(__unused struct grid *gd)
{
}
#endif
@@ -341,28 +355,18 @@ grid_create(u_int sx, u_int sy, u_int hlimit)
{
struct grid *gd;
gd = xmalloc(sizeof *gd);
gd = xcalloc(1, sizeof *gd);
gd->sx = sx;
gd->sy = sy;
if (hlimit != 0)
gd->flags = GRID_HISTORY;
else
gd->flags = 0;
gd->hscrolled = 0;
gd->hsize = 0;
gd->hlimit = hlimit;
gd->scroll_added = 0;
gd->scroll_collected = 0;
gd->scroll_generation = 0;
if (gd->sy != 0)
gd->linedata = xcalloc(gd->sy, sizeof *gd->linedata);
else
gd->linedata = NULL;
grid_check_is_clear(gd);
return (gd);
}
@@ -482,8 +486,6 @@ grid_scroll_history(struct grid *gd, u_int bg)
gd->linedata[gd->hsize].time = current_time;
gd->hsize++;
gd->scroll_added++;
grid_check_lines(gd);
}
/* Clear the history. */
@@ -533,8 +535,6 @@ grid_scroll_history_region(struct grid *gd, u_int upper, u_int lower, u_int bg)
gd->hscrolled++;
gd->hsize++;
gd->scroll_added++;
grid_check_lines(gd);
}
/* Expand line to fit to cell. */
@@ -796,8 +796,6 @@ grid_move_lines(struct grid *gd, u_int dy, u_int py, u_int ny, u_int bg)
}
if (py != 0 && (py < dy || py >= dy + ny))
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
grid_check_lines(gd);
}
/* Move a group of cells. */
@@ -1287,8 +1285,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
sy++;
dy++;
}
grid_check_lines(dst);
}
/* Mark line as dead. */
@@ -1585,8 +1581,6 @@ grid_reflow(struct grid *gd, u_int sx)
gd->linedata = target->linedata;
free(target);
gd->scroll_generation++;
grid_check_lines(gd);
}
/* Convert to position based on wrapped lines. */
@@ -1688,3 +1682,98 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set)
return (0);
return (utf8_cstrhas(set, &gc.data));
}
/* Line flags to string. */
const char *
grid_line_flags_string(int flags)
{
static char s[128];
*s = '\0';
if (flags & GRID_LINE_WRAPPED)
strlcat(s, "WRAPPED,", sizeof s);
if (flags & GRID_LINE_EXTENDED)
strlcat(s, "EXTENDED,", sizeof s);
if (flags & GRID_LINE_DEAD)
strlcat(s, "DEAD,", sizeof s);
if (flags & GRID_LINE_START_PROMPT)
strlcat(s, "START_PROMPT,", sizeof s);
if (flags & GRID_LINE_START_OUTPUT)
strlcat(s, "START_OUTPUT,", sizeof s);
if (flags & GRID_LINE_HYPERLINK)
strlcat(s, "HYPERLINK,", sizeof s);
if (*s == '\0')
return ("NONE");
s[strlen(s) - 1] = '\0';
return (s);
}
/* Cell flags to string. */
const char *
grid_cell_flags_string(int flags)
{
static char s[128];
*s = '\0';
if (flags & GRID_FLAG_FG256)
strlcat(s, "FG256,", sizeof s);
if (flags & GRID_FLAG_BG256)
strlcat(s, "BG256,", sizeof s);
if (flags & GRID_FLAG_PADDING)
strlcat(s, "PADDING,", sizeof s);
if (flags & GRID_FLAG_EXTENDED)
strlcat(s, "EXTENDED,", sizeof s);
if (flags & GRID_FLAG_SELECTED)
strlcat(s, "SELECTED,", sizeof s);
if (flags & GRID_FLAG_CLEARED)
strlcat(s, "CLEARED,", sizeof s);
if (flags & GRID_FLAG_TAB)
strlcat(s, "TAB,", sizeof s);
if (flags & GRID_FLAG_NOPALETTE)
strlcat(s, "NOPALETTE,", sizeof s);
if (*s == '\0')
return ("NONE");
s[strlen(s) - 1] = '\0';
return (s);
}
/* Cell attributes to string. */
const char *
grid_cell_attr_string(int attr)
{
static char s[256];
*s = '\0';
if (attr & GRID_ATTR_CHARSET)
strlcat(s, "CHARSET,", sizeof s);
if (attr & GRID_ATTR_BRIGHT)
strlcat(s, "BRIGHT,", sizeof s);
if (attr & GRID_ATTR_DIM)
strlcat(s, "DIM,", sizeof s);
if (attr & GRID_ATTR_UNDERSCORE)
strlcat(s, "UNDERSCORE,", sizeof s);
if (attr & GRID_ATTR_BLINK)
strlcat(s, "BLINK,", sizeof s);
if (attr & GRID_ATTR_REVERSE)
strlcat(s, "REVERSE,", sizeof s);
if (attr & GRID_ATTR_HIDDEN)
strlcat(s, "HIDDEN,", sizeof s);
if (attr & GRID_ATTR_ITALICS)
strlcat(s, "ITALICS,", sizeof s);
if (attr & GRID_ATTR_STRIKETHROUGH)
strlcat(s, "STRIKETHROUGH,", sizeof s);
if (attr & GRID_ATTR_UNDERSCORE_2)
strlcat(s, "UNDERSCORE_2,", sizeof s);
if (attr & GRID_ATTR_UNDERSCORE_3)
strlcat(s, "UNDERSCORE_3,", sizeof s);
if (attr & GRID_ATTR_UNDERSCORE_4)
strlcat(s, "UNDERSCORE_4,", sizeof s);
if (attr & GRID_ATTR_UNDERSCORE_5)
strlcat(s, "UNDERSCORE_5,", sizeof s);
if (attr & GRID_ATTR_OVERLINE)
strlcat(s, "OVERLINE,", sizeof s);
if (*s == '\0')
return ("NONE");
s[strlen(s) - 1] = '\0';
return (s);
}

View File

@@ -2818,7 +2818,7 @@ input_exit_rename(struct input_ctx *ictx)
if (ictx->input_len == 0) {
o = options_get_only(w->options, "automatic-rename");
if (o != NULL)
options_remove_or_default(o, -1, NULL);
options_remove_or_default(o, NULL, NULL);
if (!options_get_number(w->options, "automatic-rename"))
window_set_name(w, "", 1);
} else {

View File

@@ -59,17 +59,45 @@
" '#{?mouse_hyperlink,Type #[underscore]#{=/9/...:mouse_hyperlink},}' 'C-h' {copy-mode -q; send-keys -l -- \"#{q:mouse_hyperlink}\"}" \
" '#{?mouse_hyperlink,Copy #[underscore]#{=/9/...:mouse_hyperlink},}' 'h' {copy-mode -q; set-buffer -- \"#{q:mouse_hyperlink}\"}" \
" ''" \
" '#{?#{#{pane_floating_flag}},Move,}' '' {display-menu -xL -yL -T '#[align=centre]Move' " DEFAULT_MOVE_MENU " }" \
" '#{?#{#{pane_floating_flag}},Move & Resize,}' '' {display-menu -xL -yL -T '#[align=centre]Move & Resize' " DEFAULT_MOVE_RESIZE_MENU " }" \
" '#{?#{#{pane_floating_flag}},Tile,}' 't' { join-pane }" \
" '#{?#{!:#{pane_floating_flag}},Float,}' 'f' { break-pane -W }" \
" '#{?#{!:#{pane_floating_flag}},Horizontal Split,}' 'h' {split-window -h}" \
" '#{?#{!:#{pane_floating_flag}},Vertical Split,}' 'v' {split-window -v}" \
" ''" \
" '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' 'u' {swap-pane -U}" \
" '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' 'd' {swap-pane -D}" \
" '#{?#{!:#{pane_floating_flag}},#{?pane_marked_set,,-}Swap Marked,}' 's' {swap-pane}" \
" '#{?pane_marked_set,,-}Swap Marked' 's' {swap-pane}" \
" ''" \
" 'Kill' 'X' {kill-pane}" \
" 'Respawn' 'R' {respawn-pane -k}" \
" '#{?pane_marked,Unmark,Mark}' 'm' {select-pane -m}" \
" '#{?#{>:#{window_panes},1},,-}#{?window_zoomed_flag,Unzoom,Zoom}' 'z' {resize-pane -Z}"
#define DEFAULT_MOVE_MENU \
" 'Centre' 'c' {move-pane -P centre}" \
" ''" \
" 'Top Left' '1' {move-pane -P top-left}" \
" 'Top Right' '2' {move-pane -P top-right}" \
" 'Bottom Left' '3' {move-pane -P bottom-left}" \
" 'Bottom Right' '4' {move-pane -P bottom-right}" \
" ''" \
" 'Top' 't' {move-pane -P top-centre}" \
" 'Bottom' 'b' {move-pane -P bottom-centre}" \
" 'Left' 'l' {move-pane -P centre-left}" \
" 'Right' 'r' {move-pane -P centre-right}"
#define DEFAULT_MOVE_RESIZE_MENU \
" 'Fill' '0' {resize-pane -x100% -y100%; move-pane -P top-left}" \
" ''" \
" 'Top Left' '1' {resize-pane -x50% -y50%; move-pane -P top-left}" \
" 'Top Right' '2' {resize-pane -x50% -y50%; move-pane -P top-right}" \
" 'Bottom Left' '3' {resize-pane -x50% -y50%; move-pane -P bottom-left}" \
" 'Bottom Right' '4' {resize-pane -x50% -y50%; move-pane -P bottom-right}" \
" ''" \
" 'Top' 't' {resize-pane -x100% -y50%; move-pane -P top-centre}" \
" 'Bottom' 'b' {resize-pane -x100% -y50%; move-pane -P bottom-centre}" \
" 'Left' 'l' {resize-pane -x50% -y100%; move-pane -P centre-left}" \
" 'Right' 'r' {resize-pane -x50% -y100%; move-pane -P centre-right}"
static int key_bindings_cmp(struct key_binding *, struct key_binding *);
RB_GENERATE_STATIC(key_bindings, key_binding, entry, key_bindings_cmp);
@@ -360,13 +388,14 @@ key_bindings_init(void)
"bind -N 'Rename current session' '$' { command-prompt -I'#S' { rename-session -- '%%' } }",
"bind -N 'Split window horizontally' % { split-window -h }",
"bind -N 'Kill current window' & { confirm-before -p\"kill-window #W? (y/n)\" kill-window }",
"bind -N 'Prompt for window index to select' \"'\" { command-prompt -T window-target -pindex { select-window -t ':%%' } }",
"bind -N 'Prompt for window index to select' \"'\" { command-prompt -pindex { select-window -t ':%%' } }",
"bind -N 'New floating pane' * { new-pane }",
"bind -N 'Toggle pane between floating and tiled' @ { if -F '#{pane_floating_flag}' { join-pane } { break-pane -W } }",
"bind -N 'Switch to previous client' ( { switch-client -p }",
"bind -N 'Switch to next client' ) { switch-client -n }",
"bind -N 'Rename current window' , { command-prompt -I'#W' { rename-window -- '%%' } }",
"bind -N 'Delete the most recent paste buffer' - { delete-buffer }",
"bind -N 'Move the current window' . { command-prompt -T target { move-window -t '%%' } }",
"bind -N 'Move the current window' . { command-prompt { move-window -t '%%' } }",
"bind -N 'Describe key binding' '/' { command-prompt -kpkey { list-keys -1N '%%' } }",
"bind -N 'Select window 0' 0 { select-window -t:=0 }",
"bind -N 'Select window 1' 1 { select-window -t:=1 }",
@@ -407,10 +436,8 @@ key_bindings_init(void)
"bind -N 'Choose a window from a list' w { choose-tree -Zw }",
"bind -N 'Kill the active pane' x { confirm-before -p\"kill-pane #P? (y/n)\" kill-pane }",
"bind -N 'Zoom the active pane' z { resize-pane -Z }",
"bind -N 'Move pane to top-left corner' '{' { resize-pane -x50% -y50%; move-pane -P top-left }",
"bind -N 'Move pane to top-right corner' '}' { resize-pane -x50% -y50%; move-pane -P top-right }",
"bind -N 'Move pane to bottom-left corner' 'M-{' { resize-pane -x50% -y50%; move-pane -P bottom-left }",
"bind -N 'Move pane to bottom-right corner' 'M-}' { resize-pane -x50% -y50%; move-pane -P bottom-right }",
"bind -N 'Swap the active pane with the pane above' '{' { swap-pane -U }",
"bind -N 'Swap the active pane with the pane below' '}' { swap-pane -D }",
"bind -N 'Show messages' '~' { show-messages }",
"bind -N 'Enter copy mode and scroll up' PPage { copy-mode -u }",
"bind -N 'Select the pane above the active pane' -r Up { select-pane -U }",
@@ -441,6 +468,28 @@ key_bindings_init(void)
"bind -N 'Resize the pane left' -r C-Left if -F '#{?floating_pane_flag}' { resizep -R-1 } { resize-pane -L }",
"bind -N 'Resize the pane right' -r C-Right { resize-pane -R }",
/* Floating pane movement. */
"bind -N 'Move a floating pane' g { switch-client -Tmove }",
"bind -Tmove -N 'Move pane to top-left corner' 1 { move-pane -P top-left }",
"bind -Tmove -N 'Move pane to top-right corner' 2 { move-pane -P top-right }",
"bind -Tmove -N 'Move pane to bottom-left corner' 3 { move-pane -P bottom-left }",
"bind -Tmove -N 'Move pane to bottom-right corner' 4 { move-pane -P bottom-right }",
"bind -Tmove -N 'Move pane to top-left corner and resize' M-1 { resize-pane -x50% -y50%; move-pane -P top-left }",
"bind -Tmove -N 'Move pane to top-right corner and resize' M-2 { resize-pane -x50% -y50%; move-pane -P top-right }",
"bind -Tmove -N 'Move pane to bottom-left corner and resize' M-3 { resize-pane -x50% -y50%; move-pane -P bottom-left }",
"bind -Tmove -N 'Move pane to bottom-right corner and resize' M-4 { resize-pane -x50% -y50%; move-pane -P bottom-right }",
"bind -Tmove -N 'Move pane to top' 'Up' { move-pane -P top-centre }",
"bind -Tmove -N 'Move pane to bottom' 'Down' { move-pane -P bottom-centre }",
"bind -Tmove -N 'Move pane to left' 'Left' { move-pane -P centre-left }",
"bind -Tmove -N 'Move pane to right' 'Right' { move-pane -P centre-right }",
"bind -Tmove -N 'Move pane to top and resize' 'M-Up' { resizep -x100% -y50%; move-pane -P top-centre }",
"bind -Tmove -N 'Move pane to bottom and resize' 'M-Down' { resizep -x100% -y50%; move-pane -P bottom-centre }",
"bind -Tmove -N 'Move pane to left and resize' 'M-Left' { resizep -x50% -y100%; move-pane -P centre-left }",
"bind -Tmove -N 'Move pane to right and resize' 'M-Right' { resizep -x50% -y100%; move-pane -P centre-right }",
"bind -Tmove -N 'Move pane to fill the window' 0 { resize-pane -x100% -y100%; move-pane -P top-left }",
"bind -Tmove -N 'Display move menu' , { if -F '#{pane_floating_flag}' { display-menu -xP -yP -T '#[align=centre]Move' " DEFAULT_MOVE_MENU " } }",
"bind -Tmove -N 'Display move and resize menu' . { if -F '#{pane_floating_flag}' { display-menu -xP -yP -T '#[align=centre]Move & Resize' " DEFAULT_MOVE_RESIZE_MENU " } }",
/* Menu keys */
"bind -N 'Display window menu' < { display-menu -xW -yW -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_WINDOW_MENU " }",
"bind -N 'Display pane menu' > { display-menu -xP -yP -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " }",
@@ -479,6 +528,7 @@ key_bindings_init(void)
/* Mouse button 1 down on default pane-border-format */
"bind -n MouseDown1Control9 { display-menu -t= -xM -yM -O -T 'Kill pane #{pane_index}?' 'Yes' 'y' { kill-pane -t= } 'No' 'n' {}}",
"bind -n MouseDown1Control8 { resize-pane -Z }",
"bind -n MouseDown1Control7 { if -Ft= '#{pane_floating_flag}' { join-pane } { break-pane -W } }",
/* Mouse wheel down on status line. */
"bind -n WheelDownStatus { next-window }",

View File

@@ -101,10 +101,10 @@ layout_append(struct layout_cell *lc, char *buf, size_t len)
return (0);
if (lc->wp != NULL) {
tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d,%u",
lc->sx, lc->sy, lc->xoff, lc->yoff, lc->wp->id);
lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff, lc->wp->id);
} else {
tmplen = xsnprintf(tmp, sizeof tmp, "%ux%u,%d,%d",
lc->sx, lc->sy, lc->xoff, lc->yoff);
lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff);
}
if (tmplen > (sizeof tmp) - 1)
return (-1);
@@ -145,24 +145,24 @@ layout_check(struct layout_cell *lc)
break;
case LAYOUT_LEFTRIGHT:
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (lcchild->sy != lc->sy)
if (lcchild->g.sy != lc->g.sy)
return (0);
if (!layout_check(lcchild))
return (0);
n += lcchild->sx + 1;
n += lcchild->g.sx + 1;
}
if (n - 1 != lc->sx)
if (n - 1 != lc->g.sx)
return (0);
break;
case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (lcchild->sx != lc->sx)
if (lcchild->g.sx != lc->g.sx)
return (0);
if (!layout_check(lcchild))
return (0);
n += lcchild->sy + 1;
n += lcchild->g.sy + 1;
}
if (n - 1 != lc->sy)
if (n - 1 != lc->g.sy)
return (0);
break;
}
@@ -237,21 +237,21 @@ layout_parse(struct window *w, const char *layout, char **cause)
break;
case LAYOUT_LEFTRIGHT:
TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) {
sy = lcchild->sy + 1;
sx += lcchild->sx + 1;
sy = lcchild->g.sy + 1;
sx += lcchild->g.sx + 1;
}
break;
case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &tiled_lc->cells, entry) {
sx = lcchild->sx + 1;
sy += lcchild->sy + 1;
sx = lcchild->g.sx + 1;
sy += lcchild->g.sy + 1;
}
break;
}
if (tiled_lc->type != LAYOUT_WINDOWPANE &&
(tiled_lc->sx != sx || tiled_lc->sy != sy)) {
(tiled_lc->g.sx != sx || tiled_lc->g.sy != sy)) {
layout_print_cell(tiled_lc, __func__, 0);
tiled_lc->sx = sx - 1; tiled_lc->sy = sy - 1;
tiled_lc->g.sx = sx - 1; tiled_lc->g.sy = sy - 1;
}
/* Check the new layout. */
@@ -262,10 +262,10 @@ layout_parse(struct window *w, const char *layout, char **cause)
/* Resize window to the layout size. */
if (sx != 0 && sy != 0)
window_resize(w, tiled_lc->sx, tiled_lc->sy, -1, -1);
window_resize(w, tiled_lc->g.sx, tiled_lc->g.sy, -1, -1);
/* Destroy the old layout and swap to the new. */
layout_free_cell(w->layout_root);
layout_free_cell(w->layout_root, 0);
w->layout_root = tiled_lc;
/* Assign the panes into the cells. */
@@ -291,7 +291,7 @@ layout_parse(struct window *w, const char *layout, char **cause)
return (0);
fail:
layout_free_cell(tiled_lc);
layout_free_cell(tiled_lc, 0);
return (-1);
}
@@ -358,10 +358,10 @@ layout_construct_cell(struct layout_cell *lcparent, const char **layout)
}
lc = layout_create_cell(lcparent);
lc->sx = sx;
lc->sy = sy;
lc->xoff = xoff;
lc->yoff = yoff;
lc->g.sx = sx;
lc->g.sy = sy;
lc->g.xoff = xoff;
lc->g.yoff = yoff;
return (lc);
}
@@ -423,6 +423,6 @@ layout_construct(struct layout_cell *lcparent, const char **layout,
return (0);
fail:
layout_free_cell(*lc);
layout_free_cell(*lc, 0);
return (-1);
}

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->g.sx = w->sx;
lcchild->g.sy = w->sy;
}
}
/* Spread out cells. */
layout_spread_cell(w, lc);
layout_spread_cell(w, lcroot);
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -206,15 +202,14 @@ layout_set_even_v(struct window *w)
static void
layout_set_main_h(struct window *w)
{
struct window_pane *wp;
struct layout_cell *lc, *lcmain, *lcother, *lcchild;
struct window_pane *wp, *wpmain;
struct layout_cell *lcroot, *lcmain, *lcother, *lcchild;
u_int n, mainh, otherh, sx, sy;
char *cause;
const char *s;
layout_print_cell(w->layout_root, __func__, 1);
/* Get number of panes. */
n = window_count_panes(w, 0);
if (n <= 1)
return;
@@ -255,52 +250,48 @@ layout_set_main_h(struct window *w)
if (sx < w->sx)
sx = w->sx;
/* Free old tree and create a new root. */
layout_free(w);
lc = w->layout_root = layout_create_cell(NULL);
layout_set_size(lc, sx, mainh + otherh + 1, 0, 0);
layout_make_node(lc, LAYOUT_TOPBOTTOM);
layout_free(w, 1);
lcroot = w->layout_root = layout_create_cell(NULL);
layout_set_size(lcroot, sx, mainh + otherh + 1, 0, 0);
layout_make_node(lcroot, LAYOUT_TOPBOTTOM);
/* Create the main pane. */
lcmain = layout_create_cell(lc);
wpmain = layout_set_first_tiled(w);
lcmain = wpmain->layout_cell;
lcmain->parent = lcroot;
layout_set_size(lcmain, sx, mainh, 0, 0);
layout_make_leaf(lcmain, layout_first_tiled(w));
TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry);
TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry);
/* Create the other pane. */
lcother = layout_create_cell(lc);
layout_set_size(lcother, sx, otherh, 0, 0);
if (n == 1) {
wp = TAILQ_NEXT(layout_first_tiled(w), entry);
while (wp != NULL && window_pane_is_floating(wp))
wp = TAILQ_NEXT(wpmain, entry);
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
layout_make_leaf(lcother, wp);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_set_size(lcother, sx, otherh, 0, 0);
layout_make_node(lcother, LAYOUT_LEFTRIGHT);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_TAIL(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, PANE_MINIMUM, otherh, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, PANE_MINIMUM, otherh,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -308,15 +299,14 @@ layout_set_main_h(struct window *w)
static void
layout_set_main_h_mirrored(struct window *w)
{
struct window_pane *wp;
struct layout_cell *lc, *lcmain, *lcother, *lcchild;
struct window_pane *wp, *wpmain;
struct layout_cell *lcroot, *lcmain, *lcother, *lcchild;
u_int n, mainh, otherh, sx, sy;
char *cause;
const char *s;
layout_print_cell(w->layout_root, __func__, 1);
/* Get number of panes. */
n = window_count_panes(w, 0);
if (n <= 1)
return;
@@ -357,52 +347,48 @@ layout_set_main_h_mirrored(struct window *w)
if (sx < w->sx)
sx = w->sx;
/* Free old tree and create a new root. */
layout_free(w);
lc = w->layout_root = layout_create_cell(NULL);
layout_set_size(lc, sx, mainh + otherh + 1, 0, 0);
layout_make_node(lc, LAYOUT_TOPBOTTOM);
layout_free(w, 1);
lcroot = w->layout_root = layout_create_cell(NULL);
layout_set_size(lcroot, sx, mainh + otherh + 1, 0, 0);
layout_make_node(lcroot, LAYOUT_TOPBOTTOM);
wpmain = layout_set_first_tiled(w);
lcmain = wpmain->layout_cell;
lcmain->parent = lcroot;
layout_set_size(lcmain, sx, mainh, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry);
/* Create the other pane. */
lcother = layout_create_cell(lc);
layout_set_size(lcother, sx, otherh, 0, 0);
if (n == 1) {
wp = TAILQ_NEXT(layout_first_tiled(w), entry);
while (wp != NULL && window_pane_is_floating(wp))
wp = TAILQ_NEXT(wpmain, entry);
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
layout_make_leaf(lcother, wp);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_set_size(lcother, sx, otherh, 0, 0);
layout_make_node(lcother, LAYOUT_LEFTRIGHT);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_HEAD(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, PANE_MINIMUM, otherh, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, PANE_MINIMUM, otherh,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Create the main pane. */
lcmain = layout_create_cell(lc);
layout_set_size(lcmain, sx, mainh, 0, 0);
layout_make_leaf(lcmain, layout_first_tiled(w));
TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry);
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -410,21 +396,20 @@ layout_set_main_h_mirrored(struct window *w)
static void
layout_set_main_v(struct window *w)
{
struct window_pane *wp;
struct layout_cell *lc, *lcmain, *lcother, *lcchild;
struct window_pane *wp, *wpmain;
struct layout_cell *lcroot, *lcmain, *lcother, *lcchild;
u_int n, mainw, otherw, sx, sy;
char *cause;
const char *s;
layout_print_cell(w->layout_root, __func__, 1);
/* Get number of panes. */
n = window_count_panes(w, 0);
if (n <= 1)
return;
n--; /* take off main pane */
/* Find available width - take off one line for the border. */
/* Find available width - take off one column for the border. */
sx = w->sx - 1;
/* Get the main pane width. */
@@ -459,52 +444,48 @@ layout_set_main_v(struct window *w)
if (sy < w->sy)
sy = w->sy;
/* Free old tree and create a new root. */
layout_free(w);
lc = w->layout_root = layout_create_cell(NULL);
layout_set_size(lc, mainw + otherw + 1, sy, 0, 0);
layout_make_node(lc, LAYOUT_LEFTRIGHT);
layout_free(w, 1);
lcroot = w->layout_root = layout_create_cell(NULL);
layout_set_size(lcroot, mainw + otherw + 1, sy, 0, 0);
layout_make_node(lcroot, LAYOUT_LEFTRIGHT);
/* Create the main pane. */
lcmain = layout_create_cell(lc);
wpmain = layout_set_first_tiled(w);
lcmain = wpmain->layout_cell;
lcmain->parent = lcroot;
layout_set_size(lcmain, mainw, sy, 0, 0);
layout_make_leaf(lcmain, layout_first_tiled(w));
TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry);
TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry);
/* Create the other pane. */
lcother = layout_create_cell(lc);
layout_set_size(lcother, otherw, sy, 0, 0);
if (n == 1) {
wp = TAILQ_NEXT(layout_first_tiled(w), entry);
while (wp != NULL && window_pane_is_floating(wp))
wp = TAILQ_NEXT(wpmain, entry);
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
layout_make_leaf(lcother, wp);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_make_node(lcother, LAYOUT_TOPBOTTOM);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
layout_set_size(lcother, otherw, sy, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, otherw, PANE_MINIMUM, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, otherw, PANE_MINIMUM,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -512,8 +493,8 @@ layout_set_main_v(struct window *w)
static void
layout_set_main_v_mirrored(struct window *w)
{
struct window_pane *wp;
struct layout_cell *lc, *lcmain, *lcother, *lcchild;
struct window_pane *wp, *wpmain;
struct layout_cell *lcroot, *lcmain, *lcother, *lcchild;
u_int n, mainw, otherw, sx, sy;
char *cause;
const char *s;
@@ -526,7 +507,7 @@ layout_set_main_v_mirrored(struct window *w)
return;
n--; /* take off main pane */
/* Find available width - take off one line for the border. */
/* Find available width - take off one column for the border. */
sx = w->sx - 1;
/* Get the main pane width. */
@@ -561,62 +542,58 @@ layout_set_main_v_mirrored(struct window *w)
if (sy < w->sy)
sy = w->sy;
/* Free old tree and create a new root. */
layout_free(w);
lc = w->layout_root = layout_create_cell(NULL);
layout_set_size(lc, mainw + otherw + 1, sy, 0, 0);
layout_make_node(lc, LAYOUT_LEFTRIGHT);
layout_free(w, 1);
lcroot = w->layout_root = layout_create_cell(NULL);
layout_set_size(lcroot, mainw + otherw + 1, sy, 0, 0);
layout_make_node(lcroot, LAYOUT_LEFTRIGHT);
wpmain = layout_set_first_tiled(w);
lcmain = wpmain->layout_cell;
lcmain->parent = lcroot;
layout_set_size(lcmain, mainw, sy, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, lcmain, entry);
/* Create the other pane. */
lcother = layout_create_cell(lc);
layout_set_size(lcother, otherw, sy, 0, 0);
if (n == 1) {
wp = TAILQ_NEXT(layout_first_tiled(w), entry);
while (wp != NULL && window_pane_is_floating(wp))
wp = TAILQ_NEXT(wpmain, entry);
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
layout_make_leaf(lcother, wp);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_make_node(lcother, LAYOUT_TOPBOTTOM);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
layout_set_size(lcother, otherw, sy, 0, 0);
TAILQ_INSERT_HEAD(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, otherw, PANE_MINIMUM, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, otherw, PANE_MINIMUM,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Create the main pane. */
lcmain = layout_create_cell(lc);
layout_set_size(lcmain, mainw, sy, 0, 0);
layout_make_leaf(lcmain, layout_first_tiled(w));
TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry);
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
void
static void
layout_set_tiled(struct window *w)
{
struct options *oo = w->options;
struct window_pane *wp;
struct layout_cell *lc, *lcrow, *lcchild;
struct layout_cell *lcroot, *lcrow, *lcchild;
u_int n, width, height, used, sx, sy;
u_int i, j, columns, rows, max_columns;
@@ -647,56 +624,59 @@ layout_set_tiled(struct window *w)
if (height < PANE_MINIMUM)
height = PANE_MINIMUM;
/* Free old tree and create a new root. */
layout_free(w);
lc = w->layout_root = layout_create_cell(NULL);
sx = ((width + 1) * columns) - 1;
if (sx < w->sx)
sx = w->sx;
sy = ((height + 1) * rows) - 1;
if (sy < w->sy)
sy = w->sy;
layout_set_size(lc, sx, sy, 0, 0);
layout_make_node(lc, LAYOUT_TOPBOTTOM);
/* Create a grid of the cells, skipping any floating panes. */
layout_free(w, 1);
lcroot = w->layout_root = layout_create_cell(NULL);
layout_set_size(lcroot, sx, sy, 0, 0);
layout_make_node(lcroot, LAYOUT_TOPBOTTOM);
/* Create a grid of the tiled cells. */
wp = TAILQ_FIRST(&w->panes);
while (wp != NULL && window_pane_is_floating(wp))
wp = TAILQ_NEXT(wp, entry);
for (j = 0; j < rows; j++) {
while (wp != NULL && !layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
/* If this is the last cell, all done. */
if (wp == NULL)
break;
/* Create the new row. */
lcrow = layout_create_cell(lc);
layout_set_size(lcrow, w->sx, height, 0, 0);
TAILQ_INSERT_TAIL(&lc->cells, lcrow, entry);
lcchild = wp->layout_cell;
/* If only one column, just use the row directly. */
if (n - (j * columns) == 1 || columns == 1) {
layout_make_leaf(lcrow, wp);
lcchild->parent = lcroot;
TAILQ_INSERT_TAIL(&lcroot->cells, lcchild, entry);
layout_set_size(lcchild, w->sx, height, 0, 0);
wp = TAILQ_NEXT(wp, entry);
while (wp != NULL && window_pane_is_floating(wp))
wp = TAILQ_NEXT(wp, entry);
continue;
}
/* Add in the columns. */
/* Create the new row. */
lcrow = layout_create_cell(lcroot);
layout_make_node(lcrow, LAYOUT_LEFTRIGHT);
layout_set_size(lcrow, w->sx, height, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, lcrow, entry);
/* Add in the columns. */
for (i = 0; i < columns; i++) {
/* Create and add a pane cell. */
lcchild = layout_create_cell(lcrow);
layout_set_size(lcchild, width, height, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild->parent = lcrow;
TAILQ_INSERT_TAIL(&lcrow->cells, lcchild, entry);
layout_set_size(lcchild, width, height, 0, 0);
/* Move to the next non-floating cell. */
wp = TAILQ_NEXT(wp, entry);
while (wp != NULL && window_pane_is_floating(wp))
while (wp != NULL &&
!layout_cell_is_tiled(wp->layout_cell))
wp = TAILQ_NEXT(wp, entry);
if (wp == NULL)
break;
lcchild = wp->layout_cell;
}
/*
@@ -713,21 +693,19 @@ layout_set_tiled(struct window *w)
w->sx - used);
}
/* Adjust the last row height to fit if necessary. */
used = (rows * height) + rows - 1;
if (w->sy > used) {
lcrow = TAILQ_LAST(&lc->cells, layout_cells);
lcrow = TAILQ_LAST(&lcroot->cells, layout_cells);
layout_resize_adjust(w, lcrow, LAYOUT_TOPBOTTOM,
w->sy - used);
}
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}

347
layout.c
View File

@@ -56,52 +56,53 @@ static int layout_set_size_check(struct window *, struct layout_cell *,
static void layout_resize_child_cells(struct window *,
struct layout_cell *);
/* Initializes cell geometry to sentinel values. */
static void
layout_geometry_init(struct layout_geometry *lg)
{
lg->sx = UINT_MAX;
lg->sy = UINT_MAX;
lg->xoff = INT_MAX;
lg->yoff = INT_MAX;
}
/* Create a new layout cell. */
struct layout_cell *
layout_create_cell(struct layout_cell *lcparent)
{
struct layout_cell *lc;
lc = xmalloc(sizeof *lc);
lc = xcalloc(1, sizeof *lc);
lc->type = LAYOUT_WINDOWPANE;
lc->flags = 0;
lc->parent = lcparent;
TAILQ_INIT(&lc->cells);
lc->sx = UINT_MAX;
lc->sy = UINT_MAX;
lc->xoff = INT_MAX;
lc->yoff = INT_MAX;
lc->saved_sx = UINT_MAX;
lc->saved_sy = UINT_MAX;
lc->saved_xoff = INT_MAX;
lc->saved_yoff = INT_MAX;
lc->wp = NULL;
layout_geometry_init(&lc->g);
layout_geometry_init(&lc->fg);
return (lc);
}
/* Free a layout cell. */
void
layout_free_cell(struct layout_cell *lc)
layout_free_cell(struct layout_cell *lc, int only_nodes)
{
struct layout_cell *lcchild;
struct layout_cell *lcchild, *lcnext;
if (lc == NULL)
if (lc == NULL || (only_nodes && lc->type == LAYOUT_WINDOWPANE))
return;
switch (lc->type) {
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
while (!TAILQ_EMPTY(&lc->cells)) {
lcchild = TAILQ_FIRST(&lc->cells);
TAILQ_REMOVE(&lc->cells, lcchild, entry);
layout_free_cell(lcchild);
lcchild = TAILQ_FIRST(&lc->cells);
while (lcchild != NULL) {
lcnext = TAILQ_NEXT(lcchild, entry);
if (!only_nodes || lcchild->type != LAYOUT_WINDOWPANE) {
TAILQ_REMOVE(&lc->cells, lcchild, entry);
layout_free_cell(lcchild, only_nodes);
}
lcchild = lcnext;
}
break;
case LAYOUT_WINDOWPANE:
@@ -140,8 +141,8 @@ layout_print_cell(struct layout_cell *lc, const char *hdr, u_int n)
break;
}
log_debug("%s:%*s%p type %s [parent %p] wp=%p [%d,%d %ux%u]", hdr, n,
" ", lc, type, lc->parent, lc->wp, lc->xoff, lc->yoff, lc->sx,
lc->sy);
" ", lc, type, lc->parent, lc->wp, lc->g.xoff, lc->g.yoff, lc->g.sx,
lc->g.sy);
switch (lc->type) {
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
@@ -160,10 +161,10 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y)
struct layout_cell *lcchild, *last = NULL;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if ((int)x >= lcchild->xoff &&
(int)x < lcchild->xoff + (int)lcchild->sx &&
(int)y >= lcchild->yoff &&
(int)y < lcchild->yoff + (int)lcchild->sy) {
if ((int)x >= lcchild->g.xoff &&
(int)x < lcchild->g.xoff + (int)lcchild->g.sx &&
(int)y >= lcchild->g.yoff &&
(int)y < lcchild->g.yoff + (int)lcchild->g.sy) {
/* Inside the cell - recurse. */
return (layout_search_by_border(lcchild, x, y));
}
@@ -175,13 +176,13 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y)
switch (lc->type) {
case LAYOUT_LEFTRIGHT:
if ((int)x < lcchild->xoff &&
(int)x >= last->xoff + (int)last->sx)
if ((int)x < lcchild->g.xoff &&
(int)x >= last->g.xoff + (int)last->g.sx)
return (last);
break;
case LAYOUT_TOPBOTTOM:
if ((int)y < lcchild->yoff &&
(int)y >= last->yoff + (int)last->sy)
if ((int)y < lcchild->g.yoff &&
(int)y >= last->g.yoff + (int)last->g.sy)
return (last);
break;
case LAYOUT_WINDOWPANE:
@@ -198,11 +199,11 @@ layout_search_by_border(struct layout_cell *lc, u_int x, u_int y)
void
layout_set_size(struct layout_cell *lc, u_int sx, u_int sy, int xoff, int yoff)
{
lc->sx = sx;
lc->sy = sy;
lc->g.sx = sx;
lc->g.sy = sy;
lc->xoff = xoff;
lc->yoff = yoff;
lc->g.xoff = xoff;
lc->g.yoff = yoff;
}
/* Make a cell a leaf cell. */
@@ -255,7 +256,7 @@ layout_fix_zindexes(struct window *w, struct layout_cell *lc)
}
}
static int
int
layout_cell_is_tiled(struct layout_cell *lc)
{
int is_leaf = lc->type == LAYOUT_WINDOWPANE;
@@ -327,28 +328,28 @@ layout_fix_offsets1(struct layout_cell *lc)
int xoff, yoff;
if (lc->type == LAYOUT_LEFTRIGHT) {
xoff = lc->xoff;
xoff = lc->g.xoff;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (!layout_cell_is_tiled(lcchild) &&
!layout_cell_has_tiled_child(lcchild))
continue;
lcchild->xoff = xoff;
lcchild->yoff = lc->yoff;
lcchild->g.xoff = xoff;
lcchild->g.yoff = lc->g.yoff;
if (lcchild->type != LAYOUT_WINDOWPANE)
layout_fix_offsets1(lcchild);
xoff += lcchild->sx + 1;
xoff += lcchild->g.sx + 1;
}
} else {
yoff = lc->yoff;
yoff = lc->g.yoff;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (!layout_cell_is_tiled(lcchild) &&
!layout_cell_has_tiled_child(lcchild))
continue;
lcchild->xoff = lc->xoff;
lcchild->yoff = yoff;
lcchild->g.xoff = lc->g.xoff;
lcchild->g.yoff = yoff;
if (lcchild->type != LAYOUT_WINDOWPANE)
layout_fix_offsets1(lcchild);
yoff += lcchild->sy + 1;
yoff += lcchild->g.sy + 1;
}
}
}
@@ -363,8 +364,8 @@ layout_fix_offsets(struct window *w)
if (lc->flags & LAYOUT_CELL_FLOATING)
return;
lc->xoff = 0;
lc->yoff = 0;
lc->g.xoff = 0;
lc->g.yoff = 0;
layout_fix_offsets1(lc);
}
@@ -448,16 +449,17 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
old_sx = wp->sx;
old_sy = wp->sy;
wp->xoff = lc->xoff;
wp->yoff = lc->yoff;
sx = lc->sx;
sy = lc->sy;
wp->xoff = lc->g.xoff;
wp->yoff = lc->g.yoff;
sx = lc->g.sx;
sy = lc->g.sy;
if (!window_pane_is_floating(wp) &&
layout_add_horizontal_border(w, lc, status)) {
if (status == PANE_STATUS_TOP)
wp->yoff++;
sy--;
if (sy > 1)
sy--;
}
if (window_pane_scrollbar_reserve(wp)) {
@@ -531,14 +533,14 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
if (lc->type == LAYOUT_WINDOWPANE) {
/* Space available in this cell only. */
if (type == LAYOUT_LEFTRIGHT) {
available = lc->sx;
available = lc->g.sx;
if (w->sb == PANE_SCROLLBARS_ALWAYS)
minimum = PANE_MINIMUM + sb_style->width +
sb_style->pad;
else
minimum = PANE_MINIMUM;
} else {
available = lc->sy;
available = lc->g.sy;
if (layout_add_horizontal_border(w, lc, status))
minimum = PANE_MINIMUM + 1;
else
@@ -576,12 +578,13 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc,
enum layout_type type, int change)
{
struct layout_cell *lcchild;
int changed;
/* Adjust the cell size. */
if (type == LAYOUT_LEFTRIGHT)
lc->sx += change;
lc->g.sx += change;
else
lc->sy += change;
lc->g.sy += change;
/* If this is a leaf cell, that is all that is necessary. */
if (type == LAYOUT_WINDOWPANE)
@@ -609,6 +612,7 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc,
* until no further change is possible.
*/
while (change != 0) {
changed = 0;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (change == 0)
break;
@@ -618,13 +622,17 @@ layout_resize_adjust(struct window *w, struct layout_cell *lc,
if (change > 0) {
layout_resize_adjust(w, lcchild, type, 1);
change--;
changed = 1;
continue;
}
if (layout_resize_check(w, lcchild, type) > 0) {
layout_resize_adjust(w, lcchild, type, -1);
change++;
changed = 1;
}
}
if (!changed)
break;
}
}
@@ -636,9 +644,9 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc,
int change;
if (type == LAYOUT_LEFTRIGHT)
change = size - lc->sx;
change = size - lc->g.sx;
else
change = size - lc->sy;
change = size - lc->g.sy;
layout_resize_adjust(w, lc, type, change);
}
@@ -699,29 +707,29 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc,
if (lcparent == NULL) {
if (lc->wp != NULL)
*lcroot = NULL;
layout_free_cell(lc);
layout_free_cell(lc, 0);
return;
}
if (!layout_cell_is_tiled(lc)) {
TAILQ_REMOVE(&lcparent->cells, lc, entry);
layout_free_cell(lc);
layout_free_cell(lc, 0);
goto out;
}
lcother = layout_cell_get_neighbour(lc);
if (lcother != NULL) {
if (lcparent->type == LAYOUT_LEFTRIGHT)
change = lc->sx + 1;
change = lc->g.sx + 1;
else
change = lc->sy + 1;
change = lc->g.sy + 1;
layout_resize_adjust(w, lcother, lcparent->type, change);
} else
layout_remove_tile(w, lcparent);
/* Remove this from the parent's list. */
TAILQ_REMOVE(&lcparent->cells, lc, entry);
layout_free_cell(lc);
layout_free_cell(lc, 0);
out:
/*
@@ -735,14 +743,14 @@ out:
lc->parent = lcparent->parent;
if (lc->parent == NULL) {
if (layout_cell_is_tiled(lc)) {
lc->xoff = 0;
lc->yoff = 0;
lc->g.xoff = 0;
lc->g.yoff = 0;
}
*lcroot = lc;
} else
TAILQ_REPLACE(&lc->parent->cells, lcparent, lc, entry);
layout_free_cell(lcparent);
layout_free_cell(lcparent, 0);
}
}
@@ -760,9 +768,9 @@ layout_init(struct window *w, struct window_pane *wp)
/* Free layout for pane. */
void
layout_free(struct window *w)
layout_free(struct window *w, int only_nodes)
{
layout_free_cell(w->layout_root);
layout_free_cell(w->layout_root, only_nodes);
}
/* Resize the entire layout after window resize. */
@@ -787,29 +795,29 @@ layout_resize(struct window *w, u_int sx, u_int sy)
*/
if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING))
return;
xchange = sx - lc->sx;
xchange = sx - lc->g.sx;
xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT);
if (xchange < 0 && xchange < -xlimit)
xchange = -xlimit;
if (xlimit == 0) {
if (sx <= lc->sx) /* lc->sx is minimum possible */
if (sx <= lc->g.sx) /* lc->g.sx is minimum possible */
xchange = 0;
else
xchange = sx - lc->sx;
xchange = sx - lc->g.sx;
}
if (xchange != 0)
layout_resize_adjust(w, lc, LAYOUT_LEFTRIGHT, xchange);
/* Adjust vertically in a similar fashion. */
ychange = sy - lc->sy;
ychange = sy - lc->g.sy;
ylimit = layout_resize_check(w, lc, LAYOUT_TOPBOTTOM);
if (ychange < 0 && ychange < -ylimit)
ychange = -ylimit;
if (ylimit == 0) {
if (sy <= lc->sy) /* lc->sy is minimum possible */
if (sy <= lc->g.sy) /* lc->g.sy is minimum possible */
ychange = 0;
else
ychange = sy - lc->sy;
ychange = sy - lc->g.sy;
}
if (ychange != 0)
layout_resize_adjust(w, lc, LAYOUT_TOPBOTTOM, ychange);
@@ -840,9 +848,9 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type,
/* Work out the size adjustment. */
if (type == LAYOUT_LEFTRIGHT)
size = lc->sx;
size = lc->g.sx;
else
size = lc->sy;
size = lc->g.sy;
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells))
change = size - new_size;
else
@@ -873,13 +881,13 @@ layout_resize_floating_pane_to(struct window_pane *wp, enum layout_type type,
}
if (type == LAYOUT_TOPBOTTOM) {
if (lc->sy == size)
if (lc->g.sy == size)
return (0);
lc->sy = size;
lc->g.sy = size;
} else {
if (lc->sx == size)
if (lc->g.sx == size)
return (0);
lc->sx = size;
lc->g.sx = size;
}
redraw_invalidate_scene(wp->window);
return (0);
@@ -901,23 +909,23 @@ layout_resize_floating_pane(struct window_pane *wp, enum layout_type type,
return (0);
if (type == LAYOUT_TOPBOTTOM) {
size = lc->sy + change;
size = lc->g.sy + change;
if (size < PANE_MINIMUM || size > PANE_MAXIMUM) {
*cause = xstrdup("change is too big or too small");
return (-1);
}
lc->sy = size;
lc->g.sy = size;
if (opposite)
lc->yoff -= change;
lc->g.yoff -= change;
} else {
size = lc->sx + change;
size = lc->g.sx + change;
if (size < PANE_MINIMUM || size > PANE_MAXIMUM) {
*cause = xstrdup("change is too big or too small");
return (-1);
}
lc->sx = size;
lc->g.sx = size;
if (opposite)
lc->xoff -= change;
lc->g.xoff -= change;
}
redraw_invalidate_scene(wp->window);
return (0);
@@ -1083,13 +1091,13 @@ layout_new_pane_size(struct window *w, u_int previous, struct layout_cell *lc,
*/
min = (PANE_MINIMUM + 1) * (count_left - 1);
if (type == LAYOUT_LEFTRIGHT) {
if (lc->sx - available > min)
min = lc->sx - available;
new_size = (lc->sx * size) / previous;
if (lc->g.sx - available > min)
min = lc->g.sx - available;
new_size = (lc->g.sx * size) / previous;
} else {
if (lc->sy - available > min)
min = lc->sy - available;
new_size = (lc->sy * size) / previous;
if (lc->g.sy - available > min)
min = lc->g.sy - available;
new_size = (lc->g.sy * size) / previous;
}
/* Check against the maximum and minimum size. */
@@ -1125,9 +1133,9 @@ layout_set_size_check(struct window *w, struct layout_cell *lc,
return (0);
if (type == LAYOUT_LEFTRIGHT)
previous = lc->sx;
previous = lc->g.sx;
else
previous = lc->sy;
previous = lc->g.sy;
idx = 0;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
@@ -1163,32 +1171,32 @@ static void
layout_resize_child_cells(struct window *w, struct layout_cell *lc)
{
struct layout_cell *lcchild;
u_int previous, available, count, idx;
u_int prev, available, count, idx;
if (lc->type == LAYOUT_WINDOWPANE)
return;
/* What is the current size used? */
count = 0;
previous = 0;
prev = 0;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (!layout_cell_is_tiled(lcchild) &&
!layout_cell_has_tiled_child(lcchild))
continue;
count++;
if (lc->type == LAYOUT_LEFTRIGHT)
previous += lcchild->sx;
prev += lcchild->g.sx;
else if (lc->type == LAYOUT_TOPBOTTOM)
previous += lcchild->sy;
prev += lcchild->g.sy;
}
previous += (count - 1);
prev += (count - 1);
/* And how much is available? */
available = 0;
if (lc->type == LAYOUT_LEFTRIGHT)
available = lc->sx;
available = lc->g.sx;
else if (lc->type == LAYOUT_TOPBOTTOM)
available = lc->sy;
available = lc->g.sy;
/* Resize children into the new size. */
idx = 0;
@@ -1197,20 +1205,20 @@ layout_resize_child_cells(struct window *w, struct layout_cell *lc)
!layout_cell_has_tiled_child(lcchild))
continue;
if (lc->type == LAYOUT_TOPBOTTOM) {
lcchild->sx = lc->sx;
lcchild->xoff = lc->xoff;
lcchild->g.sx = lc->g.sx;
lcchild->g.xoff = lc->g.xoff;
} else {
lcchild->sx = layout_new_pane_size(w, previous, lcchild,
lc->type, lc->sx, count - idx, available);
available -= (lcchild->sx + 1);
lcchild->g.sx = layout_new_pane_size(w, prev, lcchild,
lc->type, lc->g.sx, count - idx, available);
available -= (lcchild->g.sx + 1);
}
if (lc->type == LAYOUT_LEFTRIGHT) {
lcchild->sy = lc->sy;
lcchild->yoff = lc->yoff;
lcchild->g.sy = lc->g.sy;
lcchild->g.yoff = lc->g.yoff;
} else {
lcchild->sy = layout_new_pane_size(w, previous, lcchild,
lc->type, lc->sy, count - idx, available);
available -= (lcchild->sy + 1);
lcchild->g.sy = layout_new_pane_size(w, prev, lcchild,
lc->type, lc->g.sy, count - idx, available);
available -= (lcchild->g.sy + 1);
}
layout_resize_child_cells(w, lcchild);
idx++;
@@ -1230,7 +1238,7 @@ layout_replace_with_node(struct window *w, struct layout_cell *lc,
lcparent = layout_create_cell(lc->parent);
layout_make_node(lcparent, type);
layout_set_size(lcparent, lc->sx, lc->sy, lc->xoff, lc->yoff);
layout_set_size(lcparent, lc->g.sx, lc->g.sy, lc->g.xoff, lc->g.yoff);
if (lc->parent == NULL)
w->layout_root = lcparent;
else
@@ -1249,7 +1257,7 @@ layout_split_check_space(struct window_pane *wp, struct layout_cell *lc,
enum layout_type type)
{
struct style *sb_style = &wp->scrollbar_style;
u_int minimum, sx = lc->sx, sy = lc->sy;
u_int minimum, sx = lc->g.sx, sy = lc->g.sy;
int status;
if (lc->flags & LAYOUT_CELL_FLOATING)
@@ -1288,7 +1296,7 @@ layout_split_sizes(struct layout_cell *lc, int size, int before,
enum layout_type type, u_int *size1, u_int *size2, u_int *saved_size)
{
u_int s1, s2, ss;
u_int sx = lc->sx, sy = lc->sy;
u_int sx = lc->g.sx, sy = lc->g.sy;
if (type == LAYOUT_LEFTRIGHT)
ss = sx;
@@ -1335,10 +1343,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size,
lc = wp->layout_cell;
/* Copy the old cell size. */
sx = lc->sx;
sy = lc->sy;
xoff = lc->xoff;
yoff = lc->yoff;
sx = lc->g.sx;
sy = lc->g.sy;
xoff = lc->g.xoff;
yoff = lc->g.yoff;
/* Check there is enough space for the two new panes. */
if (!layout_split_check_space(wp, lc, type))
@@ -1379,13 +1387,13 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size,
* must be resized before inserting the new cell.
*/
if (lc->type == LAYOUT_LEFTRIGHT) {
lc->sx = new_size;
lc->g.sx = new_size;
layout_resize_child_cells(wp->window, lc);
lc->sx = saved_size;
lc->g.sx = saved_size;
} else if (lc->type == LAYOUT_TOPBOTTOM) {
lc->sy = new_size;
lc->g.sy = new_size;
layout_resize_child_cells(wp->window, lc);
lc->sy = saved_size;
lc->g.sy = saved_size;
}
resize_first = 1;
@@ -1429,10 +1437,10 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size,
*/
if (!resize_first && type == LAYOUT_LEFTRIGHT) {
layout_set_size(lc1, size1, sy, xoff, yoff);
layout_set_size(lc2, size2, sy, xoff + lc1->sx + 1, yoff);
layout_set_size(lc2, size2, sy, xoff + lc1->g.sx + 1, yoff);
} else if (!resize_first && type == LAYOUT_TOPBOTTOM) {
layout_set_size(lc1, sx, size1, xoff, yoff);
layout_set_size(lc2, sx, size2, xoff, yoff + lc1->sy + 1);
layout_set_size(lc2, sx, size2, xoff, yoff + lc1->g.sy + 1);
}
if (full_size) {
if (!resize_first)
@@ -1449,8 +1457,8 @@ layout_split_pane(struct window_pane *wp, enum layout_type type, int size,
* layout_assign_pane before much else happens!
*/
struct layout_cell *
layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx,
u_int sy, int ox, int oy)
layout_floating_pane(struct window *w, struct window_pane *wp,
struct layout_geometry *lg)
{
struct layout_cell *lc, *lcnew, *lcparent;
@@ -1462,16 +1470,16 @@ layout_floating_pane(struct window *w, struct window_pane *wp, u_int sx,
if (lcparent == NULL) {
/*
* Adding a pane to a root that isn't node. Must create and
* insert a new root.
*/
* Adding a pane to a root that isn't a node. Must create and
* insert a new root.
*/
lcparent = layout_replace_with_node(w, lc, LAYOUT_TOPBOTTOM);
}
lcnew = layout_create_cell(lcparent);
TAILQ_INSERT_AFTER(&lcparent->cells, lc, lcnew, entry);
lcnew->flags |= LAYOUT_CELL_FLOATING;
layout_set_size(lcnew, sx, sy, ox, oy);
layout_set_size(lcnew, lg->sx, lg->sy, lg->xoff, lg->yoff);
return (lcnew);
}
@@ -1507,18 +1515,19 @@ layout_spread_cell(struct window *w, struct layout_cell *parent)
number = 0;
TAILQ_FOREACH (lc, &parent->cells, entry)
number++;
if (layout_cell_is_tiled(lc))
number++;
if (number <= 1)
return (0);
status = window_get_pane_status(w);
if (parent->type == LAYOUT_LEFTRIGHT)
size = parent->sx;
size = parent->g.sx;
else if (parent->type == LAYOUT_TOPBOTTOM) {
if (layout_add_horizontal_border(w, parent, status))
size = parent->sy - 1;
size = parent->g.sy - 1;
else
size = parent->sy;
size = parent->g.sy;
} else
return (0);
if (size < number - 1)
@@ -1535,9 +1544,11 @@ layout_spread_cell(struct window *w, struct layout_cell *parent)
changed = 0;
TAILQ_FOREACH (lc, &parent->cells, entry) {
if (!layout_cell_is_tiled(lc))
continue;
change = 0;
if (parent->type == LAYOUT_LEFTRIGHT) {
change = each - (int)lc->sx;
change = each - (int)lc->g.sx;
if (remainder > 0) {
change++;
remainder--;
@@ -1552,7 +1563,7 @@ layout_spread_cell(struct window *w, struct layout_cell *parent)
this++;
remainder--;
}
change = this - (int)lc->sy;
change = this - (int)lc->g.sy;
layout_resize_adjust(w, lc, LAYOUT_TOPBOTTOM, change);
}
if (change != 0)
@@ -1649,29 +1660,29 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args,
char **cause)
{
struct layout_cell *lcnew;
u_int sx = UINT_MAX, sy = UINT_MAX;
int ox = INT_MAX, oy = INT_MAX;
struct layout_geometry fg;
if (layout_floating_args_parse(item, args, lines, w, &sx, &sy, &ox, &oy,
cause) != 0)
layout_geometry_init(&fg);
if (layout_floating_args_parse(item, args, lines, w, &fg, cause) != 0)
return (NULL);
lcnew = layout_floating_pane(w, wp, sx, sy, ox, oy);
window_push_zoom(wp->window, 1, args_has(args, 'Z'));
lcnew = layout_floating_pane(w, wp, &fg);
return (lcnew);
}
int
layout_floating_args_parse(struct cmdq_item *item, struct args *args,
enum pane_lines lines, struct window *w, u_int *sxp, u_int *syp, int *oxp,
int *oyp, char **cause)
enum pane_lines lines, struct window *w, struct layout_geometry *lg,
char **cause)
{
int sx, sy, ox, oy;
char *error = NULL;
sx = *sxp == UINT_MAX ? w->sx / 2 : *sxp;
sy = *syp == UINT_MAX ? w->sy / 4 : *syp;
ox = *oxp == INT_MAX ? INT_MAX : *oxp;
oy = *oyp == INT_MAX ? INT_MAX : *oyp;
sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx;
sy = lg->sy == UINT_MAX ? w->sy / 4 : lg->sy;
ox = lg->xoff;
oy = lg->yoff;
if (args_has(args, 'x')) {
sx = args_percentage_and_expand(args, 'x', 0, PANE_MAXIMUM,
@@ -1723,7 +1734,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
ox = 4;
}
w->last_new_pane_x = ox;
} else
} else if (args_has(args, 'X'))
if (lines != PANE_LINES_NONE)
ox += 1;
if (oy == INT_MAX) {
@@ -1735,7 +1746,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
oy = 2;
}
w->last_new_pane_y = oy;
} else
} else if (args_has(args, 'Y'))
if (lines != PANE_LINES_NONE)
oy += 1;
@@ -1748,10 +1759,10 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
return (-1);
}
*sxp = sx;
*syp = sy;
*oxp = ox;
*oyp = oy;
lg->sx = sx;
lg->sy = sy;
lg->xoff = ox;
lg->yoff = oy;
return (0);
}
@@ -1767,7 +1778,7 @@ layout_remove_tile(struct window *w, struct layout_cell *lc)
int change;
if (lc->flags & LAYOUT_CELL_FLOATING)
return (0);
return (-1);
lcneighbour = layout_cell_get_neighbour(lc);
if (lcneighbour == NULL) {
@@ -1780,9 +1791,9 @@ layout_remove_tile(struct window *w, struct layout_cell *lc)
* neighbour.
*/
if (type == LAYOUT_TOPBOTTOM)
change = lc->sy + 1;
change = lc->g.sy + 1;
else
change = lc->sx + 1;
change = lc->g.sx + 1;
layout_resize_adjust(w, lcneighbour, type, change);
}
@@ -1792,7 +1803,7 @@ layout_remove_tile(struct window *w, struct layout_cell *lc)
*/
if (lc->parent != NULL)
layout_set_size(lc, 0, 0, 0, 0);
return (1);
return (0);
}
/*
@@ -1809,14 +1820,14 @@ layout_insert_tile(struct window *w, struct layout_cell *lc)
if (lc == NULL)
fatalx("layout cell cannot be null when tiling");
lcparent = lc->parent;
if (lc->flags & LAYOUT_CELL_FLOATING)
return (1);
if (layout_cell_is_tiled(lc))
return (-1);
lcparent = lc->parent;
if (lcparent == NULL) {
/* Only pane in the layout. */
layout_set_size(lc, w->sx, w->sy, 0, 0);
return (1);
return (0);
}
type = lcparent->type;
@@ -1828,9 +1839,9 @@ layout_insert_tile(struct window *w, struct layout_cell *lc)
*/
layout_insert_tile(w, lcparent);
if (type == LAYOUT_LEFTRIGHT)
size1 = lcparent->sx;
size1 = lcparent->g.sx;
else
size1 = lcparent->sy;
size1 = lcparent->g.sy;
layout_resize_set_size(w, lc, type, size1);
} else {
/*
@@ -1839,7 +1850,7 @@ layout_insert_tile(struct window *w, struct layout_cell *lc)
*/
lctiled = layout_cell_get_first_tiled(lcneighbour);
if (!layout_split_check_space(lctiled->wp, lcneighbour, type))
return (0);
return (-1);
layout_split_sizes(lcneighbour, -1, 0, type, &size1, &size2,
&saved_size);
layout_resize_set_size(w, lc, type, size1);
@@ -1848,13 +1859,13 @@ layout_insert_tile(struct window *w, struct layout_cell *lc)
/* Setting opposite of the 'split' size to that of the parent. */
if (lcparent->type == LAYOUT_LEFTRIGHT) {
size1 = lcparent->sy;
size1 = lcparent->g.sy;
type = LAYOUT_TOPBOTTOM;
} else {
size1 = lcparent->sx;
size1 = lcparent->g.sx;
type = LAYOUT_LEFTRIGHT;
}
layout_resize_set_size(w, lc, type, size1);
return (1);
return (0);
}

17
menu.c
View File

@@ -313,6 +313,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event)
struct mouse_event *m = &event->m;
u_int i;
int count = menu->count, old = md->choice;
int move;
const char *name = NULL;
const struct menu_item *item;
struct cmdq_state *state;
@@ -320,6 +321,14 @@ menu_key_cb(struct client *c, void *data, struct key_event *event)
char *error;
if (KEYC_IS_MOUSE(event->key)) {
/*
* A mouse move with no button held reports as a release, so
* treat it as highlight-only: it must never select or close the
* menu, otherwise a menu opened without a button already down
* (such as a submenu opened from another menu) would vanish as
* soon as the mouse moved over it.
*/
move = MOUSE_DRAG(m->b) && MOUSE_RELEASE(m->b);
if (md->flags & MENU_NOMOUSE) {
if (MOUSE_BUTTONS(m->b) != MOUSE_BUTTON_1)
return (1);
@@ -330,7 +339,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event)
m->y < md->py + 1 ||
m->y > md->py + 1 + count - 1) {
if (~md->flags & MENU_STAYOPEN) {
if (MOUSE_RELEASE(m->b))
if (!move && MOUSE_RELEASE(m->b))
return (1);
} else {
if (!MOUSE_RELEASE(m->b) &&
@@ -345,7 +354,7 @@ menu_key_cb(struct client *c, void *data, struct key_event *event)
return (0);
}
if (~md->flags & MENU_STAYOPEN) {
if (MOUSE_RELEASE(m->b))
if (!move && MOUSE_RELEASE(m->b))
goto chosen;
} else {
if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b))
@@ -558,6 +567,10 @@ menu_prepare(struct menu *menu, int flags, int starting_choice,
if (py + menu->count + 2 > c->tty.sy)
py = c->tty.sy - menu->count - 2;
/* Remember where this menu is so -x/-y L can reuse the position. */
c->menu_last_px = px;
c->menu_last_py = py;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "menu-border-lines");

View File

@@ -37,8 +37,7 @@ enum mode_tree_preview {
};
#define MODE_TREE_PREFIX_STYLE \
"#{?mode_tree_selected,#[default]#[noacs]," \
"#[fg=themelightgrey]#[bg=default]#[noacs]}"
"#[fg=themelightgrey]#[bg=default]#[noacs]"
#define MODE_TREE_PREFIX_FORMAT \
MODE_TREE_PREFIX_STYLE \
@@ -835,6 +834,11 @@ mode_tree_draw(struct mode_tree_data *mtd)
if (mtd->line_size == 0)
return;
w = mtd->width;
h = mtd->height;
if (w == 0 || h == 0)
return;
memcpy(&gc0, &grid_default_cell, sizeof gc0);
memcpy(&gc, &grid_default_cell, sizeof gc);
style_apply(&gc, oo, "tree-mode-selection-style", NULL);
@@ -844,9 +848,6 @@ mode_tree_draw(struct mode_tree_data *mtd)
dfg = gc.fg;
dfg0 = gc0.fg;
w = mtd->width;
h = mtd->height;
screen_write_start(&ctx, s);
screen_write_clearscreen(&ctx, 8);
ft = format_create_defaults(NULL, NULL, NULL, NULL, wp);
@@ -950,16 +951,16 @@ mode_tree_draw(struct mode_tree_data *mtd)
}
} else {
screen_write_clearendofline(&ctx, gc.bg);
format_draw(&ctx, &gc, prefix_width, prefix, NULL, 0);
format_draw(&ctx, &gc, prefix_width, prefix, NULL, 1);
if (left != 0) {
screen_write_cursormove(&ctx, prefix_width,
i - mtd->offset, 0);
format_draw(&ctx, &gc, left, text, NULL, 0);
format_draw(&ctx, &gc, left, text, NULL, 1);
if (mti->text != NULL && width < w) {
screen_write_cursormove(&ctx, width,
i - mtd->offset, 0);
format_draw(&ctx, &gc, w - width,
mti->text, NULL, 0);
mti->text, NULL, 1);
}
}
}

658
monitor.c Normal file
View File

@@ -0,0 +1,658 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicm@users.sourceforge.net>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <event.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
/* Subscription pane. */
struct monitor_pane {
u_int pane;
u_int idx;
char *last;
u_int generation;
RB_ENTRY(monitor_pane) entry;
};
RB_HEAD(monitor_panes, monitor_pane);
/* Subscription window. */
struct monitor_window {
u_int window;
u_int idx;
char *last;
u_int generation;
RB_ENTRY(monitor_window) entry;
};
RB_HEAD(monitor_windows, monitor_window);
/* Subscription. */
struct monitor_item {
char *name;
char *format;
enum monitor_type type;
u_int id;
u_int flags;
char *last;
struct monitor_panes panes;
struct monitor_windows windows;
RB_ENTRY(monitor_item) entry;
};
RB_HEAD(monitor_items, monitor_item);
/* Monitored subscription set. */
struct monitor_set {
struct client *client;
struct session *session;
monitor_cb cb;
void *data;
struct monitor_items items;
struct event timer;
u_int generation;
};
static void monitor_timer(__unused int, __unused short, void *);
/* Get the session for this monitor set. */
static struct session *
monitor_get_session(struct monitor_set *ms)
{
struct session *s;
if (ms->client != NULL)
return (ms->client->session);
s = ms->session;
if (s == NULL)
return (RB_MIN(sessions, &sessions));
if (session_find_by_id(s->id) != s)
return (NULL);
return (s);
}
/* Create a format tree for a subscription. */
static struct format_tree *
monitor_create_formats(struct client *c, struct session *s, struct winlink *wl,
struct window_pane *wp)
{
struct format_tree *ft;
ft = format_create(NULL, NULL, 0, FORMAT_NOJOBS);
format_defaults(ft, c, s, wl, wp);
return (ft);
}
/* Compare subscriptions. */
static int
monitor_item_cmp(struct monitor_item *m1, struct monitor_item *m2)
{
return (strcmp(m1->name, m2->name));
}
RB_GENERATE_STATIC(monitor_items, monitor_item, entry, monitor_item_cmp);
/* Compare subscription panes. */
static int
monitor_pane_cmp(struct monitor_pane *mp1, struct monitor_pane *mp2)
{
if (mp1->pane < mp2->pane)
return (-1);
if (mp1->pane > mp2->pane)
return (1);
if (mp1->idx < mp2->idx)
return (-1);
if (mp1->idx > mp2->idx)
return (1);
return (0);
}
RB_GENERATE_STATIC(monitor_panes, monitor_pane, entry, monitor_pane_cmp);
/* Compare subscription windows. */
static int
monitor_window_cmp(struct monitor_window *mw1, struct monitor_window *mw2)
{
if (mw1->window < mw2->window)
return (-1);
if (mw1->window > mw2->window)
return (1);
if (mw1->idx < mw2->idx)
return (-1);
if (mw1->idx > mw2->idx)
return (1);
return (0);
}
RB_GENERATE_STATIC(monitor_windows, monitor_window, entry, monitor_window_cmp);
/* Free a subscription. */
static void
monitor_free_item(struct monitor_set *ms, struct monitor_item *me)
{
struct monitor_pane *mp, *mp1;
struct monitor_window *mw, *mw1;
RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) {
RB_REMOVE(monitor_panes, &me->panes, mp);
free(mp->last);
free(mp);
}
RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) {
RB_REMOVE(monitor_windows, &me->windows, mw);
free(mw->last);
free(mw);
}
free(me->last);
RB_REMOVE(monitor_items, &ms->items, me);
free(me->name);
free(me->format);
free(me);
}
/* Report a changed value. */
static void
monitor_report(struct monitor_set *ms, struct monitor_item *me,
struct session *s, struct winlink *wl, struct window_pane *wp,
const char *value, const char *last)
{
struct monitor_change change = { 0 };
log_debug("%s: %s changed to %s", __func__, me->name, value);
change.name = me->name;
change.value = value;
change.last = last;
change.c = ms->client;
change.s = s;
change.wl = wl;
change.wp = wp;
ms->cb(&change, ms->data);
}
/* Check a value against its last value and report if changed. */
static void
monitor_check_value(struct monitor_set *ms, struct monitor_item *me,
struct session *s, struct winlink *wl, struct window_pane *wp,
char *value, char **last)
{
if (*last == NULL) {
*last = value;
if (me->flags & MONITOR_NOTIFY_INITIAL)
monitor_report(ms, me, s, wl, wp, value, NULL);
return;
}
if (strcmp(value, *last) == 0) {
free(value);
return;
}
monitor_report(ms, me, s, wl, wp, value, *last);
free(*last);
*last = value;
}
/* Check session subscription. */
static void
monitor_check_session(struct monitor_set *ms, struct monitor_item *me,
struct format_tree *ft)
{
struct session *s = monitor_get_session(ms);
char *value;
value = format_expand(ft, me->format);
monitor_check_value(ms, me, s, NULL, NULL, value, &me->last);
}
/* Check pane subscription. */
static void
monitor_check_pane(struct monitor_set *ms, struct monitor_item *me)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct window_pane *wp;
struct window *w;
struct winlink *wl;
struct format_tree *ft;
char *value;
struct monitor_pane *mp, find;
wp = window_pane_find_by_id(me->id);
if (wp == NULL || wp->fd == -1)
return;
w = wp->window;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session != s)
continue;
ft = monitor_create_formats(c, s, wl, wp);
value = format_expand(ft, me->format);
format_free(ft);
find.pane = wp->id;
find.idx = wl->idx;
mp = RB_FIND(monitor_panes, &me->panes, &find);
if (mp == NULL) {
mp = xcalloc(1, sizeof *mp);
mp->pane = wp->id;
mp->idx = wl->idx;
RB_INSERT(monitor_panes, &me->panes, mp);
}
monitor_check_value(ms, me, s, wl, wp, value, &mp->last);
}
}
/* Check one all-panes subscription. */
static void
monitor_check_all_panes_one(struct monitor_set *ms, struct monitor_item *me,
struct format_tree *ft, struct winlink *wl, struct window_pane *wp)
{
struct session *s = monitor_get_session(ms);
char *value;
struct monitor_pane *mp, find;
value = format_expand(ft, me->format);
find.pane = wp->id;
find.idx = wl->idx;
mp = RB_FIND(monitor_panes, &me->panes, &find);
if (mp == NULL) {
mp = xcalloc(1, sizeof *mp);
mp->pane = wp->id;
mp->idx = wl->idx;
RB_INSERT(monitor_panes, &me->panes, mp);
}
mp->generation = ms->generation;
monitor_check_value(ms, me, s, wl, wp, value, &mp->last);
}
/* Remove all-panes entries not seen during the current scan. */
static void
monitor_sweep_all_panes(struct monitor_item *me, u_int generation)
{
struct monitor_pane *mp, *mp1;
RB_FOREACH_SAFE(mp, monitor_panes, &me->panes, mp1) {
if (mp->generation == generation)
continue;
RB_REMOVE(monitor_panes, &me->panes, mp);
free(mp->last);
free(mp);
}
}
/* Check window subscription. */
static void
monitor_check_window(struct monitor_set *ms, struct monitor_item *me)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct window *w;
struct winlink *wl;
struct format_tree *ft;
char *value;
struct monitor_window *mw, find;
w = window_find_by_id(me->id);
if (w == NULL)
return;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session != s)
continue;
ft = monitor_create_formats(c, s, wl, NULL);
value = format_expand(ft, me->format);
format_free(ft);
find.window = w->id;
find.idx = wl->idx;
mw = RB_FIND(monitor_windows, &me->windows, &find);
if (mw == NULL) {
mw = xcalloc(1, sizeof *mw);
mw->window = w->id;
mw->idx = wl->idx;
RB_INSERT(monitor_windows, &me->windows, mw);
}
monitor_check_value(ms, me, s, wl, NULL, value, &mw->last);
}
}
/* Check one all-windows subscription. */
static void
monitor_check_all_windows_one(struct monitor_set *ms, struct monitor_item *me,
struct format_tree *ft, struct winlink *wl)
{
struct session *s = monitor_get_session(ms);
struct window *w = wl->window;
char *value;
struct monitor_window *mw, find;
value = format_expand(ft, me->format);
find.window = w->id;
find.idx = wl->idx;
mw = RB_FIND(monitor_windows, &me->windows, &find);
if (mw == NULL) {
mw = xcalloc(1, sizeof *mw);
mw->window = w->id;
mw->idx = wl->idx;
RB_INSERT(monitor_windows, &me->windows, mw);
}
mw->generation = ms->generation;
monitor_check_value(ms, me, s, wl, NULL, value, &mw->last);
}
/* Remove all-windows entries not seen during the current scan. */
static void
monitor_sweep_all_windows(struct monitor_item *me, u_int generation)
{
struct monitor_window *mw, *mw1;
RB_FOREACH_SAFE(mw, monitor_windows, &me->windows, mw1) {
if (mw->generation == generation)
continue;
RB_REMOVE(monitor_windows, &me->windows, mw);
free(mw->last);
free(mw);
}
}
/* Check session subscriptions. */
static void
monitor_check_sessions(struct monitor_set *ms)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct monitor_item *me, *me1;
struct format_tree *ft;
ft = monitor_create_formats(c, s, NULL, NULL);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type == MONITOR_SESSION)
monitor_check_session(ms, me, ft);
}
format_free(ft);
}
/* Check pane and window subscriptions. */
static void
monitor_check_panes_windows(struct monitor_set *ms)
{
struct monitor_item *me, *me1;
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
switch (me->type) {
case MONITOR_PANE:
monitor_check_pane(ms, me);
break;
case MONITOR_WINDOW:
monitor_check_window(ms, me);
break;
case MONITOR_SESSION:
case MONITOR_ALL_PANES:
case MONITOR_ALL_WINDOWS:
break;
}
}
}
/* Check all-panes subscriptions. */
static void
monitor_check_all_panes(struct monitor_set *ms)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct monitor_item *me, *me1;
struct window_pane *wp;
struct format_tree *ft;
struct winlink *wl;
if (++ms->generation == 0)
ms->generation = 1;
RB_FOREACH(wl, winlinks, &s->windows) {
TAILQ_FOREACH(wp, &wl->window->panes, entry) {
ft = monitor_create_formats(c, s, wl, wp);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type != MONITOR_ALL_PANES)
continue;
monitor_check_all_panes_one(ms, me, ft, wl, wp);
}
format_free(ft);
}
}
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type == MONITOR_ALL_PANES)
monitor_sweep_all_panes(me, ms->generation);
}
}
/* Check all-windows subscriptions. */
static void
monitor_check_all_windows(struct monitor_set *ms)
{
struct client *c = ms->client;
struct session *s = monitor_get_session(ms);
struct monitor_item *me, *me1;
struct format_tree *ft;
struct winlink *wl;
if (++ms->generation == 0)
ms->generation = 1;
RB_FOREACH(wl, winlinks, &s->windows) {
ft = monitor_create_formats(c, s, wl, NULL);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type != MONITOR_ALL_WINDOWS)
continue;
monitor_check_all_windows_one(ms, me, ft, wl);
}
format_free(ft);
}
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1) {
if (me->type == MONITOR_ALL_WINDOWS)
monitor_sweep_all_windows(me, ms->generation);
}
}
/* Check subscriptions. */
static void
monitor_timer(__unused int fd, __unused short events, void *data)
{
struct monitor_set *ms = data;
struct monitor_item *me;
struct timeval tv = { .tv_sec = 1 };
int have_session = 0, have_all_panes = 0;
int have_all_windows = 0;
log_debug("%s: timer fired", __func__);
evtimer_add(&ms->timer, &tv);
if (monitor_get_session(ms) == NULL)
return;
RB_FOREACH(me, monitor_items, &ms->items) {
switch (me->type) {
case MONITOR_SESSION:
have_session = 1;
break;
case MONITOR_ALL_PANES:
have_all_panes = 1;
break;
case MONITOR_ALL_WINDOWS:
have_all_windows = 1;
break;
case MONITOR_PANE:
case MONITOR_WINDOW:
break;
}
}
if (have_session)
monitor_check_sessions(ms);
monitor_check_panes_windows(ms);
if (have_all_panes)
monitor_check_all_panes(ms);
if (have_all_windows)
monitor_check_all_windows(ms);
}
/* Create a monitor set. */
static struct monitor_set *
monitor_create(monitor_cb cb, void *data)
{
struct monitor_set *ms;
ms = xcalloc(1, sizeof *ms);
ms->cb = cb;
ms->data = data;
RB_INIT(&ms->items);
return (ms);
}
/* Create a client monitor set. */
struct monitor_set *
monitor_create_client(struct client *c, monitor_cb cb, void *data)
{
struct monitor_set *ms;
ms = monitor_create(cb, data);
ms->client = c;
return (ms);
}
/* Create a monitor set for a session. */
struct monitor_set *
monitor_create_session(struct session *s, monitor_cb cb, void *data)
{
struct monitor_set *ms;
ms = monitor_create(cb, data);
ms->session = s;
if (s != NULL)
session_add_ref(s, __func__);
return (ms);
}
/* Destroy a monitor set. */
void
monitor_destroy(struct monitor_set *ms)
{
struct monitor_item *me, *me1;
if (ms != NULL) {
if (evtimer_initialized(&ms->timer))
evtimer_del(&ms->timer);
RB_FOREACH_SAFE(me, monitor_items, &ms->items, me1)
monitor_free_item(ms, me);
if (ms->session != NULL)
session_remove_ref(ms->session, __func__);
free(ms);
}
}
/* Parse a subscription. */
int
monitor_parse(const char *value, char **name, enum monitor_type *type, int *id,
char **format)
{
char *copy, *what, *split;
copy = xstrdup(value);
*id = -1;
what = strchr(copy, ':');
if (what == NULL)
goto fail;
*what++ = '\0';
split = strchr(what, ':');
if (split == NULL)
goto fail;
*split++ = '\0';
if (strcmp(what, "%*") == 0)
*type = MONITOR_ALL_PANES;
else if (sscanf(what, "%%%d", id) == 1 && *id >= 0)
*type = MONITOR_PANE;
else if (strcmp(what, "@*") == 0)
*type = MONITOR_ALL_WINDOWS;
else if (sscanf(what, "@%d", id) == 1 && *id >= 0)
*type = MONITOR_WINDOW;
else
*type = MONITOR_SESSION;
*name = xstrdup(copy);
*format = xstrdup(split);
free(copy);
return (0);
fail:
free(copy);
return (-1);
}
/* Add a subscription. */
void
monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type,
int id, const char *format, u_int flags)
{
struct monitor_item *me, find = { .name = (char *)name };
struct timeval tv = { .tv_sec = 1 };
if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL)
monitor_free_item(ms, me);
me = xcalloc(1, sizeof *me);
me->name = xstrdup(name);
me->format = xstrdup(format);
me->type = type;
me->id = id;
me->flags = flags;
RB_INIT(&me->panes);
RB_INIT(&me->windows);
RB_INSERT(monitor_items, &ms->items, me);
if (!evtimer_initialized(&ms->timer))
evtimer_set(&ms->timer, monitor_timer, ms);
if (!evtimer_pending(&ms->timer, NULL))
evtimer_add(&ms->timer, &tv);
}
/* Remove a subscription. */
void
monitor_remove(struct monitor_set *ms, const char *name)
{
struct monitor_item *me, find = { .name = (char *)name };
if ((me = RB_FIND(monitor_items, &ms->items, &find)) != NULL)
monitor_free_item(ms, me);
if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer))
evtimer_del(&ms->timer);
}

218
notify.c
View File

@@ -27,12 +27,25 @@ struct notify_entry {
const char *name;
struct cmd_find_state fs;
struct format_tree *formats;
struct options *oo;
struct client *client;
struct session *session;
struct window *window;
int pane;
const char *pbname;
int expand;
};
struct notify_monitor {
struct options *oo;
struct monitor_set *set;
struct cmd_find_state fs;
enum monitor_type type;
int id;
char *format;
};
static struct cmdq_item *
@@ -50,7 +63,31 @@ notify_insert_one_hook(struct cmdq_item *item, struct notify_entry *ne,
free(s);
}
new_item = cmdq_get_command(cmdlist, state);
return (cmdq_insert_after(item, new_item));
if (item != NULL)
return (cmdq_insert_after(item, new_item));
return (cmdq_append(NULL, new_item));
}
static struct cmd_parse_result *
notify_parse_hook(struct notify_entry *ne, struct cmd_find_state *fs,
const char *value)
{
struct cmd_parse_result *pr;
struct format_tree *ft;
char *expanded;
if (!ne->expand)
return (cmd_parse_from_string(value, NULL));
ft = format_create_defaults(NULL, ne->client, fs->s, fs->wl, fs->wp);
if (ne->formats != NULL)
format_merge(ft, ne->formats);
expanded = format_expand(ft, value);
format_free(ft);
pr = cmd_parse_from_string(expanded, NULL);
free(expanded);
return (pr);
}
static void
@@ -73,18 +110,23 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne)
else
cmd_find_copy_state(&fs, &ne->fs);
if (fs.s == NULL)
oo = global_s_options;
else
oo = fs.s->options;
o = options_get(oo, ne->name);
if (o == NULL && fs.wp != NULL) {
oo = fs.wp->options;
o = options_get(oo, ne->name);
}
if (o == NULL && fs.wl != NULL) {
oo = fs.wl->window->options;
if (ne->oo != NULL) {
oo = ne->oo;
o = options_get_only(oo, ne->name);
} else {
if (fs.s == NULL)
oo = global_s_options;
else
oo = fs.s->options;
o = options_get(oo, ne->name);
if (o == NULL && fs.wp != NULL) {
oo = fs.wp->options;
o = options_get(oo, ne->name);
}
if (o == NULL && fs.wl != NULL) {
oo = fs.wl->window->options;
o = options_get(oo, ne->name);
}
}
if (o == NULL) {
log_debug("%s: hook %s not found", __func__, ne->name);
@@ -96,7 +138,7 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne)
if (*ne->name == '@') {
value = options_get_string(oo, ne->name);
pr = cmd_parse_from_string(value, NULL);
pr = notify_parse_hook(ne, &fs, value);
switch (pr->status) {
case CMD_PARSE_ERROR:
log_debug("%s: can't parse hook %s: %s", __func__,
@@ -110,8 +152,24 @@ notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne)
} else {
a = options_array_first(o);
while (a != NULL) {
cmdlist = options_array_item_value(a)->cmdlist;
item = notify_insert_one_hook(item, ne, cmdlist, state);
if (ne->expand) {
value = options_array_item_value(a)->string;
pr = notify_parse_hook(ne, &fs, value);
switch (pr->status) {
case CMD_PARSE_ERROR:
if (pr->error != NULL)
cmdq_error(item, "%s", pr->error);
break;
case CMD_PARSE_SUCCESS:
item = notify_insert_one_hook(item, ne,
pr->cmdlist, state);
break;
}
} else {
cmdlist = options_array_item_value(a)->cmdlist;
item = notify_insert_one_hook(item, ne, cmdlist,
state);
}
a = options_array_next(a);
}
}
@@ -175,6 +233,136 @@ notify_callback(struct cmdq_item *item, void *data)
return (CMD_RETURN_NORMAL);
}
void
notify_monitor_free(void *data)
{
struct notify_monitor *nhm = data;
monitor_destroy(nhm->set);
free(nhm->format);
free(nhm);
}
void
notify_monitor_remove(struct options *oo, const char *name)
{
struct options_entry *o;
struct notify_monitor *nhm;
o = options_get_only(oo, name);
if (o == NULL)
return;
nhm = options_get_monitor_data(o);
if (nhm != NULL) {
options_set_monitor_data(o, NULL);
notify_monitor_free(nhm);
}
}
static void
notify_monitor_cb(struct monitor_change *change, void *data)
{
struct notify_monitor *nhm = data;
struct notify_entry ne;
struct cmdq_item *item;
struct window *w;
item = cmdq_running(NULL);
if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS))
return;
memset(&ne, 0, sizeof ne);
ne.name = change->name;
ne.oo = nhm->oo;
ne.client = change->c;
ne.expand = 1;
if (change->wp != NULL && change->wl != NULL)
cmd_find_from_winlink_pane(&ne.fs, change->wl, change->wp, 0);
else if (change->wl != NULL)
cmd_find_from_winlink(&ne.fs, change->wl, 0);
else if (change->s != NULL)
cmd_find_from_session(&ne.fs, change->s, 0);
else
cmd_find_copy_state(&ne.fs, &nhm->fs);
ne.formats = format_create(change->c, item, FORMAT_NONE, FORMAT_NOJOBS);
format_add(ne.formats, "hook", "%s", change->name);
format_add(ne.formats, "hook_value", "%s", change->value);
format_add(ne.formats, "hook_last", "%s",
change->last == NULL ? "" : change->last);
if (change->s != NULL) {
format_add(ne.formats, "hook_session", "$%u", change->s->id);
format_add(ne.formats, "hook_session_name", "%s", change->s->name);
}
if (change->wl != NULL) {
w = change->wl->window;
format_add(ne.formats, "hook_window", "@%u", w->id);
format_add(ne.formats, "hook_window_name", "%s", w->name);
format_add(ne.formats, "hook_window_index", "%d", change->wl->idx);
}
if (change->wp != NULL) {
format_add(ne.formats, "hook_pane", "%%%u", change->wp->id);
}
notify_insert_hook(item, &ne);
format_free(ne.formats);
}
void
notify_monitor_add(__unused struct cmdq_item *item, struct options *oo,
const char *name, enum monitor_type type, int id, const char *format,
struct cmd_find_state *fs, struct session *s)
{
struct options_entry *o;
struct notify_monitor *nhm;
notify_monitor_remove(oo, name);
o = options_get_only(oo, name);
if (o == NULL)
o = options_set_string(oo, name, 0, "%s", "");
nhm = xcalloc(1, sizeof *nhm);
nhm->oo = oo;
cmd_find_copy_state(&nhm->fs, fs);
nhm->type = type;
nhm->id = id;
nhm->format = xstrdup(format);
nhm->set = monitor_create_session(s, notify_monitor_cb, nhm);
options_set_monitor_data(o, nhm);
monitor_add(nhm->set, name, type, id, format, 0);
}
/* Convert a hook monitor to its value. */
char *
notify_monitor_to_string(struct options_entry *o)
{
struct notify_monitor *nhm = options_get_monitor_data(o);
const char *name = options_name(o);
char *s;
if (nhm == NULL)
return (NULL);
switch (nhm->type) {
case MONITOR_SESSION:
xasprintf(&s, "%s::%s", name, nhm->format);
break;
case MONITOR_PANE:
xasprintf(&s, "%s:%%%d:%s", name, nhm->id, nhm->format);
break;
case MONITOR_ALL_PANES:
xasprintf(&s, "%s:%%*:%s", name, nhm->format);
break;
case MONITOR_WINDOW:
xasprintf(&s, "%s:@%d:%s", name, nhm->id, nhm->format);
break;
case MONITOR_ALL_WINDOWS:
xasprintf(&s, "%s:@*:%s", name, nhm->format);
break;
}
return (s);
}
static void
notify_add(const char *name, struct cmd_find_state *fs, struct client *c,
struct session *s, struct window *w, struct window_pane *wp,

View File

@@ -955,7 +955,7 @@ const struct options_table_entry options_table[] = {
{ .name = "mouse",
.type = OPTIONS_TABLE_FLAG,
.scope = OPTIONS_TABLE_SESSION,
.default_num = 0,
.default_num = TMUX_MOUSE,
.text = "Whether the mouse is recognised and mouse key bindings are "
"executed. "
"Applications inside panes can use the mouse even when 'off'."
@@ -1538,6 +1538,9 @@ const struct options_table_entry options_table[] = {
"\"#{pane_title}\""
"#{?#{mouse},"
"#[align=right]"
"#[range=control|7]["
"#{?#{pane_floating_flag},t,f}"
"]#[norange]"
"#[range=control|8]["
"#{?#{window_zoomed_flag},u,z}"
"]#[norange]"

222
options.c
View File

@@ -32,18 +32,69 @@
*/
struct options_array_item {
u_int index;
char *key;
union options_value value;
RB_ENTRY(options_array_item) entry;
};
static int
options_array_key_to_number(const char *key, u_int *idx)
{
const char *errstr;
long long n;
if (*key == '\0')
return (-1);
for (const char *cp = key; *cp != '\0'; cp++) {
if (!isdigit((u_char)*cp))
return (0);
}
n = strtonum(key, 0, UINT_MAX, &errstr);
if (errstr != NULL)
return (-1);
if (idx != NULL)
*idx = n;
return (1);
}
static char *
options_array_correct_key(const char *key)
{
u_int idx;
int numeric;
char *out;
numeric = options_array_key_to_number(key, &idx);
if (numeric == -1)
return (NULL);
if (numeric == 1) {
xasprintf(&out, "%u", idx);
return (out);
}
return (xstrdup(key));
}
static int
options_array_cmp(struct options_array_item *a1, struct options_array_item *a2)
{
if (a1->index < a2->index)
u_int i1, i2;
int n1, n2;
n1 = options_array_key_to_number(a1->key, &i1);
n2 = options_array_key_to_number(a2->key, &i2);
if (n1 && n2) {
if (i1 < i2)
return (-1);
if (i1 > i2)
return (1);
return (0);
}
if (n1)
return (-1);
if (a1->index > a2->index)
if (n2)
return (1);
return (0);
return (strcmp(a1->key, a2->key));
}
RB_GENERATE_STATIC(options_array, options_array_item, entry, options_array_cmp);
@@ -56,6 +107,7 @@ struct options_entry {
int cached;
struct style style;
void *monitor_data;
RB_ENTRY(options_entry) entry;
};
@@ -259,6 +311,7 @@ options_default(struct options *oo, const struct options_table_entry *oe)
{
struct options_entry *o;
union options_value *ov;
char key[32];
u_int i;
struct cmd_parse_result *pr;
@@ -270,8 +323,10 @@ options_default(struct options *oo, const struct options_table_entry *oe)
options_array_assign(o, oe->default_str, NULL);
return (o);
}
for (i = 0; oe->default_arr[i] != NULL; i++)
options_array_set(o, i, oe->default_arr[i], 0, NULL);
for (i = 0; oe->default_arr[i] != NULL; i++) {
xsnprintf(key, sizeof key, "%u", i);
options_array_set(o, key, oe->default_arr[i], 0, NULL);
}
return (o);
}
@@ -354,6 +409,8 @@ options_remove(struct options_entry *o)
options_array_clear(o);
else
options_value_free(o, &o->value);
if (o->monitor_data != NULL)
notify_monitor_free(o->monitor_data);
RB_REMOVE(options_tree, &oo->tree, o);
free((void *)o->name);
free(o);
@@ -371,6 +428,18 @@ options_owner(struct options_entry *o)
return (o->owner);
}
void *
options_get_monitor_data(struct options_entry *o)
{
return (o->monitor_data);
}
void
options_set_monitor_data(struct options_entry *o, void *data)
{
o->monitor_data = data;
}
const struct options_table_entry *
options_table_entry(struct options_entry *o)
{
@@ -378,21 +447,21 @@ options_table_entry(struct options_entry *o)
}
static struct options_array_item *
options_array_item(struct options_entry *o, u_int idx)
options_array_item(struct options_entry *o, const char *key)
{
struct options_array_item a;
a.index = idx;
a.key = (char *)key;
return (RB_FIND(options_array, &o->value.array, &a));
}
static struct options_array_item *
options_array_new(struct options_entry *o, u_int idx)
options_array_new(struct options_entry *o, const char *key)
{
struct options_array_item *a;
a = xcalloc(1, sizeof *a);
a->index = idx;
a->key = xstrdup(key);
RB_INSERT(options_array, &o->value.array, a);
return (a);
}
@@ -402,6 +471,7 @@ options_array_free(struct options_entry *o, struct options_array_item *a)
{
options_value_free(o, &a->value);
RB_REMOVE(options_array, &o->value.array, a);
free(a->key);
free(a);
}
@@ -418,24 +488,45 @@ options_array_clear(struct options_entry *o)
}
union options_value *
options_array_get(struct options_entry *o, u_int idx)
options_array_get(struct options_entry *o, const char *key)
{
struct options_array_item *a;
char *new_key;
if (!OPTIONS_IS_ARRAY(o))
return (NULL);
a = options_array_item(o, idx);
new_key = options_array_correct_key(key);
if (new_key == NULL)
return (NULL);
a = options_array_item(o, new_key);
free(new_key);
if (a == NULL)
return (NULL);
return (&a->value);
}
union options_value *
options_array_getv(struct options_entry *o, const char *fmt, ...)
{
union options_value *ov;
va_list ap;
char *key;
va_start(ap, fmt);
xvasprintf(&key, fmt, ap);
va_end(ap);
ov = options_array_get(o, key);
free(key);
return (ov);
}
int
options_array_set(struct options_entry *o, u_int idx, const char *value,
options_array_set(struct options_entry *o, const char *key, const char *value,
int append, char **cause)
{
struct options_array_item *a;
char *new;
char *new, *new_key;
struct cmd_parse_result *pr;
long long number;
@@ -445,10 +536,18 @@ options_array_set(struct options_entry *o, u_int idx, const char *value,
return (-1);
}
new_key = options_array_correct_key(key);
if (new_key == NULL) {
if (cause != NULL)
xasprintf(cause, "bad array key: %s", key);
return (-1);
}
if (value == NULL) {
a = options_array_item(o, idx);
a = options_array_item(o, new_key);
if (a != NULL)
options_array_free(o, a);
free(new_key);
return (0);
}
@@ -460,50 +559,56 @@ options_array_set(struct options_entry *o, u_int idx, const char *value,
*cause = pr->error;
else
free(pr->error);
free(new_key);
return (-1);
case CMD_PARSE_SUCCESS:
break;
}
a = options_array_item(o, idx);
a = options_array_item(o, new_key);
if (a == NULL)
a = options_array_new(o, idx);
a = options_array_new(o, new_key);
else
options_value_free(o, &a->value);
a->value.cmdlist = pr->cmdlist;
free(new_key);
return (0);
}
if (OPTIONS_IS_STRING(o)) {
a = options_array_item(o, idx);
a = options_array_item(o, new_key);
if (a != NULL && append)
xasprintf(&new, "%s%s", a->value.string, value);
else
new = xstrdup(value);
if (a == NULL)
a = options_array_new(o, idx);
a = options_array_new(o, new_key);
else
options_value_free(o, &a->value);
a->value.string = new;
free(new_key);
return (0);
}
if (o->tableentry->type == OPTIONS_TABLE_COLOUR) {
if ((number = colour_fromstring(value)) == -1) {
xasprintf(cause, "bad colour: %s", value);
free(new_key);
return (-1);
}
a = options_array_item(o, idx);
a = options_array_item(o, new_key);
if (a == NULL)
a = options_array_new(o, idx);
a = options_array_new(o, new_key);
else
options_value_free(o, &a->value);
a->value.number = number;
free(new_key);
return (0);
}
if (cause != NULL)
*cause = xstrdup("wrong array type");
free(new_key);
return (-1);
}
@@ -512,6 +617,7 @@ options_array_assign(struct options_entry *o, const char *s, char **cause)
{
const char *separator;
char *copy, *next, *string;
char key[32];
u_int i;
separator = o->tableentry->separator;
@@ -521,10 +627,11 @@ options_array_assign(struct options_entry *o, const char *s, char **cause)
if (*s == '\0')
return (0);
for (i = 0; i < UINT_MAX; i++) {
if (options_array_item(o, i) == NULL)
if (options_array_getv(o, "%u", i) == NULL)
break;
}
return (options_array_set(o, i, s, 0, cause));
xsnprintf(key, sizeof key, "%u", i);
return (options_array_set(o, key, s, 0, cause));
}
if (*s == '\0')
@@ -534,12 +641,13 @@ options_array_assign(struct options_entry *o, const char *s, char **cause)
if (*next == '\0')
continue;
for (i = 0; i < UINT_MAX; i++) {
if (options_array_item(o, i) == NULL)
if (options_array_getv(o, "%u", i) == NULL)
break;
}
if (i == UINT_MAX)
break;
if (options_array_set(o, i, next, 0, cause) != 0) {
xsnprintf(key, sizeof key, "%u", i);
if (options_array_set(o, key, next, 0, cause) != 0) {
free(copy);
return (-1);
}
@@ -559,13 +667,13 @@ options_array_first(struct options_entry *o)
struct options_array_item *
options_array_next(struct options_array_item *a)
{
return (RB_NEXT(options_array, &o->value.array, a));
return (RB_NEXT(options_array, , a));
}
u_int
options_array_item_index(struct options_array_item *a)
const char *
options_array_item_key(struct options_array_item *a)
{
return (a->index);
return (a->key);
}
union options_value *
@@ -587,15 +695,16 @@ options_is_string(struct options_entry *o)
}
char *
options_to_string(struct options_entry *o, int idx, int numeric)
options_to_string(struct options_entry *o, const char *key, int numeric)
{
struct options_array_item *a;
char *result = NULL;
char *last = NULL;
char *next;
char *new_key;
if (OPTIONS_IS_ARRAY(o)) {
if (idx == -1) {
if (key == NULL) {
RB_FOREACH(a, options_array, &o->value.array) {
next = options_value_to_string(o, &a->value,
numeric);
@@ -612,7 +721,11 @@ options_to_string(struct options_entry *o, int idx, int numeric)
return (xstrdup(""));
return (result);
}
a = options_array_item(o, idx);
new_key = options_array_correct_key(key);
if (new_key == NULL)
return (xstrdup(""));
a = options_array_item(o, new_key);
free(new_key);
if (a == NULL)
return (xstrdup(""));
return (options_value_to_string(o, &a->value, numeric));
@@ -621,37 +734,41 @@ options_to_string(struct options_entry *o, int idx, int numeric)
}
char *
options_parse(const char *name, int *idx)
options_parse(const char *name, char **key)
{
char *copy, *cp, *end;
char *copy, *cp, *end, *raw, *new_key;
if (*name == '\0')
return (NULL);
*key = NULL;
copy = xstrdup(name);
if ((cp = strchr(copy, '[')) == NULL) {
*idx = -1;
return (copy);
}
end = strchr(cp + 1, ']');
if (end == NULL || end[1] != '\0' || !isdigit((u_char)end[-1])) {
if (end == NULL || end[1] != '\0' || end == cp + 1) {
free(copy);
return (NULL);
}
if (sscanf(cp, "[%d]", idx) != 1 || *idx < 0) {
raw = xstrndup(cp + 1, end - (cp + 1));
new_key = options_array_correct_key(raw);
free(raw);
if (new_key == NULL) {
free(copy);
return (NULL);
}
*key = new_key;
*cp = '\0';
return (copy);
}
struct options_entry *
options_parse_get(struct options *oo, const char *s, int *idx, int only)
options_parse_get(struct options *oo, const char *s, char **key, int only)
{
struct options_entry *o;
char *name;
name = options_parse(s, idx);
name = options_parse(s, key);
if (name == NULL)
return (NULL);
if (only)
@@ -659,6 +776,10 @@ options_parse_get(struct options *oo, const char *s, int *idx, int only)
else
o = options_get(oo, name);
free(name);
if (o == NULL) {
free(*key);
*key = NULL;
}
return (o);
}
@@ -675,14 +796,14 @@ options_search(const char *name)
}
char *
options_match(const char *s, int *idx, int *ambiguous)
options_match(const char *s, char **key, int *ambiguous)
{
const struct options_table_entry *oe, *found;
char *parsed;
const char *name;
size_t namelen;
parsed = options_parse(s, idx);
parsed = options_parse(s, key);
if (parsed == NULL)
return (NULL);
if (*parsed == '@') {
@@ -703,6 +824,8 @@ options_match(const char *s, int *idx, int *ambiguous)
if (found != NULL) {
*ambiguous = 1;
free(parsed);
free(*key);
*key = NULL;
return (NULL);
}
found = oe;
@@ -711,19 +834,21 @@ options_match(const char *s, int *idx, int *ambiguous)
free(parsed);
if (found == NULL) {
*ambiguous = 0;
free(*key);
*key = NULL;
return (NULL);
}
return (xstrdup(found->name));
}
struct options_entry *
options_match_get(struct options *oo, const char *s, int *idx, int only,
options_match_get(struct options *oo, const char *s, char **key, int only,
int *ambiguous)
{
char *name;
struct options_entry *o;
name = options_match(s, idx, ambiguous);
name = options_match(s, key, ambiguous);
if (name == NULL)
return (NULL);
*ambiguous = 0;
@@ -732,6 +857,10 @@ options_match_get(struct options *oo, const char *s, int *idx, int only,
else
o = options_get(oo, name);
free(name);
if (o == NULL) {
free(*key);
*key = NULL;
}
return (o);
}
@@ -1325,11 +1454,12 @@ options_push_changes(const char *name)
}
int
options_remove_or_default(struct options_entry *o, int idx, char **cause)
options_remove_or_default(struct options_entry *o, const char *key,
char **cause)
{
struct options *oo = o->owner;
if (idx == -1) {
if (key == NULL) {
if (o->tableentry != NULL &&
(oo == global_options ||
oo == global_s_options ||
@@ -1337,7 +1467,7 @@ options_remove_or_default(struct options_entry *o, int idx, char **cause)
options_default(oo, o->tableentry);
else
options_remove(o);
} else if (options_array_set(o, idx, NULL, 0, cause) != 0)
} else if (options_array_set(o, key, NULL, 0, cause) != 0)
return (-1);
return (0);
}

View File

@@ -1,10 +1,42 @@
TESTS!= echo *.sh
LOGDIR=logs
.PHONY: all $(TESTS)
.NOTPARALLEL: all $(TESTS)
.PHONY: all
.NOTPARALLEL: all
all: $(TESTS)
$(TESTS):
sh $*.sh
sleep 1
all:
@mkdir -p "$(LOGDIR)"; \
rm -f "$(LOGDIR)"/*.log; \
failed=0; failures=; \
for test in $(TESTS); do \
base=$${test##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \
rm -f "$$log"; \
printf '%-40s ' "$$test"; \
start=$$(date +%s); \
ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \
env -i LC_CTYPE=C.UTF-8 ASAN_OPTIONS="$$ASAN_OPTIONS" \
sh -x "$$test" >"$$log" 2>&1; \
if [ $$? -eq 0 ]; then \
end=$$(date +%s); \
rm -f "$$log"; \
echo "PASS ($$((end - start))s)"; \
else \
end=$$(date +%s); \
echo "FAIL ($$((end - start))s)"; \
echo " log: $$log"; \
failed=1; \
failures="$$failures $$test"; \
fi; \
sleep 1; \
done; \
if [ "$$failed" -ne 0 ]; then \
echo; \
echo "failures:"; \
for test in $$failures; do \
echo " $$test"; \
done; \
else \
rmdir "$(LOGDIR)" 2>/dev/null || true; \
fi; \
exit $$failed

Binary file not shown.

291
regress/alerts.sh Normal file
View File

@@ -0,0 +1,291 @@
#!/bin/sh
# monitor-activity, monitor-bell and monitor-silence: both the
# alert-activity, alert-bell and alert-silence hooks and the winlink alert
# flags (window_activity_flag, window_bell_flag, window_silence_flag and
# the #, !, ~ characters in window_flags). The sessions are detached so
# alert flags are set even on the current window; the *-action options
# still decide whether the hooks fire. Panes run cat: activity is
# generated by the tty echo of send-keys and a bell by sending a BEL and
# newline for cat to write back.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX_TMPDIR="$OUT"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
wait_for()
{
option=$1
expected=$2
i=0
while [ $i -lt 30 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $option to be '$expected' but got '$value'"
}
assert_unchanged()
{
option=$1
expected=$2
i=0
while [ $i -lt 10 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] || \
fail "expected $option to remain '$expected' but got '$value'"
i=$((i + 1))
sleep 0.2
done
}
wait_for_fmt()
{
target=$1
fmt=$2
expected=$3
i=0
while [ $i -lt 30 ]; do
value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $fmt for $target to be '$expected' but got '$value'"
}
assert_fmt_unchanged()
{
target=$1
fmt=$2
expected=$3
i=0
while [ $i -lt 10 ]; do
value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true)
[ "$value" = "$expected" ] || \
fail "expected $fmt for $target to remain '$expected' but got '$value'"
i=$((i + 1))
sleep 0.2
done
}
flags_have()
{
target=$1
char=$2
flags=$($TMUX display -pt "$target" '#{window_flags}') ||
fail "display window_flags failed"
case "$flags" in
*"$char"*) ;;
*) fail "expected '$char' in window_flags for $target: '$flags'" ;;
esac
}
flags_lack()
{
target=$1
char=$2
flags=$($TMUX display -pt "$target" '#{window_flags}') ||
fail "display window_flags failed"
case "$flags" in
*"$char"*) fail "unexpected '$char' in window_flags for $target: '$flags'" ;;
esac
}
bell()
{
$TMUX send-keys -t "$1" -H 07 0a || fail "send-keys bell failed"
}
activity()
{
$TMUX send-keys -t "$1" -l x || fail "send-keys activity failed"
}
$TMUX new -d -s mon -n w0 cat || fail "new-session mon failed"
$TMUX set -g @log '' || fail "set @log failed"
$TMUX set-hook -g alert-bell \
'set -gF @log "#{@log}|alert-bell:#{hook_session_name}:#{hook_window_name}"' ||
fail "set-hook alert-bell failed"
$TMUX set-hook -g alert-activity \
'set -gF @log "#{@log}|alert-activity:#{hook_session_name}:#{hook_window_name}"' ||
fail "set-hook alert-activity failed"
$TMUX set-hook -g alert-silence \
'set -gF @log "#{@log}|alert-silence:#{hook_session_name}:#{hook_window_name}"' ||
fail "set-hook alert-silence failed"
# A bell in a non-current window (monitor-bell defaults to on) fires
# alert-bell and sets the bell flag, shown as ! in window_flags.
$TMUX neww -d -t mon: -n bellw cat || fail "new-window bellw failed"
assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0
flags_lack mon:bellw '!'
bell mon:bellw
wait_for @log '|alert-bell:mon:bellw'
wait_for_fmt mon:bellw '#{window_bell_flag}' 1
flags_have mon:bellw '!'
# Bells are not deduplicated: a second bell fires the hook again even
# though the flag is still set.
bell mon:bellw
wait_for @log '|alert-bell:mon:bellw|alert-bell:mon:bellw'
# Selecting the window clears the alert flags.
$TMUX selectw -t mon:bellw || fail "select-window bellw failed"
wait_for_fmt mon:bellw '#{window_bell_flag}' 0
flags_lack mon:bellw '!'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
# A bell in the current window of a detached session still sets the flag
# and bell-action any (the default) fires the hook for the current window.
$TMUX set -g @log '' || fail "reset @log failed"
bell mon:w0
wait_for @log '|alert-bell:mon:w0'
wait_for_fmt mon:w0 '#{window_bell_flag}' 1
$TMUX selectw -t mon:bellw || fail "select-window bellw failed"
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
wait_for_fmt mon:w0 '#{window_bell_flag}' 0
# bell-action none: the flag is still set but the hook does not fire.
$TMUX set -t mon bell-action none || fail "set bell-action none failed"
$TMUX set -g @log '' || fail "reset @log failed"
bell mon:bellw
wait_for_fmt mon:bellw '#{window_bell_flag}' 1
assert_unchanged @log ''
$TMUX set -ut mon bell-action || fail "unset bell-action failed"
$TMUX selectw -t mon:bellw || fail "select-window bellw failed"
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
# monitor-bell off: neither the flag nor the hook.
$TMUX set -wt mon:bellw monitor-bell off || fail "set monitor-bell failed"
$TMUX set -g @log '' || fail "reset @log failed"
bell mon:bellw
assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0
assert_unchanged @log ''
# monitor-activity defaults to off: output sets neither flag nor hook.
$TMUX neww -d -t mon: -n actw cat || fail "new-window actw failed"
$TMUX set -g @log '' || fail "reset @log failed"
activity mon:actw
assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0
assert_unchanged @log ''
# With monitor-activity on, output fires alert-activity and sets the
# activity flag, shown as # in window_flags.
$TMUX set -wt mon:actw monitor-activity on ||
fail "set monitor-activity failed"
activity mon:actw
wait_for @log '|alert-activity:mon:actw'
wait_for_fmt mon:actw '#{window_activity_flag}' 1
flags_have mon:actw '#'
# While the flag is set further activity does not fire the hook again.
$TMUX set -g @log '' || fail "reset @log failed"
activity mon:actw
assert_unchanged @log ''
# On a detached session selecting the window does not clear the activity
# flag: session_set_current treats the selection itself as activity and
# the flag is raised again at once because the session is not attached.
# No hook fires as activity-action other does not apply to the current
# window.
$TMUX selectw -t mon:actw || fail "select-window actw failed"
assert_fmt_unchanged mon:actw '#{window_activity_flag}' 1
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
assert_fmt_unchanged mon:actw '#{window_activity_flag}' 1
assert_unchanged @log ''
# With a client attached, selecting the window does clear the activity
# flag and it stays clear for the current window.
mkfifo "$OUT/fifo" || fail "mkfifo failed"
$TMUX -C attach -t mon <"$OUT/fifo" >"$OUT/control.out" 2>&1 &
exec 3>"$OUT/fifo"
wait_for_fmt mon: '#{session_attached}' 1
$TMUX selectw -t mon:actw || fail "select-window actw failed"
wait_for_fmt mon:actw '#{window_activity_flag}' 0
flags_lack mon:actw '#'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
exec 3>&-
wait_for_fmt mon: '#{session_attached}' 0
# Once the flag is clear the alert is re-armed.
activity mon:actw
wait_for @log '|alert-activity:mon:actw'
# Activity in the current window of a detached session sets the flag but
# activity-action other (the default) does not fire the hook.
$TMUX set -wt mon:w0 monitor-activity on ||
fail "set monitor-activity w0 failed"
$TMUX set -g @log '' || fail "reset @log failed"
activity mon:w0
wait_for_fmt mon:w0 '#{window_activity_flag}' 1
assert_unchanged @log ''
$TMUX set -wut mon:w0 monitor-activity ||
fail "unset monitor-activity w0 failed"
# monitor-silence: a quiet window fires alert-silence after the interval
# and sets the silence flag, shown as ~ in window_flags. The hook fires
# only once while the flag remains set even though the timer keeps
# running.
$TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed"
assert_fmt_unchanged mon:silw '#{window_silence_flag}' 0
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX set -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed"
wait_for @log '|alert-silence:mon:silw'
wait_for_fmt mon:silw '#{window_silence_flag}' 1
flags_have mon:silw '~'
assert_unchanged @log '|alert-silence:mon:silw'
$TMUX set -wt mon:silw monitor-silence 0 ||
fail "reset monitor-silence failed"
$TMUX selectw -t mon:silw || fail "select-window silw failed"
wait_for_fmt mon:silw '#{window_silence_flag}' 0
flags_lack mon:silw '~'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
# A window linked into two sessions: the hook fires once per winlink and
# the flag is set in both sessions. Selecting the window in one session
# clears the flags on every winlink.
$TMUX new -d -s mon2 || fail "new-session mon2 failed"
$TMUX neww -d -t mon: -n shw cat || fail "new-window shw failed"
$TMUX link-window -d -s mon:shw -t mon2:5 || fail "link-window failed"
$TMUX set -g @log '' || fail "reset @log failed"
bell mon:shw
wait_for @log '|alert-bell:mon:shw|alert-bell:mon2:shw'
wait_for_fmt mon:shw '#{window_bell_flag}' 1
wait_for_fmt mon2:5 '#{window_bell_flag}' 1
$TMUX selectw -t mon2:5 || fail "select-window mon2:5 failed"
wait_for_fmt mon2:5 '#{window_bell_flag}' 0
wait_for_fmt mon:shw '#{window_bell_flag}' 0
exit 0

View File

@@ -4,9 +4,9 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -11,9 +11,9 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMUX_OUTER="$TEST_TMUX -Ltest2 -f/dev/null"
TMUX_OUTER="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX_OUTER kill-server 2>/dev/null
trap "$TMUX kill-server 2>/dev/null; $TMUX_OUTER kill-server 2>/dev/null" 0 1 15

287
regress/buffers.sh Normal file
View File

@@ -0,0 +1,287 @@
#!/bin/sh
# Tests of paste buffer command semantics, as implemented in cmd-set-buffer.c
# (set-buffer and delete-buffer), cmd-paste-buffer.c, cmd-load-buffer.c,
# cmd-save-buffer.c (save-buffer and show-buffer), cmd-list-buffers.c and
# paste.c.
#
# This exercises:
# - set-buffer creating automatic buffers (buffer0, buffer1, ... with the
# most recent first), -b creating/replacing a named buffer, -a appending,
# -n renaming and the error paths (no data, unknown buffer);
# - show-buffer for the top and for named buffers;
# - delete-buffer for the top and named buffers, and when nothing exists;
# - list-buffers -F custom formats and -f filters;
# - paste-buffer into a pane: newline-to-CR translation by default, -r raw,
# -s custom separator, -d delete-after-paste, unknown buffer error;
# - the buffer-limit option evicting the oldest automatic buffers but not
# named buffers;
# - load-buffer/save-buffer round trips including control characters and
# UTF-8, save-buffer -a appending and errors for missing files/buffers.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
TMP2=$(mktemp)
trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15
# check_ok $cmd...
#
# Run a command and require that it succeeds.
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $cmd...
#
# Run a command and require that it fails with the given error message.
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
# check_buffers $expected
#
# Compare the buffer list (as "name=content ...", most recent first) with
# $expected.
check_buffers()
{
out=$(echo $($TMUX list-buffers -F \
'#{buffer_name}=#{buffer_sample}'))
if [ "$out" != "$1" ]; then
echo "Buffer list wrong."
echo "Expected: '$1'"
echo "But got: '$out'"
exit 1
fi
}
# check_show $args $expected
#
# Compare show-buffer output with $expected.
check_show()
{
out=$($TMUX show-buffer $1 2>&1)
if [ "$out" != "$2" ]; then
echo "show-buffer $1 wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
check_ok new-session -d -s B -x 80 -y 24
# ---------------------------------------------------------------------------
# set-buffer, show-buffer, delete-buffer, list-buffers.
# Automatic buffers stack with the most recent first.
check_ok set-buffer one
check_ok set-buffer two
check_buffers 'buffer1=two buffer0=one'
check_show '' 'two'
check_show '-b buffer0' 'one'
# -a only appends to a buffer named with -b: without -b it creates a new
# automatic buffer. Empty data is silently ignored.
check_ok set-buffer -a '!'
check_buffers 'buffer2=! buffer1=two buffer0=one'
check_ok delete-buffer
check_ok set-buffer ''
check_buffers 'buffer1=two buffer0=one'
# -b names a buffer explicitly; setting it again replaces the content;
# -a appends to it.
check_ok set-buffer -b named abc
check_buffers 'named=abc buffer1=two buffer0=one'
check_ok set-buffer -b named xyz
check_ok set-buffer -a -b named 123
check_buffers 'named=xyz123 buffer1=two buffer0=one'
check_show '-b named' 'xyz123'
# -n renames; renaming to a bad source is an error, as is no data at all.
check_ok set-buffer -b named -n other
check_buffers 'other=xyz123 buffer1=two buffer0=one'
check_fail 'unknown buffer: nosuch' set-buffer -b nosuch -n foo
check_fail 'no data specified' set-buffer -b other
check_fail 'no buffer nosuch' show-buffer -b nosuch
# list-buffers -f filters.
out=$($TMUX list-buffers -f '#{==:#{buffer_name},other}' -F '#{buffer_name}')
if [ "$out" != "other" ]; then
echo "list-buffers -f wrong: '$out'"
exit 1
fi
# delete-buffer -b removes one buffer; without -b the most recent automatic
# buffer goes - named buffers are not candidates for the top, for
# show-buffer and delete-buffer alike.
check_ok delete-buffer -b buffer1
check_buffers 'other=xyz123 buffer0=one'
check_fail 'unknown buffer: buffer1' delete-buffer -b buffer1
check_ok delete-buffer
check_buffers 'other=xyz123'
check_fail 'no buffers' show-buffer
check_fail 'no buffer' delete-buffer
check_ok delete-buffer -b other
check_fail 'no buffers' show-buffer
# ---------------------------------------------------------------------------
# buffer-limit.
# Only automatic buffers count against buffer-limit and the oldest are
# evicted; named buffers survive. (Automatic buffer numbers keep counting
# up over the life of the server, so compare content only.)
check_ok set-option -g buffer-limit 3
check_ok set-buffer -b keepme precious
check_ok set-buffer a1
check_ok set-buffer a2
check_ok set-buffer a3
check_ok set-buffer a4
out=$(echo $($TMUX list-buffers -F '#{buffer_sample}'))
if [ "$out" != 'a4 a3 a2 precious' ]; then
echo "buffer-limit eviction wrong: '$out'"
exit 1
fi
check_ok set-option -g buffer-limit 50
check_ok delete-buffer -b keepme
check_ok delete-buffer; check_ok delete-buffer; check_ok delete-buffer
# ---------------------------------------------------------------------------
# paste-buffer.
# Paste into a raw, echo-free pane running cat -v so control characters are
# visible; a fresh window per paste keeps assertions simple.
# paste_line $bufdata $pasteargs $expected
#
# Set a buffer, paste it into a fresh cat -v pane and compare the first
# screen line with $expected.
paste_line()
{
$TMUX kill-window -t B:9 2>/dev/null
check_ok new-window -d -t B:9 'stty raw -echo && exec cat -v'
i=0
while [ "$($TMUX display-message -p -t B:9.0 \
'#{pane_current_command}')" != "cat" ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "cat did not start."; exit 1; }
sleep 0.1
done
check_ok set-buffer -b paste "$1"
check_ok paste-buffer $2 -b paste -t B:9.0
i=0
while out=$($TMUX capture-pane -p -t B:9.0 | sed -n 1p) && \
[ "$out" != "$3" ]; do
i=$((i + 1))
if [ $i -gt 50 ]; then
echo "Paste of '$1' ($2) wrong."
echo "Expected: '$3'"
echo "But got: '$out'"
exit 1
fi
sleep 0.1
done
}
# By default linefeeds are replaced with carriage returns (shown as ^M by
# cat -v); -r pastes raw and -s sets an explicit separator.
paste_line 'one
two' '' 'one^Mtwo'
paste_line 'one
two' '-r' 'one'
paste_line 'one
two' '-s |' 'one|two'
paste_line 'one
two' '-s XX' 'oneXXtwo'
# -d deletes the buffer after pasting.
paste_line 'gone' '-d' 'gone'
check_fail 'no buffer paste' show-buffer -b paste
# Unknown buffer is an error.
check_fail 'no buffer nosuch' paste-buffer -b nosuch -t B:9.0
check_ok kill-window -t B:9
# ---------------------------------------------------------------------------
# load-buffer and save-buffer.
# Round trip a file with control characters and UTF-8 through load-buffer
# and save-buffer.
printf 'line1\tx\033[31m\001\002\303\251\n' >"$TMP"
check_ok load-buffer -b file "$TMP"
check_ok save-buffer -b file "$TMP2"
if ! cmp -s "$TMP" "$TMP2"; then
echo "load-buffer/save-buffer round trip differs."
exit 1
fi
# save-buffer -a appends.
check_ok save-buffer -a -b file "$TMP2"
cat "$TMP" "$TMP" >"$TMP".x
if ! cmp -s "$TMP".x "$TMP2"; then
rm -f "$TMP".x
echo "save-buffer -a did not append."
exit 1
fi
rm -f "$TMP".x
# show-buffer prints the loaded content (text form).
check_ok delete-buffer -b file
# load-buffer of a missing file and save-buffer of a missing buffer or to a
# bad path are errors.
check_fail "No such file or directory: $TMP.nosuch" \
load-buffer -b x "$TMP.nosuch"
check_fail 'no buffer nosuch' save-buffer -b nosuch "$TMP2"
check_ok set-buffer -b sb data
out=$($TMUX save-buffer -b sb /nonexistent/dir/file 2>&1)
if [ $? -eq 0 ]; then
echo "save-buffer to bad path succeeded."
exit 1
fi
# save-buffer - writes to stdout and load-buffer - reads from stdin.
out=$($TMUX save-buffer -b sb -)
if [ "$out" != "data" ]; then
echo "save-buffer - wrong: '$out'"
exit 1
fi
check_ok delete-buffer -b sb
printf 'from stdin' | $TMUX load-buffer -b stdinbuf -
check_show '-b stdinbuf' 'from stdin'
check_ok delete-buffer -b stdinbuf
assert_alive
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -6,7 +6,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMP=$(mktemp)
TMP2=$(mktemp)
trap "rm -f $TMP $TMP2" 0 1 15

View File

@@ -7,7 +7,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -6,7 +6,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
fail()
@@ -35,68 +35,59 @@ $TMUX set-option -qg allow-set-title on || exit 1
$TMUX set-option -qg allow-rename on || exit 1
$TMUX set-option -qg automatic-rename off || exit 1
# Commands reject ':' and '.' for sessions and windows, but allow '#'.
$TMUX rename-session 'session#ok' || fail "session name with # rejected"
must_equal "$($TMUX display-message -p '#{session_name}')" 'session#ok'
must_fail $TMUX rename-session 'session:bad'
must_fail $TMUX rename-session 'session.bad'
# Commands allow empty names, ':', '.', '#' and '#('.
$TMUX rename-session '' || fail "empty session name rejected"
must_equal "$($TMUX display-message -p '#{session_name}')" ''
$TMUX rename-session 'session:.##(ok)' || \
fail "session name with : . or #( rejected"
must_equal "$($TMUX display-message -p '#{session_name}')" 'session:.#(ok)'
$TMUX rename-window 'window#ok' || fail "window name with # rejected"
must_equal "$($TMUX display-message -p '#{window_name}')" 'window#ok'
must_fail $TMUX rename-window 'window:bad'
must_fail $TMUX rename-window 'window.bad'
$TMUX rename-window '' || fail "empty window name rejected"
must_equal "$($TMUX display-message -p '#{window_name}')" ''
$TMUX rename-window 'window:.##(ok)' || \
fail "window name with : . or #( rejected"
must_equal "$($TMUX display-message -p '#{window_name}')" 'window:.#(ok)'
$TMUX set-option -q @name 'format#ok' || exit 1
$TMUX set-option -q @name 'format:.#(ok)' || exit 1
$TMUX rename-session '#{@name}' || fail "format in session name not expanded"
must_equal "$($TMUX display-message -p '#{session_name}')" 'format#ok'
must_equal "$($TMUX display-message -p '#{session_name}')" 'format:.#(ok)'
$TMUX rename-window '#{@name}' || fail "format in window name not expanded"
must_equal "$($TMUX display-message -p '#{window_name}')" 'format#ok'
must_fail $TMUX rename-session '#{session_name}:bad'
must_fail $TMUX rename-window '#{window_name}.bad'
must_equal "$($TMUX display-message -p '#{window_name}')" 'format:.#(ok)'
$TMUX set-option -q @name 'format:.#(ok)' || exit 1
pid=$($TMUX display-message -p '#{pid}')
created=$($TMUX new-session -dP -F '#{session_id}:#{window_id}' \
-s 'new-session#ok' -n 'new-window#ok') || \
fail "new-session name with # rejected"
-s 'new-session:.##(ok)' -n 'new-window:.##(ok)') || \
fail "new-session name with : . or #( rejected"
created_session=${created%:*}
created_window=${created#*:}
must_equal "$($TMUX display-message -pt "$created_session" '#{session_name}')" \
'new-session#ok'
'new-session:.#(ok)'
must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \
'new-window#ok'
'new-window:.#(ok)'
$TMUX kill-session -t "$created_session"
must_fail $TMUX new-session -d -s 'new-session:bad'
must_fail $TMUX new-session -d -s 'new-session.bad'
must_fail $TMUX new-session -d -n 'new-window:bad'
must_fail $TMUX new-session -d -n 'new-window.bad'
created_window=$($TMUX new-window -dP -F '#{window_id}' \
-n 'created-window#ok') || \
fail "new-window name with # rejected"
-n 'created-window:.##(ok)') || \
fail "new-window name with : . or #( rejected"
must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \
'created-window#ok'
must_fail $TMUX new-window -d -n 'created-window:bad'
must_fail $TMUX new-window -d -n 'created-window.bad'
'created-window:.#(ok)'
created=$($TMUX new-session -dP -F '#{session_id}:#{window_id}' \
-s 'new-session-#{pid}' -n 'new-window-#{pid}') || \
-s 'new-session-#{pid}:.##(ok)' -n 'new-window-#{pid}:.##(ok)') || \
fail "format in new-session name not expanded"
created_session=${created%:*}
created_window=${created#*:}
must_equal "$($TMUX display-message -pt "$created_session" '#{session_name}')" \
"new-session-$pid"
"new-session-$pid:.#(ok)"
must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \
"new-window-$pid"
"new-window-$pid:.#(ok)"
$TMUX kill-session -t "$created_session"
created_window=$($TMUX new-window -dP -F '#{window_id}' -n '#{@name}') || \
fail "format in new-window name not expanded"
must_equal "$($TMUX display-message -pt "$created_window" '#{window_name}')" \
'format#ok'
must_fail $TMUX new-session -d -s 'new-session-#{pid}:bad'
must_fail $TMUX new-session -d -n 'new-window-#{pid}.bad'
must_fail $TMUX new-window -d -n '#{window_name}:bad'
'format:.#(ok)'
# Invalid UTF-8 is never allowed for command names.
invalid=$(printf '\302')
@@ -124,9 +115,9 @@ must_equal "$($TMUX list-buffers -F '#{buffer_name}')" 'buffer#:.ok'
# Window names from escape sequences allow '#' except in '#('.
$TMUX send-keys "printf '\\033kescape#:.ok\\033\\\\'" Enter || exit 1
sleep 1
must_equal "$($TMUX display-message -p '#{window_name}')" 'escape#__ok'
must_equal "$($TMUX display-message -p '#{window_name}')" 'escape#:.ok'
# Titles from escape sequences reject only '#'.
# Titles from escape sequences allow '#' except in '#('.
$TMUX send-keys "printf '\\033]2;escape#:.ok\\007'" Enter || exit 1
sleep 1
must_equal "$($TMUX display-message -p '#{pane_title}')" 'escape#:.ok'

271
regress/choose-buffer.sh Normal file
View File

@@ -0,0 +1,271 @@
#!/bin/sh
# Tests of buffer mode (window-buffer.c) as driven by choose-buffer: that the
# -f filter removes buffers that do not match (by name and by content), that
# a filter matching nothing falls back to showing everything, that -O and -r
# change the sort order, that d deletes the selected buffer and C-t and D
# delete all tagged buffers, and that Enter runs the default command
# (paste-buffer) with the selected buffer.
#
# The list is drawn on a mode screen which capture-pane does not show, so a
# second server provides a client: an inner "tmux attach" runs inside a pane
# of the second server, and that pane is captured to read what the inner
# client rendered. Each choose-buffer call uses a distinct -F marker so a
# capture can be tied to the call it belongs to.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMP=$(mktemp -d) || exit 1
TMUX_TMPDIR="$TMP"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -rf "$TMP"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
exit 1
}
# capture the screen rendered by the inner client
capture()
{
$TMUX2 capture-pane -p -t out:0 2>/dev/null
}
# wait_for $marker
#
# Wait (up to ~10s) until the rendered screen contains $marker, so the
# capture is known to show the mode instance under test.
wait_for()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# wait_gone $marker
#
# Wait (up to ~10s) until the rendered screen no longer contains $marker.
wait_gone()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1' to disappear"
}
# wait_count $marker $n
#
# Wait (up to ~10s) until exactly $n rendered lines contain $marker. The
# matching capture is left in CAPTURED.
wait_count()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1")
[ "$c" -eq "$2" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for $2 lines of '$1' (have $c)"
}
# wait_buffers $n
#
# Wait (up to ~10s) until the test server has exactly $n paste buffers.
wait_buffers()
{
i=0
while [ "$i" -lt 50 ]; do
c=$($TMUX list-buffers -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 0.5
i=$((i + 1))
done
fail "expected $1 buffers, have $c"
}
# wait_clients $n
#
# Wait (up to ~10s) until the test server has exactly $n clients attached.
wait_clients()
{
i=0
while [ "$i" -lt 10 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 1
i=$((i + 1))
done
return 1
}
# wait_mode $target $state
#
# Wait (up to ~10s) until a pane enters or leaves mode.
wait_mode()
{
t=$1
want=$2
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \
2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "pane $t mode state is $got, expected $want"
}
exit_mode()
{
marker=$1
shift
$TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed"
wait_mode aaa:0 0
wait_gone "$marker"
}
$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1
wait_clients 1 || fail "no client attached to test server"
# Two named buffers with distinct contents; bufa is created first.
$TMUX set-buffer -b bufa "hello buffer" || exit 1
$TMUX set-buffer -b bufz "other buffer" || exit 1
# --- filter by buffer name ---------------------------------------------------
$TMUX choose-buffer -t aaa:0 -F 'B1' -f '#{==:#{buffer_name},bufa}' || exit 1
wait_count ': B1' 1
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'bufa: B1' || \
fail "bufa missing when it matches"
printf '%s\n' "$out" | grep -F -q 'bufz: B1' && \
fail "bufz shown but does not match"
[ "$(printf '%s\n' "$out" | grep -F -c ': B1')" -eq 1 ] || \
fail "expected 1 buffer"
exit_mode ': B1' q
# --- filter by buffer content ------------------------------------------------
$TMUX choose-buffer -t aaa:0 -F 'B2' -f '#{m:*hello*,#{buffer_sample}}' || \
exit 1
wait_count ': B2' 1
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'bufa: B2' || \
fail "bufa missing when content matches"
printf '%s\n' "$out" | grep -F -q 'bufz: B2' && \
fail "bufz shown but content not matched"
exit_mode ': B2' q
# --- no filter shows both buffers ---------------------------------------------
$TMUX choose-buffer -t aaa:0 -F 'B3' || exit 1
wait_count ': B3' 2
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'bufa: B3' || \
fail "bufa missing with no filter"
printf '%s\n' "$out" | grep -F -q 'bufz: B3' || \
fail "bufz missing with no filter"
[ "$(printf '%s\n' "$out" | grep -F -c ': B3')" -eq 2 ] || \
fail "expected 2 buffers"
exit_mode ': B3' q
# --- filter matching nothing ---------------------------------------------------
#
# Everything is shown and the filter indicator reports no matches.
$TMUX choose-buffer -t aaa:0 -F 'B4' -f '#{==:#{buffer_name},nosuch}' || \
exit 1
wait_count ': B4' 2
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'bufa: B4' || \
fail "bufa missing with no-match filter"
printf '%s\n' "$out" | grep -F -q 'bufz: B4' || \
fail "bufz missing with no-match filter"
printf '%s\n' "$out" | grep -F -q 'no matches' || \
fail "no matches indicator missing"
exit_mode ': B4' q
# --- sort orders ---------------------------------------------------------------
#
# By name bufa sorts first and -r reverses.
$TMUX choose-buffer -t aaa:0 -F 'B5' -O name || exit 1
wait_count ': B5' 2
printf '%s\n' "$CAPTURED" | grep -F ': B5' | head -1 | \
grep -F -q 'bufa: B5' || \
fail "bufa not first with -O name"
exit_mode ': B5' q
$TMUX choose-buffer -t aaa:0 -F 'B6' -O name -r || exit 1
wait_count ': B6' 2
printf '%s\n' "$CAPTURED" | grep -F ': B6' | head -1 | \
grep -F -q 'bufz: B6' || \
fail "bufz not first with -O name -r"
exit_mode ': B6' q
# --- d deletes the selected buffer --------------------------------------------
#
# The filter leaves only bufz listed and selected; d deletes it.
$TMUX choose-buffer -t aaa:0 -F 'G1' -f '#{==:#{buffer_name},bufz}' || exit 1
wait_for 'bufz: G1'
$TMUX send-keys -t aaa:0 d
wait_buffers 1
$TMUX list-buffers -F '#{buffer_name}' | grep -F -q 'bufa' || \
fail "wrong buffer deleted"
exit_mode ': G1' q
# --- C-t tags all buffers and D deletes the tagged ------------------------------
$TMUX set-buffer -b bufz "other buffer" || exit 1
$TMUX set-buffer -b bufb "third buffer" || exit 1
$TMUX choose-buffer -t aaa:0 -F 'G2' || exit 1
wait_count ': G2' 3
$TMUX send-keys -t aaa:0 C-t D
wait_buffers 0
wait_mode aaa:0 0
wait_gone ': G2'
# --- Enter runs the default command (paste-buffer) ------------------------------
#
# The only buffer is listed and selected; Enter leaves the mode and pastes it
# into the shell in the pane, where it appears on the screen.
$TMUX set-buffer -b bufa "hello buffer" || exit 1
$TMUX choose-buffer -t aaa:0 -F 'G3' || exit 1
wait_count ': G3' 1
exit_mode ': G3' Enter
i=0
while [ "$i" -lt 50 ]; do
$TMUX capture-pane -p -t aaa:0 | grep -F -q 'hello buffer' && \
break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "buffer not pasted into pane"
exit 0

218
regress/choose-client.sh Normal file
View File

@@ -0,0 +1,218 @@
#!/bin/sh
# Tests of client mode (window-client.c) as driven by choose-client: that the
# -f filter removes clients that do not match, that a filter matching nothing
# falls back to showing everything, and that Enter runs the default command
# (detach-client) on the selected client. Sort orders are not tested here
# because clients are named after their ttys, which are not predictable.
#
# The list is drawn on a mode screen which capture-pane does not show, so a
# second server provides the clients: two inner "tmux attach" commands run in
# panes of the second server, and the pane holding the client the mode is
# displayed on is captured. Each choose-client call uses a distinct -F marker
# so a capture can be tied to the call it belongs to.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMP=$(mktemp -d) || exit 1
TMUX_TMPDIR="$TMP"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -rf "$TMP"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
exit 1
}
# capture the screen rendered by the inner client attached to aaa
capture()
{
$TMUX2 capture-pane -p -t out:0 2>/dev/null
}
# wait_for $marker
#
# Wait (up to ~10s) until the rendered screen contains $marker, so the
# capture is known to show the mode instance under test.
wait_for()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# wait_gone $marker
#
# Wait (up to ~10s) until the rendered screen no longer contains $marker.
wait_gone()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1' to disappear"
}
# wait_count $marker $n
#
# Wait (up to ~10s) until exactly $n rendered lines contain $marker. The
# matching capture is left in CAPTURED.
wait_count()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1")
[ "$c" -eq "$2" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for $2 lines of '$1' (have $c)"
}
# wait_clients $n
#
# Wait (up to ~10s) until the test server has exactly $n clients attached.
wait_clients()
{
i=0
while [ "$i" -lt 10 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 1
i=$((i + 1))
done
return 1
}
# wait_mode $target $state
#
# Wait (up to ~10s) until a pane enters or leaves mode.
wait_mode()
{
t=$1
want=$2
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \
2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "pane $t mode state is $got, expected $want"
}
exit_mode()
{
marker=$1
shift
$TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed"
wait_mode aaa:0 0
wait_gone "$marker"
}
# One client attached to each of two sessions; the mode is displayed on the
# client attached to aaa (in window 0 of the outer server) and the filters
# tell the clients apart by their attached session.
$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1
$TMUX new-session -d -s bbb -x 80 -y 24 'cat' || exit 1
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1
$TMUX2 new-window -d -t out: "$TMUX attach -t bbb" || exit 1
wait_clients 2 || fail "expected two clients attached to test server"
# --- filter keeping only the aaa client -------------------------------------
$TMUX choose-client -t aaa:0 -F 'C1=#{client_session}' \
-f '#{==:#{client_session},aaa}' || exit 1
wait_count 'C1=' 1
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'C1=aaa' || \
fail "aaa client missing when it matches"
printf '%s\n' "$out" | grep -F -q 'C1=bbb' && \
fail "bbb client shown but does not match"
[ "$(printf '%s\n' "$out" | grep -F -c 'C1=')" -eq 1 ] || \
fail "expected 1 client"
exit_mode 'C1=' q
# --- filter keeping only the bbb client -------------------------------------
$TMUX choose-client -t aaa:0 -F 'C2=#{client_session}' \
-f '#{==:#{client_session},bbb}' || exit 1
wait_count 'C2=' 1
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'C2=bbb' || \
fail "bbb client missing when it matches"
printf '%s\n' "$out" | grep -F -q 'C2=aaa' && \
fail "aaa client shown but does not match"
[ "$(printf '%s\n' "$out" | grep -F -c 'C2=')" -eq 1 ] || \
fail "expected 1 client"
exit_mode 'C2=' q
# --- no filter shows both clients -------------------------------------------
$TMUX choose-client -t aaa:0 -F 'C3=#{client_session}' || exit 1
wait_count 'C3=' 2
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'C3=aaa' || \
fail "aaa client missing with no filter"
printf '%s\n' "$out" | grep -F -q 'C3=bbb' || \
fail "bbb client missing with no filter"
[ "$(printf '%s\n' "$out" | grep -F -c 'C3=')" -eq 2 ] || \
fail "expected 2 clients"
exit_mode 'C3=' q
# --- filter matching nothing ------------------------------------------------
#
# Everything is shown and the filter indicator reports no matches.
$TMUX choose-client -t aaa:0 -F 'C4=#{client_session}' \
-f '#{==:#{client_session},nosuch}' || exit 1
wait_count 'C4=' 2
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'C4=aaa' || \
fail "aaa client missing with no-match filter"
printf '%s\n' "$out" | grep -F -q 'C4=bbb' || \
fail "bbb client missing with no-match filter"
printf '%s\n' "$out" | grep -F -q 'no matches' || \
fail "no matches indicator missing"
exit_mode 'C4=' q
# --- Enter runs the default command (detach-client) --------------------------
#
# The filter leaves only the bbb client listed and selected; Enter detaches
# it, leaving only the aaa client attached.
$TMUX choose-client -t aaa:0 -F 'G1=#{client_session}' \
-f '#{==:#{client_session},bbb}' || exit 1
wait_for 'G1=bbb'
$TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed"
wait_clients 1 || fail "bbb client did not detach"
wait_mode aaa:0 0
wait_gone 'G1='
[ "$($TMUX list-clients -F '#{client_session}')" = "aaa" ] || \
fail "wrong client detached"
exit 0

293
regress/choose-tree.sh Normal file
View File

@@ -0,0 +1,293 @@
#!/bin/sh
# Tests of tree mode (window-tree.c) as driven by choose-tree.
#
# Filtering: the -f filter is applied per pane and removes panes, windows and
# sessions with no matching panes (a window with more than one pane and no
# matching panes must disappear - GitHub issue 5326); -h keeps a window
# listed when its only matching pane is the pane the tree is drawn in; a
# filter matching nothing falls back to showing everything.
#
# Sorting: -O and -r change the sort order.
#
# Keys: h and l collapse and expand; f prompts for a filter and c clears it;
# g goes to the top; Enter runs the default command (switch-client); x kills
# the current item after a confirmation prompt.
#
# The tree is drawn on a mode screen which capture-pane does not show, so - as
# in environ-update.sh - a second server provides a client: an inner "tmux
# attach" runs inside a pane of the second server, and that pane is captured
# to read what the inner client rendered. Each choose-tree call uses a
# distinct -F marker so a capture can be tied to the call it belongs to.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMP=$(mktemp -d) || exit 1
TMUX_TMPDIR="$TMP"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -rf "$TMP"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
exit 1
}
# capture the screen rendered by the inner client
capture()
{
$TMUX2 capture-pane -p -t out:0 2>/dev/null
}
# wait_for $marker
#
# Wait (up to ~10s) until the rendered screen contains $marker, so the
# capture is known to show the mode instance under test.
wait_for()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# wait_count $row-marker $n
#
# Wait (up to ~10s) until exactly $n rendered list rows contain $row-marker.
wait_count()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
c=$(printf '%s\n' "$CAPTURED" | grep -F -c "$1")
[ "$c" -eq "$2" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for $2 lines of '$1' (have $c)"
}
# wait_clients $n
#
# Wait (up to ~10s) until the test server has exactly $n clients attached.
wait_clients()
{
i=0
while [ "$i" -lt 10 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 1
i=$((i + 1))
done
return 1
}
# wait_mode $target $state
#
# Wait (up to ~10s) until a pane enters or leaves mode.
wait_mode()
{
t=$1
want=$2
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \
2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "pane $t mode state is $got, expected $want"
}
exit_mode()
{
$TMUX send-keys -t aaa:0 "$@" || fail "send-keys $* failed"
wait_mode aaa:0 0
}
# Session zzz is created first, so it sorts first by index, and has a
# two-pane window 0 and a single-pane window 1. Session aaa has one window
# with one pane and is where the tree is displayed. With everything expanded
# and no filter the tree is nine lines:
#
# 0 zzz 1 window 0 2 pane 0 3 pane 1 4 window 1 5 pane 0
# 6 aaa 7 window 0 8 pane 0
$TMUX new-session -d -s zzz -x 80 -y 24 'cat' || exit 1
$TMUX split-window -t zzz:0 'cat' || exit 1
$TMUX new-window -t zzz 'cat' || exit 1
$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1
wait_clients 1 || fail "no client attached to test server"
# --- filter keeping only aaa ------------------------------------------------
#
# zzz must disappear entirely: its single-pane window 1 and - the GitHub 5326
# regression - its two-pane window 0. aaa contributes exactly three lines
# (session, window, pane).
$TMUX choose-tree -t aaa:0 -F 'F1' -f '#{==:#{session_name},aaa}' || exit 1
wait_count ': F1' 3
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'aaa: F1' || \
fail "aaa missing when filter matches it"
printf '%s\n' "$out" | grep -F -q 'zzz: F1' && \
fail "zzz shown but no pane matches"
exit_mode q
# --- filter keeping only zzz ------------------------------------------------
#
# zzz contributes six lines (session, two windows, three panes); aaa must
# disappear.
$TMUX choose-tree -t aaa:0 -F 'F2' -f '#{==:#{session_name},zzz}' || exit 1
wait_count ': F2' 6
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'zzz: F2' || \
fail "zzz missing when filter matches it"
printf '%s\n' "$out" | grep -F -q 'aaa: F2' && \
fail "aaa shown but no pane matches"
exit_mode q
# --- filter matching a single pane ------------------------------------------
#
# Only pane 1 of zzz:0 matches, so the tree is exactly session zzz, window 0
# and that pane; zzz:1 and all of aaa must disappear.
$TMUX choose-tree -t aaa:0 -F 'F3' -f '#{==:#{pane_index},1}' || exit 1
wait_count ': F3' 3
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'zzz: F3' || \
fail "zzz missing when its pane matches"
printf '%s\n' "$out" | grep -F -q 'aaa: F3' && \
fail "aaa shown but no pane matches"
printf '%s\n' "$out" | grep -F -q '1: F3' || \
fail "matching pane missing"
exit_mode q
# --- filter matching nothing ------------------------------------------------
#
# Everything is shown and the filter indicator reports no matches.
$TMUX choose-tree -t aaa:0 -F 'F4' -f '#{==:#{session_name},nosuch}' || exit 1
wait_count ': F4' 9
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'aaa: F4' || \
fail "aaa missing with no-match filter"
printf '%s\n' "$out" | grep -F -q 'zzz: F4' || \
fail "zzz missing with no-match filter"
printf '%s\n' "$out" | grep -F -q 'no matches' || \
fail "no matches indicator missing"
exit_mode q
# --- -h with the tree pane as the only match --------------------------------
#
# With -h the pane the tree is drawn in is hidden, but it still counts as a
# match, so session and window aaa stay listed: two lines, no pane line.
$TMUX choose-tree -h -t aaa:0 -F 'F5' -f '#{==:#{session_name},aaa}' || \
exit 1
wait_count ': F5' 2
printf '%s\n' "$CAPTURED" | grep -F -q 'aaa: F5' || \
fail "aaa missing with -h"
exit_mode q
# --- sort orders ------------------------------------------------------------
#
# By index zzz (created first) sorts first, by name aaa does, and -r reverses.
$TMUX choose-tree -t aaa:0 -F 'F6' -O index || exit 1
wait_count ': F6' 9
printf '%s\n' "$CAPTURED" | grep -F ': F6' | head -1 | \
grep -F -q 'zzz: F6' || \
fail "zzz not first with -O index"
exit_mode q
$TMUX choose-tree -t aaa:0 -F 'F7' -O name || exit 1
wait_count ': F7' 9
printf '%s\n' "$CAPTURED" | grep -F ': F7' | head -1 | \
grep -F -q 'aaa: F7' || \
fail "aaa not first with -O name"
exit_mode q
$TMUX choose-tree -t aaa:0 -F 'F8' -O name -r || exit 1
wait_count ': F8' 9
printf '%s\n' "$CAPTURED" | grep -F ': F8' | head -1 | \
grep -F -q 'zzz: F8' || \
fail "zzz not first with -O name -r"
exit_mode q
# --- collapse and expand with h and l -----------------------------------------
#
# g moves to the top (session zzz); h collapses it, hiding its five children;
# l expands it again.
$TMUX choose-tree -t aaa:0 -F 'G1' -O index || exit 1
wait_count ': G1' 9
$TMUX send-keys -t aaa:0 g h || fail "send-keys collapse failed"
wait_count ': G1' 4
$TMUX send-keys -t aaa:0 l || fail "send-keys expand failed"
wait_count ': G1' 9
exit_mode q
# --- filter entered at the prompt with f, cleared with c ----------------------
$TMUX choose-tree -t aaa:0 -F 'G2' -O index || exit 1
wait_count ': G2' 9
$TMUX send-keys -t aaa:0 f || fail "send-keys f failed"
$TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}' || \
fail "send-keys filter failed"
$TMUX send-keys -t aaa:0 Enter || fail "send-keys Enter failed"
wait_count ': G2' 3
out=$CAPTURED
printf '%s\n' "$out" | grep -F -q 'aaa: G2' || \
fail "aaa missing with prompt filter"
printf '%s\n' "$out" | grep -F -q 'zzz: G2' && \
fail "zzz shown with prompt filter"
$TMUX send-keys -t aaa:0 c || fail "send-keys c failed"
wait_count ': G2' 9
exit_mode q
# --- Enter runs the default command (switch-client) ----------------------------
#
# g selects session zzz and Enter switches the client to it.
$TMUX choose-tree -t aaa:0 -F 'G3' -O index || exit 1
wait_count ': G3' 9
$TMUX send-keys -t aaa:0 g Enter || fail "send-keys Enter failed"
i=0
while [ "$i" -lt 50 ]; do
[ "$($TMUX list-clients -F '#{client_session}')" = "zzz" ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "client did not switch to zzz"
$TMUX switch-client -c "$($TMUX list-clients -F '#{client_name}')" -t aaa || \
exit 1
wait_mode aaa:0 0
# --- x kills the current item after confirmation -------------------------------
#
# g and four times j select window 1 of zzz; x asks for confirmation and y
# kills it, leaving zzz with one window and the tree with seven lines.
$TMUX choose-tree -t aaa:0 -F 'G4' -O index || exit 1
wait_count ': G4' 9
$TMUX send-keys -t aaa:0 g j j j j x || fail "send-keys x failed"
wait_for 'Kill window 1'
$TMUX send-keys -t aaa:0 y || fail "send-keys y failed"
wait_count ': G4' 7
[ "$($TMUX list-windows -t zzz -F x | grep -c x)" -eq 1 ] || \
fail "window 1 of zzz not killed"
exit_mode q
exit 0

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -6,7 +6,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -sfoo || exit 1

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
for i in conf/*.conf; do

View File

@@ -35,7 +35,7 @@ bind -n C-h run "(tmux display-message -p '#{pane_current_command}' | grep -iq v
bind -n C-j run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-j) || tmux select-pane -D"
bind -n C-k run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-k) || tmux select-pane -U"
bind -n C-l run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys C-l) || tmux select-pane -R"
bind -n C-\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l"
bind -n C-\\ run "(tmux display-message -p '#{pane_current_command}' | grep -iq vim && tmux send-keys 'C-\\') || tmux select-pane -l"
# C-l is taken oer by vim style pane navigation
bind C-l send-keys 'C-l'

View File

@@ -183,7 +183,7 @@ bind t swap-window -t 1 # swap the current window's position with window # 1,
unbind & # unbind default binding for `split-window -h`
bind - split-window -v -c '#{pane_current_path}' # vertical split
bind _ split-window -v -c '#{pane_current_path}' -f # full vertical split (v2.3+)
bind \ split-window -h -c '#{pane_current_path}' # horizontal split
bind \\ split-window -h -c '#{pane_current_path}' # horizontal split
bind | split-window -h -c '#{pane_current_path}' -f # full horizontal split (v2.3+)
# https://www.reddit.com/r/tmux/comments/3paqoi/tmux_21_has_been_released/cw5wy00
bind w switch-client -Tsplit_wind

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -8,7 +8,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -0,0 +1,172 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMPDIR=$(mktemp -d)
IN="$TMPDIR/in"
OUT="$TMPDIR/out"
PID=
cleanup()
{
[ -n "$PID" ] && kill "$PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup EXIT
wait_for()
{
pattern=$1
timeout=${2:-6}
i=0
while [ "$i" -lt "$timeout" ]; do
if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then
return 0
fi
sleep 1
i=$((i + 1))
done
echo "missing: $pattern"
cat "$OUT"
return 1
}
reject_for()
{
pattern=$1
timeout=${2:-3}
i=0
while [ "$i" -lt "$timeout" ]; do
if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then
echo "unexpected: $pattern"
cat "$OUT"
return 1
fi
sleep 1
i=$((i + 1))
done
return 0
}
wait_tmux()
{
target=$1
timeout=${2:-6}
i=0
while [ "$i" -lt "$timeout" ]; do
if $TMUX display-message -p -t "$target" '#{window_id}' \
>/dev/null 2>&1; then
return 0
fi
sleep 1
i=$((i + 1))
done
return 1
}
window_fields()
{
$TMUX display-message -p -t "$1" '#{window_id} #{window_index}'
}
pane_fields()
{
$TMUX display-message -p -t "$1" '#{window_id} #{window_index} #{pane_id}'
}
send()
{
printf '%s\n' "$*" >&3
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s subs -x 80 -y 24 || exit 1
sid=$($TMUX display-message -p -t subs '#{session_id}')
mkfifo "$IN"
: >"$OUT"
$TMUX -C attach-session -t subs <"$IN" >"$OUT" 2>&1 &
PID=$!
exec 3>"$IN"
send 'display-message -p ready'
wait_for 'ready' 3 || exit 1
send "refresh-client -B 'sw::#{session_windows}'"
wait_for "%subscription-changed sw $sid - - - : 1" || exit 1
send 'new-window'
wait_for "%subscription-changed sw $sid - - - : 2" || exit 1
send 'refresh-client -B sw'
send 'new-window'
reject_for "%subscription-changed sw $sid - - - : 3" || exit 1
send 'new-window -n pane-test'
wait_tmux subs:pane-test || exit 1
set -- $(pane_fields subs:pane-test)
wid=$1
widx=$2
pane=$3
send "refresh-client -B 'ap:%*:#{pane_id}'"
wait_for "%subscription-changed ap $sid $wid $widx $pane : $pane" || exit 1
send 'split-window -t subs:pane-test'
i=0
while [ "$i" -lt 6 ]; do
[ "$($TMUX list-panes -t subs:pane-test | wc -l)" -eq 2 ] && break
sleep 1
i=$((i + 1))
done
[ "$i" -lt 6 ] || exit 1
newpane=$($TMUX list-panes -t subs:pane-test -F '#{pane_id}' | tail -n 1)
wait_for "%subscription-changed ap $sid $wid $widx $newpane : $newpane" ||
exit 1
$TMUX new-session -d -s other || exit 1
$TMUX link-window -s subs:pane-test -t other:1 || exit 1
send "refresh-client -B 'sp:$newpane:#{pane_id}:#{window_id}'"
wait_for "%subscription-changed sp $sid $wid $widx $newpane : $newpane:$wid" ||
exit 1
send "refresh-client -B 'cw:$wid:#{window_id}:#{window_index}'"
wait_for "%subscription-changed cw $sid $wid $widx - : $wid:$widx" ||
exit 1
send "refresh-client -B 'aw:@*:#{window_id}'"
send 'new-window -n window-test'
wait_tmux subs:window-test || exit 1
set -- $(window_fields subs:window-test)
awid=$1
awidx=$2
wait_for "%subscription-changed aw $sid $awid $awidx - : $awid" || exit 1
send "refresh-client -B 'dup::#{session_windows}'"
wcount=$($TMUX display-message -p -t subs '#{session_windows}')
wait_for "%subscription-changed dup $sid - - - : $wcount" || exit 1
send "refresh-client -B 'dup::#{session_name}'"
wait_for "%subscription-changed dup $sid - - - : subs" || exit 1
send 'new-window -n dup-test'
wait_tmux subs:dup-test || exit 1
wcount=$($TMUX display-message -p -t subs '#{session_windows}')
reject_for "%subscription-changed dup $sid - - - : $wcount" || exit 1
send "refresh-client -B 'missing-pane:%999999:#{pane_id}'"
send "refresh-client -B 'missing-window:@999999:#{window_id}'"
reject_for '%subscription-changed missing-pane' || exit 1
reject_for '%subscription-changed missing-window' || exit 1
exit 0

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX new -d -x40 -y10 \

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -f/dev/null -Ltest"
TMUX="$TEST_TMUX -f/dev/null -LtestA$$"
$TMUX kill-server 2>/dev/null
$TMUX new -d -x40 -y10 \

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -9,7 +9,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
sleep 1

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 -LtestA$$ -f/dev/null"
# A second server on its own socket hosts the pane that runs the inner client.
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
# check_value $var $expected
#
# Compare show-environment of $var on the session with $expected.
check_value()
{
out=$($TMUX show-environment -t main "$1" 2>&1)
if [ "$out" != "$2" ]; then
echo "show-environment $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
cleanup
exit 1
fi
}
# wait_clients $n
#
# Wait (up to ~10s) until the test server has exactly $n clients attached.
wait_clients()
{
i=0
while [ "$i" -lt 10 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 1
i=$((i + 1))
done
return 1
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# The session imports MYVAR and ABSENTVAR by exact name and anything matching
# the glob TEST_*; nothing else is imported.
$TMUX set -g update-environment "MYVAR ABSENTVAR TEST_*" || exit 1
# Seed the session so the effect of attaching is visible: MYVAR will be
# overwritten by the client's value and ABSENTVAR will be cleared.
$TMUX set-environment -t main MYVAR oldvalue || exit 1
$TMUX set-environment -t main ABSENTVAR pre-existing || exit 1
# --- attach a client whose environment carries the imported variables ------
#
# MYVAR and TEST_GLOB are present in the inner client's environment; ABSENTVAR
# is deliberately absent; OTHER is present but not named by update-environment.
$TMUX2 new-session -d -x 90 -y 30 \
"MYVAR=fromclient TEST_GLOB=globval OTHER=nope $TMUX attach -t main" \
|| fail "could not start inner client"
wait_clients 1 || fail "no client attached to test server"
# MYVAR matched by name and present in the client -> imported (overwrites).
check_value MYVAR "MYVAR=fromclient"
# TEST_GLOB matched by the TEST_* glob and present -> imported.
check_value TEST_GLOB "TEST_GLOB=globval"
# ABSENTVAR named but not in the client environment -> cleared (NULL value,
# printed as -NAME).
check_value ABSENTVAR "-ABSENTVAR"
# OTHER is in the client environment but not named by update-environment, so it
# is not imported at all.
out=$($TMUX show-environment -t main OTHER 2>&1)
[ "$out" = "unknown variable: OTHER" ] || \
fail "OTHER was imported but should not have been: '$out'"
# --- -E disables the update-environment import -----------------------------
#
# Detach the client (kill its host server), reset the session variables, then
# reattach with -E: the session values must be left untouched.
$TMUX2 kill-server 2>/dev/null
wait_clients 0 || fail "client did not detach"
$TMUX set-environment -t main MYVAR oldvalue2 || exit 1
$TMUX set-environment -t main ABSENTVAR pre2 || exit 1
$TMUX2 new-session -d -x 90 -y 30 \
"MYVAR=fromclientE $TMUX attach -E -t main" \
|| fail "could not start inner -E client"
wait_clients 1 || fail "no -E client attached to test server"
# With -E neither variable is touched by the attach.
check_value MYVAR "MYVAR=oldvalue2"
check_value ABSENTVAR "ABSENTVAR=pre2"
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
fail "server died after update-environment tests"
fi
cleanup
exit 0

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 -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
# check_value $args $expected
#
# Run show-environment with $args and compare the single-line output.
check_value()
{
out=$($TMUX show-environment $1 2>&1)
if [ "$out" != "$2" ]; then
echo "show-environment $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $cmd...
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# --- set and show at session scope ----------------------------------------
check_ok set-environment FOO bar
check_value "FOO" "FOO=bar"
# setenv is an alias for set-environment; showenv for show-environment.
check_ok setenv FOO2 bar2
out=$($TMUX showenv FOO2 2>&1)
[ "$out" = "FOO2=bar2" ] || { echo "setenv/showenv alias failed: '$out'"; exit 1; }
# --- set and show at global scope -----------------------------------------
#
# The global scope is separate from the session scope: a session variable is
# not visible in the global environment.
check_ok set-environment -g GVAR gval
check_value "-g GVAR" "GVAR=gval"
check_fail "unknown variable: FOO" show-environment -g FOO
# --- overwrite replaces the value -----------------------------------------
check_ok set-environment FOO baz
check_value "FOO" "FOO=baz"
# --- shell (-s) output form and escaping ----------------------------------
#
# With -s the value is printed as a shell assignment with export, and the
# characters $ ` " and \ are backslash-escaped (POSIX double-quote rules).
check_ok set-environment ESC 'a$b`c"d\e'
check_value "-s ESC" 'ESC="a\$b\`c\"d\\e"; export ESC;'
# --- -F expands the value as a format at set time -------------------------
#
# With a resolvable target the value is expanded once when set; the stored
# value is the result, not the format.
check_ok set-environment -t main -F EXP '#{session_name}'
check_value "EXP" "EXP=main"
# --- hidden variables (-h) ------------------------------------------------
#
# set-environment -h marks a variable hidden. show-environment hides it by
# default and only prints it when -h is given; conversely a normal variable is
# omitted when -h is given.
check_ok set-environment -h SECRET s3cr
check_value "SECRET" ""
check_value "-h SECRET" "SECRET=s3cr"
check_value "-h FOO" ""
# --- -r clears a variable (NULL value, masks inheritance) -----------------
#
# A cleared entry still exists but has no value: normal form prints "-NAME"
# and shell form prints "unset NAME;".
check_ok set-environment -r FOO
check_value "FOO" "-FOO"
check_value "-s FOO" "unset FOO;"
# --- -u removes a variable entirely ---------------------------------------
check_ok set-environment -u FOO
check_fail "unknown variable: FOO" show-environment FOO
# --- show with no name lists every (non-hidden) variable ------------------
check_ok set-environment -g LISTA 1
check_ok set-environment -g LISTB 2
check_ok set-environment -gh LISTHID 3
out=$($TMUX show-environment -g 2>&1)
echo "$out" | grep -q '^LISTA=1$' || { echo "list missing LISTA"; exit 1; }
echo "$out" | grep -q '^LISTB=2$' || { echo "list missing LISTB"; exit 1; }
# A hidden variable is not listed without -h.
echo "$out" | grep -q '^LISTHID' && { echo "list showed hidden var without -h"; exit 1; }
# With -h only hidden variables are listed.
$TMUX show-environment -gh 2>&1 | grep -q '^LISTHID=3$' || \
{ echo "list -h missing LISTHID"; exit 1; }
# --- config-file assignment forms (environ_put) ---------------------------
#
# A bare NAME=value line in a config file sets a global variable; a "%hidden"
# NAME=value line sets a hidden one. Start a second server from such a config
# and read the values back.
CONF=$(mktemp)
cat > "$CONF" <<EOF
CFGVAR=fromconfig
%hidden CFGHID=hiddencfg
EOF
CTMUX="$TEST_TMUX -LtestB$$ -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

View File

@@ -15,7 +15,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
fail()

View File

@@ -6,7 +6,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
# test_format $format $expected_result
test_format()

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

@@ -0,0 +1,580 @@
#!/bin/sh
# Tests of format modifiers as described in tmux(1) FORMATS.
#
# This complements format-strings.sh (which covers escapes, conditionals,
# boolean operators and the l: literal modifier). Here we exercise the
# remaining modifiers: comparisons/matching (m, C, <, >, ==, ...), numeric
# operations (e|op|), width/padding/truncation (=, p, n, w, a, R), basename
# and dirname (b, d), time conversion (t), loops (S, W, P), colour (c) and
# modifier nesting/limits.
PATH=/bin:/usr/bin
TERM=screen
TZ=UTC
LANG=C.UTF-8
LC_ALL=C.UTF-8
export TZ LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
ESC=$(printf '\033')
# test_format $format $expected [$target]
#
# Expand $format with display-message and compare with $expected. If $target
# is given it is passed to display-message with -t.
test_format()
{
fmt="$1"
exp="$2"
target="$3"
if [ -n "$target" ]; then
out=$($TMUX display-message -t "$target" -p "$fmt")
else
out=$($TMUX display-message -p "$fmt")
fi
if [ "$out" != "$exp" ]; then
echo "Format test failed for '$fmt'."
echo "Expected: '$exp'"
echo "But got '$out'"
exit 1
fi
}
# test_expand $format $expected
#
# Expand $format in a plain format_expand context (list-windows -F on the
# single-window "tf" session) rather than the format_expand_time context of
# display-message. This matters for t/f: display-message runs the whole format
# through strftime(3), so a strftime specifier there must be doubled (%%H); in a
# format_expand context a single specifier (%H) is applied directly to the
# variable's time.
test_expand()
{
fmt="$1"
exp="$2"
out=$($TMUX list-windows -t tf -F "$fmt")
if [ "$out" != "$exp" ]; then
echo "Format test failed for '$fmt'."
echo "Expected: '$exp'"
echo "But got '$out'"
exit 1
fi
}
# assert_alive
#
# Check that the server is still responding (used after operations that could
# in principle crash it, such as recursion and division by zero).
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server did not survive: $1"
exit 1
fi
}
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# Single-window session used by test_expand for format_expand-context tests.
$TMUX new-session -d -s tf || exit 1
# User options used as inputs. Modifiers operate on variable names, so plain
# literals must be provided via options (or a nested #{l:...}). They are set
# globally (-g) so they are visible from every session, including the "tf"
# session used by test_expand.
$TMUX set -g @s 'abcdefghij' || exit 1
$TMUX set -g @path '/usr/local/bin/foo' || exit 1
$TMUX set -g @name 'window-name' || exit 1
$TMUX set -g @greek 'αβγ' || exit 1 # 6 bytes, 3 columns wide
$TMUX set -g @cjk '中文' || exit 1 # 6 bytes, 4 columns wide
$TMUX set -g @host 'myhost' || exit 1
$TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC
$TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q:
$TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e:
$TMUX set -g @sq "a'b" || exit 1 # a single quote for q/s:
$TMUX set -g @nl "$(printf 'a\nb')" || exit 1
q_s_nl=$(printf "'a\nb'")
# --- Comparisons and matching --------------------------------------------
# m: glob match, first argument is the pattern.
test_format "#{m:*foo*,barfoobar}" "1"
test_format "#{m:*foo*,barbar}" "0"
test_format "#{m:abc,abc}" "1"
# m/i: ignore case.
test_format "#{m/i:*FOO*,barfoobar}" "1"
test_format "#{m/i:*FOO*,barbar}" "0"
# m/r: regular expression.
test_format "#{m/r:^[0-9]+\$,12345}" "1"
test_format "#{m/r:^[0-9]+\$,12a45}" "0"
# m/ri: regular expression, ignore case.
test_format "#{m/ri:^ab+\$,ABBB}" "1"
test_format "#{m/ri:^ab+\$,ACCC}" "0"
# m/z: fuzzy match, returns a boolean.
test_format "#{m/z:foo,foobar}" "1"
test_format "#{m/z:xyz,foobar}" "0"
# m/p: fuzzy match, returns the matched (0-based) column positions.
test_format "#{m/p:ac,abc}" "0,2"
test_format "#{m/p:xyz,abc}" ""
# Fuzzy match against empty text.
test_format "#{m/p:x,}" ""
test_format "#{m/z:x,}" "0"
# String comparisons.
test_format "#{==:#{@host},myhost}" "1"
test_format "#{==:#{@host},other}" "0"
test_format "#{!=:abc,xyz}" "1"
test_format "#{!=:abc,abc}" "0"
test_format "#{<:3,5}" "1"
test_format "#{<:5,3}" "0"
test_format "#{>:5,3}" "1"
test_format "#{>:3,5}" "0"
test_format "#{<=:5,5}" "1"
test_format "#{<=:6,5}" "0"
test_format "#{>=:5,5}" "1"
test_format "#{>=:4,5}" "0"
# Negation and canonical boolean.
test_format "#{!:0}" "1"
test_format "#{!:1}" "0"
test_format "#{!!:}" "0"
test_format "#{!!:0}" "0"
test_format "#{!!:non-empty}" "1"
# --- Quoting (q) ---------------------------------------------------------
# q: escapes shell special characters with a backslash.
test_format "#{q:@sp}" 'a\ b\$c'
# q/s quotes with POSIX shell single quotes.
test_format "#{q/s:@sp}" "'a b\$c'"
test_format "#{q/s:@sq}" "'a'\\''b'"
test_format "#{q/s:@nl}" "$q_s_nl"
# q/e and q/h escape "#" for the format/style parser by doubling it.
test_format "#{q/e:@hash}" 'a##b'
test_format "#{q/h:@hash}" 'a##b'
# q/a quotes the value as a single shell argument.
test_format "#{q/a:@sp}" '"a b\$c"'
# --- Name existence (N) --------------------------------------------------
# N/w is true if a window with the (expanded) name exists in the session, N/s
# if a session with that name exists. The default (no argument) is /w.
$TMUX rename-window -t main:0 knownwin
test_format "#{N/s:main}" "1"
test_format "#{N/s:nosuchsession}" "0"
test_format "#{N/w:knownwin}" "1" "main:"
test_format "#{N/w:nosuchwindow}" "0" "main:"
test_format "#{N:nosuchwindow}" "0" "main:"
# --- Numeric operations (e) ----------------------------------------------
# Integer operators.
test_format "#{e|+|:2,3}" "5"
test_format "#{e|-|:10,4}" "6"
test_format "#{e|-|:2,5}" "-3"
test_format "#{e|*|:6,7}" "42"
test_format "#{e|/|:20,4}" "5"
# Modulus - both spellings (% must be doubled as it is a strftime specifier).
test_format "#{e|m|:7,3}" "1"
test_format "#{e|%%|:7,3}" "1"
# Numeric comparison operators.
test_format "#{e|==|:5,5}" "1"
test_format "#{e|!=|:5,5}" "0"
test_format "#{e|<|:2,5}" "1"
test_format "#{e|>|:9,2}" "1"
test_format "#{e|<=|:5,5}" "1"
test_format "#{e|>=|:5,5}" "1"
# Floating point with a decimal-place count.
test_format "#{e|*|f|4:5.5,3}" "16.5000"
test_format "#{e|/|f|3:1,3}" "0.333"
test_format "#{e|/|f|2:10,3}" "3.33"
# Default number of decimal places for float is two.
test_format "#{e|*|f:2.5,2}" "5.00"
# Division by zero must not crash the server (result is unspecified).
$TMUX display-message -p "#{e|/|:5,0}" >/dev/null 2>&1
$TMUX display-message -p "#{e|/|f:5,0}" >/dev/null 2>&1
assert_alive "division by zero"
# --- ASCII and repeat ----------------------------------------------------
# a: numeric value to its ASCII character.
test_format "#{a:98}" "b"
test_format "#{a:65}" "A"
# a: out-of-range or non-numeric input yields an empty string.
test_format "#{a:200}" ""
test_format "#{a:notanumber}" ""
# R: repeat first argument second-argument times.
test_format "#{R:a,3}" "aaa"
test_format "#{R:ab,2}" "abab"
# A long repeat exercises output-buffer growth during expansion.
test_format "#{n:#{R:x,300}}" "300"
# --- Width, padding and truncation ---------------------------------------
# =N truncates from the start, =-N from the end.
test_format "#{=5:@s}" "abcde"
test_format "#{=-5:@s}" "fghij"
# = with no width, or a non-numeric width, does not truncate.
test_format "#{=:@s}" "abcdefghij"
test_format "#{=/x:@s}" "abcdefghij"
# A marker is appended/prepended only when trimming actually occurs.
test_format "#{=/5/...:@s}" "abcde..."
test_format "#{=/5/...:@name}" "windo..."
test_format "#{=/20/...:@s}" "abcdefghij"
# Truncation is display-width (UTF-8) aware: a wide (2-column) character is only
# included if it fits entirely within the limit.
test_format "#{=3:@greek}" "αβγ"
test_format "#{=2:@greek}" "αβ"
test_format "#{=2:@cjk}" "中"
test_format "#{=1:@cjk}" ""
# Markers with wide characters: the marker is added when trimming occurs, and a
# limit that splits a wide character drops it entirely.
test_format "#{=/2/x:@cjk}" "中x"
test_format "#{=/1/x:@cjk}" "x"
# p pads to a width: a positive width left-aligns (pads on the right), a
# negative width right-aligns (pads on the left).
test_format "#{p12:@name}" "window-name "
test_format "#{p-12:@name}" " window-name"
# No padding once the value already meets the width.
test_format "#{p3:@name}" "window-name"
# p with no width does nothing.
test_format "#{p:@name}" "window-name"
# Padding is display-width aware: @cjk is 4 columns wide, so p6/p-6 add two
# spaces (not four).
test_format "#{p6:@cjk}" "中文 "
test_format "#{p-6:@cjk}" " 中文"
# n is byte length, w is display width.
test_format "#{n:@s}" "10"
test_format "#{w:@s}" "10"
test_format "#{n:@greek}" "6"
test_format "#{w:@greek}" "3"
test_format "#{n:@cjk}" "6"
test_format "#{w:@cjk}" "4"
# --- basename and dirname ------------------------------------------------
test_format "#{b:@path}" "foo"
test_format "#{d:@path}" "/usr/local/bin"
# --- Time conversion -----------------------------------------------------
# t: converts an integer time to a ctime(3) string.
test_format "#{t:@ts}" "Sun Sep 9 01:46:40 2001"
# t/p: shorter format for times in the past.
test_format "#{t/p:@ts}" "Sep01"
# t/r: relative time depends on the current time, just check it is non-empty.
if [ -z "$($TMUX display-message -p '#{t/r:@ts}')" ]; then
echo "Format test failed for '#{t/r:@ts}': empty result"
exit 1
fi
# t/f: custom strftime format applied to the variable's time. Tested in a
# format_expand context (list-windows -F), where a single strftime specifier is
# applied directly. (In display-message, which additionally expands the format
# through strftime, these would need to be doubled - %%Y etc.) The colon in the
# format is escaped as '#:' because it is otherwise the modifier separator.
test_expand "#{t/f/%Y:@ts}" "2001"
test_expand "#{t/f/%Y-%m-%d:@ts}" "2001-09-09"
test_expand "#{t/f/%H#:%M#:%S:@ts}" "01:46:40"
# An escaped comma in the custom format is unescaped before strftime.
test_expand "#{t/f/%Y#,end:@ts}" "2001,end"
# T: expands its argument and then runs the result through strftime with the
# current time. A value with no strftime specifier is returned unchanged.
test_format "#{T:@ts}" "1000000000"
# t/p (pretty) and t/r (relative) format times by age relative to now, with a
# different branch per age band. Build options a known number of seconds in the
# past and check each yields a non-empty result (the exact text depends on the
# wall clock, so only non-emptiness is asserted).
now=$(date +%s)
for age in 30 300 4000 90000 200000 3000000 40000000; do
$TMUX set -g @age "$((now - age))"
if [ -z "$($TMUX display-message -p '#{t/r:@age}')" ]; then
echo "Empty #{t/r:@age} for age ${age}s"
exit 1
fi
if [ -z "$($TMUX display-message -p '#{t/p:@age}')" ]; then
echo "Empty #{t/p:@age} for age ${age}s"
exit 1
fi
done
# A time in the future has no relative form.
$TMUX set -g @future "$((now + 100000))"
test_format "#{t/r:@future}" ""
# --- Content search (C) --------------------------------------------------
# Use a window running cat so the content is deterministic (no shell prompt).
$TMUX new-session -d -s search -x 80 -y 10 'cat'
sleep 1
$TMUX send-keys -t search: 'Zebra_Marker_42' Enter
sleep 1
# C: returns the (1-based) line number of a match or 0 if not found.
test_format "#{C:Zebra_Marker_42}" "1" "search:"
test_format "#{C:Absent_String_999}" "0" "search:"
test_format "#{C/r:Zebra_.*_42}" "1" "search:"
test_format "#{C/i:zebra_marker_42}" "1" "search:"
$TMUX kill-session -t search 2>/dev/null
# --- Colour (c) ----------------------------------------------------------
# c: converts a colour to its six-digit hexadecimal RGB value.
test_format "#{c:red}" "800000"
test_format "#{c:colour4}" "000080"
test_format "#{c:#7f7f7f}" "7f7f7f"
# c/f and c/b produce the SGR escape sequence for fg/bg respectively.
test_format "#{c/f:red}" "${ESC}[31m"
test_format "#{c/b:red}" "${ESC}[41m"
test_format "#{c/b:colour4}" "${ESC}[48;5;4m"
# "none" gives a reset; an unknown colour gives an empty string.
test_format "#{c/f:none}" "${ESC}[0m"
test_format "#{c:notacolour}" ""
test_format "#{c/f:notacolour}" ""
# --- Nesting and limits --------------------------------------------------
# Modifier chaining: inner b: then outer truncation/padding/length.
test_format "#{=5:#{b:@path}}" "foo"
test_format "#{=2:#{b:@path}}" "fo"
test_format "#{p6:#{b:@path}}" "foo "
test_format "#{n:#{b:@path}}" "3"
# Nested l: literal expanded then truncated.
test_format "#{=5:#{l:abcdefghij}}" "abcde"
# Deeper nesting: basename -> pad to 10 -> truncate to 5.
test_format "#{=5:#{p10:#{b:@path}}}" "foo "
# A substitution applied to a nested basename.
test_format "#{s/o/O/:#{b:@path}}" "fOO"
# Unbounded self-recursion must hit the loop limit rather than crash.
$TMUX set @rec '#{E:@rec}'
$TMUX display-message -p '#{E:@rec}' >/dev/null 2>&1
assert_alive "recursive expansion"
# --- Missing, malformed and limit inputs ---------------------------------
# An undefined variable expands to empty; modifiers on it behave sensibly.
test_format "#{@undefined}" ""
test_format "#{=5:@undefined}" ""
test_format "#{b:@undefined}" ""
test_format "#{n:@undefined}" "0"
# Malformed numeric expressions expand to empty rather than erroring out.
test_format "#{e|+|:notanumber,2}" "" # invalid left operand
test_format "#{e|+|:2,notanumber}" "" # invalid right operand
test_format "#{e|badop|:1,2}" "" # unknown operator
test_format "#{e|+|f|x:1,2}" "" # invalid precision
test_format "#{e|+|:1}" "" # too few operands
test_format "#{e|+|f|2|extra:1,2}" "" # too many arguments (limit is 3)
# Repeat with a non-numeric or zero count yields an empty string.
test_format "#{R:a,notanumber}" ""
test_format "#{R:a,0}" ""
# Comparisons with too few arguments expand to empty.
test_format "#{==:a}" ""
test_format "#{<:a}" ""
# A substitution with fewer than two arguments is a no-op.
test_format "#{s/onlyone:@s}" "abcdefghij"
# A non-numeric width for = or p is treated as no width (no change).
test_format "#{=/x:@s}" "abcdefghij"
test_format "#{p/x:@s}" "abcdefghij"
# The I (client terminal) modifier with no attached client is empty; this also
# exercises its argument parsing (/c termcap, /f feature, default). The
# non-empty terminal cases are covered with a real client in format-variables.sh.
test_format "#{I/c:RGB}" ""
test_format "#{I/f:overline}" ""
test_format "#{I:x}" ""
# --- Escaping inside modifiers -------------------------------------------
# A "," or "#" inside a modifier argument is escaped with "#".
test_format "#{s/#,/-/:#{l:a,b,c}}" "a-b-c" # escaped comma in the pattern
test_format "#{=/3/#,:@s}" "abc," # escaped comma in the marker
# The truncation marker is itself expanded as a format.
test_format "#{=/3/#{l:>}:@s}" "abc>"
# Substitution flags: a third argument of "i" is case-insensitive; an invalid
# regular expression leaves the text unchanged.
test_format "#{s/A/X/i:@s}" "Xbcdefghij"
test_format "#{s/[/X/:@s}" "abcdefghij"
# --- Unicode in modifier arguments ---------------------------------------
# Wide (CJK) and emoji text: matching, substitution, repeat and markers all
# operate on characters, and n/w report bytes/columns.
$TMUX set -g @emoji '😀😀' || exit 1 # 8 bytes, 4 columns
test_format "#{m:*中*,#{@cjk}}" "1"
test_format "#{s/文/X/:@cjk}" "中X"
test_format "#{R:中,3}" "中中中"
test_format "#{=/1/中:@s}" "a中"
test_format "#{w:@emoji}" "4"
test_format "#{n:@emoji}" "8"
test_format "#{=2:@emoji}" "😀"
# --- Server messages (show-messages) -------------------------------------
# show-messages formats each logged message (this exercises the message-time
# formatting path); just check the server survives producing it.
$TMUX show-messages >/dev/null 2>&1
assert_alive "show-messages"
# --- Verbose expansion (logging) -----------------------------------------
# display-message -v turns on format logging, so re-expanding a representative
# set of formats with -v exercises the logging code paths. Only survival is
# checked; the log text itself is not asserted.
for f in \
'#{=3:@s}' \
'#{e|+|:2,3}' \
'#{e|*|f|2:2.5,2}' \
'#{m:*a*,abc}' \
'#{<:3,5}' \
'#{s/a/X/:@s}' \
'#{b:@path}' \
'#{t:@ts}' \
'#{p6:@name}' \
'#{=3:#{b:@path}}'; do
$TMUX display-message -v -p "$f" >/dev/null 2>&1
done
assert_alive "verbose expansion"
# --- Loops and sorting (S, W, P, L) --------------------------------------
#
# These need a fully controlled server so the set of sessions, windows and
# panes (and their order) is known, so start from a clean server. This must be
# the last section as it discards the setup above.
$TMUX kill-server 2>/dev/null
sleep 0.1
# Sessions, created in this order, so session ids (and hence creation order)
# are zeta=$0, alpha=$1, mike=$2.
$TMUX new-session -d -s zeta -x 80 -y 24 || exit 1
$TMUX new-session -d -s alpha || exit 1
$TMUX new-session -d -s mike || exit 1
$TMUX set -g automatic-rename off
# S loops over every session. The default order is by session id (SORT_INDEX),
# /i is the same, /n is by name, and the r suffix reverses.
test_format "#{S:#{session_name} }" "zeta alpha mike "
test_format "#{S/i:#{session_name} }" "zeta alpha mike "
test_format "#{S/n:#{session_name} }" "alpha mike zeta "
test_format "#{S/nr:#{session_name} }" "zeta mike alpha "
test_format "#{S/ir:#{session_name} }" "mike alpha zeta "
# /t sorts by activity time; the exact order is timing-dependent, so just check
# every session is still iterated (this exercises the activity-sort branch).
test_format "#{S/t:x}" "xxx"
# An unrecognised sort letter falls back to the default order; /r on its own
# reverses that default (this covers the fall-through branch).
test_format "#{S/r:#{session_name} }" "mike alpha zeta "
# Windows in session zeta: window 0 renamed charlie, then alpha at 1, bravo at
# 2. The default order is by index (SORT_ORDER), /n is by name, r reverses.
$TMUX rename-window -t zeta:0 charlie
$TMUX new-window -d -t zeta:1 -n alpha
$TMUX new-window -d -t zeta:2 -n bravo
test_format "#{W:#{window_name} }" "charlie alpha bravo " "zeta:"
test_format "#{W/n:#{window_name} }" "alpha bravo charlie " "zeta:"
test_format "#{W/nr:#{window_name} }" "charlie bravo alpha " "zeta:"
test_format "#{W/ir:#{window_index}}" "210" "zeta:"
# /i (by index) and /t (by activity); /i matches the default order here.
test_format "#{W/i:#{window_name} }" "charlie alpha bravo " "zeta:"
test_format "#{W/t:x}" "xxx" "zeta:"
# An unrecognised sort letter falls back to the default order; /r reverses it.
test_format "#{W/r:#{window_name} }" "bravo alpha charlie " "zeta:"
# Panes in window zeta:charlie. Splitting the active (newest) pane each time
# makes pane index match creation order (0,1,2 left to right). The default
# order is by creation (SORT_CREATION), r reverses.
$TMUX split-window -h -t zeta:charlie
$TMUX split-window -h -t zeta:charlie
test_format "#{P:#{pane_index}}" "012" "zeta:charlie"
test_format "#{P/r:#{pane_index}}" "210" "zeta:charlie"
# Pane sort accepts i (pane-list order) and z (z-order). Other sort letters
# fall back to the default creation order; r reverses whichever order is used.
test_format "#{P/i:x}" "xxx" "zeta:charlie"
test_format "#{P/i:#{pane_index}}" "012" "zeta:charlie"
test_format "#{P/z:x}" "xxx" "zeta:charlie"
test_format "#{P/n:x}" "xxx" "zeta:charlie"
test_format "#{P/t:x}" "xxx" "zeta:charlie"
$TMUX new-pane -d -t zeta:charlie -x 20 -y 10 -X 1 -Y 1
test_format "#{P/i:#{pane_index}}" "0123" "zeta:charlie"
test_format "#{P/z:#{pane_index}}" "3012" "zeta:charlie"
test_format "#{P/zr:#{pane_index}}" "0123" "zeta:charlie"
# Verbose expansion of the loops, to exercise their logging paths.
$TMUX display-message -v -p "#{S:#{session_name}}" >/dev/null 2>&1
$TMUX display-message -v -t zeta: -p "#{W:#{window_name}}" >/dev/null 2>&1
$TMUX display-message -v -t zeta:charlie -p "#{P:#{pane_index}}" >/dev/null 2>&1
assert_alive "verbose loop expansion"
# L loops over attached clients. Attach two control-mode clients, each held
# open by a background process keeping a FIFO's write end open.
FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1"
FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2"
rm -f "$FIFO1" "$FIFO2"
mkfifo "$FIFO1" "$FIFO2" || exit 1
# Hold the write ends open so the control clients stay attached.
sleep 30 >"$FIFO1" &
HOLD1=$!
sleep 30 >"$FIFO2" &
HOLD2=$!
$TMUX -C attach -t zeta <"$FIFO1" >/dev/null 2>&1 &
CC1=$!
$TMUX -C attach -t alpha <"$FIFO2" >/dev/null 2>&1 &
CC2=$!
sleep 1
# Two clients attached: L emits one item per client.
test_format "#{L:x}" "xx"
# The client sort orders (default, index, name, activity, reversed) are all
# accepted; assert only the count so the test does not depend on client names or
# timing.
test_format "#{L/i:x}" "xx"
test_format "#{L/n:x}" "xx"
test_format "#{L/t:x}" "xx"
test_format "#{L/nr:x}" "xx"
test_format "#{L/r:x}" "xx"
# Now detach one and confirm the count drops to one.
kill $HOLD2 2>/dev/null
sleep 1
test_format "#{L:x}" "x"
kill $HOLD1 $CC1 $CC2 2>/dev/null
rm -f "$FIFO1" "$FIFO2"
exit 0

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 -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$1"
cleanup
exit 1
}
# click COL ROW
#
# Write an SGR mouse press then release (button 0) at 1-based COL/ROW to the
# outer pane holding the inner client.
click()
{
col="$1"
row="$2"
seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' "$col" "$row" "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
# Inner session with a single pane running cat, so its content is exactly what
# we send it.
$TMUX new-session -d -s cov -x 80 -y 24 'cat' || exit 1
$TMUX set -g mouse on
sleep 1
$TMUX send-keys -t cov:0.0 'alpha beta gamma' Enter
sleep 1
# Record every pane mouse variable when the pane is clicked.
$TMUX bind -n MouseDown1Pane run-shell \
"$TMUX set -g @m 'x=#{mouse_x} y=#{mouse_y} word=#{mouse_word} line=#{mouse_line} pane=#{mouse_pane} hl=[#{mouse_hyperlink}]'"
# Attach a real client inside an outer tmux pane. Clicks all target the first
# row, which lines up with the inner client regardless of the outer status line.
$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t cov" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Click column 3, row 1: over the first word ("alpha") of the first line.
click 3 1
M=$($TMUX show -gv @m 2>/dev/null)
[ -n "$M" ] || fail "Mouse binding did not fire (no @m)."
# mouse_x is 0-based column (SGR column 3 -> x 2); mouse_y is 0-based row 0.
case "$M" in
*"x=2 "*) ;;
*) fail "Unexpected mouse_x in: $M" ;;
esac
case "$M" in
*"y=0 "*) ;;
*) fail "Unexpected mouse_y in: $M" ;;
esac
# mouse_word is the word under the cursor, mouse_line the whole line.
case "$M" in
*"word=alpha "*) ;;
*) fail "Unexpected mouse_word in: $M" ;;
esac
case "$M" in
*"line=alpha beta gamma "*) ;;
*) fail "Unexpected mouse_line in: $M" ;;
esac
# A click in a different column selects a different word.
click 8 1
M=$($TMUX show -gv @m 2>/dev/null)
case "$M" in
*"word=beta "*) ;;
*) fail "Unexpected mouse_word for second click in: $M" ;;
esac
# The same variables have a separate path when the pane is in a mode (the word
# and line come from the mode, not the live grid). A binding in the copy-mode
# key table fires while copy mode is active.
$TMUX bind -T copy-mode MouseDown1Pane run-shell \
"$TMUX set -g @cm 'x=#{mouse_x} word=#{mouse_word} line=#{mouse_line}'"
$TMUX copy-mode -t cov:0.0
sleep 1
click 8 1
CM=$($TMUX show -gv @cm 2>/dev/null)
case "$CM" in
*"word=beta"*) ;;
*) fail "Unexpected copy-mode mouse_word in: $CM" ;;
esac
$TMUX send-keys -t cov:0.0 -X cancel
sleep 1
# Hyperlinks: a new window whose pane emits an OSC 8 hyperlink over the text
# "LINKED". Clicking it reports the target URL via mouse_hyperlink (this drives
# the grid hyperlink lookup). The emitter is written to a small script to keep
# the escape sequence readable.
LINKSH="${TMPDIR:-/tmp}/fmt-mouse-link-$$.sh"
cat >"$LINKSH" <<'EOF'
#!/bin/sh
printf '\033]8;;http://example.com\033\\LINKED\033]8;;\033\\\n'
exec cat
EOF
chmod +x "$LINKSH"
$TMUX neww -t cov: -n link "$LINKSH"
sleep 1
$TMUX select-window -t cov:link
sleep 1
click 3 1
M=$($TMUX show -gv @m 2>/dev/null)
rm -f "$LINKSH"
case "$M" in
*"hl=[http://example.com]"*) ;;
*) fail "Unexpected mouse_hyperlink in: $M" ;;
esac
cleanup
exit 0

View File

@@ -0,0 +1,314 @@
#!/bin/sh
# Exercise format and style rendering in live contexts.
PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export PATH TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
LIMIT=20000
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1" >&2
cleanup
exit 1
}
run_cmd()
{
if command -v timeout >/dev/null 2>&1; then
timeout 10 "$@"
else
"$@"
fi
}
bounded()
{
name=$1
text=$2
n=$(printf '%s' "$text" | wc -c)
[ "$n" -le "$LIMIT" ] || fail "$name too large: $n bytes"
}
tmux_run()
{
name=$1
shift
out=$(
if command -v timeout >/dev/null 2>&1; then
timeout 10 $TMUX "$@" 2>&1
else
$TMUX "$@" 2>&1
fi
)
rc=$?
bounded "$name" "$out"
[ "$rc" -eq 0 ] || fail "$name failed: $out"
printf '%s' "$out"
}
capture()
{
out=$(
if command -v timeout >/dev/null 2>&1; then
timeout 10 $TMUX2 capture-pane -p -t out:0 2>/dev/null
else
$TMUX2 capture-pane -p -t out:0 2>/dev/null
fi
)
rc=$?
bounded "capture-pane" "$out"
[ "$rc" -eq 0 ] || fail "capture-pane failed"
printf '%s\n' "$out"
}
capture_esc()
{
out=$(
if command -v timeout >/dev/null 2>&1; then
timeout 10 $TMUX2 capture-pane -pe -t out:0 \
2>/dev/null
else
$TMUX2 capture-pane -pe -t out:0 2>/dev/null
fi
)
rc=$?
bounded "capture-pane -e" "$out"
[ "$rc" -eq 0 ] || fail "capture-pane -e failed"
printf '%s\n' "$out"
}
assert_alive()
{
tmux_run "server alive ($1)" display-message -p alive >/dev/null
}
wait_for()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
if capture | grep -F "$marker" >/dev/null 2>&1; then
sleep 0.1
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for $marker"
}
render_status_options()
{
label=$1
fmt=$2
tmux_run "$label status-left" \
set-option -g status-left "SL$label: $fmt" >/dev/null
tmux_run "$label clear status-right" \
set-option -g status-right '' >/dev/null
tmux_run "$label status-format left" \
set-option -g status-format[0] '#{E:status-left}' >/dev/null
tmux_run "$label refresh status-left" refresh-client -S >/dev/null
wait_for "SL$label:"
tmux_run "$label clear status-left" \
set-option -g status-left '' >/dev/null
tmux_run "$label status-right" \
set-option -g status-right "SR$label: $fmt" >/dev/null
tmux_run "$label status-format right" \
set-option -g status-format[0] \
'#[align=right]#{E:status-right}' >/dev/null
tmux_run "$label refresh status-right" refresh-client -S >/dev/null
wait_for "SR$label:"
assert_alive "$label status-left/right"
}
render_status_format()
{
label=$1
fmt=$2
tmux_run "$label status-format" \
set-option -g status-format[0] "SF$label: $fmt" >/dev/null
tmux_run "$label refresh status-format" refresh-client -S >/dev/null
wait_for "SF$label:"
assert_alive "$label status-format"
}
render_message()
{
label=$1
fmt=$2
tmux_run "$label display-message" \
display-message -t fmt:0 -d 1000 "DM$label: $fmt" >/dev/null
wait_for "DM$label:"
assert_alive "$label display-message"
}
render_choose_tree()
{
label=$1
fmt=$2
tmux_run "$label choose-tree" \
choose-tree -t fmt:0 -F "CT$label: $fmt" >/dev/null
wait_for "CT$label:"
tmux_run "$label quit choose-tree" send-keys -t fmt:0 q >/dev/null
assert_alive "$label choose-tree"
}
render_customize()
{
label=$1
fmt=$2
tmux_run "$label customize-mode" \
customize-mode -t fmt:0 -F "CM$label: $fmt" >/dev/null
sleep 0.5
out=$(capture)
bounded "$label customize-mode capture" "$out"
tmux_run "$label quit customize-mode" send-keys -t fmt:0 q >/dev/null
assert_alive "$label customize-mode"
}
render_list_output()
{
label=$1
fmt=$2
tmux_run "$label bind-key" \
bind-key -T root F12 display-message "LK$label: $fmt" >/dev/null
out=$(tmux_run "$label list-keys" list-keys -T root F12)
bounded "$label list-keys" "$out"
printf '%s' "$out" | grep -F "LK$label:" >/dev/null 2>&1 ||
fail "$label list-keys missing rendered command"
tmux_run "$label set show option" \
set-option -g "@format_render_$label" "SO$label: $fmt" >/dev/null
out=$(tmux_run "$label show-options" show-options -g "@format_render_$label")
bounded "$label show-options" "$out"
printf '%s' "$out" | grep -F "SO$label:" >/dev/null 2>&1 ||
fail "$label show-options missing option"
assert_alive "$label list output"
}
run_corpus()
{
label=$1
fmt=$2
render_status_options "$label" "$fmt"
render_status_format "$label" "$fmt"
render_message "$label" "$fmt"
render_choose_tree "$label" "$fmt"
render_customize "$label" "$fmt"
render_list_output "$label" "$fmt"
}
check_sgr_sanity()
{
esc=$(printf '\033')
tmux_run "set message style fill" \
set-option -g message-style \
"fg=#080808,bg=#ffff00,fill=#ffff00,bold" >/dev/null
tmux_run "show filled message" \
display-message -t fmt:0 -d 1000 \
'#[bg=blue,italics] hello, #[fg=#080808,bg=default] world!' \
>/dev/null
wait_for "hello, world!"
out=$(capture_esc)
printf '%s' "$out" | grep -F 'world!' >/dev/null 2>&1 ||
fail "message text missing from SGR capture"
printf '%s' "$out" | grep "$esc" >/dev/null 2>&1 ||
fail "SGR capture has no escapes"
world_sgr=$(
printf '%s\n' "$out" |
awk -v esc="$esc" '
/world!/ {
i = index($0, "world!")
pre = substr($0, 1, i - 1)
n = split(pre, parts, esc "\\[")
print parts[n]
exit
}'
)
case "$world_sgr" in
*'48;'*) ;;
*) fail "world segment missing explicit background SGR: $world_sgr" ;;
esac
case "$world_sgr" in
*'49'*)
fail "world segment used default background instead of message fill: $world_sgr"
;;
esac
assert_alive "message SGR sanity"
}
wide_fmt()
{
printf 'wide: \0316\0225\0316\0273\0316\0273\0316\0267\0316\0275\0316\0271\0316\0272\0316\0254 \0344\0270\0255\0346\0226\0207 #{=12:\0344\0270\0255\0346\0226\0207abc}'
}
long_style_fmt()
{
printf '%s' '#[fg=colour1,bg=colour2,us=colour3,acs,bright,dim,underscore,blink,reverse,hidden,italics,strikethrough,double-underscore,curly-underscore,dotted-underscore,dashed-underscore,overline,range=user|aaaaaaaaaaaaaaaa,align=absolute-centre,list=on,fill=colour200,width=4294967295,pad=4294967295]OVERLONG-STYLE#[default]'
}
trap cleanup 0 1 15
cleanup
tmux_run "new inner session" \
new-session -d -s fmt -x 100 -y 30 "exec sleep 1000" >/dev/null
tmux_run "new inner window" \
new-window -t fmt -n second "exec sleep 1000" >/dev/null
tmux_run "select first window" select-window -t fmt:0 >/dev/null
tmux_run "status interval" set-option -g status-interval 1 >/dev/null
tmux_run "base status" set-option -g status on >/dev/null
tmux_run "nested option" \
set-option -g @nested 'NESTED-#{session_name}-#{window_index}' >/dev/null
run_cmd $TMUX2 new-session -d -s out -x 100 -y 30 "$TMUX attach -t fmt" \
>/dev/null 2>&1 || fail "failed to start outer client"
i=0
while [ "$i" -lt 50 ]; do
c=$(tmux_run "wait clients" list-clients -F x | grep -c x)
[ "$c" -eq 1 ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "inner client did not attach"
run_corpus A 'plain #{session_name}:#{window_index} #{@nested}'
run_corpus B "$(wide_fmt)"
run_corpus C '#[fg=colour10,bg=colour17,bold,italics]styled #{pane_current_command}#[default]'
run_corpus D '#[push-default]#[fg=red,bg=blue]push #{?pane_active,active,inactive}#[pop-default] after'
run_corpus E "$(long_style_fmt)"
check_sgr_sanity
cleanup
exit 0

View File

@@ -6,7 +6,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
# test_format $format $expected_result
test_format()

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

@@ -0,0 +1,415 @@
#!/bin/sh
# Tests that every format variable listed in tmux(1) (the format_table in
# format.c) can be expanded without crashing the server, and checks the value
# of a stable subset.
#
# The main point is coverage and crash-safety: each variable is expanded in a
# rich context - a real attached client (from a nested tmux), a control-mode
# client, a grouped session, two windows with a bell alert, a window with two
# panes running cat, a paste buffer and options - so the per-variable callbacks
# actually run. format-modifiers.sh covers the modifier machinery; this covers
# the variable callbacks.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
# A second server on its own socket provides a real terminal (an inner client
# attached inside one of its panes) so client terminal variables are populated.
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
# Every variable name in format_table[]. Kept as a plain word list so it can be
# iterated with normal shell word splitting.
NAMES="
active_window_index
alternate_on
alternate_saved_x
alternate_saved_y
bracket_paste_flag
buffer_created
buffer_full
buffer_mode_format
buffer_name
buffer_sample
buffer_size
client_activity
client_cell_height
client_cell_width
client_colours
client_control_mode
client_created
client_discarded
client_flags
client_height
client_key_table
client_last_session
client_mode_format
client_name
client_pid
client_prefix
client_readonly
client_session
client_termfeatures
client_termname
client_termtype
client_theme
client_tty
client_uid
client_user
client_utf8
client_width
client_written
config_files
cursor_blinking
cursor_character
cursor_colour
cursor_flag
cursor_shape
cursor_very_visible
cursor_x
cursor_y
history_all_bytes
history_bytes
history_limit
history_size
host
host_short
insert_flag
keypad_cursor_flag
keypad_flag
last_window_index
mouse_all_flag
mouse_any_flag
mouse_button_flag
mouse_hyperlink
mouse_line
mouse_pane
mouse_sgr_flag
mouse_standard_flag
mouse_status_line
mouse_status_range
mouse_utf8_flag
mouse_word
mouse_x
mouse_y
next_session_id
origin_flag
pane_active
pane_at_bottom
pane_at_left
pane_at_right
pane_at_top
pane_bg
pane_bottom
pane_current_command
pane_current_path
pane_dead
pane_dead_signal
pane_dead_status
pane_dead_time
pane_fg
pane_flags
pane_floating_flag
pane_format
pane_height
pane_id
pane_in_mode
pane_index
pane_input_off
pane_key_mode
pane_last
pane_left
pane_marked
pane_marked_set
pane_mode
pane_path
pane_pb_progress
pane_pb_state
pane_pid
pane_pipe
pane_pipe_pid
pane_right
pane_search_string
pane_start_command
pane_start_command_list
pane_start_path
pane_synchronized
pane_tabs
pane_title
pane_top
pane_tty
pane_unseen_changes
pane_width
pane_x
pane_y
pane_z
pane_zoomed_flag
pid
scroll_region_lower
scroll_region_upper
server_sessions
session_active
session_activity
session_activity_flag
session_alert
session_alerts
session_attached
session_attached_list
session_bell_flag
session_created
session_format
session_group
session_group_attached
session_group_attached_list
session_group_list
session_group_many_attached
session_group_size
session_grouped
session_id
session_last_attached
session_many_attached
session_marked
session_name
session_path
session_silence_flag
session_stack
session_windows
sixel_support
socket_path
start_time
synchronized_output_flag
tree_mode_format
uid
user
version
window_active
window_active_clients
window_active_clients_list
window_active_sessions
window_active_sessions_list
window_activity
window_activity_flag
window_bell_flag
window_bigger
window_cell_height
window_cell_width
window_end_flag
window_flags
window_format
window_height
window_id
window_index
window_last_flag
window_layout
window_linked
window_linked_sessions
window_linked_sessions_list
window_marked_flag
window_name
window_offset_x
window_offset_y
window_panes
window_raw_flags
window_silence_flag
window_stack_index
window_start_flag
window_visible_layout
window_width
window_zoomed_flag
wrap_flag
"
# test_var $name $expected [$extra_args...]
#
# Expand a single #{name} and compare against $expected. Any extra arguments
# are passed straight to display-message (e.g. -c or -t).
test_var()
{
name="$1"
exp="$2"
shift 2
out=$($TMUX display-message "$@" -p "#{$name}")
if [ "$out" != "$exp" ]; then
echo "Variable test failed for '#{$name}'."
echo "Expected: '$exp'"
echo "But got '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server did not survive: $1"
exit 1
fi
}
FIFO="${TMPDIR:-/tmp}/fmt-vars-$$"
HOLD=""
CC=""
cleanup()
{
[ -n "$HOLD" ] && kill $HOLD 2>/dev/null
[ -n "$CC" ] && kill $CC 2>/dev/null
rm -f "$FIFO"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
# A session "cov" with a window "win0" holding two panes running cat, plus a
# second window, an option and a paste buffer.
$TMUX new-session -d -s cov -x 80 -y 24 -n win0 'cat' || exit 1
$TMUX set -g automatic-rename off
$TMUX set -g monitor-bell on
$TMUX set -g monitor-activity on
$TMUX split-window -t cov:win0 -d 'cat' || exit 1
$TMUX new-window -d -t cov:1 -n win1 'cat' || exit 1
$TMUX set -g @opt 'optionvalue' || exit 1
$TMUX set-buffer -b buf0 'somebuffer' || exit 1
# A second session grouped with cov, so the session_group_* variables have real
# data to report.
$TMUX new-session -d -s cov2 -t cov || exit 1
sleep 1
$TMUX send-keys -t cov:win0.0 'some pane content' Enter
# Ring the bell in the non-current window so a bell alert is raised on the
# session (this populates session_alert/session_alerts and window_bell_flag).
$TMUX send-keys -t cov:win1.0 C-g
sleep 1
# Attach a control-mode client, held open by a background process keeping the
# write end of a FIFO open, so client_* variables have a client to read.
rm -f "$FIFO"
mkfifo "$FIFO" || exit 1
sleep 30 >"$FIFO" &
HOLD=$!
$TMUX -C attach -t cov <"$FIFO" >/dev/null 2>&1 &
CC=$!
# Attach a real client too: an inner tmux running inside a pane of the second
# server gets a genuine terminal, which populates the terminal-dependent client
# variables (client_termname, cursor_shape, the I modifier, ...).
$TMUX2 new-session -d -x 90 -y 30 "$TMUX attach -t cov" || exit 1
sleep 1
# The real (terminal) client, identified by not being in control mode.
RC=$($TMUX list-clients -F '#{client_control_mode} #{client_name}' |
awk '$1==0 { print $2; exit }')
# The control client.
CLIENT=$($TMUX list-clients -F '#{client_control_mode} #{client_name}' |
awk '$1==1 { print $2; exit }')
[ -n "$RC" ] || fail "No real client attached."
[ -n "$CLIENT" ] || fail "No control client attached."
# Expand every variable at once, with the real terminal client and a target
# pane in context, and confirm the server survives. This runs every callback.
FMT=""
for n in $NAMES; do
FMT="$FMT#{$n}"
done
$TMUX display-message -c "$RC" -t cov:win0.0 -p "$FMT" >/dev/null 2>&1
assert_alive "expanding all variables together"
# Expand each variable on its own too, so a crash can be pinned to one name.
for n in $NAMES; do
$TMUX display-message -c "$RC" -t cov:win0.0 -p "#{$n}" >/dev/null 2>&1
assert_alive "expanding #{$n}"
done
# Deterministic checks on stable variables (targeting pane 0 of window 0).
TGT="cov:win0.0"
test_var session_name "cov" -t "$TGT"
test_var window_name "win0" -t "$TGT"
test_var window_index "0" -t "$TGT"
test_var window_panes "2" -t "$TGT"
test_var session_windows "2" -t "$TGT"
test_var pane_index "0" -t "$TGT"
test_var pane_in_mode "0" -t "$TGT"
test_var pane_at_top "1" -t "$TGT"
test_var pane_at_left "1" -t "$TGT"
test_var last_window_index "1" -t "$TGT"
test_var pid "$($TMUX display-message -p '#{pid}')" -t "$TGT"
test_var pane_start_command "cat" -t "$TGT"
test_var pane_start_command_list "'cat'" -t "$TGT"
# The grouped session is reported as such.
test_var session_grouped "1" -t "cov:"
test_var session_group_size "2" -t "cov:"
# list-buffers -F formats each paste buffer (this fills in the paste-buffer
# format defaults).
if [ "$($TMUX list-buffers -F '#{buffer_name}=#{buffer_sample}')" != \
"buf0=somebuffer" ]; then
fail "Unexpected list-buffers format output."
fi
# Version reported by the variable matches tmux -V.
VER=$($TMUX -V | sed 's/^tmux //')
test_var version "$VER" -t "$TGT"
# Client variables from each kind of client.
test_var client_name "$CLIENT" -c "$CLIENT"
test_var client_control_mode "1" -c "$CLIENT"
test_var client_control_mode "0" -c "$RC"
test_var socket_path "$($TMUX display-message -p '#{socket_path}')" -c "$CLIENT"
# The real client has a terminal, so termcap/feature/environ queries work.
test_var "I/e:TERM" "$($TMUX display-message -c "$RC" -p '#{client_termname}')" \
-c "$RC"
# Termcap and feature queries against a real terminal return a boolean.
case "$($TMUX display-message -c "$RC" -p '#{I/c:colors}')" in
0|1) ;;
*) fail "Unexpected #{I/c:colors} for real client." ;;
esac
case "$($TMUX display-message -c "$RC" -p '#{I/f:256}')" in
0|1) ;;
*) fail "Unexpected #{I/f:256} for real client." ;;
esac
# Time variables through the pretty and relative modifiers: start_time is the
# recent server start, exercising the "last 24 hours" and "just now" paths.
[ -n "$($TMUX display-message -p '#{t/p:start_time}')" ] ||
fail "Empty #{t/p:start_time}."
[ -n "$($TMUX display-message -p '#{t/r:start_time}')" ] ||
fail "Empty #{t/r:start_time}."
# pane_start_command_list quotes each argv word for sh, so evaluating the
# expansion reconstructs the original argv exactly - including words with
# quotes, spaces, newlines and empty words. sh -c ignores the extra words
# (they become positional parameters), so the pane stays alive. -u stops
# the server sanitizing the newline away when printing to a non-UTF-8
# client (the test runs without a locale in the environment).
$TMUX new-session -d -s quot -x 80 -y 24 -- sh -c 'sleep 100' arg0 \
"it's a 'test'" 'two words' '' 'new
line' || fail "Failed to create quoting test session."
LIST=$($TMUX -u display-message -t 'quot:0.0' -p '#{pane_start_command_list}')
eval "set -- $LIST"
GOT=$(for a; do printf '<%s>' "$a"; done)
EXP=$(for a in sh -c 'sleep 100' arg0 "it's a 'test'" 'two words' '' 'new
line'; do printf '<%s>' "$a"; done)
if [ "$GOT" != "$EXP" ]; then
echo "pane_start_command_list did not round-trip."
echo "Expected: $EXP"
echo "But got: $GOT"
fail "Expansion was: $LIST"
fi
# A pane started with the default shell has an empty start command.
$TMUX new-window -d -t 'quot:' || fail "Failed to create shell window."
test_var pane_start_command_list "" -t "quot:1.0"
cleanup
exit 0

View File

@@ -7,7 +7,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX -f/dev/null has -tfoo </dev/null 2>/dev/null && exit 1

124
regress/hooks-after.sh Normal file
View File

@@ -0,0 +1,124 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX_TMPDIR="$OUT"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
wait_for()
{
option=$1
expected=$2
i=0
while [ $i -lt 30 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $option to be '$expected' but got '$value'"
}
assert_unchanged()
{
option=$1
expected=$2
i=0
while [ $i -lt 10 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] || \
fail "expected $option to remain '$expected' but got '$value'"
i=$((i + 1))
sleep 0.2
done
}
$TMUX new -d -s one || fail "new-session one failed"
$TMUX new -d -s two || fail "new-session two failed"
# An after hook fires with the command arguments as formats.
$TMUX set -g @after 0 || fail "set @after failed"
$TMUX set-hook -g after-rename-window \
'set -gF @after "#{hook}|#{hook_argument_0}|#{hook_flag_t}"' ||
fail "set-hook -g after-rename-window failed"
$TMUX rename-window -t one:0 first || fail "rename-window first failed"
wait_for @after 'after-rename-window|first|one:0'
# An appended hook command runs after the first.
$TMUX set -g @after2 0 || fail "set @after2 failed"
$TMUX set-hook -ga after-rename-window \
'set -gF @after2 "#{@after}+2"' ||
fail "set-hook -ga after-rename-window failed"
$TMUX rename-window -t one:0 second || fail "rename-window second failed"
wait_for @after 'after-rename-window|second|one:0'
wait_for @after2 'after-rename-window|second|one:0+2'
$TMUX set-hook -gu after-rename-window || fail "set-hook -gu failed"
# A session after hook only fires for commands targeting that session and
# the hook commands run with the command target as current state.
$TMUX set -g @safter 0 || fail "set @safter failed"
$TMUX set-hook -t two after-rename-window \
'set -gF @safter "#{hook}:#{session_name}"' ||
fail "set-hook -t two after-rename-window failed"
$TMUX rename-window -t one:0 third || fail "rename-window third failed"
assert_unchanged @safter 0
$TMUX rename-window -t two:0 fourth || fail "rename-window fourth failed"
wait_for @safter 'after-rename-window:two'
$TMUX set-hook -u -t two after-rename-window ||
fail "set-hook -u -t two failed"
# The command-error hook fires when a command fails.
$TMUX set -g @error 0 || fail "set @error failed"
$TMUX set-hook -g command-error 'set -gF @error "#{hook}"' ||
fail "set-hook -g command-error failed"
if $TMUX rename-window -t nosuchsession:0 x 2>/dev/null; then
fail "rename-window to missing session succeeded"
fi
wait_for @error 'command-error'
$TMUX set-hook -gu command-error || fail "set-hook -gu command-error failed"
# Commands run from a hook do not fire their own after hooks.
$TMUX set -g @copy 0 || fail "set @copy failed"
$TMUX set-hook -g after-copy-mode 'set -gF @copy "#{hook}"' ||
fail "set-hook -g after-copy-mode failed"
$TMUX copy-mode -t one:0 || fail "copy-mode failed"
wait_for @copy 'after-copy-mode'
$TMUX send-keys -t one:0 -X cancel || fail "cancel failed"
$TMUX set -g @copy 0 || fail "reset @copy failed"
$TMUX set -g @ran 0 || fail "set @ran failed"
$TMUX set-hook -g after-rename-window 'copy-mode -t one:0' ||
fail "set-hook -g after-rename-window nested failed"
$TMUX set-hook -ga after-rename-window 'set -g @ran 1' ||
fail "set-hook -ga after-rename-window nested failed"
$TMUX rename-window -t one:0 fifth || fail "rename-window fifth failed"
wait_for @ran 1
mode=$($TMUX display -pt one:0 '#{pane_in_mode}') ||
fail "display pane_in_mode failed"
[ "$mode" = 1 ] || fail "hook did not enter copy mode"
assert_unchanged @copy 0
exit 0

162
regress/hooks-lifecycle.sh Normal file
View File

@@ -0,0 +1,162 @@
#!/bin/sh
# End-of-life hooks: pane-exited, pane-died, window-unlinked and
# session-closed when the pane, window or session they refer to is being
# or has been destroyed. Each hook appends to @log so the order hooks fire
# in is checked as well as the hook formats for the dead objects.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX_TMPDIR="$OUT"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
wait_for()
{
option=$1
expected=$2
i=0
while [ $i -lt 30 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $option to be '$expected' but got '$value'"
}
assert_unchanged()
{
option=$1
expected=$2
i=0
while [ $i -lt 10 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] || \
fail "expected $option to remain '$expected' but got '$value'"
i=$((i + 1))
sleep 0.2
done
}
$TMUX new -d -s main || fail "new-session main failed"
$TMUX set -g @log '' || fail "set @log failed"
$TMUX set-hook -g pane-exited \
'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' ||
fail "set-hook pane-exited failed"
$TMUX set-hook -g window-unlinked \
'set -gF @log "#{@log}|window-unlinked:#{hook_session_name}:#{hook_window_name}"' ||
fail "set-hook window-unlinked failed"
$TMUX set-hook -g session-closed \
'set -gF @log "#{@log}|session-closed:#{hook_session_name}"' ||
fail "set-hook session-closed failed"
# The only pane of the only window of a session exits: pane-exited, then
# window-unlinked, then session-closed, each seeing the dead object in the
# hook formats.
pane=$($TMUX new -d -s doomed -n dwin -P -F '#{pane_id}' 'true') ||
fail "new-session doomed failed"
wait_for @log \
"|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed"
assert_unchanged @log \
"|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed"
# The dead pane, window and session cannot be used as targets but the
# server survives.
if $TMUX select-pane -t "$pane" 2>/dev/null; then
fail "dead pane still a valid target"
fi
if $TMUX list-windows -t doomed >/dev/null 2>&1; then
fail "dead session still a valid target"
fi
$TMUX list-panes -sat main >/dev/null || fail "list-panes failed"
$TMUX has -t main || fail "server died after pane exit chain"
# kill-window on the last window: window-unlinked then session-closed but
# no pane-exited for the panes in the killed window.
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX new -d -s doomed2 -n dwin2 || fail "new-session doomed2 failed"
$TMUX splitw -d -t doomed2:0 || fail "split-window doomed2 failed"
$TMUX kill-window -t doomed2:0 || fail "kill-window failed"
wait_for @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2'
assert_unchanged @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2'
$TMUX has -t main || fail "server died after kill-window chain"
# kill-session: session-closed fires first, then window-unlinked for its
# windows. A window linked into another session survives.
$TMUX new -d -s shareA -n shared || fail "new-session shareA failed"
$TMUX new -d -s shareB -n bwin || fail "new-session shareB failed"
$TMUX link-window -s shareA:shared -t shareB:7 || fail "link-window failed"
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX kill-session -t shareA || fail "kill-session shareA failed"
wait_for @log '|session-closed:shareA|window-unlinked:shareA:shared'
name=$($TMUX display -pt shareB:7 '#{window_name}') ||
fail "shared window did not survive"
[ "$name" = shared ] || fail "expected window shared but got $name"
# Killing the surviving session destroys the shared window for real while
# the session is being destroyed.
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX kill-window -t shareB:0 || fail "kill-window bwin failed"
wait_for @log '|window-unlinked:shareB:bwin'
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX kill-session -t shareB || fail "kill-session shareB failed"
wait_for @log '|session-closed:shareB|window-unlinked:shareB:shared'
$TMUX has -t main || fail "server died after kill-session chain"
# A pane-died hook can kill its own dead pane (the hook runs with the dead
# pane as current target). The kill-pane runs without hooks so pane-exited
# does not fire.
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX new -d -s roe -n rwin || fail "new-session roe failed"
$TMUX set -wt roe:0 remain-on-exit on || fail "set remain-on-exit failed"
$TMUX set-hook -g pane-died \
'set -gF @log "#{@log}|pane-died:#{hook_pane}" ; kill-pane' ||
fail "set-hook pane-died failed"
pane=$($TMUX splitw -d -t roe:0 -P -F '#{pane_id}' 'true') ||
fail "split-window roe failed"
wait_for @log "|pane-died:$pane"
assert_unchanged @log "|pane-died:$pane"
if $TMUX select-pane -t "$pane" 2>/dev/null; then
fail "pane-died hook did not kill its pane"
fi
$TMUX list-panes -t roe:0 >/dev/null || fail "surviving pane broken"
$TMUX has -t roe || fail "session roe died"
$TMUX set-hook -gu pane-died || fail "unset pane-died failed"
$TMUX kill-session -t roe || fail "kill-session roe failed"
# Killing the last session with end-of-life hooks still set: the server
# runs the hooks and exits cleanly.
$TMUX kill-session -t main || fail "kill-session main failed"
i=0
while $TMUX has 2>/dev/null; do
i=$((i + 1))
[ $i -lt 30 ] || fail "server still running after last session killed"
sleep 0.2
done
exit 0

199
regress/hooks-notify.sh Normal file
View File

@@ -0,0 +1,199 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX_TMPDIR="$OUT"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
wait_for()
{
option=$1
expected=$2
i=0
while [ $i -lt 30 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $option to be '$expected' but got '$value'"
}
$TMUX new -d -s main || fail "new-session main failed"
# session-created, session-renamed, session-closed.
$TMUX set -g @s 0 || fail "set @s failed"
$TMUX set-hook -g session-created \
'set -gF @s "#{hook}:#{hook_session_name}"' ||
fail "set-hook session-created failed"
$TMUX new -d -s tmp || fail "new-session tmp failed"
wait_for @s 'session-created:tmp'
$TMUX set-hook -gu session-created || fail "unset session-created failed"
$TMUX set-hook -g session-renamed \
'set -gF @s "#{hook}:#{hook_session_name}"' ||
fail "set-hook session-renamed failed"
$TMUX rename-session -t tmp tmp2 || fail "rename-session failed"
wait_for @s 'session-renamed:tmp2'
$TMUX set-hook -gu session-renamed || fail "unset session-renamed failed"
$TMUX set-hook -g session-closed \
'set -gF @s "#{hook}:#{hook_session_name}"' ||
fail "set-hook session-closed failed"
$TMUX kill-session -t tmp2 || fail "kill-session failed"
wait_for @s 'session-closed:tmp2'
$TMUX set-hook -gu session-closed || fail "unset session-closed failed"
# window-linked and window-unlinked from new-window and kill-window.
$TMUX set -g @w 0 || fail "set @w failed"
$TMUX set-hook -g window-linked \
'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' ||
fail "set-hook window-linked failed"
$TMUX set-hook -g window-unlinked \
'set -gF @w "#{hook}:#{hook_session_name}:#{hook_window_name}"' ||
fail "set-hook window-unlinked failed"
$TMUX neww -d -t main: -n mywin || fail "new-window failed"
wait_for @w 'window-linked:main:mywin'
# window-linked and window-unlinked from link-window and unlink-window.
$TMUX new -d -s other || fail "new-session other failed"
$TMUX set -g @w 0 || fail "reset @w failed"
$TMUX link-window -s main:mywin -t other:5 || fail "link-window failed"
wait_for @w 'window-linked:other:mywin'
$TMUX set -g @w 0 || fail "reset @w failed"
$TMUX unlink-window -t other:5 || fail "unlink-window failed"
wait_for @w 'window-unlinked:other:mywin'
$TMUX set -g @w 0 || fail "reset @w failed"
$TMUX kill-window -t main:mywin || fail "kill-window failed"
wait_for @w 'window-unlinked:main:mywin'
$TMUX set-hook -gu window-linked || fail "unset window-linked failed"
$TMUX set-hook -gu window-unlinked || fail "unset window-unlinked failed"
# window-renamed sees the window id.
window=$($TMUX display -pt main:0 '#{window_id}') ||
fail "display window_id failed"
$TMUX set -g @r 0 || fail "set @r failed"
$TMUX set-hook -g window-renamed \
'set -gF @r "#{hook}:#{hook_window}:#{hook_window_name}"' ||
fail "set-hook window-renamed failed"
$TMUX rename-window -t main:0 renamed || fail "rename-window failed"
wait_for @r "window-renamed:$window:renamed"
$TMUX set-hook -gu window-renamed || fail "unset window-renamed failed"
# window-layout-changed from split-window.
$TMUX set -g @l 0 || fail "set @l failed"
$TMUX set-hook -g window-layout-changed \
'set -gF @l "#{hook}:#{hook_window}"' ||
fail "set-hook window-layout-changed failed"
$TMUX splitw -d -t main:0 || fail "split-window failed"
wait_for @l "window-layout-changed:$window"
$TMUX set-hook -gu window-layout-changed ||
fail "unset window-layout-changed failed"
# window-pane-changed from select-pane.
$TMUX set -g @p 0 || fail "set @p failed"
$TMUX set-hook -g window-pane-changed \
'set -gF @p "#{hook}:#{hook_window}"' ||
fail "set-hook window-pane-changed failed"
$TMUX selectp -t main:0.1 || fail "select-pane failed"
wait_for @p "window-pane-changed:$window"
$TMUX set-hook -gu window-pane-changed ||
fail "unset window-pane-changed failed"
# session-window-changed from select-window.
$TMUX neww -d -t main: -n w2 || fail "new-window w2 failed"
$TMUX set -g @c 0 || fail "set @c failed"
$TMUX set-hook -g session-window-changed \
'set -gF @c "#{hook}:#{hook_session_name}"' ||
fail "set-hook session-window-changed failed"
$TMUX selectw -t main:w2 || fail "select-window failed"
wait_for @c 'session-window-changed:main'
$TMUX set-hook -gu session-window-changed ||
fail "unset session-window-changed failed"
# pane-mode-changed from entering and leaving copy mode.
$TMUX set -g @m 0 || fail "set @m failed"
pane=$($TMUX display -pt main:0.0 '#{pane_id}') ||
fail "display pane_id failed"
$TMUX set-hook -g pane-mode-changed \
'set -gF @m "#{hook}:#{hook_pane}:#{pane_in_mode}"' ||
fail "set-hook pane-mode-changed failed"
$TMUX copy-mode -t main:0.0 || fail "copy-mode failed"
wait_for @m "pane-mode-changed:$pane:1"
$TMUX send-keys -t main:0.0 -X cancel || fail "cancel failed"
wait_for @m "pane-mode-changed:$pane:0"
$TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed"
# pane-exited when a pane's command exits with remain-on-exit off.
$TMUX set -g @x 0 || fail "set @x failed"
$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' ||
fail "set-hook pane-exited failed"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') ||
fail "split-window true failed"
wait_for @x "pane-exited:$pane"
$TMUX set-hook -gu pane-exited || fail "unset pane-exited failed"
# pane-died when a pane's command exits with remain-on-exit on.
$TMUX set -g remain-on-exit on || fail "set remain-on-exit failed"
$TMUX set -g @d 0 || fail "set @d failed"
$TMUX set-hook -g pane-died 'set -gF @d "#{hook}:#{hook_pane}"' ||
fail "set-hook pane-died failed"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') ||
fail "split-window remain-on-exit failed"
wait_for @d "pane-died:$pane"
$TMUX set-hook -gu pane-died || fail "unset pane-died failed"
$TMUX killp -t "$pane" || fail "kill-pane failed"
$TMUX set -g remain-on-exit off || fail "reset remain-on-exit failed"
# pane-title-changed when a pane sets its title.
$TMUX set -g @t 0 || fail "set @t failed"
$TMUX set-hook -g pane-title-changed \
'set -gF @t "#{hook}:#{hook_pane}:#{pane_title}"' ||
fail "set-hook pane-title-changed failed"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \
'printf "\033]2;mytitle\007"; sleep 30') ||
fail "split-window title failed"
wait_for @t "pane-title-changed:$pane:mytitle"
$TMUX set-hook -gu pane-title-changed ||
fail "unset pane-title-changed failed"
$TMUX killp -t "$pane" || fail "kill-pane title failed"
# client-attached and client-detached using a control client.
$TMUX set -g @a 0 || fail "set @a failed"
$TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' ||
fail "set-hook client-attached failed"
$TMUX set-hook -g client-detached 'set -gF @a "#{hook}"' ||
fail "set-hook client-detached failed"
mkfifo "$OUT/fifo" || fail "mkfifo failed"
$TMUX -C attach -t main <"$OUT/fifo" >"$OUT/control.out" 2>&1 &
exec 3>"$OUT/fifo"
wait_for @a 'client-attached'
exec 3>&-
wait_for @a 'client-detached'
$TMUX set-hook -gu client-attached || fail "unset client-attached failed"
$TMUX set-hook -gu client-detached || fail "unset client-detached failed"
exit 0

155
regress/hooks.sh Normal file
View File

@@ -0,0 +1,155 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX_TMPDIR="$OUT"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
wait_for()
{
option=$1
expected=$2
i=0
while [ $i -lt 30 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $option to be '$expected' but got '$value'"
}
assert_unchanged()
{
option=$1
expected=$2
i=0
while [ $i -lt 10 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] || \
fail "expected $option to remain '$expected' but got '$value'"
i=$((i + 1))
sleep 0.2
done
}
$TMUX new -d -s one || fail "new-session one failed"
# A global hook fires and sees the hook formats.
$TMUX set -g @created 0 || fail "set @created failed"
$TMUX set-hook -g session-created \
'set -gF @created "#{hook}:#{hook_session_name}"' ||
fail "set-hook -g session-created failed"
$TMUX new -d -s two || fail "new-session two failed"
wait_for @created 'session-created:two'
# Hooks are arrays: an appended command runs after the first.
$TMUX set -g @second 0 || fail "set @second failed"
$TMUX set-hook -ga session-created \
'set -gF @second "#{@created}+second"' ||
fail "set-hook -ga session-created failed"
$TMUX new -d -s three || fail "new-session three failed"
wait_for @created 'session-created:three'
wait_for @second 'session-created:three+second'
# show-hooks lists both commands.
shown=$($TMUX show-hooks -g session-created) ||
fail "show-hooks -g failed"
[ "$(echo "$shown" | wc -l)" -eq 2 ] ||
fail "unexpected show-hooks output: $shown"
echo "$shown" | grep -q '^session-created\[0\]' ||
fail "missing first array item: $shown"
echo "$shown" | grep -q '^session-created\[1\]' ||
fail "missing second array item: $shown"
# Unsetting removes the whole hook.
$TMUX set-hook -gu session-created || fail "set-hook -gu failed"
shown=$($TMUX show-hooks -g session-created) ||
fail "show-hooks -g after unset failed"
if echo "$shown" | grep -q '\['; then
fail "show-hooks showed removed hook: $shown"
fi
$TMUX set -g @created 0 || fail "reset @created failed"
$TMUX new -d -s four || fail "new-session four failed"
assert_unchanged @created 0
# An unknown hook name is rejected.
if $TMUX set-hook -g no-such-hook 'display x' 2>/dev/null; then
fail "unknown hook name was accepted"
fi
# A session hook only fires for events in that session.
$TMUX set -g @renamed 0 || fail "set @renamed failed"
$TMUX set-hook -t one window-renamed \
'set -gF @renamed "#{hook}:#{hook_window_name}"' ||
fail "set-hook -t one window-renamed failed"
$TMUX rename-window -t two:0 twoname || fail "rename-window two failed"
assert_unchanged @renamed 0
$TMUX rename-window -t one:0 onename || fail "rename-window one failed"
wait_for @renamed 'window-renamed:onename'
$TMUX set-hook -u -t one window-renamed || fail "set-hook -u failed"
# A pane hook only fires for events in that pane.
pane2=$($TMUX splitw -d -t one:0 -P -F '#{pane_id}') ||
fail "split-window failed"
$TMUX set -g @mode 0 || fail "set @mode failed"
$TMUX set-hook -p -t "$pane2" pane-mode-changed \
'set -gF @mode "#{hook}:#{hook_pane}"' ||
fail "set-hook -p pane-mode-changed failed"
$TMUX copy-mode -t one:0.0 || fail "copy-mode pane 0 failed"
assert_unchanged @mode 0
$TMUX copy-mode -t "$pane2" || fail "copy-mode pane 1 failed"
wait_for @mode "pane-mode-changed:$pane2"
$TMUX send-keys -t "$pane2" -X cancel || fail "cancel failed"
$TMUX set-hook -pu -t "$pane2" pane-mode-changed ||
fail "set-hook -pu failed"
# A window hook only fires for events in that window.
$TMUX set -g @wmode 0 || fail "set @wmode failed"
$TMUX set-hook -w -t two:0 pane-mode-changed \
'set -gF @wmode "#{hook}:#{hook_window_name}"' ||
fail "set-hook -w pane-mode-changed failed"
$TMUX copy-mode -t three:0.0 || fail "copy-mode three failed"
assert_unchanged @wmode 0
$TMUX copy-mode -t two:0.0 || fail "copy-mode two failed"
wait_for @wmode 'pane-mode-changed:twoname'
$TMUX set-hook -wu -t two:0 pane-mode-changed ||
fail "set-hook -wu failed"
# Commands run from a hook do not fire hooks again.
$TMUX set -g @renames '' || fail "set @renames failed"
$TMUX set-hook -g window-renamed \
'set -gF @renames "#{@renames}x" ; rename-window -t three:0 inner' ||
fail "set-hook -g window-renamed failed"
$TMUX rename-window -t three:0 outer || fail "rename-window outer failed"
wait_for @renames 'x'
assert_unchanged @renames 'x'
name=$($TMUX display -pt three:0 '#{window_name}') ||
fail "display window_name failed"
[ "$name" = inner ] || fail "expected window name inner but got $name"
exit 0

View File

@@ -7,7 +7,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -7,7 +7,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

View File

@@ -7,7 +7,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)

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 -LtestA$$ -f/dev/null"
TMP=$(mktemp)
EXP=$(mktemp)
trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15
exit_status=0
fail()
{
echo "FAIL: $1"
diff -u "$EXP" "$TMP"
exit_status=1
}
start_pane()
{
start_pane_hlimit "$1" "$2" "$3" "$4" 0
}
start_pane_history()
{
start_pane_hlimit "$1" "$2" "$3" "$4" 2000
}
start_pane_hlimit()
{
name=$1
sx=$2
sy=$3
seq=$4
hlimit=$5
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1
$TMUX set-option -g history-limit "$hlimit" || exit 1
$TMUX new-session -d -x "$sx" -y "$sy" -s "$name" \
"printf '$seq'; sleep 2" || exit 1
$TMUX kill-session -t test-setup
sleep 0.3
}
start_cmd()
{
name=$1
sx=$2
sy=$3
cmd=$4
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x "$sx" -y "$sy" -s "$name" "$cmd" || exit 1
sleep 0.3
}
normalize_capture()
{
sed 's/[ ]*$//' |
awk '{ line[NR] = $0; if ($0 != "") last = NR }
END { for (i = 1; i <= last; i++) print line[i] }'
}
capture_grid()
{
$TMUX capture-pane -pN -t "$1:" -S 0 -E - | normalize_capture
}
check_capture()
{
name=$1
expected=$2
capture_grid "$name" >"$TMP"
printf "%s\n" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name"
}
check_cursor()
{
name=$1
expected=$2
actual=$($TMUX display-message -p -t "$name:" '#{cursor_x},#{cursor_y}')
if [ "$actual" != "$expected" ]; then
printf "%s\n" "$expected" >"$EXP"
printf "%s\n" "$actual" >"$TMP"
fail "$name cursor"
fi
}
check_flags()
{
name=$1
expected=$2
$TMUX capture-pane -pNF -t "$name:" -S 0 -E - |
normalize_capture |
awk '$0 != "-"' >"$TMP"
printf "%s\n" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name flags"
}
check_joined()
{
name=$1
expected=$2
$TMUX capture-pane -pNJ -t "$name:" -S 0 -E - |
normalize_capture >"$TMP"
printf "%s\n" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name joined"
}
capture_raw()
{
$TMUX capture-pane -pR -t "$1:"
}
capture_raw_used()
{
capture_raw "$1" |
awk '/^(G| L)/ || /^ C/ && $3 !~ /^data=\(1,1, \)$/'
}
check_raw()
{
name=$1
expected=$2
capture_raw "$name" >"$TMP"
printf "%s\n" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name raw"
}
check_raw_used()
{
name=$1
expected=$2
capture_raw_used "$name" >"$TMP"
printf "%s\n" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name raw used"
}
check_raw_has()
{
name=$1
shift
capture_raw "$name" >"$TMP"
for expected in "$@"; do
if ! grep -Fqx "$expected" "$TMP"; then
printf "%s\n" "$expected" >"$EXP"
fail "$name raw missing"
fi
done
}
check_raw_matches()
{
name=$1
shift
capture_raw "$name" >"$TMP"
for expected in "$@"; do
if ! grep -Eq "$expected" "$TMP"; then
printf "%s\n" "$expected" >"$EXP"
fail "$name raw missing"
fi
done
}

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

@@ -4,7 +4,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
sleep 1
$TMUX -f/dev/null new -x20 -y2 -d \; set -g escape-time 0 || exit 1

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

View File

@@ -0,0 +1,494 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export PATH TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
TMP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1
EXP=$(mktemp "${TMPDIR:-/tmp}/input-reflow-stress.XXXXXX") || exit 1
exit_status=0
WIDTHS="80 40 20 10 7 5 4 3 2 1 2 3 4 5 7 10 20 40 80"
LIVE_WIDTHS="68 24 80"
HEIGHT=24
HISTORY_LIMIT=220
HISTORY_BOUND=12000
JOINED_BOUND=500000
RAW_BOUND=5000000
COPY_BOUND=20000
LIVE_BOUND=50000
cleanup()
{
rm -f "$TMP" "$EXP"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
trap cleanup 0 1 15
record_fail()
{
echo "FAIL: $1" >&2
exit_status=1
}
u8()
{
printf "$1"
}
wide_a()
{
u8 '\0343\0201\0202'
}
wide_b()
{
u8 '\0347\0225\0214'
}
combining_acute()
{
u8 '\0314\0201'
}
zero_width_space()
{
u8 '\0342\0200\0213'
}
zero_width_joiner()
{
u8 '\0342\0200\0215'
}
replacement()
{
u8 '\0357\0277\0275'
}
assert_alive()
{
$TMUX display-message -p -t stress: alive >/dev/null 2>&1 ||
record_fail "server exited after $1"
}
make_orphan_payload()
{
tag=$1
printf 'OP%s-01|ECH-half|left' "$tag"
wide_b
printf 'right'
printf '\033[6D\033[1X\n'
printf 'OP%s-02|EL-inside|left' "$tag"
wide_b
printf 'right'
printf '\033[6D\033[K\n'
printf 'OP%s-03|overwrite-leading|left' "$tag"
wide_b
printf 'right'
printf '\rOP%s-03|overwrite-leading|left!\n' "$tag"
printf 'OP%s-04|right-edge|' "$tag"
i=0
while [ "$i" -lt 60 ]; do
printf '.'
i=$((i + 1))
done
wide_b
printf 'Z'
printf '\033[2D\033[1X\n'
printf 'OP%s-05|ERASED|left' "$tag"
wide_b
printf 'right\r\033[2KOP%s-05|ERASED|left clear\n' "$tag"
printf 'SENT-%s|ORPHAN-PHASE|complete\n' "$tag"
}
make_payload()
{
i=0
while [ "$i" -lt 16 ]; do
printf 'ASCII%02d|abcdefghijklmnopqrstuvwxyz\n' "$i"
printf 'WIDE%02d|' "$i"
wide_a
printf '|'
wide_b
printf '|tail\n'
printf 'COMB%02d|e' "$i"
combining_acute
printf '|zero'
zero_width_space
zero_width_joiner
printf '|done\n'
printf 'STYLE%02d|\033[1;31mred\033[0m|\033[4munder\033[0m\n' \
"$i"
printf 'LINK%02d|\033]8;;https://example.invalid/%d\007link%d\033]8;;\007|end\n' \
"$i" "$i" "$i"
printf 'WRAP%02d|%064d\n' "$i" "$i"
printf 'CRBS%02d|abcdef\rCRBS%02d|XYZ\n' "$i" "$i"
printf 'BS%02d|abc\bZ\n' "$i"
i=$((i + 1))
done
i=0
while [ "$i" -lt 6 ]; do
printf 'L%04d|stable|abcdefghijklmnopqrstuvwxyz\n' "$i"
i=$((i + 1))
done
printf 'SENT-A|SURVIVES|plain logical line\n'
printf 'SENT-B|SURVIVES|wide-free after erases\n'
make_orphan_payload PRE
printf 'SENT-C|SURVIVES|tail logical line\n'
}
make_alternate_payload()
{
printf '\033[?1049h'
printf 'ALT-SENT|alternate screen|'
wide_a
printf '\nALT-SENT|resize target\n'
}
exit_alternate_payload()
{
printf '\033[?1049l'
}
load_and_paste()
{
buffer=$1
shift
"$@" >"$EXP"
$TMUX load-buffer -b "$buffer" "$EXP" || exit 1
$TMUX paste-buffer -d -b "$buffer" -t stress:0.0 || exit 1
sleep 0.3
}
capture_joined()
{
$TMUX capture-pane -pNJ -t stress: -S -5000 -E - >"$TMP"
}
assert_joined_sane()
{
label=$1
capture_joined || {
record_fail "joined capture failed after $label"
return
}
bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt "$JOINED_BOUND" ]; then
record_fail "joined capture too large after $label: $bytes"
fi
if grep -q "$(replacement)" "$TMP"; then
record_fail "replacement character in joined capture after $label"
fi
out=$(
awk -v label="$label" '
{
line = $0
seen_on_line = 0
while (match(line, /L[0-9][0-9][0-9][0-9]\|/)) {
id = substr(line, RSTART + 1, 4) + 0
seen_on_line++
seen[id]++
ids[++count] = id
found = 1
last = id
line = substr(line, RSTART + RLENGTH)
}
if (seen_on_line > 1)
printf("fused IDs after %s: %s\n", label, $0)
}
END {
if (!found) {
printf("no line IDs after %s\n", label)
exit
}
for (i = 2; i <= count; i++) {
if (ids[i] < ids[i - 1])
drops++
}
if (drops > 1)
printf("IDs out of order after %s\n", label)
for (id in seen) {
if (seen[id] > 2)
printf("duplicate ID after %s: L%04d\n", label, id)
}
}' "$TMP"
)
[ -z "$out" ] || record_fail "$out"
for marker in \
'SENT-A|SURVIVES|plain logical line' \
'SENT-B|SURVIVES|wide-free after erases' \
'SENT-C|SURVIVES|tail logical line'
do
grep -F "$marker" "$TMP" >/dev/null 2>&1 ||
record_fail "missing sentinel after $label: $marker"
done
if grep '^OP.*-05|ERASED|.*' "$TMP" | grep -q "$(wide_b)"; then
record_fail "erased wide character visible after $label"
fi
}
assert_final_logical_text()
{
label=$1
capture_joined || {
record_fail "joined capture failed after $label"
return
}
for marker in \
'SENT-A|SURVIVES|plain logical line' \
'SENT-B|SURVIVES|wide-free after erases' \
'SENT-C|SURVIVES|tail logical line' \
'OPPRE-03|overwrite-leading|left!' \
'OPPOST-03|overwrite-leading|left!' \
'SENT-POST|ORPHAN-PHASE|complete'
do
grep -F "$marker" "$TMP" >/dev/null 2>&1 ||
record_fail "missing expected logical text after $label: $marker"
done
}
assert_raw_sane()
{
label=$1
$TMUX capture-pane -pR -t stress: >"$TMP" ||
record_fail "raw capture failed after $label"
bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt "$RAW_BOUND" ]; then
record_fail "raw capture too large after $label: $bytes"
fi
width=$($TMUX display-message -p -t stress: '#{pane_width}' 2>/dev/null)
case "$width" in
''|*[!0-9]*) width=0 ;;
esac
out=$(
awk -v label="$label" -v width="$width" '
{
sub(/^[ ]+/, "")
if ($1 != "C")
next
coord = $2
sub(/^[0-9]*,/, "", coord)
sub(/[^0-9].*$/, "", coord)
col = coord + 0
if (width > 0 && (col < 0 || col >= width))
printf("cell column outside width after %s: width %d: %s\n", label, width, $0)
data = $0
if (data !~ /data=\(/) {
printf("cell without data after %s: %s\n", label, $0)
next
}
sub(/^.*data=\(/, "", data)
split(data, parts, ",")
cell_width = parts[1] + 0
padding = ($0 ~ /flags=[^ ]*PADDING/)
if (!padding && cell_width == 0)
printf("visible zero-width cell after %s: %s\n", label, $0)
}' "$TMP"
)
[ -z "$out" ] || record_fail "$out"
}
assert_history_sane()
{
label=$1
size=$($TMUX display-message -p -t stress: '#{history_size}' 2>/dev/null)
case "$size" in
''|*[!0-9]*)
record_fail "bad history size after $label: $size"
;;
*)
if [ "$size" -gt "$HISTORY_BOUND" ]; then
record_fail "history too large after $label: $size"
fi
;;
esac
}
assert_copy_mode_sane()
{
label=$1
$TMUX capture-pane -pM -S -5000 -E - -t stress: >"$TMP" ||
record_fail "copy-mode capture failed after $label"
bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt "$COPY_BOUND" ]; then
record_fail "copy-mode capture too large after $label: $bytes"
fi
if grep -q "$(replacement)" "$TMP"; then
record_fail "replacement character in copy-mode after $label"
fi
if ! grep -Eq 'L[0-9][0-9][0-9][0-9]\|' "$TMP"; then
record_fail "no line ID in copy-mode after $label"
fi
}
run_resize_checks()
{
for width in $WIDTHS; do
$TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1
sleep 0.1
assert_alive "resize to $width"
assert_joined_sane "resize to $width"
assert_raw_sane "resize to $width"
assert_history_sane "resize to $width"
done
}
wait_outer_contains()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null &&
grep -F "$marker" "$TMP" >/dev/null 2>&1 &&
return 0
sleep 0.2
i=$((i + 1))
done
return 1
}
assert_live_client_redraw()
{
$TMUX set-option -t stress: status on >/dev/null || exit 1
$TMUX set-option -t stress: status-interval 1 >/dev/null || exit 1
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -s out -x 100 -y 12 "$TMUX attach -t stress" ||
exit 1
i=0
while [ "$i" -lt 50 ]; do
clients=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$clients" -ge 1 ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || {
record_fail "nested client did not attach"
return
}
for width in $LIVE_WIDTHS; do
$TMUX set-option -t stress: status-left "REDRAW-$width " >/dev/null ||
exit 1
$TMUX2 resize-window -t out: -x "$width" -y 12 || exit 1
sleep 0.2
done
wait_outer_contains 'REDRAW-80' ||
record_fail "outer capture missing final redraw marker"
$TMUX2 capture-pane -p -t out:0 >"$TMP" 2>/dev/null ||
record_fail "outer capture failed"
bytes=$(wc -c <"$TMP")
if [ "$bytes" -gt "$LIVE_BOUND" ]; then
record_fail "outer capture too large: $bytes"
fi
if grep -q "$(replacement)" "$TMP"; then
record_fail "replacement character in outer capture"
fi
grep -F 'SENT-POST|ORPHAN-PHASE|complete' "$TMP" >/dev/null 2>&1 ||
record_fail "outer capture missing expected sentinel"
grep -F 'REDRAW-24' "$TMP" >/dev/null 2>&1 &&
record_fail "outer capture contains stale width marker"
$TMUX2 kill-server 2>/dev/null
$TMUX set-option -t stress: status off >/dev/null || exit 1
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1
$TMUX set-option -g history-limit "$HISTORY_LIMIT" || exit 1
$TMUX new-session -d -x 80 -y "$HEIGHT" -s stress 'cat' || exit 1
$TMUX kill-session -t test-setup || exit 1
sleep 0.3
load_and_paste stress-data make_payload
assert_joined_sane "initial payload"
assert_raw_sane "initial payload"
assert_history_sane "initial payload"
$TMUX resize-window -t stress: -x 40 -y "$HEIGHT" || exit 1
sleep 0.1
load_and_paste stress-orphan-post make_orphan_payload POST
assert_joined_sane "post-resize orphan payload"
assert_raw_sane "post-resize orphan payload"
run_resize_checks
assert_final_logical_text "return to original width"
$TMUX copy-mode -H -t stress: || exit 1
$TMUX send-keys -t stress: -X history-top
sleep 0.1
assert_alive "copy-mode"
assert_copy_mode_sane "copy-mode history-top"
$TMUX send-keys -t stress: -X cancel
sleep 0.1
assert_live_client_redraw
load_and_paste stress-alt make_alternate_payload
for width in 30 12 80; do
$TMUX resize-window -t stress: -x "$width" -y "$HEIGHT" || exit 1
sleep 0.1
assert_alive "alternate resize to $width"
assert_raw_sane "alternate resize to $width"
done
load_and_paste stress-alt-exit exit_alternate_payload
sleep 0.3
assert_alive "alternate screen exit"
assert_joined_sane "alternate screen exit"
assert_raw_sane "alternate screen exit"
assert_final_logical_text "alternate screen exit"
cleanup
exit $exit_status

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 -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
sleep 0.5
TMP=$(mktemp)
EXP=$(mktemp)
trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15
$TMUX new-session -d -x 80 -y 24 -s replies \; \
set-window-option -t replies:0 remain-on-exit on || exit 1
$TMUX set-option -s set-clipboard on || exit 1
$TMUX set-option -s get-clipboard buffer || exit 1
printf Hello | $TMUX load-buffer -
sleep 0.5
exit_status=0
fail()
{
echo "FAIL: $1"
diff -u "$EXP" "$TMP"
exit_status=1
}
query()
{
name=$1
expected=$2
seq=$3
count=$4
setup=$5
$TMUX respawn-window -k -t replies:0 \
"stty raw -echo min 1 time 20; printf '$setup'; printf '$seq'; dd bs=1 count=$count 2>/dev/null | cat -v >$TMP"
sleep 0.5
printf "%s" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name"
}
query_timeout()
{
name=$1
expected=$2
seq=$3
setup=$4
$TMUX respawn-window -k -t replies:0 \
"stty raw -echo min 0 time 5; printf '$setup'; printf '$seq'; sleep 0.1; dd bs=1 count=128 2>/dev/null | cat -v >$TMP"
sleep 1
printf "%s" "$expected" >"$EXP"
cmp "$TMP" "$EXP" || fail "$name"
}
query "dsr-ok" '^[[0n' '\033[5n' 4 ''
query "dsr-cursor" '^[[1;1R' '\033[6n' 6 ''
query "da-primary" '^[[?1;2c' '\033[c' 7 ''
query "da-secondary" '^[[>84;0;0c' '\033[>c' 10 ''
query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 7 ''
query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 7 '\033[4h'
query "decrqm-cursor-keys-reset" '^[[?1;2$y' '\033[?1$p' 8 ''
query "decrqm-cursor-keys-set" '^[[?1;1$y' '\033[?1$p' 8 '\033[?1h'
query "decrqm-columns" '^[[?3;4$y' '\033[?3$p' 8 ''
query "decrqm-origin-reset" '^[[?6;2$y' '\033[?6$p' 8 ''
query "decrqm-origin-set" '^[[?6;1$y' '\033[?6$p' 8 '\033[?6h'
query "decrqm-wrap-set" '^[[?7;1$y' '\033[?7$p' 8 ''
query "decrqm-wrap-reset" '^[[?7;2$y' '\033[?7$p' 8 '\033[?7l'
query "decrqm-cursor-visible-set" '^[[?25;1$y' '\033[?25$p' 9 ''
query "decrqm-cursor-visible-reset" '^[[?25;2$y' '\033[?25$p' 9 '\033[?25l'
query "decrqm-mouse-standard-set" '^[[?1000;1$y' '\033[?1000$p' 11 '\033[?1000h'
query "decrqm-mouse-button-set" '^[[?1002;1$y' '\033[?1002$p' 11 '\033[?1002h'
query "decrqm-mouse-all-set" '^[[?1003;1$y' '\033[?1003$p' 11 '\033[?1003h'
query "decrqm-focus-set" '^[[?1004;1$y' '\033[?1004$p' 11 '\033[?1004h'
query "decrqm-mouse-utf8-set" '^[[?1005;1$y' '\033[?1005$p' 11 '\033[?1005h'
query "decrqm-mouse-sgr-set" '^[[?1006;1$y' '\033[?1006$p' 11 '\033[?1006h'
query "decrqm-bracket-paste-set" '^[[?2004;1$y' '\033[?2004$p' 11 '\033[?2004h'
query "decrqm-theme-updates-set" '^[[?2031;1$y' '\033[?2031$p' 11 '\033[?2031h'
query "decrqss-cursor-style" '^[P1$r q0 q^[\' '\033P$q q\033\\' 12 ''
query_timeout "osc-10-query" '^[]10;rgb:ffff/0000/0000^G' '\033]10;?\007' '\033]10;red\007'
query_timeout "osc-11-query" '^[]11;rgb:0000/0000/ffff^G' '\033]11;?\007' '\033]11;blue\007'
query_timeout "osc-12-query" '^[]12;rgb:0000/ffff/0000^G' '\033]12;?\007' '\033]12;green\007'
query_timeout "osc-4-query" '^[]4;1;rgb:ffff/0000/0000^G' '\033]4;1;?\007' '\033]4;1;red\007'
query_timeout "osc-104-reset-query" '' '\033]4;1;?\007' '\033]4;1;red\007\033]104;1\007'
query_timeout "osc-52-query" '^[]52;c;SGVsbG8=^G' '\033]52;c;?\007' ''
$TMUX kill-server 2>/dev/null
exit $exit_status

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

@@ -0,0 +1,118 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 - "$TEST_TMUX" <<'PY'
import os
import select
import signal
import subprocess
import sys
import tempfile
import time
tmux = sys.argv[1]
label = "testA%d" % os.getpid()
server = [tmux, "-L" + label, "-f/dev/null"]
def run(*args, check=True):
return subprocess.run(server + list(args), check=check,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def attach():
pid, fd = os.forkpty()
if pid == 0:
os.environ["TERM"] = "xterm-256color"
os.execl(tmux, tmux, "-L" + label, "-f/dev/null", "attach-session",
"-t", "requests")
os.set_blocking(fd, False)
return pid, fd
def read_until(fd, needle, timeout=5):
end = time.time() + timeout
data = b""
while time.time() < end:
r, _, _ = select.select([fd], [], [], 0.05)
if fd in r:
try:
chunk = os.read(fd, 4096)
except BlockingIOError:
chunk = b""
if chunk == b"":
continue
data += chunk
if needle in data:
return data
raise RuntimeError("did not see terminal request %r in %r" %
(needle, data))
def wait_file(path, timeout=5):
end = time.time() + timeout
while time.time() < end:
try:
with open(path, "rb") as f:
data = f.read()
if data:
return data
except FileNotFoundError:
pass
time.sleep(0.05)
return b""
def respawn(command):
run("respawn-window", "-k", "-t", "requests:0", command)
time.sleep(0.2)
def cleanup(pid=None):
if pid is not None:
try:
os.kill(pid, signal.SIGHUP)
except ProcessLookupError:
pass
run("kill-server", check=False)
run("kill-server", check=False)
run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests",
"sleep 60")
pid, fd = attach()
try:
time.sleep(0.5)
with tempfile.NamedTemporaryFile(delete=False) as f:
palette_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]4;99;?\\033\\\\'; "
"dd bs=1 count=27 2>/dev/null | cat -v >%s; sleep 1" %
palette_out)
read_until(fd, b"\033]4;99;?\033\\")
os.write(fd, b"\033]4;99;rgb:0101/0202/0303\033\\")
got = wait_file(palette_out)
expected = b"^[]4;99;rgb:0101/0202/0303^[\\"
if got != expected:
raise AssertionError("palette reply: expected %r got %r" %
(expected, got))
run("set-option", "-s", "set-clipboard", "on")
run("set-option", "-s", "get-clipboard", "request")
with tempfile.NamedTemporaryFile(delete=False) as f:
clip_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]52;c;?\\033\\\\'; "
"dd bs=1 count=21 2>/dev/null | cat -v >%s; sleep 1" %
clip_out)
data = read_until(fd, b"]52;")
if b"?" not in data:
raise RuntimeError("clipboard request missing query in %r" % data)
os.write(fd, b"\033]52;c;UmVxdWVzdA==\033\\")
got = wait_file(clip_out)
expected = b"^[]52;c;UmVxdWVzdA==^[\\"
if got != expected:
raise AssertionError("clipboard reply: expected %r got %r" %
(expected, got))
finally:
cleanup(pid)
PY

Some files were not shown because too many files have changed in this diff Show More