Compare commits

..

88 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
tmux update bot
9b0ad73a4b Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  When changing selection-mode to line, set up the selection start and end correctly, GitHub issue 5545.
  Extend word commands to use any Unicode whitespace character, GitHub issue 5562 from Hongyi Zhao.
2026-09-01 14:55:38 +00:00
nicm
bd8218e85d When changing selection-mode to line, set up the selection start and end
correctly, GitHub issue 5545.
2026-09-01 14:55:35 +00:00
nicm
8f3101d820 Extend word commands to use any Unicode whitespace character, GitHub
issue 5562 from Hongyi Zhao.
2026-09-01 14:55:35 +00:00
Nicholas Marriott
77309c07b0 Whitespace regress test from Hongyi Zhao. 2026-09-01 13:50:56 +01:00
tmux update bot
c8e889694d Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a generation counter to pane output, from Michael K Darling.
  Add format variables for history change counters, from Michael K Darling.
2026-09-01 00:07:34 +00:00
nicm
b402687fcd Add a generation counter to pane output, from Michael K Darling. 2026-09-01 00:07:32 +00:00
nicm
0530a7d89a Add format variables for history change counters, from Michael K Darling. 2026-09-01 00:07:32 +00:00
tmux update bot
6bdb9ba54c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  tmux: add utf8 terminal-features
2026-08-31 20:43:13 +00:00
kirill
f2bfd611e2 tmux: add utf8 terminal-features
OK: nicm@
2026-08-31 20:43:11 +00:00
Nicholas Marriott
9c1d5a080b Use hash for actions, from Andreas Fehlner. 2026-08-31 20:52:54 +01:00
tmux update bot
b28244e2fe Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not leak temporary commands when expanding aliases, from Jeong, Heon.
  Do not allow popups for control clients, from Noam Stolero.
  Layout cell can be replaced by unzoom, fixes use after free. From Uzair Aftab.
  Do not try to append zero size cells, from Uzair Aftab.
  Clear stale marks when a search regex fails to compile, GitHub issue 5547 from Uzair Aftab.
2026-08-31 14:56:14 +00:00
nicm
8b3834c34f Do not leak temporary commands when expanding aliases, from Jeong, Heon. 2026-08-31 14:56:12 +00:00
nicm
af3e4d2e5b Do not allow popups for control clients, from Noam Stolero. 2026-08-31 14:56:12 +00:00
nicm
c552d1bf16 Layout cell can be replaced by unzoom, fixes use after free. From Uzair
Aftab.
2026-08-31 14:56:12 +00:00
nicm
6a1ecdcce3 Do not try to append zero size cells, from Uzair Aftab. 2026-08-31 14:56:12 +00:00
nicm
39357c829c Clear stale marks when a search regex fails to compile, GitHub issue
5547 from Uzair Aftab.
2026-08-31 14:56:12 +00:00
tmux update bot
40381bdcd9 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not walk off end of grid lines if last line is wrapped, reported by Moe Khalilov.
  Do no free input context if it is NULL when popup create fails.
2026-08-28 10:32:01 +00:00
nicm
7bad4f7722 Do not walk off end of grid lines if last line is wrapped, reported by
Moe Khalilov.
2026-08-28 10:32:00 +00:00
nicm
65e811570b Do no free input context if it is NULL when popup create fails. 2026-08-28 10:32:00 +00:00
tmux update bot
dd31900f80 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Ignore focus events earlier to avoid them interfering with prefix, GitHub issue 5539.
2026-08-28 09:02:23 +00:00
nicm
013ada58c8 Ignore focus events earlier to avoid them interfering with prefix,
GitHub issue 5539.
2026-08-28 09:02:22 +00:00
Nicholas Marriott
78dad45f3b Add timeout for lock and don't keep credentials for regress, from Andreas
Fehlner.
2026-08-28 09:07:16 +01:00
Nicholas Marriott
52b452948f Check for buffer write error. 2026-08-28 08:56:12 +01:00
tmux update bot
267746603a Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Fix tiled pane resizing with floating layout cells, reported by Kevin Hovsäter.
2026-08-25 21:31:27 +00:00
nicm
b26b240613 Fix tiled pane resizing with floating layout cells, reported by Kevin
Hovsäter.
2026-08-25 21:31:25 +00:00
Nicholas Marriott
8e48c889dd Add a resize test. 2026-08-25 19:36:06 +01:00
tmux update bot
f4445a310a Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a clear-on-attach option which when turned off scrolls away the existing terminal state instead of entering the alternate screen. This is useful in environments where tmux is started as a login shell but it is important that any pre-tmux messages are still visible. GitHub issue 5508 from Josef Řídký.
2026-08-25 11:30:47 +00:00
nicm
4157b64754 Add a clear-on-attach option which when turned off scrolls away the
existing terminal state instead of entering the alternate screen. This
is useful in environments where tmux is started as a login shell but it
is important that any pre-tmux messages are still visible. GitHub issue
5508 from Josef Řídký.
2026-08-25 11:30:45 +00:00
tmux update bot
0d2aafe7b7 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not pass NULL to qsort, and add checks for invalid session or window after swap.
2026-08-25 09:38:42 +00:00
nicm
6f4c9b32af Do not pass NULL to qsort, and add checks for invalid session or window
after swap.
2026-08-25 09:38:40 +00:00
tmux update bot
eb77a75ab7 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not leak exit message on client free, from Jeong, Heon.
  Tidy up args_make_commands reference counting to fix a memory leak, from Jeong, Heon.
2026-08-25 07:59:42 +00:00
nicm
0851650c6b Do not leak exit message on client free, from Jeong, Heon. 2026-08-25 07:59:40 +00:00
nicm
1459c90a7f Tidy up args_make_commands reference counting to fix a memory leak, from
Jeong, Heon.
2026-08-25 07:59:40 +00: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
111 changed files with 5685 additions and 8971 deletions

View File

@@ -17,8 +17,9 @@ jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'tmux/tmux' if: github.repository == 'tmux/tmux'
timeout-minutes: 10
steps: steps:
- uses: dessant/lock-threads@v6 - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '30' issue-inactive-days: '30'

View File

@@ -38,7 +38,9 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: dependencies - name: dependencies
if: runner.os == 'Linux' if: runner.os == 'Linux'
@@ -81,7 +83,7 @@ jobs:
- name: logs - name: logs
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: regress-logs-${{ matrix.name }} name: regress-logs-${{ matrix.name }}
path: regress/logs/*.log path: regress/logs/*.log

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 - 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. 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 - move-pane and resize-pane can now move and resize floating panes, including
mouse dragging; 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 -k for choose commands kills the pane when the mode exits, which is useful
for modes in floating panes; 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 - pane-border-status has top-floating and bottom-floating, and there are new
default bindings under C-b g for common move and resize operations; default bindings under C-b g for common move and resize operations;
@@ -74,7 +82,7 @@ CHANGES FROM 3.7c TO 3.8
- new hooks to cover areas including client create and destroy, pane - new hooks to cover areas including client create and destroy, pane
activity, pane mode and prompt changes, pane movement and resizing, session 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, - OSC 133 escape sequences now trigger events: pane-command-started,
pane-command-finished and pane-shell-prompt. 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 while scrolling or while hovered and disappears after pane-scrollbars-timeout
(Michael Grant). (Michael Grant).
* Extend the fill-character option so both inside and outside the window can be * Add a clear-on-attach server option. When disabled, tmux does not enter the
changed. 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 * Add a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk). 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 * 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). 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. * Menus now belong to the window, so appear on all clients.
* Dragging over an existing copy mode selection now adjusts it (Michael Grant). * 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 * Copy mode no longer exits at the bottom while a selection is in progress
(issue 5349). (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 * Add style attributes for dimming colours (dim=) and for hyperlinks (link= and
nolink) (issues 4842 and 4280 from Moritz Angermann). 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, * Add or improve format variables including client_colours,
pane_start_command_list, window_manual_width, window_manual_height, 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 * Add additional pane sort orders, and a z sort order for choose-tree so
floating panes can be sorted by z-index. 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 * 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 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 * Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180). they are being killed (Bryce Miller, issue 5180).

View File

@@ -5,8 +5,7 @@ CLEANFILES = tmux.1.mdoc tmux.1.man cmd-parse.c
# Distribution tarball options. # Distribution tarball options.
EXTRA_DIST = \ EXTRA_DIST = \
CHANGES README COPYING example_tmux.conf \ CHANGES README COPYING example_tmux.conf \
osdep-*.c mdoc2man.awk tmux.1 \ osdep-*.c mdoc2man.awk tmux.1
tools/generate-image-diacritics.sh
dist_EXTRA_tmux_SOURCES = compat/*.[ch] dist_EXTRA_tmux_SOURCES = compat/*.[ch]
# Preprocessor flags. # Preprocessor flags.
@@ -247,9 +246,9 @@ if HAVE_UTF8PROC
nodist_tmux_SOURCES += compat/utf8proc.c nodist_tmux_SOURCES += compat/utf8proc.c
endif endif
# Enable image support. # Enable sixel support.
if ENABLE_IMAGES if ENABLE_SIXEL
dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c image-fallback.c dist_tmux_SOURCES += image.c image-sixel.c
endif endif
# Add bits for fuzzing if enabled. # Add bits for fuzzing if enabled.

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ /* $OpenBSD: arguments.c,v 1.67 2026/08/25 06:04:33 nicm Exp $ */
/* /*
* Copyright (c) 2010 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2010 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -762,8 +762,6 @@ args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx,
cmdq_error(item, "%s", error); cmdq_error(item, "%s", error);
free(error); free(error);
} }
else
cmdlist->references++;
args_make_commands_free(state); args_make_commands_free(state);
return (cmdlist); return (cmdlist);
} }
@@ -827,8 +825,10 @@ args_make_commands(struct args_command_state *state, int argc, char **argv,
int i; int i;
if (state->cmdlist != NULL) { if (state->cmdlist != NULL) {
if (argc == 0) if (argc == 0) {
state->cmdlist->references++;
return (state->cmdlist); return (state->cmdlist);
}
return (cmd_list_copy(state->cmdlist, argc, argv)); return (cmd_list_copy(state->cmdlist, argc, argv));
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ /* $OpenBSD: cmd-command-prompt.c,v 1.76 2026/08/25 06:04:33 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -242,9 +242,11 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s,
} else if (item == NULL) { } else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL); new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else { } else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
} }
cmd_free_argv(argc, argv); cmd_free_argv(argc, argv);

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */ /* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -591,6 +591,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
server_client_clear_overlay(tc); server_client_clear_overlay(tc);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL);
if (!modify && tc->overlay_draw != NULL) if (!modify && tc->overlay_draw != NULL)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ /* $OpenBSD: cmd-if-shell.c,v 1.87 2026/08/25 06:04:33 nicm Exp $ */
/* /*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org> * Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -98,6 +98,7 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
@@ -164,9 +165,11 @@ cmd_if_shell_callback(struct job *job)
} else if (item == NULL) { } else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL); new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else { } else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
} }
out: out:

View File

@@ -300,7 +300,6 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
struct window_pane *wp; struct window_pane *wp;
struct layout_cell *lc; struct layout_cell *lc;
int y, ly, x, lx; int y, ly, x, lx;
int old_xoff, old_yoff, old_sx, old_sy;
wp = cmd_mouse_pane(m, NULL, &wl); wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) { if (wp == NULL) {
@@ -322,17 +321,10 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1; ly = m->statusat - 1;
if (x != lx || y != ly) { if (x != lx || y != ly) {
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
lc->g.xoff += x - lx; lc->g.xoff += x - lx;
lc->g.yoff += y - ly; lc->g.yoff += y - ly;
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w); server_redraw_window_borders(w);
} }
} }

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> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -60,7 +60,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct session *s = target->s; struct session *s = target->s;
struct winlink *wl = target->wl, *new_wl = NULL; struct winlink *wl = target->wl, *new_wl = NULL;
int idx = target->idx, before, count = args_count(args); int idx = target->idx, before;
int count = args_count(args);
char *cause = NULL, *cp, *expanded, *wname = NULL; char *cause = NULL, *cp, *expanded, *wname = NULL;
const char *template, *name; const char *template, *name;
struct cmd_find_state fs; struct cmd_find_state fs;
@@ -74,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 * If -S is given, select an existing window instead of creating a
* name already exists, select it. * 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'); name = args_get(args, 'n');
if (name != NULL) { if (name != NULL) {
@@ -88,32 +91,39 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
wname = clean_name(expanded, 0); wname = clean_name(expanded, 0);
free(expanded); free(expanded);
} }
if (args_has(args, 'S') && wname != NULL && target->idx == -1) { if (args_has(args, 'S')) {
expanded = format_single(item, wname, c, s, NULL, NULL); if (idx != -1)
RB_FOREACH(wl, winlinks, &s->windows) { new_wl = winlink_find_by_index(&s->windows, idx);
if (strcmp(wl->window->name, expanded) != 0) else if (wname != NULL) {
continue; expanded = format_single(item, wname, c, s, NULL, NULL);
if (new_wl == NULL) { RB_FOREACH(wl, winlinks, &s->windows) {
new_wl = wl; if (strcmp(wl->window->name, expanded) != 0)
continue; 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); free(expanded);
return (CMD_RETURN_ERROR);
} }
free(expanded); }
if (new_wl != NULL) {
free(wname); /* Found an existing window to select instead of creating a new one. */
if (args_has(args, 'd')) if (new_wl != NULL) {
return (CMD_RETURN_NORMAL); free(wname);
if (session_set_current(s, new_wl) == 0) if (args_has(args, 'd'))
server_redraw_session(s);
if (c != NULL && c->session != NULL)
s->curw->window->latest = c;
recalculate_sizes();
return (CMD_RETURN_NORMAL); 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'); before = args_has(args, 'b');

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ /* $OpenBSD: cmd-parse.y,v 1.59 2026/08/31 07:51:56 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -802,6 +802,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd,
if (last == NULL) { if (last == NULL) {
pr->status = CMD_PARSE_SUCCESS; pr->status = CMD_PARSE_SUCCESS;
pr->cmdlist = cmd_list_new(); pr->cmdlist = cmd_list_new();
cmd_parse_free_commands(cmds);
return (1); return (1);
} }
@@ -814,6 +815,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd,
pi->flags |= CMD_PARSE_NOALIAS; pi->flags |= CMD_PARSE_NOALIAS;
cmd_parse_build_commands(cmds, pi, pr); cmd_parse_build_commands(cmds, pi, pr);
pi->flags &= ~CMD_PARSE_NOALIAS; pi->flags &= ~CMD_PARSE_NOALIAS;
cmd_parse_free_commands(cmds);
return (1); return (1);
} }

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 nicm Exp $ */ /* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */
/* /*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org> * Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -233,9 +233,11 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg)
} else if (item == NULL) { } else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL); new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else { } else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
} }
if (cdata->item != NULL) if (cdata->item != NULL)

View File

@@ -360,7 +360,6 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
enum pane_lines lines; enum pane_lines lines;
u_int sx, sy; u_int sx, sy;
int x, y, xoff, yoff, border; int x, y, xoff, yoff, border;
int old_xoff, old_yoff, old_sx, old_sy;
if (c->tty.mouse_last_pane == -1) if (c->tty.mouse_last_pane == -1)
return; return;
@@ -416,15 +415,8 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
if (sy < PANE_MINIMUM) if (sy < PANE_MINIMUM)
sy = PANE_MINIMUM; sy = PANE_MINIMUM;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
layout_set_size(lc, sx, sy, xoff, yoff); layout_set_size(lc, sx, sy, xoff, yoff);
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w); server_redraw_window_borders(w);
} }

View File

@@ -530,35 +530,15 @@ if test "x$enable_cgroups" = xyes; then
fi fi
fi fi
# Enable image support. # Enable sixel support.
AC_ARG_ENABLE(
images,
AS_HELP_STRING(--enable-images, enable Kitty/SIXEL/ASCII images)
)
# Keep the old option as an alias for one compatibility cycle.
AC_ARG_ENABLE( AC_ARG_ENABLE(
sixel, sixel,
AS_HELP_STRING(--enable-sixel, deprecated alias for --enable-images) AS_HELP_STRING(--enable-sixel, enable sixel images)
) )
if test "x$enable_sixel" = xyes; then if test "x$enable_sixel" = xyes; then
AC_MSG_WARN([--enable-sixel is deprecated, use --enable-images instead to enable.]) AC_DEFINE(ENABLE_SIXEL)
enable_images=yes
fi fi
if test "x$enable_images" = xyes; then AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes])
PKG_CHECK_MODULES(
IMAGE,
[libpng >= 1.6 zlib],
[
AM_CPPFLAGS="$IMAGE_CFLAGS $AM_CPPFLAGS"
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$IMAGE_LIBS $LIBS"
],
AC_MSG_ERROR("image support requires libpng 1.6 and zlib")
)
AC_DEFINE(ENABLE_IMAGES)
fi
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
# Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well. # Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well.
AC_MSG_CHECKING(for b64_ntop) AC_MSG_CHECKING(for b64_ntop)
@@ -1157,10 +1137,10 @@ if test "x$enable_asan" = xyes; then
else else
AC_MSG_NOTICE([ASAN: off]) AC_MSG_NOTICE([ASAN: off])
fi fi
if test "x$enable_images" = xyes; then if test "x$enable_sixel" = xyes; then
AC_MSG_NOTICE([images: on]) AC_MSG_NOTICE([SIXEL: on])
else else
AC_MSG_NOTICE([images: off]) AC_MSG_NOTICE([SIXEL: off])
fi fi
if test "x$enable_debug" = xyes; then if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on]) AC_MSG_NOTICE([debug: on])

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> * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -455,8 +455,12 @@ control_flush_deferred(struct client *c)
void void
control_write(struct client *c, const char *fmt, ...) control_write(struct client *c, const char *fmt, ...)
{ {
va_list ap; struct control_state *cs = c->control_state;
char *line; char *line;
va_list ap;
if (cs == NULL)
return;
va_start(ap, fmt); va_start(ap, fmt);
xvasprintf(&line, fmt, ap); 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; struct control_state *cs = c->control_state;
char *line; char *line;
if (cs == NULL)
return;
if (strcmp(guard, "begin") == 0) if (strcmp(guard, "begin") == 0)
cs->guard_depth++; cs->guard_depth++;
@@ -503,6 +510,9 @@ control_notify_write(struct client *c, const char *fmt, ...)
va_list ap; va_list ap;
char *line; char *line;
if (cs == NULL)
return;
va_start(ap, fmt); va_start(ap, fmt);
xvasprintf(&line, fmt, ap); xvasprintf(&line, fmt, ap);
va_end(ap); va_end(ap);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */ /* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1887,6 +1887,37 @@ format_cb_cursor_blinking(struct format_tree *ft)
return (NULL); return (NULL);
} }
/* Callback for history_added. */
static void *
format_cb_history_added(struct format_tree *ft)
{
if (ft->wp != NULL)
return (format_printf("%u", ft->wp->base.grid->scroll_added));
return (NULL);
}
/* Callback for history_collected. */
static void *
format_cb_history_collected(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
if (wp != NULL)
return (format_printf("%u", wp->base.grid->scroll_collected));
return (NULL);
}
/* Callback for history_generation. */
static void *
format_cb_history_generation(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
if (wp != NULL)
return (format_printf("%u", wp->base.grid->scroll_generation));
return (NULL);
}
/* Callback for history_limit. */ /* Callback for history_limit. */
static void * static void *
format_cb_history_limit(struct format_tree *ft) format_cb_history_limit(struct format_tree *ft)
@@ -2276,6 +2307,19 @@ format_cb_pane_last_output_time(struct format_tree *ft)
return (NULL); return (NULL);
} }
/* Callback for pane_output_generation. */
static void *
format_cb_pane_output_generation(struct format_tree *ft)
{
unsigned long long value;
if (ft->wp != NULL) {
value = ft->wp->output_generation;
return (format_printf("%llu", value));
}
return (NULL);
}
/* Callback for pane_last_prompt_time. */ /* Callback for pane_last_prompt_time. */
static void * static void *
format_cb_pane_last_prompt_time(struct format_tree *ft) format_cb_pane_last_prompt_time(struct format_tree *ft)
@@ -3018,17 +3062,6 @@ format_cb_sixel_support(__unused struct format_tree *ft)
#endif #endif
} }
/* Callback for image_support. */
static void *
format_cb_image_support(__unused struct format_tree *ft)
{
#ifdef ENABLE_IMAGES
return (xstrdup("1"));
#else
return (xstrdup("0"));
#endif
}
/* Callback for active_window_index. */ /* Callback for active_window_index. */
static void * static void *
format_cb_active_window_index(struct format_tree *ft) format_cb_active_window_index(struct format_tree *ft)
@@ -3674,12 +3707,21 @@ static const struct format_table_entry format_table[] = {
{ "cursor_y", FORMAT_TABLE_STRING, { "cursor_y", FORMAT_TABLE_STRING,
format_cb_cursor_y format_cb_cursor_y
}, },
{ "history_added", FORMAT_TABLE_STRING,
format_cb_history_added
},
{ "history_all_bytes", FORMAT_TABLE_STRING, { "history_all_bytes", FORMAT_TABLE_STRING,
format_cb_history_all_bytes format_cb_history_all_bytes
}, },
{ "history_bytes", FORMAT_TABLE_STRING, { "history_bytes", FORMAT_TABLE_STRING,
format_cb_history_bytes format_cb_history_bytes
}, },
{ "history_collected", FORMAT_TABLE_STRING,
format_cb_history_collected
},
{ "history_generation", FORMAT_TABLE_STRING,
format_cb_history_generation
},
{ "history_limit", FORMAT_TABLE_STRING, { "history_limit", FORMAT_TABLE_STRING,
format_cb_history_limit format_cb_history_limit
}, },
@@ -3692,9 +3734,6 @@ static const struct format_table_entry format_table[] = {
{ "host_short", FORMAT_TABLE_STRING, { "host_short", FORMAT_TABLE_STRING,
format_cb_host_short format_cb_host_short
}, },
{ "image_support", FORMAT_TABLE_STRING,
format_cb_image_support
},
{ "insert_flag", FORMAT_TABLE_STRING, { "insert_flag", FORMAT_TABLE_STRING,
format_cb_insert_flag format_cb_insert_flag
}, },
@@ -3863,6 +3902,9 @@ static const struct format_table_entry format_table[] = {
{ "pane_mode", FORMAT_TABLE_STRING, { "pane_mode", FORMAT_TABLE_STRING,
format_cb_pane_mode format_cb_pane_mode
}, },
{ "pane_output_generation", FORMAT_TABLE_STRING,
format_cb_pane_output_generation
},
{ "pane_path", FORMAT_TABLE_STRING, { "pane_path", FORMAT_TABLE_STRING,
format_cb_pane_path format_cb_pane_path
}, },

122
grid.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */ /* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -86,9 +86,6 @@ grid_check_is_clear(struct grid *gd)
assert(gl->extdsize == 0); assert(gl->extdsize == 0);
assert(gl->flags == 0); assert(gl->flags == 0);
assert(gl->time == 0); assert(gl->time == 0);
#ifdef ENABLE_IMAGES
assert(gl->images == NULL);
#endif
} }
} }
#else #else
@@ -356,9 +353,6 @@ grid_free_line(struct grid *gd, u_int py)
assert(gl->cellsize == 0 || gl->celldata != NULL); assert(gl->cellsize == 0 || gl->celldata != NULL);
#endif #endif
#ifdef ENABLE_IMAGES
image_grid_free_line(gd, gl);
#endif
free(gl->celldata); free(gl->celldata);
free(gl->extddata); free(gl->extddata);
memset(gl, 0, sizeof *gl); memset(gl, 0, sizeof *gl);
@@ -403,9 +397,6 @@ void
grid_destroy(struct grid *gd) grid_destroy(struct grid *gd)
{ {
grid_free_lines(gd, 0, gd->hsize + gd->sy); grid_free_lines(gd, 0, gd->hsize + gd->sy);
#ifdef ENABLE_IMAGES
image_grid_free(gd);
#endif
free(gd->linedata); free(gd->linedata);
free(gd); free(gd);
} }
@@ -738,9 +729,6 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
if (nx == 0 || ny == 0) if (nx == 0 || ny == 0)
return; return;
#ifdef ENABLE_IMAGES
image_grid_damage(gd, px, py, nx, ny);
#endif
if (px == 0 && nx == gd->sx) { if (px == 0 && nx == gd->sx) {
grid_clear_lines(gd, py, ny, bg); grid_clear_lines(gd, py, ny, bg);
@@ -776,10 +764,6 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
void void
grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg) grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
{ {
struct grid_line *gl;
#ifdef ENABLE_IMAGES
struct image_line *images;
#endif
u_int yy; u_int yy;
if (ny == 0) if (ny == 0)
@@ -791,18 +775,8 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
return; return;
for (yy = py; yy < py + ny; yy++) { for (yy = py; yy < py + ny; yy++) {
gl = &gd->linedata[yy]; grid_free_line(gd, yy);
#ifdef ENABLE_IMAGES
image_grid_damage(gd, 0, yy, gd->sx, 1);
images = gl->images;
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
grid_empty_line(gd, yy, bg); grid_empty_line(gd, yy, bg);
#ifdef ENABLE_IMAGES
gl->images = images;
#endif
} }
if (py != 0) if (py != 0)
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED; gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
@@ -867,9 +841,6 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx,
grid_expand_line(gd, py, px + nx, 8); grid_expand_line(gd, py, px + nx, 8);
grid_expand_line(gd, py, dx + nx, 8); grid_expand_line(gd, py, dx + nx, 8);
#ifdef ENABLE_IMAGES
image_grid_move_cells(gd, dx, px, py, nx);
#endif
memmove(&gl->celldata[dx], &gl->celldata[px], memmove(&gl->celldata[dx], &gl->celldata[px],
nx * sizeof *gl->celldata); nx * sizeof *gl->celldata);
if (dx + nx > gl->cellused) if (dx + nx > gl->cellused)
@@ -1242,6 +1213,7 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx,
grid_get_cell(gd, xx, py, &gc); grid_get_cell(gd, xx, py, &gc);
if (gc.flags & GRID_FLAG_PADDING) if (gc.flags & GRID_FLAG_PADDING)
continue; continue;
if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) { if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) {
grid_string_cells_code(*lastgc, &gc, code, sizeof code, grid_string_cells_code(*lastgc, &gc, code, sizeof code,
flags, s, &has_link); flags, s, &has_link);
@@ -1308,18 +1280,11 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
{ {
struct grid_line *dstl, *srcl; struct grid_line *dstl, *srcl;
u_int yy; u_int yy;
#ifdef ENABLE_IMAGES
u_int original_dy, original_sy;
#endif
if (dy + ny > dst->hsize + dst->sy) if (dy + ny > dst->hsize + dst->sy)
ny = dst->hsize + dst->sy - dy; ny = dst->hsize + dst->sy - dy;
if (sy + ny > src->hsize + src->sy) if (sy + ny > src->hsize + src->sy)
ny = src->hsize + src->sy - sy; ny = src->hsize + src->sy - sy;
#ifdef ENABLE_IMAGES
original_dy = dy;
original_sy = sy;
#endif
grid_free_lines(dst, dy, ny); grid_free_lines(dst, dy, ny);
for (yy = 0; yy < ny; yy++) { for (yy = 0; yy < ny; yy++) {
@@ -1327,9 +1292,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
dstl = &dst->linedata[dy]; dstl = &dst->linedata[dy];
memcpy(dstl, srcl, sizeof *dstl); memcpy(dstl, srcl, sizeof *dstl);
#ifdef ENABLE_IMAGES
dstl->images = NULL;
#endif
if (srcl->cellsize != 0) { if (srcl->cellsize != 0) {
dstl->celldata = xreallocarray(NULL, dstl->celldata = xreallocarray(NULL,
srcl->cellsize, sizeof *dstl->celldata); srcl->cellsize, sizeof *dstl->celldata);
@@ -1349,9 +1311,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
sy++; sy++;
dy++; dy++;
} }
#ifdef ENABLE_IMAGES
image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny);
#endif
} }
/* Mark line as dead. */ /* Mark line as dead. */
@@ -1362,30 +1321,6 @@ grid_reflow_dead(struct grid_line *gl)
gl->flags = GRID_LINE_DEAD; gl->flags = GRID_LINE_DEAD;
} }
/* Image rows are cell-aligned and must not be reflowed like text. */
static int
grid_reflow_has_image(struct grid_line *gl)
{
struct grid_cell gc;
u_int i;
#ifdef ENABLE_IMAGES
if (image_grid_line_has_images(gl))
return (1);
#endif
if (~gl->flags & GRID_LINE_EXTENDED)
return (0);
for (i = 0; i < gl->cellused; i++) {
grid_get_cell1(gl, i, &gc);
/* Kitty Unicode placeholder base character (U+10EEEE). */
if (gc.data.size >= 4 && gc.data.data[0] == 0xf4 &&
gc.data.data[1] == 0x8e && gc.data.data[2] == 0xbb &&
gc.data.data[3] == 0xae)
return (1);
}
return (0);
}
/* Add lines, return the first new one. */ /* Add lines, return the first new one. */
static struct grid_line * static struct grid_line *
grid_reflow_add(struct grid *gd, u_int n) grid_reflow_add(struct grid *gd, u_int n)
@@ -1448,10 +1383,6 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
break; break;
line = yy + 1 + lines; line = yy + 1 + lines;
/* Do not join wrapped text to a cell-aligned image row. */
if (grid_reflow_has_image(&gd->linedata[line]))
break;
/* If the next line is empty, skip it. */ /* If the next line is empty, skip it. */
if (~gd->linedata[line].flags & GRID_LINE_WRAPPED) if (~gd->linedata[line].flags & GRID_LINE_WRAPPED)
wrapped = 0; wrapped = 0;
@@ -1512,7 +1443,8 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
/* Remove the lines that were completely consumed. */ /* Remove the lines that were completely consumed. */
for (i = yy + 1; i < yy + 1 + lines; i++) { for (i = yy + 1; i < yy + 1 + lines; i++) {
grid_free_line(gd, i); free(gd->linedata[i].celldata);
free(gd->linedata[i].extddata);
grid_reflow_dead(&gd->linedata[i]); grid_reflow_dead(&gd->linedata[i]);
} }
@@ -1614,12 +1546,6 @@ grid_reflow(struct grid *gd, u_int sx)
if (gl->flags & GRID_LINE_DEAD) if (gl->flags & GRID_LINE_DEAD)
continue; continue;
/* Keep image layers at their original cell coordinates. */
if (grid_reflow_has_image(gl)) {
grid_reflow_move(target, gl);
continue;
}
/* /*
* Work out the width of this line. at is the point at which * Work out the width of this line. at is the point at which
* the available width is hit, and width is the full line * the available width is hit, and width is the full line
@@ -1709,7 +1635,7 @@ grid_wrap_position(struct grid *gd, u_int px, u_int py, u_int *wx, u_int *wy)
void void
grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy) grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy)
{ {
u_int yy, ay = 0; u_int yy, ay = 0, ey = gd->hsize + gd->sy - 1;
for (yy = 0; yy < gd->hsize + gd->sy - 1; yy++) { for (yy = 0; yy < gd->hsize + gd->sy - 1; yy++) {
if (ay == wy) if (ay == wy)
@@ -1723,7 +1649,7 @@ grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy)
* until we find the end or the line now containing wx. * until we find the end or the line now containing wx.
*/ */
if (wx == UINT_MAX) { if (wx == UINT_MAX) {
while (gd->linedata[yy].flags & GRID_LINE_WRAPPED) while (yy < ey && gd->linedata[yy].flags & GRID_LINE_WRAPPED)
yy++; yy++;
wx = gd->linedata[yy].cellused; wx = gd->linedata[yy].cellused;
} else { } else {
@@ -1785,21 +1711,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set)
{ {
struct grid_cell gc, tmp_gc; struct grid_cell gc, tmp_gc;
u_int pxx; u_int pxx;
int has_tab, has_space;
has_tab = (strchr(set, '\t') != NULL);
has_space = (strchr(set, ' ') != NULL);
grid_get_cell(gd, px, py, &gc); grid_get_cell(gd, px, py, &gc);
if (strchr(set, '\t')) { if (gc.flags & GRID_FLAG_PADDING) {
if (gc.flags & GRID_FLAG_PADDING) { if (!has_tab && !has_space)
pxx = px; return (0);
do pxx = px;
grid_get_cell(gd, --pxx, py, &tmp_gc); do
while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING); grid_get_cell(gd, --pxx, py, &tmp_gc);
if (tmp_gc.flags & GRID_FLAG_TAB) while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING);
return (tmp_gc.data.width - (px - pxx)); if (((has_tab || has_space) &&
} else if (gc.flags & GRID_FLAG_TAB) (tmp_gc.flags & GRID_FLAG_TAB)) ||
return (gc.data.width); (has_space && utf8_has_whitespace(&tmp_gc.data)))
} return (tmp_gc.data.width - (px - pxx));
if (gc.flags & GRID_FLAG_PADDING)
return (0); return (0);
}
if ((has_tab || has_space) && (gc.flags & GRID_FLAG_TAB))
return (gc.data.width);
if (has_space && utf8_has_whitespace(&gc.data))
return (gc.data.width == 0 ? 1 : gc.data.width);
return (utf8_cstrhas(set, &gc.data)); return (utf8_cstrhas(set, &gc.data));
} }
@@ -1826,6 +1760,8 @@ grid_line_flags_string(int flags)
strlcat(s, "START_OUTPUT,", sizeof s); strlcat(s, "START_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT) if (flags & GRID_LINE_END_OUTPUT)
strlcat(s, "END_OUTPUT,", sizeof s); 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) if (flags & GRID_LINE_HYPERLINK)
strlcat(s, "HYPERLINK,", sizeof s); strlcat(s, "HYPERLINK,", sizeof s);
if (*s == '\0') if (*s == '\0')

View File

@@ -1,464 +0,0 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
/* Character-cell fallback for clients without a graphical image protocol. */
enum image_glyph_detail {
IMAGE_GLYPH_ASCII,
IMAGE_GLYPH_SHADE5,
IMAGE_GLYPH_SHADE8,
IMAGE_GLYPH_HALF,
IMAGE_GLYPH_QUADRANT,
IMAGE_GLYPH_SEXTANT
};
enum image_glyph_palette {
IMAGE_GLYPH_PALETTE_8,
IMAGE_GLYPH_PALETTE_16,
IMAGE_GLYPH_PALETTE_256,
IMAGE_GLYPH_PALETTE_RGB
};
struct image_rgb {
u_char r;
u_char g;
u_char b;
};
struct image_glyph_data {
u_char *shade5;
u_char *shade8;
};
static const struct image_rgb image_ansi_colours[16] = {
{ 0x00, 0x00, 0x00 }, { 0x80, 0x00, 0x00 },
{ 0x00, 0x80, 0x00 }, { 0x80, 0x80, 0x00 },
{ 0x00, 0x00, 0x80 }, { 0x80, 0x00, 0x80 },
{ 0x00, 0x80, 0x80 }, { 0xc0, 0xc0, 0xc0 },
{ 0x80, 0x80, 0x80 }, { 0xff, 0x00, 0x00 },
{ 0x00, 0xff, 0x00 }, { 0xff, 0xff, 0x00 },
{ 0x00, 0x00, 0xff }, { 0xff, 0x00, 0xff },
{ 0x00, 0xff, 0xff }, { 0xff, 0xff, 0xff }
};
static u_int
image_glyph_distance(struct image_rgb a, struct image_rgb b)
{
int r = a.r - b.r, g = a.g - b.g, bl = a.b - b.b;
return (r * r + g * g + bl * bl);
}
static int
image_glyph_low_saturation(struct image_rgb colour)
{
u_int minimum, maximum;
minimum = maximum = colour.r;
if (colour.g < minimum)
minimum = colour.g;
if (colour.b < minimum)
minimum = colour.b;
if (colour.g > maximum)
maximum = colour.g;
if (colour.b > maximum)
maximum = colour.b;
return (maximum == 0 || (maximum - minimum) * 4 <= maximum);
}
static u_int
image_glyph_nearest_ansi(struct image_rgb colour, u_int colours)
{
u_int i, distance, best_distance = UINT_MAX, best = 0;
int neutral = image_glyph_low_saturation(colour);
for (i = 0; i < colours; i++) {
if (neutral && i != 0 && i != 7 &&
(colours != 16 || (i != 8 && i != 15)))
continue;
distance = image_glyph_distance(colour, image_ansi_colours[i]);
if (distance < best_distance) {
best_distance = distance;
best = i;
}
}
return (best);
}
static struct image_rgb
image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette,
int *output)
{
struct image_rgb result;
int value;
u_int index;
if (palette == IMAGE_GLYPH_PALETTE_RGB) {
*output = colour_join_rgb(colour.r, colour.g, colour.b);
return (colour);
}
if (palette == IMAGE_GLYPH_PALETTE_256) {
*output = colour_find_rgb(colour.r, colour.g, colour.b);
value = colour_256toRGB(*output);
colour_split_rgb(value, &result.r, &result.g, &result.b);
return (result);
}
index = image_glyph_nearest_ansi(colour,
palette == IMAGE_GLYPH_PALETTE_8 ? 8 : 16);
result = image_ansi_colours[index];
if (index < 8)
*output = index;
else
*output = 90 + index - 8;
return (result);
}
static void
image_glyph_fit_colours(const struct image_rgb *samples, u_int count,
struct image_rgb centres[2])
{
u_int sum[2][3], counts[2], group, i, j, iteration, distance;
u_int maximum = 0, first = 0, second = 0;
for (i = 0; i < count; i++) {
for (j = i + 1; j < count; j++) {
distance = image_glyph_distance(samples[i], samples[j]);
if (distance > maximum) {
maximum = distance;
first = i;
second = j;
}
}
}
centres[0] = samples[first];
centres[1] = samples[second];
for (iteration = 0; iteration < 4; iteration++) {
memset(sum, 0, sizeof sum);
memset(counts, 0, sizeof counts);
for (i = 0; i < count; i++) {
group = image_glyph_distance(samples[i], centres[1]) <
image_glyph_distance(samples[i], centres[0]);
sum[group][0] += samples[i].r;
sum[group][1] += samples[i].g;
sum[group][2] += samples[i].b;
counts[group]++;
}
for (i = 0; i < 2; i++) {
if (counts[i] == 0)
continue;
centres[i].r = (sum[i][0] + counts[i] / 2) / counts[i];
centres[i].g = (sum[i][1] + counts[i] / 2) / counts[i];
centres[i].b = (sum[i][2] + counts[i] / 2) / counts[i];
}
}
}
static int
image_glyph_set_acs(struct tty *tty, struct utf8_data *data, u_char key)
{
struct utf8_data *ud;
const char *s = tty_acs_get(tty, key);
if (s == NULL)
return (0);
ud = utf8_fromcstr(s);
if (ud[0].size == 0) {
free(ud);
return (0);
}
memcpy(data, &ud[0], sizeof *data);
free(ud);
return (1);
}
static u_char
image_glyph_block_key(enum image_glyph_detail detail, u_int mask)
{
static const u_char half[4] = {
0, TTY_ACS_IMAGE_HALF_UPPER, TTY_ACS_IMAGE_HALF_LOWER,
TTY_ACS_IMAGE_BLOCK
};
static const u_char quadrant[16] = {
0, TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, TTY_ACS_IMAGE_HALF_UPPER,
TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, TTY_ACS_IMAGE_HALF_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_HALF_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
TTY_ACS_IMAGE_HALF_LOWER,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_BLOCK
};
if (detail == IMAGE_GLYPH_HALF)
return (half[mask]);
if (detail == IMAGE_GLYPH_QUADRANT)
return (quadrant[mask]);
if (mask == 0)
return (0);
if (mask == 21)
return (TTY_ACS_IMAGE_HALF_LEFT);
if (mask == 42)
return (TTY_ACS_IMAGE_HALF_RIGHT);
if (mask == 63)
return (TTY_ACS_IMAGE_BLOCK);
return (tty_acs_image_sextant(mask));
}
static u_char *
image_glyph_make_shades(struct image *im, u_int levels)
{
struct image_glyph_data *data = im->fallback_data;
const struct image_cell *cell;
int *values, value, represented, error;
size_t cells, index;
u_int x, y, scan, level, minimum = 255, maximum = 0;
int reverse;
u_char *result;
if (data == NULL) {
data = im->fallback_data = xcalloc(1, sizeof *data);
}
result = (levels == 5 ? data->shade5 : data->shade8);
if (result != NULL)
return (result);
cells = (size_t)im->sx * im->sy;
values = xcalloc(cells, sizeof *values);
result = xcalloc(cells, 1);
for (y = 0; y < im->sy; y++) {
for (x = 0; x < im->sx; x++) {
index = (size_t)y * im->sx + x;
cell = image_get_cell(im, x, y);
value = cell->whole.brightness;
if ((u_int)value < minimum)
minimum = value;
if ((u_int)value > maximum)
maximum = value;
values[index] = value;
}
}
if (maximum > minimum) {
for (index = 0; index < cells; index++)
values[index] = (values[index] - minimum) * 255 /
(maximum - minimum);
}
for (y = 0; y < im->sy; y++) {
reverse = (y & 1);
for (scan = 0; scan < im->sx; scan++) {
x = (reverse ? im->sx - scan - 1 : scan);
index = (size_t)y * im->sx + x;
value = values[index];
if (value < 0)
value = 0;
if (value > 255)
value = 255;
level = (value * (levels - 1) + 127) / 255;
result[index] = level;
represented = level * 255 / (levels - 1);
error = value - represented;
if (!reverse && x + 1 < im->sx)
values[index + 1] += error * 7 / 16;
if (reverse && x != 0)
values[index - 1] += error * 7 / 16;
if (y + 1 == im->sy)
continue;
if (!reverse && x != 0)
values[index + im->sx - 1] += error * 3 / 16;
if (reverse && x + 1 < im->sx)
values[index + im->sx + 1] += error * 3 / 16;
values[index + im->sx] += error * 5 / 16;
if (!reverse && x + 1 < im->sx)
values[index + im->sx + 1] += error / 16;
if (reverse && x != 0)
values[index + im->sx - 1] += error / 16;
}
}
free(values);
if (levels == 5)
data->shade5 = result;
else
data->shade8 = result;
return (result);
}
static enum image_glyph_palette
image_glyph_get_palette(struct tty *tty)
{
int colours;
if (tty->term->flags & TERM_RGBCOLOURS)
return (IMAGE_GLYPH_PALETTE_RGB);
if (tty->term->flags & TERM_256COLOURS)
return (IMAGE_GLYPH_PALETTE_256);
colours = tty_term_number(tty->term, TTYC_COLORS);
if (colours >= 256)
return (IMAGE_GLYPH_PALETTE_256);
if (colours >= 16)
return (IMAGE_GLYPH_PALETTE_16);
return (IMAGE_GLYPH_PALETTE_8);
}
static enum image_glyph_detail
image_glyph_get_detail(struct tty *tty, enum image_glyph_palette palette)
{
if (tty_acs_needed(tty))
return (IMAGE_GLYPH_ASCII);
if (tty->term->flags & TERM_IMAGE_SEXTANTS)
return (IMAGE_GLYPH_SEXTANT);
if (tty->term->flags & TERM_IMAGE_QUADRANTS)
return (IMAGE_GLYPH_QUADRANT);
if (palette != IMAGE_GLYPH_PALETTE_8)
return (IMAGE_GLYPH_HALF);
if (tty_term_has(tty->term, TTYC_NOBR))
return (IMAGE_GLYPH_SHADE5);
return (IMAGE_GLYPH_SHADE8);
}
static void
image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y,
enum image_glyph_detail detail, enum image_glyph_palette palette,
struct grid_cell *out)
{
const struct image_cell *cell = image_get_cell(im, x, y);
struct image_rgb samples[6], centres[2], quantized[2];
u_int columns, rows, sx, sy, ix, iy, i, n, mask;
u_int x0, x1, y0, y1, red, green, blue, count;
int colours[2];
u_char key;
columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2);
rows = (detail == IMAGE_GLYPH_HALF ? 2 :
detail == IMAGE_GLYPH_QUADRANT ? 2 : 3);
i = 0;
for (sy = 0; sy < rows; sy++) {
for (sx = 0; sx < columns; sx++) {
x0 = sx * IMAGE_SAMPLE_COLUMNS / columns;
x1 = (sx + 1) * IMAGE_SAMPLE_COLUMNS / columns;
y0 = sy * IMAGE_SAMPLE_ROWS / rows;
y1 = (sy + 1) * IMAGE_SAMPLE_ROWS / rows;
red = green = blue = count = 0;
for (iy = y0; iy < y1; iy++) {
for (ix = x0; ix < x1; ix++) {
red += cell->samples[iy][ix].red;
green += cell->samples[iy][ix].green;
blue += cell->samples[iy][ix].blue;
count++;
}
}
samples[i].r = red / count;
samples[i].g = green / count;
samples[i].b = blue / count;
i++;
}
}
n = columns * rows;
image_glyph_fit_colours(samples, n, centres);
quantized[0] = image_glyph_quantize(centres[0], palette, &colours[0]);
quantized[1] = image_glyph_quantize(centres[1], palette, &colours[1]);
mask = 0;
for (i = 0; i < n; i++) {
if (image_glyph_distance(samples[i], quantized[1]) <
image_glyph_distance(samples[i], quantized[0]))
mask |= (1U << i);
}
key = image_glyph_block_key(detail, mask);
if (key == 0)
utf8_set(&out->data, ' ');
else if (!image_glyph_set_acs(tty, &out->data, key))
utf8_set(&out->data, ' ');
out->fg = colours[1];
out->bg = colours[0];
}
void
image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y,
const struct grid_cell *gc, struct grid_cell *out,
__unused const struct tty_style_ctx *style_ctx)
{
static const char ascii[] = " .:-=+*#%@";
static const u_char shades[5] = { 0, TTY_ACS_IMAGE_SHADE_LIGHT,
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_DARK,
TTY_ACS_IMAGE_BLOCK };
static const u_char bold_shades[8] = { 0,
TTY_ACS_IMAGE_SHADE_LIGHT, TTY_ACS_IMAGE_SHADE_LIGHT,
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_MEDIUM,
TTY_ACS_IMAGE_SHADE_DARK, TTY_ACS_IMAGE_BLOCK,
TTY_ACS_IMAGE_BLOCK };
const struct image_cell *cell;
enum image_glyph_palette palette;
enum image_glyph_detail detail;
u_char *levels, key;
u_int level = 0;
memcpy(out, gc, sizeof *out);
cell = image_get_cell(im, x, y);
if (cell == NULL) {
utf8_set(&out->data, ' ');
return;
}
palette = image_glyph_get_palette(tty);
detail = image_glyph_get_detail(tty, palette);
if (detail == IMAGE_GLYPH_ASCII) {
level = cell->whole.brightness * (sizeof ascii - 2) / 255;
utf8_set(&out->data, ascii[level]);
return;
}
if (detail == IMAGE_GLYPH_SHADE5 || detail == IMAGE_GLYPH_SHADE8) {
levels = image_glyph_make_shades(im,
detail == IMAGE_GLYPH_SHADE5 ? 5 : 8);
level = levels[(size_t)y * im->sx + x];
key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] :
bold_shades[level]);
if (key == 0)
utf8_set(&out->data, ' ');
else if (!image_glyph_set_acs(tty, &out->data, key))
utf8_set(&out->data, ' ');
out->fg = 7;
out->bg = 0;
out->attr &= ~GRID_ATTR_BRIGHT;
if (detail == IMAGE_GLYPH_SHADE8 &&
(level == 2 || level == 4 || level == 7))
out->attr |= GRID_ATTR_BRIGHT;
return;
}
image_glyph_block(tty, im, x, y, detail, palette, out);
}
void
image_free_fallback(struct image *im)
{
struct image_glyph_data *data = im->fallback_data;
if (data == NULL)
return;
free(data->shade5);
free(data->shade8);
free(data);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1725
image.c

File diff suppressed because it is too large Load Diff

182
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */ /* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -145,9 +145,6 @@ struct input_ctx {
*/ */
struct evbuffer *since_ground; struct evbuffer *since_ground;
struct event ground_timer; struct event ground_timer;
#ifdef ENABLE_IMAGES
void *kitty_state;
#endif
}; };
/* Helper functions. */ /* Helper functions. */
@@ -186,9 +183,6 @@ static void input_enter_osc(struct input_ctx *);
static void input_exit_osc(struct input_ctx *); static void input_exit_osc(struct input_ctx *);
static void input_enter_apc(struct input_ctx *); static void input_enter_apc(struct input_ctx *);
static void input_exit_apc(struct input_ctx *); static void input_exit_apc(struct input_ctx *);
#ifdef ENABLE_IMAGES
static int input_handle_kitty(struct input_ctx *, const u_char *, size_t);
#endif
static void input_enter_rename(struct input_ctx *); static void input_enter_rename(struct input_ctx *);
static void input_exit_rename(struct input_ctx *); static void input_exit_rename(struct input_ctx *);
@@ -922,9 +916,6 @@ input_free(struct input_ctx *ictx)
event_del(&ictx->request_timer); event_del(&ictx->request_timer);
free(ictx->input_buf); free(ictx->input_buf);
#ifdef ENABLE_IMAGES
kitty_free_state(ictx->kitty_state);
#endif
evbuffer_free(ictx->since_ground); evbuffer_free(ictx->since_ground);
event_del(&ictx->ground_timer); event_del(&ictx->ground_timer);
@@ -947,7 +938,6 @@ input_reset(struct input_ctx *ictx, int clear)
screen_write_start_pane(sctx, wp, &wp->base); screen_write_start_pane(sctx, wp, &wp->base);
else else
screen_write_start(sctx, &wp->base); screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
screen_write_reset(sctx); screen_write_reset(sctx);
screen_write_stop(sctx); screen_write_stop(sctx);
} }
@@ -1059,6 +1049,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
if (len == 0) if (len == 0)
return; return;
wp->output_generation++;
window_update_activity(wp->window); window_update_activity(wp->window);
if (~wp->flags & PANE_ACTIVITY) { if (~wp->flags & PANE_ACTIVITY) {
wp->flags |= PANE_ACTIVITY; wp->flags |= PANE_ACTIVITY;
@@ -1075,7 +1066,6 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
screen_write_start_pane(sctx, wp, &wp->base); screen_write_start_pane(sctx, wp, &wp->base);
else else
screen_write_start(sctx, &wp->base); screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id, log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id,
ictx->state->name, len, (int)len, buf); ictx->state->name, len, (int)len, buf);
@@ -1095,7 +1085,6 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s,
return; return;
screen_write_start_callback(sctx, s, cb, arg); screen_write_start_callback(sctx, s, cb, arg);
sctx->flags |= SCREEN_WRITE_INPUT;
input_parse(ictx, buf, len); input_parse(ictx, buf, len);
screen_write_stop(sctx); screen_write_stop(sctx);
} }
@@ -1595,7 +1584,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1: case -1:
break; break;
case 0: case 0:
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
input_reply(ictx, 1, "\033[?1;2;4c"); input_reply(ictx, 1, "\033[?1;2;4c");
#else #else
input_reply(ictx, 1, "\033[?1;2c"); input_reply(ictx, 1, "\033[?1;2c");
@@ -2113,7 +2102,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx) input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{ {
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
int n, m, o; int n, m, o;
if (ictx->param_list_len > 3) if (ictx->param_list_len > 3)
@@ -2638,10 +2627,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
const char prefix[] = "tmux;"; const char prefix[] = "tmux;";
const u_int prefixlen = (sizeof prefix) - 1; const u_int prefixlen = (sizeof prefix) - 1;
long long allow_passthrough = 0; long long allow_passthrough = 0;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
struct window *w; struct window *w;
struct sixel_image *si; struct sixel_image *si;
int p1, p2; int p2;
#endif #endif
if (wp == NULL) if (wp == NULL)
@@ -2654,18 +2643,15 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0); return (0);
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) { if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
w = wp->window; w = wp->window;
if (input_split(ictx) != 0) if (input_split(ictx) != 0)
return (0); return (0);
p1 = input_get(ictx, 0, 0, 0);
if (p1 == -1)
p1 = 0;
p2 = input_get(ictx, 1, 0, 0); p2 = input_get(ictx, 1, 0, 0);
if (p2 == -1) if (p2 == -1)
p2 = 0; p2 = 0;
si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel); si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel);
if (si != NULL) if (si != NULL)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg); screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
} }
@@ -2804,57 +2790,6 @@ input_enter_apc(struct input_ctx *ictx)
} }
/* APC terminator (ST) received. */ /* APC terminator (ST) received. */
#ifdef ENABLE_IMAGES
static int
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
{
struct screen_write_ctx *sctx = &ictx->ctx;
struct window_pane *wp = ictx->wp;
struct image *im;
u_int image_id = 0, replace_id = 0, quiet = 0;
u_int placement_id = 0;
int32_t z = 0;
char action = '\0', delete = '\0';
int status;
if (wp == NULL)
return (0);
im = kitty_parse_image(&ictx->kitty_state, buf, len,
wp->window->xpixel,
wp->window->ypixel, &image_id, &replace_id, &quiet, &action,
&delete, &placement_id, &z, &status);
if (status == KITTY_PARSE_MORE)
return (1);
if (status != KITTY_PARSE_OK) {
if (quiet < 2 && action != '\0') {
if (status == KITTY_PARSE_MISSING)
input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\",
image_id);
else
input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\",
image_id);
}
return (1);
}
if (replace_id != 0)
image_clear(sctx, replace_id);
if (im != NULL) {
if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else
image_write_kitty(sctx, im, ictx->cell.cell.bg, image_id,
placement_id, z);
image_free(image_get_id(im));
if (quiet == 0 && image_id != 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
} else if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
return (1);
}
#endif
static void static void
input_exit_apc(struct input_ctx *ictx) input_exit_apc(struct input_ctx *ictx)
{ {
@@ -2865,13 +2800,6 @@ input_exit_apc(struct input_ctx *ictx)
return; return;
log_debug("%s: \"%s\"", __func__, ictx->input_buf); log_debug("%s: \"%s\"", __func__, ictx->input_buf);
#ifdef ENABLE_IMAGES
if (ictx->input_len > 1 && ictx->input_buf[0] == 'G' &&
input_handle_kitty(ictx, ictx->input_buf + 1,
ictx->input_len - 1))
return;
#endif
if (wp != NULL && if (wp != NULL &&
options_get_number(wp->options, "allow-set-title") && options_get_number(wp->options, "allow-set-title") &&
screen_set_title(sctx->s, ictx->input_buf, 1)) { screen_set_title(sctx->s, ictx->input_buf, 1)) {
@@ -2932,12 +2860,6 @@ input_top_bit_set(struct input_ctx *ictx)
{ {
struct screen_write_ctx *sctx = &ictx->ctx; struct screen_write_ctx *sctx = &ictx->ctx;
struct utf8_data *ud = &ictx->utf8data; struct utf8_data *ud = &ictx->utf8data;
#ifdef ENABLE_IMAGES
struct grid_cell gc;
struct image *im;
u_int x, source_x, source_y, image_id, placement_id;
int32_t z;
#endif
ictx->flags &= ~INPUT_LAST; ictx->flags &= ~INPUT_LAST;
@@ -2965,20 +2887,6 @@ input_top_bit_set(struct input_ctx *ictx)
utf8_copy(&ictx->cell.cell.data, ud); utf8_copy(&ictx->cell.cell.data, ud);
screen_write_collect_add(sctx, &ictx->cell.cell); screen_write_collect_add(sctx, &ictx->cell.cell);
#ifdef ENABLE_IMAGES
if (sctx->s->cx != 0) {
x = sctx->s->cx - 1; /* cx-1 is the cell just written. */
grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc);
if (kitty_placeholder_to_image(ictx->kitty_state,
sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x,
&source_y, &image_id, &placement_id, &z)) {
grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc);
image_place_cell_kitty(sctx, im, x, sctx->s->cy, source_x,
source_y, image_id, placement_id, z);
}
}
#endif
utf8_copy(&ictx->last, &ictx->cell.cell.data); utf8_copy(&ictx->last, &ictx->cell.cell.data);
ictx->flags |= INPUT_LAST; ictx->flags |= INPUT_LAST;
@@ -3286,13 +3194,14 @@ input_osc_112(struct input_ctx *ictx, const char *p)
/* Parse the OSC 133 D exit status. */ /* Parse the OSC 133 D exit status. */
static int static int
input_osc_133_exit_status(const char *p) input_osc_133_exit_status(const char *p, int *present)
{ {
const char *end; const char *end;
char *copy; char *copy;
const char *errstr; char *endptr;
long long status; long long status;
*present = 0;
if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2) if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2)
return (0); return (0);
end = strchr(p + 2, ';'); end = strchr(p + 2, ';');
@@ -3306,9 +3215,11 @@ input_osc_133_exit_status(const char *p)
free(copy); free(copy);
return (0); return (0);
} }
status = strtonum(copy, 0, 255, &errstr); *present = 1;
errno = 0;
status = (int)strtol(copy, &endptr, 10);
free(copy); free(copy);
if (errstr != NULL) if (errno != 0 || endptr == copy || status < 0 || status > 255)
return (255); return (255);
return (status); return (status);
} }
@@ -3353,6 +3264,22 @@ input_fire_command_event(struct window_pane *wp, const char *name)
events_fire(name, ep); 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. */ /* Handle the OSC 133 sequence. */
static void static void
input_osc_133(struct input_ctx *ictx, const char *p) input_osc_133(struct input_ctx *ictx, const char *p)
@@ -3362,8 +3289,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
struct grid *gd = s->grid; struct grid *gd = s->grid;
u_int line = s->cy + gd->hsize; u_int line = s->cy + gd->hsize;
struct grid_line *gl = NULL; struct grid_line *gl = NULL;
const char *cp; int status, status_present;
int status;
if (line < gd->hsize + gd->sy) if (line < gd->hsize + gd->sy)
gl = grid_get_line(gd, line); gl = grid_get_line(gd, line);
@@ -3372,9 +3298,17 @@ input_osc_133(struct input_ctx *ictx, const char *p)
case 'A': case 'A':
case 'N': case 'N':
if (gl != NULL) { if (gl != NULL) {
memset(&gl->osc133_data, 0, sizeof gl->osc133_data); if (!(gl->flags & (GRID_LINE_START_PROMPT|
gl->osc133_data.prompt_col = s->cx; GRID_LINE_SECOND_PROMPT))) {
gl->flags |= GRID_LINE_START_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) { if (wp != NULL) {
wp->last_prompt_time = time(NULL); wp->last_prompt_time = time(NULL);
@@ -3383,25 +3317,33 @@ input_osc_133(struct input_ctx *ictx, const char *p)
break; break;
case 'P': case 'P':
if (gl != NULL) { if (gl != NULL) {
cp = strstr(p, ";k=s"); if (!(gl->flags & (GRID_LINE_START_PROMPT|
if (cp != NULL && (cp[4] == ';' || cp[4] == '\0')) GRID_LINE_SECOND_PROMPT))) {
gl->flags |= GRID_LINE_SECOND_PROMPT; gl->osc133_data.prompt_col = s->cx;
else if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_START_PROMPT; gl->flags |= GRID_LINE_SECOND_PROMPT;
gl->osc133_data.prompt_col = s->cx; else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 prompt at %u,%u", __func__, s->cx,
line);
}
} }
break; break;
case 'B': case 'B':
case 'I': case 'I':
if (gl != NULL) { if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) {
gl->flags |= GRID_LINE_START_COMMAND; gl->flags |= GRID_LINE_START_COMMAND;
gl->osc133_data.cmd_col = s->cx; gl->osc133_data.cmd_col = s->cx;
log_debug("%s: osc133 command at %u,%u", __func__, s->cx,
line);
} }
break; break;
case 'C': case 'C':
if (gl != NULL) { if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) {
gl->flags |= GRID_LINE_START_OUTPUT; gl->flags |= GRID_LINE_START_OUTPUT;
gl->osc133_data.out_start_col = s->cx; gl->osc133_data.out_start_col = s->cx;
log_debug("%s: osc133 output start at %u,%u", __func__, s->cx,
line);
} }
if (wp != NULL) { if (wp != NULL) {
wp->cmd_start_time = time(NULL); wp->cmd_start_time = time(NULL);
@@ -3412,7 +3354,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
} }
break; break;
case 'D': case 'D':
status = input_osc_133_exit_status(p); status = input_osc_133_exit_status(p, &status_present);
if (wp != NULL) { if (wp != NULL) {
wp->cmd_end_time = time(NULL); wp->cmd_end_time = time(NULL);
wp->flags &= ~PANE_CMDRUNNING; wp->flags &= ~PANE_CMDRUNNING;
@@ -3421,8 +3363,12 @@ input_osc_133(struct input_ctx *ictx, const char *p)
} }
if (gl != NULL) { if (gl != NULL) {
gl->flags |= GRID_LINE_END_OUTPUT; 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.out_end_col = s->cx;
gl->osc133_data.exit_status = status; gl->osc133_data.exit_status = status;
log_debug("%s: osc133 output end at %u,%u (status %d)",
__func__, s->cx, line, status);
} }
break; break;
} }

View File

@@ -52,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 Top,}' '<' {send -X history-top}" \
" '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ " '#{?#{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}" \ " '#{?#{==:#{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}" \ " '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \
@@ -575,6 +577,7 @@ key_bindings_init(void)
"bind -Tcopy-mode C-l { send -X recentre-top-bottom }", "bind -Tcopy-mode C-l { send -X recentre-top-bottom }",
"bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }", "bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }",
"bind -Tcopy-mode C-n { send -X cursor-down }", "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-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-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 -- '%%' } }", "bind -Tcopy-mode C-s { command-prompt -P -T search -ip'(search down)' -I'#{pane_search_string}' { send -X search-forward-incremental -- '%%' } }",
@@ -583,15 +586,19 @@ key_bindings_init(void)
"bind -Tcopy-mode Escape { send -X cancel }", "bind -Tcopy-mode Escape { send -X cancel }",
"bind -Tcopy-mode C-[ { send -X cancel }", "bind -Tcopy-mode C-[ { send -X cancel }",
"bind -Tcopy-mode Space { send -X page-down }", "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-reverse }",
"bind -Tcopy-mode \\; { send -X jump-again }", "bind -Tcopy-mode \\; { send -X jump-again }",
"bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }", "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 L { send -X line-numbers-toggle }",
"bind -Tcopy-mode N { send -X search-reverse }", "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 P { send -X toggle-position }",
"bind -Tcopy-mode R { send -X rectangle-toggle }", "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 T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode X { send -X set-mark }", "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 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 g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }",
"bind -Tcopy-mode n { send -X search-again }", "bind -Tcopy-mode n { send -X search-again }",
@@ -600,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 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 Home { send -X start-of-line }",
"bind -Tcopy-mode End { send -X end-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 MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode MouseDragEnd1Pane { send -X copy-pipe-and-cancel }", "bind -Tcopy-mode MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode WheelUpPane { select-pane; send -N5 -X scroll-up }", "bind -Tcopy-mode WheelUpPane { select-pane; send -N5 -X scroll-up }",
@@ -660,6 +667,8 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi Escape { send -X clear-selection }", "bind -Tcopy-mode-vi Escape { send -X clear-selection }",
"bind -Tcopy-mode-vi C-[ { 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 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 end-of-line }",
"bind -Tcopy-mode-vi , { send -X jump-reverse }", "bind -Tcopy-mode-vi , { send -X jump-reverse }",
"bind -Tcopy-mode-vi / { command-prompt -P -T search -p'(search down)' { send -X search-forward -- '%%' } }", "bind -Tcopy-mode-vi / { command-prompt -P -T search -p'(search down)' { send -X search-forward -- '%%' } }",
@@ -687,7 +696,10 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi K { send -X scroll-up }", "bind -Tcopy-mode-vi K { send -X scroll-up }",
"bind -Tcopy-mode-vi L { send -X bottom-line }", "bind -Tcopy-mode-vi L { send -X bottom-line }",
"bind -Tcopy-mode-vi M { send -X middle-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 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 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 T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode-vi V { send -X select-line }", "bind -Tcopy-mode-vi V { send -X select-line }",
@@ -715,7 +727,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi % { send -X next-matching-bracket }", "bind -Tcopy-mode-vi % { send -X next-matching-bracket }",
"bind -Tcopy-mode-vi Home { send -X start-of-line }", "bind -Tcopy-mode-vi Home { send -X start-of-line }",
"bind -Tcopy-mode-vi End { send -X end-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 MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode-vi MouseDragEnd1Pane { send -X copy-pipe-and-cancel }", "bind -Tcopy-mode-vi MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode-vi WheelUpPane { select-pane; send -N5 -X scroll-up }", "bind -Tcopy-mode-vi WheelUpPane { select-pane; send -N5 -X scroll-up }",

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.97 2026/08/20 09:19:24 nicm Exp $ */ /* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -492,6 +492,7 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
sx = PANE_MINIMUM; sx = PANE_MINIMUM;
else else
sx = sx - sb_w - sb_pad; sx = sx - sb_w - sb_pad;
wp->flags |= PANE_REDRAWSCROLLBAR;
} }
window_pane_resize(wp, sx, sy); window_pane_resize(wp, sx, sy);
@@ -499,11 +500,8 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
if (wp->xoff != old_xoff || if (wp->xoff != old_xoff ||
wp->yoff != old_yoff || wp->yoff != old_yoff ||
wp->sx != old_sx || wp->sx != old_sx ||
wp->sy != old_sy) { wp->sy != old_sy)
changed = 1; changed = 1;
if (window_pane_scrollbar_reserve(wp))
wp->flags |= PANE_REDRAWSCROLLBAR;
}
} }
if (changed) if (changed)
redraw_invalidate_scene(w); redraw_invalidate_scene(w);
@@ -541,6 +539,10 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
status = window_get_pane_status(w); status = window_get_pane_status(w);
/* Floating cells do not take space from the tiled layout. */
if (!layout_cell_is_tiled(lc) && !layout_cell_has_tiled_child(lc))
return (0);
if (lc->type == LAYOUT_WINDOWPANE) { if (lc->type == LAYOUT_WINDOWPANE) {
/* Space available in this cell only. */ /* Space available in this cell only. */
if (type == LAYOUT_LEFTRIGHT) { if (type == LAYOUT_LEFTRIGHT) {
@@ -570,6 +572,9 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
/* Different type: minimum of available space in child cells. */ /* Different type: minimum of available space in child cells. */
minimum = UINT_MAX; minimum = UINT_MAX;
TAILQ_FOREACH(lcchild, &lc->cells, entry) { TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (!layout_cell_is_tiled(lcchild) &&
!layout_cell_has_tiled_child(lcchild))
continue;
available = layout_resize_check(w, lcchild, type); available = layout_resize_check(w, lcchild, type);
if (available < minimum) if (available < minimum)
minimum = available; minimum = available;
@@ -663,7 +668,7 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc,
/* Find and return the nearest neighbour to a cell in a specific direction. */ /* Find and return the nearest neighbour to a cell in a specific direction. */
static struct layout_cell * static struct layout_cell *
layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction) layout_cell_get_neighbour_dir(struct layout_cell *lc, int direction)
{ {
struct layout_cell *lcn = lc; struct layout_cell *lcn = lc;
@@ -697,9 +702,9 @@ layout_cell_get_neighbour(struct layout_cell *lc)
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) if (lc == TAILQ_LAST(&lcparent->cells, layout_cells))
direction = !direction; direction = !direction;
lcother = layout_cell_get_neighbour_direction(lc, direction); lcother = layout_cell_get_neighbour_dir(lc, direction);
if (lcother == NULL) if (lcother == NULL)
lcother = layout_cell_get_neighbour_direction(lc, !direction); lcother = layout_cell_get_neighbour_dir(lc, !direction);
return (lcother); return (lcother);
} }
@@ -862,7 +867,7 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type,
size = lc->g.sx; size = lc->g.sx;
else else
size = lc->g.sy; size = lc->g.sy;
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) if (layout_cell_is_last_tiled(lc))
change = size - new_size; change = size - new_size;
else else
change = new_size - size; change = new_size - size;
@@ -987,11 +992,11 @@ layout_resize_pane(struct window_pane *wp, enum layout_type type, int change,
if (lcparent == NULL) if (lcparent == NULL)
return; return;
/* If this is the last cell, move back one. */ /* If this is the last tiled cell, move back one. */
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) { if (layout_cell_is_last_tiled(lc)) {
do lc = layout_cell_get_neighbour_dir(lc, 0);
lc = TAILQ_PREV(lc, layout_cells, entry); if (lc == NULL)
while (lc->flags & LAYOUT_CELL_FLOATING); return;
} }
layout_resize_layout(wp->window, lc, type, change, opposite); layout_resize_layout(wp->window, lc, type, change, opposite);
@@ -1009,22 +1014,22 @@ layout_resize_pane_grow(struct window *w, struct layout_cell *lc,
lcadd = lc; lcadd = lc;
/* Look towards the tail for a suitable cell for reduction. */ /* Look towards the tail for a suitable cell for reduction. */
lcremove = TAILQ_NEXT(lc, entry); lcremove = layout_cell_get_neighbour_dir(lc, 1);
while (lcremove != NULL) { while (lcremove != NULL) {
size = layout_resize_check(w, lcremove, type); size = layout_resize_check(w, lcremove, type);
if (size > 0) if (size > 0)
break; break;
lcremove = TAILQ_NEXT(lcremove, entry); lcremove = layout_cell_get_neighbour_dir(lcremove, 1);
} }
/* If none found, look towards the head. */ /* If none found, look towards the head. */
if (opposite && lcremove == NULL) { if (opposite && lcremove == NULL) {
lcremove = TAILQ_PREV(lc, layout_cells, entry); lcremove = layout_cell_get_neighbour_dir(lc, 0);
while (lcremove != NULL) { while (lcremove != NULL) {
size = layout_resize_check(w, lcremove, type); size = layout_resize_check(w, lcremove, type);
if (size > 0) if (size > 0)
break; break;
lcremove = TAILQ_PREV(lcremove, layout_cells, entry); lcremove = layout_cell_get_neighbour_dir(lcremove, 0);
} }
} }
if (lcremove == NULL) if (lcremove == NULL)
@@ -1052,13 +1057,13 @@ layout_resize_pane_shrink(struct window *w, struct layout_cell *lc,
size = layout_resize_check(w, lcremove, type); size = layout_resize_check(w, lcremove, type);
if (size != 0) if (size != 0)
break; break;
lcremove = TAILQ_PREV(lcremove, layout_cells, entry); lcremove = layout_cell_get_neighbour_dir(lcremove, 0);
} while (lcremove != NULL); } while (lcremove != NULL);
if (lcremove == NULL) if (lcremove == NULL)
return (0); return (0);
/* And add onto the next cell (from the original cell). */ /* And add onto the next cell (from the original cell). */
lcadd = TAILQ_NEXT(lc, entry); lcadd = layout_cell_get_neighbour_dir(lc, 1);
if (lcadd == NULL) if (lcadd == NULL)
return (0); return (0);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */ /* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -114,7 +114,6 @@ static const char *options_table_theme_list[] = {
static const char *options_table_copy_mode_line_numbers_list[] = { static const char *options_table_copy_mode_line_numbers_list[] = {
"off", "default", "absolute", "relative", "hybrid", NULL "off", "default", "absolute", "relative", "hybrid", NULL
}; };
/* Status line format. */ /* Status line format. */
#define OPTIONS_TABLE_STATUS_FORMAT1 \ #define OPTIONS_TABLE_STATUS_FORMAT1 \
"#[align=left range=left #{E:status-left-style}]" \ "#[align=left range=left #{E:status-left-style}]" \
@@ -305,6 +304,16 @@ const struct options_table_entry options_table[] = {
"When this is reached, the oldest buffer is deleted." "When this is reached, the oldest buffer is deleted."
}, },
{ .name = "clear-on-attach",
.type = OPTIONS_TABLE_FLAG,
.scope = OPTIONS_TABLE_SERVER,
.default_num = 1,
.text = "Whether to use the alternate screen and clear it when "
"a client is attached. When disabled, tmux does not "
"enter the alternate screen on attach so terminal "
"content before tmux remains in scrollback."
},
{ .name = "command-alias", { .name = "command-alias",
.type = OPTIONS_TABLE_STRING, .type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SERVER, .scope = OPTIONS_TABLE_SERVER,
@@ -541,7 +550,6 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_SERVER, .scope = OPTIONS_TABLE_SERVER,
.flags = OPTIONS_TABLE_IS_ARRAY, .flags = OPTIONS_TABLE_IS_ARRAY,
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title," .default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
"xterm-kitty*:kitty,"
"screen*:title," "screen*:title,"
"rxvt*:ignorefkeys", "rxvt*:ignorefkeys",
.separator = ",", .separator = ",",
@@ -1241,7 +1249,8 @@ const struct options_table_entry options_table[] = {
* underscore. * underscore.
*/ */
.default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", .default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~",
.text = "Characters considered to separate words." .text = "Characters considered to separate words; a space matches "
"any character with the Unicode White_Space property."
}, },
/* Window options. */ /* Window options. */
@@ -1374,6 +1383,13 @@ const struct options_table_entry options_table[] = {
.text = "Style of search matches in copy mode." .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", { .name = "copy-mode-current-match-style",
.type = OPTIONS_TABLE_STRING, .type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW, .scope = OPTIONS_TABLE_WINDOW,

65
popup.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 nicm Exp $ */ /* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */
/* /*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -79,7 +79,8 @@ popup_free(struct popup_data *pd)
if (pd->job != NULL) if (pd->job != NULL)
job_free(pd->job); job_free(pd->job);
input_free(pd->ictx); if (pd->ictx != NULL)
input_free(pd->ictx);
free(pd->r.ranges); free(pd->r.ranges);
screen_free(&pd->s); screen_free(&pd->s);
@@ -135,8 +136,7 @@ popup_reapply_styles(struct popup_data *pd)
} }
static void static void
popup_redraw_cb(const struct tty_ctx *ttyctx, __unused u_int py, popup_redraw_cb(const struct tty_ctx *ttyctx)
__unused u_int ny)
{ {
struct popup_data *pd = ttyctx->arg; struct popup_data *pd = ttyctx->arg;
@@ -321,60 +321,11 @@ popup_resize_cb(__unused struct client *c, void *data)
} }
} }
/*
* Report damage for a popup's rectangle, given in raw client/tty
* coordinates. Status-line cells are outside the window scene - they have no
* corresponding window content and redraw_damage_window() can't reach them -
* so if the popup's rectangle overlaps the status line, force it to redraw
* separately. The rest of the rectangle is clipped to the actual pane area
* (above or below the status line, whichever side it's on) before being
* translated into window coordinates and reported the normal way, the same
* way mouse coordinates are translated elsewhere (e.g. cmd-join-pane.c,
* cmd-split-window.c).
*/
static void
popup_damage(struct client *c, u_int px, u_int py, u_int sx, u_int sy)
{
struct window *w;
int statusat;
u_int ox, oy, osx, osy, lines, top, bottom, y0, y1;
if (c->session == NULL)
return;
w = c->session->curw->window;
lines = status_line_size(c);
statusat = status_at_line(c);
if (statusat >= 0 && py < (u_int)statusat + lines &&
py + sy > (u_int)statusat)
c->flags |= (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS);
if (statusat == 0) {
top = lines;
bottom = c->tty.sy;
} else if (statusat > 0) {
top = 0;
bottom = statusat;
} else {
top = 0;
bottom = c->tty.sy;
}
y0 = (py > top) ? py : top;
y1 = (py + sy < bottom) ? py + sy : bottom;
if (y0 >= y1)
return;
tty_window_offset(&c->tty, &ox, &oy, &osx, &osy);
redraw_damage_window(w, px + ox, y0 - top + oy, sx, y1 - y0);
}
static void static void
popup_handle_drag(struct client *c, struct popup_data *pd, popup_handle_drag(struct client *c, struct popup_data *pd,
struct mouse_event *m) struct mouse_event *m)
{ {
u_int px, py; u_int px, py;
u_int old_px = pd->px, old_py = pd->py;
u_int old_sx = pd->sx, old_sy = pd->sy;
if (!MOUSE_DRAG(m->b)) if (!MOUSE_DRAG(m->b))
pd->dragging = OFF; pd->dragging = OFF;
@@ -397,9 +348,7 @@ popup_handle_drag(struct client *c, struct popup_data *pd,
pd->dy = m->y - pd->py; pd->dy = m->y - pd->py;
pd->ppx = px; pd->ppx = px;
pd->ppy = py; pd->ppy = py;
server_redraw_client(c);
popup_damage(c, old_px, old_py, old_sx, old_sy);
c->flags |= CLIENT_REDRAWOVERLAY;
} else if (pd->dragging == SIZE) { } else if (pd->dragging == SIZE) {
if (pd->border_lines == BOX_LINES_NONE) { if (pd->border_lines == BOX_LINES_NONE) {
if (m->x < pd->px + 1) if (m->x < pd->px + 1)
@@ -426,9 +375,7 @@ popup_handle_drag(struct client *c, struct popup_data *pd,
if (pd->job != NULL) if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2); job_resize(pd->job, pd->sx - 2, pd->sy - 2);
} }
server_redraw_client(c);
popup_damage(c, old_px, old_py, old_sx, old_sy);
c->flags |= CLIENT_REDRAWOVERLAY;
} }
} }

View File

@@ -28,7 +28,7 @@ $TMUX kill-server 2>/dev/null
TMP=$(mktemp) TMP=$(mktemp)
TMP2=$(mktemp) TMP2=$(mktemp)
trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15 trap 'rm -f "$TMP" "$TMP2" "$TMP".*; $TMUX kill-server 2>/dev/null' 0 1 15
# check_ok $cmd... # check_ok $cmd...
# #
@@ -270,6 +270,27 @@ if [ $? -eq 0 ]; then
exit 1 exit 1
fi fi
# A write error after a successful open must be reported to the server.
dd if=/dev/zero of="$TMP.big" bs=100000 count=1 2>/dev/null
check_ok load-buffer -b write-error "$TMP.big"
mkfifo "$TMP.fifo" || exit 1
dd if="$TMP.fifo" of="$TMP.got" bs=1 count=8 2>/dev/null &
reader=$!
exec 3>"$TMP.fifo"
out=$($TMUX save-buffer -b write-error "$TMP.fifo" 3>&- 2>&1)
status=$?
exec 3>&-
wait "$reader"
if [ $status -eq 0 ]; then
echo "save-buffer after write error succeeded."
exit 1
fi
if [ "$(wc -c <"$TMP.got")" -ne 8 ]; then
echo "save-buffer write error test wrote wrong amount."
exit 1
fi
check_ok delete-buffer -b write-error
# save-buffer - writes to stdout and load-buffer - reads from stdin. # save-buffer - writes to stdout and load-buffer - reads from stdin.
out=$($TMUX save-buffer -b sb -) out=$($TMUX save-buffer -b sb -)
if [ "$out" != "data" ]; then if [ "$out" != "data" ]; then

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" fail "window 1 of zzz not killed"
exit_mode q 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 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

View File

@@ -0,0 +1,163 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
fail()
{
echo "$1"
[ -z "$TMUX" ] || $TMUX kill-server 2>/dev/null
exit 1
}
goto_cell()
{
row=$1
column=$2
$TMUX send-keys -X history-top || fail "$mode: history-top failed"
$TMUX send-keys -X start-of-line || fail "$mode: start-of-line failed"
[ "$row" -eq 0 ] ||
$TMUX send-keys -X -N "$row" cursor-down ||
fail "$mode: cursor-down failed"
[ "$column" -eq 0 ] ||
$TMUX send-keys -X -N "$column" cursor-right ||
fail "$mode: cursor-right failed"
}
check_cursor()
{
expected=$1
actual=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}')
[ "$actual" = "$expected" ] ||
fail "$mode: expected cursor $expected, got $actual"
}
check_word()
{
row=$1
column=$2
expected=$3
goto_cell "$row" "$column"
$TMUX set-buffer sentinel || fail "$mode: set-buffer failed"
$TMUX send-keys -X select-word || fail "$mode: select-word failed"
$TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed"
actual=$($TMUX show-buffer 2>/dev/null)
[ "$actual" = "$expected" ] ||
fail "$mode: expected word '$expected', got '$actual'"
}
check_next_word()
{
row=$1
expected=$2
goto_cell "$row" 0
$TMUX send-keys -X next-word || fail "$mode: next-word failed"
$TMUX set-buffer sentinel || fail "$mode: set-buffer failed"
$TMUX send-keys -X select-word || fail "$mode: select-word failed"
$TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed"
actual=$($TMUX show-buffer 2>/dev/null)
[ "$actual" = "$expected" ] ||
fail "$mode: expected next word '$expected', got '$actual'"
}
for mode in emacs vi; do
TMUX="$TEST_TMUX -Lunicode-whitespace-$mode-$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x16 -y20 \
"printf 'aa bb\naa\tbb\naa\302\240bb\naa\343\200\200bb\naa:bb\naaaaaaaaaaaaaaa\302\240b\nabcdefghijklmnopq\nhardone\nhardtwo\naa\341\232\200bb\naa\342\200\200bb\naa\342\200\250bb\naa\342\200\251bb\naa\342\200\257bb\naa\342\201\237bb\nxa\302\205b c\n'; exec cat" ||
fail "$mode: new-session failed"
$TMUX set-option -g window-size manual || fail "$mode: set size failed"
$TMUX set-window-option -g mode-keys "$mode" ||
fail "$mode: set mode-keys failed"
$TMUX set-window-option -g word-separators "" ||
fail "$mode: clear word-separators failed"
$TMUX copy-mode || fail "$mode: copy-mode failed"
# ASCII space and TAB retain their existing behaviour.
goto_cell 0 0
$TMUX send-keys -X next-space || fail "$mode: ASCII next-space failed"
check_cursor 3,0
goto_cell 1 0
$TMUX send-keys -X next-space || fail "$mode: TAB next-space failed"
check_cursor 8,1
$TMUX send-keys -X previous-space ||
fail "$mode: TAB previous-space failed"
check_cursor 0,1
# NBSP is whitespace for selection and word movement.
goto_cell 2 0
$TMUX send-keys -X next-word || fail "$mode: NBSP next-word failed"
check_cursor 3,2
$TMUX send-keys -X previous-word ||
fail "$mode: NBSP previous-word failed"
check_cursor 0,2
check_word 2 0 aa
goto_cell 2 0
$TMUX send-keys -X next-word-end ||
fail "$mode: NBSP next-word-end failed"
if [ "$mode" = emacs ]; then
check_cursor 2,2
else
check_cursor 1,2
fi
# U+3000 is two columns wide; both its leading and padding cells must be
# skipped as one whitespace character in either direction.
goto_cell 3 0
$TMUX send-keys -X next-word || fail "$mode: U+3000 next-word failed"
check_cursor 4,3
$TMUX send-keys -X previous-word ||
fail "$mode: U+3000 previous-word failed"
check_cursor 0,3
goto_cell 3 0
$TMUX send-keys -X next-space-end ||
fail "$mode: U+3000 next-space-end failed"
if [ "$mode" = emacs ]; then
check_cursor 2,3
else
check_cursor 1,3
fi
# Other characters in word-separators remain literal when space enables
# Unicode whitespace matching.
$TMUX set-window-option -g word-separators ': ' ||
fail "$mode: set custom word-separators failed"
goto_cell 4 0
$TMUX send-keys -X next-word || fail "$mode: literal next-word failed"
check_cursor 2,4
$TMUX set-window-option -g word-separators "" ||
fail "$mode: restore word-separators failed"
# Whitespace in the last column remains a boundary across a soft wrap.
goto_cell 5 0
$TMUX send-keys -X next-space ||
fail "$mode: wrapped NBSP next-space failed"
check_cursor 0,6
# A word split only by a soft wrap remains one word, while a hard line
# boundary still terminates selection.
check_word 7 0 abcdefghijklmnopq
check_word 9 0 hardone
# Check the remaining representative White_Space ranges and U+0085
# combined into a cell with another character.
check_next_word 11 bb
check_next_word 12 bb
check_next_word 13 bb
check_next_word 14 bb
check_next_word 15 bb
check_next_word 16 bb
goto_cell 17 0
$TMUX send-keys -X next-word ||
fail "$mode: combined U+0085 next-word failed"
check_cursor 2,17
$TMUX kill-server 2>/dev/null
done
exit 0

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

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

View File

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

View File

@@ -199,5 +199,74 @@ must_fail $TMUX resize-pane -t "$id" -x 0
must_fail $TMUX resize-pane -t "$id" -y 0 must_fail $TMUX resize-pane -t "$id" -y 0
$TMUX kill-pane -t "$id" || exit 1 $TMUX kill-pane -t "$id" || exit 1
# --- Tiled pane resize with floating cells in the layout ---
$TMUX set-option -w -u pane-border-lines || exit 1
base=$($TMUX display-message -p '#{pane_id}')
# A floating cell after the last tiled cell must not become the recipient of a
# relative or absolute resize. This is the sequence from GitHub issue 5135.
floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 50% -y 50% -X 50% -Y 50% 'sleep 100') ||
fail "new-pane for tiled resize test failed"
lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') ||
fail "split-window for tiled resize test failed"
$TMUX resize-pane -t "$lower" -U 5 || fail "relative tiled resize failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 13
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10
$TMUX resize-pane -t "$lower" -y 11 || fail "absolute tiled reset failed"
$TMUX resize-pane -t "$lower" -y 16 || fail "absolute tiled resize failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10
$TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$lower" || exit 1
# A floating cell between tiled siblings must be skipped when finding the cell
# which donates space to a resize.
lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') ||
fail "split-window for middle floating cell test failed"
floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 20 -y 8 -X 30 -Y 8 'sleep 100') ||
fail "new-pane for middle floating cell test failed"
$TMUX resize-pane -t "$base" -D 3 || fail "resize past floating cell failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 6
$TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$lower" || exit 1
# A small floating child must not limit the available tiled space in a nested
# layout with a different split direction.
lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') ||
fail "split-window for nested floating cell test failed"
right=$($TMUX split-window -dhPF '#{pane_id}' -t "$lower" 'sleep 100') ||
fail "horizontal split for nested floating cell test failed"
floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$lower" \
-x 20 -y 3 -X 30 -Y 10 'sleep 100') ||
fail "new-pane for nested floating cell test failed"
$TMUX resize-pane -t "$base" -D 3 || fail "nested tiled resize failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$right" '#{pane_top}')" 16
must_equal "$($TMUX display-message -p -t "$right" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 1
$TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$right" || exit 1
$TMUX kill-pane -t "$lower" || exit 1
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
exit 0 exit 0

View File

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

View File

@@ -71,7 +71,10 @@ cursor_very_visible
cursor_x cursor_x
cursor_y cursor_y
history_all_bytes history_all_bytes
history_added
history_bytes history_bytes
history_collected
history_generation
history_limit history_limit
history_size history_size
host host
@@ -103,6 +106,11 @@ pane_at_right
pane_at_top pane_at_top
pane_bg pane_bg
pane_bottom pane_bottom
pane_command_duration
pane_command_end_time
pane_command_running
pane_command_start_time
pane_command_status
pane_current_command pane_current_command
pane_current_path pane_current_path
pane_dead pane_dead
@@ -120,16 +128,21 @@ pane_index
pane_input_off pane_input_off
pane_key_mode pane_key_mode
pane_last pane_last
pane_last_output_time
pane_last_prompt_time
pane_left pane_left
pane_marked pane_marked
pane_marked_set pane_marked_set
pane_modal_flag
pane_mode pane_mode
pane_output_generation
pane_path pane_path
pane_pb_progress pane_pb_progress
pane_pb_state pane_pb_state
pane_pid pane_pid
pane_pipe pane_pipe
pane_pipe_pid pane_pipe_pid
pane_private_modes
pane_right pane_right
pane_search_string pane_search_string
pane_start_command pane_start_command
@@ -141,6 +154,8 @@ pane_title
pane_top pane_top
pane_tty pane_tty
pane_unseen_changes pane_unseen_changes
pane_unzoomed_height
pane_unzoomed_width
pane_width pane_width
pane_x pane_x
pane_y pane_y
@@ -206,7 +221,10 @@ window_layout
window_linked window_linked
window_linked_sessions window_linked_sessions
window_linked_sessions_list window_linked_sessions_list
window_manual_height
window_manual_width
window_marked_flag window_marked_flag
window_modal_pane
window_name window_name
window_offset_x window_offset_x
window_offset_y 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. # pane-exited when a pane's command exits with remain-on-exit off.
$TMUX set -g @x 0 || fail "set @x failed" $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" fail "set-hook pane-exited failed"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') || pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \
fail "split-window true failed" 'trap "" HUP; exec </dev/null >/dev/null 2>&1; sleep 1; exit 42') ||
wait_for @x "pane-exited:$pane" 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" $TMUX set-hook -gu pane-exited || fail "unset pane-exited failed"
# pane-died when a pane's command exits with remain-on-exit on. # pane-died when a pane's command exits with remain-on-exit on.

View File

@@ -1,151 +0,0 @@
#!/bin/sh
# Regression test for a border-status caching bug: window_make_pane_status()
# (window-border.c) gates the physical redraw of a pane's border-status
# title on a logical content diff (grid_compare against a cached copy), not
# on whether the physical screen cells were disturbed by something else in
# the meantime - such as a floating pane's own border, drawn on top of a
# tiled pane's border-status row, sliding across it and then away again.
# See tmux-image-redraw-known-bugs.md ("border-status text cache ignores
# physical damage") for the full write-up.
#
# Reproduction: a tiled pane with pane-border-status on has a floating pane
# dragged, by mouse, from directly over its border-status row to somewhere
# else. The tiled pane's title should reappear once the floating pane has
# moved off it; without the fix it stays blank.
#
# The drag starts and ends away from row 0 rather than grabbing the
# floating pane's border while it is already sitting on row 0: when a
# floating pane's own border-status row exactly coincides with the tiled
# pane's row 0, mouse hit-testing on that row attributes clicks to the
# tiled pane, not the floating one on top of it (an unrelated tmux quirk,
# not what this test is about). That only matters for the initial press,
# though - once a drag is under way, further motion events go straight to
# the already-bound per-pane callback without re-resolving which pane owns
# the coordinates, so starting the grab on an unambiguous row and dragging
# through row 0 works fine.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag COL ROW [ROW ...]
#
# Write one continuous mouse-1 gesture at column COL: a press at the first
# ROW, a drag update at each subsequent ROW in turn, and a release at the
# last ROW - all 1-based positions sent to the outer pane holding the
# inner client. This matches the default MouseDown1Border/MouseDrag1Border
# bindings used to move or resize a floating pane by its border.
#
# This must be one continuous press/drag/.../release gesture, not several
# separate drag() calls chained together: releasing clears the per-pane
# drag callback binding, so a later press has to be re-hit-tested from
# scratch, which only works reliably away from row 0 (see the header
# comment). A drag already under way is not re-hit-tested per motion
# event, so it can safely pass through row 0 as an intermediate waypoint.
drag()
{
col="$1"
shift
row="$1"
shift
seq=$(printf '\033[<0;%s;%sM' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
while [ $# -gt 1 ]; do
row="$1"
shift
seq=$(printf '\033[<32;%s;%sM' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.15
done
row="$1"
seq=$(printf '\033[<0;%s;%sm' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g pane-border-status top
$TMUX set -g default-command 'sh -c "sleep 100"'
BASE=$($TMUX list-panes -F '#{pane_id}' | head -1)
[ -n "$BASE" ] || fail "No base pane."
$TMUX select-pane -t "$BASE" -T TILEDTITLE || fail "set base title failed"
# Float a pane well clear of row 0 to start - its own top border must not
# coincide with the base pane's border-status row for the initial mouse
# press to unambiguously hit it (see the header comment). -x/-y are the
# pane's outer size including its 2-cell border frame, so request 2 more
# than the desired 14x3 content area (see layout_floating_args_parse()).
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 -T FLOATTITLE) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: the base pane's title is visible now (the float starts
# well clear of row 0).
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
head -1 $TMP | grep -q TILEDTITLE ||
fail "base title not visible before the drag - test setup is wrong"
# Drag the float up by its top border (an unambiguous row - not row 0) so
# it ends up covering row 0, hiding the base pane's title behind its own
# border, then continue the same drag back down again so it ends clear of
# row 0 once more - one continuous gesture the whole way (see drag()'s
# comment for why).
GRABCOL=$((FLEFT + FWIDTH / 2))
waypoints="$FTOP"
row=$FTOP
while [ "$row" != 1 ]; do
row=$((row - 1))
waypoints="$waypoints $row"
done
while [ "$row" != "$FTOP" ]; do
row=$((row + 1))
waypoints="$waypoints $row"
done
drag $((GRABCOL + 1)) $waypoints
drag $((GRABCOL + 1)) 1 $((GRABCOL + 1)) "$FTOP"
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# The base pane's title should be visible again now the floating pane has
# moved off its border-status row. This is expected to fail before the fix
# - see the header comment.
head -1 $TMP | grep -q TILEDTITLE ||
fail "base pane title still blank after floating pane moved away"
exit 0

View File

@@ -1,71 +0,0 @@
#!/bin/sh
# Regression test for a fixed bug: on a client detected as SIXEL-capable, a
# pane-content redraw (PANE_REDRAW) erased the image backend's rectangle
# using the redrawing pane's raw nominal geometry (wp->xoff/yoff/sx/sy)
# instead of the cells it actually owns in the current scene. When a
# floating pane occluded part of that rectangle, the erase blanked the
# floating pane's on-screen area, and since the subsequent text redraw
# correctly only refills cells the redrawing pane owns, nothing repainted
# it back in - the floating pane's content stayed blank.
#
# Fixed in screen-redraw.c:redraw_draw_pane_lines() by erasing only the
# cell ranges the pane actually owns per line (its REDRAW_SPAN_PANE spans),
# not its raw rectangle. See tmux-sixel-erase-ignores-occlusion.md and
# IMAGE-REDRAW-DISCUSSION.md for the full write-up.
#
# Confirmed by direct A/B test against this exact sequence: reliably wipes
# the floating pane's content on the unfixed code, reliably leaves it
# intact on the fixed code.
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 -Lifcw$$ -f/dev/null"
TMUX2="$TEST_TMUX -Lifcw-inner$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
$TMUX2 new-session -d -x 60 -y 20 "sh -c 'i=0; while [ \$i -lt 30 ]; do printf \"TILE%02d\\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" \
|| exit 1
$TMUX2 new-pane -x20 -y6 -X8 -Y3 -T FLOATTITLE "sh -c 'printf FLOATCONTENT; exec sleep 100'" \
|| exit 1
[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0
# The attaching client must be detected as SIXEL-capable - this bug does
# not reproduce on a fallback or Kitty client.
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 60 -y 20 || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
# Sanity check: the floating pane's content is visible before anything
# disturbs it.
$TMUX capturep -p >$TMP || exit 1
grep -q 'FLOATCONTENT' $TMP || exit 1
# Click the tiled pane (making it active), enter copy-mode scrolled up
# (matching a PageUp binding), move up one line (matching an Up binding),
# then exit copy-mode - the exit is what triggers window_pane_reset_mode(),
# which is what actually caused the erase.
$TMUX2 select-pane -t%0 || exit 1
sleep 1
$TMUX2 copy-mode -u -t%0 || exit 1
sleep 1
$TMUX2 send-keys -t%0 -X cursor-up || exit 1
sleep 1
$TMUX2 send-keys -t%0 -X cancel || exit 1
sleep 1
$TMUX capturep -p >$TMP || exit 1
grep -q 'FLOATCONTENT' $TMP || exit 1
exit 0

View File

@@ -1,110 +0,0 @@
#!/bin/sh
# Regression test: an image wider than the pane at the time it was
# displayed must show more of itself once the pane grows wide enough,
# instead of staying clipped to its original width forever.
#
# image_write() (image.c) clips an image's width to whatever fit in the
# pane when it was first displayed, and never revisits that decision -
# unlike height, which recovers naturally through ordinary scrollback,
# there is no "scroll right", so the clipped columns were permanently
# discarded. window_pane_resize() (window.c) now calls
# image_grid_resize_width() after a pane grows wider, which extends each
# existing placement's spans - using the image's own retained, immutable
# pixel data - up to whichever is smaller: the image's full width or the
# new pane width. See tmux-image-redraw-known-bugs.md for the full
# write-up.
#
# This is checked via the SIXEL raster widths reported in the client's raw
# output before and after widening the window. The redraw triggered by the
# resize is damage-based (only the newly-uncovered columns are dirtied), so
# it does not redraw the whole row as one wider raster - it sends the
# already-correct clipped portion's width again untouched, plus a *second*,
# separate raster covering just the newly-added columns. So rather than
# looking for a single wider raster, this checks that the widths seen
# across both redraws, added together, account for the fixture's full
# pixel width - i.e. the previously-clipped remainder actually appeared.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
FIXTURE=$(pwd)/monkey-2.sixel.txt
FULL=$(grep -oa '"1;1;[0-9]*;[0-9]*' $FIXTURE | head -1 | cut -d';' -f3)
[ -n "$FULL" ] || fail "could not read fixture's own raster width"
# The fixture is 360px wide - narrow enough that a 12-column pane clips it,
# comfortably below the 360px it would take to show in full.
$TMUX new-session -d -s inner -x 12 -y 40 "cat '$FIXTURE'; exec sh" || exit 1
sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which sends the image) has already happened, missing it.
$TMUX2 new-session -d -x 12 -y 40 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# The outer client is just another tmux client receiving raw PTY output
# that happens to contain sixel DCS sequences - left alone, it would
# independently decode and place its own second, differently-sized image
# on top of the inner session's, confounding the raster-width measurement
# below. Disable sixel on the outer client so only the inner session's
# placement exists.
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
before=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -n | tail -1)
[ -n "$before" ] || fail "sanity: image never reached the client"
[ "$before" -lt 360 ] || fail "sanity: image was not clipped by the narrow pane ($before)"
: >$TMP
# Widen the window well past the image's full width - resize the outer
# client, since the inner session's displayed size follows whatever
# terminal size its attached client actually has.
$TMUX2 resize-window -t "$OUTER" -x 30 -y 40 || fail "resize-window failed"
sleep 1
$TMUX refresh-client || fail "refresh-client failed"
sleep 1
# The widths seen after the resize, added together (deduplicated - the
# same chunk may legitimately be retransmitted), should account for the
# fixture's full pixel width: this is expected to fail before the fix,
# where the newly-uncovered columns are never redrawn at all and only the
# original clipped width ever appears - see the header comment.
after_total=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -nu |
awk '{s+=$1} END{print s+0}')
[ "$after_total" -gt 0 ] ||
fail "no image raster was sent at all after widening the window"
[ "$after_total" -ge $((FULL - 20)) ] ||
fail "image stayed clipped at ${before}px after widening the window (widths summed to only ${after_total}px, expected close to ${FULL}px)"
exit 0

View File

@@ -1,107 +0,0 @@
#!/bin/sh
# Regression test: an image taller than the pane, at the moment it is first
# displayed, must remain visible when scrolling back up through history -
# not show blank space for the part that was scrolled off screen before the
# user ever got to see it.
#
# image_write() (image.c) handles this case by scrolling the screen up
# (via screen_write_scrollup()) to make room, then only ever wrote spans
# for the rows that ended up on screen afterwards - the rows that were
# immediately scrolled off (origin_y of them) were pushed into history as
# blank, spanless rows and their image data was discarded. Unlike width,
# which has no "scroll right" to recover a permanent clip, height already
# has ordinary scrollback, so this was pure waste. image_write() now also
# calls image_extend_row() for those origin_y history rows (capped to
# gd->hsize, since screen_write_scrollup() may not have pushed a real
# history row for every one of them - see tmux-uint-subtraction-underflow-
# care.md), so scrolling back up recovers the image instead of showing
# empty space. See tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by comparing how many separate SIXEL rasters the client
# receives right after entering copy mode (a baseline - copy mode itself
# triggers a redraw of the still-visible rows) against how many it
# receives after scrolling to the very top of history: with the fix, more
# rasters appear (covering the newly-revealed, previously-scrolled-off
# rows); without it, scrolling reveals nothing new.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
FIXTURE=$(pwd)/monkey-2.sixel.txt
# A short (5-row) pane and a wide (30-column) one - wide enough that the
# fixture's width is not clipped, so only the height/scrolloff behaviour is
# exercised, and short enough that most of the image is scrolled off
# screen while it is still being displayed.
$TMUX new-session -d -s inner -x 30 -y 5 "cat '$FIXTURE'; exec sh" || exit 1
sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which sends the image) has already happened, missing it.
$TMUX2 new-session -d -x 30 -y 5 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# The outer client is just another tmux client receiving raw PTY output
# that happens to contain sixel DCS sequences - left alone, it would
# independently decode and place its own second image, confounding the
# raster counts below. Disable sixel on the outer client so only the
# inner session's placement exists.
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] ||
fail "sanity: image did not push anything into history - pane wasn't short enough"
# Enter copy mode and measure the baseline raster count this alone causes
# (redrawing whatever's still on screen), without having scrolled yet.
: >$TMP
$TMUX copy-mode -t inner || fail "copy-mode failed"
sleep 1
baseline=$(grep -oac '"1;1;[0-9]*;[0-9]*' $TMP)
# Scroll all the way to the top of history - this is expected to fail
# before the fix (no more rasters than the baseline ever appear, since the
# scrolled-off rows were left blank) - see the header comment.
: >$TMP
$TMUX send-keys -t inner -X history-top || fail "history-top failed"
sleep 1
after=$(grep -oac '"1;1;[0-9]*;[0-9]*' $TMP)
[ "$after" -gt 0 ] ||
fail "no image raster was sent at all after scrolling to history-top"
[ "$after" -ge "$baseline" ] ||
fail "scrolling to history-top redrew fewer rasters ($after) than just entering copy mode did ($baseline) - unexpected either way"
exit 0

View File

@@ -1,100 +0,0 @@
#!/bin/sh
# Regression test: extending a copy-mode selection by cursor movement, with
# the view otherwise unmoved (no scrolling), must not retransmit an image
# whose row the cursor passes through.
#
# window_copy_write_one() (window-copy.c) used to write text/highlight
# styling directly over image-covered cells, which - since a character
# write typically clears whatever pixel content a terminal was showing
# there - erased the image with nothing to redraw it back in. Separately,
# window_copy_write_line()'s call to image_redraw_area() used to fire
# unconditionally on every redraw, so even after fixing the erasure, the
# image would still be needlessly recomposited (and briefly flash) on
# every single cursor step even though nothing about it had changed. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by counting DCS (\033P) sequences in the client's raw
# output during the cursor movement: with the fix, extending a selection
# without scrolling never touches the image, so none should appear.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
# A small, distinctive SIXEL raster (26x26 pixels) at the top of the pane,
# matching the fixture already used in image-support.sh, followed by
# enough plain lines that the cursor can move down through the image and
# past it without the view needing to scroll.
SIXEL='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
$TMUX new-session -d -s inner -x 40 -y 20 \
"printf '$SIXEL'; for i in \$(seq 1 15); do echo line\$i; done; exec sh" ||
exit 1
sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which sends the image) has already happened, missing it.
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the image reached the client at all.
grep -qa '"1;1;26;26' $TMP || fail "sanity: image never reached the client"
: >$TMP
# Enter copy-mode, scroll to the top (where the image is) and select down
# through it one cursor step at a time - the view does not need to scroll
# for any of this, since the image is already at the top of what is
# visible.
$TMUX copy-mode -t inner || fail "copy-mode failed"
$TMUX send-keys -X history-top || fail "history-top failed"
sleep 0.2
: >$TMP
$TMUX send-keys -X begin-selection || fail "begin-selection failed"
i=0
while [ $i -lt 6 ]; do
$TMUX send-keys -X cursor-down || fail "cursor-down failed"
sleep 0.15
i=$((i + 1))
done
sleep 0.5
# No DCS sequence should have been sent - the image's row was never
# disturbed by any of this. This is expected to fail before the fix - see
# the header comment.
dcs=$(grep -ac "$(printf '\033P')" $TMP)
[ "$dcs" -eq 0 ] ||
fail "image was retransmitted ($dcs times) while just moving the selection cursor"
exit 0

View File

@@ -1,362 +0,0 @@
#!/bin/sh
# Grid-resident image layers: Kitty input, history, copy and overwrite.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Limage$$ -f/dev/null"
TMUX2="$TEST_TMUX -Limage-client$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
FIXTURE=$(pwd)/monkey-2.sixel.txt
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
$TMUX new-session -d -x 20 -y 8 "
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,m=1;/wAA/wD/\033\\'
printf '\033_Gm=0;AP8AAP///////w==\033\\'
i=0
while [ \$i -lt 10 ]; do printf '\nline-%d' \$i; i=\$((i + 1)); done
sleep 10"
sleep 1
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
# Images scroll as grid cells, while capture output contains ordinary spaces.
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] || exit 1
$TMUX capture-pane -pS- >$TMP || exit 1
grep '_G' $TMP >/dev/null && exit 1
# Copy mode duplicates the backing grid, including image layers.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX capture-pane -p >$TMP || exit 1
$TMUX send-keys -X cancel || exit 1
# Ordinary text updates the image underlay through the normal grid write path.
# Deleting the image reveals the newly written text.
$TMUX new-window -d "
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,i=8;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033[HXY'
printf '\033_Ga=d,d=i,q=2,i=8\033\\'
sleep 10"
sleep 1
[ "$($TMUX capture-pane -pt:1 -S0 -E0)" = "XY" ] || exit 1
# The protocol default is transmit-only; a later placement uses the retained
# immutable image and advances by the same canonical one-cell footprint.
$TMUX new-window -d "
printf '\033_Gq=2,f=32,s=1,v=1,c=1,r=1,i=9;/wAA/w==\033\\'
printf '\033_Ga=p,q=2,i=9\033\\'
printf '\033_Ga=d,d=I,q=2,i=9\033\\'
sleep 10"
sleep 1
[ "$($TMUX display-message -pt:2 '#{cursor_y}')" = 1 ] || exit 1
# PNG Kitty input uses the shared image decoder and canonical cell sizing.
$TMUX new-window -d "
printf '\033_Ga=T,q=2,f=100;iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURf8AAP///0EdNBEAAAABYktHRAH/Ai3eAAAAB3RJTUUH6ggCDAECH324BwAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=\033\\'
sleep 10"
sleep 1
[ "$($TMUX display-message -pt:3 '#{cursor_y}')" = 1 ] || exit 1
# SIXEL input reaches the same grid layer and copy-mode paths.
$TMUX new-window -d "cat '$FIXTURE'; sleep 10"
sleep 1
[ "$($TMUX display-message -pt:4 '#{cursor_y}')" -gt 0 ] || exit 1
$TMUX copy-mode -t:4 || exit 1
$TMUX send-keys -t:4 -X history-top || exit 1
$TMUX capture-pane -pt:4 >$TMP || exit 1
# A 26-pixel raster occupies two default 16-pixel cells, but the final cell
# remains only partially filled instead of stretching the raster to 32 pixels.
# A client without an image feature exposes this in the sampled text backend.
$TMUX kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel@' || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# Re-emitting the image preserves the 26-pixel raster rather than expanding
# it to the two complete 16-pixel grid cells occupied by the image.
# terminal-features is only detected when a client attaches, so switch to a
# SIXEL-capable client by detaching and reattaching rather than toggling the
# option on the client already attached above.
$TMUX2 set -u terminal-features || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX2 detach-client || exit 1
sleep 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
grep -a '"1;1;26;26' $TMP >/dev/null || exit 1
# A selection redraw uses the single-cell path. It must leave ASCII image
# cells visible (graphical clients skip these cells to avoid erasing pixels).
# Switch back to a fallback client the same way.
$TMUX2 set -u terminal-features || exit 1
$TMUX2 set -as terminal-features ',*:sixel@' || exit 1
$TMUX2 detach-client || exit 1
sleep 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX2 copy-mode || exit 1
$TMUX2 send-keys -X history-top || exit 1
$TMUX2 send-keys -X start-of-line || exit 1
$TMUX2 send-keys -X begin-selection || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# A retained Kitty image may be placed repeatedly using source rectangles.
# Crop the green and white right column from a red/green/blue/white image.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033_Ga=t,q=2,f=32,s=2,v=2,i=10;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=2,c=1,r=2\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "*" ] || exit 1
[ "$(sed -n 2p $TMP)" = "@" ] || exit 1
# C=1 places the image without moving the cursor or scrolling. The four-row
# image is clipped at the bottom of this four-row pane, leaving its white last
# row off screen.
$TMUX2 new-window -d "
printf '\033[2;3H'
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:1 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{cursor_x},#{cursor_y}')" = "2,1" ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 3p $TMP)" = " -" ] || exit 1
[ "$(sed -n 4p $TMP)" = " *" ] || exit 1
# With normal cursor movement, scrolling is calculated from the full image
# height. Rows which scrolled above the pane are then cropped from the top, so
# the bottom three source rows remain visible and the cursor is on the last row.
$TMUX2 new-window -d "
printf '\033[3;1H'
printf '\033_Ga=T,q=2,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:2 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{cursor_y}')" = 3 ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "-" ] || exit 1
[ "$(sed -n 2p $TMP)" = "*" ] || exit 1
[ "$(sed -n 3p $TMP)" = "@" ] || exit 1
# Image rows remain cell-aligned when a narrower terminal causes text
# reflow. The ten-column rows are clipped to five columns, not split into four
# wrapped rows.
$TMUX2 new-window -d "
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=2,c=10,r=2;/wAA//////8=\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:3 || exit 1
sleep 1
$TMUX resize-window -x 5 -y 4 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{window_width}x#{window_height}')" = "5x4" ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "....." ] || exit 1
[ "$(sed -n 2p $TMP)" = "@@@@@" ] || exit 1
[ -z "$(sed -n 3p $TMP)" ] || exit 1
# Kitty virtual placements use U+10EEEE placeholder cells. Keep their rows at
# fixed coordinates when narrowing the terminal, clipping instead of reflowing
# the second half onto the following row.
$TMUX resize-window -x 10 -y 4 || exit 1
sleep 1
$TMUX2 new-window -d "
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
sleep 10" || exit 1
$TMUX2 select-window -t:4 || exit 1
sleep 1
$TMUX resize-window -x 5 -y 4 || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -n "$(sed -n 1p $TMP)" ] || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
$TMUX resize-window -x 10 -y 4 || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP | wc -c)" = 61 ] || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
# A placement ID supplied with transmit-and-place is reused by a later place.
# This is the sequence used by chawan: the newline moves the cursor down before
# the second command, so the folder moves down one row without leaving a copy.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=T,f=100,m=0;iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAACmUlEQVR4XmOgF2CEWVRfX8/w//9/BkZGRgy7YeKNjY1kuwtsam1tLQMTE5MG0MBsoEWW6KYBxf8DxScD6UVNTU1kWcZYU1MD0qjLzMx8EmgYJy5TgJb8/ffvn+2vX7+Od3Z2kmwZC9AnIE0FIEuAhl0H4ulA9k9kk4Bi4UB1TkA8H2iRYXl5+Xd2dna8loGCG2Q2LLgZQXED1PEIaLjs379/g4Ds9VDLwQYBxRiAPpFmZWW9DFQjCDRgLxDfJeQloJpdQHPWA9X9A1kGs+gZ0BBJoKQtUOIIeqSDgheoKR6IF5ASZkAH3gKaCzLzFQsxGltaWhjq6uoWguIJ6hi82oCG8wIV+AEdpga0LBfIrmUh1oWg1AYM5iVA9UuI1JMGVDcTiCNIsghkOChIQ0NDwfasXr0ap33QeL8GUgD0HTeIZmIgAUAtYQZqYYZZSKx2oi2CWaKpqbkIhEm1jOg40tLSAjleEBgUUSAGkJ8PpN8Q6yOCFoF8AvQBTvOAqZHh+vXrDPjijGAcIcXJUqBPQOXda5iNQP5rkBiQv5SYYMTrIxUVFZC5IkDCCk8QWQHVgdS8xBeMeBNDe3s7A7CwfQl0uSmwKDIBYhdQGQbCIDZIDCQHUgNSS7ZFII2gUgEU6UADzwJz+kUgDa6zoOyzIDmoGgaKLAJpBheKkArxC7BIeQDCQPEvIEuJrQyJzkdQA38ADbcHYaBFP0ipcVGqCWDYBwPxOkYs1TkDCeA/pEkQBDRnLZD9GKhVjgUkCGTsBgomAekWIJYG4p8MlAFQrZgJMgJo/m4QzQJ1fT9QIBLI1gTiSRRaAm/gAM38DjSvH2QeuHECLW1xNk7IsRhoyXGgJVOBem+AExPMEHzNLTItYiAlVTJQCwAAJxkMkgbHo2UAAAAASUVORK5CYII=\033\\'
printf '\n'
sleep 1
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=p;\033\\'
printf '\n'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 1p $TMP)" ] || exit 1
[ -n "$(sed -n 2p $TMP)" ] || exit 1
# A nonzero Kitty placement ID identifies one placement of an image. Reusing
# the same image and placement IDs moves it rather than leaving the old cells.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=11;/////w==\033\\'
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
# Deleting one placement ID leaves other placements of the image intact.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=12;/////w==\033\\'
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=12,p=7,c=2,r=1\033\\'
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=12,p=8,c=2,r=1\033\\'
printf '\033_Ga=d,d=i,q=2,i=12,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
# Image layers retain the cells beneath them. Deleting a transparent image
# must reveal the original text rather than replacing it with spaces.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf 'XY'
printf '\033[H'
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=1,c=2,r=1,i=13,p=7;////AA==\033\\'
printf '\033_Ga=d,d=i,q=2,i=13,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
[ "$($TMUX2 capture-pane -pS0 -E0)" = "XY" ] || exit 1
# Text written over a Kitty placement updates its underlay without deleting
# the image. Deleting the placement afterwards reveals the updated text.
TEXT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf 'test\r'
printf '\033_Ga=T,q=2,C=1,f=32,s=2,v=2,c=2,r=2,i=14,p=7;/wAA/wD/AP8AAP///////w==\033\\'
printf '\r'
printf 'test\n'
printf 'test\n'
$TEST_TMUX wait-for image-text-delete-$$
printf '\033_Ga=d,d=i,q=2,i=14,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$TEXT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = ".*st" ] || exit 1
[ "$(sed -n 2p $TMP)" = " @st" ] || exit 1
$TMUX2 wait-for -S image-text-delete-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "test" ] || exit 1
[ "$(sed -n 2p $TMP)" = "test" ] || exit 1
# Kitty z-indexes and SIXEL's temporal plane are properties of the input, not
# of the outer terminal. Exercise the logical scene through the fallback
# backend: higher nonnegative z-indexes cover lower ones and text, ordinary
# negative z-indexes cover backgrounds but not text, and very negative ones
# remain below the background.
Z_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=21;/wAA/w==\033\\'
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=22;/////w==\033\\'
printf 'X\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=1,z=2,c=1,r=1\033\\'
printf '\033_Ga=p,q=2,C=1,i=22,p=2,z=1,c=1,r=1\033\\'
printf '\033[2;1H\033[41m \033[0m\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=3,z=-1,c=1,r=1\033\\'
printf '\033[3;1HY\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=4,z=-1,c=1,r=1\033\\'
printf '\033[4;1H\033[41m \033[0m\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=5,z=-1073741825,c=1,r=1\033\\'
$TEST_TMUX wait-for image-z-top-$$
printf '\033_Ga=d,d=z,q=2,z=2\033\\'
$TEST_TMUX wait-for image-z-lower-$$
printf '\033_Ga=d,d=z,q=2,z=1\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$Z_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "." ] || exit 1
[ "$(sed -n 2p $TMP)" = "." ] || exit 1
[ "$(sed -n 3p $TMP)" = "Y" ] || exit 1
[ -z "$(sed -n 4p $TMP)" ] || exit 1
$TMUX2 wait-for -S image-z-top-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "@" ] || exit 1
$TMUX2 wait-for -S image-z-lower-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "X" ] || exit 1
# A weighted median at the maximum channel level must still leave colours on
# both sides of the split. This skewed black, grey and white image used to stop
# palette generation after one colour instead of producing three.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 20 -y 4 "
printf '\033_Ga=T,q=2,C=1,f=24,s=100,v=1,c=10,r=1;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 20 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
grep -a '#2;2;' $TMP >/dev/null || exit 1
# A pane redraw on a SIXEL terminal must not clear the status line, which is
# outside the window scene and may not be redrawn after the pane scrolls.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 20 -y 8 || exit 1
$TMUX2 set -g 'status-format[0]' 'STATUS' || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 20 -y 8 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX2 send-keys -l 'seq 30' || exit 1
$TMUX2 send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -p >$TMP || exit 1
[ "$(tail -n 1 $TMP)" = "STATUS" ] || exit 1
exit 0

View File

@@ -5,9 +5,12 @@
start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n' start_pane alternate 10 3 'MAIN\033[?1049hALT\033[?1049lZ\n'
check_capture alternate 'MAINZ' 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 check_capture osc133 'xxp>cmd
xymore xymore
xzmore
xwmore
xvmore
zzout zzout
qbad qbad
qqbig qqbig
@@ -15,10 +18,13 @@ zzzok'
check_raw_matches osc133 \ check_raw_matches osc133 \
'L 0 \(0\) flags=START_PROMPT,START_COMMAND\[[0-9a-f]+\].* osc133=2,4,0,0,0' \ '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 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 2 \(2\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 3 \(3\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,1,4,255' \ 'L 3 \(3\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 4 \(4\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,2,5,255' \ 'L 4 \(4\) flags=SECOND_PROMPT\[[0-9a-f]+\].* osc133=2,0,0,0,0' \
'L 5 \(5\) flags=START_OUTPUT,END_OUTPUT\[[0-9a-f]+\].* osc133=0,0,3,5,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 $TMUX kill-server 2>/dev/null
exit $exit_status 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

File diff suppressed because one or more lines are too long

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

@@ -1,97 +0,0 @@
#!/bin/sh
# Moving a popup away from a pane prompt must recompose the prompt over the
# pane contents restored by damage redraw.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lpopup-prompt-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpopup-prompt-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
POPUP_PID=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
C="sh -c 'i=1; while [ \$i -le 10 ]; do printf \"\\033[%d;1HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\" \$i; i=\$((i + 1)); done; exec sleep 100'"
$INNER new-session -d -s inner -x 40 -y 10 "$C" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpopup-prompt-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
$INNER command-prompt -b -P -t "$CLIENT" -p 'PROMPT-MARK>' \
'display-message -- %1' || exit 1
wait_outer_has PROMPT-MARK
$INNER display-popup -t "$CLIENT" -x 0 -y 10 -w 16 -h 3 -E \
"sh -c 'printf POPUP-MARKER; exec sleep 100'" &
POPUP_PID=$!
wait_outer_has POPUP-MARKER
mouse 0 10 8 M
mouse 32 11 8 M
mouse 32 25 4 M
mouse 0 25 4 m
wait_outer_has PROMPT-MARK
exit 0

View File

@@ -1,155 +0,0 @@
#!/bin/sh
# Moving and resizing a popup must restore its old rectangle, including pane
# border status, on the attached client's terminal.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lpopup-drag-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpopup-drag-outer-$$ -f/dev/null"
BASE=$DIR/base
CAPTURE=$DIR/capture
RAW=$DIR/raw
POPUP_PID=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_rows_match()
{
first=$1
last=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n "${first},${last}p" "$BASE" >"$DIR/want"
sed -n "${first},${last}p" "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer rows $first-$last were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
# A left-button border drag moves the popup. The first motion starts the drag;
# the second changes its position.
move_popup()
{
mouse 0 10 1 M
mouse 32 11 1 M
mouse 32 26 8 M
mouse 0 26 8 m
}
# A right-button bottom-right-border drag resizes the popup.
resize_popup()
{
mouse 2 "$1" "$2" M
mouse 34 "$3" "$4" M
mouse 34 "$5" "$6" M
mouse 2 "$5" "$6" m
}
C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; printf \"\\033[19;45H\\033[38;5;201mSCOPE\\033[0m\"; exec sleep 100'"
$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -w pane-border-status top || exit 1
$INNER set-option -w pane-border-format 'DAMAGE-STATUS-RESTORED' || exit 1
$OUTER new-session -d -s outer -x 60 -y 20 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER pipe-pane -O -t outer:0.0 "cat >'$RAW'" || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpopup-drag-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has SCOPE
grep -qa '38;5;201' "$RAW" || fail "scope colour was not drawn initially"
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
grep -q DAMAGE-STATUS-RESTORED "$BASE" ||
fail "pane border status was not visible before popup"
$INNER display-popup -t "$CLIENT" -x 0 -y 0 -w 28 -h 6 -E \
"sh -c 'printf POPUP-MARKER; exec sleep 100'" &
POPUP_PID=$!
wait_outer_has POPUP-MARKER
$OUTER pipe-pane -t outer:0.0 || exit 1
$OUTER pipe-pane -O -t outer:0.0 "cat >'$RAW'" || exit 1
move_popup
wait_rows_match 1 6
$OUTER pipe-pane -t outer:0.0 || exit 1
grep -qa '38;5;201' "$RAW" &&
fail "popup move redrew content outside its old and new rectangles"
grep -q DAMAGE-STATUS-RESTORED "$CAPTURE" ||
fail "pane border status was not restored after popup move"
# The moved popup is 28x6 at zero-based 16,7. Shrink it to 21x3, then require
# the three rows vacated at the bottom to match the original scene.
resize_popup 44 13 43 13 38 11
wait_rows_match 11 13
# Grow it back to 28x6 and require a single, intact popup frame.
resize_popup 37 10 36 10 45 14
wait_outer_has POPUP-MARKER
corners=$(grep -o '┌' "$CAPTURE" | wc -l)
[ "$corners" -eq 1 ] || fail "expected one popup frame, found $corners"
exit 0

View File

@@ -1,165 +0,0 @@
#!/bin/sh
# Moving a popup away from the tmux status area must restore status and window
# cells. Window damage alone cannot describe cells outside the window scene,
# and top status lines must be removed before translating to window rows.
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
BASE=$DIR/base
CAPTURE=$DIR/capture
INNER=
OUTER=
POPUP_PID=
N=0
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup_scene()
{
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
POPUP_PID=
}
cleanup()
{
cleanup_scene
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_rows_restored()
{
first=$1
last=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n "${first},${last}p" "$BASE" >"$DIR/want"
sed -n "${first},${last}p" "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "rows $first-$last under the old popup were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
setup()
{
position=$1
lines=$2
cleanup_scene
N=$((N + 1))
INNER="$TEST_TMUX -Lpopup-status-inner-$$-$N -f/dev/null"
OUTER="$TEST_TMUX -Lpopup-status-outer-$$-$N -f/dev/null"
C="sh -c 'i=1; while [ \$i -le 10 ]; do printf \"\\033[%d;1HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\" \$i; i=\$((i + 1)); done; exec sleep 100'"
$INNER new-session -d -s inner -x 40 -y 10 "$C" || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
if [ "$lines" -eq 1 ]; then
$INNER set-option -g status on || exit 1
else
$INNER set-option -g status "$lines" || exit 1
fi
$INNER set-option -g status-position "$position" || exit 1
$INNER set-option -g status-format[0] \
'STATUS-LINE-MARK-01234567890123456789012' || exit 1
if [ "$lines" -gt 1 ]; then
$INNER set-option -g status-format[1] \
'SECOND-STATUS-MARK-012345678901234567890' || exit 1
fi
$INNER set-option -g status-interval 0 || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpopup-status-inner-$$-$N -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has STATUS-LINE-MARK
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
}
open_popup()
{
y=$1
height=$2
$INNER display-popup -t "$CLIENT" -x 0 -y "$y" -w 16 -h "$height" -E \
"sh -c 'printf POPUP-MARKER; exec sleep 100'" &
POPUP_PID=$!
wait_outer_has POPUP-MARKER
}
if [ "${STATUS_CASE:-all}" != top ]; then
# A popup clipped against a one-line bottom status must restore that line.
setup bottom 1
open_popup 10 3
mouse 0 10 8 M
mouse 32 11 8 M
mouse 32 25 4 M
mouse 0 25 4 m
wait_rows_restored 10 10
fi
if [ "${STATUS_CASE:-all}" != bottom ]; then
# A popup beginning in a two-line top status also covers the first window
# rows. Restore both coordinate spaces after it moves.
setup top 2
open_popup 1 4
mouse 0 10 1 M
mouse 32 11 1 M
mouse 32 25 7 M
mouse 0 25 7 m
wait_rows_restored 1 4
fi
exit 0

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,81 +0,0 @@
#!/bin/sh
# Switching windows in the same session must redraw the attached client. The
# session object is shared, so its current winlink cannot be compared after it
# has already been changed.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lswitch-redraw-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
$INNER new-session -d -s inner -x 40 -y 8 \
"printf '\033[2J\033[HA-WINDOW'; exec sleep 100" || exit 1
$INNER new-window -d -t inner:1 \
"printf '\033[2J\033[HB-WINDOW'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null attach-session -t inner:0" ||
exit 1
wait_for_client
wait_outer_has A-WINDOW
$INNER switch-client -c "$CLIENT" -t inner:1.0 || exit 1
[ "$($INNER display-message -p -t inner '#{window_index}')" -eq 1 ] ||
fail "server did not select window 1"
wait_outer_has B-WINDOW
exit 0

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: # This exercises:
# - new-window placement: next free index, explicit index, index in use with # - new-window placement: next free index, explicit index, index in use with
# and without -k, -a (after) and -b (before) insertion with shuffling, and # 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, # - move-window to a free index, to an occupied index with and without -k,
# -a insertion and -r renumbering (including base-index); # -a insertion and -r renumbering (including base-index);
# - renumber-windows closing gaps; # - 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_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' 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. # Clean up to a known arrangement.
check_ok kill-window -t W:wB check_ok kill-window -t W:wB
check_ok kill-window -t W:wA check_ok kill-window -t W:wA

View File

@@ -386,12 +386,10 @@ recalculate_size(struct window *w, int now)
* got a resize scheduled, then use the new size; otherwise the old. * got a resize scheduled, then use the new size; otherwise the old.
*/ */
if (w->flags & WINDOW_RESIZE) { if (w->flags & WINDOW_RESIZE) {
if (!now && changed && w->new_sx == sx && w->new_sy == sy && if (!now && changed && w->new_sx == sx && w->new_sy == sy)
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
changed = 0; changed = 0;
} else { } else {
if (!now && changed && w->sx == sx && w->sy == sy && if (!now && changed && w->sx == sx && w->sy == sy)
w->xpixel == xpixel && w->ypixel == ypixel)
changed = 0; changed = 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> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -99,12 +99,6 @@ enum redraw_span_type {
#define REDRAW_ALL 0x7fffffff #define REDRAW_ALL 0x7fffffff
#define REDRAW_IS_ALL(flags) ((flags) == REDRAW_ALL) #define REDRAW_IS_ALL(flags) ((flags) == REDRAW_ALL)
enum redraw_image_phase {
REDRAW_IMAGES_BEFORE,
REDRAW_TEXT,
REDRAW_IMAGES_AFTER
};
/* UTF-8 isolate characters. */ /* UTF-8 isolate characters. */
#define REDRAW_START_ISOLATE "\342\201\246" #define REDRAW_START_ISOLATE "\342\201\246"
#define REDRAW_END_ISOLATE "\342\201\251" #define REDRAW_END_ISOLATE "\342\201\251"
@@ -211,24 +205,6 @@ struct redraw_scene {
u_int oy; u_int oy;
}; };
/* A single damaged window-coordinate rectangle. */
struct redraw_damage {
u_int x;
u_int y;
u_int sx;
u_int sy;
int flags;
#define REDRAW_DAMAGE_SCROLL 0x1
TAILQ_ENTRY(redraw_damage) entry;
};
/*
* Cap on the number of pending damage rectangles per window before they are
* collapsed into a single rectangle covering their union.
*/
#define REDRAW_DAMAGE_MAX 16
/* Cell for building the scene. */ /* Cell for building the scene. */
struct redraw_build_cell { struct redraw_build_cell {
struct redraw_span_data data; struct redraw_span_data data;
@@ -1081,13 +1057,6 @@ redraw_free_scene(struct redraw_scene *scene)
free(scene); free(scene);
} }
/* Does a client's cached scene show this window? */
int
redraw_client_has_window(struct client *c, struct window *w)
{
return (c->redraw_scene != NULL && c->redraw_scene->w == w);
}
/* Mark a window's cached redraw scenes as out of date. */ /* Mark a window's cached redraw scenes as out of date. */
void void
redraw_invalidate_scene(struct window *w) redraw_invalidate_scene(struct window *w)
@@ -1095,184 +1064,6 @@ redraw_invalidate_scene(struct window *w)
w->redraw_scene_generation++; w->redraw_scene_generation++;
} }
/* Free all pending damage for a window. */
void
redraw_free_damage(struct window *w)
{
struct redraw_damage *rd, *rd1;
TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) {
TAILQ_REMOVE(&w->damage, rd, entry);
free(rd);
}
w->damage_count = 0;
#ifdef ENABLE_IMAGES
w->image_scroll_pending = 0;
#endif
}
/* Collapse all pending damage for a window into one rectangle - its union. */
static void
redraw_collapse_damage(struct window *w)
{
struct redraw_damage *rd, *rd1, *first;
u_int x0, y0, x1, y1;
first = TAILQ_FIRST(&w->damage);
if (first == NULL)
return;
x0 = first->x;
y0 = first->y;
x1 = first->x + first->sx;
y1 = first->y + first->sy;
TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) {
if (rd->x < x0)
x0 = rd->x;
if (rd->y < y0)
y0 = rd->y;
if (rd->x + rd->sx > x1)
x1 = rd->x + rd->sx;
if (rd->y + rd->sy > y1)
y1 = rd->y + rd->sy;
first->flags &= rd->flags;
if (rd != first) {
TAILQ_REMOVE(&w->damage, rd, entry);
free(rd);
}
}
first->x = x0;
first->y = y0;
first->sx = x1 - x0;
first->sy = y1 - y0;
w->damage_count = 1;
}
/*
* Record a damaged window-coordinate rectangle. Clips it to the window,
* merges it with an existing rectangle where doing so does not make the
* result substantially larger than the two combined, and collapses the
* whole list to its union once it grows past a modest cap.
*
* This only records damage - nothing consumes it yet.
*/
static void
redraw_damage_window_flags(struct window *w, u_int x, u_int y, u_int sx,
u_int sy, int flags)
{
struct redraw_damage *rd;
u_int x0, y0, x1, y1, area, union_area;
if (x >= w->sx || y >= w->sy)
return;
if (x + sx > w->sx)
sx = w->sx - x;
if (y + sy > w->sy)
sy = w->sy - y;
if (sx == 0 || sy == 0)
return;
TAILQ_FOREACH(rd, &w->damage, entry) {
/* Skip unless overlapping or directly adjacent. */
if (x > rd->x + rd->sx || rd->x > x + sx ||
y > rd->y + rd->sy || rd->y > y + sy)
continue;
x0 = (x < rd->x) ? x : rd->x;
y0 = (y < rd->y) ? y : rd->y;
x1 = (x + sx > rd->x + rd->sx) ? x + sx : rd->x + rd->sx;
y1 = (y + sy > rd->y + rd->sy) ? y + sy : rd->y + rd->sy;
area = sx * sy + rd->sx * rd->sy;
union_area = (x1 - x0) * (y1 - y0);
if (union_area > 2 * area)
continue;
rd->x = x0;
rd->y = y0;
rd->sx = x1 - x0;
rd->sy = y1 - y0;
rd->flags &= flags;
return;
}
rd = xcalloc(1, sizeof *rd);
rd->x = x;
rd->y = y;
rd->sx = sx;
rd->sy = sy;
rd->flags = flags;
TAILQ_INSERT_TAIL(&w->damage, rd, entry);
w->damage_count++;
if (w->damage_count > REDRAW_DAMAGE_MAX)
redraw_collapse_damage(w);
}
void
redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
{
redraw_damage_window_flags(w, x, y, sx, sy, 0);
}
#ifdef ENABLE_IMAGES
/* Record damage caused by an image moving with a terminal scroll. */
void
redraw_damage_window_scroll(struct window *w, u_int x, u_int y, u_int sx,
u_int sy)
{
if (!w->image_scroll_pending) {
w->image_scroll_epoch++;
w->image_scroll_pending = 1;
}
redraw_damage_window_flags(w, x, y, sx, sy, REDRAW_DAMAGE_SCROLL);
}
/* Remember whether this client used terminal scrolling for the damage. */
void
redraw_image_scroll_result(struct tty *tty, const struct tty_ctx *ctx,
int failed)
{
struct window_pane *wp = ctx->arg;
struct window *w;
if (wp == NULL || (w = wp->window) == NULL || !w->image_scroll_pending)
return;
if (tty->image_scroll_window != w ||
tty->image_scroll_epoch != w->image_scroll_epoch) {
tty->image_scroll_window = w;
tty->image_scroll_epoch = w->image_scroll_epoch;
tty->image_scroll_failed = 0;
}
if (failed)
tty->image_scroll_failed = 1;
}
#endif
/* Record damage for every pane border-status row in a window. */
static void
redraw_damage_window_pane_status(struct window *w)
{
struct window_pane *wp;
u_int y;
int status;
TAILQ_FOREACH(wp, &w->panes, entry) {
status = window_pane_get_pane_status(wp);
if (status == PANE_STATUS_TOP) {
if (wp->yoff == 0)
continue;
y = wp->yoff - 1;
} else if (status == PANE_STATUS_BOTTOM)
y = wp->yoff + wp->sy;
else
continue;
if (y < w->sy)
redraw_damage_window(w, 0, y, w->sx, 1);
}
}
/* Mark all cached redraw scenes as out of date. */ /* Mark all cached redraw scenes as out of date. */
void void
redraw_invalidate_all_scenes(void) redraw_invalidate_all_scenes(void)
@@ -1315,8 +1106,7 @@ redraw_get_scene(struct client *c)
/* Draw a pane span. */ /* Draw a pane span. */
static void static void
redraw_draw_pane_span(struct redraw_draw_ctx *dctx, redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int x, u_int y, u_int n, struct redraw_span *span, u_int x, u_int y, u_int n)
enum redraw_image_phase phase)
{ {
struct redraw_scene *scene = dctx->scene; struct redraw_scene *scene = dctx->scene;
struct client *c = scene->c; struct client *c = scene->c;
@@ -1334,14 +1124,7 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
px = span->data.p.px + (x - span->x); px = span->data.p.px + (x - span->x);
py = span->data.p.py; py = span->data.p.py;
#ifdef ENABLE_IMAGES tty_draw_line(tty, s, px, py, n, x, y, &style_ctx);
if (phase != REDRAW_TEXT)
image_draw_line(tty, s, px, py, n, x, y,
phase == REDRAW_IMAGES_BEFORE, &style_ctx);
else
#endif
if (phase == REDRAW_TEXT)
tty_draw_line(tty, s, px, py, n, x, y, &style_ctx);
} }
/* Get default border style for spans without a pane. */ /* Get default border style for spans without a pane. */
@@ -1593,15 +1376,10 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx,
tty_draw_line(tty, s, px, span->data.m.py, n, x, y, NULL); tty_draw_line(tty, s, px, span->data.m.py, n, x, y, NULL);
} }
/* /* Draw a span. */
* Draw a span, restricted to [clip_x, clip_x + clip_n) - a caller drawing
* the whole span passes the span's own x/width here; a caller drawing only
* a damaged sub-range passes that range instead. Overlay clipping (menus,
* popups) is then applied on top of this, exactly as before.
*/
static void static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span, redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y, u_int clip_x, u_int clip_n, enum redraw_image_phase phase) u_int y)
{ {
struct redraw_scene *scene = dctx->scene; struct redraw_scene *scene = dctx->scene;
struct redraw_span_data *data = &span->data; struct redraw_span_data *data = &span->data;
@@ -1615,19 +1393,17 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS) if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS)
return; return;
r = tty_check_overlay_range(tty, clip_x, y, clip_n); r = tty_check_overlay_range(tty, span->x, y, span->width);
for (i = 0; i < r->used; i++) { for (i = 0; i < r->used; i++) {
rr = &r->ranges[i]; rr = &r->ranges[i];
if (rr->nx == 0) if (rr->nx == 0)
continue; continue;
x = rr->px; x = rr->px;
n = rr->nx; n = rr->nx;
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE)
continue;
switch (span->data.type) { switch (span->data.type) {
case REDRAW_SPAN_PANE: case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, x, y, n, phase); redraw_draw_pane_span(dctx, span, x, y, n);
break; break;
case REDRAW_SPAN_BORDER: case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY: case REDRAW_SPAN_EMPTY:
@@ -1668,55 +1444,24 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
if (bottom > (int)scene->sy) if (bottom > (int)scene->sy)
bottom = scene->sy; bottom = scene->sy;
#ifdef ENABLE_IMAGES for (y = top; y < bottom; y++) {
/* line = &scene->lines[y];
* Only erase cells this pane currently owns in the scene, not its raw if (dctx->flags & REDRAW_STATUS_TOP)
* geometry - a floating pane may be occluding part of this pane's cy = dctx->status_lines + y;
* rectangle, and erasing under it would leave those cells blank with else
* nothing to redraw them back in. cy = y;
*/ if (flags & REDRAW_PANE) {
if (flags & REDRAW_PANE) {
for (y = top; y < bottom; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
spans = &line->spans[REDRAW_SPAN_PANE]; spans = &line->spans[REDRAW_SPAN_PANE];
TAILQ_FOREACH(span, spans, entry) { TAILQ_FOREACH(span, spans, entry) {
if (span->data.p.wp == wp) { if (span->data.p.wp == wp)
image_redraw_start(&scene->c->tty, redraw_draw_span(dctx, span, cy);
span->x, cy, span->width, 1);
}
} }
} }
} if (flags & REDRAW_PANE_SCROLLBAR) {
#endif spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
TAILQ_FOREACH(span, spans, entry) {
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE; if (span->data.sb.wp == wp)
phase <= REDRAW_IMAGES_AFTER; phase++) { redraw_draw_span(dctx, span, cy);
for (y = top; y < bottom; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
if (flags & REDRAW_PANE) {
spans = &line->spans[REDRAW_SPAN_PANE];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.p.wp == wp)
redraw_draw_span(dctx, span, cy, span->x,
span->width, phase);
}
}
if (phase == REDRAW_TEXT &&
(flags & REDRAW_PANE_SCROLLBAR)) {
spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.sb.wp == wp)
redraw_draw_span(dctx, span, cy, span->x,
span->width, phase);
}
} }
} }
} }
@@ -1732,17 +1477,13 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
struct redraw_span *span; struct redraw_span *span;
u_int y, cy, type; u_int y, cy, type;
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE; for (y = 0; y < scene->sy; y++) {
phase <= REDRAW_IMAGES_AFTER; phase++) { line = &scene->lines[y];
for (y = 0; y < scene->sy; y++) { if (dctx->flags & REDRAW_STATUS_TOP)
line = &scene->lines[y]; cy = dctx->status_lines + y;
if (dctx->flags & REDRAW_STATUS_TOP) else
cy = dctx->status_lines + y; cy = y;
else for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE)
continue;
if (!REDRAW_IS_ALL(flags)) { if (!REDRAW_IS_ALL(flags)) {
switch (type) { switch (type) {
case REDRAW_SPAN_PANE: case REDRAW_SPAN_PANE:
@@ -1779,9 +1520,7 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
} }
spans = &line->spans[type]; spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy, span->x, span->width, redraw_draw_span(dctx, span, cy);
phase);
}
} }
} }
} }
@@ -1802,8 +1541,7 @@ redraw_draw_menu_lines(struct redraw_draw_ctx *dctx)
else else
cy = y; cy = y;
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry) TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry)
redraw_draw_span(dctx, span, cy, span->x, span->width, redraw_draw_span(dctx, span, cy);
REDRAW_TEXT);
} }
} }
@@ -1883,24 +1621,6 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx,
dctx->flags |= REDRAW_ISOLATES; dctx->flags |= REDRAW_ISOLATES;
} }
/* Build a pane prompt into a one-line screen. */
static void
redraw_make_pane_prompt(struct window_pane *wp, struct screen *screen)
{
struct screen_write_ctx ctx;
struct prompt_draw_data pdd;
screen_init(screen, wp->sx, 1, 0);
screen_write_start(&ctx, screen);
pdd.ctx = &ctx;
pdd.cursor_x = &wp->prompt_cx;
pdd.area_x = 0;
pdd.area_width = wp->sx;
pdd.prompt_line = 0;
prompt_draw(wp->prompt, &pdd);
screen_write_stop(&ctx);
}
/* Draw a pane's prompt over its content. */ /* Draw a pane's prompt over its content. */
static void static void
redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp) redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
@@ -1909,6 +1629,8 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
struct client *c = scene->c; struct client *c = scene->c;
struct tty *tty = &c->tty; struct tty *tty = &c->tty;
struct screen screen; struct screen screen;
struct screen_write_ctx ctx;
struct prompt_draw_data pdd;
int ox = scene->ox, oy = scene->oy; int ox = scene->ox, oy = scene->oy;
int sx = scene->sx, sy = scene->sy; int sx = scene->sx, sy = scene->sy;
int line, cy, px, offset, width, wy; int line, cy, px, offset, width, wy;
@@ -1941,7 +1663,16 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
if (px + width > sx) if (px + width > sx)
width = sx - px; width = sx - px;
redraw_make_pane_prompt(wp, &screen); screen_init(&screen, wp->sx, 1, 0);
screen_write_start(&ctx, &screen);
pdd.ctx = &ctx;
pdd.cursor_x = &wp->prompt_cx;
pdd.area_x = 0;
pdd.area_width = wp->sx;
pdd.prompt_line = 0;
prompt_draw(wp->prompt, &pdd);
screen_write_stop(&ctx);
tty_draw_line(tty, &screen, 0, offset, width, px, cy, NULL); tty_draw_line(tty, &screen, 0, offset, width, px, cy, NULL);
screen_free(&screen); screen_free(&screen);
} }
@@ -1973,8 +1704,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
redraw = status_prompt_redraw(c); redraw = status_prompt_redraw(c);
else else
redraw = status_redraw(c); redraw = status_redraw(c);
if (!redraw && (~c->flags & CLIENT_REDRAWSTATUSALWAYS) && if (!redraw && !REDRAW_IS_ALL(flags)) {
!REDRAW_IS_ALL(flags)) {
flags &= ~REDRAW_STATUS; flags &= ~REDRAW_STATUS;
if (flags == 0) if (flags == 0)
return; return;
@@ -2008,8 +1738,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
else else
loop->flags &= ~PANE_NEWSTATUS; loop->flags &= ~PANE_NEWSTATUS;
width = redraw_pane_status_width(&dctx, loop, width = redraw_pane_status_width(&dctx, loop, &first);
&first);
if (width == 0) if (width == 0)
continue; continue;
@@ -2040,28 +1769,13 @@ 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 */
#ifdef ENABLE_IMAGES
if (wp == NULL && (flags & REDRAW_PANE)) {
y = 0;
if (dctx.flags & REDRAW_STATUS_TOP)
y = dctx.status_lines;
image_redraw_start(tty, 0, y, scene->sx, scene->sy);
}
#endif
tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL); tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL);
if (wp != NULL) if (wp != NULL)
redraw_draw_pane_lines(&dctx, wp, flags); redraw_draw_pane_lines(&dctx, wp, flags);
else else
redraw_draw_lines(&dctx, flags); redraw_draw_lines(&dctx, flags);
#ifdef ENABLE_IMAGES
if ((flags & REDRAW_PANE) &&
(image_backend_flags(tty) &
(IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_SCROLLS)) ==
IMAGE_BACKEND_GRAPHICAL)
redraw_damage_window_pane_status(scene->w);
#endif
if (flags & REDRAW_PANE) { if (flags & REDRAW_PANE) {
if (wp != NULL) if (wp != NULL)
@@ -2100,7 +1814,15 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
c->overlay_draw(c, c->overlay_data); c->overlay_draw(c, c->overlay_data);
tty_reset(tty); tty_reset(tty);
tty_sync_end(tty);
#ifdef ENABLE_SIXEL
if (wp != NULL)
tty_draw_images(c, wp);
else {
TAILQ_FOREACH(loop, &scene->w->panes, entry)
tty_draw_images(c, loop);
}
#endif
log_debug("%s: finished @%u redraw", c->name, scene->w->id); log_debug("%s: finished @%u redraw", c->name, scene->w->id);
} }
@@ -2153,7 +1875,7 @@ redraw_screen(struct client *c)
} else { } else {
if (c->flags & CLIENT_REDRAWBORDERS) if (c->flags & CLIENT_REDRAWBORDERS)
flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS); flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS);
if (c->flags & (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS)) if (c->flags & CLIENT_REDRAWSTATUS)
flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS); flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWOVERLAY) if (c->flags & CLIENT_REDRAWOVERLAY)
flags |= REDRAW_OVERLAY; flags |= REDRAW_OVERLAY;
@@ -2181,230 +1903,3 @@ redraw_pane_scrollbar(struct client *c, struct window_pane *wp)
{ {
redraw_draw(c, wp, REDRAW_PANE_SCROLLBAR); redraw_draw(c, wp, REDRAW_PANE_SCROLLBAR);
} }
/*
* A REDRAW_SPAN_STATUS span within a damaged rectangle needs its content
* rebuilt and force-drawn regardless of whether that content has logically
* changed. window_make_pane_status()'s grid_compare() only tells us
* whether the *content* changed, not whether the physical cells were
* disturbed by something else (e.g. a floating pane sliding across this
* row) - and being inside a damage rectangle already proves that happened.
* Without this, redraw_draw_span() silently skips REDRAW_SPAN_STATUS spans
* whenever PANE_NEWSTATUS is not set, leaving a pane's border-status title
* blank until some unrelated redraw happens to touch it (e.g. a focus
* change or window resize).
*/
static void
redraw_damage_refresh_status(struct redraw_draw_ctx *dctx,
struct window_pane *wp)
{
struct redraw_span *first;
u_int width;
if (wp->flags & PANE_NEWSTATUS)
return;
width = redraw_pane_status_width(dctx, wp, &first);
if (width == 0)
return;
window_make_pane_status(wp, dctx->scene->c, width, first);
wp->flags |= PANE_NEWSTATUS;
}
/*
* Grow a clipped span range by one cell on either edge that isn't already at
* the span's own boundary. A clip edge that lands mid-character (this is a
* damage rectangle, so its edges are geometric and have no idea what's in
* the grid) may be sitting on the second, padding half of a wide character
* whose first half falls just outside the requested range - growing by one
* cell is always enough to pull the whole character back in, since no grid
* cell is ever wider than two columns, and clamping to the span's own x and
* width keeps this from bleeding into a neighbouring span.
*/
static void
redraw_damage_grow_span_clip(struct redraw_span *span, u_int *xp, u_int *endp)
{
if (*xp > span->x)
(*xp)--;
if (*endp < span->x + span->width)
(*endp)++;
}
/* Recompose a pane's prompt over a damaged section of its display row. */
static void
redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int y, u_int x, u_int n)
{
struct redraw_scene *scene = dctx->scene;
struct window_pane *wp = span->data.p.wp;
struct tty *tty = &scene->c->tty;
struct visible_ranges *r;
struct visible_range *rr;
struct screen screen;
u_int i, px, width, prompt_y;
if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0)
return;
if (dctx->flags & REDRAW_STATUS_TOP)
prompt_y = 0;
else
prompt_y = wp->sy - 1;
if (span->data.p.py != prompt_y)
return;
redraw_make_pane_prompt(wp, &screen);
r = tty_check_overlay_range(tty, x, y, n);
for (i = 0; i < r->used; i++) {
rr = &r->ranges[i];
if (rr->nx == 0)
continue;
px = span->data.p.px + (rr->px - span->x);
if (px >= screen_size_x(&screen))
continue;
width = rr->nx;
if (width > screen_size_x(&screen) - px)
width = screen_size_x(&screen) - px;
tty_draw_line(tty, &screen, px, 0, width, rr->px, y, NULL);
}
screen_free(&screen);
}
/*
* Compose exactly the cells within a damaged rectangle (already in this
* client's own scene coordinates), rather than a whole pane. For each row
* in range, every span of every type whose x-range intersects the
* rectangle is drawn restricted to just the intersected sub-range.
*/
static void
redraw_draw_damage_rect(struct redraw_draw_ctx *dctx, u_int x, u_int y,
u_int sx, u_int sy, int skip_images)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_line *line;
struct redraw_spans *spans;
struct redraw_span *span;
u_int cy, yy, clip_x, clip_end, type;
if (x >= scene->sx || y >= scene->sy)
return;
if (x + sx > scene->sx)
sx = scene->sx - x;
if (y + sy > scene->sy)
sy = scene->sy - y;
if (sx == 0 || sy == 0)
return;
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_IMAGES_AFTER; phase++) {
if (skip_images && phase != REDRAW_TEXT)
continue;
for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + yy;
else
cy = yy;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE)
continue;
if (type == REDRAW_SPAN_STATUS)
continue;
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) {
clip_x = (span->x > x) ? span->x : x;
clip_end = (span->x + span->width < x + sx) ?
span->x + span->width : x + sx;
if (clip_end <= clip_x)
continue;
if (type == REDRAW_SPAN_STATUS) {
redraw_damage_refresh_status(dctx,
span->data.st.wp);
}
redraw_damage_grow_span_clip(span, &clip_x,
&clip_end);
redraw_draw_span(dctx, span, cy, clip_x,
clip_end - clip_x, phase);
if (phase == REDRAW_TEXT && type == REDRAW_SPAN_PANE) {
redraw_damage_draw_pane_prompt(dctx,
span, cy, clip_x,
clip_end - clip_x);
}
}
}
}
}
/* SIXEL image output may disturb status cells; compose them last. */
for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + yy;
else
cy = yy;
spans = &line->spans[REDRAW_SPAN_STATUS];
TAILQ_FOREACH(span, spans, entry) {
clip_x = (span->x > x) ? span->x : x;
clip_end = (span->x + span->width < x + sx) ?
span->x + span->width : x + sx;
if (clip_end <= clip_x)
continue;
redraw_damage_refresh_status(dctx, span->data.st.wp);
redraw_damage_grow_span_clip(span, &clip_x, &clip_end);
redraw_draw_span(dctx, span, cy, clip_x, clip_end - clip_x,
REDRAW_TEXT);
}
}
}
/*
* Consume a client's window's pending damage by composing exactly the
* damaged cells, after clipping each rectangle to what this client can see
* and translating it into this client's own scene coordinates.
*
* Unlike redraw_pane(), this does not redraw a whole pane's worth of cells
* for a small disturbance - only the cells within the (clipped) rectangle
* are touched, via redraw_draw_damage_rect().
*/
void
redraw_client_damage(struct client *c)
{
struct window *w = c->session->curw->window;
struct redraw_scene *scene;
struct redraw_draw_ctx dctx;
struct redraw_damage *rd;
u_int ox, oy, sx, sy, x0, y0, x1, y1;
int skip_images;
if (TAILQ_EMPTY(&w->damage))
return;
scene = redraw_get_scene(c);
if (scene == NULL)
return;
redraw_set_draw_context(&dctx, scene);
redraw_get_window_offset(c, &ox, &oy, &sx, &sy);
tty_sync_start(&c->tty);
tty_update_mode(&c->tty, c->tty.mode & ~CURSOR_MODES, NULL);
TAILQ_FOREACH(rd, &w->damage, entry) {
skip_images = 0;
#ifdef ENABLE_IMAGES
if ((rd->flags & REDRAW_DAMAGE_SCROLL) &&
(image_backend_flags(&c->tty) & IMAGE_BACKEND_SCROLLS) &&
c->tty.image_scroll_window == w &&
c->tty.image_scroll_epoch == w->image_scroll_epoch &&
!c->tty.image_scroll_failed)
skip_images = 1;
#endif
x0 = (rd->x > ox) ? rd->x : ox;
y0 = (rd->y > oy) ? rd->y : oy;
x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx;
y1 = (rd->y + rd->sy < oy + sy) ? rd->y + rd->sy : oy + sy;
if (x0 >= x1 || y0 >= y1)
continue;
log_debug("%s: %s composing damage %u,%u %ux%u", __func__,
c->name, x0 - ox, y0 - oy, x1 - x0, y1 - y0);
redraw_draw_damage_rect(&dctx, x0 - ox, y0 - oy, x1 - x0,
y1 - y0, skip_images);
}
}

View File

@@ -34,10 +34,6 @@ static void screen_write_collect_clear(struct screen_write_ctx *, u_int,
static void screen_write_collect_scroll(struct screen_write_ctx *, u_int); static void screen_write_collect_scroll(struct screen_write_ctx *, u_int);
static void screen_write_collect_flush(struct screen_write_ctx *, int, static void screen_write_collect_flush(struct screen_write_ctx *, int,
const char *); const char *);
#ifdef ENABLE_IMAGES
static void screen_write_image_damage(struct screen_write_ctx *, u_int,
u_int, u_int, u_int);
#endif
static int screen_write_overwrite(struct screen_write_ctx *, static int screen_write_overwrite(struct screen_write_ctx *,
struct grid_cell *, u_int); struct grid_cell *, u_int);
static int screen_write_combine(struct screen_write_ctx *, static int screen_write_combine(struct screen_write_ctx *,
@@ -124,20 +120,14 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
evtimer_add(&w->offset_timer, &tv); evtimer_add(&w->offset_timer, &tv);
} }
/* /* Do a full redraw. */
* Called when a write could not be applied directly to the terminal and
* needs a redraw instead. Report damage for the requested rows. wp->yoff is
* already adjusted past any top pane-border-status row, so wp->yoff + py is
* the correct window-coordinate row.
*/
static void static void
screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny) screen_write_redraw_cb(const struct tty_ctx *ttyctx)
{ {
struct window_pane *wp = ttyctx->arg; struct window_pane *wp = ttyctx->arg;
if (wp == NULL) if (wp != NULL)
return; wp->flags |= PANE_REDRAW;
redraw_damage_window(wp->window, wp->xoff, wp->yoff + py, wp->sx, ny);
} }
/* Update context for client. */ /* Update context for client. */
@@ -381,19 +371,6 @@ screen_write_init(struct screen_write_ctx *ctx, struct screen *s)
ctx->bg = 8; ctx->bg = 8;
} }
#ifdef ENABLE_IMAGES
/* Damage temporal images when application input writes text cells. */
static void
screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y,
u_int sx, u_int sy)
{
image_redraw_area(ctx, x, y, sx, sy);
if (ctx->flags & SCREEN_WRITE_INPUT)
image_grid_damage(ctx->s->grid, x, ctx->s->grid->hsize + y,
sx, sy);
}
#endif
/* Initialize writing with a pane. */ /* Initialize writing with a pane. */
void void
screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp, screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp,
@@ -1354,11 +1331,9 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
memcpy(&gc, &grid_default_cell, sizeof gc); memcpy(&gc, &grid_default_cell, sizeof gc);
utf8_set(&gc.data, 'E'); utf8_set(&gc.data, 'E');
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_all(ctx); if (image_free_all(s) && ctx->wp != NULL)
if (ctx->flags & SCREEN_WRITE_INPUT) ctx->wp->flags |= PANE_REDRAW;
image_grid_damage(s->grid, 0, s->grid->hsize,
screen_size_x(s), screen_size_y(s));
#endif #endif
for (yy = 0; yy < screen_size_y(s); yy++) { for (yy = 0; yy < screen_size_y(s); yy++) {
@@ -1403,8 +1378,9 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1) if (s->cx > screen_size_x(s) - 1)
return; return;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
screen_write_initctx(ctx, &ttyctx, 0, 1); screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1443,8 +1419,9 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1) if (s->cx > screen_size_x(s) - 1)
return; return;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
screen_write_initctx(ctx, &ttyctx, 0, 1); screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1483,8 +1460,9 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1) if (s->cx > screen_size_x(s) - 1)
return; return;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
screen_write_initctx(ctx, &ttyctx, 0, 1); screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1517,8 +1495,9 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0) if (ny == 0)
ny = 1; ny = 1;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
if (s->cy < s->rupper || s->cy > s->rlower) { if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1584,8 +1563,9 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0) if (ny == 0)
ny = 1; ny = 1;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
if (s->cy < s->rupper || s->cy > s->rlower) { if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1656,8 +1636,9 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg)
if (gl->cellsize == 0 && COLOUR_DEFAULT(bg)) if (gl->cellsize == 0 && COLOUR_DEFAULT(bg))
return; return;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
flags = gl->flags & GRID_LINE_OSC133_FLAGS; flags = gl->flags & GRID_LINE_OSC133_FLAGS;
@@ -1694,8 +1675,9 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg)
if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg))) if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg)))
return; return;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg); grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg);
@@ -1720,8 +1702,9 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg)
return; return;
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1); if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
if (s->cx > sx - 1) if (s->cx > sx - 1)
@@ -1773,8 +1756,9 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
return; return;
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_scroll(ctx, 1); if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg); grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
@@ -1824,6 +1808,9 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
struct screen *s = ctx->s; struct screen *s = ctx->s;
struct grid *gd = s->grid; struct grid *gd = s->grid;
struct grid_line *gl; struct grid_line *gl;
#ifdef ENABLE_SIXEL
int redraw = 0;
#endif
u_int rupper = s->rupper, rlower = s->rlower; u_int rupper = s->rupper, rlower = s->rlower;
gl = grid_get_line(gd, gd->hsize + s->cy); gl = grid_get_line(gd, gd->hsize + s->cy);
@@ -1844,12 +1831,13 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
return; return;
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
if (rlower == screen_size_y(s) - 1) if (rlower == screen_size_y(s) - 1)
image_redraw_scroll(ctx, 1); redraw = image_scroll_up(s, 1);
else else
image_redraw_area(ctx, 0, rupper, screen_size_x(s), redraw = image_check_line(s, rupper, rlower - rupper);
rlower - rupper); if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg); grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
@@ -1875,8 +1863,9 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg)
ctx->bg = bg; ctx->bg = bg;
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_scroll(ctx, lines); if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
for (i = 0; i < lines; i++) { for (i = 0; i < lines; i++) {
@@ -1903,8 +1892,9 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
else if (lines > s->rlower - s->rupper + 1) else if (lines > s->rlower - s->rupper + 1)
lines = s->rlower - s->rupper + 1; lines = s->rlower - s->rupper + 1;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_scroll(ctx, lines); if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
for (i = 0; i < lines; i++) for (i = 0; i < lines; i++)
@@ -1943,8 +1933,9 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r; struct visible_ranges *r;
struct visible_range *ri; struct visible_range *ri;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy); if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
screen_write_initctx(ctx, &ttyctx, 1, 1); screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2023,8 +2014,9 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r; struct visible_ranges *r;
struct visible_range *ri; struct visible_range *ri;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1); if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
screen_write_initctx(ctx, &ttyctx, 1, 1); screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2095,8 +2087,9 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r; struct visible_ranges *r;
struct visible_range *ri; struct visible_range *ri;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
image_redraw_all(ctx); if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
screen_write_initctx(ctx, &ttyctx, 1, 1); screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2162,7 +2155,7 @@ screen_write_fullredraw(struct screen_write_ctx *ctx)
screen_write_initctx(ctx, &ttyctx, 1, 0); screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL) if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy); ttyctx.redraw_cb(&ttyctx);
} }
/* Trim collected items. */ /* Trim collected items. */
@@ -2599,8 +2592,9 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
} }
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1); if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif #endif
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
@@ -2730,11 +2724,6 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
return; return;
screen_write_initctx(ctx, &ttyctx, 0, 0); screen_write_initctx(ctx, &ttyctx, 0, 0);
#ifdef ENABLE_IMAGES
/* Input semantics decide which image layers this text damages. */
screen_write_image_damage(ctx, s->cx, s->cy, width, 1);
#endif
/* Handle overwriting of UTF-8 characters. */ /* Handle overwriting of UTF-8 characters. */
gl = grid_get_line(s->grid, s->grid->hsize + s->cy); gl = grid_get_line(s->grid, s->grid->hsize + s->cy);
if (gl->flags & GRID_LINE_EXTENDED) { if (gl->flags & GRID_LINE_EXTENDED) {
@@ -2962,9 +2951,6 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
force_wide = 0; force_wide = 0;
/* Set the new cell. */ /* Set the new cell. */
#ifdef ENABLE_IMAGES
screen_write_image_damage(ctx, cx - n, cy, n, 1);
#endif
grid_view_set_cell(gd, cx - n, cy, &last); grid_view_set_cell(gd, cx - n, cy, &last);
if (force_wide) if (force_wide)
grid_view_set_padding(gd, cx - 1, cy, last.bg); grid_view_set_padding(gd, cx - 1, cy, last.bg);
@@ -3100,21 +3086,63 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len,
tty_write(tty_cmd_rawstring, &ttyctx); tty_write(tty_cmd_rawstring, &ttyctx);
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
/* Write a SIXEL image. */ /* Write a SIXEL image. */
void void
screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si,
u_int bg) u_int bg)
{ {
struct image *im; struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct tty_ctx ttyctx;
u_int x, y, sx, sy, cx = s->cx, cy = s->cy, i, lines;
struct sixel_image *new;
im = sixel_to_image(si); if (screen_size_y(s) == 1)
if (im == NULL) {
sixel_free(si);
return; return;
sixel_size_in_cells(si, &x, &y);
if (x > screen_size_x(s) || y > screen_size_y(s) - 1) {
if (x > screen_size_x(s) - cx)
sx = screen_size_x(s) - cx;
else
sx = x;
if (y > screen_size_y(s) - 1)
sy = screen_size_y(s) - 1;
else
sy = y;
new = sixel_scale(si, 0, 0, 0, y - sy, sx, sy, 1);
sixel_free(si);
if (new == NULL)
return;
sixel_size_in_cells(new, &x, &y);
si = new;
} }
image_write_sixel(ctx, im, bg);
image_free(image_get_id(im)); sy = screen_size_y(s) - cy;
if (sy <= y) {
lines = y - sy + 1;
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
for (i = 0; i < lines; i++) {
grid_view_scroll_region_up(gd, 0, screen_size_y(s) - 1,
bg);
screen_write_collect_scroll(ctx, bg);
}
ctx->scrolled += lines;
if (lines > cy)
screen_write_cursormove(ctx, -1, 0, 0);
else
screen_write_cursormove(ctx, -1, cy - lines, 0);
}
screen_write_collect_flush(ctx, 0, __func__);
screen_write_initctx(ctx, &ttyctx, 0, 0);
ttyctx.image = image_store(s, si);
tty_write(tty_cmd_sixelimage, &ttyctx);
screen_write_cursormove(ctx, 0, cy + y, 0);
} }
#endif #endif
@@ -3147,7 +3175,7 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc,
screen_write_initctx(ctx, &ttyctx, 1, 0); screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL) if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy); ttyctx.redraw_cb(&ttyctx);
} }
/* Turn alternate screen off. */ /* Turn alternate screen off. */
@@ -3172,5 +3200,5 @@ screen_write_alternateoff(struct screen_write_ctx *ctx, struct grid_cell *gc,
screen_write_initctx(ctx, &ttyctx, 1, 0); screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL) if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy); ttyctx.redraw_cb(&ttyctx);
} }

View File

@@ -93,6 +93,11 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
s->tabs = NULL; s->tabs = NULL;
s->sel = NULL; s->sel = NULL;
#ifdef ENABLE_SIXEL
TAILQ_INIT(&s->images);
TAILQ_INIT(&s->saved_images);
#endif
s->write_list = NULL; s->write_list = NULL;
s->hyperlinks = NULL; s->hyperlinks = NULL;
@@ -126,6 +131,11 @@ screen_reinit(struct screen *s, int check)
screen_clear_selection(s); screen_clear_selection(s);
screen_free_titles(s); screen_free_titles(s);
#ifdef ENABLE_SIXEL
image_free_all(s);
#endif
screen_set_progress_bar(s, PROGRESS_BAR_HIDDEN, 0); screen_set_progress_bar(s, PROGRESS_BAR_HIDDEN, 0);
screen_reset_hyperlinks(s); screen_reset_hyperlinks(s);
} }
@@ -144,6 +154,10 @@ screen_reset_hyperlinks(struct screen *s)
void void
screen_free(struct screen *s) screen_free(struct screen *s)
{ {
#ifdef ENABLE_SIXEL
struct image *im;
#endif
free(s->sel); free(s->sel);
free(s->tabs); free(s->tabs);
free(s->path); free(s->path);
@@ -160,6 +174,17 @@ screen_free(struct screen *s)
hyperlinks_free(s->hyperlinks); hyperlinks_free(s->hyperlinks);
screen_free_titles(s); screen_free_titles(s);
#ifdef ENABLE_SIXEL
/*
* Images saved when entering the alternate screen stay linked in the
* global list; move them back so they are freed and unlinked here, or
* a later eviction would write through this freed screen.
*/
TAILQ_CONCAT(&s->images, &s->saved_images, entry);
TAILQ_FOREACH(im, &s->images, entry)
im->list = &s->images;
image_free_all(s);
#endif
} }
/* Reset tabs to default, eight spaces apart. */ /* Reset tabs to default, eight spaces apart. */
@@ -352,6 +377,10 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow,
if (sy != screen_size_y(s)) if (sy != screen_size_y(s))
screen_resize_y(s, sy, eat_empty, &cy); screen_resize_y(s, sy, eat_empty, &cy);
#ifdef ENABLE_SIXEL
image_free_all(s);
#endif
if (reflow) if (reflow)
screen_reflow(s, sx, &cx, &cy, cursor); screen_reflow(s, sx, &cx, &cy, cursor);
@@ -663,6 +692,10 @@ int
screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor) screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
{ {
u_int sx, sy; u_int sx, sy;
#ifdef ENABLE_SIXEL
struct image *im;
#endif
if (SCREEN_IS_ALTERNATE(s)) if (SCREEN_IS_ALTERNATE(s))
return 0; return 0;
sx = screen_size_x(s); sx = screen_size_x(s);
@@ -676,6 +709,12 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
} }
memcpy(&s->saved_cell, gc, sizeof s->saved_cell); memcpy(&s->saved_cell, gc, sizeof s->saved_cell);
#ifdef ENABLE_SIXEL
TAILQ_CONCAT(&s->saved_images, &s->images, entry);
TAILQ_FOREACH(im, &s->saved_images, entry)
im->list = &s->saved_images;
#endif
grid_view_clear(s->grid, 0, 0, sx, sy, 8); grid_view_clear(s->grid, 0, 0, sx, sy, 8);
s->saved_flags = s->grid->flags; s->saved_flags = s->grid->flags;
@@ -689,6 +728,10 @@ int
screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor) screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
{ {
u_int sx = screen_size_x(s), sy = screen_size_y(s); u_int sx = screen_size_x(s), sy = screen_size_y(s);
#ifdef ENABLE_SIXEL
struct image *im;
#endif
/* /*
* If the current size is different, temporarily resize to the old size * If the current size is different, temporarily resize to the old size
* before copying back. * before copying back.
@@ -731,6 +774,13 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
grid_destroy(s->saved_grid); grid_destroy(s->saved_grid);
s->saved_grid = NULL; s->saved_grid = NULL;
#ifdef ENABLE_SIXEL
image_free_all(s);
TAILQ_CONCAT(&s->images, &s->saved_images, entry);
TAILQ_FOREACH(im, &s->images, entry)
im->list = &s->images;
#endif
if (s->cx > screen_size_x(s) - 1) if (s->cx > screen_size_x(s) - 1)
s->cx = screen_size_x(s) - 1; s->cx = screen_size_x(s) - 1;
if (s->cy > screen_size_y(s) - 1) if (s->cy > screen_size_y(s) - 1)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.507 2026/08/24 21:17:19 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> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -473,33 +473,7 @@ server_client_set_session(struct client *c, struct session *s)
tty_update_client_offset(c); tty_update_client_offset(c);
status_timer_start(c); status_timer_start(c);
server_client_fire_session_changed(c, old); server_client_fire_session_changed(c, old);
server_redraw_client(c);
/*
* A full redraw is only needed if the client's session or
* current window actually changed - not if this merely
* confirmed the client is still looking at the same window
* (as happens when switch-client -t targets a pane in the
* already-current window, e.g. clicking a pane name in a
* second #{P:} status line: the default MouseDown1Status
* binding resolves that click to switch-client -t=, which
* reaches here regardless of whether anything besides the
* active pane changed). Redrawing unconditionally here
* forced a full window redraw for what should have been
* just an active-pane change, already handled narrowly by
* window_set_active_pane() and window_redraw_active_switch()
* before this is reached.
*
* old and s may be the same session object, whose curw was
* already updated to the new window before this function was
* called - old->curw and s->curw would then read the same,
* already-current value, so comparing them can never detect
* a same-session window change. Compare against the client's
* own cached scene instead, which only reflects what it has
* actually drawn.
*/
if (old == NULL || old != s ||
!redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
} }
server_check_unattached(); server_check_unattached();
@@ -552,6 +526,8 @@ server_client_lost(struct client *c)
free(c->title); free(c->title);
free(c->path); free(c->path);
free((void *)c->cwd); free((void *)c->cwd);
free(c->exit_session);
free(c->exit_message);
evtimer_del(&c->repeat_timer); evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer); evtimer_del(&c->click_timer);
@@ -1486,19 +1462,8 @@ server_client_key_callback(struct cmdq_item *item, void *data)
/* /*
* Mouse drag is in progress, so fire the callback (now that * Mouse drag is in progress, so fire the callback (now that
* the mouse event is valid). * the mouse event is valid).
*
* Start a synchronized-output region here rather than
* leaving it to whatever redraw eventually follows: a drag
* callback may write directly via the pane's fast path
* immediately, with any correction only arriving later via
* redraw_client_damage(), which opens its own sync region.
* Since tty_sync_end() is only called once, at the very end
* of this client's pass in server_client_reset_state(),
* starting it here merges both into one atomic terminal
* update instead of two visible frames.
*/ */
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) { if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
tty_sync_start(&c->tty);
c->tty.mouse_drag_update(c, m); c->tty.mouse_drag_update(c, m);
goto out; goto out;
} }
@@ -1534,6 +1499,10 @@ server_client_key_callback(struct cmdq_item *item, void *data)
TAILQ_EMPTY(&wp->modes)) TAILQ_EMPTY(&wp->modes))
goto forward_key; goto forward_key;
/* Focus events are not keys and cannot be bound. */
if (key == KEYC_FOCUS_IN || key == KEYC_FOCUS_OUT)
goto forward_key;
/* /*
* Work out the current key table. If the pane is in a mode, use * Work out the current key table. If the pane is in a mode, use
* the mode table instead of the default key table. * the mode table instead of the default key table.
@@ -1931,12 +1900,7 @@ server_client_loop(void)
/* /*
* Any windows will have been redrawn as part of clients, so clear * Any windows will have been redrawn as part of clients, so clear
* their flags now. A client whose redraw was deferred this pass * their flags now.
* (waiting for outstanding tty output to drain) has already
* escalated to CLIENT_REDRAWWINDOW or CLIENT_REDRAWSCROLLBARS in
* server_client_check_redraw() to cover whatever it is about to
* lose here, so PANE_REDRAW/PANE_REDRAWSCROLLBAR and window damage
* can simply be cleared unconditionally.
*/ */
RB_FOREACH(w, windows, &windows) { RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
@@ -1947,8 +1911,6 @@ server_client_loop(void)
wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR| wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR|
PANE_ACTIVITY); PANE_ACTIVITY);
} }
redraw_free_damage(w);
check_window_name(w); check_window_name(w);
} }
@@ -2282,7 +2244,7 @@ server_client_reset_state(struct client *c)
cy += status_line_size(c); cy += status_line_size(c);
} }
if ((pane_mode & MODE_SYNC) || !cursor) if (!cursor)
mode &= ~MODE_CURSOR; mode &= ~MODE_CURSOR;
} }
} else if (c->overlay_mode == NULL || s == NULL) } else if (c->overlay_mode == NULL || s == NULL)
@@ -2290,6 +2252,10 @@ server_client_reset_state(struct client *c)
if (~pane_mode & MODE_SYNC) { if (~pane_mode & MODE_SYNC) {
log_debug("%s: cursor to %u,%u", __func__, cx, cy); log_debug("%s: cursor to %u,%u", __func__, cx, cy);
tty_cursor(tty, cx, cy); tty_cursor(tty, cx, cy);
} else {
mode &= ~CURSOR_MODES;
mode |= tty->mode & CURSOR_MODES;
s = NULL;
} }
/* /*
@@ -2451,8 +2417,6 @@ server_client_check_exit(struct client *c, int force)
proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1); proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1);
break; break;
} }
free(c->exit_session);
free(c->exit_message);
} }
/* Redraw timer callback. */ /* Redraw timer callback. */
@@ -2495,8 +2459,6 @@ server_client_any_pane_redraw(struct client *c)
if (c->flags & CLIENT_REDRAWWINDOW) if (c->flags & CLIENT_REDRAWWINDOW)
return (1); return (1);
if (!TAILQ_EMPTY(&w->damage))
return (1);
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR)) if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR))
return (1); return (1);
@@ -2542,12 +2504,7 @@ server_client_check_redraw(struct client *c)
/* /*
* If there is outstanding data, defer the redraw until it has been * If there is outstanding data, defer the redraw until it has been
* consumed. We can just add a timer to get out of the event loop and * consumed. We can just add a timer to get out of the event loop and
* end up back here. server_client_loop() clears PANE_REDRAW, * end up back here.
* PANE_REDRAWSCROLLBAR and window damage unconditionally every pass,
* so escalate to a coarser, persistent client flag that survives
* that clear and forces a full catch-up redraw once this client is
* unblocked, rather than trying to keep the fine-grained state
* around for a retry.
*/ */
n = EVBUFFER_LENGTH(tty->out); n = EVBUFFER_LENGTH(tty->out);
if (n != 0 || (tty->flags & TTY_BLOCK)) { if (n != 0 || (tty->flags & TTY_BLOCK)) {
@@ -2561,8 +2518,6 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started"); log_debug("redraw timer started");
evtimer_add(&ev, &tv); evtimer_add(&ev, &tv);
} }
if (!TAILQ_EMPTY(&w->damage))
c->flags |= CLIENT_REDRAWWINDOW;
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) { if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW; c->flags |= CLIENT_REDRAWWINDOW;
@@ -2596,19 +2551,6 @@ server_client_check_redraw(struct client *c)
redraw_pane_scrollbar(c, wp); redraw_pane_scrollbar(c, wp);
} }
} }
/*
* Window damage is also what makes server_client_any_pane_
* redraw() decide a redraw is needed at all, independently of
* any CLIENT_ALLREDRAWFLAGS bit. Every current damage source
* happens to set one of those flags too, so the block below
* always consumes it - but consume it here too in case that
* ever stops holding, since server_client_loop() clears
* window damage unconditionally every pass regardless of
* whether it was actually drawn.
*/
if (!TAILQ_EMPTY(&w->damage) && (~c->flags & CLIENT_ALLREDRAWFLAGS))
redraw_client_damage(c);
} }
/* /*
@@ -2622,7 +2564,6 @@ server_client_check_redraw(struct client *c)
} }
server_client_set_progress_bar(c); server_client_set_progress_bar(c);
redraw_screen(c); redraw_screen(c);
redraw_client_damage(c);
} }
/* Put the tty back how it was. */ /* Put the tty back how it was. */

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> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -379,9 +379,9 @@ server_destroy_pane(struct window_pane *wp, int notify)
wp->pipe_fd = -1; wp->pipe_fd = -1;
} }
remain_on_exit = options_get_number(wp->options, "remain-on-exit"); if (~wp->flags & PANE_STATUSREADY)
if (remain_on_exit != 0 && (~wp->flags & PANE_STATUSREADY))
return; return;
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
switch (remain_on_exit) { switch (remain_on_exit) {
case 0: case 0:
break; break;

4
sort.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ /* $OpenBSD: sort.c,v 1.10 2026/08/25 07:23:30 nicm Exp $ */
/* /*
* Copyright (c) 2026 Dane Jensen <dhcjensen@gmail.com> * Copyright (c) 2026 Dane Jensen <dhcjensen@gmail.com>
@@ -32,7 +32,7 @@ sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *,
u_int i; u_int i;
void *tmp, **ll; void *tmp, **ll;
if (sort_crit->order == SORT_END) if (len < 2 || sort_crit->order == SORT_END)
return; return;
if (sort_crit->order == SORT_ORDER) { if (sort_crit->order == SORT_ORDER) {

View File

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

View File

@@ -30,8 +30,7 @@
/* Default style. */ /* Default style. */
static struct style style_default = { static struct style style_default = {
{ { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 },
},
0, 0,
0, 0,

233
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1158 2026/08/20 09:19:24 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> .\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\" .\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\" .\"
.Dd $Mdocdate: August 20 2026 $ .Dd $Mdocdate: September 3 2026 $
.Dt TMUX 1 .Dt TMUX 1
.Os .Os
.Sh NAME .Sh NAME
@@ -2004,6 +2004,25 @@ Same as
.Ic copy\-pipe .Ic copy\-pipe
but also exit copy mode. but also exit copy mode.
.It Xo .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 .Ic copy\-selection
.Op Fl CP .Op Fl CP
.Op Ar prefix .Op Ar prefix
@@ -2169,6 +2188,27 @@ Hide line numbers in copy mode.
.Xc .Xc
Toggle line numbers in copy mode. Toggle line numbers in copy mode.
.It Xo .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 .Ic middle\-line
(vi: M) (vi: M)
(emacs: M\-r) (emacs: M\-r)
@@ -2208,20 +2248,46 @@ Move to the end of the next word.
.Xc .Xc
Same as Same as
.Ic next\-word .Ic next\-word
but use a space alone as the word separator. but treat all non-whitespace characters as part of a word.
.It Xo .It Xo
.Ic next\-space\-end .Ic next\-space\-end
(vi: E) (vi: E)
.Xc .Xc
Same as Same as
.Ic next\-word\-end .Ic next\-word\-end
but use a space alone as the word separator. but treat all non-whitespace characters as part of a word.
.It Xo .It Xo
.Ic other\-end .Ic other\-end
(vi: o) (vi: o)
.Xc .Xc
Switch at which end of the selection the cursor sits. Switch at which end of the selection the cursor sits.
.It Xo .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 .Ic page\-down
(vi: C\-f) (vi: C\-f)
(emacs: PageDown) (emacs: PageDown)
@@ -2262,6 +2328,14 @@ Same as
.Ic pipe .Ic pipe
but also exit copy mode. but also exit copy mode.
.It Xo .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 .Ic previous\-matching\-bracket
(emacs: M\-C\-b) (emacs: M\-C\-b)
.Xc .Xc
@@ -2289,7 +2363,7 @@ Move to the previous word.
.Xc .Xc
Same as Same as
.Ic previous\-word .Ic previous\-word
but use a space alone as the word separator. but treat all non-whitespace characters as part of a word.
.It Xo .It Xo
.Ic rectangle\-on .Ic rectangle\-on
.Xc .Xc
@@ -2456,6 +2530,16 @@ backward becomes forward).
.Xc .Xc
Select the current line. Select the current line.
.It Xo .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 .Ic select\-word
.Xc .Xc
Select the current word. Select the current word.
@@ -2494,6 +2578,12 @@ Toggle the visibility of the position indicator in the top right.
Move to the top line. Move to the top line.
.El .El
.Pp .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: The search commands come in several varieties:
.Ql search\-forward .Ql search\-forward
and 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. 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. If the shell does not send these escape sequences, these commands do nothing.
.Pp .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 Copy commands may take an optional buffer prefix argument which is used
to generate the buffer name (the default is to generate the buffer name (the default is
.Ql buffer .Ql buffer
@@ -2574,8 +2673,8 @@ Word separators can be customized with the
session option. session option.
Next word moves to the start of the next word, next word end to the end of the Next word moves to the start of the next word, next word end to the end of the
next word and previous word to the start of the previous word. next word and previous word to the start of the previous word.
The three next and previous space keys work similarly but use a space alone as The three next and previous space keys work similarly but treat all
the word separator. non-whitespace characters as part of a word.
Setting Setting
.Em word\-separators .Em word\-separators
to the empty string makes next/previous word equivalent to next/previous space. to the empty string makes next/previous word equivalent to next/previous space.
@@ -2601,12 +2700,19 @@ The synopsis for the
command is: command is:
.Bl -tag -width Ds .Bl -tag -width Ds
.It Xo Ic copy\-mode .It Xo Ic copy\-mode
.Op Fl dekHMqSu .Op Fl UcdekHMqSu
.Op Fl s Ar src\-pane .Op Fl s Ar src\-pane
.Op Fl t Ar target\-pane .Op Fl t Ar target\-pane
.Xc .Xc
Enter copy mode. Enter copy mode.
.Pp .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 .Fl u
enters copy mode and scrolls one page up and enters copy mode and scrolls one page up and
.Fl d .Fl d
@@ -2647,6 +2753,60 @@ bind PageDown copy\-mode \-ed
kills the pane when the mode is exited. kills the pane when the mode is exited.
.El .El
.Pp .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 A number of preset arrangements of panes are available, these are called
layouts. layouts.
These may be selected with the 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 shown, unless the
.Fl k .Fl k
flag is used, in which case it is destroyed. flag is used, in which case it is destroyed.
.Ar window\-name
is the name given to the new window, if any.
.Pp
If If
.Fl S .Fl S
is given and a window named is given, an existing window is selected instead of creating a new one
.Ar window\-name (unless
already exists, it is selected (unless
.Fl d .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 .Pp
.Ar shell\-command .Ar shell\-command
is the command to execute. is the command to execute.
@@ -4754,6 +4921,15 @@ for backspace.
Set the number of buffers; as new buffers are added to the top of the stack, Set the number of buffers; as new buffers are added to the top of the stack,
old ones are removed from the bottom if necessary to maintain this maximum old ones are removed from the bottom if necessary to maintain this maximum
length. length.
.It Xo Ic clear\-on\-attach
.Op Ic on | off
.Xc
When enabled (the default), the client terminal switches to the alternate
screen and clears it when a client is attached.
When disabled,
.Nm
does not enter the alternate screen; instead the existing terminal content is
scrolled into the scrollback buffer so it remains accessible.
.It Xo Ic command\-alias[] .It Xo Ic command\-alias[]
.Ar name=value .Ar name=value
.Xc .Xc
@@ -5070,20 +5246,12 @@ Supports extended keys.
Supports focus reporting. Supports focus reporting.
.It hyperlinks .It hyperlinks
Supports OSC 8 hyperlinks. Supports OSC 8 hyperlinks.
.It image-quadrants
Allows image fallbacks to use Unicode quadrant block characters.
.It image-sextants
Allows image fallbacks to use Unicode sextant block characters.
.It ignorefkeys .It ignorefkeys
Ignore function keys from Ignore function keys from
.Xr terminfo 5 .Xr terminfo 5
and use the and use the
.Nm .Nm
internal set only. internal set only.
.It kitty
Supports Kitty graphics using Unicode placeholders.
The terminal must support the Kitty Unicode placeholder extension, not only
the basic Kitty graphics protocol.
.It margins .It margins
Supports DECSLRM margins. Supports DECSLRM margins.
.It mouse .It mouse
@@ -5112,6 +5280,8 @@ Supports
title setting. title setting.
.It usstyle .It usstyle
Allows underscore style and colour to be set. Allows underscore style and colour to be set.
.It utf8
Supports UTF\-8 output.
.El .El
.It Ic terminal\-overrides[] Ar string .It Ic terminal\-overrides[] Ar string
Allow terminal descriptions read using Allow terminal descriptions read using
@@ -5646,6 +5816,11 @@ If set to both, a bell and a message are produced.
Sets the session's conception of what characters are considered word Sets the session's conception of what characters are considered word
separators, for the purposes of the next and previous word commands in separators, for the purposes of the next and previous word commands in
copy mode. copy mode.
A space in
.Ar string
matches any character with the Unicode
.Em White_Space
property.
.El .El
.Pp .Pp
Available window options are: Available window options are:
@@ -5783,6 +5958,20 @@ section.
.It Ic copy\-mode\-position\-format Ar format .It Ic copy\-mode\-position\-format Ar format
Format of the position indicator in copy mode. Format of the position indicator in copy mode.
.Pp .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 .It Ic copy\-mode\-position\-style Ar style
Set the style of the position indicator in copy mode. Set the style of the position indicator in copy mode.
For how to specify For how to specify
@@ -7315,7 +7504,10 @@ The following variables are available, where appropriate:
.It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode" .It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode"
.It Li "cursor_x" Ta "" Ta "Cursor X position in pane" .It Li "cursor_x" Ta "" Ta "Cursor X position in pane"
.It Li "cursor_y" Ta "" Ta "Cursor Y position in pane" .It Li "cursor_y" Ta "" Ta "Cursor Y position in pane"
.It Li "history_added" Ta "" Ta "Number of lines scrolled into pane history"
.It Li "history_bytes" Ta "" Ta "Number of bytes in window history" .It Li "history_bytes" Ta "" Ta "Number of bytes in window history"
.It Li "history_collected" Ta "" Ta "Number of oldest pane history lines discarded at the history limit"
.It Li "history_generation" Ta "" Ta "Generation incremented when pane history is cleared or reflowed"
.It Li "history_limit" Ta "" Ta "Maximum window history lines" .It Li "history_limit" Ta "" Ta "Maximum window history lines"
.It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "history_size" Ta "" Ta "Size of history in lines"
.It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook" Ta "" Ta "Name of running hook, if any"
@@ -7414,6 +7606,7 @@ The following variables are available, where appropriate:
.It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set"
.It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal" .It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal"
.It Li "pane_mode" Ta "" Ta "Name of pane mode, if any" .It Li "pane_mode" Ta "" Ta "Name of pane mode, if any"
.It Li "pane_output_generation" Ta "" Ta "Generation incremented when pane output is processed"
.It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)" .It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)"
.It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pid" Ta "" Ta "PID of first process in pane"
.It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped"

254
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1430 2026/08/24 21:17:19 nicm Exp $ */ /* $OpenBSD: tmux.h,v 1.1433 2026/09/01 12:49:49 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -68,7 +68,6 @@ struct options_array_item;
struct options_entry; struct options_entry;
struct prompt; struct prompt;
struct window_pane_prompt; struct window_pane_prompt;
struct redraw_damage;
struct redraw_scene; struct redraw_scene;
struct redraw_span; struct redraw_span;
struct screen_write_citem; struct screen_write_citem;
@@ -76,12 +75,7 @@ struct screen_write_cline;
struct screen_write_ctx; struct screen_write_ctx;
struct session; struct session;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
struct image;
struct image_backend;
struct image_rect;
struct image_line;
struct image_store;
struct sixel_image; struct sixel_image;
#endif #endif
@@ -489,6 +483,7 @@ enum tty_code_code {
TTYC_ICH1, TTYC_ICH1,
TTYC_IL, TTYC_IL,
TTYC_IL1, TTYC_IL1,
TTYC_IND,
TTYC_INDN, TTYC_INDN,
TTYC_INVIS, TTYC_INVIS,
TTYC_KCBT, TTYC_KCBT,
@@ -820,6 +815,7 @@ struct colour_palette {
#define GRID_LINE_START_OUTPUT 0x40 #define GRID_LINE_START_OUTPUT 0x40
#define GRID_LINE_END_OUTPUT 0x80 #define GRID_LINE_END_OUTPUT 0x80
#define GRID_LINE_HYPERLINK 0x100 #define GRID_LINE_HYPERLINK 0x100
#define GRID_LINE_END_OUTPUT_STATUS 0x200
/* All OSC 133 flags. */ /* All OSC 133 flags. */
#define GRID_LINE_OSC133_FLAGS \ #define GRID_LINE_OSC133_FLAGS \
@@ -827,7 +823,8 @@ struct colour_palette {
GRID_LINE_SECOND_PROMPT| \ GRID_LINE_SECOND_PROMPT| \
GRID_LINE_START_COMMAND| \ GRID_LINE_START_COMMAND| \
GRID_LINE_START_OUTPUT| \ GRID_LINE_START_OUTPUT| \
GRID_LINE_END_OUTPUT) GRID_LINE_END_OUTPUT| \
GRID_LINE_END_OUTPUT_STATUS)
/* Grid string flags. */ /* Grid string flags. */
#define GRID_STRING_WITH_SEQUENCES 0x1 #define GRID_STRING_WITH_SEQUENCES 0x1
@@ -864,7 +861,7 @@ struct colour_palette {
struct grid_cell { struct grid_cell {
struct utf8_data data; struct utf8_data data;
u_short attr; u_short attr;
u_short flags; u_char flags;
int fg; int fg;
int bg; int bg;
int us; int us;
@@ -875,7 +872,7 @@ struct grid_cell {
struct grid_extd_entry { struct grid_extd_entry {
utf8_char data; utf8_char data;
u_short attr; u_short attr;
u_short flags; u_char flags;
int fg; int fg;
int bg; int bg;
int us; int us;
@@ -893,7 +890,7 @@ struct grid_cell_entry {
u_char data; u_char data;
} data; } data;
}; };
u_short flags; u_char flags;
} __packed; } __packed;
/* OSC 133 data for a grid line. */ /* OSC 133 data for a grid line. */
@@ -917,9 +914,6 @@ struct grid_line {
u_int time; u_int time;
struct osc133_data osc133_data; struct osc133_data osc133_data;
u_short flags; u_short flags;
#ifdef ENABLE_IMAGES
struct image_line *images;
#endif
}; };
/* Entire grid of cells. */ /* Entire grid of cells. */
@@ -939,9 +933,6 @@ struct grid {
u_int scroll_generation; u_int scroll_generation;
struct grid_line *linedata; struct grid_line *linedata;
#ifdef ENABLE_IMAGES
struct image_store *images;
#endif
}; };
/* Virtual cursor in a grid. */ /* Virtual cursor in a grid. */
@@ -1033,65 +1024,24 @@ struct style {
u_int link; u_int link;
}; };
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
/* Private ACS keys used by the text image backend. */ /* Image. */
#define TTY_ACS_IMAGE_SHADE_LIGHT 0x80
#define TTY_ACS_IMAGE_SHADE_MEDIUM 0x81
#define TTY_ACS_IMAGE_SHADE_DARK 0x82
#define TTY_ACS_IMAGE_BLOCK 0x83
#define TTY_ACS_IMAGE_HALF_UPPER 0x84
#define TTY_ACS_IMAGE_HALF_LOWER 0x85
#define TTY_ACS_IMAGE_HALF_LEFT 0x86
#define TTY_ACS_IMAGE_HALF_RIGHT 0x87
#define TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT 0x88
#define TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT 0x89
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT 0x8a
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT 0x8b
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT 0x8c
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT 0x8d
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT 0x8e
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT 0x8f
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT 0x90
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT 0x91
#define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92
#define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd
struct image_sample {
u_char red;
u_char green;
u_char blue;
u_char alpha;
u_char brightness;
};
#define IMAGE_SAMPLE_COLUMNS 2
#define IMAGE_SAMPLE_ROWS 6
struct image_cell {
struct image_sample whole;
struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS];
};
struct image { struct image {
u_int id; struct screen *s;
u_int references; struct sixel_image *data;
u_int flags; char *fallback;
u_int parent_id;
u_int source_id; u_int px;
u_int width; u_int py;
u_int height;
u_int canvas_width;
u_int canvas_height;
u_int sx; u_int sx;
u_int sy; u_int sy;
size_t stride;
size_t size;
u_char *pixels;
struct sixel_image *sixel;
struct image_cell *cells;
void *fallback_data;
RB_ENTRY(image) entry; struct images *list;
TAILQ_ENTRY (image) entry;
TAILQ_ENTRY (image) all_entry;
}; };
RB_HEAD(images, image); TAILQ_HEAD(images, image);
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
#endif #endif
/* Cursor style. */ /* Cursor style. */
@@ -1150,6 +1100,11 @@ struct screen {
bitstr_t *tabs; bitstr_t *tabs;
struct screen_sel *sel; struct screen_sel *sel;
#ifdef ENABLE_SIXEL
struct images images;
struct images saved_images;
#endif
struct screen_write_cline *write_list; struct screen_write_cline *write_list;
struct hyperlinks *hyperlinks; struct hyperlinks *hyperlinks;
@@ -1167,7 +1122,6 @@ struct screen_write_ctx {
#define SCREEN_WRITE_SYNC 0x1 #define SCREEN_WRITE_SYNC 0x1
#define SCREEN_WRITE_OBSCURED 0x2 #define SCREEN_WRITE_OBSCURED 0x2
#define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4 #define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4
#define SCREEN_WRITE_INPUT 0x8
screen_write_init_ctx_cb init_ctx_cb; screen_write_init_ctx_cb init_ctx_cb;
void *arg; void *arg;
@@ -1405,9 +1359,10 @@ struct window_pane {
char tty[TTY_NAME_MAX]; char tty[TTY_NAME_MAX];
int status; int status;
struct timeval dead_time; struct timeval dead_time;
struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct cmdq_item *wait_item;
struct spawn_editor_state *editor; struct spawn_editor_state *editor;
uint64_t output_generation;
time_t last_output_time; time_t last_output_time;
time_t last_prompt_time; time_t last_prompt_time;
time_t cmd_start_time; time_t cmd_start_time;
@@ -1473,7 +1428,6 @@ struct window_pane {
TAILQ_HEAD(window_panes, window_pane); TAILQ_HEAD(window_panes, window_pane);
TAILQ_HEAD(window_panes_zindex, window_pane); TAILQ_HEAD(window_panes_zindex, window_pane);
RB_HEAD(window_pane_tree, window_pane); RB_HEAD(window_pane_tree, window_pane);
TAILQ_HEAD(redraw_damages, redraw_damage);
/* Window structure. */ /* Window structure. */
struct window { struct window {
@@ -1517,13 +1471,6 @@ struct window {
uint64_t redraw_scene_generation; uint64_t redraw_scene_generation;
struct redraw_damages damage;
u_int damage_count;
#ifdef ENABLE_IMAGES
uint64_t image_scroll_epoch;
int image_scroll_pending;
#endif
struct menu_data *menu; struct menu_data *menu;
u_int menu_last_px; u_int menu_last_px;
u_int menu_last_py; u_int menu_last_py;
@@ -1791,11 +1738,6 @@ struct tty_term {
#define TERM_VT100LIKE 0x20 #define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40 #define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80 #define TERM_INVALIDMS 0x80
#define TERM_KITTY 0x100
#ifdef ENABLE_IMAGES
#define TERM_IMAGE_QUADRANTS 0x200
#define TERM_IMAGE_SEXTANTS 0x400
#endif
int flags; int flags;
LIST_ENTRY(tty_term) entry; LIST_ENTRY(tty_term) entry;
@@ -1898,17 +1840,10 @@ struct tty {
struct event key_timer; struct event key_timer;
struct tty_key *key_tree; struct tty_key *key_tree;
#ifdef ENABLE_IMAGES
const struct image_backend *image_backend;
void *image_data;
struct window *image_scroll_window;
uint64_t image_scroll_epoch;
int image_scroll_failed;
#endif
}; };
/* Terminal command context. */ /* Terminal command context. */
typedef void (*tty_ctx_redraw_cb)(const struct tty_ctx *, u_int, u_int); typedef void (*tty_ctx_redraw_cb)(const struct tty_ctx *);
typedef int (*tty_ctx_set_client_cb)(struct tty_ctx *, struct client *); typedef int (*tty_ctx_set_client_cb)(struct tty_ctx *, struct client *);
struct tty_ctx { struct tty_ctx {
struct screen *s; struct screen *s;
@@ -1941,7 +1876,7 @@ struct tty_ctx {
size_t size; size_t size;
} sel; } sel;
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
struct image *image; struct image *image;
#endif #endif
}; };
@@ -3061,6 +2996,10 @@ void tty_cmd_scrolldown(struct tty *, const struct tty_ctx *);
void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *); void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *);
void tty_cmd_setselection(struct tty *, const struct tty_ctx *); void tty_cmd_setselection(struct tty *, const struct tty_ctx *);
void tty_cmd_rawstring(struct tty *, const struct tty_ctx *); void tty_cmd_rawstring(struct tty *, const struct tty_ctx *);
#ifdef ENABLE_SIXEL
void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *);
void tty_draw_images(struct client *, struct window_pane *);
#endif
void tty_cmd_syncstart(struct tty *, const struct tty_ctx *); void tty_cmd_syncstart(struct tty *, const struct tty_ctx *);
void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *); void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *);
@@ -3102,7 +3041,6 @@ void tty_default_features(struct client *, const char *, u_int);
/* tty-acs.c */ /* tty-acs.c */
int tty_acs_needed(struct tty *); int tty_acs_needed(struct tty *);
const char *tty_acs_get(struct tty *, u_char); const char *tty_acs_get(struct tty *, u_char);
u_char tty_acs_image_sextant(u_int);
int tty_acs_reverse_get(struct tty *, const char *, size_t); int tty_acs_reverse_get(struct tty *, const char *, size_t);
const struct utf8_data *tty_acs_double_borders(int); const struct utf8_data *tty_acs_double_borders(int);
const struct utf8_data *tty_acs_heavy_borders(int); const struct utf8_data *tty_acs_heavy_borders(int);
@@ -3705,7 +3643,7 @@ void screen_write_setselection(struct screen_write_ctx *, const char *,
u_char *, u_int); u_char *, u_int);
void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int, void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int,
int); int);
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
void screen_write_sixelimage(struct screen_write_ctx *, void screen_write_sixelimage(struct screen_write_ctx *,
struct sixel_image *, u_int); struct sixel_image *, u_int);
#endif #endif
@@ -3719,17 +3657,8 @@ void redraw_screen(struct client *);
void redraw_pane(struct client *, struct window_pane *); void redraw_pane(struct client *, struct window_pane *);
void redraw_pane_scrollbar(struct client *, struct window_pane *); void redraw_pane_scrollbar(struct client *, struct window_pane *);
void redraw_free_scene(struct redraw_scene *); void redraw_free_scene(struct redraw_scene *);
int redraw_client_has_window(struct client *, struct window *);
void redraw_invalidate_scene(struct window *); void redraw_invalidate_scene(struct window *);
void redraw_invalidate_all_scenes(void); void redraw_invalidate_all_scenes(void);
void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int);
#ifdef ENABLE_IMAGES
void redraw_damage_window_scroll(struct window *, u_int, u_int, u_int,
u_int);
void redraw_image_scroll_result(struct tty *, const struct tty_ctx *, int);
#endif
void redraw_free_damage(struct window *);
void redraw_client_damage(struct client *);
int redraw_get_status_border_cell_type(struct redraw_span **, u_int); int redraw_get_status_border_cell_type(struct redraw_span **, u_int);
/* screen.c */ /* screen.c */
@@ -3902,8 +3831,6 @@ int window_pane_get_pane_status(struct window_pane *);
struct style_range *window_pane_status_get_range(struct window_pane *, u_int, struct style_range *window_pane_status_get_range(struct window_pane *, u_int,
u_int); u_int);
int window_pane_is_floating(struct window_pane *); int window_pane_is_floating(struct window_pane *);
void window_pane_redraw_floating(struct window *,
struct window_pane *, int, int, int, int);
/* window-border.c */ /* window-border.c */
void window_set_fill_cells(struct window *); void window_set_fill_cells(struct window *);
@@ -4182,6 +4109,7 @@ void session_update_history(struct session *);
/* utf8.c */ /* utf8.c */
enum utf8_state utf8_towc (const struct utf8_data *, wchar_t *); enum utf8_state utf8_towc (const struct utf8_data *, wchar_t *);
enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *); enum utf8_state utf8_fromwc(wchar_t wc, struct utf8_data *);
int utf8_has_whitespace(const struct utf8_data *);
void utf8_update_width_cache(void); void utf8_update_width_cache(void);
utf8_char utf8_build_one(u_char); utf8_char utf8_build_one(u_char);
enum utf8_state utf8_from_data(const struct utf8_data *, utf8_char *); enum utf8_state utf8_from_data(const struct utf8_data *, utf8_char *);
@@ -4295,7 +4223,7 @@ struct winlink *spawn_window(struct spawn_context *, char **);
struct window_pane *spawn_pane(struct spawn_context *, char **); struct window_pane *spawn_pane(struct spawn_context *, char **);
typedef void (*spawn_finish_edit_cb)(char *, size_t, void *); typedef void (*spawn_finish_edit_cb)(char *, size_t, void *);
struct spawn_editor_state *spawn_editor(struct client *, const char *, size_t, 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 *); void spawn_cancel_editor(struct spawn_editor_state *);
pid_t spawn_get_editor_pid(struct spawn_editor_state *); pid_t spawn_get_editor_pid(struct spawn_editor_state *);
void spawn_editor_finish(struct window_pane *); void spawn_editor_finish(struct window_pane *);
@@ -4303,108 +4231,17 @@ void spawn_editor_finish(struct window_pane *);
/* regsub.c */ /* regsub.c */
char *regsub(const char *, const char *, const char *, int); char *regsub(const char *, const char *, const char *, int);
#ifdef ENABLE_IMAGES #ifdef ENABLE_SIXEL
/* image.c */ /* image.c */
#define IMAGE_BACKEND_GRAPHICAL 0x1 int image_free_all(struct screen *);
#define IMAGE_BACKEND_SCROLLS 0x2 struct image *image_store(struct screen *, struct sixel_image *);
int image_check_line(struct screen *, u_int, u_int);
int image_check_area(struct screen *, u_int, u_int, u_int, u_int);
int image_scroll_up(struct screen *, u_int);
struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int,
u_char *);
struct image *image_create_view(struct image *, u_int, u_int, u_int,
u_int, u_int, u_int, u_int, u_int, u_int, u_int);
struct image *image_find(u_int);
u_int image_get_id(const struct image *);
void image_get_size(const struct image *, u_int *, u_int *);
void image_get_canvas_size(const struct image *, u_int *, u_int *);
void image_get_size_in_cells(const struct image *, u_int *, u_int *);
const u_char *image_get_pixels(const struct image *, size_t *, size_t *);
void image_set_no_cursor(struct image *);
struct sixel_image *image_get_sixel(const struct image *);
void image_set_sixel(struct image *, struct sixel_image *);
void image_ref(u_int);
void image_free(u_int);
void image_write_sixel(struct screen_write_ctx *, struct image *,
u_int);
void image_write_kitty(struct screen_write_ctx *, struct image *,
u_int, u_int, u_int, int32_t);
void image_get_pixel_rect(const struct image *, u_int, u_int,
u_int, u_int, u_int *, u_int *, u_int *, u_int *);
void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *,
u_int *);
u_char *image_base64_decode(const char *, size_t, size_t, size_t *);
u_char *image_png_decode(const u_char *, size_t, size_t, u_int *,
u_int *);
void image_redraw_area(struct screen_write_ctx *, u_int, u_int,
u_int, u_int);
void image_redraw_all(struct screen_write_ctx *);
void image_redraw_scroll(struct screen_write_ctx *, u_int);
void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
int image_backend_flags(struct tty *);
void image_tty_update(struct tty *);
void image_tty_geometry_changed(struct tty *);
void image_tty_free(struct tty *, int);
void image_draw_line(struct tty *, struct screen *, u_int, u_int,
u_int, u_int, u_int, int, const struct tty_style_ctx *);
void image_get_fallback_cell(struct tty *, struct image *, u_int,
u_int, const struct grid_cell *, struct grid_cell *,
const struct tty_style_ctx *);
const struct image_cell *image_get_cell(struct image *, u_int, u_int);
void image_free_fallback(struct image *);
int image_get_fallback_at(struct tty *, struct screen *, u_int,
u_int, const struct grid_cell *, struct grid_cell *,
const struct tty_style_ctx *);
struct image *image_rect_get_image(const struct image_rect *);
const struct grid_cell *image_rect_get_cell(
const struct image_rect *);
void image_rect_get_coords(const struct image_rect *,
u_int *, u_int *, u_int *, u_int *, u_int *, u_int *);
int32_t image_rect_get_z(const struct image_rect *);
void image_clear(struct screen_write_ctx *, u_int);
void image_clear_kitty(struct screen_write_ctx *, char, u_int,
u_int, int32_t);
void image_grid_damage(struct grid *, u_int, u_int, u_int, u_int);
void image_grid_free_line(struct grid *, struct grid_line *);
void image_grid_free(struct grid *);
void image_grid_move_cells(struct grid *, u_int, u_int, u_int,
u_int);
void image_grid_duplicate_lines(struct grid *, u_int, struct grid *,
u_int, u_int);
void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *,
u_int, u_int, u_int, u_int);
void image_grid_resize_width(struct grid *, u_int);
int image_grid_line_has_images(const struct grid_line *);
int image_grid_check_area(struct grid *, u_int, u_int, u_int,
u_int);
int image_grid_get_source(struct grid *, u_int, u_int,
struct image *, u_int *, u_int *);
void image_place_cell_kitty(struct screen_write_ctx *, struct image *,
u_int, u_int, u_int, u_int, u_int, u_int, int32_t);
#define KITTY_PARSE_ERROR -1
#define KITTY_PARSE_OK 0
#define KITTY_PARSE_MORE 1
#define KITTY_PARSE_MISSING 2
struct image *kitty_parse_image(void **, const u_char *, size_t, u_int,
u_int, u_int *, u_int *, u_int *, char *, char *, u_int *,
int32_t *, int *);
int kitty_placeholder_to_image(void *, struct grid *,
struct grid_cell *, u_int, u_int, struct image **, u_int *,
u_int *, u_int *, u_int *, int32_t *);
void kitty_free_state(void *);
void kitty_draw_rect(struct tty *,
const struct image_rect *, const struct tty_style_ctx *);
void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void kitty_free_output_state(struct tty *, int);
#endif
#ifdef ENABLE_IMAGES
/* image-sixel.c */ /* image-sixel.c */
#define SIXEL_COLOUR_REGISTERS 1024 #define SIXEL_COLOUR_REGISTERS 1024
void sixel_draw_rect(struct tty *, struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int);
const struct image_rect *, const struct tty_style_ctx *);
void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void sixel_free_output(struct tty *, int);
struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int,
u_int);
void sixel_free(struct sixel_image *); void sixel_free(struct sixel_image *);
void sixel_log(struct sixel_image *); void sixel_log(struct sixel_image *);
void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *); void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *);
@@ -4413,7 +4250,6 @@ struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int,
char *sixel_print(struct sixel_image *, struct sixel_image *, char *sixel_print(struct sixel_image *, struct sixel_image *,
size_t *); size_t *);
struct screen *sixel_to_screen(struct sixel_image *); struct screen *sixel_to_screen(struct sixel_image *);
struct image *sixel_to_image(struct sixel_image *);
#endif #endif
/* server-acl.c */ /* server-acl.c */

View File

@@ -1,51 +0,0 @@
#!/bin/sh
# Generate the Unicode diacritic table used by Kitty image placeholders.
#
# Input must be UnicodeData.txt from Unicode 6.0.0:
# https://www.unicode.org/Public/6.0.0/ucd/UnicodeData.txt
# SHA-256: 90b45b777346bef027556f9c6cb3ea5d7d745bd60d6762855c08d8a22d34f771
#
# The selection follows the public Kitty graphics protocol: nonspacing marks
# with canonical combining class 230, NSM bidi class, no decomposition, minus
# characters which may be combined or normalized in ways unsuitable for a
# row or column index.
if [ "$#" -ne 1 ]; then
echo "usage: $0 UnicodeData.txt" >&2
exit 1
fi
awk -F ';' '
BEGIN {
omit["0300"] = omit["0301"] = omit["0302"] = 1
omit["0303"] = omit["0304"] = omit["0306"] = 1
omit["0307"] = omit["0308"] = omit["0309"] = 1
omit["030A"] = omit["030B"] = omit["030C"] = 1
omit["030F"] = omit["0311"] = omit["0313"] = 1
omit["0314"] = omit["0342"] = omit["0653"] = 1
omit["0654"] = 1
}
$3 == "Mn" && $4 == "230" && $5 == "NSM" && $6 == "" && !omit[$1] {
point[++count] = $1
}
END {
if (count != 297) {
printf "expected 297 diacritics, found %u\n", count > "/dev/stderr"
exit 1
}
print "static const uint32_t kitty_diacritics[] = {"
for (i = 1; i <= count; i++) {
if ((i - 1) % 8 == 0)
printf "\t"
printf "0x%s", point[i]
if (i != count)
printf ","
if (i == count || i % 8 == 0)
printf "\n"
else
printf " "
}
print "};"
}
' "$1"

View File

@@ -1,104 +0,0 @@
#!/bin/sh
# Compact visual check for image subcell geometry and layering. Run inside a
# tmux pane on Kitty or a SIXEL terminal; it needs at least 90 columns/24 rows.
if [ -z "$TMUX" ]; then
echo "Run this script inside tmux." >&2
exit 1
fi
columns=$(tput cols 2>/dev/null || echo 0)
lines=$(tput lines 2>/dev/null || echo 0)
if [ "$columns" -lt 90 ] || [ "$lines" -lt 24 ]; then
echo "This test needs at least 90 columns and 24 rows." >&2
exit 1
fi
at()
{
printf '\033[%s;%sH' "$1" "$2"
}
kitty()
{
printf '\033_G%s\033\\' "$1"
}
kitty_solid()
{
id=$1
rgba=$2
payload=$(i=0; while [ "$i" -lt 256 ]; do
printf "$rgba"
i=$((i + 1))
done | base64 | tr -d '\n')
kitty "a=t,q=2,f=32,s=16,v=16,i=$id;$payload"
}
sixel_folder()
{
printf '\033P0;1q"1;1;26;26#0;2;50;50;50#1;2;0;0;0#2;2;49;49;49#3;2;33;33;33#4;2;47;47;47#5;2;44;44;44#6;2;48;48;48#2?O?C!5?S!4?__?_?__$#0?_wW!5KGwo_-#0?~~!8?!10@BB}w$#2!12?a_A?A?A?A??@E-#0?~~!7?KK~~KK!7?~~$#2!9?K!6?K-#0?F^[owowowowowowowowooS^F$#2?G!10?@@!7?G??G-\033\\'
}
feature_info=$(tmux display-message -p \
'term=#{client_termname} features=#{client_termfeatures} cells=#{client_cell_width}x#{client_cell_height}')
printf '\033[2J\033[H\033[?25l'
at 1 2
printf 'tmux image edge-case check'
at 2 2
printf '%s' "$feature_info"
kitty_solid 201 '\377\000\000\377'
kitty_solid 202 '\000\377\000\377'
kitty_solid 203 '\000\000\377\377'
# The 26-pixel folder occupies whole grid cells, but its visible right and
# bottom edges must remain partial rather than stretching to those boundaries.
at 4 2
printf '1. partial SIXEL: folder should be the same size as direct output'
at 4 72
sixel_folder
# Chawan emits X/Y for its image padding. The red square must begin slightly
# below and right of the [img] anchor, never over it.
at 8 2
printf '2. Kitty X/Y: RED begins below/right of the [img] anchor'
at 8 72
printf '[img]'
at 9 72
kitty 'a=p,q=2,C=1,i=201,p=1,x=0,y=0,w=16,h=16,c=3,r=2,X=6,Y=6'
# Positive Kitty z overlays text and other lower z images; negative z is
# below text. The shared red/blue edge makes any wrong order obvious.
at 12 2
printf '3. Kitty z: BLUE covers RED; GREEN stays behind TEXT'
at 12 72
printf 'TEXT'
at 12 72
kitty 'a=p,q=2,C=1,i=202,p=2,x=0,y=0,w=16,h=16,c=4,r=2,z=-1'
at 14 72
kitty 'a=p,q=2,C=1,i=201,p=3,x=0,y=0,w=16,h=16,c=4,r=2,z=1'
at 14 74
kitty 'a=p,q=2,C=1,i=203,p=4,x=0,y=0,w=16,h=16,c=4,r=2,z=2'
# SIXEL is temporal: a later image covers text, while later text destroys the
# affected image cells. Both results should match on Kitty and SIXEL output.
at 18 2
printf '4. SIXEL after text: folder covers FOLDER'
at 18 72
printf 'FOLDER'
at 18 72
sixel_folder
at 21 2
printf '5. SIXEL before text: TEXT WINS, no folder remnant'
at 21 72
sixel_folder
at 21 72
printf 'TEXT WINS'
at 23 2
printf 'Take one screenshot, then press Enter to return to the shell.'
printf '\033[?25h'
IFS= read -r _image_edge_reply

View File

@@ -1,109 +0,0 @@
#!/bin/sh
# One-screen visual check for tmux image input and layering. Run this inside a
# tmux pane attached to Kitty, WezTerm, Microsoft Terminal, or a basic terminal.
if [ -z "$TMUX" ]; then
echo "Run this script inside tmux." >&2
exit 1
fi
columns=$(tput cols 2>/dev/null || echo 0)
lines=$(tput lines 2>/dev/null || echo 0)
if [ "$columns" -lt 72 ] || [ "$lines" -lt 23 ]; then
echo "This test needs at least 72 columns and 23 rows." >&2
exit 1
fi
at()
{
printf '\033[%s;%sH' "$1" "$2"
}
kitty()
{
printf '\033_G%s\033\\' "$1"
}
# Transmit a 16x16 solid RGBA texture. A one-pixel texture is unsuitable here
# because Kitty filters its transparent texture border into scaled edges.
kitty_solid()
{
id=$1
rgba=$2
payload=$(i=0; while [ "$i" -lt 256 ]; do
printf "$rgba"
i=$((i + 1))
done | base64 | tr -d '\n')
kitty "a=t,q=2,f=32,s=16,v=16,i=$id;$payload"
}
# A small 26x26 transparent SIXEL folder, used by the supplied bptato tests.
sixel_folder()
{
printf '\033P0;1q"1;1;26;26#0;2;50;50;50#1;2;0;0;0#2;2;49;49;49#3;2;33;33;33#4;2;47;47;47#5;2;44;44;44#6;2;48;48;48#2?O?C!5?S!4?__?_?__$#0?_wW!5KGwo_-#0?~~!8?!10@BB}w$#2!12?a_A?A?A?A??@E-#0?~~!7?KK~~KK!7?~~$#2!9?K!6?K-#0?F^[owowowowowowowowooS^F$#2?G!10?@@!7?G??G-\033\\'
}
feature_info=$(tmux display-message -p \
'term=#{client_termname} features=#{client_termfeatures} cells=#{client_cell_width}x#{client_cell_height}')
printf '\033[2J\033[H\033[?25l'
at 1 2
printf 'tmux image visual check'
at 2 2
printf '%s' "$feature_info"
# Red is placed after its text at a positive Kitty z-index, so it must cover
# the text. Green is placed below the text at a negative Kitty z-index.
kitty_solid 101 '\377\000\000\377'
kitty_solid 102 '\000\377\000\377'
kitty_solid 103 '\000\000\377\377'
kitty_solid 104 '\377\000\000\177'
at 4 2
printf '1. positive Kitty z: RED must hide TEXT UNDER at right'
at 4 62
printf 'TEXT UNDER'
at 4 60
kitty 'a=p,q=2,C=1,i=101,p=1,x=1,y=1,w=14,h=14,c=18,r=2,z=1'
at 7 2
printf '2. negative Kitty z: GREEN remains behind TEXT ON TOP'
at 7 60
kitty 'a=p,q=2,C=1,i=102,p=2,x=1,y=1,w=14,h=14,c=18,r=2,z=-1'
at 7 62
printf 'TEXT ON TOP'
at 10 2
printf '3. Kitty z overlap: RED left, BLUE on top at right'
at 11 55
kitty 'a=p,q=2,C=1,i=101,p=3,x=1,y=1,w=14,h=14,c=18,r=3,z=1'
at 11 64
kitty 'a=p,q=2,C=1,i=103,p=4,x=1,y=1,w=14,h=14,c=9,r=3,z=2'
at 15 2
printf '4. alpha: half-transparent RED, dithered on SIXEL'
at 15 60
kitty 'a=p,q=2,C=1,i=104,p=5,x=1,y=1,w=14,h=14,c=18,r=2,z=1'
# SIXEL semantics are temporal: later text destroys a SIXEL image, while a
# later SIXEL image appears over existing text.
at 18 2
printf '5. SIXEL after text: folder covers FOLDER at right'
at 18 65
printf 'FOLDER'
at 18 65
sixel_folder
at 21 2
printf '6. SIXEL before text: no folder remains behind TEXT WINS'
at 21 65
sixel_folder
at 21 65
printf 'TEXT WINS'
at 23 2
printf 'Take one screenshot, then press Enter to return to the shell.'
at 23 66
printf '\033[?25h'
IFS= read -r _image_visual_reply

View File

@@ -64,65 +64,9 @@ static const struct tty_acs_entry tty_acs_table[] = {
{ '{', "\317\200" }, /* greek pi */ { '{', "\317\200" }, /* greek pi */
{ '|', "\342\211\240" }, /* not-equal */ { '|', "\342\211\240" }, /* not-equal */
{ '}', "\302\243" }, /* UK pound sign */ { '}', "\302\243" }, /* UK pound sign */
{ '~', "\302\267" }, /* bullet */ { '~', "\302\267" } /* bullet */
#ifdef ENABLE_IMAGES
{ TTY_ACS_IMAGE_SHADE_LIGHT, "\342\226\221" },
{ TTY_ACS_IMAGE_SHADE_MEDIUM, "\342\226\222" },
{ TTY_ACS_IMAGE_SHADE_DARK, "\342\226\223" },
{ TTY_ACS_IMAGE_BLOCK, "\342\226\210" },
{ TTY_ACS_IMAGE_HALF_UPPER, "\342\226\200" },
{ TTY_ACS_IMAGE_HALF_LOWER, "\342\226\204" },
{ TTY_ACS_IMAGE_HALF_LEFT, "\342\226\214" },
{ TTY_ACS_IMAGE_HALF_RIGHT, "\342\226\220" },
{ TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, "\342\226\226" },
{ TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT, "\342\226\227" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT, "\342\226\230" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
"\342\226\231" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT, "\342\226\232" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
"\342\226\233" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
"\342\226\234" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, "\342\226\235" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT, "\342\226\236" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
"\342\226\237" }
#endif
}; };
#ifdef ENABLE_IMAGES
static char tty_acs_image_sextants[60][5];
static void
tty_acs_image_sextants_init(void)
{
u_int cp, i;
if (tty_acs_image_sextants[0][0] != '\0')
return;
for (i = 0; i < nitems(tty_acs_image_sextants); i++) {
cp = 0x1fb00 + i;
tty_acs_image_sextants[i][0] = 0xf0 | (cp >> 18);
tty_acs_image_sextants[i][1] = 0x80 | ((cp >> 12) & 0x3f);
tty_acs_image_sextants[i][2] = 0x80 | ((cp >> 6) & 0x3f);
tty_acs_image_sextants[i][3] = 0x80 | (cp & 0x3f);
}
}
u_char
tty_acs_image_sextant(u_int mask)
{
if (mask == 0 || mask == 21 || mask == 42 || mask >= 63)
return (0);
if (mask < 21)
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 1);
if (mask < 42)
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 2);
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 3);
}
#endif
/* Table mapping UTF-8 to ACS entries. */ /* Table mapping UTF-8 to ACS entries. */
struct tty_acs_reverse_entry { struct tty_acs_reverse_entry {
const char *string; const char *string;
@@ -295,15 +239,6 @@ tty_acs_get(struct tty *tty, u_char ch)
return (&tty->term->acs[ch][0]); return (&tty->term->acs[ch][0]);
} }
#ifdef ENABLE_IMAGES
if (ch >= TTY_ACS_IMAGE_SEXTANT_FIRST &&
ch <= TTY_ACS_IMAGE_SEXTANT_LAST) {
tty_acs_image_sextants_init();
return (tty_acs_image_sextants[
ch - TTY_ACS_IMAGE_SEXTANT_FIRST]);
}
#endif
/* Otherwise look up the UTF-8 translation. */ /* Otherwise look up the UTF-8 translation. */
entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table), entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table),
sizeof tty_acs_table[0], tty_acs_cmp); sizeof tty_acs_table[0], tty_acs_cmp);

View File

@@ -123,10 +123,6 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
struct grid *gd = s->grid; struct grid *gd = s->grid;
const struct grid_cell *gcp; const struct grid_cell *gcp;
struct grid_cell gc, ngc, last; struct grid_cell gc, ngc, last;
#ifdef ENABLE_IMAGES
struct grid_cell image_gc;
int image_status;
#endif
struct grid_line *gl; struct grid_line *gl;
u_int i, j, last_i, cx, ex, width; u_int i, j, last_i, cx, ex, width;
u_int cellsize, bg; u_int cellsize, bg;
@@ -251,35 +247,20 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
if (px >= ex || i >= ex - px) { if (px >= ex || i >= ex - px) {
/* Outside the area being drawn. */ /* Outside the area being drawn. */
empty = nx - i;
gcp = &grid_default_cell; gcp = &grid_default_cell;
#ifdef ENABLE_IMAGES
image_status = image_get_fallback_at(tty, s, px + i,
py, gcp, &image_gc, style_ctx);
if (image_status == 1) {
gcp = &image_gc;
empty = 0;
} else if (image_status == -1)
empty = 1;
else
#endif
empty = nx - i;
} else { } else {
/* Get the current cell. */ /* Get the current cell. */
grid_view_get_cell(gd, px + i, py, &gc); grid_view_get_cell(gd, px + i, py, &gc);
gcp = &gc;
#ifdef ENABLE_IMAGES
if (image_get_fallback_at(tty, s, px + i, py, &gc,
&image_gc, style_ctx) == 1)
gcp = &image_gc;
#endif
/* Work out empty cells. */ /* Work out empty cells. */
empty = tty_draw_line_get_empty(gcp, &last, nx - i); empty = tty_draw_line_get_empty(&gc, &last,
nx - i);
if (empty != 0) if (empty != 0)
; gcp = &gc;
else { else {
/* Update for codeset if needed. */ /* Update for codeset if needed. */
gcp = tty_check_codeset(tty, gcp); gcp = tty_check_codeset(tty, &gc);
/* And for selection. */ /* And for selection. */
if (gcp->flags & GRID_FLAG_SELECTED) { if (gcp->flags & GRID_FLAG_SELECTED) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-features.c,v 1.42 2026/08/17 14:47:41 nicm Exp $ */ /* $OpenBSD: tty-features.c,v 1.43 2026/08/31 12:41:03 kirill Exp $ */
/* /*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -224,11 +224,9 @@ static const struct tty_feature tty_feature_strikethrough = {
0 0
}; };
#define TTY_FEATURE_SYNC "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;"
/* Terminal supports synchronized updates. */ /* Terminal supports synchronized updates. */
static const char *const tty_feature_sync_capabilities[] = { static const char *const tty_feature_sync_capabilities[] = {
TTY_FEATURE_SYNC, "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;",
NULL NULL
}; };
static const struct tty_feature tty_feature_sync = { static const struct tty_feature tty_feature_sync = {
@@ -359,34 +357,6 @@ static const struct tty_feature tty_feature_sixel = {
TERM_SIXEL TERM_SIXEL
}; };
/* Terminal has Kitty graphics protocol Unicode placeholder capability. */
static const char *const tty_feature_kitty_capabilities[] = {
"Kty",
TTY_FEATURE_SYNC,
NULL
};
static const struct tty_feature tty_feature_kitty = {
"kitty",
tty_feature_kitty_capabilities,
TERM_KITTY
};
#ifdef ENABLE_IMAGES
static const char *const tty_feature_image_capabilities[] = {
NULL
};
static const struct tty_feature tty_feature_image_quadrants = {
"image-quadrants",
tty_feature_image_capabilities,
TERM_IMAGE_QUADRANTS
};
static const struct tty_feature tty_feature_image_sextants = {
"image-sextants",
tty_feature_image_capabilities,
TERM_IMAGE_SEXTANTS
};
#endif
/* Terminal supports the OSC 9;4 progress bar. */ /* Terminal supports the OSC 9;4 progress bar. */
static const char *const tty_feature_progressbar_capabilities[] = { static const char *const tty_feature_progressbar_capabilities[] = {
"Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\", "Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\",
@@ -398,6 +368,13 @@ static const struct tty_feature tty_feature_progressbar = {
0 0
}; };
/* Terminal supports UTF-8. */
static const struct tty_feature tty_feature_utf8 = {
"utf8",
NULL,
0
};
/* Available terminal features. */ /* Available terminal features. */
static const struct tty_feature *const tty_features[] = { static const struct tty_feature *const tty_features[] = {
&tty_feature_256, &tty_feature_256,
@@ -409,11 +386,6 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_extkeys, &tty_feature_extkeys,
&tty_feature_focus, &tty_feature_focus,
&tty_feature_ignorefkeys, &tty_feature_ignorefkeys,
&tty_feature_kitty,
#ifdef ENABLE_IMAGES
&tty_feature_image_quadrants,
&tty_feature_image_sextants,
#endif
&tty_feature_margins, &tty_feature_margins,
&tty_feature_mouse, &tty_feature_mouse,
&tty_feature_osc7, &tty_feature_osc7,
@@ -425,7 +397,8 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_strikethrough, &tty_feature_strikethrough,
&tty_feature_sync, &tty_feature_sync,
&tty_feature_title, &tty_feature_title,
&tty_feature_usstyle &tty_feature_usstyle,
&tty_feature_utf8
}; };
/* Parse features for client. */ /* Parse features for client. */
@@ -508,6 +481,9 @@ tty_feature_present(struct tty_term *term, const char *name)
u_int i; u_int i;
char *copy; char *copy;
if (strcmp(name, "utf8") == 0)
return ((term->tty->client->flags & CLIENT_UTF8) != 0);
for (i = 0; i < nitems(tty_features); i++) { for (i = 0; i < nitems(tty_features); i++) {
tf = tty_features[i]; tf = tty_features[i];
if (strcmp(tf->name, name) == 0) { if (strcmp(tf->name, name) == 0) {
@@ -521,7 +497,8 @@ tty_feature_present(struct tty_term *term, const char *name)
* We don't just have the feature flag set. Check if the capabilities * We don't just have the feature flag set. Check if the capabilities
* supported by the client are actual set instead. * supported by the client are actual set instead.
*/ */
if (tf == NULL || strcmp(name, "ignorefkeys") == 0) if (tf == NULL || tf->capabilities == NULL ||
strcmp(name, "ignorefkeys") == 0)
return (0); return (0);
if (tf->flags != 0 && (term->flags & tf->flags) != tf->flags) if (tf->flags != 0 && (term->flags & tf->flags) != tf->flags)
return (0); return (0);
@@ -569,6 +546,8 @@ tty_apply_features(struct tty_term *term)
} }
} }
term->flags |= tf->flags; term->flags |= tf->flags;
if (tf == &tty_feature_utf8)
c->flags |= CLIENT_UTF8;
} }
if ((term->applied_features|feat) == term->applied_features) if ((term->applied_features|feat) == term->applied_features)
return (0); return (0);
@@ -644,7 +623,6 @@ tty_default_features(struct client *c, const char *name, u_int version)
"extkeys," "extkeys,"
"focus," "focus,"
"hyperlinks," "hyperlinks,"
"sixel,"
"usstyle" "usstyle"
}, },
{ .name = "ghostty", { .name = "ghostty",
@@ -658,20 +636,7 @@ tty_default_features(struct client *c, const char *name, u_int version)
"osc7," "osc7,"
"sync," "sync,"
"usstyle," "usstyle,"
"progressbar," "progressbar"
"kitty"
},
{ .name = "kitty",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
"ccolour,"
"cstyle,"
"extkeys,"
"focus,"
"hyperlinks,"
"osc7,"
"sync,"
"usstyle,"
"kitty"
}, },
{ .name = "Rio", { .name = "Rio",
.features = TTY_FEATURES_BASE_MODERN_XTERM "," .features = TTY_FEATURES_BASE_MODERN_XTERM ","

View File

@@ -729,7 +729,6 @@ tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size)
char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0; char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0;
tty_set_size(tty, tty->sx, tty->sy, char_x, char_y); tty_set_size(tty, tty->sx, tty->sy, char_x, char_y);
tty_invalidate(tty); tty_invalidate(tty);
recalculate_sizes();
tty->flags &= ~TTY_WINSIZEQUERY; tty->flags &= ~TTY_WINSIZEQUERY;
*size = end + 1; *size = end + 1;

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