Compare commits

..

90 Commits

Author SHA1 Message Date
Michael Grant
7188c20c71 Avoid redrawing kitty images when terminal scrolling moves them. 2026-08-31 17:46:19 +01:00
Michael Grant
710640d8fa Fix pane-border-status not being redrawn properly. 2026-08-31 16:49:32 +01:00
Michael Grant
aa6b52eff9 window: redraw border status after moving floating panes 2026-08-25 12:14:41 +01:00
Michael Grant
84a2ccdbcb build: warn that --enable-sixel is deprecated 2026-08-25 12:02:20 +01:00
Michael Grant
87400d67b0 Improve image fallback rendering 2026-08-25 11:53:39 +01:00
Michael Grant
2a535f7d82 image: keep the scrolled-off portion of a placement instead of discarding it
image_write() handles an image taller than the pane by scrolling the
screen up to make room, but only ever created spans for the rows that
ended up on screen afterwards - the rows immediately scrolled off (up to
origin_y of them) were pushed into history as blank, spanless rows and
that part of the image was permanently discarded. Unlike width, which has
no "scroll right" to recover a permanent clip, height already has
ordinary scrollback, so this was pure waste: scrolling back up just
showed empty space where part of the image used to be.

image_write() now also calls image_extend_row() for those origin_y rows,
capped to however many real history rows screen_write_scrollup() actually
created (it clamps its own request to one scroll-region height per call,
so a single call may not push everywhere origin_y implies - looping it
until the full amount is pushed, done here too, makes that gap rare, but
the cap keeps the fill-in safe even if it isn't).
2026-08-25 11:24:14 +01:00
Michael Grant
4e2abff2d6 window-copy: avoid erasing and needlessly re-flashing images during copy-mode redraws
window_copy_write_one() used to write text/highlight styling directly
over image-covered cells via the normal path, which - since a character
write typically clears whatever pixel content a terminal was showing
there - erased the image with nothing to redraw it back in. Image-covered
cells now bypass styling and go straight into the grid instead (a
non-graphical client's ASCII fallback for the image still depends on this
same write, so it can't just be skipped).

window_copy_write_line()'s call to image_redraw_area() used to fire
unconditionally on every redraw, needlessly recompositing (and briefly
flashing) the image on every single cursor step even after the erasure
above was fixed, even though nothing about the image had changed. It now
only fires when window_copy_update_image_refresh() (new) determines the
view's underlying history position actually moved since the last redraw.

Also adds window_copy_visible_has_images(), used by a later commit's
scroll-region fix as a bypass to a full redraw when the visible range
carries image data - line insert/delete only shifts character cells, not
already-sent image pixels, so scroll's normal fast path leaves stale or
missing image content behind.
2026-08-25 11:24:14 +01:00
Michael Grant
e40ef98b1e image: extend existing placements when a pane grows wider
image_write() only ever creates spans for as much of an image as fit in
the pane at the time it was placed - the rest of the image's pixels are
still retained (struct image is immutable and kept for as long as any
placement references it), but nothing revisited that clipping decision,
so a pane that was too narrow when an image was displayed stayed clipped
forever, even after growing wide enough to fit the rest. Unlike height,
which recovers via ordinary scrollback, there is no equivalent "scroll
right" - this is the only way the extra width is ever recovered.

image_grid_resize_width() finds every placement referenced in a grid's
rows, works out how far its spans already reach and its origin column,
and extends them (via a new image_extend_row(), factored out of
image_write()'s own span-building loop) up to whichever is smaller: the
image's own full width or the new pane width. window_pane_resize() calls
it after a pane grows wider.
2026-08-25 11:24:14 +01:00
Michael Grant
ebb44d4715 image: report damage narrowly instead of flagging the whole pane for redraw
image_redraw_area() used to unconditionally flag the whole pane
(PANE_REDRAW) whenever any part of it needed an image recomposited - a
pane can be much taller than the area actually disturbed (a small scroll
region, or a single line insert/delete far from where an image sits), so
this erased and retransmitted every image anywhere in the pane regardless
of proximity to the affected area. Report damage for just the area via
redraw_damage_window() instead.

image_redraw_scroll() similarly now scopes to the current scroll region
rather than redrawing the whole pane via image_redraw_all().
2026-08-25 11:24:14 +01:00
Michael Grant
e63e4fe0d0 regress: add failing test for border-status redraw bug
window_make_pane_status() gates physical redraw of border-status text on
a logical content diff, not physical damage - if something else disturbs
those cells, the text stays blank because tmux believes nothing changed.
This test currently fails and is expected to keep failing until the
general damage-tracking redraw work lands (see IMAGE-REDRAW-PLAN.md).
2026-08-25 11:23:52 +01:00
Michael Grant
f42cc5d956 regress: fix contradictory terminal-features assertions in image-support.sh
Three adjacent blocks shared one 'set -as terminal-features' setting
with contradictory requirements - fallback text, then real SIXEL, then
fallback text again in a single client attach. terminal-features is
only detected at client attach, not live, so this needs detach/reattach
between blocks rather than toggling the option in place.
2026-08-25 11:23:52 +01:00
Michael Grant
56e9ca2dbe screen-redraw: erase only cells a pane owns, not its raw rectangle
image_redraw_start() was called with a pane's full nominal geometry
rather than the cells it actually owns in the current scene. When a
floating pane occluded part of that rectangle, the erase blanked the
floating pane's on-screen content, and the following text redraw
correctly skipped those cells (not owned by this pane), leaving them
blank with nothing to restore them.
2026-08-25 11:23:52 +01:00
Michael Grant
a80e2d1af7 sixel: preserve image aspect across client geometries
Scale SIXEL rasters uniformly when clients have different cell dimensions, while retaining the original logical cell canvas as blank padding.

Map crops using real cell boundaries so partial final cells are not stretched or compressed.
2026-08-25 11:23:52 +01:00
Michael Grant
b094564203 build: rename image support option 2026-08-25 11:23:52 +01:00
Michael Grant
c0e38f3748 Cleanup and simplification. 2026-08-25 11:23:52 +01:00
Michael Grant
45f32d71ef Fix indents. 2026-08-25 11:23:52 +01:00
Michael Grant
40cd869d77 Readability and clarity. 2026-08-25 11:23:52 +01:00
Michael Grant
4c3983e286 image: add manual visual checks 2026-08-25 11:23:52 +01:00
Michael Grant
c2426fcf08 image: preserve fractional cell extents 2026-08-25 11:23:52 +01:00
Michael Grant
c97ceaffe8 image: select backend after terminal setup 2026-08-25 11:23:52 +01:00
Michael Grant
047308b156 image: retain ordered placements in the grid
Replace the per-cell image marker with sparse placement spans attached to
grid lines. A placement owns all of its spans and records the input
protocol, application image and placement IDs, z-index, and creation order.

This retains overlapping image layers without storing a list in every grid
cell. Grid operations move, split, clip, and remove only the affected spans.

Use the input protocol to determine image/text interaction: later text
damages SIXEL spans, while Kitty placements remain and are ordered by their
z-index. Rendering then adapts that one logical scene for each client,
rather than changing its semantics according to whether the outer terminal
uses Kitty or SIXEL.
2026-08-25 11:23:52 +01:00
Michael Grant
cf80845be7 Change some names of things for clarity. 2026-08-25 11:23:52 +01:00
Michael Grant
b6dbf10057 Redraw SIXEL images before or after text depending on which was placed first in the cell. 2026-08-25 11:23:52 +01:00
Michael Grant
54d3f77c2c The implementation now distinguishes a newly placed image cell from one later overwritten by text. On a SIXEL terminal, damaged cells are omitted from image output; on Kitty they remain ordinary positional placements. 2026-08-25 11:22:26 +01:00
Michael Grant
ed63826b15 Cleanup. 2026-08-25 11:22:26 +01:00
Michael Grant
b89f8b0624 Update Kitty image names 2026-08-25 11:22:26 +01:00
Michael Grant
c1cab6ea34 More cleanup of "histogram" name. 2026-08-25 11:22:26 +01:00
Michael Grant
89a7dcc8ab Shorten image and SIXEL names 2026-08-25 11:22:26 +01:00
Michael Grant
48b6641d97 Avoid overflow in image cell size calculation 2026-08-25 11:22:26 +01:00
Michael Grant
37b71800b5 Move image cell drawing into image.c 2026-08-25 11:22:26 +01:00
Michael Grant
5c9ae058bf Expose image backend flags 2026-08-25 11:22:26 +01:00
Michael Grant
db7ec915ea Do not enable sync for SIXEL 2026-08-25 11:22:26 +01:00
Michael Grant
04644c7267 Comment sixel_from_image(). 2026-08-25 11:22:26 +01:00
Michael Grant
d53f8eff9e Add comments to image functions 2026-08-25 11:22:26 +01:00
Michael Grant
03efb5e293 Preserve text behind transparent Kitty images. 2026-08-25 11:22:26 +01:00
Michael Grant
2603f84b0a Revert "Clear text beneath SIXEL image placements."
This reverts commit 64cea48f91.
2026-08-25 11:22:26 +01:00
Michael Grant
e5bcf59768 Clear text beneath SIXEL image placements. 2026-08-25 11:22:26 +01:00
Michael Grant
69ee9cf7f4 Ordinary text writes replaced the entire grid cell, dropping GRID_FLAG_IMAGE and releasing the
mage reference. Deferred pane redraws explain the intermittent “image never appears” behavior.

Fixed:
  - Preserves image metadata while updating the text underlay in the collected ASCII path: grid.c:779
  - Handles the single-cell/non-ASCII path and schedules an image redraw: screen-write.c:2631
  - Uses the requested if/else for new_id.
  - Adds a deterministic regression reproducing point (2): regress/image-support.sh:240

  This matches Kitty’s requirement that normal text operations must not affect graphics; graphics require
  explicit deletion. Kitty graphics protocol
2026-08-25 11:22:26 +01:00
Michael Grant
ae0fa8068c Preserve Kitty image crop width after cell rounding. 2026-08-25 11:22:26 +01:00
Michael Grant
3442c1f26f Fix underlay bookkeeping bug. 2026-08-25 11:22:26 +01:00
Michael Grant
b6c709307e Dither alpha in SIXEL output 2026-08-25 11:22:26 +01:00
Michael Grant
002d38cb6b Revert "Clear text beneath graphical images"
This reverts commit fb589fd9f9.
2026-08-25 11:22:26 +01:00
Michael Grant
fb931f1676 Use compact image references in grid cells 2026-08-25 11:22:26 +01:00
Michael Grant
ba2dc2cfb6 Clear text beneath graphical images 2026-08-25 11:22:26 +01:00
Michael Grant
628acfd802 Pass through wrapped Kitty graphics 2026-08-25 11:22:26 +01:00
Michael Grant
95b6adbd61 Simplify image support terminology 2026-08-25 11:22:26 +01:00
Michael Grant
0b2739c62c Use one image support switch 2026-08-25 11:22:26 +01:00
Michael Grant
596de59d11 Make image types opaque 2026-08-25 11:22:26 +01:00
Michael Grant
7d2d187db4 Inline basic image fallback renderer 2026-08-25 11:22:26 +01:00
Michael Grant
674cdd94f5 Add bptato's test to the regression test script. 2026-08-25 11:22:26 +01:00
Michael Grant
75b606c3df The two Chawan symptoms now have matching fixes:
- Layout corruption: C=1 is respected.
  - Lingering remnants: tmux now tracks Kitty’s (image ID, placement ID) identity, replaces moved placements, and honors
    placement-specific d=i,...,p=... deletion.
2026-08-25 11:22:26 +01:00
Michael Grant
d674f34f25 Support Kitty Unicode placeholder image transmission
- Parse Kitty graphics commands wrapped in tmux passthrough sequences.
  - Accept unpadded base64 payloads emitted by icat.
  - Handle chunked transfers whose final chunk omits m.
  - Track virtual image placements.
  - Convert Unicode placeholders into shared image cells for Kitty and SIXEL clients.
  - Add regression coverage for wrapped, chunked, unpadded input.
2026-08-25 11:22:26 +01:00
Michael Grant
8b44703b41 Fix SIXEL median-cut palette generation 2026-08-25 11:22:26 +01:00
Michael Grant
c0ab9b7b3f Revert "Removed unconditional Floyd–Steinberg dithering from image-sixel.c:1077. Kitty images now use the adaptive 256-colour median-cut palette with direct nearest-colour mapping."
This reverts commit ab591ae430.
2026-08-25 11:22:26 +01:00
Michael Grant
f81c41929b Fix kitty image resize issue when the kitty image resized smaller than the image, the image was overflowing lines and banding. 2026-08-25 11:22:26 +01:00
Michael Grant
cc4a76d0be Removed unconditional Floyd–Steinberg dithering from image-sixel.c:1077. Kitty images now use the adaptive 256-colour median-cut palette with direct nearest-colour mapping. 2026-08-25 11:22:26 +01:00
Michael Grant
52e2e2b0c0 C=1 is parsed and stored as an image-placement flag, suppressing cursor movement and scrolling as required by the Kitty protocol. 2026-08-25 11:22:26 +01:00
Michael Grant
e602dfa771 Keep SIXEL palettes across redraw spans 2026-08-25 11:22:26 +01:00
Michael Grant
5fc24e373a Rename image damage functions to redraw 2026-08-25 11:22:26 +01:00
Michael Grant
a4d6004d40 Correct image copyrights 2026-08-25 11:22:26 +01:00
Michael Grant
260cde9f48 Remove obsolete Kitty diacritics comment 2026-08-25 11:22:26 +01:00
Michael Grant
c8fe307682 Support Kitty source rectangle placements 2026-08-25 11:22:26 +01:00
Michael Grant
2c7a5f73c4 Improve SIXEL colour quantization
Build an adaptive 256-colour palette with median cut instead of using a fixed colour cube. Apply Floyd-Steinberg error diffusion while mapping pixels to reduce banding and preserve image detail.
2026-08-25 11:22:26 +01:00
Michael Grant
643bb662a0 Preserve SIXEL pixel dimensions
Retain the padded canonical pixel canvas separately from the image content. Scale only the populated portion of partial edge cells so an image keeps its exact raster size on the originating terminal while retaining its cell footprint.
2026-08-25 11:22:26 +01:00
Michael Grant
9731adb300 Correct SIXEL HLS colour conversion
Use the DEC hue origin and channel order, and round percentage and HLS conversions to the nearest byte value.
2026-08-25 11:22:26 +01:00
Michael Grant
52863f0fe9 Redraw images after pixel geometry changes
A resize may temporarily leave the terminal cell pixel dimensions unknown while tmux queries the terminal. Schedule another client redraw when the pixel geometry response arrives so the graphical backend replaces the temporary text fallback.
2026-08-25 11:22:26 +01:00
Michael Grant
4e7f3d5520 Use sixel images for WezTerm
Enable the sixel feature when WezTerm is detected and clarify that the kitty terminal feature requires Unicode placeholder support rather than only the basic graphics protocol.
2026-08-25 11:22:26 +01:00
Michael Grant
4e379f38d6 Remove local image ASCII test tool references 2026-08-25 11:22:26 +01:00
Michael Grant
d8da527882 Refactor to centralize image-specific rendering and lifecycle code in image.c and the image-* backends. 2026-08-25 11:22:26 +01:00
Michael Grant
5432f2ef0a Add tool to generate the kitty diacritics directly from the public UnicodeData.txt file. 2026-08-25 11:22:26 +01:00
Michael Grant
8fc9c3a5f9 Add missing regression test files. 2026-08-25 11:22:26 +01:00
Michael Grant
0852a39e31 Move image-diacritics.h into image-kitty.c. 2026-08-25 11:22:26 +01:00
Michael Grant
825b2c39b8 Fix copyrights. 2026-08-25 11:22:26 +01:00
Michael Grant
80f2ab7237 Add missing .h file. 2026-08-25 11:22:26 +01:00
Michael Grant
3e6e0a2860 New image related files. 2026-08-25 11:22:26 +01:00
Michael Grant
d601d0d8c5 Fix image flickering. 2026-08-25 11:22:26 +01:00
Michael Grant
f72e789240 Initial commit. Implement generalised image support. 2026-08-25 11:22:26 +01:00
Michael Grant
f41b983e04 screen-write, tty, popup, window: fix remaining untested damage gaps
Two more fixes based on Michael K. Darling's branch
(github.com/darlingm/tmux, pr5516-regression-fixes), taken as-is -
neither is caught by any test in regress/ yet, found by code review
rather than a failing test:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

449
active.c
View File

@@ -1,449 +0,0 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <stdlib.h>
#include "tmux.h"
/* Local last-window stack entry. */
struct active_window_entry {
u_int window;
TAILQ_ENTRY(active_window_entry) entry;
};
/* Local last-window stack. */
TAILQ_HEAD(active_window_stack, active_window_entry);
/* Per-client and per-session local active window state. */
struct active_window {
struct client *client;
struct session *session;
u_int window;
struct active_window_stack lastw;
RB_ENTRY(active_window) entry;
};
RB_HEAD(active_windows, active_window);
static int
active_window_cmp(struct active_window *aw1, struct active_window *aw2)
{
uintptr_t c1 = (uintptr_t)aw1->client;
uintptr_t c2 = (uintptr_t)aw2->client;
uintptr_t s1 = (uintptr_t)aw1->session;
uintptr_t s2 = (uintptr_t)aw2->session;
if (c1 < c2)
return (-1);
if (c1 > c2)
return (1);
if (s1 < s2)
return (-1);
if (s1 > s2)
return (1);
return (0);
}
RB_GENERATE_STATIC(active_windows, active_window, entry, active_window_cmp);
static struct active_window *active_get(struct client *, struct session *);
static struct active_window *active_add(struct client *, struct session *,
struct winlink *);
static void active_free(struct active_window *);
static struct winlink *active_resolve(struct active_window *);
static void active_stack_remove(struct active_window_stack *, u_int);
static void active_stack_push(struct active_window_stack *, u_int);
static void active_changed(struct client *, struct session *,
struct winlink *, struct winlink *);
/* All local active window states. */
static struct active_windows active_windows = RB_INITIALIZER(&active_windows);
/* Find local active window state for a client and session. */
static struct active_window *
active_get(struct client *c, struct session *s)
{
struct active_window aw;
if (c == NULL || s == NULL)
return (NULL);
aw.client = c;
aw.session = s;
return (RB_FIND(active_windows, &active_windows, &aw));
}
/* Add local active window state for a client and session. */
static struct active_window *
active_add(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
aw = active_get(c, s);
if (aw == NULL) {
aw = xcalloc(1, sizeof *aw);
aw->client = c;
aw->session = s;
TAILQ_INIT(&aw->lastw);
RB_INSERT(active_windows, &active_windows, aw);
}
if (wl != NULL)
aw->window = wl->window->id;
return (aw);
}
/* Free local active window state. */
static void
active_free(struct active_window *aw)
{
struct active_window_entry *awe;
while (!TAILQ_EMPTY(&aw->lastw)) {
awe = TAILQ_FIRST(&aw->lastw);
TAILQ_REMOVE(&aw->lastw, awe, entry);
free(awe);
}
RB_REMOVE(active_windows, &active_windows, aw);
free(aw);
}
/* Resolve local active window state to a valid winlink. */
static struct winlink *
active_resolve(struct active_window *aw)
{
struct winlink *wl, *next;
u_int window;
if (aw == NULL || aw->session->curw == NULL)
return (NULL);
wl = winlink_find_by_window_id(&aw->session->windows, aw->window);
if (wl != NULL)
return (wl);
while (!TAILQ_EMPTY(&aw->lastw)) {
window = TAILQ_FIRST(&aw->lastw)->window;
next = winlink_find_by_window_id(&aw->session->windows, window);
active_stack_remove(&aw->lastw, window);
if (next != NULL) {
aw->window = next->window->id;
return (next);
}
}
aw->window = aw->session->curw->window->id;
return (aw->session->curw);
}
/* Remove a window from a local last-window stack. */
static void
active_stack_remove(struct active_window_stack *stack, u_int window)
{
struct active_window_entry *awe, *awe1;
TAILQ_FOREACH_SAFE(awe, stack, entry, awe1) {
if (awe->window == window) {
TAILQ_REMOVE(stack, awe, entry);
free(awe);
}
}
}
/* Push a window onto a local last-window stack. */
static void
active_stack_push(struct active_window_stack *stack, u_int window)
{
struct active_window_entry *awe;
active_stack_remove(stack, window);
awe = xcalloc(1, sizeof *awe);
awe->window = window;
TAILQ_INSERT_HEAD(stack, awe, entry);
}
/* Notify the server that a client's effective window changed. */
static void
active_changed(struct client *c, struct session *s, struct winlink *wl,
struct winlink *old)
{
struct event_payload *ep;
struct cmd_find_state fs;
if (wl == NULL || wl == old)
return;
if (options_get_number(global_options, "focus-events")) {
if (old != NULL)
window_update_focus(old->window);
window_update_focus(wl->window);
}
winlink_clear_flags(wl);
window_update_activity(wl->window);
tty_update_window_offset(wl->window);
if (c != NULL && c->session == s)
wl->window->latest = c;
ep = event_payload_create();
cmd_find_from_winlink(&fs, wl, 0);
event_payload_set_target(ep, &fs);
event_payload_set_client(ep, "client", c);
event_payload_set_session(ep, "session", s);
event_payload_set_window(ep, "window", wl->window);
event_payload_set_window(ep, "new_window", wl->window);
event_payload_set_int(ep, "window_index", wl->idx);
event_payload_set_int(ep, "new_window_index", wl->idx);
if (old != NULL) {
event_payload_set_window(ep, "old_window", old->window);
event_payload_set_int(ep, "old_window_index", old->idx);
}
events_fire("client-window-changed", ep);
if (c != NULL) {
c->flags |= CLIENT_REDRAWWINDOW|CLIENT_REDRAWSTATUS|
CLIENT_REDRAWBORDERS;
tty_update_client_offset(c);
status_update_cache(s);
}
recalculate_sizes();
}
/* Return if a client has local window selection for a session. */
int
active_is_local_window(struct client *c, struct session *s)
{
return (active_get(c, s) != NULL);
}
/* Make a client use local or shared window selection for a session. */
void
active_set_local_window(struct client *c, struct session *s,
enum active_window_mode mode)
{
struct active_window *aw;
struct winlink *wl, *old;
if (c == NULL || s == NULL || s->curw == NULL)
return;
old = active_get_effective_winlink(c, s);
if (mode == ACTIVE_LOCAL) {
active_add(c, s, old);
if (c != NULL && c->session == s)
c->flags |= CLIENT_REDRAWSTATUS;
return;
}
aw = active_get(c, s);
if (aw != NULL) {
active_free(aw);
wl = active_get_effective_winlink(c, s);
active_changed(c, s, wl, old);
if (c != NULL && c->session == s)
c->flags |= CLIENT_REDRAWSTATUS;
}
}
/* Return the effective winlink for a client and session. */
struct winlink *
active_get_effective_winlink(struct client *c, struct session *s)
{
struct active_window *aw;
if (s == NULL)
return (NULL);
aw = active_get(c, s);
if (aw != NULL)
return (active_resolve(aw));
return (s->curw);
}
/* Return the effective window for a client and session. */
struct window *
active_get_effective_window(struct client *c, struct session *s)
{
struct winlink *wl;
wl = active_get_effective_winlink(c, s);
if (wl == NULL)
return (NULL);
return (wl->window);
}
/* Select a winlink for a client and session. */
int
active_select_window(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
struct winlink *old;
int changed;
if (wl == NULL)
return (-1);
aw = active_get(c, s);
if (aw == NULL)
return (session_set_current(s, wl));
old = active_resolve(aw);
if (old == wl)
return (1);
if (old != NULL)
active_stack_push(&aw->lastw, old->window->id);
active_stack_remove(&aw->lastw, wl->window->id);
aw->window = wl->window->id;
changed = (old != wl);
active_changed(c, s, wl, old);
return (changed ? 0 : 1);
}
/* Select a window by index. */
int
active_select_window_index(struct client *c, struct session *s, int idx)
{
struct winlink *wl;
wl = winlink_find_by_index(&s->windows, idx);
return (active_select_window(c, s, wl));
}
/* Select the next window. */
int
active_next_window(struct client *c, struct session *s, int alert)
{
struct winlink *wl;
wl = active_get_effective_winlink(c, s);
if (wl == NULL)
return (-1);
do {
wl = winlink_next(wl);
if (wl == NULL)
wl = RB_MIN(winlinks, &s->windows);
if (!alert || (wl->flags & WINLINK_ALERTFLAGS))
return (active_select_window(c, s, wl));
} while (wl != active_get_effective_winlink(c, s));
return (-1);
}
/* Select the previous window. */
int
active_previous_window(struct client *c, struct session *s, int alert)
{
struct winlink *wl;
wl = active_get_effective_winlink(c, s);
if (wl == NULL)
return (-1);
do {
wl = winlink_previous(wl);
if (wl == NULL)
wl = RB_MAX(winlinks, &s->windows);
if (!alert || (wl->flags & WINLINK_ALERTFLAGS))
return (active_select_window(c, s, wl));
} while (wl != active_get_effective_winlink(c, s));
return (-1);
}
/* Select the last window. */
int
active_last_window(struct client *c, struct session *s)
{
struct active_window *aw;
struct winlink *wl;
u_int window;
aw = active_get(c, s);
if (aw == NULL)
return (session_last(s));
while (!TAILQ_EMPTY(&aw->lastw)) {
window = TAILQ_FIRST(&aw->lastw)->window;
wl = winlink_find_by_window_id(&s->windows, window);
if (wl != NULL)
return (active_select_window(c, s, wl));
active_stack_remove(&aw->lastw, window);
}
return (-1);
}
/* Remove all state for a client. */
void
active_remove_client(struct client *c)
{
struct active_window *aw, *aw1;
RB_FOREACH_SAFE(aw, active_windows, &active_windows, aw1) {
if (aw->client == c)
active_free(aw);
}
}
/* Remove all state for a session. */
void
active_remove_session(struct session *s)
{
struct active_window *aw, *aw1;
RB_FOREACH_SAFE(aw, active_windows, &active_windows, aw1) {
if (aw->session == s)
active_free(aw);
}
}
/* Remove a window from all local state. */
void
active_remove_window(struct window *w)
{
struct active_window *aw;
RB_FOREACH(aw, active_windows, &active_windows) {
active_stack_remove(&aw->lastw, w->id);
if (aw->window == w->id && aw->session->curw != NULL)
aw->window = aw->session->curw->window->id;
}
}
/* Return if this winlink is the effective winlink. */
int
active_is_effective_window(struct client *c, struct session *s,
struct winlink *wl)
{
return (wl != NULL && wl == active_get_effective_winlink(c, s));
}
/* Return if this winlink is the last effective winlink. */
int
active_is_last_window(struct client *c, struct session *s, struct winlink *wl)
{
struct active_window *aw;
struct active_window_entry *awe;
if (wl == NULL)
return (0);
aw = active_get(c, s);
if (aw == NULL)
return (wl == TAILQ_FIRST(&s->lastw));
TAILQ_FOREACH(awe, &aw->lastw, entry) {
if (winlink_find_by_window_id(&s->windows, awe->window) == NULL)
continue;
return (wl->window->id == awe->window);
}
return (0);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */ /* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -483,7 +483,7 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
char *title, *cause = NULL; char *title, *cause = NULL;
int flags = 0, starting_choice = 0; int flags = 0, starting_choice = 0;
u_int px, py, i, count = args_count(args); u_int px, py, i, count = args_count(args);
struct options *o = target->w->options; struct options *o = target->s->curw->window->options;
struct options_entry *oe; struct options_entry *oe;
if (args_has(args, 'C')) { if (args_has(args, 'C')) {
@@ -584,15 +584,13 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
u_int px, py, w, h, count = args_count(args); u_int px, py, w, h, count = args_count(args);
struct args_value *av; struct args_value *av;
struct environ *env = NULL; struct environ *env = NULL;
struct options *o = target->w->options; struct options *o = s->curw->window->options;
struct options_entry *oe; struct options_entry *oe;
if (args_has(args, 'C')) { if (args_has(args, 'C')) {
server_client_clear_overlay(tc); server_client_clear_overlay(tc);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL);
if (!modify && tc->overlay_draw != NULL) if (!modify && tc->overlay_draw != NULL)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);

View File

@@ -884,13 +884,13 @@ cmd_find_from_client(struct cmd_find_state *fs, struct client *c, int flags)
if (c->session != NULL) { if (c->session != NULL) {
cmd_find_clear_state(fs, flags); cmd_find_clear_state(fs, flags);
fs->wl = active_get_effective_winlink(c, c->session); fs->wp = c->session->curw->window->active;
fs->wp = fs->wl->window->active;
if (fs->wp == NULL) { if (fs->wp == NULL) {
cmd_find_from_session(fs, c->session, flags); cmd_find_from_session(fs, c->session, flags);
return (0); return (0);
} }
fs->s = c->session; fs->s = c->session;
fs->wl = fs->s->curw;
fs->w = fs->wl->window; fs->w = fs->wl->window;
cmd_find_log_state(__func__, fs); cmd_find_log_state(__func__, fs);
@@ -931,57 +931,6 @@ unknown_pane:
return (cmd_find_from_nothing(fs, flags)); return (cmd_find_from_nothing(fs, flags));
} }
/* Find state from mouse target. */
static int
cmd_find_target_from_mouse(struct cmd_find_state *fs, struct cmdq_item *item,
enum cmd_find_type type)
{
struct mouse_event *m = &cmdq_get_event(item)->m;
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
switch (type) {
case CMD_FIND_PANE:
if (m->s != -1 && m->w == -1 && m->wp == -1 &&
(fs->s = session_find_by_id(m->s)) != NULL) {
if (tc != NULL && tc->session == fs->s)
fs->wl = active_get_effective_winlink(tc, fs->s);
else if (c != NULL && c->session == fs->s)
fs->wl = active_get_effective_winlink(c, fs->s);
else
fs->wl = fs->s->curw;
if (fs->wl != NULL) {
fs->w = fs->wl->window;
fs->wp = fs->w->active;
break;
}
}
fs->wp = cmd_mouse_pane(m, &fs->s, &fs->wl);
if (fs->wp != NULL) {
fs->w = fs->wl->window;
break;
}
/* FALLTHROUGH */
case CMD_FIND_WINDOW:
case CMD_FIND_SESSION:
fs->wl = cmd_mouse_window(m, &fs->s);
if (fs->s != NULL && m->w == -1 &&
tc != NULL && tc->session == fs->s)
fs->wl = active_get_effective_winlink(tc, fs->s);
else if (fs->s != NULL && m->w == -1 &&
c != NULL && c->session == fs->s)
fs->wl = active_get_effective_winlink(c, fs->s);
if (fs->wl == NULL && fs->s != NULL)
fs->wl = fs->s->curw;
if (fs->wl != NULL) {
fs->w = fs->wl->window;
fs->wp = fs->w->active;
}
break;
}
return (fs->wp == NULL ? -1 : 0);
}
/* /*
* Split target into pieces and resolve for the given type. Fills in the given * Split target into pieces and resolve for the given type. Fills in the given
* state. Returns 0 on success or -1 on error. * state. Returns 0 on success or -1 on error.
@@ -990,6 +939,7 @@ int
cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item, cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item,
const char *target, enum cmd_find_type type, int flags) const char *target, enum cmd_find_type type, int flags)
{ {
struct mouse_event *m;
struct client *c; struct client *c;
struct cmd_find_state current; struct cmd_find_state current;
char *colon, *period, *copy = NULL, tmp[256]; char *colon, *period, *copy = NULL, tmp[256];
@@ -1065,15 +1015,35 @@ cmd_find_target(struct cmd_find_state *fs, struct cmdq_item *item,
cmdq_error(item, "no current client"); cmdq_error(item, "no current client");
goto error; goto error;
} }
fs->wl = active_get_effective_winlink(c, c->session); fs->wl = c->session->curw;
fs->wp = fs->wl->window->active; fs->wp = c->session->curw->window->active;
fs->w = fs->wl->window; fs->w = c->session->curw->window;
goto found; goto found;
} }
/* Mouse target is a plain = or {mouse}. */ /* Mouse target is a plain = or {mouse}. */
if (strcmp(target, "=") == 0 || strcmp(target, "{mouse}") == 0) { if (strcmp(target, "=") == 0 || strcmp(target, "{mouse}") == 0) {
if (cmd_find_target_from_mouse(fs, item, type) != 0) { m = &cmdq_get_event(item)->m;
switch (type) {
case CMD_FIND_PANE:
fs->wp = cmd_mouse_pane(m, &fs->s, &fs->wl);
if (fs->wp != NULL) {
fs->w = fs->wl->window;
break;
}
/* FALLTHROUGH */
case CMD_FIND_WINDOW:
case CMD_FIND_SESSION:
fs->wl = cmd_mouse_window(m, &fs->s);
if (fs->wl == NULL && fs->s != NULL)
fs->wl = fs->s->curw;
if (fs->wl != NULL) {
fs->w = fs->wl->window;
fs->wp = fs->w->active;
}
break;
}
if (fs->wp == NULL) {
if (~flags & CMD_FIND_QUIET) if (~flags & CMD_FIND_QUIET)
cmdq_error(item, "no mouse target"); cmdq_error(item, "no mouse target");
goto error; goto error;

View File

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

View File

@@ -300,6 +300,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
struct window_pane *wp; struct window_pane *wp;
struct layout_cell *lc; struct layout_cell *lc;
int y, ly, x, lx; int y, ly, x, lx;
int old_xoff, old_yoff, old_sx, old_sy;
wp = cmd_mouse_pane(m, NULL, &wl); wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) { if (wp == NULL) {
@@ -321,10 +322,17 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1; ly = m->statusat - 1;
if (x != lx || y != ly) { if (x != lx || y != ly) {
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = wp->sx;
old_sy = wp->sy;
lc->g.xoff += x - lx; lc->g.xoff += x - lx;
lc->g.yoff += y - ly; lc->g.yoff += y - ly;
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w); server_redraw_window_borders(w);
} }
} }
@@ -413,16 +421,13 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *current = cmdq_get_current(item); struct cmd_find_state *current = cmdq_get_current(item);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct cmd_find_state *source = cmdq_get_source(item); struct cmd_find_state *source = cmdq_get_source(item);
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct client *ac;
struct session *dst_s; struct session *dst_s;
struct winlink *src_wl, *dst_wl; struct winlink *src_wl, *dst_wl;
struct window *src_w, *dst_w; struct window *src_w, *dst_w;
struct window_pane *src_wp, *dst_wp; struct window_pane *src_wp, *dst_wp;
const char *s; const char *s;
char *cause = NULL; char *cause = NULL;
int flags = 0, dst_idx, local; int flags = 0, dst_idx;
struct layout_cell *lc; struct layout_cell *lc;
dst_s = target->s; dst_s = target->s;
@@ -430,10 +435,6 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
dst_wp = target->wp; dst_wp = target->wp;
dst_w = dst_wl->window; dst_w = dst_wl->window;
dst_idx = dst_wl->idx; dst_idx = dst_wl->idx;
ac = tc;
if (ac == NULL || ac->session != dst_s)
ac = c;
local = active_is_local_window(ac, dst_s);
if (cmd_get_entry(self) == &cmd_move_pane_entry) { if (cmd_get_entry(self) == &cmd_move_pane_entry) {
if (args_has(args, 'M')) if (args_has(args, 'M'))
@@ -519,18 +520,9 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
if (!args_has(args, 'd')) { if (!args_has(args, 'd')) {
window_set_active_pane(dst_w, src_wp, 1); window_set_active_pane(dst_w, src_wp, 1);
if (local) session_select(dst_s, dst_idx);
active_select_window(ac, dst_s, dst_wl); cmd_find_from_session(current, dst_s, 0);
else server_redraw_session(dst_s);
session_select(dst_s, dst_idx);
if (local && ac != NULL && ac->session == dst_s)
cmd_find_from_client(current, ac, 0);
else
cmd_find_from_session(current, dst_s, 0);
if (local && ac != NULL && ac->session == dst_s)
server_redraw_client(ac);
else
server_redraw_session(dst_s);
} else } else
server_status_session(dst_s); server_status_session(dst_s);

View File

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

View File

@@ -58,11 +58,9 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct spawn_context sc = { 0 }; struct spawn_context sc = { 0 };
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct client *ac;
struct session *s = target->s; struct session *s = target->s;
struct winlink *wl = target->wl, *new_wl = NULL; struct winlink *wl = target->wl, *new_wl = NULL;
int idx = target->idx, before, count = args_count(args); int idx = target->idx, before, count = args_count(args);
int detached, local;
char *cause = NULL, *cp, *expanded, *wname = NULL; char *cause = NULL, *cp, *expanded, *wname = NULL;
const char *template, *name; const char *template, *name;
struct cmd_find_state fs; struct cmd_find_state fs;
@@ -109,15 +107,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
free(wname); free(wname);
if (args_has(args, 'd')) if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
if (active_select_window(c, s, new_wl) == 0) { if (session_set_current(s, new_wl) == 0)
if (active_is_local_window(c, s) && c != NULL && server_redraw_session(s);
c->session == s)
server_redraw_client(c);
else
server_redraw_session(s);
}
if (c != NULL && c->session != NULL) if (c != NULL && c->session != NULL)
active_get_effective_window(c, s)->latest = c; s->curw->window->latest = c;
recalculate_sizes(); recalculate_sizes();
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
@@ -147,16 +140,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
sc.idx = idx; sc.idx = idx;
sc.cwd = args_get(args, 'c'); sc.cwd = args_get(args, 'c');
ac = tc;
if (ac == NULL || ac->session != s)
ac = c;
detached = args_has(args, 'd');
local = (!detached && active_is_local_window(ac, s));
sc.flags = 0; sc.flags = 0;
if (args_has(args, 'E') || (count == 1 && *args_string(args, 0) == '\0')) if (args_has(args, 'E') || (count == 1 && *args_string(args, 0) == '\0'))
sc.flags |= SPAWN_EMPTY; sc.flags |= SPAWN_EMPTY;
if (detached || local) if (args_has(args, 'd'))
sc.flags |= SPAWN_DETACHED; sc.flags |= SPAWN_DETACHED;
if (args_has(args, 'k')) if (args_has(args, 'k'))
sc.flags |= SPAWN_KILL; sc.flags |= SPAWN_KILL;
@@ -166,13 +153,7 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
free(cause); free(cause);
goto fail; goto fail;
} }
if (local) { if (!args_has(args, 'd') || new_wl == s->curw) {
if (active_select_window(ac, s, new_wl) == 0)
cmd_find_from_winlink(current, new_wl, 0);
if (ac != NULL && ac->session == s)
server_redraw_client(ac);
server_status_session_group(s);
} else if (!detached || new_wl == s->curw) {
cmd_find_from_winlink(current, new_wl, 0); cmd_find_from_winlink(current, new_wl, 0);
server_redraw_session_group(s); server_redraw_session_group(s);
} else } else

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */ /* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 nicm Exp $ */
/* /*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org> * Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -79,10 +79,7 @@ static void
cmd_run_shell_print(struct job *job, const char *msg) cmd_run_shell_print(struct job *job, const char *msg)
{ {
struct cmd_run_shell_data *cdata = job_get_data(job); struct cmd_run_shell_data *cdata = job_get_data(job);
struct client *c;
struct session *s;
struct window_pane *wp = NULL; struct window_pane *wp = NULL;
struct window *w;
struct cmd_find_state fs; struct cmd_find_state fs;
struct window_mode_entry *wme; struct window_mode_entry *wme;
@@ -93,12 +90,8 @@ cmd_run_shell_print(struct job *job, const char *msg)
cmdq_print(cdata->item, "%s", msg); cmdq_print(cdata->item, "%s", msg);
return; return;
} }
c = cdata->client; if (cdata->client != NULL && cdata->client->session != NULL)
if (c != NULL && c->session != NULL) { wp = cdata->client->session->curw->window->active;
s = c->session;
w = active_get_effective_window(c, s);
wp = w->active;
}
if (wp == NULL && cmd_find_from_nothing(&fs, 0) == 0) if (wp == NULL && cmd_find_from_nothing(&fs, 0) == 0)
wp = fs.wp; wp = fs.wp;
if (wp == NULL) if (wp == NULL)
@@ -240,11 +233,9 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg)
} else if (item == NULL) { } else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL); new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else { } else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item)); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
} }
if (cdata->item != NULL) if (cdata->item != NULL)

View File

@@ -68,11 +68,10 @@ cmd_select_pane_redraw(struct window *w)
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (c->session == NULL || (c->flags & CLIENT_CONTROL)) if (c->session == NULL || (c->flags & CLIENT_CONTROL))
continue; continue;
if (active_get_effective_window(c, c->session) == w && if (c->session->curw->window == w && tty_window_bigger(&c->tty))
tty_window_bigger(&c->tty))
server_redraw_client(c); server_redraw_client(c);
else { else {
if (active_get_effective_window(c, c->session) == w) if (c->session->curw->window == w)
c->flags |= CLIENT_REDRAWBORDERS; c->flags |= CLIENT_REDRAWBORDERS;
if (session_has(c->session, w)) if (session_has(c->session, w))
c->flags |= CLIENT_REDRAWSTATUS; c->flags |= CLIENT_REDRAWSTATUS;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

161
format.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */ /* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -806,7 +806,7 @@ format_cb_window_active_clients(struct format_tree *ft)
if (client_session == NULL) if (client_session == NULL)
continue; continue;
if (w == active_get_effective_window(loop, client_session)) if (w == client_session->curw->window)
n++; n++;
} }
@@ -838,7 +838,7 @@ format_cb_window_active_clients_list(struct format_tree *ft)
if (client_session == NULL) if (client_session == NULL)
continue; continue;
if (w == active_get_effective_window(loop, client_session)) { if (w == client_session->curw->window) {
if (EVBUFFER_LENGTH(buffer) > 0) if (EVBUFFER_LENGTH(buffer) > 0)
evbuffer_add(buffer, ",", 1); evbuffer_add(buffer, ",", 1);
evbuffer_add_printf(buffer, "%s", loop->name); evbuffer_add_printf(buffer, "%s", loop->name);
@@ -1887,37 +1887,6 @@ format_cb_cursor_blinking(struct format_tree *ft)
return (NULL); return (NULL);
} }
/* Callback for history_added. */
static void *
format_cb_history_added(struct format_tree *ft)
{
if (ft->wp != NULL)
return (format_printf("%u", ft->wp->base.grid->scroll_added));
return (NULL);
}
/* Callback for history_collected. */
static void *
format_cb_history_collected(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
if (wp != NULL)
return (format_printf("%u", wp->base.grid->scroll_collected));
return (NULL);
}
/* Callback for history_generation. */
static void *
format_cb_history_generation(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
if (wp != NULL)
return (format_printf("%u", wp->base.grid->scroll_generation));
return (NULL);
}
/* Callback for history_limit. */ /* Callback for history_limit. */
static void * static void *
format_cb_history_limit(struct format_tree *ft) format_cb_history_limit(struct format_tree *ft)
@@ -2307,19 +2276,6 @@ format_cb_pane_last_output_time(struct format_tree *ft)
return (NULL); return (NULL);
} }
/* Callback for pane_output_generation. */
static void *
format_cb_pane_output_generation(struct format_tree *ft)
{
unsigned long long value;
if (ft->wp != NULL) {
value = ft->wp->output_generation;
return (format_printf("%llu", value));
}
return (NULL);
}
/* Callback for pane_last_prompt_time. */ /* Callback for pane_last_prompt_time. */
static void * static void *
format_cb_pane_last_prompt_time(struct format_tree *ft) format_cb_pane_last_prompt_time(struct format_tree *ft)
@@ -3062,52 +3018,26 @@ format_cb_sixel_support(__unused struct format_tree *ft)
#endif #endif
} }
/* Callback for image_support. */
static void *
format_cb_image_support(__unused struct format_tree *ft)
{
#ifdef ENABLE_IMAGES
return (xstrdup("1"));
#else
return (xstrdup("0"));
#endif
}
/* Callback for active_window_index. */ /* Callback for active_window_index. */
static void * static void *
format_cb_active_window_index(struct format_tree *ft) format_cb_active_window_index(struct format_tree *ft)
{ {
struct winlink *wl; if (ft->s != NULL)
return (format_printf("%u", ft->s->curw->idx));
if (ft->s != NULL) {
wl = active_get_effective_winlink(ft->c, ft->s);
if (wl != NULL)
return (format_printf("%u", wl->idx));
}
return (NULL); return (NULL);
} }
/* Callback for active_window_id. */
static void *
format_cb_active_window_id(struct format_tree *ft)
{
struct winlink *wl;
if (ft->s != NULL) {
wl = active_get_effective_winlink(ft->c, ft->s);
if (wl != NULL)
return (format_printf("@%u", wl->window->id));
}
return (NULL);
}
/* Callback for local_active_window. */
static void *
format_cb_local_active_window(struct format_tree *ft)
{
if (ft->c != NULL && ft->s != NULL)
return (format_printf("%d", active_is_local_window(ft->c, ft->s)));
return (xstrdup("0"));
}
/* Callback for active_window_mode. */
static void *
format_cb_active_window_mode(struct format_tree *ft)
{
if (ft->c != NULL && ft->s != NULL && active_is_local_window(ft->c, ft->s))
return (xstrdup("local"));
return (xstrdup("shared"));
}
/* Callback for last_window_index. */ /* Callback for last_window_index. */
static void * static void *
format_cb_last_window_index(struct format_tree *ft) format_cb_last_window_index(struct format_tree *ft)
@@ -3125,11 +3055,8 @@ format_cb_last_window_index(struct format_tree *ft)
static void * static void *
format_cb_window_active(struct format_tree *ft) format_cb_window_active(struct format_tree *ft)
{ {
struct winlink *wl;
if (ft->wl != NULL) { if (ft->wl != NULL) {
wl = active_get_effective_winlink(ft->c, ft->wl->session); if (ft->wl == ft->wl->session->curw)
if (ft->wl == wl)
return (xstrdup("1")); return (xstrdup("1"));
return (xstrdup("0")); return (xstrdup("0"));
} }
@@ -3209,7 +3136,7 @@ static void *
format_cb_window_flags(struct format_tree *ft) format_cb_window_flags(struct format_tree *ft)
{ {
if (ft->wl != NULL) if (ft->wl != NULL)
return (xstrdup(window_printable_flags(ft->c, ft->wl, 1))); return (xstrdup(window_printable_flags(ft->wl, 1)));
return (NULL); return (NULL);
} }
@@ -3396,7 +3323,7 @@ static void *
format_cb_window_raw_flags(struct format_tree *ft) format_cb_window_raw_flags(struct format_tree *ft)
{ {
if (ft->wl != NULL) if (ft->wl != NULL)
return (xstrdup(window_printable_flags(ft->c, ft->wl, 0))); return (xstrdup(window_printable_flags(ft->wl, 0)));
return (NULL); return (NULL);
} }
@@ -3606,15 +3533,9 @@ struct format_table_entry {
* Only variables which are added by the caller go into the tree. * Only variables which are added by the caller go into the tree.
*/ */
static const struct format_table_entry format_table[] = { static const struct format_table_entry format_table[] = {
{ "active_window_id", FORMAT_TABLE_STRING,
format_cb_active_window_id
},
{ "active_window_index", FORMAT_TABLE_STRING, { "active_window_index", FORMAT_TABLE_STRING,
format_cb_active_window_index format_cb_active_window_index
}, },
{ "active_window_mode", FORMAT_TABLE_STRING,
format_cb_active_window_mode
},
{ "alternate_on", FORMAT_TABLE_STRING, { "alternate_on", FORMAT_TABLE_STRING,
format_cb_alternate_on format_cb_alternate_on
}, },
@@ -3753,21 +3674,12 @@ static const struct format_table_entry format_table[] = {
{ "cursor_y", FORMAT_TABLE_STRING, { "cursor_y", FORMAT_TABLE_STRING,
format_cb_cursor_y format_cb_cursor_y
}, },
{ "history_added", FORMAT_TABLE_STRING,
format_cb_history_added
},
{ "history_all_bytes", FORMAT_TABLE_STRING, { "history_all_bytes", FORMAT_TABLE_STRING,
format_cb_history_all_bytes format_cb_history_all_bytes
}, },
{ "history_bytes", FORMAT_TABLE_STRING, { "history_bytes", FORMAT_TABLE_STRING,
format_cb_history_bytes format_cb_history_bytes
}, },
{ "history_collected", FORMAT_TABLE_STRING,
format_cb_history_collected
},
{ "history_generation", FORMAT_TABLE_STRING,
format_cb_history_generation
},
{ "history_limit", FORMAT_TABLE_STRING, { "history_limit", FORMAT_TABLE_STRING,
format_cb_history_limit format_cb_history_limit
}, },
@@ -3780,6 +3692,9 @@ static const struct format_table_entry format_table[] = {
{ "host_short", FORMAT_TABLE_STRING, { "host_short", FORMAT_TABLE_STRING,
format_cb_host_short format_cb_host_short
}, },
{ "image_support", FORMAT_TABLE_STRING,
format_cb_image_support
},
{ "insert_flag", FORMAT_TABLE_STRING, { "insert_flag", FORMAT_TABLE_STRING,
format_cb_insert_flag format_cb_insert_flag
}, },
@@ -3792,9 +3707,6 @@ static const struct format_table_entry format_table[] = {
{ "last_window_index", FORMAT_TABLE_STRING, { "last_window_index", FORMAT_TABLE_STRING,
format_cb_last_window_index format_cb_last_window_index
}, },
{ "local_active_window", FORMAT_TABLE_STRING,
format_cb_local_active_window
},
{ "mouse_all_flag", FORMAT_TABLE_STRING, { "mouse_all_flag", FORMAT_TABLE_STRING,
format_cb_mouse_all_flag format_cb_mouse_all_flag
}, },
@@ -3951,9 +3863,6 @@ static const struct format_table_entry format_table[] = {
{ "pane_mode", FORMAT_TABLE_STRING, { "pane_mode", FORMAT_TABLE_STRING,
format_cb_pane_mode format_cb_pane_mode
}, },
{ "pane_output_generation", FORMAT_TABLE_STRING,
format_cb_pane_output_generation
},
{ "pane_path", FORMAT_TABLE_STRING, { "pane_path", FORMAT_TABLE_STRING,
format_cb_pane_path format_cb_pane_path
}, },
@@ -5344,18 +5253,15 @@ format_add_window_neighbour(struct format_tree *nft, struct winlink *wl,
struct session *s, const char *prefix) struct session *s, const char *prefix)
{ {
struct options_entry *o; struct options_entry *o;
struct winlink *current;
const char *oname; const char *oname;
char *key, *prefixed, *oval; char *key, *prefixed, *oval;
current = active_get_effective_winlink(nft->c, s);
xasprintf(&key, "%s_window_index", prefix); xasprintf(&key, "%s_window_index", prefix);
format_add(nft, key, "%u", wl->idx); format_add(nft, key, "%u", wl->idx);
free(key); free(key);
xasprintf(&key, "%s_window_active", prefix); xasprintf(&key, "%s_window_active", prefix);
format_add(nft, key, "%d", wl == current); format_add(nft, key, "%d", wl == s->curw);
free(key); free(key);
o = options_first(wl->window->options); o = options_first(wl->window->options);
@@ -5383,10 +5289,10 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
struct cmdq_item *item = ft->item; struct cmdq_item *item = ft->item;
struct format_tree *nft; struct format_tree *nft;
struct format_expand_state next; struct format_expand_state next;
char *all, *active_fmt, *use, *expanded, *value; char *all, *active, *use, *expanded, *value;
struct evbuffer *buffer; struct evbuffer *buffer;
size_t size; size_t size;
struct winlink *wl, **l, *active_wl; struct winlink *wl, **l;
struct window *w; struct window *w;
int i, n; int i, n;
@@ -5395,9 +5301,9 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
return (NULL); return (NULL);
} }
if (format_choose(es, fmt, &all, &active_fmt, 0) != 0) { if (format_choose(es, fmt, &all, &active, 0) != 0) {
all = xstrdup(fmt); all = xstrdup(fmt);
active_fmt = NULL; active = NULL;
} }
buffer = evbuffer_new(); buffer = evbuffer_new();
@@ -5405,13 +5311,12 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
fatalx("out of memory"); fatalx("out of memory");
l = sort_get_winlinks_session(s, &n, sc); l = sort_get_winlinks_session(s, &n, sc);
active_wl = active_get_effective_winlink(ft->c, s);
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
wl = l[i]; wl = l[i];
w = wl->window; w = wl->window;
format_log(es, "window loop: %u @%u", wl->idx, w->id); format_log(es, "window loop: %u @%u", wl->idx, w->id);
if (active_fmt != NULL && wl == active_wl) if (active != NULL && wl == s->curw)
use = active_fmt; use = active;
else else
use = all; use = all;
nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags); nft = format_create(c, item, FORMAT_WINDOW|w->id, ft->flags);
@@ -5420,11 +5325,11 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
format_add(nft, "loop_last_flag", "%d", i == n - 1); format_add(nft, "loop_last_flag", "%d", i == n - 1);
/* Add neighbour window data to the format tree. */ /* Add neighbour window data to the format tree. */
if (i > 0 && l[i - 1] == active_wl) if (i > 0 && l[i - 1] == s->curw)
format_add(nft, "window_after_active", "1"); format_add(nft, "window_after_active", "1");
else else
format_add(nft, "window_after_active", "0"); format_add(nft, "window_after_active", "0");
if (i + 1 < n && l[i + 1] == active_wl) if (i + 1 < n && l[i + 1] == s->curw)
format_add(nft, "window_before_active", "1"); format_add(nft, "window_before_active", "1");
else else
format_add(nft, "window_before_active", "0"); format_add(nft, "window_before_active", "0");
@@ -5444,7 +5349,7 @@ format_loop_windows(struct format_expand_state *es, const char *fmt)
free(expanded); free(expanded);
} }
free(active_fmt); free(active);
free(all); free(all);
if ((size = EVBUFFER_LENGTH(buffer)) != 0) if ((size = EVBUFFER_LENGTH(buffer)) != 0)
@@ -7032,7 +6937,7 @@ format_defaults(struct format_tree *ft, struct client *c, struct session *s,
if (s == NULL && c != NULL) if (s == NULL && c != NULL)
s = c->session; s = c->session;
if (wl == NULL && s != NULL) if (wl == NULL && s != NULL)
wl = active_get_effective_winlink(c, s); wl = s->curw;
if (wp == NULL && wl != NULL) if (wp == NULL && wl != NULL)
wp = wl->window->active; wp = wl->window->active;

120
grid.c
View File

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

464
image-fallback.c Normal file
View File

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

1308
image-kitty.c Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1745
image.c

File diff suppressed because it is too large Load Diff

108
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */ /* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -145,6 +145,9 @@ struct input_ctx {
*/ */
struct evbuffer *since_ground; struct evbuffer *since_ground;
struct event ground_timer; struct event ground_timer;
#ifdef ENABLE_IMAGES
void *kitty_state;
#endif
}; };
/* Helper functions. */ /* Helper functions. */
@@ -183,6 +186,9 @@ static void input_enter_osc(struct input_ctx *);
static void input_exit_osc(struct input_ctx *); static void input_exit_osc(struct input_ctx *);
static void input_enter_apc(struct input_ctx *); static void input_enter_apc(struct input_ctx *);
static void input_exit_apc(struct input_ctx *); static void input_exit_apc(struct input_ctx *);
#ifdef ENABLE_IMAGES
static int input_handle_kitty(struct input_ctx *, const u_char *, size_t);
#endif
static void input_enter_rename(struct input_ctx *); static void input_enter_rename(struct input_ctx *);
static void input_exit_rename(struct input_ctx *); static void input_exit_rename(struct input_ctx *);
@@ -916,6 +922,9 @@ input_free(struct input_ctx *ictx)
event_del(&ictx->request_timer); event_del(&ictx->request_timer);
free(ictx->input_buf); free(ictx->input_buf);
#ifdef ENABLE_IMAGES
kitty_free_state(ictx->kitty_state);
#endif
evbuffer_free(ictx->since_ground); evbuffer_free(ictx->since_ground);
event_del(&ictx->ground_timer); event_del(&ictx->ground_timer);
@@ -938,6 +947,7 @@ input_reset(struct input_ctx *ictx, int clear)
screen_write_start_pane(sctx, wp, &wp->base); screen_write_start_pane(sctx, wp, &wp->base);
else else
screen_write_start(sctx, &wp->base); screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
screen_write_reset(sctx); screen_write_reset(sctx);
screen_write_stop(sctx); screen_write_stop(sctx);
} }
@@ -1049,7 +1059,6 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
if (len == 0) if (len == 0)
return; return;
wp->output_generation++;
window_update_activity(wp->window); window_update_activity(wp->window);
if (~wp->flags & PANE_ACTIVITY) { if (~wp->flags & PANE_ACTIVITY) {
wp->flags |= PANE_ACTIVITY; wp->flags |= PANE_ACTIVITY;
@@ -1066,6 +1075,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
screen_write_start_pane(sctx, wp, &wp->base); screen_write_start_pane(sctx, wp, &wp->base);
else else
screen_write_start(sctx, &wp->base); screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id, log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id,
ictx->state->name, len, (int)len, buf); ictx->state->name, len, (int)len, buf);
@@ -1085,6 +1095,7 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s,
return; return;
screen_write_start_callback(sctx, s, cb, arg); screen_write_start_callback(sctx, s, cb, arg);
sctx->flags |= SCREEN_WRITE_INPUT;
input_parse(ictx, buf, len); input_parse(ictx, buf, len);
screen_write_stop(sctx); screen_write_stop(sctx);
} }
@@ -1584,7 +1595,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1: case -1:
break; break;
case 0: case 0:
#ifdef ENABLE_SIXEL #ifdef ENABLE_IMAGES
input_reply(ictx, 1, "\033[?1;2;4c"); input_reply(ictx, 1, "\033[?1;2;4c");
#else #else
input_reply(ictx, 1, "\033[?1;2c"); input_reply(ictx, 1, "\033[?1;2c");
@@ -2102,7 +2113,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx) input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{ {
#ifdef ENABLE_SIXEL #ifdef ENABLE_IMAGES
int n, m, o; int n, m, o;
if (ictx->param_list_len > 3) if (ictx->param_list_len > 3)
@@ -2627,10 +2638,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
const char prefix[] = "tmux;"; const char prefix[] = "tmux;";
const u_int prefixlen = (sizeof prefix) - 1; const u_int prefixlen = (sizeof prefix) - 1;
long long allow_passthrough = 0; long long allow_passthrough = 0;
#ifdef ENABLE_SIXEL #ifdef ENABLE_IMAGES
struct window *w; struct window *w;
struct sixel_image *si; struct sixel_image *si;
int p2; int p1, p2;
#endif #endif
if (wp == NULL) if (wp == NULL)
@@ -2643,15 +2654,18 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0); return (0);
} }
#ifdef ENABLE_SIXEL #ifdef ENABLE_IMAGES
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) { if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
w = wp->window; w = wp->window;
if (input_split(ictx) != 0) if (input_split(ictx) != 0)
return (0); return (0);
p1 = input_get(ictx, 0, 0, 0);
if (p1 == -1)
p1 = 0;
p2 = input_get(ictx, 1, 0, 0); p2 = input_get(ictx, 1, 0, 0);
if (p2 == -1) if (p2 == -1)
p2 = 0; p2 = 0;
si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel); si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel);
if (si != NULL) if (si != NULL)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg); screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
} }
@@ -2790,6 +2804,57 @@ input_enter_apc(struct input_ctx *ictx)
} }
/* APC terminator (ST) received. */ /* APC terminator (ST) received. */
#ifdef ENABLE_IMAGES
static int
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
{
struct screen_write_ctx *sctx = &ictx->ctx;
struct window_pane *wp = ictx->wp;
struct image *im;
u_int image_id = 0, replace_id = 0, quiet = 0;
u_int placement_id = 0;
int32_t z = 0;
char action = '\0', delete = '\0';
int status;
if (wp == NULL)
return (0);
im = kitty_parse_image(&ictx->kitty_state, buf, len,
wp->window->xpixel,
wp->window->ypixel, &image_id, &replace_id, &quiet, &action,
&delete, &placement_id, &z, &status);
if (status == KITTY_PARSE_MORE)
return (1);
if (status != KITTY_PARSE_OK) {
if (quiet < 2 && action != '\0') {
if (status == KITTY_PARSE_MISSING)
input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\",
image_id);
else
input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\",
image_id);
}
return (1);
}
if (replace_id != 0)
image_clear(sctx, replace_id);
if (im != NULL) {
if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else
image_write_kitty(sctx, im, ictx->cell.cell.bg, image_id,
placement_id, z);
image_free(image_get_id(im));
if (quiet == 0 && image_id != 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
} else if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
return (1);
}
#endif
static void static void
input_exit_apc(struct input_ctx *ictx) input_exit_apc(struct input_ctx *ictx)
{ {
@@ -2800,6 +2865,13 @@ input_exit_apc(struct input_ctx *ictx)
return; return;
log_debug("%s: \"%s\"", __func__, ictx->input_buf); log_debug("%s: \"%s\"", __func__, ictx->input_buf);
#ifdef ENABLE_IMAGES
if (ictx->input_len > 1 && ictx->input_buf[0] == 'G' &&
input_handle_kitty(ictx, ictx->input_buf + 1,
ictx->input_len - 1))
return;
#endif
if (wp != NULL && if (wp != NULL &&
options_get_number(wp->options, "allow-set-title") && options_get_number(wp->options, "allow-set-title") &&
screen_set_title(sctx->s, ictx->input_buf, 1)) { screen_set_title(sctx->s, ictx->input_buf, 1)) {
@@ -2860,6 +2932,12 @@ input_top_bit_set(struct input_ctx *ictx)
{ {
struct screen_write_ctx *sctx = &ictx->ctx; struct screen_write_ctx *sctx = &ictx->ctx;
struct utf8_data *ud = &ictx->utf8data; struct utf8_data *ud = &ictx->utf8data;
#ifdef ENABLE_IMAGES
struct grid_cell gc;
struct image *im;
u_int x, source_x, source_y, image_id, placement_id;
int32_t z;
#endif
ictx->flags &= ~INPUT_LAST; ictx->flags &= ~INPUT_LAST;
@@ -2887,6 +2965,20 @@ input_top_bit_set(struct input_ctx *ictx)
utf8_copy(&ictx->cell.cell.data, ud); utf8_copy(&ictx->cell.cell.data, ud);
screen_write_collect_add(sctx, &ictx->cell.cell); screen_write_collect_add(sctx, &ictx->cell.cell);
#ifdef ENABLE_IMAGES
if (sctx->s->cx != 0) {
x = sctx->s->cx - 1; /* cx-1 is the cell just written. */
grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc);
if (kitty_placeholder_to_image(ictx->kitty_state,
sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x,
&source_y, &image_id, &placement_id, &z)) {
grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc);
image_place_cell_kitty(sctx, im, x, sctx->s->cy, source_x,
source_y, image_id, placement_id, z);
}
}
#endif
utf8_copy(&ictx->last, &ictx->cell.cell.data); utf8_copy(&ictx->last, &ictx->cell.cell.data);
ictx->flags |= INPUT_LAST; ictx->flags |= INPUT_LAST;

View File

@@ -31,8 +31,6 @@
" 'Rename' 'r' {command-prompt -I '#S' {rename-session -- '%%'}}" \ " 'Rename' 'r' {command-prompt -I '#S' {rename-session -- '%%'}}" \
" 'Detach' 'd' {detach-client}" \ " 'Detach' 'd' {detach-client}" \
" ''" \ " ''" \
" '#{?local_active_window,Shared Window,Local Window}' 'L' {select-window #{?local_active_window,-S,-L}}" \
" ''" \
" 'New Session' 's' {new-session}" \ " 'New Session' 's' {new-session}" \
" 'New Window' 'w' {new-window}" " 'New Window' 'w' {new-window}"
#define DEFAULT_WINDOW_MENU \ #define DEFAULT_WINDOW_MENU \

View File

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

2
menu.c
View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -305,16 +305,6 @@ const struct options_table_entry options_table[] = {
"When this is reached, the oldest buffer is deleted." "When this is reached, the oldest buffer is deleted."
}, },
{ .name = "clear-on-attach",
.type = OPTIONS_TABLE_FLAG,
.scope = OPTIONS_TABLE_SERVER,
.default_num = 1,
.text = "Whether to use the alternate screen and clear it when "
"a client is attached. When disabled, tmux does not "
"enter the alternate screen on attach so terminal "
"content before tmux remains in scrollback."
},
{ .name = "command-alias", { .name = "command-alias",
.type = OPTIONS_TABLE_STRING, .type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SERVER, .scope = OPTIONS_TABLE_SERVER,
@@ -551,6 +541,7 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_SERVER, .scope = OPTIONS_TABLE_SERVER,
.flags = OPTIONS_TABLE_IS_ARRAY, .flags = OPTIONS_TABLE_IS_ARRAY,
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title," .default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
"xterm-kitty*:kitty,"
"screen*:title," "screen*:title,"
"rxvt*:ignorefkeys", "rxvt*:ignorefkeys",
.separator = ",", .separator = ",",
@@ -1125,8 +1116,7 @@ const struct options_table_entry options_table[] = {
{ .name = "status-style", { .name = "status-style",
.type = OPTIONS_TABLE_STRING, .type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SESSION, .scope = OPTIONS_TABLE_SESSION,
.default_str = "bg=#{?local_active_window,themeblue,themegreen}," .default_str = "bg=themegreen,fg=themeblack",
"fg=themeblack",
.flags = OPTIONS_TABLE_IS_STYLE, .flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",", .separator = ",",
.text = "Style of the status line." .text = "Style of the status line."
@@ -1251,8 +1241,7 @@ const struct options_table_entry options_table[] = {
* underscore. * underscore.
*/ */
.default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~", .default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~",
.text = "Characters considered to separate words; a space matches " .text = "Characters considered to separate words."
"any character with the Unicode White_Space property."
}, },
/* Window options. */ /* Window options. */
@@ -1983,8 +1972,6 @@ const struct options_table_entry options_table[] = {
"Run when a client is resized."), "Run when a client is resized."),
OPTIONS_TABLE_HOOK("client-session-changed", "", OPTIONS_TABLE_HOOK("client-session-changed", "",
"Run when a client's attached session is changed."), "Run when a client's attached session is changed."),
OPTIONS_TABLE_HOOK("client-window-changed", "",
"Run when a client's effective window is changed."),
OPTIONS_TABLE_HOOK("client-light-theme", "", OPTIONS_TABLE_HOOK("client-light-theme", "",
"Run when a client switches to a light theme."), "Run when a client switches to a light theme."),
OPTIONS_TABLE_HOOK("client-dark-theme", "", OPTIONS_TABLE_HOOK("client-dark-theme", "",

73
popup.c
View File

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

View File

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

View File

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

View File

@@ -1,163 +0,0 @@
#!/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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

362
regress/image-support.sh Executable file
View File

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

File diff suppressed because one or more lines are too long

View File

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

155
regress/popup-drag-redraw.sh Executable file
View File

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

165
regress/popup-drag-status-line.sh Executable file
View File

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

View File

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

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

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

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

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

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

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

81
regress/switch-client-redraw.sh Executable file
View File

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

View File

@@ -348,10 +348,10 @@ recalculate_size_skip_client(struct client *loop, __unused int type,
* is not the current window - this is used for aggressive-resize. * is not the current window - this is used for aggressive-resize.
* Otherwise skip any session that doesn't contain the window. * Otherwise skip any session that doesn't contain the window.
*/ */
if (active_get_effective_window(loop, loop->session) == NULL) if (loop->session->curw == NULL)
return (1); return (1);
if (current) if (current)
return (active_get_effective_window(loop, loop->session) != w); return (loop->session->curw->window != w);
return (session_has(loop->session, w) == 0); return (session_has(loop->session, w) == 0);
} }
@@ -386,10 +386,12 @@ recalculate_size(struct window *w, int now)
* got a resize scheduled, then use the new size; otherwise the old. * got a resize scheduled, then use the new size; otherwise the old.
*/ */
if (w->flags & WINDOW_RESIZE) { if (w->flags & WINDOW_RESIZE) {
if (!now && changed && w->new_sx == sx && w->new_sy == sy) if (!now && changed && w->new_sx == sx && w->new_sy == sy &&
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
changed = 0; changed = 0;
} else { } else {
if (!now && changed && w->sx == sx && w->sy == sy) if (!now && changed && w->sx == sx && w->sy == sy &&
w->xpixel == xpixel && w->ypixel == ypixel)
changed = 0; changed = 0;
} }

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.509 2026/08/28 07:36:01 nicm Exp $ */ /* $OpenBSD: server-client.c,v 1.507 2026/08/24 21:17:19 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -107,7 +107,7 @@ server_client_set_overlay(struct client *c, u_int delay,
c->tty.flags |= TTY_FREEZE; c->tty.flags |= TTY_FREEZE;
if (c->overlay_mode == NULL) if (c->overlay_mode == NULL)
c->tty.flags |= TTY_NOCURSOR; c->tty.flags |= TTY_NOCURSOR;
window_update_focus(active_get_effective_window(c, c->session)); window_update_focus(c->session->curw->window);
server_redraw_client(c); server_redraw_client(c);
} }
@@ -134,7 +134,7 @@ server_client_clear_overlay(struct client *c)
c->tty.flags &= ~(TTY_FREEZE|TTY_NOCURSOR); c->tty.flags &= ~(TTY_FREEZE|TTY_NOCURSOR);
if (c->session != NULL) if (c->session != NULL)
window_update_focus(active_get_effective_window(c, c->session)); window_update_focus(c->session->curw->window);
server_redraw_client(c); server_redraw_client(c);
} }
@@ -379,8 +379,7 @@ server_client_attached_lost(struct client *c)
found = NULL; found = NULL;
TAILQ_FOREACH(loop, &clients, entry) { TAILQ_FOREACH(loop, &clients, entry) {
s = loop->session; s = loop->session;
if (loop == c || s == NULL || if (loop == c || s == NULL || s->curw->window != w)
active_get_effective_window(loop, s) != w)
continue; continue;
if (found == NULL || timercmp(&loop->activity_time, if (found == NULL || timercmp(&loop->activity_time,
&found->activity_time, >)) &found->activity_time, >))
@@ -461,20 +460,46 @@ server_client_set_session(struct client *c, struct session *s)
c->flags |= CLIENT_FOCUSED; c->flags |= CLIENT_FOCUSED;
if (old != NULL && old->curw != NULL) if (old != NULL && old->curw != NULL)
window_update_focus(active_get_effective_window(c, old)); window_update_focus(old->curw->window);
if (s != NULL) { if (s != NULL) {
active_get_effective_window(c, s)->latest = c; s->curw->window->latest = c;
recalculate_sizes(); recalculate_sizes();
window_update_focus(active_get_effective_window(c, s)); window_update_focus(s->curw->window);
session_update_activity(s, NULL); session_update_activity(s, NULL);
session_theme_changed(s); session_theme_changed(s);
gettimeofday(&s->last_attached_time, NULL); gettimeofday(&s->last_attached_time, NULL);
active_get_effective_winlink(c, s)->flags &= ~WINLINK_ALERTFLAGS; s->curw->flags &= ~WINLINK_ALERTFLAGS;
alerts_check_session(s); alerts_check_session(s);
tty_update_client_offset(c); tty_update_client_offset(c);
status_timer_start(c); status_timer_start(c);
server_client_fire_session_changed(c, old); server_client_fire_session_changed(c, old);
server_redraw_client(c);
/*
* A full redraw is only needed if the client's session or
* current window actually changed - not if this merely
* confirmed the client is still looking at the same window
* (as happens when switch-client -t targets a pane in the
* already-current window, e.g. clicking a pane name in a
* second #{P:} status line: the default MouseDown1Status
* binding resolves that click to switch-client -t=, which
* reaches here regardless of whether anything besides the
* active pane changed). Redrawing unconditionally here
* forced a full window redraw for what should have been
* just an active-pane change, already handled narrowly by
* window_set_active_pane() and window_redraw_active_switch()
* before this is reached.
*
* old and s may be the same session object, whose curw was
* already updated to the new window before this function was
* called - old->curw and s->curw would then read the same,
* already-current value, so comparing them can never detect
* a same-session window change. Compare against the client's
* own cached scene instead, which only reflects what it has
* actually drawn.
*/
if (old == NULL || old != s ||
!redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
} }
server_check_unattached(); server_check_unattached();
@@ -494,7 +519,6 @@ server_client_lost(struct client *c)
server_client_clear_overlay(c); server_client_clear_overlay(c);
status_prompt_clear(c); status_prompt_clear(c);
status_message_clear(c); status_message_clear(c);
active_remove_client(c);
RB_FOREACH_SAFE(cf, client_files, &c->files, cf1) { RB_FOREACH_SAFE(cf, client_files, &c->files, cf1) {
cf->error = EINTR; cf->error = EINTR;
@@ -528,8 +552,6 @@ server_client_lost(struct client *c)
free(c->title); free(c->title);
free(c->path); free(c->path);
free((void *)c->cwd); free((void *)c->cwd);
free(c->exit_session);
free(c->exit_message);
evtimer_del(&c->repeat_timer); evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer); evtimer_del(&c->click_timer);
@@ -686,7 +708,7 @@ server_client_in_scrollbar_area(struct window_pane *wp, int px, int py)
static void static void
server_client_update_scrollbar_hover(struct client *c, int type, int px, int py) server_client_update_scrollbar_hover(struct client *c, int type, int px, int py)
{ {
struct window *w = active_get_effective_window(c, c->session); struct window *w = c->session->curw->window;
struct window_pane *wp; struct window_pane *wp;
if (type != KEYC_TYPE_MOUSEMOVE) if (type != KEYC_TYPE_MOUSEMOVE)
@@ -858,7 +880,7 @@ server_client_check_mouse(struct client *c, struct key_event *event)
{ {
struct mouse_event *m = &event->m; struct mouse_event *m = &event->m;
struct session *s = c->session, *fs; struct session *s = c->session, *fs;
struct window *w = active_get_effective_window(c, s); struct window *w = s->curw->window;
struct winlink *fwl; struct winlink *fwl;
struct window_pane *wp, *fwp, *lwp = NULL; struct window_pane *wp, *fwp, *lwp = NULL;
u_int x, y, sx, sy, px, py, n, sl_mpos = 0; u_int x, y, sx, sy, px, py, n, sl_mpos = 0;
@@ -1361,7 +1383,7 @@ server_client_update_latest(struct client *c)
if (c->session == NULL) if (c->session == NULL)
return; return;
w = active_get_effective_window(c, c->session); w = c->session->curw->window;
if (w->latest == c) if (w->latest == c)
return; return;
@@ -1440,7 +1462,7 @@ server_client_key_callback(struct cmdq_item *item, void *data)
/* Check the client is good to accept input. */ /* Check the client is good to accept input. */
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS)) if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
goto out; goto out;
wl = active_get_effective_winlink(c, s); wl = s->curw;
/* Update the activity timer. */ /* Update the activity timer. */
memcpy(&c->last_activity_time, &c->activity_time, memcpy(&c->last_activity_time, &c->activity_time,
@@ -1464,8 +1486,19 @@ server_client_key_callback(struct cmdq_item *item, void *data)
/* /*
* Mouse drag is in progress, so fire the callback (now that * Mouse drag is in progress, so fire the callback (now that
* the mouse event is valid). * the mouse event is valid).
*
* Start a synchronized-output region here rather than
* leaving it to whatever redraw eventually follows: a drag
* callback may write directly via the pane's fast path
* immediately, with any correction only arriving later via
* redraw_client_damage(), which opens its own sync region.
* Since tty_sync_end() is only called once, at the very end
* of this client's pass in server_client_reset_state(),
* starting it here merges both into one atomic terminal
* update instead of two visible frames.
*/ */
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) { if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
tty_sync_start(&c->tty);
c->tty.mouse_drag_update(c, m); c->tty.mouse_drag_update(c, m);
goto out; goto out;
} }
@@ -1501,10 +1534,6 @@ server_client_key_callback(struct cmdq_item *item, void *data)
TAILQ_EMPTY(&wp->modes)) TAILQ_EMPTY(&wp->modes))
goto forward_key; goto forward_key;
/* Focus events are not keys and cannot be bound. */
if (key == KEYC_FOCUS_IN || key == KEYC_FOCUS_OUT)
goto forward_key;
/* /*
* Work out the current key table. If the pane is in a mode, use * Work out the current key table. If the pane is in a mode, use
* the mode table instead of the default key table. * the mode table instead of the default key table.
@@ -1699,7 +1728,7 @@ out:
static int static int
server_client_handle_menu_key(struct client *c, struct key_event *event) server_client_handle_menu_key(struct client *c, struct key_event *event)
{ {
struct window *w = active_get_effective_window(c, c->session); struct window *w = c->session->curw->window;
struct key_event new_event; struct key_event new_event;
struct mouse_event *m; struct mouse_event *m;
u_int ox, oy, sx, sy; u_int ox, oy, sx, sy;
@@ -1738,7 +1767,6 @@ server_client_handle_key0(struct client *c, struct key_event *event,
{ {
struct session *s = c->session; struct session *s = c->session;
struct cmdq_item *item; struct cmdq_item *item;
struct window *w;
struct window_pane *wp; struct window_pane *wp;
/* Check the client is good to accept input. */ /* Check the client is good to accept input. */
@@ -1809,10 +1837,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
} }
} }
w = active_get_effective_window(c, s); wp = s->curw->window->active;
wp = w->active;
if (wp == NULL || !window_pane_has_prompt(wp)) { if (wp == NULL || !window_pane_has_prompt(wp)) {
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &s->curw->window->panes, entry) {
if (window_pane_has_prompt(wp) && if (window_pane_has_prompt(wp) &&
window_pane_is_visible(wp)) window_pane_is_visible(wp))
break; break;
@@ -1904,7 +1931,12 @@ server_client_loop(void)
/* /*
* Any windows will have been redrawn as part of clients, so clear * Any windows will have been redrawn as part of clients, so clear
* their flags now. * their flags now. A client whose redraw was deferred this pass
* (waiting for outstanding tty output to drain) has already
* escalated to CLIENT_REDRAWWINDOW or CLIENT_REDRAWSCROLLBARS in
* server_client_check_redraw() to cover whatever it is about to
* lose here, so PANE_REDRAW/PANE_REDRAWSCROLLBAR and window damage
* can simply be cleared unconditionally.
*/ */
RB_FOREACH(w, windows, &windows) { RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
@@ -1915,6 +1947,8 @@ server_client_loop(void)
wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR| wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR|
PANE_ACTIVITY); PANE_ACTIVITY);
} }
redraw_free_damage(w);
check_window_name(w); check_window_name(w);
} }
@@ -2154,7 +2188,7 @@ static void
server_client_reset_state(struct client *c) server_client_reset_state(struct client *c)
{ {
struct tty *tty = &c->tty; struct tty *tty = &c->tty;
struct window *w = active_get_effective_window(c, c->session); struct window *w = c->session->curw->window;
struct window_pane *wp = w->active, *loop; struct window_pane *wp = w->active, *loop;
struct screen *s = NULL; struct screen *s = NULL;
struct options *oo = c->session->options; struct options *oo = c->session->options;
@@ -2417,6 +2451,8 @@ server_client_check_exit(struct client *c, int force)
proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1); proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1);
break; break;
} }
free(c->exit_session);
free(c->exit_message);
} }
/* Redraw timer callback. */ /* Redraw timer callback. */
@@ -2434,7 +2470,7 @@ server_client_redraw_timer(__unused int fd, __unused short events,
static void static void
server_client_check_modes(struct client *c) server_client_check_modes(struct client *c)
{ {
struct window *w; struct window *w = c->session->curw->window;
struct window_pane *wp; struct window_pane *wp;
struct window_mode_entry *wme; struct window_mode_entry *wme;
@@ -2442,7 +2478,6 @@ server_client_check_modes(struct client *c)
return; return;
if (~c->flags & CLIENT_REDRAWSTATUS) if (~c->flags & CLIENT_REDRAWSTATUS)
return; return;
w = active_get_effective_window(c, c->session);
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
wme = TAILQ_FIRST(&wp->modes); wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL && wme->mode->update != NULL) if (wme != NULL && wme->mode->update != NULL)
@@ -2455,11 +2490,13 @@ static int
server_client_any_pane_redraw(struct client *c) server_client_any_pane_redraw(struct client *c)
{ {
struct session *s = c->session; struct session *s = c->session;
struct window *w = active_get_effective_window(c, s); struct window *w = s->curw->window;
struct window_pane *wp; struct window_pane *wp;
if (c->flags & CLIENT_REDRAWWINDOW) if (c->flags & CLIENT_REDRAWWINDOW)
return (1); return (1);
if (!TAILQ_EMPTY(&w->damage))
return (1);
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR)) if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR))
return (1); return (1);
@@ -2473,7 +2510,7 @@ server_client_check_redraw(struct client *c)
{ {
struct session *s = c->session; struct session *s = c->session;
struct tty *tty = &c->tty; struct tty *tty = &c->tty;
struct window *w = active_get_effective_window(c, s); struct window *w = s->curw->window;
struct window_pane *wp; struct window_pane *wp;
int needed, tflags, mode = tty->mode; int needed, tflags, mode = tty->mode;
struct timeval tv = { .tv_usec = 1000 }; struct timeval tv = { .tv_usec = 1000 };
@@ -2505,7 +2542,12 @@ server_client_check_redraw(struct client *c)
/* /*
* If there is outstanding data, defer the redraw until it has been * If there is outstanding data, defer the redraw until it has been
* consumed. We can just add a timer to get out of the event loop and * consumed. We can just add a timer to get out of the event loop and
* end up back here. * end up back here. server_client_loop() clears PANE_REDRAW,
* PANE_REDRAWSCROLLBAR and window damage unconditionally every pass,
* so escalate to a coarser, persistent client flag that survives
* that clear and forces a full catch-up redraw once this client is
* unblocked, rather than trying to keep the fine-grained state
* around for a retry.
*/ */
n = EVBUFFER_LENGTH(tty->out); n = EVBUFFER_LENGTH(tty->out);
if (n != 0 || (tty->flags & TTY_BLOCK)) { if (n != 0 || (tty->flags & TTY_BLOCK)) {
@@ -2519,6 +2561,8 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started"); log_debug("redraw timer started");
evtimer_add(&ev, &tv); evtimer_add(&ev, &tv);
} }
if (!TAILQ_EMPTY(&w->damage))
c->flags |= CLIENT_REDRAWWINDOW;
TAILQ_FOREACH(wp, &w->panes, entry) { TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) { if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW; c->flags |= CLIENT_REDRAWWINDOW;
@@ -2552,6 +2596,19 @@ server_client_check_redraw(struct client *c)
redraw_pane_scrollbar(c, wp); redraw_pane_scrollbar(c, wp);
} }
} }
/*
* Window damage is also what makes server_client_any_pane_
* redraw() decide a redraw is needed at all, independently of
* any CLIENT_ALLREDRAWFLAGS bit. Every current damage source
* happens to set one of those flags too, so the block below
* always consumes it - but consume it here too in case that
* ever stops holding, since server_client_loop() clears
* window damage unconditionally every pass regardless of
* whether it was actually drawn.
*/
if (!TAILQ_EMPTY(&w->damage) && (~c->flags & CLIENT_ALLREDRAWFLAGS))
redraw_client_damage(c);
} }
/* /*
@@ -2565,6 +2622,7 @@ server_client_check_redraw(struct client *c)
} }
server_client_set_progress_bar(c); server_client_set_progress_bar(c);
redraw_screen(c); redraw_screen(c);
redraw_client_damage(c);
} }
/* Put the tty back how it was. */ /* Put the tty back how it was. */
@@ -2612,15 +2670,14 @@ static void
server_client_set_path(struct client *c) server_client_set_path(struct client *c)
{ {
struct session *s = c->session; struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
const char *path; const char *path;
if (w == NULL || w->active == NULL) if (s->curw == NULL || s->curw->window->active == NULL)
return; return;
if (w->active->base.path == NULL) if (s->curw->window->active->base.path == NULL)
path = ""; path = "";
else else
path = w->active->base.path; path = s->curw->window->active->base.path;
if (c->path == NULL || strcmp(path, c->path) != 0) { if (c->path == NULL || strcmp(path, c->path) != 0) {
free(c->path); free(c->path);
c->path = xstrdup(path); c->path = xstrdup(path);
@@ -2633,12 +2690,11 @@ static void
server_client_set_progress_bar(struct client *c) server_client_set_progress_bar(struct client *c)
{ {
struct session *s = c->session; struct session *s = c->session;
struct window *w = active_get_effective_window(c, s);
struct progress_bar *pane_pb; struct progress_bar *pane_pb;
if (w == NULL || w->active == NULL) if (s->curw == NULL || s->curw->window->active == NULL)
return; return;
pane_pb = &w->active->base.progress_bar; pane_pb = &s->curw->window->active->base.progress_bar;
if (pane_pb->state == c->progress_bar.state && if (pane_pb->state == c->progress_bar.state &&
pane_pb->progress == c->progress_bar.progress) pane_pb->progress == c->progress_bar.progress)
return; return;
@@ -3228,7 +3284,7 @@ server_client_print(struct client *c, int parse, struct evbuffer *evb)
goto out; goto out;
} }
wp = active_get_effective_window(c, c->session)->active; wp = c->session->curw->window->active;
wme = TAILQ_FIRST(&wp->modes); wme = TAILQ_FIRST(&wp->modes);
if (wme == NULL || wme->mode != &window_view_mode) if (wme == NULL || wme->mode != &window_view_mode)
window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL, window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL,

View File

@@ -122,7 +122,8 @@ server_redraw_window(struct window *w)
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL && if (c->session != NULL &&
active_get_effective_window(c, c->session) == w) c->session->curw != NULL &&
c->session->curw->window == w)
server_redraw_client(c); server_redraw_client(c);
} }
} }
@@ -134,7 +135,8 @@ server_redraw_window_menu(struct window *w)
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL && if (c->session != NULL &&
active_get_effective_window(c, c->session) == w) c->session->curw != NULL &&
c->session->curw->window == w)
c->flags |= CLIENT_REDRAWMENU; c->flags |= CLIENT_REDRAWMENU;
} }
} }
@@ -146,7 +148,8 @@ server_redraw_window_borders(struct window *w)
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL && if (c->session != NULL &&
active_get_effective_window(c, c->session) == w) c->session->curw != NULL &&
c->session->curw->window == w)
c->flags |= CLIENT_REDRAWBORDERS; c->flags |= CLIENT_REDRAWBORDERS;
} }
} }

View File

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

4
sort.c
View File

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

View File

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

61
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1163 2026/09/01 12:49:49 nicm Exp $ .\" $OpenBSD: tmux.1,v 1.1158 2026/08/20 09:19:24 nicm Exp $
.\" .\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> .\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\" .\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\" .\"
.Dd $Mdocdate: September 1 2026 $ .Dd $Mdocdate: August 20 2026 $
.Dt TMUX 1 .Dt TMUX 1
.Os .Os
.Sh NAME .Sh NAME
@@ -2208,14 +2208,14 @@ Move to the end of the next word.
.Xc .Xc
Same as Same as
.Ic next\-word .Ic next\-word
but treat all non-whitespace characters as part of a word. but use a space alone as the word separator.
.It Xo .It Xo
.Ic next\-space\-end .Ic next\-space\-end
(vi: E) (vi: E)
.Xc .Xc
Same as Same as
.Ic next\-word\-end .Ic next\-word\-end
but treat all non-whitespace characters as part of a word. but use a space alone as the word separator.
.It Xo .It Xo
.Ic other\-end .Ic other\-end
(vi: o) (vi: o)
@@ -2289,7 +2289,7 @@ Move to the previous word.
.Xc .Xc
Same as Same as
.Ic previous\-word .Ic previous\-word
but treat all non-whitespace characters as part of a word. but use a space alone as the word separator.
.It Xo .It Xo
.Ic rectangle\-on .Ic rectangle\-on
.Xc .Xc
@@ -2574,8 +2574,8 @@ Word separators can be customized with the
session option. session option.
Next word moves to the start of the next word, next word end to the end of the Next word moves to the start of the next word, next word end to the end of the
next word and previous word to the start of the previous word. next word and previous word to the start of the previous word.
The three next and previous space keys work similarly but treat all The three next and previous space keys work similarly but use a space alone as
non-whitespace characters as part of a word. the word separator.
Setting Setting
.Em word\-separators .Em word\-separators
to the empty string makes next/previous word equivalent to next/previous space. to the empty string makes next/previous word equivalent to next/previous space.
@@ -4038,22 +4038,12 @@ and
.Ic swap\-window . .Ic swap\-window .
.Tg selectw .Tg selectw
.It Xo Ic select\-window .It Xo Ic select\-window
.Op Fl LlnpST .Op Fl lnpT
.Op Fl t Ar target\-window .Op Fl t Ar target\-window
.Xc .Xc
.D1 Pq alias: Ic selectw .D1 Pq alias: Ic selectw
Select the window at Select the window at
.Ar target\-window . .Ar target\-window .
.Fl L
makes the attached client use a local current window for the target session.
Window navigation from that client then changes only that client's effective
window and leaves the session current window unchanged.
.Fl S
returns the client to the shared session current window.
Without
.Fl L ,
the current window is shared by the session and stored in the session.
.Pp
.Fl l , .Fl l ,
.Fl n .Fl n
and and
@@ -4764,15 +4754,6 @@ for backspace.
Set the number of buffers; as new buffers are added to the top of the stack, Set the number of buffers; as new buffers are added to the top of the stack,
old ones are removed from the bottom if necessary to maintain this maximum old ones are removed from the bottom if necessary to maintain this maximum
length. length.
.It Xo Ic clear\-on\-attach
.Op Ic on | off
.Xc
When enabled (the default), the client terminal switches to the alternate
screen and clears it when a client is attached.
When disabled,
.Nm
does not enter the alternate screen; instead the existing terminal content is
scrolled into the scrollback buffer so it remains accessible.
.It Xo Ic command\-alias[] .It Xo Ic command\-alias[]
.Ar name=value .Ar name=value
.Xc .Xc
@@ -5089,12 +5070,20 @@ Supports extended keys.
Supports focus reporting. Supports focus reporting.
.It hyperlinks .It hyperlinks
Supports OSC 8 hyperlinks. Supports OSC 8 hyperlinks.
.It image-quadrants
Allows image fallbacks to use Unicode quadrant block characters.
.It image-sextants
Allows image fallbacks to use Unicode sextant block characters.
.It ignorefkeys .It ignorefkeys
Ignore function keys from Ignore function keys from
.Xr terminfo 5 .Xr terminfo 5
and use the and use the
.Nm .Nm
internal set only. internal set only.
.It kitty
Supports Kitty graphics using Unicode placeholders.
The terminal must support the Kitty Unicode placeholder extension, not only
the basic Kitty graphics protocol.
.It margins .It margins
Supports DECSLRM margins. Supports DECSLRM margins.
.It mouse .It mouse
@@ -5123,8 +5112,6 @@ Supports
title setting. title setting.
.It usstyle .It usstyle
Allows underscore style and colour to be set. Allows underscore style and colour to be set.
.It utf8
Supports UTF\-8 output.
.El .El
.It Ic terminal\-overrides[] Ar string .It Ic terminal\-overrides[] Ar string
Allow terminal descriptions read using Allow terminal descriptions read using
@@ -5659,11 +5646,6 @@ If set to both, a bell and a message are produced.
Sets the session's conception of what characters are considered word Sets the session's conception of what characters are considered word
separators, for the purposes of the next and previous word commands in separators, for the purposes of the next and previous word commands in
copy mode. copy mode.
A space in
.Ar string
matches any character with the Unicode
.Em White_Space
property.
.El .El
.Pp .Pp
Available window options are: Available window options are:
@@ -7276,10 +7258,7 @@ shows the four frames in turn, changing every 200 milliseconds.
The following variables are available, where appropriate: The following variables are available, where appropriate:
.Bl -column "XXXXXXXXXXXXXXXXXXX" "XXXXX" .Bl -column "XXXXXXXXXXXXXXXXXXX" "XXXXX"
.It Sy "Variable name" Ta Sy "Alias" Ta Sy "Replaced with" .It Sy "Variable name" Ta Sy "Alias" Ta Sy "Replaced with"
.It Li "active_window_id" Ta "" Ta "ID of effective active window" .It Li "active_window_index" Ta "" Ta "Index of active window in session"
.It Li "active_window_index" Ta "" Ta "Index of effective active window"
.It Li "local_active_window" Ta "" Ta "1 if client has local window selection"
.It Li "active_window_mode" Ta "" Ta "Window selection mode, shared or local"
.It Li "alternate_on" Ta "" Ta "1 if pane is in alternate screen" .It Li "alternate_on" Ta "" Ta "1 if pane is in alternate screen"
.It Li "alternate_saved_x" Ta "" Ta "Saved cursor X in alternate screen" .It Li "alternate_saved_x" Ta "" Ta "Saved cursor X in alternate screen"
.It Li "alternate_saved_y" Ta "" Ta "Saved cursor Y in alternate screen" .It Li "alternate_saved_y" Ta "" Ta "Saved cursor Y in alternate screen"
@@ -7336,10 +7315,7 @@ The following variables are available, where appropriate:
.It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode" .It Li "cursor_very_visible" Ta "" Ta "1 if the cursor is in very visible mode"
.It Li "cursor_x" Ta "" Ta "Cursor X position in pane" .It Li "cursor_x" Ta "" Ta "Cursor X position in pane"
.It Li "cursor_y" Ta "" Ta "Cursor Y position in pane" .It Li "cursor_y" Ta "" Ta "Cursor Y position in pane"
.It Li "history_added" Ta "" Ta "Number of lines scrolled into pane history"
.It Li "history_bytes" Ta "" Ta "Number of bytes in window history" .It Li "history_bytes" Ta "" Ta "Number of bytes in window history"
.It Li "history_collected" Ta "" Ta "Number of oldest pane history lines discarded at the history limit"
.It Li "history_generation" Ta "" Ta "Generation incremented when pane history is cleared or reflowed"
.It Li "history_limit" Ta "" Ta "Maximum window history lines" .It Li "history_limit" Ta "" Ta "Maximum window history lines"
.It Li "history_size" Ta "" Ta "Size of history in lines" .It Li "history_size" Ta "" Ta "Size of history in lines"
.It Li "hook" Ta "" Ta "Name of running hook, if any" .It Li "hook" Ta "" Ta "Name of running hook, if any"
@@ -7438,7 +7414,6 @@ The following variables are available, where appropriate:
.It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set"
.It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal" .It Li "pane_modal_flag" Ta "" Ta "1 if pane is modal"
.It Li "pane_mode" Ta "" Ta "Name of pane mode, if any" .It Li "pane_mode" Ta "" Ta "Name of pane mode, if any"
.It Li "pane_output_generation" Ta "" Ta "Generation incremented when pane output is processed"
.It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)" .It Li "pane_path" Ta "" Ta "Path of pane (can be set by application)"
.It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pid" Ta "" Ta "PID of first process in pane"
.It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped"
@@ -7516,7 +7491,7 @@ The following variables are available, where appropriate:
.It Li "uid" Ta "" Ta "Server UID" .It Li "uid" Ta "" Ta "Server UID"
.It Li "user" Ta "" Ta "Server user" .It Li "user" Ta "" Ta "Server user"
.It Li "version" Ta "" Ta "Server version" .It Li "version" Ta "" Ta "Server version"
.It Li "window_active" Ta "" Ta "1 if window is the effective active window" .It Li "window_active" Ta "" Ta "1 if window active"
.It Li "window_active_clients" Ta "" Ta "Number of clients viewing this window" .It Li "window_active_clients" Ta "" Ta "Number of clients viewing this window"
.It Li "window_active_clients_list" Ta "" Ta "List of clients viewing this window" .It Li "window_active_clients_list" Ta "" Ta "List of clients viewing this window"
.It Li "window_active_sessions" Ta "" Ta "Number of sessions on which this window is active" .It Li "window_active_sessions" Ta "" Ta "Number of sessions on which this window is active"

276
tmux.h
View File

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

View File

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

104
tools/image-edge-test.sh Normal file
View File

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

109
tools/image-visual-test.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

@@ -729,6 +729,7 @@ tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size)
char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0; char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0;
tty_set_size(tty, tty->sx, tty->sy, char_x, char_y); tty_set_size(tty, tty->sx, tty->sy, char_x, char_y);
tty_invalidate(tty); tty_invalidate(tty);
recalculate_sizes();
tty->flags &= ~TTY_WINSIZEQUERY; tty->flags &= ~TTY_WINSIZEQUERY;
*size = end + 1; *size = end + 1;
@@ -1020,12 +1021,12 @@ complete_key:
/* Check for focus events. */ /* Check for focus events. */
if (key == KEYC_FOCUS_OUT) { if (key == KEYC_FOCUS_OUT) {
c->flags &= ~CLIENT_FOCUSED; c->flags &= ~CLIENT_FOCUSED;
window_update_focus(active_get_effective_window(c, c->session)); window_update_focus(c->session->curw->window);
events_fire_client("client-focus-out", c); events_fire_client("client-focus-out", c);
} else if (key == KEYC_FOCUS_IN) { } else if (key == KEYC_FOCUS_IN) {
c->flags |= CLIENT_FOCUSED; c->flags |= CLIENT_FOCUSED;
events_fire_client("client-focus-in", c); events_fire_client("client-focus-in", c);
window_update_focus(active_get_effective_window(c, c->session)); window_update_focus(c->session->curw->window);
} }
/* Fire the key. */ /* Fire the key. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-term.c,v 1.109 2026/08/25 08:37:08 nicm Exp $ */ /* $OpenBSD: tty-term.c,v 1.108 2026/08/17 14:47:41 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -111,7 +111,6 @@ static const struct tty_term_code_entry tty_term_codes[] = {
[TTYC_ICH] = { TTYCODE_STRING, "ich" }, [TTYC_ICH] = { TTYCODE_STRING, "ich" },
[TTYC_IL1] = { TTYCODE_STRING, "il1" }, [TTYC_IL1] = { TTYCODE_STRING, "il1" },
[TTYC_IL] = { TTYCODE_STRING, "il" }, [TTYC_IL] = { TTYCODE_STRING, "il" },
[TTYC_IND] = { TTYCODE_STRING, "ind" },
[TTYC_INDN] = { TTYCODE_STRING, "indn" }, [TTYC_INDN] = { TTYCODE_STRING, "indn" },
[TTYC_INVIS] = { TTYCODE_STRING, "invis" }, [TTYC_INVIS] = { TTYCODE_STRING, "invis" },
[TTYC_KCBT] = { TTYCODE_STRING, "kcbt" }, [TTYC_KCBT] = { TTYCODE_STRING, "kcbt" },
@@ -525,6 +524,7 @@ tty_term_apply_overrides(struct tty_term *term)
tty_term_validate(term); tty_term_validate(term);
} }
/* Mark terminals whose Ms capability does not contain a clipboard argument. */
static void static void
tty_term_validate(struct tty_term *term) tty_term_validate(struct tty_term *term)
{ {

210
tty.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty.c,v 1.480 2026/08/25 08:37:08 nicm Exp $ */ /* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -68,11 +68,6 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code,
static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int); static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int);
static int tty_check_overlay(struct tty *, u_int, u_int); static int tty_check_overlay(struct tty *, u_int, u_int);
#ifdef ENABLE_SIXEL
static void tty_write_one(void (*)(struct tty *, const struct tty_ctx *),
struct client *, struct tty_ctx *);
#endif
#define tty_use_margin(tty) \ #define tty_use_margin(tty) \
(tty->term->flags & TERM_DECSLRM) (tty->term->flags & TERM_DECSLRM)
#define tty_full_width(tty, ctx) \ #define tty_full_width(tty, ctx) \
@@ -163,10 +158,21 @@ tty_resize(struct tty *tty)
void void
tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel) tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel)
{ {
#ifdef ENABLE_IMAGES
int geometry_changed;
geometry_changed = (tty->xpixel != xpixel || tty->ypixel != ypixel);
#endif
tty->sx = sx; tty->sx = sx;
tty->sy = sy; tty->sy = sy;
tty->xpixel = xpixel; tty->xpixel = xpixel;
tty->ypixel = ypixel; tty->ypixel = ypixel;
#ifdef ENABLE_IMAGES
if (geometry_changed) {
image_tty_geometry_changed(tty);
server_redraw_client(tty->client);
}
#endif
} }
static void static void
@@ -284,6 +290,10 @@ tty_open(struct tty *tty, char **cause)
} }
tty->flags |= TTY_OPENED; tty->flags |= TTY_OPENED;
#ifdef ENABLE_IMAGES
image_tty_update(tty);
#endif
tty->flags &= ~(TTY_NOCURSOR|TTY_FREEZE|TTY_BLOCK|TTY_TIMER); tty->flags &= ~(TTY_NOCURSOR|TTY_FREEZE|TTY_BLOCK|TTY_TIMER);
event_set(&tty->event_in, c->fd, EV_PERSIST|EV_READ, event_set(&tty->event_in, c->fd, EV_PERSIST|EV_READ,
@@ -338,7 +348,6 @@ tty_start_tty(struct tty *tty)
{ {
struct client *c = tty->client; struct client *c = tty->client;
struct termios tio; struct termios tio;
u_int i;
setblocking(c->fd, 0); setblocking(c->fd, 0);
event_add(&tty->event_in, NULL); event_add(&tty->event_in, NULL);
@@ -354,21 +363,10 @@ tty_start_tty(struct tty *tty)
if (tcsetattr(c->fd, TCSANOW, &tio) == 0) if (tcsetattr(c->fd, TCSANOW, &tio) == 0)
tcflush(c->fd, TCOFLUSH); tcflush(c->fd, TCOFLUSH);
if (options_get_number(global_options, "clear-on-attach")) { tty_putcode(tty, TTYC_SMCUP);
tty_putcode(tty, TTYC_SMCUP);
tty_putcode(tty, TTYC_CLEAR);
} else {
tty_putcode_ii(tty, TTYC_CSR, 0, tty->sy - 1);
tty_putcode_ii(tty, TTYC_CUP, 0, tty->sy - 1);
if (tty_term_has(tty->term, TTYC_INDN))
tty_putcode_i(tty, TTYC_INDN, tty->sy + 1);
else if (tty_term_has(tty->term, TTYC_IND)) {
for (i = 0; i < tty->sy + 1; i++)
tty_putcode(tty, TTYC_IND);
} else
tty_putcode(tty, TTYC_CLEAR);
}
tty_putcode(tty, TTYC_SMKX); tty_putcode(tty, TTYC_SMKX);
tty_putcode(tty, TTYC_CLEAR);
if (tty_acs_needed(tty)) { if (tty_acs_needed(tty)) {
log_debug("%s: using capabilities for ACS", c->name); log_debug("%s: using capabilities for ACS", c->name);
@@ -484,8 +482,7 @@ tty_stop_tty(struct tty *tty)
tty_raw(tty, tty_term_string(tty->term, TTYC_RMACS)); tty_raw(tty, tty_term_string(tty->term, TTYC_RMACS));
tty_raw(tty, tty_term_string(tty->term, TTYC_SGR0)); tty_raw(tty, tty_term_string(tty->term, TTYC_SGR0));
tty_raw(tty, tty_term_string(tty->term, TTYC_RMKX)); tty_raw(tty, tty_term_string(tty->term, TTYC_RMKX));
if (options_get_number(global_options, "clear-on-attach")) tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR));
tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR));
if (tty->cstyle != SCREEN_CURSOR_DEFAULT) { if (tty->cstyle != SCREEN_CURSOR_DEFAULT) {
if (tty_term_has(tty->term, TTYC_SE)) if (tty_term_has(tty->term, TTYC_SE))
tty_raw(tty, tty_term_string(tty->term, TTYC_SE)); tty_raw(tty, tty_term_string(tty->term, TTYC_SE));
@@ -510,10 +507,7 @@ tty_stop_tty(struct tty *tty)
if (tty_use_margin(tty)) if (tty_use_margin(tty))
tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG)); tty_raw(tty, tty_term_string(tty->term, TTYC_DSMG));
if (options_get_number(global_options, "clear-on-attach")) tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP));
tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP));
else
tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR));
if (tty->term->flags & TERM_VT100LIKE) if (tty->term->flags & TERM_VT100LIKE)
tty_raw(tty, "\033[?2031l"); tty_raw(tty, "\033[?2031l");
@@ -529,6 +523,9 @@ tty_close(struct tty *tty)
tty_stop_tty(tty); tty_stop_tty(tty);
if (tty->flags & TTY_OPENED) { if (tty->flags & TTY_OPENED) {
#ifdef ENABLE_IMAGES
image_tty_free(tty, 1);
#endif
evbuffer_free(tty->in); evbuffer_free(tty->in);
event_del(&tty->event_in); event_del(&tty->event_in);
evbuffer_free(tty->out); evbuffer_free(tty->out);
@@ -545,6 +542,9 @@ void
tty_free(struct tty *tty) tty_free(struct tty *tty)
{ {
tty_close(tty); tty_close(tty);
#ifdef ENABLE_IMAGES
image_tty_free(tty, 0);
#endif
free(tty->r.ranges); free(tty->r.ranges);
} }
@@ -556,6 +556,9 @@ tty_update_features(struct tty *tty)
if (tty_apply_features(tty->term)) if (tty_apply_features(tty->term))
tty_term_apply_overrides(tty->term); tty_term_apply_overrides(tty->term);
#ifdef ENABLE_IMAGES
image_tty_update(tty);
#endif
if (tty_use_margin(tty)) if (tty_use_margin(tty))
tty_putcode(tty, TTYC_ENMG); tty_putcode(tty, TTYC_ENMG);
@@ -955,7 +958,7 @@ int
tty_window_bigger(struct tty *tty) tty_window_bigger(struct tty *tty)
{ {
struct client *c = tty->client; struct client *c = tty->client;
struct window *w = active_get_effective_window(c, c->session); struct window *w = c->session->curw->window;
return (tty->sx < w->sx || tty->sy - status_line_size(c) < w->sy); return (tty->sx < w->sx || tty->sy - status_line_size(c) < w->sy);
} }
@@ -977,7 +980,7 @@ static int
tty_window_offset1(struct tty *tty, u_int *ox, u_int *oy, u_int *sx, u_int *sy) tty_window_offset1(struct tty *tty, u_int *ox, u_int *oy, u_int *sx, u_int *sy)
{ {
struct client *c = tty->client; struct client *c = tty->client;
struct window *w = active_get_effective_window(c, c->session); struct window *w = c->session->curw->window;
struct window_pane *wp = w->active; struct window_pane *wp = w->active;
u_int cx, cy, lines; u_int cx, cy, lines;
@@ -1044,7 +1047,8 @@ tty_update_window_offset(struct window *w)
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL && if (c->session != NULL &&
active_get_effective_window(c, c->session) == w) c->session->curw != NULL &&
c->session->curw->window == w)
tty_update_client_offset(c); tty_update_client_offset(c);
} }
} }
@@ -1119,7 +1123,7 @@ tty_redraw_region(struct tty *tty, const struct tty_ctx *ctx)
*/ */
if (tty_large_region(tty, ctx) || ctx->flags & TTY_CTX_PANE_OBSCURED) { if (tty_large_region(tty, ctx) || ctx->flags & TTY_CTX_PANE_OBSCURED) {
log_debug("%s: %s large region redraw", __func__, c->name); log_debug("%s: %s large region redraw", __func__, c->name);
ctx->redraw_cb(ctx); ctx->redraw_cb(ctx, ctx->orupper, ctx->orlower - ctx->orupper + 1);
return; return;
} }
@@ -1539,60 +1543,6 @@ tty_check_overlay_range(struct tty *tty, u_int px, u_int py, u_int nx)
return (c->overlay_check(c, c->overlay_data, px, py, nx)); return (c->overlay_check(c, c->overlay_data, px, py, nx));
} }
#ifdef ENABLE_SIXEL
/* Update context for client. */
static int
tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct window_pane *wp = ttyctx->arg;
if (active_get_effective_window(c, c->session) != wp->window)
return (0);
if (wp->layout_cell == NULL)
return (0);
if (tty_window_offset(&c->tty, &ttyctx->wox, &ttyctx->woy, &ttyctx->wsx,
&ttyctx->wsy))
ttyctx->flags |= TTY_CTX_WINDOW_BIGGER;
else
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
ttyctx->yoff = ttyctx->ryoff = wp->yoff;
if (status_at_line(c) == 0)
ttyctx->yoff += status_line_size(c);
return (1);
}
void
tty_draw_images(struct client *c, struct window_pane *wp)
{
struct image *im;
struct tty_ctx ttyctx;
TAILQ_FOREACH(im, &wp->screen->images, entry) {
memset(&ttyctx, 0, sizeof ttyctx);
/* Set the client independent properties. */
ttyctx.ocx = im->px;
ttyctx.ocy = im->py;
ttyctx.orlower = wp->screen->rlower;
ttyctx.orupper = wp->screen->rupper;
ttyctx.xoff = ttyctx.rxoff = wp->xoff;
ttyctx.sx = wp->sx;
ttyctx.sy = wp->sy;
ttyctx.image = im;
ttyctx.arg = wp;
ttyctx.set_client_cb = tty_set_client_cb;
ttyctx.flags |= TTY_CTX_INVISIBLE_PANES;
tty_write_one(tty_cmd_sixelimage, c, &ttyctx);
}
}
#endif
void void
tty_sync_start(struct tty *tty) tty_sync_start(struct tty *tty)
{ {
@@ -1666,19 +1616,6 @@ tty_write(void (*cmdfn)(struct tty *, const struct tty_ctx *),
} }
} }
#ifdef ENABLE_SIXEL
/* Only write to the incoming tty instead of every client. */
static void
tty_write_one(void (*cmdfn)(struct tty *, const struct tty_ctx *),
struct client *c, struct tty_ctx *ctx)
{
if (ctx->set_client_cb == NULL)
return;
if ((ctx->set_client_cb(ctx, c)) == 1)
cmdfn(&c->tty, ctx);
}
#endif
void void
tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx) tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx)
{ {
@@ -1828,10 +1765,17 @@ tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx)
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1 || ctx->sy == 1 ||
c->overlay_check != NULL) { c->overlay_check != NULL) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1899,10 +1843,17 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx)
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1 || ctx->sy == 1 ||
c->overlay_check != NULL) { c->overlay_check != NULL) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1939,10 +1890,17 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx)
ctx->sx == 1 || ctx->sx == 1 ||
ctx->sy == 1 || ctx->sy == 1 ||
c->overlay_check != NULL) { c->overlay_check != NULL) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx); tty_redraw_region(tty, ctx);
return; return;
} }
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx); tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -2031,7 +1989,7 @@ tty_cmd_alignmenttest(struct tty *tty, const struct tty_ctx *ctx)
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) || if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
c->overlay_check != NULL) { c->overlay_check != NULL) {
ctx->redraw_cb(ctx); ctx->redraw_cb(ctx, 0, ctx->sy);
return; return;
} }
@@ -2113,7 +2071,7 @@ tty_cmd_cells(struct tty *tty, const struct tty_ctx *ctx)
tty->cy == tty->rlower) tty->cy == tty->rlower)
tty_draw_pane(tty, ctx, ctx->ocy); tty_draw_pane(tty, ctx, ctx->ocy);
else else
ctx->redraw_cb(ctx); ctx->redraw_cb(ctx, ctx->ocy, 1);
return; return;
} }
@@ -2172,58 +2130,6 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx)
tty_invalidate(tty); tty_invalidate(tty);
} }
#ifdef ENABLE_SIXEL
void
tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx)
{
struct image *im = ctx->image;
struct sixel_image *si = im->data;
struct sixel_image *new;
char *data;
size_t size;
u_int cx = ctx->ocx, cy = ctx->ocy, sx, sy;
u_int i, j, x, y, rx, ry;
int fallback = 0;
if ((~tty->term->flags & TERM_SIXEL) &&
!tty_term_has(tty->term, TTYC_SXL))
fallback = 1;
if (tty->xpixel == 0 || tty->ypixel == 0)
fallback = 1;
sixel_size_in_cells(si, &sx, &sy);
log_debug("%s: image is %ux%u", __func__, sx, sy);
if (!tty_clamp_area(tty, ctx, cx, cy, sx, sy, &i, &j, &x, &y, &rx, &ry))
return;
log_debug("%s: clamping to %u,%u-%u,%u", __func__, i, j, rx, ry);
if (fallback == 1) {
data = xstrdup(im->fallback);
size = strlen(data);
} else {
new = sixel_scale(si, tty->xpixel, tty->ypixel, i, j, rx, ry, 0);
if (new == NULL)
return;
data = sixel_print(new, si, &size);
}
if (data != NULL) {
log_debug("%s: %zu bytes: %s", __func__, size, data);
tty_region_off(tty);
tty_margin_off(tty);
tty_cursor(tty, x, y);
tty->flags |= TTY_NOBLOCK;
tty_add(tty, data, size);
tty_invalidate(tty);
free(data);
}
if (fallback == 0)
sixel_free(new);
}
#endif
void void
tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx) tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx)
{ {

68
utf8.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: utf8.c,v 1.72 2026/09/01 12:49:49 nicm Exp $ */ /* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -603,72 +603,6 @@ utf8_towc(const struct utf8_data *ud, wchar_t *wc)
return (UTF8_DONE); return (UTF8_DONE);
} }
/* Check for a Unicode whitespace character. */
int
utf8_has_whitespace(const struct utf8_data *ud)
{
struct utf8_data tmp;
wchar_t wc;
u_int offset = 0, size;
u_char ch;
while (offset < ud->size) {
ch = ud->data[offset];
if (ch < 0x80) {
wc = ch;
size = 1;
} else {
if (ch >= 0xc2 && ch <= 0xdf)
size = 2;
else if (ch >= 0xe0 && ch <= 0xef)
size = 3;
else if (ch >= 0xf0 && ch <= 0xf4)
size = 4;
else
return (0);
if (size > ud->size - offset)
return (0);
memset(&tmp, 0, sizeof tmp);
memcpy(tmp.data, ud->data + offset, size);
tmp.size = tmp.have = size;
if (utf8_towc(&tmp, &wc) != UTF8_DONE)
return (0);
}
offset += size;
switch (wc) {
case 0x0009:
case 0x000A:
case 0x000B:
case 0x000C:
case 0x000D:
case 0x0020:
case 0x0085:
case 0x00A0:
case 0x1680:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007:
case 0x2008:
case 0x2009:
case 0x200A:
case 0x2028:
case 0x2029:
case 0x202F:
case 0x205F:
case 0x3000:
return (1);
}
}
return (0);
}
/* Convert wide character to UTF-8 character. */ /* Convert wide character to UTF-8 character. */
enum utf8_state enum utf8_state
utf8_fromwc(wchar_t wc, struct utf8_data *ud) utf8_fromwc(wchar_t wc, struct utf8_data *ud)

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-client.c,v 1.49 2026/08/31 12:41:03 kirill Exp $ */ /* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */
/* /*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -102,8 +102,7 @@ static const char *window_client_info_lines[] = {
WINDOW_CLIENT_FEATURE(sync) " " WINDOW_CLIENT_FEATURE(sync) " "
WINDOW_CLIENT_FEATURE(title), WINDOW_CLIENT_FEATURE(title),
" #[#{E:tree-mode-border-style},acs]x#[default] " " #[#{E:tree-mode-border-style},acs]x#[default] "
WINDOW_CLIENT_FEATURE(usstyle) " " WINDOW_CLIENT_FEATURE(usstyle),
WINDOW_CLIENT_FEATURE(utf8),
"#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]", "#[#{E:tree-mode-border-style},acs]qqqqqqqqqqqqqqn#{R:q,#{window_width}}#[default]",
"#[fg=themelightgrey]prefix #[#{E:tree-mode-border-style},acs]x#[default] " "#[fg=themelightgrey]prefix #[#{E:tree-mode-border-style},acs]x#[default] "
@@ -269,13 +268,12 @@ window_client_draw_info(__unused void *modedata, void *itemdata,
struct window_client_itemdata *item = itemdata; struct window_client_itemdata *item = itemdata;
struct client *c = item->c; struct client *c = item->c;
struct screen *s = ctx->s; struct screen *s = ctx->s;
struct window *w; struct window *w = c->session->curw->window;
struct grid_cell gc; struct grid_cell gc;
u_int cx = s->cx, cy = s->cy, i; u_int cx = s->cx, cy = s->cy, i;
struct format_tree *ft; struct format_tree *ft;
char *expanded; char *expanded;
w = active_get_effective_window(c, c->session);
ft = format_create_defaults(NULL, c, NULL, NULL, NULL); ft = format_create_defaults(NULL, c, NULL, NULL, NULL);
if (c->tty.term->flags & TERM_INVALIDMS) if (c->tty.term->flags & TERM_INVALIDMS)
format_add(ft, "clipboard_invalid", "1"); format_add(ft, "clipboard_invalid", "1");

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-copy.c,v 1.429 2026/09/01 13:04:29 nicm Exp $ */ /* $OpenBSD: window-copy.c,v 1.426 2026/08/19 19:55:04 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -73,6 +73,10 @@ static void window_copy_write_line(struct window_mode_entry *,
struct screen_write_ctx *, u_int); struct screen_write_ctx *, u_int);
static void window_copy_write_lines(struct window_mode_entry *, static void window_copy_write_lines(struct window_mode_entry *,
struct screen_write_ctx *, u_int, u_int); struct screen_write_ctx *, u_int, u_int);
#ifdef ENABLE_IMAGES
static void window_copy_update_image_refresh(struct window_copy_mode_data *);
static int window_copy_visible_has_images(struct window_copy_mode_data *);
#endif
static char *window_copy_match_at_cursor(struct window_copy_mode_data *); static char *window_copy_match_at_cursor(struct window_copy_mode_data *);
static void window_copy_scroll_to(struct window_mode_entry *, u_int, u_int, static void window_copy_scroll_to(struct window_mode_entry *, u_int, u_int,
int); int);
@@ -279,6 +283,10 @@ struct window_copy_mode_data {
u_int oy; /* number of lines scrolled up */ u_int oy; /* number of lines scrolled up */
u_int image_base; /* hsize - oy images were last drawn for */
int image_base_set;
int image_refresh; /* current redraw needs image refresh */
u_int selx; /* beginning of selection */ u_int selx; /* beginning of selection */
u_int sely; u_int sely;
@@ -636,6 +644,9 @@ window_copy_init(struct window_mode_entry *wme,
data->my = screen_hsize(data->backing) + data->cy - data->oy; data->my = screen_hsize(data->backing) + data->cy - data->oy;
data->showmark = 0; data->showmark = 0;
#ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data);
#endif
screen_write_start(&ctx, &data->screen); screen_write_start(&ctx, &data->screen);
for (i = 0; i < screen_size_y(&data->screen); i++) for (i = 0; i < screen_size_y(&data->screen); i++)
window_copy_write_line(wme, &ctx, i); window_copy_write_line(wme, &ctx, i);
@@ -1197,6 +1208,9 @@ window_copy_size_changed(struct window_mode_entry *wme)
window_copy_clear_selection(wme); window_copy_clear_selection(wme);
window_copy_clear_marks(wme); window_copy_clear_marks(wme);
#ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data);
#endif
screen_write_start(&ctx, s); screen_write_start(&ctx, s);
window_copy_write_lines(wme, &ctx, 0, screen_size_y(s)); window_copy_write_lines(wme, &ctx, 0, screen_size_y(s));
screen_write_stop(&ctx); screen_write_stop(&ctx);
@@ -2207,76 +2221,15 @@ window_copy_cmd_selection_mode(struct window_copy_cmd_state *cs)
struct window_mode_entry *wme = cs->wme; struct window_mode_entry *wme = cs->wme;
struct options *so = cs->s->options; struct options *so = cs->s->options;
struct window_copy_mode_data *data = wme->data; struct window_copy_mode_data *data = wme->data;
struct grid_reader gr;
const char *s = args_string(cs->wargs, 0); const char *s = args_string(cs->wargs, 0);
u_int sx, sy, ex, ey, fx, fy, x, y;
if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0) if (s == NULL || strcasecmp(s, "char") == 0 || strcasecmp(s, "c") == 0)
data->selflag = SEL_CHAR; data->selflag = SEL_CHAR;
else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) { else if (strcasecmp(s, "word") == 0 || strcasecmp(s, "w") == 0) {
data->separators = options_get_string(so, "word-separators"); data->separators = options_get_string(so, "word-separators");
data->selflag = SEL_WORD; data->selflag = SEL_WORD;
} else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0) { } else if (strcasecmp(s, "line") == 0 || strcasecmp(s, "l") == 0)
data->selflag = SEL_LINE; data->selflag = SEL_LINE;
if (data->screen.sel == NULL)
return (WINDOW_COPY_CMD_MOVE);
/*
* Line selection normally starts with select-line, which sets
* up the reset positions used when the cursor changes
* direction. Do the same when changing an existing selection
* to line mode.
*/
if (data->cursordrag == CURSORDRAG_SEL) {
fx = data->endselx;
fy = data->endsely;
} else {
fx = data->selx;
fy = data->sely;
}
sx = data->selx;
sy = data->sely;
ex = data->endselx;
ey = data->endsely;
if (ey < sy || (ey == sy && ex < sx)) {
x = sx; sx = ex; ex = x;
y = sy; sy = ey; ey = y;
}
grid_reader_start(&gr, data->backing->grid, sx, sy);
grid_reader_cursor_start_of_line(&gr, 1);
grid_reader_get_cursor(&gr, &sx, &sy);
grid_reader_start(&gr, data->backing->grid, ex, ey);
grid_reader_cursor_end_of_line(&gr, 1, 0);
grid_reader_get_cursor(&gr, &ex, &ey);
data->rectflag = 0;
data->selrx = data->selx = sx;
data->selry = data->sely = sy;
data->endselrx = data->endselx = ex;
data->endselry = data->endsely = ey;
x = data->cx;
y = screen_hsize(data->backing) + data->cy - data->oy;
data->dx = fx;
data->dy = fy;
if (data->cursordrag != CURSORDRAG_NONE &&
(y < fy || (y == fy && x < fx))) {
data->lineflag = LINE_SEL_RIGHT_LEFT;
data->cursordrag = CURSORDRAG_SEL;
window_copy_scroll_to(wme, sx, sy, 1);
} else {
data->lineflag = LINE_SEL_LEFT_RIGHT;
if (data->cursordrag != CURSORDRAG_NONE) {
data->cursordrag = CURSORDRAG_ENDSEL;
x = window_copy_cursor_limit(wme, ey, 0);
window_copy_scroll_to(wme, x, ey, 1);
}
}
if (data->cursordrag == CURSORDRAG_NONE)
window_copy_set_selection(wme, 0, 0);
return (WINDOW_COPY_CMD_REDRAW);
}
return (WINDOW_COPY_CMD_MOVE); return (WINDOW_COPY_CMD_MOVE);
} }
@@ -4362,7 +4315,7 @@ window_copy_stringify(struct grid *gd, u_int py, u_int first, u_int last,
} }
if (dlen == 1) if (dlen == 1)
buf[bx++] = *d; buf[bx++] = *d;
else if (dlen != 0) { else {
memcpy(buf + bx, d, dlen); memcpy(buf + bx, d, dlen);
bx += dlen; bx += dlen;
} }
@@ -4912,8 +4865,6 @@ window_copy_search_marks(struct window_mode_entry *wme, struct screen *ssp,
cflags |= REG_ICASE; cflags |= REG_ICASE;
if (regcomp(&reg, sbuf, cflags) != 0) { if (regcomp(&reg, sbuf, cflags) != 0) {
free(sbuf); free(sbuf);
free(data->searchmark);
data->searchmark = NULL;
return (0); return (0);
} }
free(sbuf); free(sbuf);
@@ -5223,6 +5174,34 @@ window_copy_write_one(struct window_mode_entry *wme,
for (fx = 0; fx < nx; fx++) { for (fx = 0; fx < nx; fx++) {
grid_get_cell(gd, fx, fy, &gc); grid_get_cell(gd, fx, fy, &gc);
if (fx + gc.data.width <= nx) { if (fx + gc.data.width <= nx) {
#ifdef ENABLE_IMAGES
/*
* Write image-covered cells directly into the grid,
* skipping both window_copy_update_style() (so a
* selection, current-line or search-mark highlight
* never sweeps visibly over the image before it is
* recomposited separately - see the
* image_redraw_area() call below) and
* screen_write_cell(), whose built-in
* screen_write_image_damage() call (screen-write.c)
* fires on every write regardless of whether
* anything actually changed, which would needlessly
* re-damage - and so retransmit - the image on every
* redraw. The cell must still land in the grid as
* normal: a non-graphical client's ASCII fallback for
* the image is an ordinary character here, not
* something recomposited separately, and depends on
* this write the same as any other cell.
*/
if (image_grid_check_area(gd, fx, fy, gc.data.width,
1)) {
grid_view_set_cell(ctx->s->grid, px + fx, py,
&gc);
screen_write_cursormove(ctx,
px + fx + gc.data.width, py, 0);
continue;
}
#endif
window_copy_update_style(wme, fx, fy, &gc, mgc, cgc, window_copy_update_style(wme, fx, fy, &gc, mgc, cgc,
mkgc, clgc); mkgc, clgc);
if (gc.flags & GRID_FLAG_PADDING) { if (gc.flags & GRID_FLAG_PADDING) {
@@ -5471,6 +5450,29 @@ window_copy_write_line(struct window_mode_entry *wme,
window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py,
content_sx, &mgc, &cgc, &mkgc, &clgc); content_sx, &mgc, &cgc, &mkgc, &clgc);
#ifdef ENABLE_IMAGES
/*
* Copy the backing line's image layers separately from its text
* cells. This is not implied by the text cells just written above:
* those go via the pane's normal (frequently fast, direct-write)
* path, which knows nothing about image content.
*
* Only report it as needing a redraw when the view has actually
* moved (data->image_refresh, set once per window_copy_redraw_lines
* call) - text writes never touch image-covered cells (see
* window_copy_write_one), so if the view is unmoved this row's
* images are already exactly as they should be and redrawing them
* anyway just flashes the image on every unrelated redraw (e.g.
* every step of a selection drag) for no visible benefit.
*/
image_grid_free_line(s->grid,
&s->grid->linedata[s->grid->hsize + py]);
image_grid_copy_area(s->grid, width, s->grid->hsize + py,
data->backing->grid, 0, hsize - data->oy + py, content_sx, 1);
if (data->image_refresh)
image_redraw_area(ctx, width, py, content_sx, 1);
#endif
if (py == 0 && s->rupper < s->rlower && !data->hide_position) { if (py == 0 && s->rupper < s->rlower && !data->hide_position) {
value = options_get_string(oo, "copy-mode-position-format"); value = options_get_string(oo, "copy-mode-position-format");
if (*value != '\0') { if (*value != '\0') {
@@ -5531,6 +5533,55 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y)
window_copy_redraw_lines(wme, start, end - start + 1); window_copy_redraw_lines(wme, start, end - start + 1);
} }
#ifdef ENABLE_IMAGES
/*
* Only rows whose underlying history position has moved since the last
* call need their images recomposited - most redraws are just a selection
* or cursor-line style change with the view otherwise unmoved, and that
* never touches image-covered cells (see window_copy_write_one()), so
* redrawing images for it is needless: on a fast drag it is visible as the
* image briefly flashing on every step even though nothing about it
* actually changed.
*
* Every place that calls window_copy_write_line()/window_copy_write_lines()
* must call this first - it is not implied by them, since some (the
* initial full-screen draw on entering copy mode, window_copy_scroll_up(),
* window_copy_scroll_down()) write directly rather than going through
* window_copy_redraw_lines().
*/
static void
window_copy_update_image_refresh(struct window_copy_mode_data *data)
{
u_int base;
base = screen_hsize(data->backing) - data->oy;
data->image_refresh = !data->image_base_set || base != data->image_base;
data->image_base = base;
data->image_base_set = 1;
}
/*
* Whether any part of the currently visible backing range carries image
* data. A terminal's line insert/delete only shifts character cells - the
* pixels of a sixel or Kitty image already on screen stay exactly where
* they were sent, so a scroll that uses that fast path (see
* window_copy_scroll_up()/window_copy_scroll_down()) leaves stale or
* missing image content for every row except the single new one it
* explicitly rewrites. When this returns true, callers should fall back to
* a full window_copy_redraw_screen() instead, so every visible row's image
* is recomposited at its correct new position.
*/
static int
window_copy_visible_has_images(struct window_copy_mode_data *data)
{
struct grid *gd = data->backing->grid;
u_int sy = screen_size_y(&data->screen);
return (image_grid_check_area(gd, 0, screen_hsize(data->backing) -
data->oy, screen_size_x(&data->screen), sy));
}
#endif
static void static void
window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny) window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
{ {
@@ -5540,6 +5591,10 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
struct screen_write_ctx ctx; struct screen_write_ctx ctx;
u_int i; u_int i;
#ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data);
#endif
if (window_copy_line_number_width(wme) != 0) { if (window_copy_line_number_width(wme) != 0) {
screen_write_start(&ctx, &data->screen); screen_write_start(&ctx, &data->screen);
for (i = py; i < py + ny; i++) for (i = py; i < py + ny; i++)
@@ -6928,6 +6983,13 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny)
window_copy_redraw_screen(wme); window_copy_redraw_screen(wme);
return; return;
} }
#ifdef ENABLE_IMAGES
if (window_copy_visible_has_images(data)) {
window_copy_redraw_screen(wme);
return;
}
window_copy_update_image_refresh(data);
#endif
if (window_copy_line_numbers_active(wme)) { if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) != if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
@@ -6999,6 +7061,13 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny)
window_copy_redraw_screen(wme); window_copy_redraw_screen(wme);
return; return;
} }
#ifdef ENABLE_IMAGES
if (window_copy_visible_has_images(data)) {
window_copy_redraw_screen(wme);
return;
}
window_copy_update_image_refresh(data);
#endif
if (window_copy_line_numbers_active(wme)) { if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) != if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-panes.c,v 1.5 2026/08/25 06:04:33 nicm Exp $ */ /* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1009,7 +1009,6 @@ window_panes_run_command(struct window_panes_modedata *data, struct client *c,
} else { } else {
new_item = cmdq_get_command(cmdlist, NULL); new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} }
free(expanded); free(expanded);
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-switch.c,v 1.3 2026/08/25 07:23:30 nicm Exp $ */ /* $OpenBSD: window-switch.c,v 1.2 2026/07/14 17:17:18 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -227,8 +227,7 @@ window_switch_build(struct window_switch_modedata *data)
m = xreallocarray(m, n + 1, sizeof *m); m = xreallocarray(m, n + 1, sizeof *m);
m[n++] = item; m[n++] = item;
} }
if (n > 1) qsort(m, n, sizeof *m, window_switch_compare);
qsort(m, n, sizeof *m, window_switch_compare);
free(data->matches); free(data->matches);
data->matches = m; data->matches = m;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-tree.c,v 1.96 2026/08/25 07:23:30 nicm Exp $ */ /* $OpenBSD: window-tree.c,v 1.95 2026/08/24 07:06:58 nicm Exp $ */
/* /*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1022,10 +1022,6 @@ window_tree_swap(void *cur_itemdata, void *other_itemdata,
window_tree_pull_item(other, &other_session, &other_winlink, window_tree_pull_item(other, &other_session, &other_winlink,
&other_pane); &other_pane);
if (cur_session == NULL || cur_winlink == NULL)
return (0);
if (other_session == NULL || other_winlink == NULL)
return (0);
if (cur_session != other_session) if (cur_session != other_session)
return (0); return (0);

147
window.c
View File

@@ -415,6 +415,7 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel)
TAILQ_INIT(&w->panes); TAILQ_INIT(&w->panes);
TAILQ_INIT(&w->z_index); TAILQ_INIT(&w->z_index);
TAILQ_INIT(&w->last_panes); TAILQ_INIT(&w->last_panes);
TAILQ_INIT(&w->damage);
w->active = NULL; w->active = NULL;
w->lastlayout = -1; w->lastlayout = -1;
@@ -451,7 +452,6 @@ window_destroy(struct window *w)
{ {
log_debug("window @%u destroyed (%d references)", w->id, w->references); log_debug("window @%u destroyed (%d references)", w->id, w->references);
active_remove_window(w);
window_unzoom(w, 0); window_unzoom(w, 0);
RB_REMOVE(windows, &windows, w); RB_REMOVE(windows, &windows, w);
@@ -461,6 +461,7 @@ window_destroy(struct window *w)
menu_destroy(w); menu_destroy(w);
window_destroy_panes(w); window_destroy_panes(w);
redraw_free_damage(w);
if (event_initialized(&w->name_event)) if (event_initialized(&w->name_event))
evtimer_del(&w->name_event); evtimer_del(&w->name_event);
@@ -673,7 +674,6 @@ void
window_pane_update_focus(struct window_pane *wp) window_pane_update_focus(struct window_pane *wp)
{ {
struct client *c; struct client *c;
struct window *w;
int focused = 0; int focused = 0;
if (wp != NULL && (~wp->flags & PANE_EXITED)) { if (wp != NULL && (~wp->flags & PANE_EXITED)) {
@@ -681,12 +681,10 @@ window_pane_update_focus(struct window_pane *wp)
focused = 0; focused = 0;
else { else {
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (c->session == NULL) if (c->session != NULL &&
continue; c->session->attached != 0 &&
w = active_get_effective_window(c, c->session);
if (c->session->attached != 0 &&
(c->flags & CLIENT_FOCUSED) && (c->flags & CLIENT_FOCUSED) &&
w == wp->window && c->session->curw->window == wp->window &&
c->overlay_draw == NULL && c->overlay_draw == NULL &&
wp->window->menu == NULL) { wp->window->menu == NULL) {
focused = 1; focused = 1;
@@ -715,6 +713,7 @@ int
window_set_active_pane(struct window *w, struct window_pane *wp, int notify) window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
{ {
struct window_pane *lastwp; struct window_pane *lastwp;
int unzoomed;
log_debug("%s: pane %%%u", __func__, wp->id); log_debug("%s: pane %%%u", __func__, wp->id);
@@ -722,7 +721,8 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
return (0); return (0);
if (w->modal != NULL && wp != w->modal) if (w->modal != NULL && wp != w->modal)
return (0); return (0);
if ((w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp)) unzoomed = (w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp);
if (unzoomed)
window_unzoom(w, 1); window_unzoom(w, 1);
lastwp = w->active; lastwp = w->active;
@@ -739,7 +739,19 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
} }
tty_update_window_offset(w); tty_update_window_offset(w);
server_redraw_window(w);
/*
* Unzooming changes every pane's geometry and needs a full window
* redraw. Otherwise, only the previous and new active pane's border
* and status appearance changed, so avoid redrawing unaffected pane
* content.
*/
if (unzoomed)
server_redraw_window(w);
else {
server_redraw_window_borders(w);
server_status_window(w);
}
if (notify) if (notify)
window_fire_pane_changed(w, w->active, lastwp); window_fire_pane_changed(w, w->active, lastwp);
@@ -1291,7 +1303,7 @@ window_destroy_panes(struct window *w)
} }
const char * const char *
window_printable_flags(struct client *c, struct winlink *wl, int escape) window_printable_flags(struct winlink *wl, int escape)
{ {
struct session *s = wl->session; struct session *s = wl->session;
static char flags[32]; static char flags[32];
@@ -1306,9 +1318,9 @@ window_printable_flags(struct client *c, struct winlink *wl, int escape)
flags[pos++] = '!'; flags[pos++] = '!';
if (wl->flags & WINLINK_SILENCE) if (wl->flags & WINLINK_SILENCE)
flags[pos++] = '~'; flags[pos++] = '~';
if (active_is_effective_window(c, s, wl)) if (wl == s->curw)
flags[pos++] = '*'; flags[pos++] = '*';
if (active_is_last_window(c, s, wl)) if (wl == TAILQ_FIRST(&s->lastw))
flags[pos++] = '-'; flags[pos++] = '-';
if (server_check_marked() && wl == marked_pane.wl) if (server_check_marked() && wl == marked_pane.wl)
flags[pos++] = 'M'; flags[pos++] = 'M';
@@ -1658,6 +1670,10 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy)
log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy); log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy);
screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL); screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL);
#ifdef ENABLE_IMAGES
if (sx > r->osx)
image_grid_resize_width(wp->base.grid, sx);
#endif
wme = TAILQ_FIRST(&wp->modes); wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL && wme->mode->resize != NULL) if (wme != NULL && wme->mode->resize != NULL)
@@ -2895,3 +2911,110 @@ window_pane_is_floating(struct window_pane *wp)
return (0); return (0);
return (1); return (1);
} }
/*
* Report damage for a floating pane's rectangle, grown by one cell on every
* side - a floating pane draws its border frame at xoff-1/yoff-1 through
* xoff+sx/yoff+sy (see the "floating" case in screen-redraw.c), one cell
* outside its own content area, so damage for just the content area leaves
* the frame's previous position undrawn as the pane moves. If a scrollbar
* is reserved, its side of the frame is pushed out further still by its
* width and padding (also matched in screen-redraw.c), so grow that side
* to match.
*/
static void
window_pane_damage_floating(struct window *w, struct window_pane *wp,
int xoff, int yoff, int sx, int sy)
{
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
if (window_pane_scrollbar_reserve(wp)) {
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
sb_left = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
else
sb_right = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
}
x0 = xoff - 1 - sb_left;
x1 = xoff + sx + sb_right;
y0 = yoff - 1;
y1 = yoff + sy;
if (x0 < 0)
x0 = 0;
if (y0 < 0)
y0 = 0;
if (x1 < x0 || y1 < y0)
return;
redraw_damage_window(w, (u_int)x0, (u_int)y0, (u_int)(x1 - x0) + 1,
(u_int)(y1 - y0) + 1);
}
/*
* Whether a pane's scrollbar strip - not its whole body - intersects a
* window-coordinate rectangle. A reserved scrollbar occupies a strip of
* scrollbar_style.width+pad columns just outside the pane's own content
* area (see the scrollbar-reserve case in layout_fix_panes(), layout.c),
* on whichever side w->sb_pos points to.
*/
static int
window_pane_scrollbar_intersects(struct window *w, struct window_pane *wp,
u_int x, u_int y, u_int sx, u_int sy)
{
int sb_x, sb_w, ix = (int)x, iy = (int)y, isx = (int)sx;
int isy = (int)sy;
if (!window_pane_scrollbar_reserve(wp))
return (0);
sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad;
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
sb_x = (int)wp->xoff - sb_w;
else
sb_x = (int)wp->xoff + (int)wp->sx;
return (sb_x < ix + isx && sb_x + sb_w > ix &&
(int)wp->yoff < iy + isy && (int)wp->yoff + (int)wp->sy > iy);
}
/*
* Report damage for only a floating pane's old and new area, rather than
* the whole window - a floating pane move or resize only disturbs what it
* was covering and what it now covers. Scrollbars aren't covered by the
* damage system, so a pane whose *scrollbar strip* (not its whole body)
* intersects either area is still flagged directly for a scrollbar redraw.
* Checking the whole pane body here, rather than just its narrow scrollbar
* strip, meant merely dragging over a pane's ordinary content set
* PANE_REDRAWSCROLLBAR on every such pane on every motion event, triggering
* a needless scrollbar redraw (and the redraw pass it forces) each time
* even though the scrollbar itself never moved.
*
* Shared by every command that drags a floating pane around by the mouse:
* resize-pane's own border drag (cmd-resize-pane.c), move-pane -M's
* alternate Alt-drag (cmd-join-pane.c), and split-window/new-pane's
* interactive resize of a newly-created floating pane (cmd-split-window.c).
*/
void
window_pane_redraw_floating(struct window *w, struct window_pane *wp,
int old_xoff, int old_yoff, int old_sx, int old_sy)
{
struct window_pane *loop;
window_pane_damage_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
window_pane_damage_floating(w, wp, wp->xoff, wp->yoff, wp->sx,
wp->sy);
TAILQ_FOREACH(loop, &w->panes, entry) {
if (window_pane_scrollbar_intersects(w, loop,
(u_int)old_xoff, (u_int)old_yoff, (u_int)old_sx,
(u_int)old_sy) ||
window_pane_scrollbar_intersects(w, loop, wp->xoff,
wp->yoff, wp->sx, wp->sy))
loop->flags |= PANE_REDRAWSCROLLBAR;
}
/* Session status formats may depend on the pane's new geometry. */
server_status_window(w);
server_redraw_window(w);
}