Compare commits

..

55 Commits

Author SHA1 Message Date
Michael Grant
2ec4328380 Fixed clear erasinng the usable C marker, use beginning of buffer in this case, up to D marker. 2026-09-14 03:09:42 +01:00
Michael Grant
8746405e34 In copy-mode if cursor is below last line, select-output uses the previous command. 2026-09-14 02:50:32 +01:00
Michael Grant
8c0ac1fe05 Fixed output may end on the next prompt's line. 2026-09-09 06:01:10 +01:00
Michael Grant
dc65677ff9 Add more logging for osc133. 2026-09-07 14:22:54 +01:00
Michael Grant
2142818e25 Undo these as top level tmux commands. 2026-09-07 14:12:01 +01:00
Michael Grant
cda0fe9f7b Act on previous cmd output when cursor on prompt at end of copy-buffer. 2026-09-07 05:21:25 +01:00
Michael Grant
4cc9fc91fa Bugfix copy-output outside copy-mode now properly copies last output. 2026-09-05 20:47:21 +01:00
Michael Grant
081b79fd6e - Added top-level commands:
- select-output
    - copy-output
    - copy-pipe-output
    - pipe-output
    - open-output
    - open-selection

- Added -a support to operate on the entire pane/copy-mode buffer, including panes without OSC 133 markers.
- Output extraction now supports unfinished commands with C but no D, ending at the current pane cursor.
- Top-level commands use the most recent OSC 133 output when outside copy mode.
- select-output enters copy mode and selects the output; other top-level operations use a temporary copy-mode snapshot.
- Added copy-mode bindings to open the selection when present, otherwise the current command output:
    - emacs: e
    - vi: M-e

- Added copy-mode bidings to select the output of command cursor is on:
    - emacs copy mode: C-o → select-output
    - vi copy mode: M-o → select-output

- Updated command registration, build sources, and tmux.1 documentation.
- Added/extended regress/output-commands.sh.
- Verified with make, regress/collapse-output.sh, and regress/output-commands.sh.
2026-09-05 18:43:15 +01:00
Michael Grant
6294be4986 Merge branch 'master' into osc133_line_collapsible 2026-09-05 15:15:29 +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
Michael Grant
c5a5b99788 Rename output to fold in places for consistency. 2026-08-01 11:59:18 +01:00
Michael Grant
e23219ec04 Add comments during my review including one fix, separator text after D also expandable. 2026-08-01 11:47:25 +01:00
Michael Grant
0f29c47278 Cleanup. Rename things for consistency. Fix indenting. 2026-08-01 08:26:05 +01:00
Michael Grant
7c9fdcb770 Add k=c and a helper function to check for secondary prompt. 2026-07-31 10:39:09 +01:00
Michael Grant
0330d8ac57 Change line-numbers menu to On/Off. 2026-07-29 23:35:11 +01:00
Michael Grant
0a7788b66f Add Fold View menu item and O key to toggle fold view. 2026-07-29 23:25:36 +01:00
Michael Grant
458d045bd2 With collapsible output active, line-numbers-on now shows each visible line’s original buffer line number, so hidden lines remain counted. 2026-07-29 22:10:19 +01:00
Michael Grant
a81154d6d9 Fix missing ! before commands with no output and non-zero exit status. 2026-07-22 23:40:01 +01:00
Michael Grant
cffc86dae5 Merge branch 'osc133_line_collapsible' of github.com:tmux/tmux into osc133_line_collapsible 2026-07-22 11:39:04 +01:00
Michael Grant
4cfd155ffc Add copy-mode collapse output regression test. 2026-07-22 11:38:07 +01:00
Michael Grant
670ea5d083 Add regression test for copy-mode-exit-status. 2026-07-22 11:35:59 +01:00
Michael Grant
eaeb234dd4 Remove code to put exit-code on right and go for just simple ! on left of +. 2026-07-22 11:35:59 +01:00
Michael Grant
5948722d5a Add highlighting to the current line. 2026-07-22 11:35:59 +01:00
Michael Grant
d8ad7cd9c5 Add exit status on right. 2026-07-22 11:35:59 +01:00
Michael Grant
b49cc2d936 Fix stray L on empty commands. 2026-07-22 11:35:59 +01:00
Michael Grant
abb7108e25 Add vertical bar next to expanded lines. 2026-07-22 11:35:59 +01:00
Michael Grant
d080378464 Add collapsible lines. 2026-07-22 11:35:59 +01:00
Michael Grant
473eca949a Add regression test for copy-mode-exit-status. 2026-07-22 11:35:28 +01:00
Michael Grant
f0eaa5e9d6 Remove code to put exit-code on right and go for just simple ! on left of +. 2026-07-22 11:33:46 +01:00
Michael Grant
f44246c764 Add highlighting to the current line. 2026-07-21 19:19:31 +01:00
Michael Grant
5d4d41da57 Add exit status on right. 2026-07-21 18:48:16 +01:00
Michael Grant
bacbb4d9d8 Fix stray L on empty commands. 2026-07-21 15:03:52 +01:00
Michael Grant
07a3a16ae7 Add vertical bar next to expanded lines. 2026-07-21 07:42:20 +01:00
Michael Grant
f41288ee27 Add collapsible lines. 2026-07-20 16:26:02 +01:00
Nicholas Marriott
457b6cbefd Put OSC 133 data into grid_line. 2026-07-20 11:24:33 +01:00
80 changed files with 4546 additions and 1244 deletions

49
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,6 +29,9 @@ 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;
- command-prompt -P opens a prompt inside a pane rather than on the status
line;
- pane-border-status has top-floating and bottom-floating, and there are new
default bindings under C-b g for common move and resize operations;
@@ -74,7 +82,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 +95,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 +126,8 @@ 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 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 +141,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 +158,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.
@@ -145,7 +177,10 @@ CHANGES FROM 3.7c TO 3.8
* 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).
* 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 \

449
active.c
View File

@@ -1,449 +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);
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);
static struct active_window *active_get(struct client *, struct session *);
static struct active_window *active_add(struct client *, struct session *,
struct winlink *);
static void active_free(struct active_window *);
static struct winlink *active_resolve(struct active_window *);
static void active_stack_remove(struct active_window_stack *, u_int);
static void active_stack_push(struct active_window_stack *, u_int);
static void active_changed(struct client *, struct session *,
struct winlink *, struct winlink *);
/* All local active window states. */
static struct active_windows active_windows = RB_INITIALIZER(&active_windows);
/* Find local active window state for a client and session. */
static struct active_window *
active_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_add(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
aw = active_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);
}
/* Free local active window state. */
static void
active_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_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_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);
}
/* Remove a window from a local last-window stack. */
static void
active_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_stack_push(struct active_window_stack *stack, u_int window)
{
struct active_window_entry *awe;
active_stack_remove(stack, window);
awe = xcalloc(1, sizeof *awe);
awe->window = window;
TAILQ_INSERT_HEAD(stack, awe, entry);
}
/* Notify the server that a client's effective window changed. */
static void
active_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();
}
/* Return if a client has local window selection for a session. */
int
active_is_local_window(struct client *c, struct session *s)
{
return (active_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_window_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_add(c, s, old);
if (c != NULL && c->session == s)
c->flags |= CLIENT_REDRAWSTATUS;
return;
}
aw = active_get(c, s);
if (aw != NULL) {
active_free(aw);
wl = active_get_effective_winlink(c, s);
active_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_get(c, s);
if (aw != NULL)
return (active_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_get(c, s);
if (aw == NULL)
return (session_set_current(s, wl));
old = active_resolve(aw);
if (old == wl)
return (1);
if (old != NULL)
active_stack_push(&aw->lastw, old->window->id);
active_stack_remove(&aw->lastw, wl->window->id);
aw->window = wl->window->id;
changed = (old != wl);
active_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_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_stack_remove(&aw->lastw, window);
}
return (-1);
}
/* Remove all state for a client. */
void
active_remove_client(struct client *c)
{
struct active_window *aw, *aw1;
RB_FOREACH_SAFE(aw, active_windows, &active_windows, aw1) {
if (aw->client == c)
active_free(aw);
}
}
/* Remove all 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_free(aw);
}
}
/* Remove a window from all local state. */
void
active_remove_window(struct window *w)
{
struct active_window *aw;
RB_FOREACH(aw, active_windows, &active_windows) {
active_stack_remove(&aw->lastw, w->id);
if (aw->window == w->id && aw->session->curw != NULL)
aw->window = aw->session->curw->window->id;
}
}
/* 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_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);
}

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

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

View File

@@ -483,7 +483,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')) {
@@ -584,7 +584,7 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
u_int px, py, w, h, count = args_count(args);
struct args_value *av;
struct environ *env = NULL;
struct options *o = target->w->options;
struct options *o = s->curw->window->options;
struct options_entry *oe;
if (args_has(args, 'C')) {

View File

@@ -884,13 +884,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 = fs->wl->window->active;
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 +931,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 +939,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 +1015,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

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

@@ -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;
struct session *s;
struct window_pane *wp = NULL;
struct window *w;
struct cmd_find_state fs;
struct window_mode_entry *wme;
@@ -93,12 +90,8 @@ cmd_run_shell_print(struct job *job, const char *msg)
cmdq_print(cdata->item, "%s", msg);
return;
}
c = cdata->client;
if (c != NULL && c->session != NULL) {
s = c->session;
w = active_get_effective_window(c, s);
wp = w->active;
}
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

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

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

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

@@ -222,28 +222,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 +359,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.67 2026/09/03 21:35:38 nicm Exp $ */
/*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -455,8 +455,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 +483,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 +510,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);

View File

@@ -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);
@@ -3066,48 +3066,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)
@@ -3125,11 +3088,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"));
}
@@ -3209,7 +3169,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);
}
@@ -3396,7 +3356,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);
}
@@ -3606,15 +3566,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
},
@@ -3792,9 +3746,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
},
@@ -5344,18 +5295,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);
@@ -5383,10 +5331,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;
@@ -5395,9 +5343,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();
@@ -5405,13 +5353,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);
@@ -5420,11 +5367,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");
@@ -5444,7 +5391,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)
@@ -7032,7 +6979,7 @@ 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 = wl->window->active;

2
grid.c
View File

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

74
input.c
View File

@@ -3194,13 +3194,14 @@ input_osc_112(struct input_ctx *ictx, const char *p)
/* Parse the OSC 133 D exit status. */
static int
input_osc_133_exit_status(const char *p)
input_osc_133_exit_status(const char *p, int *present)
{
const char *end;
char *copy;
const char *errstr;
char *endptr;
long long status;
*present = 0;
if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2)
return (0);
end = strchr(p + 2, ';');
@@ -3214,9 +3215,11 @@ input_osc_133_exit_status(const char *p)
free(copy);
return (0);
}
status = strtonum(copy, 0, 255, &errstr);
*present = 1;
errno = 0;
status = (int)strtol(copy, &endptr, 10);
free(copy);
if (errstr != NULL)
if (errno != 0 || endptr == copy || status < 0 || status > 255)
return (255);
return (status);
}
@@ -3261,6 +3264,22 @@ input_fire_command_event(struct window_pane *wp, const char *name)
events_fire(name, ep);
}
/* Check if an OSC 133 prompt is secondary or a continuation. */
static int
input_osc_133_secondary_prompt(const char *p)
{
const char *cp;
while ((cp = strstr(p, ";k=")) != NULL) {
cp += 3;
if ((*cp == 's' || *cp == 'c') &&
(cp[1] == '\0' || cp[1] == ';'))
return (1);
p = cp;
}
return (0);
}
/* Handle the OSC 133 sequence. */
static void
input_osc_133(struct input_ctx *ictx, const char *p)
@@ -3270,8 +3289,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
struct grid *gd = s->grid;
u_int line = s->cy + gd->hsize;
struct grid_line *gl = NULL;
const char *cp;
int status;
int status, status_present;
if (line < gd->hsize + gd->sy)
gl = grid_get_line(gd, line);
@@ -3280,9 +3298,17 @@ input_osc_133(struct input_ctx *ictx, const char *p)
case 'A':
case 'N':
if (gl != NULL) {
memset(&gl->osc133_data, 0, sizeof gl->osc133_data);
gl->osc133_data.prompt_col = s->cx;
gl->flags |= GRID_LINE_START_PROMPT;
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 %s at %u,%u", __func__,
input_osc_133_secondary_prompt(p) ? "secondary prompt" :
"prompt", s->cx, line);
}
}
if (wp != NULL) {
wp->last_prompt_time = time(NULL);
@@ -3291,25 +3317,33 @@ input_osc_133(struct input_ctx *ictx, const char *p)
break;
case 'P':
if (gl != NULL) {
cp = strstr(p, ";k=s");
if (cp != NULL && (cp[4] == ';' || cp[4] == '\0'))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
gl->osc133_data.prompt_col = s->cx;
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 prompt at %u,%u", __func__, s->cx,
line);
}
}
break;
case 'B':
case 'I':
if (gl != NULL) {
if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) {
gl->flags |= GRID_LINE_START_COMMAND;
gl->osc133_data.cmd_col = s->cx;
log_debug("%s: osc133 command at %u,%u", __func__, s->cx,
line);
}
break;
case 'C':
if (gl != NULL) {
if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) {
gl->flags |= GRID_LINE_START_OUTPUT;
gl->osc133_data.out_start_col = s->cx;
log_debug("%s: osc133 output start at %u,%u", __func__, s->cx,
line);
}
if (wp != NULL) {
wp->cmd_start_time = time(NULL);
@@ -3320,7 +3354,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
break;
case 'D':
status = input_osc_133_exit_status(p);
status = input_osc_133_exit_status(p, &status_present);
if (wp != NULL) {
wp->cmd_end_time = time(NULL);
wp->flags &= ~PANE_CMDRUNNING;
@@ -3329,8 +3363,12 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
if (gl != NULL) {
gl->flags |= GRID_LINE_END_OUTPUT;
if (status_present)
gl->flags |= GRID_LINE_END_OUTPUT_STATUS;
gl->osc133_data.out_end_col = s->cx;
gl->osc133_data.exit_status = status;
log_debug("%s: osc133 output end at %u,%u (status %d)",
__func__, s->cx, line, status);
}
break;
}

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

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

@@ -114,7 +114,6 @@ static const char *options_table_theme_list[] = {
static const char *options_table_copy_mode_line_numbers_list[] = {
"off", "default", "absolute", "relative", "hybrid", NULL
};
/* Status line format. */
#define OPTIONS_TABLE_STATUS_FORMAT1 \
"#[align=left range=left #{E:status-left-style}]" \
@@ -1125,8 +1124,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."
@@ -1385,6 +1383,13 @@ const struct options_table_entry options_table[] = {
.text = "Style of search matches in copy mode."
},
{ .name = "copy-mode-exit-status-format",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.default_str = "#{?exit_status,#[fg=themered]!, }",
.text = "Format of OSC 133 exit status indicator in copy mode."
},
{ .name = "copy-mode-current-match-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
@@ -1983,8 +1988,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", "",

View File

@@ -103,9 +103,9 @@ popup_reapply_styles(struct popup_data *pd)
if (s == NULL)
return;
o = active_get_effective_window(c, s)->options;
o = s->curw->window->options;
ft = format_create_defaults(NULL, c, s, NULL, NULL);
ft = format_create_defaults(NULL, c, s, s->curw, NULL);
/* Reapply popup style from options. */
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
@@ -570,9 +570,9 @@ popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
struct style sytmp;
if (s != NULL)
o = active_get_effective_window(c, s)->options;
o = s->curw->window->options;
else
o = active_get_effective_window(c, c->session)->options;
o = c->session->curw->window->options;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "popup-border-lines");

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

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

View File

@@ -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,63 @@
#!/bin/sh
# Popups require a tty overlay 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

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

View File

@@ -0,0 +1,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.

View File

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

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

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

168
regress/output-commands.sh Normal file
View File

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

View File

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

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

@@ -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.158 2026/09/01 19:50:58 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -295,7 +295,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;
@@ -969,7 +969,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 +1079,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;
@@ -1140,7 +1140,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);
@@ -1603,16 +1603,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_window(c, s)->active;
dctx->active = s->curw->window->active;
lines = status_line_size(c);
if (options_get_number(oo, "status-position") == 0)
@@ -1685,7 +1683,7 @@ 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;
@@ -1740,8 +1738,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 +1769,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)
@@ -1817,7 +1814,6 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
c->overlay_draw(c, c->overlay_data);
tty_reset(tty);
tty_sync_end(tty);
#ifdef ENABLE_SIXEL
if (wp != NULL)
@@ -1885,7 +1881,7 @@ redraw_screen(struct client *c)
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 +1893,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

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

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.510 2026/09/01 19:50:58 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -107,7 +107,7 @@ server_client_set_overlay(struct client *c, u_int delay,
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));
window_update_focus(c->session->curw->window);
server_redraw_client(c);
}
@@ -134,7 +134,7 @@ server_client_clear_overlay(struct client *c)
c->tty.flags &= ~(TTY_FREEZE|TTY_NOCURSOR);
if (c->session != NULL)
window_update_focus(active_get_effective_window(c, c->session));
window_update_focus(c->session->curw->window);
server_redraw_client(c);
}
@@ -379,8 +379,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 +460,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);
@@ -494,7 +493,6 @@ server_client_lost(struct client *c)
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;
@@ -686,7 +684,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)
@@ -858,7 +856,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;
@@ -1361,7 +1359,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;
@@ -1440,7 +1438,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,
@@ -1699,7 +1697,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;
@@ -1738,7 +1736,6 @@ 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. */
@@ -1809,10 +1806,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;
@@ -2154,7 +2150,7 @@ 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 *w = c->session->curw->window;
struct window_pane *wp = w->active, *loop;
struct screen *s = NULL;
struct options *oo = c->session->options;
@@ -2248,7 +2244,7 @@ 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)
@@ -2256,6 +2252,10 @@ server_client_reset_state(struct client *c)
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;
}
/*
@@ -2434,7 +2434,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;
@@ -2442,7 +2442,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)
@@ -2455,7 +2454,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)
@@ -2473,7 +2472,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 };
@@ -2612,15 +2611,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);
@@ -2633,12 +2631,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;
@@ -3228,7 +3225,7 @@ server_client_print(struct client *c, int parse, struct evbuffer *evb)
goto out;
}
wp = active_get_effective_window(c, c->session)->active;
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.152 2026/09/03 19:12:36 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,9 +379,9 @@ 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;

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

@@ -701,7 +701,7 @@ spawn_editor_finish(struct window_pane *wp)
struct spawn_editor_state *
spawn_editor(struct client *c, const char *buf, size_t len,
spawn_finish_edit_cb cb, void *arg)
const char *editor, spawn_finish_edit_cb cb, void *arg)
{
struct spawn_editor_state *es;
struct spawn_context sc = { 0 };
@@ -715,13 +715,13 @@ spawn_editor(struct client *c, const char *buf, size_t len,
FILE *f;
char *cmd, *cause = NULL;
char path[] = _PATH_TMP "tmux.XXXXXXXX";
const char *editor;
int fd;
if (w->modal != NULL)
return (NULL);
editor = options_get_string(global_options, "editor");
if (editor == NULL)
editor = options_get_string(global_options, "editor");
fd = mkstemp(path);
if (fd == -1)
return (NULL);

214
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.1164 2026/09/03 21:04:11 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 3 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -2004,6 +2004,25 @@ Same as
.Ic copy\-pipe
but also exit copy mode.
.It Xo
.Ic copy\-pipe\-output
.Op Fl aCP
.Op Ar command
.Op Ar prefix
.Xc
Copy the current OSC 133 command output, pipe it to
.Ar command ,
and clear the selection.
.Ar prefix
is used to name the new paste buffer.
.It Xo
.Ic copy\-output
.Op Fl aCP
.Op Ar prefix
.Xc
Copy the current OSC 133 command output and clear the selection.
.Ar prefix
is used to name the new paste buffer.
.It Xo
.Ic copy\-selection
.Op Fl CP
.Op Ar prefix
@@ -2169,6 +2188,27 @@ Hide line numbers in copy mode.
.Xc
Toggle line numbers in copy mode.
.It Xo
.Ic collapse\-output
.Op Fl a
.Xc
Collapse the output for the command on the cursor line.
With
.Fl a ,
collapse all command output in the copy-mode buffer.
Text before the first OSC 133 prompt is treated as an initial command output
region.
.It Xo
.Ic expand\-output
.Op Fl a
.Xc
Expand the output for the command on the cursor line.
With
.Fl a ,
expand all command output in the copy-mode buffer.
.Pp
The Tab key toggles output for the command on the cursor line and S-Tab
toggles all command output.
.It Xo
.Ic middle\-line
(vi: M)
(emacs: M\-r)
@@ -2222,6 +2262,32 @@ but treat all non-whitespace characters as part of a word.
.Xc
Switch at which end of the selection the cursor sits.
.It Xo
.Ic open\-output
.Op Fl a
.Op Ar command
.Xc
Open the current OSC 133 command output in an editor.
If
.Ar command
is given, it is used as the editor command; otherwise, the
.Ic editor
option is used.
.It Xo
.Ic open\-selection
.Op Ar command
.Xc
Open the current selection in an editor.
If
.Ar command
is given, it is used as the editor command; otherwise, the
.Ic editor
option is used.
In emacs mode,
.Ql e ,
and in vi mode,
.Ql M\-e ,
opens the selection if one is present, otherwise the command output.
.It Xo
.Ic page\-down
(vi: C\-f)
(emacs: PageDown)
@@ -2262,6 +2328,14 @@ Same as
.Ic pipe
but also exit copy mode.
.It Xo
.Ic pipe\-output
.Op Fl a
.Op Ar command
.Xc
Pipe the current OSC 133 command output to
.Ar command
and clear the selection.
.It Xo
.Ic previous\-matching\-bracket
(emacs: M\-C\-b)
.Xc
@@ -2456,6 +2530,16 @@ backward becomes forward).
.Xc
Select the current line.
.It Xo
.Ic select\-output
.Op Fl a
.Xc
Select the current OSC 133 command output.
If it is collapsed, it is expanded first.
The default emacs binding is
.Ql C\-o
and the default vi binding is
.Ql M\-o .
.It Xo
.Ic select\-word
.Xc
Select the current word.
@@ -2494,6 +2578,12 @@ Toggle the visibility of the position indicator in the top right.
Move to the top line.
.El
.Pp
The output commands use OSC 133 output text from the C marker to the D marker.
If there is no D marker, the output ends at the end of the copy-mode buffer.
With
.Fl a ,
they use the entire copy-mode buffer instead.
.Pp
The search commands come in several varieties:
.Ql search\-forward
and
@@ -2539,6 +2629,15 @@ Finding the beginning of command output requires the shell to emit an escape
sequence (\e033]133;C\e033\e\e) to tell tmux where the output begins.
If the shell does not send these escape sequences, these commands do nothing.
.Pp
The output commands operate on text from an OSC 133
.Ql C
marker up to its matching
.Ql D
marker, or the current end of the buffer if there is no
.Ql D
marker.
They do nothing if the cursor is not associated with an output range.
.Pp
Copy commands may take an optional buffer prefix argument which is used
to generate the buffer name (the default is
.Ql buffer
@@ -2601,12 +2700,19 @@ The synopsis for the
command is:
.Bl -tag -width Ds
.It Xo Ic copy\-mode
.Op Fl dekHMqSu
.Op Fl UcdekHMqSu
.Op Fl s Ar src\-pane
.Op Fl t Ar target\-pane
.Xc
Enter copy mode.
.Pp
.Fl c
enters copy mode with OSC 133 command output collapsed.
.Fl U
enters copy mode with OSC 133 output controls shown and all output expanded.
When output is collapsed, the prompt and entered command are shown and text
from the output start marker to the next primary prompt marker is hidden.
.Pp
.Fl u
enters copy mode and scrolls one page up and
.Fl d
@@ -2647,6 +2753,60 @@ bind PageDown copy\-mode \-ed
kills the pane when the mode is exited.
.El
.Pp
The following commands operate on OSC 133 command output. In copy mode, they
use the output on the cursor line; otherwise, they use the most recent output
in the target pane. If an output has a C marker but no D marker, it ends at the
end of the pane buffer. If there is no OSC 133 output, they do nothing unless
.Fl a
is given, which uses the entire buffer.
.Bl -tag -width Ds
.It Xo Ic select\-output
.Op Fl a
.Op Fl t Ar target\-pane
.Xc
Enter copy mode if necessary and select the output.
.It Xo Ic copy\-output
.Op Fl aCP
.Op Fl t Ar target\-pane
.Op Ar prefix
.Xc
Copy the output to a paste buffer.
.It Xo Ic copy\-pipe\-output
.Op Fl aCP
.Op Fl t Ar target\-pane
.Op Ar command
.Op Ar prefix
.Xc
Copy the output to a paste buffer and pipe it to
.Ar command .
.It Xo Ic pipe\-output
.Op Fl a
.Op Fl t Ar target\-pane
.Op Ar command
.Xc
Pipe the output to
.Ar command .
.It Xo Ic open\-output
.Op Fl a
.Op Fl t Ar target\-pane
.Op Ar command
.Xc
Open the output in an editor. If
.Ar command
is given, it is used as the editor command; otherwise the
.Ic editor
option is used.
.It Xo Ic open\-selection
.Op Fl t Ar target\-pane
.Op Ar command
.Xc
Open the current copy-mode selection in an editor. If
.Ar command
is given, it is used as the editor command; otherwise the
.Ic editor
option is used.
.El
.Pp
A number of preset arrangements of panes are available, these are called
layouts.
These may be selected with the
@@ -3590,13 +3750,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.
@@ -4038,22 +4205,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
@@ -5801,6 +5958,20 @@ section.
.It Ic copy\-mode\-position\-format Ar format
Format of the position indicator in copy mode.
.Pp
.It Ic copy\-mode\-exit\-status\-format Ar format
Format of the OSC 133 exit status indicator in the copy-mode gutter.
The default displays a theme red
.Ql !
for a nonzero status and a space otherwise.
The gutter grows to fit the expanded format.
The format is drawn to the left of the collapse control, and its alignment
applies within that area.
The format is expanded with
.Ql exit_status
and
.Ql exit_status_present ,
which is set only when the OSC 133 end-of-output marker includes a status.
.Pp
.It Ic copy\-mode\-position\-style Ar style
Set the style of the position indicator in copy mode.
For how to specify
@@ -7276,10 +7447,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"
@@ -7516,7 +7684,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"

34
tmux.h
View File

@@ -815,6 +815,7 @@ struct colour_palette {
#define GRID_LINE_START_OUTPUT 0x40
#define GRID_LINE_END_OUTPUT 0x80
#define GRID_LINE_HYPERLINK 0x100
#define GRID_LINE_END_OUTPUT_STATUS 0x200
/* All OSC 133 flags. */
#define GRID_LINE_OSC133_FLAGS \
@@ -822,7 +823,8 @@ struct colour_palette {
GRID_LINE_SECOND_PROMPT| \
GRID_LINE_START_COMMAND| \
GRID_LINE_START_OUTPUT| \
GRID_LINE_END_OUTPUT)
GRID_LINE_END_OUTPUT| \
GRID_LINE_END_OUTPUT_STATUS)
/* Grid string flags. */
#define GRID_STRING_WITH_SEQUENCES 0x1
@@ -2777,32 +2779,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_window_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_window_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 *);
/* format-draw.c */
void format_draw(struct screen_write_ctx *, const struct grid_cell *,
u_int, const char *, struct style_ranges *, int);
@@ -3807,7 +3783,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 *);
@@ -4247,7 +4223,7 @@ struct winlink *spawn_window(struct spawn_context *, char **);
struct window_pane *spawn_pane(struct spawn_context *, char **);
typedef void (*spawn_finish_edit_cb)(char *, size_t, void *);
struct spawn_editor_state *spawn_editor(struct client *, const char *, size_t,
spawn_finish_edit_cb, void *);
const char *, spawn_finish_edit_cb, void *);
void spawn_cancel_editor(struct spawn_editor_state *);
pid_t spawn_get_editor_pid(struct spawn_editor_state *);
void spawn_editor_finish(struct window_pane *);

View File

@@ -1020,12 +1020,12 @@ complete_key:
/* Check for focus events. */
if (key == KEYC_FOCUS_OUT) {
c->flags &= ~CLIENT_FOCUSED;
window_update_focus(active_get_effective_window(c, c->session));
window_update_focus(c->session->curw->window);
events_fire_client("client-focus-out", c);
} else if (key == KEYC_FOCUS_IN) {
c->flags |= CLIENT_FOCUSED;
events_fire_client("client-focus-in", c);
window_update_focus(active_get_effective_window(c, c->session));
window_update_focus(c->session->curw->window);
}
/* Fire the key. */

9
tty.c
View File

@@ -955,7 +955,7 @@ int
tty_window_bigger(struct tty *tty)
{
struct client *c = tty->client;
struct window *w = active_get_effective_window(c, c->session);
struct window *w = c->session->curw->window;
return (tty->sx < w->sx || tty->sy - status_line_size(c) < w->sy);
}
@@ -977,7 +977,7 @@ static int
tty_window_offset1(struct tty *tty, u_int *ox, u_int *oy, u_int *sx, u_int *sy)
{
struct client *c = tty->client;
struct window *w = active_get_effective_window(c, c->session);
struct window *w = c->session->curw->window;
struct window_pane *wp = w->active;
u_int cx, cy, lines;
@@ -1044,7 +1044,8 @@ tty_update_window_offset(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)
tty_update_client_offset(c);
}
}
@@ -1546,7 +1547,7 @@ tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct window_pane *wp = ttyctx->arg;
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);

View File

@@ -157,7 +157,7 @@ window_pane_get_border_style(struct window_pane *wp, struct client *c,
struct grid_cell *saved;
int *flag;
if (wp == active_get_effective_window(c, c->session)->active) {
if (wp == c->session->curw->window->active) {
flag = &wp->active_border_gc_set;
saved = &wp->active_border_gc;
option = "pane-active-border-style";
@@ -168,7 +168,7 @@ window_pane_get_border_style(struct window_pane *wp, struct client *c,
}
if (!*flag) {
ft = format_create_defaults(NULL, c, s, NULL, wp);
ft = format_create_defaults(NULL, c, s, s->curw, wp);
style_apply(saved, wp->options, option, ft);
format_free(ft);
*flag = 1;
@@ -197,7 +197,7 @@ window_make_pane_status(struct window_pane *wp, struct client *c, u_int width,
return (0);
ft = format_create(c, NULL, FORMAT_PANE|wp->id, FORMAT_STATUS);
format_defaults(ft, c, c->session, NULL, wp);
format_defaults(ft, c, c->session, c->session->curw, wp);
fmt = options_get_string(wp->options, "pane-border-format");
expanded = format_expand_time(ft, fmt);

View File

@@ -606,7 +606,8 @@ window_buffer_start_edit(struct window_buffer_modedata *data,
ed->name = xstrdup(paste_buffer_name(pb));
ed->pb = pb;
ed->editor = spawn_editor(c, buf, len, window_buffer_edit_close_cb, ed);
ed->editor = spawn_editor(c, buf, len, NULL,
window_buffer_edit_close_cb, ed);
if (ed->editor == NULL)
window_buffer_finish_edit(ed);
else {

View File

@@ -269,13 +269,12 @@ window_client_draw_info(__unused void *modedata, void *itemdata,
struct window_client_itemdata *item = itemdata;
struct client *c = item->c;
struct screen *s = ctx->s;
struct window *w;
struct window *w = c->session->curw->window;
struct grid_cell gc;
u_int cx = s->cx, cy = s->cy, i;
struct format_tree *ft;
char *expanded;
w = active_get_effective_window(c, c->session);
ft = format_create_defaults(NULL, c, NULL, NULL, NULL);
if (c->tty.term->flags & TERM_INVALIDMS)
format_add(ft, "clipboard_invalid", "1");

File diff suppressed because it is too large Load Diff

View File

@@ -2104,8 +2104,8 @@ window_customize_start_edit(struct window_customize_modedata *data,
buf = "\n";
len = 1;
}
ed->editor = spawn_editor(c, buf, len, window_customize_edit_close_cb,
ed);
ed->editor = spawn_editor(c, buf, len, NULL,
window_customize_edit_close_cb, ed);
free(value);
if (ed->editor == NULL)
window_customize_finish_edit(ed);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-switch.c,v 1.3 2026/08/25 07:23:30 nicm Exp $ */
/* $OpenBSD: window-switch.c,v 1.4 2026/09/01 21:03:55 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -374,6 +374,9 @@ window_switch_init(struct window_mode_entry *wme,
data->prompt = prompt_create(&pd);
prompt_update(data->prompt, "(search) ", data->filter);
s = &data->screen;
screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0);
if (!args_has(args, 'Z'))
data->zoomed = -1;
else {
@@ -382,9 +385,6 @@ window_switch_init(struct window_mode_entry *wme,
server_redraw_window(wp->window);
}
s = &data->screen;
screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0);
window_switch_build(data);
prompt_incremental_start(data->prompt);
window_switch_draw_screen(wme);

View File

@@ -451,7 +451,6 @@ window_destroy(struct window *w)
{
log_debug("window @%u destroyed (%d references)", w->id, w->references);
active_remove_window(w);
window_unzoom(w, 0);
RB_REMOVE(windows, &windows, w);
@@ -673,7 +672,6 @@ void
window_pane_update_focus(struct window_pane *wp)
{
struct client *c;
struct window *w;
int focused = 0;
if (wp != NULL && (~wp->flags & PANE_EXITED)) {
@@ -681,12 +679,10 @@ window_pane_update_focus(struct window_pane *wp)
focused = 0;
else {
TAILQ_FOREACH(c, &clients, entry) {
if (c->session == NULL)
continue;
w = active_get_effective_window(c, c->session);
if (c->session->attached != 0 &&
if (c->session != NULL &&
c->session->attached != 0 &&
(c->flags & CLIENT_FOCUSED) &&
w == wp->window &&
c->session->curw->window == wp->window &&
c->overlay_draw == NULL &&
wp->window->menu == NULL) {
focused = 1;
@@ -1291,7 +1287,7 @@ window_destroy_panes(struct window *w)
}
const char *
window_printable_flags(struct client *c, struct winlink *wl, int escape)
window_printable_flags(struct winlink *wl, int escape)
{
struct session *s = wl->session;
static char flags[32];
@@ -1306,9 +1302,9 @@ window_printable_flags(struct client *c, struct winlink *wl, int escape)
flags[pos++] = '!';
if (wl->flags & WINLINK_SILENCE)
flags[pos++] = '~';
if (active_is_effective_window(c, s, wl))
if (wl == s->curw)
flags[pos++] = '*';
if (active_is_last_window(c, s, wl))
if (wl == TAILQ_FIRST(&s->lastw))
flags[pos++] = '-';
if (server_check_marked() && wl == marked_pane.wl)
flags[pos++] = 'M';