Merge branch 'master' into events

This commit is contained in:
Nicholas Marriott
2026-07-08 11:29:17 +01:00
13 changed files with 419 additions and 21 deletions

View File

@@ -30,8 +30,8 @@ const struct cmd_entry cmd_copy_mode_entry = {
.name = "copy-mode",
.alias = NULL,
.args = { "deHMqSs:t:u", 0, 0, NULL },
.usage = "[-deHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE,
.args = { "dekHMqSs:t:u", 0, 0, NULL },
.usage = "[-dekHMqSu] [-s src-pane] " CMD_TARGET_PANE_USAGE,
.source = { 's', CMD_FIND_PANE, 0 },
.target = { 't', CMD_FIND_PANE, 0 },

View File

@@ -141,7 +141,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
sc.cwd = args_get(args, 'c');
sc.flags = 0;
if (args_has(args, 'E'))
if (args_has(args, 'E') || (count == 1 && *args_string(args, 0) == '\0'))
sc.flags |= SPAWN_EMPTY;
if (args_has(args, 'd'))
sc.flags |= SPAWN_DETACHED;

View File

@@ -105,7 +105,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
flags |= SPAWN_FULLSIZE;
input = args_has(args, 'I');
if (input)
if (input || (count == 1 && *args_string(args, 0) == '\0'))
empty = 1;
else
empty = args_has(args, 'E');

View File

@@ -1310,7 +1310,7 @@ layout_split_sizes(struct layout_cell *lc, int size, int before,
s2 = size;
if (s2 < PANE_MINIMUM)
s2 = PANE_MINIMUM;
else if (s2 > sx - 2)
else if (s2 > ss - 2)
s2 = ss - 2;
s1 = ss - 1 - s2;

View File

@@ -101,6 +101,8 @@ $TMUX set -g @ts '1000000000' || exit 1 # 2001-09-09 01:46:40 UTC
$TMUX set -g @sp 'a b$c' || exit 1 # shell-special characters for q:
$TMUX set -g @hash 'a#b' || exit 1 # a "#" for q/e:
$TMUX set -g @sq "a'b" || exit 1 # a single quote for q/s:
$TMUX set -g @sub 'abABab' || exit 1
$TMUX set -g @slash 'foo/bar foo/' || exit 1
$TMUX set -g @nl "$(printf 'a\nb')" || exit 1
q_s_nl=$(printf "'a\nb'")
@@ -426,9 +428,23 @@ test_format "#{=/3/#,:@s}" "abc," # escaped comma in the marker
# The truncation marker is itself expanded as a format.
test_format "#{=/3/#{l:>}:@s}" "abc>"
# Substitution flags: a third argument of "i" is case-insensitive; an invalid
# regular expression leaves the text unchanged.
# Substitution, including regular expressions, back references, different
# delimiters, empty matches and the "i" case-insensitive flag.
test_format "#{s/z/X/:@s}" "abcdefghij"
test_format "#{s/[bd]/X/:@s}" "aXcXefghij"
test_format "#{s/A/X/i:@s}" "Xbcdefghij"
test_format "#{s/a(.)/\\1x/i:@sub}" "bxBxbx"
test_format "#{s/(.)(.)/\\2\\1/:@s}" "badcfehgji"
test_format "#{s|foo/|bar/|:@slash}" "bar/bar bar/"
test_format "#{s/^abc/ABC/:@s}" "ABCdefghij"
test_format "#{s/^(.)(.)/\\2\\1/:@s}" "bacdefghij"
test_format "#{s/^x*//:@s}" "abcdefghij"
test_format "#{s/^/X/:@s}" "Xabcdefghij"
test_format "#{s/^x*/X/:@s}" "Xabcdefghij"
test_format "#{s/$/X/:@s}" "abcdefghijX"
test_format "#{s/x*//:@s}" "abcdefghij"
test_format "#{s/x*/X/:@s}" "aXbXcXdXeXfXgXhXiXjX"
# An invalid regular expression leaves the text unchanged.
test_format "#{s/[/X/:@s}" "abcdefghij"

171
regress/mode-kill.sh Executable file
View File

@@ -0,0 +1,171 @@
#!/bin/sh
# Tests of the -k flag on the mode-entering commands (cmd-copy-mode.c and
# cmd-choose-tree.c). With -k the pane is killed when the mode is exited: this
# is stored on the mode entry in window_pane_set_mode() and acted on in
# window_pane_reset_mode() (window.c). It is exercised here for:
#
# - copy-mode -k (window-copy.c);
# - choose-tree -k (window-tree.c);
# - choose-buffer -k (window-buffer.c).
#
# choose-tree and choose-buffer share cmd_choose_tree_exec(), which also backs
# choose-client and customize-mode, so those are not repeated.
#
# Each mode is entered in the active pane of a two-pane window: exiting with
# -k must remove that pane and leave the other. copy-mode is left with the
# server-side "-X cancel" and needs no client, so those tests run first. The
# tree modes only act on a key once the client has drawn the mode, so - as in
# choose-tree.sh - a second server then provides a client (an inner "tmux
# attach") and its pane is captured to wait until the mode has rendered before
# the exit key is sent.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMP=$(mktemp -d) || exit 1
TMUX_TMPDIR="$TMP"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -rf "$TMP"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
exit 1
}
# capture the screen rendered by the inner client.
capture()
{
$TMUX2 capture-pane -p -t out:0 2>/dev/null
}
# wait_clients $n: wait until the test server has exactly $n clients.
wait_clients()
{
i=0
while [ "$i" -lt 50 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "expected $1 clients, have $c"
}
# wait_mode $target $state: wait until a pane enters (1) or leaves (0) mode.
wait_mode()
{
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t "$1" '#{pane_in_mode}' \
2>/dev/null)
[ "$got" = "$2" ] && return 0
sleep 0.2
i=$((i + 1))
done
fail "pane $1 mode state is '$got', expected '$2'"
}
# wait_for $marker: wait until the rendered screen contains $marker.
wait_for()
{
i=0
while [ "$i" -lt 50 ]; do
capture | grep -F -q "$1" && return 0
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1' to render"
}
# pane_gone $pane-id: true if the pane no longer exists. (display-message -t
# on a missing pane id falls back to a default target and succeeds, so the
# pane list is searched instead.)
pane_gone()
{
! $TMUX list-panes -s -t m -F '#{pane_id}' 2>/dev/null | \
grep -q -x "$1"
}
# open_window: make a fresh two-pane window and leave the new (active) pane
# as $active and the other as $other.
open_window()
{
$TMUX new-window -t m: -n w 'cat' || fail "new-window failed"
$TMUX split-window -t m:w 'cat' || fail "split-window failed"
other=$($TMUX display-message -p -t m:w.0 '#{pane_id}')
active=$($TMUX display-message -p -t m:w '#{pane_id}')
}
# check_killed $label: wait for the active pane to be killed, leaving only the
# other pane, then drop the window.
check_killed()
{
i=0
while [ "$i" -lt 50 ]; do
pane_gone "$active" && break
sleep 0.2
i=$((i + 1))
done
pane_gone "$active" || fail "$1: pane not killed on exit"
panes=$($TMUX list-panes -t m:w -F '#{pane_id}' | tr '\n' ' ')
[ "$panes" = "$other " ] || \
fail "$1: expected only $other left, have $panes"
$TMUX kill-window -t m:w 2>/dev/null
}
# Session m; window 0 keeps a live pane so the session (and later the client)
# survives each test killing a pane.
$TMUX new-session -d -s m -x 80 -y 24 'cat' || exit 1
# --- copy-mode -k kills the pane, plain copy-mode does not -------------------
#
# copy-mode is exited with the server-side "-X cancel", so no client is needed
# and none is attached yet.
open_window
$TMUX copy-mode -k -t m:w || fail "copy-mode -k failed"
wait_mode "$active" 1
$TMUX send-keys -t m:w -X cancel || fail "copy cancel failed"
check_killed 'copy-mode -k'
open_window
$TMUX copy-mode -t m:w || fail "copy-mode failed"
wait_mode "$active" 1
$TMUX send-keys -t m:w -X cancel || fail "copy cancel failed"
wait_mode "$active" 0
pane_gone "$active" && fail 'copy-mode: pane killed without -k'
$TMUX kill-window -t m:w 2>/dev/null
# --- choose-tree -k and choose-buffer -k kill the pane ----------------------
#
# These need the client to draw the mode before a key acts, so attach one now.
# A paste buffer is needed for choose-buffer to have something to show, and a
# distinct -F marker per call is waited for in the capture so the exit key is
# only sent once the mode is drawn.
$TMUX set-buffer 'mode-kill buffer' || exit 1
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t m" || exit 1
wait_clients 1
open_window
$TMUX choose-tree -k -F 'TREEMARK' -t m:w || fail "choose-tree -k failed"
wait_for 'TREEMARK'
$TMUX send-keys -t m:w q || fail "choose-tree exit failed"
check_killed 'choose-tree -k'
open_window
$TMUX choose-buffer -k -F 'BUFMARK' -t m:w || fail "choose-buffer -k failed"
wait_for 'BUFMARK'
$TMUX send-keys -t m:w q || fail "choose-buffer exit failed"
check_killed 'choose-buffer -k'
exit 0

View File

@@ -399,6 +399,19 @@ check_fmt 'P:9.0' '#{pane_pid}' ''
check_fail 'command cannot be given for empty pane' \
new-window -d -E -t P:10 -n empty 'true'
# An empty string as the sole argument is equivalent to -E: the pane is
# created empty, running no command.
check_ok new-window -d -t P:12 -n empty-str ''
check_fmt 'P:12.0' '#{pane_dead}' '0'
check_fmt 'P:12.0' '#{pane_pid}' ''
check_ok kill-window -t P:12
# A missing command (rather than an empty one) runs the default command, so
# the pane is not empty and has a process.
check_ok new-window -d -t P:12 -n default-cmd
check_fmt 'P:12.0' '#{?pane_pid,live,empty}' 'live'
check_ok kill-window -t P:12
# respawn-pane -E stores the command and cwd without starting it.
tmp=${TMPDIR:-/tmp}/tmux-pane-ops-empty-$$
rm -f "$tmp"
@@ -453,6 +466,13 @@ check_fmt 'P:2' '#{window_panes}' '2'
check_fail 'command cannot be given for empty pane' \
split-window -d -E -t P:2.0 'sleep 5'
# An empty string as the sole argument splits with an empty pane, like -E.
eid=$($TMUX split-window -d -P -F '#{pane_id}' -t P:2.0 '')
check_fmt "$eid" '#{pane_dead}' '0'
check_fmt "$eid" '#{pane_pid}' ''
check_ok kill-pane -t "$eid"
check_fmt 'P:2' '#{window_panes}' '2'
# -e adds to the new pane's environment.
eid=$($TMUX split-window -d -P -F '#{pane_id}' -e GREETING=hello -t P:2.0 \
'echo $GREETING; exec cat')

View File

@@ -105,6 +105,104 @@ check_windows()
fi
}
# wait_client_session $expected
#
# Wait for the only test client to be attached to $expected.
wait_client_session()
{
expected=$1
i=0
while [ $i -lt 30 ]; do
out=$($TMUX list-clients -F '#{client_session}' 2>/dev/null ||
true)
if [ "$out" = "$expected" ]; then
return
fi
i=$((i + 1))
sleep 0.1
done
echo "Client session wrong."
echo "Expected: '$expected'"
echo "But got: '$out'"
exit 1
}
# attach_control_client $session
#
# Attach a control client to $session and keep its input open on fd 9.
attach_control_client()
{
fifo=$(mktemp -u)
mkfifo "$fifo" || exit 1
$TMUX -C attach-session -t "$1" <"$fifo" >/dev/null 2>&1 &
control_pid=$!
exec 9>"$fifo"
rm -f "$fifo"
wait_client_session "$1"
}
# check_attached_destroy $mode
#
# Run an attached tmux inside a pane and destroy its only session.
check_attached_destroy()
{
mode=$1
outer=DODouter$mode
outdir=$(mktemp -d) || exit 1
script=$outdir/inner.sh
rcfile=$outdir/rc
cat >"$script" <<-EOF
#!/bin/sh
"$TEST_TMUX" -LtestInner$$-$mode -f/dev/null new \\; \
set -g detach-on-destroy $mode \\; send exit Enter
printf '%s\n' \$? >"$rcfile"
EOF
chmod +x "$script" || exit 1
check_ok new-session -d -s "$outer" -x 80 -y 24
check_ok send-keys -t "$outer:0.0" "sh $script" Enter
i=0
while [ $i -lt 50 ]; do
[ -f "$rcfile" ] && break
i=$((i + 1))
sleep 0.1
done
pane=$($TMUX capture-pane -pt "$outer:0.0" -S -)
if [ ! -f "$rcfile" ]; then
echo "Inner tmux did not exit."
echo "$pane"
$TMUX kill-session -t "$outer" 2>/dev/null || true
rm -rf "$outdir"
exit 1
fi
rc=$(cat "$rcfile")
if [ "$rc" != 0 ]; then
echo "Inner tmux exited with status $rc."
echo "$pane"
$TMUX kill-session -t "$outer" 2>/dev/null || true
rm -rf "$outdir"
exit 1
fi
case "$pane" in
*"server exited unexpectedly"*)
echo "Inner tmux server crashed."
echo "$pane"
$TMUX kill-session -t "$outer" 2>/dev/null || true
rm -rf "$outdir"
exit 1
;;
esac
check_ok kill-session -t "$outer"
rm -rf "$outdir"
}
# ---------------------------------------------------------------------------
# new-session and has-session.
@@ -203,6 +301,38 @@ check_fail '-f only valid with -a' kill-session -f 'x' -t S1
check_ok kill-session -C -t S2
check_ok has-session -t S2
# detach-on-destroy previous and next move attached clients in
# alphabetical order and must not crash when a session is destroyed.
check_ok new-session -d -s DODa -x 80 -y 24
check_ok new-session -d -s DODb -x 80 -y 24
check_ok new-session -d -s DODc -x 80 -y 24
check_ok set-option -t DODb detach-on-destroy previous
attach_control_client DODb
check_ok kill-session -t DODb
wait_client_session DODa
exec 9>&-
wait "$control_pid" 2>/dev/null || true
check_ok kill-session -t DODa
check_ok kill-session -t DODc
check_ok new-session -d -s DODa -x 80 -y 24
check_ok new-session -d -s DODb -x 80 -y 24
check_ok new-session -d -s DODc -x 80 -y 24
check_ok set-option -t DODb detach-on-destroy next
attach_control_client DODb
check_ok kill-session -t DODb
wait_client_session DODc
exec 9>&-
wait "$control_pid" 2>/dev/null || true
check_ok kill-session -t DODa
check_ok kill-session -t DODc
# With only one session, previous and next have no replacement session; the
# attached client must exit cleanly rather than being moved to the dying
# session.
check_attached_destroy previous
check_attached_destroy next
# -a kills every other session.
check_ok kill-session -a -t S1
check_sessions 'S1'

57
regress/theme-report.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/bin/sh
# Test that the theme reported to a pane (mode 2031 / DSR 996) follows the
# terminal's reported theme, not a guess from the background colour.
#
# An inner client is attached inside an outer tmux pane. The outer pane has a
# light background, so the outer server answers the inner client's DSR 996 with
# light. The inner server is given a dark window background, so a guess from the
# background colour would say dark. A pane in the inner server then queries DSR
# 996: the answer must be light (2), following the client's reported theme, not
# dark (1) from the background.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
$INNER kill-server 2>/dev/null
$OUTER kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP; $INNER kill-server 2>/dev/null; $OUTER kill-server 2>/dev/null" \
0 1 15
# Inner server, dark background, keep panes alive across the query respawn.
$INNER new-session -d -x80 -y24 || exit 1
$INNER set -g remain-on-exit on
$INNER set -g window-style 'bg=black'
# Outer server with a light background, running the inner client attached so the
# inner client has a real terminal that reports a theme.
$OUTER new-session -d -x80 -y24 || exit 1
$OUTER set -g window-style 'bg=white'
$OUTER new-window "$INNER attach" || exit 1
sleep 2
# Query DSR 996 from an inner pane and capture the CSI ? 997 ; Ps n reply.
$INNER respawnw -k -t:0 -- sh -c "
exec 2>/dev/null
stty raw -echo
printf '\033[?996n'
dd bs=1 count=9 2>/dev/null | cat -v > $TMP
sleep 1
"
sleep 2
actual=$(cat "$TMP")
expected='^[[?997;2n' # 2 = light (from the terminal), not 1 = dark (from bg)
if [ "$actual" = "$expected" ]; then
[ -n "$VERBOSE" ] && echo "[PASS] reported terminal theme ($actual)"
exit 0
fi
echo "[FAIL] expected '$expected' (light, from terminal), got '$actual'"
exit 1

View File

@@ -1667,7 +1667,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
}
}
tty_sync_start(tty);
tty_update_mode(tty, 0, NULL);
tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL);
if (wp != NULL)
redraw_draw_pane_lines(&dctx, wp, flags);

View File

@@ -464,6 +464,7 @@ server_destroy_session(struct session *s)
{
struct client *c;
struct session *s_new = NULL, *cs_new = NULL, *use_s;
struct sort_criteria sort_crit = { .order = SORT_NAME };
int detach_on_destroy;
detach_on_destroy = options_get_number(s->options, "detach-on-destroy");
@@ -472,9 +473,11 @@ server_destroy_session(struct session *s)
else if (detach_on_destroy == 2)
s_new = server_find_session(s, server_newer_detached_session);
else if (detach_on_destroy == 3)
s_new = session_previous_session(s, NULL);
s_new = session_previous_session(s, &sort_crit);
else if (detach_on_destroy == 4)
s_new = session_next_session(s, NULL);
s_new = session_next_session(s, &sort_crit);
if (s_new == s)
s_new = NULL;
/*
* If no suitable new session was found above, then look for any

5
tmux.1
View File

@@ -2587,7 +2587,7 @@ The synopsis for the
command is:
.Bl -tag -width Ds
.It Xo Ic copy\-mode
.Op Fl deHMqSu
.Op Fl dekHMqSu
.Op Fl s Ar src\-pane
.Op Fl t Ar target\-pane
.Xc
@@ -2627,6 +2627,9 @@ example with:
bind PageUp copy\-mode \-eu
bind PageDown copy\-mode \-ed
.Ed
.Pp
.Fl k
kills the pane when the mode is exited.
.El
.Pp
A number of preset arrangements of panes are available, these are called

View File

@@ -2461,7 +2461,6 @@ window_pane_get_theme(struct window_pane *wp)
{
struct window *w;
struct client *loop;
enum client_theme theme;
int found_light = 0, found_dark = 0;
if (wp == NULL)
@@ -2469,14 +2468,9 @@ window_pane_get_theme(struct window_pane *wp)
w = wp->window;
/*
* Derive theme from pane background color, if it's not the default
* colour.
* Prefer a theme reported by an attached client with mode 2031 or DSR
* 996: the terminal knows its own light or dark mode.
*/
theme = colour_totheme(window_pane_get_bg(wp));
if (theme != THEME_UNKNOWN)
return (theme);
/* Try to find a client that has a theme. */
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->flags & CLIENT_UNATTACHEDFLAGS)
continue;
@@ -2493,12 +2487,16 @@ window_pane_get_theme(struct window_pane *wp)
break;
}
}
if (found_dark && !found_light)
return (THEME_DARK);
if (found_light && !found_dark)
return (THEME_LIGHT);
return (THEME_UNKNOWN);
/*
* Otherwise guess from the pane background colour, for terminals which
* do not report a theme themselves.
*/
return (colour_totheme(window_pane_get_bg(wp)));
}
void