diff --git a/regress/side-status-clamp.sh b/regress/side-status-clamp.sh new file mode 100644 index 000000000..59687f85b --- /dev/null +++ b/regress/side-status-clamp.sh @@ -0,0 +1,143 @@ +#!/bin/sh + +# With side-status left, a client narrower than the window must clamp pane +# output to its visible part without killing the server or drawing over the +# side status line. Each inner client runs in an outer pane of its size. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +DIR=$(mktemp -d) || exit 1 +TRIGGER=$DIR/trigger +CAPTURE=$DIR/capture + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 + rm -f "$TRIGGER" "$CAPTURE" + rmdir "$DIR" +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +# Start the inner server with one pane running $1, then attach a big and a +# small client from outer panes of sizes $2 and $3 (WxH). +start() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 + rm -f "$TRIGGER" + + $TMUX new-session -d -s inner -x 200 -y 50 "$1" || exit 1 + $TMUX set -g status off || exit 1 + $TMUX set -g automatic-rename off || exit 1 + $TMUX set -g window-size largest || exit 1 + $TMUX set -g side-status left || exit 1 +} +attach() +{ + $TMUX2 new-session -d -s big -x "${1%x*}" -y "${1#*x}" 'sleep 100' || + exit 1 + $TMUX2 set -g status off || exit 1 + $TMUX2 set -g window-size manual || exit 1 + $TMUX2 new-session -d -s small -x "${2%x*}" -y "${2#*x}" 'sleep 100' || + exit 1 + $TMUX2 respawn-pane -k -t big:0.0 "$TMUX attach -t inner" || exit 1 + sleep 0.5 + $TMUX2 respawn-pane -k -t small:0.0 "$TMUX attach -t inner" || exit 1 + sleep 1 + $TMUX has-session 2>/dev/null || fail "server exited on attach" + SMALL=$($TMUX list-clients -F '#{client_width} #{client_name}' | + sort -n | head -n 1 | cut -d' ' -f2) +} +alive() +{ + $TMUX has-session 2>/dev/null || fail "server exited: $1" +} + +# A clear below the cursor in a pane wider than the small client's view. +start "sh -c 'while [ ! -e $TRIGGER ]; do sleep 0.1; done + printf \"\\033[J\"; exec sleep 100'" +$TMUX set -g side-status-width 31 || exit 1 +attach 167x49 40x12 +: >"$TRIGGER" +sleep 1 +alive "clear in a pane wider than the client" + +# The same clear with the small client panned right, so the pane is cut off +# on both sides. The side status line keeps the window on its first row. +start "sh -c 'while [ ! -e $TRIGGER ]; do sleep 0.1; done + printf \"\\033[J\"; exec sleep 100'" +$TMUX rename-window -t inner:0 work || exit 1 +$TMUX set -g side-status-width 31 || exit 1 +attach 167x49 40x12 +$TMUX refresh-client -t "$SMALL" -R 5 || exit 1 +sleep 0.5 +: >"$TRIGGER" +sleep 1 +alive "clear in a pane cut off on both sides" +$TMUX2 capture-pane -p -t small:0.0 >"$CAPTURE" || exit 1 +case "$(head -n 1 "$CAPTURE")" in +0:work*) ;; +*) fail "side status first row cleared" ;; +esac + +# Output in the right pane of a split, cut off on the right. +start 'sleep 100' +$TMUX split-window -h -d "sh -c 'while [ ! -e $TRIGGER ]; do sleep 0.1; done + printf ab; exec sleep 100'" || exit 1 +attach 120x30 80x30 +: >"$TRIGGER" +sleep 1 +alive "output in a pane cut off on the right" + +# Panned right by less than the side status width, the left pane is cut off +# on the left. Its output is drawn after the side status line, not over it. +start "sh -c 'while [ ! -e $TRIGGER ]; do sleep 0.1; done + printf \"%053d\" 0 | tr 0 X; exec sleep 100'" +$TMUX split-window -h -d 'sleep 100' || exit 1 +attach 120x30 80x30 +$TMUX refresh-client -t "$SMALL" -R 10 || exit 1 +sleep 0.5 +: >"$TRIGGER" +sleep 1 +alive "output in a pane cut off on the left" +$TMUX2 capture-pane -p -t small:0.0 >"$CAPTURE" || exit 1 +row=$(head -n 1 "$CAPTURE") +case "$row" in +?????????????│XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX│*) ;; +*) fail "row is '$row', expected the side status then 43 X" ;; +esac + +# Panned into the middle of the right pane, which is cut off on both sides. +# The visible columns of the pane are drawn, counted from the pane's left. +start 'sleep 100' +$TMUX split-window -h -d -l 100 "sh -c 'while [ ! -e $TRIGGER ]; do sleep 0.1; done + printf \"%010d\" 0 | sed s/0/0123456789/g; exec sleep 100'" || exit 1 +attach 214x30 40x30 +$TMUX refresh-client -t "$SMALL" -R 120 || exit 1 +sleep 0.5 +: >"$TRIGGER" +sleep 1 +alive "output in a pane cut off on both sides" +$TMUX2 capture-pane -p -t small:0.0 >"$CAPTURE" || exit 1 +row=$(head -n 1 "$CAPTURE") +case "$row" in +?????????????│01234567890123456789012345) ;; +*) fail "row is '$row', expected the side status then pane columns 20-45" ;; +esac + +exit 0 diff --git a/regress/side-status-click.sh b/regress/side-status-click.sh new file mode 100644 index 000000000..d69c1740f --- /dev/null +++ b/regress/side-status-click.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# Clicking a window on the side status line, including the current window, +# dispatches the Status binding with that window. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +# Click row $1 of the side status line and check the binding got $2. +click() +{ + $TMUX set -g @click none || exit 1 + seq=$(printf '\033[<0;3;%sM\033[<0;3;%sm' "$1" "$1") + $TMUX2 send-keys -t outer:0.0 -l "$seq" || fail "send mouse failed" + sleep 1 + got=$($TMUX show -gv @click) + [ "$got" = "$2" ] || fail "row $1: got '$got', expected '$2'" +} + +$TMUX new-session -d -s inner -n zero -x 60 -y 20 'sleep 100' || exit 1 +$TMUX new-window -d -n one 'sleep 100' || exit 1 +$TMUX new-window -d -n two 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g side-status left || exit 1 +$TMUX bind -n MouseDown1Status \ + set -gF @click '#{mouse_status_range} #{window_name}' || exit 1 +$TMUX bind -n MouseDown1StatusDefault set -g @click default || exit 1 +$TMUX2 new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +# The current window is first, then in the middle of the list. +click 1 'window zero' +click 2 'window one' +click 4 default +$TMUX select-window -t inner:1 || exit 1 +sleep 0.5 +click 1 'window zero' +click 2 'window one' +click 3 'window two' + +# A range closed in the same style as the line break ends on its own row. +$TMUX set -g side-status-format '#[range=user|foo]one#[norange,nl]two' || + exit 1 +sleep 0.5 +click 1 'foo one' +click 2 default + +# A range still open at a line break carries on to the next row. +$TMUX set -g side-status-format '#[range=user|foo]one#[nl]two#[norange]' || + exit 1 +sleep 0.5 +click 1 'foo one' +click 2 'foo one' + +exit 0 diff --git a/regress/side-status-drag.sh b/regress/side-status-drag.sh new file mode 100644 index 000000000..ffa515880 --- /dev/null +++ b/regress/side-status-drag.sh @@ -0,0 +1,90 @@ +#!/bin/sh + +# A Ctrl-drag starting on the side status line must not create a floating +# pane, and must still dispatch Status bindings. Ctrl-clicking a window on the +# side status line swaps windows, so panes are compared across the session. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +mouse() +{ + seq=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $TMUX2 send-keys -t outer:0.0 -l "$seq" || fail "send mouse failed" + sleep 0.2 +} + +check_drag() +{ + sx=$1 + sy=$2 + ex=$3 + ey=$4 + + # First move along the side status line, then into the pane. + mouse 16 "$sx" "$sy" M + mouse 48 "$sx" $((sy + 1)) M + mouse 48 "$ex" "$ey" M + mouse 48 $((ex + 2)) "$ey" M + mouse 16 $((ex + 2)) "$ey" m + + panes=$($TMUX list-panes -s -F '#{pane_id}') || fail "server exited" + panes=$(echo "$panes" | sort) + [ "$panes" = "$BASE" ] || + fail "side status drag from $sx,$sy created a pane: $panes" +} + +$TMUX new-session -d -s inner -x 60 -y 20 'sleep 100' || exit 1 +$TMUX new-window -d 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g default-command 'sleep 100' || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g side-status left || exit 1 +BASE=$($TMUX list-panes -s -F '#{pane_id}') || exit 1 +BASE=$(echo "$BASE" | sort) +$TMUX2 new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +# Blank rows and the window list on the left. +check_drag 3 10 30 12 +check_drag 2 1 30 12 +check_drag 2 2 30 12 + +$TMUX set -g side-status right || exit 1 +sleep 0.5 +check_drag 55 10 20 12 + +# The horizontal status line takes the bottom row below the side status. +$TMUX set -g side-status left || exit 1 +$TMUX set -g status on || exit 1 +sleep 0.5 +check_drag 3 10 30 12 + +# A drag starting on a window range dispatches the Status binding. +$TMUX bind -n C-MouseDrag1Status set -g @status-drag status || exit 1 +$TMUX set -g @status-drag '' || exit 1 +check_drag 2 2 30 12 +got=$($TMUX show -gv @status-drag) +[ "$got" = status ] || fail "got drag '$got', expected 'status'" + +exit 0 diff --git a/regress/side-status-format.sh b/regress/side-status-format.sh new file mode 100644 index 000000000..5f8501a2d --- /dev/null +++ b/regress/side-status-format.sh @@ -0,0 +1,139 @@ +#!/bin/sh + +# Split formats into lines, preserve headers and footers while scrolling, +# and clip wide or combining characters at the side status boundary. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" +DIR=$(mktemp -d) || exit 1 +HEIGHT=8 + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 + rm -f "$DIR/actual" "$DIR/expected" + rmdir "$DIR" +} +trap cleanup 0 1 15 + +check() +{ + format=$1 + shift + $TMUX set -g side-status-format "$format" || exit 1 + : >"$DIR/expected" + n=0 + for line in "$@"; do + printf '%s\n' "$line" >>"$DIR/expected" + n=$((n + 1)) + done + while [ "$n" -lt "$HEIGHT" ]; do + printf ' │\n' >>"$DIR/expected" + n=$((n + 1)) + done + i=0 + while [ "$i" -lt 50 ]; do + $TMUX2 capture-pane -p -t outer:0.0 >"$DIR/actual" || exit 1 + cmp -s "$DIR/actual" "$DIR/expected" && return + sleep 0.1 + i=$((i + 1)) + done + echo "format '$format' at height $HEIGHT differs:" >&2 + diff -u "$DIR/expected" "$DIR/actual" >&2 + exit 1 +} + +resize() +{ + HEIGHT=$1 + $TMUX2 resize-window -t outer:0 -y "$HEIGHT" || exit 1 +} + +$TMUX new-session -d -s inner -x 40 -y 8 'sleep 100' || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g side-status left || exit 1 +$TMUX set -g side-status-width 8 || exit 1 +$TMUX2 new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g window-size manual || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +# Empty, consecutive, leading and trailing breaks, with both spellings. +check '' +check 'one#[nl]#[nl]three#[nl]' 'one │' ' │' 'three │' +check ' + +three +' ' │' ' │' 'three │' +check 'one +two' 'one │' 'two │' + +# Escaped hashes must not turn literal text into a line break. +check '##[nl]#[nl]two' '#[nl] │' 'two │' +check '####[nl]#[nl]two' '##[nl] │' 'two │' +check '###[nl]two' '# │' 'two │' +check 'one##' 'one# │' + +# Invalid or unfinished styles do not consume the following valid lines. +check 'one#[invalid-style]two#[nl]three' 'onetwo │' 'three │' +check 'one#[unfinished' 'one │' +check '#[ignore]A#[nl]B +C' 'A#[nl]B│' 'C │' + +# Alignment carries over a break, as do character widths. +check '#[align=centre]X#[nl]Y' ' X │' ' Y │' +check '#[align=absolute-centre]X#[nl]Y' ' X │' ' Y │' +check 'abcde界#[nl]abcdef界#[nl]abcdeéf' \ + 'abcde界│' 'abcdef │' 'abcdeéf│' + +format='H1#[nl]H2#[nl]#[list=on]L0#[nl]L1#[nl]' +format=$format'#[list=focus]L2#[nl]#[list=on]L3#[nl]L4#[nl]' +format=$format'#[nolist]F1#[nl]F2' +check "$format" 'H1 │' 'H2 │' 'L0 │' 'L1 │' \ + 'L2 │' 'L3 │' 'F1 │' 'F2 │' +resize 6 +check "$format" 'H1 │' 'H2 │' 'L1 │' 'L2 │' \ + 'F1 │' 'F2 │' +resize 4 +check "$format" 'H1 │' 'H2 │' 'F1 │' 'F2 │' +resize 3 +check "$format" 'H1 │' 'H2 │' 'F1 │' +resize 1 +check "$format" 'H1 │' + +# Markers consume only list lines and cannot replace the focused line. +format='H#[nl]#[list=on]#[list=left-marker]^#[nl]' +format=$format'#[list=right-marker]v#[nl]#[list=on]L0#[nl]L1#[nl]' +format=$format'#[list=focus]L2#[nl]#[list=on]L3#[nl]L4#[nl]' +format=$format'#[nolist]F' +resize 5 +check "$format" 'H │' '^ │' 'L2 │' 'v │' 'F │' +resize 3 +check "$format" 'H │' 'L2 │' 'F │' +resize 2 +check "$format" 'H │' 'F │' + +# A large logical list does not require a tall terminal. +format=$(awk 'BEGIN { + printf "H#[nl]#[list=on]" + for (i = 0; i < 1000; i++) { + if (i == 800) + printf "#[list=focus]" + if (i == 801) + printf "#[list=on]" + printf "L%d#[nl]", i + } + printf "#[nolist]F" +}') +resize 3 +check "$format" 'H │' 'L800 │' 'F │' + +exit 0 diff --git a/regress/side-status-image.sh b/regress/side-status-image.sh new file mode 100644 index 000000000..2d52f3265 --- /dev/null +++ b/regress/side-status-image.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +# Images are drawn and erased in the pane's columns with either side status +# position and with the horizontal status off, above or below the window. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_test import image_test, sixel +from image_terminal import Terminal + +side = 14 +w, h = 16, 32 +wire = sixel(w, h, [(100, 0, 0)], [0] * (w * h)) + +def placed(cx, cy): + return {(cx * 8 + x, cy * 16 + y): (255, 0, 0) + for y in range(h) for x in range(w)} + +def side_cells(terminal, xoff, yoff, height): + return {(x, y): terminal.cells.get((x, y)) + for y in range(yoff, yoff + height) + for x in range(xoff, xoff + side)} + +def check(position, status): + with image_test(sys.argv[1]) as test: + test.run("set-option", "-g", "side-status", position) + test.run("set-option", "-g", "side-status-width", str(side)) + if status != "off": + test.run("set-option", "-g", "status", "2") + test.run("set-option", "-g", "status-position", status) + xoff = side if position == "left" else 0 + yoff = 2 if status == "top" else 0 + height = 16 if status == "off" else 14 + side_x = 0 if position == "left" else 40 - side + separator = side - 1 if position == "left" else side_x + client = test.attach() + test.emit(b'\033[3;3H' + wire) + + # A full redraw. + test.redraw() + terminal = Terminal() + terminal.feed(client.data) + terminal.check(placed(xoff + 2, yoff + 2)) + cells = side_cells(terminal, side_x, yoff, height) + assert all(cells[separator, y] not in (None, ' ') + for y in range(yoff, yoff + height)), \ + (position, status, 'side status line not drawn') + + # Inserting a line above the image redraws the damaged area. + test.emit(b'\033[3;1H\033[L') + terminal.feed(client.data) + terminal.check(placed(xoff + 2, yoff + 3)) + assert side_cells(terminal, side_x, yoff, height) == cells, \ + (position, status, 'side status erased by damage') + + # Leaving copy mode redraws only the pane. + client.clear() + test.run("copy-mode") + test.wait(lambda: test.run("display-message", "-p", + "#{pane_in_mode}") == "1") + test.run("send-keys", "-X", "cancel") + test.wait(lambda: test.run("display-message", "-p", + "#{pane_in_mode}") == "0") + client.read(0.2) + terminal.feed(client.data) + terminal.check(placed(xoff + 2, yoff + 3)) + assert side_cells(terminal, side_x, yoff, height) == cells, \ + (position, status, 'side status erased by pane redraw') + +for position in ("left", "right"): + for status in ("off", "top", "bottom"): + check(position, status) +PY diff --git a/regress/side-status-margins.sh b/regress/side-status-margins.sh new file mode 100644 index 000000000..4f8f789c9 --- /dev/null +++ b/regress/side-status-margins.sh @@ -0,0 +1,72 @@ +#!/bin/sh + +# On a terminal with left and right margins, scrolling a pane beside the side +# status line sets the margins to the pane's columns. The inner client is +# told the terminal has margins and its output is recorded raw from the outer +# pane, where ESC [ left ; right s sets the margins (1-based). + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +DIR=$(mktemp -d) || exit 1 +TRIGGER=$DIR/trigger +OUT=$DIR/out + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 + rm -f "$TRIGGER" "$OUT" + rmdir "$DIR" +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +# Scroll a full width pane with the side status line on side $1 and check the +# margins sent are $2. +check() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 + rm -f "$TRIGGER" "$OUT" + + $TMUX new-session -d -s inner -x 60 -y 10 "sh -c ' + while [ ! -e $TRIGGER ]; do sleep 0.1; done + i=0 + while [ \$i -lt 15 ]; do i=\$((i + 1)); echo line\$i; done + exec sleep 100'" || exit 1 + $TMUX set -g status off || exit 1 + $TMUX set -g side-status "$1" || exit 1 + $TMUX set -as terminal-features ',*:margins' || exit 1 + $TMUX2 new-session -d -s outer -x 60 -y 10 'sleep 100' || exit 1 + $TMUX2 set -g status off || exit 1 + $TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 + sleep 1 + $TMUX2 pipe-pane -t outer:0.0 "cat >$OUT" || exit 1 + sleep 0.5 + : >"$TRIGGER" + sleep 1 + + got=$(tr '\033' E <"$OUT" | grep -o 'E\[[0-9]*;[0-9]*s' | sort -u | + tr -d 'E[s' | tr '\n' ' ') + [ "$got" = "$2 " ] || fail "side-status $1: margins '$got', expected '$2'" +} + +# Pane in columns 15 to 60 after a left side status line of width 14. +check left '15;60' + +# Pane in columns 1 to 46 before a right side status line. +check right '1;46' + +exit 0 diff --git a/regress/side-status-mouse.sh b/regress/side-status-mouse.sh new file mode 100644 index 000000000..ebeafa3cf --- /dev/null +++ b/regress/side-status-mouse.sh @@ -0,0 +1,109 @@ +#!/bin/sh + +# Status coordinates and session/window targets on either side, with the +# horizontal status off, above or below the window. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +click() +{ + expected="$3|$(($1 - 1))|$(($2 - 1))" + $TMUX set -g @mouse none || exit 1 + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' "$1" "$2" "$1" "$2") + $TMUX2 send-keys -t outer:0.0 -l "$seq" || exit 1 + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX show -gv @mouse) + [ "$got" = "$expected" ] && break + sleep 0.1 + i=$((i + 1)) + done + [ "$got" = "$expected" ] || { + echo "$side/$status at $1,$2: '$got', expected '$expected'" >&2 + exit 1 + } + sleep 0.5 +} + +$TMUX new-session -d -s inner -n zero -x 80 -y 20 'sleep 100' || exit 1 +$TMUX new-window -d -n one 'sleep 100' || exit 1 +$TMUX new-session -d -s beta -n other -x 100 -y 30 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g automatic-rename off || exit 1 +$TMUX set -g side-status-width 14 || exit 1 +[ "$($TMUX display -p '#{mouse_absolute_x}|#{mouse_absolute_y}')" = '|' ] || + exit 1 +$TMUX set -g 'status-format[0]' \ + '#[range=window|1]window#[norange]' || exit 1 +$TMUX set -g 'status-format[1]' 'second' || exit 1 +format='#{mouse_status_range}|#{mouse_status_line}|#{mouse_x}|#{mouse_y}' +format=$format'|#{session_name}|#{window_name}' +format=$format'|#{mouse_absolute_x}|#{mouse_absolute_y}' +for key in MouseDown1Status MouseDown1StatusDefault MouseDown1StatusBorder \ + MouseDown1Pane; do + $TMUX bind -n "$key" set -gF @mouse "$format" || exit 1 +done +$TMUX2 new-session -d -s outer -x 80 -y 20 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +for side in left right; do + $TMUX set -g side-status "$side" || exit 1 + if [ "$side" = left ]; then + column=3 + separator=14 + pane_column=17 + separator_x=13 + else + column=69 + separator=67 + pane_column=3 + separator_x=0 + fi + for status in off top bottom; do + offset=0 + if [ "$status" = off ]; then + $TMUX set -g status off || exit 1 + else + $TMUX set -g status 2 || exit 1 + $TMUX set -g status-position "$status" || exit 1 + [ "$status" = top ] && offset=2 + fi + sleep 0.5 + + # The current session's windows are between the session headers. + click "$column" "$((offset + 1))" 'session|0|2|0|inner|zero' + click "$((column + 3))" "$((offset + 3))" \ + 'window|2|5|2|inner|one' + click "$column" "$((offset + 4))" 'session|3|2|3|beta|other' + click "$separator" "$((offset + 6))" \ + "|5|$separator_x|5|inner|zero" + click "$pane_column" "$((offset + 7))" '||2|6|inner|zero' + + # Horizontal status coordinates are relative to its first line, + # even if the clicked window's pane extends behind the status. + if [ "$status" != off ]; then + line=1 + [ "$status" = bottom ] && line=19 + click 3 "$line" 'window|0|2|0|inner|one' + click 3 "$((line + 1))" '|1|2|1|inner|zero' + fi + done +done + +exit 0 diff --git a/regress/side-status-off-redraw.sh b/regress/side-status-off-redraw.sh new file mode 100644 index 000000000..b10a99891 --- /dev/null +++ b/regress/side-status-off-redraw.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +# With the side status line off, a full redraw after changing window still +# draws pane border status lines that have not changed. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +check() +{ + got=$($TMUX2 capture-pane -p -t outer:0.0 | head -n 1) + [ "$got" = "$2" ] || fail "$1: top row is '$got', expected '$2'" +} + +$TMUX new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g pane-border-status top || exit 1 +$TMUX set -g pane-border-format ' #{pane_index} ' || exit 1 +$TMUX split-window -h 'sleep 100' || exit 1 +$TMUX new-window 'sleep 100' || exit 1 +$TMUX select-window -t:0 || exit 1 +$TMUX2 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +SPLIT='── 0 ───────────────┬── 1 ──────────────' +check attach "$SPLIT" + +$TMUX select-window -t:1 || exit 1 +sleep 0.5 +$TMUX select-window -t:0 || exit 1 +sleep 0.5 +check "after select-window" "$SPLIT" + +exit 0 diff --git a/regress/side-status-pane-drag.sh b/regress/side-status-pane-drag.sh new file mode 100644 index 000000000..9f3e4b5ff --- /dev/null +++ b/regress/side-status-pane-drag.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +# Creating, moving and resizing panes must use window coordinates with a +# side status line, including when the horizontal status is at the top. + +PATH=/bin:/usr/bin +TERM=screen +export PATH TERM + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$side/$status: $*" >&2 + exit 1 +} + +# Send window coordinates as one-based terminal coordinates. +mouse() +{ + seq=$(printf '\033[<%s;%s;%s%s' "$1" \ + "$(( $2 + xoff + 1 ))" "$(( $3 + yoff + 1 ))" "$4") + $TMUX2 send-keys -t outer:0.0 -l "$seq" || fail "send mouse failed" + sleep 0.2 +} + +drag() +{ + mouse "$1" "$2" "$3" M + mouse "$(( $1 + 32 ))" "$4" "$5" M + mouse "$1" "$4" "$5" m +} + +check() +{ + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display -p -t "$1" \ + '#{pane_left},#{pane_top},#{pane_width},#{pane_height}') + [ "$got" = "$2" ] && return + sleep 0.1 + i=$((i + 1)) + done + fail "pane $1: got '$got', expected '$2'" +} + +$TMUX new-session -d -s inner -x 80 -y 24 'sleep 100' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g default-command 'sleep 100' || exit 1 +$TMUX set -g side-status-width 14 || exit 1 +BASE=$($TMUX display -p '#{pane_id}') || exit 1 +$TMUX2 new-session -d -s outer -x 80 -y 24 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +for side in left right; do + $TMUX set -g side-status "$side" || exit 1 + xoff=0 + [ "$side" = left ] && xoff=14 + for status in off top bottom; do + yoff=0 + if [ "$status" = off ]; then + $TMUX set -g status off || exit 1 + else + $TMUX set -g status 2 || exit 1 + $TMUX set -g status-position "$status" || exit 1 + [ "$status" = top ] && yoff=2 + fi + sleep 0.5 + + # Ctrl-drag creates a floating pane inside the rectangle. + drag 16 2 1 10 5 + id=$($TMUX list-panes -F \ + '#{?pane_floating_flag,#{pane_id},}' | tail -1) + [ -n "$id" ] || fail "drag did not create a floating pane" + check "$id" '3,2,7,3' + $TMUX kill-pane -t "$id" || exit 1 + + # Alt-drag moves a floating pane without changing its size. + id=$($TMUX new-pane -dPF '#{pane_id}' \ + -x 14 -y 7 -X 5 -Y 3 'sleep 100') || exit 1 + check "$id" '6,4,12,5' + drag 8 8 5 12 7 + check "$id" '10,6,12,5' + + # Dragging the bottom right border resizes both dimensions. + drag 0 22 11 25 13 + check "$id" '10,6,15,7' + $TMUX kill-pane -t "$id" || exit 1 + + # The same terminal offsets apply to tiled pane borders. + id=$($TMUX split-window -dhPF '#{pane_id}' \ + -t "$BASE" 'sleep 100') || exit 1 + width=$($TMUX display -p -t "$BASE" '#{pane_width}') + drag 0 "$width" 5 "$((width + 4))" 5 + got=$($TMUX display -p -t "$BASE" '#{pane_width}') + [ "$got" = "$((width + 4))" ] || + fail "tiled width: got '$got', expected '$((width + 4))'" + $TMUX kill-pane -t "$id" || exit 1 + done +done + +exit 0 diff --git a/regress/side-status-prompt.sh b/regress/side-status-prompt.sh new file mode 100644 index 000000000..db0e3ad67 --- /dev/null +++ b/regress/side-status-prompt.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +# A pane prompt redrawn after the pane scrolls stays beside the side status +# line. With side-status left the pane is not full width, so scrolling is +# redrawn as damage, which draws the prompt again over the pane. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +DIR=$(mktemp -d) || exit 1 +TRIGGER=$DIR/trigger +CAPTURE=$DIR/capture + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +wait_outer_has() +{ + i=0 + while [ "$i" -lt 50 ]; do + $TMUX2 capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null + grep -q "$1" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $1" +} + +# The pane waits for the trigger, then prints enough lines to scroll. +$TMUX new-session -d -s inner -n work -x 60 -y 20 "sh -c ' + while [ ! -e $TRIGGER ]; do sleep 0.1; done + i=0 + while [ \$i -lt 40 ]; do i=\$((i + 1)); echo line\$i; done + exec sleep 100'" || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g automatic-rename off || exit 1 +$TMUX set -g side-status left || exit 1 +$TMUX2 new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 + +i=0 +while [ "$i" -lt 50 ]; do + CLIENT=$($TMUX list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && break + sleep 0.1 + i=$((i + 1)) +done +[ -n "$CLIENT" ] || fail "inner client did not attach" + +$TMUX command-prompt -b -P -t "$CLIENT" -p PROMPTX 'display -p %%' || + exit 1 +wait_outer_has PROMPTX + +: >"$TRIGGER" +wait_outer_has line40 +sleep 0.5 + +# The bottom row is the side status line followed by the prompt. +$TMUX2 capture-pane -p -t outer:0.0 >"$CAPTURE" || exit 1 +want="$(printf '%13s' '')│PROMPTX" +got=$(tail -n 1 "$CAPTURE") +[ "$got" = "$want" ] || fail "bottom row is '$got', expected '$want'" + +exit 0 diff --git a/regress/side-status-refresh.sh b/regress/side-status-refresh.sh new file mode 100644 index 000000000..9cde7a34c --- /dev/null +++ b/regress/side-status-refresh.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +# The side status line must refresh every status-interval seconds on its own, +# whether or not the horizontal status line is on, and whether it was on when +# the client attached or turned on afterwards. The client status timer drives +# both lines, so this checks it is armed in both cases with status off. +# +# The side status line is rendered by an inner tmux attached inside an outer +# tmux pane. Its format is a #() command which appends a byte to a file each +# time it runs, and a #() command is run again only when the format is +# expanded in a later second, so the file grows once per refresh. Nothing in +# the test asks for a redraw between the two counts, so growth means the +# timer refreshed the side status line by itself. + +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" + +COUNT1=$(mktemp) +COUNT2=$(mktemp) +trap "rm -f $COUNT1 $COUNT2; $TMUX kill-server 2>/dev/null; \ + $TMUX2 kill-server 2>/dev/null" 0 1 15 + +fail() { + echo "$*" >&2 + exit 1 +} + +count() { + wc -c <"$1" | tr -d ' ' +} + +# Attach the inner server in an outer pane, then wait for the first draw. +attach() { + $TMUX new -d -x30 -y8 "$TMUX2 attach" || exit 1 + sleep 1 +} + +# Check the side status line refreshed at least twice in three seconds. +refreshes() { + before=$(count "$1") + sleep 3 + after=$(count "$1") + [ $((after - before)) -ge 2 ] || + fail "$2: side status ran $before then $after times, did not refresh" +} + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +# Side status on before the client attaches, with status off. +$TMUX2 new -d -x30 -y8 "sh -c 'exec sleep 100'" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g status-interval 1 || exit 1 +$TMUX2 set -g side-status left || exit 1 +$TMUX2 set -g side-status-format "#(printf x >>$COUNT1)" || exit 1 +attach +refreshes "$COUNT1" "on at attach" + +# Side status turned on after the client attached, with status off. A new +# inner server so the client timer starts from nothing. +$TMUX2 kill-server || exit 1 +sleep 1 +$TMUX2 new -d -x30 -y8 "sh -c 'exec sleep 100'" || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 set -g status-interval 1 || exit 1 +$TMUX2 set -g side-status-format "#(printf x >>$COUNT2)" || exit 1 +attach +$TMUX2 set -g side-status left || exit 1 +sleep 1 +refreshes "$COUNT2" "on after attach" + +exit 0 diff --git a/regress/side-status-resize.sh b/regress/side-status-resize.sh new file mode 100644 index 000000000..3191cc916 --- /dev/null +++ b/regress/side-status-resize.sh @@ -0,0 +1,197 @@ +#!/bin/sh + +# Drag the separator on either side, including across the moving boundary and +# horizontal status. Check limits, release, text bindings and a second client. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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" + +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$side/$status: $*" >&2 + exit 1 +} + +mouse() +{ + seq=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4") + $TMUX2 send-keys -t outer:0.0 -l "$seq" || fail "send mouse failed" + sleep 0.1 +} + +check() +{ + i=0 + while [ "$i" -lt 50 ]; do + width=$($TMUX show -Av -t inner side-status-width) + window=$($TMUX display -p -t inner '#{window_width}') + if [ "$width" = "$1" ] && [ "$window" = "$((80 - $1))" ]; then + break + fi + sleep 0.1 + i=$((i + 1)) + done + [ "$width,$window" = "$1,$((80 - $1))" ] || + fail "side/window widths $width/$window, expected $1/$((80 - $1))" + column=$1 + [ "$side" = right ] && column=$((81 - $1)) + padding=$(printf '%*s' "$((column - 1))" '') + for target in outer other; do + i=0 + while [ "$i" -lt 50 ]; do + row=$($TMUX2 capture-pane -p -t "$target:0.0" | + sed -n "$((offset + 2))p") + case "$row" in + "$padding│"*) break ;; + esac + sleep 0.1 + i=$((i + 1)) + done + [ "$i" -lt 50 ] || fail "$target: wrong separator column: $row" + done +} + +$TMUX new-session -d -s inner -n zero -x 80 -y 20 'sleep 180' || exit 1 +$TMUX set -g mouse on || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g side-status-format \ + '#[range=window|0]window#[norange]' || exit 1 +$TMUX bind -n MouseDrag1Status set -g @text-drag yes || exit 1 +$TMUX bind -n MouseDrag1StatusDefault set -g @blank-drag yes || exit 1 +$TMUX2 new-session -d -s outer -x 80 -y 20 'sleep 180' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 new-session -d -s other -x 80 -y 20 'sleep 180' || exit 1 +for target in outer other; do + $TMUX2 respawn-pane -k -t "$target:0.0" \ + "$TMUX attach -t inner" || exit 1 +done +sleep 1 + +for side in left right; do + $TMUX set -t inner side-status "$side" || exit 1 + for status in off top bottom; do + offset=0 + if [ "$status" = off ]; then + $TMUX set -t inner status off || exit 1 + else + $TMUX set -t inner status 2 || exit 1 + $TMUX set -t inner status-position "$status" || exit 1 + [ "$status" = top ] && offset=2 + fi + $TMUX set -t inner side-status-width 14 || exit 1 + check 14 + if [ "$side" = left ]; then + start=14 grow=20 shrink=10 smallest=1 largest=80 + else + start=67 grow=61 shrink=71 smallest=80 largest=1 + fi + + # Grow into the window, then shrink past the starting point. + mouse 0 "$start" "$((offset + 6))" M + mouse 32 "$grow" "$((offset + 6))" M + check 20 + mouse 32 "$shrink" 1 M + check 10 + mouse 32 "$largest" "$((offset + 6))" M + check 79 + mouse 32 "$smallest" "$((offset + 6))" M + check 2 + mouse 0 "$smallest" "$((offset + 6))" m + + # Motion after release must not resize, and the separator stays + # draggable at the minimum width. + mouse 35 "$grow" "$((offset + 6))" M + check 2 + sleep 0.5 + start=2 + [ "$side" = right ] && start=79 + mouse 0 "$start" "$((offset + 6))" M + mouse 32 "$grow" "$((offset + 6))" M + check 20 + mouse 0 "$grow" "$((offset + 6))" m + sleep 0.5 + + # Dragging text or blank content dispatches its usual binding. + text=2 + [ "$side" = right ] && text=63 + $TMUX set -g @text-drag no || exit 1 + mouse 0 "$text" "$((offset + 1))" M + mouse 32 "$((text + 1))" "$((offset + 1))" M + mouse 32 40 "$((offset + 6))" M + mouse 0 40 "$((offset + 6))" m + got=$($TMUX show -gv @text-drag) + [ "$got" = yes ] || fail "text drag binding did not fire" + check 20 + sleep 0.5 + $TMUX set -g @blank-drag no || exit 1 + mouse 0 "$text" "$((offset + 8))" M + mouse 32 40 "$((offset + 8))" M + mouse 0 40 "$((offset + 8))" m + got=$($TMUX show -gv @blank-drag) + [ "$got" = yes ] || fail "blank drag binding did not fire" + check 20 + + # A modified drag on the separator does not resize it. + mouse 16 "$grow" "$((offset + 6))" M + mouse 48 40 "$((offset + 6))" M + mouse 16 40 "$((offset + 6))" m + check 20 + if [ "$status" != off ]; then + line=1 + [ "$status" = bottom ] && line=19 + mouse 0 "$grow" "$line" M + mouse 32 40 "$((offset + 6))" M + mouse 0 40 "$((offset + 6))" m + check 20 + fi + [ "$($TMUX show -gv side-status-width)" = 14 ] || + fail "drag changed the global width" + + # Double-click removes the local width and inherits the default. + sleep 0.5 + seq=$(printf '\033[<0;%s;%sM\033[<0;%s;%sm' \ + "$grow" "$((offset + 6))" "$grow" "$((offset + 6))") + $TMUX2 send-keys -t outer:0.0 -l "$seq$seq" || exit 1 + check 14 + [ -z "$($TMUX show -v -q -t inner side-status-width)" ] || + fail "double-click did not remove the local width" + done +done + +# The resize action is an ordinary binding and can be replaced or removed. +$TMUX set -t inner side-status-width 20 || exit 1 +check 20 +$TMUX set -g @border-drag no || exit 1 +$TMUX set -g @border-end no || exit 1 +$TMUX bind -n MouseDrag1StatusBorder \ + set -gF @border-drag '#{mouse_absolute_x},#{mouse_absolute_y}' || exit 1 +$TMUX bind -n MouseDragEnd1StatusBorder set -g @border-end yes || exit 1 +mouse 0 61 6 M +mouse 32 40 6 M +mouse 32 42 6 M +mouse 0 42 6 m +check 20 +[ "$($TMUX show -gv @border-drag)" = 41,5 ] || + fail "custom border binding did not get the terminal coordinates" +[ "$($TMUX show -gv @border-end)" = yes ] || + fail "border drag end binding did not fire" +$TMUX unbind -n MouseDrag1StatusBorder || exit 1 +mouse 0 61 6 M +mouse 32 40 6 M +mouse 0 40 6 m +check 20 + +exit 0 diff --git a/regress/side-status-results/side-left-floating.result b/regress/side-status-results/side-left-floating.result new file mode 100644 index 000000000..36e51977a --- /dev/null +++ b/regress/side-status-results/side-left-floating.result @@ -0,0 +1,24 @@ +0:editor │┌──────────┐ +1:server* ││FLOAT │ +2:logs- ││ │ + │└──────────┘ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ diff --git a/regress/side-status-results/side-left.result b/regress/side-status-results/side-left.result new file mode 100644 index 000000000..f0f886a52 --- /dev/null +++ b/regress/side-status-results/side-left.result @@ -0,0 +1,24 @@ +0:editor │SERVER +1:server* │ +2:logs- │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ diff --git a/regress/side-status-results/side-off.result b/regress/side-status-results/side-off.result new file mode 100644 index 000000000..8a939862a --- /dev/null +++ b/regress/side-status-results/side-off.result @@ -0,0 +1,24 @@ +SERVER + + + + + + + + + + + + + + + + + + + + + + +[test]0:editor 1:server* 2:logs- diff --git a/regress/side-status-results/side-right-narrow.result b/regress/side-status-results/side-right-narrow.result new file mode 100644 index 000000000..7e4821bab --- /dev/null +++ b/regress/side-status-results/side-right-narrow.result @@ -0,0 +1,24 @@ +SERVER │0:editor + │1:server* + │2:logs- + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ +[test]0:editor 1:server* 2:logs- diff --git a/regress/side-status-results/side-right-status.result b/regress/side-status-results/side-right-status.result new file mode 100644 index 000000000..8fa0afc2e --- /dev/null +++ b/regress/side-status-results/side-right-status.result @@ -0,0 +1,24 @@ +SERVER │0:editor + │1:server* + │2:logs- + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ +[test]0:editor 1:server* 2:logs- diff --git a/regress/side-status-results/side-right.result b/regress/side-status-results/side-right.result new file mode 100644 index 000000000..0e5e2a5b6 --- /dev/null +++ b/regress/side-status-results/side-right.result @@ -0,0 +1,24 @@ +SERVER │0:editor + │1:server* + │2:logs- + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ diff --git a/regress/side-status-results/side-sessions.result b/regress/side-status-results/side-sessions.result new file mode 100644 index 000000000..e6e11ef37 --- /dev/null +++ b/regress/side-status-results/side-sessions.result @@ -0,0 +1,24 @@ +work │SERVER +├─→ 0:editor │ +├─→ 1:server* │ +└─→ 2:logs- │ +beta │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ + │ diff --git a/regress/side-status-scroll.sh b/regress/side-status-scroll.sh new file mode 100644 index 000000000..e7972b01a --- /dev/null +++ b/regress/side-status-scroll.sh @@ -0,0 +1,148 @@ +#!/bin/sh + +# With more windows than rows, the default side status format scrolls the +# window list to keep the current window visible and replaces the first and +# last visible rows with up and down markers. + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH 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) || exit 1 +cleanup() +{ + $TMUX2 kill-server >/dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 + rm -f "$TMP" +} +trap cleanup 0 1 15 + +$TMUX new-session -d -s inner -n w0 -x 60 -y 10 'sleep 100' || exit 1 +i=1 +while [ "$i" -lt 15 ]; do + $TMUX new-window -d -n "w$i" 'sleep 100' || exit 1 + i=$((i + 1)) +done +$TMUX set -g status off || exit 1 +$TMUX set -g side-status left || exit 1 +$TMUX select-window -t inner:7 || exit 1 +$TMUX2 new-session -d -s outer -x 60 -y 10 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +# Compare the outer pane with the expected rows on standard input. +compare() +{ + $TMUX2 capture-pane -p -t outer:0.0 >"$TMP" || exit 1 + if ! cmp -s - "$TMP"; then + echo "$1 differs:" >&2 + cat "$TMP" >&2 + exit 1 + fi +} + +compare "scrolled side status" </dev/null 2>&1 + $TMUX kill-server >/dev/null 2>&1 +} +trap cleanup 0 1 15 + +fail() +{ + echo "$*" >&2 + exit 1 +} + +# Check the second side status row starts with $2. +row2() +{ + got=$($TMUX2 capture-pane -ep -t outer:0.0 | sed -n 2p) + case "$got" in + "$2"*) ;; + *) fail "$1: row 2 is '$got'" ;; + esac +} + +$TMUX new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1 +$TMUX set -g status off || exit 1 +$TMUX set -g side-status left || exit 1 +$TMUX2 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1 +$TMUX2 set -g status off || exit 1 +$TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 +sleep 1 + +ESC=$(printf '\033') + +$TMUX set -g side-status-style bold || exit 1 +$TMUX set -g side-status-format '#[nobold]one#[nl]two' || exit 1 +sleep 1 +row2 "nobold" "two" + +$TMUX set -g side-status-style default || exit 1 +$TMUX set -g side-status-format \ + '#[fg=red]#[push-default]one#[nl]#[fg=blue]#[default]two' || exit 1 +sleep 1 +row2 "push-default" "$ESC[31mtwo" + +$TMUX set -g side-status-format \ + '#[fg=red]#[set-default]#[fg=blue]#[push-default]one#[nl]'\ +'#[pop-default]#[default]two' || exit 1 +sleep 1 +row2 "set-default" "$ESC[31mtwo" + +$TMUX set -g side-status-format '#[align=right]one#[nl]two' || exit 1 +sleep 1 +row2 "align" " two" + +$TMUX set -g side-status-format '#[fill=red]one#[nl]two' || exit 1 +sleep 1 +row2 "fill" "two$ESC[41m" + +$TMUX set -g side-status-format '#[link=https://example.com/]one#[nl]two' || + exit 1 +sleep 1 +row2 "link" "$ESC]8;" + +exit 0 diff --git a/regress/side-status.sh b/regress/side-status.sh new file mode 100644 index 000000000..86b58acfd --- /dev/null +++ b/regress/side-status.sh @@ -0,0 +1,89 @@ +#!/bin/sh + +# Side status line rendering: geometry on the left and right, a floating pane +# against the window edge, coexistence with the horizontal status line, width +# changes and turning it off. Scenes are rendered in an inner tmux attached +# inside an outer tmux pane; the outer pane is captured and compared with +# goldens in side-status-results/. +# +# Run with GENERATE=1 to (re)create the golden files. + +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" +RESULTS=side-status-results + +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 +} + +compare() { + sleep 1 + $TMUX capturep -p >$TMP || exit 1 + if [ -n "$GENERATE" ]; then + cp $TMP "$RESULTS/$1.result" || exit 1 + else + cmp -s $TMP "$RESULTS/$1.result" || + fail "scene $1 differs from $RESULTS/$1.result" + fi +} + +mkdir -p "$RESULTS" + +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null +$TMUX new-session -d -x 80 -y 24 || exit 1 + +# Inner server with fixed pane content so captures are stable. +$TMUX2 new-session -d -s work -n editor "sh -c 'printf EDITOR; exec sleep 100'" +$TMUX2 new-window -n server "sh -c 'printf SERVER; exec sleep 100'" +$TMUX2 new-window -n logs "sh -c 'printf LOGS; exec sleep 100'" +$TMUX2 select-window -t:1 +$TMUX2 set -g status off +$TMUX2 set -g status-left "[test]" +$TMUX2 set -g status-right "" +$TMUX2 set -g side-status left + +$TMUX send-keys "exec $TEST_TMUX -LtestB$$ -f/dev/null attach" Enter +sleep 1 +compare side-left + +# A floating pane at the window's top left corner sits beside the side status +# line, not over it. +id=$($TMUX2 new-pane -dPF '#{pane_id}' -x 12 -y 4 -X 0 -Y 0 \ + "sh -c 'printf FLOAT; exec sleep 100'") || exit 1 +compare side-left-floating +$TMUX2 kill-pane -t "$id" || exit 1 + +$TMUX2 set -g side-status right +compare side-right + +$TMUX2 set -g status on +compare side-right-status + +$TMUX2 set -g side-status-width 12 +compare side-right-narrow + +$TMUX2 set -g side-status off +compare side-off + +# A second session collapses the bar into session rows with the current +# session's windows indented below its row. +$TMUX2 set -g side-status left +$TMUX2 set -g side-status-width 24 +$TMUX2 set -g status off +$TMUX2 new-session -d -s beta -n shell "sh -c 'printf SHELL; exec sleep 100'" +$TMUX2 refresh-client -S +compare side-sessions + +exit 0 diff --git a/regress/status-ignore-size.sh b/regress/status-ignore-size.sh index 92950cce9..878b27ed0 100644 --- a/regress/status-ignore-size.sh +++ b/regress/status-ignore-size.sh @@ -1,7 +1,7 @@ #!/bin/sh -# A client attached with ignore-size that is shorter than the status line -# must have it turned off rather than kill the server. +# A client attached with ignore-size that is too small for the status line or +# the side status line must have them turned off rather than kill the server. # Each inner client runs in an outer pane of its size. PATH=/bin:/usr/bin @@ -51,4 +51,10 @@ $TMUX set -g status 5 || exit 1 attach 80x24 80x4 $TMUX has-session 2>/dev/null || fail "server exited: status line too tall" +# A side status line wider than the client. +$TMUX set -g status on || exit 1 +$TMUX set -g side-status left || exit 1 +attach 80x24 13x24 +$TMUX has-session 2>/dev/null || fail "server exited: side status too wide" + exit 0 diff --git a/regress/status-menu.sh b/regress/status-menu.sh index 8ef806d69..c49681269 100644 --- a/regress/status-menu.sh +++ b/regress/status-menu.sh @@ -1,7 +1,7 @@ #!/bin/sh -# Status menus are displayed in the visible window and their commands -# target the clicked window, including when it is not current. +# Status menus are displayed in the visible window, target the clicked +# window, and are placed beside the clicked row on either side status line. PATH=/bin:/usr/bin TERM=screen @@ -29,6 +29,7 @@ fail() } # Right-button press at column $1, row $2 (1-based), targeting window $3. +# If supplied, $4 is the expected row and column of the first menu item's text. check_menu() { seq=$(printf '\033[<2;%s;%sM' "$1" "$2") @@ -42,6 +43,14 @@ check_menu() done grep -q 'Swap Left' "$CAPTURE" || fail "window $3 menu not visible" grep -q "─$3:" "$CAPTURE" || fail "window $3 title missing" + if [ -n "$4" ]; then + got=$(awk '{ + gsub(/│/, "|") + if (index($0, "Swap Left")) + print NR, index($0, "Swap Left") + }' "$CAPTURE") + [ "$got" = "$4" ] || fail "menu at '$got', expected '$4'" + fi # Mark acts on the clicked window without selecting it. Other windows # exercise dragging into the menu and releasing the right button. @@ -81,15 +90,49 @@ $TMUX new-window -d -n one 'sleep 100' || exit 1 $TMUX new-window -d -n two 'sleep 100' || exit 1 $TMUX set -g mouse on || exit 1 $TMUX set -g automatic-rename off || exit 1 -$TMUX set -g status 2 || exit 1 -$TMUX set -g 'status-format[0]' \ - '#[range=window|0]0:zero#[norange] #[range=window|1]1:one#[norange]' || \ - exit 1 +$TMUX set -g side-status-width 14 || exit 1 $TMUX2 new-session -d -s outer -x 80 -y 24 'sleep 100' || exit 1 $TMUX2 set -g status off || exit 1 $TMUX2 respawn-pane -k -t outer:0.0 "$TMUX attach -t inner" || exit 1 sleep 1 +for side in left right; do + $TMUX set -g side-status "$side" || exit 1 + if [ "$side" = left ]; then + column=3 + text_column=17 + else + column=69 + text_column=51 + fi + for status in off bottom top; do + offset=0 + if [ "$status" = off ]; then + $TMUX set -g status off || exit 1 + else + $TMUX set -g status 2 || exit 1 + $TMUX set -g status-position "$status" || exit 1 + [ "$status" = top ] && offset=2 + fi + sleep 0.5 + for window in 0 1; do + row=$((window + offset + 1)) + check_menu "$column" "$row" "$window" \ + "$((row + 1)) $text_column" + done + done +done + +# A noncurrent window's dimensions must not constrain the visible menu. +$TMUX resize-window -t inner:1 -x 10 -y 5 || exit 1 +check_menu 69 4 1 '5 51' + +# The same target/display distinction applies to the horizontal status line. +$TMUX set -g side-status off || exit 1 +$TMUX set -g status 2 || exit 1 +$TMUX set -g 'status-format[0]' \ + '#[range=window|0]0:zero#[norange] #[range=window|1]1:one#[norange]' || \ + exit 1 for position in top bottom; do $TMUX set -g status-position "$position" || exit 1 if [ "$position" = top ]; then @@ -98,12 +141,8 @@ for position in top bottom; do row=23 fi sleep 0.5 - check_menu 2 "$row" 0 - check_menu 9 "$row" 1 + check_menu 2 "$row" 0 '' + check_menu 9 "$row" 1 '' done -# A noncurrent window's dimensions must not constrain the visible menu. -$TMUX resize-window -t inner:1 -x 10 -y 5 || exit 1 -check_menu 9 23 1 - exit 0