Compare commits

..

72 Commits

Author SHA1 Message Date
Nicholas Marriott
b98ce20a10 Minor bits missed (-T show title and popup_* format variables). 2026-09-11 09:41:33 +01:00
Nicholas Marriott
5ea118e3e0 Merge branch 'master' into no_more_overlays
# Conflicts:
#	regress/modal-pane.sh
#	server-client.c
2026-09-11 09:18:53 +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
Nicholas Marriott
b15c62ac07 Add some more key bits to better emulate display-popup. 2026-09-10 10:09:15 +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
7049aa826a Do not allow 0x0 panes and remove a dead member. 2026-09-09 14:39:14 +01:00
Nicholas Marriott
19320c6969 Merge branch 'master' into no_more_overlays 2026-09-09 14:00:20 +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
tmux update bot
578e07fcbc Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not attempt to write to control clients which have been destroyed, reported by Artur Penttinen in GitHub issue 5570.
2026-09-04 06:00:37 +00:00
nicm
93169e2e62 Do not attempt to write to control clients which have been destroyed,
reported by Artur Penttinen in GitHub issue 5570.
2026-09-04 06:00:36 +00:00
tmux update bot
db24c029be Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Extend neww -S to work with -t as well as -t, GitHub issue 5571 from Mazunki Hoksaas.
2026-09-03 23:28:11 +00:00
nicm
a6f9a206b7 Extend neww -S to work with -t as well as -t, GitHub issue 5571 from
Mazunki Hoksaas.
2026-09-03 23:28:09 +00:00
Nicholas Marriott
ce253864f8 Test for late run-shell on a client that has exited. 2026-09-03 22:39:09 +01:00
tmux update bot
79f1f43830 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Wait for pane exit status in all cases not just remain-on-exit since events need it.
2026-09-03 21:24:20 +00:00
nicm
7b666e62be Wait for pane exit status in all cases not just remain-on-exit since
events need it.
2026-09-03 21:24:19 +00:00
Nicholas Marriott
5d5f66239a Test for -S from Mazunki Hoksaas. 2026-09-03 22:04:34 +01:00
Nicholas Marriott
4b1586fa81 Test for exit status. 2026-09-03 20:12:44 +01:00
Nicholas Marriott
9fa390aee6 Fix error in test. 2026-09-02 12:32:29 +01:00
Nicholas Marriott
ad632726bd Some more regress tests. 2026-09-02 11:16:47 +01:00
Nicholas Marriott
1dd1717462 Fix tests for macOS. 2026-09-02 08:43:21 +01:00
tmux update bot
73db721dab Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Create screen before zoom may need to use it.
2026-09-02 06:15:13 +00:00
nicm
6bb6c286a9 Create screen before zoom may need to use it. 2026-09-02 06:15:11 +00:00
tmux update bot
ca34d190b5 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not allow cursor on/off to escape synchronized updates.
2026-09-01 23:20:22 +00:00
nicm
57a13664cc Do not allow cursor on/off to escape synchronized updates. 2026-09-01 23:20:20 +00:00
Nicholas Marriott
59dc0e75c4 Add a couple more tests. 2026-09-01 22:07:08 +01:00
Nicholas Marriott
2d6aed1d92 Add some more tests for recent stuff. 2026-09-01 21:55:50 +01:00
Nicholas Marriott
3dd575ce55 Update CHANGES. 2026-09-01 21:48:30 +01:00
Nicholas Marriott
60e871658f Remove +x. 2026-09-01 20:52:44 +01:00
Nicholas Marriott
c49dfdf496 Regress test for cursor updates. 2026-09-01 20:51:20 +01:00
Nicholas Marriott
78c0dc5be0 Merge branch 'master' into no_more_overlays
# Conflicts:
#	cmd-display-menu.c
#	popup.c
#	screen-write.c
2026-09-01 20:28:59 +01:00
Nicholas Marriott
81fa44f263 Merge branch 'master' into no_more_overlays
# Conflicts:
#	regress/modal-pane.sh
2026-08-21 15:58:07 +01:00
Nicholas Marriott
ba1bfeeb9c Merge branch 'master' into no_more_overlays 2026-08-19 14:49:33 +01:00
Nicholas Marriott
5047e4eb1a Add a way to make a modal pane capture all key presses and use for
display-popup compatibility.
2026-08-19 09:34:39 +01:00
Nicholas Marriott
8a1f4620b0 Merge branch 'master' into no_more_overlays 2026-08-19 09:00:04 +01:00
Nicholas Marriott
7d32e7f379 Fix bad merge. 2026-08-07 16:18:30 +01:00
Nicholas Marriott
6671efcaf7 Merge branch 'master' into no_more_overlays 2026-08-07 16:10:51 +01:00
Nicholas Marriott
858a9d7376 Merge branch 'master' into no_more_overlays 2026-07-21 08:35:07 +01:00
Your Name
980877fb5c Merge branch 'master' into no_more_overlays 2026-07-16 21:26:02 +01:00
Nicholas Marriott
c7a6a735bc Another popup leftover. 2026-07-16 13:10:51 +01:00
Nicholas Marriott
381eaaa3a3 No more popups and overlays. 2026-07-16 13:04:29 +01:00
108 changed files with 6551 additions and 3859 deletions

64
CHANGES
View File

@@ -9,7 +9,12 @@ CHANGES FROM 3.7c TO 3.8
- modal panes may be created with new-pane -O; a window can have one modal
pane and it prevents interaction with the other panes while it is active.
A modal pane is now used for the editor in buffer mode;
-C closes the pane when the mouse is clicked outside it and -K sends all
keys, including the prefix key, to the pane. A modal pane is now used for
the editor in buffer mode;
- new-pane -A creates a floating pane which remains above a zoomed pane;
modal panes do this by default;
- move-pane and resize-pane can now move and resize floating panes, including
mouse dragging;
@@ -24,10 +29,15 @@ CHANGES FROM 3.7c TO 3.8
-k for choose commands kills the pane when the mode exits, which is useful
for modes in floating panes;
- 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;
- command-prompt -P opens a prompt inside a pane rather than on the status
line;
- floating panes are unzoomed before creation to avoid a crash.
- 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.
* 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:
@@ -74,7 +84,7 @@ CHANGES FROM 3.7c TO 3.8
- new hooks to cover areas including client create and destroy, pane
activity, pane mode and prompt changes, pane movement and resizing, session
group changes and window creation, close and zoom;
group changes and window creation, close, swap and zoom;
- OSC 133 escape sequences now trigger events: pane-command-started,
pane-command-finished and pane-shell-prompt.
@@ -87,10 +97,23 @@ CHANGES FROM 3.7c TO 3.8
while scrolling or while hovered and disappears after pane-scrollbars-timeout
(Michael Grant).
* Extend the fill-character option so both inside and outside the window can be
changed.
* Add a clear-on-attach server option. When disabled, tmux does not enter the
alternate screen on attach and instead scrolls the existing terminal content
into the scrollback buffer so it remains accessible (issue 5508).
* Change set-option and set-hooks to use formats and add a -F flag to each.
* Copy mode can now automatically refresh as pane content changes with the
refresh-on, refresh-off and refresh-toggle commands. Automatic refresh is off
by default; refresh-now refreshes once and is bound to r. These replace
refresh-from-pane (issue 5165).
* Extend the fill-character option so both inside and outside the window can be
changed, with a new default for unused areas inside the window.
* Change show-options and show-hooks to use formats and add a -F flag to each.
* Allow individual terminal features to be disabled by adding @ to their names
in terminal-features, and add a utf8 feature for terminals which support
UTF-8 output.
* Add a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk).
@@ -105,6 +128,12 @@ 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.
* Dragging over an existing copy mode selection now adjusts it (Michael Grant).
@@ -118,6 +147,13 @@ CHANGES FROM 3.7c TO 3.8
* Copy mode no longer exits at the bottom while a selection is in progress
(issue 5349).
* Add copy-mode-current-line-style to set the style of the line containing the
cursor in copy mode (issue 5391).
* Copy mode word commands now recognise all Unicode whitespace characters; a
space in word-separators matches any Unicode whitespace character (issue
5562).
* Add style attributes for dimming colours (dim=) and for hyperlinks (link= and
nolink) (issues 4842 and 4280 from Moritz Angermann).
@@ -128,7 +164,9 @@ CHANGES FROM 3.7c TO 3.8
* Add or improve format variables including client_colours,
pane_start_command_list, window_manual_width, window_manual_height,
pane_last_output_time, pane_modal_flag and window_modal_pane.
pane_last_output_time, pane_modal_flag, window_modal_pane, pane_private_modes,
pane_output_generation, history_added, history_collected, history_generation,
hook_fire_count and hook_fire_time.
* Add additional pane sort orders, and a z sort order for choose-tree so
floating panes can be sorted by z-index.
@@ -140,12 +178,18 @@ 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).
* Fix control mode clients hanging on exit if pty data was still queued (Ben
Maurer, issue 5356), and avoid sending notifications to clients which are
already exiting (Ben Maurer, issue 5357).
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). 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

@@ -85,7 +85,6 @@ endif
# List of sources.
dist_tmux_SOURCES = \
active.c \
alerts.c \
arguments.c \
attributes.c \
@@ -175,6 +174,7 @@ dist_tmux_SOURCES = \
input-keys.c \
input.c \
job.c \
json.c \
key-bindings.c \
key-string.c \
layout-custom.c \
@@ -188,7 +188,6 @@ dist_tmux_SOURCES = \
options-table.c \
options.c \
paste.c \
popup.c \
proc.c \
prompt-history.c \
prompt.c \

737
active.c
View File

@@ -1,737 +0,0 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 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 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 <stdlib.h>
#include "tmux.h"
/* Local last-window stack entry. */
struct active_window_entry {
u_int window;
TAILQ_ENTRY(active_window_entry) entry;
};
/* Local last-window stack. */
TAILQ_HEAD(active_window_stack, active_window_entry);
/* Per-client and per-session local active window state. */
struct active_window {
struct client *client;
struct session *session;
u_int window;
struct active_window_stack lastw;
RB_ENTRY(active_window) entry;
};
RB_HEAD(active_windows, active_window);
/* Pane entry in a client-local last-pane stack. */
struct active_pane_entry {
struct window_pane *wp;
TAILQ_ENTRY(active_pane_entry) entry;
};
TAILQ_HEAD(active_pane_stack, active_pane_entry);
/* Per-client and per-window local active pane state. */
struct active_pane {
struct client *client;
struct window *window;
enum active_mode mode;
struct window_pane *pane;
struct active_pane_stack last_panes;
RB_ENTRY(active_pane) entry;
};
RB_HEAD(active_panes, active_pane);
/* Compare local active window states by client and session. */
static int
active_window_cmp(struct active_window *aw1, struct active_window *aw2)
{
uintptr_t c1 = (uintptr_t)aw1->client;
uintptr_t c2 = (uintptr_t)aw2->client;
uintptr_t s1 = (uintptr_t)aw1->session;
uintptr_t s2 = (uintptr_t)aw2->session;
if (c1 < c2)
return (-1);
if (c1 > c2)
return (1);
if (s1 < s2)
return (-1);
if (s1 > s2)
return (1);
return (0);
}
RB_GENERATE_STATIC(active_windows, active_window, entry, active_window_cmp);
/* Compare local active pane states by client and window. */
static int
active_pane_cmp(struct active_pane *ap1, struct active_pane *ap2)
{
uintptr_t c1 = (uintptr_t)ap1->client;
uintptr_t c2 = (uintptr_t)ap2->client;
uintptr_t w1 = (uintptr_t)ap1->window;
uintptr_t w2 = (uintptr_t)ap2->window;
if (c1 < c2)
return (-1);
if (c1 > c2)
return (1);
if (w1 < w2)
return (-1);
if (w1 > w2)
return (1);
return (0);
}
RB_GENERATE_STATIC(active_panes, active_pane, entry, active_pane_cmp);
/* All local active window states. */
static struct active_windows active_windows = RB_INITIALIZER(&active_windows);
/* All local active pane states. */
static struct active_panes active_panes = RB_INITIALIZER(&active_panes);
/* Find local active window state for a client and session. */
static struct active_window *
active_window_get(struct client *c, struct session *s)
{
struct active_window aw;
if (c == NULL || s == NULL)
return (NULL);
aw.client = c;
aw.session = s;
return (RB_FIND(active_windows, &active_windows, &aw));
}
/* Add local active window state for a client and session. */
static struct active_window *
active_window_add(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
aw = active_window_get(c, s);
if (aw == NULL) {
aw = xcalloc(1, sizeof *aw);
aw->client = c;
aw->session = s;
TAILQ_INIT(&aw->lastw);
RB_INSERT(active_windows, &active_windows, aw);
}
if (wl != NULL)
aw->window = wl->window->id;
return (aw);
}
/* Remove a window from a local last-window stack. */
static void
active_window_stack_remove(struct active_window_stack *stack, u_int window)
{
struct active_window_entry *awe, *awe1;
TAILQ_FOREACH_SAFE(awe, stack, entry, awe1) {
if (awe->window == window) {
TAILQ_REMOVE(stack, awe, entry);
free(awe);
}
}
}
/* Push a window onto a local last-window stack. */
static void
active_window_stack_push(struct active_window_stack *stack, u_int window)
{
struct active_window_entry *awe;
active_window_stack_remove(stack, window);
awe = xcalloc(1, sizeof *awe);
awe->window = window;
TAILQ_INSERT_HEAD(stack, awe, entry);
}
/* Free local active window state. */
static void
active_window_free(struct active_window *aw)
{
struct active_window_entry *awe;
while (!TAILQ_EMPTY(&aw->lastw)) {
awe = TAILQ_FIRST(&aw->lastw);
TAILQ_REMOVE(&aw->lastw, awe, entry);
free(awe);
}
RB_REMOVE(active_windows, &active_windows, aw);
free(aw);
}
/* Resolve local active window state to a valid winlink. */
static struct winlink *
active_window_resolve(struct active_window *aw)
{
struct winlink *wl, *next;
u_int window;
if (aw == NULL || aw->session->curw == NULL)
return (NULL);
wl = winlink_find_by_window_id(&aw->session->windows, aw->window);
if (wl != NULL)
return (wl);
while (!TAILQ_EMPTY(&aw->lastw)) {
window = TAILQ_FIRST(&aw->lastw)->window;
next = winlink_find_by_window_id(&aw->session->windows, window);
active_window_stack_remove(&aw->lastw, window);
if (next != NULL) {
aw->window = next->window->id;
return (next);
}
}
aw->window = aw->session->curw->window->id;
return (aw->session->curw);
}
/* Notify the server that a client's effective window changed. */
static void
active_window_changed(struct client *c, struct session *s, struct winlink *wl,
struct winlink *old)
{
struct event_payload *ep;
struct cmd_find_state fs;
if (wl == NULL || wl == old)
return;
if (options_get_number(global_options, "focus-events")) {
if (old != NULL)
window_update_focus(old->window);
window_update_focus(wl->window);
}
winlink_clear_flags(wl);
window_update_activity(wl->window);
tty_update_window_offset(wl->window);
if (c != NULL && c->session == s)
wl->window->latest = c;
ep = event_payload_create();
cmd_find_from_winlink(&fs, wl, 0);
event_payload_set_target(ep, &fs);
event_payload_set_client(ep, "client", c);
event_payload_set_session(ep, "session", s);
event_payload_set_window(ep, "window", wl->window);
event_payload_set_window(ep, "new_window", wl->window);
event_payload_set_int(ep, "window_index", wl->idx);
event_payload_set_int(ep, "new_window_index", wl->idx);
if (old != NULL) {
event_payload_set_window(ep, "old_window", old->window);
event_payload_set_int(ep, "old_window_index", old->idx);
}
events_fire("client-window-changed", ep);
if (c != NULL) {
c->flags |= CLIENT_REDRAWWINDOW|CLIENT_REDRAWSTATUS|
CLIENT_REDRAWBORDERS;
tty_update_client_offset(c);
status_update_cache(s);
}
recalculate_sizes();
}
/* Find local active pane state for a client and window. */
static struct active_pane *
active_pane_get(struct client *c, struct window *w)
{
struct active_pane ap;
if (c == NULL || w == NULL)
return (NULL);
ap.client = c;
ap.window = w;
return (RB_FIND(active_panes, &active_panes, &ap));
}
/* Add local active pane state for a client and window. */
static struct active_pane *
active_pane_add(struct client *c, struct window *w)
{
struct active_pane *ap;
ap = active_pane_get(c, w);
if (ap == NULL) {
ap = xcalloc(1, sizeof *ap);
ap->client = c;
ap->window = w;
TAILQ_INIT(&ap->last_panes);
RB_INSERT(active_panes, &active_panes, ap);
}
return (ap);
}
/* Remove a pane from a client-local last-pane stack. */
static void
active_pane_stack_remove(struct active_pane *ap,
struct window_pane *wp)
{
struct active_pane_entry *ape, *ape1;
TAILQ_FOREACH_SAFE(ape, &ap->last_panes, entry, ape1) {
if (ape->wp == wp) {
TAILQ_REMOVE(&ap->last_panes, ape, entry);
free(ape);
}
}
}
/* Push a pane onto a client-local last-pane stack. */
static void
active_pane_stack_push(struct active_pane *ap, struct window_pane *wp)
{
struct active_pane_entry *ape;
if (wp == NULL)
return;
active_pane_stack_remove(ap, wp);
ape = xcalloc(1, sizeof *ape);
ape->wp = wp;
TAILQ_INSERT_HEAD(&ap->last_panes, ape, entry);
}
/* Free local active pane state. */
static void
active_pane_free(struct active_pane *ap)
{
struct active_pane_entry *ape, *ape1;
RB_REMOVE(active_panes, &active_panes, ap);
TAILQ_FOREACH_SAFE(ape, &ap->last_panes, entry, ape1) {
TAILQ_REMOVE(&ap->last_panes, ape, entry);
free(ape);
}
free(ap);
}
/* Check whether a pane can be the effective pane for a window. */
static int
active_pane_valid(struct window *w, struct window_pane *wp)
{
if (w == NULL || wp == NULL || wp->window != w)
return (0);
if (!window_has_pane(w, wp))
return (0);
if (w->modal != NULL && wp != w->modal)
return (0);
return (1);
}
/* Get the shared pane for a window, accounting for modal panes. */
static struct window_pane *
active_pane_default(struct window *w)
{
if (w == NULL)
return (NULL);
if (w->modal != NULL)
return (w->modal);
return (w->active);
}
/* Return if a client has local window selection for a session. */
int
active_is_local_window(struct client *c, struct session *s)
{
return (active_window_get(c, s) != NULL);
}
/* Make a client use local or shared window selection for a session. */
void
active_set_local_window(struct client *c, struct session *s,
enum active_mode mode)
{
struct active_window *aw;
struct winlink *wl, *old;
if (c == NULL || s == NULL || s->curw == NULL)
return;
old = active_get_effective_winlink(c, s);
if (mode == ACTIVE_LOCAL) {
active_window_add(c, s, old);
if (c != NULL && c->session == s)
c->flags |= CLIENT_REDRAWSTATUS;
return;
}
aw = active_window_get(c, s);
if (aw != NULL) {
active_window_free(aw);
wl = active_get_effective_winlink(c, s);
active_window_changed(c, s, wl, old);
if (c != NULL && c->session == s)
c->flags |= CLIENT_REDRAWSTATUS;
}
}
/* Return the effective winlink for a client and session. */
struct winlink *
active_get_effective_winlink(struct client *c, struct session *s)
{
struct active_window *aw;
if (s == NULL)
return (NULL);
aw = active_window_get(c, s);
if (aw != NULL)
return (active_window_resolve(aw));
return (s->curw);
}
/* Return the effective window for a client and session. */
struct window *
active_get_effective_window(struct client *c, struct session *s)
{
struct winlink *wl;
wl = active_get_effective_winlink(c, s);
if (wl == NULL)
return (NULL);
return (wl->window);
}
/* Select a winlink for a client and session. */
int
active_select_window(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
struct winlink *old;
int changed;
if (wl == NULL)
return (-1);
aw = active_window_get(c, s);
if (aw == NULL)
return (session_set_current(s, wl));
old = active_window_resolve(aw);
if (old == wl)
return (1);
if (old != NULL)
active_window_stack_push(&aw->lastw, old->window->id);
active_window_stack_remove(&aw->lastw, wl->window->id);
aw->window = wl->window->id;
changed = (old != wl);
active_window_changed(c, s, wl, old);
return (changed ? 0 : 1);
}
/* Select a window by index. */
int
active_select_window_index(struct client *c, struct session *s, int idx)
{
struct winlink *wl;
wl = winlink_find_by_index(&s->windows, idx);
return (active_select_window(c, s, wl));
}
/* Select the next window. */
int
active_next_window(struct client *c, struct session *s, int alert)
{
struct winlink *wl;
wl = active_get_effective_winlink(c, s);
if (wl == NULL)
return (-1);
do {
wl = winlink_next(wl);
if (wl == NULL)
wl = RB_MIN(winlinks, &s->windows);
if (!alert || (wl->flags & WINLINK_ALERTFLAGS))
return (active_select_window(c, s, wl));
} while (wl != active_get_effective_winlink(c, s));
return (-1);
}
/* Select the previous window. */
int
active_previous_window(struct client *c, struct session *s, int alert)
{
struct winlink *wl;
wl = active_get_effective_winlink(c, s);
if (wl == NULL)
return (-1);
do {
wl = winlink_previous(wl);
if (wl == NULL)
wl = RB_MAX(winlinks, &s->windows);
if (!alert || (wl->flags & WINLINK_ALERTFLAGS))
return (active_select_window(c, s, wl));
} while (wl != active_get_effective_winlink(c, s));
return (-1);
}
/* Select the last window. */
int
active_last_window(struct client *c, struct session *s)
{
struct active_window *aw;
struct winlink *wl;
u_int window;
aw = active_window_get(c, s);
if (aw == NULL)
return (session_last(s));
while (!TAILQ_EMPTY(&aw->lastw)) {
window = TAILQ_FIRST(&aw->lastw)->window;
wl = winlink_find_by_window_id(&s->windows, window);
if (wl != NULL)
return (active_select_window(c, s, wl));
active_window_stack_remove(&aw->lastw, window);
}
return (-1);
}
/* Return if this winlink is the effective winlink. */
int
active_is_effective_window(struct client *c, struct session *s,
struct winlink *wl)
{
return (wl != NULL && wl == active_get_effective_winlink(c, s));
}
/* Return if this winlink is the last effective winlink. */
int
active_is_last_window(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
struct active_window_entry *awe;
if (wl == NULL)
return (0);
aw = active_window_get(c, s);
if (aw == NULL)
return (wl == TAILQ_FIRST(&s->lastw));
TAILQ_FOREACH(awe, &aw->lastw, entry) {
if (winlink_find_by_window_id(&s->windows, awe->window) == NULL)
continue;
return (wl->window->id == awe->window);
}
return (0);
}
/* Return if a client has local pane selection for a window. */
int
active_has_local_pane(struct client *c, struct window *w)
{
struct active_pane *ap;
ap = active_pane_get(c, w);
return (ap != NULL && ap->mode == ACTIVE_LOCAL);
}
/* Make a client use local or shared pane selection for a window. */
void
active_set_pane_mode(struct client *c, struct window *w, enum active_mode mode)
{
struct active_pane *ap;
struct window_pane *wp;
if (c == NULL || w == NULL)
return;
if (mode == ACTIVE_SHARED) {
ap = active_pane_get(c, w);
if (ap != NULL)
ap->mode = ACTIVE_SHARED;
server_redraw_client(c);
server_status_client(c);
return;
}
if (server_client_how_many() < 2) {
ap = active_pane_get(c, w);
if (ap != NULL)
ap->mode = ACTIVE_SHARED;
server_redraw_client(c);
server_status_client(c);
return;
}
wp = active_get_effective_pane(c, w);
if (wp == NULL)
wp = active_pane_default(w);
ap = active_pane_add(c, w);
ap->mode = ACTIVE_LOCAL;
ap->pane = wp;
server_redraw_client(c);
server_status_client(c);
}
/* Return the effective active pane for a client and window. */
struct window_pane *
active_get_effective_pane(struct client *c, struct window *w)
{
struct active_pane *ap;
struct window_pane *wp;
wp = active_pane_default(w);
ap = active_pane_get(c, w);
if (ap == NULL || ap->mode != ACTIVE_LOCAL)
return (wp);
if (!active_pane_valid(w, ap->pane)) {
ap->pane = wp;
if (wp != NULL)
active_pane_stack_remove(ap, wp);
return (wp);
}
return (ap->pane);
}
/* Return the effective last pane for a client and window. */
struct window_pane *
active_get_last_pane(struct client *c, struct window *w)
{
struct active_pane *ap;
struct active_pane_entry *ape, *ape1;
if (!active_has_local_pane(c, w))
return (w == NULL ? NULL : TAILQ_FIRST(&w->last_panes));
ap = active_pane_get(c, w);
TAILQ_FOREACH_SAFE(ape, &ap->last_panes, entry, ape1) {
if (active_pane_valid(w, ape->wp))
return (ape->wp);
TAILQ_REMOVE(&ap->last_panes, ape, entry);
free(ape);
}
return (NULL);
}
/* Select a pane using either shared or local state. */
int
active_set_pane(struct client *c, struct window *w, struct window_pane *wp,
int notify)
{
struct active_pane *ap;
struct window_pane *lastwp;
if (!active_pane_valid(w, wp))
return (0);
if (!active_has_local_pane(c, w))
return (window_set_active_pane(w, wp, notify));
lastwp = active_get_effective_pane(c, w);
if (wp == lastwp)
return (0);
ap = active_pane_add(c, w);
active_pane_stack_remove(ap, wp);
active_pane_stack_push(ap, lastwp);
ap->pane = wp;
if (c != NULL) {
server_redraw_client(c);
server_status_client(c);
}
return (1);
}
/* Remove all local pane state if fewer than two clients remain. */
void
active_check_clients(void)
{
struct active_pane *ap, *ap1;
if (server_client_how_many() >= 2)
return;
RB_FOREACH_SAFE(ap, active_panes, &active_panes, ap1) {
if (ap->mode == ACTIVE_LOCAL) {
server_redraw_client(ap->client);
server_status_client(ap->client);
}
active_pane_free(ap);
}
}
/* Remove all state for a client. */
void
active_remove_client(struct client *c)
{
struct active_window *aw, *aw1;
struct active_pane *ap, *ap1;
RB_FOREACH_SAFE(aw, active_windows, &active_windows, aw1) {
if (aw->client == c)
active_window_free(aw);
}
RB_FOREACH_SAFE(ap, active_panes, &active_panes, ap1) {
if (ap->client == c)
active_pane_free(ap);
}
}
/* Remove all window state for a session. */
void
active_remove_session(struct session *s)
{
struct active_window *aw, *aw1;
RB_FOREACH_SAFE(aw, active_windows, &active_windows, aw1) {
if (aw->session == s)
active_window_free(aw);
}
}
/* Remove a window from all local state. */
void
active_remove_window(struct window *w)
{
struct active_window *aw;
struct active_pane *ap, *ap1;
RB_FOREACH(aw, active_windows, &active_windows) {
active_window_stack_remove(&aw->lastw, w->id);
if (aw->window == w->id && aw->session->curw != NULL)
aw->window = aw->session->curw->window->id;
}
RB_FOREACH_SAFE(ap, active_panes, &active_panes, ap1) {
if (ap->window == w)
active_pane_free(ap);
}
}
/* Remove a pane from all local pane state. */
void
active_remove_pane(struct window_pane *wp)
{
struct active_pane *ap, *ap1;
RB_FOREACH_SAFE(ap, active_panes, &active_panes, ap1) {
active_pane_stack_remove(ap, wp);
if (ap->pane == wp)
ap->pane = NULL;
}
}

View File

@@ -314,7 +314,7 @@ alerts_set_message(struct winlink *wl, const char *type, const char *option)
tty_putcode(&c->tty, TTYC_BEL);
if (visual == VISUAL_OFF)
continue;
if (active_get_effective_winlink(c, c->session) == wl) {
if (c->session->curw == wl) {
status_message_set(c, -1, 1, 0, 0,
"%s in current window", type);
} else {

View File

@@ -93,7 +93,7 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag,
if (wl != NULL) {
if (wp != NULL)
window_set_active_pane(wp->window, wp, 1);
active_select_window(c, s, wl);
session_set_current(s, wl);
if (wp != NULL)
cmd_find_from_winlink_pane(current, wl, wp, 0);
else

View File

@@ -93,18 +93,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *current = cmdq_get_current(item);
struct cmd_find_state *target = cmdq_get_target(item);
struct cmd_find_state *source = cmdq_get_source(item);
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct client *ac;
struct winlink *wl = source->wl;
struct winlink *current_wl;
struct session *src_s = source->s;
struct session *dst_s = target->s;
struct window_pane *wp = source->wp;
struct window *w = wl->window, *old_w = w;
char *cause, *cp;
int idx = target->idx, before, old_idx = wl->idx;
int detached, local;
const char *template, *name = args_get(args, 'n');
if (wp == w->modal) {
@@ -121,18 +117,11 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
}
before = args_has(args, 'b');
ac = tc;
if (ac == NULL || ac->session != dst_s)
ac = c;
detached = args_has(args, 'd');
local = (!detached && active_is_local_window(ac, dst_s));
if (args_has(args, 'a') || before) {
if (target->wl != NULL)
idx = winlink_shuffle_up(dst_s, target->wl, before);
else {
current_wl = active_get_effective_winlink(ac, dst_s);
idx = winlink_shuffle_up(dst_s, current_wl, before);
}
else
idx = winlink_shuffle_up(dst_s, dst_s->curw, before);
if (idx == -1)
return (CMD_RETURN_ERROR);
}
@@ -140,7 +129,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
if (window_count_panes(w, 1) == 1) {
if (server_link_window(src_s, wl, dst_s, idx, 0,
!detached && !local, &cause) != 0) {
!args_has(args, 'd'), &cause) != 0) {
cmdq_error(item, "%s", cause);
free(cause);
return (CMD_RETURN_ERROR);
@@ -153,13 +142,6 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
wl = winlink_find_by_window(&dst_s->windows, w);
if (wl == NULL)
return (CMD_RETURN_ERROR);
if (local) {
active_select_window(ac, dst_s, wl);
if (ac != NULL && ac->session == dst_s)
cmd_find_from_client(current, ac, 0);
if (ac != NULL && ac->session == dst_s)
server_redraw_client(ac);
}
window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx);
goto out;
}
@@ -203,17 +185,9 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
window_remove_ref(w, __func__);
events_fire_window("window-created", w);
window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx);
if (!detached) {
if (local)
active_select_window(ac, dst_s, wl);
else
session_select(dst_s, wl->idx);
if (local && ac != NULL && ac->session == dst_s)
cmd_find_from_client(current, ac, 0);
else
cmd_find_from_session(current, dst_s, 0);
if (local && ac != NULL && ac->session == dst_s)
server_redraw_client(ac);
if (!args_has(args, 'd')) {
session_select(dst_s, wl->idx);
cmd_find_from_session(current, dst_s, 0);
}
server_redraw_session(src_s);

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

@@ -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)
@@ -483,7 +286,7 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
char *title, *cause = NULL;
int flags = 0, starting_choice = 0;
u_int px, py, i, count = args_count(args);
struct options *o = target->w->options;
struct options *o = target->s->curw->window->options;
struct options_entry *oe;
if (args_has(args, 'C')) {
@@ -565,6 +368,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 +391,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 = target->w->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] == '+')
@@ -884,13 +888,13 @@ cmd_find_from_client(struct cmd_find_state *fs, struct client *c, int flags)
if (c->session != NULL) {
cmd_find_clear_state(fs, flags);
fs->wl = active_get_effective_winlink(c, c->session);
fs->wp = active_get_effective_pane(c, fs->wl->window);
fs->wp = c->session->curw->window->active;
if (fs->wp == NULL) {
cmd_find_from_session(fs, c->session, flags);
return (0);
}
fs->s = c->session;
fs->wl = fs->s->curw;
fs->w = fs->wl->window;
cmd_find_log_state(__func__, fs);
@@ -931,57 +935,6 @@ unknown_pane:
return (cmd_find_from_nothing(fs, flags));
}
/* Find state from mouse target. */
static int
cmd_find_target_from_mouse(struct cmd_find_state *fs, struct cmdq_item *item,
enum cmd_find_type type)
{
struct mouse_event *m = &cmdq_get_event(item)->m;
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
switch (type) {
case CMD_FIND_PANE:
if (m->s != -1 && m->w == -1 && m->wp == -1 &&
(fs->s = session_find_by_id(m->s)) != NULL) {
if (tc != NULL && tc->session == fs->s)
fs->wl = active_get_effective_winlink(tc, fs->s);
else if (c != NULL && c->session == fs->s)
fs->wl = active_get_effective_winlink(c, fs->s);
else
fs->wl = fs->s->curw;
if (fs->wl != NULL) {
fs->w = fs->wl->window;
fs->wp = fs->w->active;
break;
}
}
fs->wp = cmd_mouse_pane(m, &fs->s, &fs->wl);
if (fs->wp != NULL) {
fs->w = fs->wl->window;
break;
}
/* FALLTHROUGH */
case CMD_FIND_WINDOW:
case CMD_FIND_SESSION:
fs->wl = cmd_mouse_window(m, &fs->s);
if (fs->s != NULL && m->w == -1 &&
tc != NULL && tc->session == fs->s)
fs->wl = active_get_effective_winlink(tc, fs->s);
else if (fs->s != NULL && m->w == -1 &&
c != NULL && c->session == fs->s)
fs->wl = active_get_effective_winlink(c, fs->s);
if (fs->wl == NULL && fs->s != NULL)
fs->wl = fs->s->curw;
if (fs->wl != NULL) {
fs->w = fs->wl->window;
fs->wp = fs->w->active;
}
break;
}
return (fs->wp == NULL ? -1 : 0);
}
/*
* Split target into pieces and resolve for the given type. Fills in the given
* state. Returns 0 on success or -1 on error.
@@ -990,6 +943,7 @@ int
cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item,
const char *target, enum cmd_find_type type, int flags)
{
struct mouse_event *m;
struct client *c;
struct cmd_find_state current;
char *colon, *period, *copy = NULL, tmp[256];
@@ -1065,15 +1019,35 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item,
cmdq_error(item, "no current client");
goto error;
}
fs->wl = active_get_effective_winlink(c, c->session);
fs->wp = fs->wl->window->active;
fs->w = fs->wl->window;
fs->wl = c->session->curw;
fs->wp = c->session->curw->window->active;
fs->w = c->session->curw->window;
goto found;
}
/* Mouse target is a plain = or {mouse}. */
if (strcmp(target, "=") == 0 || strcmp(target, "{mouse}") == 0) {
if (cmd_find_target_from_mouse(fs, item, type) != 0) {
m = &cmdq_get_event(item)->m;
switch (type) {
case CMD_FIND_PANE:
fs->wp = cmd_mouse_pane(m, &fs->s, &fs->wl);
if (fs->wp != NULL) {
fs->w = fs->wl->window;
break;
}
/* FALLTHROUGH */
case CMD_FIND_WINDOW:
case CMD_FIND_SESSION:
fs->wl = cmd_mouse_window(m, &fs->s);
if (fs->wl == NULL && fs->s != NULL)
fs->wl = fs->s->curw;
if (fs->wl != NULL) {
fs->w = fs->wl->window;
fs->wp = fs->w->active;
}
break;
}
if (fs->wp == NULL) {
if (~flags & CMD_FIND_QUIET)
cmdq_error(item, "no mouse target");
goto error;

View File

@@ -413,16 +413,13 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *current = cmdq_get_current(item);
struct cmd_find_state *target = cmdq_get_target(item);
struct cmd_find_state *source = cmdq_get_source(item);
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct client *ac;
struct session *dst_s;
struct winlink *src_wl, *dst_wl;
struct window *src_w, *dst_w;
struct window_pane *src_wp, *dst_wp;
const char *s;
char *cause = NULL;
int flags = 0, dst_idx, local;
int flags = 0, dst_idx;
struct layout_cell *lc;
dst_s = target->s;
@@ -430,10 +427,6 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
dst_wp = target->wp;
dst_w = dst_wl->window;
dst_idx = dst_wl->idx;
ac = tc;
if (ac == NULL || ac->session != dst_s)
ac = c;
local = active_is_local_window(ac, dst_s);
if (cmd_get_entry(self) == &cmd_move_pane_entry) {
if (args_has(args, 'M'))
@@ -519,18 +512,9 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
if (!args_has(args, 'd')) {
window_set_active_pane(dst_w, src_wp, 1);
if (local)
active_select_window(ac, dst_s, dst_wl);
else
session_select(dst_s, dst_idx);
if (local && ac != NULL && ac->session == dst_s)
cmd_find_from_client(current, ac, 0);
else
cmd_find_from_session(current, dst_s, 0);
if (local && ac != NULL && ac->session == dst_s)
server_redraw_client(ac);
else
server_redraw_session(dst_s);
session_select(dst_s, dst_idx);
cmd_find_from_session(current, dst_s, 0);
server_redraw_session(dst_s);
} else
server_status_session(dst_s);

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

@@ -62,15 +62,12 @@ cmd_move_window_exec(struct cmd *self, struct cmdq_item *item)
struct args *args = cmd_get_args(self);
struct cmd_find_state *source = cmdq_get_source(item);
struct cmd_find_state target;
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct client *ac;
const char *tflag = args_get(args, 't');
struct session *src = source->s;
struct session *dst;
struct winlink *wl = source->wl, *dstwl, *current_wl;
struct winlink *wl = source->wl;
char *cause;
int idx, kflag, dflag, sflag, before, local;
int idx, kflag, dflag, sflag, before;
if (args_has(args, 'r')) {
if (cmd_find_target(&target, item, tflag, CMD_FIND_SESSION,
@@ -93,37 +90,23 @@ cmd_move_window_exec(struct cmd *self, struct cmdq_item *item)
dflag = args_has(args, 'd');
sflag = args_has(args, 's');
ac = tc;
if (ac == NULL || ac->session != dst)
ac = c;
local = (!dflag && active_is_local_window(ac, dst));
before = args_has(args, 'b');
if (args_has(args, 'a') || before) {
if (target.wl != NULL)
idx = winlink_shuffle_up(dst, target.wl, before);
else {
current_wl = active_get_effective_winlink(ac, dst);
idx = winlink_shuffle_up(dst, current_wl, before);
}
else
idx = winlink_shuffle_up(dst, dst->curw, before);
if (idx == -1)
return (CMD_RETURN_ERROR);
}
if (server_link_window(src, wl, dst, idx, kflag, !dflag && !local,
&cause) != 0) {
if (server_link_window(src, wl, dst, idx, kflag, !dflag, &cause) != 0) {
cmdq_error(item, "%s", cause);
free(cause);
return (CMD_RETURN_ERROR);
}
dstwl = winlink_find_by_window(&dst->windows, wl->window);
if (cmd_get_entry(self) == &cmd_move_window_entry)
server_unlink_window(src, wl);
if (local && dstwl != NULL) {
active_select_window(ac, dst, dstwl);
if (ac != NULL && ac->session == dst)
server_redraw_client(ac);
}
/*
* Renumber the winlinks in the src session only, the destination

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-new-window.c,v 1.103 2026/07/08 08:07:42 nicm Exp $ */
/* $OpenBSD: cmd-new-window.c,v 1.104 2026/09/03 21:04:11 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -58,11 +58,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item);
struct spawn_context sc = { 0 };
struct client *tc = cmdq_get_target_client(item);
struct client *ac;
struct session *s = target->s;
struct winlink *wl = target->wl, *new_wl = NULL;
int idx = target->idx, before, count = args_count(args);
int detached, local;
int idx = target->idx, before;
int count = args_count(args);
char *cause = NULL, *cp, *expanded, *wname = NULL;
const char *template, *name;
struct cmd_find_state fs;
@@ -76,8 +75,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
}
/*
* If -S and -n are given and -t is not and a single window with this
* name already exists, select it.
* If -S is given, select an existing window instead of creating a
* new one: preferring -t if it already points at a window, or
* otherwise -n if one exists with that name. If neither matches,
* fall through and create a window as normal.
*/
name = args_get(args, 'n');
if (name != NULL) {
@@ -90,37 +91,39 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
wname = clean_name(expanded, 0);
free(expanded);
}
if (args_has(args, 'S') && wname != NULL && target->idx == -1) {
expanded = format_single(item, wname, c, s, NULL, NULL);
RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0)
continue;
if (new_wl == NULL) {
new_wl = wl;
continue;
if (args_has(args, 'S')) {
if (idx != -1)
new_wl = winlink_find_by_index(&s->windows, idx);
else if (wname != NULL) {
expanded = format_single(item, wname, c, s, NULL, NULL);
RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0)
continue;
if (new_wl == NULL) {
new_wl = wl;
continue;
}
cmdq_error(item, "multiple windows named %s",
wname);
free(wname);
free(expanded);
return (CMD_RETURN_ERROR);
}
cmdq_error(item, "multiple windows named %s", wname);
free(wname);
free(expanded);
return (CMD_RETURN_ERROR);
}
free(expanded);
if (new_wl != NULL) {
free(wname);
if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL);
if (active_select_window(c, s, new_wl) == 0) {
if (active_is_local_window(c, s) && c != NULL &&
c->session == s)
server_redraw_client(c);
else
server_redraw_session(s);
}
if (c != NULL && c->session != NULL)
active_get_effective_window(c, s)->latest = c;
recalculate_sizes();
}
/* Found an existing window to select instead of creating a new one. */
if (new_wl != NULL) {
free(wname);
if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL);
}
if (session_set_current(s, new_wl) == 0)
server_redraw_session(s);
if (c != NULL && c->session != NULL)
s->curw->window->latest = c;
recalculate_sizes();
return (CMD_RETURN_NORMAL);
}
before = args_has(args, 'b');
@@ -147,16 +150,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
sc.idx = idx;
sc.cwd = args_get(args, 'c');
ac = tc;
if (ac == NULL || ac->session != s)
ac = c;
detached = args_has(args, 'd');
local = (!detached && active_is_local_window(ac, s));
sc.flags = 0;
if (args_has(args, 'E') || (count == 1 && *args_string(args, 0) == '\0'))
sc.flags |= SPAWN_EMPTY;
if (detached || local)
if (args_has(args, 'd'))
sc.flags |= SPAWN_DETACHED;
if (args_has(args, 'k'))
sc.flags |= SPAWN_KILL;
@@ -166,13 +163,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
free(cause);
goto fail;
}
if (local) {
if (active_select_window(ac, s, new_wl) == 0)
cmd_find_from_winlink(current, new_wl, 0);
if (ac != NULL && ac->session == s)
server_redraw_client(ac);
server_status_session_group(s);
} else if (!detached || new_wl == s->curw) {
if (!args_has(args, 'd') || new_wl == s->curw) {
cmd_find_from_winlink(current, new_wl, 0);
server_redraw_session_group(s);
} else

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

@@ -203,7 +203,7 @@ cmd_refresh_client_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'c'))
tc->pan_window = NULL;
else {
w = active_get_effective_window(tc, tc->session);
w = tc->session->curw->window;
if (tc->pan_window != w) {
tc->pan_window = w;
tc->pan_ox = tty->oox;

View File

@@ -79,10 +79,7 @@ static void
cmd_run_shell_print(struct job *job, const char *msg)
{
struct cmd_run_shell_data *cdata = job_get_data(job);
struct client *c = cdata->client;
struct session *s;
struct window_pane *wp = NULL;
struct window *w;
struct cmd_find_state fs;
struct window_mode_entry *wme;
@@ -93,11 +90,8 @@ cmd_run_shell_print(struct job *job, const char *msg)
cmdq_print(cdata->item, "%s", msg);
return;
}
if (c != NULL && c->session != NULL) {
s = c->session;
w = active_get_effective_window(c, s);
wp = active_get_effective_pane(c, w);
}
if (cdata->client != NULL && cdata->client->session != NULL)
wp = cdata->client->session->curw->window->active;
if (wp == NULL && cmd_find_from_nothing(&fs, 0) == 0)
wp = fs.wp;
if (wp == NULL)

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

@@ -33,8 +33,8 @@ const struct cmd_entry cmd_select_pane_entry = {
.name = "select-pane",
.alias = "selectp",
.args = { "DdegLlMmP:RsST:t:UZ", 0, 0, NULL }, /* -P and -g deprecated */
.usage = "[-DdeLlMmRsSUZ] [-T title] " CMD_TARGET_PANE_USAGE,
.args = { "DdegLlMmP:RT:t:UZ", 0, 0, NULL }, /* -P and -g deprecated */
.usage = "[-DdeLlMmRUZ] [-T title] " CMD_TARGET_PANE_USAGE,
.target = { 't', CMD_FIND_PANE, 0 },
@@ -68,11 +68,10 @@ cmd_select_pane_redraw(struct window *w)
TAILQ_FOREACH(c, &clients, entry) {
if (c->session == NULL || (c->flags & CLIENT_CONTROL))
continue;
if (active_get_effective_window(c, c->session) == w &&
tty_window_bigger(&c->tty))
if (c->session->curw->window == w && tty_window_bigger(&c->tty))
server_redraw_client(c);
else {
if (active_get_effective_window(c, c->session) == w)
if (c->session->curw->window == w)
c->flags |= CLIENT_REDRAWBORDERS;
if (session_has(c->session, w))
c->flags |= CLIENT_REDRAWSTATUS;
@@ -85,7 +84,6 @@ static enum cmd_retval
cmd_select_pane_marked_pane(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct cmd_find_state *target = cmdq_get_target(item);
struct event_payload *ep;
struct cmd_find_state fs;
@@ -139,9 +137,8 @@ cmd_select_pane_marked_pane(struct cmd *self, struct cmdq_item *item)
server_status_window(mwp->window);
}
if (window_pane_is_floating(wp)) {
if (!active_has_local_pane(c, wp->window))
window_redraw_active_switch(wp->window, wp);
active_set_pane(c, wp->window, wp, 1);
window_redraw_active_switch(wp->window, wp);
window_set_active_pane(wp->window, wp, 1);
}
return (CMD_RETURN_NORMAL);
}
@@ -155,40 +152,26 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item);
struct event_payload *ep;
struct cmd_find_state fs;
struct client *c = cmdq_get_client(item);
struct winlink *wl = target->wl;
struct window *w = wl->window;
struct session *s = target->s;
struct window_pane *wp = target->wp, *lastwp, *activewp;
struct window_pane *wp = target->wp, *lastwp;
struct options *oo = wp->options;
char *title;
const char *style;
struct options_entry *o;
int visible, Zflag = args_has(args, 'Z');
if (args_has(args, 's') || args_has(args, 'S')) {
if (c == NULL || c->session == NULL) {
cmdq_error(item, "no current client");
return (CMD_RETURN_ERROR);
}
if (args_has(args, 's'))
active_set_pane_mode(c, w, ACTIVE_LOCAL);
else
active_set_pane_mode(c, w, ACTIVE_SHARED);
return (CMD_RETURN_NORMAL);
}
if (entry == &cmd_last_pane_entry || args_has(args, 'l')) {
/*
* Check for no last pane found in case the other pane was
* spawned without being visited (for example split-window -d).
*/
lastwp = active_get_last_pane(c, w);
lastwp = TAILQ_FIRST(&w->last_panes);
if (lastwp == NULL && window_count_panes(w, 1) == 2) {
activewp = active_get_effective_pane(c, w);
lastwp = TAILQ_PREV(activewp, window_panes, entry);
lastwp = TAILQ_PREV(w->active, window_panes, entry);
if (lastwp == NULL)
lastwp = TAILQ_NEXT(activewp, entry);
lastwp = TAILQ_NEXT(w->active, entry);
}
if (lastwp == NULL) {
cmdq_error(item, "no last pane");
@@ -209,9 +192,8 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
visible = window_pane_is_visible(lastwp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
if (!active_has_local_pane(c, w))
window_redraw_active_switch(w, lastwp);
if (active_set_pane(c, w, lastwp, 1)) {
window_redraw_active_switch(w, lastwp);
if (window_set_active_pane(w, lastwp, 1)) {
cmd_find_from_winlink(current, wl, 0);
cmd_select_pane_redraw(w);
}
@@ -289,8 +271,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
}
activewp = active_get_effective_pane(c, w);
if (wp == activewp)
if (wp == w->active)
return (CMD_RETURN_NORMAL);
if (w->modal != NULL && wp != w->modal)
visible = 1;
@@ -298,9 +279,8 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
if (!active_has_local_pane(c, w))
window_redraw_active_switch(w, wp);
if (active_set_pane(c, w, wp, 1))
window_redraw_active_switch(w, wp);
if (window_set_active_pane(w, wp, 1))
cmd_find_from_winlink_pane(current, wl, wp, 0);
cmdq_insert_hook(s, item, current, "after-select-pane");
cmd_select_pane_redraw(w);

View File

@@ -33,8 +33,8 @@ const struct cmd_entry cmd_select_window_entry = {
.name = "select-window",
.alias = "selectw",
.args = { "LlSnpTt:", 0, 0, NULL },
.usage = "[-LlnpST] " CMD_TARGET_WINDOW_USAGE,
.args = { "lnpTt:", 0, 0, NULL },
.usage = "[-lnpT] " CMD_TARGET_WINDOW_USAGE,
.target = { 't', CMD_FIND_WINDOW, 0 },
@@ -88,9 +88,8 @@ cmd_select_window_exec(struct cmd *self, struct cmdq_item *item)
struct client *c = cmdq_get_client(item);
struct cmd_find_state *current = cmdq_get_current(item);
struct cmd_find_state *target = cmdq_get_target(item);
struct winlink *wl = target->wl, *current_wl;
struct winlink *wl = target->wl;
struct session *s = target->s;
enum active_mode mode;
int next, previous, last, activity;
next = (cmd_get_entry(self) == &cmd_next_window_entry);
@@ -103,104 +102,48 @@ cmd_select_window_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'l'))
last = 1;
if (args_has(args, 'L') || args_has(args, 'S')) {
if (c == NULL || c->session == NULL) {
cmdq_error(item, "no current client");
return (CMD_RETURN_ERROR);
}
if (args_has(args, 'S'))
mode = ACTIVE_SHARED;
else
mode = ACTIVE_LOCAL;
active_set_local_window(c, s, mode);
if (!next &&
!previous &&
!last &&
!args_has(args, 't') &&
!args_has(args, 'T')) {
if (current->s == s) {
if (c->session == s)
cmd_find_from_client(current, c, 0);
else
cmd_find_from_session(current, s, 0);
}
if (c->session == s)
server_redraw_client(c);
else
server_redraw_session(s);
cmdq_insert_hook(s, item, current, "after-select-window");
recalculate_sizes();
return (CMD_RETURN_NORMAL);
}
}
if (active_is_local_window(c, s))
mode = ACTIVE_LOCAL;
else
mode = ACTIVE_SHARED;
if (next || previous || last) {
activity = args_has(args, 'a');
if (next) {
if (active_next_window(c, s, activity) != 0) {
if (session_next(s, activity) != 0) {
cmdq_error(item, "no next window");
return (CMD_RETURN_ERROR);
}
} else if (previous) {
if (active_previous_window(c, s, activity) != 0) {
if (session_previous(s, activity) != 0) {
cmdq_error(item, "no previous window");
return (CMD_RETURN_ERROR);
}
} else {
if (active_last_window(c, s) != 0) {
if (session_last(s) != 0) {
cmdq_error(item, "no last window");
return (CMD_RETURN_ERROR);
}
}
if (mode == ACTIVE_LOCAL && c != NULL && c->session == s)
cmd_find_from_client(current, c, 0);
else
cmd_find_from_session(current, s, 0);
if (mode == ACTIVE_LOCAL && c != NULL && c->session == s)
server_redraw_client(c);
else
server_redraw_session(s);
cmd_find_from_session(current, s, 0);
server_redraw_session(s);
cmdq_insert_hook(s, item, current, "after-select-window");
} else {
/*
* If -T and select-window is invoked on same window as
* current, switch to previous window.
*/
current_wl = active_get_effective_winlink(c, s);
if (args_has(args, 'T') && wl == current_wl) {
if (active_last_window(c, s) != 0) {
if (args_has(args, 'T') && wl == s->curw) {
if (session_last(s) != 0) {
cmdq_error(item, "no last window");
return (-1);
}
if (current->s == s) {
if (mode == ACTIVE_LOCAL && c != NULL &&
c->session == s)
cmd_find_from_client(current, c, 0);
else
cmd_find_from_session(current, s, 0);
}
if (mode == ACTIVE_LOCAL && c != NULL && c->session == s)
server_redraw_client(c);
else
server_redraw_session(s);
} else if (active_select_window(c, s, wl) == 0) {
if (mode == ACTIVE_LOCAL && c != NULL && c->session == s)
cmd_find_from_client(current, c, 0);
else
if (current->s == s)
cmd_find_from_session(current, s, 0);
if (mode == ACTIVE_LOCAL && c != NULL && c->session == s)
server_redraw_client(c);
else
server_redraw_session(s);
server_redraw_session(s);
} else if (session_select(s, wl->idx) == 0) {
cmd_find_from_session(current, s, 0);
server_redraw_session(s);
}
cmdq_insert_hook(s, item, current, "after-select-window");
}
if (c != NULL && c->session != NULL)
active_get_effective_window(c, s)->latest = c;
s->curw->window->latest = c;
recalculate_sizes();
return (CMD_RETURN_NORMAL);

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) {

View File

@@ -48,14 +48,10 @@ cmd_swap_window_exec(struct cmd *self, struct cmdq_item *item)
struct args *args = cmd_get_args(self);
struct cmd_find_state *source = cmdq_get_source(item);
struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct client *ac;
struct session *src = source->s, *dst = target->s;
struct session_group *sg_src, *sg_dst;
struct winlink *wl_src = source->wl, *wl_dst = target->wl;
struct window *w_src, *w_dst;
int local_src, local_dst;
sg_src = session_group_contains(src);
sg_dst = session_group_contains(dst);
@@ -83,22 +79,10 @@ cmd_swap_window_exec(struct cmd *self, struct cmdq_item *item)
if (marked_pane.wl == wl_src)
marked_pane.wl = wl_dst;
ac = tc;
if (ac == NULL)
ac = c;
local_dst = active_is_local_window(ac, dst);
local_src = active_is_local_window(ac, src);
if (args_has(args, 'd')) {
if (local_dst)
active_select_window(ac, dst, wl_dst);
else
session_select(dst, wl_dst->idx);
if (src != dst) {
if (local_src)
active_select_window(ac, src, wl_src);
else
session_select(src, wl_src->idx);
}
session_select(dst, wl_dst->idx);
if (src != dst)
session_select(src, wl_src->idx);
}
session_group_synchronize_from(src);
server_redraw_session_group(src);

View File

@@ -53,12 +53,11 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state target;
const char *tflag = args_get(args, 't');
enum cmd_find_type type;
int flags, visible, window_target;
int Zflag = args_has(args, 'Z');
int flags, visible, Zflag = args_has(args, 'Z');
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct session *s;
struct winlink *wl, *current_wl;
struct winlink *wl;
struct window *w;
struct window_pane *wp;
const char *tablename;
@@ -66,13 +65,8 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
struct sort_criteria sort_crit;
uid_t uid;
window_target = 0;
if (tflag != NULL) {
if (tflag[strcspn(tflag, ":.%")] != '\0' ||
strcmp(tflag, "=") == 0)
window_target = 1;
}
if (window_target) {
if (tflag != NULL &&
(tflag[strcspn(tflag, ":.%")] != '\0' || strcmp(tflag, "=") == 0)) {
type = CMD_FIND_PANE;
flags = 0;
} else {
@@ -143,8 +137,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
} else {
if (cmdq_get_client(item) == NULL)
return (CMD_RETURN_NORMAL);
if (window_target && wl != NULL && wp != NULL &&
wp != wl->window->active) {
if (wl != NULL && wp != NULL && wp != wl->window->active) {
w = wl->window;
if (w->modal != NULL && wp != w->modal)
visible = 1;
@@ -157,13 +150,9 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
if (window_target && wl != NULL) {
active_select_window(tc, s, wl);
if (active_is_local_window(tc, s)) {
current_wl = active_get_effective_winlink(tc, s);
cmd_find_from_winlink(current, current_wl, 0);
} else
cmd_find_from_session(current, s, 0);
if (wl != NULL) {
session_set_current(s, wl);
cmd_find_from_session(current, s, 0);
}
}

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. */
@@ -222,28 +229,6 @@ control_client_session_changed_cb(__unused const char *name,
}
}
/* Notify control clients that a client changed its effective window. */
static void
control_client_window_changed_cb(__unused const char *name,
struct event_payload *ep, __unused void *sink_data)
{
struct client *cc = event_payload_get_client(ep, "client");
struct session *s = event_payload_get_session(ep, "session");
struct window *w = event_payload_get_window(ep, "window");
struct client *c;
if (cc == NULL || s == NULL || w == NULL)
return;
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%client-window-changed %s $%u @%u",
cc->name, s->id, w->id);
}
}
/* Notify control clients that a client detached. */
static void
control_client_detached_cb(__unused const char *name, struct event_payload *ep,
@@ -381,7 +366,6 @@ control_build_events(void)
{ "window-linked", control_window_linked_cb },
{ "window-renamed", control_window_renamed_cb },
{ "client-session-changed", control_client_session_changed_cb },
{ "client-window-changed", control_client_window_changed_cb },
{ "client-detached", control_client_detached_cb },
{ "session-renamed", control_session_renamed_cb },
{ "session-created", control_session_created_cb },

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 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);
@@ -455,8 +505,12 @@ control_flush_deferred(struct client *c)
void
control_write(struct client *c, const char *fmt, ...)
{
va_list ap;
char *line;
struct control_state *cs = c->control_state;
char *line;
va_list ap;
if (cs == NULL)
return;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
@@ -479,6 +533,9 @@ control_write_guard(struct client *c, const char *guard, long t, u_int number,
struct control_state *cs = c->control_state;
char *line;
if (cs == NULL)
return;
if (strcmp(guard, "begin") == 0)
cs->guard_depth++;
@@ -503,6 +560,9 @@ control_notify_write(struct client *c, const char *fmt, ...)
va_list ap;
char *line;
if (cs == NULL)
return;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
va_end(ap);
@@ -997,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);
}

165
format.c
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.417 2026/09/08 15:42:26 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -806,7 +806,7 @@ format_cb_window_active_clients(struct format_tree *ft)
if (client_session == NULL)
continue;
if (w == active_get_effective_window(loop, client_session))
if (w == client_session->curw->window)
n++;
}
@@ -838,7 +838,7 @@ format_cb_window_active_clients_list(struct format_tree *ft)
if (client_session == NULL)
continue;
if (w == active_get_effective_window(loop, client_session)) {
if (w == client_session->curw->window) {
if (EVBUFFER_LENGTH(buffer) > 0)
evbuffer_add(buffer, ",", 1);
evbuffer_add_printf(buffer, "%s", loop->name);
@@ -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. */
@@ -1067,8 +1083,7 @@ format_cb_pane_fg(struct format_tree *ft)
if (wp == NULL)
return (NULL);
tty_default_colours(&gc, wp,
active_get_effective_pane(ft->c, wp->window), NULL);
tty_default_colours(&gc, wp, NULL);
return (xstrdup(colour_tostring(gc.fg)));
}
@@ -1119,8 +1134,7 @@ format_cb_pane_bg(struct format_tree *ft)
if (wp == NULL)
return (NULL);
tty_default_colours(&gc, wp,
active_get_effective_pane(ft->c, wp->window), NULL);
tty_default_colours(&gc, wp, NULL);
return (xstrdup(colour_tostring(gc.bg)));
}
@@ -2192,45 +2206,14 @@ format_cb_pane_private_modes(struct format_tree *ft)
static void *
format_cb_pane_active(struct format_tree *ft)
{
struct window_pane *active;
if (ft->wp != NULL) {
active = active_get_effective_pane(ft->c, ft->wp->window);
if (ft->wp == active)
if (ft->wp == ft->wp->window->active)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (NULL);
}
/* Callback for pane_local_active. */
static void *
format_cb_pane_local_active(struct format_tree *ft)
{
struct window_pane *active;
if (ft->wp != NULL && active_has_local_pane(ft->c, ft->wp->window)) {
active = active_get_effective_pane(ft->c, ft->wp->window);
if (ft->wp == active)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (NULL);
}
/* Callback for pane_local_mode. */
static void *
format_cb_pane_local_mode(struct format_tree *ft)
{
struct window *w = ft->w;
if (ft->wp != NULL)
w = ft->wp->window;
if (active_has_local_pane(ft->c, w))
return (xstrdup("1"));
return (xstrdup("0"));
}
/* Callback for pane_at_left. */
static void *
format_cb_pane_at_left(struct format_tree *ft)
@@ -2522,7 +2505,7 @@ static void *
format_cb_pane_last(struct format_tree *ft)
{
if (ft->wp != NULL) {
if (ft->wp == active_get_last_pane(ft->c, ft->wp->window))
if (ft->wp == TAILQ_FIRST(&ft->wp->window->last_panes))
return (xstrdup("1"));
return (xstrdup("0"));
}
@@ -3099,48 +3082,11 @@ format_cb_sixel_support(__unused struct format_tree *ft)
static void *
format_cb_active_window_index(struct format_tree *ft)
{
struct winlink *wl;
if (ft->s != NULL) {
wl = active_get_effective_winlink(ft->c, ft->s);
if (wl != NULL)
return (format_printf("%u", wl->idx));
}
if (ft->s != NULL)
return (format_printf("%u", ft->s->curw->idx));
return (NULL);
}
/* Callback for active_window_id. */
static void *
format_cb_active_window_id(struct format_tree *ft)
{
struct winlink *wl;
if (ft->s != NULL) {
wl = active_get_effective_winlink(ft->c, ft->s);
if (wl != NULL)
return (format_printf("@%u", wl->window->id));
}
return (NULL);
}
/* Callback for local_active_window. */
static void *
format_cb_local_active_window(struct format_tree *ft)
{
if (ft->c != NULL && ft->s != NULL)
return (format_printf("%d", active_is_local_window(ft->c, ft->s)));
return (xstrdup("0"));
}
/* Callback for active_window_mode. */
static void *
format_cb_active_window_mode(struct format_tree *ft)
{
if (ft->c != NULL && ft->s != NULL && active_is_local_window(ft->c, ft->s))
return (xstrdup("local"));
return (xstrdup("shared"));
}
/* Callback for last_window_index. */
static void *
format_cb_last_window_index(struct format_tree *ft)
@@ -3158,11 +3104,8 @@ format_cb_last_window_index(struct format_tree *ft)
static void *
format_cb_window_active(struct format_tree *ft)
{
struct winlink *wl;
if (ft->wl != NULL) {
wl = active_get_effective_winlink(ft->c, ft->wl->session);
if (ft->wl == wl)
if (ft->wl == ft->wl->session->curw)
return (xstrdup("1"));
return (xstrdup("0"));
}
@@ -3242,7 +3185,7 @@ static void *
format_cb_window_flags(struct format_tree *ft)
{
if (ft->wl != NULL)
return (xstrdup(window_printable_flags(ft->c, ft->wl, 1)));
return (xstrdup(window_printable_flags(ft->wl, 1)));
return (NULL);
}
@@ -3429,7 +3372,7 @@ static void *
format_cb_window_raw_flags(struct format_tree *ft)
{
if (ft->wl != NULL)
return (xstrdup(window_printable_flags(ft->c, ft->wl, 0)));
return (xstrdup(window_printable_flags(ft->wl, 0)));
return (NULL);
}
@@ -3639,15 +3582,9 @@ struct format_table_entry {
* Only variables which are added by the caller go into the tree.
*/
static const struct format_table_entry format_table[] = {
{ "active_window_id", FORMAT_TABLE_STRING,
format_cb_active_window_id
},
{ "active_window_index", FORMAT_TABLE_STRING,
format_cb_active_window_index
},
{ "active_window_mode", FORMAT_TABLE_STRING,
format_cb_active_window_mode
},
{ "alternate_on", FORMAT_TABLE_STRING,
format_cb_alternate_on
},
@@ -3825,9 +3762,6 @@ static const struct format_table_entry format_table[] = {
{ "last_window_index", FORMAT_TABLE_STRING,
format_cb_last_window_index
},
{ "local_active_window", FORMAT_TABLE_STRING,
format_cb_local_active_window
},
{ "mouse_all_flag", FORMAT_TABLE_STRING,
format_cb_mouse_all_flag
},
@@ -3972,12 +3906,6 @@ static const struct format_table_entry format_table[] = {
{ "pane_left", FORMAT_TABLE_STRING,
format_cb_pane_left
},
{ "pane_local_active", FORMAT_TABLE_STRING,
format_cb_pane_local_active
},
{ "pane_local_mode", FORMAT_TABLE_STRING,
format_cb_pane_local_mode
},
{ "pane_marked", FORMAT_TABLE_STRING,
format_cb_pane_marked
},
@@ -5322,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
@@ -5383,18 +5312,15 @@ format_add_window_neighbour(struct format_tree *nft, struct winlink *wl,
struct session *s, const char *prefix)
{
struct options_entry *o;
struct winlink *current;
const char *oname;
char *key, *prefixed, *oval;
current = active_get_effective_winlink(nft->c, s);
xasprintf(&key, "%s_window_index", prefix);
format_add(nft, key, "%u", wl->idx);
free(key);
xasprintf(&key, "%s_window_active", prefix);
format_add(nft, key, "%d", wl == current);
format_add(nft, key, "%d", wl == s->curw);
free(key);
o = options_first(wl->window->options);
@@ -5422,10 +5348,10 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
struct cmdq_item *item = ft->item;
struct format_tree *nft;
struct format_expand_state next;
char *all, *active_fmt, *use, *expanded, *value;
char *all, *active, *use, *expanded, *value;
struct evbuffer *buffer;
size_t size;
struct winlink *wl, **l, *active_wl;
struct winlink *wl, **l;
struct window *w;
int i, n;
@@ -5434,9 +5360,9 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
return (NULL);
}
if (format_choose(es, fmt, &all, &active_fmt, 0) != 0) {
if (format_choose(es, fmt, &all, &active, 0) != 0) {
all = xstrdup(fmt);
active_fmt = NULL;
active = NULL;
}
buffer = evbuffer_new();
@@ -5444,13 +5370,12 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
fatalx("out of memory");
l = sort_get_winlinks_session(s, &n, sc);
active_wl = active_get_effective_winlink(ft->c, s);
for (i = 0; i < n; i++) {
wl = l[i];
w = wl->window;
format_log(es, "window loop: %u @%u", wl->idx, w->id);
if (active_fmt != NULL && wl == active_wl)
use = active_fmt;
if (active != NULL && wl == s->curw)
use = active;
else
use = all;
nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags);
@@ -5459,11 +5384,11 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
format_add(nft, "loop_last_flag", "%d", i == n - 1);
/* Add neighbour window data to the format tree. */
if (i > 0 && l[i - 1] == active_wl)
if (i > 0 && l[i - 1] == s->curw)
format_add(nft, "window_after_active", "1");
else
format_add(nft, "window_after_active", "0");
if (i + 1 < n && l[i + 1] == active_wl)
if (i + 1 < n && l[i + 1] == s->curw)
format_add(nft, "window_before_active", "1");
else
format_add(nft, "window_before_active", "0");
@@ -5483,7 +5408,7 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
free(expanded);
}
free(active_fmt);
free(active);
free(all);
if ((size = EVBUFFER_LENGTH(buffer)) != 0)
@@ -7071,9 +6996,9 @@ format_defaults(struct format_tree *ft, struct client *c, struct session *s,
if (s == NULL && c != NULL)
s = c->session;
if (wl == NULL && s != NULL)
wl = active_get_effective_winlink(c, s);
wl = s->curw;
if (wp == NULL && wl != NULL)
wp = active_get_effective_pane(c, wl->window);
wp = wl->window->active;
if (c != NULL)
format_defaults_client(ft, c);

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);

17
input.c
View File

@@ -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);
@@ -3078,8 +3076,7 @@ input_osc_10(struct input_ctx *ictx, const char *p)
return;
c = window_pane_get_fg_control_client(wp);
if (c == -1) {
tty_default_colours(&defaults, wp, wp->window->active,
NULL);
tty_default_colours(&defaults, wp, NULL);
if (COLOUR_DEFAULT(defaults.fg))
c = window_pane_get_fg(wp);
else
@@ -3426,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

@@ -31,8 +31,6 @@
" 'Rename' 'r' {command-prompt -I '#S' {rename-session -- '%%'}}" \
" 'Detach' 'd' {detach-client}" \
" ''" \
" '#{?local_active_window,Shared Window,Local Window}' 'L' {select-window #{?local_active_window,-S,-L}}" \
" ''" \
" 'New Session' 's' {new-session}" \
" 'New Window' 'w' {new-window}"
#define DEFAULT_WINDOW_MENU \
@@ -74,8 +72,6 @@
" '#{?#{!:#{pane_floating_flag}},Horizontal Split,}' 'h' {split-window -h}" \
" '#{?#{!:#{pane_floating_flag}},Vertical Split,}' 'v' {split-window -v}" \
" ''" \
" '#{?#{>=:#{window_active_clients},2},#{?pane_local_mode,Shared Active Pane,Local Active Pane},}' 'a' {if -F '#{pane_local_mode}' {select-pane -S} {select-pane -s}}" \
" ''" \
" '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Up,}' 'u' {swap-pane -U}" \
" '#{?#{&&:#{!:#{pane_floating_flag}},#{>:#{window_panes},1}},Swap Down,}' 'd' {swap-pane -D}" \
" '#{?pane_marked_set,,-}Swap Marked' 's' {swap-pane}" \
@@ -507,7 +503,6 @@ key_bindings_init(void)
/* Mouse button 1 down on pane. */
"bind -n MouseDown1Pane { select-pane -t=; send -M }",
"bind -n M-MouseDown1Pane { move-pane -P front -t= }",
/* Mouse button 1 drag on pane. */
"bind -n MouseDrag1Pane { if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -M } }",
@@ -529,7 +524,6 @@ key_bindings_init(void)
/* Mouse button 1 on border. */
"bind -n MouseDown1Border { select-pane -M }",
"bind -n M-MouseDown1Border { move-pane -P front -t= }",
/* Mouse button 1 drag on border. */
"bind -n MouseDrag1Border { resize-pane -M }",

File diff suppressed because it is too large Load Diff

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.99 2026/09/09 07:03:39 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;
@@ -509,18 +486,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 +700,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;

2
menu.c
View File

@@ -586,7 +586,7 @@ menu_display(struct menu *menu, int flags, int starting_choice,
struct options *o;
if (fs == NULL)
w = active_get_effective_window(c, c->session);
w = c->session->curw->window;
else
w = fs->w;
o = w->options;

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.245 2026/09/10 11:02:18 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -78,7 +78,7 @@ static const char *options_table_pane_border_indicators_list[] = {
static const char *options_table_pane_border_lines_list[] = {
"single", "double", "heavy", "simple", "number", "spaces", "none", NULL
};
static const char *options_table_popup_border_lines_list[] = {
static const char *options_table_box_lines_list[] = {
"single", "double", "heavy", "simple", "rounded", "padded", "none", NULL
};
static const char *options_table_set_clipboard_list[] = {
@@ -91,7 +91,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
@@ -492,7 +492,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_box_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."
@@ -1125,8 +1125,7 @@ const struct options_table_entry options_table[] = {
{ .name = "status-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SESSION,
.default_str = "bg=#{?local_active_window,themeblue,themegreen},"
"fg=themeblack",
.default_str = "bg=themegreen,fg=themeblack",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Style of the status line."
@@ -1554,9 +1553,8 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.default_str = "fg=#{?pane_modal_flag,themeblue,"
"#{?pane_marked,thememagenta,"
"#{?pane_local_active,themecyan,"
"#{?synchronize-panes,themered,"
"#{?pane_in_mode,themeyellow,themegreen}}}}}",
"#{?pane_in_mode,themeyellow,themegreen}}}}",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Style of the active pane border."
@@ -1667,40 +1665,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."
},
@@ -1984,8 +1956,6 @@ const struct options_table_entry options_table[] = {
"Run when a client is resized."),
OPTIONS_TABLE_HOOK("client-session-changed", "",
"Run when a client's attached session is changed."),
OPTIONS_TABLE_HOOK("client-window-changed", "",
"Run when a client's effective window is changed."),
OPTIONS_TABLE_HOOK("client-light-theme", "",
"Run when a client switches to a light theme."),
OPTIONS_TABLE_HOOK("client-dark-theme", "",

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 = active_get_effective_window(c, s)->options;
ft = format_create_defaults(NULL, c, s, NULL, 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 = active_get_effective_window(c, s)->options;
else
o = active_get_effective_window(c, c->session)->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,151 +0,0 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMPDIR=$(mktemp -d)
IN1="$TMPDIR/in1"
IN2="$TMPDIR/in2"
OUT1="$TMPDIR/out1"
OUT2="$TMPDIR/out2"
PID1=
PID2=
cleanup()
{
[ -n "$PID1" ] && kill "$PID1" 2>/dev/null
[ -n "$PID2" ] && kill "$PID2" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup EXIT
fail()
{
echo "$1"
echo "client1:"
cat "$OUT1" 2>/dev/null
echo "client2:"
cat "$OUT2" 2>/dev/null
exit 1
}
wait_for()
{
file=$1
pattern=$2
i=0
while [ "$i" -lt 60 ]; do
if grep -F -- "$pattern" "$file" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
i=$((i + 1))
done
fail "missing '$pattern' in $file"
}
send1()
{
printf '%s\n' "$*" >&3
}
send2()
{
printf '%s\n' "$*" >&4
}
check_server()
{
out=$($TMUX display-message -p -t "$1" "$2")
[ "$out" = "$3" ] || fail "expected '$3' for $1 $2, got '$out'"
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s aw -x 80 -y 24 -n w0 || exit 1
$TMUX new-window -d -t aw: -n w1 || exit 1
$TMUX new-window -d -t aw: -n w2 || exit 1
$TMUX new-session -d -s aw2 -x 80 -y 24 -n x0 || exit 1
$TMUX new-window -d -t aw2: -n x1 || exit 1
mkfifo "$IN1" "$IN2" || exit 1
: >"$OUT1"
: >"$OUT2"
$TMUX -C attach-session -t aw <"$IN1" >"$OUT1" 2>&1 &
PID1=$!
$TMUX -C attach-session -t aw <"$IN2" >"$OUT2" 2>&1 &
PID2=$!
exec 3>"$IN1"
exec 4>"$IN2"
send1 'display-message -p C1READY'
send2 'display-message -p C2READY'
wait_for "$OUT1" C1READY
wait_for "$OUT2" C2READY
# Default shared selection: client 1 changes session current, client 2 follows.
send1 'display-message -p "C1A #{active_window_index} #{local_active_window} #{window_index}"'
wait_for "$OUT1" 'C1A 0 0 0'
send1 'select-window -t :1'
wait_for "$OUT2" '%session-window-changed $0 @1'
send2 'display-message -p "C2A #{active_window_index} #{local_active_window} #{window_index}"'
wait_for "$OUT2" 'C2A 1 0 1'
check_server 'aw:' '#{window_index}' '1'
# Local selection changes only client 1 and becomes the default command target.
send1 'select-window -L'
send1 'select-window -t :2'
send1 'display-message -p "C1B #{active_window_index} #{local_active_window} #{window_index}"'
wait_for "$OUT1" 'C1B 2 1 2'
send2 'display-message -p "C2B #{active_window_index} #{local_active_window} #{window_index}"'
wait_for "$OUT2" 'C2B 1 0 1'
check_server 'aw:' '#{window_index}' '1'
# Local last-window history is independent of the session shared stack.
send1 'last-window'
send1 'display-message -p "C1C #{active_window_index} #{window_index}"'
wait_for "$OUT1" 'C1C 1 1'
send1 'next-window'
send1 'display-message -p "C1D #{active_window_index} #{window_index}"'
wait_for "$OUT1" 'C1D 2 2'
check_server 'aw:' '#{window_index}' '1'
# Session switches preserve per-session local selections.
send1 'switch-client -t aw2'
send1 'select-window -L'
send1 'select-window -t :1'
send1 'display-message -p "C1E #{session_name} #{active_window_index} #{local_active_window}"'
wait_for "$OUT1" 'C1E aw2 1 1'
send1 'switch-client -t aw'
send1 'display-message -p "C1F #{session_name} #{active_window_index} #{local_active_window}"'
wait_for "$OUT1" 'C1F aw 2 1'
# Returning to shared mode immediately follows the session current window.
send1 'select-window -S'
send1 'display-message -p "C1G #{active_window_index} #{local_active_window} #{window_index}"'
wait_for "$OUT1" 'C1G 1 0 1'
# Invalid local windows recover to a valid session window without changing curw.
send1 'select-window -L'
send1 'select-window -t :2'
$TMUX kill-window -t aw:2 || fail 'kill-window failed'
send1 'display-message -p "C1H #{active_window_index} #{local_active_window} #{window_index}"'
wait_for "$OUT1" 'C1H 1 1 1'
check_server 'aw:' '#{window_index}' '1'
# Creating a window without -d in local mode selects it locally only.
send1 'new-window -n w3'
send1 'display-message -p "C1I #{local_active_window} #{window_name}"'
wait_for "$OUT1" 'C1I 1 w3'
send2 'display-message -p "C2C #{local_active_window} #{window_name}"'
wait_for "$OUT2" 'C2C 0 w1'
check_server 'aw:' '#{window_name}' 'w1'
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

0
regress/cfg-client-lost.sh Executable file → Normal file
View File

View File

@@ -290,4 +290,92 @@ wait_count ': G4' 7
fail "window 1 of zzz not killed"
exit_mode q
# --- help and information preview --------------------------------------------
# The complete tree help is taller than the 24-line test terminal, where the
# help renderer deliberately leaves the tree unchanged.
$TMUX2 resize-window -t out:0 -x 80 -y 50 || exit 1
i=0
while [ "$i" -lt 50 ]; do
# The inner session has a one-line status, leaving a 49-line window.
[ "$($TMUX display-message -p -t aaa:0 '#{window_height}')" -ge 49 ] &&
break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "attached client did not resize for tree help"
$TMUX choose-tree -t aaa:0 -F 'G5' -O index || exit 1
wait_count ': G5' 7
$TMUX2 send-keys -t out:0 F1 || fail "send-keys F1 failed"
wait_for 'Swap current and previous window'
# The first key closes help; the second exits the tree.
$TMUX send-keys -t aaa:0 q q || fail "send-keys help exit failed"
wait_mode aaa:0 0
$TMUX choose-tree -t aaa:0 -F 'G6' -O index || exit 1
wait_count ': G6' 7
$TMUX send-keys -t aaa:0 i || fail "send-keys info failed"
wait_for 'Session'
$TMUX send-keys -t aaa:0 i || fail "send-keys preview failed"
exit_mode q
# --- window swapping and command prompt for current/tagged items -------------
# Restrict a window-only tree to two disposable windows in aaa. J (the
# Shift-Down equivalent) swaps their window objects while leaving their indexes
# in place.
$TMUX new-window -d -t aaa: -n swap-a 'cat' || exit 1
$TMUX new-window -d -t aaa: -n swap-b 'cat' || exit 1
aidx=$($TMUX list-windows -t aaa -F '#{window_index}:#{window_name}' | \
awk -F: '$2 == "swap-a" {print $1}')
bidx=$($TMUX list-windows -t aaa -F '#{window_index}:#{window_name}' | \
awk -F: '$2 == "swap-b" {print $1}')
$TMUX choose-tree -w -t aaa:0 -O index -F 'WT #{window_name}' \
-f '#{m:swap-*,#{window_name}}' || exit 1
wait_count 'WT swap-' 2
$TMUX send-keys -t aaa:0 g j J || fail "window tree swap failed"
i=0
while [ "$i" -lt 50 ]; do
aname=$($TMUX display-message -p -t "aaa:$aidx" '#{window_name}')
bname=$($TMUX display-message -p -t "aaa:$bidx" '#{window_name}')
[ "$aname:$bname" = 'swap-b:swap-a' ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "window tree did not swap the two windows"
# ':' runs an entered command for the current item and rebuilds the tree when
# its queued command completes.
$TMUX set-option -g @tree-command '' || exit 1
$TMUX send-keys -t aaa:0 : || fail "window tree command prompt failed"
wait_for '(current)'
$TMUX send-keys -t aaa:0 -l "set-option -g @tree-command '%%'" || exit 1
$TMUX send-keys -t aaa:0 Enter || exit 1
i=0
while [ "$i" -lt 50 ]; do
value=$($TMUX show-option -gqv @tree-command)
[ -n "$value" ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "window tree entered command did not run"
case "$value" in
=aaa:*.) ;;
*) fail "window tree command target was '$value'" ;;
esac
# Tag both disposable windows and kill them through the tagged confirmation
# callback. The mode-hosting window is outside the filter and survives.
$TMUX send-keys -t aaa:0 g j t j t X || fail "tagged kill keys failed"
wait_for 'Kill 2 tagged?'
$TMUX send-keys -t aaa:0 y || fail "tagged kill confirmation failed"
i=0
while [ "$i" -lt 50 ]; do
left=$($TMUX list-windows -t aaa -F '#{window_name}' | \
grep -c '^swap-' || true)
[ "$left" -eq 0 ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "tagged windows were not killed"
exit_mode q
exit 0

View File

@@ -0,0 +1,96 @@
#!/bin/sh
# With clear-on-attach disabled tmux must preserve the terminal contents by
# scrolling them away, rather than entering and clearing the alternate screen.
# Capture the bytes written by a real attached client for both settings.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER_ON="$TEST_TMUX -LtestIon$$ -f/dev/null"
OUTER_ON="$TEST_TMUX -LtestOon$$ -f/dev/null"
INNER_OFF="$TEST_TMUX -LtestIoff$$ -f/dev/null"
OUTER_OFF="$TEST_TMUX -LtestOoff$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER_ON kill-server 2>/dev/null
$INNER_ON kill-server 2>/dev/null
$OUTER_OFF kill-server 2>/dev/null
$INNER_OFF kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
$current_inner list-clients 2>/dev/null | grep -q . && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
capture_attach()
{
setting=$1
output=$2
go=$DIR/go-$setting
if [ "$setting" = on ]; then
current_inner=$INNER_ON
current_outer=$OUTER_ON
else
current_inner=$INNER_OFF
current_outer=$OUTER_OFF
fi
$current_inner new-session -d -s inner -x40 -y10 \
'exec sleep 100' || exit 1
$current_inner set-option -g status off || exit 1
$current_inner set-option -g clear-on-attach "$setting" || exit 1
$current_outer new-session -d -s outer -x40 -y10 \
"while [ ! -e '$go' ]; do sleep 0.01; done; exec $current_inner attach-session -t inner" || exit 1
$current_outer set-option -g status off || exit 1
$current_outer pipe-pane -O "cat >'$output'" || exit 1
touch "$go"
wait_for_client
sleep 0.5
$current_outer pipe-pane -O || exit 1
}
on=$DIR/on
off=$DIR/off
capture_attach on "$on"
capture_attach off "$off"
smcup=$(printf '\033[?1049h')
grep -Fq "$smcup" "$on" || fail "clear-on-attach on did not use smcup"
if grep -Fq "$smcup" "$off"; then
fail "clear-on-attach off used smcup"
fi
# Some screen terminfo entries have indn (CSI Ps S), while older ones only
# have ind (newline). The preserving path uses indn once or repeats ind once
# for every line in the terminal plus one.
escape=$(printf '\033')
if ! LC_ALL=C grep -Eq "${escape}\\[[0-9]+S" "$off"; then
hex=$(od -An -tx1 -v "$off" | tr -d ' \n')
case "$hex" in
*0a0a0a0a0a0a0a0a0a0a0a*) ;;
*) fail "clear-on-attach off did not scroll the old contents away" ;;
esac
fi
exit 0

View File

@@ -0,0 +1,82 @@
#!/bin/sh
# send-keys -K injects a key into a client. Commands produced by that key must
# be inserted immediately after send-keys, before later commands in the same
# command list. Focus events arriving between a prefix and its following key
# must be forwarded without cancelling the prefix table.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
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_for_option()
{
option=$1
expected=$2
i=0
while [ "$i" -lt 50 ]; do
actual=$($INNER show-option -gv "$option" 2>/dev/null)
[ "$actual" = "$expected" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "$option is '$actual', expected '$expected'"
}
cleanup
$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g focus-events on || exit 1
$OUTER new-session -d -s outer -x40 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g status off || exit 1
wait_for_client
# The binding generated by send-keys -K must run before the command which
# follows send-keys in this command list. Before the queue-ordering fix this
# produces AB rather than BA.
$INNER set-option -g @order '' || exit 1
$INNER bind-key -n a set-option -gF @order '#{@order}B' || exit 1
$INNER send-keys -K -c "$client" a \; \
set-option -gF @order '#{@order}A' || exit 1
wait_for_option @order BA
# Put the real attached client in the prefix table, deliver FocusIn, then the
# bound key. FocusIn is not bindable and must not reset the prefix table.
$INNER bind-key x set-option -g @focus-prefix yes || exit 1
$OUTER send-keys C-b || exit 1
focus_in=$(printf '\033[I')
$OUTER send-keys -l "$focus_in" || exit 1
$OUTER send-keys x || exit 1
wait_for_option @focus-prefix yes
exit 0

101
regress/clock-mode.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/bin/sh
# Exercise clock-mode initialization, all four styles, its one-second timer,
# resize into the compact renderer, arbitrary-key exit, and cleanup.
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
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
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
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_mode()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER display-message -p -t clock:0 '#{pane_mode}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "pane mode is '$got', expected '$want'"
}
wait_hashes()
{
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -q '#' && return 0
sleep 0.1
i=$((i + 1))
done
fail "large clock did not render"
}
$INNER new-session -d -s clock -x80 -y24 '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 -w clock-mode-colour red || exit 1
$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t clock" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
for style in 12 24 12-with-seconds 24-with-seconds; do
$INNER set-option -w -t clock:0 clock-mode-style "$style" || exit 1
$INNER clock-mode -t clock:0 || exit 1
wait_mode clock-mode
wait_hashes
# Let at least one timer callback observe a new second.
[ "$style" != 12-with-seconds ] || sleep 1.2
$INNER send-keys -t clock:0 x || exit 1
wait_mode ''
done
# Resizing an active mode below the large-glyph threshold selects the compact
# renderer. A visible time contains a colon and no block-clock hash.
$INNER clock-mode -t clock:0 || exit 1
wait_mode clock-mode
$INNER resize-window -t clock:0 -x20 -y5 || exit 1
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Eq '[0-9][0-9]?:[0-9][0-9]' && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "compact clock did not render after resize"
printf '%s\n' "$captured" | grep -q '#' &&
fail "compact clock still used large glyphs"
$INNER send-keys -t clock:0 Enter || exit 1
wait_mode ''
$INNER has-session -t clock || fail "server died during clock-mode tests"
exit 0

View File

@@ -0,0 +1,150 @@
#!/bin/sh
# Exercise confirm-before's waiting and background callbacks, custom keys and
# prompts, default-yes handling and errors. Also cover all three lock command
# entry points with a harmless lock-command which leaves a marker.
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
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
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
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_capture()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Fq "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "timed out waiting for '$marker'"
}
wait_option()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER show-option -gqv @confirmed 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "@confirmed is '$got', expected '$want'"
}
wait_file()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
[ -f "$marker" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "lock command did not create $marker"
}
$INNER new-session -d -s test -x80 -y24 'exec sleep 100' || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t test" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
client=$($INNER list-clients -F '#{client_name}')
[ -n "$client" ] || fail "inner client did not attach"
$INNER bind-key -n M-a confirm-before \
'set-option -g @confirmed accepted' || exit 1
$INNER bind-key -n M-b confirm-before -b -p 'Background?' \
'set-option -g @confirmed background' || exit 1
$INNER bind-key -n M-c confirm-before -c x -p 'Custom?' \
'set-option -g @confirmed custom' || exit 1
$INNER bind-key -n M-y confirm-before -y -p 'Default?' \
'set-option -g @confirmed default' || exit 1
# A non-confirming response closes the waiting prompt without running its
# command; reopening and accepting inserts the command after the waiting item.
$INNER set-option -g @confirmed sentinel || exit 1
$OUTER send-keys M-a || exit 1
wait_capture "Confirm 'set-option'? (y/n)"
$OUTER send-keys n || exit 1
sleep 0.2
wait_option sentinel
$OUTER send-keys M-a || exit 1
wait_capture "Confirm 'set-option'? (y/n)"
$OUTER send-keys y || exit 1
wait_option accepted
# -b has no waiting queue item and appends its command to the client queue.
$OUTER send-keys M-b || exit 1
wait_capture 'Background?'
$OUTER send-keys y || exit 1
wait_option background
# A custom printable key is accepted, and -y makes Enter affirmative.
$OUTER send-keys M-c || exit 1
wait_capture 'Custom?'
$OUTER send-keys x || exit 1
wait_option custom
$OUTER send-keys M-y || exit 1
wait_capture 'Default?'
$OUTER send-keys Enter || exit 1
wait_option default
# Reject invalid multi-character and control confirmation keys.
if $INNER confirm-before -b -t "$client" -c xx 'display-message x' \
>/dev/null 2>&1; then
fail "multi-character confirm key was accepted"
fi
if $INNER confirm-before -b -t "$client" -c "$(printf '\001')" \
'display-message x' >/dev/null 2>&1; then
fail "control confirm key was accepted"
fi
# lock-client, lock-session and lock-server all send lock-command to the real
# attached client. The command returns immediately and unlocks the client.
for kind in client session server; do
marker="$DIR/locked-$kind"
$INNER set-option -t test lock-command "printf locked >$marker" || exit 1
case "$kind" in
client) $INNER lock-client -t "$client" || exit 1 ;;
session) $INNER lock-session -t test || exit 1 ;;
server) $INNER lock-server || exit 1 ;;
esac
wait_file "$marker"
done
$INNER has-session -t test || fail "server died during prompt or lock tests"
exit 0

0
regress/control-client-exit-stalled.sh Executable file → Normal file
View File

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

@@ -0,0 +1,64 @@
#!/bin/sh
# 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.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -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
RAN=$DIR/popup-ran
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
exec 3>&-
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
mkfifo "$FIFO" || exit 1
$TMUX new-session -d -s control -x40 -y10 'exec sleep 100' || exit 1
$TMUX -C attach-session -t control <"$FIFO" >"$OUT" 2>&1 &
control_pid=$!
exec 3>"$FIFO"
i=0
while [ "$i" -lt 50 ]; do
$TMUX list-clients -F '#{client_flags}' 2>/dev/null |
grep -q 'control-mode' && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "control client did not attach"
printf '%s\n' "display-popup -E 'touch $RAN'" >&3
printf '%s\n' "display-message -p CONTROL-POPUP-ALIVE" >&3
i=0
while [ "$i" -lt 50 ]; do
grep -q 'CONTROL-POPUP-ALIVE' "$OUT" 2>/dev/null && break
if ! kill -0 "$control_pid" 2>/dev/null; then
fail "control client exited after display-popup"
fi
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "command after control-client popup did not run"
$TMUX has-session -t control || fail "server exited after control-client popup"
[ ! -e "$RAN" ] || fail "control client started a popup command"
exit 0

View File

@@ -0,0 +1,38 @@
#!/bin/sh
# A waited run-shell command can complete after a control client has closed its
# input and exited. The command queue still references the client until the job
# callback runs, but control_stop has already destroyed c->control_state. Late
# command output must be discarded instead of dereferencing the stopped control
# state and killing the server.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
$TMUX kill-server 2>/dev/null
OUT=$(mktemp)
trap "$TMUX kill-server 2>/dev/null; rm -f $OUT" 0 1 15
$TMUX -f/dev/null new-session -d -s main || exit 1
printf '%s\n' 'run-shell "sleep 0.2; echo TEST"' |
$TMUX -f/dev/null -C >"$OUT" 2>&1
sleep 1
$TMUX has-session -t main 2>/dev/null || {
echo "server exited after late run-shell output"
cat "$OUT"
exit 1
}
grep -q '^%exit' "$OUT" || {
echo "control client did not exit"
cat "$OUT"
exit 1
}
exit 0

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]

0
regress/control-notify-guard.sh Executable file → Normal file
View File

View File

@@ -0,0 +1,253 @@
#!/bin/sh
# Exercise the broad non-mouse copy-mode command surface: navigation, search,
# jumps, state toggles, line/end-of-line copies, append, and pipe variants.
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
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
x()
{
$TMUX send-keys -t copy:0 -X "$@" || fail "copy command failed: $*"
}
wait_mode()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t copy:0 '#{pane_in_mode}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "pane_in_mode is '$got', expected '$want'"
}
fresh()
{
$TMUX copy-mode -t copy:0 || exit 1
wait_mode 1
x history-top
x start-of-line
}
select_text()
{
fresh
x begin-selection
x cursor-right
x cursor-right
}
$TMUX new-session -d -s copy -x40 -y8 \
"sh -c 'i=0; while [ \$i -lt 40 ]; do printf \" line-%02d needle (a[b]c) end\\n\" \$i; i=\$((i + 1)); done; printf \"\\nparagraph two needle\\n\\nparagraph three\\n\"; printf \"\\033]133;A\\007prompt-one\\n\\033]133;C\\007output-one\\n\\033]133;A\\007prompt-two\\n\\033]133;C\\007output-two\\n\"; exec sleep 100'" ||
exit 1
$TMUX set-option -g status off || exit 1
$TMUX set-option -g history-limit 200 || exit 1
i=0
while [ "$i" -lt 50 ]; do
history=$($TMUX display-message -p -t copy:0 '#{history_size}')
[ "$history" -ge 30 ] && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "history did not fill"
fresh
# Copy-mode-specific format callbacks expose the word, line, hyperlink and
# active search match at the cursor.
formats=$($TMUX display-message -p -t copy:0 \
'#{copy_cursor_word}|#{copy_cursor_line}|#{copy_cursor_hyperlink}|#{search_match}') ||
exit 1
[ -n "$formats" ] || fail "copy-mode cursor formats were empty"
# Cursor, viewport and paragraph commands. Each is asserted by successful
# dispatch and the final mode-liveness check; detailed word/selection geometry
# is covered by the existing copy-mode tests.
for command in back-to-indentation bottom-line top-line middle-line \
cursor-centre-vertical cursor-centre-horizontal end-of-line history-bottom \
history-top halfpage-down halfpage-up page-down page-up next-paragraph \
previous-paragraph next-matching-bracket previous-matching-bracket; do
x "$command"
done
x goto-line 15
x set-mark
x cursor-down
x jump-to-mark
# OSC 133 prompt and output markers support shell-integration navigation.
x history-top
x next-prompt
case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in
*prompt-one*) ;;
*) fail "next-prompt did not find prompt-one" ;;
esac
x next-prompt
x previous-prompt
case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in
*prompt-one*) ;;
*) fail "previous-prompt did not return to prompt-one" ;;
esac
x next-prompt -o
case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in
*output-one*) ;;
*) fail "next-prompt -o did not find output-one" ;;
esac
# Character jumps remember their direction and character for repeat/reverse.
x jump-forward e
x jump-again
x jump-reverse
x jump-backward e
x jump-to-forward e
x jump-to-backward e
# Direct, text and incremental searches cover both directions and the saved
# search used by again/reverse.
x search-forward needle
x search-again
x search-reverse
x search-backward needle
x search-forward-text needle
x search-backward-text needle
x search-forward-incremental '=needle'
x search-forward-incremental '+needle'
x search-backward-incremental '=needle'
x search-backward-incremental '-needle'
# Stateful display and behaviour toggles.
for command in rectangle-on rectangle-off rectangle-toggle \
line-numbers-on line-numbers-off line-numbers-toggle \
refresh-on refresh-now refresh-off refresh-toggle \
scroll-exit-on scroll-exit-off scroll-exit-toggle toggle-position; do
x "$command"
done
wait_mode 1
x cancel
wait_mode 0
# Line and end-of-line copy commands work without an explicit selection.
fresh
x select-line
x copy-selection-and-cancel
case "$($TMUX show-buffer)" in
*line-00*) ;;
*) fail "select-line did not select the current line" ;;
esac
fresh
x copy-line
[ -n "$($TMUX show-buffer)" ] || fail "copy-line produced an empty buffer"
x cancel
fresh
x copy-end-of-line
[ -n "$($TMUX show-buffer)" ] || fail "copy-end-of-line produced an empty buffer"
x cancel
# Append variants retain or close the mode as advertised.
$TMUX set-buffer -b append-buffer prefix || exit 1
select_text
x append-selection
appended=$($TMUX show-buffer -b append-buffer)
case "$appended" in
prefix*) ;;
*) fail "append-selection produced '$appended'" ;;
esac
x cancel
$TMUX set-buffer -b append-cancel-buffer prefix || exit 1
select_text
x append-selection-and-cancel
wait_mode 0
appended=$($TMUX show-buffer -b append-cancel-buffer)
case "$appended" in
prefix*) ;;
*) fail "append-selection-and-cancel produced '$appended'" ;;
esac
# The explicit cancel variants close copy mode.
fresh
x copy-line-and-cancel
wait_mode 0
fresh
x copy-end-of-line-and-cancel
wait_mode 0
select_text
x copy-selection-and-cancel
wait_mode 0
# Pipe wrappers have distinct command handlers. /dev/null keeps the test
# deterministic while exercising asynchronous pipe job creation and cleanup.
fresh
x copy-pipe-line 'cat >/dev/null'
x cancel
fresh
x copy-pipe-line-and-cancel 'cat >/dev/null'
wait_mode 0
fresh
x copy-pipe-end-of-line 'cat >/dev/null'
x cancel
fresh
x copy-pipe-end-of-line-and-cancel 'cat >/dev/null'
wait_mode 0
select_text
x copy-pipe-no-clear 'cat >/dev/null'
x pipe-no-clear 'cat >/dev/null'
x copy-pipe 'cat >/dev/null'
x pipe 'cat >/dev/null'
x cancel
select_text
x copy-pipe-and-cancel 'cat >/dev/null'
wait_mode 0
select_text
x pipe-and-cancel 'cat >/dev/null'
wait_mode 0
# Commands which cancel on downward movement have separate handlers.
fresh
x history-bottom
x bottom-line
x cursor-down-and-cancel
wait_mode 0
fresh
x history-bottom
x halfpage-down-and-cancel
wait_mode 0
fresh
x history-bottom
x page-down-and-cancel
wait_mode 0
fresh
x history-bottom
x bottom-line
x scroll-down-and-cancel
wait_mode 0
$TMUX has-session -t copy || fail "server died during copy-mode commands"
exit 0

View File

@@ -0,0 +1,81 @@
#!/bin/sh
# Changing an existing character selection to line mode must reset both ends
# to complete lines, including when the cursor subsequently crosses the fixed
# end. An invalid regular-expression search must also discard marks left by a
# previous successful search.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
}
trap cleanup 0 1 15
$TMUX new-session -d -x30 -y6 \
"printf 'alpha\nbeta\ngamma\ndelta\n'; exec sleep 100" || exit 1
$TMUX set-option -g window-size manual || exit 1
sleep 1
# A valid regular-expression search creates marks. Replacing it with an
# invalid expression, growing the mode, and scrolling must not leave an old
# mark array indexed with the new geometry (the original bug was an ASan heap
# overflow in this sequence).
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X search-forward beta || exit 1
[ "$($TMUX display-message -p '#{search_present}')" = 1 ] ||
fail "successful search did not create marks"
$TMUX send-keys -X search-forward '[' || exit 1
$TMUX resize-window -x40 -y8 || exit 1
$TMUX send-keys -X scroll-down || exit 1
[ "$($TMUX display-message -p '#{search_present}')" = 0 ] ||
fail "invalid regular expression left stale marks after resize"
$TMUX send-keys -X cancel || exit 1
# Select part of alpha through part of beta, then change the existing
# selection to line mode. Both lines, and no partial columns, must be copied.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X selection-mode line || exit 1
[ "$($TMUX display-message -p '#{selection_mode}')" = line ] ||
fail "selection did not change to line mode"
$TMUX send-keys -X copy-selection || exit 1
expected=$(printf 'alpha\nbeta')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "line-mode selection did not expand to complete lines"
# Start with the cursor above the fixed end so selection-mode itself takes the
# reverse-direction path, then extend it through another line.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -N2 -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-up || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X selection-mode line || exit 1
$TMUX send-keys -X cursor-up || exit 1
$TMUX send-keys -X copy-selection || exit 1
expected=$(printf 'alpha\nbeta\ngamma')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "reversed line-mode selection did not keep complete lines"
exit 0

0
regress/copy-mode-unicode-whitespace.sh Executable file → Normal file
View File

118
regress/find-window.sh Normal file
View File

@@ -0,0 +1,118 @@
#!/bin/sh
# Exercise find-window's content, name and title filters in every combination,
# glob/regex and case-insensitive suffixes, plus zoom restoration.
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
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
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
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_mode()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER display-message -p -t find:base.0 '#{pane_mode}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "pane mode is '$got', expected '$want'"
}
run_find()
{
label=$1
expected=$2
shift 2
$INNER find-window -t find:base.0 "$@" || fail "$label failed"
wait_mode tree-mode
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Fq "$expected" && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "$label did not show '$expected'"
$INNER send-keys -t find:base.0 q || exit 1
wait_mode ''
}
$INNER new-session -d -s find -n base -x80 -y24 \
"sh -c 'printf \"BaseBody\\n\"; exec sleep 100'" || exit 1
$INNER split-window -d -h -t find:base \
"sh -c 'printf \"OtherBody\\n\"; exec sleep 100'" || exit 1
$INNER new-window -d -t find: -n NameNeedle 'exec sleep 100' || exit 1
$INNER new-window -d -t find: -n body \
"sh -c 'printf \"BodyNeedle\\n\"; exec sleep 100'" || exit 1
$INNER new-window -d -t find: -n titled 'exec sleep 100' || exit 1
$INNER select-pane -t find:titled.0 -T TitleNeedle || 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 -x80 -y24 "$INNER attach -t find:base" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
# Wait until the body text is in the pane history before searching it.
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t find:body.0 | grep -Fq BodyNeedle && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "pane content was not ready"
# No flags means C+N+T. Then cover all three pairs and all three singletons.
run_find default NameNeedle NameNeedle
run_find content-name NameNeedle -C -N NameNeedle
run_find content-title titled -C -T TitleNeedle
run_find name-title titled -N -T TitleNeedle
run_find content-only body -C BodyNeedle
run_find name-only NameNeedle -N NameNeedle
run_find title-only titled -T TitleNeedle
# Glob case folding and all regular-expression suffix variants.
run_find insensitive NameNeedle -N -i nameneedle
run_find regex NameNeedle -N -r '^NameNeedle$'
run_find regex-insensitive NameNeedle -N -r -i '^nameneedle$'
# -Z temporarily zooms the target window and restores its prior state on exit.
$INNER find-window -Z -t find:base.0 -N NameNeedle || exit 1
wait_mode tree-mode
[ "$($INNER display-message -p -t find:base '#{window_zoomed_flag}')" = 1 ] ||
fail "find-window -Z did not zoom"
$INNER send-keys -t find:base.0 q || exit 1
wait_mode ''
[ "$($INNER display-message -p -t find:base '#{window_zoomed_flag}')" = 0 ] ||
fail "find-window -Z did not restore zoom"
exit 0

View File

@@ -71,7 +71,10 @@ cursor_very_visible
cursor_x
cursor_y
history_all_bytes
history_added
history_bytes
history_collected
history_generation
history_limit
history_size
host
@@ -103,6 +106,11 @@ pane_at_right
pane_at_top
pane_bg
pane_bottom
pane_command_duration
pane_command_end_time
pane_command_running
pane_command_start_time
pane_command_status
pane_current_command
pane_current_path
pane_dead
@@ -120,16 +128,21 @@ pane_index
pane_input_off
pane_key_mode
pane_last
pane_last_output_time
pane_last_prompt_time
pane_left
pane_marked
pane_marked_set
pane_modal_flag
pane_mode
pane_output_generation
pane_path
pane_pb_progress
pane_pb_state
pane_pid
pane_pipe
pane_pipe_pid
pane_private_modes
pane_right
pane_search_string
pane_start_command
@@ -141,6 +154,8 @@ pane_title
pane_top
pane_tty
pane_unseen_changes
pane_unzoomed_height
pane_unzoomed_width
pane_width
pane_x
pane_y
@@ -206,7 +221,10 @@ window_layout
window_linked
window_linked_sessions
window_linked_sessions_list
window_manual_height
window_manual_width
window_marked_flag
window_modal_pane
window_name
window_offset_x
window_offset_y

View File

@@ -218,11 +218,13 @@ $TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed"
# pane-exited when a pane's command exits with remain-on-exit off.
$TMUX set -g @x 0 || fail "set @x failed"
$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' ||
$TMUX set-hook -g pane-exited \
'set -gF @x "#{hook}:#{hook_pane}:#{hook_exit_status}:#{hook_exit_success}"' ||
fail "set-hook pane-exited failed"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') ||
fail "split-window true failed"
wait_for @x "pane-exited:$pane"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \
'trap "" HUP; exec </dev/null >/dev/null 2>&1; sleep 1; exit 42') ||
fail "split-window exit 42 failed"
wait_for @x "pane-exited:$pane:42:0"
$TMUX set-hook -gu pane-exited || fail "unset pane-exited failed"
# pane-died when a pane's command exits with remain-on-exit on.

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

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

@@ -0,0 +1,986 @@
#!/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 no v1 dump at all, 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
# no v1 dump to make. In particular 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" ''
# 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 there is still no v1 dump to make -
# 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" ''
# 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

126
regress/layout-set.sh Normal file
View File

@@ -0,0 +1,126 @@
#!/bin/sh
# Exercise every predefined layout, next/previous cycling, lookup and restore
# paths, one-pane early returns, minimum-size handling and floating panes.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
SEEN="$DIR/layouts"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
layout()
{
$TMUX display-message -p -t layouts:many '#{window_layout}'
}
check_layout()
{
name=$1
$TMUX select-layout -t layouts:many "$name" ||
fail "select-layout $name failed"
value=$(layout)
[ -n "$value" ] || fail "$name produced an empty layout"
if grep -Fxq "$value" "$SEEN"; then
fail "$name produced the same layout as an earlier preset"
fi
printf '%s\n' "$value" >>"$SEEN"
[ "$($TMUX list-panes -t layouts:many -F x | wc -l)" -eq 5 ] ||
fail "$name lost a pane"
[ "$($TMUX list-panes -t layouts:many -F '#{pane_floating_flag}' | \
grep -c '^1$')" -eq 1 ] || fail "$name did not preserve the floating pane"
}
$TMUX new-session -d -s layouts -n many -x100 -y40 'exec sleep 100' || exit 1
$TMUX set-option -g window-size manual || exit 1
$TMUX split-window -d -h -t layouts:many 'exec sleep 100' || exit 1
$TMUX split-window -d -v -t layouts:many 'exec sleep 100' || exit 1
$TMUX split-window -d -h -t layouts:many 'exec sleep 100' || exit 1
floating=$($TMUX new-pane -dP -F '#{pane_id}' -t layouts:many \
-x20 -y8 -X10 -Y5 'exec sleep 100') || exit 1
: >"$SEEN"
# Options exercise percentage and explicit secondary-size calculations.
$TMUX set-option -w -t layouts:many main-pane-height 30% || exit 1
$TMUX set-option -w -t layouts:many other-pane-height 12 || exit 1
$TMUX set-option -w -t layouts:many main-pane-width 35% || exit 1
$TMUX set-option -w -t layouts:many other-pane-width 20 || exit 1
for name in even-horizontal even-vertical main-horizontal \
main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do
check_layout "$name"
done
# Exact and unique-prefix lookup work; ambiguous and invalid names fail.
$TMUX select-layout -t layouts:many even-h || fail "unique layout prefix failed"
$TMUX select-layout -t layouts:many main >/dev/null 2>&1 &&
fail "ambiguous layout prefix succeeded"
$TMUX select-layout -t layouts:many no-such-layout >/dev/null 2>&1 &&
fail "invalid layout name succeeded"
# Cycle through all layouts in both directions, including wraparound. The
# aliases and select-layout flags share layout_set_next/previous.
first=$(layout)
i=0
while [ "$i" -lt 7 ]; do
$TMUX next-layout -t layouts:many || exit 1
i=$((i + 1))
done
[ "$(layout)" = "$first" ] || fail "next-layout did not wrap"
i=0
while [ "$i" -lt 7 ]; do
$TMUX previous-layout -t layouts:many || exit 1
i=$((i + 1))
done
[ "$(layout)" = "$first" ] || fail "previous-layout did not wrap"
$TMUX select-layout -t layouts:many -n || exit 1
$TMUX select-layout -t layouts:many -p || exit 1
# No argument reapplies the last preset; -o restores the previous layout; -E
# spreads the current cell without changing the pane set.
$TMUX kill-pane -t "$floating" || exit 1
$TMUX select-layout -t layouts:many tiled || exit 1
tiled=$(layout)
$TMUX select-layout -t layouts:many even-horizontal || exit 1
$TMUX select-layout -t layouts:many -o || exit 1
[ "$(layout)" = "$tiled" ] || fail "select-layout -o did not restore layout"
$TMUX select-layout -t layouts:many || exit 1
$TMUX select-layout -t layouts:many -E || exit 1
# Each arranger has a deliberate one-pane early return.
$TMUX new-window -d -t layouts: -n one 'exec sleep 100' || exit 1
one=$($TMUX display-message -p -t layouts:one '#{window_layout}')
for name in even-horizontal even-vertical main-horizontal \
main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do
$TMUX select-layout -t layouts:one "$name" || exit 1
[ "$($TMUX display-message -p -t layouts:one '#{window_layout}')" = "$one" ] ||
fail "$name changed a one-pane layout"
done
# Force the minimum-size paths without requiring a terminal client.
$TMUX resize-window -t layouts:many -x10 -y6 || exit 1
for name in even-horizontal even-vertical main-horizontal \
main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do
$TMUX select-layout -t layouts:many "$name" || exit 1
done
$TMUX has-session -t layouts || fail "server died during layout tests"
exit 0

46
regress/list-commands.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/sh
# Exercise list-commands for the full table, a single command, aliases, custom
# and empty formats, and lookup errors.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
# Keep a server alive across commands. Otherwise a STARTSERVER-only command
# may exit between invocations and leave a short socket-cleanup race.
$TMUX new-session -d -s list-commands 'exec sleep 100' || exit 1
all=$($TMUX list-commands) || exit 1
printf '%s\n' "$all" | grep -q '^attach-session (attach)' || exit 1
printf '%s\n' "$all" | grep -q '^list-commands (lscm)' || exit 1
one=$($TMUX list-commands -F \
'#{command_list_name}|#{command_list_alias}|#{command_list_usage}' \
list-commands) || exit 1
case "$one" in
'list-commands|lscm|'*) ;;
*) exit 1 ;;
esac
# Aliases resolve to their command, while an empty expansion prints no line.
[ "$($TMUX list-commands -F '#{command_list_name}' lscm)" = list-commands ] ||
exit 1
[ -z "$($TMUX list-commands -F '' list-commands)" ] || exit 1
$TMUX list-commands tmux-no-command-$$ >/dev/null 2>&1 && exit 1
$TMUX list-commands list >/dev/null 2>&1 && exit 1
exit 0

View File

@@ -1,201 +0,0 @@
#!/bin/sh
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"
TMPDIR=${TMPDIR:-/tmp}/tmux-local-pane.$$
mkdir -p "$TMPDIR" || exit 1
IN1="$TMPDIR/in1"
IN2="$TMPDIR/in2"
OUT1="$TMPDIR/out1"
OUT2="$TMPDIR/out2"
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup 0 1 15
fail()
{
echo "$1"
exit 1
}
wait_for()
{
file=$1
pattern=$2
i=0
while [ "$i" -lt 20 ]; do
if grep -q "$pattern" "$file"; then
return 0
fi
sleep 1
i=$((i + 1))
done
echo "missing '$pattern' in $file"
cat "$file"
return 1
}
send1()
{
printf '%s\n' "$*" >&3
}
send2()
{
printf '%s\n' "$*" >&4
}
check_fmt()
{
out=$($TMUX display-message -p -t "$1" "$2" 2>&1)
if [ "$out" != "$3" ]; then
echo "Format '$2' for '$1' wrong."
echo "Expected: '$3'"
echo "But got: '$out'"
exit 1
fi
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s active -x 80 -y 24 || exit 1
$TMUX split-window -d -h -t active:0 || exit 1
$TMUX split-window -d -v -t active:0.0 || exit 1
style=$($TMUX show-options -wgv pane-active-border-style) || exit 1
case "$style" in
*pane_local_active*themecyan*) ;;
*) fail "pane-active-border-style does not include local active colour" ;;
esac
p0=$($TMUX display-message -p -t active:0.0 '#{pane_id}') || exit 1
p1=$($TMUX display-message -p -t active:0.1 '#{pane_id}') || exit 1
p2=$($TMUX display-message -p -t active:0.2 '#{pane_id}') || exit 1
$TMUX select-pane -t "$p0" || exit 1
check_fmt active:0 '#{pane_id}' "$p0"
mkfifo "$IN1" "$IN2" || exit 1
: >"$OUT1"
: >"$OUT2"
$TMUX -C attach-session -t active <"$IN1" >"$OUT1" 2>&1 &
PID1=$!
exec 3>"$IN1"
send1 'display-message -p C1_READY'
wait_for "$OUT1" 'C1_READY' || exit 1
# One attached client cannot enter local mode.
send1 'select-pane -s'
send1 "select-pane -t $p1"
send1 'display-message -p "C1_SINGLE:#{pane_id}:#{pane_active}:#{pane_local_active}"'
wait_for "$OUT1" "C1_SINGLE:$p1:1:" || exit 1
check_fmt active:0 '#{pane_id}' "$p1"
send1 "select-pane -t $p0"
check_fmt active:0 '#{pane_id}' "$p0"
$TMUX -C attach-session -t active <"$IN2" >"$OUT2" 2>&1 &
PID2=$!
exec 4>"$IN2"
send2 'display-message -p C2_READY'
wait_for "$OUT2" 'C2_READY' || exit 1
# Both clients start in shared mode and see the window active pane.
send1 'display-message -p "C1_START:#{pane_id}:#{pane_active}:#{pane_local_active}"'
send2 'display-message -p "C2_START:#{pane_id}:#{pane_active}:#{pane_local_active}"'
wait_for "$OUT1" "C1_START:$p0:1:" || exit 1
wait_for "$OUT2" "C2_START:$p0:1:" || exit 1
# Client 1 enters local mode and selects a pane without changing shared state.
send1 'select-pane -s'
send1 "select-pane -t $p1"
send1 'display-message -p "C1_LOCAL:#{pane_id}:#{pane_active}:#{pane_local_active}:#{pane_local_mode}"'
send1 "display-message -p -t $p0 \"C1_LOCAL_INACTIVE:#{pane_id}:#{pane_active}:#{pane_local_active}\""
wait_for "$OUT1" "C1_LOCAL:$p1:1:1:1" || exit 1
wait_for "$OUT1" "C1_LOCAL_INACTIVE:$p0:0:0" || exit 1
check_fmt active:0 '#{pane_id}' "$p0"
send2 'display-message -p "C2_SHARED:#{pane_id}:#{pane_active}:#{pane_local_active}"'
wait_for "$OUT2" "C2_SHARED:$p0:1:" || exit 1
# Client 2 can keep an independent local pane in the same window.
send2 'select-pane -s'
send2 "select-pane -t $p2"
send2 'display-message -p "C2_LOCAL:#{pane_id}:#{pane_active}:#{pane_local_active}"'
wait_for "$OUT2" "C2_LOCAL:$p2:1:1" || exit 1
send1 'display-message -p "C1_STILL:#{pane_id}:#{pane_active}:#{pane_local_active}"'
wait_for "$OUT1" "C1_STILL:$p1:1:1" || exit 1
# Local last-pane history is independent of the shared window history.
send1 "select-pane -t $p2"
send1 'last-pane'
send1 'display-message -p "C1_LAST:#{pane_id}:#{pane_last}"'
wait_for "$OUT1" "C1_LAST:$p1:0" || exit 1
check_fmt active:0 '#{pane_id}' "$p0"
# Switching back to shared mode immediately follows window->active.
send1 'select-pane -S'
send1 'display-message -p "C1_SHARED_AGAIN:#{pane_id}:#{pane_active}"'
wait_for "$OUT1" "C1_SHARED_AGAIN:$p0:1" || exit 1
send1 "select-pane -t $p1"
check_fmt active:0 '#{pane_id}' "$p1"
send2 'display-message -p "C2_STILL_LOCAL:#{pane_id}:#{pane_active}"'
wait_for "$OUT2" "C2_STILL_LOCAL:$p2:1" || exit 1
# Local selection survives switching away and back to the window.
send2 'new-window -n other'
send2 'previous-window'
send2 'display-message -p "C2_AFTER_WINDOW_SWITCH:#{pane_id}:#{pane_active}"'
wait_for "$OUT2" "C2_AFTER_WINDOW_SWITCH:$p2:1" || exit 1
# Removing a locally active pane recovers to the shared active pane.
$TMUX kill-pane -t "$p2" || exit 1
send2 'display-message -p "C2_AFTER_KILL:#{pane_id}:#{pane_active}"'
wait_for "$OUT2" "C2_AFTER_KILL:$p1:1" || exit 1
# Local selection of a floating pane does not raise it unless requested.
f1=$($TMUX new-pane -dPF '#{pane_id}' -x 20 -y 6 -X 8 -Y 3 \
-t active:0 'sleep 100') || exit 1
f2=$($TMUX new-pane -dPF '#{pane_id}' -x 20 -y 6 -X 12 -Y 5 \
-t active:0 'sleep 100') || exit 1
check_fmt "$f1" '#{pane_z}' 1
check_fmt "$f2" '#{pane_z}' 0
send1 'select-pane -s'
send1 "select-pane -t $f1"
send1 "display-message -p -t $f1 \"C1_FLOAT_LOCAL:#{pane_id}:#{pane_active}:#{pane_z}\""
wait_for "$OUT1" "C1_FLOAT_LOCAL:$f1:1:1" || exit 1
check_fmt "$f1" '#{pane_z}' 1
send1 "select-pane -M -t $f1"
send1 "display-message -p -t $f1 \"C1_FLOAT_BORDER:#{pane_id}:#{pane_active}:#{pane_z}\""
wait_for "$OUT1" "C1_FLOAT_BORDER:$f1:1:1" || exit 1
check_fmt "$f1" '#{pane_z}' 1
send1 "move-pane -P front -t $f1"
send1 "display-message -p -t $f1 \"C1_FLOAT_RAISE:#{pane_z}\""
wait_for "$OUT1" "C1_FLOAT_RAISE:0" || exit 1
check_fmt "$f1" '#{pane_z}' 0
$TMUX kill-pane -t "$f1" || exit 1
$TMUX kill-pane -t "$f2" || exit 1
# When only one client remains, local mode is cleared.
send1 'select-pane -s'
send1 "select-pane -t $p0"
send1 'display-message -p "C1_LOCAL_BEFORE_DETACH:#{pane_id}:#{pane_local_active}"'
wait_for "$OUT1" "C1_LOCAL_BEFORE_DETACH:$p0:1" || exit 1
kill "$PID2" 2>/dev/null
wait "$PID2" 2>/dev/null
send1 'display-message -p "C1_AFTER_DETACH:#{pane_id}:#{pane_local_active}"'
wait_for "$OUT1" "C1_AFTER_DETACH:$p1:" || exit 1
kill "$PID1" 2>/dev/null
wait "$PID1" 2>/dev/null
$TMUX display-message -p alive >/dev/null 2>&1 || fail "server died"
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'

0
regress/mode-kill.sh Executable file → Normal file
View File

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

@@ -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

View File

@@ -0,0 +1,165 @@
#!/bin/sh
# Cover prompt word movement in emacs and vi modes, ambiguous inline command
# completion, and the show/clear-prompt-history command surface.
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
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
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
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_result()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER show-option -gqv @result 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "prompt result is '$got', expected '$want'"
}
bind_prompt()
{
initial=$1
$INNER bind-key -n M-r command-prompt -I "$initial" -p '(word)' \
"set-option -g @result '%%'" || exit 1
}
run_prompt()
{
want=$1
shift
$INNER set-option -g @result sentinel || exit 1
$OUTER send-keys M-r || exit 1
sleep 0.2
$OUTER send-keys "$@" || exit 1
$OUTER send-keys Enter || exit 1
wait_result "$want"
}
run_vi_prompt()
{
want=$1
shift
$INNER set-option -g @result sentinel || exit 1
$OUTER send-keys M-r || exit 1
sleep 0.2
# Send Escape separately so the terminal's escape-time handling does not
# combine it with the first vi command as a Meta key.
$OUTER send-keys Escape || exit 1
# The inner client must see Escape as a complete key, not the prefix of a
# Meta sequence (the default escape-time is 500 milliseconds).
sleep 0.7
$OUTER send-keys "$@" || exit 1
$OUTER send-keys Enter || exit 1
wait_result "$want"
}
$INNER new-session -d -s prompt -x80 -y24 'exec sleep 100' || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t prompt" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
# Emacs Meta-f stops after the first word; Meta-b returns to the start of the
# previous word. Inserting a marker makes the cursor position observable.
$INNER set-option -g status-keys emacs || exit 1
bind_prompt 'one two'
run_prompt 'oneX two' Home M-f X
bind_prompt 'one two'
run_prompt 'one Xtwo' M-b X
# Vi translation and the distinct separator-aware and WORD motions.
$INNER set-option -g status-keys vi || exit 1
bind_prompt 'one-two three'
run_vi_prompt 'one-two Xthree' b i X
bind_prompt 'one-two three'
run_vi_prompt 'one-two Xthree' B i X
bind_prompt 'one-two three'
run_vi_prompt 'oneX-two three' 0 w i X
bind_prompt 'one-two three'
run_vi_prompt 'one-two Xthree' 0 W i X
bind_prompt 'one-two three'
run_vi_prompt 'oneX-two three' 0 e a X
bind_prompt 'one-two three'
run_vi_prompt 'one-twoX three' 0 E a X
# "show-" has several command matches and no longer common prefix. Tab keeps
# the input and draws the sorted candidates inline.
$INNER set-option -g status-keys emacs || exit 1
bind_prompt ''
$OUTER send-keys M-r || exit 1
sleep 0.2
$OUTER send-keys -l 'show-' || exit 1
$OUTER send-keys Tab || exit 1
sleep 0.2
captured=$(capture)
printf '%s\n' "$captured" | grep -Fq 'show-buffer' ||
fail "ambiguous completion list was not drawn"
printf '%s\n' "$captured" | grep -Fq 'show-environment' ||
fail "ambiguous completion list was incomplete"
$OUTER send-keys Escape || exit 1
# Add entries to both history rings through real prompts.
bind_prompt 'history-command'
run_prompt 'history-command'
$INNER bind-key -n M-s command-prompt -T search -I history-search \
-p '(search-history)' "set-option -g @result '%%'" || exit 1
$OUTER send-keys M-s || exit 1
$OUTER send-keys Enter || exit 1
wait_result history-search
command_history=$($INNER show-prompt-history -T command) || exit 1
printf '%s\n' "$command_history" | grep -Fq 'history-command' ||
fail "command history entry missing"
search_history=$($INNER show-prompt-history -T search) || exit 1
printf '%s\n' "$search_history" | grep -Fq 'history-search' ||
fail "search history entry missing"
all_history=$($INNER show-prompt-history) || exit 1
printf '%s\n' "$all_history" | grep -Fq 'History for command:' || exit 1
printf '%s\n' "$all_history" | grep -Fq 'History for search:' || exit 1
$INNER show-prompt-history -T invalid >/dev/null 2>&1 &&
fail "invalid show history type succeeded"
$INNER clear-prompt-history -T invalid >/dev/null 2>&1 &&
fail "invalid clear history type succeeded"
$INNER clear-prompt-history -T command || exit 1
$INNER show-prompt-history -T command | grep -Fq history-command &&
fail "type-specific history clear failed"
$INNER clear-prompt-history || exit 1
$INNER show-prompt-history | grep -Fq history-search &&
fail "all-history clear failed"
exit 0

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,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

View File

@@ -1,14 +0,0 @@
POP00 POPUP
POP01
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 ab┌──────────────────────┐fghij
POP01 ab│POPUP │fghij
POP02 ab│ │fghij
POP03 ab│ │fghij
POP04 ab│ │fghij
POP05 ab│ │fghij
POP06 ab│ │fghij
POP07 ab└──────────────────────┘fghij
POP08 abcdefghij │POP08 abcdefghij
POP09 abcdefghij │POP09 abcdefghij
POP10 abcdefghij │POP10 abcdefghij
POP11 abcdefghij │POP11 abcdefghij
POP12 abcdefghij │POP12 abcdefghij

View File

@@ -0,0 +1,96 @@
#!/bin/sh
# Auto-hide scrollbars appear on activity or pointer hover, then disappear
# after pane-scrollbars-timeout. Compare captures of the same copy-mode view so
# only the overlay scrollbar changes.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUTER="$TEST_TMUX -LtestA$$ -f/dev/null"
INNER="$TEST_TMUX -LtestB$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
HIDDEN=$DIR/hidden
VISIBLE=$DIR/visible
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
capture()
{
$OUTER capture-pane -pe -t outer:0.0 >"$1" || exit 1
}
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
$INNER list-clients 2>/dev/null | grep -q . && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
$INNER new-session -d -s inner -x40 -y12 \
"sh -c 'seq 50; 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 mouse on || exit 1
$INNER set-window-option pane-scrollbars auto-hide || exit 1
$INNER set-window-option pane-scrollbars-timeout 3000 || exit 1
$INNER set-window-option pane-scrollbars-style \
'bg=colour196,fg=colour231,width=1,pad=0' || exit 1
$OUTER new-session -d -s outer -x40 -y12 "$INNER attach -t inner" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
wait_for_client
# Enter copy mode and choose a stable view. The initial show timer is allowed
# to expire before taking the hidden reference capture.
$INNER copy-mode -H || exit 1
$INNER send-keys -X history-top || exit 1
sleep 3.5
capture "$HIDDEN"
# A copy-mode page movement shows the overlay and starts its timer. Capture
# that view immediately, then again after the timeout without moving it.
$INNER send-keys -X page-down || exit 1
sleep 0.1
capture "$VISIBLE"
sleep 3.5
capture "$AFTER"
cmp -s "$VISIBLE" "$AFTER" && fail "scrollbar did not hide after timeout"
# Use the post-movement hidden scene as the reference for pointer hover. SGR
# mouse column 40 is the right-hand scrollbar; moving to column 5 restarts the
# timer without changing the copy-mode view.
cp "$AFTER" "$HIDDEN"
hover=$(printf '\033[<35;40;5M')
$OUTER send-keys -l "$hover" || exit 1
sleep 0.1
capture "$VISIBLE"
cmp -s "$HIDDEN" "$VISIBLE" && fail "scrollbar did not appear on hover"
away=$(printf '\033[<35;5;5M')
$OUTER send-keys -l "$away" || exit 1
sleep 3.5
capture "$AFTER"
cmp -s "$HIDDEN" "$AFTER" || fail "scrollbar did not hide after hover"
exit 0

112
regress/server-access.sh Normal file
View File

@@ -0,0 +1,112 @@
#!/bin/sh
# Exercise the unprivileged server-access surface: user and group ACL entries,
# read-only/writable transitions, listing, removal, implied addition, and
# command errors. Actual connections from another UID require privileges and
# deliberately do not form part of this test.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
OUT=$(mktemp) || exit 1
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -f "$OUT"
}
trap cleanup 0 1 15
expect_fail()
{
message=$1
shift
if $TMUX "$@" >"$OUT" 2>&1; then
fail "$message: command unexpectedly succeeded"
fi
}
assert_entry()
{
entry=$1
$TMUX server-access -l >"$OUT" || exit 1
grep -Fxq "$entry" "$OUT" ||
fail "missing ACL entry '$entry'"
}
assert_no_entry()
{
entry=$1
$TMUX server-access -l >"$OUT" || exit 1
if grep -Fxq "$entry" "$OUT"; then
fail "unexpected ACL entry '$entry'"
fi
}
owner_uid=$(id -u)
owner_name=$(id -un)
# nobody is the non-owner test identity. Derive its primary group rather than
# assuming whether the system calls it nobody, nogroup, or something else.
acl_user=nobody
acl_group=$(id -gn "$acl_user" 2>/dev/null) ||
fail "the nobody account is unavailable"
[ "$owner_name" != "$acl_user" ] || fail "test cannot run as nobody"
$TMUX new-session -d -s access 'exec sleep 100' || exit 1
# The owner is installed as writable at server startup (root is intentionally
# omitted from display output).
if [ "$owner_uid" != 0 ]; then
assert_entry "$owner_name (U,W)"
fi
# User ACL lifecycle, including duplicate and missing-entry errors.
$TMUX server-access -a "$acl_user" || exit 1
assert_entry "$acl_user (U,W)"
expect_fail "duplicate user addition" server-access -a "$acl_user"
$TMUX server-access -r "$acl_user" || exit 1
assert_entry "$acl_user (U,R)"
$TMUX server-access -w "$acl_user" || exit 1
assert_entry "$acl_user (U,W)"
$TMUX server-access -d "$acl_user" || exit 1
assert_no_entry "$acl_user (U,W)"
expect_fail "removing absent user" server-access -d "$acl_user"
# -r and -w imply -a when an entry is absent.
$TMUX server-access -r "$acl_user" || exit 1
assert_entry "$acl_user (U,R)"
$TMUX server-access -d "$acl_user" || exit 1
# The same lifecycle for a group exercises the separate ACL key space.
$TMUX server-access -g -a -r "$acl_group" || exit 1
assert_entry "$acl_group (G,R)"
$TMUX server-access -g -w "$acl_group" || exit 1
assert_entry "$acl_group (G,W)"
expect_fail "duplicate group addition" server-access -g -a "$acl_group"
$TMUX server-access -g -d "$acl_group" || exit 1
assert_no_entry "$acl_group (G,W)"
expect_fail "removing absent group" server-access -g -d "$acl_group"
# Parser and lookup errors, plus the immutable owner/root entries.
expect_fail "missing ACL subject" server-access -a
expect_fail "unknown user" server-access -a "tmux-no-user-$$"
expect_fail "unknown group" server-access -g -a "tmux-no-group-$$"
expect_fail "conflicting add/delete" server-access -a -d "$acl_user"
expect_fail "conflicting read/write" server-access -r -w "$acl_user"
expect_fail "changing server owner" server-access -r "$owner_name"
if [ "$owner_name" != root ]; then
expect_fail "changing root access" server-access -a root
fi
$TMUX has-session -t access || fail "server exited during ACL updates"
exit 0

0
regress/server-socket-error.sh Executable file → Normal file
View File

0
regress/set-hook-E.sh Executable file → Normal file
View File

203
regress/switch-mode.sh Normal file
View File

@@ -0,0 +1,203 @@
#!/bin/sh
# Exercise switch-mode's session and window lists with a real attached client:
# fuzzy filtering, movement and selection, the default and custom commands,
# no-match cancellation, resize, zoom restoration, and -k mode teardown.
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
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
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
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_capture()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Fq "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "timed out waiting for '$marker'"
}
wait_count()
{
marker=$1
want=$2
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
count=$(printf '%s\n' "$captured" | grep -Fc "$marker")
[ "$count" -eq "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "found $count '$marker' rows, expected $want"
}
wait_format()
{
target=$1
format=$2
want=$3
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER display-message -p -t "$target" "$format" \
2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "$target format $format is '$got', expected '$want'"
}
wait_client_session()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER list-clients -F '#{client_session}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "client session is '$got', expected '$want'"
}
# Three alphabetically ordered sessions. alpha:main has two panes so -Z can be
# observed; the two pick-* windows make a deterministic two-row window filter.
$INNER new-session -d -s alpha -n main -x60 -y15 'exec sleep 100' || exit 1
$INNER split-window -d -h -t alpha:main 'exec sleep 100' || exit 1
$INNER new-window -d -t alpha: -n pick-one 'exec sleep 100' || exit 1
$INNER new-session -d -s bravo -n pick-two -x60 -y15 \
'exec sleep 100' || exit 1
$INNER new-session -d -s charlie -n last -x60 -y15 \
'exec sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER select-window -t alpha:main || exit 1
$OUTER new-session -d -s outer -x60 -y15 "$INNER attach -t alpha" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
wait_client_session alpha
# Session rows are sorted alpha, bravo, charlie. Up from the first row wraps to
# charlie; Enter runs a custom command with the selected session target.
$INNER set-option -g @picked '' || exit 1
$INNER switch-mode -s -t alpha:main.0 -F 'SESSION #{session_name}' \
"set-option -g @picked '%%'" || exit 1
wait_count 'SESSION ' 3
$INNER send-keys -t alpha:main.0 Up Enter || exit 1
wait_format alpha:main.0 '#{@picked}' '=charlie:'
wait_format alpha:main.0 '#{pane_in_mode}' 0
# Fuzzy filtering keeps only bravo for "brv". The default command switches the
# real client to that session and closes the mode.
$INNER switch-mode -s -t alpha:main.0 -F 'SESSION #{session_name}' || exit 1
wait_count 'SESSION ' 3
$INNER send-keys -t alpha:main.0 -l brv || exit 1
wait_count 'SESSION ' 1
printf '%s\n' "$captured" | grep -Fq 'SESSION bravo' ||
fail "fuzzy session filter did not select bravo"
$INNER send-keys -t alpha:main.0 Enter || exit 1
wait_client_session bravo
client=$($INNER list-clients -F '#{client_name}')
$INNER switch-client -c "$client" -t alpha || exit 1
wait_client_session alpha
# Window mode sees pick-one then pick-two for the "pick" filter. Down selects
# the second row, and the custom command receives its window target.
$INNER set-option -g @picked '' || exit 1
$INNER switch-mode -w -t alpha:main.0 -F \
'WINDOW #{session_name}:#{window_name}' \
"set-option -g @picked '%%'" || exit 1
wait_capture 'WINDOW '
$INNER send-keys -t alpha:main.0 -l pick || exit 1
wait_count 'WINDOW ' 2
$INNER send-keys -t alpha:main.0 Down Enter || exit 1
picked=$($INNER show-option -gv @picked)
case "$picked" in
=bravo:0.) ;;
*) fail "window selection produced '$picked', expected '=bravo:0.'" ;;
esac
# Navigation with an empty result set must be harmless, and Escape cancels
# without running the custom command.
$INNER set-option -g @picked unchanged || exit 1
$INNER switch-mode -s -t alpha:main.0 -F 'NONE #{session_name}' \
"set-option -g @picked '%%'" || exit 1
wait_capture 'NONE '
$INNER send-keys -t alpha:main.0 -l no-such-session || exit 1
wait_count 'NONE ' 0
$INNER send-keys -t alpha:main.0 Up Down PPage NPage Home End || exit 1
wait_format alpha:main.0 '#{pane_mode}' switch-mode
$INNER send-keys -t alpha:main.0 Escape || exit 1
wait_format alpha:main.0 '#{pane_in_mode}' 0
[ "$($INNER show-option -gv @picked)" = unchanged ] ||
fail "cancelled switch-mode ran its command"
# -Z temporarily zooms an unzoomed window. Resizing while the mode is open
# rebuilds and redraws it; exiting restores the original unzoomed state.
$INNER switch-mode -Zs -t alpha:main.0 -F 'RESIZE #{session_name}' || exit 1
wait_capture 'RESIZE alpha'
wait_format alpha:main.0 '#{window_zoomed_flag}' 1
$INNER resize-window -t alpha:main -x50 -y12 || exit 1
wait_format alpha:main.0 '#{window_width}x#{window_height}' 50x12
wait_format alpha:main.0 '#{pane_mode}' switch-mode
$INNER send-keys -t alpha:main.0 Escape || exit 1
wait_format alpha:main.0 '#{window_zoomed_flag}' 0
# If the window was already zoomed, leaving -Z mode must keep it zoomed.
$INNER resize-pane -Z -t alpha:main.0 || exit 1
$INNER switch-mode -Zs -t alpha:main.0 -F 'ZOOMED #{session_name}' || exit 1
wait_capture 'ZOOMED alpha'
$INNER send-keys -t alpha:main.0 Escape || exit 1
wait_format alpha:main.0 '#{window_zoomed_flag}' 1
$INNER resize-pane -Z -t alpha:main.0 || exit 1
# -k uses the shared mode teardown flag. Put it on a disposable pane and make
# sure exiting the mode removes that pane.
kill_pane=$($INNER split-window -d -P -F '#{pane_id}' -t alpha:main \
'exec sleep 100') || exit 1
$INNER switch-mode -ks -t "$kill_pane" -F 'KILL #{session_name}' || exit 1
wait_format "$kill_pane" '#{pane_mode}' switch-mode
$INNER send-keys -t "$kill_pane" Escape || exit 1
i=0
while [ "$i" -lt 50 ]; do
$INNER list-panes -s -t alpha -F '#{pane_id}' 2>/dev/null | \
grep -q -x "$kill_pane" || break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "switch-mode -k did not kill its pane"
exit 0

View File

@@ -0,0 +1,160 @@
#!/bin/sh
# A synchronized update elsewhere in the pane must not hide a stationary
# cursor while tmux waits for the end of the update.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
if [ "$#" -eq 0 ]; then
TEST_TMUX="$TEST_TMUX" sh "$0" pane || exit 1
TEST_TMUX="$TEST_TMUX" sh "$0" prompt || exit 1
exit 0
fi
case $1 in
pane|prompt) MODE=$1 ;;
*) echo "usage: $0 [pane|prompt]" >&2; exit 1 ;;
esac
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
CLIENT_BYTES=$DIR/client-bytes
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
ASSERT=$DIR/assert-cursor.pl
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
$INNER list-clients -F '#{client_termfeatures}' 2>/dev/null |
grep -q 'sync' && return 0
sleep 0.1
i=$((i + 1))
done
fail "sync-capable client did not attach"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
my $control = $ENV{CONTROL};
(my $dir = $control) =~ s{/[^/]+$}{};
# Draw a prompt and leave its cursor stationary on the last line.
syswrite STDOUT, "\e[2J\e[HWorking .\e[24;1H> ";
open my $ready, '>', "$dir/ready" or die "$dir/ready: $!\n";
close $ready;
while (!-e $control) {
select undef, undef, undef, 0.01;
}
# Deliberately split BSU, content, and ESU across pane reads. This mirrors an
# animation producer which begins a frame before it has finished rendering it.
for my $spinner (qw(| / - \\)) {
syswrite STDOUT, "\e[?2026h";
select undef, undef, undef, 0.05;
syswrite STDOUT, "\e[1;9H$spinner\e[24;3H";
select undef, undef, undef, 0.01;
syswrite STDOUT, "\e[?2026l";
select undef, undef, undef, 0.15;
}
select undef, undef, undef, 5;
PERL
cat >"$ASSERT" <<'PERL'
use strict;
use warnings;
my $path = shift;
open my $fh, '<:raw', $path or die "$path: $!\n";
local $/;
my $bytes = <$fh>;
my ($bsu, $esu, $hide, $show) =
("\e[?2026h", "\e[?2026l", "\e[?25l", "\e[?25h");
my ($sync, $outside, $hides, $shows) = (0, 0, 0, 0);
for (my $i = 0; $i < length($bytes);) {
if (substr($bytes, $i, length($bsu)) eq $bsu) {
$sync++;
$i += length($bsu);
next;
}
if (substr($bytes, $i, length($esu)) eq $esu) {
$sync-- if $sync;
$i += length($esu);
next;
}
if (substr($bytes, $i, length($hide)) eq $hide) {
$hides++;
$outside++ unless $sync;
$i += length($hide);
next;
}
if (substr($bytes, $i, length($show)) eq $show) {
$shows++;
$outside++ unless $sync;
$i += length($show);
next;
}
$i++;
}
printf "%s cursor hides: %d; cursor shows: %d; changes outside sync: %d\n",
$ENV{MODE}, $hides, $shows, $outside;
die "stationary cursor changed outside synchronized output\n" if $outside;
PERL
$INNER new-session -d -s inner -x 80 -y 24 \
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
$OUTER new-session -d -s outer -x 80 -y 24 \
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
wait_for_client
if [ "$MODE" = prompt ]; then
$OUTER send-keys -t outer:0.0 C-b : || exit 1
sleep 0.2
fi
i=0
while [ "$i" -lt 50 ] && [ ! -e "$DIR/ready" ]; do
sleep 0.1
i=$((i + 1))
done
[ -e "$DIR/ready" ] || fail "application emitter did not become ready"
$OUTER pipe-pane -O -t outer:0.0 "cat >'$CLIENT_BYTES'" || exit 1
: >"$CONTROL"
sleep 2
$OUTER pipe-pane -t outer:0.0 || exit 1
MODE=$MODE perl "$ASSERT" "$CLIENT_BYTES"

0
regress/tab-cell-background.sh Executable file → Normal file
View File

View File

@@ -97,6 +97,10 @@ check "p:.{top-left}" "%0"
check_ok select-pane -t p:0.%0
check "p:0.+" "1" '#{pane_index}' # next pane
check "p:0.-" "3" '#{pane_index}' # previous pane (wraps)
check_fail "can't find pane: +0" "p:0.+0"
check_fail "can't find pane: +foo" "p:0.+foo"
check_fail "can't find pane: -0" "p:0.-0"
check_fail "can't find pane: -foo" "p:0.-foo"
# --- last pane (!) --------------------------------------------------------
check_ok select-pane -t p:0.%2

View File

@@ -154,6 +154,13 @@ check "alpha:{previous}" "3"
check "alpha:!" "2" # last window
check "alpha:{last}" "2"
# Malformed relative offsets must fail rather than silently selecting the
# current window.
check_fail "can't find window: +0" "alpha:+0"
check_fail "can't find window: +foo" "alpha:+foo"
check_fail "can't find window: -0" "alpha:-0"
check_fail "can't find window: -foo" "alpha:-foo"
# --- combined and empty forms ---------------------------------------------
#
# Empty targets use the current pane from TMUX_PANE when there is no client.

View File

@@ -0,0 +1,58 @@
#!/bin/sh
# The utf8 terminal feature must mark an attached client as UTF-8 even when
# neither its environment nor -u does so. It must also be reported by the
# client terminal-feature format lookup.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0 1 15
cleanup
$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1
$INNER set-option -g status off || exit 1
# A clean C-locale environment avoids the ordinary UTF-8 detection paths.
# -T utf8 is therefore solely responsible for setting client_utf8.
client_command="env -i PATH=/bin:/usr/bin TERM=screen LC_ALL=C $TEST_TMUX -T utf8 -LtestI$$ -f/dev/null attach-session -t inner"
$OUTER new-session -d -s outer -x40 -y10 "$client_command" || exit 1
$OUTER set-option -g status off || exit 1
i=0
while [ "$i" -lt 50 ]; do
client=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
utf8=$($INNER list-clients -F '#{client_utf8}' 2>/dev/null)
[ -n "$client" ] && [ "$utf8" = 1 ] && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "utf8 terminal feature did not set client_utf8"
features=$($INNER list-clients -F '#{client_termfeatures}')
case ",$features," in
*,utf8,*) ;;
*) fail "utf8 is missing from client_termfeatures: $features" ;;
esac
present=$($INNER display-message -c "$client" -p '#{I/f:utf8}')
[ "$present" = 1 ] || fail "utf8 terminal feature lookup returned '$present'"
exit 0

0
regress/theme-report.sh Executable file → Normal file
View File

View File

@@ -8,7 +8,7 @@
# This exercises:
# - new-window placement: next free index, explicit index, index in use with
# and without -k, -a (after) and -b (before) insertion with shuffling, and
# -S selecting an existing window by name instead of creating;
# -S selecting an existing window by name or target instead of creating;
# - move-window to a free index, to an occupied index with and without -k,
# -a insertion and -r renumbering (including base-index);
# - renumber-windows closing gaps;
@@ -140,6 +140,36 @@ check_ok new-window -S -t W: -n w3
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '5:w3'
# -S with a target-window that already exists selects it directly, ignoring
# -n (with -d it does not switch).
check_ok select-window -t W:0
check_ok new-window -S -t W:9 -n ignored
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '9:w9k'
check_ok select-window -t W:0
check_ok new-window -S -d -t W:9 -n ignored
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '0:wB'
# -S with a target-window that does not yet exist still creates it normally.
check_ok new-window -S -t W:20 -n w20
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k 20:w20'
check_fmt 'W:' '#{window_index}:#{window_name}' '20:w20'
check_ok kill-window -t W:20
# -S also works with a relative target-window (+N): selects the window at
# that offset if it exists, or creates it there if not.
check_ok select-window -t W:0
check_ok new-window -S -t W:+1
check_fmt 'W:' '#{window_index}:#{window_name}' '1:w0'
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_ok select-window -t W:0
check_ok new-window -S -t W:+6 -n rel6
check_fmt 'W:' '#{window_index}:#{window_name}' '6:rel6'
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 6:rel6 9:w9k'
check_ok kill-window -t W:6
# Clean up to a known arrangement.
check_ok kill-window -t W:wB
check_ok kill-window -t W:wA

View File

@@ -348,10 +348,10 @@ recalculate_size_skip_client(struct client *loop, __unused int type,
* is not the current window - this is used for aggressive-resize.
* Otherwise skip any session that doesn't contain the window.
*/
if (active_get_effective_window(loop, loop->session) == NULL)
if (loop->session->curw == NULL)
return (1);
if (current)
return (active_get_effective_window(loop, loop->session) != w);
return (loop->session->curw->window != w);
return (session_has(loop->session, w) == 0);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-redraw.c,v 1.157 2026/07/24 08:49:23 nicm Exp $ */
/* $OpenBSD: screen-redraw.c,v 1.159 2026/09/09 08:31:42 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -33,7 +33,7 @@
* this is done at various points, such as when a pane is moved or resized. The
* scene only includes the part of the client used for the window: panes, pane
* status lines, borders, scrollbars, and any area outside the window. The
* client status line and overlay are not included.
* client status line is not included.
*
* A scene is made from spans. A span is a horizontal run of cells on one
* visible line that can be drawn in the same way. Each span has a type, for
@@ -93,7 +93,6 @@ enum redraw_span_type {
#define REDRAW_PANE_SCROLLBAR 0x20
#define REDRAW_STATUS 0x40
#define REDRAW_MENU 0x80
#define REDRAW_OVERLAY 0x100
/* Draw everything. */
#define REDRAW_ALL 0x7fffffff
@@ -264,8 +263,6 @@ redraw_flags_to_string(int flags)
strlcat(s, "scrollbar ", sizeof s);
if (flags & REDRAW_MENU)
strlcat(s, "menu ", sizeof s);
if (flags & REDRAW_OVERLAY)
strlcat(s, "overlay ", sizeof s);
if (REDRAW_IS_ALL(flags))
strlcat(s, "all ", sizeof s);
if (*s != '\0')
@@ -295,7 +292,7 @@ static void
redraw_set_context(struct client *c, struct redraw_build_ctx *bctx)
{
struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
struct window *w = s->curw->window;
memset(bctx, 0, sizeof *bctx);
bctx->c = c;
@@ -713,7 +710,7 @@ redraw_mark_pane_borders(struct redraw_build_ctx *bctx, struct window_pane *wp,
} else {
mark_right = (right <= (int)bctx->w->sx);
mark_bottom = (bottom <= (int)bctx->w->sy);
if (pane_status == PANE_STATUS_TOP)
if (pane_status == PANE_STATUS_TOP && bottom < (int)bctx->w->sy)
mark_bottom = 0;
else if (pane_status == PANE_STATUS_BOTTOM)
mark_top = 0;
@@ -969,7 +966,7 @@ static struct redraw_scene *
redraw_make_scene(struct client *c)
{
struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
struct window *w = s->curw->window;
struct redraw_build_ctx bctx;
struct redraw_scene *scene;
struct redraw_build_cell *bc, *last;
@@ -1079,7 +1076,7 @@ static struct redraw_scene *
redraw_get_scene(struct client *c)
{
struct redraw_scene *scene = c->redraw_scene;
struct window *w = active_get_effective_window(c, c->session);
struct window *w = c->session->curw->window;
const char *reason = NULL;
u_int ox, oy, sx, sy;
@@ -1117,7 +1114,7 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
struct tty_style_ctx style_ctx;
u_int px, py;
tty_default_colours(&defaults, wp, dctx->active, &style_ctx.dim);
tty_default_colours(&defaults, wp, &style_ctx.dim);
style_ctx.defaults = &defaults;
style_ctx.palette = &wp->palette;
style_ctx.hyperlinks = s->hyperlinks;
@@ -1140,7 +1137,7 @@ redraw_get_default_border_style(struct redraw_draw_ctx *dctx,
struct grid_cell *dgc = &dctx->default_gc;
if (~dctx->flags & REDRAW_DEFAULT_SET) {
ft = format_create_defaults(NULL, c, s, NULL, NULL);
ft = format_create_defaults(NULL, c, s, s->curw, NULL);
memcpy(dgc, &grid_default_cell, sizeof *dgc);
style_add(dgc, oo, "pane-border-style", ft);
format_free(ft);
@@ -1334,7 +1331,7 @@ redraw_draw_scrollbar_span(struct redraw_draw_ctx *dctx,
memcpy(&slgc, &gc, sizeof slgc);
slgc.fg = gc.bg;
slgc.bg = gc.fg;
tty_default_colours(&pad_gc, wp, dctx->active, NULL);
tty_default_colours(&pad_gc, wp, NULL);
sb_w = sb_style->width;
sb_pad = sb_style->pad;
@@ -1381,45 +1378,30 @@ static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_span_data *data = &span->data;
enum redraw_span_type type = data->type;
struct client *c = scene->c;
struct tty *tty = &c->tty;
struct visible_ranges *r;
struct visible_range *rr;
u_int i, x, n;
if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS)
return;
r = tty_check_overlay_range(tty, span->x, y, span->width);
for (i = 0; i < r->used; i++) {
rr = &r->ranges[i];
if (rr->nx == 0)
continue;
x = rr->px;
n = rr->nx;
switch (span->data.type) {
case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY:
case REDRAW_SPAN_OUTSIDE:
redraw_draw_border_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_STATUS:
redraw_draw_status_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_SCROLLBAR:
redraw_draw_scrollbar_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_MENU:
redraw_draw_menu_span(dctx, span, x, y, n);
break;
}
switch (span->data.type) {
case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY:
case REDRAW_SPAN_OUTSIDE:
redraw_draw_border_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_STATUS:
redraw_draw_status_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_SCROLLBAR:
redraw_draw_scrollbar_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_MENU:
redraw_draw_menu_span(dctx, span, span->x, y, span->width);
break;
}
}
@@ -1603,16 +1585,14 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx,
struct session *s = c->session;
struct options *oo = s->options;
struct tty *tty = &c->tty;
struct winlink *wl;
u_int lines;
memset(dctx, 0, sizeof *dctx);
dctx->scene = scene;
wl = active_get_effective_winlink(c, s);
if (server_is_marked(s, wl, marked_pane.wp))
if (server_is_marked(s, s->curw, marked_pane.wp))
dctx->marked = marked_pane.wp;
dctx->active = active_get_effective_pane(c, wl->window);
dctx->active = s->curw->window->active;
lines = status_line_size(c);
if (options_get_number(oo, "status-position") == 0)
@@ -1685,15 +1665,13 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
{
struct redraw_draw_ctx dctx;
struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
struct window *w = s->curw->window;
struct tty *tty = &c->tty;
struct screen *sl;
struct redraw_scene *scene;
struct window_pane *loop;
u_int width, i, y, lines, j;
u_int width, i, y, lines;
struct redraw_span *first;
struct visible_ranges *r;
struct visible_range *rr;
int redraw;
if (c->flags & CLIENT_SUSPENDED)
@@ -1740,8 +1718,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
else
loop->flags &= ~PANE_NEWSTATUS;
width = redraw_pane_status_width(&dctx, loop,
&first);
width = redraw_pane_status_width(&dctx, loop, &first);
if (width == 0)
continue;
@@ -1772,7 +1749,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
}
}
}
tty_sync_start(tty);
tty_sync_start(tty); /* end in server_client_reset_state */
tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL);
if (wp != NULL)
@@ -1802,22 +1779,11 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
else
y = c->tty.sy - lines;
sl = c->status.active;
for (i = 0; i < lines; i++) {
r = tty_check_overlay_range(tty, 0, y + i, tty->sx);
for (j = 0; j < r->used; j++) {
rr = &r->ranges[j];
if (rr->nx == 0)
continue;
tty_draw_line(tty, sl, rr->px, i, rr->nx,
rr->px, y + i, NULL);
}
}
for (i = 0; i < lines; i++)
tty_draw_line(tty, sl, 0, i, tty->sx, 0, y + i, NULL);
}
if (c->overlay_draw != NULL && (flags & REDRAW_OVERLAY))
c->overlay_draw(c, c->overlay_data);
tty_reset(tty);
tty_sync_end(tty);
#ifdef ENABLE_SIXEL
if (wp != NULL)
@@ -1871,21 +1837,16 @@ redraw_screen(struct client *c)
{
int flags = 0;
if (c->flags & CLIENT_REDRAWWINDOW) {
if (c->flags & CLIENT_REDRAWOVERLAY)
redraw_draw(c, NULL, REDRAW_ALL);
else
redraw_draw(c, NULL, REDRAW_ALL & ~REDRAW_OVERLAY);
} else {
if (c->flags & CLIENT_REDRAWWINDOW)
redraw_draw(c, NULL, REDRAW_ALL);
else {
if (c->flags & CLIENT_REDRAWBORDERS)
flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWSTATUS)
flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWOVERLAY)
flags |= REDRAW_OVERLAY;
if (c->flags & CLIENT_REDRAWMENU)
flags |= REDRAW_MENU;
if (active_get_effective_window(c, c->session)->menu != NULL)
if (c->session->curw->window->menu != NULL)
flags |= REDRAW_MENU;
if (flags != 0)
redraw_draw(c, NULL, flags);
@@ -1897,7 +1858,7 @@ void
redraw_pane(struct client *c, struct window_pane *wp)
{
redraw_draw(c, wp, REDRAW_PANE|REDRAW_PANE_SCROLLBAR);
if (active_get_effective_window(c, c->session)->menu != NULL)
if (c->session->curw->window->menu != NULL)
redraw_draw(c, NULL, REDRAW_MENU);
}

View File

@@ -134,7 +134,7 @@ screen_write_redraw_cb(const struct tty_ctx *ttyctx)
static int
screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct window_pane *wp = ttyctx->arg, *active;
struct window_pane *wp = ttyctx->arg;
if (ttyctx->flags & TTY_CTX_INVISIBLE_PANES) {
if (session_has(c->session, wp->window))
@@ -142,7 +142,7 @@ screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
return (0);
}
if (active_get_effective_window(c, c->session) != wp->window)
if (c->session->curw->window != wp->window)
return (0);
if (wp->layout_cell == NULL)
return (0);
@@ -160,14 +160,11 @@ screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
return (-1);
}
active = active_get_effective_pane(c, wp->window);
if (tty_window_offset(&c->tty, &ttyctx->wox, &ttyctx->woy, &ttyctx->wsx,
&ttyctx->wsy))
ttyctx->flags |= TTY_CTX_WINDOW_BIGGER;
else
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
tty_default_colours(&ttyctx->defaults, wp, active,
&ttyctx->style_ctx.dim);
ttyctx->xoff = ttyctx->rxoff = wp->xoff;
ttyctx->yoff = ttyctx->ryoff = wp->yoff;
@@ -299,7 +296,7 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
ttyctx->redraw_cb = screen_write_redraw_cb;
if (ctx->wp != NULL) {
tty_default_colours(&ttyctx->defaults, ctx->wp,
ctx->wp->window->active, &ttyctx->style_ctx.dim);
&ttyctx->style_ctx.dim);
ttyctx->style_ctx.palette = &ctx->wp->palette;
ttyctx->set_client_cb = screen_write_set_client_cb;
ttyctx->arg = ctx->wp;
@@ -308,21 +305,16 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
if (~ctx->flags & SCREEN_WRITE_SYNC) {
/*
* For the active pane showing its base screen or for an
* overlay (no pane), only use synchronized updates if
* requested (commands that move the cursor); for other panes
* or a pane in a mode, always use it, since the cursor will
* have to move.
* For the active pane showing its base screen, only use
* synchronized updates if requested (commands that move the
* cursor); for other panes or a pane in a mode, always use it,
* since the cursor will have to move.
*/
if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active ||
ctx->wp->screen != &ctx->wp->base))
ttyctx->flags |= TTY_CTX_SYNC;
else {
if (ctx->wp == NULL)
ttyctx->flags |= TTY_CTX_OVERLAY_SYNC;
if (is_sync)
ttyctx->flags |= TTY_CTX_SYNC;
}
else if (is_sync)
ttyctx->flags |= TTY_CTX_SYNC;
tty_write(tty_cmd_syncstart, ttyctx);
ctx->flags |= SCREEN_WRITE_SYNC;
}
@@ -717,7 +709,7 @@ screen_write_fast_copy(struct screen_write_ctx *ctx, struct screen *src,
if (!window_position_is_visible(r, xoff + s->cx))
break;
ttyctx.cell = &gc;
ttyctx.flags &= (TTY_CTX_OVERLAY_SYNC|TTY_CTX_SYNC);
ttyctx.flags &= TTY_CTX_SYNC;
tty_write(tty_cmd_cell, &ttyctx);
ttyctx.ocx++;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.509 2026/08/28 07:36:01 nicm Exp $ */
/* $OpenBSD: server-client.c,v 1.512 2026/09/10 11:02:18 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -67,77 +67,6 @@ server_client_how_many(void)
return (n);
}
/* Overlay timer callback. */
static void
server_client_overlay_timer(__unused int fd, __unused short events, void *data)
{
server_client_clear_overlay(data);
}
/* Set an overlay on client. */
void
server_client_set_overlay(struct client *c, u_int delay,
overlay_check_cb checkcb, overlay_mode_cb modecb,
overlay_draw_cb drawcb, overlay_key_cb keycb, overlay_free_cb freecb,
overlay_resize_cb resizecb, void *data)
{
struct timeval tv;
if (c->overlay_draw != NULL)
server_client_clear_overlay(c);
tv.tv_sec = delay / 1000;
tv.tv_usec = (delay % 1000) * 1000L;
if (event_initialized(&c->overlay_timer))
evtimer_del(&c->overlay_timer);
evtimer_set(&c->overlay_timer, server_client_overlay_timer, c);
if (delay != 0)
evtimer_add(&c->overlay_timer, &tv);
c->overlay_check = checkcb;
c->overlay_mode = modecb;
c->overlay_draw = drawcb;
c->overlay_key = keycb;
c->overlay_free = freecb;
c->overlay_resize = resizecb;
c->overlay_data = data;
if (c->overlay_check == NULL)
c->tty.flags |= TTY_FREEZE;
if (c->overlay_mode == NULL)
c->tty.flags |= TTY_NOCURSOR;
window_update_focus(active_get_effective_window(c, c->session));
server_redraw_client(c);
}
/* Clear overlay mode on client. */
void
server_client_clear_overlay(struct client *c)
{
if (c->overlay_draw == NULL)
return;
if (event_initialized(&c->overlay_timer))
evtimer_del(&c->overlay_timer);
if (c->overlay_free != NULL)
c->overlay_free(c, c->overlay_data);
c->overlay_check = NULL;
c->overlay_mode = NULL;
c->overlay_draw = NULL;
c->overlay_key = NULL;
c->overlay_free = NULL;
c->overlay_resize = NULL;
c->overlay_data = NULL;
c->tty.flags &= ~(TTY_FREEZE|TTY_NOCURSOR);
if (c->session != NULL)
window_update_focus(active_get_effective_window(c, c->session));
server_redraw_client(c);
}
/* Are these ranges empty? That is, nothing is visible. */
int
server_client_ranges_is_empty(struct visible_ranges *r)
@@ -161,52 +90,6 @@ server_client_ensure_ranges(struct visible_ranges *r, u_int n)
r->size = n;
}
/*
* Given overlay position and dimensions, return parts of the input range which
* are visible.
*/
void
server_client_overlay_range(u_int x, u_int y, u_int sx, u_int sy, u_int px,
u_int py, u_int nx, struct visible_ranges *r)
{
u_int ox, onx;
/* Trivial case of no overlap in the y direction. */
if (py < y || py > y + sy - 1) {
server_client_ensure_ranges(r, 1);
r->ranges[0].px = px;
r->ranges[0].nx = nx;
r->used = 1;
return;
}
server_client_ensure_ranges(r, 2);
/* Visible bit to the left of the popup. */
if (px < x) {
r->ranges[0].px = px;
r->ranges[0].nx = x - px;
if (r->ranges[0].nx > nx)
r->ranges[0].nx = nx;
} else {
r->ranges[0].px = 0;
r->ranges[0].nx = 0;
}
/* Visible bit to the right of the popup. */
ox = x + sx;
if (px > ox)
ox = px;
onx = px + nx;
if (onx > ox) {
r->ranges[1].px = ox;
r->ranges[1].nx = onx - ox;
} else {
r->ranges[1].px = 0;
r->ranges[1].nx = 0;
}
r->used = 2;
}
/* Check if this client is inside this server. */
int
server_client_check_nested(struct client *c)
@@ -379,8 +262,7 @@ server_client_attached_lost(struct client *c)
found = NULL;
TAILQ_FOREACH(loop, &clients, entry) {
s = loop->session;
if (loop == c || s == NULL ||
active_get_effective_window(loop, s) != w)
if (loop == c || s == NULL || s->curw->window != w)
continue;
if (found == NULL || timercmp(&loop->activity_time,
&found->activity_time, >))
@@ -461,15 +343,15 @@ server_client_set_session(struct client *c, struct session *s)
c->flags |= CLIENT_FOCUSED;
if (old != NULL && old->curw != NULL)
window_update_focus(active_get_effective_window(c, old));
window_update_focus(old->curw->window);
if (s != NULL) {
active_get_effective_window(c, s)->latest = c;
s->curw->window->latest = c;
recalculate_sizes();
window_update_focus(active_get_effective_window(c, s));
window_update_focus(s->curw->window);
session_update_activity(s, NULL);
session_theme_changed(s);
gettimeofday(&s->last_attached_time, NULL);
active_get_effective_winlink(c, s)->flags &= ~WINLINK_ALERTFLAGS;
s->curw->flags &= ~WINLINK_ALERTFLAGS;
alerts_check_session(s);
tty_update_client_offset(c);
status_timer_start(c);
@@ -491,10 +373,8 @@ server_client_lost(struct client *c)
cfg_client = NULL;
c->flags |= CLIENT_DEAD;
server_client_clear_overlay(c);
status_prompt_clear(c);
status_message_clear(c);
active_remove_client(c);
RB_FOREACH_SAFE(cf, client_files, &c->files, cf1) {
cf->error = EINTR;
@@ -508,7 +388,6 @@ server_client_lost(struct client *c)
server_client_attached_lost(c);
events_fire_client("client-detached", c);
}
active_check_clients();
if (c->name != NULL && (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL)))
events_fire_client("client-closed", c);
@@ -687,7 +566,7 @@ server_client_in_scrollbar_area(struct window_pane *wp, int px, int py)
static void
server_client_update_scrollbar_hover(struct client *c, int type, int px, int py)
{
struct window *w = active_get_effective_window(c, c->session);
struct window *w = c->session->curw->window;
struct window_pane *wp;
if (type != KEYC_TYPE_MOUSEMOVE)
@@ -859,7 +738,7 @@ server_client_check_mouse(struct client *c, struct key_event *event)
{
struct mouse_event *m = &event->m;
struct session *s = c->session, *fs;
struct window *w = active_get_effective_window(c, s);
struct window *w = s->curw->window;
struct winlink *fwl;
struct window_pane *wp, *fwp, *lwp = NULL;
u_int x, y, sx, sy, px, py, n, sl_mpos = 0;
@@ -1362,7 +1241,7 @@ server_client_update_latest(struct client *c)
if (c->session == NULL)
return;
w = active_get_effective_window(c, c->session);
w = c->session->curw->window;
if (w->latest == c)
return;
@@ -1394,15 +1273,19 @@ server_client_repeat_time(struct client *c, struct key_binding *bd)
return (repeat);
}
/* Handle a key press on a dead pane waiting for a key. */
/* Handle a key press which closes a dead pane. */
static int
server_client_handle_dead_key(struct window_pane *wp, key_code key)
{
int remain_on_exit;
if (wp == NULL ||
(~wp->flags & PANE_EXITED) ||
KEYC_IS_MOUSE(key) ||
KEYC_IS_PASTE(key) ||
options_get_number(wp->options, "remain-on-exit") != 3)
KEYC_IS_PASTE(key))
return (0);
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
if (remain_on_exit != 3 && remain_on_exit != 4)
return (0);
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
@@ -1441,7 +1324,7 @@ server_client_key_callback(struct cmdq_item *item, void *data)
/* Check the client is good to accept input. */
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
goto out;
wl = active_get_effective_winlink(c, s);
wl = s->curw;
/* Update the activity timer. */
memcpy(&c->last_activity_time, &c->activity_time,
@@ -1700,7 +1583,7 @@ out:
static int
server_client_handle_menu_key(struct client *c, struct key_event *event)
{
struct window *w = active_get_effective_window(c, c->session);
struct window *w = c->session->curw->window;
struct key_event new_event;
struct mouse_event *m;
u_int ox, oy, sx, sy;
@@ -1739,17 +1622,12 @@ server_client_handle_key0(struct client *c, struct key_event *event,
{
struct session *s = c->session;
struct cmdq_item *item;
struct window *w;
struct window_pane *wp;
/* Check the client is good to accept input. */
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
return (0);
/*
* Handle theme reporting keys before overlays so they work even when a
* popup is open.
*/
if (event->key == KEYC_REPORT_LIGHT_THEME) {
server_client_report_theme(c, THEME_LIGHT);
return (0);
@@ -1760,9 +1638,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
/*
* Key presses in overlay mode, for panes capturing all keys and in the
* command prompt are a special case. The queue might be blocked so they
* need to be processed immediately rather than queued.
* Dead panes waiting for a key, modal cancel keys, panes capturing all keys
* and the command prompt are special cases. The queue might be blocked so
* they need to be processed immediately rather than queued.
*/
if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) {
@@ -1771,25 +1649,20 @@ server_client_handle_key0(struct client *c, struct key_event *event,
status_message_clear(c);
}
if (c->overlay_key != NULL) {
switch (c->overlay_key(c, c->overlay_data, event)) {
case 0:
return (0);
case 1:
server_client_clear_overlay(c);
return (0);
}
}
server_client_clear_overlay(c);
wp = s->curw->window->active;
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (wp != NULL &&
wp == wp->window->modal &&
(wp->flags & PANE_CLOSEONCANCEL) &&
(event->key == '\033' || event->key == ('c'|KEYC_CTRL))) {
server_kill_pane(wp);
return (0);
}
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
TAILQ_EMPTY(&wp->modes) &&
!KEYC_IS_MOUSE(event->key)) {
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (~wp->flags & PANE_EXITED) {
window_pane_key(wp, c, s, s->curw, event->key,
&event->m);
@@ -1810,10 +1683,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
}
w = active_get_effective_window(c, s);
wp = w->active;
wp = s->curw->window->active;
if (wp == NULL || !window_pane_has_prompt(wp)) {
TAILQ_FOREACH(wp, &w->panes, entry) {
TAILQ_FOREACH(wp, &s->curw->window->panes, entry) {
if (window_pane_has_prompt(wp) &&
window_pane_is_visible(wp))
break;
@@ -2155,8 +2027,8 @@ static void
server_client_reset_state(struct client *c)
{
struct tty *tty = &c->tty;
struct window *w = active_get_effective_window(c, c->session);
struct window_pane *wp, *loop;
struct window *w = c->session->curw->window;
struct window_pane *wp = w->active, *loop;
struct screen *s = NULL;
struct options *oo = c->session->options;
int mode = 0, cursor, flags, pane_mode = 0;
@@ -2164,7 +2036,6 @@ server_client_reset_state(struct client *c)
u_int sb_w;
struct visible_ranges *r;
wp = active_get_effective_pane(c, w);
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
@@ -2172,11 +2043,8 @@ server_client_reset_state(struct client *c)
flags = (tty->flags & TTY_BLOCK);
tty->flags &= ~TTY_BLOCK;
/* Get mode from overlay if any, else from screen. */
if (c->overlay_draw != NULL) {
if (c->overlay_mode != NULL)
s = c->overlay_mode(c, c->overlay_data, &cx, &cy);
} else if (w->menu != NULL) {
/* Get mode from the menu if any, else from the screen. */
if (w->menu != NULL) {
menu_get_cursor(w->menu, &cx, &cy);
s = menu_screen(w->menu);
} else if (wp != NULL && c->prompt == NULL)
@@ -2198,7 +2066,7 @@ server_client_reset_state(struct client *c)
if (c->prompt != NULL) {
prompt = 1;
status_prompt_cursor(c, &cx, &cy);
} else if (wp != NULL && c->overlay_draw == NULL) {
} else if (wp != NULL) {
if (w->menu != NULL) {
tty_window_offset(tty, &ox, &oy, &sx, &sy);
if (cx < ox || cx >= ox + sx ||
@@ -2250,14 +2118,18 @@ server_client_reset_state(struct client *c)
cy += status_line_size(c);
}
if ((pane_mode & MODE_SYNC) || !cursor)
if (!cursor)
mode &= ~MODE_CURSOR;
}
} else if (c->overlay_mode == NULL || s == NULL)
} else if (s == NULL)
mode &= ~MODE_CURSOR;
if (~pane_mode & MODE_SYNC) {
log_debug("%s: cursor to %u,%u", __func__, cx, cy);
tty_cursor(tty, cx, cy);
} else {
mode &= ~CURSOR_MODES;
mode |= tty->mode & CURSOR_MODES;
s = NULL;
}
/*
@@ -2266,7 +2138,7 @@ server_client_reset_state(struct client *c)
* movement events.
*/
if (options_get_number(oo, "mouse")) {
if (c->overlay_draw == NULL && w->menu == NULL) {
if (w->menu == NULL) {
mode &= ~ALL_MOUSE_MODES;
TAILQ_FOREACH(loop, &w->panes, entry) {
if (loop->screen->mode & MODE_MOUSE_ALL)
@@ -2282,7 +2154,7 @@ server_client_reset_state(struct client *c)
}
/* Clear bracketed paste mode if at the prompt. */
if (c->overlay_draw == NULL && prompt)
if (prompt)
mode &= ~MODE_BRACKETPASTE;
/* Set the terminal mode and reset attributes. */
@@ -2436,7 +2308,7 @@ server_client_redraw_timer(__unused int fd, __unused short events,
static void
server_client_check_modes(struct client *c)
{
struct window *w;
struct window *w = c->session->curw->window;
struct window_pane *wp;
struct window_mode_entry *wme;
@@ -2444,7 +2316,6 @@ server_client_check_modes(struct client *c)
return;
if (~c->flags & CLIENT_REDRAWSTATUS)
return;
w = active_get_effective_window(c, c->session);
TAILQ_FOREACH(wp, &w->panes, entry) {
wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL && wme->mode->update != NULL)
@@ -2457,7 +2328,7 @@ static int
server_client_any_pane_redraw(struct client *c)
{
struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
struct window *w = s->curw->window;
struct window_pane *wp;
if (c->flags & CLIENT_REDRAWWINDOW)
@@ -2475,7 +2346,7 @@ server_client_check_redraw(struct client *c)
{
struct session *s = c->session;
struct tty *tty = &c->tty;
struct window *w = active_get_effective_window(c, s);
struct window *w = s->curw->window;
struct window_pane *wp;
int needed, tflags, mode = tty->mode;
struct timeval tv = { .tv_usec = 1000 };
@@ -2485,11 +2356,10 @@ server_client_check_redraw(struct client *c)
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_ALLREDRAWFLAGS) {
log_debug("%s: redraw%s%s%s%s%s", c->name,
log_debug("%s: redraw%s%s%s%s", c->name,
(c->flags & CLIENT_REDRAWWINDOW) ? " window" : "",
(c->flags & CLIENT_REDRAWSTATUS) ? " status" : "",
(c->flags & CLIENT_REDRAWBORDERS) ? " borders" : "",
(c->flags & CLIENT_REDRAWOVERLAY) ? " overlay" : "",
(c->flags & CLIENT_REDRAWMENU) ? " menu" : "");
}
@@ -2614,15 +2484,14 @@ static void
server_client_set_path(struct client *c)
{
struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
const char *path;
if (w == NULL || w->active == NULL)
if (s->curw == NULL || s->curw->window->active == NULL)
return;
if (w->active->base.path == NULL)
if (s->curw->window->active->base.path == NULL)
path = "";
else
path = w->active->base.path;
path = s->curw->window->active->base.path;
if (c->path == NULL || strcmp(path, c->path) != 0) {
free(c->path);
c->path = xstrdup(path);
@@ -2635,12 +2504,11 @@ static void
server_client_set_progress_bar(struct client *c)
{
struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
struct progress_bar *pane_pb;
if (w == NULL || w->active == NULL)
if (s->curw == NULL || s->curw->window->active == NULL)
return;
pane_pb = &w->active->base.progress_bar;
pane_pb = &s->curw->window->active->base.progress_bar;
if (pane_pb->state == c->progress_bar.state &&
pane_pb->progress == c->progress_bar.progress)
return;
@@ -2699,10 +2567,6 @@ server_client_dispatch(struct imsg *imsg, void *arg)
tty_resize(&c->tty);
tty_repeat_requests(&c->tty, 0);
recalculate_sizes();
if (c->overlay_resize == NULL)
server_client_clear_overlay(c);
else
c->overlay_resize(c, c->overlay_data);
server_redraw_client(c);
if (c->session != NULL)
server_client_fire_resized(c, old_sx, old_sy);
@@ -3089,6 +2953,8 @@ server_client_control_flags(struct client *c, const char *next)
return (CLIENT_CONTROL_NOOUTPUT);
if (strcmp(next, "wait-exit") == 0)
return (CLIENT_CONTROL_WAITEXIT);
if (strcmp(next, "new-layouts") == 0)
return (CLIENT_CONTROL_NEWLAYOUTS);
return (0);
}
@@ -3155,6 +3021,8 @@ server_client_get_flags(struct client *c)
strlcat(s, "no-output,", sizeof s);
if (c->flags & CLIENT_CONTROL_WAITEXIT)
strlcat(s, "wait-exit,", sizeof s);
if (c->flags & CLIENT_CONTROL_NEWLAYOUTS)
strlcat(s, "new-layouts,", sizeof s);
if (c->flags & CLIENT_CONTROL_PAUSEAFTER) {
xsnprintf(tmp, sizeof tmp, "pause-after=%u,",
c->pause_age / 1000);
@@ -3177,7 +3045,6 @@ server_client_remove_pane(struct window_pane *wp)
{
struct client *c;
active_remove_pane(wp);
TAILQ_FOREACH(c, &clients, entry) {
if (c->tty.mouse_last_pane == (int)wp->id) {
c->tty.mouse_last_pane = -1;
@@ -3193,7 +3060,6 @@ server_client_print(struct client *c, int parse, struct evbuffer *evb)
{
void *data = EVBUFFER_DATA(evb);
size_t size = EVBUFFER_LENGTH(evb);
struct window *w;
struct window_pane *wp;
struct window_mode_entry *wme;
char *sanitized, *msg, *line, empty = '\0';
@@ -3232,8 +3098,7 @@ server_client_print(struct client *c, int parse, struct evbuffer *evb)
goto out;
}
w = active_get_effective_window(c, c->session);
wp = active_get_effective_pane(c, w);
wp = c->session->curw->window->active;
wme = TAILQ_FIRST(&wp->modes);
if (wme == NULL || wme->mode != &window_view_mode)
window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL,

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.151 2026/08/20 09:19:24 nicm Exp $ */
/* $OpenBSD: server-fn.c,v 1.153 2026/09/10 11:02:18 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -122,7 +122,8 @@ server_redraw_window(struct window *w)
TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL &&
active_get_effective_window(c, c->session) == w)
c->session->curw != NULL &&
c->session->curw->window == w)
server_redraw_client(c);
}
}
@@ -134,7 +135,8 @@ server_redraw_window_menu(struct window *w)
TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL &&
active_get_effective_window(c, c->session) == w)
c->session->curw != NULL &&
c->session->curw->window == w)
c->flags |= CLIENT_REDRAWMENU;
}
}
@@ -146,7 +148,8 @@ server_redraw_window_borders(struct window *w)
TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL &&
active_get_effective_window(c, c->session) == w)
c->session->curw != NULL &&
c->session->curw->window == w)
c->flags |= CLIENT_REDRAWBORDERS;
}
}
@@ -376,13 +379,14 @@ server_destroy_pane(struct window_pane *wp, int notify)
wp->pipe_fd = -1;
}
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
if (remain_on_exit != 0 && (~wp->flags & PANE_STATUSREADY))
if (~wp->flags & PANE_STATUSREADY)
return;
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
switch (remain_on_exit) {
case 0:
break;
case 2:
case 4:
if (WIFEXITED(wp->status) && WEXITSTATUS(wp->status) == 0)
break;
/* FALLTHROUGH */

View File

@@ -203,7 +203,6 @@ session_destroy(struct session *s, int notify, const char *from)
if (s->curw == NULL)
return;
s->curw = NULL;
active_remove_session(s);
RB_REMOVE(sessions, &sessions, s);
if (notify)

View File

@@ -577,8 +577,6 @@ status_prompt_set(struct client *c, struct cmd_find_state *fs,
struct prompt_create_data pd;
struct status_prompt_data *spd;
server_client_clear_overlay(c);
status_message_clear(c);
status_prompt_clear(c);
status_push_screen(c);

296
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1163 2026/09/01 12:49:49 nicm Exp $
.\" $OpenBSD: tmux.1,v 1.1169 2026/09/10 11:02:18 nicm Exp $
.\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\"
.Dd $Mdocdate: September 1 2026 $
.Dd $Mdocdate: September 10 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -1116,6 +1116,8 @@ The flags are:
.Bl -tag -width Ds
.It ignore\-size
the client does not affect the size of other clients
.It new\-layouts
use the new layout string format
.It no\-detach\-on\-destroy
do not detach the client when the session it is attached to is destroyed if
there are any other sessions
@@ -2699,8 +2701,8 @@ For example:
.Bd -literal -offset indent
$ tmux list\-windows
0: ksh [159x48]
layout: bb62,159x48,0,0{79x48,0,0,79x48,80,0}
$ tmux select\-layout \[aq]bb62,159x48,0,0{79x48,0,0,79x48,80,0}\[aq]
layout: {"V":2,"L":{"t":"h","w":159,"h":48,"x":0,"y":0,"c":[{"t":"p","w":79,"h":48,"x":0,"y":0,"l":0,"i":0,"I":"%0"},{"t":"p","w":79,"h":48,"x":80,"y":0,"a":true,"i":1,"I":"%2"}]}}
$ tmux select\-layout \[aq]{"V":2,"L":{"t":"h","w":159,"h":48,"x":0,"y":0,"c":[{"t":"p","w":79,"h":48,"x":0,"y":0,"l":0,"i":0,"I":"%0"},{"t":"p","w":79,"h":48,"x":80,"y":0,"a":true,"i":1,"I":"%2"}]}}\[aq]
.Ed
.Pp
.Nm
@@ -2788,7 +2790,7 @@ The pane must not already be floating or hidden, and the window must not
be zoomed.
.Tg capturep
.It Xo Ic capture\-pane
.Op Fl aeFHLpPRqCJMN
.Op Fl aeFHILpPRqCJMN
.Op Fl b Ar buffer\-name
.Op Fl E Ar end\-line
.Op Fl S Ar start\-line
@@ -2830,6 +2832,8 @@ captures only any output that the pane has received that is the beginning of an
as-yet incomplete escape sequence.
.Fl L
includes the line number at the start of each line and
.Fl I
includes the time each line entered the history, or zero if not available.
.Fl F
includes the flags (where
.Ql -
@@ -3590,13 +3594,20 @@ represents the window to be created; if the target already exists an error is
shown, unless the
.Fl k
flag is used, in which case it is destroyed.
.Ar window\-name
is the name given to the new window, if any.
.Pp
If
.Fl S
is given and a window named
.Ar window\-name
already exists, it is selected (unless
is given, an existing window is selected instead of creating a new one
(unless
.Fl d
is also given in which case the command does nothing).
is also given in which case the command does nothing):
.Ar target\-window
is preferred if it already identifies an existing window; otherwise a
window named
.Ar window\-name
is selected if one exists.
.Pp
.Ar shell\-command
is the command to execute.
@@ -3644,7 +3655,7 @@ but a different format may be specified with
.Fl F .
.Tg newp
.It Xo Ic new\-pane
.Op Fl AbCdefhIkKLMOPvWZ
.Op Fl AbCDefhIkKLMOPvWZ
.Op Fl B Ar border\-lines
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
@@ -3706,6 +3717,13 @@ all keys, including the prefix key, are passed directly to the modal pane.
With
.Fl C ,
the modal pane is closed when the mouse is clicked outside it.
With
.Fl D ,
the modal pane is closed when
.Ql Escape
or
.Ql C-c
is pressed.
.Pp
The
.Fl L
@@ -3993,7 +4011,7 @@ applies the last set layout if possible (undoes the most recent layout change).
spreads the current pane and any panes next to it out evenly.
.Tg selectp
.It Xo Ic select\-pane
.Op Fl ADdeLlMmRsSUZ
.Op Fl DdeLlMmRUZ
.Op Fl T Ar title
.Op Fl t Ar target\-pane
.Xc
@@ -4001,8 +4019,6 @@ spreads the current pane and any panes next to it out evenly.
Make pane
.Ar target\-pane
the active pane in its window.
.Fl A
raises the pane if it is floating.
If one of
.Fl D ,
.Fl L ,
@@ -4017,15 +4033,6 @@ keeps the window zoomed if it was zoomed.
is the same as using the
.Ic last\-pane
command.
.Fl s
switches the client to local pane selection for the target window when more
than one client is attached; the pane currently active for the client is
retained.
Local pane selection is disabled automatically when fewer than two clients
remain attached.
.Fl S
switches the client back to shared pane selection for the target window, so the
window's active pane is used.
.Fl e
enables or
.Fl d
@@ -4049,22 +4056,12 @@ and
.Ic swap\-window .
.Tg selectw
.It Xo Ic select\-window
.Op Fl LlnpST
.Op Fl lnpT
.Op Fl t Ar target\-window
.Xc
.D1 Pq alias: Ic selectw
Select the window at
.Ar target\-window .
.Fl L
makes the attached client use a local current window for the target session.
Window navigation from that client then changes only that client's effective
window and leaves the session current window unchanged.
.Fl S
returns the client to the shared session current window.
Without
.Fl L ,
the current window is shared by the session and stored in the session.
.Pp
.Fl l ,
.Fl n
and
@@ -5356,10 +5353,16 @@ section on how to specify
.Ar style .
.It Ic menu\-border\-lines Ar type
Set the type of characters used for drawing menu borders.
See
.Ic popup\-border\-lines
for possible values for
.Ar border\-lines .
.Ar type
may be one of
.Ic single ,
.Ic rounded ,
.Ic double ,
.Ic heavy ,
.Ic simple ,
.Ic padded ,
or
.Ic none .
.It Ic message\-command\-style Ar style
Set status line message command style.
This is used for the command prompt with
@@ -6017,48 +6020,6 @@ see the
section.
Attributes are ignored.
.Pp
.It Ic popup\-style Ar style
Set the popup style.
See the
.Sx STYLES
section on how to specify
.Ar style .
Attributes are ignored.
.Pp
.It Ic popup\-border\-style Ar style
Set the popup border style.
See the
.Sx STYLES
section on how to specify
.Ar style .
Attributes are ignored.
.Pp
.It Ic popup\-border\-lines Ar type
Set the type of characters used for drawing popup borders.
.Ar type
may be one of:
.Bl -tag -width Ds
.It single
single lines using ACS or UTF\-8 characters (default)
.It rounded
variation of single with rounded corners using UTF\-8 characters
.It double
double lines using UTF\-8 characters
.It heavy
heavy lines using UTF\-8 characters
.It simple
simple ASCII characters
.It padded
simple ASCII space character
.It none
no border
.El
.Pp
.Ql double
and
.Ql heavy
will fall back to standard ACS line drawing when UTF\-8 is not supported.
.Pp
.It Xo Ic pane\-scrollbars
.Op Ic off | modal | on | auto\-hide
.Xc
@@ -6338,7 +6299,7 @@ uses when the colour with that index is requested.
The index may be from zero to 255.
.Pp
.It Xo Ic remain\-on\-exit
.Op Ic on | off | failed | key
.Op Ic on | off | failed | key | failed\-key
.Xc
A pane with this flag set is not destroyed when the program running in it
exits.
@@ -6348,6 +6309,10 @@ then only when the program exit status is not zero.
If set to
.Ic key ,
the pane stays open and closes when a key is pressed.
If set to
.Ic failed\-key ,
the pane stays open and closes when a key is pressed only if the program exit
status is not zero.
The pane may be reactivated with the
.Ic respawn\-pane
command.
@@ -7287,10 +7252,7 @@ shows the four frames in turn, changing every 200 milliseconds.
The following variables are available, where appropriate:
.Bl -column "XXXXXXXXXXXXXXXXXXX" "XXXXX"
.It Sy "Variable name" Ta Sy "Alias" Ta Sy "Replaced with"
.It Li "active_window_id" Ta "" Ta "ID of effective active window"
.It Li "active_window_index" Ta "" Ta "Index of effective active window"
.It Li "local_active_window" Ta "" Ta "1 if client has local window selection"
.It Li "active_window_mode" Ta "" Ta "Window selection mode, shared or local"
.It Li "active_window_index" Ta "" Ta "Index of active window in session"
.It Li "alternate_on" Ta "" Ta "1 if pane is in alternate screen"
.It Li "alternate_saved_x" Ta "" Ta "Saved cursor X in alternate screen"
.It Li "alternate_saved_y" Ta "" Ta "Saved cursor Y in alternate screen"
@@ -7444,8 +7406,6 @@ The following variables are available, where appropriate:
.It Li "pane_last" Ta "" Ta "1 if last pane"
.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output"
.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began"
.It Li "pane_local_active" Ta "" Ta "1 if active pane is client-local"
.It Li "pane_local_mode" Ta "" Ta "1 if client has local pane selection"
.It Li "pane_left" Ta "" Ta "Left of pane"
.It Li "pane_marked" Ta "" Ta "1 if this is the marked pane"
.It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set"
@@ -7529,7 +7489,7 @@ The following variables are available, where appropriate:
.It Li "uid" Ta "" Ta "Server UID"
.It Li "user" Ta "" Ta "Server user"
.It Li "version" Ta "" Ta "Server version"
.It Li "window_active" Ta "" Ta "1 if window is the effective active window"
.It Li "window_active" Ta "" Ta "1 if window active"
.It Li "window_active_clients" Ta "" Ta "Number of clients viewing this window"
.It Li "window_active_clients_list" Ta "" Ta "List of clients viewing this window"
.It Li "window_active_sessions" Ta "" Ta "Number of sessions on which this window is active"
@@ -8206,10 +8166,16 @@ command should be omitted.
.Pp
.Fl b
sets the type of characters used for drawing menu borders.
See
.Ic popup\-border\-lines
for possible values for
.Ar border\-lines .
.Ar border\-lines
may be one of
.Ic single ,
.Ic rounded ,
.Ic double ,
.Ic heavy ,
.Ic simple ,
.Ic padded ,
or
.Ic none .
.Pp
.Fl H
sets the style for the selected menu item (see
@@ -8245,15 +8211,18 @@ Both may be a row or column number, or one of the following special values:
.It Li "S" Ta Fl y Ta "The line above or below the status line"
.El
.Pp
Or a format, which is expanded including the following additional variables:
Or a format, which is expanded with the following additional variables.
The
.Ql popup_
prefix is retained for compatibility:
.Bl -column "XXXXXXXXXXXXXXXXXXXXXXXXXX" -offset indent
.It Sy "Variable name" Ta Sy "Replaced with"
.It Li "popup_centre_x" Ta "Centered in the client"
.It Li "popup_centre_y" Ta "Centered in the client"
.It Li "popup_height" Ta "Height of menu or popup"
.It Li "popup_centre_x" Ta "Centered in the window"
.It Li "popup_centre_y" Ta "Centered in the window"
.It Li "popup_height" Ta "Height of the menu"
.It Li "popup_last_x" Ta "Left of the last menu"
.It Li "popup_last_y" Ta "Bottom of the last menu"
.It Li "popup_mouse_bottom" Ta "Bottom of at the mouse"
.It Li "popup_mouse_bottom" Ta "Bottom at the mouse"
.It Li "popup_mouse_centre_x" Ta "Horizontal centre at the mouse"
.It Li "popup_mouse_centre_y" Ta "Vertical centre at the mouse"
.It Li "popup_mouse_top" Ta "Top at the mouse"
@@ -8264,7 +8233,7 @@ Or a format, which is expanded including the following additional variables:
.It Li "popup_pane_right" Ta "Right of the pane"
.It Li "popup_pane_top" Ta "Top of the pane"
.It Li "popup_status_line_y" Ta "Above or below the status line"
.It Li "popup_width" Ta "Width of menu or popup"
.It Li "popup_width" Ta "Width of the menu"
.It Li "popup_window_status_line_x" Ta "At the window position in status line"
.It Li "popup_window_status_line_y" Ta "At the status line showing the window"
.El
@@ -8296,7 +8265,7 @@ The following keys are available in menus:
.El
.Tg display
.It Xo Ic display\-message
.Op Fl aCIlNpv
.Op Fl aCIjlNpv
.Op Fl c Ar target\-client
.Op Fl d Ar delay
.Op Fl t Ar target\-pane
@@ -8336,6 +8305,12 @@ if
.Fl t
is given, otherwise the active pane.
.Pp
If
.Fl j
is given,
.Ar message
is parsed as JSON and printed.
.Pp
.Fl v
prints verbose logging as the format is parsed and
.Fl a
@@ -8344,116 +8319,6 @@ lists the format variables and their values.
.Fl I
forwards any input read from stdin to the empty pane given by
.Ar target\-pane .
.Tg popup
.It Xo Ic display\-popup
.Op Fl BCEkN
.Op Fl b Ar border\-lines
.Op Fl c Ar target\-client
.Op Fl d Ar start\-directory
.Op Fl e Ar environment
.Op Fl h Ar height
.Op Fl s Ar style
.Op Fl S Ar border\-style
.Op Fl t Ar target\-pane
.Op Fl T Ar title
.Op Fl w Ar width
.Op Fl x Ar position
.Op Fl y Ar position
.Op Ar shell\-command Op Ar argument ...
.Xc
.D1 Pq alias: Ic popup
Display a popup running
.Ar shell\-command
(or
.Ar default\-command
when omitted) on
.Ar target\-client .
A popup is a rectangular box drawn over the top of any panes.
If the command is run inside an existing popup, that popup is modified.
Only the
.Fl b ,
.Fl B ,
.Fl C ,
.Fl E ,
.Fl EE ,
.Fl K ,
.Fl N ,
.Fl s ,
and
.Fl S
options are accepted in this case;
all others are ignored.
.Pp
.Fl E
closes the popup automatically when
.Ar shell\-command
exits.
Two
.Fl E
closes the popup only if
.Ar shell\-command
exited with success.
.Fl k
allows any key to dismiss the popup instead of only
.Ql Escape
or
.Ql C\-c .
.Pp
.Fl x
and
.Fl y
give the position of the popup, they have the same meaning as for the
.Ic display\-menu
command.
.Fl w
and
.Fl h
give the width and height - both may be a percentage (followed by
.Ql % ) .
If omitted, half of the terminal size is used.
.Pp
.Fl B
does not surround the popup by a border.
.Pp
.Fl b
sets the type of characters used for drawing popup borders.
When
.Fl B
is specified, the
.Fl b
option is ignored.
See
.Ic popup\-border\-lines
for possible values for
.Ar border\-lines .
.Pp
.Fl s
sets the style for the popup and
.Fl S
sets the style for the popup border (see
.Sx STYLES ) .
.Pp
.Fl e
takes the form
.Ql VARIABLE=value
and sets an environment variable for the popup; it may be specified multiple
times.
.Pp
.Fl T
is a format for the popup title (see
.Sx FORMATS ) .
.Pp
The
.Fl C
flag closes any popup on the client.
.Pp
.Fl N
disables any previously specified
.Fl E ,
.Fl EE ,
or
.Fl k
option.
.Tg showphist
.It Xo Ic show\-prompt\-history
.Op Fl T Ar prompt\-type
@@ -9081,7 +8946,7 @@ and flags (currently not used).
For example:
.Bd -literal -offset indent
%begin 1363006971 2 1
0: ksh* (1 panes) [80x24] [layout b25f,80x24,0,0,2] @2 (active)
0: ksh* (1 panes) [80x24] [layout {"V":2,"L":{"t":"p","w":80,"h":24,"x":0,"y":0,"a":true,"i":0,"I":"%2"}}] @2 (active)
%end 1363006971 2 1
.Ed
.Pp
@@ -9142,10 +9007,15 @@ The layout of a window with ID
.Ar window\-id
changed.
The new layout is
.Ar window\-layout .
The window's visible layout is
.Ar window\-visible\-layout
and the window flags are
.Ar window\-layout
and the window's visible layout is
.Ar window\-visible\-layout .
If the
.Ar new\-layouts
flag is set, both layout fields use the new format string; see
.Ic refresh\-client
.Fl f .
The window flags are
.Ar window\-flags .
.It Ic %message Ar message
A message sent with the

129
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */
/* $OpenBSD: tmux.h,v 1.1439 2026/09/10 11:02:18 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -60,6 +60,7 @@ struct input_ctx;
struct input_request;
struct input_requests;
struct job;
struct json_node;
struct menu_data;
struct mode_tree_data;
struct mouse_event;
@@ -1338,6 +1339,7 @@ struct window_pane {
#define PANE_CLOSEONCLICK 0x80000
#define PANE_CAPTUREALLKEYS 0x100000
#define PANE_FLOATOVERZOOM 0x200000
#define PANE_CLOSEONCANCEL 0x400000
bitstr_t *sync_dirty;
u_int sync_dirty_size;
@@ -1794,7 +1796,6 @@ struct tty {
size_t discarded;
struct termios tio;
struct visible_ranges r;
struct grid_cell cell;
struct grid_cell last_cell;
@@ -1856,7 +1857,6 @@ struct tty_ctx {
#define TTY_CTX_INVISIBLE_PANES 0x2
#define TTY_CTX_WINDOW_BIGGER 0x4
#define TTY_CTX_SYNC 0x8
#define TTY_CTX_OVERLAY_SYNC 0x10
#define TTY_CTX_CELL_INVALIDATE 0x20
#define TTY_CTX_PANE_OBSCURED 0x40
@@ -2191,16 +2191,6 @@ struct prompt_draw_data {
u_int prompt_line;
};
/* Overlay callbacks */
typedef struct visible_ranges *(*overlay_check_cb)(struct client *, void *,
u_int, u_int, u_int);
typedef struct screen *(*overlay_mode_cb)(struct client *, void *, u_int *,
u_int *);
typedef void (*overlay_draw_cb)(struct client *, void *);
typedef int (*overlay_key_cb)(struct client *, void *, struct key_event *);
typedef void (*overlay_free_cb)(struct client *, void *);
typedef void (*overlay_resize_cb)(struct client *, void *);
/* Client connection. */
struct client {
const char *name;
@@ -2286,7 +2276,7 @@ struct client {
#define CLIENT_SIZECHANGED 0x400000
#define CLIENT_STATUSOFF 0x800000
#define CLIENT_REDRAWSTATUSALWAYS 0x1000000
#define CLIENT_REDRAWOVERLAY 0x2000000
/* 0x2000000 unused */
#define CLIENT_CONTROL_NOOUTPUT 0x4000000
#define CLIENT_DEFAULTSOCKET 0x8000000
#define CLIENT_STARTSERVER 0x10000000
@@ -2296,17 +2286,17 @@ struct client {
#define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL
#define CLIENT_CONTROL_WAITEXIT 0x200000000ULL
#define CLIENT_WINDOWSIZECHANGED 0x400000000ULL
/* 0x800000000ULL unused */
#define CLIENT_CONTROL_NEWLAYOUTS 0x800000000ULL
#define CLIENT_BRACKETPASTING 0x1000000000ULL
#define CLIENT_ASSUMEPASTING 0x2000000000ULL
#define CLIENT_WRITE_ACK 0x4000000000ULL
#define CLIENT_NO_DETACH_ON_DESTROY 0x8000000000ULL
#define CLIENT_CONTROL_DISCARD 0x1000000000ULL
#define CLIENT_ALLREDRAWFLAGS \
(CLIENT_REDRAWWINDOW| \
CLIENT_REDRAWSTATUS| \
CLIENT_REDRAWSTATUSALWAYS| \
CLIENT_REDRAWBORDERS| \
CLIENT_REDRAWOVERLAY| \
CLIENT_REDRAWMENU)
#define CLIENT_UNATTACHEDFLAGS \
(CLIENT_DEAD| \
@@ -2352,15 +2342,6 @@ struct client {
u_int pan_ox;
u_int pan_oy;
overlay_check_cb overlay_check;
overlay_mode_cb overlay_mode;
overlay_draw_cb overlay_draw;
overlay_key_cb overlay_key;
overlay_free_cb overlay_free;
overlay_resize_cb overlay_resize;
void *overlay_data;
struct event overlay_timer;
struct client_files files;
u_int source_file_depth;
@@ -2777,42 +2758,6 @@ void events_fire_window(const char *, struct window *);
void events_fire_pane(const char *, struct window_pane *);
void events_fire_winlink(const char *, struct winlink *);
/* active.c */
enum active_mode {
ACTIVE_SHARED,
ACTIVE_LOCAL
};
int active_is_local_window(struct client *, struct session *);
void active_set_local_window(struct client *, struct session *,
enum active_mode);
struct winlink *active_get_effective_winlink(struct client *,
struct session *);
struct window *active_get_effective_window(struct client *,
struct session *);
int active_select_window(struct client *, struct session *,
struct winlink *);
int active_select_window_index(struct client *, struct session *, int);
int active_next_window(struct client *, struct session *, int);
int active_previous_window(struct client *, struct session *, int);
int active_last_window(struct client *, struct session *);
void active_remove_client(struct client *);
void active_remove_session(struct session *);
void active_remove_window(struct window *);
int active_is_effective_window(struct client *, struct session *,
struct winlink *);
int active_is_last_window(struct client *, struct session *,
struct winlink *);
int active_has_local_pane(struct client *, struct window *);
void active_set_pane_mode(struct client *, struct window *,
enum active_mode);
struct window_pane *active_get_effective_pane(struct client *,
struct window *);
struct window_pane *active_get_last_pane(struct client *, struct window *);
int active_set_pane(struct client *, struct window *,
struct window_pane *, int);
void active_check_clients(void);
void active_remove_pane(struct window_pane *);
/* format-draw.c */
void format_draw(struct screen_write_ctx *, const struct grid_cell *,
u_int, const char *, struct style_ranges *, int);
@@ -2998,8 +2943,6 @@ void tty_default_attributes(struct tty *, u_int,
void tty_update_mode(struct tty *, int, struct screen *);
const struct grid_cell *tty_check_codeset(struct tty *,
const struct grid_cell *);
struct visible_ranges *tty_check_overlay_range(struct tty *, u_int, u_int,
u_int);
void tty_sync_start(struct tty *);
void tty_sync_end(struct tty *);
int tty_open(struct tty *, char **);
@@ -3035,8 +2978,7 @@ void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *);
void tty_draw_images(struct client *, struct window_pane *);
#endif
void tty_cmd_syncstart(struct tty *, const struct tty_ctx *);
void tty_default_colours(struct grid_cell *, struct window_pane *,
struct window_pane *, u_int *);
void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *);
/* tty-term.c */
extern struct tty_terms tty_terms;
@@ -3351,14 +3293,8 @@ int server_create_socket(uint64_t, char **);
/* server-client.c */
u_int server_client_how_many(void);
void server_client_set_overlay(struct client *, u_int, overlay_check_cb,
overlay_mode_cb, overlay_draw_cb, overlay_key_cb,
overlay_free_cb, overlay_resize_cb, void *);
void server_client_clear_overlay(struct client *);
void server_client_ensure_ranges(struct visible_ranges *, u_int);
int server_client_ranges_is_empty(struct visible_ranges *);
void server_client_overlay_range(u_int, u_int, u_int, u_int, u_int, u_int,
u_int, struct visible_ranges *);
void server_client_set_key_table(struct client *, const char *);
const char *server_client_get_key_table(struct client *);
int server_client_check_nested(struct client *);
@@ -3467,7 +3403,7 @@ void recalculate_sizes_now(int);
/* input.c */
#define INPUT_BUF_DEFAULT_SIZE 1048576
struct input_ctx *input_init(struct window_pane *, struct bufferevent *,
struct colour_palette *, struct client *);
struct colour_palette *);
void input_free(struct input_ctx *);
void input_reset(struct input_ctx *, int);
struct evbuffer *input_pending(struct input_ctx *);
@@ -3786,6 +3722,7 @@ struct window_pane *window_pane_previous_by_number(struct window *,
struct window_pane *, u_int);
int window_pane_index(struct window_pane *, u_int *);
int window_pane_zindex(struct window_pane *, u_int *);
int window_pane_last_index(struct window_pane *, u_int *);
u_int window_count_panes(struct window *, int);
void window_destroy_panes(struct window *);
struct window_pane *window_pane_find_by_id_str(const char *);
@@ -3818,7 +3755,7 @@ int window_pane_is_visible(struct window_pane *);
int window_pane_exited(struct window_pane *);
u_int window_pane_search(struct window_pane *, const char *, int,
int);
const char *window_printable_flags(struct client *, struct winlink *, int);
const char *window_printable_flags(struct winlink *, int);
const char *window_pane_printable_flags(struct window_pane *);
struct window_pane *window_pane_find_up(struct window_pane *);
struct window_pane *window_pane_find_down(struct window_pane *);
@@ -3885,7 +3822,7 @@ struct visible_ranges *window_visible_ranges(struct window_pane *, int, int,
u_int, struct visible_ranges *);
/* layout.c */
u_int layout_count_cells(struct layout_cell *);
u_int layout_count_cells(struct layout_cell *, int);
struct layout_cell *layout_create_cell(struct layout_cell *);
void layout_free_cell(struct layout_cell *, int);
void layout_print_cell(struct layout_cell *, const char *, u_int);
@@ -3897,8 +3834,8 @@ struct layout_cell *layout_search_by_border(struct layout_cell *, u_int, u_int);
void layout_set_size(struct layout_cell *, u_int, u_int, int, int);
void layout_make_leaf(struct layout_cell *, struct window_pane *);
void layout_make_node(struct layout_cell *, enum layout_type);
void layout_fix_zindexes(struct window *, struct layout_cell *);
int layout_cell_is_tiled(struct layout_cell *);
int layout_cell_has_tiled_child(struct layout_cell *);
int layout_add_horizontal_border(struct layout_cell *,
struct layout_cell *, int);
void layout_fix_offsets(struct window *);
@@ -3949,7 +3886,8 @@ int layout_remove_tile(struct window *, struct layout_cell *);
int layout_insert_tile(struct window *, struct layout_cell *);
/* layout-custom.c */
char *layout_dump(struct window *, struct layout_cell *);
#define LAYOUT_CUSTOM_OLD_FORMAT 0x1
char *layout_dump(struct window *, struct layout_cell *, int);
int layout_parse(struct window *, const char *, char **);
/* layout-set.c */
@@ -4220,20 +4158,6 @@ void menu_get_cursor(struct menu_data *, u_int *, u_int *);
void menu_resize(struct menu_data *, struct window *);
int menu_key(struct client *, struct menu_data *, struct key_event *);
/* popup.c */
#define POPUP_CLOSEEXIT 0x1
#define POPUP_CLOSEEXITZERO 0x2
#define POPUP_CLOSEANYKEY 0x4
typedef void (*popup_close_cb)(int, void *);
int popup_display(int, enum box_lines, struct cmdq_item *, u_int,
u_int, u_int, u_int, struct environ *, const char *, int,
char **, const char *, const char *, struct client *,
struct session *, const char *, const char *,
popup_close_cb, void *);
int popup_present(struct client *);
int popup_modify(struct client *, const char *, const char *,
const char *, enum box_lines, int);
/* style.c */
int style_parse(struct style *,const struct grid_cell *,
const char *);
@@ -4309,4 +4233,29 @@ struct hyperlinks *hyperlinks_copy(struct hyperlinks *);
void hyperlinks_reset(struct hyperlinks *);
void hyperlinks_free(struct hyperlinks *);
/* json.c */
struct json_node *json_parse(const char *, char **);
void json_destroy_node(struct json_node *);
char *json_to_string(struct json_node *);
struct json_node *json_find(struct json_node *, const char *);
struct json_node *json_array_first(struct json_node *);
struct json_node *json_array_next(struct json_node *);
int json_get_string(struct json_node *, const char **);
int json_get_number(struct json_node *, int64_t *);
int json_get_boolean(struct json_node *, int *);
int json_get_object(struct json_node *,
struct json_node **);
int json_get_array(struct json_node *,
struct json_node **);
int json_find_string(struct json_node *, const char *,
const char **, char **);
int json_find_number(struct json_node *, const char *,
int64_t *, char **);
int json_find_boolean(struct json_node *, const char *,
int *, char **);
int json_find_object(struct json_node *, const char *,
struct json_node **, char **);
int json_find_array(struct json_node *, const char *,
struct json_node **, char **);
#endif /* TMUX_H */

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