Compare commits

..

99 Commits

Author SHA1 Message Date
Michael Grant
d4adce7e00 screen-redraw: use the prompt's own screen to clip its damage range
redraw_damage_draw_pane_prompt() recomposes a pane's separately rendered
prompt (wp->prompt) over a damaged sub-range of a REDRAW_SPAN_PANE span,
reusing the (x, n) range that redraw_client_damage() already clipped and
grew against the pane's own *content* grid (wp->screen) via
redraw_damage_grow_span_clip(). That growing exists specifically to avoid
splitting a wide character at the range's edge - but the prompt is drawn
into its own, freshly allocated one-line screen with no relationship to
the content grid, so a range that's clean (or correctly grown) for the
content can still land mid-character in the prompt's own grid.

This is invisible whenever the pane's content is plain ASCII: the content
grid has no padding cells to find, so redraw_damage_grow_span_clip() never
grows the range at all, and the raw geometric range - however it landed -
is passed straight through to the prompt. If that range's end lands right
after a base cell whose padding half falls just outside it, tty_draw_line()
has no room left for that cell's second column
(tty_draw_line_get_empty()'s gc->data.width > nx check) and blanks it
entirely, even though the pane's own content never needed the fix at all.

Reproduced with a floating pane's CJK prompt straddling the boundary
between two tiled panes underneath it: a palette change (OSC 4) in one of
the tiled panes triggers a redraw of its own rectangle, which is occluded
by the floating pane but still geometrically overlaps its prompt row,
recomposing a partial range of the prompt that cuts through a character.

Fix: extract the grid-probing tail of redraw_span_cell_is_padding() into
redraw_screen_cell_is_padding(), usable against any screen, and add
redraw_damage_grow_screen_clip() - the same left/right one-step growth as
redraw_damage_grow_span_clip(), but against an explicit screen and
column origin. redraw_damage_draw_pane_prompt() now re-derives its own
(x0, x1) range from the caller's (x, n) by growing it against the
prompt's own screen before drawing, clamped to the span so it can't bleed
into a neighbouring one.

New regress/floating-pane-prompt-wide-character.sh constructs the exact
tiled-pane-boundary scenario above and checks the CJK prompt text is
intact after the trigger. Verified failing 3/3 against the pre-fix code
(a character is blanked) and passing 5/5 standalone plus 2/2 in the full
regress suite (twice, since this touches the same damage-composition path
as every other redraw) against the fix, with no other tests newly broken
(prompt-words-history.sh and the untracked image-support scratch tests
are pre-existing, unrelated failures).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:16:25 +01:00
Michael Grant
04dbc6d89b screen-redraw: don't let one client's pane title leak to another
redraw_damage_refresh_status() force-regenerates a pane's border-status
title when a damage rectangle touches it (window_make_pane_status()'s
own content-diff cache can't tell "physically disturbed" from "never
changed"), guarded by the per-pane PANE_NEWSTATUS flag. But the
rendered content is per-client - window_make_pane_status() formats
pane-border-format using the requesting client's own context, so
fields like #{client_name} genuinely differ per client - while
wp->status_screen/PANE_NEWSTATUS are shared by every client viewing
the pane. With two clients attached to the same session, whichever
client's damage pass ran first rendered its own text and set the flag;
every other client's damage pass in that tick, or any later one, since
nothing else clears the flag on this path, found it already set and
skipped rendering - silently reusing the first client's text.

Fix: a per-pass serial (redraw_status_serial, bumped once per
redraw_client_damage() call - one call is one client's one redraw
pass) instead of a sticky flag. Deduplicates repeated calls within the
same client's same pass exactly as before, but forces a fresh,
correctly-client-formatted render whenever a different client or a
later pass touches the same pane's status - without needing to track
and later invalidate a client pointer with its own lifetime.

Also added a permanent log_debug() line for the actual regenerate,
since this class of bug (a damage pass silently trusting stale
per-pane state that should have been per-client) is otherwise
invisible to any external capture: an unrelated periodic per-client
status refresh reliably repaints each client's title correctly again
within the very same tick, before anything is ever flushed to either
terminal, so the wrong content this bug produces was never actually
observable in a capture-pane-based test - confirmed by direct
instrumentation while building the regression test below, which is
exactly why the fix is verified via this log rather than a capture.

regress/floating-pane-status-cross-client.sh attaches two clients to
one session, each with its own pane-border-format referencing
#{client_name}, and triggers a damage-only palette update (OSC 4) in
a tiled pane whose geometry overlaps a floating pane's own
border-status row - carefully picked so the trigger has no side
effect that would otherwise force a normal, already-correct, full
per-client status re-render in the same pass, which would mask the
result either way. Verified failing 3/3 against the pre-fix code
(neither client's damage pass regenerates at all - both silently
reuse whatever a much earlier full redraw left in the shared buffer)
and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:13:53 +01:00
Michael Grant
104a0cee99 server-client: fix damage being composed twice per redraw pass
`(~c->flags & CLIENT_ALLREDRAWFLAGS)` - for a multi-bit mask, ~x & MASK
means "at least one of these bits is unset" (almost always true), not
"none of these bits are set" as the comment and surrounding logic
clearly intend. Every floating-pane drag command unconditionally sets
CLIENT_REDRAWBORDERS (server_redraw_window_borders(), called from
cmd-resize-pane.c/cmd-join-pane.c/cmd-split-window.c) alongside
reporting window damage, so this fallback fired on every single drag
step: redraw_client_damage(c) ran here, then ran again a few lines
later at the CLIENT_ALLREDRAWFLAGS block for the exact same
rectangles. Confirmed via the server's own -vv log: a 6-step drag
produced 12 "composing damage" lines in matched pairs (identical
position and size, milliseconds apart) with no fix, 6 with it.

No memory-safety issue - redraw_client_damage() only reads w->damage,
never frees it - just wasted work rendering the same rectangles twice
per pass.

Fix: `(c->flags & CLIENT_ALLREDRAWFLAGS) == 0`, matching the comment's
actual intent.

regress/floating-pane-drag-no-double-composite.sh drags a floating
pane and asserts no two consecutive "composing damage" log lines share
the same position and size (comparing both together, since distinct
drag steps commonly share the same rectangle size and only the
position differs). Verified failing 3/3 against the pre-fix code (all
6 rectangles doubled each run) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:09:28 +01:00
Michael Grant
a7f74730d7 server-client: don't let a drag's own sync-start defer its redraw
server_client_key_callback()'s mouse-drag dispatch opens a
synchronized-output frame (tty_sync_start()) before running the drag
callback, on every single drag motion event - deliberately, so a fast-
path write and any later damage-composed correction land in one atomic
terminal update instead of two visible frames. But
server_client_check_redraw() later in the same pass checks
EVBUFFER_LENGTH(tty->out) != 0 to decide whether to defer this pass's
redraw, and nothing drains tty->out in between (the actual write
happens later, via libevent) - so the frame-open sequence just queued
(8 bytes: "\033[?2026h" on a synchronized-output-capable terminal)
makes that check see "outstanding output" and defer against itself,
escalating the drag's damage to a full-window redraw on every single
motion event. Confirmed via the server's own -vv log: a 6-step drag
produced five "redraw deferred (8 left)" lines, one per motion event,
with no fix.

Fix: record how much was already queued at the instant the sync frame
opened (tty->sync_offset), and have the redraw check discount
anything queued after that point - it's already part of the frame
this pass is committed to flushing, not a reason to defer. If the
buffer was genuinely non-empty before the frame opened, sync_offset
holds that real backlog and deferral still happens correctly.

regress/floating-pane-drag-sync-no-self-defer.sh drags a floating pane
on a synchronized-output-capable terminal and asserts the server log
never shows the self-inflicted 8-byte deferral. Verified failing 3/3
against the pre-fix code (5 occurrences per run, matching the manual
-vv repro) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:06:09 +01:00
Michael Grant
4a1e445b38 window-copy: redraw pane styles on a scrollbar-driven focus change
window_copy_scroll() (called from both scrollbar-slider-drag paths -
window_copy_cmd_scroll_to_mouse() and copy-mode -S) calls
window_set_active_pane() to switch focus to the dragged pane, but was
the one caller in the whole codebase that did this without pairing it
with window_redraw_active_switch() first, and without falling back to
a full server_redraw_window() either. Every other window_set_active_
pane() caller does one or the other.

This used to be harmless because window_set_active_pane() itself did
an unconditional full redraw on every active-pane change - narrowing
that to borders/status-only for the non-zoomed case (this branch) made
pane *body* colours only repaint when something sets PANE_REDRAW,
which is exactly what window_redraw_active_switch() does by comparing
cached window-style/window-active-style colours. Without it, dragging
an inactive pane's scrollbar slider changes which pane is active
(borders and status update immediately) while both panes keep their
stale body colours until an unrelated redraw happens to touch them.

Fix: call window_redraw_active_switch() immediately before window_set_
active_pane(), matching the established pattern (e.g.
cmd-resize-pane.c's mouse-drag handler).

regress/window-copy-scrollbar-drag-focus-style.sh sets clearly
distinguishable window-active-style/window-style backgrounds, drags
the *inactive* pane's scrollbar slider, and checks - via an attached
client's own received bytes, since window-style is applied during
redraw composition rather than stored in the grid, so capture-pane
alone would not reflect it - that the newly active pane immediately
shows the active-style colour. Verified failing 3/3 against the
pre-fix code (showing both panes with swapped/stale colours, exactly
as reported) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:01:52 +01:00
Michael Grant
9a3aa26a33 screen-write: clip negative floating-pane offsets before reporting damage
screen_write_redraw_cb() passed wp->xoff/wp->yoff straight through as
u_int to redraw_damage_window(). Both are genuinely signed and can be
negative for a floating pane positioned partly off the window's left
or top edge (layout_floating_args_parse() explicitly allows -X/-Y
down to -sx/-sy). A negative value wraps to a huge u_int,
redraw_damage_window()'s first bounds check (x >= w->sx) rejects the
whole rectangle, and nothing gets redrawn - not even the pane's
visible portion.

This is broader than just the alternate-screen-exit case that first
surfaced it: screen_write_pane_is_obscured() routes any scrolling
output in such a pane through this same callback, so a partly
off-screen floating pane lost every scroll repaint, not just its
post-alternate-screen one.

Fixed at the call site (matching the existing correct reference
pattern in window_pane_damage_floating(), window.c): compute in signed
int, clip negative offsets to the window's own origin and shrink the
corresponding size to match, then convert to u_int only once the
rectangle is known to be sane. Left redraw_damage_window()'s own
signature alone - it has a second caller with an inclusive-bounds
convention that a signature change would need to reconcile, and the
bug is specific to this call site not clamping before converting.

regress/floating-pane-offscreen-alternate-redraw.sh creates a floating
pane with -X -5 (partly off the left edge), cycles it through the
alternate screen, and checks an attached client's own received bytes
(not capture-pane, which reads the grid directly and would pass
regardless of whether the client was ever told to redraw) show the
primary screen's content correctly restored in the pane's visible
columns. Verified failing 3/3 against the pre-fix code and passing
5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:53:32 +01:00
Michael Grant
1ce64bd087 screen-redraw: fix wide characters getting blanked on the right clip edge
redraw_damage_grow_span_clip() guards its left-edge growth against
walking onto an unrelated wide character's base cell (via
redraw_span_left_grow_ok(), commit 6f65c318), but the right edge was
left growing unconditionally, on the reasoning that "tty_draw_line()
already draws a wide character in full even when the requested range
clips off its trailing padding half" - that reasoning was wrong.

When the right edge lands cleanly on a fresh character's base cell (a
character fully outside the range), growing right pulls in only that
base column. tty_draw_line()'s tty_draw_line_get_empty() then sees
gc->data.width > nx for the truncated remainder and treats it as an
empty cell to clear via tty_draw_line_clear() - a different code path
than the leading-padding-clear the left-edge fix reasoned about, but
exactly as destructive: it blanks a character that was never inside
the damage rectangle at all.

The padding check the left-edge fix added doesn't actually care about
direction - "is the cell at this scene x-coordinate a padding cell" is
the same question whether asked of a range's start or its end - so
renamed redraw_span_left_grow_ok() to redraw_span_cell_is_padding()
and apply it symmetrically to both edges.

regress/floating-pane-drag-wide-character-right.sh mirrors the
existing left-edge test for this edge: constructs the exact column
parity needed (the vacated rectangle's right edge landing on a base
cell) deterministically rather than relying on luck, verified failing
3/3 against the pre-fix code and passing 5/5 against the fix. The
existing left-edge test continues to pass unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:47:21 +01:00
Michael Grant
6f65c31887 screen-redraw: fix wide characters getting blanked by damage-clip growth
redraw_damage_grow_span_clip() widens a composed damage rectangle's
clipped edge by one cell whenever it isn't already at the span's own
boundary, to pull a wide character's base half back into range when
the edge happens to land on its padding half. This was unconditional -
it never checked which half it was actually touching.

When the edge instead already lands cleanly on a fresh character's
base cell (nothing to pull in - that character is simply outside the
rectangle), growing left walks one cell further, into the *previous*,
unrelated character's padding half, and blanks it: tty_draw_line()
treats any leading padding cell in its draw range as proof the range
starts mid-character and clears it (the "If there is padding at the
start, we must have truncated a wide character" branch, tty-draw.c).
Net effect: redrawing a damage rectangle can destroy a wide character
sitting just outside it, on whichever side the edge's column parity
happens to be unlucky.

This surfaced via dragging a display-popup pane (now backed by a
floating pane upstream, since popups were folded into the general
floating-pane mechanism) away from wide-character content, but it is a
general bug in any damage-composed redraw, not popup-specific: an
identical drag against an ordinary floating pane reproduces it
whenever the parity lines up the same way, confirmed while building
the new regress test below. It only looked popup-specific because
display-popup's new floating-pane-backed drag moves the pane on the
very first motion event, reliably hitting the bad parity, whereas the
old (now-removed) popup.c's drag handler didn't move on the first
event and tended to land on the safe parity by chance.

Also confirmed this is not a tmux/terminal wide-character width
disagreement: utf8_width()'s only override table is emoji/regional-
indicator ranges (no CJK), so a codepoint like U+754C falls straight
through to wcwidth(); the -vv log's own "wcwidth(0754C) returned 2"
line during the repro confirms tmux and libc agree on width 2. The bug
is in the redraw-clipping logic, not the width calculation.

Fix: only grow the left edge when the cell actually there is a padding
cell (redraw_span_left_grow_ok()), for the span types that can contain
one - pane content, a pane's status line, and a menu, the only spans
drawn via tty_draw_line() against a real backing screen. Border and
scrollbar spans draw single synthesized cells directly and can never
split a wide character, so their unconditional growth is untouched.
The right edge doesn't need the same guard: tty_draw_line() already
draws a wide character in full even when the requested range clips off
its trailing padding half, so growing right is at worst redundant,
never destructive.

regress/floating-pane-drag-wide-character.sh reproduces this
deterministically with an ordinary floating pane (not a popup, since
the bug isn't popup-specific): it creates the pane, checks its real
resulting position (rather than hand-computing the border-framing
offset), and retries one column over if needed until the vacated
rectangle's left edge lands on a base cell - the bad-parity case every
earlier manual repro landed on only by chance. Verified failing 3/3
against the pre-fix code and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 08:42:25 +01:00
Michael Grant
bc20fc6da4 regress: remove tests for the now-obsolete popup-as-overlay drag mechanic
display-popup is now a compatibility shim over a floating pane (upstream
removed the whole overlay/popup machinery in favour of floating panes -
see the preceding merge). These three tests moved/resized a popup with
hardcoded mouse coordinates tuned to the old popup implementation's
exact positioning and clamping math; under the new floating-pane-backed
implementation the popup's actual on-screen geometry shifts slightly,
so the hardcoded grab points miss the border entirely and the popup
never moves. This isn't a redraw regression: the underlying drag
mechanism (resize-pane -M / move-pane -M) is already covered by
floating-pane-drag-scrollbar-strip.sh and still works correctly for a
popup in popup-drag-wide-character.sh, which is unaffected and stays.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:30:22 +01:00
Michael Grant
d74b980d80 regress: give check-names.sh a clean shell
The test's session used the default $SHELL - the developer's own
interactive bash, whose PS1 embeds an OSC 0 title-setter for tmux/xterm
TERM types (common on this machine, not test-controlled). That redrew
the terminal title on every prompt, clobbering the test's own
OSC-2-set-title assertions regardless of how long it waited afterwards.
Use bash --noprofile --norc, matching the pattern already used by
style-trim.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:25:07 +01:00
Michael Grant
ac4daa57c9 Merge branch 'master' into redraw-damage-rectangles
# Conflicts:
#	popup.c
#	screen-redraw.c
#	tmux.h
#	tty.c
#	window.c
2026-09-21 23:25:01 +01:00
Nicholas Marriott
c3326194b6 Turn on Linux arm also. 2026-09-21 22:34:05 +01:00
Nicholas Marriott
6a8c21fb41 macOS parallel issues. 2026-09-21 22:27:43 +01:00
Nicholas Marriott
326b9a4f16 Another timing race. 2026-09-21 22:18:44 +01:00
Nicholas Marriott
6871fcb157 Add missing test cleanup. 2026-09-21 22:16:06 +01:00
Nicholas Marriott
1d9943c6e6 Keep window alive. 2026-09-21 22:12:33 +01:00
Nicholas Marriott
f5c4998db3 Make regress parallel friendly. 2026-09-21 22:00:47 +01:00
Nicholas Marriott
cc81429d8a Do not make test so timing sensitive. 2026-09-21 21:51:30 +01:00
tmux update bot
3a6c2e7877 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not do anything in session_detach and rely on the caller destroying the session, otherwise grouped sessions can reuse a session with no windows. From Jeong, Heon in GitHub issue 5620.
2026-09-21 18:56:57 +00:00
nicm
21b3da3bab Do not do anything in session_detach and rely on the caller destroying
the session, otherwise grouped sessions can reuse a session with no
windows. From Jeong, Heon in GitHub issue 5620.
2026-09-21 18:56:55 +00:00
tmux update bot
a7bd2415ba Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add support for borderless menus, like panes. GitHub issue 5447 from harikp2002 at gmail dot com.
  Do not trim all lines to make a zero line grid which reflow does not like, from Kaixuan Li.
  Do not unzoom when resizing a floating pane that was created with -A. Similarly, skip hidden floating panes when changing Z order. Reported by Clark Wang.
2026-09-21 13:44:41 +00:00
nicm
dda0e4d489 Add support for borderless menus, like panes. GitHub issue 5447 from harikp2002
at gmail dot com.
2026-09-21 13:44:39 +00:00
nicm
e361b8f8ff Do not trim all lines to make a zero line grid which reflow does not like, from
Kaixuan Li.
2026-09-21 13:44:39 +00:00
nicm
9be3a35178 Do not unzoom when resizing a floating pane that was created with -A.
Similarly, skip hidden floating panes when changing Z order. Reported by Clark
Wang.
2026-09-21 13:44:39 +00:00
Nicholas Marriott
324c636c5c Test for 5447 and for copy-mode zero line crash. 2026-09-21 13:14:44 +01:00
Nicholas Marriott
166851bf87 Update tests and bits after popups removed. 2026-09-21 12:05:37 +01:00
Nicholas Marriott
5aa17a0cfa Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Remove popups and all the associated overlay machinery (they were the last user of it). display-popup stays but becomes an (undocumented) compatibility command to open a floating pane.
  Fix typo (too few 0s) in CLIENT_CONTROL_DISCARD, from someone in GitHub issue 5622.
2026-09-21 12:03:10 +01:00
nicm
34cd5da4e3 Remove popups and all the associated overlay machinery (they were the last user
of it). display-popup stays but becomes an (undocumented) compatibility command
to open a floating pane.
2026-09-21 11:01:12 +00:00
nicm
3d5f946f35 Fix typo (too few 0s) in CLIENT_CONTROL_DISCARD, from someone in GitHub issue
5622.
2026-09-21 11:01:12 +00:00
Nicholas Marriott
81794f3047 Test for newp -A and resize. 2026-09-21 11:39:09 +01:00
Nicholas Marriott
541ca0e844 Merge branch 'master' into redraw-damage-rectangles 2026-09-21 11:09:55 +01:00
Thomas Adam
c612f35fd8 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  build tmux with debug symbols, ok claudio nicm
  If no floating panes, reset default starting position, and reset if any part of the pane goes outside the window.
  Empty string for invalid old-style formats causes old iTerm2 versions to crash, so emit "0000," instead.
  Fix session_*_flag format variables which loop over the windows (they should only be false if all windows do not have the flag, not the first one). GitHub issue 5599.
  Do not loop forever if someone tries to give WCHAR_MAX a width, GitHub issue 5602.
  Expand -c for run-shell like the other -c flags, reported by Saúl Nogueras.
  Reset layout manually instead of calling window_unzoom which can go down the notification path and end up double freeing the pane (this was previously removed in 2015 but added back to fix a problem with late destroy - this is a better fix). GitHub issue 5591 from Romain Francoise.
2026-09-20 20:05:57 +01:00
sthen
56b36d671e build tmux with debug symbols, ok claudio nicm 2026-09-20 12:06:27 +00:00
nicm
eee95e6479 If no floating panes, reset default starting position, and reset if any part of
the pane goes outside the window.
2026-09-20 12:06:27 +00:00
nicm
bbd00148de Empty string for invalid old-style formats causes old iTerm2 versions to crash,
so emit "0000," instead.
2026-09-20 12:06:27 +00:00
nicm
2e9189c01d Fix session_*_flag format variables which loop over the windows (they should
only be false if all windows do not have the flag, not the first one). GitHub
issue 5599.
2026-09-20 12:06:27 +00:00
nicm
d4dc032599 Do not loop forever if someone tries to give WCHAR_MAX a width, GitHub issue
5602.
2026-09-20 12:06:27 +00:00
nicm
d1d07f9790 Expand -c for run-shell like the other -c flags, reported by Saúl Nogueras. 2026-09-20 12:06:27 +00:00
nicm
b25af0a08c Reset layout manually instead of calling window_unzoom which can go down the
notification path and end up double freeing the pane (this was previously
removed in 2015 but added back to fix a problem with late destroy - this is a
better fix). GitHub issue 5591 from Romain Francoise.
2026-09-20 12:06:27 +00:00
Nicholas Marriott
1eed85c344 Change tests for 0000, for invalid layouts. 2026-09-20 09:37:59 +01:00
Nicholas Marriott
73042686d8 Test for 5599. 2026-09-20 09:19:35 +01:00
Nicholas Marriott
313ad6fdc5 Tests for 5602 and for run-shell -c. 2026-09-20 09:11:24 +01:00
Nicholas Marriott
594465fbe7 Regress test for 5591 and another one I forget. 2026-09-20 08:44:56 +01:00
Michael Grant
07d40d5fa9 cmd-attach-session: redraw pane styles when changing active pane
Call window_redraw_active_switch() before selecting a pane so differing window-style and window-active-style colours are updated immediately.\n\nAdd a regression test for selecting a pane through attach-session.
2026-09-14 04:36:57 +01:00
tmux update bot
e880cf63e0 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add rounded borders option for panes like popups.
2026-09-11 17:00:20 +00:00
nicm
325b40e8c4 Add rounded borders option for panes like popups. 2026-09-11 17:00:18 +00:00
tmux update bot
f95363aa83 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Rather than allowing floating panes to remain outside the window and invisible after resize, move them and resize them so they are fully inside the window. GitHub issue 5582 from Noam Stolero.
2026-09-11 12:58:31 +00:00
nicm
f8fc53b6f8 Rather than allowing floating panes to remain outside the window and
invisible after resize, move them and resize them so they are fully
inside the window. GitHub issue 5582 from Noam Stolero.
2026-09-11 12:58:29 +00:00
Nicholas Marriott
d215280b00 Add rounded border tests. 2026-09-11 11:17:35 +01:00
Nicholas Marriott
d859a0f410 Test for resizing floating panes, from Noam Stolero. 2026-09-11 09:16:55 +01:00
Nicholas Marriott
10d00cc7cb Modal pane test changes. 2026-09-11 09:10:14 +01:00
Nicholas Marriott
14a01dd337 Note changes from Heon Jeong. 2026-09-11 09:05:06 +01:00
tmux update bot
a958883625 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add remain-on-exit failed-key and a -D flag to new-pane to have a modal pane wait for Escape/C-c. Both to allow better compatibility with popups.
2026-09-10 13:04:13 +00:00
nicm
d202aa7ac1 Add remain-on-exit failed-key and a -D flag to new-pane to have a modal
pane wait for Escape/C-c. Both to allow better compatibility with
popups.
2026-09-10 13:04:11 +00:00
Nicholas Marriott
d9a5e82678 Fix tests. 2026-09-10 12:00:01 +01:00
tmux update bot
13c10f672c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a nesting limit for v1 layouts, reported by M Khalilov, fix based on issue 5572 from Afonso Januário. Also tweak some language while here.
2026-09-09 19:23:09 +00:00
nicm
b7c5030004 Add a nesting limit for v1 layouts, reported by M Khalilov, fix based on
issue 5572 from Afonso Januário. Also tweak some language while here.
2026-09-09 19:23:07 +00:00
Nicholas Marriott
80e87aaf16 Merge branch 'master' into redraw-damage-rectangles 2026-09-09 14:05:48 +01:00
Nicholas Marriott
4839123510 Tweak changes. 2026-09-09 13:50:42 +01:00
Nicholas Marriott
9b3268a2a0 Bump version. 2026-09-09 13:28:07 +01:00
Nicholas Marriott
5aeacf1cae Update CHANGES. 2026-09-09 13:06:56 +01:00
tmux update bot
7774731b95 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Draw bottom border with pane status line at the top.
  Bound the memory used by buffered control mode command replies (to 64 MB), GitHub issue 5565 from kagari dot shusei at proton dot me.
  Do not adjust prompt position on invalid Unicode, from Kaixuan Li.
2026-09-09 10:12:52 +00:00
nicm
0544f0b210 Draw bottom border with pane status line at the top. 2026-09-09 10:12:50 +00:00
nicm
a3129249d1 Bound the memory used by buffered control mode command replies (to 64
MB), GitHub issue 5565 from kagari dot shusei at proton dot me.
2026-09-09 10:12:50 +00:00
nicm
930c81b819 Do not adjust prompt position on invalid Unicode, from Kaixuan Li. 2026-09-09 10:12:50 +00:00
Nicholas Marriott
fc695b4e86 Test for v1 nesting limit, based on a changed from Afonso Januário. 2026-09-09 10:01:32 +01:00
Nicholas Marriott
36fb7ab084 Tests of border. 2026-09-09 09:32:14 +01:00
tmux update bot
90a7dd4654 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add new layout format which includes floating panes. The new format is now a JSON subset which is less fragile and easier to handle than the old custom format. The old (version 1) format is still supported for control mode clients for now - they must set the new-layouts flag to receive the new format. From Dane Jensen.
2026-09-09 08:27:51 +00:00
nicm
bf43fdc0c7 Add new layout format which includes floating panes. The new format is
now a JSON subset which is less fragile and easier to handle than the
old custom format. The old (version 1) format is still supported for
control mode clients for now - they must set the new-layouts flag to
receive the new format. From Dane Jensen.
2026-09-09 08:27:49 +00:00
Nicholas Marriott
077d6e53cd Test for invalid prompt. 2026-09-09 08:53:51 +01:00
Nicholas Marriott
4e19f7a8dc New layout format regress test, from Dane Jensen. 2026-09-09 08:04:23 +01:00
tmux update bot
7941f7b863 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  The client for the layout format is the client we are sending it to, not the target client.
2026-09-08 21:40:59 +00:00
nicm
c7102ebbe5 The client for the layout format is the client we are sending it to, not
the target client.
2026-09-08 21:40:57 +00:00
tmux update bot
1b87cb752e Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add new-layouts flag for control clients so they can default to old layouts for backwards compatibility.
2026-09-08 11:38:06 +00:00
nicm
d9692f7ecf Add new-layouts flag for control clients so they can default to old
layouts for backwards compatibility.
2026-09-08 11:38:04 +00:00
Nicholas Marriott
98637576bd Test for new-layouts flag. 2026-09-08 11:27:25 +01:00
Nicholas Marriott
d8bdb23108 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a function to find last pane index, from Dane Jensen.
  Add a parser for a subset of JSON, will be used for new layout strings (and maybe some other stuff), from Dane Jensen.
  Add capture-pane -I to show times.
2026-09-08 11:25:17 +01:00
nicm
2191c9b172 Add a function to find last pane index, from Dane Jensen. 2026-09-08 10:22:40 +00:00
nicm
01d1d6580a Add a parser for a subset of JSON, will be used for new layout strings
(and maybe some other stuff), from Dane Jensen.
2026-09-08 10:22:40 +00:00
nicm
96f4c7b9fa Add capture-pane -I to show times. 2026-09-08 10:22:40 +00:00
Nicholas Marriott
6a24d5437b JSON test. 2026-09-08 09:36:26 +01:00
Nicholas Marriott
8396a63128 capture-pane -I test. 2026-09-08 08:32:05 +01:00
tmux update bot
d44bfda26d Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Error on invalid relative targets, such as +foo or -0. GitHub issue 5576 from imcusg at gmail dot com.
  Do not silently make a session monitor if the target is unknown, GitHub issue 5575 from zzchun12826 at gmail dot com.
2026-09-07 17:56:25 +00:00
nicm
00f3899aea Error on invalid relative targets, such as +foo or -0. GitHub issue 5576
from imcusg at gmail dot com.
2026-09-07 17:56:23 +00:00
nicm
557967c36c Do not silently make a session monitor if the target is unknown, GitHub
issue 5575 from zzchun12826 at gmail dot com.
2026-09-07 17:56:23 +00:00
Nicholas Marriott
73db0a54e5 Tests from GitHub issue 5576. 2026-09-07 13:05:38 +01:00
Michael Grant
f41b983e04 screen-write, tty, popup, window: fix remaining untested damage gaps
Two more fixes based on Michael K. Darling's branch
(github.com/darlingm/tmux, pr5516-regression-fixes), taken as-is -
neither is caught by any test in regress/ yet, found by code review
rather than a failing test:

- screen_write_redraw_cb() (screen-write.c) reported damage for only a
  single row, using ttyctx->ocy as if every fallback redraw were a
  single-cell write. But it's also the callback for cases that can
  legitimately span many rows - a large scroll-region fallback
  (tty_redraw_region(), when tty_large_region() or the pane is
  obscured), a full reset, and entering/leaving the alternate screen.
  For those, only the top row of the affected area ever got marked as
  damaged, leaving the rest stale until an unrelated redraw happened to
  cover it. Changed the shared tty_ctx_redraw_cb typedef to carry
  (py, ny) - the actual row range - and updated every call site to pass
  the range it actually knows about, instead of hardcoding a single
  row.

- window_pane_redraw_floating() (window.c) never refreshed the status
  line after moving/resizing a floating pane, so a status format
  depending on that pane's geometry (e.g. #{pane_width}) could go
  stale until an unrelated status refresh happened. Added a
  server_status_window(w) call.

Also confirmed the window_pane_scrollbar_intersects() parameter
naming cleanup (loop -> wp) discussed earlier was already done in an
earlier "Cleanup." commit - nothing left to do there.

All 9 tests in regress/ plus the two pre-existing floating-pane tests
plus a further 19-test sweep of redraw/tty/input/sync-adjacent
regress tests pass.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:49:46 +01:00
Michael Grant
48e33179d4 server-client, popup, screen-redraw: fix damage-system regressions
Fixes the four bugs caught by the regression tests added in aed1209c
(popup-drag-status-line.sh, popup-drag-wide-character.sh,
popup-drag-pane-prompt.sh, switch-client-redraw.sh), based on fixes
from Michael K. Darling (github.com/darlingm/tmux, pr5516-regression-
fixes), reviewed and adapted:

- server-client.c: server_client_set_session()'s check for whether the
  client's window actually changed compared old->curw to s->curw, but
  when old == s these read the same, already-updated field, so a
  same-session window switch was never detected. Compare against the
  client's own cached redraw scene instead (redraw_client_has_window(),
  new in screen-redraw.c/tmux.h). Taken from darlingm as-is.

- popup.c: popup_damage() only translated a popup's client-coordinate
  rectangle into window coordinates, so a popup dragged across the
  status line never triggered a status-line redraw once it moved away -
  status_redraw()'s own "skip if content unchanged" optimization
  suppressed it, since only the popup moved, not the status content.
  Now detects overlap with the status line and forces a redraw via the
  existing (previously unused) CLIENT_REDRAWSTATUSALWAYS flag, and
  properly clips the reported rectangle to the pane area for
  status-at-top/bottom/off. Taken from darlingm as-is.

- screen-redraw.c: redraw_draw_damage_rect() clipped a span to a damage
  rectangle's raw geometric edges, which have no idea what's in the
  grid, so a clip edge could land mid-character and tear a wide
  character in half. Added redraw_damage_grow_span_clip(): widen the
  clip by one cell on each edge that isn't already at the span's own
  boundary. Reimplemented simpler than darlingm's version (which walked
  grid cells per span type via a switch and direct grid lookups) -
  since no grid cell is ever wider than two columns, an unconditional
  one-cell margin is always enough to pull a split character back in,
  with no need to inspect grid content at all.

- screen-redraw.c: redraw_draw_damage_rect() also never re-overlaid a
  pane's active in-pane prompt after drawing its underlying content, so
  damage crossing a prompt row erased it until an unrelated redraw
  restored it. Factored the existing full-redraw prompt-building code
  into a shared redraw_make_pane_prompt() helper and added
  redraw_damage_draw_pane_prompt(), which recomposes the prompt over
  the drawn range. Taken from darlingm as-is.

All 9 regression tests in regress/ now pass. redraw_damage_grow_span_clip
was verified independently by disabling it and confirming
popup-drag-wide-character.sh reproduces its original failure.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:25:29 +01:00
Michael Grant
aed1209c02 regress: add Michael K. Darling's damage/redraw regression tests
From github.com/darlingm/tmux, branch pr5516-regression-fixes. Adds 9
regression tests covering gaps found in the redraw-damage-rectangles
branch: screen-write full/region redraw fallback, same-session window
switches, wide-character clipping at damage edges, pane prompts and
status lines surviving damage, floating-pane status format refresh,
and multi-client damage delivery.

redraw-multiclient.sh is adapted here to use ASCII pane borders
(pane-border-lines simple) instead of darlingm's original UTF-8
borders: the original reliably "failed" under this test's nested
tmux-in-tmux harness (relaying through an outer tmux client) due to
that harness mis-rendering a cell that held a multi-byte UTF-8 border
character being overwritten by later plain content - confirmed to be
a nested-relay artifact, not a real bug, by replaying the identical
drag sequence against a real terminal (xterm), where it never
reproduces. ASCII borders avoid the artifact; the test still reliably
catches the real "damage consumed by only one client" bug it targets
(verified by reintroducing that bug and confirming the test fails).

The other 8 tests are added verbatim from darlingm's branch. Four of
them (popup-drag-status-line.sh, popup-drag-wide-character.sh,
popup-drag-pane-prompt.sh, switch-client-redraw.sh) currently FAIL on
this branch, since the source fixes they test for have not been
merged yet - only the tests are being added here.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:14:49 +01:00
Michael Grant
6739bd03f5 Merge remote-tracking branch 'origin/master' into redraw-damage-rectangles
# Conflicts:
#	server-client.c
#	window.c
2026-08-25 08:08:17 +01:00
Michael Grant
f66eeef8a5 Added a fallback in server_client_check_redraw() as recommended by codex to catch an unserviced edge-case. 2026-08-24 13:36:34 +01:00
Michael Grant
2d220d9a7f Cleanup. 2026-08-24 13:25:12 +01:00
Michael Grant
da4895d559 popup: damage-based redraw during drag/resize instead of full client redraw
popup_handle_drag()'s MOVE and SIZE branches each called
server_redraw_client(c) unconditionally, redrawing the client's entire
window on every drag step. Report damage for just the popup's old and
new rectangle instead, via a new popup_damage() (translating from raw
client/tty coordinates into window coordinates), and set
CLIENT_REDRAWOVERLAY so the popup itself still redraws.
2026-08-22 16:54:25 +01:00
Michael Grant
b254f557e4 md-join-pane, cmd-split-window, cmd-resize-pane: use shared floating-pane
redraw instead of full client redraw

The three interactive mouse-drag paths that move or resize a floating
pane (move-pane -M's Alt-drag, split-window/new-pane's interactive
resize, and resize-pane's own border drag) each unconditionally called
server_redraw_window(w), redrawing every pane in the window for a change
that only ever disturbs the floating pane's own old and new rectangle.
Switch all three to window_pane_redraw_floating().
2026-08-22 16:53:08 +01:00
Michael Grant
b26eeb5229 window: redraw only borders/status on active-pane change
window_set_active_pane() unconditionally called server_redraw_window(w)
on every active-pane change, redrawing every pane's content even though
only the previous and new active pane's border/status appearance
actually changed. Unzooming (which does change every pane's geometry)
still gets the full redraw; otherwise this now only redraws borders and
status.
2026-08-22 16:42:56 +01:00
Michael Grant
48cdd85886 server-client: consume window damage during the normal redraw pass
redraw_client_damage() (added previously, unused until now) is called
from server_client_check_redraw()'s normal redraw pass, and
server_client_any_pane_redraw() now also checks for pending window
damage so a client with only damage (no PANE_REDRAW/PANE_REDRAWSCROLLBAR
flags) still gets its redraw pass run.

server_client_check_redraw() now returns whether the redraw was deferred
(waiting for outstanding tty output to drain) rather than performed. A
deferred redraw no longer escalates to a full CLIENT_REDRAWWINDOW to
avoid losing what was pending - server_client_loop() now only clears
PANE_REDRAW, PANE_REDRAWSCROLLBAR and window damage once every client
viewing a window actually drew this pass (tracked via a new per-window
redraw_deferred flag), otherwise they're left in place and retried in
their normal, narrowly-scoped form.

server_client_set_session() now redraws only if the client's session or
current window actually changed, not on every call (e.g. switch-client
-t= from clicking a pane name in the status line resolves here even
when nothing besides the active pane changed).

A drag callback's mouse_drag_update() now opens a sync region itself
(tty_sync_start()) before its first write, so a fast-path write it makes
directly and a later correction arriving via redraw_client_damage() end
up in the same atomic terminal update instead of two visible frames.
2026-08-22 16:36:25 +01:00
Michael Grant
f255f089fc window: add floating-pane damage/redraw helpers
window_pane_redraw_floating() reports damage for only a floating pane's
old and new rectangle (via redraw_damage_window(), grown by one cell to
cover its border frame - see the "floating" case in screen-redraw.c),
instead of the caller falling back to a full window redraw. Any other
pane whose *scrollbar strip* - not its whole body - intersects either
rectangle still gets PANE_REDRAWSCROLLBAR directly, since scrollbars
aren't covered by the damage system.
2026-08-22 16:32:21 +01:00
Michael Grant
d6074895bb screen-redraw, screen-write: add damage-rectangle tracking and composition
Introduce a per-window list of damaged rectangles (struct redraw_damage)
and redraw_damage_window() to record them, redraw_client_damage() to
consume them by composing exactly the damaged cells (via a new
redraw_draw_damage_rect(), which also force-refreshes any pane-status
span it touches, since window_make_pane_status()'s content-diff check
has no way to know the physical cells were disturbed by something else).

redraw_draw_span() now takes an explicit [clip_x, clip_x + clip_n) range
instead of always drawing a span's full width, so a damage rectangle can
redraw just the portion of a span it actually covers.

screen_write_redraw_cb() - the fallback when a write can't be applied
directly to the terminal - now reports damage for just the affected row
via this mechanism, instead of unconditionally flagging the whole pane
for a full redraw.
2026-08-22 15:45:29 +01:00
Michael Grant
faba411289 layout: only mark scrollbar for redraw when the pane actually changed
wp->flags |= PANE_REDRAWSCROLLBAR was set unconditionally whenever a pane
  reserved a scrollbar, even if layout_fix_panes() left its geometry
  completely unchanged - forcing a needless scrollbar redraw on every layout
  pass. Move it inside the existing "did this pane's geometry actually
  change" check.
2026-08-22 13:51:03 +01:00
125 changed files with 8407 additions and 4458 deletions

View File

@@ -27,10 +27,10 @@ jobs:
runner: ubuntu-24.04
make: make
configure: --enable-utf8proc --enable-asan
#- name: ubuntu-24.04-arm64
# runner: ubuntu-24.04-arm
# make: make
# configure: --enable-utf8proc --enable-asan
- name: ubuntu-24.04-arm64
runner: ubuntu-24.04-arm
make: make
configure: --enable-utf8proc --enable-asan
- name: macos-26-arm64
runner: macos-26
make: gmake
@@ -72,14 +72,14 @@ jobs:
- name: build
run: |
sh autogen.sh
sh autogen.sh
./configure ${{ matrix.configure }}
${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: test
run: |
cd regress
${{ matrix.make }}
${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: logs
if: failure()

15
CHANGES
View File

@@ -33,9 +33,11 @@ CHANGES FROM 3.7c TO 3.8
line;
- pane-border-status has top-floating and bottom-floating, and there are new
default bindings under C-b g for common move and resize operations;
default bindings under C-b g for common move and resize operations.
- floating panes are unzoomed before creation to avoid a crash.
* Layout strings now use a JSON subset format which includes floating panes.
The old format is still accepted; control mode clients receive old layouts
unless they set the new-layouts flag.
* Add support for themes and improve default colours:
@@ -126,6 +128,10 @@ CHANGES FROM 3.7c TO 3.8
* Add new-window -E, respawn-pane -E and respawn-window -E as more convenient
methods to create an empty pane (rather than using '' for the command).
* Add capture-pane -I to include the time each line entered history.
* Various memory leak fixes from Heon Jeong.
* Add a default C-b T binding to change the current pane title.
* Menus now belong to the window, so appear on all clients.
@@ -172,6 +178,8 @@ CHANGES FROM 3.7c TO 3.8
* The mouse option now defaults to on.
* The active-pane client flag has been removed.
* Fix send-keys -K so keys are inserted in the correct place in the input
queue, like keys from key bindings (issue 3476).
@@ -180,7 +188,8 @@ CHANGES FROM 3.7c TO 3.8
already exiting (Ben Maurer, issue 5357). Queue notifications so they are not
sent inside %begin/%end (issue 5458), do not let a stuck client prevent the
server from exiting (issue 5444), and reset control mode offsets when a pane
is respawned (issue 5498).
is respawned (issue 5498). Bound buffered command replies so a control mode
client that stops reading cannot consume unlimited memory (issue 5565).
* Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180).

View File

@@ -174,6 +174,7 @@ dist_tmux_SOURCES = \
input-keys.c \
input.c \
job.c \
json.c \
key-bindings.c \
key-string.c \
layout-custom.c \
@@ -187,7 +188,6 @@ dist_tmux_SOURCES = \
options-table.c \
options.c \
paste.c \
popup.c \
proc.c \
prompt-history.c \
prompt.c \

View File

@@ -35,7 +35,7 @@ The usual local layout is:
```sh
cd /some/where/useful
git clone https://github.com/tmux/tmux.git tmux-portable
git clone https://github.com/ThomasAdam/tmux-obsd.git tmux-openbsd-cutover
git clone https://github.com/tmux/tmux-openbsd-cutover.git tmux-openbsd-cutover
```
The exact directory names do not matter, but the examples below use:
@@ -56,18 +56,19 @@ The cutover repository has three important branches:
# Adding the OpenBSD remote to portable
In the portable repository, add the cutover repository as a remote:
In the portable repository, add the published cutover repository as a remote.
This works regardless of which branch is checked out in a local cutover clone:
```sh
cd /path/to/tmux-portable
git remote add tmux-openbsd /path/to/tmux-openbsd-cutover
git remote add tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
git config remote.tmux-openbsd.tagOpt --no-tags
```
If the remote already exists, update it instead:
```sh
git remote set-url tmux-openbsd /path/to/tmux-openbsd-cutover
git remote set-url tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
git config remote.tmux-openbsd.tagOpt --no-tags
```
@@ -77,6 +78,32 @@ Fetch the cutover master branch explicitly:
git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master
```
To merge unpublished changes from a local cutover clone instead, first ensure
it has an up-to-date local `master` branch. A normal clone may check out
`automation` and have only `origin/master`; fetching `master` from that clone
will then fail with `couldn't find remote ref master`.
With a clean cutover working tree:
```sh
cd /path/to/tmux-openbsd-cutover
git fetch --no-tags origin
git switch master
git merge --ff-only origin/master
```
`git switch master` creates a tracking branch from `origin/master` if there is
no local `master` yet. If the fast-forward fails, reconcile the local cutover
changes before continuing; do not reset them away.
Then, in portable, point the remote at that clone and fetch its local `master`:
```sh
cd /path/to/tmux-portable
git remote set-url tmux-openbsd /path/to/tmux-openbsd-cutover
git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master
```
# Automated syncing
The normal sync is performed by the GitHub Actions workflow in the
@@ -99,7 +126,9 @@ OpenBSD changes.
If the workflow fails while merging into portable, do the merge locally and
push the result.
Start from an up-to-date portable master:
Start with a clean working tree and an up-to-date portable master. If a merge
is already in progress, skip to resolving conflicts, or abort it before
starting again:
```sh
cd /path/to/tmux-portable
@@ -108,10 +137,15 @@ git checkout master
git pull --ff-only origin master
```
Fetch the cutover branch:
Fetch the published cutover branch. Update an existing remote as well, since
it may point at a local clone without a `master` branch or with a stale one:
```sh
git remote add tmux-openbsd /path/to/tmux-openbsd-cutover 2>/dev/null || true
if git remote get-url tmux-openbsd >/dev/null 2>&1; then
git remote set-url tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
else
git remote add tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
fi
git config remote.tmux-openbsd.tagOpt --no-tags
git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master
```
@@ -122,7 +156,22 @@ Merge it:
git merge --no-ff --log refs/remotes/tmux-openbsd/master
```
Resolve conflicts by deciding whether portable or OpenBSD owns the file.
If merging a local cutover branch instead, use the local-clone preparation
and fetch commands above in place of this fetch block.
When the merge reports conflicts, it leaves the merge in progress. List the
unresolved files, edit the conflict markers to combine the required portable
and OpenBSD changes, then stage each resolved file:
```sh
git diff --name-only --diff-filter=U
git diff -- path/to/file
git add path/to/file
```
For files that should come entirely from one side, decide whether portable or
OpenBSD owns the file before using the commands below. They replace the whole
file, including changes outside the conflicting hunks.
Useful commands:
@@ -140,11 +189,25 @@ git add path/to/file
This takes the OpenBSD/cutover version of a conflicted file.
For a modify/delete conflict, the side that deleted the file has no version
to check out. For example, portable generates `Makefile` using autotools and
does not track OpenBSD's `Makefile`. If Git reports that `Makefile` was deleted
in HEAD and modified in cutover, keep the portable deletion with:
```sh
git rm -- Makefile
```
This removes the OpenBSD file left by the merge; regenerate the portable
`Makefile` with your usual configure command before building. Use this only
for the unmerged OpenBSD file, not an existing generated build file.
Before committing, inspect the result:
```sh
git status
git diff --check
git diff --cached --check
git diff --cached --stat
```

View File

@@ -91,8 +91,10 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag,
wp = target.wp;
if (wl != NULL) {
if (wp != NULL)
if (wp != NULL) {
window_redraw_active_switch(wp->window, wp);
window_set_active_pane(wp->window, wp, 1);
}
session_set_current(s, wl);
if (wp != NULL)
cmd_find_from_winlink_pane(current, wl, wp, 0);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-capture-pane.c,v 1.68 2026/07/20 11:16:33 nicm Exp $ */
/* $OpenBSD: cmd-capture-pane.c,v 1.69 2026/09/08 07:31:59 nicm Exp $ */
/*
* Copyright (c) 2009 Jonathan Alvarado <radobobo@users.sourceforge.net>
@@ -42,8 +42,8 @@ const struct cmd_entry cmd_capture_pane_entry = {
.name = "capture-pane",
.alias = "capturep",
.args = { "ab:CeE:FHJLMNpPqRS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] "
.args = { "ab:CeE:FHIJLMNpPqRS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHIJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] "
"[-S start-line] " CMD_TARGET_PANE_USAGE,
.target = { 't', CMD_FIND_PANE, 0 },
@@ -200,7 +200,7 @@ cmd_capture_pane_pending(struct args *args, struct window_pane *wp,
tmp[0] = line[i];
tmp[1] = '\0';
} else
xsnprintf(tmp, sizeof tmp, "\\%03hho", line[i]);
snprintf(tmp, sizeof tmp, "\\%03hho", line[i]);
buf = cmd_capture_pane_append(buf, len, tmp,
strlen(tmp));
}
@@ -259,7 +259,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
struct grid_cell *gc = NULL;
struct window_mode_entry *wme;
int n, join_lines, number_lines, flags = 0;
int show_flags, hyperlinks;
int show_flags, show_time, hyperlinks;
u_int *links = NULL, nlinks = 0;
u_int i, sx, top, bottom, tmp;
char *cause, *buf = NULL, *line, b[64], *cp;
@@ -342,6 +342,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
flags |= GRID_STRING_TRIM_SPACES;
number_lines = args_has(args, 'L');
show_flags = args_has(args, 'F');
show_time = args_has(args, 'I');
hyperlinks = args_has(args, 'H');
if (hyperlinks)
links = xreallocarray(NULL, gd->sx, sizeof *links);
@@ -358,6 +359,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
free(line);
continue;
}
gl = grid_peek_line(gd, i);
if (number_lines) {
if (i >= gd->hsize)
@@ -368,11 +370,16 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
if (n >= 0)
buf = cmd_capture_pane_append(buf, len, b, n);
}
if (show_time) {
n = snprintf(b, sizeof b, "%llu ",
(unsigned long long)grid_line_time(gl));
if (n >= 0)
buf = cmd_capture_pane_append(buf, len, b, n);
}
if (show_flags) {
cp = b;
*cp = '\0';
gl = grid_peek_line(gd, i);
if (gl->flags & GRID_LINE_DEAD)
*cp++ = 'D';
if (gl->flags & GRID_LINE_HYPERLINK)
@@ -393,7 +400,6 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
}
buf = cmd_capture_pane_append(buf, len, line, linelen);
gl = grid_peek_line(gd, i);
if (!join_lines || !(gl->flags & GRID_LINE_WRAPPED))
buf[(*len)++] = '\n';

View File

@@ -30,8 +30,8 @@ const struct cmd_entry cmd_copy_mode_entry = {
.name = "copy-mode",
.alias = NULL,
.args = { "UcdekHMqSs:t:u", 0, 0, NULL },
.usage = "[-UcdekHMqSu] [-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

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */
/* $OpenBSD: cmd-display-menu.c,v 1.55 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -54,8 +54,8 @@ const struct cmd_entry cmd_display_popup_entry = {
.name = "display-popup",
.alias = "popup",
.args = { "Bb:Cc:d:e:Eh:kNs:S:t:T:w:x:y:", 0, -1, NULL },
.usage = "[-BCEkN] [-b border-lines] [-c target-client] "
.args = { "Bb:Cc:d:e:Eh:ks:S:t:T:w:x:y:", 0, -1, NULL },
.usage = "[-BCEk] [-b border-lines] [-c target-client] "
"[-d start-directory] [-e environment] [-h height] "
"[-s style] [-S border-style] " CMD_TARGET_PANE_USAGE
" [-T title] [-w width] [-x position] [-y position] "
@@ -91,203 +91,6 @@ cmd_display_menu_args_parse(struct args *args, u_int idx, __unused char **cause)
return (type);
}
static int
cmd_display_menu_get_popup_pos(struct client *tc, struct cmdq_item *item,
struct args *args, u_int *px, u_int *py, u_int w, u_int h)
{
struct tty *tty = &tc->tty;
struct cmd_find_state *target = cmdq_get_target(item);
struct key_event *event = cmdq_get_event(item);
struct session *s = tc->session;
struct winlink *wl = target->wl;
struct window_pane *wp = target->wp;
struct style_ranges *ranges = NULL;
struct style_range *sr = NULL;
const char *xp, *yp;
char *p;
int top;
u_int line, ox, oy, sx, sy, lines, position;
long n;
struct format_tree *ft;
/*
* Work out the position from the -x and -y arguments. This is the
* bottom-left position.
*/
/* If the popup is too big, stop now. */
if (w > tty->sx || h > tty->sy)
return (0);
/* Create format with mouse position if any. */
ft = format_create_from_target(item);
if (event->m.valid) {
format_add(ft, "popup_mouse_x", "%u", event->m.x);
format_add(ft, "popup_mouse_y", "%u", event->m.y);
}
/* Position of the previous menu, for -x/-y L. */
format_add(ft, "popup_last_x", "%u", target->w->menu_last_px);
format_add(ft, "popup_last_y", "%u", target->w->menu_last_py + h);
/*
* If there are any status lines, add this window position and the
* status line position.
*/
top = status_at_line(tc);
if (top != -1) {
lines = status_line_size(tc);
if (top == 0)
top = lines;
else
top = 0;
position = options_get_number(s->options, "status-position");
for (line = 0; line < lines; line++) {
ranges = &tc->status.entries[line].ranges;
TAILQ_FOREACH(sr, ranges, entry) {
if (sr->type != STYLE_RANGE_WINDOW)
continue;
if (sr->argument == (u_int)wl->idx)
break;
}
if (sr != NULL)
break;
}
if (sr != NULL) {
format_add(ft, "popup_window_status_line_x", "%u",
sr->start);
if (position == 0) {
format_add(ft, "popup_window_status_line_y",
"%u", line + 1 + h);
} else {
format_add(ft, "popup_window_status_line_y",
"%u", tty->sy - lines + line);
}
}
if (position == 0)
format_add(ft, "popup_status_line_y", "%u", lines + h);
else {
format_add(ft, "popup_status_line_y", "%u",
tty->sy - lines);
}
} else
top = 0;
/* Popup width and height. */
format_add(ft, "popup_width", "%u", w);
format_add(ft, "popup_height", "%u", h);
/* Position so popup is in the centre. */
n = (long)(tty->sx - 1) / 2 - w / 2;
if (n < 0)
format_add(ft, "popup_centre_x", "%u", 0);
else
format_add(ft, "popup_centre_x", "%ld", n);
n = (tty->sy - 1) / 2 + h / 2;
if (n >= tty->sy)
format_add(ft, "popup_centre_y", "%u", tty->sy - h);
else
format_add(ft, "popup_centre_y", "%ld", n);
/* Position of popup relative to mouse. */
if (event->m.valid) {
n = (long)event->m.x - w / 2;
if (n < 0)
format_add(ft, "popup_mouse_centre_x", "%u", 0);
else
format_add(ft, "popup_mouse_centre_x", "%ld", n);
n = event->m.y - h / 2;
if (n + h >= tty->sy) {
format_add(ft, "popup_mouse_centre_y", "%u",
tty->sy - h);
} else
format_add(ft, "popup_mouse_centre_y", "%ld", n);
n = (long)event->m.y + h;
if (n >= tty->sy)
format_add(ft, "popup_mouse_top", "%u", tty->sy - 1);
else
format_add(ft, "popup_mouse_top", "%ld", n);
n = event->m.y - h;
if (n < 0)
format_add(ft, "popup_mouse_bottom", "%u", 0);
else
format_add(ft, "popup_mouse_bottom", "%ld", n);
}
/* Position in pane. */
tty_window_offset(&tc->tty, &ox, &oy, &sx, &sy);
n = top + wp->yoff - oy + h;
if (n >= tty->sy)
format_add(ft, "popup_pane_top", "%u", tty->sy - h);
else
format_add(ft, "popup_pane_top", "%ld", n);
format_add(ft, "popup_pane_bottom", "%u", top + wp->yoff + wp->sy - oy);
format_add(ft, "popup_pane_left", "%u", wp->xoff - ox);
n = (long)wp->xoff + wp->sx - ox - w;
if (n < 0)
format_add(ft, "popup_pane_right", "%u", 0);
else
format_add(ft, "popup_pane_right", "%ld", n);
/* Expand horizontal position. */
xp = args_get(args, 'x');
if (xp == NULL || strcmp(xp, "C") == 0)
xp = "#{popup_centre_x}";
else if (strcmp(xp, "R") == 0)
xp = "#{popup_pane_right}";
else if (strcmp(xp, "P") == 0)
xp = "#{popup_pane_left}";
else if (strcmp(xp, "M") == 0)
xp = "#{popup_mouse_centre_x}";
else if (strcmp(xp, "L") == 0)
xp = "#{popup_last_x}";
else if (strcmp(xp, "W") == 0)
xp = "#{popup_window_status_line_x}";
p = format_expand(ft, xp);
n = strtol(p, NULL, 10);
if (n + w >= tty->sx)
n = tty->sx - w;
else if (n < 0)
n = 0;
*px = n;
log_debug("%s: -x: %s = %s = %u (-w %u)", __func__, xp, p, *px, w);
free(p);
/* Expand vertical position */
yp = args_get(args, 'y');
if (yp == NULL || strcmp(yp, "C") == 0)
yp = "#{popup_centre_y}";
else if (strcmp(yp, "P") == 0)
yp = "#{popup_pane_bottom}";
else if (strcmp(yp, "M") == 0)
yp = "#{popup_mouse_top}";
else if (strcmp(yp, "L") == 0)
yp = "#{popup_last_y}";
else if (strcmp(yp, "S") == 0)
yp = "#{popup_status_line_y}";
else if (strcmp(yp, "W") == 0)
yp = "#{popup_window_status_line_y}";
p = format_expand(ft, yp);
n = strtol(p, NULL, 10);
if (n < h)
n = 0;
else
n -= h;
if (n + h >= tty->sy)
n = tty->sy - h;
else if (n < 0)
n = 0;
*py = n;
log_debug("%s: -y: %s = %s = %u (-h %u)", __func__, yp, p, *py, h);
free(p);
format_free(ft);
return (1);
}
static int
cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item,
struct args *args, u_int *px, u_int *py, u_int w, u_int h)
@@ -482,8 +285,8 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
enum box_lines lines = BOX_LINES_DEFAULT;
char *title, *cause = NULL;
int flags = 0, starting_choice = 0;
u_int px, py, i, count = args_count(args);
struct options *o = target->s->curw->window->options;
u_int px, py, sx, sy, i, count = args_count(args);
struct options *o = target->w->options;
struct options_entry *oe;
if (args_has(args, 'C')) {
@@ -531,9 +334,6 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
}
if (menu->count == 0)
goto out;
if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py,
menu->width + 4, menu->count + 2))
goto out;
value = args_get(args, 'b');
if (value != NULL) {
@@ -544,7 +344,11 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "menu-border-lines %s", cause);
goto fail;
}
}
} else
lines = options_get_number(o, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py, sx, sy))
goto out;
if (args_has(args, 'O'))
flags |= MENU_STAYOPEN;
@@ -565,6 +369,22 @@ fail:
return (CMD_RETURN_ERROR);
}
static enum pane_lines
cmd_display_popup_get_lines(const char *value, char **cause)
{
const struct options_table_entry *oe;
if (value == NULL)
value = "single";
else if (strcmp(value, "rounded") == 0)
value = "single";
else if (strcmp(value, "padded") == 0)
value = "spaces";
oe = options_search("pane-border-lines");
return (options_find_choice(oe, value, cause));
}
static enum cmd_retval
cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
{
@@ -572,148 +392,199 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item);
struct session *s = target->s;
struct client *tc = cmdq_get_target_client(item);
struct tty *tty = &tc->tty;
const char *value, *shell, *shellcmd = NULL;
const char *style = args_get(args, 's');
struct winlink *wl = target->wl;
struct window *w = wl->window;
struct window_pane *wp = target->wp, *new_wp = NULL;
struct spawn_context sc = { 0 };
struct layout_cell *lc = NULL;
struct layout_geometry lg;
struct event_payload *ep;
struct cmd_find_state fs;
const char *value, *style = args_get(args, 's');
const char *border_style = args_get(args, 'S');
char *cwd = NULL, *cause = NULL, **argv = NULL;
char *title = NULL;
int modify = popup_present(tc);
int flags = -1, argc = 0;
enum box_lines lines = BOX_LINES_DEFAULT;
u_int px, py, w, h, count = args_count(args);
char *cause = NULL, *title = NULL;
enum pane_lines lines = PANE_LINES_SINGLE;
u_int px, py, sx, sy, count = args_count(args);
struct args_value *av;
struct environ *env = NULL;
struct options *o = s->curw->window->options;
struct options_entry *oe;
long long ll;
if (args_has(args, 'C')) {
server_client_clear_overlay(tc);
if (w->modal != NULL)
server_kill_pane(w->modal);
return (CMD_RETURN_NORMAL);
}
if (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL);
if (!modify && tc->overlay_draw != NULL)
if (w->modal != NULL)
return (CMD_RETURN_NORMAL);
if (!modify) {
h = tty->sy / 2;
if (args_has(args, 'h')) {
h = args_percentage(args, 'h', 1, tty->sy, tty->sy,
&cause);
if (cause != NULL) {
cmdq_error(item, "height %s", cause);
goto fail;
}
}
w = tty->sx / 2;
if (args_has(args, 'w')) {
w = args_percentage(args, 'w', 1, tty->sx, tty->sx,
&cause);
if (cause != NULL) {
cmdq_error(item, "width %s", cause);
goto fail;
}
}
if (w > tty->sx)
w = tty->sx;
if (h > tty->sy)
h = tty->sy;
if (!cmd_display_menu_get_popup_pos(tc, item, args, &px, &py,
w, h))
goto out;
value = args_get(args, 'd');
if (value != NULL)
cwd = format_single_from_target(item, value);
else
cwd = xstrdup(server_client_get_cwd(tc, s));
if (count == 0) {
shellcmd = options_get_string(s->options,
"default-command");
} else if (count == 1)
shellcmd = args_string(args, 0);
if (count <= 1 && (shellcmd == NULL || *shellcmd == '\0')) {
shellcmd = NULL;
shell = options_get_string(s->options, "default-shell");
if (!checkshell(shell))
shell = _PATH_BSHELL;
cmd_append_argv(&argc, &argv, shell);
} else
args_to_vector(args, &argc, &argv);
if (args_has(args, 'e') >= 1) {
env = environ_create();
av = args_first_value(args, 'e');
while (av != NULL) {
environ_put(env, av->string, 0);
av = args_next_value(av);
}
}
}
value = args_get(args, 'b');
if (args_has(args, 'B'))
lines = BOX_LINES_NONE;
lines = PANE_LINES_NONE;
else if (value != NULL) {
oe = options_get(o, "popup-border-lines");
lines = options_find_choice(options_table_entry(oe), value,
&cause);
lines = cmd_display_popup_get_lines(value, &cause);
if (cause != NULL) {
cmdq_error(item, "popup-border-lines %s", cause);
cmdq_error(item, "pane-border-lines %s", cause);
goto fail;
}
}
sy = w->sy / 2;
if (args_has(args, 'h')) {
ll = args_percentage(args, 'h', 1, w->sy, w->sy, &cause);
if (cause != NULL) {
cmdq_error(item, "height %s", cause);
goto fail;
}
sy = ll;
}
sx = w->sx / 2;
if (args_has(args, 'w')) {
ll = args_percentage(args, 'w', 1, w->sx, w->sx, &cause);
if (cause != NULL) {
cmdq_error(item, "width %s", cause);
goto fail;
}
sx = ll;
}
if (sx > w->sx)
sx = w->sx;
if (sy > w->sy)
sy = w->sy;
if ((lines == PANE_LINES_NONE && (sx < 1 || sy < 1)) ||
(lines != PANE_LINES_NONE && (sx < 3 || sy < 3)))
goto out;
if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py, sx, sy))
goto out;
lg.sx = sx;
lg.sy = sy;
lg.xoff = px;
lg.yoff = py;
if (lines != PANE_LINES_NONE) {
lg.sx -= 2;
lg.sy -= 2;
lg.xoff++;
lg.yoff++;
}
window_push_zoom(w, 0, 1);
lc = layout_floating_pane(w, wp, &lg);
if (lc == NULL) {
window_pop_zoom(w);
goto out;
}
sc.item = item;
sc.s = s;
sc.wl = wl;
sc.tc = tc;
sc.wp0 = wp;
sc.lc = lc;
sc.idx = -1;
sc.cwd = args_get(args, 'd');
sc.flags = (SPAWN_FLOATING|SPAWN_MODAL|SPAWN_FLOATOVERZOOM);
if (count != 1 || *args_string(args, 0) != '\0')
args_to_vector(args, &sc.argc, &sc.argv);
sc.environ = environ_create();
av = args_first_value(args, 'e');
while (av != NULL) {
environ_put(sc.environ, av->string, 0);
av = args_next_value(av);
}
new_wp = spawn_pane(&sc, &cause);
if (new_wp == NULL) {
cmdq_error(item, "create pane failed: %s", cause);
free(cause);
cause = NULL;
window_pop_zoom(w);
goto fail;
}
window_pop_zoom(w);
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (!args_has(args, 'E'))
new_wp->flags |= PANE_CLOSEONCANCEL;
options_set_number(new_wp->options, "pane-border-lines", lines);
if (args_has(args, 'E') > 1) {
if (args_has(args, 'k'))
options_set_number(new_wp->options, "remain-on-exit", 4);
else
options_set_number(new_wp->options, "remain-on-exit", 2);
} else if (args_has(args, 'E'))
options_set_number(new_wp->options, "remain-on-exit", 0);
else if (args_has(args, 'k'))
options_set_number(new_wp->options, "remain-on-exit", 3);
else
options_set_number(new_wp->options, "remain-on-exit", 1);
options_set_string(new_wp->options, "remain-on-exit-format", 0, "%s", "");
if (style != NULL) {
if (options_set_string(new_wp->options, "window-style", 0,
"%s", style) == NULL) {
cmdq_error(item, "bad style: %s", style);
goto fail;
}
options_set_string(new_wp->options, "window-active-style", 0,
"%s", style);
new_wp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|
PANE_THEMECHANGED);
}
if (border_style != NULL) {
if (options_set_string(new_wp->options, "pane-border-style", 0,
"%s", border_style) == NULL) {
cmdq_error(item, "bad border style: %s", border_style);
goto fail;
}
options_set_string(new_wp->options, "pane-active-border-style",
0, "%s", border_style);
}
if (args_has(args, 'T'))
title = format_single_from_target(item, args_get(args, 'T'));
else
title = xstrdup("");
if (args_has(args, 'N') || !modify)
flags = 0;
if (args_has(args, 'E') > 1) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEEXITZERO;
} else if (args_has(args, 'E')) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEEXIT;
}
if (args_has(args, 'k')) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEANYKEY;
if (title != NULL) {
options_set_number(new_wp->options, "pane-border-status",
PANE_STATUS_TOP);
options_set_string(new_wp->options, "pane-border-format", 0,
"%s", "#{pane_title}");
screen_set_title(&new_wp->base, title, 0);
ep = event_payload_create();
cmd_find_from_pane(&fs, new_wp, 0);
event_payload_set_target(ep, &fs);
event_payload_set_pane(ep, "pane", new_wp);
event_payload_set_window(ep, "window", new_wp->window);
event_payload_set_string(ep, "new_title", "%s", title);
events_fire("pane-title-changed", ep);
}
if (modify) {
popup_modify(tc, title, style, border_style, lines, flags);
goto out;
}
if (popup_display(flags, lines, item, px, py, w, h, env, shellcmd, argc,
argv, cwd, title, tc, s, style, border_style, NULL, NULL) != 0)
goto out;
environ_free(env);
free(cwd);
new_wp->wait_item = item;
server_redraw_session(s);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
free(title);
cmd_free_argv(argc, argv);
return (CMD_RETURN_WAIT);
out:
cmd_free_argv(argc, argv);
environ_free(env);
free(cwd);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
free(title);
return (CMD_RETURN_NORMAL);
fail:
free(cause);
cmd_free_argv(argc, argv);
environ_free(env);
free(cwd);
if (new_wp != NULL) {
server_client_remove_pane(new_wp);
layout_close_pane(new_wp);
window_remove_pane(new_wp->window, new_wp);
}
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
free(title);
return (CMD_RETURN_ERROR);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-message.c,v 1.65 2026/02/23 08:46:57 nicm Exp $ */
/* $OpenBSD: cmd-display-message.c,v 1.66 2026/09/08 08:33:10 nicm Exp $ */
/*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -39,8 +39,8 @@ const struct cmd_entry cmd_display_message_entry = {
.name = "display-message",
.alias = "display",
.args = { "aCc:d:lINpt:F:v", 0, 1, NULL },
.usage = "[-aCIlNpv] [-c target-client] [-d delay] [-F format] "
.args = { "aCc:d:jlINpt:F:v", 0, 1, NULL },
.usage = "[-aCIjlNpv] [-c target-client] [-d delay] [-F format] "
CMD_TARGET_PANE_USAGE " [message]",
.target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL },
@@ -67,14 +67,15 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
struct winlink *wl = target->wl;
struct window_pane *wp = target->wp;
const char *template;
char *msg, *cause;
char *msg, *cause = NULL;
int delay = -1, flags, Nflag = args_has(args, 'N');
int Cflag = args_has(args, 'C');
struct format_tree *ft;
u_int count = args_count(args);
struct evbuffer *evb;
struct json_node *jn;
if (args_has(args, 'I')) {
if (args_has(args, 'I') && !args_has(args, 'j')) {
if (wp == NULL)
return (CMD_RETURN_NORMAL);
switch (window_pane_start_input(wp, item, &cause)) {
@@ -107,7 +108,9 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
template = args_string(args, 0);
else
template = args_get(args, 'F');
if (template == NULL)
if (args_has(args, 'j') && template == NULL)
template = "";
else if (template == NULL)
template = DISPLAY_MESSAGE_TEMPLATE;
/*
@@ -129,7 +132,7 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, flags);
format_defaults(ft, c, s, wl, wp);
if (args_has(args, 'a')) {
if (args_has(args, 'a') && !args_has(args, 'j')) {
format_each(ft, cmd_display_message_each, item);
format_free(ft);
return (CMD_RETURN_NORMAL);
@@ -139,6 +142,19 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
msg = xstrdup(template);
else
msg = format_expand_time(ft, template);
if (args_has(args, 'j')) {
jn = json_parse(msg, &cause);
if (jn == NULL) {
cmdq_error(item, "%s", cause);
free(cause);
free(msg);
format_free(ft);
return (CMD_RETURN_ERROR);
}
free(msg);
msg = json_to_string(jn);
json_destroy_node(jn);
}
if (cmdq_get_client(item) == NULL)
cmdq_error(item, "%s", msg);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-find.c,v 1.87 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: cmd-find.c,v 1.88 2026/09/07 12:05:12 nicm Exp $ */
/*
* Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -388,9 +388,11 @@ cmd_find_get_window_with_session(struct cmd_find_state *fs, const char *window)
/* Try as an offset. */
if (!exact && (window[0] == '+' || window[0] == '-')) {
if (window[1] != '\0')
n = strtonum(window + 1, 1, INT_MAX, NULL);
else
if (window[1] != '\0') {
n = strtonum(window + 1, 1, INT_MAX, &errstr);
if (errstr != NULL)
return (-1);
} else
n = 1;
s = fs->s;
if (fs->flags & CMD_FIND_WINDOW_INDEX) {
@@ -626,9 +628,11 @@ cmd_find_get_pane_with_window(struct cmd_find_state *fs, const char *pane)
/* Try as an offset. */
if (pane[0] == '+' || pane[0] == '-') {
if (pane[1] != '\0')
n = strtonum(pane + 1, 1, INT_MAX, NULL);
else
if (pane[1] != '\0') {
n = strtonum(pane + 1, 1, INT_MAX, &errstr);
if (errstr != NULL)
return (-1);
} else
n = 1;
wp = fs->w->active;
if (pane[0] == '+')

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */
/* $OpenBSD: cmd-join-pane.c,v 1.75 2026/09/21 10:33:16 nicm Exp $ */
/*
* Copyright (c) 2011 George Nachman <tmux@georgester.com>
@@ -133,7 +133,7 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
} else if (strcmp(position, "back") == 0) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp))
if (!window_pane_is_floating_with_hidden(owp))
break;
}
if (owp != NULL)
@@ -142,24 +142,30 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
TAILQ_INSERT_TAIL(&w->z_index, wp, zentry);
} else if (strcmp(position, "forward") == 0) {
owp = TAILQ_PREV(wp, window_panes_zindex, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_PREV(owp, window_panes_zindex, zentry);
if (owp != NULL) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_BEFORE(owp, wp, zentry);
}
} else if (strcmp(position, "backward") == 0) {
owp = TAILQ_NEXT(wp, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_NEXT(owp, zentry);
if (owp != NULL && window_pane_is_floating(owp)) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry);
}
} else if (strcmp(position, "forward-loop") == 0) {
owp = TAILQ_PREV(wp, window_panes_zindex, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_PREV(owp, window_panes_zindex, zentry);
TAILQ_REMOVE(&w->z_index, wp, zentry);
if (owp != NULL)
TAILQ_INSERT_BEFORE(owp, wp, zentry);
else {
TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp))
if (!window_pane_is_floating_with_hidden(owp))
break;
}
if (owp != NULL)
@@ -169,6 +175,8 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
}
} else if (strcmp(position, "backward-loop") == 0) {
owp = TAILQ_NEXT(wp, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_NEXT(owp, zentry);
if (owp != NULL && window_pane_is_floating(owp)) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry);
@@ -300,6 +308,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
struct window_pane *wp;
struct layout_cell *lc;
int y, ly, x, lx;
int old_xoff, old_yoff, old_sx, old_sy;
wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) {
@@ -321,10 +330,17 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1;
if (x != lx || y != ly) {
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
lc->g.xoff += x - lx;
lc->g.yoff += y - ly;
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w);
}
}
@@ -347,8 +363,10 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl,
n = 0;
TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp))
if (!window_pane_is_floating_with_hidden(owp))
break;
if (owp->layout_cell == NULL)
continue;
if (n >= z)
break;
n++;
@@ -443,7 +461,6 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR);
}
server_unzoom_window(dst_w);
if ((s = args_get(args, 'P')) != NULL)
return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
if ((s = args_get(args, 'z')) != NULL)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-list-panes.c,v 1.40 2026/06/01 14:01:09 nicm Exp $ */
/* $OpenBSD: cmd-list-panes.c,v 1.41 2026/09/08 10:20:08 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -97,6 +97,7 @@ cmd_list_panes_window(struct cmd *self, struct session *s, struct winlink *wl,
struct cmdq_item *item, int type)
{
struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct window_pane *wp, **l;
u_int i, n;
struct format_tree *ft;
@@ -147,7 +148,7 @@ cmd_list_panes_window(struct cmd *self, struct session *s, struct winlink *wl,
wp = l[i];
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", n);
format_defaults(ft, NULL, s, wl, wp);
format_defaults(ft, c, s, wl, wp);
if (filter != NULL) {
expanded = format_expand(ft, filter);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-list-sessions.c,v 1.36 2026/02/27 08:25:12 nicm Exp $ */
/* $OpenBSD: cmd-list-sessions.c,v 1.37 2026/09/08 10:20:08 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -53,6 +53,7 @@ static enum cmd_retval
cmd_list_sessions_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct session **l;
u_int n, i;
struct format_tree *ft;
@@ -76,7 +77,7 @@ cmd_list_sessions_exec(struct cmd *self, struct cmdq_item *item)
for (i = 0; i < n; i++) {
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", i);
format_defaults(ft, NULL, l[i], NULL, NULL);
format_defaults(ft, c, l[i], NULL, NULL);
if (filter != NULL) {
expanded = format_expand(ft, filter);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-list-windows.c,v 1.50 2026/02/27 08:25:12 nicm Exp $ */
/* $OpenBSD: cmd-list-windows.c,v 1.51 2026/09/08 10:20:08 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -60,6 +60,7 @@ cmd_list_windows_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_client(item);
struct winlink *wl, **l;
struct session *s;
u_int i, n;
@@ -94,7 +95,7 @@ cmd_list_windows_exec(struct cmd *self, struct cmdq_item *item)
s = wl->session;
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", n);
format_defaults(ft, NULL, s, wl, NULL);
format_defaults(ft, c, s, wl, NULL);
if (filter != NULL) {
expanded = format_expand(ft, filter);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-parse.y,v 1.59 2026/08/31 07:51:56 nicm Exp $ */
/* $OpenBSD: cmd-parse.y,v 1.60 2026/09/08 10:20:08 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -210,7 +210,7 @@ expanded : format
cmd_find_from_client(&fs, c, 0);
fsp = &fs;
}
ft = format_create(NULL, pi->item, FORMAT_NONE, flags);
ft = format_create(c, pi->item, FORMAT_NONE, flags);
format_defaults(ft, c, fsp->s, fsp->wl, fsp->wp);
$$ = format_expand(ft, $1);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-resize-pane.c,v 1.68 2026/08/31 07:44:39 nicm Exp $ */
/* $OpenBSD: cmd-resize-pane.c,v 1.69 2026/09/21 10:33:16 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -91,7 +91,8 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window(w);
return (CMD_RETURN_NORMAL);
}
server_unzoom_window(w);
if (!window_pane_is_floating(wp))
server_unzoom_window(w);
lc = wp->layout_cell; /* may have been replaced by unzoom */
if (args_has(args, 'x')) {
@@ -238,6 +239,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
int y, ly, x, lx, sx, sy, new_sx, new_sy;
int left, right;
int new_xoff, new_yoff, resizes = 0;
int old_xoff, old_yoff, old_sx, old_sy;
wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) {
@@ -248,6 +250,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
lc = wp->layout_cell;
sx = wp->sx;
sy = wp->sy;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = (int)wp->sx;
old_sy = (int)wp->sy;
left = wp->xoff - 1;
right = wp->xoff + sx;
if (window_pane_scrollbar_reserve(wp) &&
@@ -347,7 +353,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
}
if (resizes != 0) {
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w);
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */
/* $OpenBSD: cmd-run-shell.c,v 1.95 2026/09/20 07:59:55 nicm Exp $ */
/*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -164,7 +164,7 @@ cmd_run_shell_exec(struct cmd *self, struct cmdq_item *item)
if (cdata->client != NULL)
cdata->client->references++;
if (args_has(args, 'c'))
cdata->cwd = xstrdup(args_get(args, 'c'));
cdata->cwd = format_single_from_target(item, args_get(args, 'c'));
else
cdata->cwd = xstrdup(server_client_get_cwd(c, s));

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-select-layout.c,v 1.43 2026/07/10 13:38:45 nicm Exp $ */
/* $OpenBSD: cmd-select-layout.c,v 1.45 2026/09/09 07:03:39 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -73,12 +73,13 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_target_client(item);
struct winlink *wl = target->wl;
struct window *w = wl->window;
struct window_pane *wp = target->wp;
const char *layoutname;
char *oldlayout, *cause;
int next, previous, layout;
char *oldlayout, *cause = NULL;
int next, previous, layout, flags = 0;
server_unzoom_window(w);
@@ -89,8 +90,12 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'p'))
previous = 1;
if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
oldlayout = w->old_layout;
w->old_layout = layout_dump(w, w->layout_root);
w->old_layout = layout_dump(w, w->layout_root, flags);
if (next || previous) {
if (next)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.150 2026/08/20 09:19:24 nicm Exp $ */
/* $OpenBSD: cmd-split-window.c,v 1.151 2026/09/10 11:02:18 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 = { "AbB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-AbCdefhIkKLMOPvWZ] [-B border-lines] "
.args = { "AbB:Cc:Dde:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-AbCDefhIkKLMOPvWZ] [-B border-lines] "
"[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] "
@@ -219,6 +219,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK;
if (args_has(args, 'D') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCANCEL;
style = args_get(args, 's');
if (style != NULL) {
@@ -360,6 +362,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
enum pane_lines lines;
u_int sx, sy;
int x, y, xoff, yoff, border;
int old_xoff, old_yoff, old_sx, old_sy;
if (c->tty.mouse_last_pane == -1)
return;
@@ -415,8 +418,15 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
if (sy < PANE_MINIMUM)
sy = PANE_MINIMUM;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
layout_set_size(lc, sx, sy, xoff, yoff);
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w);
}

View File

@@ -1,6 +1,6 @@
# configure.ac
AC_INIT([tmux], next-3.8)
AC_INIT([tmux], next-3.9)
AC_PREREQ([2.60])
AC_CONFIG_AUX_DIR(etc)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */
/* $OpenBSD: control-notify.c,v 1.39 2026/09/08 10:20:08 nicm Exp $ */
/*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -70,6 +70,7 @@ control_window_layout_changed_cb(__unused const char *name,
struct session *s;
struct winlink *wl;
struct window *w = event_payload_get_window(ep, "window");
struct format_tree *ft;
const char *template;
char *cp;
@@ -84,19 +85,25 @@ control_window_layout_changed_cb(__unused const char *name,
* and we don't need to inform the client about the layout change
* because the whole window will go away soon.
*/
wl = TAILQ_FIRST(&w->winlinks);
if (wl == NULL || w->layout_root == NULL)
if (TAILQ_FIRST(&w->winlinks) == NULL || w->layout_root == NULL)
return;
cp = format_single(NULL, template, NULL, NULL, wl, NULL);
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
continue;
s = c->session;
if (winlink_find_by_window_id(&s->windows, w->id) != NULL)
control_notify_write(c, "%s", cp);
wl = winlink_find_by_window_id(&s->windows, w->id);
if (wl == NULL)
continue;
ft = format_create(c, NULL, FORMAT_NONE, 0);
format_defaults(ft, c, s, wl, NULL);
cp = format_expand(ft, template);
format_free(ft);
control_notify_write(c, "%s", cp);
free(cp);
}
free(cp);
}
/* Notify control clients that window pane changed. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.67 2026/09/03 21:35:38 nicm Exp $ */
/* $OpenBSD: control.c,v 1.68 2026/09/09 08:30:05 nicm Exp $ */
/*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -108,6 +108,7 @@ struct control_state {
u_int pending_count;
TAILQ_HEAD(, control_block) all_blocks;
size_t queued_reply_bytes;
struct bufferevent *read_event;
struct bufferevent *write_event;
@@ -132,6 +133,9 @@ struct control_state {
/* Maximum age for clients that are not using pause mode. */
#define CONTROL_MAXIMUM_AGE 300000
/* Maximum buffered command replies for a client that is not reading. */
#define CONTROL_MAXIMUM_REPLY_BUFFER (64 * 1024 * 1024)
/* Flags to ignore client. */
#define CONTROL_IGNORE_FLAGS \
(CLIENT_CONTROL_NOOUTPUT| \
@@ -165,6 +169,15 @@ RB_GENERATE_STATIC(control_windows, control_window, entry, control_window_cmp);
static void
control_free_block(struct control_state *cs, struct control_block *cb)
{
size_t size;
if (cb->size == 0 && cb->line != NULL) {
size = strlen(cb->line) + 1;
if (cs->queued_reply_bytes > size)
cs->queued_reply_bytes -= size;
else
cs->queued_reply_bytes = 0;
}
free(cb->line);
TAILQ_REMOVE(&cs->all_blocks, cb, all_entry);
free(cb);
@@ -407,16 +420,52 @@ control_reset_pane(struct client *c, struct window_pane *wp)
memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
}
/*
* Check if the replies buffered for a client, including one about to be
* added, have grown too large and kill it if so. Returns 1 if further output
* for the client should be dropped.
*/
static int
control_check_reply_buffer(struct client *c, size_t added)
{
struct control_state *cs = c->control_state;
size_t size;
if (c->flags & CLIENT_CONTROL_DISCARD)
return (1);
size = EVBUFFER_LENGTH(cs->write_event->output);
size += cs->queued_reply_bytes;
size += added;
if (size < CONTROL_MAXIMUM_REPLY_BUFFER)
return (0);
log_debug("%s: %s: %zu bytes of replies buffered", __func__, c->name,
size);
if (~c->flags & CLIENT_EXIT) {
c->exit_message = xstrdup("too far behind");
c->flags |= CLIENT_EXIT;
control_discard(c);
}
c->flags |= CLIENT_CONTROL_DISCARD;
return (1);
}
/* Write an already-formatted line, queueing it behind %output if needed. */
static void
control_write_line(struct client *c, char *line)
{
struct control_state *cs = c->control_state;
struct control_block *cb;
size_t size = strlen(line) + 1;
if (control_check_reply_buffer(c, size)) {
free(line);
return;
}
if (TAILQ_EMPTY(&cs->all_blocks)) {
log_debug("%s: %s: writing line: %s", __func__, c->name, line);
bufferevent_write(cs->write_event, line, strlen(line));
bufferevent_write(cs->write_event, line, size - 1);
bufferevent_write(cs->write_event, "\n", 1);
bufferevent_enable(cs->write_event, EV_WRITE);
free(line);
@@ -426,6 +475,7 @@ control_write_line(struct client *c, char *line)
cb = xcalloc(1, sizeof *cb);
cb->line = line;
TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
cs->queued_reply_bytes += size;
cb->t = get_timer();
log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
@@ -1007,6 +1057,7 @@ control_discard_all(struct client *c)
control_discard(c);
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb);
cs->queued_reply_bytes = 0;
bufferevent_disable(cs->write_event, EV_WRITE);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */
/* $OpenBSD: format.c,v 1.418 2026/09/20 08:19:31 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -855,26 +855,42 @@ format_cb_window_active_clients_list(struct format_tree *ft)
static void *
format_cb_window_layout(struct format_tree *ft)
{
struct window *w = ft->w;
struct client *c = ft->client;
struct window *w = ft->w;
struct layout_cell *lcroot;
int flags = 0;
if (w == NULL)
return (NULL);
if (w->saved_layout_root != NULL)
return (layout_dump(w, w->saved_layout_root));
return (layout_dump(w, w->layout_root));
lcroot = w->saved_layout_root;
else
lcroot = w->layout_root;
if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
return (layout_dump(w, lcroot, flags));
}
/* Callback for window_visible_layout. */
static void *
format_cb_window_visible_layout(struct format_tree *ft)
{
struct client *c = ft->client;
struct window *w = ft->w;
int flags = 0;
if (w == NULL)
return (NULL);
return (layout_dump(w, w->layout_root));
if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
return (layout_dump(w, w->layout_root, flags));
}
/* Callback for pane_start_command. */
@@ -2860,10 +2876,10 @@ format_cb_session_activity_flag(struct format_tree *ft)
if (ft->s != NULL) {
RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (ft->wl->flags & WINLINK_ACTIVITY)
if (wl->flags & WINLINK_ACTIVITY)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (xstrdup("0"));
}
return (NULL);
}
@@ -2878,8 +2894,8 @@ format_cb_session_bell_flag(struct format_tree *ft)
RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (wl->flags & WINLINK_BELL)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (xstrdup("0"));
}
return (NULL);
}
@@ -2892,10 +2908,10 @@ format_cb_session_silence_flag(struct format_tree *ft)
if (ft->s != NULL) {
RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (ft->wl->flags & WINLINK_SILENCE)
if (wl->flags & WINLINK_SILENCE)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (xstrdup("0"));
}
return (NULL);
}
@@ -5234,6 +5250,7 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt)
format_log(es, "session loop: $%u", s->id);
if (active != NULL &&
ft->c != NULL &&
ft->c->session != NULL &&
s->id == ft->c->session->id)
use = active;
else

View File

@@ -44,7 +44,7 @@ LLVMFuzzerTestOneInput(const u_char *data, size_t size)
w = window_create(PANE_WIDTH, PANE_HEIGHT, 0, 0);
wp = window_add_pane(w, NULL, 0, 0);
bufferevent_pair_new(libevent, BEV_OPT_CLOSE_ON_FREE, vpty);
wp->ictx = input_init(wp, vpty[0], NULL, NULL);
wp->ictx = input_init(wp, vpty[0], NULL);
window_add_ref(w, __func__);
wp->fd = open("/dev/null", O_WRONLY);

2
grid.c
View File

@@ -1760,8 +1760,6 @@ grid_line_flags_string(int flags)
strlcat(s, "START_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT)
strlcat(s, "END_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT_STATUS)
strlcat(s, "END_OUTPUT_STATUS,", sizeof s);
if (flags & GRID_LINE_HYPERLINK)
strlcat(s, "HYPERLINK,", sizeof s);
if (*s == '\0')

90
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */
/* $OpenBSD: input.c,v 1.272 2026/09/21 10:22:31 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -101,7 +101,6 @@ struct input_ctx {
struct bufferevent *event;
struct screen_write_ctx ctx;
struct colour_palette *palette;
struct client *c;
struct input_cell cell;
struct input_cell old_cell;
@@ -874,7 +873,7 @@ input_restore_state(struct input_ctx *ictx)
/* Initialise input parser. */
struct input_ctx *
input_init(struct window_pane *wp, struct bufferevent *bev,
struct colour_palette *palette, struct client *c)
struct colour_palette *palette)
{
struct input_ctx *ictx;
@@ -882,7 +881,6 @@ input_init(struct window_pane *wp, struct bufferevent *bev,
ictx->wp = wp;
ictx->event = bev;
ictx->palette = palette;
ictx->c = c;
ictx->input_space = INPUT_BUF_START;
ictx->input_buf = xmalloc(INPUT_BUF_START);
@@ -3194,14 +3192,13 @@ input_osc_112(struct input_ctx *ictx, const char *p)
/* Parse the OSC 133 D exit status. */
static int
input_osc_133_exit_status(const char *p, int *present)
input_osc_133_exit_status(const char *p)
{
const char *end;
char *copy;
char *endptr;
const char *errstr;
long long status;
*present = 0;
if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2)
return (0);
end = strchr(p + 2, ';');
@@ -3215,11 +3212,9 @@ input_osc_133_exit_status(const char *p, int *present)
free(copy);
return (0);
}
*present = 1;
errno = 0;
status = (int)strtol(copy, &endptr, 10);
status = strtonum(copy, 0, 255, &errstr);
free(copy);
if (errno != 0 || endptr == copy || status < 0 || status > 255)
if (errstr != NULL)
return (255);
return (status);
}
@@ -3264,22 +3259,6 @@ input_fire_command_event(struct window_pane *wp, const char *name)
events_fire(name, ep);
}
/* Check if an OSC 133 prompt is secondary or a continuation. */
static int
input_osc_133_secondary_prompt(const char *p)
{
const char *cp;
while ((cp = strstr(p, ";k=")) != NULL) {
cp += 3;
if ((*cp == 's' || *cp == 'c') &&
(cp[1] == '\0' || cp[1] == ';'))
return (1);
p = cp;
}
return (0);
}
/* Handle the OSC 133 sequence. */
static void
input_osc_133(struct input_ctx *ictx, const char *p)
@@ -3289,7 +3268,8 @@ input_osc_133(struct input_ctx *ictx, const char *p)
struct grid *gd = s->grid;
u_int line = s->cy + gd->hsize;
struct grid_line *gl = NULL;
int status, status_present;
const char *cp;
int status;
if (line < gd->hsize + gd->sy)
gl = grid_get_line(gd, line);
@@ -3298,17 +3278,9 @@ input_osc_133(struct input_ctx *ictx, const char *p)
case 'A':
case 'N':
if (gl != NULL) {
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 %s at %u,%u", __func__,
input_osc_133_secondary_prompt(p) ? "secondary prompt" :
"prompt", s->cx, line);
}
memset(&gl->osc133_data, 0, sizeof gl->osc133_data);
gl->osc133_data.prompt_col = s->cx;
gl->flags |= GRID_LINE_START_PROMPT;
}
if (wp != NULL) {
wp->last_prompt_time = time(NULL);
@@ -3317,33 +3289,25 @@ input_osc_133(struct input_ctx *ictx, const char *p)
break;
case 'P':
if (gl != NULL) {
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 prompt at %u,%u", __func__, s->cx,
line);
}
cp = strstr(p, ";k=s");
if (cp != NULL && (cp[4] == ';' || cp[4] == '\0'))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
gl->osc133_data.prompt_col = s->cx;
}
break;
case 'B':
case 'I':
if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) {
if (gl != NULL) {
gl->flags |= GRID_LINE_START_COMMAND;
gl->osc133_data.cmd_col = s->cx;
log_debug("%s: osc133 command at %u,%u", __func__, s->cx,
line);
}
break;
case 'C':
if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) {
if (gl != NULL) {
gl->flags |= GRID_LINE_START_OUTPUT;
gl->osc133_data.out_start_col = s->cx;
log_debug("%s: osc133 output start at %u,%u", __func__, s->cx,
line);
}
if (wp != NULL) {
wp->cmd_start_time = time(NULL);
@@ -3354,7 +3318,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
break;
case 'D':
status = input_osc_133_exit_status(p, &status_present);
status = input_osc_133_exit_status(p);
if (wp != NULL) {
wp->cmd_end_time = time(NULL);
wp->flags &= ~PANE_CMDRUNNING;
@@ -3363,12 +3327,8 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
if (gl != NULL) {
gl->flags |= GRID_LINE_END_OUTPUT;
if (status_present)
gl->flags |= GRID_LINE_END_OUTPUT_STATUS;
gl->osc133_data.out_end_col = s->cx;
gl->osc133_data.exit_status = status;
log_debug("%s: osc133 output end at %u,%u (status %d)",
__func__, s->cx, line, status);
}
break;
}
@@ -3463,15 +3423,9 @@ input_osc_52(struct input_ctx *ictx, const char *p)
return;
if (wp == NULL) {
/* Popup window. */
if (ictx->c == NULL) {
free(out);
return;
}
tty_set_selection(&ictx->c->tty, clip, out, outlen);
paste_add(NULL, out, outlen);
free(out);
return;
} else {
/* Normal window. */
screen_write_start_pane(&ctx, wp, NULL);
screen_write_setselection(&ctx, clip, out, outlen);
screen_write_stop(&ctx);

1001
json.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -52,9 +52,7 @@
" '#{?#{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}" \
" ''" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Line Numbers Off,Line Numbers On},}' 'L' {send -X line-numbers-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_fold_view,Fold View Off,Fold View On},}' 'O' {send -X fold-view-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},Edit,}' 'e' {if -F '#{selection_present}' {send -X open-selection} {send -X open-output}}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \
" ''" \
" '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \
@@ -577,7 +575,6 @@ key_bindings_init(void)
"bind -Tcopy-mode C-l { send -X recentre-top-bottom }",
"bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }",
"bind -Tcopy-mode C-n { send -X cursor-down }",
"bind -Tcopy-mode C-o { send -X select-output }",
"bind -Tcopy-mode C-p { send -X cursor-up }",
"bind -Tcopy-mode C-r { command-prompt -P -T search -ip'(search up)' -I'#{pane_search_string}' { send -X search-backward-incremental -- '%%' } }",
"bind -Tcopy-mode C-s { command-prompt -P -T search -ip'(search down)' -I'#{pane_search_string}' { send -X search-forward-incremental -- '%%' } }",
@@ -586,19 +583,15 @@ key_bindings_init(void)
"bind -Tcopy-mode Escape { send -X cancel }",
"bind -Tcopy-mode C-[ { send -X cancel }",
"bind -Tcopy-mode Space { send -X page-down }",
"bind -Tcopy-mode Tab { send -X toggle-output }",
"bind -Tcopy-mode BTab { send -X toggle-output -a }",
"bind -Tcopy-mode , { send -X jump-reverse }",
"bind -Tcopy-mode \\; { send -X jump-again }",
"bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }",
"bind -Tcopy-mode L { send -X line-numbers-toggle }",
"bind -Tcopy-mode N { send -X search-reverse }",
"bind -Tcopy-mode O { send -X fold-view-toggle }",
"bind -Tcopy-mode P { send -X toggle-position }",
"bind -Tcopy-mode R { send -X rectangle-toggle }",
"bind -Tcopy-mode T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode X { send -X set-mark }",
"bind -Tcopy-mode e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }",
"bind -Tcopy-mode f { command-prompt -P -1p'(jump forward)' { send -X jump-forward -- '%%' } }",
"bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }",
"bind -Tcopy-mode n { send -X search-again }",
@@ -607,7 +600,7 @@ key_bindings_init(void)
"bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }",
"bind -Tcopy-mode Home { send -X start-of-line }",
"bind -Tcopy-mode End { send -X end-of-line }",
"bind -Tcopy-mode MouseDown1Pane { select-pane; send -X toggle-output -m }",
"bind -Tcopy-mode MouseDown1Pane select-pane",
"bind -Tcopy-mode MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode WheelUpPane { select-pane; send -N5 -X scroll-up }",
@@ -667,8 +660,6 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi Escape { send -X clear-selection }",
"bind -Tcopy-mode-vi C-[ { send -X clear-selection }",
"bind -Tcopy-mode-vi Space { send -X begin-selection }",
"bind -Tcopy-mode-vi Tab { send -X toggle-output }",
"bind -Tcopy-mode-vi BTab { send -X toggle-output -a }",
"bind -Tcopy-mode-vi '$' { send -X end-of-line }",
"bind -Tcopy-mode-vi , { send -X jump-reverse }",
"bind -Tcopy-mode-vi / { command-prompt -P -T search -p'(search down)' { send -X search-forward -- '%%' } }",
@@ -696,10 +687,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi K { send -X scroll-up }",
"bind -Tcopy-mode-vi L { send -X bottom-line }",
"bind -Tcopy-mode-vi M { send -X middle-line }",
"bind -Tcopy-mode-vi M-e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }",
"bind -Tcopy-mode-vi M-o { send -X select-output }",
"bind -Tcopy-mode-vi N { send -X search-reverse }",
"bind -Tcopy-mode-vi O { send -X fold-view-toggle }",
"bind -Tcopy-mode-vi P { send -X toggle-position }",
"bind -Tcopy-mode-vi T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode-vi V { send -X select-line }",
@@ -727,7 +715,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi % { send -X next-matching-bracket }",
"bind -Tcopy-mode-vi Home { send -X start-of-line }",
"bind -Tcopy-mode-vi End { send -X end-of-line }",
"bind -Tcopy-mode-vi MouseDown1Pane { select-pane; send -X toggle-output -m }",
"bind -Tcopy-mode-vi MouseDown1Pane { select-pane }",
"bind -Tcopy-mode-vi MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode-vi MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode-vi WheelUpPane { select-pane; send -N5 -X scroll-up }",

File diff suppressed because it is too large Load Diff

110
layout.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */
/* $OpenBSD: layout.c,v 1.101 2026/09/20 08:42:46 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -106,7 +106,7 @@ layout_free_cell(struct layout_cell *lc, int only_nodes)
}
break;
case LAYOUT_WINDOWPANE:
if (lc->wp != NULL) {
if (lc->wp != NULL && lc->wp->layout_cell != NULL) {
lc->wp->layout_cell->parent = NULL;
lc->wp->layout_cell = NULL;
}
@@ -233,29 +233,6 @@ layout_make_node(struct layout_cell *lc, enum layout_type type)
lc->wp = NULL;
}
/* Fix z-indexes. */
void
layout_fix_zindexes(struct window *w, struct layout_cell *lc)
{
struct layout_cell *lcchild;
if (lc == NULL)
return;
switch (lc->type) {
case LAYOUT_WINDOWPANE:
TAILQ_INSERT_TAIL(&w->z_index, lc->wp, zentry);
break;
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &lc->cells, entry)
layout_fix_zindexes(w, lcchild);
return;
default:
fatalx("bad layout type");
}
}
int
layout_cell_is_tiled(struct layout_cell *lc)
{
@@ -265,7 +242,7 @@ layout_cell_is_tiled(struct layout_cell *lc)
return is_leaf && !is_floating;
}
static int
int
layout_cell_has_tiled_child(struct layout_cell *lc)
{
struct layout_cell *lcchild;
@@ -492,7 +469,6 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
sx = PANE_MINIMUM;
else
sx = sx - sb_w - sb_pad;
wp->flags |= PANE_REDRAWSCROLLBAR;
}
window_pane_resize(wp, sx, sy);
@@ -500,8 +476,11 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
if (wp->xoff != old_xoff ||
wp->yoff != old_yoff ||
wp->sx != old_sx ||
wp->sy != old_sy)
wp->sy != old_sy) {
changed = 1;
if (window_pane_scrollbar_reserve(wp))
wp->flags |= PANE_REDRAWSCROLLBAR;
}
}
if (changed)
redraw_invalidate_scene(w);
@@ -509,18 +488,20 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
/* Count the number of available cells in a layout. */
u_int
layout_count_cells(struct layout_cell *lc)
layout_count_cells(struct layout_cell *lc, int with_floating)
{
struct layout_cell *lcchild;
u_int count = 0;
switch (lc->type) {
case LAYOUT_WINDOWPANE:
if (lc->flags & LAYOUT_CELL_FLOATING && !with_floating)
return 0;
return (1);
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &lc->cells, entry)
count += layout_count_cells(lcchild);
count += layout_count_cells(lcchild, with_floating);
return (count);
default:
fatalx("bad layout type");
@@ -721,7 +702,7 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc,
/* If no parent, this is the last pane in a window. */
lcparent = lc->parent;
if (lcparent == NULL) {
if (lc->wp != NULL)
if (*lcroot == lc)
*lcroot = NULL;
layout_free_cell(lc, 0);
return;
@@ -789,6 +770,49 @@ layout_free(struct window *w, int only_nodes)
layout_free_cell(w->layout_root, only_nodes);
}
/* Move and resize floating panes so they stay inside the window. */
static void
layout_clamp_floating_panes(struct window *w, u_int sx, u_int sy)
{
struct window_pane *wp;
struct layout_cell *lc;
u_int pad, avail, csx, csy;
TAILQ_FOREACH(wp, &w->z_index, zentry) {
lc = wp->layout_cell;
if (lc == NULL || (~lc->flags & LAYOUT_CELL_FLOATING))
continue;
if (window_pane_get_pane_lines(wp) == PANE_LINES_NONE)
pad = 0;
else
pad = 1;
csx = lc->g.sx;
avail = (sx > 2 * pad) ? sx - 2 * pad : 0;
if (csx > avail)
csx = (avail > PANE_MINIMUM) ? avail : PANE_MINIMUM;
csy = lc->g.sy;
avail = (sy > 2 * pad) ? sy - 2 * pad : 0;
if (csy > avail)
csy = (avail > PANE_MINIMUM) ? avail : PANE_MINIMUM;
if (csx != lc->g.sx || csy != lc->g.sy)
layout_set_size(lc, csx, csy, lc->g.xoff, lc->g.yoff);
if (lc->g.xoff + lc->g.sx + pad > sx) {
if (lc->g.sx + 2 * pad >= sx)
lc->g.xoff = pad;
else
lc->g.xoff = sx - lc->g.sx - pad;
}
if (lc->g.yoff + lc->g.sy + pad > sy) {
if (lc->g.sy + 2 * pad >= sy)
lc->g.yoff = pad;
else
lc->g.yoff = sy - lc->g.sy - pad;
}
}
}
/* Resize the entire layout after window resize. */
void
layout_resize(struct window *w, u_int sx, u_int sy)
@@ -809,8 +833,11 @@ layout_resize(struct window *w, u_int sx, u_int sy)
* out proportionately - this should leave the layout fitting the new
* window size.
*/
if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING))
if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING)) {
layout_clamp_floating_panes(w, sx, sy);
layout_fix_panes(w, NULL);
return;
}
xchange = sx - lc->g.sx;
xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT);
if (xchange < 0 && xchange < -xlimit)
@@ -840,6 +867,7 @@ layout_resize(struct window *w, u_int sx, u_int sy)
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_clamp_floating_panes(w, sx, sy);
layout_fix_panes(w, NULL);
}
@@ -1702,7 +1730,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
enum pane_lines lines, struct window *w, struct layout_geometry *lg,
char **cause)
{
int sx, sy, ox, oy;
int sx, sy, ox, oy, pad;
char *error = NULL;
sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx;
@@ -1751,12 +1779,20 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
}
}
if (!window_has_floating_panes(w)) {
w->last_new_pane_x = 0;
w->last_new_pane_y = 0;
}
if (ox == INT_MAX) {
if (w->last_new_pane_x == 0)
ox = 4;
else {
if (lines != PANE_LINES_NONE)
pad = 1;
else
pad = 0;
ox = w->last_new_pane_x + 4;
if (w->last_new_pane_x > w->sx)
if (ox + sx + pad > (int)w->sx)
ox = 4;
}
w->last_new_pane_x = ox;
@@ -1767,8 +1803,12 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
if (w->last_new_pane_y == 0)
oy = 2;
else {
if (lines != PANE_LINES_NONE)
pad = 1;
else
pad = 0;
oy = w->last_new_pane_y + 2;
if (w->last_new_pane_y > w->sy)
if (oy + sy + pad > (int)w->sy)
oy = 2;
}
w->last_new_pane_y = oy;

59
menu.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */
/* $OpenBSD: menu.c,v 1.71 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -51,6 +51,18 @@ struct menu_data {
void *data;
};
void
menu_get_size(struct menu *menu, enum box_lines lines, u_int *sx, u_int *sy)
{
if (lines == BOX_LINES_NONE) {
*sx = menu->item_width + 2;
*sy = menu->count;
} else {
*sx = menu->width + 4;
*sy = menu->count + 2;
}
}
void
menu_add_items(struct menu *menu, const struct menu_item *items,
struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
@@ -142,6 +154,8 @@ menu_add_item(struct menu *menu, const struct menu_item *item,
width = format_width(new_item->name);
if (*new_item->name == '-')
width--;
if (width > menu->item_width)
menu->item_width = width;
if (width > menu->width)
menu->width = width;
}
@@ -238,7 +252,7 @@ menu_update(struct menu_data *md)
screen_write_clearscreen(&ctx, 8);
if (md->border_lines != BOX_LINES_NONE) {
screen_write_box(&ctx, menu->width + 4, menu->count + 2,
screen_write_box(&ctx, menu_width(md), menu_height(md),
md->border_lines, &md->border_style_gc, menu->title);
}
@@ -286,11 +300,13 @@ menu_destroy(struct window *w)
void
menu_get_cursor(struct menu_data *md, u_int *cx, u_int *cy)
{
*cx = md->px + 2;
u_int border = (md->border_lines != BOX_LINES_NONE);
*cx = md->px + 1 + border;
if (md->choice == -1)
*cy = md->py;
else
*cy = md->py + 1 + md->choice;
*cy = md->py + border + md->choice;
}
struct screen *
@@ -302,13 +318,19 @@ menu_screen(struct menu_data *md)
u_int
menu_width(struct menu_data *md)
{
return (md->menu->width + 4);
u_int sx, sy;
menu_get_size(md->menu, md->border_lines, &sx, &sy);
return (sx);
}
u_int
menu_height(struct menu_data *md)
{
return (md->menu->count + 2);
u_int sx, sy;
menu_get_size(md->menu, md->border_lines, &sx, &sy);
return (sy);
}
u_int
@@ -338,6 +360,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
enum cmd_parse_status status;
char *error;
key_code key;
u_int border;
if (KEYC_IS_MOUSE(event->key)) {
/*
@@ -353,10 +376,10 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
return (1);
return (0);
}
if (m->x < md->px ||
m->x > md->px + 4 + menu->width ||
m->y < md->py + 1 ||
m->y > md->py + 1 + n - 1) {
border = (md->border_lines != BOX_LINES_NONE);
if (m->x < md->px || m->x >= md->px + menu_width(md) ||
m->y < md->py + border ||
m->y >= md->py + border + n) {
if (~md->flags & MENU_STAYOPEN) {
if (!move && MOUSE_RELEASE(m->b))
return (1);
@@ -379,7 +402,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b))
goto chosen;
}
md->choice = m->y - (md->py + 1);
md->choice = m->y - (md->py + border);
if (md->choice != old)
server_redraw_window_menu(md->w);
return (0);
@@ -546,20 +569,16 @@ menu_resize(struct menu_data *md, struct window *w)
if (md == NULL)
return;
nx = md->px;
ny = md->py;
sx = md->menu->width + 4;
sy = md->menu->count + 2;
menu_get_size(md->menu, md->border_lines, &sx, &sy);
if (nx + sx > w->sx) {
if (w->sx <= sx)
nx = 0;
else
nx = w->sx - sx;
}
if (ny + sy > w->sy) {
if (w->sy <= sy)
ny = 0;
@@ -591,8 +610,9 @@ menu_display(struct menu *menu, int flags, int starting_choice,
w = fs->w;
o = w->options;
sx = menu->width + 4;
sy = menu->count + 2;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (sx >= w->sx)
px = 0;
else if (px + sx > w->sx)
@@ -604,9 +624,6 @@ menu_display(struct menu *menu, int flags, int starting_choice,
w->menu_last_px = px;
w->menu_last_py = py;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "menu-border-lines");
md = xcalloc(1, sizeof *md);
md->w = w;
md->flags = flags;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */
/* $OpenBSD: mode-tree.c,v 1.102 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1375,12 +1375,14 @@ static void
mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x,
u_int y, int outside)
{
struct window *w = mtd->wp->window;
struct mode_tree_item *mti;
struct menu *menu;
const struct menu_item *items;
struct mode_tree_menu *mtm;
char *title;
u_int line;
enum box_lines lines;
u_int line, sx, sy;
if (mtd->offset + y > mtd->line_size - 1)
line = mtd->current;
@@ -1405,14 +1407,17 @@ mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x,
mtm->line = line;
mtd->references++;
if (x >= (menu->width + 4) / 2)
x -= (menu->width + 4) / 2;
lines = options_get_number(w->options, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (x >= sx / 2)
x -= sx / 2;
else
x = 0;
x += mtd->wp->xoff;
y += mtd->wp->yoff;
if (menu_display(menu, 0, 0, NULL, x, y, c, BOX_LINES_DEFAULT, NULL,
NULL, NULL, NULL, mode_tree_menu_callback, mtm) != 0) {
if (menu_display(menu, 0, 0, NULL, x, y, c, lines, NULL, NULL, NULL,
NULL, mode_tree_menu_callback, mtm) != 0) {
mode_tree_remove_ref(mtd);
free(mtm);
menu_free(menu);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: monitor.c,v 1.7 2026/07/27 19:15:58 nicm Exp $ */
/* $OpenBSD: monitor.c,v 1.8 2026/09/07 10:15:25 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -614,8 +614,10 @@ monitor_parse(const char *value, char **name, enum monitor_type *type, int *id,
*type = MONITOR_ALL_WINDOWS;
else if (sscanf(what, "@%d", id) == 1 && *id >= 0)
*type = MONITOR_WINDOW;
else
else if (*what == '\0')
*type = MONITOR_SESSION;
else
goto fail;
*name = xstrdup(copy);
*format = xstrdup(split);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */
/* $OpenBSD: options-table.c,v 1.247 2026/09/21 10:22:31 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -76,9 +76,10 @@ static const char *options_table_pane_border_indicators_list[] = {
"off", "colour", "arrows", "both", NULL
};
static const char *options_table_pane_border_lines_list[] = {
"single", "double", "heavy", "simple", "number", "spaces", "none", NULL
"single", "double", "heavy", "simple", "number", "spaces", "none",
"rounded", NULL
};
static const char *options_table_popup_border_lines_list[] = {
static const char *options_table_menu_border_lines_list[] = {
"single", "double", "heavy", "simple", "rounded", "padded", "none", NULL
};
static const char *options_table_set_clipboard_list[] = {
@@ -91,7 +92,7 @@ static const char *options_table_window_size_list[] = {
"largest", "smallest", "manual", "latest", NULL
};
static const char *options_table_remain_on_exit_list[] = {
"off", "on", "failed", "key", NULL
"off", "on", "failed", "key", "failed-key", NULL
};
static const char *options_table_destroy_unattached_list[] = {
"off", "on", "keep-last", "keep-group", NULL
@@ -114,6 +115,7 @@ static const char *options_table_theme_list[] = {
static const char *options_table_copy_mode_line_numbers_list[] = {
"off", "default", "absolute", "relative", "hybrid", NULL
};
/* Status line format. */
#define OPTIONS_TABLE_STATUS_FORMAT1 \
"#[align=left range=left #{E:status-left-style}]" \
@@ -491,7 +493,7 @@ const struct options_table_entry options_table[] = {
{ .name = "menu-border-lines",
.type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW,
.choices = options_table_popup_border_lines_list,
.choices = options_table_menu_border_lines_list,
.default_num = BOX_LINES_SINGLE,
.text = "Type of characters used to draw menu border lines. Some of "
"these are only supported on terminals with UTF-8 support."
@@ -1383,13 +1385,6 @@ const struct options_table_entry options_table[] = {
.text = "Style of search matches in copy mode."
},
{ .name = "copy-mode-exit-status-format",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.default_str = "#{?exit_status,#[fg=themered]!, }",
.text = "Format of OSC 133 exit status indicator in copy mode."
},
{ .name = "copy-mode-current-match-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
@@ -1671,40 +1666,14 @@ const struct options_table_entry options_table[] = {
.text = "Pane scrollbar position."
},
{ .name = "popup-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
.default_str = "bg=themedarkgrey,fg=themewhite",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Default style of popups."
},
{ .name = "popup-border-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
.default_str = "bg=themedarkgrey,fg=themelightgrey",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Default style of popup borders."
},
{ .name = "popup-border-lines",
.type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW,
.choices = options_table_popup_border_lines_list,
.default_num = BOX_LINES_SINGLE,
.text = "Type of characters used to draw popup border lines. Some of "
"these are only supported on terminals with UTF-8 support."
},
{ .name = "remain-on-exit",
.type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.choices = options_table_remain_on_exit_list,
.default_num = 0,
.text = "Whether panes should remain ('on'), remain until a key is "
"pressed ('key') or be automatically killed ('off' or "
"pressed after any exit ('key') or after a failure "
"('failed-key'), or be automatically killed ('off' or "
"'failed') when the program inside exits."
},

661
popup.c
View File

@@ -1,661 +0,0 @@
/* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */
/*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "tmux.h"
struct popup_data {
struct client *c;
struct cmdq_item *item;
int flags;
char *title;
char *style;
char *border_style;
struct grid_cell border_cell;
enum box_lines border_lines;
struct screen s;
struct grid_cell defaults;
struct colour_palette palette;
struct visible_ranges r;
struct job *job;
struct input_ctx *ictx;
int status;
popup_close_cb cb;
void *arg;
int close;
/* Current position and size. */
u_int px;
u_int py;
u_int sx;
u_int sy;
/* Preferred position and size. */
u_int ppx;
u_int ppy;
u_int psx;
u_int psy;
enum { OFF, MOVE, SIZE } dragging;
u_int dx;
u_int dy;
u_int lx;
u_int ly;
u_int lb;
};
static void
popup_free(struct popup_data *pd)
{
server_client_unref(pd->c);
if (pd->job != NULL)
job_free(pd->job);
if (pd->ictx != NULL)
input_free(pd->ictx);
free(pd->r.ranges);
screen_free(&pd->s);
colour_palette_free(&pd->palette);
free(pd->title);
free(pd->style);
free(pd->border_style);
free(pd);
}
static void
popup_reapply_styles(struct popup_data *pd)
{
struct client *c = pd->c;
struct session *s = c->session;
struct options *o;
struct format_tree *ft;
struct style sytmp;
if (s == NULL)
return;
o = s->curw->window->options;
ft = format_create_defaults(NULL, c, s, s->curw, NULL);
/* Reapply popup style from options. */
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
style_apply(&pd->defaults, o, "popup-style", ft);
if (pd->style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->defaults, pd->style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
pd->defaults.attr = 0;
/* Reapply border style from options. */
memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
style_apply(&pd->border_cell, o, "popup-border-style", ft);
if (pd->border_style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->border_cell,
pd->border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
pd->border_cell.attr = 0;
format_free(ft);
}
static void
popup_redraw_cb(const struct tty_ctx *ttyctx)
{
struct popup_data *pd = ttyctx->arg;
pd->c->flags |= CLIENT_REDRAWOVERLAY;
}
static int
popup_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct popup_data *pd = ttyctx->arg;
if (c != pd->c)
return (0);
if (pd->c->flags & CLIENT_REDRAWOVERLAY)
return (0);
ttyctx->wox = 0;
ttyctx->woy = 0;
ttyctx->wsx = c->tty.sx;
ttyctx->wsy = c->tty.sy;
if (pd->border_lines == BOX_LINES_NONE) {
ttyctx->xoff = ttyctx->rxoff = pd->px;
ttyctx->yoff = ttyctx->ryoff = pd->py;
} else {
ttyctx->xoff = ttyctx->rxoff = pd->px + 1;
ttyctx->yoff = ttyctx->ryoff = pd->py + 1;
}
return (1);
}
static void
popup_init_ctx_cb(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
{
struct popup_data *pd = ctx->arg;
memcpy(&ttyctx->defaults, &pd->defaults, sizeof ttyctx->defaults);
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
ttyctx->style_ctx.defaults = &ttyctx->defaults;
ttyctx->style_ctx.palette = &pd->palette;
ttyctx->redraw_cb = popup_redraw_cb;
ttyctx->set_client_cb = popup_set_client_cb;
ttyctx->arg = pd;
}
static struct screen *
popup_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy)
{
struct popup_data *pd = data;
if (pd->border_lines == BOX_LINES_NONE) {
*cx = pd->px + pd->s.cx;
*cy = pd->py + pd->s.cy;
} else {
*cx = pd->px + 1 + pd->s.cx;
*cy = pd->py + 1 + pd->s.cy;
}
return (&pd->s);
}
/* Return parts of the input range which are not obstructed by the popup. */
static struct visible_ranges *
popup_check_cb(__unused struct client* c, void *data, u_int px, u_int py,
u_int nx)
{
struct popup_data *pd = data;
struct visible_ranges *r = &pd->r;
server_client_overlay_range(pd->px, pd->py, pd->sx, pd->sy, px, py, nx,
r);
return (r);
}
static void
popup_draw_cb(struct client *c, void *data)
{
struct popup_data *pd = data;
struct tty *tty = &c->tty;
struct screen s;
struct screen_write_ctx ctx;
u_int i, px = pd->px, py = pd->py;
struct grid_cell defaults;
struct tty_style_ctx style_ctx;
popup_reapply_styles(pd);
screen_init(&s, pd->sx, pd->sy, 0);
if (pd->s.hyperlinks != NULL) {
hyperlinks_free(s.hyperlinks);
s.hyperlinks = hyperlinks_copy(pd->s.hyperlinks);
}
screen_write_start(&ctx, &s);
screen_write_clearscreen(&ctx, 8);
if (pd->border_lines == BOX_LINES_NONE) {
screen_write_cursormove(&ctx, 0, 0, 0);
screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx, pd->sy);
} else if (pd->sx > 2 && pd->sy > 2) {
screen_write_box(&ctx, pd->sx, pd->sy, pd->border_lines,
&pd->border_cell, pd->title);
screen_write_cursormove(&ctx, 1, 1, 0);
screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx - 2,
pd->sy - 2);
}
screen_write_stop(&ctx);
memcpy(&defaults, &pd->defaults, sizeof defaults);
if (defaults.fg == 8)
defaults.fg = pd->palette.fg;
if (defaults.bg == 8)
defaults.bg = pd->palette.bg;
style_ctx.defaults = &defaults;
style_ctx.palette = &pd->palette;
style_ctx.dim = 0;
style_ctx.hyperlinks = s.hyperlinks;
c->overlay_check = NULL;
c->overlay_data = NULL;
for (i = 0; i < pd->sy; i++)
tty_draw_line(tty, &s, 0, i, pd->sx, px, py + i, &style_ctx);
screen_free(&s);
c->overlay_check = popup_check_cb;
c->overlay_data = pd;
}
static void
popup_free_cb(__unused struct client *c, void *data)
{
struct popup_data *pd = data;
struct cmdq_item *item = pd->item;
if (pd->cb != NULL)
pd->cb(pd->status, pd->arg);
if (item != NULL) {
if (cmdq_get_client(item) != NULL &&
cmdq_get_client(item)->session == NULL)
cmdq_get_client(item)->retval = pd->status;
cmdq_continue(item);
}
popup_free(pd);
}
static void
popup_resize_cb(__unused struct client *c, void *data)
{
struct popup_data *pd = data;
struct tty *tty = &c->tty;
if (pd == NULL)
return;
/* Adjust position and size. */
if (pd->psy > tty->sy)
pd->sy = tty->sy;
else
pd->sy = pd->psy;
if (pd->psx > tty->sx)
pd->sx = tty->sx;
else
pd->sx = pd->psx;
if (pd->ppy + pd->sy > tty->sy)
pd->py = tty->sy - pd->sy;
else
pd->py = pd->ppy;
if (pd->ppx + pd->sx > tty->sx)
pd->px = tty->sx - pd->sx;
else
pd->px = pd->ppx;
/* Avoid zero size screens. */
if (pd->border_lines == BOX_LINES_NONE) {
screen_resize(&pd->s, pd->sx, pd->sy, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx, pd->sy );
} else if (pd->sx > 2 && pd->sy > 2) {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
}
static void
popup_handle_drag(struct client *c, struct popup_data *pd,
struct mouse_event *m)
{
u_int px, py;
if (!MOUSE_DRAG(m->b))
pd->dragging = OFF;
else if (pd->dragging == MOVE) {
if (m->x < pd->dx)
px = 0;
else if (m->x - pd->dx + pd->sx > c->tty.sx)
px = c->tty.sx - pd->sx;
else
px = m->x - pd->dx;
if (m->y < pd->dy)
py = 0;
else if (m->y - pd->dy + pd->sy > c->tty.sy)
py = c->tty.sy - pd->sy;
else
py = m->y - pd->dy;
pd->px = px;
pd->py = py;
pd->dx = m->x - pd->px;
pd->dy = m->y - pd->py;
pd->ppx = px;
pd->ppy = py;
server_redraw_client(c);
} else if (pd->dragging == SIZE) {
if (pd->border_lines == BOX_LINES_NONE) {
if (m->x < pd->px + 1)
return;
if (m->y < pd->py + 1)
return;
} else {
if (m->x < pd->px + 3)
return;
if (m->y < pd->py + 3)
return;
}
pd->sx = m->x - pd->px;
pd->sy = m->y - pd->py;
pd->psx = pd->sx;
pd->psy = pd->sy;
if (pd->border_lines == BOX_LINES_NONE) {
screen_resize(&pd->s, pd->sx, pd->sy, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx, pd->sy);
} else {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
server_redraw_client(c);
}
}
static int
popup_key_cb(struct client *c, void *data, struct key_event *event)
{
struct popup_data *pd = data;
struct mouse_event *m = &event->m;
const char *buf;
size_t len;
u_int px, py;
enum { NONE, LEFT, RIGHT, TOP, BOTTOM } border = NONE;
if (KEYC_IS_MOUSE(event->key)) {
if (pd->dragging != OFF) {
popup_handle_drag(c, pd, m);
goto out;
}
if (m->x < pd->px ||
m->x > pd->px + pd->sx - 1 ||
m->y < pd->py ||
m->y > pd->py + pd->sy - 1) {
return (0);
}
if (pd->border_lines != BOX_LINES_NONE) {
if (m->x == pd->px)
border = LEFT;
else if (m->x == pd->px + pd->sx - 1)
border = RIGHT;
else if (m->y == pd->py)
border = TOP;
else if (m->y == pd->py + pd->sy - 1)
border = BOTTOM;
}
if ((m->b & MOUSE_MASK_MODIFIERS) == 0 &&
MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_3 &&
(border == LEFT || border == TOP))
goto out;
if (((m->b & MOUSE_MASK_MODIFIERS) == MOUSE_MASK_META) ||
(border != NONE && !MOUSE_DRAG(m->lb))) {
if (!MOUSE_DRAG(m->b))
goto out;
if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_1)
pd->dragging = MOVE;
else if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_3)
pd->dragging = SIZE;
pd->dx = m->lx - pd->px;
pd->dy = m->ly - pd->py;
goto out;
}
}
if ((((pd->flags & (POPUP_CLOSEEXIT|POPUP_CLOSEEXITZERO)) == 0) ||
pd->job == NULL) &&
(event->key == '\033' || event->key == ('c'|KEYC_CTRL)))
return (1);
if (pd->job == NULL && (pd->flags & POPUP_CLOSEANYKEY) &&
!KEYC_IS_MOUSE(event->key) && !KEYC_IS_PASTE(event->key))
return (1);
if (pd->job != NULL) {
if (KEYC_IS_MOUSE(event->key)) {
/* Must be inside, checked already. */
if (pd->border_lines == BOX_LINES_NONE) {
px = m->x - pd->px;
py = m->y - pd->py;
} else {
px = m->x - pd->px - 1;
py = m->y - pd->py - 1;
}
if (!input_key_get_mouse(&pd->s, m, px, py, &buf, &len))
return (0);
bufferevent_write(job_get_event(pd->job), buf, len);
return (0);
}
input_key(&pd->s, job_get_event(pd->job), event->key);
}
return (0);
out:
pd->lx = m->x;
pd->ly = m->y;
pd->lb = m->b;
return (0);
}
static void
popup_job_update_cb(struct job *job)
{
struct popup_data *pd = job_get_data(job);
struct evbuffer *evb = job_get_event(job)->input;
struct client *c = pd->c;
struct screen *s = &pd->s;
void *data = EVBUFFER_DATA(evb);
size_t size = EVBUFFER_LENGTH(evb);
if (size == 0)
return;
c->overlay_check = NULL;
c->overlay_data = NULL;
input_parse_screen(pd->ictx, s, popup_init_ctx_cb, pd, data, size);
c->overlay_check = popup_check_cb;
c->overlay_data = pd;
evbuffer_drain(evb, size);
}
static void
popup_job_complete_cb(struct job *job)
{
struct popup_data *pd = job_get_data(job);
int status;
status = job_get_status(pd->job);
if (WIFEXITED(status))
pd->status = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
pd->status = WTERMSIG(status);
else
pd->status = 0;
pd->job = NULL;
if ((pd->flags & POPUP_CLOSEEXIT) ||
((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
server_client_clear_overlay(pd->c);
}
int
popup_present(struct client *c)
{
return (c->overlay_draw == popup_draw_cb);
}
int
popup_modify(struct client *c, const char *title, const char *style,
const char *border_style, enum box_lines lines, int flags)
{
struct popup_data *pd = c->overlay_data;
struct style sytmp;
if (title != NULL) {
if (pd->title != NULL)
free(pd->title);
pd->title = xstrdup(title);
}
if (border_style != NULL) {
free(pd->border_style);
pd->border_style = xstrdup(border_style);
style_set(&sytmp, &pd->border_cell);
if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
if (style != NULL) {
free(pd->style);
pd->style = xstrdup(style);
style_set(&sytmp, &pd->defaults);
if (style_parse(&sytmp, &pd->defaults, style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
if (lines != BOX_LINES_DEFAULT) {
if (lines == BOX_LINES_NONE && pd->border_lines != lines) {
screen_resize(&pd->s, pd->sx, pd->sy, 1);
job_resize(pd->job, pd->sx, pd->sy);
} else if (pd->border_lines == BOX_LINES_NONE &&
pd->border_lines != lines) {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 1);
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
pd->border_lines = lines;
tty_resize(&c->tty);
}
if (flags != -1)
pd->flags = flags;
server_redraw_client(c);
return (0);
}
int
popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
u_int py, u_int sx, u_int sy, struct environ *env, const char *shellcmd,
int argc, char **argv, const char *cwd, const char *title, struct client *c,
struct session *s, const char *style, const char *border_style,
popup_close_cb cb, void *arg)
{
struct popup_data *pd;
u_int jx, jy;
struct options *o;
struct style sytmp;
if (s != NULL)
o = s->curw->window->options;
else
o = c->session->curw->window->options;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "popup-border-lines");
if (lines == BOX_LINES_NONE) {
if (sx < 1 || sy < 1)
return (-1);
jx = sx;
jy = sy;
} else {
if (sx < 3 || sy < 3)
return (-1);
jx = sx - 2;
jy = sy - 2;
}
if (c->tty.sx < sx || c->tty.sy < sy)
return (-1);
pd = xcalloc(1, sizeof *pd);
pd->item = item;
pd->flags = flags;
if (title != NULL)
pd->title = xstrdup(title);
if (style != NULL)
pd->style = xstrdup(style);
if (border_style != NULL)
pd->border_style = xstrdup(border_style);
pd->c = c;
pd->c->references++;
pd->cb = cb;
pd->arg = arg;
pd->status = 128 + SIGHUP;
pd->border_lines = lines;
memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
style_apply(&pd->border_cell, o, "popup-border-style", NULL);
if (border_style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
pd->border_cell.attr = 0;
screen_init(&pd->s, jx, jy, 0);
screen_set_default_cursor(&pd->s, global_w_options);
colour_palette_init(&pd->palette);
colour_palette_from_option(&pd->palette, global_w_options);
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
style_apply(&pd->defaults, o, "popup-style", NULL);
if (style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->defaults, style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
pd->defaults.attr = 0;
pd->px = px;
pd->py = py;
pd->sx = sx;
pd->sy = sy;
pd->ppx = px;
pd->ppy = py;
pd->psx = sx;
pd->psy = sy;
pd->job = job_run(shellcmd, argc, argv, env, s, cwd,
popup_job_update_cb, popup_job_complete_cb, NULL, pd,
JOB_NOWAIT|JOB_PTY|JOB_KEEPWRITE|JOB_DEFAULTSHELL, jx, jy);
if (pd->job == NULL) {
popup_free(pd);
return (-1);
}
pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette, c);
server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd);
return (0);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */
/* $OpenBSD: prompt.c,v 1.7 2026/09/09 07:53:03 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1493,9 +1493,11 @@ append_key:
utf8_set(&tmp, key);
if (key <= 0x1f || key == 0x7f)
tmp.width = 2;
} else if (KEYC_IS_UNICODE(key))
} else if (KEYC_IS_UNICODE(key)) {
utf8_to_data(key, &tmp);
else
if (tmp.size == 0)
return (PROMPT_KEY_HANDLED);
} else
return (PROMPT_KEY_HANDLED);
pr->buffer = xreallocarray(pr->buffer, size + 2,

View File

@@ -1,33 +1,17 @@
TESTS!= echo *.sh
LOGDIR=logs
.PHONY: all
.NOTPARALLEL: all
.PHONY: all prepare $(TESTS)
all:
@mkdir -p "$(LOGDIR)"; \
rm -f "$(LOGDIR)"/*.log; \
failed=0; failures=; \
all: prepare $(TESTS)
@failed=0; failures=; \
for test in $(TESTS); do \
base=$${test##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \
rm -f "$$log"; \
printf '%-40s ' "$$test"; \
start=$$(date +%s); \
env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \
sh -x "$$test" >"$$log" 2>&1; \
if [ $$? -eq 0 ]; then \
end=$$(date +%s); \
rm -f "$$log"; \
echo "PASS ($$((end - start))s)"; \
else \
end=$$(date +%s); \
echo "FAIL ($$((end - start))s)"; \
echo " log: $$log"; \
if [ -f "$$log" ]; then \
failed=1; \
failures="$$failures $$test"; \
fi; \
sleep 1; \
done; \
if [ "$$failed" -ne 0 ]; then \
echo; \
@@ -39,3 +23,22 @@ all:
rmdir "$(LOGDIR)" 2>/dev/null || true; \
fi; \
exit $$failed
prepare:
@mkdir -p "$(LOGDIR)"
@rm -f "$(LOGDIR)"/*.log
$(TESTS): prepare
@base="$@"; base=$${base##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \
start=$$(date +%s); \
if env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \
sh -x "$@" >"$$log" 2>&1; then \
end=$$(date +%s); \
rm -f "$$log"; \
printf '%-40s PASS (%ss)\n' "$@" "$$((end - start))"; \
else \
end=$$(date +%s); \
printf '%-40s FAIL (%ss)\n log: %s\n' \
"$@" "$$((end - start))" "$$log"; \
fi

View File

@@ -2,8 +2,9 @@
# monitor-activity, monitor-bell and monitor-silence: both the
# alert-activity, alert-bell and alert-silence hooks and the winlink alert
# flags (window_activity_flag, window_bell_flag, window_silence_flag and
# the #, !, ~ characters in window_flags). The sessions are detached so
# flags (window_activity_flag, window_bell_flag, window_silence_flag,
# their session equivalents and the #, !, ~ characters in window_flags).
# The sessions are detached so
# alert flags are set even on the current window; the *-action options
# still decide whether the hooks fire. Panes run cat: activity is
# generated by the tty echo of send-keys and a bell by sending a BEL and
@@ -111,6 +112,24 @@ flags_have()
esac
}
assert_session_flag()
{
flag="#{session_${1}_flag}"
expected=$2
# Only mon exists here. Check both window contexts and list-sessions.
for target in mon:w0 "mon:$3"; do
value=$($TMUX display -pt "$target" "$flag") ||
fail "display $flag failed"
[ "$value" = "$expected" ] ||
fail "expected $flag for $target to be '$expected' but got '$value'"
done
value=$($TMUX list-sessions -F "$flag") ||
fail "list-sessions $flag failed"
[ "$value" = "$expected" ] ||
fail "expected list-sessions $flag to be '$expected' but got '$value'"
}
flags_lack()
{
target=$1
@@ -150,10 +169,12 @@ $TMUX set-hook -g alert-silence \
# alert-bell and sets the bell flag, shown as ! in window_flags.
$TMUX neww -d -t mon: -n bellw cat || fail "new-window bellw failed"
assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0
assert_session_flag bell 0 bellw
flags_lack mon:bellw '!'
bell mon:bellw
wait_for @log '|alert-bell:mon:bellw'
wait_for_fmt mon:bellw '#{window_bell_flag}' 1
assert_session_flag bell 1 bellw
flags_have mon:bellw '!'
# Bells are not deduplicated: a second bell fires the hook again even
@@ -164,6 +185,7 @@ wait_for @log '|alert-bell:mon:bellw|alert-bell:mon:bellw'
# Selecting the window clears the alert flags.
$TMUX selectw -t mon:bellw || fail "select-window bellw failed"
wait_for_fmt mon:bellw '#{window_bell_flag}' 0
assert_session_flag bell 0 bellw
flags_lack mon:bellw '!'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
@@ -173,6 +195,7 @@ $TMUX set -g @log '' || fail "reset @log failed"
bell mon:w0
wait_for @log '|alert-bell:mon:w0'
wait_for_fmt mon:w0 '#{window_bell_flag}' 1
assert_session_flag bell 1 bellw
$TMUX selectw -t mon:bellw || fail "select-window bellw failed"
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
wait_for_fmt mon:w0 '#{window_bell_flag}' 0
@@ -199,6 +222,7 @@ $TMUX neww -d -t mon: -n actw cat || fail "new-window actw failed"
$TMUX set -g @log '' || fail "reset @log failed"
activity mon:actw
assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0
assert_session_flag activity 0 actw
assert_unchanged @log ''
# With monitor-activity on, output fires alert-activity and sets the
@@ -208,6 +232,7 @@ $TMUX set -wt mon:actw monitor-activity on ||
activity mon:actw
wait_for @log '|alert-activity:mon:actw'
wait_for_fmt mon:actw '#{window_activity_flag}' 1
assert_session_flag activity 1 actw
flags_have mon:actw '#'
# While the flag is set further activity does not fire the hook again.
@@ -234,6 +259,7 @@ exec 3>"$OUT/fifo"
wait_for_fmt mon: '#{session_attached}' 1
$TMUX selectw -t mon:actw || fail "select-window actw failed"
wait_for_fmt mon:actw '#{window_activity_flag}' 0
assert_session_flag activity 0 actw
flags_lack mon:actw '#'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
exec 3>&-
@@ -250,6 +276,7 @@ $TMUX set -wt mon:w0 monitor-activity on ||
$TMUX set -g @log '' || fail "reset @log failed"
activity mon:w0
wait_for_fmt mon:w0 '#{window_activity_flag}' 1
assert_session_flag activity 1 actw
assert_unchanged @log ''
$TMUX set -wut mon:w0 monitor-activity ||
fail "unset monitor-activity w0 failed"
@@ -273,16 +300,19 @@ $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
# running.
$TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed"
assert_fmt_unchanged mon:silw '#{window_silence_flag}' 0
assert_session_flag silence 0 silw
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX set -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed"
wait_for @log '|alert-silence:mon:silw'
wait_for_fmt mon:silw '#{window_silence_flag}' 1
assert_session_flag silence 1 silw
flags_have mon:silw '~'
assert_unchanged @log '|alert-silence:mon:silw'
$TMUX set -wt mon:silw monitor-silence 0 ||
fail "reset monitor-silence failed"
$TMUX selectw -t mon:silw || fail "select-window silw failed"
wait_for_fmt mon:silw '#{window_silence_flag}' 0
assert_session_flag silence 0 silw
flags_lack mon:silw '~'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"

View File

@@ -0,0 +1,79 @@
#!/bin/sh
# Selecting a pane with attach-session from an already attached client must
# redraw pane contents when window-style and window-active-style differ.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lattach-redraw-outer-$$ -f/dev/null"
BEFORE=$DIR/before
AFTER=$DIR/after
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
LEFT=$($INNER new-session -dPF '#{pane_id}' -s inner -x 40 -y 8 \
"printf 'LEFT'; exec sleep 100") || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g default-terminal screen || exit 1
$INNER split-window -h -t "$LEFT" "printf 'RIGHT'; exec sleep 100" || exit 1
$INNER set-option -w -t "$LEFT" window-style bg=red || exit 1
$INNER set-option -w -t "$LEFT" window-active-style bg=blue || exit 1
$INNER bind-key -n x attach-session -t "$LEFT" || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
sleep 1
$OUTER send-keys -t outer:0.0 x || exit 1
sleep 1
[ "$($INNER display-message -p -t inner '#{pane_id}')" = "$LEFT" ] ||
fail "attach-session did not select the target pane"
$OUTER capture-pane -pe -t outer:0.0 >"$BEFORE" || exit 1
# A forced redraw produces the correct scene. It must be identical to the
# scene drawn immediately by attach-session.
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 1
$OUTER capture-pane -pe -t outer:0.0 >"$AFTER" || exit 1
cmp -s "$BEFORE" "$AFTER" ||
fail "attach-session left stale active/inactive pane styles"
exit 0

View File

@@ -0,0 +1,38 @@
#!/bin/sh
# capture-pane -I line timestamps
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMP=$(mktemp)
trap '$TMUX kill-server 2>/dev/null; rm -f "$TMP"' 0 1 15
$TMUX kill-server 2>/dev/null
before=$(date +%s)
$TMUX new-session -d -x 40 -y 5 'seq 1 12; sleep 10' || exit 1
sleep 1
after=$(date +%s)
$TMUX capture-pane -pILF -S - -E - >"$TMP" || exit 1
awk -v before="$before" -v after="$after" '
$1 !~ /^-?[0-9]+$/ || $2 !~ /^[0-9]+$/ { exit 1 }
$4 == "1" {
if ($2 < before || $2 > after)
exit 1
history = 1
}
$4 == "9" {
if ($2 != 0)
exit 1
visible = 1
}
END { if (!history || !visible) exit 1 }
' "$TMP" || {
cat "$TMP"
exit 1
}
exit 0

View File

@@ -5,6 +5,15 @@
PATH=/bin:/usr/bin
TERM=screen
# The pane's shell must not be the user's own interactive shell: a custom
# PS1/PROMPT_COMMAND that sets the terminal title (as many do, for tmux/xterm
# TERM types) would redraw over the titles this test sets and checks on
# every prompt, regardless of how long it waits first.
shell=
if command -v bash >/dev/null 2>&1; then
shell='bash --noprofile --norc +o history'
fi
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
@@ -30,7 +39,7 @@ must_equal()
[ "$got" = "$want" ] || fail "got '$got', expected '$want'"
}
$TMUX new-session -d -x 80 -y 24 || exit 1
$TMUX new-session -d -x 80 -y 24 -- $shell || exit 1
$TMUX set-option -qg allow-set-title on || exit 1
$TMUX set-option -qg allow-rename on || exit 1
$TMUX set-option -qg automatic-rename off || exit 1

View File

@@ -0,0 +1,53 @@
#!/bin/sh
# Width cache ranges must include their endpoint without overflowing wchar_t.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
SERVER=
WATCHDOG=
cleanup()
{
if [ -n "$WATCHDOG" ]; then
kill "$WATCHDOG" 2>/dev/null
wait "$WATCHDOG" 2>/dev/null
fi
[ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$TMUX new-session -d 'exec sleep 60' || exit 1
SERVER=$($TMUX display-message -p '#{pid}') || exit 1
# kill-server cannot stop a server stuck rebuilding the width cache.
(
sleep 5
kill -9 "$SERVER" 2>/dev/null
) &
WATCHDOG=$!
$TMUX set -g codepoint-widths 'U+1F600=2' || exit 1
# Cover both signed and unsigned 32-bit wchar_t. Values above WCHAR_MAX
# are ignored by the parser on platforms where they cannot be represented.
for value in U+7FFFFFFF=1 U+7FFFFFFE-U+7FFFFFFF=2 \
U+FFFFFFFF=1 U+FFFFFFFE-U+FFFFFFFF=2; do
$TMUX set -g codepoint-widths "$value" || exit 1
[ "$($TMUX display-message -p alive)" = alive ] || exit 1
done
# Check that an ordinary range still includes both endpoints and stops there.
$TMUX set -g codepoint-widths 'U+03B1-U+03B3=2' || exit 1
$TMUX set -g @text 'αβγδ' || exit 1
[ "$($TMUX display-message -p '#{w:@text}')" = 7 ] || exit 1
$TMUX set -gu codepoint-widths || exit 1
[ "$($TMUX display-message -p '#{w:@text}')" = 4 ] || exit 1
$TMUX kill-server || exit 1
SERVER=
exit 0

View File

@@ -1,81 +0,0 @@
#!/bin/sh
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
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007p\\$ \\033]133;B\\007echo hi\\n\\033]133;C\\007hello\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward hello || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != hello ] || exit 1
$TMUX send-keys -X search-backward separator || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != separator ] || exit 1
$TMUX send-keys BTab || exit 1
$TMUX send-keys -X search-backward hello || exit 1
shown=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$shown" = hello ] || exit 1
$TMUX send-keys -X select-line || exit 1
$TMUX send-keys -X copy-selection || exit 1
selected=$($TMUX show-buffer)
[ "$selected" = hello ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"first\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward separator || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != separator ] || exit 1
$TMUX send-keys BTab || exit 1
$TMUX send-keys -X search-backward separator || exit 1
shown=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$shown" = separator ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007P> \\033]133;B\\007for i in 1 2 3; do\\n\\033]133;A;k=s\\007> \\033]133;B\\007echo \\$i\\n\\033]133;P;k=s\\007> \\033]133;B\\007done\\033]133;C\\007\\nRESULT\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007P> \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward RESULT || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != RESULT ] || exit 1
$TMUX send-keys -X search-backward 'echo $i' || exit 1
shown=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$shown" = 'echo $i' ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode -U || exit 1
$TMUX send-keys -X search-backward RESULT || exit 1
$TMUX send-keys Tab || exit 1
cursor=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$cursor" = done ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007P0> \\033]133;B\\007\\r\\n\\033]133;D;0\\007empty-separator\\n\\033]133;A\\007P1> \\033]133;B\\007\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -c || exit 1
collapsed=$($TMUX capture-pane -p)
case "$collapsed" in
*"+ P0>"*) ;;
*) exit 1 ;;
esac
$TMUX send-keys -X search-backward empty-separator || exit 1
hidden=$($TMUX display-message -p '#{copy_cursor_line}')
[ "$hidden" != empty-separator ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -17,7 +17,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)

View File

@@ -19,7 +19,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)

View File

@@ -0,0 +1,123 @@
#!/bin/sh
# Check that control client flags are available in every format expansion path
# used to produce layouts. The layout strings themselves are tested separately.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export PATH TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
FIFO=$DIR/input
OUT=$DIR/output
CFG=$DIR/flags.conf
PID=
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
exec 3>&-
[ -n "$PID" ] && kill "$PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup EXIT
wait_for()
{
pattern=$1
i=0
while [ "$i" -lt 50 ]; do
grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0
if [ -n "$PID" ] && ! kill -0 "$PID" 2>/dev/null; then
fail "control client exited waiting for: $pattern"
fi
sleep 0.1
i=$((i + 1))
done
fail "missing: $pattern"
}
send()
{
printf '%s\n' "$*" >&3
}
check_commands()
{
name=$1
expected=$2
match='#{m:*new-layouts*,#{client_flags}}'
send "display-message -p 'DISPLAY-$name $match'"
wait_for "DISPLAY-$name $expected"
send "list-panes -F 'PANES-$name $match'"
wait_for "PANES-$name $expected"
send "list-windows -F 'WINDOWS-$name $match'"
wait_for "WINDOWS-$name $expected"
send "list-sessions -F 'SESSIONS-$name $match'"
wait_for "SESSIONS-$name $expected"
send "list-clients -F 'CLIENTS-$name $match'"
wait_for "CLIENTS-$name $expected"
}
check_config()
{
name=$1
expected=$2
send "source-file '$CFG'"
send "display-message -p 'CONFIG-$name #{@config-new-layouts}'"
wait_for "CONFIG-$name $expected"
}
cat >"$CFG" <<'EOF'
%if #{m:*new-layouts*,#{client_flags}}
set-option -g @config-new-layouts 1
%else
set-option -g @config-new-layouts 0
%endif
EOF
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s layouts -x 80 -y 24 || exit 1
$TMUX split-window -h -t layouts: || exit 1
mkfifo "$FIFO" || exit 1
: >"$OUT"
$TMUX -C attach-session -t layouts <"$FIFO" >"$OUT" 2>&1 &
PID=$!
exec 3>"$FIFO"
send 'display-message -p READY'
wait_for READY
check_commands OFF 0
check_config OFF 0
send 'refresh-client -f new-layouts'
check_commands ON 1
check_config ON 1
send 'refresh-client -f !new-layouts'
check_commands OFF-AGAIN 0
check_config OFF-AGAIN 0
# The list commands also run for unattached command clients. Supplying that
# client to formats must not make the session loop dereference a NULL session.
$TMUX list-sessions -F '#{S:all,active}' >/dev/null ||
fail "session loop failed for unattached command client"
$TMUX has-session -t layouts || fail "server exited"
exit 0

View File

@@ -1,6 +1,7 @@
#!/bin/sh
# Popups require a tty overlay and cannot be displayed by a control client.
# Popups require an attached terminal and cannot be displayed by a control
# client.
# A popup command from control mode must be ignored cleanly, leaving the
# client command queue and server usable.

View File

@@ -10,7 +10,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
OUT=$(mktemp)

View File

@@ -30,8 +30,11 @@ killw
EOF
sleep 1
$TMUX has || exit 1
$TMUX lsp -aF '#{pane_id} #{window_layout}' >$TMP || exit 1
cat <<EOF|cmp -s $TMP - || exit 1
# Use a control client to request legacy layouts, keeping only pane lines
# from the control protocol output.
$TMUX -C lsp -aF '#{pane_id} #{window_layout}' |
grep '^%[0-9]' >$TMP || exit 1
cat <<EOF|cmp $TMP - || exit 1
%0 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3]
%3 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3]
%2 dcbd,200x200,0,0[200x100,0,0,2,200x99,0,101,4]

View File

@@ -10,7 +10,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
FIFO=$(mktemp -u)

View File

@@ -1,102 +0,0 @@
#!/bin/sh
# Check OSC 133 exit status indicators in copy mode using a real client.
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"
fail() {
echo "$*" >&2
exit 1
}
capture() {
$TMUX capture-pane -pS0 -E- >$TMP || exit 1
}
check_grep() {
grep -Fq "$1" $TMP || fail "missing pattern: $1"
}
check_no_grep() {
grep -Fq "$1" $TMP && fail "unexpected pattern: $1"
}
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" 0 1 15
$TMUX2 new-session -d -x80 -y10 \
"printf '\033]133;A\007p\$ \033]133;B\007one\n\033]133;C\007out1\n\033]133;D;0\007\033]133;A\007p\$ \033]133;B\007two\n\033]133;C\007out2\n\033]133;D;123\007\033]133;A\007p\$ \033]133;B\007silent\n\033]133;C\007\033]133;D;7\007\033]133;A\007p\$ \033]133;B\007three\n\033]133;C\007out3\n\033]133;D\007\033]133;A\007p\$ \033]133;B\007'; exec sleep 100" || \
exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g copy-mode-position-format '#[align=left]POS' || exit 1
$TMUX2 list-keys -Tcopy-mode O | grep -Fq 'fold-view-toggle' || exit 1
$TMUX2 list-keys -Tcopy-mode-vi O | grep -Fq 'fold-view-toggle' || exit 1
$TMUX new-session -d -x80 -y10 || exit 1
$TMUX set -g status off || exit 1
$TMUX send -l "$TMUX2 attach" || exit 1
$TMUX send Enter || exit 1
sleep 1
$TMUX2 copy-mode -U || exit 1
$TMUX2 send -X history-top || exit 1
sleep 1
capture
check_grep "!- p\$ two"
check_grep "! p\$ silent"
check_no_grep "!+ p\$ silent"
check_grep "POS"
# Rebuilding with collapsed output must not expand copy-mode formats before
# the viewport has been restored for the shorter backing grid.
$TMUX2 send -X collapse-output -a || exit 1
sleep 1
capture
check_grep "!+ p\$ two"
check_grep "! p\$ silent"
check_no_grep "!+ p\$ silent"
$TMUX2 send -X line-numbers-on || exit 1
sleep 1
capture
check_grep " 3 !+ p\$ two"
$TMUX2 send -X fold-view-toggle || exit 1
[ "$($TMUX2 display -p '#{copy_fold_view}')" = 0 ] || exit 1
sleep 1
capture
check_grep "out2"
check_no_grep "!+ p\$ two"
check_grep " 3 p\$ two"
$TMUX2 send -X fold-view-toggle || exit 1
[ "$($TMUX2 display -p '#{copy_fold_view}')" = 1 ] || exit 1
sleep 1
capture
check_grep " 3 !- p\$ two"
# A format wider than the standard three-column gutter is not truncated.
$TMUX2 send -X cancel || exit 1
$TMUX2 set -g copy-mode-exit-status-format \
'#[align=right]#{?exit_status,!#{exit_status}, }' || exit 1
$TMUX2 copy-mode -c || exit 1
$TMUX2 send -X history-top || exit 1
sleep 1
capture
check_grep "!123+ p\$ two"
$TMUX2 send -X select-line || exit 1
$TMUX2 send -X copy-selection || exit 1
selection=$($TMUX2 show-buffer)
case $selection in
*RC=*) fail "exit status was copied" ;;
esac
exit 0

View File

@@ -0,0 +1,63 @@
#!/bin/sh
# Trimming a blank source must leave a visible line for cursor reflow.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 3 15
$TMUX new-session -d -x80 -y24 -s test 'sleep 100' || exit 1
$TMUX set-option -g window-size manual || exit 1
$TMUX new-window -t test:1 'sleep 100' || exit 1
$TMUX resize-window -t test:0 -x20 -y24 || exit 1
# Exercise widening, narrowing, and equal widths with no source history.
for pair in '0 1' '1 0' '1 2'; do
set -- $pair
if [ "$2" = 2 ]; then
$TMUX new-window -t test:2 'sleep 100' || exit 1
$TMUX resize-window -t test:2 -x80 -y24 || exit 1
fi
source=test:$1.0
target=test:$2.0
[ "$($TMUX display-message -p -t "$source" '#{history_size}')" = 0 ] ||
fail "source has unexpected history"
$TMUX copy-mode -s "$source" -t "$target" || exit 1
$TMUX has-session -t test || fail "server died copying blank source"
[ "$($TMUX display-message -p -t "$target" \
'#{pane_in_mode} #{copy_cursor_x} #{copy_cursor_y}')" = '1 0 0' ] ||
fail "blank source did not enter copy mode at the first cell"
$TMUX send-keys -t "$target" -X cancel || exit 1
done
# Real content still trims trailing empty lines, clamping the cursor to beta.
$TMUX respawn-pane -k -t test:0.0 \
"printf 'alpha\r\nbeta\r\n\r\n\r\n'; exec sleep 100" || exit 1
i=0
while [ "$($TMUX display-message -p -t test:0.0 '#{cursor_y}')" != 4 ]; do
i=$((i + 1))
[ "$i" -lt 10 ] || fail "source output did not arrive"
sleep 1
done
$TMUX copy-mode -s test:0.0 -t test:1.0 || exit 1
[ "$($TMUX display-message -p -t test:1.0 \
'#{pane_in_mode} #{copy_cursor_y} #{copy_cursor_line}')" = '1 1 beta' ] ||
fail "nonblank source content or trailing-line trimming changed"
exit 0

View File

@@ -0,0 +1,108 @@
#!/bin/sh
# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating()
# (cmd-resize-pane.c) reported damage for just a dragged floating pane's
# content rectangle, not the one-cell border frame drawn around it (see the
# "floating" case in screen-redraw.c, which draws that frame at
# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own
# content area). Damage scoped to only the content area left the frame's
# previous position undrawn as the pane moved, so dragging it left a trail
# of un-erased border frames behind - visible as several "corners" stacked
# up rather than just the pane's current one.
#
# This bug has nothing to do with images - it reproduces with a plain
# floating pane and no image support required.
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"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
#
# Write a plain (unmodified) SGR button-1 press, drag update and release at
# 1-based positions to the outer pane holding the inner client - this
# matches the default MouseDown1Border/MouseDrag1Border bindings used to
# move or resize a floating pane by its border.
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g default-command 'sh -c "sleep 100"'
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: exactly one floating pane, so exactly one top-left corner,
# before dragging anything.
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n"
# Drag the floating pane by its top border (row FTOP-1, some column within
# its width) down several rows in a few separate steps, then release. A
# single drag() call already does press/motion/release, so call it several
# times in a row to simulate a multi-step real drag.
GRABCOL=$((FLEFT + FWIDTH / 2))
STARTROW=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((STARTROW + i + 1))
drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow
i=$((i + 1))
done
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# Exactly one top-left corner should remain - the pane's current position.
# This is expected to fail before the fix: multiple corners (a trail of
# un-erased frames) would remain from the intermediate drag positions.
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)"
exit 0

View File

@@ -0,0 +1,100 @@
#!/bin/sh
# server_client_check_redraw() had `(~c->flags & CLIENT_ALLREDRAWFLAGS)` as
# a fallback condition guarding a call to redraw_client_damage() - for a
# multi-bit mask, `~x & MASK` means "at least one of these bits is unset"
# (almost always true), not "none of these bits are set" as the comment
# and surrounding logic clearly intend. Every floating-pane drag command
# unconditionally sets CLIENT_REDRAWBORDERS (server_redraw_window_borders()
# in cmd-resize-pane.c/cmd-join-pane.c/cmd-split-window.c) alongside
# reporting window damage, so this fallback fired on every single drag
# step, composing the exact same damage rectangle a second time a few
# lines later at the CLIENT_ALLREDRAWFLAGS block - wasted work, not a
# correctness issue, but a clean, deterministic signal to check for via
# the server's own -vv log.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Ldoublecomp-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Top-border drag (a move): each step both reports window damage and sets
# CLIENT_REDRAWBORDERS (server_redraw_window_borders() in the caller),
# which is exactly the combination the buggy fallback misfired on.
mouse 0 "$GRABCOL" "$BORDERROW" M
i=0
steps=6
while [ $i -lt $steps ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
# Each drag step should compose its damage exactly once. If any rectangle
# was composed twice, the same "x,y WxH" text appears on two consecutive
# composing-damage lines - compare the position+size together, since
# distinct steps commonly share the same size (only the position differs).
dup=$(grep "composing damage" "$LOG" | awk '{print $(NF-1), $NF}' |
uniq -d | wc -l)
[ "$dup" -eq 0 ] ||
fail "$dup damage rectangle(s) were composed twice in the same pass"
exit 0

View File

@@ -0,0 +1,140 @@
#!/bin/sh
# Regression test: dragging a floating pane across another pane's ordinary
# content must not redraw that other pane's scrollbar, unless the drag
# actually crosses the scrollbar's own strip.
#
# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag
# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the
# floating pane's old or new rectangle, rather than just its narrow
# scrollbar strip - so dragging a floating pane back and forth over an
# ordinary tiled pane's content (never touching its scrollbar) still
# needlessly redrew that pane's scrollbar on every motion step. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by giving the non-dragged pane a distinctive scrollbar
# colour and counting how many times its SGR code appears in the client's
# raw output while the floating pane is dragged vertically over that pane's
# body, well clear of its scrollbar column: with the fix, it should never
# reappear after the initial draw.
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"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.5
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on || fail "set mouse failed"
$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed"
$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed"
$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed"
PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
[ -n "$LEFT" ] || fail "could not identify left pane"
# A distinctive scrollbar colour for the non-dragged (left) pane only.
$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' ||
fail "set pane-scrollbars-style failed"
ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}')
ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}')
AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}')
AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}')
[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)"
# A small floating pane placed well inside the left pane's content area,
# clear of its (right-hand) scrollbar column by several columns.
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \
-X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] ||
fail "sanity: floating pane too close to the scrollbar column"
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which draws the scrollbars) has already happened, missing it.
$TMUX2 new-session -d -x 60 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the distinctive scrollbar colour reaches the client at all.
grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client"
: >$TMP
# Drag the floating pane straight up and down by its top border, staying at
# a fixed column the whole time - this never crosses the left pane's
# scrollbar strip, only its ordinary content.
GRABCOL=$((FLEFT + FWIDTH / 2))
row=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((row + 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
i=0
while [ $i -lt 6 ]; do
newrow=$((row - 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
# The scrollbar colour should never reappear - its geometry never changed,
# and the drag never crossed its column. This is expected to fail before
# the fix - see the header comment.
n=$(grep -ac '48;5;201' $TMP)
[ "$n" -eq 0 ] ||
fail "left pane's scrollbar was redrawn $n times while dragging over its body only"
exit 0

View File

@@ -0,0 +1,95 @@
#!/bin/sh
# server_client_key_callback()'s mouse-drag dispatch opens a synchronized-
# output frame (tty_sync_start()) before running the drag callback, on
# every single drag motion event. server_client_check_redraw() then checks
# EVBUFFER_LENGTH(tty->out) != 0 later in the same pass to decide whether
# to defer this pass's redraw - nothing drains tty->out in between, so the
# frame-open sequence just queued (8 bytes: "\033[?2026h") makes that check
# see "outstanding output" and defer against itself, escalating the drag's
# damage to a full-window redraw on every motion event on any
# synchronized-output-capable terminal. This checks the server's own -vv
# log for that exact self-inflicted "8 left" deferral pattern during a
# drag, and requires it never appears.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Lsyncdefer-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER set-option -as terminal-features ',screen-256color:sync' || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Plain (non-Alt) top-border drag: "MouseDrag1Border" -> resize-pane -M ->
# a move, since grabbing the top border moves rather than resizes. Several
# small steps, each its own drag-motion event and so its own pass through
# the code under test.
mouse 0 "$GRABCOL" "$BORDERROW" M
i=0
while [ $i -lt 6 ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
n=$(grep -c "redraw deferred (8 left)" "$LOG")
[ "$n" -eq 0 ] ||
fail "drag self-deferred against its own queued sync bytes $n time(s)"
exit 0

View File

@@ -0,0 +1,206 @@
#!/bin/sh
# Mirror of floating-pane-drag-wide-character.sh for the *right* edge of a
# damage rectangle. redraw_damage_grow_span_clip() (screen-redraw.c) widens
# a damage rectangle's right edge by one cell whenever it isn't already at
# the span's own edge, to pull in a wide character's base half when the
# edge lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell (a character fully
# outside the range), growing right pulls in just that base cell -
# tty_draw_line() sees it can't fit that character's full width in the
# remaining range (tty_draw_line_get_empty()'s gc->data.width > nx check)
# and clears it, exactly as the left-edge bug cleared a neighbouring
# character's padding half.
#
# As with the left-edge test, the exact column parity needs to be
# deterministic to actually catch it. This constructs it by creating the
# floating pane, checking its real position, and recreating it one column
# over if necessary until the vacated rectangle's right edge lands on a
# base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidecharr-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
PANEWIDTH=12
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated right edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's right edge
# (xoff + PANEWIDTH + 1, the border-grown exclusive end - see
# window_pane_damage_floating(), window.c) lands on an even (base-cell)
# column.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x "$PANEWIDTH" -y 3 \
-X "$startx" -Y 5 \
'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldright=$((XOFF + PANEWIDTH + 1))
if [ $((oldright % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF + PANEWIDTH + 1) % 2)) -eq 0 ] ||
fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF + PANEWIDTH - 2))
COL2=$((XOFF + PANEWIDTH + 2))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle - far enough right
# that its new position starts past the checked columns above (which sit
# just past the pane's *original* right edge).
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0

View File

@@ -0,0 +1,200 @@
#!/bin/sh
# Damage at a floating pane's vacated edge must always redraw a complete
# grid character. redraw_damage_grow_span_clip() (screen-redraw.c) widens a
# damage rectangle's left edge by one cell whenever it isn't already at the
# span's own edge, to pull in a wide character's base half when the edge
# lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell, growing left
# walks into the *previous*, unrelated character's padding cell and blanks
# it (tty_draw_line() treats any leading padding cell as proof its own
# range starts mid-character).
#
# This is a general damage-composition bug, not specific to any one kind of
# pane, but the exact column parity needs to be deterministic to actually
# catch it (a lucky parity draws fine). This constructs it by creating the
# floating pane, checking its real position (the border-framing offset
# added to -X is not something to hand-compute), and recreating it one
# column over if necessary until the vacated rectangle's left edge lands on
# a base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidechar-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's left edge
# (xoff - 1) lands on an even (base-cell) column - the odd case is the one
# every earlier manual test happened to land on by chance.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 12 -y 3 -X "$startx" \
-Y 5 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldleft=$((XOFF - 1))
if [ $((oldleft % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF - 1) % 2)) -eq 0 ] || fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF - 4))
COL2=$((XOFF + 4))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle.
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0

View File

@@ -268,5 +268,149 @@ $TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$right" || exit 1
$TMUX kill-pane -t "$lower" || exit 1
# --- Floating panes clamped when the window shrinks (issue #5581, PR #5582) ---
#
# layout_resize clamps floating panes back inside the window when it shrinks:
# move them and, only if they cannot fit, shrink them (never below
# PANE_MINIMUM). A floating cell is the pane's content, so with the default
# single-line border the clamp counts 1 cell of border per side, exactly as
# layout_floating_args_parse does on creation; with no border it counts 0.
# Each case below gets its own window, since resize-window fixes a window at
# a manual size for the rest of its life.
# Case 1: a lone floating pane -- break-pane -W on a window's only pane --
# takes the early-return path in layout_resize, added during PR #5582's
# review round, since there is no tiled tree to walk.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for lone float failed"
# -x 20 -y 6 -> pane 18x4; -X 60 -Y 18 -> pane_left 61, pane_top 19: with the
# border, the footprint is columns 60-79, rows 18-23, flush with the 80x24
# window's right and bottom edges, so the float fits before it is shrunk.
$TMUX break-pane -W -s "$win" -x 20 -y 6 -X 60 -Y 18 ||
fail "break-pane -W for lone float failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 61
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 19
# Shrink to 40x16. The float still fits at its own size (18 <= 40-2, 4 <=
# 16-2) so only its position moves, flush to the new right/bottom edges:
# xoff = 40 - 18 - 1 = 21; yoff = 16 - 4 - 1 = 11.
$TMUX resize-window -t "$win" -x 40 -y 16 ||
fail "resize-window (lone float) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 21
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 11
# Case 5: grow the window back. The clamp must not chase the window back
# outward -- it only ever pulls a float in, never restores where it was.
$TMUX resize-window -t "$win" -x 80 -y 24 ||
fail "resize-window grow (lone float) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 21
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 11
$TMUX kill-window -t "$win" || exit 1
# Case 2: the same float, but with a tiled sibling surviving alongside it, so
# the window's root cell stays tiled and layout_resize takes the normal path
# (the clamp call after layout_fix_offsets) instead of the early return
# above. Same geometry as case 1, so the same numbers should come out.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for tiled sibling failed"
$TMUX split-window -t "$win" 'sleep 100' ||
fail "split-window for tiled sibling failed"
$TMUX break-pane -W -s "$win" -x 20 -y 6 -X 60 -Y 18 ||
fail "break-pane -W for tiled sibling failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 61
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 19
$TMUX resize-window -t "$win" -x 40 -y 16 ||
fail "resize-window (tiled sibling) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 21
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 11
$TMUX kill-window -t "$win" || exit 1
# Case 3: new-pane float, the original repro from issue #5581 and the PR
# body -- also the normal path, via the tiled base pane new-window creates.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for new-pane repro failed"
$TMUX resize-window -t "$win" -x 120 -y 40 ||
fail "resize-window to 120x40 failed"
# -x 30 -y 10 -> pane 28x8; -X 85 -Y 25 -> pane_left 86, pane_top 26.
floating=$($TMUX new-pane -t "$win" -dPF '#{pane_id}' \
-x 30 -y 10 -X 85 -Y 25 'sleep 100') ||
fail "new-pane for new-pane repro failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 28
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 86
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 26
# Shrink to 60x20: xoff = 60 - 28 - 1 = 31; yoff = 20 - 8 - 1 = 11.
$TMUX resize-window -t "$win" -x 60 -y 20 ||
fail "resize-window (new-pane repro) failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 28
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 31
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 11
$TMUX kill-window -t "$win" || exit 1
# Case 4: a float that already fits inside the shrunk window is left alone --
# assert position and size are both unchanged, not just one of them.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for untouched float failed"
# -x 20 -y 6 -> pane 18x4; -X 8 -Y 3 -> pane_left 9, pane_top 4 (as at the top
# of this file). Footprint columns 8-27, rows 3-8: well inside 60x20 too.
floating=$($TMUX new-pane -t "$win" -dPF '#{pane_id}' \
-x 20 -y 6 -X 8 -Y 3 'sleep 100') ||
fail "new-pane for untouched float failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 9
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 4
$TMUX resize-window -t "$win" -x 60 -y 20 ||
fail "resize-window (untouched float) failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 9
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 4
$TMUX kill-window -t "$win" || exit 1
# Case 6: pad = 0, via the pane-border-lines window option set to none. The
# border arithmetic differs here (no -2/+1 adjustment either on creation or
# in the clamp).
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for pad=0 float failed"
$TMUX set-option -w -t "$win" pane-border-lines none ||
fail "set pane-border-lines none failed"
# No border: -x 20 -y 6 -> pane 20x6 directly; -X 60 -Y 18 -> pane_left 60,
# pane_top 18 directly. Footprint (== content, no border) is columns 60-79,
# rows 18-23: flush right/bottom of the 80x24 window, same as case 1.
$TMUX break-pane -W -s "$win" -x 20 -y 6 -X 60 -Y 18 ||
fail "break-pane -W for pad=0 float failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 20
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 6
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 60
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 18
# Shrink to 40x16 with pad=0: xoff = 40 - 20 - 0 = 20; yoff = 16 - 6 - 0 = 10.
$TMUX resize-window -t "$win" -x 40 -y 16 ||
fail "resize-window (pad=0 float) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 20
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 6
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 20
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 10
$TMUX kill-window -t "$win" || exit 1
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -0,0 +1,111 @@
#!/bin/sh
# A floating pane positioned partly off the window's left/top edge (e.g.
# created with -X -5) has a negative wp->xoff/wp->yoff. screen_write_
# redraw_cb() (screen-write.c) used to pass these straight through as u_int
# to redraw_damage_window(), which wraps a negative offset to a huge value
# - redraw_damage_window()'s own bounds check then rejects the whole
# rectangle, so nothing gets redrawn, not even the pane's visible portion.
#
# This fires on returning from the alternate screen (screen_write_
# alternateoff()) among other paths. This test exercises exactly that:
# fills the pane's primary screen, switches it to the alternate screen and
# back, and checks the client actually receives the restored primary
# content in the pane's visible (on-screen) columns - using an attached
# client's own received bytes (via a nested outer client), not
# capture-pane, which reads the grid directly and would pass regardless of
# whether the client was ever actually told to redraw it.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Loffscreen-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_visible_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n "${CONTENTROW}p" "$CAPTURE" | grep -q '^AAAAA' && return 0
sleep 0.1
i=$((i + 1))
done
fail "primary-screen content was not restored in the pane's visible columns after returning from the alternate screen"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
print "\e[1;1H", 'A' x 15;
sleep 2;
print "\e[?1049h";
print "\e[1;1H", 'B' x 15;
sleep 2;
print "\e[?1049l";
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
# Content pane spans window columns -5..9 (partly off the left edge); only
# columns 0..9 are ever visible.
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X -5 -Y 2 \
"perl '$EMITTER'") || exit 1
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
[ "$XOFF" -lt 0 ] || fail "sanity: floating pane is not off-screen (xoff=$XOFF)"
CONTENTROW=$((YOFF + 1))
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_outer_has AAAAA
wait_outer_has BBBBB
wait_visible_restored
exit 0

View File

@@ -0,0 +1,179 @@
#!/bin/sh
# A damage rectangle's clip range is grown to avoid splitting a wide
# character, but redraw_damage_grow_span_clip() (screen-redraw.c) only ever
# checks the span's own pane *content* grid (wp->screen) for that. For a
# REDRAW_SPAN_PANE span, that same range is then also handed to
# redraw_damage_draw_pane_prompt() to recompose the pane's separately
# rendered prompt (wp->prompt, e.g. from "command-prompt -P") over the
# damaged sub-range - but the prompt is drawn into its own, freshly
# allocated one-line screen, unrelated to the pane's content grid, so a
# range grown (or left ungrown) against the content is not necessarily
# grown correctly for the prompt's own wide characters.
#
# This is invisible when the pane's own content is plain ASCII (as here):
# redraw_damage_grow_span_clip() never finds anything to grow against, so
# the raw, ungrown geometric range is passed straight through to the
# prompt - and if that range's edge lands mid-character in the *prompt's*
# grid, tty_draw_line() clears the character it cuts through
# (tty_draw_line_get_empty()'s gc->data.width > nx check, for a trailing
# base cell with no room left for its padding half).
#
# The trigger is a palette change (OSC 4) in a tiled pane that is one half
# of a vertical split running the full height of the window - occluded
# under the floating pane, but still geometrically triggering a redraw of
# its own rectangle. Positioned so the split boundary falls inside the
# floating pane's own CJK prompt, this reproduces exactly Codex's report:
# "a floating pane containing a CJK prompt across a tiled-pane boundary -
# a palette update in the tiled pane blanks a prompt character."
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpromptwide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_prompt_row_intact()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
line=$(sed -n "${PROMPTROW}p" "$CAPTURE")
case $line in
*"$PROMPTTEXT"*) return 0 ;;
esac
sleep 0.1
i=$((i + 1))
done
fail "the CJK prompt was not intact after the palette-triggered damage - got: $line"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 12 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
LEFT=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
# Split so the boundary between the two tiled panes falls at column 17 -
# used below to pick a floating-pane column that lands the boundary
# mid-character inside the prompt.
RIGHT=$($INNER split-window -t inner -h -l 43 -PF '#{pane_id}' \
'sleep 100') || exit 1
RX=$($INNER display-message -p -t "$RIGHT" '#{pane_left}') || exit 1
# Create the floating pane, then check its actual resulting position (the
# border-framing offset added to -X is not something to hand-compute). Try
# adjacent starting columns until the split boundary lands on an odd
# (padding-half) column of the prompt's own numbering, and within the
# prompt's 12-column width.
startx=10
tries=0
while [ "$tries" -lt 4 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 24 -y 5 -X "$startx" \
-Y 2 "sh -c 'i=0; while [ \$i -lt 10 ]; do \
printf AAAAAAAAAAAAAAAAAAAAAA\\\\n; i=\$((i+1)); done; sleep 100'") ||
exit 1
sleep 0.2
X1=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
Y1=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
H1=$($INNER display-message -p -t "$FLOAT" '#{pane_height}')
local=$((RX - X1 - 1))
if [ "$local" -ge 1 ] && [ "$local" -le 11 ] &&
[ $((local % 2)) -eq 1 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
local=$((RX - X1 - 1))
[ "$local" -ge 1 ] && [ "$local" -le 11 ] && [ $((local % 2)) -eq 1 ] ||
fail "could not find bad-parity starting column"
$OUTER new-session -d -s outer -x 60 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has AAAAAAAAAAAAAAAAAAAAAA
CLIENT=$($INNER list-clients -F '#{client_name}') || exit 1
$INNER select-pane -t "$FLOAT" || exit 1
PROMPTTEXT=$(printf '\344\270\255' | perl -CSD -ne 'print $_ x 6')
$INNER command-prompt -b -P -t "$CLIENT" -p "$PROMPTTEXT" \
'display-message -- %1' || exit 1
wait_outer_has "$PROMPTTEXT"
PROMPTROW=$((Y1 + H1))
# Trigger the damage: unblock the emitter so it fires the palette change in
# the left tiled pane, which is occluded under (but geometrically overlaps)
# the floating pane's prompt row.
$INNER send-keys -t "$LEFT" Enter || exit 1
wait_prompt_row_intact
exit 0

View File

@@ -0,0 +1,117 @@
#!/bin/sh
# redraw_damage_refresh_status() (screen-redraw.c) force-regenerates a
# pane's border-status title when a damage rectangle touches it, guarded
# by the per-pane PANE_NEWSTATUS flag. window_make_pane_status() formats
# pane-border-format using the requesting client's own context (so e.g.
# #{client_name} differs per client), but wp->status_screen/PANE_NEWSTATUS
# are shared by every client viewing the pane. With two clients attached
# to the same session, whichever client's damage pass runs first renders
# its own text and sets the flag; the other client's damage pass, finding
# the flag already set, used to skip rendering entirely and reuse
# whatever was already there.
#
# This checks the actual server-side decision via the -vv log rather than
# a visual capture: an unrelated periodic client status-refresh reliably
# repaints each client's title correctly again within the same tick right
# after the buggy decision is made, before anything is ever flushed to
# either terminal, so the wrong content this bug produces is never
# visible to any external capture - the log is the only place the actual
# bug (or its absence) can be observed.
#
# A floating pane with its own pane-border-status is positioned so that a
# damage rectangle from an *unrelated* palette change (OSC 4) in the
# underlying tiled pane - whose own geometry spans the whole window -
# overlaps the floating pane's title row without touching its content,
# giving a damage-only trigger with no side effect that would otherwise
# force a normal (non-buggy) full per-client status re-render in the same
# pass and mask the result either way.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Lstatuscc-inner-$$ -f/dev/null"
OUTER1="$TEST_TMUX -Lstatuscc-outer1-$$ -f/dev/null"
OUTER2="$TEST_TMUX -Lstatuscc-outer2-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER1 kill-server 2>/dev/null
$OUTER2 kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
BASEEMITTER=$DIR/base-emitter.pl
cat >"$BASEEMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$BASEEMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g pane-border-status top || exit 1
$INNER set-option -g pane-border-format 'C=#{client_name}' || exit 1
BASE=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 20 -y 3 -X 5 -Y 4 \
'sleep 100') || exit 1
$OUTER1 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER1 set-option -g status off || exit 1
$OUTER1 set-option -g window-size manual || exit 1
$OUTER1 set-option -g default-terminal screen-256color || exit 1
$OUTER1 respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
NAME1=$($INNER list-clients -F '#{client_name}') || exit 1
$OUTER2 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER2 set-option -g status off || exit 1
$OUTER2 set-option -g window-size manual || exit 1
$OUTER2 set-option -g default-terminal screen-256color || exit 1
$OUTER2 respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
ALLNAMES=$($INNER list-clients -F '#{client_name}') || exit 1
NAME2=$(echo "$ALLNAMES" | grep -v "^$NAME1\$")
[ -n "$NAME2" ] || fail "sanity: could not identify the second client"
# Let any attach-driven full redraw (and its own, non-buggy, per-client
# status render) finish completely before triggering the damage-only
# palette update.
sleep 1.5
$INNER send-keys -t "$BASE" Enter || exit 1
sleep 0.5
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
n1=$(grep -c "regenerated pane .* status for $NAME1\$" "$LOG")
n2=$(grep -c "regenerated pane .* status for $NAME2\$" "$LOG")
[ "$n1" -ge 1 ] || fail "damage pass never regenerated $NAME1's own status - it reused whatever the other client's render left behind"
[ "$n2" -ge 1 ] || fail "damage pass never regenerated $NAME2's own status - it reused whatever the other client's render left behind"
exit 0

View File

@@ -0,0 +1,102 @@
#!/bin/sh
# Resizing a floating pane must refresh session status formats which depend on
# its geometry, not only the pane scene and borders.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lfloating-status-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has_status()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
tail -1 "$CAPTURE" | grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client status did not show $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 50 -y 12 'sleep 100' || exit 1
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 10 -y 5 -X 5 -Y 3 \
'sleep 100') || fail "could not create floating pane"
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g status-left 'WIDTH=#{pane_width}' || exit 1
$INNER set-option -g status-right '' || exit 1
$INNER set-option -g status-interval 0 || exit 1
$OUTER new-session -d -s outer -x 50 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
OLD_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
wait_outer_has_status "WIDTH=$OLD_WIDTH"
RIGHT=$($INNER display-message -p -t "$FLOAT" '#{pane_right}')
TOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
X=$((RIGHT + 2))
Y=$((TOP + 2))
# Grab the right frame and enlarge the floating pane.
mouse 0 "$X" "$Y" M
mouse 32 "$((X + 1))" "$Y" M
mouse 32 "$((X + 8))" "$Y" M
mouse 0 "$((X + 8))" "$Y" m
NEW_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
[ "$NEW_WIDTH" -ne "$OLD_WIDTH" ] || fail "floating pane was not resized"
wait_outer_has_status "WIDTH=$NEW_WIDTH"
exit 0

View File

@@ -0,0 +1,158 @@
#!/bin/sh
# Moving, resizing and reordering an over-zoom float must leave the tiled
# pane zoomed, and the changes must survive an explicit unzoom.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
fail()
{
echo "$*" >&2
exit 1
}
run()
{
$TMUX "$@" || fail "failed: $*"
}
check()
{
got=$(run display-message -p -t "$1" "$2") || exit 1
[ "$got" = "$3" ] || fail "$1 $2: got '$got', expected '$3'"
}
check_zoom()
{
check "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check "$float" '#{pane_floating_flag}:#{pane_active}' "1:$active"
check "$base" '#{@unzoomed}' ''
}
run new-session -d -x 80 -y 24
base=$(run display-message -p '#{pane_id}') || exit 1
other=$(run split-window -dPF '#{pane_id}') || exit 1
layout=$(run display-message -p '#{window_layout}') || exit 1
run set-hook -g window-unzoomed 'set -g @unzoomed 1'
# Cover creation before and after zoom, with both the float and the tiled
# pane active. Modal panes use the same over-zoom flag.
for mode in before after detached modal; do
if [ "$mode" != before ]; then
run resize-pane -Z -t "$base"
fi
case "$mode" in
detached) flags=-Ad; active=0 ;;
modal) flags=-O; active=1 ;;
*) flags=-A; active=1 ;;
esac
float=$(run new-pane "$flags" -PF '#{pane_id}' -t "$base" \
-x 20 -y 8 -X 8 -Y 3 '') || exit 1
if [ "$mode" = before ]; then
run resize-pane -Z -t "$base"
fi
run set -gu @unzoomed
check_zoom
run move-pane -t "$float" -D
check "$float" '#{pane_top}' 5
check_zoom
run move-pane -t "$float" -U 2 -R 3 -L 1
check "$float" '#{pane_left}:#{pane_top}' '11:3'
check_zoom
run move-pane -t "$float" -X 25% -Y 25%
check "$float" '#{pane_left}:#{pane_top}' '21:7'
check_zoom
run move-pane -t "$float" -P bottom-right
check "$float" '#{pane_left}:#{pane_top}' '61:17'
check_zoom
run resize-pane -t "$float" -x 30 -y 10
check "$float" '#{pane_width}:#{pane_height}' '28:8'
check_zoom
run resize-pane -t "$float" -U 1 -L 2
check "$float" '#{pane_left}:#{pane_top}:#{pane_width}:#{pane_height}' \
'59:16:30:9'
check_zoom
run move-pane -t "$float" -P centre
check "$float" '#{pane_left}:#{pane_top}' '25:7'
check_zoom
# Failed commands must not change zoom either.
for command in 'move-pane -D invalid' 'move-pane -P invalid' \
'move-pane -z invalid' 'resize-pane -x invalid' \
'resize-pane -D invalid'; do
$TMUX $command -t "$float" 2>/dev/null &&
fail "unexpected success: $command"
check_zoom
done
run resize-pane -Z -t "$base"
check "$float" '#{pane_left}:#{pane_top}:#{pane_width}:#{pane_height}' \
'25:7:30:9'
run kill-pane -t "$float"
check "$base" '#{window_layout}' "$layout"
done
# Put an ordinary (hidden while zoomed) float between two over-zoom floats
# in the stacking order. Reordering must skip it when counting visible
# positions, but keep all floats ahead of the tiled panes after unzoom.
back=$(run new-pane -AdPF '#{pane_id}' -t "$base" '') || exit 1
hidden=$(run new-pane -dPF '#{pane_id}' -t "$base" '') || exit 1
float=$(run new-pane -AdPF '#{pane_id}' -t "$base" '') || exit 1
active=0
run resize-pane -Z -t "$base"
run set -gu @unzoomed
for position in backward back forward-loop; do
run move-pane -t "$float" -P front
run move-pane -t "$float" -P "$position"
check "$float" '#{pane_z}' 1
check "$back" '#{pane_z}' 0
check_zoom
done
for position in forward front backward-loop; do
run move-pane -t "$float" -P back
run move-pane -t "$float" -P "$position"
check "$float" '#{pane_z}' 0
check "$back" '#{pane_z}' 1
check_zoom
done
for z in 1 0 99; do
run move-pane -t "$float" -z "$z"
want=$z
[ "$z" = 99 ] && want=1
check "$float" '#{pane_z}' "$want"
check_zoom
done
run resize-pane -Z -t "$base"
check "$float" '#{pane_z}' 2
check "$base" '#{pane_z}' 4
run kill-pane -t "$hidden"
run kill-pane -t "$back"
# Explicit zoom toggling and operations on tiled panes retain their
# existing behaviour. The over-zoom flag alone does not make a pane a float:
# zooming the float itself gives it a tiled cell until unzoom.
run resize-pane -Z -t "$float"
check "$float" '#{pane_zoomed_flag}:#{pane_floating_flag}' '1:0'
run resize-pane -t "$float" -x 25
check "$float" '#{window_zoomed_flag}:#{pane_floating_flag}:#{pane_width}' \
'0:1:23'
run resize-pane -Z -t "$base"
run resize-pane -t "$other" -D 1
check "$base" '#{window_zoomed_flag}' 0
# A hidden ordinary float must still be unzoomed before resizing it.
hidden=$(run new-pane -dPF '#{pane_id}' -t "$base" '') || exit 1
run resize-pane -Z -t "$base"
run resize-pane -t "$hidden" -x 25
check "$hidden" '#{window_zoomed_flag}:#{pane_floating_flag}:#{pane_width}' \
'0:1:23'
exit 0

View File

@@ -7,6 +7,8 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
# test_format $format $expected_result
test_format()

View File

@@ -18,6 +18,24 @@ export TZ LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1"
FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2"
HOLD1=
HOLD2=
CC1=
CC2=
cleanup()
{
for pid in $HOLD1 $HOLD2 $CC1 $CC2; do
kill "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
done
$TMUX kill-server 2>/dev/null
rm -f "$FIFO1" "$FIFO2"
}
trap cleanup 0
trap 'exit 1' 1 2 15
ESC=$(printf '\033')
@@ -579,8 +597,6 @@ assert_alive "verbose loop expansion"
# L loops over attached clients. Attach two control-mode clients, each held
# open by a background process keeping a FIFO's write end open.
FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1"
FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2"
rm -f "$FIFO1" "$FIFO2"
mkfifo "$FIFO1" "$FIFO2" || exit 1
# Hold the write ends open so the control clients stay attached.
@@ -605,9 +621,9 @@ test_format "#{L/nr:x}" "xx"
test_format "#{L/r:x}" "xx"
# Now detach one and confirm the count drops to one.
kill $HOLD2 2>/dev/null
wait $HOLD2 2>/dev/null
HOLD2=
sleep 1
test_format "#{L:x}" "x"
kill $HOLD1 $CC1 $CC2 2>/dev/null
rm -f "$FIFO1" "$FIFO2"
exit 0

View File

@@ -7,6 +7,8 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
# test_format $format $expected_result
test_format()

View File

@@ -76,14 +76,14 @@ $TMUX set-hook -g session-closed \
fail "set-hook session-closed failed"
# The only pane of the only window of a session exits: pane-exited, then
# window-unlinked, then session-closed, each seeing the dead object in the
# session-closed, then window-unlinked, each seeing the dead object in the
# hook formats.
pane=$($TMUX new -d -s doomed -n dwin -P -F '#{pane_id}' 'true') ||
fail "new-session doomed failed"
wait_for @log \
"|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed"
"|pane-exited:$pane|session-closed:doomed|window-unlinked:doomed:dwin"
assert_unchanged @log \
"|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed"
"|pane-exited:$pane|session-closed:doomed|window-unlinked:doomed:dwin"
# The dead pane, window and session cannot be used as targets but the
# server survives.
@@ -110,14 +110,14 @@ $TMUX set-hook -g pane-exited \
'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' ||
fail "restore pane-exited hook failed"
# kill-window on the last window: window-unlinked then session-closed but
# kill-window on the last window: session-closed then window-unlinked but
# no pane-exited for the panes in the killed window.
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX new -d -s doomed2 -n dwin2 || fail "new-session doomed2 failed"
$TMUX splitw -d -t doomed2:0 || fail "split-window doomed2 failed"
$TMUX kill-window -t doomed2:0 || fail "kill-window failed"
wait_for @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2'
assert_unchanged @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2'
wait_for @log '|session-closed:doomed2|window-unlinked:doomed2:dwin2'
assert_unchanged @log '|session-closed:doomed2|window-unlinked:doomed2:dwin2'
$TMUX has -t main || fail "server died after kill-window chain"
# kill-session: session-closed fires first, then window-unlinked for its

View File

@@ -12,7 +12,8 @@ $TMUX kill-server 2>/dev/null
TMP=$(mktemp)
OUT=$(mktemp)
trap "rm -f $TMP $OUT" 0 1 15
trap 'rm -f "$TMP" "$OUT"; $TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
cat <<EOF >$TMP
if 'true' 'wibble wobble'

View File

@@ -4,6 +4,9 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
# Keep panes alive until the next case or cleanup, without reading replies.
INPUT_HOLD='while :; do sleep 3600; done'
TMP=$(mktemp)
EXP=$(mktemp)
trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15
@@ -37,10 +40,10 @@ start_pane_hlimit()
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1
$TMUX new-session -d -x 1 -y 1 -s test-setup "$INPUT_HOLD" || exit 1
$TMUX set-option -g history-limit "$hlimit" || exit 1
$TMUX new-session -d -x "$sx" -y "$sy" -s "$name" \
"printf '$seq'; sleep 2" || exit 1
"printf '$seq'; $INPUT_HOLD" || exit 1
$TMUX kill-session -t test-setup
sleep 0.3
}

View File

@@ -2,6 +2,21 @@
. ./input-common.inc
# Large strings may take longer than start_cmd's delay to reach the parser.
check_discard()
{
name=$1
printf 'OK\n' >"$EXP"
i=0
while [ "$i" -lt 100 ]; do
capture_grid "$name" >"$TMP"
cmp -s "$TMP" "$EXP" && return 0
sleep 0.05
i=$((i + 1))
done
fail "$name (timed out waiting for discard)"
}
start_cmd csi-param-discard 8 3 \
"perl -e 'print qq{\e[}, q{1} x 80, qq{\030OK}'; sleep 2"
check_capture csi-param-discard 'OK'
@@ -11,12 +26,12 @@ start_cmd csi-interm-discard 8 3 \
check_capture csi-interm-discard 'OK'
start_cmd osc-discard 8 3 \
"perl -e 'print qq{\e]2;}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2"
check_capture osc-discard 'OK'
"perl -e 'print qq{\e]2;}, q{x} x 1100000, qq{\e\\\\OK}'; exec cat"
check_discard osc-discard
start_cmd apc-discard 8 3 \
"perl -e 'print qq{\e_}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2"
check_capture apc-discard 'OK'
"perl -e 'print qq{\e_}, q{x} x 1100000, qq{\e\\\\OK}'; exec cat"
check_discard apc-discard
start_pane unknown-csi 8 3 '\033[?9999zOK'
check_capture unknown-csi 'OK'

View File

@@ -5,12 +5,9 @@
start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n'
check_capture alternate 'MAINZ'
start_pane osc133 20 12 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nxz\033]133;A;k=s\007more\nxw\033]133;P;k=c\007more\nxv\033]133;A;k=c\007more\nzz\033]133;C\007out\033]133;D;7\007\nq\033]133;C\007bad\033]133;D;-1\007\nqq\033]133;C\007big\033]133;D;300\007\nzzz\033]133;C\007ok\033]133;D\007\n'
start_pane osc133 20 8 'xx\033]133;A\007p>\033]133;B\007cmd\nxy\033]133;P;k=s\007more\nzz\033]133;C\007out\033]133;D;7\007\nq\033]133;C\007bad\033]133;D;-1\007\nqq\033]133;C\007big\033]133;D;300\007\nzzz\033]133;C\007ok\033]133;D\007\n'
check_capture osc133 'xxp>cmd
xymore
xzmore
xwmore
xvmore
zzout
qbad
qqbig
@@ -18,13 +15,10 @@ zzzok'
check_raw_matches osc133 \
'L 0 \(0\) flags=START_PROMPT,START_COMMAND\[[0-9a-f]+\].* osc133=2,4,0,0,0' \
'L 1 \(1\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 2 \(2\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 3 \(3\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 4 \(4\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,2,5,7' \
'L 6 \(6\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,1,4,255' \
'L 7 \(7\) flags=START_OUTPUT,END_OUTPUT,END_OUTPUT_STATUS\[[0-9a-f]+\].* osc133=0,0,2,5,255' \
'L 8 \(8\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,0'
'L 2 \(2\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,7' \
'L 3 \(3\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,1,4,255' \
'L 4 \(4\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,255' \
'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,0'
$TMUX kill-server 2>/dev/null
exit $exit_status

View File

@@ -51,7 +51,7 @@ $TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 5 -y 3 -s history \; \
set-option -g history-limit 3 \; \
respawn-pane -k "printf '01\n02\n03\n04\n05\n06'; sleep 2" || exit 1
respawn-pane -k "printf '01\n02\n03\n04\n05\n06'; $INPUT_HOLD" || exit 1
sleep 0.3
$TMUX capture-pane -pN -t history: -S - -E - | normalize_capture >"$TMP"
printf "%s\n" '01

137
regress/json.sh Executable file
View File

@@ -0,0 +1,137 @@
#!/bin/sh
# Test parsing and printing JSON with display-message -j.
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
fail()
{
echo "$1"
$TMUX kill-server 2>/dev/null
exit 1
}
check()
{
actual=$($TMUX display-message -plj "$1" 2>&1) ||
fail "JSON was rejected: $1: $actual"
[ "$actual" = "$2" ] ||
fail "JSON output mismatch: $1: expected $2, got $actual"
}
check_fail()
{
if $TMUX display-message -plj "$1" >/dev/null 2>&1; then
fail "invalid JSON was accepted: $1"
fi
}
$TMUX new-session -d || exit 1
check '{}' '{}'
check ' { "z":true, "a":-9223372036854775808, "m":[{}, {"x":false}] } ' \
'{"a":-9223372036854775808,"m":[{},{"x":false}],"z":true}'
check '{"max":9223372036854775807,"min":-9223372036854775808}' \
'{"max":9223372036854775807,"min":-9223372036854775808}'
check '{" key":" value","{key":"[value"}' \
'{" key":" value","{key":"[value"}'
check '{"brace":"{value","bracket":"[value","colon":":value",'\
'"comma":",value","space":" value"}' \
'{"brace":"{value","bracket":"[value","colon":":value",'\
'"comma":",value","space":" value"}'
check '{"esc":"\"\\\/\b\f\n\r\t\u0041"}' \
'{"esc":"\"\\\/\b\f\n\r\t\u0041"}'
check '{"unicode":"\u0123\uabcd\uABCD"}' \
'{"unicode":"\u0123\uabcd\uABCD"}'
actual=$($TMUX display-message -palj '{}' 2>&1) ||
fail "display-message -aj rejected JSON: $actual"
[ "$actual" = '{}' ] ||
fail "display-message -aj did not ignore -a: got $actual"
actual=$($TMUX display-message -pIlj '{}' 2>&1) ||
fail "display-message -Ij rejected JSON: $actual"
[ "$actual" = '{}' ] ||
fail "display-message -Ij did not ignore -I: got $actual"
check_fail ''
check_fail ' '
check_fail '[]'
check_fail 'true'
check_fail '1'
check_fail '"string"'
check_fail '{}{}'
check_fail '{'
check_fail '{x:1}'
check_fail '{"":1}'
check_fail '{"x" 1}'
check_fail '{"x",1}'
check_fail '{"x":}'
check_fail '{"x":,}'
check_fail '{"x":1'
check_fail '{"x":1 "y":2}'
check_fail '{"x":1,,"y":2}'
check_fail '{"x":1,}'
check_fail '{"x":1,"x":2}'
check_fail '{"x":{}}{}'
check_fail '{"x":{}} trailing'
check_fail '{"x":{'
check_fail '{"x":{}'
check_fail '{"x":['
check_fail '{"x":[}'
check_fail '{"x":[{}'
check_fail '{"x":[{},}'
check_fail '{"x":[{},]}'
check_fail '{"x":[{}{}]}'
check_fail '{"x":[{},,{}]}'
check_fail '{"x":[{"y":}]}'
check_fail '{"x":""}'
check_fail '{"x":null}'
check_fail '{"x":[null]}'
check_fail '{"x":[true]}'
check_fail '{"x":["string"]}'
check_fail '{"x":[[]]}'
check_fail '{"x":1.0}'
check_fail '{"x":1e2}'
check_fail '{"x":+1}'
check_fail '{"x":-}'
check_fail '{"x":0x10}'
check_fail '{"x":01}'
check_fail '{"x":-01}'
check_fail '{"x":[1]}'
check_fail '{"x":9223372036854775808}'
check_fail '{"x":-9223372036854775809}'
check_fail '{"x":tru}'
check_fail '{"x":True}'
check_fail '{"x":falsee}'
check_fail '{"x":"bad\q"}'
check_fail '{"x":"bad\x20"}'
check_fail '{"x":"bad\u123"}'
check_fail '{"x":"bad\u12x4"}'
check_fail '{"x":"unterminated}'
check_fail '{"x":"escaped quote\"}'
check_fail '{"x":"line
break"}'
# The maximum object nesting depth is 200.
json='{}'
n=1
while [ "$n" -lt 200 ]; do
json='{"x":'"$json"'}'
n=$((n + 1))
done
check "$json" "$json"
check_fail '{"x":'"$json"'}'
if $TMUX display-message -pj >/dev/null 2>&1; then
fail "display-message -j accepted a missing message"
fi
$TMUX kill-server 2>/dev/null
exit 0

28
regress/kill-session-zoomed.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
# Killing a session whose window is zoomed must not crash the server:
# window_destroy used to unzoom the window, which resized the panes and
# fired pane-resized with the window in the payload, and releasing that
# reference destroyed the window a second time.
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
$TMUX -f/dev/null new -d -sfoo || exit 1
$TMUX split-window -d -h -tfoo:0 || exit 1
$TMUX resize-pane -Z -tfoo:0 || exit 1
$TMUX new -d -sbar || exit 1
$TMUX kill-session -tfoo || exit 1
$TMUX has-session -tbar || exit 1
# The same with the zoomed window in a session killed by kill-server.
$TMUX new -d -sbaz || exit 1
$TMUX split-window -d -v -tbaz:0 || exit 1
$TMUX resize-pane -Z -tbaz:0 || exit 1
$TMUX kill-server || exit 1
exit 0

987
regress/layout-custom.sh Normal file
View File

@@ -0,0 +1,987 @@
#!/bin/sh
# Tests of the custom layout dumper and evaluator in layout-custom.c, and of
# the JSON tokenizer and parser in json.c that the current layout format is
# built on.
#
# layout_dump is reached through the #{window_layout} and
# #{window_visible_layout} formats and layout_parse through
# "select-layout <layout>". json.c has no command of its own either:
# layout_construct sniffs the first non-blank character and hands anything
# starting with '{' to json_parse, so select-layout is the only way into it
# from the shell as well.
#
# Both layout formats are covered:
# - the current (v2) JSON format, which is what every client except an old
# control client sees;
# - the legacy (v1) format, which is still produced for a control client that
# has not asked for the "new-layouts" flag, and which is still accepted by
# the parser (the version is sniffed from the first character).
#
# This exercises:
# - dumping a single pane, a split, the "a" (active) and "l" (last pane) keys
# and the "z" key of a floating pane;
# - #{window_visible_layout} agreeing with #{window_layout};
# - the JSON syntax itself: insignificant whitespace, backslash escapes inside
# strings, the number and boolean forms, and one failure for each way json.c
# can reject an input that a layout string can carry;
# - a dump being parsed back to exactly the same layout (round trip), after
# another layout has been applied in between, and the same for a layout with
# two floating panes in it;
# - parsing a hand-written v2 layout;
# - "i" deciding which pane goes in which cell, checked with a layout whose
# cells are written in a different order from their indexes;
# - the same layouts with their fields in reversed and scrambled orders,
# including "c" before "t" and "V" after "L", neither of which changes the
# order the fields are read in;
# - a layout with more cells than the window has panes having the bottom right
# cells dropped, in both formats;
# - a layout naming no active or last pane leaving the active pane where it was
# and emptying the last pane stack, whether it leaves "a" out or gives it as
# false;
# - parsing a v1 layout and dumping it back as v1 through a control client,
# with the checksum computed here independently of layout_checksum(), and a
# v1 layout leaving the active pane and last pane stack untouched;
# - the legacy format meeting the floating panes it cannot represent: a v1 dump
# dropping the floating cells, both where that leaves the node they were in
# with one child so that it collapses, where it does not, and where adjacent
# nested floating-only subtrees are dropped, and a v1 layout being applied to
# a window that has floating panes without disturbing them, whether the tiled
# layout it names is a single cell or a split;
# - a window whose only tiled pane has been killed, which leaves it with a
# floating cell as its layout root or with a root node holding nothing but
# floating cells, producing an empty v1 body with a checksum, and being
# parsed as v1;
# - the %layout-change notification, in both formats at once: two control
# clients watching one layout change, only one of which has asked for new
# layouts, and the number of notifications a change produces in each format;
# - failures: a bad v1 header, checksum or body, a wrong version, a missing or
# duplicated root cell, missing sizes, sizes out of range, bad cell types, a
# pane cell missing "i", leaf cells with children and node cells with fewer
# than two, more than one active pane, too few cells for the panes and
# inconsistent sizes.
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 "$*" >&2
$TMUX kill-server 2>/dev/null
exit 1
}
# must_equal $what $got $expected
must_equal()
{
if [ "$2" != "$3" ]; then
echo "$1 wrong." >&2
echo "Expected: '$3'" >&2
echo "But got: '$2'" >&2
$TMUX kill-server 2>/dev/null
exit 1
fi
}
# must_differ $what $got $unwanted
must_differ()
{
[ "$2" != "$3" ] || fail "$1 unchanged: '$2'"
}
# must_contain $what $got $wanted
must_contain()
{
case "$2" in
*"$3"*) ;;
*) fail "$1: '$2' does not contain '$3'";;
esac
}
# check_ok $cmd...
#
# Run a command and require that it succeeds.
check_ok()
{
out=$($TMUX "$@" 2>&1) || fail "Command failed (expected success): $* ($out)"
}
# check_fail $cmd...
#
# Run a command and require that it fails. The error text itself is never
# checked anywhere in this test: the wording of a message is not part of what
# the layout formats promise, so matching on it only makes the test fail when a
# message is reworded.
check_fail()
{
$TMUX "$@" >/dev/null 2>&1 &&
fail "Command succeeded (expected failure): $*"
}
# layout $target
#
# The layout of a window with pane ids replaced by %N, so that the expected
# strings do not depend on which ids the server handed out.
layout()
{
$TMUX display-message -p -t "$1" '#{window_layout}' |
sed 's/%[0-9][0-9]*/%N/g'
}
# visible_layout $target
#
# As layout(), but the visible (zoomed) layout.
visible_layout()
{
$TMUX display-message -p -t "$1" '#{window_visible_layout}' |
sed 's/%[0-9][0-9]*/%N/g'
}
# raw_layout $target
#
# The layout of a window with the real pane ids left in place.
raw_layout()
{
$TMUX display-message -p -t "$1" '#{window_layout}'
}
# v1_layout $target
#
# The legacy (v1) dump of a window, which is what a control client that has not
# asked for the "new-layouts" flag is sent. A control client wraps its output in
# %begin/%end guard lines, which are dropped here.
v1_layout()
{
$TMUX -C display-message -p -t "$1" '#{window_layout}' | grep -v '^%'
}
# v1 $body
#
# Prefix a legacy (v1) layout body with its checksum. This is a separate
# implementation of layout_checksum(): a 16 bit rotate right then add, so a
# mistake in either one shows up as a mismatch.
v1()
{
awk -v s="$1" 'BEGIN {
for (i = 32; i < 127; i++)
ord[sprintf("%c", i)] = i
csum = 0
for (i = 1; i <= length(s); i++) {
csum = int(csum / 2) + (csum % 2) * 32768
csum = (csum + ord[substr(s, i, 1)]) % 65536
}
printf "%04x,%s\n", csum, s
}'
}
# A pane cell is dumped as its geometry, then "a" if it is the active pane or
# "l" with its position on the last pane stack if it is on it, then "i" with
# its pane index, then "z" if it is floating, then "I" with its pane id.
ONE='{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%N"}}'
check_ok new-session -d -s L -x 80 -y 24 -n one
p0=$($TMUX display-message -p -t L:one.0 '#{pane_id}')
# A single leaf cell filling the window. A pane cell must carry "i", its pane
# index; "I", its pane id, is written by the dumper and is here so that the cell
# is the same shape as a dumped one. The JSON checks below care about the syntax
# around the cell rather than the cell itself.
LEAF='{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$p0"'"}'
# ---------------------------------------------------------------------------
# Dumping a single pane.
# The root cell of a new window is the pane itself, and it is the active pane
# so it has "a" rather than "l".
must_equal 'Single pane layout' "$(layout L:one)" "$ONE"
# Nothing is zoomed, so the visible layout is the same.
must_equal 'Single pane visible layout' "$(visible_layout L:one)" "$ONE"
# ---------------------------------------------------------------------------
# More cells than panes.
# The bottom right cells are closed until as many are left as there are panes,
# so a two cell layout applied to a one pane window collapses back to the
# single pane filling the window: the cell that is left takes the space of the
# one that was closed. The window has one pane to name, so the cell that is
# closed carries an id belonging to no pane of it.
check_ok select-layout -t L:one \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$p0"'"},{"t":"p","w":80,"h":12,"x":0,"y":12,"i":1,"I":"%999"}]}}'
must_equal 'Trimmed layout' "$(layout L:one)" "$ONE"
# ---------------------------------------------------------------------------
# The JSON syntax.
#
# These run on the one pane window and are written so that what they prove
# depends on json.c rather than on the layout evaluation in layout-custom.c:
# an accepted layout is only required to leave the window as its single pane,
# and values that are not part of the layout format are carried on keys
# layout-custom.c never looks at ("n", "b" and so on), which it skips, so
# numbers, booleans and escapes can be exercised on their own.
#
# Objects nested in an array nested in an object are not checked here: every
# split layout below is one.
#
# Two of json.c's rejections cannot be reached from the shell and so are not
# covered: json_parse_tokens() refusing a top level that is not an object,
# because layout_construct() only calls json_parse() once the string already
# starts with '{'; and the maximum object depth, which needs a layout built by a
# program rather than one written out here.
# check_json_ok $what $layout
#
# select-layout must parse $layout and leave the window as its single pane.
check_json_ok()
{
check_ok select-layout -t L:one "$2"
must_equal "Layout after '$1'" "$(layout L:one)" "$ONE"
}
# check_json_fail $what $layout
#
# select-layout must reject $layout.
check_json_fail()
{
$TMUX select-layout -t L:one "$2" >/dev/null 2>&1 &&
fail "$1: select-layout succeeded (expected failure)"
}
# Whitespace between tokens is skipped. A number is scanned up to the ',', ']',
# '}' or whitespace that ends it, so a space after a number is fine but one
# inside it is not.
check_json_ok 'Spaces between tokens' \
'{ "V" : 2 , "L" : { "t" : "p" , "w" : 80 , "h" : 24 , "x" : 0 , "y" : 0 , "i" : 0 , "I" : "'"$p0"'" } }'
check_json_ok 'Newlines and tabs between tokens' "$(printf '{
\t"V": 2,
\t"L": {
\t\t"t": "p",
\t\t"w": 80,
\t\t"h": 24,
\t\t"x": 0,
\t\t"y": 0,
\t\t"i": 0,
\t\t"I": "%s"
\t}
}' "$p0")"
check_json_ok 'Carriage returns between tokens' \
"$(printf '{\r"V":2,\r"L":%s\r}' "$LEAF")"
# A backslash makes the tokenizer consume the next character whatever it is, so
# an escaped quote does not end the string. The key is not one that
# layout-custom.c looks at, so all that is being checked is that the string
# ended in the right place and the object still parsed.
check_json_ok 'Escaped quote in a string' \
'{"V":2,"a\"b":0,"L":'"$LEAF"'}'
# An escaped backslash immediately before the closing quote: the escape has to
# be cleared again so that the quote after it does end the string.
check_json_ok 'Escaped backslash before the closing quote' \
'{"V":2,"a\\":0,"L":'"$LEAF"'}'
# Numbers and booleans, again on keys layout-custom.c ignores, so only json.c
# decides whether they are accepted.
check_json_ok 'Zero' '{"V":2,"n":0,"L":'"$LEAF"'}'
check_json_ok 'Several digits' '{"V":2,"n":1234567,"L":'"$LEAF"'}'
check_json_ok 'Negative number' '{"V":2,"n":-42,"L":'"$LEAF"'}'
check_json_ok 'Booleans' '{"V":2,"b":true,"d":false,"L":'"$LEAF"'}'
# Tokenizer failures. A value that runs to the end of the input has no
# terminator, so it is the tokenizer rather than the parser that gives up. Both
# the number scan and the string scan have to notice this, and with the closing
# quote escaped there is no terminator left either.
check_json_fail 'Unterminated number' '{"V":2'
check_json_fail 'Unterminated string' '{"V":"x'
check_json_fail 'Escaped closing quote' '{"V":2,"L":{"t":"p\"}}'
# Something that is not a quoted string where a key belongs.
check_json_fail 'Missing key' '{"V":2,,"L":'"$LEAF"'}'
# A key not followed by ':'.
check_json_fail 'Missing colon' '{"V","L":2}'
# A bare word that is neither "true", "false" nor a number. This is where
# "null" ends up.
check_json_fail 'Unknown literal' '{"V":null,"L":'"$LEAF"'}'
# A ':' with no value after it, so the token where the value belongs is one the
# object parser has no case for.
check_json_fail 'Missing value' '{"V":}'
# A ',' with nothing after it, and a value with no ',' before the next key.
check_json_fail 'Trailing comma in an object' '{"V":2,"L":'"$LEAF"',}'
check_json_fail 'Missing comma in an object' '{"V":2 "L":'"$LEAF"'}'
# Arrays hold objects and nothing else.
check_json_fail 'Non-object in an array' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":["x"]}}'
check_json_fail 'Trailing comma in an array' \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":['"$LEAF"',]}}'
# An empty string is two adjacent quotes with no value token between them,
# which the string parser does not accept.
check_json_fail 'Empty string' '{"V":2,"L":""}'
# A number token that strtoll does not consume all of.
check_json_fail 'Number with trailing characters' '{"V":8a,"L":'"$LEAF"'}'
# Anything after the top level object.
check_json_fail 'Data after the top level object' '{"V":2,"L":'"$LEAF"'}{}'
# None of the rejections touched the layout.
must_equal 'Layout after rejected parses' "$(layout L:one)" "$ONE"
# ---------------------------------------------------------------------------
# Dumping a split.
check_ok new-window -d -t L:2 -n two
q0=$($TMUX display-message -p -t L:two.0 '#{pane_id}')
# -l 12 gives the new (bottom) pane 12 lines, leaving 11 for the top pane and
# one for the border between them. With -d the top pane stays active.
check_ok split-window -d -v -l 12 -t L:two.0
q1=$($TMUX display-message -p -t L:two.1 '#{pane_id}')
# Nothing has changed the active pane, so the last pane stack is still empty
# and the bottom pane has neither "a" nor "l".
must_equal 'Split layout' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"i":1,"I":"%N"}]}}'
# ---------------------------------------------------------------------------
# The active and last pane keys.
# Selecting the bottom pane makes it active and pushes the top pane onto the
# last pane stack, where it is at index 0.
check_ok select-pane -t "$q1"
must_equal 'Layout after select-pane' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"l":0,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"a":true,"i":1,"I":"%N"}]}}'
# Selecting the top pane again swaps the two keys over. "i" and "I" do not
# move: they are the pane's position in the window and its id.
check_ok select-pane -t "$q0"
SPLIT='{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":12,"x":0,"y":12,"l":0,"i":1,"I":"%N"}]}}'
must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT"
# ---------------------------------------------------------------------------
# The visible layout.
# With nothing zoomed the two layout formats agree.
#
# The zoomed case is deliberately not covered here. While a pane is zoomed
# #{window_layout} dumps the saved (unzoomed) layout and
# #{window_visible_layout} the zoomed one, but that depends on how zooming
# stashes the layout root rather than on anything in layout-custom.c.
must_equal 'Visible layout' "$(visible_layout L:two)" "$SPLIT"
# ---------------------------------------------------------------------------
# Round trip.
# Make the two panes obviously uneven so that the layout applied in between
# cannot be mistaken for the saved one. A resize shows up in the dump as the
# new cell sizes and offsets.
check_ok resize-pane -t "$q0" -y 5
saved=$(raw_layout L:two)
must_equal 'Resized layout' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":5,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":18,"x":0,"y":6,"l":0,"i":1,"I":"%N"}]}}'
check_ok select-layout -t L:two even-vertical
must_differ 'Layout after even-vertical' "$(raw_layout L:two)" "$saved"
# Parsing a dump gives back exactly the same dump, pane ids included. The panes
# go back into the cells that named them: the cells are ordered by "i" and then
# given the window's panes in order, so a cell dumped with "i":k must come back
# the k'th.
check_ok select-layout -t L:two "$saved"
must_equal 'Round tripped layout' "$(raw_layout L:two)" "$saved"
# ---------------------------------------------------------------------------
# Parsing a hand-written layout.
# Laid out over several lines to keep it readable; that the whitespace is
# skipped at all is json.c's business, what matters here is that the cells come
# out of it in the right shape.
#
# "a" and "l" are given on the cells so that the active pane and the last pane
# stack are pinned by the layout rather than left to whatever a layout that
# names neither happens to produce.
check_ok select-layout -t L:two "$(printf '{
"V": 2,
"L": {
"t": "h",
"w": 80,
"h": 24,
"x": 0,
"y": 0,
"c": [
{"t": "p", "w": 30, "h": 24, "x": 0, "y": 0, "a": true, "i": 0, "I": "%s"},
{"t": "p", "w": 49, "h": 24, "x": 31, "y": 0, "l": 0, "i": 1, "I": "%s"}
]
}
}' "$q0" "$q1")"
must_equal 'Hand-written layout' "$(layout L:two)" \
'{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":30,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":49,"h":24,"x":31,"y":0,"l":0,"i":1,"I":"%N"}]}}'
# The panes are assigned to the cells in order.
must_equal 'First pane width' \
"$($TMUX display-message -p -t "$q0" '#{pane_width}')" '30'
must_equal 'Second pane width' \
"$($TMUX display-message -p -t "$q1" '#{pane_width}')" '49'
# ---------------------------------------------------------------------------
# Field order.
# Fields are looked up by key once the object has been parsed, so the order
# they are written in must give the same layout. Here every object has its keys
# reversed: "c" comes before "t" and "V" comes after "L", neither of which
# changes the order they are read in - the cell type is always read before the
# children and the version before the layout.
check_ok select-layout -t L:two \
'{"L":{"c":[{"I":"'"$q0"'","i":0,"a":true,"y":0,"x":0,"h":8,"w":80,"t":"p"},{"I":"'"$q1"'","i":1,"l":0,"y":9,"x":0,"h":15,"w":80,"t":"p"}],"y":0,"x":0,"h":24,"w":80,"t":"v"},"V":2}'
must_equal 'Reversed field order' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":15,"x":0,"y":9,"l":0,"i":1,"I":"%N"}]}}'
# Keys interleaved rather than simply reversed, with "c" in the middle. This
# time "a" is on the second cell, so the second pane becomes the active one:
# which pane is active comes from the layout, while "i" and "I" still come from
# the window. The first cell names neither "a" nor "l", so its pane is neither
# active nor on the last pane stack and the dump gives it neither key.
check_ok select-layout -t L:two \
'{"V":2,"L":{"h":24,"c":[{"w":40,"t":"p","y":0,"i":0,"h":24,"I":"'"$q0"'","x":0},{"a":true,"h":24,"I":"'"$q1"'","w":39,"y":0,"t":"p","i":1,"x":41}],"w":80,"y":0,"t":"h","x":0}}'
must_equal 'Scrambled field order' "$(layout L:two)" \
'{"V":2,"L":{"t":"h","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":40,"h":24,"x":0,"y":0,"i":0,"I":"%N"},{"t":"p","w":39,"h":24,"x":41,"y":0,"a":true,"i":1,"I":"%N"}]}}'
# ---------------------------------------------------------------------------
# The legacy (v1) format.
# The layout just applied, in v1: a left/right cell is written with braces and
# a top/bottom cell with brackets, and each leaf carries its pane id without
# the leading %.
v1body="80x24,0,0{40x24,0,0,${q0#%},39x24,41,0,${q1#%}}"
# A control client that has not asked for new layouts is dumped v1.
must_equal 'v1 dump' "$(v1_layout L:two)" "$(v1 "$v1body")"
# With the new-layouts flag the same client is dumped v2 instead. The flag is
# set with "attach -f" rather than refresh-client because refresh-client needs
# a current client, which a control client that has not attached has not got.
got=$(printf "display-message -p -t L:two '#{window_layout}'\n" |
$TMUX -C attach -f new-layouts -t L 2>&1 | grep -v '^%')
must_contain 'v2 dump for control client' "$got" '{"V":2,"L":'
# A v1 layout with a correct checksum is parsed, and dumping v1 again gives
# back the same string. That is the whole of what v1 carries: the cells take
# the sizes and offsets from the body, and the panes are assigned to them in
# order, which is what puts the same two ids back in the same two places. It is
# checked in v1 rather than against a v2 dump so that nothing v1 has no opinion
# on - the active pane, the last pane stack, the pane index - comes into it.
v1vsplit="80x24,0,0[80x11,0,0,${q0#%},80x12,0,12,${q1#%}]"
check_ok select-layout -t L:two "$(v1 "$v1vsplit")"
must_equal 'v1 round trip' "$(v1_layout L:two)" "$(v1 "$v1vsplit")"
# v1 names no active pane, last pane or z-index and must disturb none of them.
# Applying the v1 form of the layout the window already has therefore leaves
# even the v2 dump the same byte for byte, last pane stack included.
check_ok select-pane -t "$q1"
check_ok select-pane -t "$q0"
before=$(raw_layout L:two)
check_ok select-layout -t L:two "$(v1 "$v1vsplit")"
must_equal 'v1 leaves the active and last panes alone' \
"$(raw_layout L:two)" "$before"
# A v1 layout with more cells than the window has panes is trimmed like any
# other: the bottom right cell is closed and the cell above it takes its eight
# rows and the border between them, leaving 16. Pane ids in a v1 body are not
# used to place panes, so the third cell can carry any id.
v1three="80x24,0,0[80x7,0,0,${q0#%},80x7,0,8,${q1#%},80x8,0,16,999]"
check_ok select-layout -t L:two "$(v1 "$v1three")"
must_equal 'v1 layout trimmed' "$(v1_layout L:two)" \
"$(v1 "80x24,0,0[80x7,0,0,${q0#%},80x16,0,8,${q1#%}]")"
# ---------------------------------------------------------------------------
# Pane assignment order.
# "i" is what decides which pane goes into which cell: the cells are ordered by
# it and then handed the window's panes in order, so the cell with "i":0 takes
# the first pane of the window wherever that cell sits in the layout. Here the
# cells are written the other way round from their indexes - the first cell in
# the string is "i":1 and the second "i":0 - so the first pane of the window
# has to come out in the second cell.
#
# Every other layout above lists its cells in the same order as their indexes,
# which is the order the tree is walked in, so this is the only check that can
# tell the two apart.
check_ok select-layout -t L:two \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"i":1,"I":"'"$q1"'"},{"t":"p","w":80,"h":15,"x":0,"y":9,"i":0,"I":"'"$q0"'"}]}}'
must_equal 'First pane height' \
"$($TMUX display-message -p -t "$q0" '#{pane_height}')" '15'
must_equal 'Second pane height' \
"$($TMUX display-message -p -t "$q1" '#{pane_height}')" '8'
# So the dump carries the two ids the other way round from every dump above,
# and with them their indexes, which are the panes' positions in the window and
# have not moved. Neither cell named an active or last pane, so the pane that
# was active still is - it is now the one in the second cell.
swapped='{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":8,"x":0,"y":0,"i":1,"I":"'"$q1"'"},{"t":"p","w":80,"h":15,"x":0,"y":9,"a":true,"i":0,"I":"'"$q0"'"}]}}'
must_equal 'Layout with the panes swapped' "$(raw_layout L:two)" "$swapped"
# And that dump round trips, indexes out of order and all.
check_ok select-layout -t L:two "$swapped"
must_equal 'Round tripped swapped layout' "$(raw_layout L:two)" "$swapped"
# ---------------------------------------------------------------------------
# Cells that name no active or last pane.
# "a" and "l" are the only things that decide which pane is active and what is
# on the last pane stack, so a layout naming neither leaves the active pane
# where it was and empties the stack. Here the first pane of the window is
# active and the second is at index 0 of the stack beforehand; afterwards the
# first pane is still active and the stack is empty, so the second pane has no
# "l".
check_ok select-pane -t "$q1"
check_ok select-pane -t "$q0"
check_ok select-layout -t L:two \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"'"$q1"'"}]}}'
must_equal 'Layout naming no active pane' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"%N"}]}}'
# "a" may be given as false, which says the same as leaving it out: this pane
# is not the active one. A layout where every cell says so names no active pane
# at all and so leaves the active pane alone, exactly as the layout above did.
check_ok select-layout -t L:two \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":10,"x":0,"y":0,"a":false,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":13,"x":0,"y":11,"a":false,"i":1,"I":"'"$q1"'"}]}}'
must_equal 'Layout with only false active panes' "$(layout L:two)" \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":10,"x":0,"y":0,"a":true,"i":0,"I":"%N"},{"t":"p","w":80,"h":13,"x":0,"y":11,"i":1,"I":"%N"}]}}'
# ---------------------------------------------------------------------------
# Failures.
#
# Each of these is a different reason for a layout to be rejected, but only the
# rejection itself is checked; the message that comes back with it is not.
# check_layout_fail $layout
#
# select-layout must reject $layout.
check_layout_fail()
{
check_fail select-layout -t L:two "$1"
}
# A rejected layout must leave the window alone, whatever it was.
unchanged=$(raw_layout L:two)
# Not JSON and not a checksum.
check_layout_fail 'garbage'
# A v1 body with its checksum left off, and a string of nothing but hex digits.
# A v1 header is four hex digits and a comma; neither of these has one, so there
# is no header and nothing to check a body against.
check_layout_fail '80x24,0,0'
check_layout_fail 'ab'
# A v1 header with the checksum of a different body.
good=$(v1 '80x24,0,0')
check_layout_fail "${good%%,*},80x24,0,1"
# A correct checksum over a body that is not a layout: a cell with no offsets,
# and a top to bottom cell closed with '}' instead of ']'. layout_construct_v1
# returns NULL for both.
check_layout_fail "$(v1 '80x24')"
check_layout_fail "$(v1 '80x24,0,0[80x11,0,0,80x12,0,12}')"
# Fewer cells than the window has panes; unlike the other way around this
# cannot be fixed up.
check_layout_fail '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}'
# The children of a top to bottom cell must all be the width of their parent.
check_layout_fail \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":40,"h":12,"x":0,"y":12,"i":1,"I":"'"$q1"'"}]}}'
# The rest are valid JSON, so it is layout_parse_json() and
# layout_parse_json_layout() doing the rejecting rather than json.c. Each of
# them is a layout that would be applied but for the one thing being checked.
# Two root cells.
check_layout_fail \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"},"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}'
# A missing "y". A cell needs all four of "w", "h", "x" and "y".
check_layout_fail '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"i":0,"I":"'"$q0"'"}}'
# Cell sizes are bounded below by one column or row and above by 10000 of
# either. Both cases are otherwise complete two cell layouts, so the size is
# the only thing wrong with them.
check_layout_fail \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":0,"h":12,"x":0,"y":12,"i":1,"I":"'"$q1"'"}]}}'
check_layout_fail \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":10001,"x":0,"y":12,"i":1,"I":"'"$q1"'"}]}}'
# An unknown cell type: only "h", "v" and "p" exist.
check_layout_fail '{"V":2,"L":{"t":"q","w":80,"h":24,"x":0,"y":0}}'
# A pane cell needs "i", its pane index. It is "i" that says which pane goes in
# the cell; "I" is the pane id the cell was dumped with and is not read back.
check_layout_fail '{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"I":"'"$q0"'"}}'
# A node cell must have more than one child and a leaf cell must have none. A
# node is written with no "c" at all, with an empty one and with a single child.
check_layout_fail '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0}}'
check_layout_fail '{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[]}}'
check_layout_fail \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}]}}'
check_layout_fail \
'{"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'","c":[{"t":"p","w":80,"h":24,"x":0,"y":0,"i":1,"I":"'"$q1"'"}]}}'
# Only one cell may be the active pane.
check_layout_fail \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"a":true,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":12,"x":0,"y":12,"a":true,"i":1,"I":"'"$q1"'"}]}}'
# The same rejections apply whatever order the fields are written in: a leaf
# with children when "c" comes first, and a node with no children and a bad cell
# type when "t" comes last.
check_layout_fail \
'{"V":2,"L":{"c":[{"t":"p","w":80,"h":24,"x":0,"y":0,"i":1,"I":"'"$q1"'"}],"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}'
check_layout_fail '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"v"}}'
check_layout_fail '{"V":2,"L":{"w":80,"h":24,"x":0,"y":0,"t":"q"}}'
# A child that fails after a sibling has already been parsed and added to the
# parent. This is the case the cleanup at the end of layout_parse_json_layout
# exists for: the children built so far have to be freed along with the parent
# that is never returned. The second child has no "y".
check_layout_fail \
'{"V":2,"L":{"c":[{"t":"p","w":80,"h":11,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":12,"x":0,"i":1,"I":"'"$q1"'"}],"t":"v","w":80,"h":24,"x":0,"y":0}}'
# No root cell at all. Every other rejection above comes from a cell that
# failed to parse; this one is the check for "L" itself.
check_layout_fail '{"V":2}'
# The wrong version, with "V" before and after "L". Fields are looked up by
# key, so the version is read before the layout either way and the position of
# "V" in the string makes no difference.
check_layout_fail '{"V":1,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"}}'
check_layout_fail '{"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"i":0,"I":"'"$q0"'"},"V":1}'
# None of that touched the layout.
must_equal 'Layout after failures' "$(raw_layout L:two)" "$unchanged"
# ---------------------------------------------------------------------------
# Floating panes.
check_ok new-window -d -t L:3 -n float
check_ok select-window -t L:float
check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 'sleep 100'
check_ok new-pane -d -x 30 -y 8 -X 30 -Y 10 'sleep 100'
# The tiled pane and the two floating ones. A floating pane goes on the end of
# the window's pane list, so the pane indexes are in the order the panes were
# made whatever order their cells end up in.
f0=$($TMUX display-message -p -t L:float.0 '#{pane_id}')
fa=$($TMUX display-message -p -t L:float.1 '#{pane_id}')
fb=$($TMUX display-message -p -t L:float.2 '#{pane_id}')
# A floating cell is dumped with its z-index, which is what marks it as
# floating when the layout is parsed back. Two of them, so that there is an
# order between them to get wrong: the newer floating pane is in front, and a
# cell's "z" is its place in that order counting from the front.
floating=$(raw_layout L:float)
must_contain 'Floating layout front z-index' "$floating" '"z":0'
must_contain 'Floating layout back z-index' "$floating" '"z":1'
# Each floating cell goes in after the cell of the pane that was current when
# it was made, which is the tiled pane both times, so the newer floating cell
# is written before the older one while its pane comes after in the window.
# The dump therefore has its cells in one order and their indexes in another,
# and only comes back the same if the panes go by index.
check_ok select-layout -t L:float "$floating"
must_equal 'Floating layout after round trip' "$(raw_layout L:float)" "$floating"
# ---------------------------------------------------------------------------
# Floating panes and the legacy (v1) format.
#
# v1 has no way to write a floating pane down, so the two formats cannot say the
# same thing about a window that has one. Dumping v1 takes a copy of the layout,
# deletes the floating cells from the copy and dumps what is left; parsing v1
# rearranges the tiled panes and leaves the floating ones where they are. None
# of this is reached above: every v1 check so far runs on a window that has no
# floating panes, and every floating pane check so far is in v2.
# float_state $target
#
# Everything about a floating pane that a v1 layout has no way to carry, so that
# applying one can be checked against all of it at once.
float_state()
{
$TMUX display-message -p -t "$1" \
'#{pane_floating_flag} #{pane_width}x#{pane_height} #{pane_left},#{pane_top} #{pane_z}'
}
# Deleting both floating cells from the copy leaves the root node with a single
# child, and a node with a single child collapses into it, so the root of the
# copy is the tiled cell and the dump is that cell on its own filling the
# window.
must_equal 'v1 dump with floating panes' "$(v1_layout L:float)" \
"$(v1 "80x24,0,0,${f0#%}")"
# The cells are deleted from the copy, so the window itself comes through a v1
# dump untouched - floating panes, z-indexes and all.
must_equal 'Layout after a v1 dump' "$(raw_layout L:float)" "$floating"
# The same with a split, where deleting the floating cell still leaves two
# children behind and the node it was in does not collapse.
check_ok new-window -d -t L:4 -n mixed
m0=$($TMUX display-message -p -t L:mixed.0 '#{pane_id}')
check_ok split-window -d -v -l 12 -t L:mixed.0
m1=$($TMUX display-message -p -t L:mixed.1 '#{pane_id}')
check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 -t L:mixed.0 'sleep 100'
mf=$($TMUX display-message -p -t L:mixed.2 '#{pane_id}')
# A floating pane takes no space from the tiled layout, so the two tiled cells
# are the same 11 and 12 rows the split gave them.
must_equal 'v1 dump with a split and a floating pane' "$(v1_layout L:mixed)" \
"$(v1 "80x24,0,0[80x11,0,0,${m0#%},80x12,0,12,${m1#%}]")"
# A v1 layout applied to a window that has a floating pane rearranges the tiled
# panes and must leave the floating one exactly as it was: v1 names no floating
# pane, so there is nothing in it for one to be changed by. The top pane goes
# from 11 rows to 7 and the bottom one from 12 to 16.
v1mixed="80x24,0,0[80x7,0,0,${m0#%},80x16,0,8,${m1#%}]"
mfbefore=$(float_state "$mf")
check_ok select-layout -t L:mixed "$(v1 "$v1mixed")"
must_equal 'v1 layout with a floating pane' "$(v1_layout L:mixed)" \
"$(v1 "$v1mixed")"
must_equal 'Floating pane after a v1 layout' "$(float_state "$mf")" "$mfbefore"
must_equal 'Panes after a v1 layout' \
"$($TMUX display-message -p -t L:mixed '#{window_panes}')" '3'
# When the tiled layout a v1 string names is a single cell there is no node in
# the new layout for the floating cells to go back into, so one is made: the
# root cell is replaced by a top to bottom node holding it and the floating
# cells go on the end. Nothing else here reaches that.
fabefore=$(float_state "$fa")
fbbefore=$(float_state "$fb")
check_ok select-layout -t L:float "$(v1 "80x24,0,0,${f0#%}")"
must_equal 'v1 single cell layout with floating panes' "$(v1_layout L:float)" \
"$(v1 "80x24,0,0,${f0#%}")"
must_equal 'Front floating pane after a v1 layout' "$(float_state "$fb")" \
"$fbbefore"
must_equal 'Back floating pane after a v1 layout' "$(float_state "$fa")" \
"$fabefore"
must_equal 'Panes after a v1 single cell layout' \
"$($TMUX display-message -p -t L:float '#{window_panes}')" '3'
# Adjacent subtrees containing only floating panes used to be a distinct case:
# dumping v1 made a copy of the v2 tree and deleted floating cells from the
# copy, but deleting the last floating cell in the first subtree collapsed the
# parent and could leave the outer traversal holding a stale pointer to the
# second subtree.
check_ok new-window -d -t L:5 -n nested
n0=$($TMUX display-message -p -t L:nested.0 '#{pane_id}')
check_ok split-window -d -v -l 12 -t L:nested.0
n1=$($TMUX display-message -p -t L:nested.1 '#{pane_id}')
check_ok split-window -d -v -l 6 -t L:nested.1
n2=$($TMUX display-message -p -t L:nested.2 '#{pane_id}')
check_ok split-window -d -v -l 3 -t L:nested.2
n3=$($TMUX display-message -p -t L:nested.3 '#{pane_id}')
check_ok split-window -d -v -l 2 -t L:nested.3
n4=$($TMUX display-message -p -t L:nested.4 '#{pane_id}')
check_ok select-layout -t L:nested \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"h","w":30,"h":10,"x":0,"y":0,"c":[{"t":"p","w":10,"h":5,"x":2,"y":2,"i":0,"z":0},{"t":"p","w":12,"h":6,"x":5,"y":5,"i":1,"z":1}]},{"t":"h","w":30,"h":10,"x":0,"y":0,"c":[{"t":"p","w":14,"h":7,"x":8,"y":8,"i":2,"z":2},{"t":"p","w":16,"h":8,"x":11,"y":11,"i":3,"z":3}]},{"t":"p","w":80,"h":24,"x":0,"y":0,"i":4}]}}'
must_equal 'v1 dump with nested floating-only subtrees' \
"$(v1_layout L:nested)" "$(v1 "80x24,0,0,${n4#%}")"
check_ok kill-window -t L:nested
# ---------------------------------------------------------------------------
# A window with no tiled panes.
#
# Killing the last tiled pane of a window that has floating panes does not kill
# the window: that only happens when the pane being killed is the last one
# counting the floating ones. What is left is a window whose layout root is
# either a floating cell on its own, or a node holding nothing but floating
# cells, depending on how many are left. v1 has no way to write either down, so
# it must not try: a layout with no tiled panes in it produces no v1 dump at
# all, and #{window_layout} comes back empty for a client being sent v1. What v2
# makes of such a window is a separate question and is not checked here.
#
# A dead server dumps nothing either, so the checks below have to establish that
# the server is still there before an empty dump is allowed to mean anything.
# no_hang $cmd...
#
# Run a command whose result is not being checked, but which has to come back:
# only the server surviving it is checked afterwards, and a server wedged rather
# than killed would otherwise show up as the test never finishing.
no_hang()
{
if command -v timeout >/dev/null 2>&1; then
timeout 10 "$@" >/dev/null 2>&1
else
"$@" >/dev/null 2>&1
fi
return 0
}
# One floating pane left. The node it and the tiled cell were in is down to a
# single child, so it collapses and the floating cell becomes the root.
check_ok new-window -d -t L:5 -n gone1
g0=$($TMUX display-message -p -t L:gone1.0 '#{pane_id}')
check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 -t L:gone1.0 'sleep 100'
check_ok kill-pane -t "$g0"
must_equal 'Panes left with one floating pane' \
"$($TMUX display-message -p -t L:gone1 '#{window_panes}')" '1'
# The floating cell is the root and there is nothing tiled under it, so there is
# only an empty v1 body to dump. The floating cell must not be written out on
# its own, which would be a layout claiming the window is the size and position
# of the floating pane with no pane in it at all.
got=$(v1_layout L:gone1)
check_ok display-message -p alive
must_equal 'v1 dump with one floating pane and no tiled panes' "$got" '0000,'
# Two floating panes left, so the node keeps two children, does not collapse,
# and stays the root with nothing but floating cells in it.
check_ok new-window -d -t L:6 -n gone2
h0=$($TMUX display-message -p -t L:gone2.0 '#{pane_id}')
check_ok new-pane -d -x 20 -y 6 -X 8 -Y 3 -t L:gone2.0 'sleep 100'
check_ok new-pane -d -x 30 -y 8 -X 30 -Y 10 -t L:gone2.0 'sleep 100'
check_ok kill-pane -t "$h0"
must_equal 'Panes left with two floating panes' \
"$($TMUX display-message -p -t L:gone2 '#{window_panes}')" '2'
# The node is the root this time rather than the floating cell, but it has no
# tiled cell anywhere under it either, so the v1 body is still empty -
# and making one must not take the server with it.
got=$(v1_layout L:gone2)
check_ok display-message -p alive
must_equal 'v1 dump with two floating panes and no tiled panes' "$got" '0000,'
# Nor must parsing a v1 layout against it. There is no tiled pane for the
# layout to name, so whether it is applied or rejected is the format's business;
# it just has to be one of the two.
no_hang $TMUX select-layout -t L:gone2 "$(v1 '80x24,0,0,999')"
check_ok display-message -p alive
check_ok kill-window -t L:gone1
check_ok kill-window -t L:gone2
# ---------------------------------------------------------------------------
# Control mode notifications.
#
# %layout-change is what a control client actually reads a layout from, and it
# carries both #{window_layout} and #{window_visible_layout}. Its template is
# expanded once per client (control-notify.c), so two clients watching the same
# window must be told about the same change in different formats: v1 for the
# one that has not asked for new layouts, v2 for the one that has.
#
# The dumps above go through "-C display-message", which only ever reaches the
# format callbacks for the client asking. This needs clients that stay
# attached while something else changes the layout, so they go on the end of
# fifos and the change is made from outside.
DIR=$(mktemp -d) || fail 'Could not make a temporary directory'
OLDIN="$DIR/old-in"
OLDOUT="$DIR/old-out"
NEWIN="$DIR/new-in"
NEWOUT="$DIR/new-out"
OLDPID=
NEWPID=
cleanup()
{
[ -n "$OLDPID" ] && kill "$OLDPID" 2>/dev/null
[ -n "$NEWPID" ] && kill "$NEWPID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup EXIT
# wait_for $file $text
#
# Wait for $text to appear in a control client's output.
wait_for()
{
i=0
while [ "$i" -lt 6 ]; do
grep -F -- "$2" "$1" >/dev/null 2>&1 && return 0
sleep 1
i=$((i + 1))
done
echo "missing from $1: $2" >&2
cat "$1" >&2
return 1
}
mkfifo "$OLDIN" "$NEWIN" || fail 'Could not make the control client fifos'
: >"$OLDOUT"
: >"$NEWOUT"
$TMUX -C attach -t L <"$OLDIN" >"$OLDOUT" 2>&1 &
OLDPID=$!
exec 4>"$OLDIN"
$TMUX -C attach -f new-layouts -t L <"$NEWIN" >"$NEWOUT" 2>&1 &
NEWPID=$!
exec 5>"$NEWIN"
# Both clients have to be attached before the layout changes, or they miss the
# notification entirely.
printf 'display-message -p ready\n' >&4
printf 'display-message -p ready\n' >&5
wait_for "$OLDOUT" ready || fail 'Control client without new-layouts did not attach'
wait_for "$NEWOUT" ready || fail 'Control client with new-layouts did not attach'
wid=$($TMUX display-message -p -t L:two '#{window_id}')
# One layout change, made by a third client so that neither of the two is the
# one running the command. 8 lines for the top pane leaves 15 for the bottom
# and one for the border.
check_ok resize-pane -t "$q0" -y 8
# Nothing is zoomed, so both fields of the notification carry the same layout.
# The v2 one is compared against the dump rather than a literal so that it is
# the two formats being checked and not the geometry again.
v2now=$(raw_layout L:two)
v1now=$(v1 "80x24,0,0[80x8,0,0,${q0#%},80x15,0,9,${q1#%}]")
wait_for "$NEWOUT" "%layout-change $wid $v2now $v2now " ||
fail 'No v2 %layout-change for the client with new-layouts'
wait_for "$OLDOUT" "%layout-change $wid $v1now $v1now " ||
fail 'No v1 %layout-change for the client without new-layouts'
# How many notifications one layout change produces, which differs by format
# on purpose. cmd_select_layout_exec() fires window-layout-changed for any
# layout it applies, and layout_parse() fires it again for a v1 one, so v1
# arrives twice - which is what master does for every layout, and what control
# clients written against it expect. v2 is new and has no such clients, so it
# gets the single notification. Counting the delta rather than the total, with
# a settle in between, keeps this independent of what has already been sent.
n1=$(grep -c "%layout-change $wid " "$OLDOUT")
check_ok select-layout -t L:two \
'{"V":2,"L":{"t":"v","w":80,"h":24,"x":0,"y":0,"c":[{"t":"p","w":80,"h":9,"x":0,"y":0,"i":0,"I":"'"$q0"'"},{"t":"p","w":80,"h":14,"x":0,"y":10,"i":1,"I":"'"$q1"'"}]}}'
sleep 2
n2=$(grep -c "%layout-change $wid " "$OLDOUT")
must_equal 'Notifications for a v2 layout' "$((n2 - n1))" '1'
check_ok select-layout -t L:two "$(v1 "$v1vsplit")"
sleep 2
n3=$(grep -c "%layout-change $wid " "$OLDOUT")
must_equal 'Notifications for a v1 layout' "$((n3 - n2))" '2'
# And the client that did not ask for new layouts must never have been sent
# one, in that notification or any other.
grep -F '{"V":2,' "$OLDOUT" >/dev/null 2>&1 &&
fail 'Control client without new-layouts was sent a v2 layout'
if [ "$($TMUX display-message -p alive 2>&1)" != "alive" ]; then
echo "Server died." >&2
exit 1
fi
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -0,0 +1,77 @@
#!/bin/sh
# Version 1 layouts must reject excessive nesting without losing the session or
# changing its layout. Check both split types and the depth limit boundary.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX="$TEST_TMUX -S$DIR/socket -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0
trap 'exit 1' 1 2 15
# Generate a checksum-valid v1 layout with one child in every nested group.
layout()
{
awk -v depth="$1" -v open="$2" 'BEGIN {
closing = (open == "{" ? "}" : "]")
body = ""
for (i = 0; i < depth; i++)
body = body "1x1,0,0" open
body = body "1x1,0,0"
for (i = 0; i < depth; i++)
body = body closing
ord["0"] = 48; ord["1"] = 49; ord["x"] = 120
ord[","] = 44; ord["{"] = 123; ord["}"] = 125
ord["["] = 91; ord["]"] = 93
csum = 0
for (i = 1; i <= length(body); i++) {
c = substr(body, i, 1)
bit = csum % 2
csum = int(csum / 2) + bit * 32768
csum = (csum + ord[c]) % 65536
}
printf "%04x,%s", csum, body
}'
}
$TMUX new-session -d -s deep -x80 -y24 'exec sleep 100' || exit 1
for open in '{' '['; do
# A valid checksum and nesting up to the limit must still be accepted.
value=$(layout 1000 "$open") || fail "could not generate layout"
$TMUX select-layout -t deep "$value" ||
fail "layout at the depth limit was rejected ($open)"
before=$($TMUX display-message -p -t deep \
'#{pane_id} #{pane_width} #{pane_height} #{window_layout}') || exit 1
for depth in 1001 1500; do
value=$(layout "$depth" "$open") || fail "could not generate layout"
$TMUX select-layout -t deep "$value" >/dev/null 2>&1 &&
fail "excessive nesting was accepted ($open, $depth)"
$TMUX has-session -t deep ||
fail "server died on excessive nesting ($open, $depth)"
after=$($TMUX display-message -p -t deep \
'#{pane_id} #{pane_width} #{pane_height} #{window_layout}') || exit 1
[ "$before" = "$after" ] ||
fail "pane or layout changed after rejection ($open, $depth)"
done
done
exit 0

View File

@@ -337,9 +337,9 @@ $TMUX set -g @modal-prefix no
$TMUX set -g @modal-root no
$TMUX bind -n z set -g @modal-root yes
modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \
modal=$($TMUX new-pane -ODKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -OK failed"
fail "new-pane -ODK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
@@ -356,9 +356,151 @@ new_left=$(fmt "$modal" '#{pane_left}')
new_top=$(fmt "$modal" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "key-capturing modal pane did not move"
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
modal=$($TMUX new-pane -ODKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'trap "" INT; exec cat') ||
fail "new-pane -ODK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-c
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
# A dead modal does not close on Escape or C-c without -D.
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'sleep 1') ||
fail "new-pane -O failed"
check_ok set-option -p -t "$modal" remain-on-exit on
sleep 2
must_equal "$(fmt "$modal" '#{pane_dead}:#{pane_modal_flag}')" 1:1
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
check_ok kill-pane -t "$modal"
sleep 1
# failed-key closes successful panes and retains failed panes until a key.
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'sleep 1') ||
fail "new-pane -O failed"
check_ok set-option -p -t "$modal" remain-on-exit failed-key
sleep 2
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'sleep 1; exit 1') ||
fail "new-pane -O failed"
check_ok set-option -p -t "$modal" remain-on-exit failed-key
sleep 2
must_equal "$(fmt "$modal" '#{pane_dead}:#{pane_modal_flag}')" 1:1
must_equal "$($TMUX show-options -pv -t "$modal" remain-on-exit)" failed-key
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind P display-popup -E -t "$p0" -w 20 -h 5 -T popup-title 'cat'
$TMUX2 send-keys -t "$OUTER" C-b P
sleep 1
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "display-popup did not create a modal pane"
must_equal "$(fmt "$modal" '#{pane_title}')" popup-title
must_equal "$($TMUX show-options -pv -t "$modal" pane-border-status)" top
must_equal "$($TMUX show-options -pv -t "$modal" pane-border-format)" \
'#{pane_title}'
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
must_equal "$($TMUX show -gv @modal-prefix)" no
must_equal "$($TMUX show -gv @modal-root)" no
case "$($TMUX capture-pane -pt "$modal")" in
*xz*) ;;
*) fail "keys did not reach display-popup pane" ;;
esac
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
$TMUX2 send-keys -t "$OUTER" C-c
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
# Creating a popup pane must not fire the split-window hook.
check_ok set-hook -t modal after-split-window \
"set-option -g @popup-after-split yes"
check_ok set-option -g @popup-after-split no
check_ok display-popup -E -t "$p0" true
must_equal "$($TMUX show-option -gv @popup-after-split)" no
check_ok set-hook -u -t modal after-split-window
# A borderless popup must not create a zero-sized pane in a tiny window.
check_ok new-window -d -t modal: -n popup-small 'cat'
check_ok set-option -w -t modal:popup-small window-size manual
check_ok resize-window -t modal:popup-small -x 1 -y 1
small=$(fmt modal:popup-small '#{pane_id}')
check_ok bind Z display-popup -B -t "$small" 'cat'
$TMUX2 send-keys -t "$OUTER" C-b Z
sleep 1
must_equal "$(fmt modal:popup-small '#{window_panes}')" 1
must_equal "$(fmt modal:popup-small '#{window_modal_pane}')" ''
$TMUX bind D display-popup -t "$p0" -w 20 -h 5 'printf done'
$TMUX2 send-keys -t "$OUTER" C-b D
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "retained display-popup was not created"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
case "$($TMUX capture-pane -pt "$modal")" in
*'Pane is dead'*) fail "display-popup showed remain-on-exit message" ;;
esac
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind K display-popup -k -t "$p0" -w 20 -h 5 'printf done'
$TMUX2 send-keys -t "$OUTER" C-b K
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "display-popup -k was not created"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind F display-popup -EE -t "$p0" -w 20 -h 5 'exit 1'
$TMUX2 send-keys -t "$OUTER" C-b F
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "failed display-popup -EE did not remain"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
check_ok kill-pane -t "$modal"
sleep 1
check_ok display-popup -EE -t "$p0" true
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind G display-popup -EE -k -t "$p0" -w 20 -h 5 'exit 1'
$TMUX2 send-keys -t "$OUTER" C-b G
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "failed display-popup -EE -k did not remain"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
must_equal "$($TMUX show-options -pv -t "$modal" remain-on-exit)" failed-key
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
check_ok display-popup -EE -k -t "$p0" true
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
# A nonmodal floating pane may remain above zoom, and switching between it and
# the zoomed tiled pane must not unzoom the window.
check_ok new-window -d -t modal: -n float-over-zoom 'cat'

View File

@@ -0,0 +1,71 @@
#!/bin/sh
# Chooser menus are centred on the mouse using their displayed width. A
# hidden title must not move a borderless menu away from the mouse.
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"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
$TMUX new-session -d -s short -x 100 -y 30 'sleep 100' || exit 1
$TMUX set -g mouse on || exit 1
$TMUX set -g status off || exit 1
$TMUX2 new-session -d -x 100 -y 30 "$TMUX attach" || exit 1
sleep 1
for name in short aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
$TMUX rename-session "$name" || exit 1
for lines in none single padded; do
$TMUX set -g menu-border-lines "$lines" || exit 1
$TMUX choose-tree -s || exit 1
sleep 1
# Right-button press at window (60, 0), away from either edge.
seq=$(printf '\033[<2;61;1M')
$TMUX2 send-keys -l "$seq" || exit 1
sleep 1
row=2
column=54
if [ "$lines" = none ]; then
row=1
elif [ "$name" != short ]; then
column=31
fi
text=$($TMUX2 capture-pane -p | awk -v row="$row" \
-v column="$column" 'NR == row {
# Keep byte offsets equal to columns with non-Unicode awk.
gsub(/│/, "|")
print substr($0, column, 7)
}')
[ "$text" = 'Select ' ] || \
fail "$lines menu for $name: expected Select at ($column, $row), got '$text'"
# Close the menu, release the button, then leave the chooser.
$TMUX2 send-keys Escape || exit 1
seq=$(printf '\033[<2;61;1m')
$TMUX2 send-keys -l "$seq" || exit 1
$TMUX2 send-keys q || exit 1
sleep 1
done
done
exit 0

View File

@@ -86,6 +86,11 @@ check_value "-gv status-keys" "vi"
check_fail "unknown value: bogus" set -g status-keys bogus
check_value "-gv status-keys" "vi"
# pane-border-lines accepts rounded as a pane border style.
check_ok set -gw pane-border-lines rounded
check_value "-gwv pane-border-lines" "rounded"
check_ok set -gw pane-border-lines single
# --- flag options ---------------------------------------------------------
#
# focus-events is an on/off flag. Setting with no value toggles it; explicit

View File

@@ -1,168 +0,0 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -L${TEST_SOCKET:-testO$$} -f/dev/null"
OUT=/tmp/tmux-output-commands-$$
EXPECTED=$(printf 'one\ntwo')
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -f "$OUT"
}
trap cleanup EXIT HUP INT TERM
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x80 -y20 "sh -c 'printf \"\\033]133;A\\007p\\$ \\033]133;B\\007echo\\n\\033]133;C\\007one\\ntwo\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007broken\\n\\033]133;C\\007unfinished\"; exec sleep 100'" || exit 1
sleep 1
$TMUX copy-mode -U || exit 1
$TMUX send-keys -X copy-output || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X pipe-output "wc -c >$OUT" || exit 1
sleep 1
[ "$(cat "$OUT")" = 10 ] || exit 1
$TMUX send-keys -X copy-pipe-output "wc -c >$OUT" output || exit 1
sleep 1
[ "$(cat "$OUT")" = 10 ] || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X select-output || exit 1
$TMUX send-keys -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode -U || exit 1
$TMUX send-keys -X search-backward separator || exit 1
$TMUX send-keys -X copy-output || exit 1
copied=$($TMUX show-buffer)
[ "$copied" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode -U || exit 1
$TMUX send-keys -X search-backward one || exit 1
$TMUX send-keys -X copy-output || exit 1
copied=$($TMUX show-buffer)
[ "$copied" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X pipe-output "wc -l >$OUT" || exit 1
sleep 1
[ "$(cat "$OUT")" = 2 ] || exit 1
$TMUX send-keys -X copy-pipe-output "wc -l >$OUT" output || exit 1
sleep 1
[ "$(cat "$OUT")" = 2 ] || exit 1
copied=$($TMUX show-buffer)
[ "$copied" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX copy-mode -c || exit 1
$TMUX send-keys -X search-backward echo || exit 1
$TMUX send-keys -X select-output || exit 1
$TMUX send-keys -X copy-selection || exit 1
selected=$($TMUX show-buffer)
[ "$selected" = "$EXPECTED" ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX set-buffer -b keep unchanged || exit 1
$TMUX copy-mode -U || exit 1
$TMUX send-keys -X search-backward unfinished || exit 1
$TMUX send-keys -X copy-output || exit 1
[ "$($TMUX show-buffer)" = unfinished ] || exit 1
$TMUX send-keys -X cancel || exit 1
$TMUX new-window -d -n plain "printf 'alpha\\nbeta\\n'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :plain || exit 1
$TMUX send-keys -t :plain.0 -X copy-output -a || exit 1
all=$($TMUX show-buffer)
case "$all" in
*alpha*beta*) ;;
*) exit 1 ;;
esac
$TMUX send-keys -t :plain.0 -X cancel || exit 1
$TMUX set-buffer -b keep unchanged || exit 1
$TMUX copy-mode -t :plain || exit 1
$TMUX send-keys -t :plain.0 -X copy-output || exit 1
[ "$($TMUX show-buffer -b keep)" = unchanged ] || exit 1
$TMUX send-keys -t :plain.0 -X cancel || exit 1
$TMUX copy-mode -t :plain || exit 1
$TMUX send-keys -t :plain.0 -X select-output || exit 1
$TMUX send-keys -t :plain.0 -X select-output -a || exit 1
$TMUX send-keys -t :plain.0 -X copy-selection || exit 1
all=$($TMUX show-buffer)
case "$all" in
*alpha*beta*) ;;
*) exit 1 ;;
esac
$TMUX send-keys -t :plain.0 -X cancel || exit 1
$TMUX new-window -d -n empty "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\033]133;C\\007one\\n\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007true\\033]133;C\\033]133;D;0\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :empty || exit 1
$TMUX send-keys -t :empty.0 -X copy-output || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :empty.0 -X cancel || exit 1
$TMUX new-window -d -n prompt "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\033]133;C\\007one\\n\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -U -t :prompt || exit 1
$TMUX send-keys -t :prompt.0 -X copy-output || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :prompt.0 -X cancel || exit 1
$TMUX copy-mode -c -t :prompt || exit 1
$TMUX send-keys -t :prompt.0 -X expand-output || exit 1
$TMUX send-keys -t :prompt.0 -X -N 10 cursor-down || exit 1
$TMUX send-keys -t :prompt.0 -X select-output || exit 1
[ "$($TMUX display-message -p -t :prompt.0 '#{selection_present}')" = 1 ] || exit 1
$TMUX send-keys -t :prompt.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :prompt.0 -X cancel || exit 1
$TMUX new-window -d -n below "printf '\\033]133;A\\007p\\$ \\033]133;B\\007old\\n\\033]133;C\\007old1\\nold2\\nold3\\nold4\\nold5\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007ps\\n\\033]133;C\\007PID TTY\\n1 pts/0\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -c -t :below || exit 1
$TMUX send-keys -t :below.0 -X search-backward ps || exit 1
$TMUX send-keys -t :below.0 -X expand-output || exit 1
$TMUX send-keys -t :below.0 -X -N 100 cursor-down || exit 1
$TMUX send-keys -t :below.0 C-o || exit 1
[ "$($TMUX display-message -p -t :below.0 '#{selection_present}')" = 1 ] || exit 1
$TMUX send-keys -t :below.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = "$(printf 'PID TTY\\n1 pts/0')" ] || exit 1
$TMUX send-keys -t :below.0 -X cancel || exit 1
$TMUX new-window -d -n clear "printf 'old1\\nold2\\nold3\\nold4\\nold5\\nold6\\n\\033]133;A\\007p\\$ \\033]133;B\\007echo 1; clear; ps\\n\\033]133;C\\0071\\n\\033[H\\033[2JPID TTY\\n1 pts/0\\n\\033]133;D;0\\007separator\\n\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -t :clear || exit 1
$TMUX send-keys -t :clear.0 C-o || exit 1
$TMUX send-keys -t :clear.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = "$(printf 'PID TTY\\n1 pts/0')" ] || exit 1
$TMUX send-keys -t :clear.0 -X cancel || exit 1
$TMUX new-window -d -n same "printf '\\033]133;A\\007p\\$ \\033]133;B\\007echo\\n\\033]133;C\\007one\\033]133;D;0\\007\\033]133;A\\007p\\$ \\033]133;B\\007'; exec sleep 100" || exit 1
sleep 1
$TMUX copy-mode -U -t :same || exit 1
$TMUX send-keys -t :same.0 -X search-backward one || exit 1
$TMUX send-keys -t :same.0 -X select-output || exit 1
$TMUX send-keys -t :same.0 -X copy-selection || exit 1
[ "$($TMUX show-buffer)" = one ] || exit 1
$TMUX send-keys -t :same.0 -X cancel || exit 1
$TMUX copy-mode -t :plain || exit 1
$TMUX send-keys -t :plain.0 -X copy-output -a || exit 1
all=$($TMUX show-buffer)
case "$all" in
*alpha*beta*) ;;
*) exit 1 ;;
esac
$TMUX send-keys -t :plain.0 -X cancel || exit 1
exit 0

View File

@@ -104,6 +104,25 @@ assert_alive()
fi
}
# Compare layout geometry and pane ordering, ignoring the active pane and
# last-pane history in the JSON layout. Selection is checked separately.
pane_layout()
{
$TMUX display-message -p -t "$1" '#{window_layout}' |
sed 's/,"a":true//g; s/,"l":[0-9][0-9]*//g'
}
check_layout()
{
out=$(pane_layout "$1")
if [ "$out" != "$2" ]; then
echo "Layout for '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
# ---------------------------------------------------------------------------
# split-window geometry.
@@ -309,12 +328,12 @@ check_ok kill-pane -t "$p6"
# Zoom and unzoom preserve the exact tiled layout. Selecting another pane
# without -Z unzooms, while -Z transfers zoom to the selected pane.
layout=$($TMUX display-message -p -t P:0 '#{window_layout}')
layout=$(pane_layout P:0)
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_fmt P:0 '#{window_layout}' "$layout"
check_layout P:0 "$layout"
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
@@ -322,7 +341,7 @@ check_ok select-pane -Z -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_fmt P:0 '#{window_layout}' "$layout"
check_layout P:0 "$layout"
# Directional selection temporarily restores the full layout to find its
# neighbour, then follows the same unzoom or -Z transfer rules.
@@ -340,7 +359,7 @@ check_ok select-pane -D -Z -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_fmt P:0 '#{window_layout}' "$layout"
check_layout P:0 "$layout"
# The last-pane path has separate zoom handling, both with and without -Z.
check_ok select-pane -t "$p0"
@@ -357,7 +376,7 @@ check_ok select-pane -l -Z -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p0"
check_fmt P:0 '#{window_layout}' "$layout"
check_layout P:0 "$layout"
# Killing either a hidden ordinary pane or the zoom target unzooms.
check_ok new-window -d -t P:12 -n zoom-kill 'cat'

View File

@@ -0,0 +1,126 @@
#!/bin/sh
# Damage at a popup edge must redraw complete grid characters. Drawing only a
# wide character's base or padding cell leaves a two-cell hole behind.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpopup-wide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
POPUP_PID=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n '3,5p' "$BASE" >"$DIR/want"
sed -n '3,5p' "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the old popup edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
$INNER display-popup -t "$CLIENT" -x 5 -y 5 -w 10 -h 3 -E \
"sh -c 'printf POPUP; exec sleep 100'" &
POPUP_PID=$!
wait_outer_has POPUP
# The first motion starts the drag; the second moves the popup away from its
# old rectangle. Its odd x coordinate bisects the underlying double-width
# cells at both edges.
mouse 0 10 3 M
mouse 32 11 3 M
mouse 32 28 7 M
mouse 0 28 7 m
wait_old_rows_restored
exit 0

View File

@@ -217,7 +217,36 @@ $OUT send-keys M-r || exit 1
settle
status_line | grep -q '>' || fail "status-line prompt not drawn on the status line"
# --- 11. emacs cursor-marker edit, accept recovers the exact buffer. ---
# --- 11. Invalid UTF-8 input must not desynchronize the prompt buffer. ---
$OUT send-keys -H e6 85 5f || exit 1 # invalid UTF-8, then "_"
settle
$IN display-message -p '#{version}' >/dev/null 2>&1 || \
fail "invalid UTF-8 append killed inner tmux"
status_line | grep -qF "> _" || \
fail "invalid UTF-8 append did not keep prompt usable (got '$(status_line)')"
$OUT send-keys Enter || exit 1
settle
[ "$($IN show -gv @r)" = "_" ] || \
fail "invalid UTF-8 append recovered '$($IN show -gv @r)', wanted '_'"
$IN set -g @r "SENTINEL" || exit 1
$OUT send-keys M-r || exit 1
settle
$OUT send-keys -H e6 85 04 || exit 1 # invalid UTF-8, then C-d
settle
$IN display-message -p '#{version}' >/dev/null 2>&1 || \
fail "invalid UTF-8 delete killed inner tmux"
status_line | grep -q '>' || \
fail "invalid UTF-8 delete closed the prompt (got '$(status_line)')"
$OUT send-keys Escape || exit 1
settle
[ "$($IN show -gv @r)" = "SENTINEL" ] || \
fail "invalid UTF-8 delete accepted the prompt"
# --- 12. emacs cursor-marker edit, accept recovers the exact buffer. ---
$IN set -g @r "" || exit 1
$OUT send-keys M-r || exit 1
settle
$OUT send-keys -l "abc" || exit 1
$OUT send-keys Home || exit 1
$OUT send-keys -l "X" || exit 1
@@ -229,7 +258,7 @@ settle
[ "$($IN show -gv @r)" = "Xabc" ] || \
fail "status-line accept recovered '$($IN show -gv @r)', wanted 'Xabc'"
# --- 12. Unicode on the status line: insert, move, delete wide char. ---
# --- 13. Unicode on the status line: insert, move, delete wide char. ---
$IN set -g @r "" || exit 1
$OUT send-keys M-r || exit 1
settle
@@ -249,7 +278,7 @@ settle
[ "$($IN show -gv @r)" = "Za" ] || \
fail "status-line wide edit recovered '$($IN show -gv @r)', wanted 'Za'"
# --- 13. Overflow: more text than fits stays within the line and is kept. ---
# --- 14. Overflow: more text than fits stays within the line and is kept. ---
big="0123456789012345678901234567890123456789012345678901234567890123456789ABCDEFGHIJ"
$IN set -g @r "" || exit 1
$OUT send-keys M-r || exit 1
@@ -264,7 +293,7 @@ settle
# The whole buffer was kept despite only part being visible.
[ "$($IN show -gv @r)" = "$big" ] || fail "overflowing prompt lost buffer content"
# --- 14. Escape closes the status-line prompt cleanly. ---
# --- 15. Escape closes the status-line prompt cleanly. ---
$IN set -g @r "SENTINEL" || exit 1
$OUT send-keys M-r || exit 1
settle

112
regress/redraw-damage-only.sh Executable file
View File

@@ -0,0 +1,112 @@
#!/bin/sh
# Check a redraw callback which has no accompanying client redraw flags. A
# wrapped row crossing a panned viewport cannot use the direct tty path.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldamage-only-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
TRIGGER=$DIR/trigger
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 12) {
print "\e[$row;1H", 'o' x 79;
}
print "\e[1;1H";
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
my $second = ('B' x 24) . 'DAMAGE-ONLY' . ('B' x 45);
print "\e[5;1H", ('A' x 80), $second;
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 12 \
"TRIGGER='$TRIGGER' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
wait_outer_has oooooooooo
: >"$TRIGGER"
wait_inner_has DAMAGE-ONLY
wait_outer_has DAMAGE-ONLY
exit 0

186
regress/redraw-multiclient.sh Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/sh
# Redraw a moved floating pane on both attached clients viewing the same
# window. Window redraw work must not be consumed by only one client.
#
# Uses ASCII pane borders (rather than the default UTF-8 box-drawing) because
# this test nests a real tmux client inside another tmux's pane to get a
# genuine terminal to capture from; that nested-tmux relay has been observed
# to mis-render a cell that previously held a multi-byte UTF-8 border
# character being overwritten later by plain content, on the outer instance's
# own interpretation, independent of anything the inner tmux sends. That is a
# nested-test-harness artifact, not a real tmux bug - confirmed by replaying
# the exact same drag sequence against a real terminal (xterm), where it
# never reproduces. ASCII borders avoid the artifact entirely.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-multi-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_outer_has()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer pane $target did not show $marker"
}
wait_float_left()
{
comparison=$1
limit=$2
i=0
while [ "$i" -lt 50 ]; do
left=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
if [ "$comparison" = gt ] && [ "$left" -gt "$limit" ]; then
return 0
fi
if [ "$comparison" = lt ] && [ "$left" -lt "$limit" ]; then
return 0
fi
sleep 0.1
i=$((i + 1))
done
fail "floating pane did not move"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$2" "$3" "$4" "$5")
$OUTER send-keys -t "$1" -l "$sequence" || exit 1
sleep 0.1
}
drag_float()
{
target=$1
startcol=$2
startrow=$3
endcol=$4
mouse "$target" 0 "$startcol" "$startrow" M
mouse "$target" 32 "$endcol" "$startrow" M
mouse "$target" 0 "$endcol" "$startrow" m
}
assert_scene()
{
target=$1
base=$2
firstcol=$3
lastcol=$4
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
# With ASCII (simple) borders every corner and junction is the same
# '+', so one rectangular floating pane always draws exactly 4 of
# them; more means a stale frame was left behind somewhere.
corners=$(grep -o '+' "$CAPTURE" | wc -l)
[ "$corners" -eq 4 ] ||
fail "outer pane $target had $corners floating frames"
sed -n '6,11p' "$base" | cut -c"$firstcol-$lastcol" >"$DIR/want"
sed -n '6,11p' "$CAPTURE" | cut -c"$firstcol-$lastcol" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" ||
fail "outer pane $target did not restore the old floating area"
}
C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; exec sleep 100'"
$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g default-command 'sleep 100' || exit 1
$INNER set-option -g pane-border-lines simple || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER split-window -h -t outer:0.0 'sleep 100' || exit 1
PANES=$($OUTER list-panes -t outer:0 -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
RIGHT=$(echo "$PANES" | sort -k2 -n | tail -1 | cut -d' ' -f1)
[ -n "$LEFT" ] && [ -n "$RIGHT" ] || fail "could not find outer panes"
$OUTER respawn-pane -k -t "$LEFT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER respawn-pane -k -t "$RIGHT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_clients
wait_outer_has "$LEFT" BG-ROW-19
wait_outer_has "$RIGHT" BG-ROW-19
$OUTER capture-pane -p -t "$LEFT" >"$DIR/base-left" || exit 1
$OUTER capture-pane -p -t "$RIGHT" >"$DIR/base-right" || exit 1
FLOAT=$($INNER new-pane -dPF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "could not create floating pane"
wait_outer_has "$LEFT" '+'
wait_outer_has "$RIGHT" '+'
FTOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
# Move right through one client and require both clients to restore the old
# left-hand footprint.
drag_float "$LEFT" "$GRABCOL" "$FTOP" $((GRABCOL + 30))
wait_float_left gt 30
assert_scene "$LEFT" "$DIR/base-left" 1 20
assert_scene "$RIGHT" "$DIR/base-right" 1 20
# Move back through the other client and check the old right-hand footprint.
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
drag_float "$RIGHT" "$GRABCOL" "$FTOP" $((GRABCOL - 30))
wait_float_left lt 10
assert_scene "$LEFT" "$DIR/base-left" 35 60
assert_scene "$RIGHT" "$DIR/base-right" 35 60
exit 0

208
regress/redraw-screen-write.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/bin/sh
# Check that full and region screen-write fallbacks update an attached client,
# not only tmux's internal pane grid.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
INNER=
OUTER=
N=0
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_outer_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client still showed $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
wait_inner_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane still contained $marker"
}
setup()
{
mode=$1
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
N=$((N + 1))
INNER="$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-write-outer-$$-$N -f/dev/null"
$INNER new-session -d -s inner -x 40 -y 12 \
"MODE=$mode READY='$DIR/ready-$N' TRIGGER='$DIR/trigger-$N' perl '$EMITTER'" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null attach-session -t inner" ||
exit 1
}
trigger()
{
: >"$DIR/trigger-$N-${1:-1}"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $mode = $ENV{MODE};
my $ready = $ENV{READY};
my $trigger = $ENV{TRIGGER};
sub fill_screen {
my ($prefix) = @_;
print "\e[2J\e[H";
for my $row (0 .. 11) {
printf "\e[%d;1H%s-ROW-%02d", $row + 1, $prefix, $row;
}
}
if ($mode eq 'ris') {
fill_screen('RIS');
} elsif ($mode eq 'alternate') {
fill_screen('BASE');
} elsif ($mode eq 'scroll') {
fill_screen('SCROLL');
} else {
die "unknown mode $mode\n";
}
open my $fh, '>', $ready or die "$ready: $!\n";
close $fh;
while (!-e "$trigger-1") {
select undef, undef, undef, 0.01;
}
if ($mode eq 'ris') {
print "\ec";
} elsif ($mode eq 'alternate') {
print "\e[?1049h";
fill_screen('ALT');
while (!-e "$trigger-2") {
select undef, undef, undef, 0.01;
}
print "\e[?1049l";
} else {
print "\e[12;1H\r\nSCROLL-NEW";
}
sleep 100;
PERL
# RIS clears the complete screen. The source pane and attached client must both
# lose every old row.
setup ris
wait_outer_has RIS-ROW-11
trigger
wait_inner_lacks RIS-ROW
wait_outer_lacks RIS-ROW
# Leaving the alternate screen restores every row of the base screen.
setup alternate
wait_outer_has BASE-ROW-11
trigger
wait_inner_has ALT-ROW-11
wait_outer_has ALT-ROW-11
wait_outer_lacks BASE-ROW
trigger 2
wait_inner_has BASE-ROW-11
wait_outer_has BASE-ROW-11
wait_outer_lacks ALT-ROW
# Scrolling a pane which is narrower than the terminal redraws its complete
# region. Check the physical client row by row after the source grid shifts.
setup scroll
$INNER split-window -h -t inner:0 'sleep 100' || exit 1
wait_outer_has SCROLL-ROW-11
trigger
wait_inner_has SCROLL-NEW
wait_outer_has SCROLL-NEW
row=1
while [ "$row" -le 11 ]; do
expected=$(printf 'SCROLL-ROW-%02d' "$row")
actual=$(sed -n "${row}p" "$CAPTURE")
case "$actual" in
"$expected"*) ;;
*) fail "outer row $row was not redrawn as $expected" ;;
esac
row=$((row + 1))
done
actual=$(sed -n '12p' "$CAPTURE")
case "$actual" in
SCROLL-NEW*) ;;
*) fail "outer bottom row was not redrawn as SCROLL-NEW" ;;
esac
exit 0

View File

@@ -15,7 +15,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)

55
regress/run-shell-cwd.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/bin/sh
# run-shell -c should expand formats using the target pane.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMP=$(mktemp -d)
trap '$TMUX kill-server 2>/dev/null; rm -rf "$TMP"' 0 1 15
TMP=$(cd "$TMP" && pwd -P)
mkdir "$TMP/first" "$TMP/second dir" || exit 1
check_directory()
{
actual=$(cat "$TMP/out")
if [ "$actual" != "$1" ]; then
echo "Expected directory '$1', got '$actual'"
exit 1
fi
}
# The start path is available immediately, before the child is scheduled.
# Use it to test the format context in the same command queue as creation.
$TMUX new-session -d -s test -c "$TMP/first" 'sleep 60' \; \
run-shell -c '#{pane_start_path}' "pwd >'$TMP/out'" || exit 1
check_directory "$TMP/first"
# An explicit target must supply the format context, including with a delay.
pane=$($TMUX new-window -d -P -F '#{pane_id}' -t test \
-c "$TMP/second dir" 'sleep 60') || exit 1
# The current path depends on the operating system finding the child process.
i=0
while [ "$($TMUX display-message -p -t "$pane" '#{pane_current_path}')" != \
"$TMP/second dir" ]; do
if [ "$i" -ge 100 ]; then
echo "Timed out waiting for pane current directory"
exit 1
fi
sleep 0.05
i=$((i + 1))
done
$TMUX run-shell -t "$pane" -d 0.1 -c '#{pane_current_path}' \
"pwd >'$TMP/out'" || exit 1
check_directory "$TMP/second dir"
# Literal directories and the default client directory still work.
$TMUX run-shell -c "$TMP/second dir" "pwd >'$TMP/out'" || exit 1
check_directory "$TMP/second dir"
(cd "$TMP/first" && $TMUX run-shell "pwd >'$TMP/out'") || exit 1
check_directory "$TMP/first"
exit 0

View File

@@ -107,6 +107,12 @@ $TMUX2 new-pane -x28 -y8 -X4 -Y1 -B double \
"sh -c 'printf FLOAT; exec sleep 100'" || exit 1
compare floating-border-double
# Larger floating pane with rounded border lines.
new_scene 40 12
$TMUX2 new-pane -x28 -y8 -X4 -Y1 -B rounded \
"sh -c 'printf FLOAT; exec sleep 100'" || exit 1
compare floating-border-rounded
# Floating pane with no border lines: redraw_mark_pane_borders returns early so
# the float has no border at all, only its (clipped) content over the base pane.
new_scene 40 12

View File

@@ -106,9 +106,28 @@ compare menu-over-split
# Menu with no border lines.
setup 40 14
menu -b none -x6 -y8
$TMUX2 display-menu -T "This title must not reserve width" -C 1 \
-b none -x6 -y8 \
"Alpha item" a "" \
"Beta item" b "" \
"" "" "" \
"Gamma item" g "" || exit 1
sleep 1
compare menu-noborder
# Menu border style follows the explicitly targeted window, not the current
# window when a different pane is selected.
setup 40 14
$TMUX2 set menu-border-lines none || exit 1
$TMUX2 neww || exit 1
$TMUX2 display-menu -t%0 -T "Menu" -C 1 \
"Alpha item" a "" \
"Beta item" b "" \
"" "" "" \
"Gamma item" g "" || exit 1
$TMUX2 selectw -t%0 || exit 1
compare menu-target-window-noborder
# Menu with double border lines.
setup 40 14
menu -b double -x6 -y8

View File

@@ -93,6 +93,15 @@ $TMUX2 splitw -v "$C" || exit 1
$TMUX2 select-layout tiled || exit 1
compare outside-both-2x2
# Top pane status supplies internal horizontal borders, but there is no pane
# below the window to supply its bottom edge.
new_scene 28 9
$TMUX2 setw pane-border-status top || exit 1
$TMUX2 setw pane-border-format "" || exit 1
compare outside-both-status-top-single
$TMUX2 splitw -v "$C" || exit 1
compare outside-both-status-top-split
# Window BIGGER than the client: only part of the window is viewed and the view
# can be panned (refresh-client). This exercises a non-zero scene offset.
# A 2x2 grid in a 60x20 window viewed through the 40x14 client.

View File

@@ -1,101 +0,0 @@
#!/bin/sh
# Exercise drawing of popups (display-popup) over the window scene. A popup is an
# overlay drawn on top of the redraw scene (the overlay_draw path in
# screen-redraw.c), so this guards against regressions in how popups appear.
#
# A popup is modal and stays open until its command exits, so each scene fully
# re-creates the servers and re-attaches; the popup is opened in the background
# (display-popup blocks the client that runs it) and the outer pane is captured
# while it is open.
#
# 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=
TMUX2=
SETUP=0
RESULTS=screen-redraw-results
TMP=$(mktemp)
cleanup() {
rm -f "$TMP"
[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null
[ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null
}
trap cleanup 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
echo "generated $1"
else
cmp -s $TMP "$RESULTS/$1.result" || \
fail "scene $1 differs from $RESULTS/$1.result"
fi
}
C="sh -c 'i=0; while [ \$i -lt 13 ]; do printf \"POP%02d abcdefghij\n\" \$i; i=\$((i + 1)); done; exec sleep 100'"
# setup: fresh inner window attached inside a fresh outer pane, 40x14.
setup() {
[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null
[ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null
SETUP=$((SETUP + 1))
TMUX="$TEST_TMUX -LtestA$$-$SETUP -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$-$SETUP -f/dev/null"
$TMUX2 new -d -x40 -y14 "$C" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g window-size manual || exit 1
$TMUX2 resizew -x40 -y14 || exit 1
$TMUX new -d -x40 -y14 || 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
}
# popup <args>: open a popup running a fixed command, in the background (it stays
# open because the command sleeps; the servers are killed at the next setup).
popup() {
$TMUX2 display-popup "$@" -E "sh -c 'printf POPUP; exec sleep 100'" &
sleep 1
}
# Basic popup over a single pane.
setup
popup -w20 -h6 -x6 -y3
compare popup-basic
# Popup over a split: drawn on top of the pane border.
setup
$TMUX2 splitw -h "$C" || exit 1
popup -w24 -h8 -x8 -y3
compare popup-over-split
# Popup with no border lines (-B).
setup
popup -B -w20 -h6 -x6 -y3
compare popup-noborder
# Popup with double border lines.
setup
popup -b double -w20 -h6 -x6 -y3
compare popup-double
exit 0

View File

@@ -0,0 +1,12 @@
base
╭──────────────────────────╮
│FLOAT │
│ │
│ │
│ │
│ │
│ │
╰──────────────────────────╯

View File

@@ -1,11 +1,11 @@
MENU00 abcdefghij
MENU01 abcdefghij
MENU02 Menu 
MENU03  Alpha item (a)  
MENU04  Beta item (b)  
MENU05 
MENU06  Gamma item (g)  
MENU07 
MENU02 abcdefghij
MENU03 abcdefghij
MENU04 Alpha item (a) 
MENU05 Beta item (b) 
MENU06 
MENU07 Gamma item (g) 
MENU08 abcdefghij
MENU09 abcdefghij
MENU10 abcdefghij

View File

@@ -0,0 +1,13 @@
MENU00 abcdefghij
MENU01 abcdefghij
MENU02 abcdefghij
MENU03 abcdefghij
MENU04 abcd Alpha item (a) 
MENU05 abcd Beta item (b) 
MENU06 abcd 
MENU07 abcd Gamma item (g) 
MENU08 abcdefghij
MENU09 abcdefghij
MENU10 abcdefghij
MENU11 abcdefghij
MENU12 abcdefghij

View File

@@ -0,0 +1,14 @@
────────────────────────────┐···········
OUT01 abcdefghij │···········
OUT02 abcdefghij │···········
OUT03 abcdefghij │···········
OUT04 abcdefghij │···········
OUT05 abcdefghij │···········
OUT06 abcdefghij │···········
OUT07 abcdefghij │···········
│···········
────────────────────────────┘···········
········································
········································
········································
········································

View File

@@ -0,0 +1,14 @@
────────────────────────────┐···········
OUT06 abcdefghij │···········
OUT07 abcdefghij │···········
│···········
────────────────────────────┤···········
PAN05 abcdefghij │···········
PAN06 abcdefghij │···········
PAN07 abcdefghij │···········
│···········
────────────────────────────┘···········
········································
········································
········································
········································

View File

@@ -1,14 +0,0 @@
POP00 ┌──────────────────┐
POP01 │POPUP │
POP02 │ │
POP03 │ │
POP04 │ │
POP05 └──────────────────┘
POP06 abcdefghij
POP07 abcdefghij
POP08 abcdefghij
POP09 abcdefghij
POP10 abcdefghij
POP11 abcdefghij
POP12 abcdefghij

View File

@@ -1,14 +0,0 @@
POP00 ╔══════════════════╗
POP01 ║POPUP ║
POP02 ║ ║
POP03 ║ ║
POP04 ║ ║
POP05 ╚══════════════════╝
POP06 abcdefghij
POP07 abcdefghij
POP08 abcdefghij
POP09 abcdefghij
POP10 abcdefghij
POP11 abcdefghij
POP12 abcdefghij

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