Merge branch 'master' into command_parser

This commit is contained in:
Nicholas Marriott
2026-07-29 10:38:22 +01:00
50 changed files with 1506 additions and 222 deletions

View File

@@ -61,6 +61,7 @@ jobs:
automake \
bison \
libevent \
jemalloc \
make \
ncurses \
utf8proc \

11
CHANGES
View File

@@ -1,5 +1,8 @@
CHANGES FROM 3.7b TO 3.8
* Build with jemalloc on macOS to avoid what appears to be a bug in the system
calloc(3) (issue 5385).
* Many improvements to floating panes:
- new-pane and split-window can now set the title with -T and border lines
@@ -87,6 +90,14 @@ CHANGES FROM 3.7b TO 3.8
while scrolling or while hovered and disappears after pane-scrollbars-timeout
(Michael Grant).
* Extend the fill-character option so both inside and outside the window can be
changed.
* Change set-option and set-hooks to use formats and add a -F flag to each.
* Add a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk).
* Change command templates so %% is escaped for single quotes while %1 remains
unquoted, preventing single quotes in session or target names from
terminating quoted command sections (reported by Aliz Hammond).

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-break-pane.c,v 1.74 2026/07/15 13:02:33 nicm Exp $ */
/* $OpenBSD: cmd-break-pane.c,v 1.75 2026/07/23 09:38:27 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -172,6 +172,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
w->name = clean_name(name, 0);
options_set_number(w->options, "automatic-rename", 0);
}
window_set_fill_cells(w);
layout_init(w, wp);
wp->flags |= PANE_CHANGED;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-pipe-pane.c,v 1.63 2026/04/28 08:47:55 nicm Exp $ */
/* $OpenBSD: cmd-pipe-pane.c,v 1.64 2026/07/27 14:25:46 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -194,9 +194,13 @@ static void
cmd_pipe_pane_read_callback(__unused struct bufferevent *bufev, void *data)
{
struct window_pane *wp = data;
struct evbuffer *evb = wp->pipe_event->input;
struct evbuffer *evb;
size_t available;
if (wp->pipe_event == NULL)
return;
evb = wp->pipe_event->input;
available = EVBUFFER_LENGTH(evb);
log_debug("%%%u pipe read %zu", wp->id, available);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-show-options.c,v 1.73 2026/07/10 13:38:45 nicm Exp $ */
/* $OpenBSD: cmd-show-options.c,v 1.76 2026/07/27 19:15:58 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -27,14 +27,23 @@
* Show options.
*/
#define SHOW_OPTIONS_TEMPLATE \
"#{?option_value_only," \
"#{option_value}," \
"#{option_name}#{?option_has_array_key," \
"[#{option_array_key}],}" \
"#{?option_is_parent,*,}" \
"#{?option_has_value, " \
"#{?option_is_string,#{q/a:option_value},#{option_value}},}}"
#define SHOW_HOOKS_MONITOR_TEMPLATE \
"#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}"
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 *, const char *, int);
static void cmd_show_hooks_print_monitor(struct cmdq_item *,
struct options_entry *);
static enum cmd_retval cmd_show_hooks_monitor(struct cmd *, struct cmdq_item *,
int, struct options *);
static void cmd_show_hooks_print_monitor(struct cmd *,
struct cmdq_item *, struct options_entry *);
static enum cmd_retval cmd_show_options_all(struct cmd *, struct cmdq_item *,
int, struct options *);
@@ -42,8 +51,8 @@ const struct cmd_entry cmd_show_options_entry = {
.name = "show-options",
.alias = "show",
.args = { "AgHpqst:vw", 0, 1, NULL },
.usage = "[-AgHpqsvw] " CMD_TARGET_PANE_USAGE " [option]",
.args = { "AgF:Hpqst:vw", 0, 1, NULL },
.usage = "[-AgHpqsvw] [-F format] " CMD_TARGET_PANE_USAGE " [option]",
.target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL },
@@ -55,8 +64,8 @@ const struct cmd_entry cmd_show_window_options_entry = {
.name = "show-window-options",
.alias = "showw",
.args = { "gvt:", 0, 1, NULL },
.usage = "[-gv] " CMD_TARGET_WINDOW_USAGE " [option]",
.args = { "F:gvt:", 0, 1, NULL },
.usage = "[-gv] [-F format] " CMD_TARGET_WINDOW_USAGE " [option]",
.target = { 't', CMD_FIND_WINDOW, CMD_FIND_CANFAIL },
@@ -68,8 +77,8 @@ const struct cmd_entry cmd_show_hooks_entry = {
.name = "show-hooks",
.alias = NULL,
.args = { "Bgpt:w", 0, 1, NULL },
.usage = "[-Bgpw] " CMD_TARGET_PANE_USAGE " [hook]",
.args = { "BF:gpt:w", 0, 1, NULL },
.usage = "[-Bgpw] [-F format] " CMD_TARGET_PANE_USAGE " [hook]",
.target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL },
@@ -85,7 +94,7 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
struct options *oo;
char *argument, *name = NULL, *cause;
char *array_key = NULL;
int window, ambiguous, parent, scope;
int window, ambiguous, parent, print_parent, scope;
struct options_entry *o;
window = (cmd_get_entry(self) == &cmd_show_window_options_entry);
@@ -101,8 +110,14 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
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));
args_has(args, 'B')) {
o = options_first(oo);
while (o != NULL) {
cmd_show_hooks_print_monitor(self, item, o);
o = options_next(o);
}
return (CMD_RETURN_NORMAL);
}
return (cmd_show_options_all(self, item, scope, oo));
}
argument = format_single_from_target(item, args_string(args, 0));
@@ -135,11 +150,17 @@ cmd_show_options_exec(struct cmd *self, struct cmdq_item *item)
if (o != NULL) {
if (cmd_get_entry(self) == &cmd_show_hooks_entry &&
args_has(args, 'B'))
cmd_show_hooks_print_monitor(item, o);
else
cmd_show_options_print(self, item, o, array_key, parent);
}
else if (*name == '@') {
cmd_show_hooks_print_monitor(self, item, o);
else {
print_parent = parent;
if (array_key == NULL && options_is_array(o) &&
options_array_first(o) == NULL) {
print_parent = 0;
}
cmd_show_options_print(self, item, o, array_key,
print_parent);
}
} else if (*name == '@') {
if (args_has(args, 'q'))
goto out;
cmdq_error(item, "invalid option: %s", argument);
@@ -163,22 +184,24 @@ static void
cmd_show_options_print(struct cmd *self, struct cmdq_item *item,
struct options_entry *o, const char *array_key, int parent)
{
struct args *args = cmd_get_args(self);
struct options_array_item *a;
const char *name = options_name(o);
char *value, *tmp = NULL, *escaped;
struct args *args = cmd_get_args(self);
struct options_array_item *a;
struct format_tree *ft;
const char *name = options_name(o);
const char *template = args_get(args, 'F');
char *value, *line;
struct timeval tv = { 0 };
u_int fire_count;
time_t fire_time;
int is_hook = 0, is_user = 0;
int has_value = 1;
const struct options_table_entry *oe = options_table_entry(o);
if (array_key != NULL) {
xasprintf(&tmp, "%s[%s]", name, array_key);
name = tmp;
} else {
if (options_is_array(o)) {
a = options_array_first(o);
if (a == NULL) {
if (!args_has(args, 'v'))
cmdq_print(item, "%s", name);
return;
}
if (array_key != NULL)
value = options_to_string(o, array_key, 0);
else if (options_is_array(o)) {
a = options_array_first(o);
if (a != NULL) {
while (a != NULL) {
array_key = options_array_item_key(a);
cmd_show_options_print(self, item, o, array_key,
@@ -187,54 +210,130 @@ cmd_show_options_print(struct cmd *self, struct cmdq_item *item,
}
return;
}
}
if (template == NULL && args_has(args, 'v'))
return;
value = xstrdup("");
has_value = 0;
} else
value = options_to_string(o, NULL, 0);
value = options_to_string(o, array_key, 0);
if (args_has(args, 'v'))
cmdq_print(item, "%s", value);
else if (options_is_string(o)) {
escaped = args_escape(value);
if (parent)
cmdq_print(item, "%s* %s", name, escaped);
else
cmdq_print(item, "%s %s", name, escaped);
free(escaped);
if (template == NULL)
template = SHOW_OPTIONS_TEMPLATE;
if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK))
is_hook = 1;
else if (oe == NULL)
is_user = 1;
ft = format_create_from_target(item);
format_add(ft, "option_name", "%s", name);
format_add(ft, "option_value", "%s", value);
format_add(ft, "option_value_only", "%d", args_has(args, 'v'));
format_add(ft, "option_is_parent", "%d", parent);
format_add(ft, "option_is_array", "%d", options_is_array(o));
format_add(ft, "option_is_string", "%d", options_is_string(o));
format_add(ft, "option_is_hook", "%d", is_hook);
format_add(ft, "option_is_user", "%d", is_user);
format_add(ft, "option_has_value", "%d", has_value);
if (cmd_get_entry(self) == &cmd_show_hooks_entry) {
fire_count = options_get_fire_count(o);
format_add(ft, "hook_fire_count", "%u", fire_count);
fire_time = options_get_fire_time(o);
if (fire_time != 0) {
tv.tv_sec = fire_time;
format_add_tv(ft, "hook_fire_time", &tv);
}
}
if (array_key != NULL) {
format_add(ft, "option_array_key", "%s", array_key);
format_add(ft, "option_has_array_key", "1");
} else {
if (parent)
cmdq_print(item, "%s* %s", name, value);
else
cmdq_print(item, "%s %s", name, value);
format_add(ft, "option_array_key", "%s", "");
format_add(ft, "option_has_array_key", "0");
}
free(value);
line = format_expand(ft, template);
format_free(ft);
free(tmp);
cmdq_print(item, "%s", line);
free(line);
free(value);
}
static void
cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o)
cmd_show_hooks_print_monitor(struct cmd *self, struct cmdq_item *item,
struct options_entry *o)
{
char *value;
struct args *args = cmd_get_args(self);
struct format_tree *ft;
enum monitor_type type;
const char *template = args_get(args, 'F'), *format;
char *value, *target, *line;
struct timeval tv = { 0 };
u_int fire_count;
time_t fire_time;
int id;
value = hooks_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);
if (!hooks_monitor_get(o, &type, &id, &format)) {
free(value);
return;
}
return (CMD_RETURN_NORMAL);
if (template == NULL)
template = SHOW_HOOKS_MONITOR_TEMPLATE;
switch (type) {
case MONITOR_SESSION:
target = xstrdup("");
break;
case MONITOR_PANE:
xasprintf(&target, "%%%d", id);
break;
case MONITOR_ALL_PANES:
target = xstrdup("%*");
break;
case MONITOR_WINDOW:
xasprintf(&target, "@%d", id);
break;
case MONITOR_ALL_WINDOWS:
target = xstrdup("@*");
break;
}
ft = format_create_from_target(item);
format_add(ft, "option_name", "%s", options_name(o));
format_add(ft, "option_value", "%s", value);
format_add(ft, "option_value_only", "%d", 0);
format_add(ft, "option_is_parent", "%d", 0);
format_add(ft, "option_is_array", "%d", 0);
format_add(ft, "option_is_string", "%d", 1);
format_add(ft, "option_is_hook", "%d", 1);
format_add(ft, "option_is_user", "%d", 1);
format_add(ft, "option_has_value", "%d", 1);
format_add(ft, "option_array_key", "%s", "");
format_add(ft, "option_has_array_key", "0");
format_add(ft, "hook_monitor_target", "%s", target);
format_add(ft, "hook_monitor_format", "%s", format);
fire_count = hooks_monitor_get_fire_count(o);
format_add(ft, "hook_fire_count", "%u", fire_count);
fire_time = hooks_monitor_get_fire_time(o);
if (fire_time != 0) {
tv.tv_sec = fire_time;
format_add_tv(ft, "hook_fire_time", &tv);
}
line = format_expand(ft, template);
format_free(ft);
cmdq_print(item, "%s", line);
free(line);
free(target);
free(value);
}
static enum cmd_retval
@@ -244,17 +343,29 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope,
struct args *args = cmd_get_args(self);
const struct options_table_entry *oe;
struct options_entry *o;
struct options_array_item *a;
const char *name, *array_key;
int parent;
const char *name;
int parent, is_user_hook;
if (cmd_get_entry(self) != &cmd_show_hooks_entry) {
o = options_first(oo);
while (o != NULL) {
if (options_table_entry(o) == NULL)
o = options_first(oo);
while (o != NULL) {
if (options_table_entry(o) == NULL) {
name = options_name(o);
is_user_hook = 0;
if (*name == '@') {
if (hooks_is_event(name) ||
options_get_monitor_data(o) != NULL) {
is_user_hook = 1;
}
}
if (cmd_get_entry(self) != &cmd_show_hooks_entry) {
if (!is_user_hook || args_has(args, 'H')) {
cmd_show_options_print(self, item, o,
NULL, 0);
}
} else if (is_user_hook)
cmd_show_options_print(self, item, o, NULL, 0);
o = options_next(o);
}
o = options_next(o);
}
for (oe = options_table; oe->name != NULL; oe++) {
if (~oe->scope & scope)
@@ -278,24 +389,7 @@ cmd_show_options_all(struct cmd *self, struct cmdq_item *item, int scope,
} else
parent = 0;
if (!options_is_array(o))
cmd_show_options_print(self, item, o, NULL, parent);
else if ((a = options_array_first(o)) == NULL) {
if (!args_has(args, 'v')) {
name = options_name(o);
if (parent)
cmdq_print(item, "%s*", name);
else
cmdq_print(item, "%s", name);
}
} else {
while (a != NULL) {
array_key = options_array_item_key(a);
cmd_show_options_print(self, item, o, array_key,
parent);
a = options_array_next(a);
}
}
cmd_show_options_print(self, item, o, NULL, parent);
}
return (CMD_RETURN_NORMAL);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.145 2026/07/15 13:02:33 nicm Exp $ */
/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane",
.alias = "newp",
.args = { "bB:c:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bdefhIklMOPvWZ] [-B border-lines] "
.args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bCdefhIklMOPvWZ] [-B border-lines] "
"[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] "
@@ -201,6 +201,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
*/
goto fail;
}
if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK;
style = args_get(args, 's');
if (style != NULL) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-swap-window.c,v 1.29 2025/10/30 13:52:08 nicm Exp $ */
/* $OpenBSD: cmd-swap-window.c,v 1.30 2026/07/27 08:03:01 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -38,7 +38,7 @@ const struct cmd_entry cmd_swap_window_entry = {
.source = { 's', CMD_FIND_WINDOW, CMD_FIND_DEFAULT_MARKED },
.target = { 't', CMD_FIND_WINDOW, 0 },
.flags = 0,
.flags = CMD_AFTERHOOK,
.exec = cmd_swap_window_exec
};

View File

@@ -76,9 +76,7 @@ AM_CONDITIONAL(IS_OPTIMIZED, test "x$enable_optimizations" = xyes)
# Is this --enable-asan?
AC_ARG_ENABLE(
asan,
AS_HELP_STRING(--enable-asan, enable ASAN build flags),
,
[case "x$host_os" in *darwin*) enable_asan=yes;; esac]
AS_HELP_STRING(--enable-asan, enable ASAN build flags)
)
AM_CONDITIONAL(IS_ASAN, test "x$enable_asan" = xyes)
@@ -240,6 +238,7 @@ AC_LIBOBJ(getopt_long)
# Look for libevent. Try libevent_core or libevent with pkg-config first then
# look for the library.
libevent_version=off
PKG_CHECK_MODULES(
LIBEVENT_CORE,
[libevent_core >= 2],
@@ -248,6 +247,8 @@ PKG_CHECK_MODULES(
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBEVENT_CORE_LIBS $LIBS"
found_libevent=yes
libevent_version=`$PKG_CONFIG --modversion libevent_core 2>/dev/null`
test "x$libevent_version" = x && libevent_version=on
],
found_libevent=no
)
@@ -260,6 +261,8 @@ if test x$found_libevent = xno; then
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBEVENT_LIBS $LIBS"
found_libevent=yes
libevent_version=`$PKG_CONFIG --modversion libevent 2>/dev/null`
test "x$libevent_version" = x && libevent_version=on
],
found_libevent=no
)
@@ -268,7 +271,8 @@ if test x$found_libevent = xno; then
AC_SEARCH_LIBS(
event_init,
[event_core event event-1.4],
found_libevent=yes,
[found_libevent=yes
libevent_version=on],
found_libevent=no
)
fi
@@ -295,6 +299,7 @@ fi
# Look for ncurses or curses. Try pkg-config first then directly for the
# library.
ncurses_version=off
PKG_CHECK_MODULES(
LIBTINFOW,
tinfow,
@@ -303,6 +308,8 @@ PKG_CHECK_MODULES(
CPPFLAGS="$LIBTINFOW_CFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBTINFOW_LIBS $LIBS"
found_ncurses=yes
ncurses_version=`$PKG_CONFIG --modversion tinfow 2>/dev/null`
test "x$ncurses_version" = x && ncurses_version=on
],
found_ncurses=no
)
@@ -315,6 +322,8 @@ if test "x$found_ncurses" = xno; then
CPPFLAGS="$LIBTINFO_CFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBTINFO_LIBS $LIBS"
found_ncurses=yes
ncurses_version=`$PKG_CONFIG --modversion tinfo 2>/dev/null`
test "x$ncurses_version" = x && ncurses_version=on
],
found_ncurses=no
)
@@ -328,6 +337,8 @@ if test "x$found_ncurses" = xno; then
CPPFLAGS="$LIBNCURSESW_CFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBNCURSESW_LIBS $LIBS"
found_ncurses=yes
ncurses_version=`$PKG_CONFIG --modversion ncursesw 2>/dev/null`
test "x$ncurses_version" = x && ncurses_version=on
],
found_ncurses=no
)
@@ -341,6 +352,8 @@ if test "x$found_ncurses" = xno; then
CPPFLAGS="$LIBNCURSES_CFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBNCURSES_LIBS $LIBS"
found_ncurses=yes
ncurses_version=`$PKG_CONFIG --modversion ncurses 2>/dev/null`
test "x$ncurses_version" = x && ncurses_version=on
],
found_ncurses=no
)
@@ -349,7 +362,8 @@ if test "x$found_ncurses" = xno; then
AC_SEARCH_LIBS(
setupterm,
[tinfow tinfo terminfo ncursesw ncurses],
found_ncurses=yes,
[found_ncurses=yes
ncurses_version=on],
found_ncurses=no
)
if test "x$found_ncurses" = xyes; then
@@ -379,6 +393,7 @@ else
LIBS="$LIBS -lcurses"
CPPFLAGS="$CPPFLAGS -DHAVE_CURSES_H"
AC_DEFINE(HAVE_CURSES_H)
ncurses_version=on
else
AC_MSG_ERROR("curses not found")
fi
@@ -389,6 +404,7 @@ AC_CHECK_FUNCS([ \
])
# Look for utempter.
utempter_version=off
AC_ARG_ENABLE(
utempter,
AS_HELP_STRING(--enable-utempter, use utempter if it is installed)
@@ -405,17 +421,34 @@ if test "x$enable_utempter" = xyes; then
fi
if test "x$enable_utempter" = xyes; then
AC_DEFINE(HAVE_UTEMPTER)
utempter_version=on
else
AC_MSG_ERROR("utempter not found")
fi
fi
# Look for utf8proc.
utf8proc_version=off
AC_ARG_ENABLE(
utf8proc,
AS_HELP_STRING(--enable-utf8proc, use utf8proc if it is installed)
)
try_utf8proc=no
require_utf8proc=no
if test "x$enable_utf8proc" = xyes; then
try_utf8proc=yes
elif test "x$enable_utf8proc" = x; then
case "$host_os" in
*darwin*)
try_utf8proc=yes
require_utf8proc=yes
;;
esac
fi
if test "x$try_utf8proc" = xyes; then
SAVED_AM_CPPFLAGS="$AM_CPPFLAGS"
SAVED_OLD_CPPFLAGS="$CPPFLAGS"
SAVED_LIBS="$LIBS"
PKG_CHECK_MODULES(
LIBUTF8PROC,
libutf8proc,
@@ -423,7 +456,10 @@ if test "x$enable_utf8proc" = xyes; then
AM_CPPFLAGS="$LIBUTF8PROC_CFLAGS $AM_CPPFLAGS"
CPPFLAGS="$LIBUTF8PROC_CFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBUTF8PROC_LIBS $LIBS"
]
utf8proc_version=`$PKG_CONFIG --modversion libutf8proc 2>/dev/null`
test "x$utf8proc_version" = x && utf8proc_version=on
],
[:]
)
AC_CHECK_HEADER(utf8proc.h, enable_utf8proc=yes, enable_utf8proc=no)
if test "x$enable_utf8proc" = xyes; then
@@ -436,13 +472,23 @@ if test "x$enable_utf8proc" = xyes; then
fi
if test "x$enable_utf8proc" = xyes; then
AC_DEFINE(HAVE_UTF8PROC)
test "x$utf8proc_version" = xoff && utf8proc_version=on
else
AC_MSG_ERROR("utf8proc not found")
AM_CPPFLAGS="$SAVED_AM_CPPFLAGS"
CPPFLAGS="$SAVED_OLD_CPPFLAGS"
LIBS="$SAVED_LIBS"
utf8proc_version=off
if test "x$require_utf8proc" = xyes; then
enable_utf8proc=
else
AC_MSG_ERROR("utf8proc not found")
fi
fi
fi
AM_CONDITIONAL(HAVE_UTF8PROC, [test "x$enable_utf8proc" = xyes])
# Check for systemd support.
systemd_version=off
AC_ARG_ENABLE(
systemd,
AS_HELP_STRING(--enable-systemd, enable systemd integration)
@@ -456,6 +502,8 @@ if test x"$enable_systemd" = xyes; then
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$SYSTEMD_LIBS $LIBS"
found_systemd=yes
systemd_version=`$PKG_CONFIG --modversion libsystemd 2>/dev/null`
test "x$systemd_version" = x && systemd_version=on
],
found_systemd=no
)
@@ -580,11 +628,34 @@ if test "x$found_malloc_trim" = xyes; then
fi
# Build against jemalloc if requested.
jemalloc_version=off
AC_ARG_ENABLE(
jemalloc,
AS_HELP_STRING(--enable-jemalloc, use jemalloc if it is installed)
)
try_jemalloc=no
require_jemalloc=no
if test "x$enable_asan" = xyes && test "x$enable_jemalloc" = xyes; then
AC_MSG_ERROR([--enable-asan and --enable-jemalloc cannot be used together])
fi
if test "x$enable_jemalloc" = xyes; then
try_jemalloc=yes
elif test "x$enable_jemalloc" = x; then
case "$host_os" in
*darwin*)
if test "x$enable_asan" = xyes; then
enable_jemalloc=no
else
try_jemalloc=yes
require_jemalloc=yes
fi
;;
esac
fi
if test "x$try_jemalloc" = xyes; then
SAVED_AM_CPPFLAGS="$AM_CPPFLAGS"
SAVED_OLD_CPPFLAGS="$CPPFLAGS"
SAVED_LIBS="$LIBS"
PKG_CHECK_MODULES(
JEMALLOC,
jemalloc,
@@ -592,9 +663,26 @@ if test "x$enable_jemalloc" = xyes; then
AM_CPPFLAGS="$JEMALLOC_CFLAGS $AM_CPPFLAGS"
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$LIBS $JEMALLOC_LIBS"
jemalloc_version=`$PKG_CONFIG --modversion jemalloc 2>/dev/null`
test "x$jemalloc_version" = x && jemalloc_version=on
],
AC_MSG_ERROR("jemalloc not found")
enable_jemalloc=no
)
if test "x$enable_jemalloc" = xno; then
AM_CPPFLAGS="$SAVED_AM_CPPFLAGS"
CPPFLAGS="$SAVED_OLD_CPPFLAGS"
LIBS="$SAVED_LIBS"
jemalloc_version=off
if test "x$require_jemalloc" = xyes; then
enable_jemalloc=
else
AC_MSG_ERROR("jemalloc not found")
fi
else
enable_jemalloc=yes
AC_DEFINE(HAVE_JEMALLOC)
test "x$jemalloc_version" = xoff && jemalloc_version=on
fi
fi
# Check for CMSG_DATA. On some platforms like HP-UX this requires UNIX 95
@@ -941,6 +1029,20 @@ case "$host_os" in
AC_MSG_NOTICE([])
AC_MSG_ERROR([must give --enable-utf8proc or --disable-utf8proc])
fi
#
# macOS calloc(3) does not appear to always zero memory correctly,
# so complain and suggest using jemalloc instead.
#
if test "x$enable_jemalloc" = x; then
AC_MSG_NOTICE([])
AC_MSG_NOTICE([ macOS calloc(3) appears not to correctly])
AC_MSG_NOTICE([ zero allocations in some circumstances;])
AC_MSG_NOTICE([ to avoid this, configuring with])
AC_MSG_NOTICE([ --enable-jemalloc is recommended. To build])
AC_MSG_NOTICE([ without anyway, use --disable-jemalloc])
AC_MSG_NOTICE([])
AC_MSG_ERROR([must give --enable-jemalloc or --disable-jemalloc])
fi
;;
*dragonfly*)
AC_MSG_RESULT(dragonfly)
@@ -1028,6 +1130,26 @@ AC_MSG_CHECKING(lock-command)
AC_MSG_RESULT($DEFAULT_LOCK_CMD)
AC_SUBST(DEFAULT_LOCK_CMD)
# Print a summary.
AC_MSG_NOTICE([])
if test "x$enable_asan" = xyes; then
AC_MSG_NOTICE([ASAN: on])
else
AC_MSG_NOTICE([ASAN: off])
fi
if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on])
else
AC_MSG_NOTICE([debug: off])
fi
AC_MSG_NOTICE([jemalloc: $jemalloc_version])
AC_MSG_NOTICE([libevent: $libevent_version])
AC_MSG_NOTICE([ncurses: $ncurses_version])
AC_MSG_NOTICE([systemd: $systemd_version])
AC_MSG_NOTICE([utempter: $utempter_version])
AC_MSG_NOTICE([utf8proc: $utf8proc_version])
AC_MSG_NOTICE([])
# Save our CFLAGS/CPPFLAGS/LDFLAGS for the Makefile and restore the old user
# variables.
AC_SUBST(AM_CPPFLAGS)

3
file.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: file.c,v 1.20 2026/05/17 10:54:01 nicm Exp $ */
/* $OpenBSD: file.c,v 1.21 2026/07/26 15:08:15 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -185,6 +185,7 @@ file_can_print(struct client *c)
{
if (c == NULL ||
(c->flags & CLIENT_ATTACHED) ||
(c->flags & CLIENT_DEAD) ||
(c->flags & CLIENT_CONTROL))
return (0);
return (1);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format-draw.c,v 1.33 2026/06/29 17:08:52 nicm Exp $ */
/* $OpenBSD: format-draw.c,v 1.34 2026/07/21 07:20:18 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -958,7 +958,7 @@ format_draw(struct screen_write_ctx *octx, const struct grid_cell *base,
if (srs != NULL) {
if (fr != NULL && !format_is_type(fr, &sy)) {
if (s[current].cx != fr->start) {
fr->end = s[current].cx + 1;
fr->end = s[current].cx;
TAILQ_INSERT_TAIL(&frs, fr, entry);
} else
free(fr);

103
format.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.407 2026/07/20 07:42:13 nicm Exp $ */
/* $OpenBSD: format.c,v 1.409 2026/07/26 15:21:53 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -128,6 +128,7 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2)
#define FORMAT_OPTIONS 0x40000000
#define FORMAT_ENVIRON 0x80000000ULL
#define FORMAT_DIFFERENCE 0x100000000ULL
#define FORMAT_CYCLE 0x200000000ULL
/* Limit on recursion. */
#define FORMAT_LOOP_LIMIT 100
@@ -138,9 +139,13 @@ format_job_cmp(struct format_job *fj1, struct format_job *fj2)
/* How often to check the time in long loops. */
#define FORMAT_TIME_LOOP_CHECK 10000
/* Fixed animation period (ms): redraw interval and shortest frame step. */
#define FORMAT_CYCLE_PERIOD 100
/* Format expand flags. */
#define FORMAT_EXPAND_TIME 0x1
#define FORMAT_EXPAND_NOJOBS 0x2
#define FORMAT_EXPAND_NOCYCLE 0x4
/* Entry in format tree. */
struct format_entry {
@@ -413,7 +418,8 @@ format_job_get(struct format_expand_state *es, const char *cmd)
RB_INSERT(format_job_tree, jobs, fj);
}
format_copy_state(&next, es, FORMAT_EXPAND_NOJOBS);
format_copy_state(&next, es, FORMAT_EXPAND_NOJOBS|
FORMAT_EXPAND_NOCYCLE);
next.flags &= ~FORMAT_EXPAND_TIME;
expanded = format_expand1(&next, cmd);
@@ -4319,7 +4325,7 @@ format_quote_shell(const char *s)
at = out = xmalloc(strlen(s) * 2 + 1);
for (cp = s; *cp != '\0'; cp++) {
if (strchr("|&;<>()$`\\\"'*?[# =%", *cp) != NULL)
if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL)
*at++ = '\\';
*at++ = *cp;
}
@@ -4797,7 +4803,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,O,V,R,<,>
* l,m,C,a,b,c,d,I,n,t,w,q,E,T,S,W,P,O,V,R,A,<,>
* =a
* =/a
* =/a/
@@ -4816,7 +4822,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s,
break;
/* Check single character modifiers with no arguments. */
if (strchr("labdnwETSWPOVL!<>", cp[0]) != NULL &&
if (strchr("labdnwETSWPOVL!<>A", cp[0]) != NULL &&
format_is_end(cp[1])) {
format_add_modifier(&list, count, cp, 1, NULL, 0);
cp++;
@@ -4838,7 +4844,7 @@ format_build_modifiers(struct format_expand_state *es, const char **s,
}
/* Now try single character with arguments. */
if (strchr("ImCLNPSOVst=pReqWc", cp[0]) == NULL)
if (strchr("ImCLNPSOVst=pReqWcA", cp[0]) == NULL)
break;
c = cp[0];
@@ -5797,6 +5803,74 @@ fail:
return (NULL);
}
/* Callback for the cycle timer; redraw the status line. */
static void
format_cycle_callback(__unused int fd, __unused short events, void *arg)
{
struct client *c = arg;
if (c->message_string == NULL && c->prompt == NULL)
c->flags |= CLIENT_REDRAWSTATUS;
}
/* Arm the cycle timer to redraw the status line if it is not already. */
static void
format_cycle_start_timer(struct client *c)
{
struct timeval tv;
tv.tv_sec = FORMAT_CYCLE_PERIOD / 1000;
tv.tv_usec = (FORMAT_CYCLE_PERIOD % 1000) * 1000L;
if (!event_initialized(&c->cycle_timer))
evtimer_set(&c->cycle_timer, format_cycle_callback, c);
if (!evtimer_pending(&c->cycle_timer, NULL))
evtimer_add(&c->cycle_timer, &tv);
}
/* Expand the "A" animation modifier; see the manual for the syntax. */
static char *
format_cycle(struct format_expand_state *es, const char *frames, u_int count)
{
struct format_tree *ft = es->ft;
const char *start, *end, *cp;
u_int n, index, i;
/*
* A cycle is only expanded in a status format, and never in the
* command or output of #() where it would change on every frame and
* make the job run again.
*/
if (!(ft->flags & FORMAT_STATUS) || (es->flags & FORMAT_EXPAND_NOCYCLE))
return (xstrdup(""));
if (*frames == '\0')
return (xstrdup(""));
/* Count the comma-separated frames (there is at least one). */
n = 1;
for (cp = frames; *cp != '\0'; cp++) {
if (*cp == ',')
n++;
}
index = (es->start_time / (count * FORMAT_CYCLE_PERIOD)) % n;
/*
* Redraw the status line so the frames advance on their own; a
* single frame never changes so there is nothing to redraw for.
*/
if (n > 1 && ft->client != NULL)
format_cycle_start_timer(ft->client);
/* Walk to the chosen frame and return a copy of it. */
start = frames;
for (i = 0; i < index; i++)
start = strchr(start, ',') + 1;
end = strchr(start, ',');
if (end == NULL)
end = start + strlen(start);
return (xstrndup(start, end - start));
}
/* Replace a key. */
static int
format_replace(struct format_expand_state *es, const char *key, size_t keylen,
@@ -5817,6 +5891,7 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
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 cycle_count = 1;
u_int i, count, nsub = 0, nrep, check = 0;
const char *loop_flags = "";
struct format_expand_state next;
@@ -5877,6 +5952,15 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
if (errstr != NULL)
width = 0;
break;
case 'A':
modifiers |= FORMAT_CYCLE;
if (fm->argc < 1)
break;
cycle_count = strtonum(fm->argv[0], 1, 100,
&errstr);
if (errstr != NULL)
cycle_count = 1;
break;
case 'w':
modifiers |= FORMAT_WIDTH;
break;
@@ -6098,6 +6182,13 @@ format_replace(struct format_expand_state *es, const char *key, size_t keylen,
goto done;
}
/* Is this an animation cycle? */
if (modifiers & FORMAT_CYCLE) {
value = format_cycle(es, copy, cycle_count);
format_log(es, "cycle '%s' is: %s", copy, value);
goto done;
}
/* Is this a literal string? */
if (modifiers & FORMAT_LITERAL) {
format_log(es, "literal string is '%s'", copy);

63
hooks.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: hooks.c,v 1.14 2026/07/13 22:03:08 nicm Exp $ */
/* $OpenBSD: hooks.c,v 1.16 2026/07/27 19:15:58 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -25,7 +25,7 @@
#include "tmux.h"
/* Hook monitor state owned by an option entry. */
struct hook_monitor {
struct hooks_monitor {
struct options *oo;
struct monitor_set *set;
@@ -143,6 +143,7 @@ hooks_insert(struct cmdq_item *item, struct hooks_data *hd)
log_debug("%s: hook %s not found", __func__, hd->name);
return;
}
options_hook_fired(o);
if (item == NULL)
state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS);
@@ -230,7 +231,7 @@ hooks_event_cb(const char *name, struct event_payload *ep,
{
struct cmdq_item *item;
if (event_payload_get_pointer(ep, "_hook_monitor") != NULL)
if (event_payload_get_pointer(ep, "_hooks_monitor") != NULL)
return;
item = event_payload_get_pointer(ep, "_cmdq_item");
@@ -321,7 +322,7 @@ hooks_run(struct cmdq_item *item, const char *name)
void
hooks_monitor_free(void *data)
{
struct hook_monitor *hm = data;
struct hooks_monitor *hm = data;
events_remove_sink(hm->sink);
monitor_destroy(hm->set);
@@ -334,7 +335,7 @@ void
hooks_monitor_remove(struct options *oo, const char *name)
{
struct options_entry *o;
struct hook_monitor *hm;
struct hooks_monitor *hm;
o = options_get_only(oo, name);
if (o == NULL)
@@ -352,9 +353,9 @@ static void
hooks_monitor_hook_cb(const char *name, struct event_payload *ep,
void *sink_data)
{
struct hook_monitor *hm = sink_data;
struct hooks_monitor *hm = sink_data;
if (event_payload_get_pointer(ep, "_hook_monitor") == hm)
if (event_payload_get_pointer(ep, "_hooks_monitor") == hm)
hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1);
}
@@ -362,14 +363,14 @@ hooks_monitor_hook_cb(const char *name, struct event_payload *ep,
static void
hooks_monitor_cb(struct monitor_change *change, void *data)
{
struct hook_monitor *hm = data;
struct hooks_monitor *hm = data;
struct event_payload *ep;
struct winlink *wl = change->wl;
struct window_pane *wp = change->wp;
struct cmd_find_state fs;
ep = event_payload_create();
event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL);
event_payload_set_pointer(ep, "_hooks_monitor", data, NULL, NULL);
cmd_find_clear_state(&fs, 0);
if (wl != NULL && wp != NULL && wp->window == wl->window)
@@ -399,8 +400,7 @@ hooks_monitor_cb(struct monitor_change *change, void *data)
event_payload_set_session(ep, "session", change->s);
if (wl != NULL) {
if (change->s == NULL)
event_payload_set_session(ep, "session",
wl->session);
event_payload_set_session(ep, "session", wl->session);
event_payload_set_window(ep, "window", wl->window);
event_payload_set_int(ep, "window_index", wl->idx);
}
@@ -420,7 +420,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo,
int flags, struct cmd_find_state *fs, struct session *s)
{
struct options_entry *o;
struct hook_monitor *hm;
struct hooks_monitor *hm;
hooks_monitor_remove(oo, name);
o = options_get_only(oo, name);
@@ -443,7 +443,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo,
char *
hooks_monitor_to_string(struct options_entry *o)
{
struct hook_monitor *hm = options_get_monitor_data(o);
struct hooks_monitor *hm = options_get_monitor_data(o);
const char *name = options_name(o);
char *s;
@@ -469,3 +469,40 @@ hooks_monitor_to_string(struct options_entry *o)
}
return (s);
}
/* Get the parts of a hook monitor. */
int
hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id,
const char **format)
{
struct hooks_monitor *hm = options_get_monitor_data(o);
if (hm == NULL)
return (0);
*type = hm->type;
*id = hm->id;
*format = hm->format;
return (1);
}
/* Get hook monitor firing count. */
u_int
hooks_monitor_get_fire_count(struct options_entry *o)
{
struct hooks_monitor *hm = options_get_monitor_data(o);
if (hm == NULL)
return (0);
return (monitor_get_fire_count(hm->set, options_name(o)));
}
/* Get hook monitor firing time. */
time_t
hooks_monitor_get_fire_time(struct options_entry *o)
{
struct hooks_monitor *hm = options_get_monitor_data(o);
if (hm == NULL)
return (0);
return (monitor_get_fire_time(hm->set, options_name(o)));
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: key-bindings.c,v 1.189 2026/07/15 10:38:31 nicm Exp $ */
/* $OpenBSD: key-bindings.c,v 1.190 2026/07/22 19:23:59 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -45,6 +45,9 @@
" ''" \
" 'New After' 'w' {new-window -a}" \
" 'New At End' 'W' {new-window}"
#define DEFAULT_EMPTY_MENU \
" 'New Pane' 'p' {new-pane; join-pane}" \
" 'New Window' 'w' {new-window}"
#define DEFAULT_PANE_MENU \
" '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \
" '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \
@@ -549,6 +552,8 @@ key_bindings_init(void)
/* Mouse button 3 down on pane. */
"bind -n MouseDown3Pane { if -Ft= '#{||:#{mouse_any_flag},#{&&:#{pane_in_mode},#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}}}' { select-pane -t=; send -M } { display-menu -t= -xM -yM -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " } }",
"bind -n M-MouseDown3Pane { display-menu -t= -xM -yM -T '#[align=centre]#{pane_index} (#{pane_id})' " DEFAULT_PANE_MENU " }",
"bind -n MouseDown3Empty { display-menu -t= -xM -yM -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_EMPTY_MENU " }",
"bind -n M-MouseDown3Empty { display-menu -t= -xM -yM -T '#[align=centre]#{window_index}:#{window_name}' " DEFAULT_EMPTY_MENU " }",
/* Mouse on scrollbar. */
"bind -n MouseDown1ScrollbarUp { if -Ft= '#{pane_in_mode}' { send -X page-up } {copy-mode -u } }",

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */
/* $OpenBSD: monitor.c,v 1.7 2026/07/27 19:15:58 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -59,6 +59,9 @@ struct monitor_item {
struct monitor_panes panes;
struct monitor_windows windows;
u_int fire_count;
time_t fire_time;
RB_ENTRY(monitor_item) entry;
};
RB_HEAD(monitor_items, monitor_item);
@@ -180,6 +183,9 @@ monitor_report(struct monitor_set *ms, struct monitor_item *me,
log_debug("%s: %s changed to %s", __func__, me->name, value);
me->fire_count++;
me->fire_time = current_time;
change.name = me->name;
change.value = value;
change.last = last;
@@ -659,3 +665,25 @@ monitor_remove(struct monitor_set *ms, const char *name)
if (RB_EMPTY(&ms->items) && evtimer_initialized(&ms->timer))
evtimer_del(&ms->timer);
}
/* Get subscription firing count. */
u_int
monitor_get_fire_count(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)
return (0);
return (me->fire_count);
}
/* Get subscription firing time. */
time_t
monitor_get_fire_time(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)
return (0);
return (me->fire_time);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.239 2026/07/19 17:36:38 nicm Exp $ */
/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1430,6 +1430,15 @@ const struct options_table_entry options_table[] = {
.text = "Style of current line number in copy mode."
},
{ .name = "copy-mode-current-line-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
.default_str = "default",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Style of the line containing the cursor in copy mode."
},
{ .name = "copy-mode-line-number-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
@@ -1450,8 +1459,8 @@ const struct options_table_entry options_table[] = {
{ .name = "fill-character",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
.default_str = "",
.text = "Character used to fill unused parts of window."
.default_str = "#{?is_inside,#[bg=themedarkgrey] ,#[fg=themelightgrey]#[acs]~}",
.text = "Format used to fill unused parts of window."
},
{ .name = "main-pane-height",
@@ -1935,6 +1944,7 @@ const struct options_table_entry options_table[] = {
OPTIONS_TABLE_AFTER_HOOK("show-messages"),
OPTIONS_TABLE_AFTER_HOOK("show-options"),
OPTIONS_TABLE_AFTER_HOOK("split-window"),
OPTIONS_TABLE_AFTER_HOOK("swap-window"),
OPTIONS_TABLE_AFTER_HOOK("unbind-key"),
OPTIONS_TABLE_HOOK("alert-activity", "",
"Run when a window has activity."),

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options.c,v 1.90 2026/07/10 13:38:45 nicm Exp $ */
/* $OpenBSD: options.c,v 1.92 2026/07/27 19:15:58 nicm Exp $ */
/*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -107,7 +107,10 @@ struct options_entry {
int cached;
struct style style;
void *monitor_data;
u_int fire_count;
time_t fire_time;
RB_ENTRY(options_entry) entry;
};
@@ -434,6 +437,25 @@ options_set_monitor_data(struct options_entry *o, void *data)
o->monitor_data = data;
}
void
options_hook_fired(struct options_entry *o)
{
o->fire_count++;
o->fire_time = current_time;
}
u_int
options_get_fire_count(struct options_entry *o)
{
return (o->fire_count);
}
time_t
options_get_fire_time(struct options_entry *o)
{
return (o->fire_time);
}
const struct options_table_entry *
options_table_entry(struct options_entry *o)
{
@@ -1361,7 +1383,7 @@ options_push_changes(const char *name)
}
if (strcmp(name, "fill-character") == 0) {
RB_FOREACH(w, windows, &windows)
window_set_fill_character(w);
window_set_fill_cells(w);
}
if (strcmp(name, "key-table") == 0) {
TAILQ_FOREACH(loop, &clients, entry)

13
proc.c
View File

@@ -31,6 +31,10 @@
#include <ncurses.h>
#endif
#ifdef HAVE_JEMALLOC
#include <jemalloc/jemalloc.h>
#endif
#include "tmux.h"
struct tmuxproc {
@@ -181,6 +185,10 @@ proc_start(const char *name)
{
struct tmuxproc *tp;
struct utsname u;
#ifdef HAVE_JEMALLOC
const char *version;
size_t size = sizeof version;
#endif
log_open(name);
setproctitle("%s (%s)", name, socket_path);
@@ -195,6 +203,11 @@ proc_start(const char *name)
#ifdef HAVE_UTF8PROC
log_debug("using utf8proc %s", utf8proc_version());
#endif
#ifdef HAVE_JEMALLOC
if (mallctl("version", &version, &size, NULL, 0) != 0)
version = "(unknown version)";
log_debug("using jemalloc %s", version);
#endif
#ifdef NCURSES_VERSION
log_debug("using ncurses %s %06u", NCURSES_VERSION, NCURSES_VERSION_PATCH);
#endif

View File

@@ -280,6 +280,7 @@ test_user_hook()
settle
assert_equals "$($TMUX show-option -qv @cm_hook)" \
"display-message hook" "add user hook"
$TMUX set-hook -E @cm_hook || fail "fire user hook failed"
send q
wait_mode 0
@@ -287,7 +288,10 @@ test_user_hook()
repeat_key j 3
send Right j
settle
assert_contains "$(capture)" "@cm_hook" "user hook shown as hook"
screen=$(capture)
assert_contains "$screen" "@cm_hook" "user hook shown as hook"
assert_contains "$screen" "This hook has been fired 1 times, last " \
"user hook fire count and time"
send u y
settle
@@ -302,10 +306,17 @@ test_hook_array()
$TMUX set-hook -g 'after-new-session[0]' 'display-message old' ||
fail "set hook array failed"
$TMUX set-hook -g -R after-new-session || fail "fire hook array failed"
open_customize '#{==:#{option_name},after-new-session}'
repeat_key j 3
send Right j Right j
send Right j
settle
screen=$(capture)
assert_contains "$screen" "This hook has been fired " \
"array hook fire count"
assert_contains "$screen" ", last " "array hook fire time"
send Right j
settle
send a

109
regress/format-animation.sh Normal file
View File

@@ -0,0 +1,109 @@
#!/bin/sh
# Exercise the A format modifier (format_cycle in format.c). It is expanded
# only in a status format and arms the client cycle timer to redraw the status
# line as the frames advance, so the only place its behaviour shows is a
# rendered status line.
#
# The status line is rendered by an inner tmux attached inside an outer tmux
# pane; capturing the outer pane shows what the inner tmux drew. The inner
# window runs sleep and every other status line is blanked, so the capture
# contains the frame and nothing else.
#
# Each frame lasts 700 milliseconds and the capture is repeated once a second,
# so consecutive samples always land on a different frame: no exact period is
# assumed and nothing in the test asks for a redraw, so seeing the frame change
# means the status line animated on its own.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \
0 1 15
fail() {
echo "$*" >&2
exit 1
}
# Capture the outer pane and return the text the inner tmux drew.
capture() {
$TMUX capturep -p >$TMP || fail "capture failed"
tr -d '[:space:]' <$TMP
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
# Inner: a window which draws nothing and a status line which is only the
# animation, so the outer capture is the current frame on its own.
$TMUX2 new -d -x30 -y8 "sh -c 'exec sleep 100'" || exit 1
$TMUX2 set -g window-size latest || exit 1
i=1
while [ $i -le 4 ]; do
$TMUX2 set -g status-format[$i] "" || exit 1
i=$((i + 1))
done
$TMUX2 set -g status-format[0] "#{A/7:AAAA,BBBB,CCCC}" || exit 1
$TMUX new -d -x30 -y8 || exit 1
$TMUX set -g status off || exit 1
$TMUX set -g window-size manual || exit 1
$TMUX set -g default-terminal "tmux-256color" || exit 1
$TMUX send -l "$TMUX2 attach" || exit 1
$TMUX send Enter || exit 1
sleep 1
# Sample the frames. Every sample must be one of the frames and at least two
# different ones must be seen; status-interval is 15 seconds so only the cycle
# timer can have redrawn the status line.
seen=""
i=0
while [ $i -lt 4 ]; do
[ $i -eq 0 ] || sleep 1
frame=$(capture)
case "$frame" in
AAAA|BBBB|CCCC) ;;
*) fail "status line is '$frame', not a frame" ;;
esac
case " $seen " in
*" $frame "*) ;;
*) seen="$seen $frame" ;;
esac
i=$((i + 1))
done
set -- $seen
[ $# -ge 2 ] || fail "status line did not animate, only saw$seen"
# Outside a status format there is no animation at all.
out=$($TMUX2 display-message -p "#{A:ZZZZ,YYYY}") || exit 1
[ -z "$out" ] || fail "display-message gave '$out', want empty"
out=$($TMUX2 list-panes -F "#{A:ZZZZ,YYYY}") || exit 1
[ -z "$out" ] || fail "list-panes gave '$out', want empty"
# Nor inside #(), where the frames would change the command on every frame.
$TMUX2 set -g status-format[0] '#(echo "[#{A:ZZZZ,YYYY}]")' || exit 1
sleep 1
out=$(capture)
case "$out" in
*ZZZZ*|*YYYY*) fail "job command saw a frame: '$out'" ;;
*"[]"*) ;;
*) fail "job output is '$out', want []" ;;
esac
# Empty frames and a missing or bad count must not upset the server.
for f in '#{A:}' '#{A/0:a,b}' '#{A/x:a,b}' '#{A:,,,}'; do
$TMUX2 set -g status-format[0] "$f" || fail "setting $f failed"
$TMUX2 has-session >/dev/null 2>&1 || fail "server lost with $f"
done
sleep 1
$TMUX2 has-session >/dev/null 2>&1 || fail "server lost after empty frames"
exit 0

View File

@@ -85,6 +85,24 @@ echo "$shown" | grep -q '^session-created\[0\]' ||
fail "missing first array item: $shown"
echo "$shown" | grep -q '^session-created\[1\]' ||
fail "missing second array item: $shown"
shown=$($TMUX show-hooks -gF '#{option_name}:#{hook_fire_count}:#{t/p:hook_fire_time}' \
session-created) ||
fail "show-hooks -gF failed"
echo "$shown" | grep -q '^session-created:[1-9][0-9]*:[^-][^ ]*$' ||
fail "missing hook fire formats: $shown"
# User hooks are options, but show-hooks should list only registered @ hooks
# and not ordinary user options.
$TMUX set -g @not-a-hook value || fail "set @not-a-hook failed"
$TMUX set-hook -g @user-hook 'lsk' ||
fail "set-hook @user-hook failed"
shown=$($TMUX show-hooks -g) ||
fail "show-hooks -g all failed"
echo "$shown" | grep -q '^@user-hook lsk$' ||
fail "missing user hook: $shown"
if echo "$shown" | grep -q '^@not-a-hook '; then
fail "show-hooks listed user option: $shown"
fi
# Unsetting removes the whole hook.
$TMUX set-hook -gu session-created || fail "set-hook -gu failed"

View File

@@ -174,3 +174,17 @@ check_raw_matches()
fi
done
}
check_raw_no_matches()
{
name=$1
shift
capture_raw "$name" >"$TMP"
for unexpected in "$@"; do
if grep -Eq "$unexpected" "$TMP"; then
printf "%s\n" "$unexpected" >"$EXP"
fail "$name raw unexpected"
fi
done
}

View File

@@ -22,6 +22,16 @@ check_capture rename 'X'
start_pane apc-title 20 3 '\033_test-title\033\\X\n'
check_capture apc-title 'X'
cmd='i=0; while [ "$i" -lt 12 ]; do '
cmd="${cmd}printf '\\033[22;0t'; i=\$((i + 1)); done; "
cmd="${cmd}printf X; sleep 2"
start_cmd title-stack 20 3 "$cmd"
check_capture title-stack 'X'
$TMUX respawn-pane -k -t title-stack: \
"printf '\\033[22;0tY'; sleep 2" || exit 1
sleep 0.3
check_capture title-stack 'Y'
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 20 -y 3 -s osc52 "sleep 2" || exit 1

View File

@@ -41,5 +41,9 @@ start_pane combining-left 10 3 '\314\201A\n'
check_capture combining-left 'A'
check_cursor combining-left '0,1'
start_pane combining-overflow 10 3 'u\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\n'
check_raw_matches combining-overflow '^ C 0,0 data=\(1,31,u'
check_raw_no_matches combining-overflow 'data=\(0,'
$TMUX kill-server 2>/dev/null
exit $exit_status

View File

@@ -49,11 +49,33 @@ drag()
sleep 1
}
# right_click COL ROW KEY
#
# Open the right-click menu at a 1-based position and choose KEY.
right_click()
{
col="$1"
row="$2"
key="$3"
seq=$(printf '\033[<2;%s;%sM' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
$TMUX2 send-keys -t "$OUTER" "$key" 2>/dev/null
sleep 1
}
cleanup
$TMUX new-session -d -s inner -x 80 -y 24 || exit 1
$TMUX set -g mouse on
$TMUX set -g default-command 'sleep 100'
keys=$($TMUX list-keys -T root -F '#{key_command}' MouseDown3Empty) ||
fail "list MouseDown3Empty failed"
case "$keys" in
*"New Pane"*"New Window"*) ;;
*) fail "missing empty-area menu binding: $keys" ;;
esac
$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1
sleep 1
@@ -80,6 +102,17 @@ $TMUX break-pane -W -s "$BASE" || fail "break base pane failed"
$TMUX resize-pane -t "$BASE" -x10 -y4 || fail "resize floating base failed"
$TMUX move-pane -t "$BASE" -P top-left || fail "move floating base failed"
# Right-click in empty window space and choose New Pane. This creates a new
# floating pane and tiles it.
right_click 20 8 p
id=$($TMUX list-panes -F '#{?pane_active,#{pane_id},}' | tail -n 1)
[ -n "$id" ] || fail "no pane created from empty-area menu"
[ "$id" != "$BASE" ] || fail "empty-area menu did not create a new pane"
must_equal "$($TMUX display-message -p -t "$id" '#{pane_floating_flag}')" 0
$TMUX kill-pane -t "$id" || fail "kill empty-area menu pane failed"
# Drag in empty window space with no tiled pane underneath.
drag 40 10 50 15
@@ -92,6 +125,12 @@ must_equal "$($TMUX display-message -p -t "$id" '#{pane_top}')" 10
must_equal "$($TMUX display-message -p -t "$id" '#{pane_width}')" 9
must_equal "$($TMUX display-message -p -t "$id" '#{pane_height}')" 4
TILED=$($TMUX new-pane -PF '#{pane_id}' 'sleep 100') ||
fail "new pane from only floating panes failed"
$TMUX join-pane -t "$TILED" ||
fail "tile new pane from only floating panes failed"
must_equal "$($TMUX display-message -p -t "$TILED" '#{pane_floating_flag}')" 0
TILED=$($TMUX new-window -dPF '#{pane_id}' 'sleep 100') ||
fail "new tiled window failed"
$TMUX new-pane -d -M -L -t "$TILED" 'sleep 100' || fail "new-pane -M -L failed"

View File

@@ -140,6 +140,24 @@ update-environment[notify] EEE"
# sorts by ascending numeric key and keeps the gap at [1].
check_ok set -g status-format ""
check_array "-g status-format" "status-format"
out=$($TMUX show -gF \
'#{option_name}:#{option_has_value}:#{option_value}:#{option_is_array}:#{option_has_array_key}:#{option_array_key}' \
status-format 2>&1)
[ "$out" = "status-format:0::1:0:" ] || {
echo "show -F empty status-format failed."
echo "Expected: 'status-format:0::1:0:'"
echo "But got: '$out'"
exit 1
}
out=$($TMUX show -gvF \
'#{option_name}:#{option_value_only}:#{option_has_value}:#{option_value}' \
status-format 2>&1)
[ "$out" = "status-format:1:0:" ] || {
echo "show -vF empty status-format failed."
echo "Expected: 'status-format:1:0:'"
echo "But got: '$out'"
exit 1
}
check_ok set -g status-format[5] "five"
check_ok set -g status-format[0] "zero"
check_ok set -g status-format[2] "two"
@@ -154,6 +172,21 @@ status-format[5] five
status-format[foo-bar] foo-bar
status-format[xterm-256color] xterm
status-format[zoom] zoom"
out=$($TMUX show -gF \
'#{option_name}:#{option_array_key}:#{option_is_array}:#{option_has_value}:#{option_has_array_key}:#{option_value}' \
status-format 2>&1)
[ "$out" = "$(printf '%s' 'status-format:0:1:1:1:zero
status-format:1:1:1:1:one
status-format:2:1:1:1:two
status-format:5:1:1:1:five
status-format:foo-bar:1:1:1:foo-bar
status-format:xterm-256color:1:1:1:xterm
status-format:zoom:1:1:1:zoom')" ] || {
echo "show -F status-format failed."
echo "Expected formatted array output"
echo "But got:"; printf '%s\n' "$out"
exit 1
}
check_value "-gv status-format[01]" "one"
check_ok set -gu status-format[zoom]
check_value "-gv status-format[zoom]" ""

View File

@@ -168,6 +168,30 @@ check_ok set -g @str "foo"
check_ok set -ga @str "bar"
check_value "-gv @str" "foobar"
# --- show -F custom format ------------------------------------------------
#
# show-options and show-hooks use a format for their output, with the default
# format preserving the traditional output.
check_ok set -g @str "two words"
check_value "-g @str" '@str "two words"'
out=$($TMUX show -gF \
'#{option_name}=#{option_value}=#{option_is_user}=#{option_is_string}=#{option_value_only}' \
@str 2>&1)
[ "$out" = "@str=two words=1=1=0" ] || {
echo "show -F @str failed."
echo "Expected: '@str=two words=1=1=0'"
echo "But got: '$out'"
exit 1
}
out=$($TMUX show -gvF '#{option_name}=#{option_value}=#{option_value_only}' \
@str 2>&1)
[ "$out" = "@str=two words=1" ] || {
echo "show -vF @str failed."
echo "Expected: '@str=two words=1'"
echo "But got: '$out'"
exit 1
}
# --- -F expands at set time -----------------------------------------------
#
# With -F the value is expanded as a format once, at set time; without -F it is

52
regress/pipe-pane.sh Normal file
View File

@@ -0,0 +1,52 @@
#!/bin/sh
# Tests of pipe-pane behaviour.
PATH=/bin:/usr/bin
TERM=screen
LANG=C.UTF-8
LC_ALL=C.UTF-8
export TERM LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
fail()
{
echo "$1"
$TMUX kill-server 2>/dev/null
exit 1
}
check_ok()
{
if ! $TMUX "$@"; then
fail "Command failed: $*"
fi
}
check_alive()
{
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
fail "Server died"
fi
}
# A pipe-pane -I child may write after the pane process has exited. With
# remain-on-exit, the pane stays around but its bufferevent has been freed.
check_ok new-session -d -s pipe -x 80 -y 24 'sleep 0.2'
check_ok set-option -t pipe:0 remain-on-exit on
check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x'
i=0
while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
i=$((i + 1))
[ "$i" -gt 50 ] && fail "Pane did not die"
sleep 0.1
done
sleep 0.7
check_alive
$TMUX kill-server 2>/dev/null

View File

@@ -11,7 +11,7 @@ TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP" 0 1 15
trap "$TMUX kill-server 2>/dev/null; rm -f $TMP" 0 1 15
$TMUX -f/dev/null new -d "$TMUX run 'echo foo' >$TMP; sleep 10" || exit 1
sleep 1 && [ "$(cat $TMP)" = "foo" ] || exit 1
@@ -20,6 +20,15 @@ $TMUX -f/dev/null new -d "$TMUX run -t: 'echo foo' >$TMP; sleep 10" || exit 1
sleep 1 && [ "$(cat $TMP)" = "" ] || exit 1
[ "$($TMUX display -p '#{pane_mode}')" = "view-mode" ] || exit 1
$TMUX -f/dev/null new -d -s t1 'sleep 10' || exit 1
$TMUX -f/dev/null run -d 1 'echo delayed' >$TMP 2>&1 &
pid=$!
sleep 0.2
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
sleep 2
$TMUX has-session -t t1 || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -0,0 +1,63 @@
#!/bin/sh
# Exercise fill-character as a format. The window is smaller than the attached
# client so OUTSIDE spans exist, then the only tiled pane is removed so EMPTY
# spans exist inside the window around a floating pane.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \
0 1 15
fail() {
echo "$*" >&2
exit 1
}
must_equal() {
if [ "$1" != "$2" ]; then
fail "expected '$2', got '$1'"
fi
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new -d -x40 -y12 "sh -c 'printf base; exec sleep 100'" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g window-size manual || exit 1
$TMUX2 setw fill-character '#{?is_inside,I,#{?is_outside,O,X}}' || exit 1
$TMUX2 resizew -x28 -y8 || exit 1
$TMUX2 new-pane -x12 -y4 -X8 -Y2 "sh -c 'printf FLOAT; exec sleep 100'" || exit 1
tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \
awk '$1==0{print $2; exit}') || exit 1
$TMUX2 kill-pane -t "$tiled" || exit 1
$TMUX new -d -x40 -y12 || exit 1
$TMUX set -g status off || exit 1
$TMUX set -g window-size manual || exit 1
$TMUX set -g default-terminal "tmux-256color" || exit 1
$TMUX send -l "$TMUX2 attach" || exit 1
$TMUX send Enter || exit 1
sleep 1
$TMUX capturep -p >$TMP || exit 1
must_equal "$(sed -n '1p' $TMP | cut -c1-28)" \
"IIIIIIIIIIIIIIIIIIIIIIIIIIII"
must_equal "$(sed -n '1p' $TMP | cut -c29-40)" "OOOOOOOOOOOO"
must_equal "$(sed -n '9p' $TMP)" "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO"
if grep -q X "$TMP"; then
fail "fill-character used neither inside nor outside"
fi
exit 0

View File

@@ -125,6 +125,13 @@ new_scene 28 8
$TMUX2 new-pane -x16 -y5 -X18 -Y4 "sh -c 'printf OUT; exec sleep 100'" || exit 1
compare floating-outside
# Floating pane whose right border is exactly on the window right edge. The
# floating pane is clipped so the tiled pane's right border remains unbroken.
new_scene 28 8
$TMUX2 respawnp -k "sh -c 'i=0; while [ \$i -lt 8 ]; do printf \"%02d:abcdefghijklmnopq\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" || exit 1
$TMUX2 new-pane -x10 -y5 -X19 -Y2 "sh -c 'printf EDGE; exec sleep 100'" || exit 1
compare floating-clip-window-edge
# Floating pane clipped at the top-left corner (negative offsets).
new_scene 40 12
$TMUX2 new-pane -x18 -y6 -X-4 -Y-2 "sh -c 'printf TL; exec sleep 100'" || exit 1
@@ -208,9 +215,9 @@ $TMUX2 new-pane -x20 -y6 -X8 -Y7 "sh -c 'printf OVERST; exec sleep 100'" || exit
compare floating-over-status
# A window left with only a floating pane: killing the single tiled pane removes
# it from the layout, so the area it occupied is no longer owned by any pane and
# is drawn as EMPTY cells (middle dots) around the float. This is the only way to
# produce a REDRAW_SPAN_EMPTY span.
# it from the layout, so the area it occupied is no longer owned by any pane.
# Use a visible fill character for the EMPTY cells around the float.
$TMUX2 set -g fill-character '#[acs]~' || exit 1
new_scene 40 12
$TMUX2 new-pane -x20 -y6 -X8 -Y3 "sh -c 'printf FLOAT; exec sleep 100'" || exit 1
tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \
@@ -218,4 +225,11 @@ tiled=$($TMUX2 list-panes -F '#{pane_floating_flag} #{pane_id}' | \
$TMUX2 kill-pane -t "$tiled" || exit 1
compare floating-empty
# Same after pane-border-lines is set to none: empty window background is not a
# pane border and should still use the dotted window fill.
new_scene 40 12
$TMUX2 breakp -W || exit 1
$TMUX2 set pane-border-lines none || exit 1
compare floating-empty-noborder
exit 0

View File

@@ -0,0 +1,12 @@
01:abcdefghijklmnopq │···········
02:abcdefghijklmnopq │···········
03:abcdefghijklmnop┌────────│···········
04:abcdefghijklmnop│EDGE │···········
05:abcdefghijklmnop│ │···········
06:abcdefghijklmnop│ │···········
07:abcdefghijklmnop└────────│···········
│···········
────────────────────────────┘···········
········································
········································
········································

View File

@@ -0,0 +1,12 @@
········································
········································
····base ················
···· ················
···· ················
········································
········································
········································
········································
········································
········································

View File

@@ -6,7 +6,7 @@ base │···········
│OUT │···········
│ │···········
│ │···········
───────────────────────────┘···········
───────────────────────────┘···········
········································
········································
········································

View File

@@ -67,10 +67,25 @@ 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"
shown=$($TMUX show-hooks -g -BF \
'#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}:#{option_value}:#{option_is_hook}:#{option_is_user}' \
@session-name) ||
fail "show-hooks -BF failed"
[ "$shown" = '@session-name::#{session_name}:@session-name::#{session_name}:1:1' ] ||
fail "unexpected show-hooks -BF output: $shown"
shown=$($TMUX show-hooks -g) ||
fail "show-hooks -g failed"
echo "$shown" | grep -q '^@session-name ' ||
fail "show-hooks -g did not show monitor hook: $shown"
assert_unchanged @seen 0
$TMUX rename-session two || fail "rename-session two failed"
wait_for @seen '@session-name:two'
shown=$($TMUX show-hooks -g -BF '#{hook_fire_count}:#{t/p:hook_fire_time}' \
@session-name) ||
fail "show-hooks -BF fire failed"
echo "$shown" | grep -q '^1:[^-][^ ]*$' ||
fail "unexpected show-hooks -BF fire output: $shown"
$TMUX set -g @seen-last 0 || fail "set @seen-last failed"
$TMUX set-hook -g -B '@session-name::#{session_name}' \
@@ -112,6 +127,11 @@ $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"
shown=$($TMUX show-hooks -g -BF \
'#{option_name}:#{hook_monitor_target}:#{hook_monitor_format}' @pane) ||
fail "show-hooks -BF pane failed"
[ "$shown" = "@pane:%$pane_number:#{pane_width}" ] ||
fail "unexpected show-hooks -BF pane output: $shown"
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}"' ||

View File

@@ -0,0 +1,136 @@
#!/bin/sh
# Tests of the default output from show-options and show-hooks. This is
# intended to guard the output compatibility when the implementation is changed
# to use formats internally, so it deliberately does not use show -F.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
check_value()
{
out=$($TMUX show $1 2>&1)
if [ "$out" != "$(printf '%s' "$2")" ]; then
echo "show $1 failed."
echo "Expected:"; printf '%s\n' "$2"
echo "But got:"; printf '%s\n' "$out"
exit 1
fi
}
check_hooks()
{
out=$($TMUX show-hooks $1 2>&1)
if [ "$out" != "$(printf '%s' "$2")" ]; then
echo "show-hooks $1 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
}
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
# Scalar options: strings are escaped, non-strings are not, and -v suppresses
# the option name.
check_ok set -g @words "two words"
check_value "-g @words" '@words "two words"'
check_value "-gv @words" 'two words'
check_ok set -g display-time 1234
check_value "-g display-time" 'display-time 1234'
check_value "-gv display-time" '1234'
# Inherited options shown with -A are marked with an asterisk.
check_ok set -g status-left "GLOBAL"
check_ok set -u status-left
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:"; printf '%s\n' "$out"
exit 1
fi
# Arrays: an empty array prints just the name unless -v is used; populated
# arrays include every key, including key 0.
check_ok set -g status-format ""
check_value "-g status-format" 'status-format'
check_value "-gv status-format" ''
out=$($TMUX show -A 2>/dev/null | grep '^status-format\*')
if [ "$out" != "status-format*" ]; then
echo "show -A did not mark inherited empty status-format."
echo "But got:"; printf '%s\n' "$out"
exit 1
fi
check_value "-A status-format" 'status-format'
check_ok set -g update-environment "AAA BBB,CCC"
check_value "-g update-environment" 'update-environment[0] AAA
update-environment[1] BBB
update-environment[2] CCC'
check_value "-gv update-environment[0]" 'AAA'
# Hooks use the same array output style, and monitor hooks have their own
# default output form.
check_ok set-hook -g window-renamed[first] "display-message renamed"
check_hooks "-g window-renamed" 'window-renamed[first] display-message renamed'
check_ok set-hook -g -B '@monitor:%*:#{pane_width}'
check_hooks "-g -B @monitor" '@monitor:%*:#{pane_width}'
check_ok set-hook -g @user-hook "display-message user"
check_hooks "-g @user-hook" '@user-hook "display-message user"'
out=$($TMUX show -g 2>&1)
echo "$out" | grep -q "^@monitor ''$" && {
echo "show -g showed monitor hook without -H."
echo "But got:"; printf '%s\n' "$out"
exit 1
}
echo "$out" | grep -q '^@user-hook "display-message user"$' && {
echo "show -g showed user hook without -H."
echo "But got:"; printf '%s\n' "$out"
exit 1
}
out=$($TMUX show -gH 2>&1)
echo "$out" | grep -q "^@monitor ''$" || {
echo "show -gH did not show monitor hook."
echo "But got:"; printf '%s\n' "$out"
exit 1
}
echo "$out" | grep -q '^@user-hook "display-message user"$' || {
echo "show -gH did not show user hook."
echo "But got:"; printf '%s\n' "$out"
exit 1
}
out=$($TMUX show-hooks -g 2>&1)
echo "$out" | grep -q "^@monitor ''$" || {
echo "show-hooks -g did not show monitor hook."
echo "But got:"; printf '%s\n' "$out"
exit 1
}
echo "$out" | grep -q '^@user-hook "display-message user"$' || {
echo "show-hooks -g did not show user hook."
echo "But got:"; printf '%s\n' "$out"
exit 1
}
assert_alive "after show-options output tests"
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -34,6 +34,18 @@ captureen() {
$TMUX capturep -peNS0 -E- >$TMP || exit 1
}
timed() {
if command -v timeout >/dev/null 2>&1; then
timeout 5 "$@"
else
"$@"
fi
}
capture_timed() {
timed $TMUX capturep -pS0 -E- >$TMP || exit 1
}
check_line() {
line=$1
want=$2
@@ -81,6 +93,8 @@ $TMUX2 neww -d \
"printf '\033(0x\033(B'; exec sleep 100" || exit 1
$TMUX2 neww -d \
"awk 'BEGIN { for (i = 0; i < 1100; i++) printf \"a\" }'; exec sleep 100" || exit 1
$TMUX2 neww -d \
"printf 'u\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245\314\245'; exec sleep 100" || exit 1
$TMUX2 selectw -t:0 || exit 1
$TMUX -f/dev/null new -d -x20 -y6 || exit 1
@@ -245,4 +259,12 @@ capture
len=$(sed -n 1p $TMP | wc -c)
[ "$len" -ge 1100 ] || fail "long same-style line was truncated"
# Too many combining marks on one base character must not leave a standalone
# width-zero cell that can make tty_draw_line loop forever on redraw.
$TMUX resizew -x20 -y6 || exit 1
timed $TMUX2 selectw -t:13 || fail "zero-width overflow select hung"
sleep 1
capture_timed
check_grep '^u'
exit 0

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-redraw.c,v 1.153 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: screen-redraw.c,v 1.157 2026/07/24 08:49:23 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -320,7 +320,7 @@ redraw_reset_cell(struct redraw_build_ctx *bctx, u_int x, u_int y)
struct window *w = bctx->w;
memset(bc, 0, sizeof *bc);
if (bctx->ox + x <= w->sx && bctx->oy + y <= w->sy)
if (bctx->ox + x < w->sx && bctx->oy + y < w->sy)
bc->data.type = REDRAW_SPAN_EMPTY;
else
bc->data.type = REDRAW_SPAN_OUTSIDE;
@@ -547,7 +547,8 @@ redraw_mark_border_cell(struct redraw_build_ctx *bctx, int wx, int wy,
* merged.
*/
if (!floating) {
if (bc->data.type == REDRAW_SPAN_EMPTY)
if (bc->data.type == REDRAW_SPAN_EMPTY ||
bc->data.type == REDRAW_SPAN_OUTSIDE)
reset = 1;
else if (bc->data.type != REDRAW_SPAN_BORDER)
return;
@@ -696,20 +697,22 @@ redraw_mark_pane_borders(struct redraw_build_ctx *bctx, struct window_pane *wp,
bottom = wp->yoff + wp->sy;
mark_left = (left >= 0);
mark_right = (right <= (int)bctx->w->sx);
mark_top = (top >= 0);
mark_bottom = (bottom <= (int)bctx->w->sy);
if (floating) {
mark_right = (right < (int)bctx->w->sx);
mark_bottom = (bottom < (int)bctx->w->sy);
if (left < 0)
left = 0;
if (right > (int)bctx->w->sx)
if (right >= (int)bctx->w->sx)
right = (int)bctx->w->sx - 1;
if (top < 0)
top = 0;
if (bottom > (int)bctx->w->sy)
if (bottom >= (int)bctx->w->sy)
bottom = (int)bctx->w->sy - 1;
} else {
mark_right = (right <= (int)bctx->w->sx);
mark_bottom = (bottom <= (int)bctx->w->sy);
if (pane_status == PANE_STATUS_TOP)
mark_bottom = 0;
else if (pane_status == PANE_STATUS_BOTTOM)
@@ -1230,7 +1233,15 @@ redraw_draw_border_span(struct redraw_draw_ctx *dctx,
if (wp == NULL) {
redraw_get_default_border_style(dctx, &gc, &pane_lines);
window_get_border_cell(w, NULL, pane_lines, cell_type, &gc);
if (span->data.type == REDRAW_SPAN_OUTSIDE)
window_get_fill_cell(w, 0, &gc);
else if (span->data.type == REDRAW_SPAN_EMPTY)
window_get_fill_cell(w, 1, &gc);
else {
if (span->data.type != REDRAW_SPAN_BORDER)
pane_lines = PANE_LINES_SINGLE;
window_get_border_cell(NULL, pane_lines, cell_type, &gc);
}
} else {
window_pane_get_border_style(wp, c, &gc);
window_pane_get_border_cell(wp, cell_type, &gc);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.284 2026/07/20 11:16:33 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.285 2026/07/26 09:02:08 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -2892,7 +2892,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
/* Check if this combined character would be too long. */
if (last.data.size + ud->size > sizeof last.data.data)
return (0);
return (zero_width);
/* Combining; flush any pending output. */
screen_write_collect_flush(ctx, 0, __func__);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen.c,v 1.105 2026/06/29 18:17:28 nicm Exp $ */
/* $OpenBSD: screen.c,v 1.107 2026/07/26 09:20:54 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -69,6 +69,7 @@ screen_free_titles(struct screen *s)
free(s->titles);
s->titles = NULL;
s->ntitles = 0;
}
/* Create a new screen. */
@@ -80,6 +81,7 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
s->title = xstrdup("");
s->titles = NULL;
s->ntitles = 0;
s->path = NULL;
s->cstyle = SCREEN_CURSOR_DEFAULT;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.497 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: server-client.c,v 1.499 2026/07/22 08:19:14 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -525,6 +525,8 @@ server_client_lost(struct client *c)
evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer);
if (event_initialized(&c->cycle_timer))
evtimer_del(&c->cycle_timer);
key_bindings_unref_table(c->keytable);
@@ -1057,6 +1059,11 @@ have_event:
c->tty.mouse_scrolling_flag = 0;
c->tty.mouse_slider_mpos = -1;
c->tty.mouse_last_pane = -1;
if ((w->modal->flags & PANE_CLOSEONCLICK) &&
(type == KEYC_TYPE_MOUSEDOWN ||
type == KEYC_TYPE_SECONDCLICK ||
type == KEYC_TYPE_TRIPLECLICK))
server_kill_pane(w->modal);
return (KEYC_UNKNOWN);
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.148 2026/07/14 19:07:03 nicm Exp $ */
/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -371,6 +371,12 @@ server_destroy_pane(struct window_pane *wp, int notify)
close(wp->fd);
wp->fd = -1;
}
if (wp->pipe_fd != -1) {
bufferevent_free(wp->pipe_event);
wp->pipe_event = NULL;
close(wp->pipe_fd);
wp->pipe_fd = -1;
}
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
if (remain_on_exit != 0 && (~wp->flags & PANE_STATUSREADY))

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: spawn.c,v 1.49 2026/07/15 13:02:33 nicm Exp $ */
/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -222,6 +222,7 @@ spawn_window(struct spawn_context *sc, char **cause)
w->name = xstrdup(sc->name);
options_set_number(w->options, "automatic-rename", 0);
}
window_set_fill_cells(w);
}
/* Switch to the new window if required. */

85
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1144 2026/07/19 17:36:38 nicm Exp $
.\" $OpenBSD: tmux.1,v 1.1151 2026/07/28 07:04:18 nicm Exp $
.\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\"
.Dd $Mdocdate: July 19 2026 $
.Dd $Mdocdate: July 28 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -3638,7 +3638,7 @@ but a different format may be specified with
.Fl F .
.Tg newp
.It Xo Ic new\-pane
.Op Fl bdefhIkLMOPvWZ
.Op Fl bCdefhIkLMOPvWZ
.Op Fl B Ar border\-lines
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
@@ -3694,6 +3694,9 @@ creates a modal pane.
A modal pane is always the active pane and prevents interaction with any other
panes while it is active.
A window can only have one modal pane and it must be a floating pane.
With
.Fl C ,
the modal pane is closed when the mouse is clicked outside it.
.Pp
The
.Fl L
@@ -4690,6 +4693,7 @@ the result would be the default background and a blue foreground.
.Tg show
.It Xo Ic show\-options
.Op Fl AgHpqsvw
.Op Fl F Ar format
.Op Fl t Ar target\-pane
.Op Ar option
.Xc
@@ -4717,6 +4721,10 @@ Global session or window options are listed if
is used.
.Fl v
shows only the option value, not the name.
With
.Fl F ,
.Ar format
is used for each option.
If
.Fl q
is set, no error will be returned if
@@ -5708,8 +5716,13 @@ Set the time in milliseconds for which the indicators shown by the
.Ic display\-panes
command appear.
.Pp
.It Ic fill\-character Ar character
Set the character used to fill areas of the terminal unused by a window.
.It Ic fill\-character Ar format
Set the format used to fill areas of the terminal unused by a pane.
The format is expanded once for areas inside the window with
.Ql is_inside
set, and once for areas outside the window with
.Ql is_outside
set.
.Pp
.It Ic main\-pane\-height Ar height
.It Ic main\-pane\-width Ar width
@@ -5775,6 +5788,14 @@ see the
.Sx STYLES
section.
.Pp
.It Ic copy\-mode\-current\-line\-style Ar style
Set the style of the line containing the cursor in copy mode.
For how to specify
.Ar style ,
see the
.Sx STYLES
section.
.Pp
.It Ic copy\-mode\-line\-number\-style Ar style
Set style of line numbers in copy mode.
For how to specify
@@ -6586,6 +6607,7 @@ run
immediately.
.It Xo Ic show\-hooks
.Op Fl Bgpw
.Op Fl F Ar format
.Op Fl t Ar target\-pane
.Op Ar hook
.Xc
@@ -6598,6 +6620,10 @@ With
shows the subscriptions installed with
.Em set\-hook
.Fl B .
With
.Fl F ,
.Ar format
is used for each hook.
.El
.Sh MOUSE SUPPORT
If the
@@ -7012,6 +7038,20 @@ escape
command arguments; with
.Ql s
use single quotes.
.Pp
Some variables such as
.Ic pane_title
and
.Ic pane_path
are set by escape sequences received from pane and may contain arbitrary
content including shell metacharacters.
.Ql q:\&
should be used to escape these with commands passed to
.Ic run\-shell ,
.Ic if\-shell ,
or
.Ic pipe\-pane .
.Pp
.Ql E:\&
will expand the format twice, for example
.Ql #{E:status\-left}
@@ -7176,6 +7216,26 @@ For example
will be replaced by
.Ql #{?pane_in_mode,yes,no} .
.Pp
The
.Ql A
modifier shows one of a list of frames in turn:
.Ql #{A/count:frame,frame,...}
or
.Ql #{A:frame,frame,...} .
Each frame is shown for
.Ar count
periods of 100 milliseconds before the next, wrapping back to the first after
the last.
If no count is given, one is used.
The
.Ql A
modifier is expanded only in the status line and
.Ic pane\-border\-format ;
if present, the status line or pane borders are redrawn every 100 milliseconds.
For example,
.Ql #{A/2:|,/,\-,\e}
shows the four frames in turn, changing every 200 milliseconds.
.Pp
The following variables are available, where appropriate:
.Bl -column "XXXXXXXXXXXXXXXXXXX" "XXXXX"
.It Sy "Variable name" Ta Sy "Alias" Ta Sy "Replaced with"
@@ -7241,7 +7301,11 @@ 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_fire_count" Ta "" Ta "Number of times hook has fired"
.It Li "hook_fire_time" Ta "" Ta "Time hook last fired"
.It Li "hook_last" Ta "" Ta "Previous value for a monitor hook"
.It Li "hook_monitor_format" Ta "" Ta "Format for a monitor hook"
.It Li "hook_monitor_target" Ta "" Ta "Target 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"
@@ -7283,6 +7347,17 @@ The following variables are available, where appropriate:
.It Li "next_session_id" Ta "" Ta "Unique session ID for next new session"
.It Li "next_window_active" Ta "" Ta "1 if next window in W: loop is active"
.It Li "next_window_index" Ta "" Ta "Index of next window in W: loop"
.It Li "option_array_key" Ta "" Ta "Array key, if option is an array item"
.It Li "option_has_array_key" Ta "" Ta "1 if option array key is present"
.It Li "option_has_value" Ta "" Ta "1 if option value is present"
.It Li "option_is_array" Ta "" Ta "1 if option is an array"
.It Li "option_is_hook" Ta "" Ta "1 if option is a hook"
.It Li "option_is_parent" Ta "" Ta "1 if option is inherited"
.It Li "option_is_string" Ta "" Ta "1 if option is a string"
.It Li "option_is_user" Ta "" Ta "1 if option is a user option"
.It Li "option_name" Ta "" Ta "Name of option"
.It Li "option_value" Ta "" Ta "Value of option"
.It Li "option_value_only" Ta "" Ta "1 if only option values will be shown"
.It Li "origin_flag" Ta "" Ta "Pane origin flag"
.It Li "pane_active" Ta "" Ta "1 if active pane"
.It Li "pane_at_bottom" Ta "" Ta "1 if pane is at the bottom of window"

23
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1411 2026/07/20 11:16:33 nicm Exp $ */
/* $OpenBSD: tmux.h,v 1.1416 2026/07/27 19:15:58 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1335,6 +1335,7 @@ struct window_pane {
#define PANE_DESTROYED 0x10000
#define PANE_CMDRUNNING 0x20000
#define PANE_ACTIVITY 0x40000
#define PANE_CLOSEONCLICK 0x80000
bitstr_t *sync_dirty;
u_int sync_dirty_size;
@@ -1474,7 +1475,8 @@ struct window {
int sb;
int sb_pos;
struct utf8_data *fill_character;
struct grid_cell inside_cell;
struct grid_cell outside_cell;
int flags;
#define WINDOW_BELL 0x1
#define WINDOW_ACTIVITY 0x2
@@ -2219,6 +2221,7 @@ struct client {
struct mouse_event click_event;
struct status_line status;
struct event cycle_timer;
enum client_theme theme;
struct input_requests input_requests;
@@ -2757,6 +2760,10 @@ void hooks_monitor_add(struct cmdq_item *, struct options *,
void hooks_monitor_remove(struct options *, const char *);
void hooks_monitor_free(void *);
char *hooks_monitor_to_string(struct options_entry *);
int hooks_monitor_get(struct options_entry *, enum monitor_type *, int *,
const char **);
u_int hooks_monitor_get_fire_count(struct options_entry *);
time_t hooks_monitor_get_fire_time(struct options_entry *);
/* options.c */
struct options *options_create(struct options *);
@@ -2774,6 +2781,9 @@ 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 *);
void options_hook_fired(struct options_entry *);
u_int options_get_fire_count(struct options_entry *);
time_t options_get_fire_time(struct options_entry *);
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 *);
@@ -3729,7 +3739,6 @@ void *window_pane_get_new_data(struct window_pane *,
struct window_pane_offset *, size_t *);
void window_pane_update_used_data(struct window_pane *,
struct window_pane_offset *, size_t);
void window_set_fill_character(struct window *);
void window_pane_default_cursor(struct window_pane *);
int window_pane_mode(struct window_pane *);
int window_pane_show_scrollbar(struct window_pane *);
@@ -3757,8 +3766,10 @@ struct style_range *window_pane_status_get_range(struct window_pane *, u_int,
int window_pane_is_floating(struct window_pane *);
/* window-border.c */
void window_get_border_cell(struct window *, struct window_pane *,
enum pane_lines, int, struct grid_cell *);
void window_set_fill_cells(struct window *);
void window_get_border_cell(struct window_pane *, enum pane_lines,
int, struct grid_cell *);
void window_get_fill_cell(struct window *, int, struct grid_cell *);
void window_pane_get_border_cell(struct window_pane *, int,
struct grid_cell *);
void window_pane_get_border_style(struct window_pane *,
@@ -3952,6 +3963,8 @@ int monitor_parse(const char *, char **, enum monitor_type *, int *,
void monitor_add(struct monitor_set *, const char *, enum monitor_type, int,
const char *, int);
void monitor_remove(struct monitor_set *, const char *);
u_int monitor_get_fire_count(struct monitor_set *, const char *);
time_t monitor_get_fire_time(struct monitor_set *, const char *);
/* control.c */
void control_discard(struct client *);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-draw.c,v 1.14 2026/06/19 10:38:29 nicm Exp $ */
/* $OpenBSD: tty-draw.c,v 1.15 2026/07/26 09:02:08 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -100,6 +100,8 @@ tty_draw_line_get_empty(const struct grid_cell *gc,
empty = nx;
else if (gc->flags & GRID_FLAG_PADDING)
empty = 1;
else if (gc->data.width == 0)
empty = 1;
else if (gc->flags & GRID_FLAG_SELECTED)
empty = 0;
else if (gc->bg == last->bg && gc->attr == 0 && gc->link == 0) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-keys.c,v 1.210 2026/07/10 13:38:45 nicm Exp $ */
/* $OpenBSD: tty-keys.c,v 1.211 2026/07/21 07:12:49 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -977,8 +977,9 @@ partial_key:
if (delay < 500)
delay = 500;
}
if ((tty->flags & (TTY_WAITFG|TTY_WAITBG) ||
(tty->flags & TTY_ALL_REQUEST_FLAGS) != TTY_ALL_REQUEST_FLAGS) ||
if (tty->flags & (TTY_WAITFG|TTY_WAITBG) ||
tty->flags & (TTY_OSC52QUERY|TTY_WINSIZEQUERY) ||
(tty->flags & TTY_ALL_REQUEST_FLAGS) != TTY_ALL_REQUEST_FLAGS ||
!TAILQ_EMPTY(&c->input_requests)) {
log_debug("%s: increasing delay (active query)", c->name);
if (delay < 500)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-border.c,v 1.2 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: window-border.c,v 1.3 2026/07/23 09:38:27 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -23,18 +23,82 @@
#include "tmux.h"
/* Set window fill cell. */
static void
window_set_fill_cell(struct window *w, int inside, struct grid_cell *gc)
{
struct format_tree *ft;
struct screen s;
struct screen_write_ctx ctx;
struct grid_cell new_gc;
const char *value;
char *expanded;
memcpy(gc, &grid_default_cell, sizeof *gc);
gc->attr |= GRID_ATTR_CHARSET;
utf8_set(&gc->data, CELL_BORDERS[CELL_NONE]);
ft = format_create(NULL, NULL, FORMAT_WINDOW|w->id, FORMAT_NOJOBS);
format_defaults(ft, NULL, NULL, NULL, w->active);
format_add(ft, "is_inside", "%d", inside);
format_add(ft, "is_outside", "%d", !inside);
value = options_get_string(w->options, "fill-character");
expanded = format_expand(ft, value);
format_free(ft);
screen_init(&s, 1, 1, 0);
screen_write_start(&ctx, &s);
format_draw(&ctx, &grid_default_cell, 1, expanded, NULL, 0);
screen_write_stop(&ctx);
free(expanded);
grid_view_get_cell(s.grid, 0, 0, &new_gc);
if (new_gc.data.width == 1)
memcpy(gc, &new_gc, sizeof *gc);
screen_free(&s);
}
/* Set window fill cells. */
void
window_set_fill_cells(struct window *w)
{
window_set_fill_cell(w, 1, &w->inside_cell);
window_set_fill_cell(w, 0, &w->outside_cell);
}
/* Merge a window fill cell over an existing style. */
static void
window_copy_fill_cell(struct grid_cell *gc, const struct grid_cell *fill)
{
utf8_copy(&gc->data, &fill->data);
gc->attr |= fill->attr;
gc->flags |= fill->flags;
if (fill->fg != 8)
gc->fg = fill->fg;
if (fill->bg != 8)
gc->bg = fill->bg;
if (fill->us != 8)
gc->us = fill->us;
}
/* Get window fill cell. */
void
window_get_fill_cell(struct window *w, int inside, struct grid_cell *gc)
{
if (inside)
window_copy_fill_cell(gc, &w->inside_cell);
else
window_copy_fill_cell(gc, &w->outside_cell);
}
/* Get border cell. */
void
window_get_border_cell(struct window *w, struct window_pane *wp,
enum pane_lines pane_lines, int cell_type, struct grid_cell *gc)
window_get_border_cell(struct window_pane *wp, enum pane_lines pane_lines,
int cell_type, struct grid_cell *gc)
{
u_int idx;
if (cell_type == CELL_NONE && w->fill_character != NULL) {
utf8_copy(&gc->data, &w->fill_character[0]);
return;
}
switch (pane_lines) {
case PANE_LINES_NUMBER:
if (cell_type == CELL_NONE) {
@@ -79,7 +143,7 @@ window_pane_get_border_cell(struct window_pane *wp, int cell_type,
{
enum pane_lines pane_lines = window_pane_get_pane_lines(wp);
window_get_border_cell(wp->window, wp, pane_lines, cell_type, gc);
window_get_border_cell(wp, pane_lines, cell_type, gc);
}
/* Get pane border style. */
@@ -147,7 +211,7 @@ window_make_pane_status(struct window_pane *wp, struct client *c, u_int width,
pane_lines = window_pane_get_pane_lines(wp);
for (i = 0; i < width; i++) {
cell_type = redraw_get_status_border_cell_type(&span, i);
window_get_border_cell(wp->window, wp, pane_lines, cell_type, &gc);
window_get_border_cell(wp, pane_lines, cell_type, &gc);
screen_write_cell(&ctx, &gc);
}
gc.attr &= ~GRID_ATTR_CHARSET;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-copy.c,v 1.422 2026/07/20 11:16:33 nicm Exp $ */
/* $OpenBSD: window-copy.c,v 1.423 2026/07/21 11:52:13 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -62,6 +62,7 @@ static void window_copy_set_line_numbers1(struct window_mode_entry *, int,
static int window_copy_line_number_mode(struct window_mode_entry *);
static int window_copy_line_number_is_absolute(struct window_mode_entry *);
static int window_copy_line_numbers_active(struct window_mode_entry *);
static int window_copy_cursor_line_active(struct window_mode_entry *);
static u_int window_copy_line_number_width(struct window_mode_entry *);
static u_int window_copy_cursor_offset(struct window_mode_entry *, u_int,
u_int);
@@ -5027,7 +5028,8 @@ window_copy_match_at_cursor(struct window_copy_mode_data *data)
static void
window_copy_update_style(struct window_mode_entry *wme, u_int fx, u_int fy,
struct grid_cell *gc, const struct grid_cell *mgc,
const struct grid_cell *cgc, const struct grid_cell *mkgc)
const struct grid_cell *cgc, const struct grid_cell *mkgc,
const struct grid_cell *clgc)
{
struct window_pane *wp = wme->wp;
struct window_copy_mode_data *data = wme->data;
@@ -5035,6 +5037,15 @@ window_copy_update_style(struct window_mode_entry *wme, u_int fx, u_int fy,
int inv = 0, found = 0;
int keys;
cy = screen_hsize(data->backing) - data->oy + data->cy;
if (fy == cy) {
if (clgc->fg != 8)
gc->fg = clgc->fg;
if (clgc->bg != 8)
gc->bg = clgc->bg;
gc->attr |= clgc->attr;
}
if (data->showmark && fy == data->my) {
gc->attr = mkgc->attr;
if (fx == data->mx)
@@ -5058,7 +5069,6 @@ window_copy_update_style(struct window_mode_entry *wme, u_int fx, u_int fy,
if (mark == 0)
return;
cy = screen_hsize(data->backing) - data->oy + data->cy;
if (window_copy_search_mark_at(data, data->cx, cy, &cursor) == 0) {
keys = options_get_number(wp->window->options, "mode-keys");
if (cursor != 0 &&
@@ -5102,7 +5112,7 @@ static void
window_copy_write_one(struct window_mode_entry *wme,
struct screen_write_ctx *ctx, u_int px, u_int py, u_int fy, u_int nx,
const struct grid_cell *mgc, const struct grid_cell *cgc,
const struct grid_cell *mkgc)
const struct grid_cell *mkgc, const struct grid_cell *clgc)
{
struct window_copy_mode_data *data = wme->data;
struct grid *gd = data->backing->grid;
@@ -5114,7 +5124,7 @@ window_copy_write_one(struct window_mode_entry *wme,
grid_get_cell(gd, fx, fy, &gc);
if (fx + gc.data.width <= nx) {
window_copy_update_style(wme, fx, fy, &gc, mgc, cgc,
mkgc);
mkgc, clgc);
if (gc.flags & GRID_FLAG_PADDING) {
if (ctx->s->cy == py && ctx->s->cx <= px + fx) {
gc.flags &= ~GRID_FLAG_PADDING;
@@ -5172,6 +5182,16 @@ window_copy_line_numbers_active(struct window_mode_entry *wme)
WINDOW_COPY_LINE_NUMBERS_OFF);
}
static int
window_copy_cursor_line_active(struct window_mode_entry *wme)
{
struct options *oo = wme->wp->window->options;
const char *s;
s = options_get_string(oo, "copy-mode-current-line-style");
return (strcmp(s, "default") != 0);
}
static u_int
window_copy_line_number_width(struct window_mode_entry *wme)
{
@@ -5288,7 +5308,8 @@ window_copy_write_line(struct window_mode_entry *wme,
struct window_copy_mode_data *data = wme->data;
struct screen *s = &data->screen;
struct options *oo = wp->window->options;
struct grid_cell gc, mgc, cgc, mkgc, ln_gc, cur_ln_gc;
struct grid_cell gc, mgc, cgc, mkgc, clgc, ln_gc;
struct grid_cell cur_ln_gc;
u_int sx = screen_size_x(s);
u_int hsize = screen_hsize(data->backing);
u_int width;
@@ -5318,6 +5339,8 @@ window_copy_write_line(struct window_mode_entry *wme,
cgc.flags |= GRID_FLAG_NOPALETTE;
style_apply(&mkgc, oo, "copy-mode-mark-style", ft);
mkgc.flags |= GRID_FLAG_NOPALETTE;
style_apply(&clgc, oo, "copy-mode-current-line-style", ft);
clgc.flags |= GRID_FLAG_NOPALETTE;
if (width != 0) {
style_apply(&ln_gc, oo, "copy-mode-line-number-style", ft);
ln_gc.flags |= GRID_FLAG_NOPALETTE;
@@ -5346,7 +5369,7 @@ window_copy_write_line(struct window_mode_entry *wme,
}
window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py,
content_sx, &mgc, &cgc, &mkgc);
content_sx, &mgc, &cgc, &mkgc, &clgc);
if (py == 0 && s->rupper < s->rlower && !data->hide_position) {
value = options_get_string(oo, "copy-mode-position-format");
@@ -5598,6 +5621,11 @@ window_copy_update_cursor(struct window_mode_entry *wme, u_int cx, u_int cy)
screen_write_stop(&ctx);
return;
}
if (old_cy != data->cy && window_copy_cursor_line_active(wme)) {
window_copy_redraw_lines(wme, old_cy, 1);
window_copy_redraw_lines(wme, data->cy, 1);
return;
}
if (old_cx == screen_size_x(s))
window_copy_redraw_lines(wme, old_cy, 1);
if (data->cx == screen_size_x(s))
@@ -6799,6 +6827,10 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny)
if (data->searchmark != NULL && !data->timeout)
window_copy_search_marks(wme, NULL, data->searchregex, 1);
window_copy_update_selection_view(wme, 0, 0);
if (window_copy_cursor_line_active(wme)) {
window_copy_redraw_screen(wme);
return;
}
if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
@@ -6866,6 +6898,10 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny)
if (data->searchmark != NULL && !data->timeout)
window_copy_search_marks(wme, NULL, data->searchregex, 1);
window_copy_update_selection_view(wme, 0, 0);
if (window_copy_cursor_line_active(wme)) {
window_copy_redraw_screen(wme);
return;
}
if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-customize.c,v 1.35 2026/07/15 12:45:39 nicm Exp $ */
/* $OpenBSD: window-customize.c,v 1.36 2026/07/28 10:35:31 nicm Exp $ */
/*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -276,6 +276,36 @@ window_customize_scope_text(enum window_customize_scope scope,
return (s);
}
static int
window_customize_write_hook_fire(struct screen_write_ctx *ctx, u_int cx,
u_int sx, u_int sy, struct options_entry *o)
{
char *fire_time_string;
u_int fire_count;
time_t fire_time;
if (options_get_monitor_data(o) != NULL) {
fire_count = hooks_monitor_get_fire_count(o);
fire_time = hooks_monitor_get_fire_time(o);
} else {
fire_count = options_get_fire_count(o);
fire_time = options_get_fire_time(o);
}
if (fire_time != 0) {
fire_time_string = format_pretty_time(fire_time, 0);
if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell,
"This hook has been fired %u times, last %s.", fire_count,
fire_time_string)) {
free(fire_time_string);
return (0);
}
free(fire_time_string);
return (1);
}
return (screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell,
"This hook has been fired %u times.", fire_count));
}
static struct window_customize_itemdata *
window_customize_add_item(struct window_customize_modedata *data)
{
@@ -692,7 +722,7 @@ window_customize_build_option(struct window_customize_modedata *data,
char *text, *expanded, *value;
int global = 0, array = 0;
int is_hook = 0, is_monitor = 0;
int is_user_hook = 0;
int is_user_hook = 0, is_any_hook;
uint64_t tag;
if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK))
@@ -701,13 +731,14 @@ window_customize_build_option(struct window_customize_modedata *data,
is_monitor = 1;
if (*name == '@' && hooks_is_event(name))
is_user_hook = 1;
is_any_hook = (is_hook || is_monitor || is_user_hook);
switch (type) {
case WINDOW_CUSTOMIZE_OPTIONS:
if (is_hook || is_monitor || is_user_hook)
if (is_any_hook)
return (0);
break;
case WINDOW_CUSTOMIZE_HOOKS:
if (!is_hook && !is_monitor && !is_user_hook)
if (!is_any_hook)
return (0);
break;
}
@@ -1227,6 +1258,7 @@ window_customize_draw_option(struct window_customize_modedata *data,
struct cmd_find_state fs;
struct format_tree *ft;
int is_hook, is_monitor, is_user_hook;
int is_any_hook;
if (!window_customize_check_item(data, item, &fs))
return;
@@ -1240,6 +1272,7 @@ window_customize_draw_option(struct window_customize_modedata *data,
is_hook = (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK));
is_monitor = (options_get_monitor_data(o) != NULL);
is_user_hook = (*name == '@' && hooks_is_event(name));
is_any_hook = (is_hook || is_monitor || is_user_hook);
if (oe != NULL && oe->unit != NULL) {
space = " ";
@@ -1306,8 +1339,16 @@ monitor:
}
if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) {
if (is_hook) {
if (array_key == NULL)
if (array_key == NULL) {
if (!screen_write_text(ctx, cx, sx,
sy - (s->cy - cy), 0, &grid_default_cell,
"This is an array hook."))
goto out;
if (!window_customize_write_hook_fire(ctx, cx,
sx, sy - (s->cy - cy), o))
goto out;
goto out;
}
} else if (array_key != NULL) {
if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy),
0, &grid_default_cell,
@@ -1333,10 +1374,13 @@ monitor:
default_value = NULL;
}
}
if (is_hook || is_monitor || is_user_hook) {
if (is_any_hook) {
if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy),
0, "Hook command: ", "%s%s%s", value, space, unit))
goto out;
if (!window_customize_write_hook_fire(ctx, cx, sx,
sy - (s->cy - cy), o))
goto out;
} else {
if (!window_customize_write_value(ctx, cx, sx, sy - (s->cy - cy),
0, "Option value: ", "%s%s%s", value, space, unit))

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window.c,v 1.366 2026/07/19 19:53:11 nicm Exp $ */
/* $OpenBSD: window.c,v 1.368 2026/07/23 09:38:27 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -437,8 +437,6 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel)
w->id = next_window_id++;
RB_INSERT(windows, &windows, w);
window_set_fill_character(w);
if (gettimeofday(&w->creation_time, NULL) != 0)
fatal("gettimeofday failed");
window_update_activity(w);
@@ -472,7 +470,6 @@ window_destroy(struct window *w)
event_del(&w->offset_timer);
options_free(w->options);
free(w->fill_character);
free(w->name);
free(w);
@@ -571,13 +568,13 @@ window_resize(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
w->sy = sy;
if (w->menu != NULL) {
menu_resize(w->menu, w);
redraw_invalidate_scene(w);
server_redraw_window(w);
}
if (xpixel != -1)
w->xpixel = xpixel;
if (ypixel != -1)
w->ypixel = ypixel;
redraw_invalidate_scene(w);
}
void
@@ -2442,25 +2439,6 @@ window_pane_update_used_data(struct window_pane *wp,
wpo->used += size;
}
void
window_set_fill_character(struct window *w)
{
const char *value;
struct utf8_data *ud;
free(w->fill_character);
w->fill_character = NULL;
value = options_get_string(w->options, "fill-character");
if (*value != '\0' && utf8_isvalid(value)) {
ud = utf8_fromcstr(value);
if (ud != NULL && ud[0].width == 1)
w->fill_character = ud;
else
free(ud);
}
}
void
window_pane_default_cursor(struct window_pane *wp)
{