Merge remote-tracking branch 'origin/master' into 5211-fix-select-layout-list-windows

# Conflicts:
#	layout-custom.c
This commit is contained in:
Michael Grant
2026-07-05 17:27:23 +01:00
104 changed files with 10026 additions and 1023 deletions

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

@@ -0,0 +1,79 @@
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 }}

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

@@ -42,8 +42,8 @@ const struct cmd_entry cmd_capture_pane_entry = {
.name = "capture-pane",
.alias = "capturep",
.args = { "ab:CeE:FHJLMNpPqS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHJLMNpPqT] " CMD_BUFFER_USAGE " [-E end-line] "
.args = { "ab:CeE:FHJLMNpPqRS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] "
"[-S start-line] " CMD_TARGET_PANE_USAGE,
.target = { 't', CMD_FIND_PANE, 0 },
@@ -75,6 +75,96 @@ cmd_capture_pane_append(char *buf, size_t *len, const char *line,
return (buf);
}
static char *
cmd_capture_pane_cell(struct screen *s, u_int xx, u_int yy)
{
struct grid *gd = s->grid;
struct hyperlinks *hl = s->hyperlinks;
struct grid_cell gc;
char *line, *data, *link, *linkid, *f, *b, *u;
char c[UTF8_SIZE + 1];
const char *uri, *iid;
u_int flags;
grid_get_cell(gd, xx, yy, &gc);
memcpy(c, gc.data.data, gc.data.size);
c[gc.data.size] = '\0';
utf8_stravis(&data, c, VIS_OCTAL|VIS_CSTYLE|VIS_TAB|VIS_NL);
if (gc.link != 0 && hyperlinks_get(hl, gc.link, &uri, &iid, NULL)) {
xasprintf(&link, "%s", uri);
if (iid != NULL && *iid != '\0')
xasprintf(&linkid, "%s", iid);
else
xasprintf(&linkid, "NONE");
} else {
xasprintf(&link, "NONE");
xasprintf(&linkid, "NONE");
}
flags = gc.flags;
if (gc.fg & COLOUR_FLAG_256)
flags |= GRID_FLAG_FG256;
if (gc.bg & COLOUR_FLAG_256)
flags |= GRID_FLAG_BG256;
xasprintf(&f, "%s[%x]", colour_tostring(gc.fg), gc.fg);
xasprintf(&b, "%s[%x]", colour_tostring(gc.bg), gc.bg);
xasprintf(&u, "%s[%x]", colour_tostring(gc.us), gc.us);
xasprintf(&line, "\t\tC %u,%u data=(%u,%u,%s) flags=%s[%x] "
"attr=%s[%x] fg=%s bg=%s us=%s link=%s linkid=%s\n",
yy, xx, gc.data.width, gc.data.size, data,
grid_cell_flags_string(flags), flags,
grid_cell_attr_string(gc.attr), gc.attr, f, b, u, link, linkid);
free(f);
free(b);
free(u);
free(link);
free(linkid);
free(data);
return (line);
}
static char *
cmd_capture_pane_grid(struct window_pane *wp, size_t *len)
{
struct screen *s = &wp->base;
struct grid *gd = s->grid;
struct grid_line *gl;
char *buf = xstrdup(""), *line;
char p[11];
u_int yy, xx, total = gd->hsize + gd->sy;
xasprintf(&line, "G %ux%u (%u/%u)\n", gd->sx, gd->sy, gd->hsize,
gd->hlimit);
buf = cmd_capture_pane_append(buf, len, line, strlen(line));
free(line);
for (yy = 0; yy < total; yy++) {
gl = grid_get_line(gd, yy);
if (yy < gd->hsize)
snprintf(p, sizeof p, "-");
else
snprintf(p, sizeof p, "%u", yy - gd->hsize);
xasprintf(&line, "\tL %u (%s) flags=%s[%x] %u/%u\n", yy,
p, grid_line_flags_string(gl->flags), gl->flags,
gl->cellused, gl->cellsize);
buf = cmd_capture_pane_append(buf, len, line, strlen(line));
free(line);
for (xx = 0; xx < gd->sx; xx++) {
line = cmd_capture_pane_cell(s, xx, yy);
buf = cmd_capture_pane_append(buf, len, line,
strlen(line));
free(line);
}
}
return (buf);
}
static char *
cmd_capture_pane_pending(struct args *args, struct window_pane *wp,
size_t *len)
@@ -323,7 +413,9 @@ cmd_capture_pane_exec(struct cmd *self, struct cmdq_item *item)
}
len = 0;
if (args_has(args, 'P') && !args_has(args, 'H'))
if (args_has(args, 'R'))
buf = cmd_capture_pane_grid(wp, &len);
else if (args_has(args, 'P') && !args_has(args, 'H'))
buf = cmd_capture_pane_pending(args, wp, &len);
else
buf = cmd_capture_pane_history(args, item, wp, &len);

View File

@@ -1011,7 +1011,7 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item,
strcmp(target, "{active}") == 0 ||
strcmp(target, "{current}") == 0) {
c = cmdq_get_client(item);
if (c == NULL) {
if (c == NULL || c->session == NULL) {
cmdq_error(item, "no current client");
goto error;
}

View File

@@ -52,7 +52,7 @@ const struct cmd_entry cmd_move_pane_entry = {
.name = "move-pane",
.alias = "movep",
.args = { "bdfhMvl:L::P:R::s:t:U::X:Y:z:", 0, 0, NULL },
.args = { "bdD::fhMvl:L::P:R::s:t:U::X:Y:z:", 0, 0, NULL },
.usage = "[-bdfhMv] [-D lines] [-l size] [-L columns] [-P position] "
"[-R columns] " CMD_SRCDST_PANE_USAGE " [-U lines] "
"[-X x-position] [-Y y-position] [-z z-index]",
@@ -363,6 +363,44 @@ 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->saved_sx = lc->sx;
lc->saved_sy = lc->sy;
lc->saved_xoff = lc->xoff;
lc->saved_yoff = lc->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 +450,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

@@ -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)
{
@@ -96,6 +185,12 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item)
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));

View File

@@ -31,6 +31,10 @@ 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);
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 },
@@ -95,6 +99,9 @@ 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));
@@ -124,8 +131,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, idx, parent);
}
else if (*name == '@') {
if (args_has(args, 'q'))
goto out;
@@ -195,6 +207,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)

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)

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

509
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
@@ -885,6 +891,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)
@@ -2313,7 +2350,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);
}
@@ -2868,6 +2905,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)
@@ -3048,6 +3098,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)
@@ -3538,6 +3601,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
},
@@ -3754,6 +3820,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
},
@@ -4034,6 +4106,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)
@@ -4137,7 +4232,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;
@@ -4252,6 +4347,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);
@@ -4462,7 +4562,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/
@@ -4481,7 +4581,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++;
@@ -4503,7 +4603,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];
@@ -4780,11 +4880,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);
@@ -4827,9 +4930,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;
@@ -4862,20 +4965,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);
}
@@ -4889,33 +4993,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);
@@ -4972,13 +5081,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);
@@ -4997,6 +5108,269 @@ 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, -1, 0);
format_add(nft, "option_value", "%s", s);
free(s);
format_add(nft, "option_is_array", "%d", is_array);
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);
char *expanded, *s;
u_int idx;
idx = options_array_item_index(a);
format_log(es, "option loop: %s[%u]", name, idx);
nft = format_create(ft->client, ft->item, FORMAT_NONE, ft->flags);
format_add(nft, "option_name", "%s", name);
s = options_to_string(o, idx, 0);
format_add(nft, "option_value", "%s", s);
free(s);
format_add(nft, "option_is_array", "1");
format_add(nft, "option_array_index", "%u", idx);
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)
@@ -5021,14 +5395,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);
}
@@ -5198,12 +5574,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;
@@ -5281,6 +5659,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;
@@ -5316,6 +5700,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;
@@ -5382,11 +5768,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) {
@@ -5479,11 +5881,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;
}
@@ -5505,6 +5924,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

@@ -59,12 +59,14 @@
" '#{?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}},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}" \
@@ -362,6 +364,7 @@ key_bindings_init(void)
"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 '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 -- '%%' } }",
@@ -479,6 +482,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

@@ -727,7 +727,7 @@ layout_construct(struct layout_parse_ctx *ctx, struct layout_cell *parent,
fail:
ctx->depth--;
layout_free_cell(lc);
layout_free_cell(lc, 0);
return (-1);
}
@@ -919,20 +919,20 @@ layout_prepare(struct window *w, const char *layout, char **cause)
}
if (layout_construct(&ctx, NULL, &root) != 0) {
*cause = xstrdup("invalid layout");
layout_free_cell(root);
layout_free_cell(root, 0);
return (NULL);
}
layout_skip_space(&ctx);
if (ctx.ptr != ctx.end || root == NULL) {
*cause = xstrdup("invalid layout");
layout_free_cell(root);
layout_free_cell(root, 0);
return (NULL);
}
if (layout_resolve_relative(root,
root->type == LAYOUT_WINDOWPANE ? w->sx : root->sx,
root->type == LAYOUT_WINDOWPANE ? w->sy : root->sy) != 0) {
*cause = xstrdup("invalid layout");
layout_free_cell(root);
layout_free_cell(root, 0);
return (NULL);
}
@@ -1004,7 +1004,7 @@ layout_prepare(struct window *w, const char *layout, char **cause)
return (prepared);
fail:
layout_free_cell(root);
layout_free_cell(root, 0);
return (NULL);
}
@@ -1014,7 +1014,7 @@ layout_free_prepared(struct layout_prepared *prepared)
{
if (prepared == NULL)
return;
layout_free_cell(prepared->root);
layout_free_cell(prepared->root, 0);
free(prepared);
}
@@ -1029,7 +1029,7 @@ layout_apply_prepared(struct window *w, struct layout_prepared *prepared)
prepared->root = NULL;
/* The layout was fully validated before the existing layout is changed. */
layout_free_cell(w->layout_root);
layout_free_cell(w->layout_root, 0);
w->layout_root = root;
wp = TAILQ_FIRST(&w->panes);
layout_assign(w, &wp, root, prepared->pane_ids);

View File

@@ -124,12 +124,12 @@ layout_set_previous(struct window *w)
}
static struct window_pane *
layout_first_tiled(struct window *w)
layout_set_first_tiled(struct window *w)
{
struct window_pane *wp;
TAILQ_FOREACH(wp, &w->panes, entry) {
if (!window_pane_is_floating(wp))
if (wp->layout_cell && layout_cell_is_tiled(wp->layout_cell))
return (wp);
}
return (NULL);
@@ -139,19 +139,15 @@ static void
layout_set_even(struct window *w, enum layout_type type)
{
struct window_pane *wp;
struct layout_cell *lc, *lcnew;
struct layout_cell *lcroot, *lcchild;
u_int n, sx, sy;
layout_print_cell(w->layout_root, __func__, 1);
/* Get number of panes. */
n = window_count_panes(w, 0);
if (n <= 1)
return;
/* Free the old root and construct a new. */
layout_free(w);
lc = w->layout_root = layout_create_cell(NULL);
if (type == LAYOUT_LEFTRIGHT) {
sx = (n * (PANE_MINIMUM + 1)) - 1;
if (sx < w->sx)
@@ -163,30 +159,30 @@ layout_set_even(struct window *w, enum layout_type type)
sy = w->sy;
sx = w->sx;
}
layout_set_size(lc, sx, sy, 0, 0);
layout_make_node(lc, type);
/* Build new leaf cells. */
layout_free(w, 1);
lcroot = w->layout_root = layout_create_cell(NULL);
layout_set_size(lcroot, sx, sy, 0, 0);
layout_make_node(lcroot, type);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
continue;
lcnew = layout_create_cell(lc);
layout_make_leaf(lcnew, wp);
lcnew->sx = w->sx;
lcnew->sy = w->sy;
TAILQ_INSERT_TAIL(&lc->cells, lcnew, entry);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcroot->cells, lcchild, entry);
lcchild->parent = lcroot;
if (layout_cell_is_tiled(lcchild)) {
lcchild->sx = w->sx;
lcchild->sy = w->sy;
}
}
/* Spread out cells. */
layout_spread_cell(w, lc);
layout_spread_cell(w, lcroot);
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->sx, lcroot->sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -206,15 +202,14 @@ layout_set_even_v(struct window *w)
static void
layout_set_main_h(struct window *w)
{
struct window_pane *wp;
struct layout_cell *lc, *lcmain, *lcother, *lcchild;
struct window_pane *wp, *wpmain;
struct layout_cell *lcroot, *lcmain, *lcother, *lcchild;
u_int n, mainh, otherh, sx, sy;
char *cause;
const char *s;
layout_print_cell(w->layout_root, __func__, 1);
/* Get number of panes. */
n = window_count_panes(w, 0);
if (n <= 1)
return;
@@ -255,52 +250,49 @@ 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);
layout_set_size(wp->layout_cell, sx, otherh, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_set_size(lcother, sx, otherh, 0, 0);
layout_make_node(lcother, LAYOUT_LEFTRIGHT);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_TAIL(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, PANE_MINIMUM, otherh, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, PANE_MINIMUM, otherh,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->sx, lcroot->sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -308,15 +300,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 +348,49 @@ 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);
layout_set_size(wp->layout_cell, sx, otherh, 0, 0);
TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_set_size(lcother, sx, otherh, 0, 0);
layout_make_node(lcother, LAYOUT_LEFTRIGHT);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
TAILQ_INSERT_HEAD(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, PANE_MINIMUM, otherh, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, PANE_MINIMUM, otherh,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Create the main pane. */
lcmain = layout_create_cell(lc);
layout_set_size(lcmain, sx, mainh, 0, 0);
layout_make_leaf(lcmain, layout_first_tiled(w));
TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry);
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->sx, lcroot->sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -410,21 +398,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 +446,49 @@ 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);
layout_set_size(wp->layout_cell, otherw, sy, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_make_node(lcother, LAYOUT_TOPBOTTOM);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
layout_set_size(lcother, otherw, sy, 0, 0);
TAILQ_INSERT_TAIL(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, otherw, PANE_MINIMUM, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, otherw, PANE_MINIMUM,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->sx, lcroot->sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
@@ -512,8 +496,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 +510,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 +545,59 @@ 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);
layout_set_size(wp->layout_cell, otherw, sy, 0, 0);
TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry);
wp->layout_cell->parent = lcroot;
} else {
lcother = layout_create_cell(lcroot);
layout_make_node(lcother, LAYOUT_TOPBOTTOM);
TAILQ_INSERT_TAIL(&lc->cells, lcother, entry);
layout_set_size(lcother, otherw, sy, 0, 0);
TAILQ_INSERT_HEAD(&lcroot->cells, lcother, entry);
/* Add the remaining panes as children. */
TAILQ_FOREACH(wp, &w->panes, entry) {
if (window_pane_is_floating(wp))
if (wp == wpmain)
continue;
if (wp == layout_first_tiled(w))
continue;
lcchild = layout_create_cell(lcother);
layout_set_size(lcchild, otherw, PANE_MINIMUM, 0, 0);
layout_make_leaf(lcchild, wp);
lcchild = wp->layout_cell;
TAILQ_INSERT_TAIL(&lcother->cells, lcchild, entry);
lcchild->parent = lcother;
if (layout_cell_is_tiled(lcchild))
layout_set_size(lcchild, otherw, PANE_MINIMUM,
0, 0);
}
layout_spread_cell(w, lcother);
}
/* Create the main pane. */
lcmain = layout_create_cell(lc);
layout_set_size(lcmain, mainw, sy, 0, 0);
layout_make_leaf(lcmain, layout_first_tiled(w));
TAILQ_INSERT_TAIL(&lc->cells, lcmain, entry);
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->sx, lcroot->sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}
void
static void
layout_set_tiled(struct window *w)
{
struct options *oo = w->options;
struct window_pane *wp;
struct layout_cell *lc, *lcrow, *lcchild;
struct layout_cell *lcroot, *lcrow, *lcchild;
u_int n, width, height, used, sx, sy;
u_int i, j, columns, rows, max_columns;
@@ -647,56 +628,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 +697,19 @@ layout_set_tiled(struct window *w)
w->sx - used);
}
/* Adjust the last row height to fit if necessary. */
used = (rows * height) + rows - 1;
if (w->sy > used) {
lcrow = TAILQ_LAST(&lc->cells, layout_cells);
lcrow = TAILQ_LAST(&lcroot->cells, layout_cells);
layout_resize_adjust(w, lcrow, LAYOUT_TOPBOTTOM,
w->sy - used);
}
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_fix_panes(w, NULL);
layout_print_cell(w->layout_root, __func__, 1);
window_resize(w, lc->sx, lc->sy, -1, -1);
window_resize(w, lcroot->sx, lcroot->sy, -1, -1);
notify_window("window-layout-changed", w);
server_redraw_window(w);
}

View File

@@ -90,20 +90,24 @@ layout_create_cell(struct layout_cell *lcparent)
/* Free a layout cell. */
void
layout_free_cell(struct layout_cell *lc)
layout_free_cell(struct layout_cell *lc, int only_nodes)
{
struct layout_cell *lcchild;
struct layout_cell *lcchild, *lcnext;
if (lc == NULL)
if (lc == NULL || (only_nodes && lc->type == LAYOUT_WINDOWPANE))
return;
switch (lc->type) {
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
while (!TAILQ_EMPTY(&lc->cells)) {
lcchild = TAILQ_FIRST(&lc->cells);
TAILQ_REMOVE(&lc->cells, lcchild, entry);
layout_free_cell(lcchild);
lcchild = TAILQ_FIRST(&lc->cells);
while (lcchild != NULL) {
lcnext = TAILQ_NEXT(lcchild, entry);
if (!only_nodes || lcchild->type != LAYOUT_WINDOWPANE) {
TAILQ_REMOVE(&lc->cells, lcchild, entry);
layout_free_cell(lcchild, only_nodes);
}
lcchild = lcnext;
}
break;
case LAYOUT_WINDOWPANE:
@@ -257,7 +261,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;
@@ -694,13 +698,13 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc,
if (lcparent == NULL) {
if (lc->wp != NULL)
*lcroot = NULL;
layout_free_cell(lc);
layout_free_cell(lc, 0);
return;
}
if (!layout_cell_is_tiled(lc)) {
TAILQ_REMOVE(&lcparent->cells, lc, entry);
layout_free_cell(lc);
layout_free_cell(lc, 0);
goto out;
}
@@ -716,7 +720,7 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc,
/* Remove this from the parent's list. */
TAILQ_REMOVE(&lcparent->cells, lc, entry);
layout_free_cell(lc);
layout_free_cell(lc, 0);
out:
/*
@@ -737,7 +741,7 @@ out:
} else
TAILQ_REPLACE(&lc->parent->cells, lcparent, lc, entry);
layout_free_cell(lcparent);
layout_free_cell(lcparent, 0);
}
}
@@ -755,9 +759,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. */
@@ -1500,7 +1504,8 @@ layout_spread_cell(struct window *w, struct layout_cell *parent)
number = 0;
TAILQ_FOREACH (lc, &parent->cells, entry)
number++;
if (layout_cell_is_tiled(lc))
number++;
if (number <= 1)
return (0);
status = window_get_pane_status(w);
@@ -1528,6 +1533,8 @@ layout_spread_cell(struct window *w, struct layout_cell *parent)
changed = 0;
TAILQ_FOREACH (lc, &parent->cells, entry) {
if (!layout_cell_is_tiled(lc))
continue;
change = 0;
if (parent->type == LAYOUT_LEFTRIGHT) {
change = each - (int)lc->sx;
@@ -1716,7 +1723,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) {
@@ -1728,7 +1735,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;
@@ -1760,7 +1767,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) {
@@ -1785,7 +1792,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);
}
/*
@@ -1802,14 +1809,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;
@@ -1832,7 +1839,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);
@@ -1849,5 +1856,5 @@ layout_insert_tile(struct window *w, struct layout_cell *lc)
}
layout_resize_set_size(w, lc, type, size1);
return (1);
return (0);
}

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 \
@@ -950,16 +949,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);
}
}
}

646
monitor.c Normal file
View File

@@ -0,0 +1,646 @@
/* $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);
}
/* 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 = format_create_defaults(NULL, 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 = format_create_defaults(NULL, 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 = format_create_defaults(NULL, 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 = format_create_defaults(NULL, 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 = format_create_defaults(NULL, 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]"

View File

@@ -56,6 +56,7 @@ struct options_entry {
int cached;
struct style style;
void *monitor_data;
RB_ENTRY(options_entry) entry;
};
@@ -354,6 +355,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 +374,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)
{

View File

@@ -3,8 +3,34 @@ TESTS!= echo *.sh
.PHONY: all $(TESTS)
.NOTPARALLEL: all $(TESTS)
all: $(TESTS)
all:
@failed=0; failures=; \
for test in $(TESTS); do \
printf '%-40s ' "$$test"; \
start=$$(date +%s); \
ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:$$ASAN_OPTIONS"; \
env -i LC_CTYPE=C.UTF-8 ASAN_OPTIONS="$$ASAN_OPTIONS" \
sh "$$test" >/dev/null 2>&1; \
if [ $$? -eq 0 ]; then \
end=$$(date +%s); \
echo "PASS ($$((end - start))s)"; \
else \
end=$$(date +%s); \
echo "FAIL ($$((end - start))s)"; \
failed=1; \
failures="$$failures $$test"; \
fi; \
sleep 1; \
done; \
if [ "$$failed" -ne 0 ]; then \
echo; \
echo "failures:"; \
for test in $$failures; do \
echo " $$test"; \
done; \
fi; \
exit $$failed
$(TESTS):
sh $*.sh
sh $@
sleep 1

Binary file not shown.

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

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

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

@@ -0,0 +1,199 @@
#!/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)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
# 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
if capture | grep -q "$1"; then
sleep 0.5
return 0
fi
sleep 0.5
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# 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
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX new-session -d -s aaa -x 80 -y 24 || 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_for 'B1'
out=$(capture)
echo "$out" | grep -q 'bufa: B1' || fail "bufa missing when it matches"
echo "$out" | grep -q 'bufz: B1' && fail "bufz shown but does not match"
[ "$(echo "$out" | grep -c ': B1')" -eq 1 ] || fail "expected 1 buffer"
$TMUX send-keys -t aaa:0 q
# --- filter by buffer content ------------------------------------------------
$TMUX choose-buffer -t aaa:0 -F 'B2' -f '#{m:*hello*,#{buffer_sample}}' || \
exit 1
wait_for 'B2'
out=$(capture)
echo "$out" | grep -q 'bufa: B2' || fail "bufa missing when content matches"
echo "$out" | grep -q 'bufz: B2' && fail "bufz shown but content not matched"
$TMUX send-keys -t aaa:0 q
# --- no filter shows both buffers ---------------------------------------------
$TMUX choose-buffer -t aaa:0 -F 'B3' || exit 1
wait_for 'B3'
out=$(capture)
echo "$out" | grep -q 'bufa: B3' || fail "bufa missing with no filter"
echo "$out" | grep -q 'bufz: B3' || fail "bufz missing with no filter"
[ "$(echo "$out" | grep -c ': B3')" -eq 2 ] || fail "expected 2 buffers"
$TMUX send-keys -t aaa:0 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_for 'B4'
out=$(capture)
echo "$out" | grep -q 'bufa: B4' || fail "bufa missing with no-match filter"
echo "$out" | grep -q 'bufz: B4' || fail "bufz missing with no-match filter"
echo "$out" | grep -q 'no matches' || fail "no matches indicator missing"
$TMUX send-keys -t aaa:0 q
# --- sort orders ---------------------------------------------------------------
#
# By name bufa sorts first and -r reverses.
$TMUX choose-buffer -t aaa:0 -F 'B5' -O name || exit 1
wait_for 'B5'
capture | grep ': B5' | head -1 | grep -q 'bufa: B5' || \
fail "bufa not first with -O name"
$TMUX send-keys -t aaa:0 q
$TMUX choose-buffer -t aaa:0 -F 'B6' -O name -r || exit 1
wait_for 'B6'
capture | grep ': B6' | head -1 | grep -q 'bufz: B6' || \
fail "bufz not first with -O name -r"
$TMUX send-keys -t aaa:0 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 -q 'bufa' || \
fail "wrong buffer deleted"
$TMUX send-keys -t aaa:0 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_for ': G2'
$TMUX send-keys -t aaa:0 C-t D
wait_buffers 0
$TMUX send-keys -t aaa:0 q
# --- 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_for 'bufa: G3'
$TMUX send-keys -t aaa:0 Enter
i=0
while [ "$i" -lt 50 ]; do
[ "$($TMUX display -p -t aaa:0 '#{pane_in_mode}')" = "0" ] && break
sleep 0.5
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "mode did not exit after Enter"
i=0
while [ "$i" -lt 50 ]; do
$TMUX capture-pane -p -t aaa:0 | grep -q 'hello buffer' && break
sleep 0.5
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "buffer not pasted into pane"
cleanup
exit 0

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

@@ -0,0 +1,140 @@
#!/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)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
# 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
if capture | grep -q "$1"; then
sleep 0.5
return 0
fi
sleep 0.5
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# 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
# 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 || exit 1
$TMUX new-session -d -s bbb -x 80 -y 24 || 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_for 'C1='
out=$(capture)
echo "$out" | grep -q 'C1=aaa' || fail "aaa client missing when it matches"
echo "$out" | grep -q 'C1=bbb' && fail "bbb client shown but does not match"
[ "$(echo "$out" | grep -c 'C1=')" -eq 1 ] || fail "expected 1 client"
$TMUX send-keys -t aaa:0 q
# --- filter keeping only the bbb client -------------------------------------
$TMUX choose-client -t aaa:0 -F 'C2=#{client_session}' \
-f '#{==:#{client_session},bbb}' || exit 1
wait_for 'C2='
out=$(capture)
echo "$out" | grep -q 'C2=bbb' || fail "bbb client missing when it matches"
echo "$out" | grep -q 'C2=aaa' && fail "aaa client shown but does not match"
[ "$(echo "$out" | grep -c 'C2=')" -eq 1 ] || fail "expected 1 client"
$TMUX send-keys -t aaa:0 q
# --- no filter shows both clients -------------------------------------------
$TMUX choose-client -t aaa:0 -F 'C3=#{client_session}' || exit 1
wait_for 'C3='
out=$(capture)
echo "$out" | grep -q 'C3=aaa' || fail "aaa client missing with no filter"
echo "$out" | grep -q 'C3=bbb' || fail "bbb client missing with no filter"
[ "$(echo "$out" | grep -c 'C3=')" -eq 2 ] || fail "expected 2 clients"
$TMUX send-keys -t aaa:0 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_for 'C4='
out=$(capture)
echo "$out" | grep -q 'C4=aaa' || fail "aaa client missing with no-match filter"
echo "$out" | grep -q 'C4=bbb' || fail "bbb client missing with no-match filter"
echo "$out" | grep -q 'no matches' || fail "no matches indicator missing"
$TMUX send-keys -t aaa:0 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
wait_clients 1 || fail "bbb client did not detach"
[ "$($TMUX list-clients -F '#{client_session}')" = "aaa" ] || \
fail "wrong client detached"
cleanup
exit 0

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

@@ -0,0 +1,247 @@
#!/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)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
# 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
if capture | grep -q "$1"; then
sleep 0.5
return 0
fi
sleep 0.5
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# wait_count $marker $n
#
# Wait (up to ~10s) until exactly $n rendered lines contain $marker.
wait_count()
{
i=0
while [ "$i" -lt 50 ]; do
[ "$(capture | grep -c "$1")" -eq "$2" ] && return 0
sleep 0.5
i=$((i + 1))
done
fail "timed out waiting for $2 lines of '$1' (have $(capture | grep -c "$1"))"
}
# 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
# 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 || exit 1
$TMUX split-window -t zzz:0 || exit 1
$TMUX new-window -t zzz || exit 1
$TMUX new-session -d -s aaa -x 80 -y 24 || 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=$(capture)
echo "$out" | grep -q 'aaa: F1' || fail "aaa missing when filter matches it"
echo "$out" | grep -q 'zzz: F1' && fail "zzz shown but no pane matches"
$TMUX send-keys -t aaa:0 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=$(capture)
echo "$out" | grep -q 'zzz: F2' || fail "zzz missing when filter matches it"
echo "$out" | grep -q 'aaa: F2' && fail "aaa shown but no pane matches"
$TMUX send-keys -t aaa:0 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=$(capture)
echo "$out" | grep -q 'zzz: F3' || fail "zzz missing when its pane matches"
echo "$out" | grep -q 'aaa: F3' && fail "aaa shown but no pane matches"
echo "$out" | grep -q '1: F3' || fail "matching pane missing"
$TMUX send-keys -t aaa:0 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_for 'F4'
out=$(capture)
echo "$out" | grep -q 'aaa: F4' || fail "aaa missing with no-match filter"
echo "$out" | grep -q 'zzz: F4' || fail "zzz missing with no-match filter"
echo "$out" | grep -q 'no matches' || fail "no matches indicator missing"
$TMUX send-keys -t aaa:0 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
capture | grep -q 'aaa: F5' || fail "aaa missing with -h"
$TMUX send-keys -t aaa:0 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_for 'F6'
capture | grep 'F6' | head -1 | grep -q 'zzz: F6' || \
fail "zzz not first with -O index"
$TMUX send-keys -t aaa:0 q
$TMUX choose-tree -t aaa:0 -F 'F7' -O name || exit 1
wait_for 'F7'
capture | grep 'F7' | head -1 | grep -q 'aaa: F7' || \
fail "aaa not first with -O name"
$TMUX send-keys -t aaa:0 q
$TMUX choose-tree -t aaa:0 -F 'F8' -O name -r || exit 1
wait_for 'F8'
capture | grep 'F8' | head -1 | grep -q 'zzz: F8' || \
fail "zzz not first with -O name -r"
$TMUX send-keys -t aaa:0 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
wait_count 'G1' 4
$TMUX send-keys -t aaa:0 l
wait_count 'G1' 9
$TMUX send-keys -t aaa:0 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
$TMUX send-keys -t aaa:0 -l '#{==:#{session_name},aaa}'
$TMUX send-keys -t aaa:0 Enter
wait_count 'G2' 3
out=$(capture)
echo "$out" | grep -q 'aaa: G2' || fail "aaa missing with prompt filter"
echo "$out" | grep -q 'zzz: G2' && fail "zzz shown with prompt filter"
$TMUX send-keys -t aaa:0 c
wait_count 'G2' 9
$TMUX send-keys -t aaa:0 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
i=0
while [ "$i" -lt 50 ]; do
[ "$($TMUX list-clients -F '#{client_session}')" = "zzz" ] && break
sleep 0.5
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
# --- 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
wait_for 'Kill window 1'
$TMUX send-keys -t aaa:0 y
wait_count 'G4' 7
[ "$($TMUX list-windows -t zzz -F x | grep -c x)" -eq 1 ] || \
fail "window 1 of zzz not killed"
$TMUX send-keys -t aaa:0 q
cleanup
exit 0

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

@@ -0,0 +1,74 @@
#!/bin/sh
# Control clients must opt in to the current layout serialization.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Lcontrol-client-layout -f/dev/null"
$TMUX kill-server 2>/dev/null
IN1=$(mktemp -u)
IN2=$(mktemp -u)
OUT1=$(mktemp)
OUT2=$(mktemp)
mkfifo "$IN1" "$IN2" || exit 1
trap '$TMUX kill-server 2>/dev/null; rm -f "$IN1" "$IN2" "$OUT1" "$OUT2"' 0 1 15
# Open both ends before starting either client so FIFO setup cannot block.
exec 3<>"$IN1"
exec 4<>"$IN2"
$TMUX new-session -d -x80 -y24 || exit 1
$TMUX split-window -h || exit 1
$TMUX new-pane -x20 -y8 -X5 -Y3 || exit 1
$TMUX -C attach <&3 >"$OUT1" &
$TMUX -C attach -f window-layout-v2 <&4 >"$OUT2" &
sleep 1
printf '%s\n' "list-windows -F 'layout:#{client_flags}:#{window_layout}'" >&3
printf '%s\n' "list-panes -F 'layout:#{client_flags}:#{window_layout}'" >&3
printf '%s\n' "list-sessions -F 'layout:#{client_flags}:#{window_layout}'" >&3
printf '%s\n' "display-message -p 'layout:#{client_flags}:#{window_layout}'" >&3
printf '%s\n' "list-windows -F 'layout:#{client_flags}:#{window_layout}'" >&4
sleep 1
# Default control output is legacy; window-layout-v2 output is current.
awk '/^layout:/ { print }' "$OUT1" | while IFS= read -r line; do
case "$line" in
*window-layout-v2*|*%0,*|*';'*) exit 1 ;;
esac
done || exit 1
[ "$(grep -ac '^layout:' "$OUT1")" -eq 6 ] || exit 1
grep -a '^layout:.*window-layout-v2.*%0,' "$OUT2" >/dev/null || exit 1
# Toggling the flag changes subsequent format expansion for the same client.
printf '%s\n' "refresh-client -f window-layout-v2" >&3
printf '%s\n' "display-message -p 'v2:#{client_flags}:#{window_layout}'" >&3
printf '%s\n' "refresh-client -f '!window-layout-v2'" >&3
printf '%s\n' "display-message -p 'legacy:#{client_flags}:#{window_layout}'" >&3
sleep 1
grep -a '^v2:.*window-layout-v2.*%0,' "$OUT1" >/dev/null || exit 1
grep -a '^legacy:' "$OUT1" | tail -1 | grep -v '%0,' >/dev/null || exit 1
# One layout change is formatted independently for each connected client.
: >"$OUT1"
: >"$OUT2"
$TMUX resize-pane -t%0 -R || exit 1
sleep 1
LEGACY=$(grep -a '^%layout-change ' "$OUT1" | tail -1)
CURRENT=$(grep -a '^%layout-change ' "$OUT2" | tail -1)
[ -n "$LEGACY" ] || exit 1
[ -n "$CURRENT" ] || exit 1
case "$LEGACY" in *%0,*|*';'*) exit 1 ;; esac
case "$CURRENT" in *%0,*) ;; *) exit 1 ;; esac
[ "$(printf '%s' "$CURRENT" | awk '{ print gsub(/%0,/, "") }')" -ge 2 ] ||
exit 1
exec 3>&-
exec 4>&-
$TMUX kill-server 2>/dev/null
exit 0

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 -Ltest$$ -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

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

@@ -0,0 +1,126 @@
#!/bin/sh
# Tests of update-environment handling (environ_update() in environ.c), which
# runs when a client attaches to a session: for each pattern in the session's
# update-environment option, a matching variable in the attaching client's
# environment is copied into the session environment, and a pattern that
# matches nothing clears that name in the session (a NULL-valued entry).
#
# This needs a real attached client with a controllable environment, so - as in
# format-variables.sh - a second server provides one: an inner "tmux attach"
# runs inside a pane of the second server, and the variables to import are set
# in that inner command's own environment.
#
# environ.sh covers the set-environment/show-environment commands themselves.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
# A second server on its own socket hosts the pane that runs the inner client.
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
}
fail()
{
echo "$1"
cleanup
exit 1
}
# check_value $var $expected
#
# Compare show-environment of $var on the session with $expected.
check_value()
{
out=$($TMUX show-environment -t main "$1" 2>&1)
if [ "$out" != "$2" ]; then
echo "show-environment $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
cleanup
exit 1
fi
}
# wait_clients $n
#
# Wait (up to ~10s) until the test server has exactly $n clients attached.
wait_clients()
{
i=0
while [ "$i" -lt 10 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 1
i=$((i + 1))
done
return 1
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# The session imports MYVAR and ABSENTVAR by exact name and anything matching
# the glob TEST_*; nothing else is imported.
$TMUX set -g update-environment "MYVAR ABSENTVAR TEST_*" || exit 1
# Seed the session so the effect of attaching is visible: MYVAR will be
# overwritten by the client's value and ABSENTVAR will be cleared.
$TMUX set-environment -t main MYVAR oldvalue || exit 1
$TMUX set-environment -t main ABSENTVAR pre-existing || exit 1
# --- attach a client whose environment carries the imported variables ------
#
# MYVAR and TEST_GLOB are present in the inner client's environment; ABSENTVAR
# is deliberately absent; OTHER is present but not named by update-environment.
$TMUX2 new-session -d -x 90 -y 30 \
"MYVAR=fromclient TEST_GLOB=globval OTHER=nope $TMUX attach -t main" \
|| fail "could not start inner client"
wait_clients 1 || fail "no client attached to test server"
# MYVAR matched by name and present in the client -> imported (overwrites).
check_value MYVAR "MYVAR=fromclient"
# TEST_GLOB matched by the TEST_* glob and present -> imported.
check_value TEST_GLOB "TEST_GLOB=globval"
# ABSENTVAR named but not in the client environment -> cleared (NULL value,
# printed as -NAME).
check_value ABSENTVAR "-ABSENTVAR"
# OTHER is in the client environment but not named by update-environment, so it
# is not imported at all.
out=$($TMUX show-environment -t main OTHER 2>&1)
[ "$out" = "unknown variable: OTHER" ] || \
fail "OTHER was imported but should not have been: '$out'"
# --- -E disables the update-environment import -----------------------------
#
# Detach the client (kill its host server), reset the session variables, then
# reattach with -E: the session values must be left untouched.
$TMUX2 kill-server 2>/dev/null
wait_clients 0 || fail "client did not detach"
$TMUX set-environment -t main MYVAR oldvalue2 || exit 1
$TMUX set-environment -t main ABSENTVAR pre2 || exit 1
$TMUX2 new-session -d -x 90 -y 30 \
"MYVAR=fromclientE $TMUX attach -E -t main" \
|| fail "could not start inner -E client"
wait_clients 1 || fail "no -E client attached to test server"
# With -E neither variable is touched by the attach.
check_value MYVAR "MYVAR=oldvalue2"
check_value ABSENTVAR "ABSENTVAR=pre2"
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
fail "server died after update-environment tests"
fi
cleanup
exit 0

187
regress/environ.sh Normal file
View File

@@ -0,0 +1,187 @@
#!/bin/sh
# Tests of the environment engine (environ.c) and its two commands,
# set-environment/setenv (cmd-set-environment.c) and show-environment/showenv
# (cmd-show-environment.c).
#
# The environment is a red-black tree of name/value entries held at two
# scopes: the global environment and each session's environment. An entry
# may be marked hidden (ENVIRON_HIDDEN) or "cleared" (a NULL value, which
# masks an inherited variable rather than removing the entry). This
# exercises: set and show at global and session scope; the shell (-s) output
# form and its escaping of $ ` " and \; hidden variables (-h) and their
# filtering; -r cleared entries printed as -NAME / "unset NAME;"; -u removal;
# -F expansion of the value at set time; the plain "NAME=value" and "%hidden"
# config-file assignment forms (environ_put); and the full set of argument and
# target errors from both commands.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check_value $args $expected
#
# Run show-environment with $args and compare the single-line output.
check_value()
{
out=$($TMUX show-environment $1 2>&1)
if [ "$out" != "$2" ]; then
echo "show-environment $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $cmd...
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# --- set and show at session scope ----------------------------------------
check_ok set-environment FOO bar
check_value "FOO" "FOO=bar"
# setenv is an alias for set-environment; showenv for show-environment.
check_ok setenv FOO2 bar2
out=$($TMUX showenv FOO2 2>&1)
[ "$out" = "FOO2=bar2" ] || { echo "setenv/showenv alias failed: '$out'"; exit 1; }
# --- set and show at global scope -----------------------------------------
#
# The global scope is separate from the session scope: a session variable is
# not visible in the global environment.
check_ok set-environment -g GVAR gval
check_value "-g GVAR" "GVAR=gval"
check_fail "unknown variable: FOO" show-environment -g FOO
# --- overwrite replaces the value -----------------------------------------
check_ok set-environment FOO baz
check_value "FOO" "FOO=baz"
# --- shell (-s) output form and escaping ----------------------------------
#
# With -s the value is printed as a shell assignment with export, and the
# characters $ ` " and \ are backslash-escaped (POSIX double-quote rules).
check_ok set-environment ESC 'a$b`c"d\e'
check_value "-s ESC" 'ESC="a\$b\`c\"d\\e"; export ESC;'
# --- -F expands the value as a format at set time -------------------------
#
# With a resolvable target the value is expanded once when set; the stored
# value is the result, not the format.
check_ok set-environment -t main -F EXP '#{session_name}'
check_value "EXP" "EXP=main"
# --- hidden variables (-h) ------------------------------------------------
#
# set-environment -h marks a variable hidden. show-environment hides it by
# default and only prints it when -h is given; conversely a normal variable is
# omitted when -h is given.
check_ok set-environment -h SECRET s3cr
check_value "SECRET" ""
check_value "-h SECRET" "SECRET=s3cr"
check_value "-h FOO" ""
# --- -r clears a variable (NULL value, masks inheritance) -----------------
#
# A cleared entry still exists but has no value: normal form prints "-NAME"
# and shell form prints "unset NAME;".
check_ok set-environment -r FOO
check_value "FOO" "-FOO"
check_value "-s FOO" "unset FOO;"
# --- -u removes a variable entirely ---------------------------------------
check_ok set-environment -u FOO
check_fail "unknown variable: FOO" show-environment FOO
# --- show with no name lists every (non-hidden) variable ------------------
check_ok set-environment -g LISTA 1
check_ok set-environment -g LISTB 2
check_ok set-environment -gh LISTHID 3
out=$($TMUX show-environment -g 2>&1)
echo "$out" | grep -q '^LISTA=1$' || { echo "list missing LISTA"; exit 1; }
echo "$out" | grep -q '^LISTB=2$' || { echo "list missing LISTB"; exit 1; }
# A hidden variable is not listed without -h.
echo "$out" | grep -q '^LISTHID' && { echo "list showed hidden var without -h"; exit 1; }
# With -h only hidden variables are listed.
$TMUX show-environment -gh 2>&1 | grep -q '^LISTHID=3$' || \
{ echo "list -h missing LISTHID"; exit 1; }
# --- config-file assignment forms (environ_put) ---------------------------
#
# A bare NAME=value line in a config file sets a global variable; a "%hidden"
# NAME=value line sets a hidden one. Start a second server from such a config
# and read the values back.
CONF=$(mktemp)
cat > "$CONF" <<EOF
CFGVAR=fromconfig
%hidden CFGHID=hiddencfg
EOF
CTMUX="$TEST_TMUX -Ltest2 -f$CONF"
$CTMUX kill-server 2>/dev/null
$CTMUX new-session -d -s c -x 80 -y 24 || { rm -f "$CONF"; exit 1; }
out=$($CTMUX show-environment -g CFGVAR 2>&1)
[ "$out" = "CFGVAR=fromconfig" ] || \
{ echo "config assignment failed: '$out'"; $CTMUX kill-server; rm -f "$CONF"; exit 1; }
out=$($CTMUX show-environment -gh CFGHID 2>&1)
[ "$out" = "CFGHID=hiddencfg" ] || \
{ echo "config %hidden failed: '$out'"; $CTMUX kill-server; rm -f "$CONF"; exit 1; }
# The %hidden variable is hidden from a plain show.
out=$($CTMUX show-environment -g CFGHID 2>&1)
[ "$out" = "" ] || \
{ echo "config %hidden not hidden: '$out'"; $CTMUX kill-server; rm -f "$CONF"; exit 1; }
$CTMUX kill-server 2>/dev/null
rm -f "$CONF"
# --- set-environment argument errors --------------------------------------
check_fail "empty variable name" set-environment "" x
check_fail "variable name contains =" set-environment "A=B" x
check_fail "can't specify a value with -u" set-environment -u FOO val
check_fail "can't specify a value with -r" set-environment -r FOO val
check_fail "no value specified" set-environment NOVAL
# --- show-environment errors ----------------------------------------------
check_fail "unknown variable: MISSING" show-environment MISSING
# --- unresolvable target errors -------------------------------------------
check_fail "no such session: nosuch" show-environment -t nosuch FOO
check_fail "no such session: nosuch" set-environment -t nosuch FOO bar
assert_alive "after environ tests"
$TMUX kill-server 2>/dev/null
exit 0

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 -Ltest -f/dev/null"
ESC=$(printf '\033')
# test_format $format $expected [$target]
#
# Expand $format with display-message and compare with $expected. If $target
# is given it is passed to display-message with -t.
test_format()
{
fmt="$1"
exp="$2"
target="$3"
if [ -n "$target" ]; then
out=$($TMUX display-message -t "$target" -p "$fmt")
else
out=$($TMUX display-message -p "$fmt")
fi
if [ "$out" != "$exp" ]; then
echo "Format test failed for '$fmt'."
echo "Expected: '$exp'"
echo "But got '$out'"
exit 1
fi
}
# test_expand $format $expected
#
# Expand $format in a plain format_expand context (list-windows -F on the
# single-window "tf" session) rather than the format_expand_time context of
# display-message. This matters for t/f: display-message runs the whole format
# through strftime(3), so a strftime specifier there must be doubled (%%H); in a
# format_expand context a single specifier (%H) is applied directly to the
# variable's time.
test_expand()
{
fmt="$1"
exp="$2"
out=$($TMUX list-windows -t tf -F "$fmt")
if [ "$out" != "$exp" ]; then
echo "Format test failed for '$fmt'."
echo "Expected: '$exp'"
echo "But got '$out'"
exit 1
fi
}
# assert_alive
#
# Check that the server is still responding (used after operations that could
# in principle crash it, such as recursion and division by zero).
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server did not survive: $1"
exit 1
fi
}
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# Single-window session used by test_expand for format_expand-context tests.
$TMUX new-session -d -s tf || exit 1
# User options used as inputs. Modifiers operate on variable names, so plain
# literals must be provided via options (or a nested #{l:...}). They are set
# globally (-g) so they are visible from every session, including the "tf"
# session used by test_expand.
$TMUX set -g @s 'abcdefghij' || exit 1
$TMUX set -g @path '/usr/local/bin/foo' || exit 1
$TMUX set -g @name 'window-name' || exit 1
$TMUX set -g @greek 'αβγ' || exit 1 # 6 bytes, 3 columns wide
$TMUX set -g @cjk '中文' || exit 1 # 6 bytes, 4 columns wide
$TMUX set -g @host 'myhost' || exit 1
$TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC
$TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q:
$TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e:
$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 -Ltest -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$1"
cleanup
exit 1
}
# click COL ROW
#
# Write an SGR mouse press then release (button 0) at 1-based COL/ROW to the
# outer pane holding the inner client.
click()
{
col="$1"
row="$2"
seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' "$col" "$row" "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
# Inner session with a single pane running cat, so its content is exactly what
# we send it.
$TMUX new-session -d -s cov -x 80 -y 24 'cat' || exit 1
$TMUX set -g mouse on
sleep 1
$TMUX send-keys -t cov:0.0 'alpha beta gamma' Enter
sleep 1
# Record every pane mouse variable when the pane is clicked.
$TMUX bind -n MouseDown1Pane run-shell \
"$TMUX set -g @m 'x=#{mouse_x} y=#{mouse_y} word=#{mouse_word} line=#{mouse_line} pane=#{mouse_pane} hl=[#{mouse_hyperlink}]'"
# Attach a real client inside an outer tmux pane. Clicks all target the first
# row, which lines up with the inner client regardless of the outer status line.
$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t cov" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Click column 3, row 1: over the first word ("alpha") of the first line.
click 3 1
M=$($TMUX show -gv @m 2>/dev/null)
[ -n "$M" ] || fail "Mouse binding did not fire (no @m)."
# mouse_x is 0-based column (SGR column 3 -> x 2); mouse_y is 0-based row 0.
case "$M" in
*"x=2 "*) ;;
*) fail "Unexpected mouse_x in: $M" ;;
esac
case "$M" in
*"y=0 "*) ;;
*) fail "Unexpected mouse_y in: $M" ;;
esac
# mouse_word is the word under the cursor, mouse_line the whole line.
case "$M" in
*"word=alpha "*) ;;
*) fail "Unexpected mouse_word in: $M" ;;
esac
case "$M" in
*"line=alpha beta gamma "*) ;;
*) fail "Unexpected mouse_line in: $M" ;;
esac
# A click in a different column selects a different word.
click 8 1
M=$($TMUX show -gv @m 2>/dev/null)
case "$M" in
*"word=beta "*) ;;
*) fail "Unexpected mouse_word for second click in: $M" ;;
esac
# The same variables have a separate path when the pane is in a mode (the word
# and line come from the mode, not the live grid). A binding in the copy-mode
# key table fires while copy mode is active.
$TMUX bind -T copy-mode MouseDown1Pane run-shell \
"$TMUX set -g @cm 'x=#{mouse_x} word=#{mouse_word} line=#{mouse_line}'"
$TMUX copy-mode -t cov:0.0
sleep 1
click 8 1
CM=$($TMUX show -gv @cm 2>/dev/null)
case "$CM" in
*"word=beta"*) ;;
*) fail "Unexpected copy-mode mouse_word in: $CM" ;;
esac
$TMUX send-keys -t cov:0.0 -X cancel
sleep 1
# Hyperlinks: a new window whose pane emits an OSC 8 hyperlink over the text
# "LINKED". Clicking it reports the target URL via mouse_hyperlink (this drives
# the grid hyperlink lookup). The emitter is written to a small script to keep
# the escape sequence readable.
LINKSH="${TMPDIR:-/tmp}/fmt-mouse-link-$$.sh"
cat >"$LINKSH" <<'EOF'
#!/bin/sh
printf '\033]8;;http://example.com\033\\LINKED\033]8;;\033\\\n'
exec cat
EOF
chmod +x "$LINKSH"
$TMUX neww -t cov: -n link "$LINKSH"
sleep 1
$TMUX select-window -t cov:link
sleep 1
click 3 1
M=$($TMUX show -gv @m 2>/dev/null)
rm -f "$LINKSH"
case "$M" in
*"hl=[http://example.com]"*) ;;
*) fail "Unexpected mouse_hyperlink in: $M" ;;
esac
cleanup
exit 0

View File

@@ -0,0 +1,295 @@
#!/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 -Lformat-render-contexts-$$ -f/dev/null"
TMUX2="$TEST_TMUX -Lformat-render-contexts-outer-$$ -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=$(run_cmd $TMUX "$@" 2>&1)
rc=$?
bounded "$name" "$out"
[ "$rc" -eq 0 ] || fail "$name failed: $out"
printf '%s' "$out"
}
capture()
{
out=$(run_cmd $TMUX2 capture-pane -p -t out:0 2>/dev/null)
rc=$?
bounded "capture-pane" "$out"
[ "$rc" -eq 0 ] || fail "capture-pane failed"
printf '%s\n' "$out"
}
capture_esc()
{
out=$(run_cmd $TMUX2 capture-pane -pe -t out:0 2>/dev/null)
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

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 -Ltest -f/dev/null"
# A second server on its own socket provides a real terminal (an inner client
# attached inside one of its panes) so client terminal variables are populated.
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
# Every variable name in format_table[]. Kept as a plain word list so it can be
# iterated with normal shell word splitting.
NAMES="
active_window_index
alternate_on
alternate_saved_x
alternate_saved_y
bracket_paste_flag
buffer_created
buffer_full
buffer_mode_format
buffer_name
buffer_sample
buffer_size
client_activity
client_cell_height
client_cell_width
client_colours
client_control_mode
client_created
client_discarded
client_flags
client_height
client_key_table
client_last_session
client_mode_format
client_name
client_pid
client_prefix
client_readonly
client_session
client_termfeatures
client_termname
client_termtype
client_theme
client_tty
client_uid
client_user
client_utf8
client_width
client_written
config_files
cursor_blinking
cursor_character
cursor_colour
cursor_flag
cursor_shape
cursor_very_visible
cursor_x
cursor_y
history_all_bytes
history_bytes
history_limit
history_size
host
host_short
insert_flag
keypad_cursor_flag
keypad_flag
last_window_index
mouse_all_flag
mouse_any_flag
mouse_button_flag
mouse_hyperlink
mouse_line
mouse_pane
mouse_sgr_flag
mouse_standard_flag
mouse_status_line
mouse_status_range
mouse_utf8_flag
mouse_word
mouse_x
mouse_y
next_session_id
origin_flag
pane_active
pane_at_bottom
pane_at_left
pane_at_right
pane_at_top
pane_bg
pane_bottom
pane_current_command
pane_current_path
pane_dead
pane_dead_signal
pane_dead_status
pane_dead_time
pane_fg
pane_flags
pane_floating_flag
pane_format
pane_height
pane_id
pane_in_mode
pane_index
pane_input_off
pane_key_mode
pane_last
pane_left
pane_marked
pane_marked_set
pane_mode
pane_path
pane_pb_progress
pane_pb_state
pane_pid
pane_pipe
pane_pipe_pid
pane_right
pane_search_string
pane_start_command
pane_start_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

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

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

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

@@ -0,0 +1,39 @@
#!/bin/sh
. ./input-common.inc
start_pane cursor 10 3 'ABCDE\r\033[2Cxy\033[1D!\033[4GZ\n'
check_capture cursor 'ABxZE'
check_cursor cursor '0,1'
start_pane saverc 10 3 'abc\0337\033[2;5HXY\0338Z\n'
check_capture saverc 'abcZ
XY'
check_cursor saverc '0,1'
start_pane hvp 10 4 'A\033[3dB\033[5GC\033[2;2fD\n'
check_capture hvp 'A
D
B C'
check_cursor hvp '0,2'
start_pane cursorlines 8 4 'A\033[2BB\033[1FC\033[1AD\n'
check_capture cursorlines 'AD
C
B'
check_cursor cursorlines '0,1'
start_pane tabs 12 3 'a\tb\n'
check_capture tabs 'a b'
check_cursor tabs '0,1'
start_pane tabclear 12 3 '\033H\ta\033[3g\r\tb\n'
check_capture tabclear ' a b'
check_cursor tabclear '0,1'
start_pane cbt 16 3 '0123456789\r\033[10C\033[Zx\n'
check_capture cbt '01234567x9'
check_cursor cbt '0,1'
$TMUX kill-server 2>/dev/null
exit $exit_status

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

@@ -0,0 +1,54 @@
#!/bin/sh
. ./input-common.inc
start_pane dch 10 3 'abcdef\r\033[3C\033[2PXY\n'
check_capture dch 'abcXY'
start_pane ich 10 3 'abcdef\r\033[3C\033[2@XY\n'
check_capture ich 'abcXYdef'
start_pane erase 10 3 'abcdef\r\033[3C\033[KZ\n'
check_capture erase 'abcZ'
start_pane el1 10 3 'abcdef\r\033[3C\033[1KZ\n'
check_capture el1 ' Zef'
start_pane ech 10 3 'abcdef\r\033[3C\033[2XX\n'
check_capture ech 'abcX f'
start_pane ed 10 3 'one\ntwo\033[2;2H\033[JX\n'
check_capture ed 'one
tX'
start_pane ed1 10 3 'one\ntwo\033[2;2H\033[1JX\n'
check_capture ed1 '
Xo'
start_pane ed2 10 3 'one\ntwo\033[2JZ\n'
check_capture ed2 '
Z'
start_pane il 8 4 '111\n222\n333\033[2;1H\033[LAAA\n'
check_capture il '111
AAA
222
333'
start_pane dl 8 4 '111\n222\n333\033[2;1H\033[MZZZ\n'
check_capture dl '111
ZZZ'
start_pane irm 10 3 'abcdef\r\033[4h\033[3CXY\033[4lZ\n'
check_capture irm 'abcXYZef'
start_pane rep 10 3 'A\033[4bB\n'
check_capture rep 'AAAAAB'
start_pane decaln 6 3 '\033#8'
check_capture decaln 'EEEEEE
EEEEEE
EEEEEE'
$TMUX kill-server 2>/dev/null
exit $exit_status

View File

@@ -0,0 +1,42 @@
#!/bin/sh
. ./input-common.inc
start_cmd csi-param-discard 8 3 \
"perl -e 'print qq{\e[}, q{1} x 80, qq{\030OK}'; sleep 2"
check_capture csi-param-discard 'OK'
start_cmd csi-interm-discard 8 3 \
"perl -e 'print qq{\e[ \030OK}'; sleep 2"
check_capture csi-interm-discard 'OK'
start_cmd osc-discard 8 3 \
"perl -e 'print qq{\e]2;}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2"
check_capture osc-discard 'OK'
start_cmd apc-discard 8 3 \
"perl -e 'print qq{\e_}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2"
check_capture apc-discard 'OK'
start_pane unknown-csi 8 3 '\033[?9999zOK'
check_capture unknown-csi 'OK'
start_pane unknown-osc 8 3 '\033]999;bad\aOK'
check_capture unknown-osc 'OK'
start_pane malformed-osc 8 3 '\033]8;id=a:id=b;http://bad\aX\033]8;id=no-separator\aY\033]9;4;5;200\a\033]9;4;z\a\033]10;notacolour\a\033]11;notacolour\a\033]12;notacolour\a\033]4;999;red\a\033]104;999\a\033]52bad\a\033]52;c;@@@\aOK'
check_capture malformed-osc 'XYOK'
check_raw_matches malformed-osc \
'C 0,0 data=\(1,1,X\).* link=NONE linkid=NONE' \
'C 0,1 data=\(1,1,Y\).* link=NONE linkid=NONE' \
'C 0,2 data=\(1,1,O\).* link=NONE linkid=NONE' \
'C 0,3 data=\(1,1,K\).* link=NONE linkid=NONE'
start_pane malformed-dcs 8 3 '\033P$qBAD\033\\OK'
check_capture malformed-dcs 'OK^[P0$r
^[\'
start_pane malformed-utf8 8 3 '\360\200\200\200A\355\240\200B'
check_capture malformed-utf8 '<27>A<EFBFBD>B'
exit $exit_status

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

@@ -0,0 +1,15 @@
#!/bin/sh
. ./input-common.inc
start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n'
check_capture alternate 'MAINZ'
start_pane osc133 10 4 '\033]133;A\007prompt\n\033]133;C\007output\n'
check_capture osc133 'prompt
output'
check_flags osc133 'P prompt
O output'
$TMUX kill-server 2>/dev/null
exit $exit_status

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

@@ -0,0 +1,37 @@
#!/bin/sh
. ./input-common.inc
start_pane hyperlink 20 3 '\033]8;id=1;https://example.com\033\\link\033]8;;\033\\ plain\n'
check_capture hyperlink 'link plain'
check_flags hyperlink 'HX link plain'
$TMUX capture-pane -peH -t hyperlink: -S 0 -E - >/dev/null || exit 1
start_pane palette 20 3 '\033]4;1;rgb:11/22/33;2;red\007\033]104;1;2\007X\n'
check_capture palette 'X'
start_pane osc-colours 20 3 '\033]10;rgb:11/22/33\007\033]11;rgb:44/55/66\007\033]12;rgb:77/88/99\007\033]110\007\033]111\007\033]112\007X\n'
check_capture osc-colours 'X'
start_pane progress 20 3 '\033]9;4;1;25\007\033]9;4;0\007\033]9;4;5;200\007X\n'
check_capture progress 'X'
start_pane rename 20 3 '\033krenamed\033\\X\n'
check_capture rename 'X'
start_pane apc-title 20 3 '\033_test-title\033\\X\n'
check_capture apc-title 'X'
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 20 -y 3 -s osc52 "sleep 2" || exit 1
$TMUX set-option -s set-clipboard on || exit 1
$TMUX respawn-pane -k -t osc52: \
"printf '\033]52;c;SGVsbG8=\007'; sleep 2" || exit 1
sleep 0.3
$TMUX save-buffer -b buffer0 - >"$TMP"
printf "Hello" >"$EXP"
cmp "$TMP" "$EXP" || fail "osc52"
$TMUX kill-server 2>/dev/null
exit $exit_status

View File

@@ -0,0 +1,84 @@
#!/bin/sh
. ./input-common.inc
start_pane bs 8 3 'abc\bd'
check_capture bs 'abd'
check_raw_matches bs \
'C 0,0 data=\(1,1,a\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1,b\) flags=NONE\[0\]' \
'C 0,2 data=\(1,1,d\) flags=NONE\[0\]'
start_pane nel 8 3 'A\033EB'
check_capture nel 'A
B'
check_raw_matches nel \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 1,0 data=\(1,1,B\) flags=NONE\[0\]'
start_pane tabstops 16 3 '\033H1\t2\033[3g\r\t3'
check_raw_matches tabstops \
'C 0,0 data=\(1,1,1\) flags=NONE\[0\]' \
'C 0,8 data=\(1,1,2\) flags=NONE\[0\]' \
'C 0,15 data=\(1,1,3\) flags=NONE\[0\]'
start_pane decaln 6 3 '\033#8'
check_raw_matches decaln \
'C 0,0 data=\(1,1,E\) flags=NONE\[0\]' \
'C 1,5 data=\(1,1,E\) flags=NONE\[0\]' \
'C 2,5 data=\(1,1,E\) flags=NONE\[0\]'
start_pane charset 8 3 '\033(0qxl\033(BZ'
check_raw_matches charset \
'C 0,0 data=\(1,1,q\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \
'C 0,1 data=\(1,1,x\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \
'C 0,2 data=\(1,1,l\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \
'C 0,3 data=\(1,1,Z\) flags=NONE\[0\] attr=NONE\[0\]'
start_pane g1charset 8 3 '\033)0\016q\017Z'
check_raw_matches g1charset \
'C 0,0 data=\(1,1,q\) flags=NONE\[0\] attr=CHARSET\[[0-9a-f]+\]' \
'C 0,1 data=\(1,1,Z\) flags=NONE\[0\] attr=NONE\[0\]'
start_pane csisave 8 3 '\033[3;3HS\033[s\033[1;1HA\033[uR'
check_raw_matches csisave \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 2,2 data=\(1,1,S\) flags=NONE\[0\]' \
'C 2,3 data=\(1,1,R\) flags=NONE\[0\]'
start_pane alternate 8 3 'main\033[?1049halt\033[?1049lback'
check_capture alternate 'mainback'
check_raw_matches alternate \
'C 0,0 data=\(1,1,m\) flags=NONE\[0\]' \
'C 0,4 data=\(1,1,b\) flags=NONE\[0\]' \
'C 0,7 data=\(1,1,k\) flags=NONE\[0\]'
start_pane sync 8 3 '\033P=1signored\033\\A\033P=2s\033\\B'
check_raw_matches sync \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1,B\) flags=NONE\[0\]'
start_pane private 8 3 '\033[?25lA\033[?25hB\033[?1000hC\033[?1000lD'
check_raw_matches private \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1,B\) flags=NONE\[0\]' \
'C 0,2 data=\(1,1,C\) flags=NONE\[0\]' \
'C 0,3 data=\(1,1,D\) flags=NONE\[0\]'
start_pane ris 8 3 'A\033cB'
check_capture ris 'B'
check_raw_matches ris \
'C 0,0 data=\(1,1,B\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\]'
start_pane keypad 8 3 '\033=A\033>B'
check_raw_matches keypad \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1,B\) flags=NONE\[0\]'
start_pane cursorstyle 8 3 '\033[5 qA\033[0 qB'
check_raw_matches cursorstyle \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1,B\) flags=NONE\[0\]'
exit $exit_status

View File

@@ -0,0 +1,33 @@
#!/bin/sh
. ./input-common.inc
start_pane absolute 8 4 'A\033[3;5HB\033[2GC\033[2D!'
check_capture absolute 'A
!C B'
check_cursor absolute '1,2'
check_raw_matches absolute \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 2,4 data=\(1,1,B\) flags=NONE\[0\]' \
'C 2,0 data=\(1,1,!\) flags=NONE\[0\]' \
'C 2,1 data=\(1,1,C\) flags=NONE\[0\]'
start_pane savecursor 8 4 '\033[4;4HS\0337\033[1;1HA\0338R'
check_capture savecursor 'A
SR'
check_cursor savecursor '5,3'
check_raw_matches savecursor \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 3,3 data=\(1,1,S\) flags=NONE\[0\]' \
'C 3,4 data=\(1,1,R\) flags=NONE\[0\]'
start_pane origin 8 5 '\033[2;4r\033[?6h\033[1;1HO\033[3;1HP\033[?6lQ'
check_raw_matches origin \
'C 1,0 data=\(1,1,O\) flags=NONE\[0\]' \
'C 3,0 data=\(1,1,P\) flags=NONE\[0\]' \
'C 0,0 data=\(1,1,Q\) flags=NONE\[0\]'
exit $exit_status

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

@@ -0,0 +1,47 @@
#!/bin/sh
. ./input-common.inc
start_pane erasechars 8 3 'ABCDEFGH\r\033[3C\033[2X'
check_capture erasechars 'ABC FGH'
check_raw_matches erasechars \
'C 0,3 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,4 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,5 data=\(1,1,F\) flags=NONE\[0\]'
start_pane deletechars 8 3 'ABCDEFGH\r\033[3C\033[3P'
check_capture deletechars 'ABCGH'
check_raw_matches deletechars \
'C 0,3 data=\(1,1,G\) flags=NONE\[0\]' \
'C 0,4 data=\(1,1,H\) flags=NONE\[0\]' \
'C 0,5 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]'
start_pane insertchars 8 3 'ABCDEF\r\033[3C\033[2@xy'
check_capture insertchars 'ABCxyDEF'
check_raw_matches insertchars \
'C 0,3 data=\(1,1,x\) flags=NONE\[0\]' \
'C 0,4 data=\(1,1,y\) flags=NONE\[0\]' \
'C 0,5 data=\(1,1,D\) flags=NONE\[0\]'
start_pane eraseline 8 3 'ABCDEFGH\r\033[4C\033[K'
check_capture eraseline 'ABCD'
check_raw_matches eraseline \
'C 0,4 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,7 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]'
start_pane erasescreen 8 3 '1111111\033[2;1H2222222\033[H\033[JZ'
check_capture erasescreen 'Z'
check_raw_matches erasescreen \
'^G 8x3 \(0/0\)$' \
'C [0-9]+,0 data=\(1,1,Z\) flags=NONE\[0\]' \
'C [0-9]+,1 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\]'
start_pane tabs 12 3 'A\tB\033[2g\r\033[IC'
check_raw_matches tabs \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,1 data=\(7,7, \) flags=TAB\[[0-9a-f]+\]' \
'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\]' \
'C 0,8 data=\(1,1,B\) flags=NONE\[0\]' \
'C 0,0 data=\(1,1,C\) flags=NONE\[0\]'
exit $exit_status

View File

@@ -0,0 +1,23 @@
#!/bin/sh
. ./input-common.inc
start_pane_hlimit trim 6 3 'one\ntwo\nthree\nfour\nfive\nsix' 2
check_raw_matches trim \
'^G 6x3 \(2/2\)$' \
'L 0 \(-\) flags=NONE\[0\]' \
'L 1 \(-\) flags=NONE\[0\]'
$TMUX clear-history -t trim:
check_raw_matches trim \
'^G 6x3 \(0/2\)$' \
'C 0,0 data=\(1,1,f\) flags=NONE\[0\]' \
'C 2,0 data=\(1,1,s\) flags=NONE\[0\]'
start_pane_hlimit edhistory 6 3 'one\ntwo\nthree\033[H\033[JZ' 5
check_raw_matches edhistory \
'^G 6x3 \([1-9][0-9]*/5\)$' \
'L [0-9]+ \(-\) flags=NONE\[0\]' \
'C [0-9]+,0 data=\(1,1,Z\) flags=NONE\[0\]'
exit $exit_status

View File

@@ -0,0 +1,21 @@
#!/bin/sh
. ./input-common.inc
start_pane_history reflow 8 4 'abcdefgh\nijklmnop\nqrstuvwx\nyz'
$TMUX resize-window -t reflow: -x 4 -y 4
sleep 0.2
check_raw_matches reflow \
'^G 4x4 \([0-9]+/2000\)$' \
'L [0-9]+ \([0-9-]+\) flags=WRAPPED\[[0-9a-f]+\]' \
'C [0-9]+,0 data=\(1,1,a\) flags=NONE\[0\]' \
'C [0-9]+,3 data=\(1,1,d\) flags=NONE\[0\]'
$TMUX resize-window -t reflow: -x 12 -y 4
sleep 0.2
check_raw_matches reflow \
'^G 12x4 \([0-9]+/2000\)$' \
'C [0-9]+,0 data=\(1,1,a\) flags=NONE\[0\]' \
'C [0-9]+,7 data=\(1,1,h\) flags=NONE\[0\]'
exit $exit_status

View File

@@ -0,0 +1,39 @@
#!/bin/sh
. ./input-common.inc
start_pane_history history 6 3 'one\ntwo\nthree\nfour\nfive'
check_raw_matches history \
'^G 6x3 \([1-9][0-9]*/2000\)$' \
'L [0-9]+ \(-\) flags=NONE\[0\]' \
'C [0-9]+,0 data=\(1,1,o\) flags=NONE\[0\]'
start_pane_history index 6 4 'A\nB\nC\033[2;3r\033[2;1HX\033D\033DY'
check_raw_matches index \
'C [0-9]+,0 data=\(1,1,X\) flags=NONE\[0\]' \
'C [0-9]+,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C [0-9]+,1 data=\(1,1,Y\) flags=NONE\[0\]'
start_pane reverse 6 4 'A\nB\nC\033[2;3r\033[2;1H\033MY'
check_raw_matches reverse \
'C 1,0 data=\(1,1,Y\) flags=NONE\[0\]' \
'C 2,0 data=\(1,1,B\) flags=NONE\[0\]'
start_pane insertline 6 4 'A\nB\nC\033[2;3r\033[2;1H\033[LY'
check_raw_matches insertline \
'C 1,0 data=\(1,1,Y\) flags=NONE\[0\]' \
'C 2,0 data=\(1,1,B\) flags=NONE\[0\]'
start_pane deleteline 6 4 'A\nB\nC\033[2;3r\033[2;1H\033[MY'
check_raw_matches deleteline \
'C 1,0 data=\(1,1,Y\) flags=NONE\[0\]' \
'C 2,0 data=\(1,1, \) flags=NONE\[0\]'
start_pane region-edge 6 4 'top\033[2;3rmid\033[2;1H\033D\033Mbot'
check_raw_matches region-edge \
'C 0,0 data=\(1,1,m\) flags=NONE\[0\]' \
'C 1,0 data=\(1,1,b\) flags=NONE\[0\]' \
'C 2,0 data=\(1,1, \) flags=NONE\[0\]' \
'C 3,0 data=\(1,1, \) flags=NONE\[0\]'
exit $exit_status

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

@@ -0,0 +1,39 @@
#!/bin/sh
. ./input-common.inc
start_pane attrs 16 3 '\033[1mB\033[2mD\033[3mI\033[4mU\033[5mK\033[7mR\033[8mH\033[9mS\033[53mO'
check_raw_matches attrs \
'C 0,0 data=\(1,1,B\) flags=NONE\[0\] attr=BRIGHT\[[0-9a-f]+\]' \
'C 0,1 data=\(1,1,D\) flags=NONE\[0\] attr=BRIGHT,DIM\[[0-9a-f]+\]' \
'C 0,2 data=\(1,1,I\) flags=NONE\[0\] attr=BRIGHT,DIM,ITALICS\[[0-9a-f]+\]' \
'C 0,3 data=\(1,1,U\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,ITALICS\[[0-9a-f]+\]' \
'C 0,5 data=\(1,1,R\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,BLINK,REVERSE,ITALICS\[[0-9a-f]+\]' \
'C 0,7 data=\(1,1,S\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,BLINK,REVERSE,HIDDEN,ITALICS,STRIKETHROUGH\[[0-9a-f]+\]' \
'C 0,8 data=\(1,1,O\) flags=NONE\[0\] attr=BRIGHT,DIM,UNDERSCORE,BLINK,REVERSE,HIDDEN,ITALICS,STRIKETHROUGH,OVERLINE\[[0-9a-f]+\]'
start_pane colours 12 3 '\033[38;5;196;48;5;17mX\033[58;5;45mY'
check_raw_matches colours \
'C 0,0 data=\(1,1,X\) flags=FG256,BG256\[[0-9a-f]+\] attr=NONE\[0\] fg=colour196\[10000c4\] bg=colour17\[1000011\]' \
'C 0,1 data=\(1,1,Y\) flags=FG256,BG256\[[0-9a-f]+\] attr=NONE\[0\] fg=colour196\[10000c4\] bg=colour17\[1000011\] us=colour45\[100002d\]'
start_pane bce 8 3 '\033[44mA\033[K'
check_raw_matches bce \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\] attr=NONE\[0\].* bg=blue\[4\]' \
'C 0,1 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\].* bg=blue\[4\]' \
'C 0,7 data=\(1,1, \) flags=CLEARED\[[0-9a-f]+\] attr=NONE\[0\].* bg=blue\[4\]'
start_pane underlines 12 3 '\033[4:2m2\033[4:3m3\033[4:4m4\033[4:5m5'
check_raw_matches underlines \
'C 0,0 data=\(1,1,2\) flags=NONE\[0\] attr=UNDERSCORE_2\[[0-9a-f]+\]' \
'C 0,1 data=\(1,1,3\) flags=NONE\[0\] attr=UNDERSCORE_3\[[0-9a-f]+\]' \
'C 0,2 data=\(1,1,4\) flags=NONE\[0\] attr=UNDERSCORE_4\[[0-9a-f]+\]' \
'C 0,3 data=\(1,1,5\) flags=NONE\[0\] attr=UNDERSCORE_5\[[0-9a-f]+\]'
start_pane hyperlink 12 3 '\033]8;id=id1;https://example.com/a\033\\A\033]8;;\033\\B'
check_raw_matches hyperlink \
'L 0 \(0\) flags=EXTENDED,HYPERLINK\[[0-9a-f]+\]' \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\].* link=https://example.com/a linkid=id1' \
'C 0,1 data=\(1,1,B\) flags=NONE\[0\].* link=NONE linkid=NONE'
exit $exit_status

View File

@@ -0,0 +1,82 @@
#!/bin/sh
. ./input-common.inc
start_pane wide 8 3 'A\343\201\202B'
check_capture wide 'AあB'
check_raw_matches wide \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,1 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane combining 8 3 'e\314\201x'
check_raw_matches combining \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(1,[0-9]+,.*\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,x\) flags=NONE\[0\]'
start_pane emoji 10 3 '\360\237\230\200Z'
check_raw_matches emoji \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,4,😀\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,Z\) flags=NONE\[0\]'
start_pane flag 10 3 '\360\237\207\254\360\237\207\247!'
check_raw_matches flag \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,8,🇬🇧\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=NONE\[0\]'
start_pane variation 10 3 '*\357\270\217!'
check_raw_matches variation \
'L 0 \(0\) flags=EXTENDED\[[0-9a-f]+\]' \
'C 0,0 data=\(2,[0-9]+,.*\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=NONE\[0\]'
start_pane invalid 10 3 '\377A'
check_raw_matches invalid \
'C 0,0 data=\(1,3,.*\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,A\) flags=NONE\[0\]'
start_pane trunc2 10 3 '\303A'
check_raw_matches trunc2 \
'C 0,0 data=\(1,3,.*\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,A\) flags=NONE\[0\]'
start_pane trunc3 10 3 '\342\202A'
check_raw_matches trunc3 \
'C 0,0 data=\(1,3,.*\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,1 data=\(1,1,A\) flags=NONE\[0\]'
start_pane overwrite-wide-left 10 3 'A\343\201\202B\r\033[1CX'
check_raw_matches overwrite-wide-left \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1,X\) flags=NONE\[0\]' \
'C 0,2 data=\(1,1, \) flags=NONE\[0\]' \
'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane overwrite-wide-pad 10 3 'A\343\201\202B\r\033[2CX'
check_raw_matches overwrite-wide-pad \
'C 0,0 data=\(1,1,A\) flags=NONE\[0\]' \
'C 0,1 data=\(1,1, \) flags=NONE\[0\]' \
'C 0,2 data=\(1,1,X\) flags=NONE\[0\]' \
'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane overwrite-wide-with-wide 10 3 'A\343\201\202B\r\033[1C\347\225\214'
check_raw_matches overwrite-wide-with-wide \
'C 0,1 data=\(2,3,界\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 0,2 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 0,3 data=\(1,1,B\) flags=NONE\[0\]'
start_pane wide-right-edge 4 3 'ABC\343\201\202Z'
check_raw_matches wide-right-edge \
'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \
'C 1,0 data=\(2,3,あ\) flags=NONE\[0\] attr=NONE\[0\]' \
'C 1,1 data=\(1,1,!\) flags=PADDING\[[0-9a-f]+\] attr=NONE\[0\]' \
'C 1,2 data=\(1,1,Z\) flags=NONE\[0\]'
exit $exit_status

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

@@ -0,0 +1,28 @@
#!/bin/sh
. ./input-common.inc
start_pane wrap 5 3 'ABCDEZ'
check_capture wrap 'ABCDE
Z'
check_raw_matches wrap \
'^G 5x3 \(0/0\)$' \
'L 0 \(0\) flags=WRAPPED\[[0-9a-f]+\]' \
'C 0,4 data=\(1,1,E\) flags=NONE\[0\]' \
'C 1,0 data=\(1,1,Z\) flags=NONE\[0\]'
start_pane nowrap 5 3 '\033[?7lABCDEZ'
check_capture nowrap 'ABCDZ'
check_raw_matches nowrap \
'^G 5x3 \(0/0\)$' \
'L 0 \(0\) flags=NONE\[0\]' \
'C 0,4 data=\(1,1,Z\) flags=NONE\[0\]'
start_pane pending 5 3 'ABCD\r\033[4CZ'
check_capture pending 'ABCDZ'
check_cursor pending '5,0'
check_raw_matches pending \
'L 0 \(0\) flags=NONE\[0\]' \
'C 0,4 data=\(1,1,Z\) flags=NONE\[0\]'
exit $exit_status

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 -Linput-reflow-stress-$$ -f/dev/null"
TMUX2="$TEST_TMUX -Linput-reflow-stress-outer-$$ -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 -Ltest -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

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

@@ -0,0 +1,117 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
python3 - "$TEST_TMUX" <<'PY'
import os
import select
import signal
import subprocess
import sys
import tempfile
import time
tmux = sys.argv[1]
server = [tmux, "-Ltest", "-f/dev/null"]
def run(*args, check=True):
return subprocess.run(server + list(args), check=check,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def attach():
pid, fd = os.forkpty()
if pid == 0:
os.environ["TERM"] = "xterm-256color"
os.execl(tmux, tmux, "-Ltest", "-f/dev/null", "attach-session",
"-t", "requests")
os.set_blocking(fd, False)
return pid, fd
def read_until(fd, needle, timeout=5):
end = time.time() + timeout
data = b""
while time.time() < end:
r, _, _ = select.select([fd], [], [], 0.05)
if fd in r:
try:
chunk = os.read(fd, 4096)
except BlockingIOError:
chunk = b""
if chunk == b"":
continue
data += chunk
if needle in data:
return data
raise RuntimeError("did not see terminal request %r in %r" %
(needle, data))
def wait_file(path, timeout=5):
end = time.time() + timeout
while time.time() < end:
try:
with open(path, "rb") as f:
data = f.read()
if data:
return data
except FileNotFoundError:
pass
time.sleep(0.05)
return b""
def respawn(command):
run("respawn-window", "-k", "-t", "requests:0", command)
time.sleep(0.2)
def cleanup(pid=None):
if pid is not None:
try:
os.kill(pid, signal.SIGHUP)
except ProcessLookupError:
pass
run("kill-server", check=False)
run("kill-server", check=False)
run("new-session", "-d", "-x", "80", "-y", "24", "-s", "requests",
"sleep 60")
pid, fd = attach()
try:
time.sleep(0.5)
with tempfile.NamedTemporaryFile(delete=False) as f:
palette_out = f.name
respawn("stty raw -echo min 1 time 50; "
"printf '\\033]4;99;?\\033\\\\'; "
"dd bs=1 count=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

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

@@ -0,0 +1,73 @@
#!/bin/sh
. ./input-common.inc
start_pane wrap 5 3 'abcdeF'
check_capture wrap 'abcde
F'
check_cursor wrap '1,1'
check_flags wrap 'W abcde
- F'
check_joined wrap 'abcdeF'
start_pane wraplast 5 3 'abcd\033[5GZQ'
check_capture wraplast 'abcdZ
Q'
check_cursor wraplast '1,1'
start_pane nowrap 5 3 '\033[?7labcdeF'
check_capture nowrap 'abcdF'
check_cursor nowrap '4,0'
start_pane origin 6 4 '111111\n222222\n333333\n444444\033[2;3r\033[?6h\033[1;1HAA\033[?6l\033[r'
check_capture origin '111111
AA2222
333333
444444'
start_pane scrollup 5 4 '11111\n22222\n33333\n44444\033[2;3r\033[3;1HAAAAA\nBBBBB\033[r'
check_capture scrollup '11111
AAAAA
BBBBB
44444'
start_pane scrolldown 5 4 '11111\n22222\n33333\n44444\033[2;3r\033[2;1H\033[TZZZZZ\033[r'
check_capture scrolldown '11111
ZZZZZ
22222
44444'
start_pane ri 5 4 '11111\n22222\n33333\n44444\033[2;3r\033[2;1H\033MZZZZZ\033[r'
check_capture ri '11111
ZZZZZ
22222
44444'
start_pane nel 5 3 'AA\033EBC\n'
check_capture nel 'AA
BC'
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 5 -y 3 -s history \; \
set-option -g history-limit 3 \; \
respawn-pane -k "printf '01\n02\n03\n04\n05\n06'; sleep 2" || exit 1
sleep 0.3
$TMUX capture-pane -pN -t history: -S - -E - | normalize_capture >"$TMP"
printf "%s\n" '01
02
03
04
05
06' >"$EXP"
cmp "$TMP" "$EXP" || fail "history limit"
$TMUX clear-history -t history:
$TMUX capture-pane -pN -t history: -S - -E - | normalize_capture >"$TMP"
printf "%s\n" '04
05
06' >"$EXP"
cmp "$TMP" "$EXP" || fail "clear-history"
$TMUX kill-server 2>/dev/null
exit $exit_status

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

@@ -0,0 +1,26 @@
#!/bin/sh
. ./input-common.inc
start_pane sgr-basic 20 3 '\033[1;2;3;4;5;7;8;9mA\033[22;23;24;25;27;28;29mB\n'
check_capture sgr-basic 'AB'
$TMUX capture-pane -peN -t sgr-basic: -S 0 -E - >/dev/null || exit 1
start_pane sgr-colour 20 3 '\033[31;42mA\033[38;5;196;48;5;22mB\033[38;2;1;2;3;48;2;4;5;6mC\033[39;49mD\n'
check_capture sgr-colour 'ABCD'
$TMUX capture-pane -peN -t sgr-colour: -S 0 -E - >/dev/null || exit 1
start_pane sgr-underline 20 3 '\033[4:1mA\033[4:2mB\033[4:3mC\033[4:4mD\033[4:5mE\033[4:0mF\n'
check_capture sgr-underline 'ABCDEF'
$TMUX capture-pane -peN -t sgr-underline: -S 0 -E - >/dev/null || exit 1
start_pane sgr-uscolour 20 3 '\033[58;5;45;4mA\033[58:2::10:20:30mB\033[59mC\n'
check_capture sgr-uscolour 'ABC'
$TMUX capture-pane -peN -t sgr-uscolour: -S 0 -E - >/dev/null || exit 1
start_pane sgr-reset 20 3 '\033[90;100mA\033[0mB\033[91;101mC\033[39;49mD\n'
check_capture sgr-reset 'ABCD'
$TMUX capture-pane -peN -t sgr-reset: -S 0 -E - >/dev/null || exit 1
$TMUX kill-server 2>/dev/null
exit $exit_status

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

@@ -0,0 +1,45 @@
#!/bin/sh
. ./input-common.inc
start_pane wide 10 3 '\343\201\202B\rX\n'
check_capture wide 'X B'
check_flags wide 'X X B'
start_pane widepad 10 3 'A\343\201\202B\r\033[2CX\n'
check_capture widepad 'A XB'
check_flags widepad 'X A XB'
start_pane wideedge 5 3 'abc\343\201\202Z\n'
check_capture wideedge 'abcあ
Z'
check_cursor wideedge '0,2'
check_joined wideedge 'abcあZ'
start_pane wideeol 5 3 'abcd\343\201\202Z\n'
check_capture wideeol 'abcd
あZ'
check_cursor wideeol '0,2'
start_pane combine 10 3 'e\314\201\n'
check_capture combine 'é'
check_cursor combine '0,1'
start_pane combinewide 10 3 '\343\201\202\314\201X\n'
check_capture combinewide 'あ́X'
check_cursor combinewide '0,1'
start_pane variation 10 3 '\342\234\224\357\270\217X\n'
check_capture variation '✔X'
check_cursor variation '0,1'
start_pane flag 10 3 '\360\237\207\254\360\237\207\247X\n'
check_capture flag '🇬🇧X'
check_cursor flag '0,1'
start_pane combining-left 10 3 '\314\201A\n'
check_capture combining-left 'A'
check_cursor combining-left '0,1'
$TMUX kill-server 2>/dev/null
exit $exit_status

View File

@@ -0,0 +1,216 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Llifecycle-deferred -f/dev/null"
TMUX2="$TEST_TMUX -Llifecycle-deferred-outer -f/dev/null"
TMPDIR=$(mktemp -d)
IN="$TMPDIR/in"
OUT="$TMPDIR/out"
CONTROL_PID=
cleanup()
{
exec 3>&- 2>/dev/null
[ -n "$CONTROL_PID" ] && kill "$CONTROL_PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
fail()
{
echo "$1" >&2
[ -s "$OUT" ] && sed -n '1,120p' "$OUT" >&2
cleanup
exit 1
}
run_tmux()
{
out=
if command -v timeout >/dev/null 2>&1; then
out=$(timeout 10 $TMUX "$@" 2>&1)
else
out=$($TMUX "$@" 2>&1)
fi
rc=$?
[ "$rc" -eq 0 ] || fail "tmux $* failed ($rc): $out"
printf '%s' "$out"
}
send_control()
{
printf '%s\n' "$1" >&3 || fail "failed to write control command: $1"
}
wait_clients()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
have=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$have" -eq "$want" ] && return 0
sleep 0.2
i=$((i + 1))
done
return 1
}
wait_format()
{
target=$1
format=$2
want=$3
i=0
while [ "$i" -lt 50 ]; do
have=$($TMUX display-message -p -t "$target" "$format" 2>/dev/null)
[ "$have" = "$want" ] && return 0
sleep 0.2
i=$((i + 1))
done
return 1
}
assert_alive()
{
run_tmux has-session -t life >/dev/null
fields=$(run_tmux display-message -p -t life \
'#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}')
case "$fields" in
life:@*:%*:*) ;;
*) fail "bad current fields after $1: $fields" ;;
esac
}
check_control_output()
{
sleep 1
if grep -E '(^%error |server exited|lost server|\(null\)|no current)' \
"$OUT" >/dev/null 2>&1; then
fail "control client reported an error or invalid object"
fi
awk '
$1 == "%session-window-changed" {
if (NF != 3 || $2 !~ /^\$[0-9]+$/ || $3 !~ /^@[0-9]+$/)
bad = 1
}
$1 == "%subscription-changed" {
colon = 0
for (i = 2; i <= NF; i++)
if ($i == ":")
colon = 1
if (NF < 7 || !colon)
bad = 1
}
END { exit bad }
' "$OUT" || fail "control client received a malformed notification"
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
run_tmux new-session -d -s life -n prompt -x 80 -y 24 'sleep 1000' \
>/dev/null
run_tmux set-option -g detach-on-destroy off >/dev/null
run_tmux new-window -t life -n tree 'sleep 1000' >/dev/null
run_tmux new-window -t life -n work 'sleep 1000' >/dev/null
run_tmux select-window -t life:prompt >/dev/null
run_tmux set-hook -g after-new-window \
'display-message -p "hook-new #{session_name}:#{window_id}:#{pane_id}"' \
>/dev/null
run_tmux set-hook -g after-split-window \
'display-message -p "hook-split #{session_name}:#{window_id}:#{pane_id}"' \
>/dev/null
run_tmux set-hook -g after-kill-pane \
'display-message -p "hook-kill #{session_name}:#{window_id}:#{pane_id}"' \
>/dev/null
run_tmux set-hook -g pane-exited \
'display-message -p "hook-exit #{session_name}:#{window_id}:#{pane_id}"' \
>/dev/null
run_tmux set-hook -g window-layout-changed \
'display-message -p "hook-layout #{session_name}:#{window_id}:#{pane_id}"' \
>/dev/null
run_tmux set-hook -g session-window-changed \
'display-message -p "hook-current #{session_name}:#{window_id}:#{pane_id}"' \
>/dev/null
run_tmux bind-key -n M-p command-prompt -P -p '(life)' \
"set -g @lifecycle-prompt '%% #{session_name}:#{window_id}:#{pane_id}'" \
>/dev/null
$TMUX2 new-session -d -s outer -n prompt -x 80 -y 24 "$TMUX attach -t life" \
|| fail "failed to start first attached client"
$TMUX2 new-window -t outer -n tree "$TMUX attach -t life" \
|| fail "failed to start second attached client"
wait_clients 2 || fail "normal clients did not attach"
$TMUX2 send-keys -t outer:prompt M-p || fail "failed to open command prompt"
sleep 1
run_tmux choose-tree -t life:tree.0 \
-F 'tree #{session_name}:#{window_id}:#{pane_id}' >/dev/null
wait_format life:tree.0 '#{pane_mode}' tree-mode || \
fail "choose-tree did not enter tree-mode"
mkfifo "$IN" || fail "failed to create control fifo"
(cat "$IN" | $TMUX -C attach -t life >"$OUT" 2>&1) &
CONTROL_PID=$!
exec 3>"$IN"
wait_clients 3 || fail "control client did not attach"
CONTROL_CLIENT=$($TMUX list-clients -F '#{client_name} #{client_control_mode}' |
awk '$2 == 1 { print $1; exit }')
[ -n "$CONTROL_CLIENT" ] || fail "missing control client"
send_control "refresh-client -B 'all:%*:#{session_name}:#{window_id}:#{pane_id}:#{session_windows}:#{window_panes}'"
send_control "refresh-client -B 'windows:@*:#{session_name}:#{window_id}:#{window_index}:#{window_panes}'"
sleep 1
run_tmux kill-pane -t life:prompt.0 >/dev/null
run_tmux kill-window -t life:tree >/dev/null
assert_alive "killing prompt and tree panes"
i=1
while [ "$i" -le 20 ]; do
# Keep this sequence fixed: failures should reproduce on the same pass.
s=ld$i
ctl=ctl$i
idx=$((50 + i))
run_tmux new-session -d -s "$s" -n base 'sleep 1000' >/dev/null
run_tmux split-window -t "$s:base" 'sleep 1000' >/dev/null
run_tmux respawn-pane -k -t "$s:base.1" 'sleep 1000' >/dev/null
run_tmux kill-pane -t "$s:base.1" >/dev/null
run_tmux new-window -t "$s" -n second 'sleep 1000' >/dev/null
run_tmux link-window -s "$s:second" -t "life:$idx" >/dev/null
run_tmux unlink-window -t "life:$idx" >/dev/null
run_tmux new-window -t "$s" -n single 'sleep 1000' >/dev/null
run_tmux kill-pane -t "$s:single.0" >/dev/null
run_tmux kill-window -t "$s:base" >/dev/null
run_tmux new-session -d -s "$ctl" -n ctl 'sleep 1000' >/dev/null
run_tmux switch-client -c "$CONTROL_CLIENT" -t "$ctl" >/dev/null
run_tmux kill-session -t "$ctl" >/dev/null
run_tmux kill-session -t "$s" >/dev/null
assert_alive "iteration $i"
i=$((i + 1))
done
check_control_output
cleanup
exit 0

297
regress/mode-mutation.sh Normal file
View File

@@ -0,0 +1,297 @@
#!/bin/sh
# Exercise modes while their backing objects are changed from outside the
# client displaying the mode. This catches stale selection indexes and pointers
# after a mode list is shrunk or rebuilt.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
TMUX2="$TEST_TMUX -Ltest2 -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
sleep 0.5
}
fail()
{
echo "$1"
cleanup
exit 1
}
capture()
{
$TMUX2 capture-pane -p -t out:0 2>/dev/null
}
assert_alive()
{
$TMUX display-message -p 'alive' >/dev/null 2>&1 || \
fail "$1: server exited"
}
wait_clients()
{
i=0
while [ "$i" -lt 50 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "expected $1 clients, have $c"
}
wait_for()
{
i=0
while [ "$i" -lt 50 ]; do
capture | grep -q "$1" && return 0
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
wait_mode()
{
t=$1
want=$2
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t "$t" '#{pane_in_mode}' \
2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "pane $t mode state is $got, expected $want"
}
repeat_key()
{
t=$1
key=$2
count=$3
i=0
while [ "$i" -lt "$count" ]; do
$TMUX send-keys -t "$t" "$key" || \
fail "failed to send $key to $t"
i=$((i + 1))
done
}
start_client()
{
s=$1
cmd=${2:-cat}
cleanup
$TMUX new-session -d -s "$s" -n main -x 80 -y 24 "$cmd" || \
fail "$s: new-session failed"
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t $s" || \
fail "$s: outer client failed"
wait_clients 1
}
new_outer_client()
{
s=$1
$TMUX2 new-window -d -t out: "$TMUX attach -t $s" || \
fail "$s: outer client failed"
}
client_for_session()
{
$TMUX list-clients -F '#{client_name} #{client_session}' |
awk -v s="$1" '$2 == s { print $1; exit }'
}
test_choose_tree()
{
start_client tree-a
$TMUX new-session -d -s tree-b -n one 'cat' || fail "tree-b failed"
$TMUX new-window -d -t tree-b -n two 'cat' || fail "tree-b:1 failed"
$TMUX new-session -d -s tree-c -n one 'cat' || fail "tree-c failed"
$TMUX new-session -d -s tree-d -n one 'cat' || fail "tree-d failed"
$TMUX split-window -d -t tree-a:0 'cat' || fail "tree split failed"
$TMUX choose-tree -t tree-a:0 -O index -F 'MT #{session_name}:#{window_index}.#{pane_index}' || \
fail "choose-tree failed"
wait_for 'MT '
repeat_key tree-a:0 j 40
$TMUX kill-session -t tree-d || fail "tree kill-session failed"
$TMUX kill-session -t tree-c || fail "tree kill-session failed"
$TMUX rename-session -t tree-b tree-renamed || fail "tree rename failed"
$TMUX rename-window -t tree-renamed:0 renamed || fail "tree rename-window failed"
$TMUX kill-window -t tree-renamed:1 || fail "tree kill-window failed"
side=$($TMUX split-window -d -P -F '#{pane_id}' -t tree-a:0 'cat') || \
fail "tree side split failed"
$TMUX break-pane -d -s "$side" || fail "tree break-pane failed"
$TMUX join-pane -d -s "$side" -t tree-a:0.0 || \
fail "tree join-pane failed"
i=0
while [ "$i" -lt 12 ]; do
$TMUX new-window -d -t tree-a -n "new$i" 'cat' || \
fail "tree new-window failed"
i=$((i + 1))
done
assert_alive "choose-tree mutation"
$TMUX send-keys -t tree-a:0 k j l h Enter || \
fail "choose-tree keys failed"
wait_mode tree-a:0 0
assert_alive "choose-tree exit"
}
test_choose_buffer()
{
start_client buffer-a
i=0
while [ "$i" -lt 30 ]; do
$TMUX set-buffer -b "mbuf$i" "buffer mutation $i" || \
fail "set-buffer failed"
i=$((i + 1))
done
$TMUX choose-buffer -t buffer-a:0 -F 'MB #{buffer_name}' || \
fail "choose-buffer failed"
wait_for 'MB '
repeat_key buffer-a:0 j 40
i=8
while [ "$i" -lt 30 ]; do
$TMUX delete-buffer -b "mbuf$i" || fail "delete-buffer failed"
i=$((i + 1))
done
i=30
while [ "$i" -lt 50 ]; do
$TMUX set-buffer -b "mbuf$i" "new buffer mutation $i" || \
fail "new set-buffer failed"
i=$((i + 1))
done
assert_alive "choose-buffer mutation"
$TMUX send-keys -t buffer-a:0 k j Enter || \
fail "choose-buffer keys failed"
wait_mode buffer-a:0 0
assert_alive "choose-buffer exit"
}
test_choose_client()
{
start_client client-a
$TMUX new-session -d -s client-b -n main 'cat' || fail "client-b failed"
$TMUX new-session -d -s client-c -n main 'cat' || fail "client-c failed"
new_outer_client client-b
new_outer_client client-c
wait_clients 3
$TMUX choose-client -t client-a:0 -F 'MC #{client_session}' || \
fail "choose-client failed"
wait_for 'MC '
repeat_key client-a:0 j 20
c=$(client_for_session client-c)
[ -n "$c" ] || fail "client-c client not found"
$TMUX detach-client -t "$c" || fail "detach client-c failed"
c=$(client_for_session client-b)
[ -n "$c" ] || fail "client-b client not found"
$TMUX detach-client -t "$c" || fail "detach client-b failed"
$TMUX new-session -d -s client-d -n main 'cat' || fail "client-d failed"
new_outer_client client-d
wait_clients 2
assert_alive "choose-client mutation"
$TMUX send-keys -t client-a:0 k j Enter || \
fail "choose-client keys failed"
wait_mode client-a:0 0
assert_alive "choose-client exit"
}
test_customize_mode()
{
start_client option-a
i=0
while [ "$i" -lt 30 ]; do
$TMUX set-option -g "@mode_mut_$i" "$i" || \
fail "set option failed"
i=$((i + 1))
done
$TMUX customize-mode -t option-a:0 -F 'MO #{option_name}=#{option_value}' || \
fail "customize-mode failed"
wait_mode option-a:0 1
repeat_key option-a:0 j 80
i=10
while [ "$i" -lt 30 ]; do
$TMUX set-option -gu "@mode_mut_$i" || fail "unset option failed"
i=$((i + 1))
done
i=30
while [ "$i" -lt 55 ]; do
$TMUX set-option -g "@mode_mut_$i" "$i" || \
fail "new option failed"
i=$((i + 1))
done
$TMUX set-option -g status-left 'mutated' || fail "status-left failed"
$TMUX rename-session -t option-a option-renamed || fail "option rename failed"
assert_alive "customize-mode mutation"
$TMUX send-keys -t option-renamed:0 k j C-d C-u q || \
fail "customize-mode keys failed"
wait_mode option-renamed:0 0
assert_alive "customize-mode exit"
}
test_copy_mode()
{
start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat'
$TMUX set-window-option -g mode-keys vi || fail "mode-keys failed"
$TMUX split-window -d -t copy-a:0 'cat' || fail "copy split failed"
$TMUX copy-mode -t copy-a:0 || fail "copy-mode failed"
wait_mode copy-a:0 1
repeat_key copy-a:0 k 20
$TMUX rename-window -t copy-a:0 renamed || fail "copy rename-window failed"
side=$($TMUX split-window -d -P -F '#{pane_id}' -t copy-a:renamed 'cat') || \
fail "copy side split failed"
$TMUX break-pane -d -s "$side" || fail "copy break-pane failed"
$TMUX join-pane -d -s "$side" -t copy-a:renamed.0 || \
fail "copy join-pane failed"
$TMUX kill-pane -t copy-a:renamed.1 || fail "copy kill-pane failed"
$TMUX new-window -d -t copy-a -n extra 'cat' || fail "copy new-window failed"
$TMUX kill-window -t copy-a:extra || fail "copy kill-window failed"
$TMUX rename-session -t copy-a copy-renamed || fail "copy rename failed"
assert_alive "copy-mode mutation"
$TMUX send-keys -t copy-renamed:renamed.0 j k C-d C-u q || \
fail "copy-mode keys failed"
wait_mode copy-renamed:renamed.0 0
assert_alive "copy-mode exit"
}
cleanup
test_choose_tree
test_choose_buffer
test_choose_client
test_customize_mode
test_copy_mode
cleanup
exit 0

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

@@ -0,0 +1,161 @@
#!/bin/sh
# Tests of array options in the options engine (options_array_* in options.c
# and the array handling in cmd-set-option.c / cmd-show-options.c).
#
# Array options are indexed by integer. This exercises: setting a whole array
# from a separator-delimited string; per-index set with option[N]; -a append
# (which lands at the next free index); show ordering by ascending index and
# preservation of gaps; per-index unset with -u; show -v of a single index and
# of a missing index; and per-option separators (user-keys splits only on
# comma, update-environment on space or comma).
#
# update-environment (session), status-format (session), user-keys (server)
# and command-alias (server) are used as representative array options.
#
# options-scope.sh covers scoping/inheritance and options-values.sh covers
# value validation.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
check_value()
{
out=$($TMUX show $1 2>&1)
if [ "$out" != "$2" ]; then
echo "show $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
# check_array $args $expected
#
# Compare the full (multi-line) show output for an array option with a
# newline-separated $expected string.
check_array()
{
out=$($TMUX show $1 2>&1)
if [ "$out" != "$(printf '%s' "$2")" ]; then
echo "show $1 (array) failed."
echo "Expected:"; printf '%s\n' "$2"
echo "But got:"; printf '%s\n' "$out"
exit 1
fi
}
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# --- whole-array assignment splits on the separator -----------------------
#
# update-environment has the default " ," separator, so a single string value
# is split into consecutive indices starting at 0.
check_ok set -g update-environment "AAA BBB,CCC"
check_array "-g update-environment" "update-environment[0] AAA
update-environment[1] BBB
update-environment[2] CCC"
# --- -a append goes to the next free index --------------------------------
check_ok set -ga update-environment "DDD"
check_array "-g update-environment" "update-environment[0] AAA
update-environment[1] BBB
update-environment[2] CCC
update-environment[3] DDD"
# --- per-index unset leaves a gap; show preserves order and gaps ----------
check_ok set -gu update-environment[1]
check_array "-g update-environment" "update-environment[0] AAA
update-environment[2] CCC
update-environment[3] DDD"
# show -v of an existing index returns its value; a missing index is empty.
check_value "-gv update-environment[0]" "AAA"
check_value "-gv update-environment[1]" ""
# --- explicit indexed set, including out-of-order and gaps ----------------
#
# status-format is a session array; assigning an empty string first clears its
# multi-index default, then set specific indices out of order and confirm show
# sorts by ascending index and keeps the gap at [1].
check_ok set -g status-format ""
check_array "-g status-format" "status-format"
check_ok set -g status-format[5] "five"
check_ok set -g status-format[0] "zero"
check_ok set -g status-format[2] "two"
check_array "-g status-format" "status-format[0] zero
status-format[2] two
status-format[5] five"
# --- comma-only separator (user-keys) -------------------------------------
#
# user-keys splits only on comma, so an embedded space stays within one entry
# (and show quotes a value containing a space).
check_ok set -g user-keys "One,Two Three"
check_array "-g user-keys" 'user-keys[0] One
user-keys[1] "Two Three"'
# --- command-type array (a hook) ------------------------------------------
#
# Hooks are command arrays: an indexed value is parsed as a command when set
# and re-printed from the parsed command list; a syntax error is reported.
check_ok set -g alert-bell[0] "display-message hi"
check_value "-gv alert-bell[0]" "display-message hi"
check_fail "syntax error" set -g alert-bell[0] "if -x {"
# --- colour-type array ----------------------------------------------------
#
# pane-colours is a colour array; an indexed value is validated as a colour.
check_ok set -w pane-colours[0] red
check_value "-wv pane-colours[0]" "red"
check_fail "bad colour: xxxyyy" set -w pane-colours[1] xxxyyy
# --- -o refuses to overwrite an already-set index -------------------------
check_ok set -g command-alias[9] "x=list-keys"
check_fail "already set: command-alias[9]" set -go command-alias[9] "y=list-keys"
# --- non-array option rejects index syntax --------------------------------
#
# status-left is a plain string; indexing it is an error.
check_fail "not an array: status-left[0]" set -g status-left[0] "x"
assert_alive "after options-array tests"
$TMUX kill-server 2>/dev/null
exit 0

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

@@ -0,0 +1,208 @@
#!/bin/sh
# Tests of the options engine scoping and inheritance, as described in the
# OPTIONS section of tmux(1) and implemented in options.c, cmd-set-option.c and
# cmd-show-options.c.
#
# This exercises: global vs session vs window vs pane precedence; -u to remove
# an option (revealing the inherited value); -gu to restore a global option to
# its compiled default; scope inference from the option name (-w/-p and the
# set-window-option alias); show -v (which does NOT walk parents) versus show -A
# (which does, marking inherited values with a trailing *); unknown/ambiguous
# option errors and -q suppression; and user options (@foo) at every scope.
#
# options-values.sh covers value validation and options-array.sh covers arrays.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check_value $args $expected
#
# Run show-option with $args and compare the single-line output with $expected.
check_value()
{
out=$($TMUX show $1 2>&1)
if [ "$out" != "$2" ]; then
echo "show $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
# check_ok $cmd...
#
# Run a command and require that it succeeds.
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $cmd...
#
# Run a command and require that it fails with the given error message.
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# --- global vs session precedence -----------------------------------------
#
# status-left is a session option. A value set at the session scope shadows
# the global one; show -v at each scope reports that scope's own value.
check_ok set -g status-left "GLOBAL"
check_ok set status-left "SESSION"
check_value "-v status-left" "SESSION"
check_value "-gv status-left" "GLOBAL"
# show -v does NOT inherit: -u removes the session entry, after which the
# session-scope show -v is empty even though the global value still exists.
check_ok set -u status-left
check_value "-v status-left" ""
check_value "-gv status-left" "GLOBAL"
# show -A walks the parent scopes and marks an inherited value with a "*".
out=$($TMUX show -A 2>/dev/null | grep '^status-left\*')
if [ "$out" != "status-left* GLOBAL" ]; then
echo "show -A did not mark inherited status-left."
echo "But got: '$out'"
exit 1
fi
# --- -gu restores the compiled default ------------------------------------
#
# Removing a global option with -u restores its built-in default rather than
# deleting it; status-left's default is the format "[#{session_name}] ".
check_ok set -g status-left "GLOBAL2"
check_value "-gv status-left" "GLOBAL2"
check_ok set -gu status-left
check_value "-gv status-left" "[#{session_name}] "
# --- scope inference from the option name ---------------------------------
#
# mode-keys is a window option, so a bare set-option infers the window scope;
# set-window-option (setw) is an explicit alias for the same thing, and -g w
# targets the global window options.
check_ok set mode-keys vi
check_value "-wv mode-keys" "vi"
check_ok setw mode-keys emacs
check_value "-wv mode-keys" "emacs"
check_ok set -gw mode-keys vi
check_value "-gwv mode-keys" "vi"
# cursor-colour is a window-and-pane option. A pane-scope value overrides a
# window-scope one for that pane.
check_ok set -w cursor-colour blue
check_ok set -p cursor-colour red
check_value "-pv cursor-colour" "red"
out=$($TMUX show -Ap 2>/dev/null | grep '^cursor-colour ')
if [ "$out" != "cursor-colour red" ]; then
echo "pane cursor-colour did not override window value."
echo "But got: '$out'"
exit 1
fi
# --- -U unsets a window option and clears pane copies ----------------------
#
# When a window option also has per-pane copies, -u on the window scope leaves
# those pane copies in place; -U additionally removes the option from every
# pane in the window, so all panes fall back to inheritance.
$TMUX split-window -t main || exit 1
panes=$($TMUX list-panes -t main -F '#{pane_id}')
set -- $panes
pa=$1
pb=$2
check_ok set -p -t "$pa" cursor-colour red
check_ok set -p -t "$pb" cursor-colour blue
check_ok set -w -t main cursor-colour green
check_value "-pv -t $pa cursor-colour" "red"
check_value "-pv -t $pb cursor-colour" "blue"
check_value "-wv -t main cursor-colour" "green"
check_ok set -Uw -t main cursor-colour
check_value "-pv -t $pa cursor-colour" ""
check_value "-pv -t $pb cursor-colour" ""
check_value "-wv -t main cursor-colour" ""
# --- unknown, ambiguous and -q --------------------------------------------
check_fail "invalid option: no-such-option" set -g no-such-option x
check_fail "ambiguous option: status-l" set -g status-l x
# A unique prefix resolves to the full option name.
check_ok set -g status-inte 5
check_value "-gv status-interval" "5"
# -q suppresses the error and exits successfully.
check_ok set -gq no-such-option x
check_ok show -gqv no-such-option
# --- errors from unresolvable targets -------------------------------------
#
# A -t target that does not resolve produces a scope-specific error from
# options_scope_from_name()/options_scope_from_flags().
check_fail "no such session: nosuch" show -t nosuch status-left
check_fail "no such window: nosuch" show -w -t nosuch mode-keys
check_fail "no such pane: nosuch" set -p -t nosuch cursor-colour red
# --- show with no option name lists every option --------------------------
#
# show without a specific option walks the whole table (cmd_show_options_all).
# Hooks are hidden unless -H is given.
$TMUX set -g @listme "here" || exit 1
if ! $TMUX show -g | grep -q '^@listme here$'; then
echo "show -g did not list @listme."
exit 1
fi
# alert-bell is a hook: only shown with -H.
if $TMUX show -g | grep -q '^alert-bell'; then
echo "show -g listed a hook without -H."
exit 1
fi
if ! $TMUX show -gH | grep -q '^alert-bell'; then
echo "show -gH did not list the alert-bell hook."
exit 1
fi
# --- user options at every scope ------------------------------------------
#
# @-prefixed user options can be created freely at any scope and do not
# inherit type checking.
check_ok set -g @u "global-user"
check_ok set @u "session-user"
check_ok set -w @u "window-user"
check_ok set -p @u "pane-user"
check_value "-gv @u" "global-user"
check_value "-v @u" "session-user"
check_value "-wv @u" "window-user"
check_value "-pv @u" "pane-user"
assert_alive "after options-scope tests"
$TMUX kill-server 2>/dev/null
exit 0

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

@@ -0,0 +1,193 @@
#!/bin/sh
# Tests of options engine value validation, as implemented by
# options_from_string() and friends in options.c.
#
# Each option table entry has a type (string, number, key, colour, flag,
# choice, command) with type-specific parsing and validation. This exercises:
# number range limits; choice options rejecting unknown values; flag options
# toggling with no value and rejecting garbage; colour and key options
# rejecting invalid input; string append with -a; -F expansion at set time;
# and -o refusing to overwrite an option that is already set.
#
# options-scope.sh covers scoping/inheritance and options-array.sh covers
# arrays.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
check_value()
{
out=$($TMUX show $1 2>&1)
if [ "$out" != "$2" ]; then
echo "show $1 failed."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
$TMUX new-session -d -s main -x 80 -y 24 || exit 1
# --- number options -------------------------------------------------------
#
# display-time is a number with a minimum of 0; a negative value and a
# non-numeric value are both rejected via strtonum(3).
check_ok set -g display-time 4000
check_value "-gv display-time" "4000"
check_fail "value is too small: -5" set -g display-time -5
check_fail "value is invalid: abc" set -g display-time abc
# A missing value is rejected for a non-flag, non-choice option.
check_fail "empty value" set -g display-time
# --- choice options -------------------------------------------------------
#
# status-keys accepts only its listed choices (vi/emacs); anything else is an
# "unknown value" error and the option keeps its previous value.
check_ok set -g status-keys vi
check_value "-gv status-keys" "vi"
check_fail "unknown value: bogus" set -g status-keys bogus
check_value "-gv status-keys" "vi"
# --- flag options ---------------------------------------------------------
#
# focus-events is an on/off flag. Setting with no value toggles it; explicit
# on/off/yes/no/1/0 are accepted (case-insensitively); anything else fails.
check_ok set -g focus-events off
check_value "-gv focus-events" "off"
check_ok set -g focus-events # toggle
check_value "-gv focus-events" "on"
check_ok set -g focus-events # toggle back
check_value "-gv focus-events" "off"
check_ok set -g focus-events yes
check_value "-gv focus-events" "on"
check_ok set -g focus-events NO
check_value "-gv focus-events" "off"
check_fail "bad value: maybe" set -g focus-events maybe
# --- colour options -------------------------------------------------------
#
# status-bg is a colour; named colours, numbers and #rrggbb are accepted,
# garbage is rejected.
check_ok set -g status-bg red
check_value "-gv status-bg" "red"
check_ok set -g status-bg colour123
check_value "-gv status-bg" "colour123"
check_ok set -g status-bg "#00ff00"
check_value "-gv status-bg" "#00ff00"
check_fail "bad colour: xxxyyy" set -g status-bg xxxyyy
# --- style options --------------------------------------------------------
#
# status-style is a style string, validated when set; a bogus style keyword is
# rejected and the old value is retained.
check_ok set -g status-style "fg=red,bg=black"
check_value "-gv status-style" "fg=red,bg=black"
check_fail "invalid style: bg=xxxyyy" set -g status-style "bg=xxxyyy"
check_value "-gv status-style" "fg=red,bg=black"
# --- key options ----------------------------------------------------------
#
# prefix is a key; a valid key name is stored in canonical form, a bad one is
# rejected.
check_ok set -g prefix C-a
check_value "-gv prefix" "C-a"
check_fail "bad key: boguskey" set -g prefix boguskey
# --- string options with extra validation ---------------------------------
#
# default-shell is a string but is checked to be an executable shell; a bogus
# path is rejected and the old value kept.
old=$($TMUX show -gv default-shell)
check_fail "not a suitable shell: /not/a/shell" set -g default-shell /not/a/shell
check_value "-gv default-shell" "$old"
# --- user options require a value ------------------------------------------
#
# A user option set with no value at all is an error.
check_fail "empty value" set -g @novalue
# --- command options ------------------------------------------------------
#
# default-client-command is a command option: the value is parsed as a tmux
# command when set and re-printed from the parsed command list. A syntax
# error is reported and the option is left unchanged.
check_ok set -g default-client-command "new-window"
check_value "-gv default-client-command" "new-window"
check_fail "syntax error" set -g default-client-command "if -x {"
check_value "-gv default-client-command" "new-window"
# --- renamed option aliases -----------------------------------------------
#
# Historical option names are mapped to their current spelling, so setting
# cursor-color updates cursor-colour.
check_ok set -w cursor-color red
check_value "-wv cursor-colour" "red"
# --- string append (-a) ---------------------------------------------------
#
# -a appends to the current string value rather than replacing it.
check_ok set -g @str "foo"
check_ok set -ga @str "bar"
check_value "-gv @str" "foobar"
# --- -F expands at set time -----------------------------------------------
#
# With -F the value is expanded as a format once, at set time; without -F it is
# stored literally.
check_ok set -gF @expanded "#{session_name}"
check_value "-gv @expanded" "main"
check_ok set -g @literal "#{session_name}"
check_value "-gv @literal" "#{session_name}"
# --- -o refuses to overwrite ----------------------------------------------
#
# -o makes set-option fail if the option is already set, leaving it unchanged;
# it succeeds for an option that is not yet set.
check_ok set -g @once "first"
check_fail "already set: @once" set -go @once "second"
check_value "-gv @once" "first"
check_ok set -go @fresh "value"
check_value "-gv @fresh" "value"
assert_alive "after options-values tests"
$TMUX kill-server 2>/dev/null
exit 0

620
regress/pane-ops.sh Normal file
View File

@@ -0,0 +1,620 @@
#!/bin/sh
# Tests of pane management command semantics (not parsing), as implemented in
# cmd-split-window.c, cmd-break-pane.c, cmd-join-pane.c (join-pane and
# move-pane), cmd-swap-pane.c, cmd-kill-pane.c, cmd-respawn-pane.c,
# cmd-respawn-window.c, cmd-resize-pane.c and cmd-select-pane.c.
#
# This exercises:
# - split-window -h/-v with -l in cells and percent, -b placing the new pane
# before (left/top of) the target and -f spanning the full window size;
# - break-pane moving a pane into a new window (-d, -n name, -a after, -P -F
# printing the new location);
# - join-pane moving a window's only pane into another window (destroying the
# source window), -b before, -l size, and the identical-panes error;
# - move-pane as an alias for join-pane;
# - swap-pane -U/-D/-s/-t, -d keeping the active pane, and the marked pane
# (select-pane -m/-M) as the default swap source;
# - kill-pane, kill-pane -a keeping only the target;
# - respawn-pane/respawn-window refusing a live pane without -k, working on a
# dead pane (remain-on-exit) and killing with -k;
# - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z
# zoom/unzoom (including implicit unzoom on split).
#
# window-ops.sh covers window-level commands and buffers.sh paste buffers.
PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check_ok $cmd...
#
# Run a command and require that it succeeds.
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $cmd...
#
# Run a command and require that it fails with the given error message.
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
# check_fmt $target $format $expected
#
# Expand a format in a target's context and compare with $expected.
check_fmt()
{
out=$($TMUX display-message -p -t "$1" "$2" 2>&1)
if [ "$out" != "$3" ]; then
echo "Format '$2' for '$1' wrong."
echo "Expected: '$3'"
echo "But got: '$out'"
exit 1
fi
}
# check_panes $target $expected
#
# Compare the pane list of a window (as "index:id ...", in index order) with
# $expected.
check_panes()
{
out=$(echo $($TMUX list-panes -t "$1" -F '#{pane_index}:#{pane_id}'))
if [ "$out" != "$2" ]; then
echo "Pane list of '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
# ---------------------------------------------------------------------------
# split-window geometry.
check_ok new-session -d -s P -x 80 -y 24 -n main
p0=$($TMUX display-message -p -t P:0.0 '#{pane_id}')
# Horizontal split with -l in cells: new pane gets exactly that width and the
# old pane the rest minus the separator line.
check_ok split-window -d -h -l 20 -t "$p0"
p1=$($TMUX display-message -p -t P:0.1 '#{pane_id}')
check_fmt "$p1" '#{pane_width}x#{pane_height}' '20x24'
check_fmt "$p0" '#{pane_width}x#{pane_height}' '59x24'
# Vertical split with a percentage of the pane being split.
check_ok split-window -d -v -l 25% -t "$p0"
p2=$($TMUX display-message -p -t P:0.1 '#{pane_id}')
check_fmt "$p2" '#{pane_width}x#{pane_height}' '59x6'
check_fmt "$p0" '#{pane_width}x#{pane_height}' '59x17'
# -b puts the new pane to the left of the target; -f makes it span the full
# window height.
check_ok split-window -d -h -b -f -l 10 -t "$p0"
p3=$($TMUX display-message -p -t P:0.0 '#{pane_id}')
check_fmt "$p3" '#{pane_width}x#{pane_height}' '10x24'
check_fmt "$p3" '#{pane_left},#{pane_top}' '0,0'
check_fmt "$p0" '#{pane_width}x#{pane_height}' '50x17'
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# The new pane becomes active unless -d is given.
check_ok select-pane -t "$p0"
check_ok split-window -d -v -t "$p0"
check_fmt 'P:0' '#{pane_id}' "$p0"
p4=$($TMUX display-message -p -t P:0.2 '#{pane_id}')
check_ok split-window -v -t "$p4"
p5=$($TMUX display-message -p -t 'P:0' '#{pane_id}')
check_ok kill-pane -t "$p5"
check_ok kill-pane -t "$p4"
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# ---------------------------------------------------------------------------
# break-pane and join-pane.
# break-pane moves a pane to a new window; -P -F prints where it went and -n
# names the new window.
out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \
-s "$p1" -t P:)
if [ "$out" != "1:$p1" ]; then
echo "break-pane -P output wrong: '$out'"
exit 1
fi
check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1'
check_fmt 'P:0' '#{window_panes}' '3'
# join-pane -v moves it back (the source window, left empty, is destroyed).
check_ok join-pane -d -v -s P:broken.0 -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
if $TMUX has-session -t P:broken 2>/dev/null; then
echo "Window 'broken' still exists after join-pane."
exit 1
fi
# The joined pane is below the target (-v, no -b).
top=$($TMUX display-message -p -t "$p2" '#{pane_bottom}')
joined=$($TMUX display-message -p -t "$p1" '#{pane_top}')
if [ "$joined" -le "$top" ]; then
echo "Joined pane is not below target ($joined <= $top)."
exit 1
fi
# join-pane -h -b puts the source to the left of the target; -l sets size.
check_ok break-pane -d -n broken -s "$p1" -t P:
check_ok join-pane -d -h -b -l 30 -s P:broken.0 -t "$p2"
check_fmt "$p1" '#{pane_width}' '30'
l1=$($TMUX display-message -p -t "$p1" '#{pane_left}')
l2=$($TMUX display-message -p -t "$p2" '#{pane_left}')
if [ "$l1" -ge "$l2" ]; then
echo "Joined pane is not left of target ($l1 >= $l2)."
exit 1
fi
# Joining a pane to itself is an error.
check_fail 'source and target panes must be different' \
join-pane -d -s "$p0" -t "$p0"
# break-pane to an occupied window index or with an invalid (non-UTF-8) name
# is an error.
check_fail 'index in use: 0' break-pane -d -s "$p1" -t P:0
check_fail "invalid window name: $(printf 'a\377b')" \
break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P:
# join-pane can move a pane from one window to another without destroying
# the source window if other panes remain. (On this branch move-pane is
# reserved for floating panes, covered by floating-pane-geometry.sh.)
check_ok new-window -d -t P:5 -n other
check_ok join-pane -d -s "$p1" -t P:5.0
check_fmt 'P:5' '#{window_panes}' '2'
check_fmt 'P:0' '#{window_panes}' '3'
check_ok join-pane -d -v -s "$p1" -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:5' '#{window_panes}' '1'
# ---------------------------------------------------------------------------
# swap-pane.
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# -s/-t swap two panes.
check_ok swap-pane -d -s "$p3" -t "$p1"
check_panes P:0 "0:$p1 1:$p0 2:$p2 3:$p3"
check_ok swap-pane -d -s "$p3" -t "$p1"
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# -U swaps the target pane with the previous pane, -D with the next; without
# -s the target is the active pane.
check_ok swap-pane -d -U -t "$p0"
check_panes P:0 "0:$p0 1:$p3 2:$p2 3:$p1"
check_ok swap-pane -d -D -t "$p0"
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# Without -d the target pane becomes the active pane (it arrives at the
# source pane's position).
check_ok select-pane -t "$p0"
check_ok swap-pane -s "$p0" -t "$p2"
check_fmt 'P:0' '#{pane_id}' "$p2"
check_panes P:0 "0:$p3 1:$p2 2:$p0 3:$p1"
check_ok swap-pane -s "$p2" -t "$p0"
check_fmt 'P:0' '#{pane_id}' "$p0"
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# With a marked pane and no -s, the marked pane is the swap source.
check_ok select-pane -m -t "$p3"
check_fmt "$p3" '#{pane_marked}' '1'
check_ok swap-pane -d -t "$p1"
check_panes P:0 "0:$p1 1:$p0 2:$p2 3:$p3"
check_ok swap-pane -d -t "$p1"
check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# select-pane -M clears the mark.
check_ok select-pane -M
check_fmt "$p3" '#{pane_marked}' '0'
check_fmt 'P:0' '#{pane_marked_set}' '0'
# ---------------------------------------------------------------------------
# resize-pane and zoom.
# Absolute -x on a horizontal split and percentage.
check_ok resize-pane -t "$p3" -x 20
check_fmt "$p3" '#{pane_width}' '20'
check_ok resize-pane -t "$p3" -x 25%
check_fmt "$p3" '#{pane_width}' '20'
check_ok resize-pane -t "$p3" -x 10
check_fmt "$p3" '#{pane_width}' '10'
# Relative adjustments: -R grows a left pane, -L shrinks it back; a count may
# be given.
check_ok resize-pane -t "$p3" -R
check_fmt "$p3" '#{pane_width}' '11'
check_ok resize-pane -t "$p3" -L
check_fmt "$p3" '#{pane_width}' '10'
check_ok resize-pane -t "$p3" -R 5
check_fmt "$p3" '#{pane_width}' '15'
check_ok resize-pane -t "$p3" -L 5
check_fmt "$p3" '#{pane_width}' '10'
# -y on a vertical split.
check_ok resize-pane -t "$p2" -y 10
check_fmt "$p2" '#{pane_height}' '10'
# p2 is the bottom pane, so its bottom border cannot move down: -D instead
# grows it by taking lines from the pane above and -U gives them back.
check_ok resize-pane -t "$p2" -D 2
check_fmt "$p2" '#{pane_height}' '12'
check_ok resize-pane -t "$p2" -U 2
check_fmt "$p2" '#{pane_height}' '10'
# Bad adjustment, width and height are errors.
check_fail 'adjustment invalid' resize-pane -t "$p2" -U nonsense
check_fail 'width invalid' resize-pane -t "$p2" -x nonsense
check_fail 'height invalid' resize-pane -t "$p2" -y nonsense
# -Z zooms: the pane temporarily fills the window and the flags show it.
check_ok resize-pane -Z -t "$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_width}x#{pane_height}' \
'1:80x24'
# Zoom is transparent to pane commands on other panes, and -Z again unzooms.
check_ok resize-pane -Z -t "$p0"
check_fmt "$p0" '#{window_zoomed_flag}' '0'
# Splitting while zoomed unzooms first.
check_ok resize-pane -Z -t "$p0"
check_fmt 'P:0' '#{window_zoomed_flag}' '1'
check_ok split-window -d -v -t "$p0"
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
check_fmt 'P:0' '#{window_panes}' '5'
p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}')
check_ok kill-pane -t "$p6"
# ---------------------------------------------------------------------------
# kill-pane.
check_fmt 'P:0' '#{window_panes}' '4'
check_ok kill-pane -t "$p3"
check_panes P:0 "0:$p0 1:$p2 2:$p1"
# -a kills every pane except the target.
check_ok kill-pane -a -t "$p0"
check_panes P:0 "0:$p0"
# Killing the last pane in a window kills the window.
check_ok new-window -d -t P:7 -n goner
check_ok kill-pane -t P:7.0
if $TMUX has-session -t P:goner 2>/dev/null; then
echo "Window 'goner' still exists after killing its only pane."
exit 1
fi
# ---------------------------------------------------------------------------
# respawn-pane and respawn-window.
# Respawning a pane whose process is alive fails without -k.
check_fail "respawn pane failed: pane P:0.0 still active" \
respawn-pane -t P:0.0
check_fail "respawn window failed: window P:0 still active" \
respawn-window -t P:0
# With remain-on-exit a pane whose command exited stays as a dead pane and
# may be respawned without -k.
check_ok set-option -g remain-on-exit on
check_ok new-window -d -t P:8 -n dead 'true'
i=0
while [ "$($TMUX display-message -p -t P:8.0 '#{pane_dead}')" != "1" ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Pane did not die."; exit 1; }
sleep 0.1
done
check_ok respawn-pane -t P:8.0 'sleep 100'
check_fmt 'P:8.0' '#{pane_dead}' '0'
# -k kills the live process and respawns.
check_ok respawn-pane -k -t P:8.0 'sleep 200'
check_fmt 'P:8.0' '#{pane_dead}' '0'
# respawn-window -k replaces the whole window (all panes) with one pane.
check_ok split-window -d -t P:8
check_fmt 'P:8' '#{window_panes}' '2'
check_ok respawn-window -k -t P:8 'sleep 300'
check_fmt 'P:8' '#{window_panes}' '1'
check_fmt 'P:8.0' '#{pane_dead}' '0'
check_ok set-option -g remain-on-exit off
# ---------------------------------------------------------------------------
# select-pane.
# A 2x2-ish arrangement: q0 on top, q1 bottom-left, q2 bottom-right.
check_ok new-window -d -t P:2 -n sel
q0=$($TMUX display-message -p -t P:2.0 '#{pane_id}')
check_ok split-window -d -v -t "$q0"
q1=$($TMUX display-message -p -t P:2.1 '#{pane_id}')
check_ok split-window -d -h -t "$q1"
q2=$($TMUX display-message -p -t P:2.2 '#{pane_id}')
# Directional selection: -D, -R and -U move by pane position.
check_ok select-pane -t "$q0"
check_ok select-pane -D -t P:2
check_fmt 'P:2' '#{pane_id}' "$q1"
check_ok select-pane -R -t P:2
check_fmt 'P:2' '#{pane_id}' "$q2"
check_ok select-pane -U -t P:2
check_fmt 'P:2' '#{pane_id}' "$q0"
# -l returns to the previously active pane; a window that never had another
# active pane has no last pane.
check_ok select-pane -l -t P:2
check_fmt 'P:2' '#{pane_id}' "$q2"
check_fail 'no last pane' select-pane -l -t P:8.0
# -d disables input to a pane, -e enables it again and -T sets the title.
check_ok select-pane -d -t "$q0"
check_fmt "$q0" '#{pane_input_off}' '1'
check_ok select-pane -e -t "$q0"
check_fmt "$q0" '#{pane_input_off}' '0'
check_ok select-pane -T mytitle -t "$q0"
check_fmt "$q0" '#{pane_title}' 'mytitle'
check_ok kill-window -t P:2
# ---------------------------------------------------------------------------
# more split-window variants.
check_ok new-window -d -t P:2 -n splits
# new-window -E creates an empty initial pane, running no command.
check_ok new-window -d -E -t P:9 -n empty
check_fmt 'P:9.0' '#{pane_dead}' '0'
check_fmt 'P:9.0' '#{pane_pid}' ''
check_fail 'command cannot be given for empty pane' \
new-window -d -E -t P:10 -n empty 'true'
# respawn-pane -E stores the command and cwd without starting it.
tmp=${TMPDIR:-/tmp}/tmux-pane-ops-empty-$$
rm -f "$tmp"
check_ok new-window -d -E -t P:10 -n empty-respawn
check_ok respawn-pane -E -c /tmp -t P:10.0 "pwd > $tmp"
if [ -e "$tmp" ]; then
echo "respawn-pane -E started command unexpectedly"
exit 1
fi
check_ok respawn-pane -t P:10.0
i=0
while [ ! -e "$tmp" ]; do
i=$((i + 1))
[ $i -gt 50 ] && echo "respawn-pane did not start stored command" && \
exit 1
sleep 0.1
done
if [ "$(cat "$tmp")" != "/tmp" ]; then
echo "respawn-pane did not use stored cwd"
exit 1
fi
rm -f "$tmp"
check_ok kill-window -t P:9
# respawn-window -E stores the command and cwd without starting it.
tmp=${TMPDIR:-/tmp}/tmux-window-ops-empty-$$
rm -f "$tmp"
check_ok new-window -d -E -t P:11 -n empty-respawn-window
check_ok respawn-window -E -c /tmp -t P:11 "pwd > $tmp"
check_fmt 'P:11.0' '#{pane_pid}' ''
if [ -e "$tmp" ]; then
echo "respawn-window -E started command unexpectedly"
exit 1
fi
check_ok respawn-window -t P:11
i=0
while [ ! -e "$tmp" ]; do
i=$((i + 1))
[ $i -gt 50 ] && echo "respawn-window did not start stored command" && \
exit 1
sleep 0.1
done
if [ "$(cat "$tmp")" != "/tmp" ]; then
echo "respawn-window did not use stored cwd"
exit 1
fi
rm -f "$tmp"
# -E splits with an empty pane, running no command; giving one is an error.
check_ok split-window -d -E -t P:2.0
check_fmt 'P:2' '#{window_panes}' '2'
check_fail 'command cannot be given for empty pane' \
split-window -d -E -t P:2.0 'sleep 5'
# -e adds to the new pane's environment.
eid=$($TMUX split-window -d -P -F '#{pane_id}' -e GREETING=hello -t P:2.0 \
'echo $GREETING; exec cat')
i=0
while out=$($TMUX capture-pane -p -t "$eid" | sed -n 1p) && \
[ "$out" != "hello" ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "split-window -e wrong: '$out'"; exit 1; }
sleep 0.1
done
# A bad -l size is an error.
check_fail 'invalid tiled geometry invalid' \
split-window -d -v -l invalid -t P:2.0
# -Z zooms the new pane.
check_ok split-window -d -Z -t P:2.0
check_fmt 'P:2' '#{window_zoomed_flag}' '1'
check_ok kill-window -t P:2
# ---------------------------------------------------------------------------
# more swap-pane: wrapping, cross-window, self and zoomed swaps.
check_ok new-window -d -t P:3 -n swaps
check_ok split-window -d -v -t P:3.0
check_ok split-window -d -v -t P:3.0
r0=$($TMUX display-message -p -t P:3.0 '#{pane_id}')
r1=$($TMUX display-message -p -t P:3.1 '#{pane_id}')
r2=$($TMUX display-message -p -t P:3.2 '#{pane_id}')
o0=$($TMUX display-message -p -t P:5.0 '#{pane_id}')
# -D on the last pane and -U on the first wrap around to the other end.
check_ok swap-pane -d -D -t "$r2"
check_panes P:3 "0:$r2 1:$r1 2:$r0"
check_ok swap-pane -d -s "$r0" -t "$r2"
check_ok swap-pane -d -U -t "$r0"
check_panes P:3 "0:$r2 1:$r1 2:$r0"
check_ok swap-pane -d -s "$r0" -t "$r2"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
# Swapping a pane with itself quietly does nothing.
check_ok swap-pane -d -s "$r1" -t "$r1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
# Panes can be swapped between different windows.
check_ok swap-pane -d -s "$o0" -t "$r1"
check_panes P:3 "0:$r0 1:$o0 2:$r2"
check_panes P:5 "0:$r1"
check_ok swap-pane -d -s "$r1" -t "$o0"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# -Z keeps the window zoomed across the swap.
check_ok resize-pane -Z -t "$r0"
check_ok swap-pane -d -Z -s "$r0" -t "$r1"
check_fmt 'P:3' '#{window_zoomed_flag}' '1'
check_ok resize-pane -Z -t P:3
check_panes P:3 "0:$r1 1:$r0 2:$r2"
# kill-pane -a -f only kills other panes matching the filter.
check_ok kill-pane -a -f '#{==:#{pane_id},'"$r2"'}' -t "$r1"
check_panes P:3 "0:$r1 1:$r0"
check_ok kill-window -t P:3
# ---------------------------------------------------------------------------
# split-window -I and -s.
check_ok new-window -d -t P:3 -n splits2
# -I fills the new (empty) pane from standard input.
printf 'stdin-stuff' | $TMUX split-window -d -I -t P:3.0
if [ $? -ne 0 ]; then
echo "split-window -I failed."
exit 1
fi
i=0
while out=$($TMUX capture-pane -p -t P:3.1 | sed -n 1p) && \
[ "$out" != "stdin-stuff" ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "split-window -I wrong: '$out'"; exit 1; }
sleep 0.1
done
# -s sets the new pane's window-style.
sid=$($TMUX split-window -d -P -F '#{pane_id}' -s 'bg=red' -t P:3.0)
out=$($TMUX show-options -v -p -t "$sid" window-style)
if [ "$out" != "bg=red" ]; then
echo "split-window -s style wrong: '$out'"
exit 1
fi
check_ok kill-window -t P:3
# ---------------------------------------------------------------------------
# more break-pane: -a insertion, selection and single-pane windows.
# check_windows $session $expected
#
# Compare the window list of a session (as "index:name ...") with $expected.
check_windows()
{
out=$(echo $($TMUX list-windows -t "$1" -F \
'#{window_index}:#{window_name}'))
if [ "$out" != "$2" ]; then
echo "Window list of '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_ok new-session -d -s Q -x 80 -y 24 -n q0
# -a breaks into a new window inserted after the target index, shuffling the
# following windows up.
check_ok new-window -d -t Q:1 -n q1
check_ok split-window -d -t Q:1
check_ok break-pane -d -a -s Q:1.1 -n qa -t Q:0
check_windows Q '0:q0 1:qa 2:q1'
# Without -d the new window is selected.
check_ok split-window -d -t Q:2
check_ok select-window -t Q:0
check_ok break-pane -s Q:2.1 -n qcur -t Q:
check_fmt 'Q:' '#{window_name}' 'qcur'
check_windows Q '0:q0 1:qa 2:q1 3:qcur'
# Breaking the only pane of a window relinks the window at a new index; -n
# still renames it.
check_ok new-window -d -t Q:5 -n qsolo
out=$($TMUX break-pane -d -P -F '#{window_name}' -s Q:5.0 -n qmoved -t Q:)
if [ "$out" != "qmoved" ]; then
echo "single-pane break-pane output wrong: '$out'"
exit 1
fi
if $TMUX has-session -t Q:qsolo 2>/dev/null; then
echo "Window 'qsolo' still exists after single-pane break-pane."
exit 1
fi
check_ok has-session -t Q:qmoved
check_ok kill-session -t Q
# ---------------------------------------------------------------------------
# resize-pane -T.
# -T trims the history: lines below the cursor position are removed and the
# cursor moves to the bottom. seq writes 100 lines (leaving 77 in history on
# a 24-line screen) and the escape sequence puts the cursor on line 5.
check_ok new-window -d -t P:2 'seq 1 100; printf "\033[5;1H"; exec cat'
i=0
while [ "$($TMUX display-message -p -t P:2.0 '#{history_size}')" != "77" ]
do
i=$((i + 1))
[ $i -gt 50 ] && { echo "History did not fill."; exit 1; }
sleep 0.1
done
check_fmt 'P:2.0' '#{cursor_y}' '4'
check_ok resize-pane -T -t P:2.0
check_fmt 'P:2.0' '#{history_size}' '58'
check_fmt 'P:2.0' '#{cursor_y}' '23'
check_ok kill-window -t P:2
assert_alive
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -131,11 +131,14 @@ $IN send-keys -l "Z" || exit 1
settle
search_is "hello Z" "C-w did not kill a word"
# C-a then C-k kills the whole line.
# C-a then C-k kills the whole line. The mode prompt no longer fills the rest
# of the row, so insert a marker to distinguish prompt input from tree content
# that may remain visible after the prompt.
$IN send-keys C-a || exit 1
$IN send-keys C-k || exit 1
$IN send-keys -l "X" || exit 1
settle
search_row | grep -q '(search) [^ ]' && fail "C-a C-k did not clear the line"
search_is "X" "C-a C-k did not clear the line"
# --- 3. Editing kept the prompt open the whole time. ---
in_tree_mode || fail "editing keys closed the mode"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
SB00 abcdefghij 
SB01 abcdefghij 
SB02 abcdefghij 
SB03 abc┌──────────────────┐ 
SB04 abc│FLOAT02 abcdef  │ 
SB05 abc│FLOAT03 abcdef  │ 
SB06 abc│FLOAT04 abcdef  │ 
SB07 abc│  │ 
SB08 abc└──────────────────┘ 
SB03 abc┌──────────────────┐ 
SB04 abc│FLOAT02 abcdef  │ 
SB05 abc│FLOAT03 abcdef  │ 
SB06 abc│FLOAT04 abcdef  │ 
SB07 abc│  │ 
SB08 abc└──────────────────┘ 
SB09 abcdefghij 
SB10 abcdefghij 
 

View File

@@ -1,10 +1,10 @@
 SB00 abcdefghij │ SBL00 abcdefghij
 SB01 abcdefghij │ SBL01 abcdefghij
 SB02 abcdefghij │ SBL02 abcdefghij
 SB03 abcdefghij │ SBL03 abcdefghij
 SB04 abcdefghij │ SBL04 abcdefghij
 SB05 abcdefghij │ SBL05 abcdefghij
 SB06 abcdefghij │ SBL06 abcdefghij
 SB00 abcdefghij │ SBL00 abcdefghij
 SB01 abcdefghij │ SBL01 abcdefghij
 SB02 abcdefghij │ SBL02 abcdefghij
 SB03 abcdefghij │ SBL03 abcdefghij
 SB04 abcdefghij │ SBL04 abcdefghij
 SB05 abcdefghij │ SBL05 abcdefghij
 SB06 abcdefghij │ SBL06 abcdefghij
 SB07 abcdefghij │ SBL07 abcdefghij
 SB08 abcdefghij │ SBL08 abcdefghij
 SB09 abcdefghij │ SBL09 abcdefghij

View File

@@ -1,10 +1,10 @@
SB00 abcdefghij  │SBR00 abcdefghij 
SB01 abcdefghij  │SBR01 abcdefghij 
SB02 abcdefghij  │SBR02 abcdefghij 
SB03 abcdefghij  │SBR03 abcdefghij 
SB04 abcdefghij  │SBR04 abcdefghij 
SB05 abcdefghij  │SBR05 abcdefghij 
SB06 abcdefghij  │SBR06 abcdefghij 
SB00 abcdefghij  │SBR00 abcdefghij 
SB01 abcdefghij  │SBR01 abcdefghij 
SB02 abcdefghij  │SBR02 abcdefghij 
SB03 abcdefghij  │SBR03 abcdefghij 
SB04 abcdefghij  │SBR04 abcdefghij 
SB05 abcdefghij  │SBR05 abcdefghij 
SB06 abcdefghij  │SBR06 abcdefghij 
SB07 abcdefghij  │SBR07 abcdefghij 
SB08 abcdefghij  │SBR08 abcdefghij 
SB09 abcdefghij  │SBR09 abcdefghij 

View File

@@ -1,8 +1,8 @@
STYLE00 abcdefghij │STYLE00 abcdefghij
STYLE01 abcdefghij │STYLE01 abcdefghij
STYLE02 abcdefghij │STYLE02 abcdefghij
STYLE03 abcdefghij │STYLE03 abcdefghij
STYLE04 abcdefghij │STYLE04 abcdefghij
STYLE00 abcdefghij │STYLE00 abcdefghij
STYLE01 abcdefghij │STYLE01 abcdefghij
STYLE02 abcdefghij │STYLE02 abcdefghij
STYLE03 abcdefghij │STYLE03 abcdefghij
STYLE04 abcdefghij │STYLE04 abcdefghij
STYLE05 abcdefghij │STYLE05 abcdefghij
STYLE06 abcdefghij │STYLE06 abcdefghij
 │

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

217
regress/session-ops.sh Normal file
View File

@@ -0,0 +1,217 @@
#!/bin/sh
# Tests of session management command semantics, as implemented in
# cmd-new-session.c, cmd-rename-session.c, cmd-kill-session.c and
# cmd-has-session.c, plus grouped sessions (new-session -t).
#
# This exercises:
# - new-session naming: explicit -s, invalid and duplicate names, automatic
# numeric names, -n naming the initial window and -A attaching to (here:
# not duplicating) an existing session;
# - session_id/session_name/session_windows formats and has-session;
# - rename-session, including duplicate and invalid names, and that the
# session keeps its id when renamed;
# - kill-session, kill-session -a (all but target), the "-f only valid with
# -a" guard, and that killing the last session stops the server;
# - grouped sessions: new-session -t shares the window list (a window made
# in one session appears in the other; killed windows disappear), current
# windows are tracked independently and destroying one grouped session
# leaves the windows in the other.
#
# session-group-resize.sh covers sizing of grouped sessions.
PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check_ok $cmd...
#
# Run a command and require that it succeeds.
check_ok()
{
if ! $TMUX "$@" </dev/null; 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 "$@" </dev/null 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
# check_fmt $target $format $expected
#
# Expand a format in a target's context and compare with $expected.
check_fmt()
{
out=$($TMUX display-message -p -t "$1" "$2" 2>&1)
if [ "$out" != "$3" ]; then
echo "Format '$2' for '$1' wrong."
echo "Expected: '$3'"
echo "But got: '$out'"
exit 1
fi
}
# check_sessions $expected
#
# Compare the session list (as "name name ...", sorted by name) with
# $expected.
check_sessions()
{
out=$(echo $($TMUX list-sessions -F '#{session_name}' | LC_ALL=C sort))
if [ "$out" != "$1" ]; then
echo "Session list wrong."
echo "Expected: '$1'"
echo "But got: '$out'"
exit 1
fi
}
# check_windows $session $expected
#
# Compare the window list of a session (as "index:name ...") with $expected.
check_windows()
{
out=$(echo $($TMUX list-windows -t "$1" -F \
'#{window_index}:#{window_name}'))
if [ "$out" != "$2" ]; then
echo "Window list of '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
# ---------------------------------------------------------------------------
# new-session and has-session.
check_ok new-session -d -s S1 -x 80 -y 24 -n first
check_fmt 'S1:' '#{session_name}:#{window_name}:#{session_windows}' \
'S1:first:1'
# A duplicate name is an error. Only invalid UTF-8 is rejected as a name:
# colons, periods and even an empty string are allowed (such sessions can
# only be targeted by id).
check_fail 'duplicate session: S1' new-session -d -s S1
badname=$(printf 'a\377b')
check_fail "invalid session name: $badname" new-session -d -s "$badname"
oddid=$($TMUX new-session -d -s 'a:b.c' -x 80 -y 24 -P -F '#{session_id}')
check_fmt "$oddid" '#{session_name}' 'a:b.c'
check_ok kill-session -t "$oddid"
emptyid=$($TMUX new-session -d -s '' -x 80 -y 24 -P -F '#{session_id}')
check_fmt "$emptyid" '#{session_name}' ''
check_ok kill-session -t "$emptyid"
# Without -s, sessions get a numeric name matching their id counter.
autoname=$($TMUX new-session -d -x 80 -y 24 -P -F '#{session_name}')
autoid=$($TMUX display-message -p -t "=$autoname:" '#{session_id}')
if [ "\$$autoname" != "$autoid" ]; then
echo "Automatic session name '$autoname' does not match id '$autoid'."
exit 1
fi
check_ok has-session -t "=$autoname"
check_ok rename-session -t "=$autoname" S2
check_sessions 'S1 S2'
# -A creates the session only if it does not exist; if it does, -A means
# attach, which a detached client without a terminal cannot do.
check_ok new-session -d -A -s S3
check_sessions 'S1 S2 S3'
check_fail 'open terminal failed: not a terminal' new-session -d -A -s S3
check_sessions 'S1 S2 S3'
check_ok kill-session -t S3
# has-session fails for a missing session.
check_fail "can't find session: nosuch" has-session -t nosuch
# ---------------------------------------------------------------------------
# rename-session.
# The id survives a rename and the old name is gone.
id=$($TMUX display-message -p -t S2: '#{session_id}')
check_ok rename-session -t S2 newname
check_sessions 'S1 newname'
check_fmt "$id" '#{session_name}' 'newname'
check_fail "can't find session: S2" has-session -t S2
# Renaming to an existing or invalid name is an error.
check_fail 'duplicate session: S1' rename-session -t newname S1
check_fail "invalid session name: $badname" rename-session -t newname \
"$badname"
check_ok rename-session -t newname S2
# ---------------------------------------------------------------------------
# grouped sessions (new-session -t).
check_ok new-session -d -s G1 -x 80 -y 24 -n shared
check_ok new-session -d -s G2 -t G1
check_fmt 'G1:' '#{session_grouped}:#{session_group_size}' '1:2'
check_fmt 'G2:' '#{session_grouped}:#{session_group_list}' '1:G1,G2'
# The window list is shared: windows created or killed in one session
# appear and disappear in the other.
check_ok new-window -d -t G2: -n added
check_windows G1 '0:shared 1:added'
check_windows G2 '0:shared 1:added'
check_ok kill-window -t G1:added
check_windows G2 '0:shared'
# The current window is tracked per session.
check_ok new-window -d -t G2:1 -n other
check_ok select-window -t G1:0
check_ok select-window -t G2:1
check_fmt 'G1:' '#{window_name}' 'shared'
check_fmt 'G2:' '#{window_name}' 'other'
# Killing one grouped session leaves the windows in the other (the group
# itself survives with a single member).
check_ok kill-session -t G2
check_windows G1 '0:shared 1:other'
check_fmt 'G1:' '#{session_grouped}:#{session_group_size}' '1:1'
check_ok kill-window -t G1:other
# ---------------------------------------------------------------------------
# kill-session.
check_sessions 'G1 S1 S2'
check_fail '-f only valid with -a' kill-session -f 'x' -t S1
# -C only clears alerts; the session survives.
check_ok kill-session -C -t S2
check_ok has-session -t S2
# -a kills every other session.
check_ok kill-session -a -t S1
check_sessions 'S1'
# Killing the last session stops the server.
check_ok kill-session -t S1
if $TMUX has-session -t S1 2>/dev/null; then
echo "Server still up after killing the last session."
exit 1
fi
exit 0

150
regress/set-hook-B.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX_TMPDIR="$OUT"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -Ltest-hook-B-$$ -f/dev/null"
fail()
{
echo "$*" >&2
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
wait_for()
{
option=$1
expected=$2
i=0
while [ $i -lt 30 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] && return 0
i=$((i + 1))
sleep 0.2
done
fail "expected $option to be '$expected' but got '$value'"
}
assert_unchanged()
{
option=$1
expected=$2
i=0
while [ $i -lt 15 ]; do
value=$($TMUX show -gqv "$option" 2>/dev/null || true)
[ "$value" = "$expected" ] || \
fail "expected $option to remain '$expected' but got '$value'"
i=$((i + 1))
sleep 0.2
done
}
$TMUX new -d -s one || fail "new-session failed"
$TMUX set -g @seen 0 || fail "set @seen failed"
$TMUX set-hook -g -B '@session-name::#{session_name}' \
'set -g @seen "#{hook}:#{hook_value}"' ||
fail "set-hook -B failed"
shown=$($TMUX show-hooks -g -B @session-name) ||
fail "show-hooks -B failed"
[ "$shown" = '@session-name::#{session_name}' ] ||
fail "unexpected show-hooks -B output: $shown"
assert_unchanged @seen 0
$TMUX rename-session two || fail "rename-session two failed"
wait_for @seen '@session-name:two'
$TMUX set -g @seen-last 0 || fail "set @seen-last failed"
$TMUX set-hook -g -B '@session-name::#{session_name}' \
'set -g @seen-last "#{hook_last}->#{hook_value}"' ||
fail "set-hook -B replacement failed"
assert_unchanged @seen-last 0
$TMUX rename-session one || fail "rename-session one failed"
wait_for @seen-last 'two->one'
$TMUX set-hook -gu -B @session-name || fail "set-hook -gu -B failed"
shown=$($TMUX show-hooks -g -B @session-name) ||
fail "show-hooks -B after remove failed"
[ -z "$shown" ] || fail "show-hooks -B showed removed monitor: $shown"
last=$($TMUX show -gqv @seen-last)
$TMUX rename-session three || fail "rename-session three failed"
assert_unchanged @seen-last "$last"
$TMUX set -gu @value || fail "unset @value failed"
$TMUX set -g @empty-seen 0 || fail "set @empty-seen failed"
$TMUX set-hook -g -B '@empty::#{@value}' \
'set -g @empty-seen "#{hook_last}->#{hook_value}"' ||
fail "set-hook -B empty failed"
assert_unchanged @empty-seen 0
$TMUX set -g @value changed || fail "set @value failed"
wait_for @empty-seen '->changed'
if $TMUX set-hook -g -B 'bad::#{session_name}' 'display-message x' \
>"$OUT/bad.out" 2>"$OUT/bad.err"; then
fail "non-@ monitor hook name was accepted"
fi
session=$($TMUX display -p '#{session_id}')
window=$($TMUX display -p '#{window_id}')
pane=$($TMUX display -p '#{pane_id}')
pane_number=${pane#%}
$TMUX set -gu @pane-value || fail "unset @pane-value failed"
$TMUX set -g @pane-seen 0 || fail "set @pane-seen failed"
$TMUX set-hook -g -B "@pane:%$pane_number:#{pane_width}" \
'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' ||
fail "set-hook -B pane selector failed"
assert_unchanged @pane-seen 0
$TMUX set-hook -g -B "@pane:%$pane_number:#{@pane-value}" \
'set -g @pane-seen "#{hook_session}:#{hook_window}:#{hook_window_index}:#{hook_pane}:#{hook_value}"' ||
fail "set-hook -B pane replacement failed"
assert_unchanged @pane-seen 0
$TMUX set -g @pane-value changed || fail "set @pane-value failed"
wait_for @pane-seen "$session:$window:0:$pane:changed"
$TMUX set -g @exact-value one || fail "set @exact-value failed"
$TMUX set -g @exact-seen 0 || fail "set @exact-seen failed"
$TMUX set -gw @foo 'set -g @exact-seen inherited' ||
fail "set global @foo failed"
$TMUX set-hook -w -B '@foo::#{@exact-value}' ||
fail "set-hook -B exact scope monitor failed"
assert_unchanged @exact-seen 0
$TMUX set -g @exact-value two || fail "set @exact-value two failed"
assert_unchanged @exact-seen 0
$TMUX set-hook -w -B '@foo::#{@exact-value}' \
'set -g @exact-seen "#{hook_value}"' ||
fail "set-hook -B exact scope command failed"
assert_unchanged @exact-seen 0
$TMUX set -g @exact-value three || fail "set @exact-value three failed"
wait_for @exact-seen three
target_pane=$($TMUX splitw -P -F '#{pane_id}') ||
fail "split-window failed"
$TMUX set -g @target-pane 0 || fail "set @target-pane failed"
$TMUX set-hook -g -B '@target:%*:#{@target-value}' \
'set -g @target-pane "#{pane_id}"' ||
fail "set-hook -B target pane failed"
assert_unchanged @target-pane 0
$TMUX set -pt "$target_pane" @target-value changed ||
fail "set pane @target-value failed"
wait_for @target-pane "$target_pane"
exit 0

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

@@ -0,0 +1,150 @@
#!/bin/sh
# Tests of pane target resolution in cmd-find.c.
#
# Building on targets.sh (session/window resolution), this exercises the pane
# half of cmd_find_target() in a known 2x2 split:
#
# - pane ids (%n), pane indices, and the +/- offset and ! last-pane tokens;
# - positional tokens {top-left}/{top-right}/{bottom-left}/{bottom-right}
# and {top}/{bottom}/{left}/{right};
# - directional tokens {up-of}/{down-of}/{left-of}/{right-of} relative to
# the active pane;
# - the ".pane" and "sess:win.pane" combined forms;
# - the marked pane, reached with ~ / {marked} and cleared with -M;
# - and the error paths: a pane id in the wrong window, a directional token
# with no neighbour, and an unset marked pane.
#
# The 2x2 split is created in a fixed order so pane ids are deterministic:
#
# +--------+--------+
# | %0 | %1 | top-left = %0 top-right = %1
# +--------+--------+ bottom-left = %2 bottom-right = %3
# | %2 | %3 |
# +--------+--------+
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check $target $expected [format]
#
# The default format is the pane id.
check()
{
fmt=${3:-'#{pane_id}'}
out=$($TMUX display-message -p -t "$1" "$fmt" 2>&1)
if [ "$out" != "$2" ]; then
echo "target '$1' resolved wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
check_fail()
{
out=$($TMUX has-session -t "$2" 2>&1)
if [ $? -eq 0 ]; then
echo "target '$2' resolved (expected failure)."
exit 1
fi
if [ "$out" != "$1" ]; then
echo "Wrong error for target '$2'."
echo "Expected: '$1'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
# --- fixture: a 2x2 split plus a single-pane window -----------------------
check_ok new-session -d -s p -x 80 -y 24
check_ok split-window -h -t p:0 # %0 left, %1 right
check_ok split-window -v -t p:0.%0 # split left: %0 top, %2 bottom
check_ok split-window -v -t p:0.%1 # split right: %1 top, %3 bottom
check_ok new-window -d -t p: -n solo # a second, single-pane window
# --- pane ids, index, offsets ---------------------------------------------
check "p:0.%3" "%3" # exact pane id
check "p:0.3" "%3" # pane by index
check "p:0.%1" "%1" # sess:win.pane form
check ".%1" "%1" # .pane form (current window)
# "sess:.pane" (empty window part) resolves the pane in the session's current
# window. Make window 0 current first.
check_ok select-window -t p:0
check "p:.%1" "%1"
check "p:.{top-left}" "%0"
# Offsets are relative to the active pane; make %0 active first.
check_ok select-pane -t p:0.%0
check "p:0.+" "1" '#{pane_index}' # next pane
check "p:0.-" "3" '#{pane_index}' # previous pane (wraps)
# --- last pane (!) --------------------------------------------------------
check_ok select-pane -t p:0.%2
check_ok select-pane -t p:0.%0 # now the last pane is %2
check "p:0.!" "%2"
# --- positional tokens (absolute geometry) --------------------------------
check "p:0.{top-left}" "%0"
check "p:0.{top-right}" "%1"
check "p:0.{bottom-left}" "%2"
check "p:0.{bottom-right}" "%3"
check "p:0.{top}" "%0" # leftmost of the top row
check "p:0.{bottom}" "%2" # leftmost of the bottom row
check "p:0.{left}" "%0" # top of the left column
check "p:0.{right}" "%1" # top of the right column
# --- directional tokens (relative to the active pane) ---------------------
#
# From the top-left pane the real neighbours are below and to the right.
check_ok select-pane -t p:0.%0
check "p:0.{down-of}" "%2"
check "p:0.{right-of}" "%1"
# From the bottom-right pane the real neighbours are above and to the left.
check_ok select-pane -t p:0.%3
check "p:0.{up-of}" "%1"
check "p:0.{left-of}" "%2"
# --- pane error paths -----------------------------------------------------
check_fail "can't find pane: %0" "p:solo.%0" # pane id, wrong window
check_fail "can't find pane: {up-of}" "p:solo.{up-of}" # no neighbour
check_fail "can't find pane: 9" "p:0.9" # no such index
# --- marked pane ----------------------------------------------------------
#
# ~ / {marked} resolve to the marked pane from anywhere; -M clears it.
check_fail "no marked target" "~" # nothing marked yet
check_ok select-pane -m -t p:0.%1
check "~" "%1"
check "{marked}" "%1"
# The mark is global: it resolves even with a different current window.
check_ok select-window -t p:solo
check "~" "%1"
check_ok select-window -t p:0
check_ok select-pane -M # clear the mark
check_fail "no marked target" "~"
check_fail "no marked target" "{marked}"
assert_alive "after pane target tests"
$TMUX kill-server 2>/dev/null
exit 0

226
regress/targets.sh Normal file
View File

@@ -0,0 +1,226 @@
#!/bin/sh
# Tests of target (session and window) resolution in cmd-find.c.
#
# A target string like "session:window.pane" is parsed by cmd_find_target()
# and resolved to a concrete session/window/pane. This exercises the session
# and window halves of that machinery:
#
# - session and window ids ($n, @n) and names;
# - exact (=name), prefix and fnmatch matching, and the ambiguous/missing
# error paths for each;
# - the combined "sess:", "sess:win", ":win" and "sess:win.pane" forms and
# the empty (current) target;
# - the offset and special window tokens (^ $ ! + - and their {start},
# {end}, {last}, {next}, {previous} spellings), including +N/-N with
# wrap-around;
# - the special whole-target tokens {active}/@/{current} and {mouse}/=;
# - the CMD_FIND_WINDOW_INDEX "can't specify pane here" guard; and
# - -s versus -t resolution on link-window/move-window.
#
# Positive cases are asserted with display-message -p -t (which renders the
# resolved target); error cases with has-session -t, which resolves strictly
# and prints the cmd-find error text.
#
# Pane resolution (directional/positional tokens, marked pane) is covered by
# targets-panes.sh.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check $target $expected [format]
#
# Resolve $target and compare the rendered value. The default format is the
# window index; pass a third argument to override.
check()
{
fmt=${3:-'#{window_index}'}
out=$($TMUX display-message -p -t "$1" "$fmt" 2>&1)
if [ "$out" != "$2" ]; then
echo "target '$1' resolved wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $target
#
# has-session resolves the target strictly and prints the cmd-find error.
check_fail()
{
out=$($TMUX has-session -t "$2" 2>&1)
if [ $? -eq 0 ]; then
echo "target '$2' resolved (expected failure)."
exit 1
fi
if [ "$out" != "$1" ]; then
echo "Wrong error for target '$2'."
echo "Expected: '$1'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
# --- fixture --------------------------------------------------------------
#
# Session alpha with four named windows (0 editor, 1 editing, 2 shell,
# 3 logs); "editor"/"editing" share a prefix for the ambiguity tests. Two
# grp* sessions share a prefix for the session ambiguity tests.
check_ok new-session -d -s alpha -x 80 -y 24
check_ok rename-window -t alpha:0 editor
check_ok new-window -d -t alpha: -n editing
check_ok new-window -d -t alpha: -n shell
check_ok new-window -d -t alpha: -n logs
check_ok new-session -d -s beta -x 80 -y 24
check_ok new-window -d -t beta: -n bw1
check_ok new-session -d -s grp1 -x 80 -y 24
check_ok new-session -d -s grp2 -x 80 -y 24
# Give alpha a last-window (2) with the current window left at 0.
check_ok select-window -t alpha:2
check_ok select-window -t alpha:0
# --- session ids and names ------------------------------------------------
sid=$($TMUX display-message -p -t alpha: '#{session_id}')
check "$sid:" "alpha" '#{session_name}'
check "=alpha:" "alpha" '#{session_name}' # exact
check "alpha:" "alpha" '#{session_name}' # full name
check "al:" "alpha" '#{session_name}' # prefix
check "al*:" "alpha" '#{session_name}' # fnmatch
# --- session error paths --------------------------------------------------
check_fail "can't find session: grp" "grp:" # ambiguous prefix
check_fail "can't find session: grp*" "grp*:" # ambiguous fnmatch
check_fail "can't find session: al" "=al:" # exact-only, no such session
check_fail "can't find session: nosuch" "nosuch:"
# --- window ids and names -------------------------------------------------
wid=$($TMUX display-message -p -t alpha:editing '#{window_id}')
# A bare @id (no session) resolves both window and session.
check "$wid" "alpha:1" '#{session_name}:#{window_index}'
check "alpha:shell" "2" # exact name
check "alpha:edito" "0" # prefix
check "alpha:=editor" "0" # exact match flag
check "alpha:sh*" "2" # fnmatch
check "alpha:1" "1" # index
# A window id qualified by a session resolves within that session; a window id
# belonging to a different session is rejected.
w2=$($TMUX display-message -p -t alpha:shell '#{window_id}')
check "alpha:$w2" "2"
bw=$($TMUX display-message -p -t beta: '#{window_id}')
check_fail "can't find window: $bw" "alpha:$bw" # window id, wrong session
# --- window error paths ---------------------------------------------------
check_fail "can't find window: edit" "alpha:edit" # ambiguous prefix
check_fail "can't find window: e*" "alpha:e*" # ambiguous fnmatch
check_fail "can't find window: nope" "alpha:nope" # missing
check_fail "can't find window: @999" "@999" # missing window id
# --- offset and special window tokens -------------------------------------
#
# alpha's current window is 0; offsets wrap around the four windows.
check "alpha:^" "0" # start
check "alpha:\$" "3" # end
check "alpha:+" "1" # next
check "alpha:-" "3" # previous (wraps)
check "alpha:+2" "2"
check "alpha:-2" "2" # wraps
check "alpha:{start}" "0"
check "alpha:{end}" "3"
check "alpha:{next}" "1"
check "alpha:{previous}" "3"
check "alpha:!" "2" # last window
check "alpha:{last}" "2"
# --- combined and empty forms ---------------------------------------------
#
# Empty targets use the current pane from TMUX_PANE when there is no client.
# This keeps the test independent of the best-session fallback.
check_ok select-window -t alpha:0
pane=$($TMUX display-message -p -t alpha:0 '#{pane_id}')
TMUX_PANE=$pane check "" "alpha" '#{session_name}' # empty target is current
TMUX_PANE=$pane check "" "alpha:0" '#{session_name}:#{window_index}'
TMUX_PANE=$pane check ":shell" "alpha:2" '#{session_name}:#{window_index}'
check "alpha:shell.0" "alpha:2" '#{session_name}:#{window_index}'
TMUX_PANE=$pane check "alpha:.0" "alpha:0" '#{session_name}:#{window_index}' # empty window part
# --- bare-name fallbacks --------------------------------------------------
#
# A bare pane target that is not a pane falls back to a window, then to a
# session, using the current session (alpha).
TMUX_PANE=$pane check "editor" "0" '#{window_index}' # bare window name
check "beta" "beta" '#{session_name}' # bare session name
# --- whole-target special tokens ------------------------------------------
#
# {active}/@/{current} need a client with a session; with only a detached
# command client they must error cleanly (regression: this used to crash the
# server via a NULL session dereference). {mouse}/= need a mouse event.
check_fail "no current client" "{active}"
check_fail "no current client" "@"
check_fail "no current client" "{current}"
check_fail "no mouse target" "{mouse}"
check_fail "no mouse target" "="
assert_alive "after whole-target special tokens"
# --- CMD_FIND_WINDOW_INDEX rejects a pane part ----------------------------
out=$($TMUX new-window -d -t 'alpha:1.%0' 2>&1)
[ $? -ne 0 ] || { echo "new-window with pane target succeeded"; exit 1; }
[ "$out" = "can't specify pane here" ] || \
{ echo "wrong pane-here error: '$out'"; exit 1; }
# --- window index targets: offsets resolve to an index --------------------
#
# new-window's -t is a window index (CMD_FIND_WINDOW_INDEX); an offset from
# the current window (0) picks the numeric index rather than an existing
# window, so "+6" creates window 6.
check_ok select-window -t alpha:0
check_ok new-window -d -t 'alpha:+6' -n offwin
check "alpha:6" "offwin" '#{window_name}'
check_ok kill-window -t alpha:6
# --- -s versus -t resolution ----------------------------------------------
#
# link-window takes a source window (-s) and a destination index (-t); each
# side is resolved independently by cmd-find.
check_ok new-session -d -s src -x 80 -y 24
check_ok new-window -d -t src: -n payload
check_ok link-window -s src:payload -t alpha:9
check "alpha:9" "payload" '#{window_name}'
# move-window relocates it; the old index must be gone.
check_ok move-window -s alpha:9 -t alpha:5
check "alpha:5" "payload" '#{window_name}'
check_fail "can't find window: 9" "alpha:9"
# --- default state with no client -----------------------------------------
#
# run-shell with no target and no attached client has cmd-find build the
# current state from nothing (the best session).
check_ok run-shell 'true'
assert_alive "after target tests"
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -90,7 +90,6 @@ You should see the Greek word 'kosme': "κόσμε"
2.3.2 U-0000E000 = ee 80 80 = "" |
2.3.3 U-0000FFFD = ef bf bd = "<22>" |
2.3.4 U-0010FFFF = f4 8f bf bf = "􏿿" |
2.3.5 U-00110000 = f4 90 80 80 = "<22>" |
|
3 Malformed sequences |
|

406
regress/window-ops.sh Normal file
View File

@@ -0,0 +1,406 @@
#!/bin/sh
# Tests of window management command semantics (not parsing), as implemented
# in cmd-new-window.c, cmd-move-window.c (move-window and link-window),
# cmd-unlink-window.c, cmd-swap-window.c, cmd-rotate-window.c,
# cmd-kill-window.c and cmd-select-window.c.
#
# This exercises:
# - new-window placement: next free index, explicit index, index in use with
# and without -k, -a (after) and -b (before) insertion with shuffling, and
# -S selecting an existing window by name instead of creating;
# - move-window to a free index, to an occupied index with and without -k,
# -a insertion and -r renumbering (including base-index);
# - renumber-windows closing gaps;
# - link-window sharing a window between two sessions (window_linked and
# window_linked_sessions), unlink-window removing one link and refusing to
# unlink the last link without -k;
# - swap-window within and between sessions, -d keeping the active window,
# and the grouped-sessions error;
# - rotate-window -U/-D rotating pane positions;
# - kill-window switching to the last (previously current) window, kill-window
# -a killing all other windows and the "-f only valid with -a" guard.
#
# pane-ops.sh covers pane-level commands and buffers.sh covers paste buffers.
PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# check_ok $cmd...
#
# Run a command and require that it succeeds.
check_ok()
{
if ! $TMUX "$@"; then
echo "Command failed (expected success): $*"
exit 1
fi
}
# check_fail $expected_error $cmd...
#
# Run a command and require that it fails with the given error message.
check_fail()
{
exp="$1"
shift
out=$($TMUX "$@" 2>&1)
if [ $? -eq 0 ]; then
echo "Command succeeded (expected failure): $*"
exit 1
fi
if [ "$out" != "$exp" ]; then
echo "Wrong error for: $*"
echo "Expected: '$exp'"
echo "But got: '$out'"
exit 1
fi
}
# check_windows $session $expected
#
# Compare the window list of a session (as "index:name index:name ...", in
# index order) with $expected.
check_windows()
{
out=$(echo $($TMUX list-windows -t "$1" -F \
'#{window_index}:#{window_name}'))
if [ "$out" != "$2" ]; then
echo "Window list of '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
# check_fmt $target $format $expected
#
# Expand a format in a target's context and compare with $expected.
check_fmt()
{
out=$($TMUX display-message -p -t "$1" "$2" 2>&1)
if [ "$out" != "$3" ]; then
echo "Format '$2' for '$1' wrong."
echo "Expected: '$3'"
echo "But got: '$out'"
exit 1
fi
}
assert_alive()
{
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died: $1"
exit 1
fi
}
# ---------------------------------------------------------------------------
# new-window placement.
check_ok new-session -d -s W -x 80 -y 24 -n w0
# Next free index.
check_ok new-window -d -t W: -n w1
check_ok new-window -d -t W: -n w2
check_windows W '0:w0 1:w1 2:w2'
# Explicit index, then the next new window fills the first free index, not
# one past the highest.
check_ok new-window -d -t W:9 -n w9
check_ok new-window -d -t W: -n w3
check_windows W '0:w0 1:w1 2:w2 3:w3 9:w9'
# Occupied index fails without -k and replaces with -k.
check_fail 'create window failed: index 9 in use' \
new-window -d -t W:9 -n dup
check_ok new-window -d -k -t W:9 -n w9k
check_windows W '0:w0 1:w1 2:w2 3:w3 9:w9k'
# -a inserts after the target, shuffling the following windows up.
check_ok new-window -d -a -t W:1 -n wA
check_windows W '0:w0 1:w1 2:wA 3:w2 4:w3 9:w9k'
# -b inserts before the target, shuffling the target and followers up.
check_ok new-window -d -b -t W:0 -n wB
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
# -S selects an existing window with the same name instead of creating (with
# -d it would not switch, so no -d here).
check_ok select-window -t W:0
check_ok new-window -S -t W: -n w3
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '5:w3'
# Clean up to a known arrangement.
check_ok kill-window -t W:wB
check_ok kill-window -t W:wA
check_ok kill-window -t W:w9k
check_ok move-window -r -t W:
check_windows W '0:w0 1:w1 2:w2 3:w3'
# ---------------------------------------------------------------------------
# move-window.
# To a free index.
check_ok move-window -d -s W:2 -t W:7
check_windows W '0:w0 1:w1 3:w3 7:w2'
# To an occupied index, without and with -k.
check_fail 'index in use: 7' move-window -d -s W:3 -t W:7
check_ok move-window -d -k -s W:3 -t W:7
check_windows W '0:w0 1:w1 7:w3'
# -a inserts after the target and shuffles.
check_ok move-window -d -a -s W:7 -t W:0
check_windows W '0:w0 1:w3 2:w1'
# -r renumbers in order, respecting base-index.
check_ok move-window -d -s W:2 -t W:8
check_ok set-option -t W base-index 5
check_ok move-window -r -t W:
check_windows W '5:w0 6:w3 7:w1'
check_ok set-option -t W base-index 0
check_ok move-window -r -t W:
check_windows W '0:w0 1:w3 2:w1'
# With the renumber-windows option on, killing a window renumbers the rest
# automatically.
check_ok set-option -t W renumber-windows on
check_ok new-window -d -t W:9 -n wtmp
check_windows W '0:w0 1:w3 2:w1 9:wtmp'
check_ok kill-window -t W:1
check_windows W '0:w0 1:w1 2:wtmp'
check_ok kill-window -t W:2
check_ok set-option -t W renumber-windows off
check_ok new-window -d -t W:2 -n w3
check_windows W '0:w0 1:w1 2:w3'
# Without -s, the current window of the client/session moves.
check_ok select-window -t W:2
check_ok move-window -d -t W:6
check_windows W '0:w0 1:w1 6:w3'
check_ok move-window -r -t W:
check_windows W '0:w0 1:w1 2:w3'
# ---------------------------------------------------------------------------
# link-window and unlink-window.
check_ok new-session -d -s L -x 80 -y 24 -n l0
# Link a window from W into L and check it is shared.
check_ok link-window -d -s W:w1 -t L:5
check_windows L '0:l0 5:w1'
check_fmt 'W:w1' '#{window_linked}' '1'
check_fmt 'L:5' '#{window_linked_sessions}' '2'
# The linked window is the same window: renaming in one session shows in the
# other.
check_ok rename-window -t L:5 shared
check_windows W '0:w0 1:shared 2:w3'
check_ok rename-window -t W:1 w1
# Linking again to an occupied index fails without -k.
check_fail 'index in use: 0' link-window -d -s W:w3 -t L:0
# Unlink removes one link; the window survives in the other session.
check_ok unlink-window -t L:5
check_windows L '0:l0'
check_windows W '0:w0 1:w1 2:w3'
check_fmt 'W:w1' '#{window_linked}' '0'
# Unlinking a window linked to only one session needs -k.
check_fail 'window only linked to one session' unlink-window -t W:w3
check_ok unlink-window -k -t W:w3
check_windows W '0:w0 1:w1'
# ---------------------------------------------------------------------------
# swap-window.
check_ok new-window -d -t W:2 -n w2
check_ok new-window -d -t W:3 -n w3
# Swap within a session: indices are exchanged.
check_ok swap-window -d -s W:0 -t W:3
check_windows W '0:w3 1:w1 2:w2 3:w0'
check_ok swap-window -d -s W:0 -t W:3
check_windows W '0:w0 1:w1 2:w2 3:w3'
# Without -d the current index does not change, so the window that arrives
# there becomes current; with -d the swapped windows are selected, so the
# source window stays current at its new index.
check_ok select-window -t W:0
check_ok swap-window -s W:0 -t W:3
check_fmt 'W:' '#{window_index}:#{window_name}' '0:w3'
check_ok swap-window -s W:3 -t W:0
check_fmt 'W:' '#{window_index}:#{window_name}' '0:w0'
check_ok swap-window -d -s W:0 -t W:3
check_fmt 'W:' '#{window_index}:#{window_name}' '3:w0'
check_ok swap-window -d -s W:3 -t W:0
check_fmt 'W:' '#{window_index}:#{window_name}' '0:w0'
# Swap between two different sessions.
check_ok swap-window -d -s W:w2 -t L:l0
check_windows W '0:w0 1:w1 2:l0 3:w3'
check_windows L '0:w2'
check_ok swap-window -d -s W:2 -t L:0
check_windows W '0:w0 1:w1 2:w2 3:w3'
check_windows L '0:l0'
# Swapping between two sessions in the same group is an error.
check_ok new-session -d -s WG -t W
check_fail "can't move window, sessions are grouped" \
swap-window -d -s W:0 -t WG:1
check_ok kill-session -t WG
check_windows W '0:w0 1:w1 2:w2 3:w3'
# ---------------------------------------------------------------------------
# rotate-window.
check_ok new-session -d -s R -x 80 -y 24
check_ok split-window -d -t R:0
check_ok split-window -d -t R:0
p0=$($TMUX display-message -p -t R:0.0 '#{pane_id}')
p1=$($TMUX display-message -p -t R:0.1 '#{pane_id}')
p2=$($TMUX display-message -p -t R:0.2 '#{pane_id}')
# check_panes $target $expected
#
# Compare the pane list of a window (as "index:id ...") with $expected.
check_panes()
{
out=$(echo $($TMUX list-panes -t "$1" -F \
'#{pane_index}:#{pane_id}'))
if [ "$out" != "$2" ]; then
echo "Pane list of '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
check_panes R:0 "0:$p0 1:$p1 2:$p2"
# -U rotates panes up (each pane moves to the previous position); -D rotates
# down. -U then -D restores the original order.
check_ok rotate-window -U -t R:0
check_panes R:0 "0:$p1 1:$p2 2:$p0"
check_ok rotate-window -D -t R:0
check_panes R:0 "0:$p0 1:$p1 2:$p2"
check_ok rotate-window -D -t R:0
check_panes R:0 "0:$p2 1:$p0 2:$p1"
check_ok rotate-window -U -t R:0
# The active position is preserved across rotation: the pane that arrives at
# the active position becomes the active pane.
check_ok select-pane -t R:0.0
check_ok rotate-window -U -t R:0
check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0"
# ---------------------------------------------------------------------------
# kill-window.
# Killing the current window switches to the last (previously current)
# window.
check_ok select-window -t W:1
check_ok select-window -t W:3
check_fmt 'W:' '#{window_index}' '3'
check_ok kill-window -t W:3
check_fmt 'W:' '#{window_index}' '1'
check_windows W '0:w0 1:w1 2:w2'
# -f is only valid with -a.
check_fail '-f only valid with -a' kill-window -f 'x' -t W:0
# -a kills every window except the target.
check_ok kill-window -a -t W:w1
check_windows W '1:w1'
# ---------------------------------------------------------------------------
# select-window, next-window, previous-window.
# -P prints where the new window went; an invalid (non-UTF-8) name is an
# error.
out=$($TMUX new-window -d -t W:0 -n wa -P -F '#{window_index}:#{window_name}')
if [ "$out" != "0:wa" ]; then
echo "new-window -P output wrong: '$out'"
exit 1
fi
check_fail "invalid window name: $(printf 'a\377b')" \
new-window -d -t W: -n "$(printf 'a\377b')"
check_ok new-window -d -t W:2 -n wc
check_windows W '0:wa 1:w1 2:wc'
# -n and -p select the next and previous window, wrapping at the ends.
check_ok select-window -t W:0
check_ok select-window -n -t W:
check_fmt 'W:' '#{window_index}' '1'
check_ok select-window -n -t W:
check_fmt 'W:' '#{window_index}' '2'
check_ok select-window -n -t W:
check_fmt 'W:' '#{window_index}' '0'
check_ok select-window -p -t W:
check_fmt 'W:' '#{window_index}' '2'
# next-window and previous-window are the same code.
check_ok next-window -t W:
check_fmt 'W:' '#{window_index}' '0'
check_ok previous-window -t W:
check_fmt 'W:' '#{window_index}' '2'
# -l selects the previously current window and select-window -T on the
# already-current window does the same.
check_ok select-window -t W:1
check_ok select-window -l -t W:
check_fmt 'W:' '#{window_index}' '2'
check_ok select-window -T -t W:2
check_fmt 'W:' '#{window_index}' '1'
check_ok select-window -T -t W:2
check_fmt 'W:' '#{window_index}' '2'
# With -a, next-window looks for a window with an alert and fails if there
# is none; a fresh session has no last window.
check_fail 'no next window' next-window -a -t W:
check_fail 'no previous window' previous-window -a -t W:
check_ok new-session -d -s F -x 80 -y 24
check_fail 'no last window' select-window -l -t F:
check_fail 'no last window' select-window -T -t F:0
check_ok kill-session -t F
# ---------------------------------------------------------------------------
# more kill-window -a: no-op, filters and multiply-linked windows.
# -a with a single window in the session does nothing.
check_ok kill-window -a -t L:0
check_windows L '0:l0'
# -a -f only kills other windows matching the filter.
check_ok kill-window -a -f '#{==:#{window_name},wc}' -t W:0
check_windows W '0:wa 1:w1'
# If the current window is linked into the session more than once, -a kills
# it too - taking the whole session with it here.
check_ok new-session -d -s D -x 80 -y 24 -n d0
check_ok link-window -d -s D:0 -t D:5
check_ok new-window -d -t D:1 -n dx
check_ok select-window -t D:0
check_ok kill-window -a -t D:0
if $TMUX has-session -t D 2>/dev/null; then
echo "Session D survived kill-window -a on multiply-linked window."
exit 1
fi
check_fmt 'R:0' '#{window_panes}' '3'
assert_alive
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -1655,12 +1655,14 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
if (wp != NULL) {
if (wp->base.mode & MODE_SYNC)
screen_write_stop_sync(wp);
screen_write_clear_dirty(wp);
} else {
TAILQ_FOREACH(loop, &scene->w->panes, entry) {
if (!window_pane_is_visible(loop))
continue;
if (loop->base.mode & MODE_SYNC)
screen_write_stop_sync(loop);
screen_write_clear_dirty(loop);
}
}
}

View File

@@ -38,6 +38,7 @@ static int screen_write_overwrite(struct screen_write_ctx *,
struct grid_cell *, u_int);
static int screen_write_combine(struct screen_write_ctx *,
const struct grid_cell *);
static void screen_write_flush_dirty(struct window_pane *);
struct screen_write_citem {
u_int x;
@@ -214,6 +215,48 @@ screen_write_pane_is_obscured(struct screen_write_ctx *ctx)
return (0);
}
/* Should we draw to the TTY? */
static int
screen_write_should_draw_lines(struct screen_write_ctx *ctx, u_int y, u_int ny)
{
struct window_pane *wp = ctx->wp;
struct screen *s = ctx->s;
u_int sy = screen_size_y(s);
bitstr_t *bs;
if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP)))
return (0);
if (s->mode & MODE_SYNC) {
if (wp != NULL && y < sy && ny != 0) {
bs = wp->sync_dirty;
if (ny > sy - y)
ny = sy - y;
if (bs == NULL || wp->sync_dirty_size != sy) {
if (bs != NULL && wp->sync_dirty_size != sy) {
y = 0;
ny = sy;
}
free(bs);
bs = wp->sync_dirty = bit_alloc(sy);
if (bs == NULL)
fatal("bit_alloc failed");
wp->sync_dirty_size = sy;
}
bit_nset(bs, y, y + ny - 1);
}
return (0);
}
return (1);
}
/* Should we draw this line to the TTY? */
static int
screen_write_should_draw_line(struct screen_write_ctx *ctx, u_int y)
{
return (screen_write_should_draw_lines(ctx, y, 1));
}
/* Set up context for TTY command. */
static void
screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
@@ -979,7 +1022,7 @@ screen_write_sync_callback(__unused int fd, __unused short events, void *arg)
if (wp->base.mode & MODE_SYNC) {
wp->base.mode &= ~MODE_SYNC;
wp->flags |= PANE_REDRAW;
screen_write_flush_dirty(wp);
}
}
@@ -1004,14 +1047,14 @@ screen_write_start_sync(struct window_pane *wp)
void
screen_write_stop_sync(struct window_pane *wp)
{
if (wp == NULL)
if (wp == NULL || (~wp->base.mode & MODE_SYNC))
return;
if (event_initialized(&wp->sync_timer))
evtimer_del(&wp->sync_timer);
wp->base.mode &= ~MODE_SYNC;
wp->flags |= PANE_REDRAW;
screen_write_flush_dirty(wp);
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
}
@@ -1166,9 +1209,6 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
struct visible_ranges *r;
struct visible_range *ri;
if (s->mode & MODE_SYNC)
return;
r = window_visible_ranges(wp, xoff, yoff + yy, sx, NULL);
for (i = 0; i < r->used; i++) {
ri = &r->ranges[i];
@@ -1207,6 +1247,44 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
}
}
/* Redraw dirty lines. */
static void
screen_write_flush_dirty(struct window_pane *wp)
{
struct screen_write_ctx ctx;
struct tty_ctx ttyctx;
struct screen *s = &wp->base;
u_int y, sy = screen_size_y(s), lines = 0;
if (wp->sync_dirty == NULL)
return;
screen_write_start_pane(&ctx, wp, s);
screen_write_initctx(&ctx, &ttyctx, 1, 1);
for (y = 0; y < sy; y++) {
if (bit_test(wp->sync_dirty, y)) {
screen_write_redraw_line(&ctx, &ttyctx, y);
lines++;
}
}
log_debug("%s: %%%u had %u dirty lines", __func__, wp->id, lines);
screen_write_stop(&ctx);
screen_write_clear_dirty(wp);
}
/* Clear any dirty lines. */
void
screen_write_clear_dirty(struct window_pane *wp)
{
if (wp != NULL && wp->sync_dirty != NULL) {
free(wp->sync_dirty);
wp->sync_dirty = NULL;
wp->sync_dirty_size = 0;
}
}
/* Redraw all visible cells in a pane. */
static void
screen_write_redraw_pane(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
@@ -1249,7 +1327,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
screen_write_initctx(ctx, &ttyctx, 1, 1);
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, 0, screen_size_y(s)))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_alignmenttest, &ttyctx);
@@ -1290,7 +1368,7 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = nx;
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_line(ctx, s->cy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_insertcharacter, &ttyctx);
@@ -1331,7 +1409,7 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = nx;
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_line(ctx, s->cy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_deletecharacter, &ttyctx);
@@ -1372,7 +1450,7 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = nx;
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_line(ctx, s->cy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_clearcharacter, &ttyctx);
@@ -1413,7 +1491,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = ny;
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_insertline, &ttyctx);
@@ -1440,7 +1518,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = ny;
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, s->cy, s->rlower + 1 - s->cy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_insertline, &ttyctx);
@@ -1457,7 +1535,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct tty_ctx ttyctx;
u_int sy = screen_size_y(s);
u_int sy = screen_size_y(s), ry;
if (ny == 0)
ny = 1;
@@ -1481,7 +1559,8 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = ny;
if (s->mode & MODE_SYNC)
ry = s->rlower + 1 - s->rupper;
if (!screen_write_should_draw_lines(ctx, s->rupper, ry))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_deleteline, &ttyctx);
@@ -1492,8 +1571,9 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
return;
}
if (ny > s->rlower + 1 - s->cy)
ny = s->rlower + 1 - s->cy;
ry = s->rlower + 1 - s->cy;
if (ny > ry)
ny = ry;
if (ny == 0)
return;
@@ -1508,7 +1588,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = ny;
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, s->cy, ry))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_deleteline, &ttyctx);
@@ -1642,29 +1722,34 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
{
struct screen *s = ctx->s;
struct tty_ctx ttyctx;
u_int ry;
if (s->cy != s->rupper) {
if (s->cy > 0)
screen_write_set_cursor(ctx, -1, s->cy - 1);
return;
}
if (s->cy == s->rupper) {
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
screen_write_collect_flush(ctx, 0, __func__);
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
screen_write_collect_flush(ctx, 0, __func__);
screen_write_initctx(ctx, &ttyctx, 1, 1);
ttyctx.bg = bg;
screen_write_initctx(ctx, &ttyctx, 1, 1);
ttyctx.bg = bg;
if (s->mode & MODE_SYNC)
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_reverseindex, &ttyctx);
return;
}
ry = s->rlower + 1 - s->rupper;
if (!screen_write_should_draw_lines(ctx, s->rupper, ry))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_reverseindex, &ttyctx);
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
} else if (s->cy > 0)
screen_write_set_cursor(ctx, -1, s->cy - 1);
screen_write_redraw_pane(ctx, &ttyctx);
}
/* Set scroll region. */
@@ -1714,20 +1799,24 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
ctx->bg = bg;
}
if (s->cy == s->rlower) {
if (s->cy != s->rlower) {
if (s->cy < screen_size_y(s) - 1)
screen_write_set_cursor(ctx, -1, s->cy + 1);
return;
}
#ifdef ENABLE_SIXEL
if (rlower == screen_size_y(s) - 1)
redraw = image_scroll_up(s, 1);
else
redraw = image_check_line(s, rupper, rlower - rupper);
if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
if (rlower == screen_size_y(s) - 1)
redraw = image_scroll_up(s, 1);
else
redraw = image_check_line(s, rupper, rlower - rupper);
if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
screen_write_collect_scroll(ctx, bg);
ctx->scrolled++;
} else if (s->cy < screen_size_y(s) - 1)
screen_write_set_cursor(ctx, -1, s->cy + 1);
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
screen_write_collect_scroll(ctx, bg);
ctx->scrolled++;
}
/* Scroll up. */
@@ -1767,7 +1856,7 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct tty_ctx ttyctx;
u_int i;
u_int i, ry;
screen_write_initctx(ctx, &ttyctx, 1, 1);
ttyctx.bg = bg;
@@ -1788,7 +1877,8 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = lines;
if (s->mode & MODE_SYNC)
ry = s->rlower + 1 - s->rupper;
if (!screen_write_should_draw_lines(ctx, s->rupper, ry))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED || ctx->wp == NULL) {
tty_write(tty_cmd_scrolldown, &ttyctx);
@@ -1841,7 +1931,7 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
screen_write_collect_clear(ctx, s->cy + 1, sy - (s->cy + 1));
screen_write_collect_flush(ctx, 0, __func__);
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, s->cy, sy - s->cy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) {
tty_write(tty_cmd_clearendofscreen, &ttyctx);
@@ -1916,7 +2006,7 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
screen_write_collect_clear(ctx, 0, s->cy);
screen_write_collect_flush(ctx, 0, __func__);
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, 0, s->cy + 1))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) {
tty_write(tty_cmd_clearstartofscreen, &ttyctx);
@@ -1989,7 +2079,7 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
screen_write_collect_clear(ctx, 0, sy);
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_lines(ctx, 0, sy))
return;
if (~ttyctx.flags & TTY_CTX_PANE_OBSCURED) {
tty_write(tty_cmd_clearscreen, &ttyctx);
@@ -2303,12 +2393,21 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only,
const char *from)
{
struct screen *s = ctx->s;
struct window_pane *wp = ctx->wp;
u_int y, cx, cy, items = 0;
struct screen_write_citem *ci, *tmp;
struct screen_write_cline *cl;
if (s->mode & MODE_SYNC)
if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP)))
goto discard;
if (s->mode & MODE_SYNC) {
for (y = 0; y < screen_size_y(s); y++) {
cl = &s->write_list[y];
if (!TAILQ_EMPTY(&cl->items))
screen_write_should_draw_line(ctx, y);
}
goto discard;
}
if (ctx->scrolled != 0) {
if (!screen_write_collect_flush_scrolled(ctx))
@@ -2646,12 +2745,12 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
if (s->mode & MODE_INSERT) {
screen_write_collect_flush(ctx, 0, __func__);
ttyctx.n = width;
if (~s->mode & MODE_SYNC)
if (screen_write_should_draw_line(ctx, s->cy))
tty_write(tty_cmd_insertcharacter, &ttyctx);
}
/* If not writing, done now. */
if (skip || s->mode & MODE_SYNC)
if (skip || !screen_write_should_draw_line(ctx, s->cy))
return;
/* Do a full line redraw if needed. */
@@ -2671,7 +2770,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
for (i = 0, vis = 0; i < r->used; i++)
vis += r->ranges[i].nx;
if (vis >= width) {
if (~s->mode & MODE_SYNC)
if (screen_write_should_draw_line(ctx, s->cy))
tty_write(tty_cmd_cell, &ttyctx);
return;
}
@@ -2681,7 +2780,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
* spaces in the visible regions.
*/
utf8_set(&tmp_gc.data, ' ');
if (s->mode & MODE_SYNC)
if (!screen_write_should_draw_line(ctx, s->cy))
return;
for (i = 0; i < r->used; i++) {
ri = &r->ranges[i];
@@ -2824,7 +2923,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
ttyctx.cell = &last;
if (force_wide)
ttyctx.flags |= TTY_CTX_CELL_INVALIDATE;
if (~s->mode & MODE_SYNC)
if (screen_write_should_draw_line(ctx, cy))
tty_write(tty_cmd_cell, &ttyctx);
screen_write_set_cursor(ctx, cx, cy);

View File

@@ -99,12 +99,12 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
s->write_list = NULL;
s->hyperlinks = NULL;
screen_reinit(s);
screen_reinit(s, 1);
}
/* Reinitialise screen. */
void
screen_reinit(struct screen *s)
screen_reinit(struct screen *s, int check)
{
s->cx = 0;
s->cy = 0;
@@ -123,7 +123,8 @@ screen_reinit(struct screen *s)
s->saved_cy = UINT_MAX;
screen_reset_tabs(s);
if (check)
grid_check_is_clear(s->grid);
grid_clear_lines(s->grid, s->grid->hsize, s->grid->sy, 8);
screen_clear_selection(s);

View File

@@ -1193,6 +1193,8 @@ server_client_update_theme_colours(struct client *c)
format_defaults(ft, c, NULL, NULL, NULL);
theme = c->theme;
if (theme == THEME_UNKNOWN)
theme = colour_totheme(c->tty.bg);
if (option == 2)
theme = THEME_LIGHT;
else if (option == 3)
@@ -2048,19 +2050,18 @@ server_client_reset_state(struct client *c)
if (!window_position_is_visible(r, cx))
cursor = 0;
if (window_pane_scrollbar_overlay_visible(wp)) {
sb_w = wp->scrollbar_style.width;
if (sb_w > wp->sx)
sb_w = wp->sx;
if (sb_w != 0 &&
w->sb_pos ==
PANE_SCROLLBARS_LEFT) {
if (s->cx < sb_w)
if (window_pane_scrollbar_overlay_visible(wp)) {
sb_w = wp->scrollbar_style.width;
if (sb_w > wp->sx)
sb_w = wp->sx;
if (sb_w != 0 &&
w->sb_pos == PANE_SCROLLBARS_LEFT) {
if (s->cx < sb_w)
cursor = 0;
} else if (sb_w != 0 &&
s->cx >= wp->sx - sb_w)
cursor = 0;
} else if (sb_w != 0 &&
s->cx >= wp->sx - sb_w)
cursor = 0;
}
}
if (status_at_line(c) == 0)
cy += status_line_size(c);
@@ -2089,10 +2090,10 @@ server_client_reset_state(struct client *c)
mode |= MODE_MOUSE_ALL;
}
}
if (options_get_number(oo, "focus-follows-mouse") ||
w->sb == PANE_SCROLLBARS_MODAL ||
w->sb == PANE_SCROLLBARS_AUTOHIDE)
mode |= MODE_MOUSE_ALL;
if (options_get_number(oo, "focus-follows-mouse") ||
w->sb == PANE_SCROLLBARS_MODAL ||
w->sb == PANE_SCROLLBARS_AUTOHIDE)
mode |= MODE_MOUSE_ALL;
else if (~mode & MODE_MOUSE_ALL)
mode |= MODE_MOUSE_BUTTON;
}

17
spawn.c
View File

@@ -104,7 +104,7 @@ spawn_window(struct spawn_context *sc, char **cause)
sc->wp0 = TAILQ_FIRST(&w->panes);
TAILQ_REMOVE(&w->panes, sc->wp0, entry);
layout_free(w);
layout_free(w, 0);
window_destroy_panes(w);
TAILQ_INSERT_HEAD(&w->panes, sc->wp0, entry);
@@ -264,14 +264,20 @@ spawn_pane(struct spawn_context *sc, char **cause)
free(cwd);
return (NULL);
}
if (sc->wp0->fd != -1) {
if (sc->wp0->event != NULL) {
bufferevent_free(sc->wp0->event);
sc->wp0->event = NULL;
}
if (sc->wp0->fd != -1) {
close(sc->wp0->fd);
sc->wp0->fd = -1;
}
window_pane_reset_mode_all(sc->wp0);
screen_reinit(&sc->wp0->base);
input_free(sc->wp0->ictx);
sc->wp0->ictx = NULL;
screen_reinit(&sc->wp0->base, 0);
if (sc->wp0->ictx != NULL) {
input_free(sc->wp0->ictx);
sc->wp0->ictx = NULL;
}
new_wp = sc->wp0;
new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN);
} else {
@@ -387,6 +393,7 @@ spawn_pane(struct spawn_context *sc, char **cause)
new_wp->base.mode |= MODE_CRLF;
goto complete;
}
new_wp->flags &= ~PANE_EMPTY;
/* Store current working directory and change to new one. */
if (getcwd(path, sizeof path) != NULL) {

View File

@@ -488,7 +488,7 @@ status_message_redraw(struct client *c)
status_message_area(c, &ax, &aw);
ft = format_create_defaults(NULL, c, NULL, NULL, NULL);
memcpy(&gc, &grid_default_cell, sizeof gc);
style_apply(&gc, s->options, "message-style", ft);
/*
* Set #{message} in the format tree. If styles should be ignored in

119
tmux.1
View File

@@ -2773,7 +2773,7 @@ The pane must not already be floating or hidden, and the window must not
be zoomed.
.Tg capturep
.It Xo Ic capture\-pane
.Op Fl aeFHLpPqCJMN
.Op Fl aeFHLpPRqCJMN
.Op Fl b Ar buffer\-name
.Op Fl E Ar end\-line
.Op Fl S Ar start\-line
@@ -2833,6 +2833,8 @@ With
.Fl H ,
only hyperlinks in the specified lines are captured.
Multiple hyperlinks on the same line are separated by spaces.
.Fl R
dumps the internal grid data for diagnostics.
.Pp
.Fl S
and
@@ -3250,6 +3252,15 @@ is omitted and a marked pane is present (see
.Ic select\-pane
.Fl m ) ,
the marked pane is used rather than the current pane.
.Pp
If
.Ar src\-pane
is floating and
.Ar dst\-pane
is either unspecified or equal to
.Ar src\-pane ,
.Ar src\-pane
is returned to its previous position in the layout.
.Tg killp
.It Xo Ic kill\-pane
.Op Fl a
@@ -3546,7 +3557,7 @@ the
option.
.Tg neww
.It Xo Ic new\-window
.Op Fl abdkPS
.Op Fl abdEkPS
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
.Op Fl F Ar format
@@ -3570,6 +3581,9 @@ is the new window location.
If
.Fl d
is given, the session does not make the new window the current window.
If
.Fl E
is given, the initial pane is created without a running command.
.Ar target\-window
represents the window to be created; if the target already exists an error is
shown, unless the
@@ -3869,7 +3883,7 @@ This command will automatically set
to manual in the window options.
.Tg respawnp
.It Xo Ic respawn\-pane
.Op Fl k
.Op Fl \&Ek
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
.Op Fl t Ar target\-pane
@@ -3886,6 +3900,9 @@ executed.
The pane must be already inactive, unless
.Fl k
is given, in which case any existing command is killed.
If
.Fl E
is given, the pane is left without a running command.
.Fl c
specifies a new working directory for the pane.
The
@@ -3895,7 +3912,7 @@ option has the same meaning as for the
command.
.Tg respawnw
.It Xo Ic respawn\-window
.Op Fl k
.Op Fl \&Ek
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
.Op Fl t Ar target\-window
@@ -3912,6 +3929,9 @@ executed.
The window must be already inactive, unless
.Fl k
is given, in which case any existing command is killed.
If
.Fl E
is given, the window is left with one pane and without a running command.
.Fl c
specifies a new working directory for the window.
The
@@ -6558,6 +6578,7 @@ Hooks are managed with these commands:
.Bl -tag -width Ds
.It Xo Ic set\-hook
.Op Fl agpRuw
.Op Fl B Ar name:what:format
.Op Fl t Ar target\-pane
.Ar hook\-name
.Op Ar command
@@ -6574,18 +6595,53 @@ The flags are the same as for
.Ic set\-option .
.Pp
With
.Fl B ,
.Ar name:what:format
uses the same subscription syntax as
.Ic refresh\-client
.Fl B :
.Ar name
is the hook to run,
.Ar what
selects the session, pane, all panes, window, or all windows, and
.Ar format
is expanded once a second.
For monitor hooks,
.Ar name
must begin with
.Ql @ .
If
.Ar command
is given, it is stored as the
.Ql @
hook command; otherwise only the monitor is created or replaced.
Note that monitor hooks are not inherited, the hook is only run from
the scope where it is created.
With
.Fl u ,
the subscription named by
.Fl B
is removed.
.Pp
With
.Fl R ,
run
.Ar hook\-name
immediately.
.It Xo Ic show\-hooks
.Op Fl gpw
.Op Fl Bgpw
.Op Fl t Ar target\-pane
.Op Ar hook
.Xc
Shows hooks.
The flags are the same as for
.Ic show\-options .
.Pp
With
.Fl B ,
shows the subscriptions installed with
.Em set\-hook
.Fl B .
.El
.Sh MOUSE SUPPORT
If the
@@ -6877,6 +6933,12 @@ results in
replaces a
.Nm
colour by its six-digit hexadecimal RGB value.
If an argument of
.Ql f
or
.Ql b
is given, it will instead produce the SGR escape sequence to set the foreground
or background colour respectively.
.Pp
A limit may be placed on the length of the resultant string by prefixing it
by an
@@ -6928,7 +6990,7 @@ will use shorter but less accurate time format for times in the past.
.Ql r
.Pq Ql t/r
will show the time relative to the current time, for example
.Ql \1m
.Ql \&1m
or
.Ql 2m23s .
A custom format may be given using an
@@ -6986,7 +7048,9 @@ or with
.Ql a
escape
.Nm
command arguments.
command arguments; with
.Ql s
use single quotes.
.Ql E:\&
will expand the format twice, for example
.Ql #{E:status\-left}
@@ -7020,7 +7084,13 @@ to sort in reverse order.
.Ql /r\&
can also be used with
.Ql P:\&
to reverse the sort order by pane index.
to reverse the sort order; by default panes are sorted by creation order.
.Ql P:\&
can also take
.Ql /i\&
to sort by pane index or
.Ql /z\&
to sort by z-index.
For example,
.Ql S/nr:\&
to sort sessions by name in reverse order.
@@ -7046,6 +7116,33 @@ prefix, for example a user option
on the next window is available as
.Ql next_@color .
.Pp
.Ql O:\&
will loop over each option;
array options are looped once for each array item.
.Ql O:\&
may be given a flag to choose the options table:
.Bl -column "Flag" "Table" -offset indent
.It Sy "Flag" Ta Sy "Table"
.It Li "s" Ta "session"
.It Li "w" Ta "window"
.It Li "p" Ta "pane"
.It Li "v" Ta "server"
.El
.Pp
.Ql g
chooses global options.
The default is
.Ql s .
.Ql V:\&
will loop over each environment variable.
Its flags are:
.Bl -column "Flag" "Environment" -offset indent
.It Sy "Flag" Ta Sy "Environment"
.It Li "s" Ta "session environment"
.It Li "g" Ta "global environment"
.It Li "c" Ta "client environment"
.El
.Pp
.Ql N:\&
checks if a window (without any suffix or with the
.Ql w
@@ -7182,10 +7279,13 @@ The following variables are available, where appropriate:
.It Li "history_size" Ta "" Ta "Size of history in lines"
.It Li "hook" Ta "" Ta "Name of running hook, if any"
.It Li "hook_client" Ta "" Ta "Name of client where hook was run, if any"
.It Li "hook_last" Ta "" Ta "Previous value for a monitor hook"
.It Li "hook_pane" Ta "" Ta "ID of pane where hook was run, if any"
.It Li "hook_session" Ta "" Ta "ID of session where hook was run, if any"
.It Li "hook_session_name" Ta "" Ta "Name of session where hook was run, if any"
.It Li "hook_value" Ta "" Ta "New value for a monitor hook"
.It Li "hook_window" Ta "" Ta "ID of window where hook was run, if any"
.It Li "hook_window_index" Ta "" Ta "Index of window where hook was run, if any"
.It Li "hook_window_name" Ta "" Ta "Name of window where hook was run, if any"
.It Li "host" Ta "#H" Ta "Hostname of local host"
.It Li "host_short" Ta "#h" Ta "Hostname of local host (no domain name)"
@@ -7259,6 +7359,7 @@ The following variables are available, where appropriate:
.It Li "pane_right" Ta "" Ta "Right of pane"
.It Li "pane_search_string" Ta "" Ta "Last search string in copy mode"
.It Li "pane_start_command" Ta "" Ta "Command pane started with"
.It Li "pane_start_command_list" Ta "" Ta "Command pane started with, quoted"
.It Li "pane_start_path" Ta "" Ta "Path pane started with"
.It Li "pane_synchronized" Ta "" Ta "1 if pane is synchronized"
.It Li "pane_tabs" Ta "" Ta "Pane tab positions"
@@ -7344,6 +7445,8 @@ The following variables are available, where appropriate:
.It Li "window_linked" Ta "" Ta "1 if window is linked across sessions"
.It Li "window_linked_sessions" Ta "" Ta "Number of sessions this window is linked to"
.It Li "window_linked_sessions_list" Ta "" Ta "List of sessions this window is linked to"
.It Li "window_manual_height" Ta "" Ta "Manual height of window, if set"
.It Li "window_manual_width" Ta "" Ta "Manual width of window, if set"
.It Li "window_marked_flag" Ta "" Ta "1 if window contains the marked pane"
.It Li "window_name" Ta "#W" Ta "Name of window"
.It Li "window_offset_x" Ta "" Ta "X offset into window if larger than client"

67
tmux.h
View File

@@ -99,6 +99,9 @@ struct winlink;
#ifndef TMUX_LOCK_CMD
#define TMUX_LOCK_CMD "lock -np"
#endif
#ifndef TMUX_MOUSE
#define TMUX_MOUSE 0
#endif
/* Minimum and maximum layout cell size, NOT including border lines. */
#define PANE_MINIMUM 1
@@ -1294,6 +1297,9 @@ struct window_pane {
#define PANE_UNSEENCHANGES 0x4000
#define PANE_REDRAWSCROLLBAR 0x8000
bitstr_t *sync_dirty;
u_int sync_dirty_size;
u_int sb_slider_y;
u_int sb_slider_h;
int sb_auto_visible;
@@ -2318,14 +2324,26 @@ struct client {
};
TAILQ_HEAD(clients, client);
/* Control mode subscription type. */
enum control_sub_type {
CONTROL_SUB_SESSION,
CONTROL_SUB_PANE,
CONTROL_SUB_ALL_PANES,
CONTROL_SUB_WINDOW,
CONTROL_SUB_ALL_WINDOWS
/* Monitor. */
enum monitor_type {
MONITOR_SESSION,
MONITOR_PANE,
MONITOR_ALL_PANES,
MONITOR_WINDOW,
MONITOR_ALL_WINDOWS
};
#define MONITOR_NOTIFY_INITIAL 0x1
struct monitor_change {
const char *name;
const char *value;
const char *last;
struct client *c;
struct session *s;
struct winlink *wl;
struct window_pane *wp;
};
typedef void (*monitor_cb)(struct monitor_change *, void *);
/* Key binding and key table. */
struct key_binding {
@@ -2646,6 +2664,12 @@ char *format_trim_right(const char *, u_int);
/* notify.c */
void notify_hook(struct cmdq_item *, const char *);
void notify_monitor_add(struct cmdq_item *, struct options *,
const char *, enum monitor_type, int, const char *,
struct cmd_find_state *, struct session *);
void notify_monitor_remove(struct options *, const char *);
void notify_monitor_free(void *);
char *notify_monitor_to_string(struct options_entry *);
void notify_client(const char *, struct client *);
void notify_session(const char *, struct session *);
void notify_winlink(const char *, struct winlink *);
@@ -2668,6 +2692,8 @@ struct options_entry *options_default(struct options *,
char *options_default_to_string(const struct options_table_entry *);
const char *options_name(struct options_entry *);
struct options *options_owner(struct options_entry *);
void *options_get_monitor_data(struct options_entry *);
void options_set_monitor_data(struct options_entry *, void *);
const struct options_table_entry *options_table_entry(struct options_entry *);
struct options_entry *options_get_only(struct options *, const char *);
struct options_entry *options_get(struct options *, const char *);
@@ -3305,6 +3331,7 @@ void colour_split_rgb(int, u_char *, u_char *, u_char *);
int colour_force_rgb(int);
int colour_dim(int, u_int);
const char *colour_tostring(int);
const char *colour_toescape(struct client *, int, int);
enum client_theme colour_totheme(int);
int colour_fromstring(const char *);
const char *colour_theme_option(u_int, enum client_theme);
@@ -3329,6 +3356,7 @@ bitstr_t *fuzzy_match(const char *, const char *, u_int, u_int *);
/* grid.c */
extern const struct grid_cell grid_default_cell;
void grid_check_is_clear(struct grid *);
void grid_empty_line(struct grid *, u_int, u_int);
void grid_set_tab(struct grid_cell *, u_int);
int grid_cells_equal(const struct grid_cell *, const struct grid_cell *);
@@ -3338,6 +3366,9 @@ struct grid *grid_create(u_int, u_int, u_int);
void grid_destroy(struct grid *);
void grid_free_lines(struct grid *, u_int, u_int);
int grid_compare(struct grid *, struct grid *);
const char *grid_line_flags_string(int);
const char *grid_cell_flags_string(int);
const char *grid_cell_attr_string(int);
void grid_collect_history(struct grid *, int);
void grid_remove_history(struct grid *, u_int );
void grid_scroll_history(struct grid *, u_int);
@@ -3446,6 +3477,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int);
void screen_write_mode_clear(struct screen_write_ctx *, int);
void screen_write_start_sync(struct window_pane *);
void screen_write_stop_sync(struct window_pane *);
void screen_write_clear_dirty(struct window_pane *);
void screen_write_cursorup(struct screen_write_ctx *, u_int);
void screen_write_cursordown(struct screen_write_ctx *, u_int);
void screen_write_cursorright(struct screen_write_ctx *, u_int);
@@ -3499,7 +3531,7 @@ int redraw_get_status_border_cell_type(struct redraw_span **, u_int);
/* screen.c */
void screen_init(struct screen *, u_int, u_int, u_int);
void screen_reinit(struct screen *);
void screen_reinit(struct screen *, int);
void screen_free(struct screen *);
void screen_reset_tabs(struct screen *);
void screen_reset_hyperlinks(struct screen *);
@@ -3681,7 +3713,7 @@ struct visible_ranges *window_visible_ranges(struct window_pane *, int, int,
u_int layout_count_cells(struct layout_cell *);
int layout_has_tiled(struct layout_cell *);
struct layout_cell *layout_create_cell(struct layout_cell *);
void layout_free_cell(struct layout_cell *);
void layout_free_cell(struct layout_cell *, int);
void layout_print_cell(struct layout_cell *, const char *, u_int);
void layout_destroy_cell(struct window *, struct layout_cell *,
struct layout_cell **);
@@ -3692,6 +3724,7 @@ void layout_set_size(struct layout_cell *, u_int, u_int, int, int);
void layout_make_leaf(struct layout_cell *, struct window_pane *);
void layout_make_node(struct layout_cell *, enum layout_type);
void layout_fix_zindexes(struct window *, struct layout_cell *);
int layout_cell_is_tiled(struct layout_cell *);
void layout_fix_offsets(struct window *);
void layout_fix_panes(struct window *, struct window_pane *);
void layout_resize_adjust(struct window *, struct layout_cell *,
@@ -3700,7 +3733,7 @@ void layout_resize_set_size(struct window *, struct layout_cell *,
enum layout_type, u_int);
struct layout_cell *layout_cell_get_neighbour(struct layout_cell *);
void layout_init(struct window *, struct window_pane *);
void layout_free(struct window *);
void layout_free(struct window *, int);
void layout_resize(struct window *, u_int, u_int);
void layout_resize_pane(struct window_pane *, enum layout_type,
int, int);
@@ -3845,6 +3878,16 @@ void check_window_name(struct window *);
char *default_window_name(struct window *);
char *parse_window_name(const char *);
/* monitor.c */
struct monitor_set *monitor_create_client(struct client *, monitor_cb, void *);
struct monitor_set *monitor_create_session(struct session *, monitor_cb, void *);
void monitor_destroy(struct monitor_set *);
int monitor_parse(const char *, char **, enum monitor_type *, int *,
char **);
void monitor_add(struct monitor_set *, const char *, enum monitor_type, int,
const char *, u_int);
void monitor_remove(struct monitor_set *, const char *);
/* control.c */
void control_discard(struct client *);
void control_start(struct client *);
@@ -3860,8 +3903,8 @@ void control_reset_offsets(struct client *);
void printflike(2, 3) control_write(struct client *, const char *, ...);
void control_write_output(struct client *, struct window_pane *);
int control_all_done(struct client *);
void control_add_sub(struct client *, const char *, enum control_sub_type,
int, const char *);
void control_add_sub(struct client *, const char *, enum monitor_type, int,
const char *);
void control_remove_sub(struct client *, const char *);
/* control-notify.c */

View File

@@ -589,6 +589,7 @@ tty_default_features(int *feat, const char *name, u_int version)
"cstyle,"
"extkeys,"
"focus,"
"overline,"
"hyperlinks,"
"osc7,"
"sync,"

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