diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index fce6d3fbd..cfa60f59f 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -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 }, diff --git a/cmd-new-window.c b/cmd-new-window.c index 6e6d70ccc..a54aaaa20 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -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; diff --git a/cmd-split-window.c b/cmd-split-window.c index 426038b86..5dfb75b61 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -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'); diff --git a/layout.c b/layout.c index 9f4519048..68d5e9514 100644 --- a/layout.c +++ b/layout.c @@ -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; diff --git a/regress/format-modifiers.sh b/regress/format-modifiers.sh index b6340b622..631f1e10e 100644 --- a/regress/format-modifiers.sh +++ b/regress/format-modifiers.sh @@ -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" diff --git a/regress/mode-kill.sh b/regress/mode-kill.sh new file mode 100755 index 000000000..a3e0d09b9 --- /dev/null +++ b/regress/mode-kill.sh @@ -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 diff --git a/regress/pane-ops.sh b/regress/pane-ops.sh index 762bea200..29c79e2e2 100644 --- a/regress/pane-ops.sh +++ b/regress/pane-ops.sh @@ -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') diff --git a/regress/session-ops.sh b/regress/session-ops.sh index 08c5dfbae..7fc2b65cb 100644 --- a/regress/session-ops.sh +++ b/regress/session-ops.sh @@ -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' diff --git a/regress/theme-report.sh b/regress/theme-report.sh new file mode 100755 index 000000000..4c42244b4 --- /dev/null +++ b/regress/theme-report.sh @@ -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 diff --git a/screen-redraw.c b/screen-redraw.c index 21499d426..b9bd75485 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -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); diff --git a/server-fn.c b/server-fn.c index 3f8feca79..41e78bad6 100644 --- a/server-fn.c +++ b/server-fn.c @@ -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 diff --git a/tmux.1 b/tmux.1 index 60644a662..a3bc01c12 100644 --- a/tmux.1 +++ b/tmux.1 @@ -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 diff --git a/window.c b/window.c index 28457da05..dbfaabafe 100644 --- a/window.c +++ b/window.c @@ -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