Compare commits

..

72 Commits

Author SHA1 Message Date
Michael Grant
6b3d0946b4 docs: preserve established image semantics in redraw plan 2026-08-20 12:25:43 +01:00
Michael Grant
588dc54753 WIP: preserve image redraw experiment and recovery plan 2026-08-19 15:46:09 +01:00
Michael Grant
ec9859b15f 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-18 22:57:27 +01:00
Michael Grant
3f5f9cab90 build: rename image support option 2026-08-18 12:03:19 +01:00
Michael Grant
33bbd60d98 Merge branch 'master' into 4902-image-support 2026-08-17 21:47:49 +01:00
Michael Grant
48658fd659 Merge branch 'master' into 4902-image-support 2026-08-17 21:39:27 +01:00
Michael Grant
1962365106 Cleanup and simplification. 2026-08-17 08:42:29 +01:00
Michael Grant
870dba6664 Fix indents. 2026-08-17 07:19:56 +01:00
Michael Grant
6ef98552da Readability and clarity. 2026-08-16 17:09:11 +01:00
Michael Grant
a4abd61663 image: add manual visual checks 2026-08-10 15:46:47 +01:00
Michael Grant
b263d13df0 image: preserve fractional cell extents 2026-08-10 15:08:26 +01:00
Michael Grant
dadc6ef69f image: select backend after terminal setup 2026-08-10 14:34:16 +01:00
Michael Grant
001b10f2b3 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-10 12:04:37 +01:00
Michael Grant
a5808bf868 Change some names of things for clarity. 2026-08-09 22:13:15 +01:00
Michael Grant
e97c3d697d Redraw SIXEL images before or after text depending on which was placed first in the cell. 2026-08-09 08:32:25 +01:00
Michael Grant
c6a59c29ae 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-08 22:35:10 +01:00
Michael Grant
0ac7289c48 Cleanup. 2026-08-08 22:16:24 +01:00
Michael Grant
7dfd835c3f Update Kitty image names 2026-08-08 10:58:15 +01:00
Michael Grant
bbcccee9fc More cleanup of "histogram" name. 2026-08-08 10:51:33 +01:00
Michael Grant
4819dc34df Shorten image and SIXEL names 2026-08-08 10:51:17 +01:00
Michael Grant
6535274d98 Avoid overflow in image cell size calculation 2026-08-08 10:34:00 +01:00
Michael Grant
9a3b71ac64 Move image cell drawing into image.c 2026-08-08 10:27:31 +01:00
Michael Grant
4c9523c814 Expose image backend flags 2026-08-08 10:22:05 +01:00
Michael Grant
7e6c49b5a5 Do not enable sync for SIXEL 2026-08-08 09:14:07 +01:00
Michael Grant
468259933f Comment sixel_from_image(). 2026-08-07 22:42:22 +01:00
Michael Grant
4380fceb2b Add comments to image functions 2026-08-07 22:16:16 +01:00
Michael Grant
08c237f0cb Preserve text behind transparent Kitty images. 2026-08-07 22:06:09 +01:00
Michael Grant
5d0d6c29b4 Revert "Clear text beneath SIXEL image placements."
This reverts commit 64cea48f91.
2026-08-07 19:14:46 +01:00
Michael Grant
64cea48f91 Clear text beneath SIXEL image placements. 2026-08-07 16:39:28 +01:00
Michael Grant
bc998ede98 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-07 16:13:07 +01:00
Michael Grant
5c496c765a Preserve Kitty image crop width after cell rounding. 2026-08-07 16:09:00 +01:00
Michael Grant
3cd6526d46 Fix underlay bookkeeping bug. 2026-08-07 11:34:16 +01:00
Michael Grant
1dacc18ce0 Dither alpha in SIXEL output 2026-08-07 11:21:10 +01:00
Michael Grant
e3f6c1849d Revert "Clear text beneath graphical images"
This reverts commit fb589fd9f9.
2026-08-07 09:27:20 +01:00
Michael Grant
a8dfb979e6 Use compact image references in grid cells 2026-08-06 22:36:46 +01:00
Michael Grant
fb589fd9f9 Clear text beneath graphical images 2026-08-06 22:17:19 +01:00
Michael Grant
945152029d Pass through wrapped Kitty graphics 2026-08-06 19:08:59 +01:00
Michael Grant
8536e26517 Simplify image support terminology 2026-08-06 16:17:25 +01:00
Michael Grant
01d5c9f420 Use one image support switch 2026-08-06 14:27:33 +01:00
Michael Grant
a35871110f Make image types opaque 2026-08-06 12:24:10 +01:00
Michael Grant
cdff709ab3 Inline basic image fallback renderer 2026-08-06 10:25:29 +01:00
Michael Grant
2788616b6f Add bptato's test to the regression test script. 2026-08-06 09:11:45 +01:00
Michael Grant
51f1c0de51 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-06 00:06:59 +01:00
Michael Grant
045a25c89a 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-05 23:26:11 +01:00
Michael Grant
abfd8cee51 Fix SIXEL median-cut palette generation 2026-08-05 21:44:25 +01:00
Michael Grant
ccc90ea220 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-05 21:38:28 +01:00
Michael Grant
6cb01394ed Merge remote-tracking branch 'origin/master' into 4902-image-support
# Conflicts:
#	tmux.h
2026-08-05 14:17:14 +01:00
Michael Grant
6b8d581135 Fix kitty image resize issue when the kitty image resized smaller than the image, the image was overflowing lines and banding. 2026-08-05 13:51:07 +01:00
Michael Grant
ab591ae430 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-05 13:22:15 +01:00
Michael Grant
d06eb0bc2b C=1 is parsed and stored as an image-placement flag, suppressing cursor movement and scrolling as required by the Kitty protocol. 2026-08-05 11:54:22 +01:00
Michael Grant
2a0e47fef0 Merge branch 'master' into 4902-image-support 2026-08-05 11:10:58 +01:00
Michael Grant
3d07d479db Keep SIXEL palettes across redraw spans 2026-08-04 08:25:34 +01:00
Michael Grant
ff9b3e5cd7 Rename image damage functions to redraw 2026-08-03 13:15:04 +01:00
Michael Grant
9416ffe9bf Correct image copyrights 2026-08-03 12:56:26 +01:00
Michael Grant
748fb9ff1c Remove obsolete Kitty diacritics comment 2026-08-03 12:55:58 +01:00
Michael Grant
cfab0590a4 Support Kitty source rectangle placements 2026-08-03 12:31:11 +01:00
Michael Grant
a4b8d1c822 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-03 12:24:55 +01:00
Michael Grant
7748e525a4 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-03 12:24:55 +01:00
Michael Grant
01db040717 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-03 12:22:23 +01:00
Michael Grant
94f6f3b125 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-03 10:49:41 +01:00
Michael Grant
da6812d15f 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-03 10:39:11 +01:00
Michael Grant
791752ffff Remove local image ASCII test tool references 2026-08-03 09:24:16 +01:00
Michael Grant
fe938ea657 Refactor to centralize image-specific rendering and lifecycle code in image.c and the image-* backends. 2026-08-03 09:13:45 +01:00
Michael Grant
f68aad1b41 Add tool to generate the kitty diacritics directly from the public UnicodeData.txt file. 2026-08-02 16:15:51 +01:00
Michael Grant
7225365b29 Add missing regression test files. 2026-08-02 12:37:09 +01:00
Michael Grant
d50902b14d Move image-diacritics.h into image-kitty.c. 2026-08-02 10:41:59 +01:00
Michael Grant
4c04fc805e Fix copyrights. 2026-08-02 10:38:26 +01:00
Michael Grant
27f12454a1 Add missing .h file. 2026-08-02 10:36:49 +01:00
Michael Grant
fe5f775f76 New image related files. 2026-08-01 08:27:42 +01:00
Michael Grant
4da7f897ea Fix image flickering. 2026-07-30 22:47:31 +01:00
Michael Grant
9a2060db06 Merge branch '4902-image-support' of github.com:tmux/tmux into 4902-image-support 2026-07-30 22:16:16 +01:00
Michael Grant
f4a57f8b95 Initial commit. Implement generalised image support. 2026-07-30 22:15:53 +01:00
58 changed files with 6235 additions and 1710 deletions

View File

@@ -17,7 +17,7 @@ jobs:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
if: github.repository == 'tmux/tmux'
timeout-minutes: 300
timeout-minutes: 45
strategy:
fail-fast: false

715
IMAGE-REDRAW-PLAN.md Normal file
View File

@@ -0,0 +1,715 @@
# Image Redraw and Damage Plan
## Purpose
Make image redraws correct and efficient for tiled panes, floating panes,
scrolling, copy-mode selection, pane borders, pane status lines, scrollbars,
and clients using different output protocols.
The intended model is retained: the visible screen is a scene of horizontal
cell spans. A change damages cell-aligned regions, and the compositor redraws
only scene spans intersecting those regions.
Synchronized output may hide unavoidable intermediate terminal updates, but
it must not be required for correctness.
## Current state and recovery point
The last clean, pushed commit on `4902-image-support` is:
```
ec9859b1 sixel: preserve image aspect across client geometries
```
That commit already contains the important persistent model:
- sparse image placements represented by spans attached to grid lines;
- input-protocol semantics and placement ordering;
- source coordinates for cropping placement spans;
- movement, copying, duplication, and removal of image spans with grid data;
- per-client output backends for Kitty, SIXEL, and fallback rendering; and
- cached image/rendering data.
The current uncommitted changes touch eight files and mix several attempted
optimizations. They are known to damage SIXEL in Mintty and cause unnecessary
redraws with floating panes. Do not commit them as the solution.
Before discarding them, save one patch outside the branch for reference:
```
git diff -- image.c regress/image-support.sh screen-redraw.c screen-write.c \
server-client.c tmux.h tty.c window-copy.c \
> /tmp/4902-image-redraw-experiment.patch
```
Then restore those tracked files to `ec9859b1` before beginning the new work.
Do this only after the user explicitly approves the rollback.
### Ideas worth retaining, but reimplementing separately
- Redraw damage should be restricted to scene-visible spans.
- Image-related selection tests are useful.
- Floating-pane and pane-status regression scenarios are useful.
- Window chrome must be emitted after pane image/text composition whenever
the same damaged region contains both.
- A placement's complete canvas may need to be represented even where its
pixels are transparent; visibility and alpha must then be tested while
compositing rather than by losing the placement geometry.
- Native scrolling should be selected according to the affected region and
backend behaviour.
### Changes not worth retaining in their present form
- Promoting an obscured-pane update to `PANE_REDRAW` and then sometimes to
`CLIENT_REDRAWWINDOW`.
- Repairing images directly from individual `tty_cmd_*` text functions.
- Separate ad-hoc redraw paths for images, selection, scrollbars, borders,
and floating panes.
- The current `TTY_CTX_IMAGE_SCROLL`, `TTY_CTX_PANE_FULL_WIDTH`, and
`CLIENT_REDRAWSCROLLBARS` plumbing as a combined patch.
- Assuming an unchanged pane-status string does not need physical redraw
after some other drawing operation has covered its cells.
## Required invariants
These rules should be written down in code comments and enforced throughout
the implementation.
1. **Input determines semantics.** Whether text removes an image layer, lies
above it, or lies below it is determined by the protocol received from the
application. It must not depend on whether the outer client uses Kitty,
SIXEL, or fallback output.
2. **Output backends encode a scene.** An output backend may choose different
escape sequences, caches, or cropping, but it must render the same logical
result.
3. **One compositor owns repair.** No output path may independently erase and
reconstruct image cells without going through the same scene ordering.
4. **Damage is regional.** Ordinary content changes do not become pane-wide
or window-wide redraws. Full redraw remains appropriate for attachment,
resize, geometry changes, terminal reset, or invalid scene geometry.
5. **Damage uses cell coordinates.** Pixel offsets are handled when an image
backend maps the damaged cells to a source-pixel crop.
6. **Logical state changes before physical output.** Grid text and image spans
are moved, removed, or inserted first. Rendering then reads the completed
logical state.
7. **Chrome is part of composition.** Pane borders, pane status lines,
scrollbars, menus, and floating panes must not be repaired independently in
an order that allows image output to cover them afterward.
8. **No-image behaviour remains unchanged.** When a damaged region does not
intersect image data or graphical overlays, existing fast text output and
native scrolling should remain available.
## Established image semantics
The image semantics present at the `ec9859b1` recovery point are a
compatibility requirement. They resulted from the review discussion in PR
#5445, particularly the temporal-versus-positional model described in
[Theory and Practice of Sprixels](https://nick-black.com/dankwiki/index.php?title=Theory_and_Practice_of_Sprixels#Transparency_and_z-ordering),
and were accepted after commit `a4abd616`, which added the corresponding
manual visual checks.
- SIXEL input is temporal. Transparent pixels preserve what existed when the
SIXEL was emitted. A later text write annihilates the affected SIXEL cells;
the SIXEL does not remain logically behind that text.
- Kitty input is positional. Negative-z placements remain below the text plane;
nonnegative placements remain above it. Text writes do not convert or remove
Kitty placements merely because the output client uses SIXEL.
- Overlapping placements remain separate ordered layers. Transparent pixels in
an upper placement reveal retained lower placements or text.
- Input protocol, z-index, and placement order determine the logical scene. The
attached client's output protocol only determines how that scene is encoded.
- Redraw, damage, scrolling, copy mode, and backend conversion must preserve
these rules exactly.
The manual checks introduced by `a4abd616` and the corresponding regression
cases are semantic compatibility tests, not merely visual diagnostics.
## Coordinate spaces
Keep conversions explicit. Do not pass ambiguous `x` and `y` values between
layers.
- **Grid coordinates:** include history and belong to a pane's `struct grid`.
- **Pane coordinates:** visible cells inside a pane.
- **Window coordinates:** include pane offsets and floating-pane positions.
- **Client scene coordinates:** window coordinates clipped by the client's
viewport (`ox`, `oy`, `sx`, and `sy`).
- **Terminal coordinates:** include the client status-line offset and any
terminal viewport offset.
- **Image source coordinates:** identify cell rows/columns within a placement;
the backend maps these to source pixels using terminal cell geometry and
placement pixel offsets.
Store damage in window coordinates. Translate and clip it separately for each
client when drawing, because clients may have different viewports, status-line
positions, cell geometries, and output backends.
## Compartmentalization
Keep image implementation details in the existing image files. The rest of
tmux should know only that some scene cells require image-aware composition.
### `image.c`
Own all protocol-independent image state and composition policy:
- placements, spans, input protocol, z-index, serial ordering, and alpha;
- applying input semantics when text overwrites image cells;
- testing whether a grid rectangle intersects image placements;
- mapping a damaged cell rectangle to ordered image fragments;
- deciding which logical image layers are below or above the text plane;
- generic backend dispatch; and
- public image helpers used by the redraw subsystem.
### `image-sixel.c`
Own only SIXEL-specific output details:
- cached quantized/indexed representations;
- mapping cell crops to pixel and six-pixel-band crops;
- emitting SIXEL fragments;
- alpha and palette encoding; and
- backend-specific reset, erase, and scrolling capabilities.
### `image-kitty.c`
Own only Kitty-specific output details:
- client-side transmission and placement caches;
- placement creation, clipping, movement, and deletion;
- Kitty IDs and geometry; and
- backend-specific reset and scrolling behaviour.
### Other tmux files
- `screen-redraw.c` owns scene construction, damage clipping, and the ordering
of pane content versus window chrome. It calls generic `image.c` helpers and
contains no Kitty or SIXEL protocol logic.
- `screen-write.c` updates logical grid state and reports pane/window damage.
It must not render image fragments or select an output backend.
- `tty.c` owns terminal text and scrolling primitives. It receives an already
selected native-scroll operation; it must not reconstruct image layers or
call image repair after individual characters.
- `server-client.c` schedules and drains per-client damage. It must not inspect
image placements or output protocols.
- `window-copy.c` reports copy-mode damage and copies logical image state using
generic image helpers; it must not render images itself.
- `tmux.h` exposes only the minimum generic structures and function prototypes
required across those boundaries.
Prefer one generic call from `screen-redraw.c` into `image.c` over `ENABLE_IMAGES`
branches scattered through tty commands. A build without image support should
retain the existing redraw path with minimal conditional code.
## Damage representation
Add a small regional damage abstraction owned by the redraw subsystem. Do not
add per-cell queues or enlarge `struct grid_cell`.
A damage entry needs:
```
x, y, sx, sy window-cell rectangle
reason content, scroll exposure, pane move, chrome, and so on
```
The exact names should follow tmux style. A short list or dynamic array per
client is sufficient. When a new rectangle is added:
- clip it to the window;
- discard empty rectangles;
- merge overlapping or directly adjacent rectangles where that does not make
the result substantially larger;
- cap the list at a modest number; above the cap, replace it with their union;
- promote to a full redraw only if geometry is invalid or the union covers
most of the client scene.
A shared window damage list must not be cleared after the first client draws.
Either append translated window rectangles to every affected client, or give
damage events monotonically increasing sequence numbers so each client can
consume them independently. The simpler initial implementation is to append
window-coordinate rectangles to each client currently displaying the window.
Suggested public operations are conceptually:
```
redraw_damage_window(window, x, y, sx, sy, reason)
redraw_damage_pane(window_pane, x, y, sx, sy, reason)
redraw_damage_client(client, x, y, sx, sy, reason)
```
Do not expose image-backend details through these calls.
## Regional scene composition
Extend `screen-redraw.c` so an existing `redraw_scene` can be drawn through a
set of clipping rectangles.
For every damaged line:
1. Intersect the damage rectangle with that line's scene spans.
2. Split only at damage boundaries; do not rebuild the complete scene.
3. Draw intersecting pane content in logical layer order.
4. Draw window chrome intersecting the same rectangle last.
For a pane span, the scene has already selected the topmost visible pane at
that position. Composition inside that pane should then be explicit:
1. image placements below the text plane, such as negative Kitty z-indexes;
2. the text plane, including the requested copy-mode style;
3. surviving temporal SIXEL placements and images above the text plane;
4. pane borders, pane status, scrollbars, menus, and other chrome.
The exact placement order is already represented by `struct image_placement`
and its input protocol, z-index, and serial. Do not derive it from the client's
output backend.
Only the topmost pane content is normally needed because panes are opaque.
Underlying pane content is retained in its own grid and becomes visible when a
floating pane moves, at which time the old floating-pane rectangle is damaged
and recomposed.
### Image fragments
An `image_span` is placement metadata, not a separately stored bitmap. For an
intersection, compute the corresponding source-cell rectangle and ask the
backend to emit that crop from its cached rendering.
- Do not rerun quantization or dithering for every small damage rectangle.
- SIXEL output operates in six-pixel vertical bands. Expand or clip the
backend operation as required, then ensure any additionally touched cells
are included in the same composition pass.
- A transparent top image requires lower image layers to be drawn first.
- Kitty placements may be deleted, clipped, or replaced only within the
damaged placement region.
## Fast text writes
Do not route every ordinary character through a deferred full compositor.
For an application text write:
1. Apply input-protocol damage rules to the logical image spans. For example,
text written after a SIXEL image removes the affected SIXEL coverage.
2. Test whether the affected cells still intersect any image layer or scene
overlay requiring composition.
3. If not, retain the existing direct `tty_cmd_cell`/`tty_cmd_cells` path.
4. If they do, suppress conflicting direct terminal repair and enqueue only
those cells for regional composition.
This decision belongs above the backend-specific tty output functions. Avoid
callbacks such as `image_draw_after_text()` from several `tty_cmd_*` functions;
they reproduce ordering logic in multiple places.
## Selection and copy mode
Copy mode must not mutate the application's logical image placements.
- Treat selection as a temporary tmux UI style on the text plane.
- Follow the chosen policy that selection highlighting does not overwrite
image-covered cells.
- Mouse motion should damage only cells whose selection state actually
changed.
- If changed selection cells do not intersect visible images, draw them using
the normal text fast path.
- If they intersect an image canvas, compose those cells once; do not erase a
whole image or pane.
- Entering or leaving copy mode should damage the visible pane rectangle once,
not repeatedly for every unchanged mouse event.
Copy-mode image spans should be copied before its text view is composed, but
this should be implemented through the same logical state and damage API, not
through special tty repair calls.
## Floating panes
### Focus changes
Changing the active pane should damage only cells whose appearance changes:
- affected pane borders;
- pane status cells whose style or contents change; and
- any active-pane indicators.
It must not redraw pane interiors or retransmit unrelated SIXEL images.
### Moving or resizing a floating pane
For an old rectangle `A` and new rectangle `B`:
- invalidate the scene geometry;
- damage `A union B` (represented as a few rectangles rather than necessarily
one large bounding box);
- rebuild the scene because pane ownership changed;
- recompose only spans intersecting those rectangles.
The old rectangle restores tiled panes and any image fragments that were
previously hidden. The new rectangle draws the floating pane and its chrome.
Multiple overlapping floating panes follow their window z-order.
## Scrolling
Implement scrolling in three stages. Do not begin with the most complicated
native-scroll optimization.
### Stage 1: correct regional redraw
1. Move grid text and image spans using the existing grid operations.
2. Record the pane scroll rectangle and newly exposed rows.
3. Recompose that rectangle from the completed logical scene.
This may still retransmit more image data than desired, but it establishes a
correct regional baseline without pane- or window-wide redraws.
### Stage 2: native scrolling without obstructions
Use the terminal's native scroll operation only when:
- the backend/terminal is known to scroll its image pixels consistently;
- the source and destination rectangle belong to the same visible pane;
- no floating pane, menu, overlay, pane border/status, or reserved scrollbar
intersects it;
- required vertical and horizontal margins are available and reliable; and
- existing BCE and terminal-size checks permit the operation.
After native scrolling, compose only newly exposed rows and any protocol-sized
boundary expansion.
### Stage 3: mixed native scrolling around obstructions
Derive safe rectangles from visible scene spans:
- a destination cell is native-scrollable only when its source cell and
destination cell are both visible parts of the same pane;
- coalesce equal horizontal ranges across adjacent rows into rectangles;
- native-scroll those maximal rectangles;
- recompose cells crossing an obstruction boundary and newly exposed cells.
With one floating pane, left and right strips can often scroll over the full
height. Above and below it, cells whose source crosses behind the floating pane
must be recomposed rather than recovered from terminal pixels.
As a simpler optional strategy, native-scroll the full tiled-pane rectangle
and recompose the floating pane's old/current rectangles afterward. This is
correct if all disturbed cells are marked, but may visibly flash without
synchronized output. Prefer safe-rectangle scrolling once correctness is
established.
If rectangle decomposition becomes too fragmented, fall back to regional
composition. It is an optimization, never a separate correctness path.
## Redraw scheduling
Do not turn queued regional damage into `CLIENT_REDRAWWINDOW` merely because
the tty output buffer is nonempty. Preserve the damage list and process it
after output drains.
Use full redraw only for:
- initial attachment;
- client or terminal geometry changes;
- a terminal reset or invalid backend cache;
- layout/viewport changes where rebuilding and clipping regions is not yet
safe; or
- a damage union large enough that full redraw is cheaper.
Focus changes, selection changes, ordinary scrolling, and moving one floating
pane should remain regional.
## Debugging instrumentation
Add temporary debug logging before optimizing:
- damage reason and window rectangle;
- client clipping and coordinate translation;
- scene generation used;
- native-scroll rectangles selected;
- spans recomposed in each layer phase; and
- image fragments emitted by each backend.
The log should make it possible to answer, for every SIXEL retransmission,
which damage rectangle required it. Remove excessively noisy instrumentation
before the final commit or keep it behind normal verbose logging.
## Tests
Build the tests before each optimization and keep each test focused on one
invariant.
### Automated regressions
1. **No-image baseline:** ordinary output, scrolling, copy mode, floating pane
focus, borders, status lines, and scrollbars behave unchanged.
2. **Focus only:** changing focus with a SIXEL elsewhere emits no SIXEL DCS and
updates only the affected border/status cells.
3. **Selection outside image:** emits no image output.
4. **Selection crossing image:** the image remains intact during and after the
drag; only changed non-image cells are highlighted.
5. **Unobstructed scroll:** logical image spans move with text; where native
scrolling is supported, no complete SIXEL retransmission occurs.
6. **Scroll with floating pane:** both pane-status lines, borders, scrollbar,
floating-pane contents, and visible image fragments remain correct.
7. **Move floating pane:** old and new rectangles are restored; unrelated image
regions are not emitted.
8. **Multiple floating panes:** overlapping z-order remains correct while the
top pane moves.
9. **Transparent placement:** lower images/text show through correctly, and
moving/selection does not treat transparency as absent placement geometry.
10. **Cross-backend clients:** the same input scene produces equivalent results
on Kitty, SIXEL, and fallback clients attached simultaneously.
11. **Pane status top/bottom:** unchanged status contents are physically
restored if their cells were damaged.
12. **Scrollbar reserved/overlay:** scroll thumb and track remain present and
images do not overwrite them.
Where possible, capture raw terminal output and assert both presence and
absence of DCS/APC sequences. `capture-pane` alone cannot prove physical output
ordering because it observes logical grid contents rather than terminal image
pixels.
### Manual matrix
Test at least:
- Microsoft Terminal with SIXEL;
- Mintty with SIXEL;
- WezTerm with SIXEL;
- Kitty with Kitty output;
- a Kitty client attached to a session created from a SIXEL client and the
reverse; and
- a terminal without graphical image support.
For each, test scrolling by application output and mouse wheel, selecting text,
entering/leaving copy mode, changing focus, opening/closing/moving a floating
pane, pane status at top and bottom, and scrollbars.
## Implementation steps and commit boundaries
This must not be implemented as one monolithic change. Each numbered step is a
separate commit, review, and testing gate. Do not begin the next step until the
current step passes its focused tests. If a step exposes a pre-existing bug,
record it before expanding the patch.
### Step 0: archive the experiment and recover the baseline
**Goal:** Begin development from known source rather than repairing the mixed
experimental diff.
**Work:**
- Preserve the current diff and this plan on a clearly named WIP branch.
- Return `4902-image-support` to clean commit `ec9859b1`.
- Build with and without image support.
- Run the existing image regression and ordinary tmux regression suite.
**Gate:** The branch is clean and existing behaviour is reproduced. No product
code is changed in this step.
### Step 1: add focused regressions and tracing
**Goal:** Establish observable failures before changing redraw behaviour.
**Primary files:** `regress/image-support.sh`, temporary debug logging in
`screen-redraw.c` if necessary.
**Work:** Add independent tests for focus-only changes, selection crossing an
image, unobstructed scrolling, scrolling beneath a floating pane, moving a
floating pane, both pane-status lines, and scrollbars. Raw terminal output must
be used where a test needs to prove that SIXEL or Kitty data was or was not
emitted.
**Gate:** Each test either passes on the baseline or has a documented expected
failure. Tests must not depend on timing races or visual screenshots. The
existing edge-case checks introduced by `a4abd616` must pass unchanged on the
recovery baseline and establish the semantic compatibility result expected by
later gates.
**Not included:** No redraw implementation changes.
### Step 2: introduce regional damage scheduling
**Goal:** Represent and preserve small window-coordinate damage rectangles
without changing the rendered result.
**Primary files:** `screen-redraw.c`, `server-client.c`, minimal declarations in
`tmux.h`.
**Work:** Add per-client damage storage, clipping, merging, a complexity cap,
and verbose logging. Existing callers may initially convert the damage to the
same redraw they used before. Pending tty output must retain the region rather
than promote it automatically to a full-window redraw.
**Gate:** No-image and image output remain visually and byte-for-byte equivalent
where practical. Unit-style tests verify rectangle clipping and merging.
**Not included:** No image-layer traversal, selection changes, floating-pane
optimization, or native scrolling.
### Step 3: add a generic image-region composition API
**Goal:** Give the scene compositor one protocol-independent way to render the
image layers intersecting a cell rectangle.
**Primary files:** `image.c`, `image-sixel.c`, `image-kitty.c`, with only generic
prototypes in `tmux.h`.
**Work:**
- In `image.c`, intersect damage with placement spans and preserve logical
input/z/serial order.
- In `image-sixel.c`, emit a crop from cached quantized data and handle SIXEL
band alignment.
- In `image-kitty.c`, update/delete/place only the intersecting cached
placements.
- Define clearly separate below-text and above-text passes.
**Gate:** Rendering a whole image as adjacent damaged rectangles produces the
same final scene as rendering it in one operation. Test transparent and
overlapping placements on both output backends. The `a4abd616` semantic
compatibility checks must continue to pass unchanged.
**Not included:** No changes to individual tty text commands and no native
scroll optimization.
### Step 4: clip scene composition to damage rectangles
**Goal:** Make `screen-redraw.c` repaint only scene spans intersecting queued
damage.
**Primary files:** `screen-redraw.c`; generic calls into `image.c`.
**Work:** For each damaged line, intersect existing redraw spans, compose the
selected pane's below-text images, text, and above-text images, then draw
intersecting chrome last. Full redraw continues to use the same compositor with
a full-client damage rectangle.
**Gate:** Full and tiled panes redraw correctly; pane borders, pane status,
scrollbars, menus, and images have deterministic output order. No floating-pane
movement or scroll optimization is added yet.
### Step 5: route text and selection damage through the compositor
**Goal:** Repair only cells where text or copy-mode selection interacts with
image layers.
**Primary files:** `screen-write.c`, `window-copy.c`, `screen-redraw.c`, and
logical helpers in `image.c`.
**Work:** Apply input semantics to logical spans first. Preserve direct tty text
output when no image placement intersects the cells. Otherwise enqueue those
cells for composition. Selection changes damage only cells whose selection
state changed and follow the policy of not overwriting image-covered cells.
**Gate:** Dragging selection across an image never damages it, entering/leaving
copy mode restores the same scene, and selection outside images emits no image
data. The `a4abd616` semantic compatibility checks must continue to pass
unchanged.
**Not included:** Do not add image callbacks to `tty_cmd_cell`,
`tty_cmd_cells`, or `tty_cmd_clearcharacter`.
### Step 6: make floating-pane changes regional
**Goal:** Focus, movement, resize, open, and close operations damage only the
cells whose scene ownership or chrome changed.
**Primary files:** floating-pane/window code that reports geometry changes,
plus `screen-redraw.c`. Image implementation remains in the image files.
**Work:** Focus changes enqueue border/status cells only. Movement and resize
enqueue the old and new pane rectangles and invalidate scene geometry. Regional
composition restores underlying panes/images in the old rectangle and draws
the floating pane in the new rectangle.
**Gate:** Changing focus emits no unrelated SIXEL/APC. Moving a floating pane
does not damage images, either pane-status line, borders, or scrollbars. Test
one and multiple overlapping floating panes.
### Step 7: implement correct regional scrolling without native optimization
**Goal:** Establish correct scrolling using logical state plus regional
composition.
**Primary files:** grid/image state helpers in `image.c`, damage reporting in
`screen-write.c`, composition in `screen-redraw.c`.
**Work:** Move text and image spans first, then damage the scroll rectangle and
compose it. Do not use special image repair from `tty.c`.
**Gate:** Application scrolling, mouse-wheel copy-mode scrolling, partial
scroll regions, scrollbars, and scrolling beneath floating panes are correct on
Kitty, Microsoft Terminal, Mintty, and WezTerm. This step may redraw more than
the final optimized version, but must not redraw outside the scroll rectangle.
The `a4abd616` semantic compatibility checks must continue to pass unchanged.
### Step 8: enable native scrolling for unobstructed rectangles
**Goal:** Avoid retransmitting image pixels when the terminal can safely move
them.
**Primary files:** scroll planning in `screen-redraw.c`, generic backend
capability in `image.c`, backend facts in `image-sixel.c` and `image-kitty.c`,
terminal primitive execution in `tty.c`.
**Work:** Select native scrolling only when source/destination cells belong to
the same unobstructed pane and terminal margins/backend behaviour permit it.
Compose newly exposed rows afterward. Retain Step 7 as the fallback.
**Gate:** Raw output proves the native scroll is used and a complete image is
not retransmitted. Final output must match Step 7 on every tested backend.
### Step 9: decompose scrolling around floating panes
**Goal:** Combine native scrolling of safe rectangles with composition of
obstructed and boundary rectangles.
**Primary files:** scroll planning and scene-span traversal in
`screen-redraw.c`; no new protocol logic outside the image files.
**Work:** Derive maximal rectangles where both source and destination are
visible cells of the same pane, coalesce adjacent equal ranges, natively scroll
them, and compose everything else. Enforce a fragmentation threshold that
falls back to Step 7.
**Gate:** Scrolling beneath one or several floating panes produces the same
final scene as Step 7, reduces emitted image data, and never loses pane status,
borders, scrollbars, or images.
### Step 10: cleanup, performance checks, and full regression
**Goal:** Remove superseded paths only after all replacements are proven.
**Work:** Remove obsolete redraw helpers and flags introduced or made unused by
the new path. Reduce temporary logging. Check memory growth, rectangle-list
bounds, output byte counts, and behaviour without image support. Run the entire
regression suite and the manual terminal matrix.
**Gate:** All completion criteria below are satisfied. Synchronized output may
improve appearance, but disabling it does not alter final contents. The
`a4abd616` semantic compatibility checks must continue to pass unchanged.
Do not squash these implementation steps during development. Keeping their
boundaries visible makes it possible to bisect correctness and performance
regressions. They can be reorganized later only after review.
## Completion criteria
The work is complete when:
- Mintty, Microsoft Terminal, WezTerm, and Kitty show no persistent damage;
- focus and selection do not retransmit unrelated images;
- floating-pane movement redraws only old/new affected regions;
- scrolling preserves images, pane borders, both tiled and floating pane
status lines, and scrollbars;
- input semantics produce the same composition on every output backend;
- no-image performance and behaviour remain unchanged;
- full regression tests pass; and
- synchronized output improves appearance but disabling it never changes the
final screen contents.

View File

@@ -5,7 +5,8 @@ CLEANFILES = tmux.1.mdoc tmux.1.man cmd-parse.c
# Distribution tarball options.
EXTRA_DIST = \
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]
# Preprocessor flags.
@@ -246,9 +247,9 @@ if HAVE_UTF8PROC
nodist_tmux_SOURCES += compat/utf8proc.c
endif
# Enable sixel support.
if ENABLE_SIXEL
dist_tmux_SOURCES += image.c image-sixel.c
# Enable image support.
if ENABLE_IMAGES
dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c
endif
# Add bits for fuzzing if enabled.

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */
/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -712,9 +712,6 @@ client_dispatch_wait(struct imsg *imsg)
fprintf(stderr, "server version is too old for client\n");
proc_exit(client_proc);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}
@@ -801,8 +798,5 @@ client_dispatch_attached(struct imsg *imsg)
system(data);
proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */
/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */
/*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -850,18 +850,7 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...)
if (c == NULL) {
cmd_get_source(cmd, &file, &line);
if (!cfg_finished) {
if (file != NULL)
cfg_add_cause("%s:%u: %s", file, line, msg);
else
cfg_add_cause("%s", msg);
} else {
if (file != NULL) {
server_add_message("message: %s:%u: %s", file,
line, msg);
} else
server_add_message("message: %s", msg);
}
cfg_add_cause("%s:%u: %s", file, line, msg);
} else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) {
server_add_message("%s message: %s", c->name, msg);
if (~c->flags & CLIENT_UTF8) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -530,15 +530,34 @@ if test "x$enable_cgroups" = xyes; then
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(
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
AC_DEFINE(ENABLE_SIXEL)
enable_images=yes
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.
AC_MSG_CHECKING(for b64_ntop)
@@ -1137,10 +1156,10 @@ if test "x$enable_asan" = xyes; then
else
AC_MSG_NOTICE([ASAN: off])
fi
if test "x$enable_sixel" = xyes; then
AC_MSG_NOTICE([SIXEL: on])
if test "x$enable_images" = xyes; then
AC_MSG_NOTICE([images: on])
else
AC_MSG_NOTICE([SIXEL: off])
AC_MSG_NOTICE([images: off])
fi
if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on])

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */
/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */
/*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -388,25 +388,6 @@ control_pause_pane(struct client *c, struct window_pane *wp)
}
}
/*
* Reset a pane after its buffer has been replaced: drop any output still
* queued from the old buffer and start again from the pane's own offset.
*/
void
control_reset_pane(struct client *c, struct window_pane *wp)
{
struct control_pane *cp;
if (c->control_state == NULL)
return;
cp = control_get_pane(c, wp);
if (cp == NULL)
return;
control_discard_pane(c, cp);
memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
}
/* Write an already-formatted line, queueing it behind %output if needed. */
static void
control_write_line(struct client *c, char *line)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */
/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -3018,6 +3018,17 @@ format_cb_sixel_support(__unused struct format_tree *ft)
#endif
}
/* Callback for image_support. */
static void *
format_cb_image_support(__unused struct format_tree *ft)
{
#ifdef ENABLE_IMAGES
return (xstrdup("1"));
#else
return (xstrdup("0"));
#endif
}
/* Callback for active_window_index. */
static void *
format_cb_active_window_index(struct format_tree *ft)
@@ -3681,6 +3692,9 @@ static const struct format_table_entry format_table[] = {
{ "host_short", FORMAT_TABLE_STRING,
format_cb_host_short
},
{ "image_support", FORMAT_TABLE_STRING,
format_cb_image_support
},
{ "insert_flag", FORMAT_TABLE_STRING,
format_cb_insert_flag
},
@@ -4387,7 +4401,7 @@ format_quote_shell(const char *s)
at = out = xmalloc(strlen(s) * 2 + 1);
for (cp = s; *cp != '\0'; cp++) {
if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL)
if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL)
*at++ = '\\';
*at++ = *cp;
}

82
grid.c
View File

@@ -86,6 +86,9 @@ grid_check_is_clear(struct grid *gd)
assert(gl->extdsize == 0);
assert(gl->flags == 0);
assert(gl->time == 0);
#ifdef ENABLE_IMAGES
assert(gl->images == NULL);
#endif
}
}
#else
@@ -353,6 +356,9 @@ grid_free_line(struct grid *gd, u_int py)
assert(gl->cellsize == 0 || gl->celldata != NULL);
#endif
#ifdef ENABLE_IMAGES
image_grid_free_line(gd, gl);
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
@@ -397,6 +403,9 @@ void
grid_destroy(struct grid *gd)
{
grid_free_lines(gd, 0, gd->hsize + gd->sy);
#ifdef ENABLE_IMAGES
image_grid_free(gd);
#endif
free(gd->linedata);
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)
return;
#ifdef ENABLE_IMAGES
image_grid_damage(gd, px, py, nx, ny);
#endif
if (px == 0 && nx == gd->sx) {
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
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;
if (ny == 0)
@@ -775,8 +791,18 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
return;
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);
#ifdef ENABLE_IMAGES
gl->images = images;
#endif
}
if (py != 0)
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, dx + nx, 8);
#ifdef ENABLE_IMAGES
image_grid_move_cells(gd, dx, px, py, nx);
#endif
memmove(&gl->celldata[dx], &gl->celldata[px],
nx * sizeof *gl->celldata);
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);
if (gc.flags & GRID_FLAG_PADDING)
continue;
if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) {
grid_string_cells_code(*lastgc, &gc, code, sizeof code,
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;
u_int yy;
#ifdef ENABLE_IMAGES
u_int original_dy, original_sy;
#endif
if (dy + ny > dst->hsize + dst->sy)
ny = dst->hsize + dst->sy - dy;
if (sy + ny > src->hsize + src->sy)
ny = src->hsize + src->sy - sy;
#ifdef ENABLE_IMAGES
original_dy = dy;
original_sy = sy;
#endif
grid_free_lines(dst, dy, ny);
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];
memcpy(dstl, srcl, sizeof *dstl);
#ifdef ENABLE_IMAGES
dstl->images = NULL;
#endif
if (srcl->cellsize != 0) {
dstl->celldata = xreallocarray(NULL,
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++;
dy++;
}
#ifdef ENABLE_IMAGES
image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny);
#endif
}
/* Mark line as dead. */
@@ -1321,6 +1362,30 @@ grid_reflow_dead(struct grid_line *gl)
gl->flags = GRID_LINE_DEAD;
}
/* Image rows are cell-aligned and must not be reflowed like text. */
static int
grid_reflow_has_image(struct grid_line *gl)
{
struct grid_cell gc;
u_int i;
#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. */
static struct grid_line *
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;
line = yy + 1 + lines;
/* Do not join wrapped text to a cell-aligned image row. */
if (grid_reflow_has_image(&gd->linedata[line]))
break;
/* If the next line is empty, skip it. */
if (~gd->linedata[line].flags & GRID_LINE_WRAPPED)
wrapped = 0;
@@ -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. */
for (i = yy + 1; i < yy + 1 + lines; i++) {
free(gd->linedata[i].celldata);
free(gd->linedata[i].extddata);
grid_free_line(gd, 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)
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
* the available width is hit, and width is the full line

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

1620
image.c

File diff suppressed because it is too large Load Diff

109
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */
/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -145,6 +145,9 @@ struct input_ctx {
*/
struct evbuffer *since_ground;
struct event ground_timer;
#ifdef ENABLE_IMAGES
void *kitty_state;
#endif
};
/* 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_enter_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_exit_rename(struct input_ctx *);
@@ -916,6 +922,9 @@ input_free(struct input_ctx *ictx)
event_del(&ictx->request_timer);
free(ictx->input_buf);
#ifdef ENABLE_IMAGES
kitty_free_state(ictx->kitty_state);
#endif
evbuffer_free(ictx->since_ground);
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);
else
screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
screen_write_reset(sctx);
screen_write_stop(sctx);
}
@@ -1065,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);
else
screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id,
ictx->state->name, len, (int)len, buf);
@@ -1084,6 +1095,7 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s,
return;
screen_write_start_callback(sctx, s, cb, arg);
sctx->flags |= SCREEN_WRITE_INPUT;
input_parse(ictx, buf, len);
screen_write_stop(sctx);
}
@@ -1583,7 +1595,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1:
break;
case 0:
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
input_reply(ictx, 1, "\033[?1;2;4c");
#else
input_reply(ictx, 1, "\033[?1;2c");
@@ -1978,7 +1990,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx)
screen_write_mode_clear(sctx, MODE_BRACKETPASTE);
break;
case 2026:
screen_write_end_sync(sctx);
screen_write_stop_sync(ictx->wp);
break;
case 2031:
screen_write_mode_clear(sctx, MODE_THEME_UPDATES);
@@ -2101,7 +2113,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
int n, m, o;
if (ictx->param_list_len > 3)
@@ -2626,10 +2638,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
const char prefix[] = "tmux;";
const u_int prefixlen = (sizeof prefix) - 1;
long long allow_passthrough = 0;
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
struct window *w;
struct sixel_image *si;
int p2;
int p1, p2;
#endif
if (wp == NULL)
@@ -2642,15 +2654,18 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0);
}
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
w = wp->window;
if (input_split(ictx) != 0)
return (0);
p1 = input_get(ictx, 0, 0, 0);
if (p1 == -1)
p1 = 0;
p2 = input_get(ictx, 1, 0, 0);
if (p2 == -1)
p2 = 0;
si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel);
si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel);
if (si != NULL)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
}
@@ -2789,6 +2804,57 @@ input_enter_apc(struct input_ctx *ictx)
}
/* 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
input_exit_apc(struct input_ctx *ictx)
{
@@ -2799,6 +2865,13 @@ input_exit_apc(struct input_ctx *ictx)
return;
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 &&
options_get_number(wp->options, "allow-set-title") &&
screen_set_title(sctx->s, ictx->input_buf, 1)) {
@@ -2859,6 +2932,12 @@ input_top_bit_set(struct input_ctx *ictx)
{
struct screen_write_ctx *sctx = &ictx->ctx;
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;
@@ -2886,6 +2965,20 @@ input_top_bit_set(struct input_ctx *ictx)
utf8_copy(&ictx->cell.cell.data, ud);
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);
ictx->flags |= INPUT_LAST;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.97 2026/08/20 09:19:24 nicm Exp $ */
/* $OpenBSD: layout.c,v 1.96 2026/07/15 13:02:33 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1650,10 +1650,7 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
if (window_active_pane_is_over_zoom(w))
window_push_zoom(w, 0, 1);
else
window_push_zoom(w, 1, (flags & SPAWN_ZOOM));
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
lc = layout_split_pane(wp, type, size, flags);
if (lc == NULL)
*cause = xstrdup("no space for a new pane");
@@ -1680,10 +1677,8 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
if (flags & SPAWN_FLOATOVERZOOM)
window_push_zoom(wp->window, 0, 1);
else if (window_active_pane_is_over_zoom(w))
window_push_zoom(wp->window, 0, 1);
if (flags & SPAWN_MODAL)
window_push_modal_zoom(w);
else
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
lcnew = layout_floating_pane(w, wp, &fg);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.243 2026/08/25 08:37:08 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>
@@ -305,16 +305,6 @@ const struct options_table_entry options_table[] = {
"When this is reached, the oldest buffer is deleted."
},
{ .name = "clear-on-attach",
.type = OPTIONS_TABLE_FLAG,
.scope = OPTIONS_TABLE_SERVER,
.default_num = 1,
.text = "Whether to use the alternate screen and clear it when "
"a client is attached. When disabled, tmux does not "
"enter the alternate screen on attach so terminal "
"content before tmux remains in scrollback."
},
{ .name = "command-alias",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SERVER,
@@ -551,6 +541,7 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_SERVER,
.flags = OPTIONS_TABLE_IS_ARRAY,
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
"xterm-kitty*:kitty,"
"screen*:title,"
"rxvt*:ignorefkeys",
.separator = ",",

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

@@ -0,0 +1,437 @@
#!/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.
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 refresh-client -R || exit 1
sleep 1
grep -a '"1;1;26;26' $TMP >/dev/null || exit 1
# A selection redraw uses the single-cell path. It must leave ASCII image
# cells visible (graphical clients skip these cells to avoid erasing pixels).
$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 -N 4 -X cursor-right || exit 1
$TMUX2 send-keys -X begin-selection || exit 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
sleep 1
$TMUX pipe-pane || exit 1
grep -a '"1;1;' $TMP >/dev/null && exit 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# Selection must also leave transparent cells inside the image canvas alone.
# This 26-pixel canvas paints only its first pixel column, while the text below
# it keeps the copy-mode cursor movable across both occupied terminal cells.
$TMUX2 send-keys -X cancel || exit 1
$TMUX2 new-window -d "
printf 'ab\r'
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0~-#0~-#0~-#0~-#0B\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:1 || exit 1
sleep 1
$TMUX2 copy-mode || exit 1
$TMUX2 send-keys -X top-line || exit 1
$TMUX2 send-keys -X start-of-line || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
$TMUX2 send-keys -X begin-selection || exit 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
sleep 1
$TMUX pipe-pane || exit 1
grep -a 'b' $TMP >/dev/null && exit 1
# Scrolling moves the terminal's image rows with its text rows. It must not
# force tmux to erase and retransmit the complete SIXEL image.
$TMUX2 send-keys -X cancel || exit 1
SCROLL_WINDOW=$($TMUX2 new-window -P -F '#{window_id}' \
"cat '$FIXTURE'; exec sh") || exit 1
sleep 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -t"$SCROLL_WINDOW" -l \
"i=0; while [ \$i -lt 12 ]; do printf 'scroll-marker-%d\\n' \$i; i=\$((i + 1)); done" || exit 1
$TMUX2 send-keys -t"$SCROLL_WINDOW" Enter || exit 1
sleep 1
grep -a 'scroll-marker-11' $TMP >/dev/null || exit 1
grep -a '"1;1;' $TMP >/dev/null && 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
# A partial redraw of a tiled pane must clear only its visible scene spans.
# Clearing its entire rectangle would erase a floating pane above it, while
# the tiled pane redraw correctly skips the obscured area and cannot restore it.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 40 -y 15 \
"printf 'TILED PANE'; sleep 10" || exit 1
TILED_PANE=$($TMUX2 display-message -p '#{pane_id}') || exit 1
FLOATING_PANE=$($TMUX2 new-pane -dP -F '#{pane_id}' -x 20 -y 6 -X 10 -Y 4 \
"printf 'FLOATING PANE'; sleep 10") || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX2 select-pane -t"$FLOATING_PANE" || exit 1
$TMUX new-session -d -x 40 -y 15 || 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 copy-mode -t"$TILED_PANE" || exit 1
sleep 1
$TMUX capture-pane -p >$TMP || exit 1
grep 'TILED PANE' $TMP >/dev/null || exit 1
grep 'FLOATING PANE' $TMP >/dev/null || exit 1
# An obscured pane is normally redrawn a line at a time when it scrolls, but
# that path cannot restore graphical image layers. It must defer to the scene
# redraw so the visible part of the image is emitted around the floating pane.
IMAGE_PANE=$($TMUX2 new-window -dP -F '#{pane_id}' \
"cat '$FIXTURE'; exec sh") || exit 1
$TMUX2 set -g pane-border-status top || exit 1
$TMUX2 set -pt"$IMAGE_PANE" pane-border-format 'IMAGE STATUS' || exit 1
FLOATING_PANE=$($TMUX2 new-pane -dP -F '#{pane_id}' -x 20 -y 6 -X 10 -Y 4 \
"printf 'FLOATING PANE'; sleep 10") || exit 1
$TMUX2 set -pt"$FLOATING_PANE" pane-border-format 'FLOAT STATUS' || exit 1
$TMUX2 select-pane -t"$FLOATING_PANE" || exit 1
sleep 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -t"$IMAGE_PANE" -l \
"i=0; while [ \$i -lt 5 ]; do printf 'image-scroll-%d\\n' \$i; i=\$((i + 1)); done" || exit 1
$TMUX2 send-keys -t"$IMAGE_PANE" Enter || exit 1
sleep 1
$TMUX pipe-pane || exit 1
grep -a 'image-scroll-4' $TMP >/dev/null || exit 1
grep -a '"1;1;' $TMP >/dev/null || exit 1
$TMUX capture-pane -p >$TMP || exit 1
grep 'IMAGE STATUS' $TMP >/dev/null || exit 1
grep 'FLOAT STATUS' $TMP >/dev/null || exit 1
exit 0

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -14,8 +14,8 @@ DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
INNER="$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsync-output-outer-$$ -f/dev/null"
CLIENT_BYTES=$DIR/client-bytes
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
@@ -112,7 +112,7 @@ $INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
$OUTER new-session -d -s outer -x 80 -y 24 \
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" ||
"$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1

View File

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

View File

@@ -17,8 +17,7 @@
# unlink the last link without -k;
# - swap-window within and between sessions, -d keeping the active window,
# and the grouped-sessions error;
# - rotate-window -U/-D rotating pane positions, with and without preserving
# zoom using -Z;
# - rotate-window -U/-D rotating pane positions;
# - kill-window switching to the last (previously current) window, kill-window
# -a killing all other windows and the "-f only valid with -a" guard.
#
@@ -307,24 +306,6 @@ check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0"
# Rotation without -Z unzooms. With -Z it preserves zoom and transfers it to
# the pane which arrives at the active position.
layout=$($TMUX display-message -p -t R:0 '#{window_layout}')
check_ok resize-pane -Z -t "$p0"
check_ok rotate-window -U -t R:0
check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'0:0:1'
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{window_layout}' "$layout"
check_ok resize-pane -Z -t "$p0"
check_ok rotate-window -U -Z -t R:0
check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{window_layout}' "$layout"
# ---------------------------------------------------------------------------
# kill-window.

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */
/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -42,8 +42,8 @@ resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy)
void
resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
{
struct window_pane *zwp;
u_int old_sx = w->sx, old_sy = w->sy;
u_int old_sx = w->sx, old_sy = w->sy;
int zoomed;
/* Check size limits. */
if (sx < WINDOW_MINIMUM)
@@ -56,8 +56,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
sy = WINDOW_MAXIMUM;
/* If the window is zoomed, unzoom. */
zwp = window_zoomed_pane(w);
if (zwp != NULL)
zoomed = w->flags & WINDOW_ZOOMED;
if (zoomed)
window_unzoom(w, 1);
/* Resize the layout first. */
@@ -73,8 +73,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
sx, sy, w->layout_root->g.sx, w->layout_root->g.sy);
/* Restore the window zoom state. */
if (zwp != NULL && window_has_pane(w, zwp))
window_zoom(zwp);
if (zoomed)
window_zoom(w->active);
tty_update_window_offset(w);
server_redraw_window(w);
@@ -386,10 +386,12 @@ recalculate_size(struct window *w, int now)
* got a resize scheduled, then use the new size; otherwise the old.
*/
if (w->flags & WINDOW_RESIZE) {
if (!now && changed && w->new_sx == sx && w->new_sy == sy)
if (!now && changed && w->new_sx == sx && w->new_sy == sy &&
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
changed = 0;
} 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;
}

View File

@@ -99,6 +99,13 @@ enum redraw_span_type {
#define REDRAW_ALL 0x7fffffff
#define REDRAW_IS_ALL(flags) ((flags) == REDRAW_ALL)
enum redraw_image_phase {
REDRAW_IMAGES_BEFORE,
REDRAW_TEXT,
REDRAW_IMAGES_AFTER,
REDRAW_CHROME
};
/* UTF-8 isolate characters. */
#define REDRAW_START_ISOLATE "\342\201\246"
#define REDRAW_END_ISOLATE "\342\201\251"
@@ -1106,7 +1113,8 @@ redraw_get_scene(struct client *c)
/* Draw a pane span. */
static void
redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int x, u_int y, u_int n)
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 client *c = scene->c;
@@ -1124,7 +1132,14 @@ redraw_draw_pane_span(struct redraw_draw_ctx *dctx,
px = span->data.p.px + (x - span->x);
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. */
@@ -1379,7 +1394,7 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx,
/* Draw a span. */
static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y)
u_int y, enum redraw_image_phase phase)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_span_data *data = &span->data;
@@ -1400,10 +1415,15 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
continue;
x = rr->px;
n = rr->nx;
if (type == REDRAW_SPAN_PANE) {
if (phase == REDRAW_CHROME)
continue;
} else if (phase != REDRAW_CHROME)
continue;
switch (span->data.type) {
case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, x, y, n);
redraw_draw_pane_span(dctx, span, x, y, n, phase);
break;
case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY:
@@ -1432,6 +1452,11 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
struct redraw_line *line;
struct redraw_spans *spans;
struct redraw_span *span;
#ifdef ENABLE_IMAGES
struct visible_ranges *r;
struct visible_range *rr;
u_int i;
#endif
u_int cy;
int y, top, bottom;
@@ -1444,24 +1469,53 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
if (bottom > (int)scene->sy)
bottom = scene->sy;
for (y = top; y < bottom; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
#ifdef ENABLE_IMAGES
if (flags & REDRAW_PANE) {
for (y = top; y < bottom; y++) {
line = &scene->lines[y];
cy = y;
if (flags & REDRAW_PANE) {
if (dctx->flags & REDRAW_STATUS_TOP)
cy += dctx->status_lines;
spans = &line->spans[REDRAW_SPAN_PANE];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.p.wp == wp)
redraw_draw_span(dctx, span, cy);
if (span->data.p.wp != wp)
continue;
r = tty_check_overlay_range(&scene->c->tty,
span->x, cy, span->width);
for (i = 0; i < r->used; i++) {
rr = &r->ranges[i];
if (rr->nx != 0) {
image_redraw_start(&scene->c->tty,
rr->px, cy, rr->nx, 1);
}
}
}
}
if (flags & REDRAW_PANE_SCROLLBAR) {
spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.sb.wp == wp)
redraw_draw_span(dctx, span, cy);
}
#endif
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_CHROME; phase++) {
for (y = top; y < bottom; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
if (flags & REDRAW_PANE) {
spans = &line->spans[REDRAW_SPAN_PANE];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.p.wp == wp)
redraw_draw_span(dctx, span, cy, phase);
}
}
if (phase == REDRAW_CHROME &&
(flags & REDRAW_PANE_SCROLLBAR)) {
spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.sb.wp == wp)
redraw_draw_span(dctx, span, cy, phase);
}
}
}
}
@@ -1477,13 +1531,15 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
struct redraw_span *span;
u_int y, cy, type;
for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_CHROME; phase++) {
for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + y;
else
cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (!REDRAW_IS_ALL(flags)) {
switch (type) {
case REDRAW_SPAN_PANE:
@@ -1520,7 +1576,8 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
}
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy);
redraw_draw_span(dctx, span, cy, phase);
}
}
}
}
@@ -1541,7 +1598,7 @@ redraw_draw_menu_lines(struct redraw_draw_ctx *dctx)
else
cy = y;
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry)
redraw_draw_span(dctx, span, cy);
redraw_draw_span(dctx, span, cy, REDRAW_TEXT);
}
}
@@ -1731,9 +1788,10 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
}
if (flags & REDRAW_PANE_STATUS) {
redraw = 0;
redraw = !!(flags & REDRAW_PANE_BORDER);
TAILQ_FOREACH(loop, &scene->w->panes, entry) {
if (REDRAW_IS_ALL(flags))
if (REDRAW_IS_ALL(flags) ||
(flags & REDRAW_PANE_BORDER))
loop->flags |= PANE_NEWSTATUS;
else
loop->flags &= ~PANE_NEWSTATUS;
@@ -1771,6 +1829,14 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
}
}
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);
if (wp != NULL)
@@ -1817,15 +1883,6 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
tty_reset(tty);
tty_sync_end(tty);
#ifdef ENABLE_SIXEL
if (wp != NULL)
tty_draw_images(c, wp);
else {
TAILQ_FOREACH(loop, &scene->w->panes, entry)
tty_draw_images(c, loop);
}
#endif
log_debug("%s: finished @%u redraw", c->name, scene->w->id);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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_flush(struct screen_write_ctx *, int,
const char *);
#ifdef ENABLE_IMAGES
static int 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 *,
struct grid_cell *, u_int);
static int screen_write_combine(struct screen_write_ctx *,
@@ -135,6 +139,8 @@ static int
screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct window_pane *wp = ttyctx->arg;
u_int sx, sb_w = 0;
int xoff;
if (ttyctx->flags & TTY_CTX_INVISIBLE_PANES) {
if (session_has(c->session, wp->window))
@@ -168,6 +174,21 @@ screen_write_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
ttyctx->xoff = ttyctx->rxoff = wp->xoff;
ttyctx->yoff = ttyctx->ryoff = wp->yoff;
ttyctx->flags &= ~TTY_CTX_PANE_FULL_WIDTH;
if (~ttyctx->flags & TTY_CTX_WINDOW_BIGGER &&
!window_has_floating_panes(wp->window)) {
xoff = wp->xoff;
sx = wp->sx;
if (window_pane_scrollbar_reserve(wp)) {
sb_w = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
sx += sb_w;
if (wp->window->sb_pos == PANE_SCROLLBARS_LEFT)
xoff -= sb_w;
}
if (xoff == 0 && sx >= c->tty.sx)
ttyctx->flags |= TTY_CTX_PANE_FULL_WIDTH;
}
if (status_at_line(c) == 0)
ttyctx->yoff += status_line_size(c);
@@ -305,14 +326,12 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
if (~ctx->flags & SCREEN_WRITE_SYNC) {
/*
* For the active pane showing its base screen or for an
* overlay (no pane), only use synchronized updates if
* requested (commands that move the cursor); for other panes
* or a pane in a mode, always use it, since the cursor will
* have to move.
* For the active pane or for an overlay (no pane), we want to
* only use synchronized updates if requested (commands that
* move the cursor); for other panes, always use it, since the
* cursor will have to move.
*/
if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active ||
ctx->wp->screen != &ctx->wp->base))
if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active)
ttyctx->flags |= TTY_CTX_SYNC;
else {
if (ctx->wp == NULL)
@@ -340,18 +359,10 @@ screen_write_make_list(struct screen *s)
void
screen_write_free_list(struct screen *s)
{
struct screen_write_cline *cl;
struct screen_write_citem *ci, *ci1;
u_int y;
u_int y;
for (y = 0; y < screen_size_y(s); y++) {
cl = &s->write_list[y];
TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) {
TAILQ_REMOVE(&cl->items, ci, entry);
screen_write_free_citem(ci);
}
free(cl->data);
}
for (y = 0; y < screen_size_y(s); y++)
free(s->write_list[y].data);
free(s->write_list);
}
@@ -371,6 +382,23 @@ screen_write_init(struct screen_write_ctx *ctx, struct screen *s)
ctx->bg = 8;
}
#ifdef ENABLE_IMAGES
/* Damage temporal images when application input writes text cells. */
static int
screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y,
u_int sx, u_int sy)
{
int found;
if (~ctx->flags & SCREEN_WRITE_INPUT)
return (0);
found = image_grid_check_area(ctx->s->grid, x,
ctx->s->grid->hsize + y, sx, sy);
image_grid_damage(ctx->s->grid, x, ctx->s->grid->hsize + y, sx, sy);
return (found);
}
#endif
/* Initialize writing with a pane. */
void
screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp,
@@ -1069,19 +1097,6 @@ screen_write_stop_sync(struct window_pane *wp)
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
}
/* Flush pending output before clearing sync mode. */
void
screen_write_end_sync(struct screen_write_ctx *ctx)
{
struct window_pane *wp = ctx->wp;
if (wp == NULL)
return;
if (wp->base.mode & MODE_SYNC)
screen_write_collect_flush(ctx, 0, __func__);
screen_write_stop_sync(wp);
}
/* Cursor up by ny. */
void
screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny)
@@ -1308,15 +1323,14 @@ screen_write_clear_dirty(struct window_pane *wp)
}
}
/* Redraw all visible cells in a pane. */
/* Redraw an obscured pane through the scene to preserve floating panes. */
static void
screen_write_redraw_pane(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
screen_write_redraw_pane(struct screen_write_ctx *ctx)
{
struct screen *s = ctx->s;
u_int yy;
for (yy = 0; yy < screen_size_y(s); yy++)
screen_write_redraw_line(ctx, ttyctx, yy);
if (ctx->wp != NULL) {
ctx->wp->flags |= PANE_REDRAW;
server_redraw_window_borders(ctx->wp->window);
}
}
/* VT100 alignment test. */
@@ -1331,9 +1345,11 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
memcpy(&gc, &grid_default_cell, sizeof gc);
utf8_set(&gc.data, 'E');
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
if (ctx->flags & SCREEN_WRITE_INPUT)
image_grid_damage(s->grid, 0, s->grid->hsize,
screen_size_x(s), screen_size_y(s));
#endif
for (yy = 0; yy < screen_size_y(s); yy++) {
@@ -1357,7 +1373,7 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
}
/* Insert nx characters. */
@@ -1378,9 +1394,8 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1)
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1419,9 +1434,8 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1)
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1460,9 +1474,8 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1)
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1495,11 +1508,6 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
if (ny > sy - s->cy)
ny = sy - s->cy;
@@ -1521,7 +1529,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
return;
}
@@ -1548,7 +1556,7 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
}
/* Delete ny lines. */
@@ -1563,11 +1571,6 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
if (ny > sy - s->cy)
ny = sy - s->cy;
@@ -1590,7 +1593,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
return;
}
@@ -1618,7 +1621,7 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
}
/* Clear line at cursor. */
@@ -1636,9 +1639,8 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg)
if (gl->cellsize == 0 && COLOUR_DEFAULT(bg))
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
flags = gl->flags & GRID_LINE_OSC133_FLAGS;
@@ -1675,9 +1677,8 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg)
if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg)))
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg);
@@ -1702,9 +1703,8 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
if (s->cx > sx - 1)
@@ -1749,6 +1749,9 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
struct screen *s = ctx->s;
struct tty_ctx ttyctx;
u_int ry;
#ifdef ENABLE_IMAGES
int image_scroll;
#endif
if (s->cy != s->rupper) {
if (s->cy > 0)
@@ -1756,16 +1759,20 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_scroll = image_grid_check_area(s->grid, 0,
s->grid->hsize + s->rupper, screen_size_x(s),
s->rlower - s->rupper + 1);
#endif
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
screen_write_collect_flush(ctx, 0, __func__);
screen_write_initctx(ctx, &ttyctx, 1, 1);
ttyctx.bg = bg;
#ifdef ENABLE_IMAGES
if (image_scroll)
ttyctx.flags |= TTY_CTX_IMAGE_SCROLL;
#endif
ry = s->rlower + 1 - s->rupper;
if (!screen_write_should_draw_lines(ctx, s->rupper, ry))
@@ -1775,7 +1782,7 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
}
/* Set scroll region. */
@@ -1808,9 +1815,6 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct grid_line *gl;
#ifdef ENABLE_SIXEL
int redraw = 0;
#endif
u_int rupper = s->rupper, rlower = s->rlower;
gl = grid_get_line(gd, gd->hsize + s->cy);
@@ -1831,13 +1835,10 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
return;
}
#ifdef ENABLE_SIXEL
if (rlower == screen_size_y(s) - 1)
redraw = image_scroll_up(s, 1);
else
redraw = image_check_line(s, rupper, rlower - rupper);
if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
if (image_grid_check_area(gd, 0, gd->hsize + rupper,
screen_size_x(s), rlower - rupper + 1))
ctx->scrolled_images = 1;
#endif
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
@@ -1863,9 +1864,10 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg)
ctx->bg = bg;
}
#ifdef ENABLE_SIXEL
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
if (image_grid_check_area(gd, 0, gd->hsize + s->rupper,
screen_size_x(s), s->rlower - s->rupper + 1))
ctx->scrolled_images = 1;
#endif
for (i = 0; i < lines; i++) {
@@ -1892,9 +1894,10 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
else if (lines > s->rlower - s->rupper + 1)
lines = s->rlower - s->rupper + 1;
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
if (image_grid_check_area(gd, 0, gd->hsize + s->rupper,
screen_size_x(s), s->rlower - s->rupper + 1))
ttyctx.flags |= TTY_CTX_IMAGE_SCROLL;
#endif
for (i = 0; i < lines; i++)
@@ -1911,7 +1914,7 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
return;
}
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
}
/* Carriage return (cursor to start of line). */
@@ -1933,9 +1936,8 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2014,9 +2016,8 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_SIXEL
if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1);
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2087,9 +2088,8 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2297,7 +2297,7 @@ screen_write_collect_flush_scrolled(struct screen_write_ctx *ctx)
screen_write_initctx(ctx, &ttyctx, 1, 1);
if (ttyctx.flags & TTY_CTX_PANE_OBSCURED && wp != NULL) {
screen_write_redraw_pane(ctx, &ttyctx);
screen_write_redraw_pane(ctx);
return 0;
}
if (wp != NULL && window_pane_scrollbar_overlay_visible(wp)) {
@@ -2314,6 +2314,10 @@ screen_write_collect_flush_scrolled(struct screen_write_ctx *ctx)
ttyctx.orlower -= (wp->yoff + wp->sy - wp->window->sy);
ttyctx.n = ctx->scrolled;
ttyctx.bg = ctx->bg;
#ifdef ENABLE_IMAGES
if (ctx->scrolled_images)
ttyctx.flags |= TTY_CTX_IMAGE_SCROLL;
#endif
tty_write(tty_cmd_scrollup, &ttyctx);
if (wp != NULL)
@@ -2443,6 +2447,9 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only,
if (!screen_write_collect_flush_scrolled(ctx))
goto discard;
ctx->scrolled = 0;
#ifdef ENABLE_IMAGES
ctx->scrolled_images = 0;
#endif
}
ctx->bg = 8;
@@ -2466,6 +2473,9 @@ discard:
}
}
ctx->scrolled = 0;
#ifdef ENABLE_IMAGES
ctx->scrolled_images = 0;
#endif
ctx->bg = 8;
}
@@ -2592,9 +2602,8 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
}
}
#ifdef ENABLE_SIXEL
if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1);
#endif
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
@@ -2684,6 +2693,9 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
u_int sx = screen_size_x(s), sy = screen_size_y(s);
u_int width = ud->width, xx, not_wrap, i, n, vis;
int selected, skip = 1, redraw = 0;
#ifdef ENABLE_IMAGES
int selection_image = 0;
#endif
int yoff = 0, xoff = 0;
struct visible_ranges *r;
struct visible_range *ri;
@@ -2724,6 +2736,12 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
return;
screen_write_initctx(ctx, &ttyctx, 0, 0);
#ifdef ENABLE_IMAGES
/* Input semantics decide which image layers this text damages. */
if (screen_write_image_damage(ctx, s->cx, s->cy, width, 1))
skip = 0;
#endif
/* Handle overwriting of UTF-8 characters. */
gl = grid_get_line(s->grid, s->grid->hsize + s->cy);
if (gl->flags & GRID_LINE_EXTENDED) {
@@ -2771,6 +2789,14 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
/* Update the selected flag and set the cell. */
selected = screen_check_selection(s, s->cx, s->cy);
#ifdef ENABLE_IMAGES
/* Selection highlighting must not overwrite image-covered cells. */
if (selected && image_grid_check_area(gd, s->cx,
gd->hsize + s->cy, width, 1)) {
selected = 0;
selection_image = 1;
}
#endif
if (selected && (~gc->flags & GRID_FLAG_SELECTED)) {
memcpy(&tmp_gc, gc, sizeof tmp_gc);
tmp_gc.flags |= GRID_FLAG_SELECTED;
@@ -2781,6 +2807,11 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
grid_view_set_cell(gd, s->cx, s->cy, &tmp_gc);
} else if (!skip)
grid_view_set_cell(gd, s->cx, s->cy, gc);
#ifdef ENABLE_IMAGES
if (selection_image)
skip = 1;
else
#endif
if (selected)
skip = 0;
@@ -2862,11 +2893,10 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
struct grid *gd = s->grid;
const struct utf8_data *ud = &gc->data;
struct options *oo = global_options;
u_int i, n, cx = s->cx, cy = s->cy, vis;
u_int i, n, cx = s->cx, cy = s->cy, vis, yoff = 0;
struct grid_cell last;
struct tty_ctx ttyctx;
int force_wide = 0, zero_width = 0;
int xoff = 0, yoff = 0;
struct visible_ranges *r;
/* Ignore U+3164 HANGUL_FILLER entirely. */
@@ -2951,6 +2981,9 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
force_wide = 0;
/* 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);
if (force_wide)
grid_view_set_padding(gd, cx - 1, cy, last.bg);
@@ -2960,11 +2993,9 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
* obscured in the middle, only on left or right, but there could be an
* empty range in the visible ranges so we add them all up.
*/
if (wp != NULL) {
xoff = wp->xoff;
if (wp != NULL)
yoff = wp->yoff;
}
r = window_visible_ranges(wp, xoff + cx - n, cy + yoff, n, NULL);
r = window_visible_ranges(wp, cx - n, cy + yoff, n, NULL);
for (i = 0, vis = 0; i < r->used; i++)
vis += r->ranges[i].nx;
if (vis < n) {
@@ -3086,63 +3117,21 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len,
tty_write(tty_cmd_rawstring, &ttyctx);
}
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
/* Write a SIXEL image. */
void
screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si,
u_int bg)
{
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct tty_ctx ttyctx;
u_int x, y, sx, sy, cx = s->cx, cy = s->cy, i, lines;
struct sixel_image *new;
struct image *im;
if (screen_size_y(s) == 1)
return;
sixel_size_in_cells(si, &x, &y);
if (x > screen_size_x(s) || y > screen_size_y(s) - 1) {
if (x > screen_size_x(s) - cx)
sx = screen_size_x(s) - cx;
else
sx = x;
if (y > screen_size_y(s) - 1)
sy = screen_size_y(s) - 1;
else
sy = y;
new = sixel_scale(si, 0, 0, 0, y - sy, sx, sy, 1);
im = sixel_to_image(si);
if (im == NULL) {
sixel_free(si);
if (new == NULL)
return;
sixel_size_in_cells(new, &x, &y);
si = new;
return;
}
sy = screen_size_y(s) - cy;
if (sy <= y) {
lines = y - sy + 1;
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
for (i = 0; i < lines; i++) {
grid_view_scroll_region_up(gd, 0, screen_size_y(s) - 1,
bg);
screen_write_collect_scroll(ctx, bg);
}
ctx->scrolled += lines;
if (lines > cy)
screen_write_cursormove(ctx, -1, 0, 0);
else
screen_write_cursormove(ctx, -1, cy - lines, 0);
}
screen_write_collect_flush(ctx, 0, __func__);
screen_write_initctx(ctx, &ttyctx, 0, 0);
ttyctx.image = image_store(s, si);
tty_write(tty_cmd_sixelimage, &ttyctx);
screen_write_cursormove(ctx, 0, cy + y, 0);
image_write_sixel(ctx, im, bg);
image_free(image_get_id(im));
}
#endif

View File

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

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.508 2026/08/25 06:14:16 nicm Exp $ */
/* $OpenBSD: server-client.c,v 1.503 2026/08/17 07:56:56 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -45,7 +45,6 @@ static void server_client_set_path(struct client *);
static void server_client_set_progress_bar(struct client *);
static void server_client_reset_state(struct client *);
static void server_client_update_latest(struct client *);
static int server_client_handle_dead_key(struct window_pane *, key_code);
static void server_client_dispatch(struct imsg *, void *);
static int server_client_dispatch_command(struct client *, struct imsg *);
static int server_client_dispatch_identify(struct client *, struct imsg *);
@@ -524,10 +523,7 @@ server_client_lost(struct client *c)
input_cancel_requests(c);
free(c->title);
free(c->path);
free((void *)c->cwd);
free(c->exit_session);
free(c->exit_message);
evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer);
@@ -804,7 +800,8 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py,
} else {
/* Try the pane borders. */
TAILQ_FOREACH(fwp, &w->panes, entry) {
if (!window_pane_is_visible(fwp))
if ((w->flags & WINDOW_ZOOMED) &&
(~fwp->flags & PANE_ZOOMED))
continue;
if (window_pane_is_floating(fwp) &&
window_pane_get_pane_lines(fwp) == PANE_LINES_NONE)
@@ -1391,21 +1388,6 @@ server_client_repeat_time(struct client *c, struct key_binding *bd)
return (repeat);
}
/* Handle a key press on a dead pane waiting for a key. */
static int
server_client_handle_dead_key(struct window_pane *wp, key_code key)
{
if (wp == NULL ||
(~wp->flags & PANE_EXITED) ||
KEYC_IS_MOUSE(key) ||
KEYC_IS_PASTE(key) ||
options_get_number(wp->options, "remain-on-exit") != 3)
return (0);
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
return (1);
}
/*
* Handle data key input from client. This owns and can modify the key event it
* is given and is responsible for freeing it.
@@ -1491,14 +1473,6 @@ server_client_key_callback(struct cmdq_item *item, void *data)
server_client_is_assume_paste(c))
goto paste_key;
/* Forward keys directly if this pane is capturing all keys. */
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
(~wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
TAILQ_EMPTY(&wp->modes))
goto forward_key;
/*
* Work out the current key table. If the pane is in a mode, use
* the mode table instead of the default key table.
@@ -1663,8 +1637,15 @@ try_again:
}
forward_key:
if (server_client_handle_dead_key(wp, key))
if (wp != NULL &&
(wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
!KEYC_IS_PASTE(key) &&
options_get_number(wp->options, "remain-on-exit") == 3) {
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
goto out;
}
if (c->flags & CLIENT_READONLY)
goto out;
if (wp != NULL)
@@ -1752,9 +1733,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
/*
* Key presses in overlay mode, for panes capturing all keys and in the
* command prompt are a special case. The queue might be blocked so they
* need to be processed immediately rather than queued.
* Key presses in overlay mode and the command prompt are a special
* case. The queue might be blocked so they need to be processed
* immediately rather than queued.
*/
if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) {
@@ -1774,21 +1755,6 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
server_client_clear_overlay(c);
wp = s->curw->window->active;
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
TAILQ_EMPTY(&wp->modes) &&
!KEYC_IS_MOUSE(event->key)) {
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (~wp->flags & PANE_EXITED) {
window_pane_key(wp, c, s, s->curw, event->key,
&event->m);
return (0);
}
}
if (server_client_handle_menu_key(c, event))
return (0);
if (c->prompt != NULL) {
@@ -2409,6 +2375,8 @@ server_client_check_exit(struct client *c, int force)
proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1);
break;
}
free(c->exit_session);
free(c->exit_message);
}
/* Redraw timer callback. */
@@ -2473,13 +2441,14 @@ server_client_check_redraw(struct client *c)
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_ALLREDRAWFLAGS) {
log_debug("%s: redraw%s%s%s%s%s", c->name,
if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS)) {
log_debug("%s: redraw%s%s%s%s%s%s", c->name,
(c->flags & CLIENT_REDRAWWINDOW) ? " window" : "",
(c->flags & CLIENT_REDRAWSTATUS) ? " status" : "",
(c->flags & CLIENT_REDRAWBORDERS) ? " borders" : "",
(c->flags & CLIENT_REDRAWOVERLAY) ? " overlay" : "",
(c->flags & CLIENT_REDRAWMENU) ? " menu" : "");
(c->flags & CLIENT_REDRAWMENU) ? " menu" : "",
(c->flags & CLIENT_REDRAWSCROLLBARS) ? " scrollbars" : "");
}
/* Work out if a redraw is actually needed. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.151 2026/08/20 09:19:24 nicm Exp $ */
/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -226,11 +226,10 @@ server_kill_pane(struct window_pane *wp)
server_kill_window(w, 1);
recalculate_sizes();
} else {
window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM);
server_unzoom_window(w);
server_client_remove_pane(wp);
layout_close_pane(wp);
window_remove_pane(w, wp);
window_pop_zoom(w);
server_redraw_window(w);
}
}
@@ -422,17 +421,15 @@ server_destroy_pane(struct window_pane *wp, int notify)
if (notify)
server_fire_pane_exit("pane-exited", wp);
window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM);
server_unzoom_window(w);
server_client_remove_pane(wp);
layout_close_pane(wp);
window_remove_pane(w, wp);
if (TAILQ_EMPTY(&w->panes))
server_kill_window(w, 1);
else {
window_pop_zoom(w);
else
server_redraw_window(w);
}
}
static void
@@ -554,7 +551,6 @@ server_check_unattached(void)
continue;
break;
}
server_destroy_session(s);
session_destroy(s, 1, __func__);
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */
/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -129,7 +129,6 @@ server_create_socket(uint64_t flags, char **cause)
mask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) {
saved_errno = errno;
umask(mask);
close(fd);
errno = saved_errno;
goto fail;

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

29
spawn.c
View File

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

View File

@@ -30,7 +30,8 @@
/* Default style. */
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,

26
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1159 2026/08/25 08:37:08 nicm Exp $
.\" $OpenBSD: tmux.1,v 1.1156 2026/08/17 14:47:41 nicm Exp $
.\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\"
.Dd $Mdocdate: August 25 2026 $
.Dd $Mdocdate: August 17 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -3644,7 +3644,7 @@ but a different format may be specified with
.Fl F .
.Tg newp
.It Xo Ic new\-pane
.Op Fl AbCdefhIkKLMOPvWZ
.Op Fl bCdefhIkLMOPvWZ
.Op Fl B Ar border\-lines
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
@@ -3693,17 +3693,14 @@ sets the pane border lines for floating panes; see
.Fl M
may be used when bound to a mouse drag key; the new floating pane is resized to
follow the mouse drag.
.Fl A
creates a floating pane which remains visible above a zoomed pane.
It has no effect with
.Fl L .
.Fl O
creates a modal pane.
A modal pane is always the active pane and prevents interaction with any other
panes while it is active.
A window can only have one modal pane and it must be a floating pane.
With
.Fl K ,
all keys, including the prefix key, are passed directly to the modal pane.
With
.Fl C ,
the modal pane is closed when the mouse is clicked outside it.
.Pp
@@ -4754,15 +4751,6 @@ for backspace.
Set the number of buffers; as new buffers are added to the top of the stack,
old ones are removed from the bottom if necessary to maintain this maximum
length.
.It Xo Ic clear\-on\-attach
.Op Ic on | off
.Xc
When enabled (the default), the client terminal switches to the alternate
screen and clears it when a client is attached.
When disabled,
.Nm
does not enter the alternate screen; instead the existing terminal content is
scrolled into the scrollback buffer so it remains accessible.
.It Xo Ic command\-alias[]
.Ar name=value
.Xc
@@ -5085,6 +5073,10 @@ Ignore function keys from
and use the
.Nm
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
Supports DECSLRM margins.
.It mouse

186
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1431 2026/08/25 08:37:08 nicm Exp $ */
/* $OpenBSD: tmux.h,v 1.1424 2026/08/17 14:47:41 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -75,7 +75,12 @@ struct screen_write_cline;
struct screen_write_ctx;
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;
#endif
@@ -483,7 +488,6 @@ enum tty_code_code {
TTYC_ICH1,
TTYC_IL,
TTYC_IL1,
TTYC_IND,
TTYC_INDN,
TTYC_INVIS,
TTYC_KCBT,
@@ -859,7 +863,7 @@ struct colour_palette {
struct grid_cell {
struct utf8_data data;
u_short attr;
u_char flags;
u_short flags;
int fg;
int bg;
int us;
@@ -870,7 +874,7 @@ struct grid_cell {
struct grid_extd_entry {
utf8_char data;
u_short attr;
u_char flags;
u_short flags;
int fg;
int bg;
int us;
@@ -888,7 +892,7 @@ struct grid_cell_entry {
u_char data;
} data;
};
u_char flags;
u_short flags;
} __packed;
/* OSC 133 data for a grid line. */
@@ -912,6 +916,9 @@ struct grid_line {
u_int time;
struct osc133_data osc133_data;
u_short flags;
#ifdef ENABLE_IMAGES
struct image_line *images;
#endif
};
/* Entire grid of cells. */
@@ -931,6 +938,9 @@ struct grid {
u_int scroll_generation;
struct grid_line *linedata;
#ifdef ENABLE_IMAGES
struct image_store *images;
#endif
};
/* Virtual cursor in a grid. */
@@ -1022,24 +1032,8 @@ struct style {
u_int link;
};
#ifdef ENABLE_SIXEL
/* Image. */
struct image {
struct screen *s;
struct sixel_image *data;
char *fallback;
u_int px;
u_int py;
u_int sx;
u_int sy;
struct images *list;
TAILQ_ENTRY (image) entry;
TAILQ_ENTRY (image) all_entry;
};
TAILQ_HEAD(images, image);
#ifdef ENABLE_IMAGES
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
#endif
/* Cursor style. */
@@ -1098,11 +1092,6 @@ struct screen {
bitstr_t *tabs;
struct screen_sel *sel;
#ifdef ENABLE_SIXEL
struct images images;
struct images saved_images;
#endif
struct screen_write_cline *write_list;
struct hyperlinks *hyperlinks;
@@ -1120,12 +1109,16 @@ struct screen_write_ctx {
#define SCREEN_WRITE_SYNC 0x1
#define SCREEN_WRITE_OBSCURED 0x2
#define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4
#define SCREEN_WRITE_INPUT 0x8
screen_write_init_ctx_cb init_ctx_cb;
void *arg;
struct screen_write_citem *item;
u_int scrolled;
#ifdef ENABLE_IMAGES
int scrolled_images;
#endif
u_int bg;
};
@@ -1336,8 +1329,6 @@ struct window_pane {
#define PANE_CMDRUNNING 0x20000
#define PANE_ACTIVITY 0x40000
#define PANE_CLOSEONCLICK 0x80000
#define PANE_CAPTUREALLKEYS 0x100000
#define PANE_FLOATOVERZOOM 0x200000
bitstr_t *sync_dirty;
u_int sync_dirty_size;
@@ -1444,7 +1435,6 @@ struct window {
struct window_pane *active;
struct window_pane *modal;
struct window_pane *modal_last;
struct window_pane *was_zoomed;
struct window_panes last_panes;
struct window_panes z_index;
struct window_panes panes;
@@ -1487,6 +1477,7 @@ struct window {
#define WINDOW_ZOOMED 0x8
#define WINDOW_WASZOOMED 0x10
#define WINDOW_RESIZE 0x20
#define WINDOW_WASMODALZOOMED 0x40
#define WINDOW_ALERTFLAGS (WINDOW_BELL|WINDOW_ACTIVITY|WINDOW_SILENCE)
int alerts_queued;
@@ -1734,7 +1725,8 @@ struct tty_term {
#define TERM_RGBCOLOURS 0x10
#define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80
#define TERM_KITTY 0x80
#define TERM_INVALIDMS 0x100
int flags;
LIST_ENTRY(tty_term) entry;
@@ -1814,9 +1806,8 @@ struct tty {
#define TTY_WAITFG 0x2000
#define TTY_WAITBG 0x4000
#define TTY_BRACKETPASTE 0x8000
#define TTY_HAVESYNC 0x10000
#define TTY_ALL_REQUEST_FLAGS \
(TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC)
(TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA)
int flags;
struct tty_term *term;
@@ -1837,6 +1828,10 @@ struct tty {
struct event key_timer;
struct tty_key *key_tree;
#ifdef ENABLE_IMAGES
const struct image_backend *image_backend;
void *image_data;
#endif
};
/* Terminal command context. */
@@ -1858,6 +1853,8 @@ struct tty_ctx {
#define TTY_CTX_OVERLAY_SYNC 0x10
#define TTY_CTX_CELL_INVALIDATE 0x20
#define TTY_CTX_PANE_OBSCURED 0x40
#define TTY_CTX_IMAGE_SCROLL 0x80
#define TTY_CTX_PANE_FULL_WIDTH 0x100
union {
u_int n;
@@ -1873,7 +1870,7 @@ struct tty_ctx {
size_t size;
} sel;
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
struct image *image;
#endif
};
@@ -2537,7 +2534,6 @@ struct spawn_context {
#define SPAWN_HORIZONTAL 0x200
#define SPAWN_SPLIT 0x400
#define SPAWN_MODAL 0x800
#define SPAWN_FLOATOVERZOOM 0x1000
};
/* Paste buffer. */
@@ -2993,10 +2989,6 @@ void tty_cmd_scrolldown(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_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_default_colours(struct grid_cell *, struct window_pane *, u_int *);
@@ -3605,7 +3597,6 @@ void screen_write_mode_set(struct screen_write_ctx *, int);
void screen_write_mode_clear(struct screen_write_ctx *, int);
void screen_write_start_sync(struct window_pane *);
void screen_write_stop_sync(struct window_pane *);
void screen_write_end_sync(struct screen_write_ctx *);
void screen_write_clear_dirty(struct window_pane *);
void screen_write_cursorup(struct screen_write_ctx *, u_int);
void screen_write_cursordown(struct screen_write_ctx *, u_int);
@@ -3640,7 +3631,7 @@ void screen_write_setselection(struct screen_write_ctx *, const char *,
u_char *, u_int);
void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int,
int);
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
void screen_write_sixelimage(struct screen_write_ctx *,
struct sixel_image *, u_int);
#endif
@@ -3735,8 +3726,8 @@ void window_resize(struct window *, u_int, u_int, int, int);
void window_pane_send_resize(struct window_pane *, u_int, u_int);
int window_zoom(struct window_pane *);
int window_unzoom(struct window *, int);
int window_active_pane_is_over_zoom(struct window *);
struct window_pane *window_zoomed_pane(struct window *);
void window_push_modal_zoom(struct window *);
int window_pop_modal_zoom(struct window *);
int window_push_zoom(struct window *, int, int);
int window_pop_zoom(struct window *);
void window_lost_pane(struct window *, struct window_pane *);
@@ -4041,7 +4032,6 @@ void control_set_pane_on(struct client *, struct window_pane *);
void control_set_pane_off(struct client *, struct window_pane *);
void control_continue_pane(struct client *, struct window_pane *);
void control_pause_pane(struct client *, struct window_pane *);
void control_reset_pane(struct client *, struct window_pane *);
void control_set_window_size(struct client *, u_int, u_int, u_int);
int control_get_window_size(struct client *, u_int, u_int *, u_int *);
void control_clear_window_size(struct client *, u_int);
@@ -4227,17 +4217,106 @@ void spawn_editor_finish(struct window_pane *);
/* regsub.c */
char *regsub(const char *, const char *, const char *, int);
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
/* image.c */
int image_free_all(struct screen *);
struct image *image_store(struct screen *, struct sixel_image *);
int image_check_line(struct screen *, u_int, u_int);
int image_check_area(struct screen *, u_int, u_int, u_int, u_int);
int image_scroll_up(struct screen *, u_int);
#define IMAGE_BACKEND_GRAPHICAL 0x1
#define IMAGE_BACKEND_SCROLLS 0x2
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_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_draw_after_text(struct tty *, struct screen *, u_int,
u_int, u_int, u_int, u_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 *);
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);
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 */
#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_log(struct sixel_image *);
void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *);
@@ -4246,6 +4325,7 @@ struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int,
char *sixel_print(struct sixel_image *, struct sixel_image *,
size_t *);
struct screen *sixel_to_screen(struct sixel_image *);
struct image *sixel_to_image(struct sixel_image *);
#endif
/* 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

@@ -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;
const struct grid_cell *gcp;
struct grid_cell gc, ngc, last;
#ifdef ENABLE_IMAGES
struct grid_cell image_gc;
int image_status;
#endif
struct grid_line *gl;
u_int i, j, last_i, cx, ex, width;
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) {
/* Outside the area being drawn. */
empty = nx - i;
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 {
/* Get the current cell. */
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. */
empty = tty_draw_line_get_empty(&gc, &last,
nx - i);
empty = tty_draw_line_get_empty(gcp, &last, nx - i);
if (empty != 0)
gcp = &gc;
;
else {
/* Update for codeset if needed. */
gcp = tty_check_codeset(tty, &gc);
gcp = tty_check_codeset(tty, gcp);
/* And for selection. */
if (gcp->flags & GRID_FLAG_SELECTED) {

View File

@@ -224,9 +224,11 @@ static const struct tty_feature tty_feature_strikethrough = {
0
};
#define TTY_FEATURE_SYNC "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;"
/* Terminal supports synchronized updates. */
static const char *const tty_feature_sync_capabilities[] = {
"Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;",
TTY_FEATURE_SYNC,
NULL
};
static const struct tty_feature tty_feature_sync = {
@@ -357,6 +359,18 @@ static const struct tty_feature tty_feature_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
};
/* Terminal supports the OSC 9;4 progress bar. */
static const char *const tty_feature_progressbar_capabilities[] = {
"Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\",
@@ -379,6 +393,7 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_extkeys,
&tty_feature_focus,
&tty_feature_ignorefkeys,
&tty_feature_kitty,
&tty_feature_margins,
&tty_feature_mouse,
&tty_feature_osc7,
@@ -609,6 +624,7 @@ tty_default_features(struct client *c, const char *name, u_int version)
"extkeys,"
"focus,"
"hyperlinks,"
"sixel,"
"usstyle"
},
{ .name = "ghostty",
@@ -622,7 +638,20 @@ tty_default_features(struct client *c, const char *name, u_int version)
"osc7,"
"sync,"
"usstyle,"
"progressbar"
"progressbar,"
"kitty"
},
{ .name = "kitty",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
"ccolour,"
"cstyle,"
"extkeys,"
"focus,"
"hyperlinks,"
"osc7,"
"sync,"
"usstyle,"
"kitty"
},
{ .name = "Rio",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-keys.c,v 1.214 2026/08/18 09:01:20 nicm Exp $ */
/* $OpenBSD: tty-keys.c,v 1.213 2026/08/17 14:47:41 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -59,7 +59,6 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t,
size_t *);
static int tty_keys_extended_device_attributes(struct tty *, const char *,
size_t, size_t *);
static int tty_keys_sync(struct tty *, const char *, size_t, size_t *);
static int tty_keys_palette(struct tty *, const char *, size_t, size_t *);
/* A key tree entry. */
@@ -729,6 +728,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;
tty_set_size(tty, tty->sx, tty->sy, char_x, char_y);
tty_invalidate(tty);
recalculate_sizes();
tty->flags &= ~TTY_WINSIZEQUERY;
*size = end + 1;
@@ -772,17 +772,6 @@ tty_keys_next(struct tty *tty)
goto partial_key;
}
/* Is this a synchronized update mode response? */
switch (tty_keys_sync(tty, buf, len, &size)) {
case 0: /* yes */
key = KEYC_UNKNOWN;
goto complete_key;
case -1: /* no, or not valid */
break;
case 1: /* partial */
goto partial_key;
}
/* Is this a primary device attributes response? */
switch (tty_keys_device_attributes(tty, buf, len, &size)) {
case 0: /* yes */
@@ -1533,54 +1522,6 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len,
return (0);
}
/*
* Handle a synchronized update mode response. Returns 0 for success, -1 for
* failure, 1 for partial.
*/
static int
tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size)
{
struct client *c = tty->client;
static const char prefix[] = "\033[?2026;";
size_t i;
int status;
*size = 0;
if (tty->flags & TTY_HAVESYNC)
return (-1);
/* The response is always \033[?2026;Ps$y. */
for (i = 0; i < (sizeof prefix) - 1; i++) {
if (i == len)
return (1);
if (buf[i] != prefix[i])
return (-1);
}
if (i == len)
return (1);
if (buf[i] < '0' || buf[i] > '4')
return (-1);
status = buf[i++] - '0';
if (i == len)
return (1);
if (buf[i++] != '$')
return (-1);
if (i == len)
return (1);
if (buf[i++] != 'y')
return (-1);
*size = i;
if (status == 1 || status == 2 || status == 3) {
tty_parse_client_features(c, "sync", ",");
tty_update_features(tty);
}
log_debug("%s: received DECRPM %.*s", c->name, (int)*size, buf);
tty->flags |= TTY_HAVESYNC;
return (0);
}
/*
* Handle secondary device attributes input. Returns 0 for success, -1 for
* failure, 1 for partial.

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>
@@ -111,7 +111,6 @@ static const struct tty_term_code_entry tty_term_codes[] = {
[TTYC_ICH] = { TTYCODE_STRING, "ich" },
[TTYC_IL1] = { TTYCODE_STRING, "il1" },
[TTYC_IL] = { TTYCODE_STRING, "il" },
[TTYC_IND] = { TTYCODE_STRING, "ind" },
[TTYC_INDN] = { TTYCODE_STRING, "indn" },
[TTYC_INVIS] = { TTYCODE_STRING, "invis" },
[TTYC_KCBT] = { TTYCODE_STRING, "kcbt" },
@@ -525,6 +524,7 @@ tty_term_apply_overrides(struct tty_term *term)
tty_term_validate(term);
}
/* Mark terminals whose Ms capability does not contain a clipboard argument. */
static void
tty_term_validate(struct tty_term *term)
{

317
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.478 2026/08/17 14:47:41 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -62,17 +62,15 @@ static void tty_region(struct tty *, u_int, u_int);
static void tty_margin_pane(struct tty *, const struct tty_ctx *);
static void tty_margin(struct tty *, u_int, u_int);
static int tty_large_region(struct tty *, const struct tty_ctx *);
static int tty_scroll_images(struct tty *, const struct tty_ctx *, int *);
static void tty_redraw_region(struct tty *, const struct tty_ctx *);
static void tty_emulate_repeat(struct tty *, enum tty_code_code,
enum tty_code_code, u_int);
static void tty_draw_pane_line(struct tty *, const struct tty_ctx *, u_int,
u_int, u_int, u_int, 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);
#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) \
(tty->term->flags & TERM_DECSLRM)
#define tty_full_width(tty, ctx) \
@@ -163,10 +161,21 @@ tty_resize(struct tty *tty)
void
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->sy = sy;
tty->xpixel = xpixel;
tty->ypixel = ypixel;
#ifdef ENABLE_IMAGES
if (geometry_changed) {
image_tty_geometry_changed(tty);
server_redraw_client(tty->client);
}
#endif
}
static void
@@ -284,6 +293,10 @@ tty_open(struct tty *tty, char **cause)
}
tty->flags |= TTY_OPENED;
#ifdef ENABLE_IMAGES
image_tty_update(tty);
#endif
tty->flags &= ~(TTY_NOCURSOR|TTY_FREEZE|TTY_BLOCK|TTY_TIMER);
event_set(&tty->event_in, c->fd, EV_PERSIST|EV_READ,
@@ -338,7 +351,6 @@ tty_start_tty(struct tty *tty)
{
struct client *c = tty->client;
struct termios tio;
u_int i;
setblocking(c->fd, 0);
event_add(&tty->event_in, NULL);
@@ -354,21 +366,10 @@ tty_start_tty(struct tty *tty)
if (tcsetattr(c->fd, TCSANOW, &tio) == 0)
tcflush(c->fd, TCOFLUSH);
if (options_get_number(global_options, "clear-on-attach")) {
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_SMCUP);
tty_putcode(tty, TTYC_SMKX);
tty_putcode(tty, TTYC_CLEAR);
if (tty_acs_needed(tty)) {
log_debug("%s: using capabilities for ACS", c->name);
@@ -415,8 +416,6 @@ tty_send_requests(struct tty *tty)
tty_puts(tty, "\033[>c");
if (~tty->flags & TTY_HAVEXDA)
tty_puts(tty, "\033[>q");
if (~tty->flags & TTY_HAVESYNC)
tty_puts(tty, "\033[?2026$p");
tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\");
tty->flags |= (TTY_WAITBG|TTY_WAITFG);
} else
@@ -484,8 +483,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_SGR0));
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_term_has(tty->term, TTYC_SE))
tty_raw(tty, tty_term_string(tty->term, TTYC_SE));
@@ -510,10 +508,7 @@ tty_stop_tty(struct tty *tty)
if (tty_use_margin(tty))
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));
else
tty_raw(tty, tty_term_string(tty->term, TTYC_CLEAR));
tty_raw(tty, tty_term_string(tty->term, TTYC_RMCUP));
if (tty->term->flags & TERM_VT100LIKE)
tty_raw(tty, "\033[?2031l");
@@ -529,6 +524,9 @@ tty_close(struct tty *tty)
tty_stop_tty(tty);
if (tty->flags & TTY_OPENED) {
#ifdef ENABLE_IMAGES
image_tty_free(tty, 1);
#endif
evbuffer_free(tty->in);
event_del(&tty->event_in);
evbuffer_free(tty->out);
@@ -545,6 +543,9 @@ void
tty_free(struct tty *tty)
{
tty_close(tty);
#ifdef ENABLE_IMAGES
image_tty_free(tty, 0);
#endif
free(tty->r.ranges);
}
@@ -556,6 +557,9 @@ tty_update_features(struct tty *tty)
if (tty_apply_features(tty->term))
tty_term_apply_overrides(tty->term);
#ifdef ENABLE_IMAGES
image_tty_update(tty);
#endif
if (tty_use_margin(tty))
tty_putcode(tty, TTYC_ENMG);
@@ -1089,6 +1093,26 @@ tty_large_region(__unused struct tty *tty, const struct tty_ctx *ctx)
return (ctx->orlower - ctx->orupper >= ctx->sy / 2);
}
/* Work out whether and how the terminal can scroll image pixels. */
static int
tty_scroll_images(struct tty *tty, const struct tty_ctx *ctx, int *full_width)
{
*full_width = 0;
#ifdef ENABLE_IMAGES
if (~ctx->flags & TTY_CTX_IMAGE_SCROLL)
return (1);
if (image_backend_flags(tty) & IMAGE_BACKEND_SCROLLS)
return (1);
if (ctx->flags & TTY_CTX_PANE_FULL_WIDTH) {
*full_width = 1;
return (1);
}
return (0);
#else
return (1);
#endif
}
/*
* Return if BCE is needed but the terminal doesn't have it - it'll need to be
* emulated.
@@ -1422,11 +1446,34 @@ tty_clear_pane_area(struct tty *tty, const struct tty_ctx *ctx, u_int py,
tty_clear_area(tty, ctx, y, ry, x, rx, bg);
}
/* Redraw part of a pane line, including its ordered image layers. */
static void
tty_draw_pane_line(struct tty *tty, const struct tty_ctx *ctx, u_int px,
u_int py, u_int nx, u_int atx, u_int aty)
{
#ifdef ENABLE_IMAGES
int has_images;
has_images = image_grid_check_area(ctx->s->grid, px,
ctx->s->grid->hsize + py, nx, 1);
if (has_images) {
image_redraw_start(tty, atx, aty, nx, 1);
image_draw_line(tty, ctx->s, px, py, nx, atx, aty, 1,
&ctx->style_ctx);
}
#endif
tty_draw_line(tty, ctx->s, px, py, nx, atx, aty, &ctx->style_ctx);
#ifdef ENABLE_IMAGES
if (has_images)
image_draw_line(tty, ctx->s, px, py, nx, atx, aty, 0,
&ctx->style_ctx);
#endif
}
/* Redraw a line of a screen at py. */
static void
tty_draw_pane(struct tty *tty, const struct tty_ctx *ctx, u_int py)
{
struct screen *s = ctx->s;
u_int nx = ctx->sx, i, x, rx, ry, j;
struct visible_ranges *r;
struct visible_range *rr;
@@ -1439,8 +1486,8 @@ tty_draw_pane(struct tty *tty, const struct tty_ctx *ctx, u_int py)
rr = &r->ranges[j];
if (rr->nx == 0)
continue;
tty_draw_line(tty, s, rr->px - ctx->xoff, py, rr->nx,
rr->px, ctx->yoff + py, &ctx->style_ctx);
tty_draw_pane_line(tty, ctx, rr->px - ctx->xoff, py,
rr->nx, rr->px, ctx->yoff + py);
}
return;
}
@@ -1450,8 +1497,8 @@ tty_draw_pane(struct tty *tty, const struct tty_ctx *ctx, u_int py)
rr = &r->ranges[j];
if (rr->nx == 0)
continue;
tty_draw_line(tty, s, i + rr->px - x, py, rr->nx,
rr->px, ry, &ctx->style_ctx);
tty_draw_pane_line(tty, ctx, i + rr->px - x, py,
rr->nx, rr->px, ry);
}
}
}
@@ -1540,60 +1587,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));
}
#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 (c->session->curw->window != 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
tty_sync_start(struct tty *tty)
{
@@ -1667,19 +1660,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
tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx)
{
@@ -1727,9 +1707,19 @@ tty_cmd_deletecharacter(struct tty *tty, const struct tty_ctx *ctx)
void
tty_cmd_clearcharacter(struct tty *tty, const struct tty_ctx *ctx)
{
#ifdef ENABLE_IMAGES
u_int px, py;
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_clear_pane_line(tty, ctx, ctx->ocy, ctx->ocx, ctx->n, ctx->bg);
#ifdef ENABLE_IMAGES
px = ctx->xoff + ctx->ocx - ctx->wox;
py = ctx->yoff + ctx->ocy - ctx->woy;
image_draw_after_text(tty, ctx->s, ctx->ocx, ctx->ocy, ctx->n,
px, py, &ctx->style_ctx);
#endif
}
void
@@ -1738,7 +1728,8 @@ tty_cmd_insertline(struct tty *tty, const struct tty_ctx *ctx)
struct client *c = tty->client;
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
!tty_full_width(tty, ctx) ||
(!tty_full_width(tty, ctx) &&
(~ctx->flags & TTY_CTX_PANE_FULL_WIDTH)) ||
tty_fake_bce(tty, &ctx->defaults, ctx->bg) ||
!tty_term_has(tty->term, TTYC_CSR) ||
!tty_term_has(tty->term, TTYC_IL1) ||
@@ -1765,7 +1756,8 @@ tty_cmd_deleteline(struct tty *tty, const struct tty_ctx *ctx)
struct client *c = tty->client;
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
!tty_full_width(tty, ctx) ||
(!tty_full_width(tty, ctx) &&
(~ctx->flags & TTY_CTX_PANE_FULL_WIDTH)) ||
tty_fake_bce(tty, &ctx->defaults, ctx->bg) ||
!tty_term_has(tty->term, TTYC_CSR) ||
!tty_term_has(tty->term, TTYC_DL1) ||
@@ -1816,12 +1808,15 @@ void
tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx)
{
struct client *c = tty->client;
int full_width;
if (ctx->ocy != ctx->orupper)
return;
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!tty_full_width(tty, ctx) && !tty_use_margin(tty)) ||
if (!tty_scroll_images(tty, ctx, &full_width) ||
(ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!full_width && !tty_full_width(tty, ctx) &&
!tty_use_margin(tty)) ||
tty_fake_bce(tty, &ctx->defaults, 8) ||
!tty_term_has(tty->term, TTYC_CSR) ||
(!tty_term_has(tty->term, TTYC_RI) &&
@@ -1836,7 +1831,10 @@ tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx)
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
tty_margin_pane(tty, ctx);
if (full_width)
tty_margin_off(tty);
else
tty_margin_pane(tty, ctx);
tty_cursor_pane(tty, ctx, ctx->ocx, ctx->orupper);
if (tty_term_has(tty->term, TTYC_RI))
@@ -1849,12 +1847,15 @@ void
tty_cmd_linefeed(struct tty *tty, const struct tty_ctx *ctx)
{
struct client *c = tty->client;
int full_width;
if (ctx->ocy != ctx->orlower)
return;
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!tty_full_width(tty, ctx) && !tty_use_margin(tty)) ||
if (!tty_scroll_images(tty, ctx, &full_width) ||
(ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!full_width && !tty_full_width(tty, ctx) &&
!tty_use_margin(tty)) ||
tty_fake_bce(tty, &ctx->defaults, 8) ||
!tty_term_has(tty->term, TTYC_CSR) ||
ctx->sx == 1 ||
@@ -1867,7 +1868,10 @@ tty_cmd_linefeed(struct tty *tty, const struct tty_ctx *ctx)
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
tty_margin_pane(tty, ctx);
if (full_width)
tty_margin_off(tty);
else
tty_margin_pane(tty, ctx);
/*
* If we want to wrap a pane while using margins, the cursor needs to
@@ -1892,9 +1896,12 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx)
{
struct client *c = tty->client;
u_int i;
int full_width;
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!tty_full_width(tty, ctx) && !tty_use_margin(tty)) ||
if (!tty_scroll_images(tty, ctx, &full_width) ||
(ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!full_width && !tty_full_width(tty, ctx) &&
!tty_use_margin(tty)) ||
tty_fake_bce(tty, &ctx->defaults, 8) ||
!tty_term_has(tty->term, TTYC_CSR) ||
ctx->sx == 1 ||
@@ -1907,7 +1914,10 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx)
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
tty_margin_pane(tty, ctx);
if (full_width)
tty_margin_off(tty);
else
tty_margin_pane(tty, ctx);
if (ctx->n == 1 || !tty_term_has(tty->term, TTYC_INDN)) {
if (!tty_use_margin(tty))
@@ -1930,9 +1940,12 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx)
{
u_int i;
struct client *c = tty->client;
int full_width;
if ((ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!tty_full_width(tty, ctx) && !tty_use_margin(tty)) ||
if (!tty_scroll_images(tty, ctx, &full_width) ||
(ctx->flags & TTY_CTX_WINDOW_BIGGER) ||
(!full_width && !tty_full_width(tty, ctx) &&
!tty_use_margin(tty)) ||
tty_fake_bce(tty, &ctx->defaults, 8) ||
!tty_term_has(tty->term, TTYC_CSR) ||
(!tty_term_has(tty->term, TTYC_RI) &&
@@ -1947,7 +1960,10 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx)
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
tty_margin_pane(tty, ctx);
if (full_width)
tty_margin_off(tty);
else
tty_margin_pane(tty, ctx);
tty_cursor_pane(tty, ctx, ctx->ocx, ctx->orupper);
if (tty_term_has(tty->term, TTYC_RIN))
@@ -2070,6 +2086,16 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx)
if (vis < gcp->data.width) {
tty_draw_line(tty, s, s->cx, s->cy, gcp->data.width,
px, py, &ctx->style_ctx);
#ifdef ENABLE_IMAGES
for (i = 0; i < r->used; i++) {
if (r->ranges[i].nx == 0)
continue;
image_draw_after_text(tty, s,
ctx->ocx + r->ranges[i].px - px, ctx->ocy,
r->ranges[i].nx, r->ranges[i].px, py,
&ctx->style_ctx);
}
#endif
return;
}
}
@@ -2086,6 +2112,11 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx)
tty_cell(tty, ctx->cell, &ctx->style_ctx);
#ifdef ENABLE_IMAGES
image_draw_after_text(tty, s, ctx->ocx, ctx->ocy, gcp->data.width,
px, py, &ctx->style_ctx);
#endif
if (ctx->flags & TTY_CTX_CELL_INVALIDATE)
tty_invalidate(tty);
}
@@ -2133,6 +2164,10 @@ tty_cmd_cells(struct tty *tty, const struct tty_ctx *ctx)
cx = ri->px - ctx->xoff + ctx->wox;
tty_cursor_pane_unless_wrap(tty, ctx, cx, ctx->ocy);
tty_putn(tty, cp + ri->px - px, ri->nx, ri->nx);
#ifdef ENABLE_IMAGES
image_draw_after_text(tty, ctx->s, cx, ctx->ocy,
ri->nx, ri->px, py, &ctx->style_ctx);
#endif
}
}
}
@@ -2173,58 +2208,6 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx)
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
tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx)
{

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */
/* $OpenBSD: window-buffer.c,v 1.51 2026/07/15 12:45:39 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -567,7 +567,9 @@ window_buffer_edit_close_cb(char *buf, size_t len, void *arg)
}
oldbuf = paste_buffer_data(pb, &oldlen);
if (oldlen != 0 && oldbuf[oldlen - 1] != '\n' && buf[len - 1] == '\n')
if (oldlen != '\0' &&
oldbuf[oldlen - 1] != '\n' &&
buf[len - 1] == '\n')
len--;
if (len != 0)
paste_replace(pb, buf, len);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-copy.c,v 1.426 2026/08/19 19:55:04 nicm Exp $ */
/* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -4093,7 +4093,7 @@ window_copy_search_lr_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py,
endline = gd->hsize + gd->sy - 1;
pywrap = py;
while (buf != NULL &&
pywrap < endline &&
pywrap <= endline &&
len < WINDOW_COPY_SEARCH_MAX_LINE) {
gl = grid_get_line(gd, pywrap);
if (~gl->flags & GRID_LINE_WRAPPED)
@@ -4152,7 +4152,7 @@ window_copy_search_rl_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py,
endline = gd->hsize + gd->sy - 1;
pywrap = py;
while (buf != NULL &&
pywrap < endline &&
pywrap <= endline &&
len < WINDOW_COPY_SEARCH_MAX_LINE) {
gl = grid_get_line(gd, pywrap);
if (~gl->flags & GRID_LINE_WRAPPED)
@@ -4353,7 +4353,6 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy,
break;
}
}
ncells = cell;
/* Locate starting cell. */
cell = 0;
@@ -5364,6 +5363,14 @@ window_copy_write_line(struct window_mode_entry *wme,
else
content_sx = sx;
#ifdef ENABLE_IMAGES
/* Make the backing line's image layers available before drawing text. */
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);
#endif
screen_write_cursormove(ctx, 0, py, 0);
ft = format_create_defaults(NULL, NULL, NULL, NULL, wp);
@@ -5407,7 +5414,6 @@ window_copy_write_line(struct window_mode_entry *wme,
window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py,
content_sx, &mgc, &cgc, &mkgc, &clgc);
if (py == 0 && s->rupper < s->rlower && !data->hide_position) {
value = options_get_string(oo, "copy-mode-position-format");
if (*value != '\0') {

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>
@@ -1009,7 +1009,6 @@ window_panes_run_command(struct window_panes_modedata *data, struct client *c,
} else {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
}
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>
@@ -227,8 +227,7 @@ window_switch_build(struct window_switch_modedata *data)
m = xreallocarray(m, n + 1, sizeof *m);
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);
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.94 2026/07/14 17:17:18 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -642,7 +642,6 @@ window_tree_draw_session(struct window_tree_modedata *data, struct session *s,
}
free(label);
}
format_free(ft);
if (loop != end - 1) {
screen_write_cursormove(ctx, cx + offset + width, cy,
@@ -791,7 +790,6 @@ window_tree_draw_window(struct window_tree_modedata *data, struct session *s,
}
free(label);
}
format_free(ft);
if (loop != end - 1) {
screen_write_cursormove(ctx, cx + offset + width, cy,
@@ -1022,10 +1020,6 @@ window_tree_swap(void *cur_itemdata, void *other_itemdata,
window_tree_pull_item(other, &other_session, &other_winlink,
&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)
return (0);

136
window.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */
/* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -718,7 +718,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
return (0);
if (w->modal != NULL && wp != w->modal)
return (0);
if ((w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp))
if (w->flags & WINDOW_ZOOMED)
window_unzoom(w, 1);
lastwp = w->active;
@@ -918,18 +918,13 @@ window_zoom(struct window_pane *wp)
{
struct window *w = wp->window;
struct window_pane *wp1;
struct layout_cell *lc;
struct layout_geometry lg;
if (w->flags & WINDOW_ZOOMED)
return (-1);
if (window_count_panes(w, 1) == 1)
return (-1);
if (w->active != wp &&
(w->active == NULL ||
(~w->active->flags & PANE_FLOATOVERZOOM) ||
!window_pane_is_floating(w->active)))
if (w->active != wp)
window_set_active_pane(w, wp, 1);
wp->flags |= PANE_ZOOMED;
@@ -940,22 +935,6 @@ window_zoom(struct window_pane *wp)
w->saved_layout_root = w->layout_root;
layout_init(w, wp);
TAILQ_FOREACH(wp1, &w->panes, entry) {
lc = wp1->saved_layout_cell;
if (wp1 == wp ||
(~wp1->flags & PANE_FLOATOVERZOOM) ||
lc == NULL ||
(~lc->flags & LAYOUT_CELL_FLOATING))
continue;
memcpy(&lg, &lc->g, sizeof lg);
lc = layout_floating_pane(w, wp, &lg);
layout_assign_pane(lc, wp1, 0);
}
/* A floating zoom target is now tiled, so put it behind the floats. */
if (wp->saved_layout_cell->flags & LAYOUT_CELL_FLOATING) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_TAIL(&w->z_index, wp, zentry);
}
w->flags |= WINDOW_ZOOMED;
events_fire_window("window-zoomed", w);
events_fire_window("window-layout-changed", w);
@@ -967,26 +946,11 @@ window_zoom(struct window_pane *wp)
int
window_unzoom(struct window *w, int notify)
{
struct window_pane *wp, *zoomed = NULL;
struct layout_cell *slc;
struct window_pane *wp;
if (~w->flags & WINDOW_ZOOMED)
if (!(w->flags & WINDOW_ZOOMED))
return (-1);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_ZOOMED)
zoomed = wp;
if (~wp->flags & PANE_FLOATOVERZOOM)
continue;
if (wp->flags & PANE_ZOOMED)
continue;
slc = wp->saved_layout_cell;
if (slc == NULL || wp->layout_cell == NULL)
continue;
memcpy(&slc->g, &wp->layout_cell->g, sizeof slc->g);
memcpy(&slc->fg, &wp->layout_cell->fg, sizeof slc->fg);
}
w->flags &= ~WINDOW_ZOOMED;
layout_free(w, 0);
w->layout_root = w->saved_layout_root;
@@ -997,22 +961,6 @@ window_unzoom(struct window *w, int notify)
wp->saved_layout_cell = NULL;
wp->flags &= ~PANE_ZOOMED;
}
/* Put a floating zoom target back into the floating part of the list. */
if (zoomed != NULL && window_pane_is_floating(zoomed)) {
TAILQ_REMOVE(&w->z_index, zoomed, zentry);
if (zoomed == w->active)
TAILQ_INSERT_HEAD(&w->z_index, zoomed, zentry);
else {
TAILQ_FOREACH(wp, &w->z_index, zentry) {
if (!window_pane_is_floating(wp))
break;
}
if (wp == NULL)
TAILQ_INSERT_TAIL(&w->z_index, zoomed, zentry);
else
TAILQ_INSERT_BEFORE(wp, zoomed, zentry);
}
}
layout_fix_panes(w, NULL);
if (notify) {
@@ -1024,69 +972,48 @@ window_unzoom(struct window *w, int notify)
return (0);
}
struct window_pane *
window_zoomed_pane(struct window *w)
void
window_push_modal_zoom(struct window *w)
{
struct window_pane *wp;
if (~w->flags & WINDOW_ZOOMED)
return (NULL);
TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) {
if (wp->layout_cell != NULL && !window_pane_is_floating(wp))
return (wp);
}
return (NULL);
if (w->flags & WINDOW_ZOOMED)
w->flags |= WINDOW_WASMODALZOOMED;
else
w->flags &= ~WINDOW_WASMODALZOOMED;
window_unzoom(w, 1);
}
int
window_active_pane_is_over_zoom(struct window *w)
window_pop_modal_zoom(struct window *w)
{
if (~w->flags & WINDOW_ZOOMED)
struct window_pane *wp = w->active;
if (~w->flags & WINDOW_WASMODALZOOMED)
return (0);
if (w->active == NULL)
return (0);
if (~w->active->flags & PANE_FLOATOVERZOOM)
return (0);
return (window_pane_is_floating(w->active));
w->flags &= ~WINDOW_WASMODALZOOMED;
if (wp != NULL && window_has_pane(w, wp))
return (window_zoom(wp) == 0);
return (0);
}
int
window_push_zoom(struct window *w, int always, int flag)
{
struct window_pane *wp = window_zoomed_pane(w);
log_debug("%s: @%u %d", __func__, w->id,
flag && (w->flags & WINDOW_ZOOMED));
if (flag && (always || (w->flags & WINDOW_ZOOMED)))
w->flags |= WINDOW_WASZOOMED;
else
w->flags &= ~WINDOW_WASZOOMED;
if (w->flags & WINDOW_WASZOOMED)
w->was_zoomed = wp;
else
w->was_zoomed = NULL;
return (window_unzoom(w, 1) == 0);
}
int
window_pop_zoom(struct window *w)
{
struct window_pane *wp = w->was_zoomed;
log_debug("%s: @%u %d", __func__, w->id,
!!(w->flags & WINDOW_WASZOOMED));
if (w->flags & WINDOW_WASZOOMED) {
w->flags &= ~WINDOW_WASZOOMED;
w->was_zoomed = NULL;
if (w->active != NULL &&
((~w->active->flags & PANE_FLOATOVERZOOM) ||
!window_pane_is_floating(w->active)))
wp = w->active;
if (wp == NULL || !window_has_pane(w, wp))
wp = w->active;
if (wp != NULL)
return (window_zoom(wp) == 0);
}
if (w->flags & WINDOW_WASZOOMED)
return (window_zoom(w->active) == 0);
return (0);
}
@@ -1138,8 +1065,8 @@ window_lost_pane(struct window *w, struct window_pane *wp)
server_clear_marked();
if (wp == w->modal_last)
w->modal_last = NULL;
if (wp == w->was_zoomed)
w->was_zoomed = NULL;
if (w->modal_last == NULL)
w->flags &= ~WINDOW_WASMODALZOOMED;
window_pane_stack_remove(&w->last_panes, wp);
if (wp == w->active) {
@@ -1166,6 +1093,7 @@ window_lost_pane(struct window *w, struct window_pane *wp)
}
} else if (wp == w->modal) {
w->modal = w->modal_last = NULL;
w->flags &= ~WINDOW_WASMODALZOOMED;
}
redraw_invalidate_scene(w);
}
@@ -1173,9 +1101,13 @@ window_lost_pane(struct window *w, struct window_pane *wp)
void
window_remove_pane(struct window *w, struct window_pane *wp)
{
int pop = (wp == w->modal);
window_lost_pane(w, wp);
TAILQ_REMOVE(&w->panes, wp, entry);
TAILQ_REMOVE(&w->z_index, wp, zentry);
if (pop && window_pop_modal_zoom(w))
server_redraw_window(w);
redraw_invalidate_scene(w);
window_pane_destroy(wp);
}
@@ -1331,8 +1263,6 @@ window_pane_printable_flags(struct window_pane *wp)
flags[pos++] = 'Z';
if (window_pane_is_floating(wp))
flags[pos++] = 'F';
if (wp->flags & PANE_FLOATOVERZOOM)
flags[pos++] = 'A';
if (wp == w->modal)
flags[pos++] = 'O';
flags[pos] = '\0';
@@ -1478,8 +1408,6 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp)
void
window_pane_scrollbar_redraw(struct window_pane *wp)
{
if (!window_pane_scrollbar_visible(wp))
return;
if (window_pane_scrollbar_overlay_visible(wp)) {
wp->flags |= PANE_REDRAW;
return;
@@ -1547,16 +1475,16 @@ window_pane_free(struct window_pane *wp)
log_debug("pane %%%u freed (%d references)", wp->id, wp->references);
free(wp->searchstr);
screen_free(&wp->status_screen);
screen_free(&wp->base);
free(wp->r.ranges);
options_free(wp->options);
free((void *)wp->cwd);
free(wp->shell);
cmd_free_argv(wp->argc, wp->argv);
colour_palette_free(&wp->palette);
style_ranges_free(&wp->border_status_line.ranges);
free(wp->border_status_line.expanded);
free(wp);
}
@@ -2041,7 +1969,7 @@ window_pane_is_visible(struct window_pane *wp)
{
if (~wp->window->flags & WINDOW_ZOOMED)
return (1);
return (wp->layout_cell != NULL);
return (wp == wp->window->active);
}
int