Compare commits

..

186 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
tmux update bot
af29c080ba Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Correct a comparison ('\0' should be 0).
  If a pane has no scrollbar, don't bother to redraw it, and if redraw is deferred change scrollbar redraws into redraw-all-scrollbars not redraw entire window. GitHub issue 5529.
2026-08-25 01:46:56 +00:00
nicm
50ea32162f Correct a comparison ('\0' should be 0). 2026-08-25 01:46:55 +00:00
nicm
2d5328a860 If a pane has no scrollbar, don't bother to redraw it, and if redraw is
deferred change scrollbar redraws into redraw-all-scrollbars not redraw
entire window. GitHub issue 5529.
2026-08-25 01:46:55 +00:00
tmux update bot
e5db7598b2 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not add messages to cfg_causes after config has finished, GitHub issue 5528.
2026-08-24 22:03:29 +00:00
nicm
dc78d8f9ea Do not add messages to cfg_causes after config has finished, GitHub
issue 5528.
2026-08-24 22:03:27 +00:00
tmux update bot
022b8451e3 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Always use synchronized updates in copy mode (like when not in the active pane), since copy mode will almost always moved the cursor. From Artem Goldenberg in GitHub issue 5525.
2026-08-24 16:44:37 +00:00
nicm
5b54c7d41a Always use synchronized updates in copy mode (like when not in the
active pane), since copy mode will almost always moved the cursor. From
Artem Goldenberg in GitHub issue 5525.
2026-08-24 16:44:34 +00:00
tmux update bot
f94036b2b0 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Free any remaining items when destroying screen, from Jeong, Heon.
2026-08-24 15:57:10 +00:00
nicm
7d358208b9 Free any remaining items when destroying screen, from Jeong, Heon. 2026-08-24 15:57:09 +00:00
tmux update bot
c8ff8ea96f Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add {} to shell escape characters (special in bash/ksh/csh), GitHub issue 5514.
2026-08-24 15:02:17 +00:00
nicm
402d366bba Add {} to shell escape characters (special in bash/ksh/csh), GitHub
issue 5514.
2026-08-24 15:02:15 +00:00
Nicholas Marriott
41e701ad84 Return early if image is zero height when generating fallback, otherwise we can
allocated too little space. Reported by Vivek Parikh.
2026-08-24 15:28:17 +01:00
tmux update bot
f763f94556 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not leak cached status line, from Jeong, Heon.
  Free pane fallback range when freeing pane, from Jeong, Heon.
  Do not leak path when destroying client, from Jeong, Heon.
  Do not leak format when drawing preview, from Jeong, Heon.
  Do not leak buffer name on failure, from Jeong, Heon.
2026-08-24 08:28:15 +00:00
nicm
c9f5ecb4b5 Do not leak cached status line, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
7792a74c76 Free pane fallback range when freeing pane, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
4cabc2ae85 Do not leak path when destroying client, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
995b395795 Do not leak format when drawing preview, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
3d44f75d09 Do not leak buffer name on failure, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
tmux update bot
c1f947a3c5 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Use correct X position when combining characters, GitHub issue 5511 from Francesc Rosas.
2026-08-21 11:56:01 +00:00
nicm
696a16cca2 Use correct X position when combining characters, GitHub issue 5511 from
Francesc Rosas.
2026-08-21 11:55:59 +00:00
Nicholas Marriott
936bb6e7cd Regress for GitHub issue 5511 from Francesc Rosas. 2026-08-21 10:54:29 +01:00
tmux update bot
8dfa903346 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a way for floating panes to stay above zoom (-A) flag. Use by default for modal panes to match popups.
2026-08-20 12:57:29 +00:00
nicm
95a7170709 Add a way for floating panes to stay above zoom (-A) flag. Use by
default for modal panes to match popups.
2026-08-20 12:57:26 +00:00
Nicholas Marriott
554c4bbde4 Tests for modal panes on top. 2026-08-20 10:19:19 +01:00
Nicholas Marriott
ac23f21a2c More regress tests for zoom. 2026-08-20 09:18:58 +01:00
tmux update bot
6696151008 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Stop at the right end line when walking wrapped lines, GitHub issue 5479.
2026-08-19 21:05:08 +00:00
nicm
50d0348b64 Stop at the right end line when walking wrapped lines, GitHub issue 5479. 2026-08-19 21:05:06 +00:00
tmux update bot
bf7bbba7ea Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a -K flag to new-pane to make a modal pane capture all keys like popups used to.
2026-08-19 13:10:09 +00:00
nicm
b28b1b7b9a Add a -K flag to new-pane to make a modal pane capture all keys like
popups used to.
2026-08-19 13:10:08 +00:00
Nicholas Marriott
8499d8007e new-pane -K test bits. 2026-08-19 11:56:27 +01:00
tmux update bot
e5a2058c7c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Use DECRQM to detect mode 2026, from Michael Grant.
2026-08-18 12:03:17 +00:00
nicm
e94fcd96f9 Use DECRQM to detect mode 2026, from Michael Grant. 2026-08-18 12:03:15 +00:00
Nicholas Marriott
c5017e6da8 Bump time limit. 2026-08-18 11:49:34 +01:00
tmux update bot
0169f9e18c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Change correct depth counter for source-file when used from hooks, GitHub issue 5437.
2026-08-18 09:49:28 +00:00
nicm
b170c19274 Change correct depth counter for source-file when used from hooks,
GitHub issue 5437.
2026-08-18 09:49:26 +00:00
tmux update bot
22b3679752 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  When a pane is respawned, reset the control mode offsets. GitHub issue 5498 from Alex Rattray.
  Correctly clear clients referring to a session with destroy-unattached, GitHub issue 5497 from Jeong, Heon.
  Restore umask if bind fails, from nachalsa at naver dot com.
2026-08-18 09:04:02 +00:00
nicm
4714f478fe When a pane is respawned, reset the control mode offsets. GitHub issue
5498 from Alex Rattray.
2026-08-18 09:04:00 +00:00
nicm
72c5037f06 Correctly clear clients referring to a session with destroy-unattached,
GitHub issue 5497 from Jeong, Heon.
2026-08-18 09:04:00 +00:00
nicm
6238793ad7 Restore umask if bind fails, from nachalsa at naver dot com. 2026-08-18 09:04:00 +00:00
Nicholas Marriott
da641e4e8d Add test for GitHub issue 5498 from Alex Rattray. 2026-08-18 08:45:11 +01:00
Nicholas Marriott
19a085a672 Fix socket names for macOS. 2026-08-18 08:23:16 +01:00
tmux update bot
f672a0f677 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Log unknown message types.
2026-08-17 23:38:12 +00:00
nicm
db194ced5b Log unknown message types. 2026-08-17 23:38:11 +00:00
tmux update bot
11ed661e2c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Flush output before ending sync. GitHub issue 5495 from xiangzhedev at gmail dot com.
2026-08-17 21:11:33 +00:00
nicm
b2de803871 Flush output before ending sync. GitHub issue 5495 from xiangzhedev at
gmail dot com.
2026-08-17 21:11:31 +00:00
Nicholas Marriott
30abbe36a8 Regress for sync, from xiangzhedev@gmail.com. 2026-08-17 21:04:26 +01:00
tmux update bot
62044f02df Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Allow features to be disabled using @ suffix in terminal-features, from Michael Grant.
2026-08-17 16:39:40 +00:00
nicm
a18d4e0067 Allow features to be disabled using @ suffix in terminal-features, from
Michael Grant.
2026-08-17 16:39:38 +00:00
Nicholas Marriott
0f241b7447 Update CHANGES. 2026-08-17 13:59:10 +01:00
tmux update bot
efaa1fe31c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  If writing a file fails, propagate the error to the server via a new message. Use a client flag rather than bumping the protocol version. GitHub issue 5451.
  Add default terminal features for Rio, GitHub issue 5489 from Raphael Amorim.
2026-08-17 09:45:11 +00:00
nicm
1729bb8e8a If writing a file fails, propagate the error to the server via a new
message. Use a client flag rather than bumping the protocol version.
GitHub issue 5451.
2026-08-17 09:45:10 +00:00
nicm
9fdd04ae61 Add default terminal features for Rio, GitHub issue 5489 from Raphael
Amorim.
2026-08-17 09:45:10 +00:00
tmux update bot
87a8bda023 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Remove flags from both keys for menus, GitHub issue 5484.
2026-08-17 08:54:28 +00:00
nicm
d384557b63 Remove flags from both keys for menus, GitHub issue 5484. 2026-08-17 08:54:26 +00:00
tmux update bot
534a4f81fc Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Attach window to session earlier so resize cannot cause customize mode to blow up. Reported by Marcel Partap.
  Do not copy too many positions when deleting, GitHub issue 5478 from Uzair Aftab.
2026-08-17 07:58:26 +00:00
nicm
a0d3585c6c Attach window to session earlier so resize cannot cause customize mode
to blow up. Reported by Marcel Partap.
2026-08-17 07:58:24 +00:00
nicm
ec34ff52cc Do not copy too many positions when deleting, GitHub issue 5478 from
Uzair Aftab.
2026-08-17 07:58:24 +00:00
Nicholas Marriott
0ac8138ce3 Add a break-pane test for customize mode. 2026-08-17 08:06:31 +01:00
Nicholas Marriott
c41cad4c5f Merge pull request #5481 from r1w1s1/fix-getpeereid-socklen
compat: use socklen_t for getsockopt length
2026-08-17 07:52:02 +01:00
r1w1s1
f466d6289e compat: use socklen_t for getsockopt length 2026-08-10 15:38:55 -03:00
tmux update bot
851c5a933d Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Only unzoom in the floating-split case, modal panes take care of themselves.
2026-08-07 09:33:41 +00:00
nicm
10048f6579 Only unzoom in the floating-split case, modal panes take care of themselves. 2026-08-07 09:33:39 +00:00
tmux update bot
a598b2a910 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Always unzoom before splitting window, for floating panes also. We already unzoomed for tiled panes and this does the same for floating panes (until we support having them float over a zoomed pane). From Éric NICOLAS.
2026-08-07 03:40:47 +00:00
nicm
2ec1038ffd Always unzoom before splitting window, for floating panes also. We
already unzoomed for tiled panes and this does the same for floating
panes (until we support having them float over a zoomed pane). From Éric
NICOLAS.
2026-08-07 03:40:45 +00:00
Nicholas Marriott
cbb1da66c3 Merge pull request #5471 from mathstuf/actions-only-on-main-repo
ci: only run `lock` and `regress` workflows on the main repo
2026-08-06 15:44:32 +01:00
Ben Boeckel
35ab660042 ci: only run lock and regress workflows on the main repo
Forks are just wasting CI cycles spinning actions which are probably not
looked at. Also avoid locking issues and PRs on any forks with
upstream's policies.
2026-08-06 10:23:46 -04:00
tmux update bot
40a485db9c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add T key to change pane title, from Fouad Wahabi in GitHub issue 5461.
2026-08-06 11:42:00 +00:00
nicm
641e75e6e2 Add T key to change pane title, from Fouad Wahabi in GitHub issue 5461. 2026-08-06 11:41:58 +00:00
tmux update bot
2e394bef11 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a refresh-now command and change the r binding back to it in copy mode.
2026-08-05 15:40:06 +00:00
nicm
4a9dbac435 Add a refresh-now command and change the r binding back to it in copy mode. 2026-08-05 15:40:04 +00:00
tmux update bot
d60dbcf91c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Ignore Ms if it is invalid (for example no %p1) and report in client mode, GitHub issue 5460.
2026-08-05 11:31:10 +00:00
nicm
bd483a63cf Ignore Ms if it is invalid (for example no %p1) and report in client
mode, GitHub issue 5460.
2026-08-05 11:31:08 +00:00
tmux update bot
d5dc0ba855 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Use ACS arrows for tree mode.
  Do not crash when synchronizing groups and there is no current window. GitHub issue 5467 from Ju-an Zhang.
  Add pane_private_modes format with list of DEC private modes, from George Nachman.
  Do not log when logging is off, from Michael Grant.
2026-08-05 09:10:55 +00:00
nicm
e56a042153 Use ACS arrows for tree mode. 2026-08-05 09:10:54 +00:00
nicm
1995b5559d Do not crash when synchronizing groups and there is no current window.
GitHub issue 5467 from Ju-an Zhang.
2026-08-05 09:10:54 +00:00
nicm
fe384e06bc Add pane_private_modes format with list of DEC private modes, from
George Nachman.
2026-08-05 09:10:54 +00:00
nicm
0198ee9e51 Do not log when logging is off, from Michael Grant. 2026-08-05 09:10:54 +00:00
Nicholas Marriott
72e705cbd6 Tweak times for macOS. 2026-08-05 08:26:10 +01:00
tmux update bot
d5afb67a81 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Switch from imsg_get to imsgbuf_get and switch type of n to int.
2026-08-04 17:23:12 +00:00
claudio
3894397b07 Switch from imsg_get to imsgbuf_get and switch type of n to int.
OK tb@
2026-08-04 17:23:10 +00:00
tmux update bot
36381f0884 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not attempt to drain write buffer on stuck clients, since it requires a weird dance to make libevent do it. Instead, just ignore the client and destroy the buffer normally if not drained in 10 seconds.
2026-08-04 15:37:15 +00:00
nicm
5cd1976a4a Do not attempt to drain write buffer on stuck clients, since it requires
a weird dance to make libevent do it. Instead, just ignore the client
and destroy the buffer normally if not drained in 10 seconds.
2026-08-04 15:37:14 +00:00
tmux update bot
c9ae3f6a02 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Check for floating only for flags that require a floating pane, reported by Ilya Grigoriev.
2026-08-03 23:50:33 +00:00
nicm
d2322d0114 Check for floating only for flags that require a floating pane, reported
by Ilya Grigoriev.
2026-08-03 23:50:32 +00:00
tmux update bot
7a431ec7b9 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not let a stuck client prevent the server from exiting - give up after 10 seconds. GitHub issue 5444 from Ben Maurer.
2026-08-03 21:33:23 +00:00
nicm
114aa68082 Do not let a stuck client prevent the server from exiting - give up
after 10 seconds. GitHub issue 5444 from Ben Maurer.
2026-08-03 21:33:21 +00:00
Nicholas Marriott
f07a76d959 Use move-pane for a test. 2026-08-03 21:30:44 +01:00
Nicholas Marriott
ca0d77855f Client exit test from Ben Maurer. 2026-08-03 21:19:10 +01:00
Nicholas Marriott
cd3f577c85 Add test for control mode events and guards. 2026-08-03 20:27:10 +01:00
tmux update bot
9228f97d7d Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Queue control mode notifications rather than emitting them inside %begin/%end, GitHub issue 5458 from George Nachman.
2026-08-03 16:29:11 +00:00
nicm
6db5175e4e Queue control mode notifications rather than emitting them inside
%begin/%end, GitHub issue 5458 from George Nachman.
2026-08-03 16:29:09 +00:00
Nicholas Marriott
fba5ece05c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Store background colour in padding cells and correctly clear adjacent cells when tabs are overwritten, GitHu issue 5441 from Ayman Bagabas.
  Exit failure on socket failure, from Rayan Salhab.
  Pessimize compiler flags for screen-redraw.c to prevent tmux from dumping core upon startup on landisk.
2026-08-03 17:27:11 +01:00
nicm
01a775a46c Store background colour in padding cells and correctly clear adjacent
cells when tabs are overwritten, GitHu issue 5441 from Ayman Bagabas.
2026-08-03 13:59:13 +00:00
nicm
4918360ea1 Exit failure on socket failure, from Rayan Salhab. 2026-08-03 13:59:13 +00:00
Nicholas Marriott
433a5cf580 Add regress test from Ayman Bagabas. 2026-08-03 13:59:27 +01:00
Nicholas Marriott
fc9270c943 Test for socket error from Rayan Salhab. 2026-08-03 11:09:04 +01:00
Nicholas Marriott
fa8c5789a6 Sleep after kill. 2026-08-03 09:53:50 +01:00
miod
cce56fa19b Pessimize compiler flags for screen-redraw.c to prevent tmux from dumping core
upon startup on landisk.
2026-08-02 23:13:19 +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
116 changed files with 7773 additions and 2871 deletions

View File

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

View File

@@ -16,7 +16,8 @@ jobs:
regress:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
if: github.repository == 'tmux/tmux'
timeout-minutes: 300
strategy:
fail-fast: false
@@ -37,7 +38,9 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: dependencies
if: runner.os == 'Linux'
@@ -80,7 +83,7 @@ jobs:
- name: logs
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: regress-logs-${{ matrix.name }}
path: regress/logs/*.log

67
CHANGES
View File

@@ -1,7 +1,4 @@
CHANGES FROM 3.7b TO 3.8
* Build with jemalloc on macOS to avoid what appears to be a bug in the system
calloc(3) (issue 5385).
CHANGES FROM 3.7c TO 3.8
* Many improvements to floating panes:
@@ -12,7 +9,12 @@ CHANGES FROM 3.7b TO 3.8
- modal panes may be created with new-pane -O; a window can have one modal
pane and it prevents interaction with the other panes while it is active.
A modal pane is now used for the editor in buffer mode;
-C closes the pane when the mouse is clicked outside it and -K sends all
keys, including the prefix key, to the pane. A modal pane is now used for
the editor in buffer mode;
- new-pane -A creates a floating pane which remains above a zoomed pane;
modal panes do this by default;
- move-pane and resize-pane can now move and resize floating panes, including
mouse dragging;
@@ -27,6 +29,9 @@ CHANGES FROM 3.7b TO 3.8
-k for choose commands kills the pane when the mode exits, which is useful
for modes in floating panes;
- command-prompt -P opens a prompt inside a pane rather than on the status
line;
- pane-border-status has top-floating and bottom-floating, and there are new
default bindings under C-b g for common move and resize operations;
@@ -77,7 +82,7 @@ CHANGES FROM 3.7b TO 3.8
- new hooks to cover areas including client create and destroy, pane
activity, pane mode and prompt changes, pane movement and resizing, session
group changes and window creation, close and zoom;
group changes and window creation, close, swap and zoom;
- OSC 133 escape sequences now trigger events: pane-command-started,
pane-command-finished and pane-shell-prompt.
@@ -90,10 +95,23 @@ CHANGES FROM 3.7b TO 3.8
while scrolling or while hovered and disappears after pane-scrollbars-timeout
(Michael Grant).
* Extend the fill-character option so both inside and outside the window can be
changed.
* Add a clear-on-attach server option. When disabled, tmux does not enter the
alternate screen on attach and instead scrolls the existing terminal content
into the scrollback buffer so it remains accessible (issue 5508).
* Change set-option and set-hooks to use formats and add a -F flag to each.
* Copy mode can now automatically refresh as pane content changes with the
refresh-on, refresh-off and refresh-toggle commands. Automatic refresh is off
by default; refresh-now refreshes once and is bound to r. These replace
refresh-from-pane (issue 5165).
* Extend the fill-character option so both inside and outside the window can be
changed, with a new default for unused areas inside the window.
* Change show-options and show-hooks to use formats and add a -F flag to each.
* Allow individual terminal features to be disabled by adding @ to their names
in terminal-features, and add a utf8 feature for terminals which support
UTF-8 output.
* Add a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk).
@@ -108,6 +126,8 @@ CHANGES FROM 3.7b TO 3.8
* Add new-window -E, respawn-pane -E and respawn-window -E as more convenient
methods to create an empty pane (rather than using '' for the command).
* Add a default C-b T binding to change the current pane title.
* Menus now belong to the window, so appear on all clients.
* Dragging over an existing copy mode selection now adjusts it (Michael Grant).
@@ -121,7 +141,12 @@ CHANGES FROM 3.7b TO 3.8
* Copy mode no longer exits at the bottom while a selection is in progress
(issue 5349).
* Fix scrollbar initial state so scrollbars appear on new windows (issue 5339).
* Add copy-mode-current-line-style to set the style of the line containing the
cursor in copy mode (issue 5391).
* Copy mode word commands now recognise all Unicode whitespace characters; a
space in word-separators matches any Unicode whitespace character (issue
5562).
* Add style attributes for dimming colours (dim=) and for hyperlinks (link= and
nolink) (issues 4842 and 4280 from Moritz Angermann).
@@ -133,7 +158,9 @@ CHANGES FROM 3.7b TO 3.8
* Add or improve format variables including client_colours,
pane_start_command_list, window_manual_width, window_manual_height,
pane_last_output_time, pane_modal_flag and window_modal_pane.
pane_last_output_time, pane_modal_flag, window_modal_pane, pane_private_modes,
pane_output_generation, history_added, history_collected, history_generation,
hook_fire_count and hook_fire_time.
* Add additional pane sort orders, and a z sort order for choose-tree so
floating panes can be sorted by z-index.
@@ -150,7 +177,10 @@ CHANGES FROM 3.7b TO 3.8
* Fix control mode clients hanging on exit if pty data was still queued (Ben
Maurer, issue 5356), and avoid sending notifications to clients which are
already exiting (Ben Maurer, issue 5357).
already exiting (Ben Maurer, issue 5357). Queue notifications so they are not
sent inside %begin/%end (issue 5458), do not let a stuck client prevent the
server from exiting (issue 5444), and reset control mode offsets when a pane
is respawned (issue 5498).
* Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180).
@@ -169,7 +199,18 @@ CHANGES FROM 3.7b TO 3.8
* Do not crash looking for the next or previous session (issue 5344), or when
no client is available.
* Check time periodically in loops rather than for every item (issue 5367).
CHANGES FROM 3.7b TO 3.7c
* Build with jemalloc on macOS to avoid what appears to be a bug in calloc
(issue 5385).
* Fix scrollbar initial state so they appear on new windows (issue 5339).
* Check time periodically in loops rather than every one (issue 5367).
* Use message-style again as default for message-format.
* Unzoom before creating floating panes to avoid a crash.
CHANGES FROM 3.7a TO 3.7b

View File

@@ -246,8 +246,8 @@ if HAVE_UTF8PROC
nodist_tmux_SOURCES += compat/utf8proc.c
endif
# Enable image support.
if ENABLE_IMAGES
# Enable sixel support.
if ENABLE_SIXEL
dist_tmux_SOURCES += image.c image-sixel.c
endif

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>
@@ -762,8 +762,6 @@ args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx,
cmdq_error(item, "%s", error);
free(error);
}
else
cmdlist->references++;
args_make_commands_free(state);
return (cmdlist);
}
@@ -827,8 +825,10 @@ args_make_commands(struct args_command_state *state, int argc, char **argv,
int i;
if (state->cmdlist != NULL) {
if (argc == 0)
if (argc == 0) {
state->cmdlist->references++;
return (state->cmdlist);
}
return (cmd_list_copy(state->cmdlist, argc, argv));
}

16
cfg.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cfg.c,v 1.90 2026/07/14 17:17:17 nicm Exp $ */
/* $OpenBSD: cfg.c,v 1.91 2026/08/03 13:38:42 nicm Exp $ */
/*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -223,13 +223,15 @@ cfg_print_causes(struct cmdq_item *item)
{
struct client *c = cmdq_get_client(item);
u_int i;
char *cause;
for (i = 0; i < cfg_ncauses; i++) {
cause = cfg_causes[i];
if (c != NULL && (c->flags & CLIENT_CONTROL))
control_write(c, "%%config-error %s", cfg_causes[i]);
control_notify_write(c, "%%config-error %s", cause);
else
cmdq_print(item, "%s", cfg_causes[i]);
free(cfg_causes[i]);
cmdq_print(item, "%s", cause);
free(cause);
}
free(cfg_causes);
@@ -244,14 +246,16 @@ cfg_show_causes(struct session *s)
struct window_pane *wp;
struct window_mode_entry *wme;
u_int i;
char *cause;
if (cfg_ncauses == 0)
return;
if (c != NULL && (c->flags & CLIENT_CONTROL)) {
for (i = 0; i < cfg_ncauses; i++) {
control_write(c, "%%config-error %s", cfg_causes[i]);
free(cfg_causes[i]);
cause = cfg_causes[i];
control_notify_write(c, "%%config-error %s", cause);
free(cause);
}
goto out;
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */
/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -276,7 +276,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
proc_set_signals(client_proc, client_signal);
/* Save the flags. */
client_flags = flags;
client_flags = flags|CLIENT_WRITE_ACK;
log_debug("flags are %#llx", (unsigned long long)client_flags);
/* Initialize the client socket and start the server. */
@@ -712,6 +712,9 @@ client_dispatch_wait(struct imsg *imsg)
fprintf(stderr, "server version is too old for client\n");
proc_exit(client_proc);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}
@@ -798,5 +801,8 @@ client_dispatch_attached(struct imsg *imsg)
system(data);
proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-break-pane.c,v 1.75 2026/07/23 09:38:27 nicm Exp $ */
/* $OpenBSD: cmd-break-pane.c,v 1.76 2026/08/17 07:04:45 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -174,13 +174,14 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item)
}
window_set_fill_cells(w);
if (idx == -1)
idx = -1 - options_get_number(dst_s->options, "base-index");
wl = session_attach(dst_s, w, idx, &cause); /* can't fail */
layout_init(w, wp);
wp->flags |= PANE_CHANGED;
colour_palette_from_option(&wp->palette, wp->options);
if (idx == -1)
idx = -1 - options_get_number(dst_s->options, "base-index");
wl = session_attach(dst_s, w, idx, &cause); /* can't fail */
window_remove_ref(w, __func__);
events_fire_window("window-created", w);
window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx);

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>
@@ -242,9 +242,11 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s,
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
cmd_free_argv(argc, argv);

View File

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

View File

@@ -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>
@@ -591,6 +591,8 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
server_client_clear_overlay(tc);
return (CMD_RETURN_NORMAL);
}
if (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL);
if (!modify && tc->overlay_draw != NULL)
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>
@@ -98,6 +98,7 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_ERROR);
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
return (CMD_RETURN_NORMAL);
}
@@ -164,9 +165,11 @@ cmd_if_shell_callback(struct job *job)
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
out:

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-join-pane.c,v 1.73 2026/07/17 15:24:30 nicm Exp $ */
/* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */
/*
* Copyright (c) 2011 George Nachman <tmux@georgester.com>
@@ -431,25 +431,23 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
if (cmd_get_entry(self) == &cmd_move_pane_entry) {
if (args_has(args, 'M'))
return (cmd_join_pane_mouse_update(item));
if (!window_pane_is_floating(dst_wp)) {
cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR);
}
if ((s = args_get(args, 'P')) != NULL) {
server_unzoom_window(dst_w);
return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
}
if ((s = args_get(args, 'z')) != NULL) {
server_unzoom_window(dst_w);
return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s));
}
if (args_has(args, 'X') ||
if (args_has(args, 'P') ||
args_has(args, 'z') ||
args_has(args, 'X') ||
args_has(args, 'Y') ||
args_has(args, 'U') ||
args_has(args, 'D') ||
args_has(args, 'L') ||
args_has(args, 'R')) {
if (!window_pane_is_floating(dst_wp)) {
cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR);
}
server_unzoom_window(dst_w);
if ((s = args_get(args, 'P')) != NULL)
return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
if ((s = args_get(args, 'z')) != NULL)
return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s));
return (cmd_join_pane_move(item, args, dst_wl, dst_wp));
}
}

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>
@@ -60,7 +60,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct client *tc = cmdq_get_target_client(item);
struct session *s = target->s;
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;
const char *template, *name;
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
* name already exists, select it.
* If -S is given, select an existing window instead of creating a
* new one: preferring -t if it already points at a window, or
* otherwise -n if one exists with that name. If neither matches,
* fall through and create a window as normal.
*/
name = args_get(args, 'n');
if (name != NULL) {
@@ -88,32 +91,39 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
wname = clean_name(expanded, 0);
free(expanded);
}
if (args_has(args, 'S') && wname != NULL && target->idx == -1) {
expanded = format_single(item, wname, c, s, NULL, NULL);
RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0)
continue;
if (new_wl == NULL) {
new_wl = wl;
continue;
if (args_has(args, 'S')) {
if (idx != -1)
new_wl = winlink_find_by_index(&s->windows, idx);
else if (wname != NULL) {
expanded = format_single(item, wname, c, s, NULL, NULL);
RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0)
continue;
if (new_wl == NULL) {
new_wl = wl;
continue;
}
cmdq_error(item, "multiple windows named %s",
wname);
free(wname);
free(expanded);
return (CMD_RETURN_ERROR);
}
cmdq_error(item, "multiple windows named %s", wname);
free(wname);
free(expanded);
return (CMD_RETURN_ERROR);
}
free(expanded);
if (new_wl != NULL) {
free(wname);
if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL);
if (session_set_current(s, new_wl) == 0)
server_redraw_session(s);
if (c != NULL && c->session != NULL)
s->curw->window->latest = c;
recalculate_sizes();
}
/* Found an existing window to select instead of creating a new one. */
if (new_wl != NULL) {
free(wname);
if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL);
}
if (session_set_current(s, new_wl) == 0)
server_redraw_session(s);
if (c != NULL && c->session != NULL)
s->curw->window->latest = c;
recalculate_sizes();
return (CMD_RETURN_NORMAL);
}
before = args_has(args, 'b');

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>
@@ -802,6 +802,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd,
if (last == NULL) {
pr->status = CMD_PARSE_SUCCESS;
pr->cmdlist = cmd_list_new();
cmd_parse_free_commands(cmds);
return (1);
}
@@ -814,6 +815,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd,
pi->flags |= CMD_PARSE_NOALIAS;
cmd_parse_build_commands(cmds, pi, pr);
pi->flags &= ~CMD_PARSE_NOALIAS;
cmd_parse_free_commands(cmds);
return (1);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-queue.c,v 1.121 2026/07/14 17:17:17 nicm Exp $ */
/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */
/*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -802,7 +802,7 @@ cmdq_guard(struct cmdq_item *item, const char *guard, int flags)
u_int number = item->number;
if (c != NULL && (c->flags & CLIENT_CONTROL))
control_write(c, "%%%s %ld %u %d", guard, t, number, flags);
control_write_guard(c, guard, t, number, flags);
}
/* Show message from command. */
@@ -850,7 +850,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...)
if (c == NULL) {
cmd_get_source(cmd, &file, &line);
cfg_add_cause("%s:%u: %s", file, line, msg);
if (!cfg_finished) {
if (file != NULL)
cfg_add_cause("%s:%u: %s", file, line, msg);
else
cfg_add_cause("%s", msg);
} else {
if (file != NULL) {
server_add_message("message: %s:%u: %s", file,
line, msg);
} else
server_add_message("message: %s", msg);
}
} else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) {
server_add_message("%s message: %s", c->name, msg);
if (~c->flags & CLIENT_UTF8) {

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>
@@ -92,6 +92,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
}
server_unzoom_window(w);
lc = wp->layout_cell; /* may have been replaced by unzoom */
if (args_has(args, 'x')) {
x = args_percentage(args, 'x', 0, PANE_MAXIMUM, w->sx, &cause);

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>
@@ -233,9 +233,11 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg)
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
if (cdata->item != NULL)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -160,6 +160,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
char *title;
const char *style;
struct options_entry *o;
int visible, Zflag = args_has(args, 'Z');
if (entry == &cmd_last_pane_entry || args_has(args, 'l')) {
/*
@@ -185,14 +186,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window_borders(lastwp->window);
server_status_window(lastwp->window);
} else {
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && lastwp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(lastwp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, lastwp);
if (window_set_active_pane(w, lastwp, 1)) {
cmd_find_from_winlink(current, wl, 0);
cmd_select_pane_redraw(w);
}
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
return (CMD_RETURN_NORMAL);
@@ -268,14 +273,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
if (wp == w->active)
return (CMD_RETURN_NORMAL);
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && wp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, wp);
if (window_set_active_pane(w, wp, 1))
cmd_find_from_winlink_pane(current, wl, wp, 0);
cmdq_insert_hook(s, item, current, "after-select-pane");
cmd_select_pane_redraw(w);
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
return (CMD_RETURN_NORMAL);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */
/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "%s", cause);
goto fail;
}
free(bufname);
return (CMD_RETURN_NORMAL);
}
@@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "no data specified");
goto fail;
}
if ((newsize = strlen(args_string(args, 0))) == 0)
if ((newsize = strlen(args_string(args, 0))) == 0) {
free(bufname);
return (CMD_RETURN_NORMAL);
}
if (args_has(args, 'a') && pb != NULL) {
olddata = paste_buffer_data(pb, &bufsize);
@@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'w') && tc != NULL)
tty_set_selection(&tc->tty, "", bufdata, bufsize);
free(bufname);
return (CMD_RETURN_NORMAL);
fail:

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */
/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */
/*
* Copyright (c) 2008 Tiago Cunha <me@tiagocunha.org>
@@ -50,6 +50,7 @@ const struct cmd_entry cmd_source_file_entry = {
struct cmd_source_file_data {
struct cmdq_item *item;
struct client *client;
int flags;
struct cmdq_item *after;
@@ -60,10 +61,24 @@ struct cmd_source_file_data {
u_int nfiles;
};
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
static void
cmd_source_file_free_data(struct cmd_source_file_data *cdata)
{
struct client *c = cmdq_get_client(item);
u_int i;
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
if (cdata->client != NULL)
server_client_unref(cdata->client);
free(cdata);
}
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, void *data)
{
struct cmd_source_file_data *cdata = data;
struct client *c = cdata->client;
if (c == NULL) {
cmd_source_file_depth--;
@@ -74,36 +89,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
}
cfg_print_causes(item);
cmd_source_file_free_data(cdata);
return (CMD_RETURN_NORMAL);
}
static void
cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata)
cmd_source_file_complete(struct cmd_source_file_data *cdata)
{
struct client *c = cdata->client;
struct cmdq_item *new_item;
u_int i;
if (cfg_finished) {
if (cdata->retval == CMD_RETURN_ERROR &&
c != NULL &&
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL);
cmdq_insert_after(cdata->after, new_item);
if (!cfg_finished) {
cmd_source_file_free_data(cdata);
return;
}
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
free(cdata);
if (cdata->retval == CMD_RETURN_ERROR &&
c != NULL &&
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata);
cmdq_insert_after(cdata->after, new_item);
}
static void
cmd_source_file_done(struct client *c, const char *path, int error,
int closed, struct evbuffer *buffer, void *data)
cmd_source_file_done(__unused struct client *oc, const char *path,
int error, int closed, struct evbuffer *buffer, void *data)
{
struct cmd_source_file_data *cdata = data;
struct cmdq_item *item = cdata->item;
struct client *c = cdata->client;
void *bdata = EVBUFFER_DATA(buffer);
size_t bsize = EVBUFFER_LENGTH(buffer);
u_int n;
@@ -127,7 +142,7 @@ cmd_source_file_done(struct client *c, const char *path, int error,
if (n < cdata->nfiles)
file_read(c, cdata->files[n], cmd_source_file_done, cdata);
else {
cmd_source_file_complete(c, cdata);
cmd_source_file_complete(cdata);
cmdq_continue(item);
}
}
@@ -187,6 +202,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
cdata = xcalloc(1, sizeof *cdata);
cdata->item = item;
cdata->client = c;
if (c != NULL)
c->references++;
if (args_has(args, 'q'))
cdata->flags |= CMD_PARSE_QUIET;
@@ -249,7 +267,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
file_read(c, cdata->files[0], cmd_source_file_done, cdata);
retval = CMD_RETURN_WAIT;
} else
cmd_source_file_complete(c, cdata);
cmd_source_file_complete(cdata);
free(cwd);
return (retval);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */
/* $OpenBSD: cmd-split-window.c,v 1.150 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane",
.alias = "newp",
.args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bCdefhIklMOPvWZ] [-B border-lines] "
.args = { "AbB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-AbCdefhIkKLMOPvWZ] [-B border-lines] "
"[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] "
@@ -89,6 +89,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state fs;
struct key_event *event = cmdq_get_event(item);
int input, empty, is_floating, flags = 0;
int restore_zoom = 0;
const char *template, *style, *value;
char *cause = NULL, *cp, *title;
const struct options_table_entry *oe;
@@ -96,9 +97,16 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
enum pane_lines lines;
u_int count = args_count(args);
if (window_active_pane_is_over_zoom(w))
restore_zoom = 1;
if (cmd_get_entry(self) == &cmd_new_pane_entry)
is_floating = !args_has(args, 'L');
else {
if (!window_pane_is_visible(wp))
restore_zoom = 0;
if (!restore_zoom)
window_unzoom(w, 1);
is_floating = window_pane_is_floating(wp);
flags |= SPAWN_SPLIT;
}
@@ -132,7 +140,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'Z'))
flags |= SPAWN_ZOOM;
if (args_has(args, 'O'))
flags |= SPAWN_MODAL;
flags |= SPAWN_MODAL|SPAWN_FLOATOVERZOOM;
if (is_floating && args_has(args, 'A'))
flags |= SPAWN_FLOATOVERZOOM;
if ((w->flags & WINDOW_ZOOMED) && (flags & SPAWN_FLOATOVERZOOM))
restore_zoom = 1;
input = args_has(args, 'I');
if (input || (count == 1 && *args_string(args, 0) == '\0'))
@@ -168,6 +180,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (cause != NULL) {
cmdq_error(item, "%s", cause);
free(cause);
if (restore_zoom)
window_pop_zoom(w);
return (CMD_RETURN_ERROR);
}
@@ -201,6 +215,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
*/
goto fail;
}
if (args_has(args, 'K') && args_has(args, 'O'))
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK;
@@ -270,7 +286,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (~flags & SPAWN_DETACHED)
cmd_find_from_winlink_pane(current, wl, new_wp, 0);
if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) {
if (restore_zoom) {
window_pop_zoom(wp->window);
server_redraw_window(wp->window);
} else if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) {
window_pop_zoom(wp->window);
server_redraw_window(wp->window);
}
@@ -321,8 +340,9 @@ fail:
if (!is_floating)
layout_close_pane(new_wp);
window_remove_pane(wp->window, new_wp);
} else if (args_has(args, 'O'))
window_pop_modal_zoom(wp->window);
}
if (restore_zoom || (~flags & SPAWN_FLOATING))
window_pop_zoom(wp->window);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */
/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -53,7 +53,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state target;
const char *tflag = args_get(args, 't');
enum cmd_find_type type;
int flags;
int flags, visible, Zflag = args_has(args, 'Z');
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct session *s;
@@ -139,11 +139,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
if (wl != NULL && wp != NULL && wp != wl->window->active) {
w = wl->window;
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && wp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, wp);
window_set_active_pane(w, wp, 1);
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
if (wl != NULL) {

View File

@@ -31,7 +31,7 @@ getpeereid(int s, uid_t *uid, gid_t *gid)
{
#ifdef HAVE_SO_PEERCRED
struct ucred uc;
int len = sizeof uc;
socklen_t len = sizeof uc;
if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &uc, &len) == -1)
return (-1);

View File

@@ -530,24 +530,15 @@ if test "x$enable_cgroups" = xyes; then
fi
fi
# Enable image support.
AC_ARG_ENABLE(
images,
AS_HELP_STRING(--enable-images, enable SIXEL and ASCII images)
)
# Keep the old option as an alias for one compatibility cycle.
# Enable sixel support.
AC_ARG_ENABLE(
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
enable_images=yes
AC_DEFINE(ENABLE_SIXEL)
fi
if test "x$enable_images" = xyes; then
AC_DEFINE(ENABLE_IMAGES)
fi
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes])
# Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well.
AC_MSG_CHECKING(for b64_ntop)
@@ -1146,10 +1137,10 @@ if test "x$enable_asan" = xyes; then
else
AC_MSG_NOTICE([ASAN: off])
fi
if test "x$enable_images" = xyes; then
AC_MSG_NOTICE([images: on])
if test "x$enable_sixel" = xyes; then
AC_MSG_NOTICE([SIXEL: on])
else
AC_MSG_NOTICE([images: off])
AC_MSG_NOTICE([SIXEL: off])
fi
if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on])

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */
/* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */
/*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -44,7 +44,8 @@ control_pane_mode_changed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%pane-mode-changed %%%u", wp->id);
control_notify_write(c, "%%pane-mode-changed %%%u",
wp->id);
}
return;
}
@@ -53,8 +54,9 @@ control_pane_mode_changed_cb(__unused const char *name,
if (value == NULL)
return;
TAILQ_FOREACH(c, &clients, entry) {
if (CONTROL_SHOULD_NOTIFY_CLIENT(c))
control_write(c, "%%pane-mode-changed %s", value);
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_notify_write(c, "%%pane-mode-changed %s", value);
}
free(value);
}
@@ -92,7 +94,7 @@ control_window_layout_changed_cb(__unused const char *name,
continue;
s = c->session;
if (winlink_find_by_window_id(&s->windows, w->id) != NULL)
control_write(c, "%s", cp);
control_notify_write(c, "%s", cp);
}
free(cp);
}
@@ -111,7 +113,7 @@ control_window_pane_changed_cb(__unused const char *name,
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%window-pane-changed @%u %%%u", w->id,
control_notify_write(c, "%%window-pane-changed @%u %%%u", w->id,
w->active->id);
}
}
@@ -133,9 +135,11 @@ control_window_unlinked_cb(__unused const char *name, struct event_payload *ep,
cs = c->session;
if (winlink_find_by_window_id(&cs->windows, w->id) != NULL)
control_write(c, "%%window-close @%u", w->id);
else
control_write(c, "%%unlinked-window-close @%u", w->id);
control_notify_write(c, "%%window-close @%u", w->id);
else {
control_notify_write(c, "%%unlinked-window-close @%u",
w->id);
}
}
}
@@ -156,9 +160,11 @@ control_window_linked_cb(__unused const char *name, struct event_payload *ep,
cs = c->session;
if (winlink_find_by_window_id(&cs->windows, w->id) != NULL)
control_write(c, "%%window-add @%u", w->id);
else
control_write(c, "%%unlinked-window-add @%u", w->id);
control_notify_write(c, "%%window-add @%u", w->id);
else {
control_notify_write(c, "%%unlinked-window-add @%u",
w->id);
}
}
}
@@ -179,11 +185,11 @@ control_window_renamed_cb(__unused const char *name, struct event_payload *ep,
cs = c->session;
if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) {
control_write(c, "%%window-renamed @%u %s", w->id,
w->name);
} else {
control_write(c, "%%unlinked-window-renamed @%u %s",
control_notify_write(c, "%%window-renamed @%u %s",
w->id, w->name);
} else {
control_notify_write(c,
"%%unlinked-window-renamed @%u %s", w->id, w->name);
}
}
}
@@ -206,11 +212,12 @@ control_client_session_changed_cb(__unused const char *name,
continue;
if (cc == c) {
control_write(c, "%%session-changed $%u %s", s->id,
s->name);
control_notify_write(c, "%%session-changed $%u %s",
s->id, s->name);
} else {
control_write(c, "%%client-session-changed %s $%u %s",
cc->name, s->id, s->name);
control_notify_write(c,
"%%client-session-changed %s $%u %s", cc->name,
s->id, s->name);
}
}
}
@@ -226,8 +233,9 @@ control_client_detached_cb(__unused const char *name, struct event_payload *ep,
if (cc == NULL)
return;
TAILQ_FOREACH(c, &clients, entry) {
if (CONTROL_SHOULD_NOTIFY_CLIENT(c))
control_write(c, "%%client-detached %s", cc->name);
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_notify_write(c, "%%client-detached %s", cc->name);
}
}
@@ -244,8 +252,8 @@ control_session_renamed_cb(__unused const char *name, struct event_payload *ep,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%session-renamed $%u %s", s->id, s->name);
control_notify_write(c, "%%session-renamed $%u %s", s->id,
s->name);
}
}
@@ -259,8 +267,7 @@ control_session_created_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%sessions-changed");
control_notify_write(c, "%%sessions-changed");
}
}
@@ -274,8 +281,7 @@ control_session_closed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%sessions-changed");
control_notify_write(c, "%%sessions-changed");
}
}
@@ -298,9 +304,8 @@ control_session_window_changed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%session-window-changed $%u @%u", s->id,
s->curw->window->id);
control_notify_write(c, "%%session-window-changed $%u @%u",
s->id, s->curw->window->id);
}
}
@@ -317,8 +322,7 @@ control_paste_buffer_changed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%paste-buffer-changed %s", pbname);
control_notify_write(c, "%%paste-buffer-changed %s", pbname);
}
}
@@ -335,8 +339,7 @@ control_paste_buffer_deleted_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue;
control_write(c, "%%paste-buffer-deleted %s", pbname);
control_notify_write(c, "%%paste-buffer-deleted %s", pbname);
}
}

197
control.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.62 2026/07/17 08:37:29 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>
@@ -53,6 +53,17 @@ struct control_block {
TAILQ_ENTRY(control_block) all_entry;
};
/*
* A notification line deferred because it was generated while a command's
* %begin/%end guard block was open. Notifications must never appear inside a
* guard block, so they are held here and flushed once the block closes.
*/
struct control_line {
char *line;
TAILQ_ENTRY(control_line) entry;
};
/* Control client pane. */
struct control_pane {
u_int pane;
@@ -102,6 +113,13 @@ struct control_state {
struct bufferevent *write_event;
struct monitor_set *subs;
/*
* Depth of open %begin/%end guard blocks and notifications deferred
* until the outermost block closes.
*/
int guard_depth;
TAILQ_HEAD(, control_line) deferred;
};
/* Low and high watermarks. */
@@ -352,7 +370,7 @@ control_continue_pane(struct client *c, struct window_pane *wp)
cp->flags &= ~CONTROL_PANE_PAUSED;
memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
control_write(c, "%%continue %%%u", wp->id);
control_notify_write(c, "%%continue %%%u", wp->id);
}
}
@@ -366,52 +384,149 @@ control_pause_pane(struct client *c, struct window_pane *wp)
if (~cp->flags & CONTROL_PANE_PAUSED) {
cp->flags |= CONTROL_PANE_PAUSED;
control_discard_pane(c, cp);
control_write(c, "%%pause %%%u", wp->id);
control_notify_write(c, "%%pause %%%u", wp->id);
}
}
/* Write a line. */
static void printflike(2, 0)
control_vwrite(struct client *c, const char *fmt, va_list ap)
/*
* Reset a pane after its buffer has been replaced: drop any output still
* queued from the old buffer and start again from the pane's own offset.
*/
void
control_reset_pane(struct client *c, struct window_pane *wp)
{
struct control_state *cs = c->control_state;
char *s;
struct control_pane *cp;
xvasprintf(&s, fmt, ap);
log_debug("%s: %s: writing line: %s", __func__, c->name, s);
bufferevent_write(cs->write_event, s, strlen(s));
bufferevent_write(cs->write_event, "\n", 1);
bufferevent_enable(cs->write_event, EV_WRITE);
free(s);
if (c->control_state == NULL)
return;
cp = control_get_pane(c, wp);
if (cp == NULL)
return;
control_discard_pane(c, cp);
memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
}
/* Write a line. */
void
control_write(struct client *c, const char *fmt, ...)
/* Write an already-formatted line, queueing it behind %output if needed. */
static void
control_write_line(struct client *c, char *line)
{
struct control_state *cs = c->control_state;
struct control_block *cb;
va_list ap;
va_start(ap, fmt);
if (TAILQ_EMPTY(&cs->all_blocks)) {
control_vwrite(c, fmt, ap);
va_end(ap);
log_debug("%s: %s: writing line: %s", __func__, c->name, line);
bufferevent_write(cs->write_event, line, strlen(line));
bufferevent_write(cs->write_event, "\n", 1);
bufferevent_enable(cs->write_event, EV_WRITE);
free(line);
return;
}
cb = xcalloc(1, sizeof *cb);
xvasprintf(&cb->line, fmt, ap);
cb->line = line;
TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
cb->t = get_timer();
log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
bufferevent_enable(cs->write_event, EV_WRITE);
}
/* Flush notifications that were deferred while a guard block was open. */
static void
control_flush_deferred(struct client *c)
{
struct control_state *cs = c->control_state;
struct control_line *cl, *cl1;
TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) {
TAILQ_REMOVE(&cs->deferred, cl, entry);
control_write_line(c, cl->line);
free(cl);
}
}
/*
* Write a line of command output or error text. This is a sink for arbitrary
* user-controlled text (command output, capture-pane, error messages), so it
* must never try to interpret the content: guard tracking is done only in
* control_write_guard.
*/
void
control_write(struct client *c, const char *fmt, ...)
{
struct control_state *cs = c->control_state;
char *line;
va_list ap;
if (cs == NULL)
return;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
va_end(ap);
control_write_line(c, line);
}
/*
* Write a %begin, %end or %error guard around a command's output. This is the
* only place guard lines are produced, so the block depth is maintained here;
* when the outermost block closes any deferred notifications are flushed after
* it. "guard" is always one of the fixed strings from cmdq_guard, never user
* text.
*/
void
control_write_guard(struct client *c, const char *guard, long t, u_int number,
int flags)
{
struct control_state *cs = c->control_state;
char *line;
if (cs == NULL)
return;
if (strcmp(guard, "begin") == 0)
cs->guard_depth++;
xasprintf(&line, "%%%s %ld %u %d", guard, t, number, flags);
control_write_line(c, line);
if (strcmp(guard, "begin") != 0 && cs->guard_depth > 0 &&
--cs->guard_depth == 0)
control_flush_deferred(c);
}
/*
* Write a notification line. Notifications must never appear inside a command's
* %begin/%end guard block, so if one is open the line is deferred until it
* closes.
*/
void
control_notify_write(struct client *c, const char *fmt, ...)
{
struct control_state *cs = c->control_state;
struct control_line *cl;
va_list ap;
char *line;
if (cs == NULL)
return;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
va_end(ap);
if (cs->guard_depth == 0) {
control_write_line(c, line);
return;
}
log_debug("%s: %s: deferring notification: %s", __func__, c->name,
line);
cl = xcalloc(1, sizeof *cl);
cl->line = line;
TAILQ_INSERT_TAIL(&cs->deferred, cl, entry);
}
/* Check age for this pane. */
@@ -438,7 +553,7 @@ control_check_age(struct client *c, struct window_pane *wp,
return (0);
cp->flags |= CONTROL_PANE_PAUSED;
control_discard_pane(c, cp);
control_write(c, "%%pause %%%u", wp->id);
control_notify_write(c, "%%pause %%%u", wp->id);
} else {
if (age < CONTROL_MAXIMUM_AGE)
return (0);
@@ -805,14 +920,17 @@ control_sub_change(struct monitor_change *change, __unused void *data)
if (wp != NULL) {
w = wp->window;
control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s",
control_notify_write(c,
"%%subscription-changed %s $%u @%u %u %%%u : %s",
change->name, s->id, w->id, wl->idx, wp->id, change->value);
} else if (wl != NULL) {
w = wl->window;
control_write(c, "%%subscription-changed %s $%u @%u %u - : %s",
control_notify_write(c,
"%%subscription-changed %s $%u @%u %u - : %s",
change->name, s->id, w->id, wl->idx, change->value);
} else {
control_write(c, "%%subscription-changed %s $%u - - - : %s",
control_notify_write(c,
"%%subscription-changed %s $%u - - - : %s",
change->name, s->id, change->value);
}
}
@@ -835,6 +953,7 @@ control_start(struct client *c)
RB_INIT(&cs->windows);
TAILQ_INIT(&cs->pending_list);
TAILQ_INIT(&cs->all_blocks);
TAILQ_INIT(&cs->deferred);
cs->subs = monitor_create_client(c, control_sub_change, NULL);
cs->read_event = bufferevent_new(c->fd, control_read_callback,
@@ -878,6 +997,19 @@ control_discard(struct client *c)
bufferevent_disable(cs->read_event, EV_READ);
}
/* Discard all tmux-owned queued control blocks and stop writing. */
void
control_discard_all(struct client *c)
{
struct control_state *cs = c->control_state;
struct control_block *cb, *cb1;
control_discard(c);
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb);
bufferevent_disable(cs->write_event, EV_WRITE);
}
/* Stop control mode. */
void
control_stop(struct client *c)
@@ -885,12 +1017,19 @@ control_stop(struct client *c)
struct control_state *cs = c->control_state;
struct control_block *cb, *cb1;
struct control_window *cw, *cw1;
struct control_line *cl, *cl1;
if (cs == NULL)
return;
monitor_destroy(cs->subs);
TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) {
TAILQ_REMOVE(&cs->deferred, cl, entry);
free(cl->line);
free(cl);
}
if (~c->flags & CLIENT_CONTROLCONTROL)
bufferevent_free(cs->write_event);
bufferevent_free(cs->read_event);

86
file.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: file.c,v 1.21 2026/07/26 15:08:15 nicm Exp $ */
/* $OpenBSD: file.c,v 1.22 2026/08/17 07:56:56 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -502,7 +502,8 @@ file_push(struct client_file *cf)
} else if (cf->stream > 2) {
close.stream = cf->stream;
proc_send(cf->peer, MSG_WRITE_CLOSE, -1, &close, sizeof close);
file_fire_done(cf);
if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK))
file_fire_done(cf);
}
free(msg);
}
@@ -527,14 +528,48 @@ file_write_left(struct client_files *files)
return (waiting != 0);
}
/* Finish writing a client file. */
static void
file_write_finished(struct client_file *cf)
{
struct msg_write_done msg;
if (cf->event != NULL) {
bufferevent_free(cf->event);
cf->event = NULL;
}
if (cf->fd != -1) {
if (close(cf->fd) != 0 && cf->error == 0)
cf->error = errno;
cf->fd = -1;
}
msg.stream = cf->stream;
msg.error = cf->error;
proc_send(cf->peer, MSG_WRITE_DONE, -1, &msg, sizeof msg);
if (cf->cb != NULL)
cf->cb(NULL, NULL, 0, -1, NULL, cf->data);
file_free(cf);
}
/* Client file write error callback. */
static void
file_write_error_callback(__unused struct bufferevent *bev, __unused short what,
file_write_error_callback(__unused struct bufferevent *bev, short what,
void *arg)
{
struct client_file *cf = arg;
int error;
if (what & EVBUFFER_ERROR)
error = errno;
else
error = EIO;
if (error == 0)
error = EIO;
log_debug("write error file %d", cf->stream);
cf->error = error;
bufferevent_free(cf->event);
cf->event = NULL;
@@ -542,7 +577,9 @@ file_write_error_callback(__unused struct bufferevent *bev, __unused short what,
close(cf->fd);
cf->fd = -1;
if (cf->cb != NULL)
if (cf->closed)
file_write_finished(cf);
else if (cf->cb != NULL)
cf->cb(NULL, NULL, 0, -1, NULL, cf->data);
}
@@ -554,15 +591,10 @@ file_write_callback(__unused struct bufferevent *bev, void *arg)
log_debug("write check file %d", cf->stream);
if (cf->cb != NULL)
if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0)
file_write_finished(cf);
else if (cf->cb != NULL)
cf->cb(NULL, NULL, 0, -1, NULL, cf->data);
if (cf->closed && EVBUFFER_LENGTH(cf->event->output) == 0) {
bufferevent_free(cf->event);
close(cf->fd);
RB_REMOVE(client_files, cf->tree, cf);
file_free(cf);
}
}
/* Handle a file write open message (client). */
@@ -663,14 +695,10 @@ file_write_close(struct client_files *files, struct imsg *imsg)
if ((cf = RB_FIND(client_files, files, &find)) == NULL)
fatalx("unknown stream number");
log_debug("close file %d", cf->stream);
cf->closed = 1;
if (cf->event == NULL || EVBUFFER_LENGTH(cf->event->output) == 0) {
if (cf->event != NULL)
bufferevent_free(cf->event);
if (cf->fd != -1)
close(cf->fd);
RB_REMOVE(client_files, files, cf);
file_free(cf);
file_write_finished(cf);
}
}
@@ -829,6 +857,28 @@ file_write_ready(struct client_files *files, struct imsg *imsg)
return (0);
}
/* Handle a write done message (server). */
int
file_write_done(struct client_files *files, struct imsg *imsg)
{
struct msg_write_done *msg = imsg->data;
size_t msglen = imsg->hdr.len - IMSG_HEADER_SIZE;
struct client_file find, *cf;
if (msglen != sizeof *msg)
return (-1);
find.stream = msg->stream;
if ((cf = RB_FIND(client_files, files, &find)) == NULL)
return (0);
if (cf->c == NULL || (~cf->c->flags & CLIENT_WRITE_ACK))
return (0);
log_debug("file %d write done", cf->stream);
cf->error = msg->error;
file_fire_done(cf);
return (0);
}
/* Handle read data message (server). */
int
file_read_data(struct client_files *files, struct imsg *imsg)

133
format.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.410 2026/07/29 20:43:20 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>
@@ -1887,6 +1887,37 @@ format_cb_cursor_blinking(struct format_tree *ft)
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. */
static void *
format_cb_history_limit(struct format_tree *ft)
@@ -2101,6 +2132,60 @@ format_cb_synchronized_output_flag(struct format_tree *ft)
return (NULL);
}
/* Callback for pane_private_modes. */
static void *
format_cb_pane_private_modes(struct format_tree *ft)
{
static const struct {
int mode;
int number;
} table[] = {
{ MODE_KCURSOR, 1 }, /* DECCKM */
{ MODE_ORIGIN, 6 }, /* DECOM */
{ MODE_WRAP, 7 }, /* DECAWM */
{ MODE_CURSOR_BLINKING, 12 }, /* cursor blinking */
{ MODE_CURSOR, 25 }, /* DECTCEM */
{ MODE_MOUSE_STANDARD, 1000 }, /* mouse normal tracking */
{ MODE_MOUSE_BUTTON, 1002 }, /* mouse button tracking */
{ MODE_MOUSE_ALL, 1003 }, /* mouse any tracking */
{ MODE_FOCUSON, 1004 }, /* focus reporting */
{ MODE_MOUSE_UTF8, 1005 }, /* mouse: UTF-8 */
{ MODE_MOUSE_SGR, 1006 }, /* mouse: SGR */
{ MODE_BRACKETPASTE, 2004 }, /* bracketed paste */
{ MODE_SYNC, 2026 }, /* synchronized output */
{ MODE_THEME_UPDATES, 2031 }, /* theme update notifications */
};
int mode;
char *value = NULL, *tmp;
u_int i;
if (ft->wp == NULL)
return (NULL);
mode = ft->wp->base.mode;
for (i = 0; i < nitems(table); i++) {
if (~mode & table[i].mode)
continue;
/*
* Only report cursor blinking when set by the application, not
* when it comes from the cursor-style option.
*/
if (table[i].mode == MODE_CURSOR_BLINKING &&
(~mode & MODE_CURSOR_BLINKING_SET))
continue;
if (value == NULL)
xasprintf(&value, "%d", table[i].number);
else {
xasprintf(&tmp, "%s,%d", value, table[i].number);
free(value);
value = tmp;
}
}
if (value == NULL)
return (xstrdup(""));
return (value);
}
/* Callback for pane_active. */
static void *
format_cb_pane_active(struct format_tree *ft)
@@ -2222,6 +2307,19 @@ format_cb_pane_last_output_time(struct format_tree *ft)
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. */
static void *
format_cb_pane_last_prompt_time(struct format_tree *ft)
@@ -2964,17 +3062,6 @@ format_cb_sixel_support(__unused struct format_tree *ft)
#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. */
static void *
format_cb_active_window_index(struct format_tree *ft)
@@ -3620,12 +3707,21 @@ static const struct format_table_entry format_table[] = {
{ "cursor_y", FORMAT_TABLE_STRING,
format_cb_cursor_y
},
{ "history_added", FORMAT_TABLE_STRING,
format_cb_history_added
},
{ "history_all_bytes", FORMAT_TABLE_STRING,
format_cb_history_all_bytes
},
{ "history_bytes", FORMAT_TABLE_STRING,
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,
format_cb_history_limit
},
@@ -3638,9 +3734,6 @@ static const struct format_table_entry format_table[] = {
{ "host_short", FORMAT_TABLE_STRING,
format_cb_host_short
},
{ "image_support", FORMAT_TABLE_STRING,
format_cb_image_support
},
{ "insert_flag", FORMAT_TABLE_STRING,
format_cb_insert_flag
},
@@ -3809,6 +3902,9 @@ static const struct format_table_entry format_table[] = {
{ "pane_mode", FORMAT_TABLE_STRING,
format_cb_pane_mode
},
{ "pane_output_generation", FORMAT_TABLE_STRING,
format_cb_pane_output_generation
},
{ "pane_path", FORMAT_TABLE_STRING,
format_cb_pane_path
},
@@ -3827,6 +3923,9 @@ static const struct format_table_entry format_table[] = {
{ "pane_pipe_pid", FORMAT_TABLE_STRING,
format_cb_pane_pipe_pid
},
{ "pane_private_modes", FORMAT_TABLE_STRING,
format_cb_pane_private_modes
},
{ "pane_right", FORMAT_TABLE_STRING,
format_cb_pane_right
},
@@ -4214,6 +4313,8 @@ format_log_debug_cb(const char *key, const char *value, void *arg)
void
format_log_debug(struct format_tree *ft, const char *prefix)
{
if (log_get_level() == 0)
return;
format_each(ft, format_log_debug_cb, (void *)prefix);
}
@@ -4342,7 +4443,7 @@ format_quote_shell(const char *s)
at = out = xmalloc(strlen(s) * 2 + 1);
for (cp = s; *cp != '\0'; cp++) {
if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL)
if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL)
*at++ = '\\';
*at++ = *cp;
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */
/* $OpenBSD: grid-view.c,v 1.39 2026/08/03 12:58:53 nicm Exp $ */
/*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -47,9 +47,9 @@ grid_view_set_cell(struct grid *gd, u_int px, u_int py,
/* Set padding. */
void
grid_view_set_padding(struct grid *gd, u_int px, u_int py)
grid_view_set_padding(struct grid *gd, u_int px, u_int py, int bg)
{
grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py));
grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py), bg);
}
/* Set cells. */

245
grid.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid.c,v 1.155 2026/07/29 17:42:56 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>
@@ -41,9 +41,6 @@
/* Default grid cell data. */
const struct grid_cell grid_default_cell = {
{ { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 8, 0
#ifdef ENABLE_IMAGES
, 0, 0, 0
#endif
};
/*
@@ -52,38 +49,16 @@ const struct grid_cell grid_default_cell = {
*/
static const struct grid_cell grid_padding_cell = {
{ { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 8, 0
#ifdef ENABLE_IMAGES
, 0, 0, 0
#endif
};
/* Cleared grid cell data. */
static const struct grid_cell grid_cleared_cell = {
{ { ' ' }, 0, 1, 1 }, 0, GRID_FLAG_CLEARED, 8, 8, 8, 0
#ifdef ENABLE_IMAGES
, 0, 0, 0
#endif
};
static const struct grid_cell_entry grid_cleared_entry = {
{ .data = { 0, 8, 8, ' ' } }, GRID_FLAG_CLEARED
};
#ifdef ENABLE_IMAGES
/* Return the image referenced by a stored cell, or zero. */
static u_int
grid_entry_image(struct grid_line *gl, struct grid_cell_entry *gce)
{
struct grid_extd_entry *gee;
if (~gce->flags & GRID_FLAG_EXTENDED || gce->offset >= gl->extdsize)
return (0);
gee = &gl->extddata[gce->offset];
if (~gee->flags & GRID_FLAG_IMAGE)
return (0);
return (image_get_id_by_grid_id(gee->image_id));
}
#endif
#ifdef __APPLE__
void
grid_check_is_clear(struct grid *gd)
@@ -159,10 +134,6 @@ grid_need_extended_cell(const struct grid_cell_entry *gce,
return (1);
if (gc->flags & GRID_FLAG_TAB)
return (1);
#ifdef ENABLE_IMAGES
if (gc->flags & GRID_FLAG_IMAGE)
return (1);
#endif
return (0);
}
@@ -210,11 +181,6 @@ grid_extended_cell(struct grid_line *gl, struct grid_cell_entry *gce,
gee->bg = gc->bg;
gee->us = gc->us;
gee->link = gc->link;
#ifdef ENABLE_IMAGES
gee->image_id = image_get_grid_id(gc->image_id);
gee->image_x = gc->image_x;
gee->image_y = gc->image_y;
#endif
return (gee);
}
@@ -302,12 +268,6 @@ grid_clear_cell(struct grid *gd, u_int px, u_int py, u_int bg, int moved)
struct grid_extd_entry *gee;
u_int old_offset = gce->offset;
int had_extd = (gce->flags & GRID_FLAG_EXTENDED);
#ifdef ENABLE_IMAGES
u_int image_id = grid_entry_image(gl, gce);
if (!moved && image_id != 0)
image_free(image_id);
#endif
memcpy(gce, &grid_cleared_entry, sizeof *gce);
if (!moved && had_extd && old_offset < gl->extdsize) {
@@ -354,13 +314,6 @@ grid_cells_look_equal(const struct grid_cell *gc1, const struct grid_cell *gc2)
return (0);
if (gc1->link != gc2->link)
return (0);
#ifdef ENABLE_IMAGES
if ((gc1->flags & GRID_FLAG_IMAGE) &&
(gc1->image_id != gc2->image_id ||
gc1->image_x != gc2->image_x ||
gc1->image_y != gc2->image_y))
return (0);
#endif
return (1);
}
@@ -393,10 +346,6 @@ static void
grid_free_line(struct grid *gd, u_int py)
{
struct grid_line *gl = &gd->linedata[py];
#ifdef ENABLE_IMAGES
struct grid_cell_entry *gce;
u_int image_id, px;
#endif
#ifdef __APPLE__
assert(gl->cellused <= gl->cellsize);
@@ -404,14 +353,6 @@ grid_free_line(struct grid *gd, u_int py)
assert(gl->cellsize == 0 || gl->celldata != NULL);
#endif
#ifdef ENABLE_IMAGES
for (px = 0; px < gl->cellsize; px++) {
gce = &gl->celldata[px];
image_id = grid_entry_image(gl, gce);
if (image_id != 0)
image_free(image_id);
}
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
@@ -683,11 +624,6 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc)
gc->bg = gee->bg;
gc->us = gee->us;
gc->link = gee->link;
#ifdef ENABLE_IMAGES
gc->image_id = image_get_id_by_grid_id(gee->image_id);
gc->image_x = gee->image_x;
gc->image_y = gee->image_y;
#endif
if (gc->flags & GRID_FLAG_TAB)
grid_set_tab(gc, gee->data);
@@ -708,11 +644,6 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc)
gc->us = 8;
utf8_set(&gc->data, gce->data.data);
gc->link = 0;
#ifdef ENABLE_IMAGES
gc->image_id = 0;
gc->image_x = 0;
gc->image_y = 0;
#endif
}
/* Get cell for reading. */
@@ -732,9 +663,6 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc)
{
struct grid_line *gl;
struct grid_cell_entry *gce;
#ifdef ENABLE_IMAGES
u_int old_id, new_id = 0;
#endif
if (grid_check_y(gd, __func__, py) != 0)
return;
@@ -746,17 +674,6 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc)
gl->cellused = px + 1;
gce = &gl->celldata[px];
#ifdef ENABLE_IMAGES
old_id = grid_entry_image(gl, gce);
if (gc->flags & GRID_FLAG_IMAGE)
new_id = gc->image_id;
if (old_id != new_id) {
if (old_id != 0)
image_free(old_id);
if (new_id != 0)
image_ref(new_id);
}
#endif
if (grid_need_extended_cell(gce, gc))
grid_extended_cell(gl, gce, gc);
else
@@ -765,9 +682,13 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc)
/* Set padding at position. */
void
grid_set_padding(struct grid *gd, u_int px, u_int py)
grid_set_padding(struct grid *gd, u_int px, u_int py, int bg)
{
grid_set_cell(gd, px, py, &grid_padding_cell);
struct grid_cell gc;
memcpy(&gc, &grid_padding_cell, sizeof gc);
gc.bg = bg;
grid_set_cell(gd, px, py, &gc);
}
/* Set cells at position. */
@@ -778,10 +699,6 @@ grid_set_cells(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc,
struct grid_line *gl;
struct grid_cell_entry *gce;
struct grid_extd_entry *gee;
const struct grid_cell *new_gc;
#ifdef ENABLE_IMAGES
struct grid_cell old_gc, image_gc;
#endif
u_int i;
if (grid_check_y(gd, __func__, py) != 0)
@@ -795,39 +712,11 @@ grid_set_cells(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc,
for (i = 0; i < slen; i++) {
gce = &gl->celldata[px + i];
new_gc = gc;
#ifdef ENABLE_IMAGES
{
u_int old_id = grid_entry_image(gl, gce);
u_int new_id;
if (old_id != 0 && (~gc->flags & GRID_FLAG_IMAGE)) {
grid_get_cell1(gl, px + i, &old_gc);
memcpy(&image_gc, gc, sizeof image_gc);
image_gc.flags |= GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED;
image_gc.image_id = old_gc.image_id;
image_gc.image_x = old_gc.image_x;
image_gc.image_y = old_gc.image_y;
new_gc = &image_gc;
}
if (new_gc->flags & GRID_FLAG_IMAGE)
new_id = new_gc->image_id;
else
new_id = 0;
if (old_id != new_id) {
if (old_id != 0)
image_free(old_id);
if (new_id != 0)
image_ref(new_id);
}
}
#endif
if (grid_need_extended_cell(gce, new_gc)) {
gee = grid_extended_cell(gl, gce, new_gc);
if (grid_need_extended_cell(gce, gc)) {
gee = grid_extended_cell(gl, gce, gc);
gee->data = utf8_build_one(s[i]);
} else
grid_store_cell(gce, new_gc, s[i]);
grid_store_cell(gce, gc, s[i]);
}
}
@@ -942,9 +831,6 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx,
{
struct grid_line *gl;
u_int xx;
#ifdef ENABLE_IMAGES
u_int image_id;
#endif
if (nx == 0 || px == dx)
return;
@@ -955,16 +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, dx + nx, 8);
#ifdef ENABLE_IMAGES
/* References in the source range are transferred, not duplicated. */
for (xx = dx; xx < dx + nx; xx++) {
if (xx >= px && xx < px + nx)
continue;
image_id = grid_entry_image(gl, &gl->celldata[xx]);
if (image_id != 0)
image_free(image_id);
}
#endif
memmove(&gl->celldata[dx], &gl->celldata[px],
nx * sizeof *gl->celldata);
if (dx + nx > gl->cellused)
@@ -1337,13 +1213,6 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx,
grid_get_cell(gd, xx, py, &gc);
if (gc.flags & GRID_FLAG_PADDING)
continue;
#ifdef ENABLE_IMAGES
if (gc.flags & GRID_FLAG_IMAGE) {
utf8_set(&gc.data, ' ');
gc.flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED);
gc.image_id = gc.image_x = gc.image_y = 0;
}
#endif
if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) {
grid_string_cells_code(*lastgc, &gc, code, sizeof code,
@@ -1411,9 +1280,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
{
struct grid_line *dstl, *srcl;
u_int yy;
#ifdef ENABLE_IMAGES
u_int px, image_id;
#endif
if (dy + ny > dst->hsize + dst->sy)
ny = dst->hsize + dst->sy - dy;
@@ -1442,13 +1308,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
} else
dstl->extddata = NULL;
#ifdef ENABLE_IMAGES
for (px = 0; px < dstl->cellsize; px++) {
image_id = grid_entry_image(dstl, &dstl->celldata[px]);
if (image_id != 0)
image_ref(image_id);
}
#endif
sy++;
dy++;
}
@@ -1462,30 +1321,6 @@ grid_reflow_dead(struct grid_line *gl)
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;
if (~gl->flags & GRID_LINE_EXTENDED)
return (0);
for (i = 0; i < gl->cellused; i++) {
grid_get_cell1(gl, i, &gc);
#ifdef ENABLE_IMAGES
if (gc.flags & GRID_FLAG_IMAGE)
return (1);
#endif
/* 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. */
static struct grid_line *
grid_reflow_add(struct grid *gd, u_int n)
@@ -1548,10 +1383,6 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
break;
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 (~gd->linedata[line].flags & GRID_LINE_WRAPPED)
wrapped = 0;
@@ -1612,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. */
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]);
}
@@ -1668,15 +1500,6 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy,
}
width += gc.data.width;
grid_set_cell(target, xx, line, &gc);
#ifdef ENABLE_IMAGES
/*
* The tail of the original line is discarded below. Transfer
* its image reference to the new cell rather than duplicating
* it.
*/
if (gc.flags & GRID_FLAG_IMAGE)
image_free(gc.image_id);
#endif
xx++;
}
if (flags & GRID_LINE_WRAPPED)
@@ -1723,12 +1546,6 @@ grid_reflow(struct grid *gd, u_int sx)
if (gl->flags & GRID_LINE_DEAD)
continue;
/* Keep image markers 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
* the available width is hit, and width is the full line
@@ -1818,7 +1635,7 @@ grid_wrap_position(struct grid *gd, u_int px, u_int py, u_int *wx, u_int *wy)
void
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++) {
if (ay == wy)
@@ -1832,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.
*/
if (wx == UINT_MAX) {
while (gd->linedata[yy].flags & GRID_LINE_WRAPPED)
while (yy < ey && gd->linedata[yy].flags & GRID_LINE_WRAPPED)
yy++;
wx = gd->linedata[yy].cellused;
} else {
@@ -1894,21 +1711,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set)
{
struct grid_cell gc, tmp_gc;
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);
if (strchr(set, '\t')) {
if (gc.flags & GRID_FLAG_PADDING) {
pxx = px;
do
grid_get_cell(gd, --pxx, py, &tmp_gc);
while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING);
if (tmp_gc.flags & GRID_FLAG_TAB)
return (tmp_gc.data.width - (px - pxx));
} else if (gc.flags & GRID_FLAG_TAB)
return (gc.data.width);
}
if (gc.flags & GRID_FLAG_PADDING)
if (gc.flags & GRID_FLAG_PADDING) {
if (!has_tab && !has_space)
return (0);
pxx = px;
do
grid_get_cell(gd, --pxx, py, &tmp_gc);
while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING);
if (((has_tab || has_space) &&
(tmp_gc.flags & GRID_FLAG_TAB)) ||
(has_space && utf8_has_whitespace(&tmp_gc.data)))
return (tmp_gc.data.width - (px - pxx));
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));
}
@@ -1935,6 +1760,8 @@ grid_line_flags_string(int flags)
strlcat(s, "START_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT)
strlcat(s, "END_OUTPUT,", sizeof s);
if (flags & GRID_LINE_END_OUTPUT_STATUS)
strlcat(s, "END_OUTPUT_STATUS,", sizeof s);
if (flags & GRID_LINE_HYPERLINK)
strlcat(s, "HYPERLINK,", sizeof s);
if (*s == '\0')

File diff suppressed because it is too large Load Diff

817
image.c
View File

@@ -2,7 +2,6 @@
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
* 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
@@ -19,708 +18,210 @@
#include <sys/types.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
/* An average of part of an image cell. RGB is premultiplied. */
struct image_sample {
u_char red;
u_char green;
u_char blue;
u_char alpha;
u_char brightness;
};
static struct images all_images = TAILQ_HEAD_INITIALIZER(all_images);
static u_int all_images_count;
#define MAX_IMAGE_COUNT 20
/* Half blocks, quadrants and sextants all divide evenly into a 2 by 6 grid. */
#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];
};
/* Immutable image data and cell geometry. */
struct image {
u_int id;
u_short grid_id;
u_int references;
u_int width;
u_int height;
u_int canvas_width;
u_int canvas_height;
u_int sx;
u_int sy;
size_t stride;
size_t size;
u_char *pixels;
/* Original indexed SIXEL data, if this image arrived as SIXEL. */
struct sixel_image *sixel;
struct image_cell *cells; /* lazily generated text samples */
RB_ENTRY(image) entry;
};
RB_HEAD(images, image);
/* A cell-aligned part of an image to draw at a terminal position. */
struct image_rect {
struct image *image;
struct grid_cell cell;
u_int source_x;
u_int source_y;
u_int width;
u_int height;
u_int destination_x;
u_int destination_y;
};
static struct images images = RB_INITIALIZER(&images);
static u_int image_next_id;
static u_short image_next_grid_id;
static struct image *image_grid_ids[USHRT_MAX + 1];
struct image_backend {
const char *name;
int flags;
void (*draw_rect)(struct tty *,
const struct image_rect *, const struct tty_style_ctx *);
void (*free)(struct tty *, int);
};
static const struct image_backend image_backend_ascii = {
"ascii", IMAGE_BACKEND_SCROLLS, NULL, NULL
};
static const struct image_backend image_backend_sixel = {
"sixel", IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_TEMPORAL,
sixel_draw_rect, sixel_free_output
};
/* Compare images by server ID. */
static int
image_cmp(struct image *a, struct image *b)
static void printflike(3, 4)
image_log(struct image *im, const char* from, const char* fmt, ...)
{
if (a->id < b->id)
return (-1);
if (a->id > b->id)
return (1);
return (0);
}
RB_GENERATE_STATIC(images, image, entry, image_cmp);
va_list ap;
char s[128];
/* Find the image backend supported by a terminal. */
static const struct image_backend *
image_tty_find_backend(struct tty *tty)
{
if (tty->term != NULL && tty->term->flags & TERM_SIXEL &&
tty->xpixel != 0 && tty->ypixel != 0)
return (&image_backend_sixel);
return (&image_backend_ascii);
}
/* Update a terminal's image backend after its capabilities change. */
void
image_tty_update(struct tty *tty)
{
const struct image_backend *backend;
backend = image_tty_find_backend(tty);
if (tty->image_backend == backend)
if (log_get_level() == 0)
return;
if (tty->image_backend != NULL && tty->image_backend->free != NULL)
tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED));
tty->image_data = NULL;
tty->image_backend = backend;
log_debug("%s: %s image backend is %s", __func__,
tty->client->name, backend->name);
if (fmt == NULL) {
log_debug("%s: %p (%ux%u %u,%u)", from, im, im->sx, im->sy,
im->px, im->py);
return;
}
va_start(ap, fmt);
vsnprintf(s, sizeof s, fmt, ap);
va_end(ap);
log_debug("%s: %p (%ux%u %u,%u): %s", from, im, im->sx, im->sy,
im->px, im->py, s);
}
/* Return the flags for a terminal's image backend. */
int
image_backend_flags(struct tty *tty)
{
image_tty_update(tty);
return (tty->image_backend->flags);
}
/* Discard image backend state after a terminal geometry change. */
void
image_tty_geometry_changed(struct tty *tty)
{
image_tty_update(tty);
if (tty->image_backend->free != NULL)
tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED));
}
/* Free image backend state for a terminal. */
void
image_tty_free(struct tty *tty, int send)
{
if (tty->image_backend != NULL && tty->image_backend->free != NULL)
tty->image_backend->free(tty, send);
tty->image_backend = NULL;
tty->image_data = NULL;
}
/* Average a rectangle of image pixels into one fallback sample. */
static void
image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y,
uint64_t sample_columns, uint64_t sample_rows, struct image_sample *sample)
image_free(struct image *im)
{
const u_char *pixel;
uint64_t red = 0, green = 0, blue = 0, alpha = 0;
uint64_t brightness = 0, count = 0;
u_int x, y, x0, x1, y0, y1;
image_log(im, __func__, NULL);
x0 = sample_x * im->canvas_width / sample_columns;
x1 = ((sample_x + 1) * im->canvas_width + sample_columns - 1) /
sample_columns;
y0 = sample_y * im->canvas_height / sample_rows;
y1 = ((sample_y + 1) * im->canvas_height + sample_rows - 1) /
sample_rows;
if (x1 <= x0)
x1 = x0 + 1;
if (y1 <= y0)
y1 = y0 + 1;
count = (uint64_t)(x1 - x0) * (y1 - y0);
if (x0 >= im->width || y0 >= im->height)
return;
if (x1 > im->width)
x1 = im->width;
if (y1 > im->height)
y1 = im->height;
TAILQ_REMOVE(&all_images, im, all_entry);
all_images_count--;
for (y = y0; y < y1; y++) {
for (x = x0; x < x1; x++) {
pixel = im->pixels + y * im->stride + x * 4;
red += pixel[0] * pixel[3] / 255;
green += pixel[1] * pixel[3] / 255;
blue += pixel[2] * pixel[3] / 255;
alpha += pixel[3];
brightness += ((2126ULL * pixel[0] +
7152ULL * pixel[1] + 722ULL * pixel[2]) / 10000) *
pixel[3] / 255;
}
}
if (count == 0)
return;
sample->red = red / count;
sample->green = green / count;
sample->blue = blue / count;
sample->alpha = alpha / count;
sample->brightness = brightness / count;
}
/* Build fallback samples for every image cell. */
static void
image_make_cells(struct image *im)
{
struct image_cell *cell;
uint64_t columns, rows;
u_int x, y, sample_x, sample_y;
columns = (uint64_t)im->sx * IMAGE_SAMPLE_COLUMNS;
rows = (uint64_t)im->sy * IMAGE_SAMPLE_ROWS;
im->cells = xcalloc((size_t)im->sx * im->sy, sizeof *im->cells);
for (y = 0; y < im->sy; y++) {
for (x = 0; x < im->sx; x++) {
cell = &im->cells[(size_t)y * im->sx + x];
image_sample(im, x, y, im->sx, im->sy, &cell->whole);
for (sample_y = 0; sample_y < IMAGE_SAMPLE_ROWS;
sample_y++) {
for (sample_x = 0;
sample_x < IMAGE_SAMPLE_COLUMNS; sample_x++) {
image_sample(im,
(uint64_t)x * IMAGE_SAMPLE_COLUMNS + sample_x,
(uint64_t)y * IMAGE_SAMPLE_ROWS + sample_y,
columns, rows,
&cell->samples[sample_y][sample_x]);
}
}
}
}
}
/* Find an image by server ID. */
struct image *
image_find(u_int id)
{
struct image find;
find.id = id;
return (RB_FIND(images, &images, &find));
}
/* Return an image's server ID. */
u_int
image_get_id(const struct image *im)
{
return (im->id);
}
/* Return an image's pixel dimensions. */
void
image_get_size(const struct image *im, u_int *width, u_int *height)
{
if (width != NULL)
*width = im->width;
if (height != NULL)
*height = im->height;
}
/* Return an image canvas's pixel dimensions. */
void
image_get_canvas_size(const struct image *im, u_int *width,
u_int *height)
{
if (width != NULL)
*width = im->canvas_width;
if (height != NULL)
*height = im->canvas_height;
}
/* Return an image's cell dimensions. */
void
image_get_size_in_cells(const struct image *im, u_int *sx, u_int *sy)
{
if (sx != NULL)
*sx = im->sx;
if (sy != NULL)
*sy = im->sy;
}
/* Return an image's RGBA pixels and layout. */
const u_char *
image_get_pixels(const struct image *im, size_t *stride, size_t *size)
{
if (stride != NULL)
*stride = im->stride;
if (size != NULL)
*size = im->size;
return (im->pixels);
}
/* Return an image's original SIXEL data. */
struct sixel_image *
image_get_sixel(const struct image *im)
{
return (im->sixel);
}
/* Associate original SIXEL data with an image. */
void
image_set_sixel(struct image *im, struct sixel_image *si)
{
im->sixel = si;
}
/* Return the image for a drawing rectangle. */
struct image *
image_rect_get_image(const struct image_rect *rectangle)
{
return (rectangle->image);
}
/* Return the source and destination coordinates of a drawing rectangle. */
void
image_rect_get_coords(const struct image_rect *rectangle,
u_int *source_x, u_int *source_y, u_int *width, u_int *height,
u_int *destination_x, u_int *destination_y)
{
*source_x = rectangle->source_x;
*source_y = rectangle->source_y;
*width = rectangle->width;
*height = rectangle->height;
*destination_x = rectangle->destination_x;
*destination_y = rectangle->destination_y;
}
/* Create and register an immutable image. */
struct image *
image_create(u_int width, u_int height, u_int canvas_width,
u_int canvas_height, u_int sx, u_int sy, u_char *pixels)
{
struct image *im;
u_int i;
if (width == 0 || height == 0 || canvas_width < width ||
canvas_height < height || sx == 0 || sy == 0 || pixels == NULL)
return (NULL);
if ((uint64_t)width * height * 4 > SIZE_MAX)
return (NULL);
if ((uint64_t)sx * sy > SIZE_MAX / sizeof *im->cells)
return (NULL);
if (sx > USHRT_MAX || sy > USHRT_MAX)
return (NULL);
for (i = 0; i < USHRT_MAX; i++) {
if (++image_next_grid_id == 0)
image_next_grid_id++;
if (image_grid_ids[image_next_grid_id] == NULL)
break;
}
if (i == USHRT_MAX)
return (NULL);
im = xcalloc(1, sizeof *im);
do {
if (++image_next_id == 0)
image_next_id++;
im->id = image_next_id;
} while (image_find(im->id) != NULL);
im->references = 1;
im->grid_id = image_next_grid_id;
im->width = width;
im->height = height;
im->canvas_width = canvas_width;
im->canvas_height = canvas_height;
im->sx = sx;
im->sy = sy;
im->stride = (size_t)width * 4;
im->size = im->stride * height;
im->pixels = pixels;
RB_INSERT(images, &images, im);
image_grid_ids[im->grid_id] = im;
log_debug("%s: image %u is %ux%u pixels on %ux%u canvas, "
"%ux%u cells", __func__, im->id, width, height, canvas_width,
canvas_height, sx, sy);
return (im);
}
/* Return the compact grid ID for an image. */
u_short
image_get_grid_id(u_int id)
{
struct image *im = image_find(id);
if (im == NULL)
return (0);
return (im->grid_id);
}
/* Return the server image ID for a compact grid ID. */
u_int
image_get_id_by_grid_id(u_short grid_id)
{
struct image *im;
if (grid_id == 0 || (im = image_grid_ids[grid_id]) == NULL)
return (0);
return (im->id);
}
/* Add a reference to an image. */
void
image_ref(u_int id)
{
struct image *im = image_find(id);
if (im == NULL)
fatalx("reference to missing image %u", id);
if (im->references == UINT_MAX)
fatalx("too many references to image %u", id);
im->references++;
}
/* Drop a reference to an image. */
void
image_free(u_int id)
{
struct image *im = image_find(id);
if (im == NULL)
fatalx("free of missing image %u", id);
if (--im->references != 0)
return;
log_debug("%s: freeing image %u", __func__, id);
RB_REMOVE(images, &images, im);
image_grid_ids[im->grid_id] = NULL;
free(im->pixels);
if (im->sixel != NULL)
sixel_free(im->sixel);
free(im->cells);
TAILQ_REMOVE(im->list, im, entry);
sixel_free(im->data);
free(im->fallback);
free(im);
}
/* Return a precomputed fallback cell sample. */
static const struct image_cell *
image_get_cell(struct image *im, u_int x, u_int y)
{
if (im == NULL || x >= im->sx || y >= im->sy)
return (NULL);
if (im->cells == NULL)
image_make_cells(im);
return (&im->cells[(size_t)y * im->sx + x]);
}
/* Return the brightness of an image cell. */
u_char
image_get_brightness(struct image *im, u_int x, u_int y)
{
const struct image_cell *cell;
cell = image_get_cell(im, x, y);
if (cell == NULL)
return (0);
return (cell->whole.brightness);
}
/* Fill a grid cell with an ASCII brightness glyph. */
static void
image_get_text_cell(struct image *im, u_int x, u_int y,
const struct grid_cell *gc, struct grid_cell *out)
{
static const char ramp[] = " .:-=+*#%@";
u_int level;
memcpy(out, gc, sizeof *out);
level = image_get_brightness(im, x, y) * (sizeof ramp - 2) / 255;
utf8_set(&out->data, ramp[level]);
out->flags &= ~GRID_FLAG_IMAGE;
}
/* Get the terminal cell used to draw an image marker. */
int
image_get_draw_cell(struct tty *tty, const struct grid_cell *gc,
struct grid_cell *out)
image_free_all(struct screen *s)
{
struct image *im = image_find(gc->image_id);
struct image *im, *im1;
int redraw = !TAILQ_EMPTY(&s->images);
if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL) {
memcpy(out, gc, sizeof *out);
out->flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED|
GRID_FLAG_SELECTED);
return (1);
}
if (gc->flags & GRID_FLAG_IMAGE_DAMAGED) {
memcpy(out, gc, sizeof *out);
out->flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED);
return (0);
}
image_get_text_cell(im, gc->image_x, gc->image_y, gc, out);
return (0);
if (redraw)
log_debug ("%s", __func__);
TAILQ_FOREACH_SAFE(im, &s->images, entry, im1)
image_free(im);
return (redraw);
}
/* Store an image marker in a grid cell. */
void
image_set_cell(struct grid_cell *gc, struct image *im, u_int x, u_int y)
/* Create text placeholder for an image. */
static void
image_fallback(char **ret, u_int sx, u_int sy)
{
/* Keep the cell contents as the underlay for transparent pixels. */
gc->flags &= ~GRID_FLAG_IMAGE_DAMAGED;
gc->flags |= GRID_FLAG_IMAGE;
gc->image_id = im->id;
gc->image_x = x;
gc->image_y = y;
}
char *buf, *label;
u_int py, size, lsize;
/* Convert a cell-aligned image rectangle into source pixel coordinates. */
void
image_get_pixel_rect(const struct image *im, u_int x, u_int y,
u_int width, u_int height, u_int *px, u_int *py, u_int *pwidth,
u_int *pheight)
{
u_int x1, y1;
*px = *py = *pwidth = *pheight = 0;
if (im == NULL || x >= im->sx || y >= im->sy || width == 0 ||
height == 0)
return;
if (width > im->sx - x)
width = im->sx - x;
if (height > im->sy - y)
height = im->sy - y;
*px = (uint64_t)x * im->canvas_width / im->sx;
*py = (uint64_t)y * im->canvas_height / im->sy;
x1 = ((uint64_t)(x + width) * im->canvas_width + im->sx - 1) /
im->sx;
y1 = ((uint64_t)(y + height) * im->canvas_height + im->sy - 1) /
im->sy;
if (*px >= im->width || *py >= im->height) {
*px = *py = 0;
if (sy == 0) {
*ret = xstrdup("");
return;
}
if (x1 <= *px)
x1 = *px + 1;
if (y1 <= *py)
y1 = *py + 1;
if (x1 > im->width)
x1 = im->width;
if (y1 > im->height)
y1 = im->height;
*pwidth = x1 - *px;
*pheight = y1 - *py;
/* Allocate first line. */
lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1;
if (sx < lsize - 3)
size = lsize - 1;
else
size = sx + 2;
/* Remaining lines. Every placeholder line has \r\n at the end. */
size += (sx + 2) * (sy - 1) + 1;
*ret = buf = xmalloc(size);
/* Render first line. */
if (sx < lsize - 3) {
memcpy(buf, label, lsize);
buf += lsize - 1;
} else {
memcpy(buf, label, lsize - 3);
buf += lsize - 3;
memset(buf, '+', sx - lsize + 3);
buf += sx - lsize + 3;
snprintf(buf, 3, "\r\n");
buf += 2;
}
/* Remaining lines. */
for (py = 1; py < sy; py++) {
memset(buf, '+', sx);
buf += sx;
snprintf(buf, 3, "\r\n");
buf += 2;
}
free(label);
}
/* Calculate the cell dimensions required for pixel dimensions. */
void
image_size_in_cells(u_int width, u_int height, u_int xpixel, u_int ypixel,
u_int *sx, u_int *sy)
struct image*
image_store(struct screen *s, struct sixel_image *si)
{
if (xpixel == 0)
xpixel = 8;
if (ypixel == 0)
ypixel = 16;
*sx = width / xpixel + (width % xpixel != 0);
*sy = height / ypixel + (height % ypixel != 0);
struct image *im;
im = xcalloc(1, sizeof *im);
im->s = s;
im->data = si;
im->px = s->cx;
im->py = s->cy;
sixel_size_in_cells(si, &im->sx, &im->sy);
image_fallback(&im->fallback, im->sx, im->sy);
image_log(im, __func__, NULL);
im->list = &s->images;
TAILQ_INSERT_TAIL(im->list, im, entry);
TAILQ_INSERT_TAIL(&all_images, im, all_entry);
if (++all_images_count == MAX_IMAGE_COUNT)
image_free(TAILQ_FIRST(&all_images));
return (im);
}
/* Return if a screen area contains image markers. */
static int
int
image_check_line(struct screen *s, u_int py, u_int ny)
{
struct image *im, *im1;
int redraw = 0, in;
TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) {
in = (py + ny > im->py && py < im->py + im->sy);
image_log(im, __func__, "py=%u, ny=%u, in=%d", py, ny, in);
if (in) {
image_free(im);
redraw = 1;
}
}
return (redraw);
}
int
image_check_area(struct screen *s, u_int px, u_int py, u_int nx, u_int ny)
{
struct grid_cell gc;
u_int x, y, ex, ey;
u_int sx = screen_size_x(s), sy = screen_size_y(s);
struct image *im, *im1;
int redraw = 0, in;
if (px >= sx || py >= sy || nx == 0 || ny == 0)
return (0);
if (nx > sx - px)
ex = sx;
else
ex = px + nx;
if (ny > sy - py)
ey = sy;
else
ey = py + ny;
for (y = py; y < ey; y++) {
for (x = px; x < ex; x++) {
grid_view_get_cell(s->grid, x, y, &gc);
if (gc.flags & GRID_FLAG_IMAGE)
return (1);
TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) {
in = (py < im->py + im->sy &&
py + ny > im->py &&
px < im->px + im->sx &&
px + nx > im->px);
image_log(im, __func__, "py=%u, ny=%u, in=%d", py, ny, in);
if (in) {
image_free(im);
redraw = 1;
}
}
return (0);
return (redraw);
}
/* Redraw image markers in a screen area. */
void
image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx,
u_int ny)
int
image_scroll_up(struct screen *s, u_int lines)
{
if (ctx->wp != NULL && image_check_area(ctx->s, px, py, nx, ny))
ctx->wp->flags |= PANE_REDRAW;
}
struct image *im, *im1;
int redraw = 0;
u_int sx, sy;
struct sixel_image *new;
/* Redraw all image markers on a screen. */
void
image_redraw_all(struct screen_write_ctx *ctx)
{
image_redraw_area(ctx, 0, 0, screen_size_x(ctx->s),
screen_size_y(ctx->s));
}
/* Redraw images after a scrolling operation. */
void
image_redraw_scroll(struct screen_write_ctx *ctx, __unused u_int lines)
{
image_redraw_all(ctx);
}
/* Draw a span's graphical image markers before or after its text. */
void
image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py,
u_int nx, u_int atx, u_int aty, int before,
const struct tty_style_ctx *style_ctx)
{
const struct image_backend *backend;
struct image_rect rectangle;
struct grid_cell gc, next;
struct image *im;
u_int i, run;
image_tty_update(tty);
backend = tty->image_backend;
if (~backend->flags & IMAGE_BACKEND_GRAPHICAL)
return;
if (before && (~backend->flags & IMAGE_BACKEND_TEMPORAL))
return;
for (i = 0; i < nx; i += run) {
grid_view_get_cell(s->grid, px + i, py, &gc);
if (~gc.flags & GRID_FLAG_IMAGE) {
run = 1;
TAILQ_FOREACH_SAFE(im, &s->images, entry, im1) {
if (im->py >= lines) {
image_log(im, __func__, "1, lines=%u", lines);
im->py -= lines;
redraw = 1;
continue;
}
if (backend->flags & IMAGE_BACKEND_TEMPORAL) {
if ((before && (~gc.flags & GRID_FLAG_IMAGE_DAMAGED)) ||
(!before && (gc.flags & GRID_FLAG_IMAGE_DAMAGED))) {
run = 1;
continue;
}
}
im = image_find(gc.image_id);
if (im == NULL) {
run = 1;
if (im->py + im->sy <= lines) {
image_log(im, __func__, "2, lines=%u", lines);
image_free(im);
redraw = 1;
continue;
}
for (run = 1; i + run < nx; run++) {
grid_view_get_cell(s->grid, px + i + run, py, &next);
if (~next.flags & GRID_FLAG_IMAGE)
break;
if ((backend->flags & IMAGE_BACKEND_TEMPORAL) &&
((before && (~next.flags & GRID_FLAG_IMAGE_DAMAGED)) ||
(!before && (next.flags & GRID_FLAG_IMAGE_DAMAGED))))
break;
if (next.image_id != gc.image_id ||
next.image_y != gc.image_y ||
next.image_x != gc.image_x + run)
break;
}
rectangle.image = im;
memcpy(&rectangle.cell, &gc, sizeof rectangle.cell);
rectangle.source_x = gc.image_x;
rectangle.source_y = gc.image_y;
rectangle.width = run;
rectangle.height = 1;
rectangle.destination_x = atx + i;
rectangle.destination_y = aty;
backend->draw_rect(tty, &rectangle, style_ctx);
sx = im->sx;
sy = (im->py + im->sy) - lines;
image_log(im, __func__, "3, lines=%u, sy=%u", lines, sy);
new = sixel_scale(im->data, 0, 0, 0, im->sy - sy, sx, sy, 1);
sixel_free(im->data);
im->data = new;
im->py = 0;
sixel_size_in_cells(im->data, &im->sx, &im->sy);
free(im->fallback);
image_fallback(&im->fallback, im->sx, im->sy);
redraw = 1;
}
}
/*
* Put image marker cells at the cursor. The pixel object is immutable; only
* the marker rectangle is clipped to the available pane cells.
*/
void
image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg)
{
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct grid_cell gc;
u_int cx = s->cx, cy = s->cy;
u_int x, y, sx, sy, lines;
image_get_size_in_cells(im, &sx, &sy);
if (sx > screen_size_x(s) - cx)
sx = screen_size_x(s) - cx;
if (sy > screen_size_y(s) - 1)
sy = screen_size_y(s) - 1;
if (sx == 0 || sy == 0)
return;
if (screen_size_y(s) - cy <= sy) {
lines = sy - (screen_size_y(s) - cy) + 1;
screen_write_scrollup(ctx, lines, bg);
if (lines > cy)
screen_write_cursormove(ctx, -1, 0, 0);
else
screen_write_cursormove(ctx, -1, cy - lines, 0);
cy = s->cy;
}
for (y = 0; y < sy; y++) {
for (x = 0; x < sx; x++) {
grid_view_get_cell(gd, cx + x, cy + y, &gc);
image_set_cell(&gc, im, x, y);
grid_view_set_cell(gd, cx + x, cy + y, &gc);
}
}
image_redraw_area(ctx, cx, cy, sx, sy);
screen_write_cursormove(ctx, 0, cy + sy, 0);
return (redraw);
}

94
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 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>
@@ -1049,6 +1049,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
if (len == 0)
return;
wp->output_generation++;
window_update_activity(wp->window);
if (~wp->flags & PANE_ACTIVITY) {
wp->flags |= PANE_ACTIVITY;
@@ -1583,7 +1584,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1:
break;
case 0:
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
input_reply(ictx, 1, "\033[?1;2;4c");
#else
input_reply(ictx, 1, "\033[?1;2c");
@@ -1978,7 +1979,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx)
screen_write_mode_clear(sctx, MODE_BRACKETPASTE);
break;
case 2026:
screen_write_stop_sync(ictx->wp);
screen_write_end_sync(sctx);
break;
case 2031:
screen_write_mode_clear(sctx, MODE_THEME_UPDATES);
@@ -2101,7 +2102,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
int n, m, o;
if (ictx->param_list_len > 3)
@@ -2626,10 +2627,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
const char prefix[] = "tmux;";
const u_int prefixlen = (sizeof prefix) - 1;
long long allow_passthrough = 0;
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
struct window *w;
struct sixel_image *si;
int p1, p2;
int p2;
#endif
if (wp == NULL)
@@ -2642,18 +2643,15 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0);
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
w = wp->window;
if (input_split(ictx) != 0)
return (0);
p1 = input_get(ictx, 0, 0, 0);
if (p1 == -1)
p1 = 0;
p2 = input_get(ictx, 1, 0, 0);
if (p2 == -1)
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)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
}
@@ -3196,13 +3194,14 @@ input_osc_112(struct input_ctx *ictx, const char *p)
/* Parse the OSC 133 D exit status. */
static int
input_osc_133_exit_status(const char *p)
input_osc_133_exit_status(const char *p, int *present)
{
const char *end;
char *copy;
const char *errstr;
char *endptr;
long long status;
*present = 0;
if (p[1] != ';' || p[2] == '\0' || strchr(p + 2, '=') == p + 2)
return (0);
end = strchr(p + 2, ';');
@@ -3216,9 +3215,11 @@ input_osc_133_exit_status(const char *p)
free(copy);
return (0);
}
status = strtonum(copy, 0, 255, &errstr);
*present = 1;
errno = 0;
status = (int)strtol(copy, &endptr, 10);
free(copy);
if (errstr != NULL)
if (errno != 0 || endptr == copy || status < 0 || status > 255)
return (255);
return (status);
}
@@ -3263,6 +3264,22 @@ input_fire_command_event(struct window_pane *wp, const char *name)
events_fire(name, ep);
}
/* Check if an OSC 133 prompt is secondary or a continuation. */
static int
input_osc_133_secondary_prompt(const char *p)
{
const char *cp;
while ((cp = strstr(p, ";k=")) != NULL) {
cp += 3;
if ((*cp == 's' || *cp == 'c') &&
(cp[1] == '\0' || cp[1] == ';'))
return (1);
p = cp;
}
return (0);
}
/* Handle the OSC 133 sequence. */
static void
input_osc_133(struct input_ctx *ictx, const char *p)
@@ -3272,8 +3289,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
struct grid *gd = s->grid;
u_int line = s->cy + gd->hsize;
struct grid_line *gl = NULL;
const char *cp;
int status;
int status, status_present;
if (line < gd->hsize + gd->sy)
gl = grid_get_line(gd, line);
@@ -3282,9 +3298,17 @@ input_osc_133(struct input_ctx *ictx, const char *p)
case 'A':
case 'N':
if (gl != NULL) {
memset(&gl->osc133_data, 0, sizeof gl->osc133_data);
gl->osc133_data.prompt_col = s->cx;
gl->flags |= GRID_LINE_START_PROMPT;
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 %s at %u,%u", __func__,
input_osc_133_secondary_prompt(p) ? "secondary prompt" :
"prompt", s->cx, line);
}
}
if (wp != NULL) {
wp->last_prompt_time = time(NULL);
@@ -3293,25 +3317,33 @@ input_osc_133(struct input_ctx *ictx, const char *p)
break;
case 'P':
if (gl != NULL) {
cp = strstr(p, ";k=s");
if (cp != NULL && (cp[4] == ';' || cp[4] == '\0'))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
gl->osc133_data.prompt_col = s->cx;
if (!(gl->flags & (GRID_LINE_START_PROMPT|
GRID_LINE_SECOND_PROMPT))) {
gl->osc133_data.prompt_col = s->cx;
if (input_osc_133_secondary_prompt(p))
gl->flags |= GRID_LINE_SECOND_PROMPT;
else
gl->flags |= GRID_LINE_START_PROMPT;
log_debug("%s: osc133 prompt at %u,%u", __func__, s->cx,
line);
}
}
break;
case 'B':
case 'I':
if (gl != NULL) {
if (gl != NULL && !(gl->flags & GRID_LINE_START_COMMAND)) {
gl->flags |= GRID_LINE_START_COMMAND;
gl->osc133_data.cmd_col = s->cx;
log_debug("%s: osc133 command at %u,%u", __func__, s->cx,
line);
}
break;
case 'C':
if (gl != NULL) {
if (gl != NULL && !(gl->flags & GRID_LINE_START_OUTPUT)) {
gl->flags |= GRID_LINE_START_OUTPUT;
gl->osc133_data.out_start_col = s->cx;
log_debug("%s: osc133 output start at %u,%u", __func__, s->cx,
line);
}
if (wp != NULL) {
wp->cmd_start_time = time(NULL);
@@ -3322,7 +3354,7 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
break;
case 'D':
status = input_osc_133_exit_status(p);
status = input_osc_133_exit_status(p, &status_present);
if (wp != NULL) {
wp->cmd_end_time = time(NULL);
wp->flags &= ~PANE_CMDRUNNING;
@@ -3331,8 +3363,12 @@ input_osc_133(struct input_ctx *ictx, const char *p)
}
if (gl != NULL) {
gl->flags |= GRID_LINE_END_OUTPUT;
if (status_present)
gl->flags |= GRID_LINE_END_OUTPUT_STATUS;
gl->osc133_data.out_end_col = s->cx;
gl->osc133_data.exit_status = status;
log_debug("%s: osc133 output end at %u,%u (status %d)",
__func__, s->cx, line, status);
}
break;
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: key-bindings.c,v 1.190 2026/07/22 19:23:59 nicm Exp $ */
/* $OpenBSD: key-bindings.c,v 1.192 2026/08/06 09:05:04 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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 Bottom,}' '>' {send -X history-bottom}" \
" ''" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Line Numbers Off,Line Numbers On},}' 'L' {send -X line-numbers-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?copy_fold_view,Fold View Off,Fold View On},}' 'O' {send -X fold-view-toggle}" \
" '#{?#{==:#{pane_mode},copy-mode},Edit,}' 'e' {if -F '#{selection_present}' {send -X open-selection} {send -X open-output}}" \
" '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \
" ''" \
" '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \
@@ -421,6 +423,7 @@ key_bindings_init(void)
"bind -N 'Spread panes out evenly' E { select-layout -E }",
"bind -N 'Switch to the last client' L { switch-client -l }",
"bind -N 'Clear the marked pane' M { select-pane -M }",
"bind -N 'Change the pane title' T { command-prompt -I'#T' { select-pane -T '%%' } }",
"bind -N 'Enter copy mode' [ { copy-mode }",
"bind -N 'Paste the most recent paste buffer' ] { paste-buffer -p }",
"bind -N 'Create a new window' c { new-window }",
@@ -574,6 +577,7 @@ key_bindings_init(void)
"bind -Tcopy-mode C-l { send -X recentre-top-bottom }",
"bind -Tcopy-mode M-l { send -X cursor-centre-horizontal }",
"bind -Tcopy-mode C-n { send -X cursor-down }",
"bind -Tcopy-mode C-o { send -X select-output }",
"bind -Tcopy-mode C-p { send -X cursor-up }",
"bind -Tcopy-mode C-r { command-prompt -P -T search -ip'(search up)' -I'#{pane_search_string}' { send -X search-backward-incremental -- '%%' } }",
"bind -Tcopy-mode C-s { command-prompt -P -T search -ip'(search down)' -I'#{pane_search_string}' { send -X search-forward-incremental -- '%%' } }",
@@ -582,24 +586,28 @@ key_bindings_init(void)
"bind -Tcopy-mode Escape { send -X cancel }",
"bind -Tcopy-mode C-[ { send -X cancel }",
"bind -Tcopy-mode Space { send -X page-down }",
"bind -Tcopy-mode Tab { send -X toggle-output }",
"bind -Tcopy-mode BTab { send -X toggle-output -a }",
"bind -Tcopy-mode , { send -X jump-reverse }",
"bind -Tcopy-mode \\; { send -X jump-again }",
"bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }",
"bind -Tcopy-mode L { send -X line-numbers-toggle }",
"bind -Tcopy-mode N { send -X search-reverse }",
"bind -Tcopy-mode O { send -X fold-view-toggle }",
"bind -Tcopy-mode P { send -X toggle-position }",
"bind -Tcopy-mode R { send -X rectangle-toggle }",
"bind -Tcopy-mode T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode X { send -X set-mark }",
"bind -Tcopy-mode e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }",
"bind -Tcopy-mode f { command-prompt -P -1p'(jump forward)' { send -X jump-forward -- '%%' } }",
"bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }",
"bind -Tcopy-mode n { send -X search-again }",
"bind -Tcopy-mode q { send -X cancel }",
"bind -Tcopy-mode r { send -X refresh-toggle }",
"bind -Tcopy-mode r { send -X refresh-now }",
"bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }",
"bind -Tcopy-mode Home { send -X start-of-line }",
"bind -Tcopy-mode End { send -X end-of-line }",
"bind -Tcopy-mode MouseDown1Pane select-pane",
"bind -Tcopy-mode MouseDown1Pane { select-pane; send -X toggle-output -m }",
"bind -Tcopy-mode MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode WheelUpPane { select-pane; send -N5 -X scroll-up }",
@@ -659,6 +667,8 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi Escape { send -X clear-selection }",
"bind -Tcopy-mode-vi C-[ { send -X clear-selection }",
"bind -Tcopy-mode-vi Space { send -X begin-selection }",
"bind -Tcopy-mode-vi Tab { send -X toggle-output }",
"bind -Tcopy-mode-vi BTab { send -X toggle-output -a }",
"bind -Tcopy-mode-vi '$' { send -X end-of-line }",
"bind -Tcopy-mode-vi , { send -X jump-reverse }",
"bind -Tcopy-mode-vi / { command-prompt -P -T search -p'(search down)' { send -X search-forward -- '%%' } }",
@@ -686,7 +696,10 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi K { send -X scroll-up }",
"bind -Tcopy-mode-vi L { send -X bottom-line }",
"bind -Tcopy-mode-vi M { send -X middle-line }",
"bind -Tcopy-mode-vi M-e { if -F '#{selection_present}' { send -X open-selection } { send -X open-output } }",
"bind -Tcopy-mode-vi M-o { send -X select-output }",
"bind -Tcopy-mode-vi N { send -X search-reverse }",
"bind -Tcopy-mode-vi O { send -X fold-view-toggle }",
"bind -Tcopy-mode-vi P { send -X toggle-position }",
"bind -Tcopy-mode-vi T { command-prompt -P -1p'(jump to backward)' { send -X jump-to-backward -- '%%' } }",
"bind -Tcopy-mode-vi V { send -X select-line }",
@@ -705,7 +718,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi n { send -X search-again }",
"bind -Tcopy-mode-vi o { send -X other-end }",
"bind -Tcopy-mode-vi q { send -X cancel }",
"bind -Tcopy-mode-vi r { send -X refresh-toggle }",
"bind -Tcopy-mode-vi r { send -X refresh-now }",
"bind -Tcopy-mode-vi t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }",
"bind -Tcopy-mode-vi v { send -X rectangle-toggle }",
"bind -Tcopy-mode-vi w { send -X next-word }",
@@ -714,7 +727,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi % { send -X next-matching-bracket }",
"bind -Tcopy-mode-vi Home { send -X start-of-line }",
"bind -Tcopy-mode-vi End { send -X end-of-line }",
"bind -Tcopy-mode-vi MouseDown1Pane { select-pane }",
"bind -Tcopy-mode-vi MouseDown1Pane { select-pane; send -X toggle-output -m }",
"bind -Tcopy-mode-vi MouseDrag1Pane { select-pane; send -X begin-selection }",
"bind -Tcopy-mode-vi MouseDragEnd1Pane { send -X copy-pipe-and-cancel }",
"bind -Tcopy-mode-vi WheelUpPane { select-pane; send -N5 -X scroll-up }",

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.96 2026/07/15 13:02:33 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>
@@ -539,6 +539,10 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
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) {
/* Space available in this cell only. */
if (type == LAYOUT_LEFTRIGHT) {
@@ -568,6 +572,9 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
/* Different type: minimum of available space in child cells. */
minimum = UINT_MAX;
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);
if (available < minimum)
minimum = available;
@@ -661,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. */
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;
@@ -695,9 +702,9 @@ layout_cell_get_neighbour(struct layout_cell *lc)
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells))
direction = !direction;
lcother = layout_cell_get_neighbour_direction(lc, direction);
lcother = layout_cell_get_neighbour_dir(lc, direction);
if (lcother == NULL)
lcother = layout_cell_get_neighbour_direction(lc, !direction);
lcother = layout_cell_get_neighbour_dir(lc, !direction);
return (lcother);
}
@@ -860,7 +867,7 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type,
size = lc->g.sx;
else
size = lc->g.sy;
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells))
if (layout_cell_is_last_tiled(lc))
change = size - new_size;
else
change = new_size - size;
@@ -985,11 +992,11 @@ layout_resize_pane(struct window_pane *wp, enum layout_type type, int change,
if (lcparent == NULL)
return;
/* If this is the last cell, move back one. */
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) {
do
lc = TAILQ_PREV(lc, layout_cells, entry);
while (lc->flags & LAYOUT_CELL_FLOATING);
/* If this is the last tiled cell, move back one. */
if (layout_cell_is_last_tiled(lc)) {
lc = layout_cell_get_neighbour_dir(lc, 0);
if (lc == NULL)
return;
}
layout_resize_layout(wp->window, lc, type, change, opposite);
@@ -1007,22 +1014,22 @@ layout_resize_pane_grow(struct window *w, struct layout_cell *lc,
lcadd = lc;
/* 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) {
size = layout_resize_check(w, lcremove, type);
if (size > 0)
break;
lcremove = TAILQ_NEXT(lcremove, entry);
lcremove = layout_cell_get_neighbour_dir(lcremove, 1);
}
/* If none found, look towards the head. */
if (opposite && lcremove == NULL) {
lcremove = TAILQ_PREV(lc, layout_cells, entry);
lcremove = layout_cell_get_neighbour_dir(lc, 0);
while (lcremove != NULL) {
size = layout_resize_check(w, lcremove, type);
if (size > 0)
break;
lcremove = TAILQ_PREV(lcremove, layout_cells, entry);
lcremove = layout_cell_get_neighbour_dir(lcremove, 0);
}
}
if (lcremove == NULL)
@@ -1050,13 +1057,13 @@ layout_resize_pane_shrink(struct window *w, struct layout_cell *lc,
size = layout_resize_check(w, lcremove, type);
if (size != 0)
break;
lcremove = TAILQ_PREV(lcremove, layout_cells, entry);
lcremove = layout_cell_get_neighbour_dir(lcremove, 0);
} while (lcremove != NULL);
if (lcremove == NULL)
return (0);
/* 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)
return (0);
@@ -1650,7 +1657,10 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
if (window_active_pane_is_over_zoom(w))
window_push_zoom(w, 0, 1);
else
window_push_zoom(w, 1, (flags & SPAWN_ZOOM));
lc = layout_split_pane(wp, type, size, flags);
if (lc == NULL)
*cause = xstrdup("no space for a new pane");
@@ -1677,8 +1687,10 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
if (flags & SPAWN_MODAL)
window_push_modal_zoom(w);
if (flags & SPAWN_FLOATOVERZOOM)
window_push_zoom(wp->window, 0, 1);
else if (window_active_pane_is_over_zoom(w))
window_push_zoom(wp->window, 0, 1);
else
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
lcnew = layout_floating_pane(w, wp, &fg);

6
menu.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: menu.c,v 1.69 2026/07/14 19:07:03 nicm Exp $ */
/* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -337,6 +337,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
struct cmdq_state *state;
enum cmd_parse_status status;
char *error;
key_code key;
if (KEYC_IS_MOUSE(event->key)) {
/*
@@ -387,7 +388,8 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
name = menu->items[i].name;
if (name == NULL || *name == '-')
continue;
if ((event->key & ~KEYC_MASK_FLAGS) == menu->items[i].key) {
key = (event->key & ~KEYC_MASK_FLAGS);
if (key == (menu->items[i].key & ~KEYC_MASK_FLAGS)) {
md->choice = i;
goto chosen;
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: mode-tree.c,v 1.100 2026/07/14 19:07:03 nicm Exp $ */
/* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -47,7 +47,7 @@ enum mode_tree_preview {
"#[acs]x" MODE_TREE_PREFIX_STYLE " }," \
"#{mode_tree_repeat}}" \
"#{?mode_tree_branch," \
"#[acs]#{?mode_tree_last,mq,tq}" MODE_TREE_PREFIX_STYLE "> ,}" \
"#[acs]#{?mode_tree_last,mq,tq}+" MODE_TREE_PREFIX_STYLE " ,}" \
"#{?mode_tree_has_children," \
"#{?mode_tree_expanded,#[fg=themered]-" MODE_TREE_PREFIX_STYLE " ," \
"#[fg=themegreen]+" MODE_TREE_PREFIX_STYLE " }," \

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

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>
@@ -79,7 +79,8 @@ popup_free(struct popup_data *pd)
if (pd->job != NULL)
job_free(pd->job);
input_free(pd->ictx);
if (pd->ictx != NULL)
input_free(pd->ictx);
free(pd->r.ranges);
screen_free(&pd->s);

6
proc.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */
/* $OpenBSD: proc.c,v 1.32 2026/08/04 13:16:03 claudio Exp $ */
/*
* Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -79,7 +79,7 @@ static void
proc_event_cb(__unused int fd, short events, void *arg)
{
struct tmuxpeer *peer = arg;
ssize_t n;
int n;
struct imsg imsg;
if (!(peer->flags & PEER_BAD) && (events & EV_READ)) {
@@ -88,7 +88,7 @@ proc_event_cb(__unused int fd, short events, void *arg)
return;
}
for (;;) {
if ((n = imsg_get(&peer->ibuf, &imsg)) == -1) {
if ((n = imsgbuf_get(&peer->ibuf, &imsg)) == -1) {
peer->dispatchcb(NULL, peer->arg);
return;
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */
/* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1334,7 +1334,7 @@ process_key:
if (pr->index != size) {
memmove(pr->buffer + pr->index,
pr->buffer + pr->index + 1,
(size + 1 - pr->index) *
(size - pr->index) *
sizeof *pr->buffer);
goto changed;
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
#!/bin/sh
# A control client whose terminal has stopped accepting output (for example
# its ssh connection or terminal emulator died) never drains the output that
# server_client_check_exit waits on before sending MSG_EXIT, and may then
# never close its socket either. This must not prevent the server from
# exiting: after kill-server the server previously stayed alive forever with
# server_exit set, closing every new connection immediately, so every new
# client failed with "server exited unexpectedly" until the server was killed
# by hand. The server now bounds the exit handshake: it discards output it
# can never deliver and drops the client if it still does not close.
#
# The stalled terminal is simulated by a fifo whose read end is held open but
# never read.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)
FIFO=$DIR/fifo
SERVER=
mkfifo "$FIFO" || exit 1
cleanup() {
[ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null
$TMUX kill-server 2>/dev/null
exec 8<&- 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
# A detached session whose pane floods printable output forever.
$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1
SERVER=$($TMUX display -pt rt '#{pid}')
# Attach a control client with output down the fifo; stdin held open by sleep
# so it stays attached. The fifo fills and is never read, so a backlog forms
# in the server that can never be delivered.
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 &
exec 8<"$FIFO"
n=0
while [ $n -lt 50 ]; do
$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break
sleep 0.1
n=$((n + 1))
done
$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . ||
{ echo "control client did not attach"; exit 1; }
# Let the pane flood fill the fifo so the client's output is stuck.
sleep 2
# Ask the server to exit. This sets CLIENT_EXIT on the stalled client, whose
# output can never drain.
$TMUX kill-server 2>/dev/null
# The server must exit within the exit timeout (10 seconds) plus slack,
# rather than staying in limbo forever rejecting new clients.
n=0
while [ $n -lt 100 ]; do
if ! kill -0 "$SERVER" 2>/dev/null; then
SERVER=
exit 0
fi
sleep 0.2
n=$((n + 1))
done
echo "server did not exit after kill-server; new clients see:"
$TMUX ls 2>&1
exit 1

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

View File

@@ -0,0 +1,158 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMPDIR=$(mktemp -d)
IN="$TMPDIR/in"
OUT="$TMPDIR/out"
BADCFG="$TMPDIR/bad.conf"
PID=
cleanup()
{
exec 3>&- 2>/dev/null
[ -n "$PID" ] && kill "$PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
[ -s "$OUT" ] && cat "$OUT" >&2
exit 1
}
wait_for()
{
pattern=$1
timeout=${2:-60}
i=0
while [ "$i" -lt "$timeout" ]; do
grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0
sleep 0.1
i=$((i + 1))
done
fail "missing: $pattern"
}
wait_for_count()
{
pattern=$1
expected=$2
timeout=${3:-60}
i=0
while [ "$i" -lt "$timeout" ]; do
count=$(grep -F -c -- "$pattern" "$OUT" 2>/dev/null)
[ "$count" -ge "$expected" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "missing count $expected for: $pattern"
}
send()
{
printf '%s\n' "$1" >&3 || fail "failed to send: $1"
}
# A notification is a standalone protocol message. It must never be part of
# the response between the real %begin and matching %end or %error for a
# command. Ignore guard-looking command output while a real guard is open.
check_guards()
{
awk '
function notification(line) {
return line ~ /^%(sessions-changed|session-changed |unlinked-window-add )/ ||
line ~ /^%(pause |continue |config-error )/
}
!open && $0 ~ /^%begin [0-9]+ [0-9]+ [0-9]+$/ {
open = 1
time = $2
number = $3
flags = $4
next
}
open && ($0 == "%end " time " " number " " flags ||
$0 == "%error " time " " number " " flags) {
open = 0
next
}
notification($0) {
seen[$1]++
if (open) {
print "notification inside guard: " $0 > "/dev/stderr"
bad = 1
}
}
END {
if (open) {
print "unterminated command guard" > "/dev/stderr"
bad = 1
}
if (seen["%sessions-changed"] < 2 ||
seen["%session-changed"] < 1 ||
seen["%unlinked-window-add"] < 2 ||
seen["%pause"] < 1 || seen["%continue"] < 1 ||
seen["%config-error"] < 1) {
print "missing expected notification" > "/dev/stderr"
bad = 1
}
exit bad
}' "$OUT" || fail "bad control protocol notification ordering"
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s guard -x 80 -y 24 || exit 1
pane=$($TMUX display-message -p -t guard:0.0 '#{pane_id}') || exit 1
mkfifo "$IN" || exit 1
: >"$OUT"
$TMUX -C attach-session -t guard <"$IN" >"$OUT" 2>&1 &
PID=$!
exec 3>"$IN"
# Attaching generates a session notification synchronously from inside the
# attach command. It must follow the closing guard.
wait_for '%session-changed '
# Guard-looking output is arbitrary command output, not protocol state. Make
# new-session both generate notifications and print an unmatched fake %begin;
# it must not leave those notifications permanently deferred.
send "set-option -g @fake-begin '%begin 1 2 3'"
send "new-session -d -P -F '#{@fake-begin}' -s fake-begin"
wait_for_count '%sessions-changed' 1
# Likewise, fake %end output from a command that generates multiple
# notifications must not flush them before the command actually ends.
send "set-option -g @fake-end '%end 1 2 3'"
send "new-session -d -P -F '#{@fake-end}' -s fake-end"
wait_for_count '%sessions-changed' 2
# Exercise notification paths outside control-notify.c.
send "refresh-client -A '$pane:pause'"
wait_for "%pause $pane"
send "refresh-client -A '$pane:continue'"
wait_for "%continue $pane"
printf '%s\n' 'not-a-command' >"$BADCFG"
send "source-file '$BADCFG'"
wait_for '%config-error '
# A final command guarantees all earlier command guards have completed before
# the transcript is checked.
send "display-message -p 'guard-check-done'"
wait_for 'guard-check-done'
check_guards
exit 0

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

@@ -117,6 +117,7 @@ $TMUX send-keys -X copy-selection
# Test that vi cursor movement does not stop on the padding cell of a wide
# character at the end of a line.
$TMUX kill-server 2>/dev/null
sleep 1
$TMUX new -d -x20 -y5 \
"printf 'abc中\nxyz\n'; exec cat" || exit 1
$TMUX set-window-option -g mode-keys vi

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

@@ -199,5 +199,74 @@ must_fail $TMUX resize-pane -t "$id" -x 0
must_fail $TMUX resize-pane -t "$id" -y 0
$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
exit 0

View File

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

View File

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

View File

@@ -1,99 +0,0 @@
#!/bin/sh
# Grid-resident SIXEL image markers: history, copy, overwrite and fallback.
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)
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 '\033Pq\"1;1;8;16#0;2;100;100;100#0~~~~~~~~\044-~~~~~~~~\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 'Pq' $TMP >/dev/null && exit 1
# Copy mode duplicates the backing grid, including image marker references.
$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 overwrites marker cells through the normal grid write path.
$TMUX new-window -d "
printf '\033Pq\"1;1;8;16#0;2;100;100;100#0~~~~~~~~\044-~~~~~~~~\033\\'
printf '\033[HXY'
sleep 10"
sleep 1
[ "$($TMUX capture-pane -pt:1 -S0 -E0)" = "XY" ] || 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.
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 refresh-client -R || exit 1
sleep 1
grep -a '"1;1;26;26' $TMP >/dev/null || exit 1
# Selection redraws must leave text image cells visible.
$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
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# Image marker rows remain cell-aligned when a narrower terminal causes text
# reflow. The second image cell is clipped, not moved to the following row.
$TMUX2 send-keys -X cancel || exit 1
$TMUX2 new-window -d "
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:1 || exit 1
sleep 1
$TMUX resize-window -x 1 -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
exit 0

View File

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

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

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

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

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

View File

@@ -105,6 +105,24 @@ drag()
sleep 1
}
meta_drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
check_ok new-session -d -s modal -x 80 -y 24 'cat'
@@ -127,12 +145,16 @@ case "$(fmt "$modal" '#{pane_flags}')" in
*O*) ;;
*) fail "modal pane flags do not include O" ;;
esac
case "$(fmt "$modal" '#{pane_flags}')" in
*A*) ;;
*) fail "modal pane flags do not include A" ;;
esac
case "$(fmt modal:0 '#{window_flags}')" in
*O*) ;;
*) fail "modal window flags do not include O" ;;
esac
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
check_fail "window already has a modal pane" \
new-pane -O -x 10 -y 4 'cat'
@@ -159,12 +181,14 @@ under=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') ||
sleep 1
must_equal "$(fmt modal:0 '#{pane_id}')" "$modal"
must_equal "$(fmt "$under" '#{pane_active}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
float=$($TMUX new-pane -PF '#{pane_id}' -x 10 -y 4 -X 5 -Y 3 'cat') ||
fail "new floating pane under modal failed"
sleep 1
must_equal "$(fmt modal:0 '#{pane_id}')" "$modal"
must_equal "$(fmt "$float" '#{pane_active}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
check_ok new-window -d -t modal: -n other 'cat'
check_ok select-window -t modal:other
@@ -303,6 +327,164 @@ check_ok kill-pane -t "$modal"
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
must_equal "$(fmt modal:0 '#{pane_id}')" "$p0"
must_equal "$(fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 0:0
ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') ||
fail "new-pane -K without -O failed"
check_ok kill-pane -t "$ignored"
$TMUX set -g @modal-prefix no
$TMUX set -g @modal-root no
$TMUX bind -n z set -g @modal-root yes
modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -OK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
must_equal "$($TMUX show -gv @modal-prefix)" no
must_equal "$($TMUX show -gv @modal-root)" no
case "$($TMUX capture-pane -pt "$modal")" in
*xz*) ;;
*) fail "keys did not reach key-capturing modal pane" ;;
esac
left=$(fmt "$modal" '#{pane_left}')
top=$(fmt "$modal" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$modal" '#{pane_left}')
new_top=$(fmt "$modal" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "key-capturing modal pane did not move"
check_ok kill-pane -t "$modal"
sleep 1
# A nonmodal floating pane may remain above zoom, and switching between it and
# the zoomed tiled pane must not unzoom the window.
check_ok new-window -d -t modal: -n float-over-zoom 'cat'
base=$(fmt modal:float-over-zoom '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
check_ok resize-pane -Z -t "$base"
over=$($TMUX new-pane -APF '#{pane_id}' -t "$base" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -A failed"
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1
case "$(fmt "$over" '#{pane_flags}')" in
*A*) ;;
*) fail "float-over-zoom pane flags do not include A" ;;
esac
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok select-pane -t "$base"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok select-pane -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
client=$($TMUX list-clients -F '#{client_name}' | head -1)
[ -n "$client" ] || fail "no client for switch-client test"
check_ok switch-client -c "$client" -t "$base"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok switch-client -c "$client" -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok kill-pane -t "$over"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok resize-pane -Z -t "$base"
ignored=$($TMUX new-pane -ALdPF '#{pane_id}' -t "$base" 'cat') ||
fail "new-pane -A -L failed"
case "$(fmt "$ignored" '#{pane_flags}')" in
*A*) fail "tiled pane flags include A" ;;
*) ;;
esac
# Existing floating panes are filtered when zoom begins: -A panes remain in the
# visible layout and ordinary floating panes do not.
check_ok new-window -d -t modal: -n existing-over-zoom 'cat'
base=$(fmt modal:existing-over-zoom '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "pre-existing new-pane -A failed"
under=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 15 -y 4 -X 2 -Y 2 'cat') ||
fail "pre-existing ordinary new-pane failed"
check_ok select-window -t modal:existing-over-zoom
check_ok select-pane -t "$base"
check_ok resize-pane -Z -t "$base"
must_equal "$(fmt "$over" '#{pane_floating_flag}')" 1
must_equal "$(fmt "$under" '#{pane_floating_flag}')" 0
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
# Geometry changed in the visible zoom layout is copied back when unzooming.
check_ok select-pane -t "$over"
left=$(fmt "$over" '#{pane_left}')
top=$(fmt "$over" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$over" '#{pane_left}')
new_top=$(fmt "$over" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "float-over-zoom pane did not move"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok resize-pane -Z -t "$base"
must_equal "$(fmt "$over" '#{pane_left}:#{pane_top}')" \
"$new_left:$new_top"
must_equal "$(fmt "$under" '#{pane_floating_flag}')" 1
# Resizing with the over-zoom pane active restores the original zoom target.
check_ok resize-pane -Z -t "$base"
check_ok select-pane -t "$over"
check_ok resize-window -t modal:existing-over-zoom -x 90 -y 30
must_equal "$(fmt "$base" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}')" \
90x30:1:1
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1
# Natural pane exit uses a different removal path from kill-pane and must also
# preserve zoom.
dying=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 12 -y 4 -X 4 -Y 3 'true') ||
fail "short-lived new-pane -A failed"
i=0
while $TMUX list-panes -a -F '#{pane_id}' | grep -qx "$dying"; do
i=$((i + 1))
[ $i -gt 50 ] && fail "short-lived float-over-zoom pane did not exit"
sleep 0.1
done
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
# A pane with -A is also above a zoom target which was itself floating. The
# temporary tiled target must sit behind retained floating panes, then return
# to the normal floating z order when unzoomed.
check_ok new-window -d -t modal: -n floating-zoom-target 'cat'
base=$(fmt modal:floating-zoom-target '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
target=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 30 -y 10 -X 10 -Y 5 'cat') ||
fail "floating zoom target creation failed"
over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 15 -y 5 -X 15 -Y 8 'cat') ||
fail "float-over-zoom pane creation failed"
check_ok select-window -t modal:floating-zoom-target
check_ok select-pane -t "$target"
must_equal "$(fmt "$target" '#{pane_z}')" 0
must_equal "$(fmt "$over" '#{pane_z}')" 1
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$target" \
'#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_floating_flag}:#{pane_z}')" \
1:1:0:2
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0
check_ok select-pane -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$over" '#{pane_active}:#{pane_z}')" 1:0
must_equal "$(fmt "$target" '#{pane_floating_flag}:#{pane_z}')" 1:1
# If the target remains active, it returns to the front on unzoom.
check_ok select-pane -t "$target"
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$target" '#{pane_active}:#{pane_z}')" 1:0
must_equal "$(fmt "$over" '#{pane_z}')" 1
cleanup
exit 0

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

View File

@@ -269,6 +269,22 @@ test_customize_mode()
assert_alive "customize-mode exit"
}
test_customize_break_pane()
{
start_client option-break
side=$($TMUX split-window -d -P -F '#{pane_id}' \
-t option-break:0 'cat') || fail "customize split failed"
$TMUX customize-mode -t "$side" || fail "customize-mode failed"
wait_mode "$side" 1
$TMUX break-pane -d -s "$side" || fail "customize break-pane failed"
assert_alive "customize-mode break-pane"
wait_mode "$side" 1
$TMUX send-keys -t "$side" q || fail "customize-mode quit failed"
wait_mode "$side" 0
}
test_copy_mode()
{
start_client copy-a 'i=0; while [ $i -lt 200 ]; do echo "copy mutation line $i"; i=$((i + 1)); done; cat'
@@ -302,6 +318,7 @@ test_choose_tree
test_choose_buffer
test_choose_client
test_customize_mode
test_customize_break_pane
test_copy_mode
cleanup
exit 0

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

@@ -19,7 +19,10 @@
# - respawn-pane/respawn-window refusing a live pane without -k, working on a
# dead pane (remain-on-exit) and killing with -k;
# - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z
# zoom/unzoom (including implicit unzoom on split).
# zoom/unzoom, layout round trips, zoom target selection, pane removal,
# window resizing and implicit unzoom on layout changes;
# - swap-pane preserving one or two zoomed windows, including cross-window
# swaps of hidden panes and zoom targets.
#
# window-ops.sh covers window-level commands and buffers.sh paste buffers.
@@ -144,7 +147,8 @@ check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# break-pane and join-pane.
# break-pane moves a pane to a new window; -P -F prints where it went and -n
# names the new window.
# names the new window. Moving a pane out of a zoomed window unzooms it first.
check_ok resize-pane -Z -t "$p0"
out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \
-s "$p1" -t P:)
if [ "$out" != "1:$p1" ]; then
@@ -153,10 +157,14 @@ if [ "$out" != "1:$p1" ]; then
fi
check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1'
check_fmt 'P:0' '#{window_panes}' '3'
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
# join-pane -v moves it back (the source window, left empty, is destroyed).
# join-pane -v moves it back (the source window, left empty, is destroyed) and
# also unzooms the destination before changing its layout.
check_ok resize-pane -Z -t "$p0"
check_ok join-pane -d -v -s P:broken.0 -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
if $TMUX has-session -t P:broken 2>/dev/null; then
echo "Window 'broken' still exists after join-pane."
exit 1
@@ -192,13 +200,13 @@ check_fail "invalid window name: $(printf 'a\377b')" \
break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P:
# join-pane can move a pane from one window to another without destroying
# the source window if other panes remain. (On this branch move-pane is
# reserved for floating panes, covered by floating-pane-geometry.sh.)
# the source window if other panes remain. move-pane does the same when no
# floating-pane movement flags are given.
check_ok new-window -d -t P:5 -n other
check_ok join-pane -d -s "$p1" -t P:5.0
check_fmt 'P:5' '#{window_panes}' '2'
check_fmt 'P:0' '#{window_panes}' '3'
check_ok join-pane -d -v -s "$p1" -t "$p2"
check_ok move-pane -d -v -s "$p1" -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:5' '#{window_panes}' '1'
@@ -299,6 +307,122 @@ check_fmt 'P:0' '#{window_panes}' '5'
p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}')
check_ok kill-pane -t "$p6"
# Zoom and unzoom preserve the exact tiled layout. Selecting another pane
# without -Z unzooms, while -Z transfers zoom to the selected pane.
layout=$($TMUX display-message -p -t P:0 '#{window_layout}')
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_fmt P:0 '#{window_layout}' "$layout"
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -Z -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_fmt P:0 '#{window_layout}' "$layout"
# Directional selection temporarily restores the full layout to find its
# neighbour, then follows the same unzoom or -Z transfer rules.
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok select-pane -D -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -D -Z -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_fmt P:0 '#{window_layout}' "$layout"
# The last-pane path has separate zoom handling, both with and without -Z.
check_ok select-pane -t "$p0"
check_ok select-pane -t "$p2"
check_ok resize-pane -Z -t "$p2"
check_ok select-pane -l -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'0:0:1'
check_ok select-pane -t "$p0"
check_ok select-pane -t "$p2"
check_ok resize-pane -Z -t "$p2"
check_ok select-pane -l -Z -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p0"
check_fmt P:0 '#{window_layout}' "$layout"
# Killing either a hidden ordinary pane or the zoom target unzooms.
check_ok new-window -d -t P:12 -n zoom-kill 'cat'
zk0=$($TMUX display-message -p -t P:12.0 '#{pane_id}')
zk1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat')
zk2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat')
check_ok resize-pane -Z -t "$zk0"
check_ok kill-pane -t "$zk1"
check_fmt "$zk0" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'2:0:0'
check_ok resize-pane -Z -t "$zk0"
check_ok kill-pane -t "$zk0"
check_fmt "$zk2" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'1:0:0'
check_ok kill-window -t P:12
# The same cases through natural process exit exercise server_destroy_pane.
check_ok new-window -d -t P:13 -n zoom-exit 'cat'
ze0=$($TMUX display-message -p -t P:13.0 '#{pane_id}')
ze1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat')
ze2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat')
check_ok resize-pane -Z -t "$ze0"
check_ok send-keys -t "$ze1" C-d
i=0
while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 2 ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Hidden pane did not exit."; exit 1; }
sleep 0.1
done
check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
check_ok resize-pane -Z -t "$ze2"
check_ok send-keys -t "$ze2" C-d
i=0
while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 1 ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Zoomed pane did not exit."; exit 1; }
sleep 0.1
done
check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
check_ok kill-window -t P:13
# resize-window restores both tiled and floating zoom targets after rebuilding
# the layout at the new size.
check_ok new-window -d -t P:14 -n zoom-resize 'cat'
zr0=$($TMUX display-message -p -t P:14.0 '#{pane_id}')
check_ok split-window -d -t P:14.0 'cat'
check_ok resize-pane -Z -t "$zr0"
check_ok resize-window -t P:14 -x 90 -y 30
check_fmt "$zr0" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'90x30:1:1'
check_ok resize-pane -Z -t "$zr0"
zrf=$($TMUX new-pane -dP -F '#{pane_id}' -t P:14 -x 20 -y 6 'cat')
zrf_size=$($TMUX display-message -p -t "$zrf" \
'#{pane_width}x#{pane_height}')
check_ok resize-pane -Z -t "$zrf"
check_ok resize-window -t P:14 -x 100 -y 32
check_fmt "$zrf" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'100x32:1:1'
check_ok resize-pane -Z -t "$zrf"
check_fmt "$zrf" '#{pane_floating_flag}:#{pane_width}x#{pane_height}' \
"1:$zrf_size"
check_ok kill-window -t P:14
# ---------------------------------------------------------------------------
# kill-pane.
@@ -307,8 +431,10 @@ check_ok kill-pane -t "$p3"
check_panes P:0 "0:$p0 1:$p2 2:$p1"
# -a kills every pane except the target.
check_ok resize-pane -Z -t "$p0"
check_ok kill-pane -a -t "$p0"
check_panes P:0 "0:$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
# Killing the last pane in a window kills the window.
check_ok new-window -d -t P:7 -n goner
@@ -516,6 +642,10 @@ check_panes P:3 "0:$r0 1:$r1 2:$r2"
# Swapping a pane with itself quietly does nothing.
check_ok swap-pane -d -s "$r1" -t "$r1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_ok resize-pane -Z -t "$r1"
check_ok swap-pane -d -Z -s "$r1" -t "$r1"
check_fmt "$r1" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check_ok resize-pane -Z -t "$r1"
# Panes can be swapped between different windows.
check_ok swap-pane -d -s "$o0" -t "$r1"
@@ -525,10 +655,38 @@ check_ok swap-pane -d -s "$r1" -t "$o0"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# With both windows zoomed, -Z preserves each window's zoom when hidden panes
# are exchanged and when the zoom targets themselves are exchanged.
o1=$($TMUX split-window -dP -F '#{pane_id}' -t P:5.0)
check_ok resize-pane -Z -t "$r0"
check_ok resize-pane -Z -t "$o0"
check_ok swap-pane -d -Z -s "$r1" -t "$o1"
check_fmt "$r0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1'
check_fmt "$o0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1'
check_fmt "$r1" '#{window_name}' 'other'
check_fmt "$o1" '#{window_name}' 'swaps'
check_ok swap-pane -d -Z -s "$r0" -t "$o0"
check_fmt "$o0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1'
check_fmt "$r0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1'
check_ok resize-pane -Z -t "$o0"
check_ok resize-pane -Z -t "$r0"
# Restore the original panes before the remaining swap tests.
check_ok swap-pane -d -s "$r0" -t "$o0"
check_ok swap-pane -d -s "$r1" -t "$o1"
check_ok kill-pane -t "$o1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# -Z keeps the window zoomed across the swap.
check_ok resize-pane -Z -t "$r0"
check_ok swap-pane -d -Z -s "$r0" -t "$r1"
check_fmt 'P:3' '#{window_zoomed_flag}' '1'
check_fmt "$r0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check_ok resize-pane -Z -t P:3
check_panes P:3 "0:$r1 1:$r0 2:$r2"

View File

@@ -35,9 +35,9 @@ check_alive()
# A pipe-pane -I child may write after the pane process has exited. With
# remain-on-exit, the pane stays around but its bufferevent has been freed.
check_ok new-session -d -s pipe -x 80 -y 24 'sleep 0.2'
check_ok new-session -d -s pipe -x 80 -y 24 'sleep 1'
check_ok set-option -t pipe:0 remain-on-exit on
check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x'
check_ok pipe-pane -t pipe:0.0 -I 'sleep 2; printf x'
i=0
while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
@@ -46,7 +46,7 @@ while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
sleep 0.1
done
sleep 0.7
sleep 2
check_alive
$TMUX kill-server 2>/dev/null

View File

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

View File

@@ -0,0 +1,87 @@
#!/bin/sh
# respawn-pane frees the pane's buffer and creates an empty one. The pane
# offsets and each control client's offsets used to keep pointing into the old
# buffer, so if an attached control client had not caught up on the pane's
# output, the next read from the new process ran off the end of the new buffer
# and the server crashed in input_parse.
#
# Two ways for a client to be behind are tested: a control client whose output
# goes down a fifo that is never read (a second, healthy control client keeps
# the pane being read), and a healthy control client viewing a session that
# the pane's window has been moved out of.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)
FIFO=$DIR/fifo
SERVER=
mkfifo "$FIFO" || exit 1
cleanup() {
[ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null
$TMUX kill-server 2>/dev/null
exec 8<&- 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
alive() {
if ! kill -0 "$SERVER" 2>/dev/null; then
SERVER=
echo "server died after $1"
exit 1
fi
$TMUX has -t rt || exit 1
}
wait_clients() {
n=0
while [ $n -lt 50 ]; do
[ "$($TMUX lsc 2>/dev/null | wc -l)" -ge $1 ] && return
sleep 0.1
n=$((n + 1))
done
echo "control clients did not attach"; exit 1
}
# A detached session with a pane that writes a line every 10 milliseconds.
$TMUX -f/dev/null new -d -x 80 -y 24 -s rt \
'while :; do echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; sleep 0.01; done' ||
exit 1
SERVER=$($TMUX display -pt rt '#{pid}')
# One control client with output down the unread fifo and one reading
# normally; stdin of both held open by sleep so they stay attached.
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 &
exec 8<"$FIFO"
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >/dev/null 2>&1 &
wait_clients 2
# Let the first client fall behind, then respawn the pane. The new process
# writes at once and the server must survive reading it.
sleep 3
$TMUX respawn-pane -k -t rt:0 'echo respawned; sleep 60' || exit 1
sleep 1
alive "respawn-pane with a lagging control client"
# Now a pane whose window is moved out of the session both clients view: their
# offsets for it stop advancing, so they fall behind until it is respawned.
$TMUX neww -d -t rt \
'while :; do echo yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy; sleep 0.01; done' ||
exit 1
sleep 1
$TMUX new -d -s other || exit 1
$TMUX movew -d -s rt:1 -t other: || exit 1
sleep 2
$TMUX respawn-pane -k -t other:1 'echo respawned; sleep 60' || exit 1
sleep 1
alive "respawn-pane on a window moved out of the clients' session"
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

View File

@@ -0,0 +1,49 @@
#!/bin/sh
# Server socket creation failures must produce a nonzero client exit status.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
# Root bypasses directory mode bits, so it cannot trigger this failure.
[ "$(id -u)" -eq 0 ] && exit 0
TMP=$(mktemp -d) || exit 1
SOCKET="$TMP/ro/socket"
cleanup()
{
chmod 700 "$TMP/ro" 2>/dev/null
rm -rf "$TMP"
}
fail()
{
echo "$1" >&2
cleanup
exit 1
}
mkdir "$TMP/ro" || exit 1
chmod 500 "$TMP/ro" || exit 1
trap cleanup 0 1 15
check_failure()
{
output=$($TEST_TMUX -S "$SOCKET" -f/dev/null "$@" 2>&1)
retval=$?
[ "$retval" -ne 0 ] || fail "tmux $* exited zero: $output"
case "$output" in
"error creating $SOCKET ("*) ;;
*) fail "tmux $* produced unexpected error: $output" ;;
esac
}
check_failure start-server
check_failure new-session -d
exit 0

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

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

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

View File

@@ -0,0 +1,173 @@
#!/bin/sh
# Application synchronized updates must remain one physical client transaction,
# including when BSU, printable cells and ESU arrive in one pane read.
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 -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-sync-output.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"
}
wait_for_marker()
{
i=0
while [ "$i" -lt 50 ]; do
grep -q 'FRAME_000001_ROW_23' "$CLIENT_BYTES" 2>/dev/null && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive both synchronized frames"
}
wait_for_stable_bytes()
{
previous=-1
stable=0
i=0
while [ "$i" -lt 50 ]; do
current=$(wc -c <"$CLIENT_BYTES" 2>/dev/null) || current=0
if [ "$current" -gt 0 ] && [ "$current" -eq "$previous" ]; then
stable=$((stable + 1))
[ "$stable" -eq 5 ] && return 0
else
stable=0
fi
previous=$current
sleep 0.1
i=$((i + 1))
done
fail "client byte stream did not become stable"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
my $control = $ENV{CONTROL};
(my $dir = $control) =~ s{/[^/]+$}{};
open my $ready, '>', "$dir/ready" or die "$dir/ready: $!\n";
close $ready;
while (!-e $control) {
select undef, undef, undef, 0.01;
}
for my $frame (0 .. 1) {
my @rows;
for my $row (0 .. 23) {
my $marker = sprintf "FRAME_%06d_ROW_%02d_", $frame, $row;
push @rows, substr($marker . ('X' x 79), 0, 79);
}
my $frame = "\e[?2026h\e[H" . join("\r\n", @rows) .
"\e[?2026l";
my $written = syswrite STDOUT, $frame;
die "short synchronized frame write\n"
unless defined $written && $written == length $frame;
select undef, undef, undef, 0.2;
}
select undef, undef, undef, 2;
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
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"
wait_for_marker
wait_for_stable_bytes
$OUTER pipe-pane -t outer:0.0 || exit 1
cat >"$ASSERT" <<'PERL'
use strict;
use warnings;
my $path = shift;
open my $fh, '<:raw', $path or die "$path: $!\n";
local $/;
my $bytes = <$fh>;
my ($on, $off) = ("\e[?2026h", "\e[?2026l");
my ($active, %seen);
for (my $i = 0; $i < length($bytes);) {
if (substr($bytes, $i, length($on)) eq $on) {
die "duplicate synchronized-output start at byte $i\n" if $active;
$active = 1;
$i += length($on);
next;
}
if (substr($bytes, $i, length($off)) eq $off) {
die "unmatched synchronized-output end at byte $i\n" unless $active;
$active = 0;
$i += length($off);
next;
}
if (substr($bytes, $i) =~ /\AFRAME_(\d{6})_ROW_(\d{2})_/) {
my $marker = "$1:$2";
die "marker $marker outside synchronized output\n" unless $active;
$seen{$marker}++;
}
$i++;
}
die "unterminated synchronized-output transaction\n" if $active;
for my $frame (0 .. 1) {
for my $row (0 .. 23) {
my $marker = sprintf "%06d:%02d", $frame, $row;
die "missing marker $marker\n" unless $seen{$marker};
}
}
PERL
perl "$ASSERT" "$CLIENT_BYTES" || exit 1

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"

View File

@@ -0,0 +1,43 @@
#!/bin/sh
# When a write breaks up a wide character or a tab, the columns it covered must
# keep the background colour, both in the grid and on the terminal. The second
# server is attached to the first so that what is actually sent is checked, not
# just what tmux has stored.
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
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP" 0 1 15
# 1. Write inside a tab. 2. Overwrite the right half of a wide character.
# 3. Two tabs of different colours, the first partly deleted so that padding
# from both ends up next to each other, then written over.
# The sleep is so that the client is attached before anything is written and
# the changes are sent as they happen rather than as one redraw.
$TMUX2 -f/dev/null new -d -x24 -y3 "
sleep 2
printf '\033[44m\033[K\t\033[4GX'
printf '\033[2;1H\033[44m\033[K\344\270\226\033[2GX'
printf '\033[3;1H\033[42m\033[K\t\033[43m\033[K\t\033[0m\033[4G\033[8P\rx'
cat" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX -f/dev/null new -x24 -y4 -d "$TMUX2 attach" || exit 1
sleep 4
$TMUX capturep -pe -S0 -E2 >$TMP || exit 1
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
printf '\033[44m X\n X\n\033[49mx\033[42m \033[43m \033[49m\n' |
cmp - $TMP || exit 1
exit 0

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

@@ -105,6 +105,21 @@ sleep 1
CLIENT=$($TMUX2 list-clients -F '#{client_name}' | head -1)
[ -n "$CLIENT" ] || fail "no inner client"
# A variation selector which widens the previous character must use window
# coordinates when checking visibility in a pane with a nonzero x offset.
$TMUX2 set -s variation-selector-always-wide on || exit 1
WINDOW=$($TMUX2 neww -dPF '#{window_id}' "exec sleep 100") || exit 1
PANE=$($TMUX2 splitw -dhPF '#{pane_id}' -t "$WINDOW" \
"exec sleep 100") || exit 1
$TMUX2 selectw -t "$WINDOW" || exit 1
$TMUX2 respawnp -k -t "$PANE" \
"printf '\nA\342\234\217\357\270\217B'; exec sleep 100" || exit 1
sleep 1
$TMUX2 capturep -p -t "$PANE" >$TMP || exit 1
EXPECTED=$(printf 'A\342\234\217\357\270\217B')
check_line 2 "$EXPECTED"
$TMUX2 killw -t "$WINDOW" || exit 1
# Long line, then short line: default cells after cellsize must clear stale text.
capture
check_line 1 "abcdefghijklmnopqrst"

View File

@@ -8,7 +8,7 @@
# This exercises:
# - new-window placement: next free index, explicit index, index in use with
# and without -k, -a (after) and -b (before) insertion with shuffling, and
# -S selecting an existing window by name instead of creating;
# -S selecting an existing window by name or target instead of creating;
# - move-window to a free index, to an occupied index with and without -k,
# -a insertion and -r renumbering (including base-index);
# - renumber-windows closing gaps;
@@ -17,7 +17,8 @@
# unlink the last link without -k;
# - swap-window within and between sessions, -d keeping the active window,
# and the grouped-sessions error;
# - rotate-window -U/-D rotating pane positions;
# - rotate-window -U/-D rotating pane positions, with and without preserving
# zoom using -Z;
# - kill-window switching to the last (previously current) window, kill-window
# -a killing all other windows and the "-f only valid with -a" guard.
#
@@ -139,6 +140,36 @@ check_ok new-window -S -t W: -n w3
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '5:w3'
# -S with a target-window that already exists selects it directly, ignoring
# -n (with -d it does not switch).
check_ok select-window -t W:0
check_ok new-window -S -t W:9 -n ignored
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '9:w9k'
check_ok select-window -t W:0
check_ok new-window -S -d -t W:9 -n ignored
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_fmt 'W:' '#{window_index}:#{window_name}' '0:wB'
# -S with a target-window that does not yet exist still creates it normally.
check_ok new-window -S -t W:20 -n w20
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k 20:w20'
check_fmt 'W:' '#{window_index}:#{window_name}' '20:w20'
check_ok kill-window -t W:20
# -S also works with a relative target-window (+N): selects the window at
# that offset if it exists, or creates it there if not.
check_ok select-window -t W:0
check_ok new-window -S -t W:+1
check_fmt 'W:' '#{window_index}:#{window_name}' '1:w0'
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 9:w9k'
check_ok select-window -t W:0
check_ok new-window -S -t W:+6 -n rel6
check_fmt 'W:' '#{window_index}:#{window_name}' '6:rel6'
check_windows W '0:wB 1:w0 2:w1 3:wA 4:w2 5:w3 6:rel6 9:w9k'
check_ok kill-window -t W:6
# Clean up to a known arrangement.
check_ok kill-window -t W:wB
check_ok kill-window -t W:wA
@@ -306,6 +337,24 @@ check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0"
# Rotation without -Z unzooms. With -Z it preserves zoom and transfers it to
# the pane which arrives at the active position.
layout=$($TMUX display-message -p -t R:0 '#{window_layout}')
check_ok resize-pane -Z -t "$p0"
check_ok rotate-window -U -t R:0
check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'0:0:1'
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{window_layout}' "$layout"
check_ok resize-pane -Z -t "$p0"
check_ok rotate-window -U -Z -t R:0
check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{window_layout}' "$layout"
# ---------------------------------------------------------------------------
# kill-window.

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */
/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -42,8 +42,8 @@ resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy)
void
resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
{
u_int old_sx = w->sx, old_sy = w->sy;
int zoomed;
struct window_pane *zwp;
u_int old_sx = w->sx, old_sy = w->sy;
/* Check size limits. */
if (sx < WINDOW_MINIMUM)
@@ -56,8 +56,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
sy = WINDOW_MAXIMUM;
/* If the window is zoomed, unzoom. */
zoomed = w->flags & WINDOW_ZOOMED;
if (zoomed)
zwp = window_zoomed_pane(w);
if (zwp != NULL)
window_unzoom(w, 1);
/* Resize the layout first. */
@@ -73,8 +73,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
sx, sy, w->layout_root->g.sx, w->layout_root->g.sy);
/* Restore the window zoom state. */
if (zoomed)
window_zoom(w->active);
if (zwp != NULL && window_has_pane(w, zwp))
window_zoom(zwp);
tty_update_window_offset(w);
server_redraw_window(w);
@@ -386,12 +386,10 @@ recalculate_size(struct window *w, int now)
* got a resize scheduled, then use the new size; otherwise the old.
*/
if (w->flags & WINDOW_RESIZE) {
if (!now && changed && w->new_sx == sx && w->new_sy == sy &&
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
if (!now && changed && w->new_sx == sx && w->new_sy == sy)
changed = 0;
} else {
if (!now && changed && w->sx == sx && w->sy == sy &&
w->xpixel == xpixel && w->ypixel == ypixel)
if (!now && changed && w->sx == sx && w->sy == sy)
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>
@@ -99,12 +99,6 @@ enum redraw_span_type {
#define REDRAW_ALL 0x7fffffff
#define REDRAW_IS_ALL(flags) ((flags) == REDRAW_ALL)
enum redraw_image_phase {
REDRAW_IMAGES_BEFORE,
REDRAW_TEXT,
REDRAW_IMAGES_AFTER
};
/* UTF-8 isolate characters. */
#define REDRAW_START_ISOLATE "\342\201\246"
#define REDRAW_END_ISOLATE "\342\201\251"
@@ -1112,8 +1106,7 @@ redraw_get_scene(struct client *c)
/* Draw a pane span. */
static void
redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int x, u_int y, u_int n,
enum redraw_image_phase phase)
struct redraw_span *span, u_int x, u_int y, u_int n)
{
struct redraw_scene *scene = dctx->scene;
struct client *c = scene->c;
@@ -1131,14 +1124,7 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
px = span->data.p.px + (x - span->x);
py = span->data.p.py;
#ifdef ENABLE_IMAGES
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);
tty_draw_line(tty, s, px, py, n, x, y, &style_ctx);
}
/* Get default border style for spans without a pane. */
@@ -1393,7 +1379,7 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx,
/* Draw a span. */
static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y, enum redraw_image_phase phase)
u_int y)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_span_data *data = &span->data;
@@ -1414,12 +1400,10 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
continue;
x = rr->px;
n = rr->nx;
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE)
continue;
switch (span->data.type) {
case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, x, y, n, phase);
redraw_draw_pane_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY:
@@ -1460,28 +1444,24 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
if (bottom > (int)scene->sy)
bottom = scene->sy;
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_IMAGES_AFTER; phase++) {
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, phase);
}
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);
}
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, phase);
}
}
if (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);
}
}
}
@@ -1497,17 +1477,13 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
struct redraw_span *span;
u_int y, cy, type;
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_IMAGES_AFTER; phase++) {
for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE)
continue;
for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (!REDRAW_IS_ALL(flags)) {
switch (type) {
case REDRAW_SPAN_PANE:
@@ -1544,8 +1520,7 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
}
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy, phase);
}
redraw_draw_span(dctx, span, cy);
}
}
}
@@ -1566,7 +1541,7 @@ redraw_draw_menu_lines(struct redraw_draw_ctx *dctx)
else
cy = y;
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry)
redraw_draw_span(dctx, span, cy, REDRAW_TEXT);
redraw_draw_span(dctx, span, cy);
}
}
@@ -1763,8 +1738,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
else
loop->flags &= ~PANE_NEWSTATUS;
width = redraw_pane_status_width(&dctx, loop,
&first);
width = redraw_pane_status_width(&dctx, loop, &first);
if (width == 0)
continue;
@@ -1795,7 +1769,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
}
}
}
tty_sync_start(tty);
tty_sync_start(tty); /* end in server_client_reset_state */
tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL);
if (wp != NULL)
@@ -1840,7 +1814,15 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
c->overlay_draw(c, c->overlay_data);
tty_reset(tty);
tty_sync_end(tty);
#ifdef ENABLE_SIXEL
if (wp != NULL)
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);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.285 2026/07/26 09:02:08 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -305,12 +305,14 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
if (~ctx->flags & SCREEN_WRITE_SYNC) {
/*
* For the active pane or for an overlay (no pane), we want to
* only use synchronized updates if requested (commands that
* move the cursor); for other panes, always use it, since the
* cursor will have to move.
* For the active pane showing its base screen or for an
* overlay (no pane), only use synchronized updates if
* requested (commands that move the cursor); for other panes
* or a pane in a mode, always use it, since the cursor will
* have to move.
*/
if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active)
if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active ||
ctx->wp->screen != &ctx->wp->base))
ttyctx->flags |= TTY_CTX_SYNC;
else {
if (ctx->wp == NULL)
@@ -338,10 +340,18 @@ screen_write_make_list(struct screen *s)
void
screen_write_free_list(struct screen *s)
{
u_int y;
struct screen_write_cline *cl;
struct screen_write_citem *ci, *ci1;
u_int y;
for (y = 0; y < screen_size_y(s); y++)
free(s->write_list[y].data);
for (y = 0; y < screen_size_y(s); y++) {
cl = &s->write_list[y];
TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) {
TAILQ_REMOVE(&cl->items, ci, entry);
screen_write_free_citem(ci);
}
free(cl->data);
}
free(s->write_list);
}
@@ -1059,6 +1069,19 @@ screen_write_stop_sync(struct window_pane *wp)
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
}
/* Flush pending output before clearing sync mode. */
void
screen_write_end_sync(struct screen_write_ctx *ctx)
{
struct window_pane *wp = ctx->wp;
if (wp == NULL)
return;
if (wp->base.mode & MODE_SYNC)
screen_write_collect_flush(ctx, 0, __func__);
screen_write_stop_sync(wp);
}
/* Cursor up by ny. */
void
screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny)
@@ -1308,8 +1331,9 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
memcpy(&gc, &grid_default_cell, sizeof gc);
utf8_set(&gc.data, 'E');
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
for (yy = 0; yy < screen_size_y(s); yy++) {
@@ -1354,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)
return;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1394,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)
return;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1434,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)
return;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1468,8 +1495,9 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1535,8 +1563,9 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1607,8 +1636,9 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg)
if (gl->cellsize == 0 && COLOUR_DEFAULT(bg))
return;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
flags = gl->flags & GRID_LINE_OSC133_FLAGS;
@@ -1645,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)))
return;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg);
@@ -1671,8 +1702,9 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cx > sx - 1)
@@ -1724,8 +1756,9 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
@@ -1775,6 +1808,9 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct grid_line *gl;
#ifdef ENABLE_SIXEL
int redraw = 0;
#endif
u_int rupper = s->rupper, rlower = s->rlower;
gl = grid_get_line(gd, gd->hsize + s->cy);
@@ -1795,12 +1831,13 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
return;
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
if (rlower == screen_size_y(s) - 1)
image_redraw_scroll(ctx, 1);
redraw = image_scroll_up(s, 1);
else
image_redraw_area(ctx, 0, rupper, screen_size_x(s),
rlower - rupper);
redraw = image_check_line(s, rupper, rlower - rupper);
if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
@@ -1826,8 +1863,9 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg)
ctx->bg = bg;
}
#ifdef ENABLE_IMAGES
image_redraw_scroll(ctx, lines);
#ifdef ENABLE_SIXEL
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
for (i = 0; i < lines; i++) {
@@ -1854,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)
lines = s->rlower - s->rupper + 1;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
for (i = 0; i < lines; i++)
@@ -1894,8 +1933,9 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -1974,8 +2014,9 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1);
#ifdef ENABLE_SIXEL
if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2046,8 +2087,9 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2461,15 +2503,62 @@ screen_write_collect_insert_clear(struct screen_write_ctx *ctx, u_int px,
}
}
/*
* Clear a cell that is being broken up by a write over part of it, keeping the
* background so the columns it covered do not lose their colour.
*/
static void
screen_write_clear_cell(struct grid *gd, u_int px, u_int py)
{
struct grid_cell gc;
int bg;
grid_view_get_cell(gd, px, py, &gc);
bg = gc.bg;
memcpy(&gc, &grid_default_cell, sizeof gc);
gc.bg = bg;
grid_view_set_cell(gd, px, py, &gc);
}
/*
* Insert clears for a range of cells already cleared in the grid. Adjacent
* cells may have come from different characters and so have different
* backgrounds, so this is done in runs of the same colour.
*/
static void
screen_write_insert_clears(struct screen_write_ctx *ctx, u_int px, u_int nx)
{
struct screen *s = ctx->s;
struct grid_cell gc;
u_int xx, start = px, n;
int bg = 8;
for (xx = px; xx < px + nx; xx++) {
grid_view_get_cell(s->grid, xx, s->cy, &gc);
if (xx == start)
bg = gc.bg;
else if (gc.bg != bg) {
n = xx - start;
log_debug("%s: from %u, size %u", __func__, start, n);
screen_write_collect_insert_clear(ctx, start, n, bg);
start = xx;
bg = gc.bg;
}
}
log_debug("%s: from %u, size %u", __func__, start, xx - start);
screen_write_collect_insert_clear(ctx, start, xx - start, bg);
}
/* Finish and store collected cells. */
void
screen_write_collect_end(struct screen_write_ctx *ctx)
{
struct screen *s = ctx->s;
struct screen_write_citem *ci = ctx->item, *bci = NULL, *aci;
struct screen_write_citem *ci = ctx->item;
struct screen_write_cline *cl = &s->write_list[s->cy];
struct grid_cell gc;
u_int xx;
u_int xx, bx = 0, bnx = 0;
if (ci->used == 0)
return;
@@ -2485,8 +2574,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
grid_view_get_cell(s->grid, xx, s->cy, &gc);
if (~gc.flags & GRID_FLAG_PADDING)
break;
grid_view_set_cell(s->grid, xx, s->cy,
&grid_default_cell);
screen_write_clear_cell(s->grid, xx, s->cy);
log_debug("%s: padding erased (before) at %u (cx %u)",
__func__, xx, s->cx);
}
@@ -2495,51 +2583,36 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
grid_view_get_cell(s->grid, 0, s->cy, &gc);
if (gc.data.width > 1 ||
(gc.flags & GRID_FLAG_PADDING)) {
grid_view_set_cell(s->grid, xx, s->cy,
&grid_default_cell);
screen_write_clear_cell(s->grid, xx, s->cy);
log_debug("%s: padding erased (before) at %u "
"(cx %u)", __func__, xx, s->cx);
}
}
if (xx != s->cx) {
bci = ctx->item;
bci->type = CLEAR;
bci->x = xx;
bci->bg = 8;
bci->used = s->cx - xx;
log_debug("%s: padding erased (before): from %u, "
"size %u", __func__, bci->x, bci->used);
bx = xx;
bnx = s->cx - xx;
}
}
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, s->cx, s->cy, ci->used, 1);
#ifdef ENABLE_SIXEL
if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
ci->used);
if (bci != NULL)
screen_write_collect_insert(ctx, bci);
if (bnx != 0)
screen_write_insert_clears(ctx, bx, bnx);
screen_write_set_cursor(ctx, s->cx + ci->used, -1);
for (xx = s->cx; xx < screen_size_x(s); xx++) {
grid_view_get_cell(s->grid, xx, s->cy, &gc);
if (~gc.flags & GRID_FLAG_PADDING)
break;
grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell);
log_debug("%s: padding erased (after) at %u (cx %u)",
__func__, xx, s->cx);
}
if (xx != s->cx) {
aci = ctx->item;
aci->type = CLEAR;
aci->x = s->cx;
aci->bg = 8;
aci->used = xx - s->cx;
log_debug("%s: padding erased (after): from %u, size %u",
__func__, aci->x, aci->used);
screen_write_collect_insert(ctx, aci);
screen_write_clear_cell(s->grid, xx, s->cy);
log_debug("%s: padding erased (after) at %u (cx %u)", __func__,
xx, s->cx);
}
if (xx != s->cx)
screen_write_insert_clears(ctx, s->cx, xx - s->cx);
}
/* Write cell data, collecting if necessary. */
@@ -2607,9 +2680,6 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
struct grid_line *gl;
struct grid_cell_entry *gce;
struct grid_cell tmp_gc, now_gc;
#ifdef ENABLE_IMAGES
struct grid_cell image_gc;
#endif
struct tty_ctx ttyctx;
u_int sx = screen_size_x(s), sy = screen_size_y(s);
u_int width = ud->width, xx, not_wrap, i, n, vis;
@@ -2654,20 +2724,6 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
return;
screen_write_initctx(ctx, &ttyctx, 0, 0);
#ifdef ENABLE_IMAGES
/* Update the text underlay without removing an image placement. */
grid_view_get_cell(gd, s->cx, s->cy, &now_gc);
if ((now_gc.flags & GRID_FLAG_IMAGE) &&
(~gc->flags & GRID_FLAG_IMAGE)) {
image_redraw_area(ctx, s->cx, s->cy, width, 1);
memcpy(&image_gc, gc, sizeof image_gc);
image_gc.flags |= GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED;
image_gc.image_id = now_gc.image_id;
image_gc.image_x = now_gc.image_x;
image_gc.image_y = now_gc.image_y;
gc = &image_gc;
}
#endif
/* Handle overwriting of UTF-8 characters. */
gl = grid_get_line(s->grid, s->grid->hsize + s->cy);
if (gl->flags & GRID_LINE_EXTENDED) {
@@ -2684,7 +2740,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
*/
for (xx = s->cx + 1; xx < s->cx + width; xx++) {
log_debug("%s: new padding at %u,%u", __func__, xx, s->cy);
grid_view_set_padding(gd, xx, s->cy);
grid_view_set_padding(gd, xx, s->cy, gc->bg);
skip = 0;
}
@@ -2806,10 +2862,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
struct grid *gd = s->grid;
const struct utf8_data *ud = &gc->data;
struct options *oo = global_options;
u_int i, n, cx = s->cx, cy = s->cy, vis, yoff = 0;
u_int i, n, cx = s->cx, cy = s->cy, vis;
struct grid_cell last;
struct tty_ctx ttyctx;
int force_wide = 0, zero_width = 0;
int xoff = 0, yoff = 0;
struct visible_ranges *r;
/* Ignore U+3164 HANGUL_FILLER entirely. */
@@ -2896,16 +2953,18 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
/* Set the new cell. */
grid_view_set_cell(gd, cx - n, cy, &last);
if (force_wide)
grid_view_set_padding(gd, cx - 1, cy);
grid_view_set_padding(gd, cx - 1, cy, last.bg);
/*
* Check if all of this character is visible. No character will be
* obscured in the middle, only on left or right, but there could be an
* empty range in the visible ranges so we add them all up.
*/
if (wp != NULL)
if (wp != NULL) {
xoff = wp->xoff;
yoff = wp->yoff;
r = window_visible_ranges(wp, cx - n, cy + yoff, n, NULL);
}
r = window_visible_ranges(wp, xoff + cx - n, cy + yoff, n, NULL);
for (i = 0, vis = 0; i < r->used; i++)
vis += r->ranges[i].nx;
if (vis < n) {
@@ -2965,12 +3024,12 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc,
if (~tmp_gc.flags & GRID_FLAG_PADDING)
break;
log_debug("%s: padding at %u,%u", __func__, xx, s->cy);
grid_view_set_cell(gd, xx, s->cy, &grid_default_cell);
screen_write_clear_cell(gd, xx, s->cy);
}
/* Overwrite the character at the start of this padding. */
log_debug("%s: character at %u,%u", __func__, xx, s->cy);
grid_view_set_cell(gd, xx, s->cy, &grid_default_cell);
screen_write_clear_cell(gd, xx, s->cy);
done = 1;
}
@@ -2988,17 +3047,7 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc,
break;
log_debug("%s: overwrite at %u,%u", __func__, xx,
s->cy);
if (gc->flags & GRID_FLAG_TAB) {
memcpy(&tmp_gc, gc, sizeof tmp_gc);
memset(tmp_gc.data.data, 0,
sizeof tmp_gc.data.data);
*tmp_gc.data.data = ' ';
tmp_gc.data.width = tmp_gc.data.size =
tmp_gc.data.have = 1;
grid_view_set_cell(gd, xx, s->cy, &tmp_gc);
} else
grid_view_set_cell(gd, xx, s->cy,
&grid_default_cell);
screen_write_clear_cell(gd, xx, s->cy);
done = 1;
}
}
@@ -3037,21 +3086,63 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len,
tty_write(tty_cmd_rawstring, &ttyctx);
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
/* Write a SIXEL image. */
void
screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si,
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 (im == NULL) {
sixel_free(si);
if (screen_size_y(s) == 1)
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(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

View File

@@ -93,6 +93,11 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
s->tabs = NULL;
s->sel = NULL;
#ifdef ENABLE_SIXEL
TAILQ_INIT(&s->images);
TAILQ_INIT(&s->saved_images);
#endif
s->write_list = NULL;
s->hyperlinks = NULL;
@@ -126,6 +131,11 @@ screen_reinit(struct screen *s, int check)
screen_clear_selection(s);
screen_free_titles(s);
#ifdef ENABLE_SIXEL
image_free_all(s);
#endif
screen_set_progress_bar(s, PROGRESS_BAR_HIDDEN, 0);
screen_reset_hyperlinks(s);
}
@@ -144,6 +154,10 @@ screen_reset_hyperlinks(struct screen *s)
void
screen_free(struct screen *s)
{
#ifdef ENABLE_SIXEL
struct image *im;
#endif
free(s->sel);
free(s->tabs);
free(s->path);
@@ -160,6 +174,17 @@ screen_free(struct screen *s)
hyperlinks_free(s->hyperlinks);
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. */
@@ -352,6 +377,10 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow,
if (sy != screen_size_y(s))
screen_resize_y(s, sy, eat_empty, &cy);
#ifdef ENABLE_SIXEL
image_free_all(s);
#endif
if (reflow)
screen_reflow(s, sx, &cx, &cy, cursor);
@@ -663,6 +692,10 @@ int
screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
{
u_int sx, sy;
#ifdef ENABLE_SIXEL
struct image *im;
#endif
if (SCREEN_IS_ALTERNATE(s))
return 0;
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);
#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);
s->saved_flags = s->grid->flags;
@@ -689,6 +728,10 @@ int
screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
{
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
* before copying back.
@@ -731,6 +774,13 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
grid_destroy(s->saved_grid);
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)
s->cx = screen_size_x(s) - 1;
if (s->cy > screen_size_y(s) - 1)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.500 2026/07/28 13:17:45 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>
@@ -36,7 +36,8 @@ static void server_client_check_window_resize(struct window *);
static key_code server_client_check_mouse(struct client *, struct key_event *);
static void server_client_repeat_timer(int, short, void *);
static void server_client_click_timer(int, short, void *);
static void server_client_check_exit(struct client *);
static void server_client_check_exit(struct client *, int);
static void server_client_exit_timer(int, short, void *);
static void server_client_check_redraw(struct client *);
static void server_client_check_modes(struct client *);
static void server_client_set_title(struct client *);
@@ -44,6 +45,7 @@ static void server_client_set_path(struct client *);
static void server_client_set_progress_bar(struct client *);
static void server_client_reset_state(struct client *);
static void server_client_update_latest(struct client *);
static int server_client_handle_dead_key(struct window_pane *, key_code);
static void server_client_dispatch(struct imsg *, void *);
static int server_client_dispatch_command(struct client *, struct imsg *);
static int server_client_dispatch_identify(struct client *, struct imsg *);
@@ -309,6 +311,7 @@ server_client_create(int fd)
evtimer_set(&c->repeat_timer, server_client_repeat_timer, c);
evtimer_set(&c->click_timer, server_client_click_timer, c);
evtimer_set(&c->exit_timer, server_client_exit_timer, c);
c->click_wp = -1;
@@ -521,10 +524,14 @@ server_client_lost(struct client *c)
input_cancel_requests(c);
free(c->title);
free(c->path);
free((void *)c->cwd);
free(c->exit_session);
free(c->exit_message);
evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer);
evtimer_del(&c->exit_timer);
if (event_initialized(&c->cycle_timer))
evtimer_del(&c->cycle_timer);
@@ -797,8 +804,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py,
} else {
/* Try the pane borders. */
TAILQ_FOREACH(fwp, &w->panes, entry) {
if ((w->flags & WINDOW_ZOOMED) &&
(~fwp->flags & PANE_ZOOMED))
if (!window_pane_is_visible(fwp))
continue;
if (window_pane_is_floating(fwp) &&
window_pane_get_pane_lines(fwp) == PANE_LINES_NONE)
@@ -1385,6 +1391,21 @@ server_client_repeat_time(struct client *c, struct key_binding *bd)
return (repeat);
}
/* Handle a key press on a dead pane waiting for a key. */
static int
server_client_handle_dead_key(struct window_pane *wp, key_code key)
{
if (wp == NULL ||
(~wp->flags & PANE_EXITED) ||
KEYC_IS_MOUSE(key) ||
KEYC_IS_PASTE(key) ||
options_get_number(wp->options, "remain-on-exit") != 3)
return (0);
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
return (1);
}
/*
* Handle data key input from client. This owns and can modify the key event it
* is given and is responsible for freeing it.
@@ -1470,6 +1491,18 @@ server_client_key_callback(struct cmdq_item *item, void *data)
server_client_is_assume_paste(c))
goto paste_key;
/* Forward keys directly if this pane is capturing all keys. */
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
(~wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
TAILQ_EMPTY(&wp->modes))
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
* the mode table instead of the default key table.
@@ -1634,15 +1667,8 @@ try_again:
}
forward_key:
if (wp != NULL &&
(wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
!KEYC_IS_PASTE(key) &&
options_get_number(wp->options, "remain-on-exit") == 3) {
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
if (server_client_handle_dead_key(wp, key))
goto out;
}
if (c->flags & CLIENT_READONLY)
goto out;
if (wp != NULL)
@@ -1730,9 +1756,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
/*
* Key presses in overlay mode and the command prompt are a special
* case. The queue might be blocked so they need to be processed
* immediately rather than queued.
* Key presses in overlay mode, for panes capturing all keys and in the
* command prompt are a special case. The queue might be blocked so they
* need to be processed immediately rather than queued.
*/
if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) {
@@ -1752,6 +1778,21 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
server_client_clear_overlay(c);
wp = s->curw->window->active;
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
TAILQ_EMPTY(&wp->modes) &&
!KEYC_IS_MOUSE(event->key)) {
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (~wp->flags & PANE_EXITED) {
window_pane_key(wp, c, s, s->curw, event->key,
&event->m);
return (0);
}
}
if (server_client_handle_menu_key(c, event))
return (0);
if (c->prompt != NULL) {
@@ -1849,7 +1890,7 @@ server_client_loop(void)
/* Check clients. */
TAILQ_FOREACH(c, &clients, entry) {
server_client_check_exit(c);
server_client_check_exit(c, 0);
if (c->session != NULL && c->session->curw != NULL) {
server_client_check_modes(c);
server_client_check_redraw(c);
@@ -2203,7 +2244,7 @@ server_client_reset_state(struct client *c)
cy += status_line_size(c);
}
if ((pane_mode & MODE_SYNC) || !cursor)
if (!cursor)
mode &= ~MODE_CURSOR;
}
} else if (c->overlay_mode == NULL || s == NULL)
@@ -2211,6 +2252,10 @@ server_client_reset_state(struct client *c)
if (~pane_mode & MODE_SYNC) {
log_debug("%s: cursor to %u,%u", __func__, cx, cy);
tty_cursor(tty, cx, cy);
} else {
mode &= ~CURSOR_MODES;
mode |= tty->mode & CURSOR_MODES;
s = NULL;
}
/*
@@ -2285,9 +2330,37 @@ server_client_click_timer(__unused int fd, __unused short events, void *data)
c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK);
}
/* Check if client should be exited. */
/* Start client exit timer. */
static void
server_client_check_exit(struct client *c)
server_client_start_exit_timer(struct client *c)
{
struct timeval tv = { .tv_sec = 10 };
if (!evtimer_pending(&c->exit_timer, NULL))
evtimer_add(&c->exit_timer, &tv);
}
/* Exit timer has expired: stop waiting for the client. */
static void
server_client_exit_timer(__unused int fd, __unused short events, void *data)
{
struct client *c = data;
if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_EXITED) {
log_debug("%s: %s took too long to exit", __func__, c->name);
server_client_lost(c);
} else if (c->flags & CLIENT_EXIT) {
log_debug("%s: %s took too long to flush", __func__, c->name);
server_client_check_exit(c, 1);
}
}
/* Check if client should be exited, abandoning buffered output if forced. */
static void
server_client_check_exit(struct client *c, int force)
{
struct client_file *cf;
const char *name = c->exit_session;
@@ -2300,16 +2373,29 @@ server_client_check_exit(struct client *c)
return;
if (c->flags & CLIENT_CONTROL) {
control_discard(c);
if (!control_all_done(c))
return;
if (force)
control_discard_all(c);
else {
control_discard(c);
if (!control_all_done(c)) {
server_client_start_exit_timer(c);
return;
}
}
}
RB_FOREACH(cf, client_files, &c->files) {
if (EVBUFFER_LENGTH(cf->buffer) != 0)
return;
if (!force) {
RB_FOREACH(cf, client_files, &c->files) {
if (EVBUFFER_LENGTH(cf->buffer) != 0) {
server_client_start_exit_timer(c);
return;
}
}
}
c->flags |= CLIENT_EXITED;
evtimer_del(&c->exit_timer);
server_client_start_exit_timer(c);
switch (c->exit_type) {
case CLIENT_EXIT_RETURN:
if (c->exit_message != NULL)
@@ -2331,8 +2417,6 @@ server_client_check_exit(struct client *c)
proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1);
break;
}
free(c->exit_session);
free(c->exit_message);
}
/* Redraw timer callback. */
@@ -2408,7 +2492,7 @@ server_client_check_redraw(struct client *c)
/* Work out if a redraw is actually needed. */
needed = 0;
if (c->flags & CLIENT_ALLREDRAWFLAGS)
if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS))
needed = 1;
else if (server_client_any_pane_redraw(c))
needed = 1;
@@ -2434,8 +2518,14 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
if (server_client_any_pane_redraw(c))
c->flags |= CLIENT_REDRAWWINDOW;
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
break;
}
if (wp->flags & PANE_REDRAWSCROLLBAR)
c->flags |= CLIENT_REDRAWSCROLLBARS;
}
return;
}
@@ -2454,7 +2544,8 @@ server_client_check_redraw(struct client *c)
log_debug("%s: redraw pane %%%u", __func__,
wp->id);
redraw_pane(c, wp);
} else if (wp->flags & PANE_REDRAWSCROLLBAR) {
} else if ((wp->flags & PANE_REDRAWSCROLLBAR) ||
(c->flags & CLIENT_REDRAWSCROLLBARS)) {
log_debug("%s: redraw scrollbar %%%u", __func__,
wp->id);
redraw_pane_scrollbar(c, wp);
@@ -2484,7 +2575,8 @@ server_client_check_redraw(struct client *c)
* All the redraw flags can now be cleared. Also record how many bytes
* were written.
*/
c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_STATUSFORCE);
c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS|
CLIENT_STATUSFORCE);
c->redraw = EVBUFFER_LENGTH(tty->out);
log_debug("%s: redraw added %zu bytes", c->name, c->redraw);
}
@@ -2651,6 +2743,10 @@ server_client_dispatch(struct imsg *imsg, void *arg)
if (file_write_ready(&c->files, imsg) != 0)
goto bad;
break;
case MSG_WRITE_DONE:
if (file_write_done(&c->files, imsg) != 0)
goto bad;
break;
case MSG_READ:
if (file_read_data(&c->files, imsg) != 0)
goto bad;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 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>
@@ -226,10 +226,11 @@ server_kill_pane(struct window_pane *wp)
server_kill_window(w, 1);
recalculate_sizes();
} else {
server_unzoom_window(w);
window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM);
server_client_remove_pane(wp);
layout_close_pane(wp);
window_remove_pane(w, wp);
window_pop_zoom(w);
server_redraw_window(w);
}
}
@@ -378,9 +379,9 @@ server_destroy_pane(struct window_pane *wp, int notify)
wp->pipe_fd = -1;
}
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
if (remain_on_exit != 0 && (~wp->flags & PANE_STATUSREADY))
if (~wp->flags & PANE_STATUSREADY)
return;
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
switch (remain_on_exit) {
case 0:
break;
@@ -421,15 +422,17 @@ server_destroy_pane(struct window_pane *wp, int notify)
if (notify)
server_fire_pane_exit("pane-exited", wp);
server_unzoom_window(w);
window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM);
server_client_remove_pane(wp);
layout_close_pane(wp);
window_remove_pane(w, wp);
if (TAILQ_EMPTY(&w->panes))
server_kill_window(w, 1);
else
else {
window_pop_zoom(w);
server_redraw_window(w);
}
}
static void
@@ -551,6 +554,7 @@ server_check_unattached(void)
continue;
break;
}
server_destroy_session(s);
session_destroy(s, 1, __func__);
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */
/* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -129,6 +129,7 @@ server_create_socket(uint64_t flags, char **cause)
mask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) {
saved_errno = errno;
umask(mask);
close(fd);
errno = saved_errno;
goto fail;
@@ -240,6 +241,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base,
if (cause != NULL) {
if (c != NULL) {
c->exit_message = cause;
c->retval = 1;
c->flags |= CLIENT_EXIT;
} else {
fprintf(stderr, "%s\n", cause);
@@ -398,6 +400,7 @@ server_accept(int fd, short events, __unused void *data)
c = server_client_create(newfd);
if (!server_acl_join(c)) {
c->exit_message = xstrdup("access not allowed");
c->retval = 1;
c->flags |= CLIENT_EXIT;
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */
/* $OpenBSD: session.c,v 1.107 2026/08/05 07:35:35 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -709,8 +709,10 @@ session_group_synchronize1(struct session *target, struct session *s)
/* Fix up the current window. */
if (s->curw != NULL)
s->curw = winlink_find_by_index(&s->windows, s->curw->idx);
else
else if (target->curw != NULL)
s->curw = winlink_find_by_index(&s->windows, target->curw->idx);
if (s->curw == NULL)
s->curw = RB_MIN(winlinks, &s->windows);
/* Fix up the last window stack. */
memcpy(&old_lastw, &s->lastw, sizeof old_lastw);

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>
@@ -32,7 +32,7 @@ sort_qsort(void *l, u_int len, u_int size, int (*cmp)(const void *,
u_int i;
void *tmp, **ll;
if (sort_crit->order == SORT_END)
if (len < 2 || sort_crit->order == SORT_END)
return;
if (sort_crit->order == SORT_ORDER) {

35
spawn.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */
/* $OpenBSD: spawn.c,v 1.52 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -243,7 +243,7 @@ struct window_pane *
spawn_pane(struct spawn_context *sc, char **cause)
{
struct cmdq_item *item = sc->item;
struct client *c;
struct client *c, *loop;
struct session *s = sc->s;
struct session *ts;
struct window *w = sc->wl->window;
@@ -331,6 +331,20 @@ spawn_pane(struct spawn_context *sc, char **cause)
input_free(sc->wp0->ictx);
sc->wp0->ictx = NULL;
}
/*
* The old buffer is gone and the new one starts empty, so
* offsets into the old buffer no longer mean anything. Reset
* them, and drop output control clients still had queued.
*/
sc->wp0->offset.used = 0;
sc->wp0->base_offset = 0;
sc->wp0->pipe_offset.used = 0;
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->flags & CLIENT_CONTROL)
control_reset_pane(loop, sc->wp0);
}
new_wp = sc->wp0;
new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN);
} else {
@@ -346,6 +360,8 @@ spawn_pane(struct spawn_context *sc, char **cause)
}
if (sc->flags & SPAWN_FLOATING)
new_wp->layout_cell->flags |= LAYOUT_CELL_FLOATING;
if (sc->flags & SPAWN_FLOATOVERZOOM)
new_wp->flags |= PANE_FLOATOVERZOOM;
/*
* If window currently zoomed, window_set_active_pane calls
@@ -685,7 +701,7 @@ spawn_editor_finish(struct window_pane *wp)
struct spawn_editor_state *
spawn_editor(struct client *c, const char *buf, size_t len,
spawn_finish_edit_cb cb, void *arg)
const char *editor, spawn_finish_edit_cb cb, void *arg)
{
struct spawn_editor_state *es;
struct spawn_context sc = { 0 };
@@ -699,13 +715,13 @@ spawn_editor(struct client *c, const char *buf, size_t len,
FILE *f;
char *cmd, *cause = NULL;
char path[] = _PATH_TMP "tmux.XXXXXXXX";
const char *editor;
int fd;
if (w->modal != NULL)
return (NULL);
editor = options_get_string(global_options, "editor");
if (editor == NULL)
editor = options_get_string(global_options, "editor");
fd = mkstemp(path);
if (fd == -1)
return (NULL);
@@ -731,10 +747,10 @@ spawn_editor(struct client *c, const char *buf, size_t len,
lg.sy = w->sy * 9 / 10;
lg.xoff = w->sx / 2 - lg.sx / 2;
lg.yoff = w->sy / 2 - lg.sy / 2;
window_push_modal_zoom(w);
window_push_zoom(w, 0, 1);
lc = layout_floating_pane(w, NULL, &lg);
if (lc == NULL) {
window_pop_modal_zoom(w);
window_pop_zoom(w);
spawn_editor_free(es);
return (NULL);
}
@@ -751,17 +767,18 @@ spawn_editor(struct client *c, const char *buf, size_t len,
sc.environ = env;
sc.idx = -1;
sc.cwd = _PATH_TMP;
sc.flags = SPAWN_FLOATING|SPAWN_MODAL;
sc.flags = SPAWN_FLOATING|SPAWN_MODAL|SPAWN_FLOATOVERZOOM;
wp = spawn_pane(&sc, &cause);
free(cmd);
environ_free(env);
if (wp == NULL) {
free(cause);
window_pop_modal_zoom(w);
window_pop_zoom(w);
spawn_editor_free(es);
return (NULL);
}
window_pop_zoom(w);
options_set_number(wp->options, "remain-on-exit", 0);
es->pid = wp->pid;
wp->editor = es;

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */
/* $OpenBSD: tmux-protocol.h,v 1.3 2026/08/17 07:56:56 nicm Exp $ */
/*
* Copyright (c) 2021 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -67,7 +67,8 @@ enum msgtype {
MSG_WRITE,
MSG_WRITE_READY,
MSG_WRITE_CLOSE,
MSG_READ_CANCEL
MSG_READ_CANCEL,
MSG_WRITE_DONE
};
/*
@@ -116,4 +117,9 @@ struct msg_write_close {
int stream;
};
struct msg_write_done {
int stream;
int error;
};
#endif /* TMUX_PROTOCOL_H */

247
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1152 2026/07/29 08:58:33 nicm Exp $
.\" $OpenBSD: tmux.1,v 1.1164 2026/09/03 21:04:11 nicm Exp $
.\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\"
.Dd $Mdocdate: July 29 2026 $
.Dd $Mdocdate: September 3 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -356,6 +356,8 @@ Force redraw of the attached client.
Select a new session for the attached client interactively.
.It t
Show the time.
.It T
Change the current pane title.
.It w
Choose the current window interactively.
.It x
@@ -2002,6 +2004,25 @@ Same as
.Ic copy\-pipe
but also exit copy mode.
.It Xo
.Ic copy\-pipe\-output
.Op Fl aCP
.Op Ar command
.Op Ar prefix
.Xc
Copy the current OSC 133 command output, pipe it to
.Ar command ,
and clear the selection.
.Ar prefix
is used to name the new paste buffer.
.It Xo
.Ic copy\-output
.Op Fl aCP
.Op Ar prefix
.Xc
Copy the current OSC 133 command output and clear the selection.
.Ar prefix
is used to name the new paste buffer.
.It Xo
.Ic copy\-selection
.Op Fl CP
.Op Ar prefix
@@ -2167,6 +2188,27 @@ Hide line numbers in copy mode.
.Xc
Toggle line numbers in copy mode.
.It Xo
.Ic collapse\-output
.Op Fl a
.Xc
Collapse the output for the command on the cursor line.
With
.Fl a ,
collapse all command output in the copy-mode buffer.
Text before the first OSC 133 prompt is treated as an initial command output
region.
.It Xo
.Ic expand\-output
.Op Fl a
.Xc
Expand the output for the command on the cursor line.
With
.Fl a ,
expand all command output in the copy-mode buffer.
.Pp
The Tab key toggles output for the command on the cursor line and S-Tab
toggles all command output.
.It Xo
.Ic middle\-line
(vi: M)
(emacs: M\-r)
@@ -2206,20 +2248,46 @@ Move to the end of the next word.
.Xc
Same as
.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
.Ic next\-space\-end
(vi: E)
.Xc
Same as
.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
.Ic other\-end
(vi: o)
.Xc
Switch at which end of the selection the cursor sits.
.It Xo
.Ic open\-output
.Op Fl a
.Op Ar command
.Xc
Open the current OSC 133 command output in an editor.
If
.Ar command
is given, it is used as the editor command; otherwise, the
.Ic editor
option is used.
.It Xo
.Ic open\-selection
.Op Ar command
.Xc
Open the current selection in an editor.
If
.Ar command
is given, it is used as the editor command; otherwise, the
.Ic editor
option is used.
In emacs mode,
.Ql e ,
and in vi mode,
.Ql M\-e ,
opens the selection if one is present, otherwise the command output.
.It Xo
.Ic page\-down
(vi: C\-f)
(emacs: PageDown)
@@ -2260,6 +2328,14 @@ Same as
.Ic pipe
but also exit copy mode.
.It Xo
.Ic pipe\-output
.Op Fl a
.Op Ar command
.Xc
Pipe the current OSC 133 command output to
.Ar command
and clear the selection.
.It Xo
.Ic previous\-matching\-bracket
(emacs: M\-C\-b)
.Xc
@@ -2287,7 +2363,7 @@ Move to the previous word.
.Xc
Same as
.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
.Ic rectangle\-on
.Xc
@@ -2319,10 +2395,14 @@ the bottom and is paused while a selection is in progress.
.Xc
Turn off automatic refresh of the content from the pane.
.It Xo
.Ic refresh\-toggle
.Ic refresh\-now
(vi: r)
(emacs: r)
.Xc
Refresh the content from the pane once.
.It Xo
.Ic refresh\-toggle
.Xc
Toggle automatic refresh of the content from the pane.
.It Xo
.Ic scroll\-bottom
@@ -2450,6 +2530,16 @@ backward becomes forward).
.Xc
Select the current line.
.It Xo
.Ic select\-output
.Op Fl a
.Xc
Select the current OSC 133 command output.
If it is collapsed, it is expanded first.
The default emacs binding is
.Ql C\-o
and the default vi binding is
.Ql M\-o .
.It Xo
.Ic select\-word
.Xc
Select the current word.
@@ -2488,6 +2578,12 @@ Toggle the visibility of the position indicator in the top right.
Move to the top line.
.El
.Pp
The output commands use OSC 133 output text from the C marker to the D marker.
If there is no D marker, the output ends at the end of the copy-mode buffer.
With
.Fl a ,
they use the entire copy-mode buffer instead.
.Pp
The search commands come in several varieties:
.Ql search\-forward
and
@@ -2533,6 +2629,15 @@ Finding the beginning of command output requires the shell to emit an escape
sequence (\e033]133;C\e033\e\e) to tell tmux where the output begins.
If the shell does not send these escape sequences, these commands do nothing.
.Pp
The output commands operate on text from an OSC 133
.Ql C
marker up to its matching
.Ql D
marker, or the current end of the buffer if there is no
.Ql D
marker.
They do nothing if the cursor is not associated with an output range.
.Pp
Copy commands may take an optional buffer prefix argument which is used
to generate the buffer name (the default is
.Ql buffer
@@ -2568,8 +2673,8 @@ Word separators can be customized with the
session option.
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.
The three next and previous space keys work similarly but use a space alone as
the word separator.
The three next and previous space keys work similarly but treat all
non-whitespace characters as part of a word.
Setting
.Em word\-separators
to the empty string makes next/previous word equivalent to next/previous space.
@@ -2595,12 +2700,19 @@ The synopsis for the
command is:
.Bl -tag -width Ds
.It Xo Ic copy\-mode
.Op Fl dekHMqSu
.Op Fl UcdekHMqSu
.Op Fl s Ar src\-pane
.Op Fl t Ar target\-pane
.Xc
Enter copy mode.
.Pp
.Fl c
enters copy mode with OSC 133 command output collapsed.
.Fl U
enters copy mode with OSC 133 output controls shown and all output expanded.
When output is collapsed, the prompt and entered command are shown and text
from the output start marker to the next primary prompt marker is hidden.
.Pp
.Fl u
enters copy mode and scrolls one page up and
.Fl d
@@ -2641,6 +2753,60 @@ bind PageDown copy\-mode \-ed
kills the pane when the mode is exited.
.El
.Pp
The following commands operate on OSC 133 command output. In copy mode, they
use the output on the cursor line; otherwise, they use the most recent output
in the target pane. If an output has a C marker but no D marker, it ends at the
end of the pane buffer. If there is no OSC 133 output, they do nothing unless
.Fl a
is given, which uses the entire buffer.
.Bl -tag -width Ds
.It Xo Ic select\-output
.Op Fl a
.Op Fl t Ar target\-pane
.Xc
Enter copy mode if necessary and select the output.
.It Xo Ic copy\-output
.Op Fl aCP
.Op Fl t Ar target\-pane
.Op Ar prefix
.Xc
Copy the output to a paste buffer.
.It Xo Ic copy\-pipe\-output
.Op Fl aCP
.Op Fl t Ar target\-pane
.Op Ar command
.Op Ar prefix
.Xc
Copy the output to a paste buffer and pipe it to
.Ar command .
.It Xo Ic pipe\-output
.Op Fl a
.Op Fl t Ar target\-pane
.Op Ar command
.Xc
Pipe the output to
.Ar command .
.It Xo Ic open\-output
.Op Fl a
.Op Fl t Ar target\-pane
.Op Ar command
.Xc
Open the output in an editor. If
.Ar command
is given, it is used as the editor command; otherwise the
.Ic editor
option is used.
.It Xo Ic open\-selection
.Op Fl t Ar target\-pane
.Op Ar command
.Xc
Open the current copy-mode selection in an editor. If
.Ar command
is given, it is used as the editor command; otherwise the
.Ic editor
option is used.
.El
.Pp
A number of preset arrangements of panes are available, these are called
layouts.
These may be selected with the
@@ -3584,13 +3750,20 @@ represents the window to be created; if the target already exists an error is
shown, unless the
.Fl k
flag is used, in which case it is destroyed.
.Ar window\-name
is the name given to the new window, if any.
.Pp
If
.Fl S
is given and a window named
.Ar window\-name
already exists, it is selected (unless
is given, an existing window is selected instead of creating a new one
(unless
.Fl d
is also given in which case the command does nothing).
is also given in which case the command does nothing):
.Ar target\-window
is preferred if it already identifies an existing window; otherwise a
window named
.Ar window\-name
is selected if one exists.
.Pp
.Ar shell\-command
is the command to execute.
@@ -3638,7 +3811,7 @@ but a different format may be specified with
.Fl F .
.Tg newp
.It Xo Ic new\-pane
.Op Fl bCdefhIkLMOPvWZ
.Op Fl AbCdefhIkKLMOPvWZ
.Op Fl B Ar border\-lines
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
@@ -3687,14 +3860,17 @@ sets the pane border lines for floating panes; see
.Fl M
may be used when bound to a mouse drag key; the new floating pane is resized to
follow the mouse drag.
It has no effect with
.Fl L .
.Fl A
creates a floating pane which remains visible above a zoomed pane.
.Fl O
creates a modal pane.
A modal pane is always the active pane and prevents interaction with any other
panes while it is active.
A window can only have one modal pane and it must be a floating pane.
With
.Fl K ,
all keys, including the prefix key, are passed directly to the modal pane.
With
.Fl C ,
the modal pane is closed when the mouse is clicked outside it.
.Pp
@@ -4745,6 +4921,15 @@ for backspace.
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
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[]
.Ar name=value
.Xc
@@ -5041,6 +5226,10 @@ This is an array option where each entry is a colon-separated string made up
of a terminal type pattern (matched using
.Xr glob 7
patterns) followed by a list of terminal features.
A feature may be suffixed with
.Ql @
to disable it; for example,
.Ql xterm*:sync@ .
The available features are:
.Bl -tag -width Ds
.It 256
@@ -5091,6 +5280,8 @@ Supports
title setting.
.It usstyle
Allows underscore style and colour to be set.
.It utf8
Supports UTF\-8 output.
.El
.It Ic terminal\-overrides[] Ar string
Allow terminal descriptions read using
@@ -5625,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
separators, for the purposes of the next and previous word commands in
copy mode.
A space in
.Ar string
matches any character with the Unicode
.Em White_Space
property.
.El
.Pp
Available window options are:
@@ -5762,6 +5958,20 @@ section.
.It Ic copy\-mode\-position\-format Ar format
Format of the position indicator in copy mode.
.Pp
.It Ic copy\-mode\-exit\-status\-format Ar format
Format of the OSC 133 exit status indicator in the copy-mode gutter.
The default displays a theme red
.Ql !
for a nonzero status and a space otherwise.
The gutter grows to fit the expanded format.
The format is drawn to the left of the collapse control, and its alignment
applies within that area.
The format is expanded with
.Ql exit_status
and
.Ql exit_status_present ,
which is set only when the OSC 133 end-of-output marker includes a status.
.Pp
.It Ic copy\-mode\-position\-style Ar style
Set the style of the position indicator in copy mode.
For how to specify
@@ -7294,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_x" Ta "" Ta "Cursor X 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_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_size" Ta "" Ta "Size of history in lines"
.It Li "hook" Ta "" Ta "Name of running hook, if any"
@@ -7393,10 +7606,12 @@ The following variables are available, where appropriate:
.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_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_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_pid" Ta "" Ta "PID of pipe process, if any"
.It Li "pane_private_modes" Ta "" Ta "List of enabled DECSET private modes"
.It Li "pane_pb_state" Ta "" Ta "Pane progress bar state, one of hidden, normal, error, indeterminate, paused (can be set by application)"
.It Li "pane_pb_progress" Ta "" Ta "Pane progress bar progress percentage (can be set by application)"
.It Li "pane_right" Ta "" Ta "Right of pane"

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