fix(tmux): complete control mode protocol parsing#11217
fix(tmux): complete control mode protocol parsing#11217daiimus wants to merge 2 commits intoghostty-org:mainfrom
Conversation
Add missing protocol handling for tmux control mode: control.zig: - Add unescapeOctal() to decode \NNN octal escapes in %output data. tmux encodes bytes <32 and backslash as octal; without decoding, all ANSI escape sequences arrived as literal text (e.g. \033 instead of ESC). 9 unit tests + 5 integration tests. - Parse %window-close notification with window ID extraction. - Parse %session-window-changed notification with session/window IDs. - Parse %exit notification explicitly (previously only generated synthetically on unexpected input in idle state). - Add window_close and session_window_changed to Notification union. output.zig: - Add window_name variable for list-windows format strings. - 1 test.
There was a problem hiding this comment.
Pull request overview
This PR completes tmux control mode protocol parsing by adding octal escape decoding for %output data, three new notification parsers (%window-close, %session-window-changed, %exit), and a window_name output variable. It is a prerequisite for full tmux control mode support (#1935).
Changes:
- Adds
unescapeOctal()toParserincontrol.zigfor in-place\NNNdecoding of%outputpayloads, plus 9 unit tests and 5 integration tests - Adds
window_closeandsession_window_changednotification variants and parsers (including the%exitexplicit handler) with corresponding tests - Adds
window_nameas a string-passthroughVariableinoutput.zigwith 1 test
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/terminal/tmux/control.zig |
Adds unescapeOctal, new notification parsers for %window-close, %session-window-changed, and %exit, new Notification union variants, and tests |
src/terminal/tmux/output.zig |
Adds window_name variable with string passthrough parsing and a test |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Widen unescapeOctal intermediate to u16 to prevent overflow on values above 255 (\400+), treating them as literal characters - Add tests for octal overflow boundary (max u8 \377, overflow \400) - Add test for %exit with reason string (e.g. "lost-server") - Fix test name "parse window_name" -> "parse window name" - Add no-op switch arms for window_close and session_window_changed notifications in viewer.zig to fix compilation AI: Claude Code (claude-sonnet-4-20250514). Assisted with test generation and review feedback triage. All changes reviewed and understood by contributor.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
mitchellh
left a comment
There was a problem hiding this comment.
One question I have here is why do the octal decoding at all. I think the whole idea was to keep that data uncoded to avoid both an error path (though, none in this case) and extra work if the data is not read.
There's some other suspicious code here I want to look more deeply into.
| } | ||
| } else { | ||
| data[write] = data[read]; | ||
| read += 1; |
There was a problem hiding this comment.
one small issue on this fallback branch. in tmux control mode a raw \ is the escape introducer and its literal payload is encoded as \134 so if we do end up here due to malformed octal seq, treating \ as literal paylaod is the wrong default
| } else { | ||
| data[write] = data[read]; | ||
| read += 1; | ||
| } |
There was a problem hiding this comment.
one small issue on this fallback branch. in tmux control mode a raw \ is the escape introducer and its literal payload is encoded as \134 so if we do end up here due to malformed octal seq, treating \ as literal payload is the wrong default
| // Compute the octal value using u16 to avoid overflow. | ||
| // Values above 255 (\400+) cannot represent a byte, so | ||
| // treat them as literal characters. | ||
| const val: u16 = (@as(u16, data[read + 1] - '0') << 6) | | ||
| (@as(u16, data[read + 2] - '0') << 3) | |
There was a problem hiding this comment.
don't think we need the u16 cast. tmux only encodes bytes < 32 and \ (92), so the max decoded value is \134 = 92.
noticed this PR when facing octal decoder bug. we need decoding cause tmux control mode (wiki encodes bytes < ASCII 32 and \ as \ooo (backslash + three octal digits) for %output and %extended-output notifications. so if we don't decode them, the literal encoded characters will show up on the terminal (e.g: , 0, 1, 1 instead of TAB). I have a cleaner implementation of just the decoder part if it's useful. happy to open a separate PR |
|
@h3nock I would definitely defer to your cleaner implementation. I've waded into waters a bit too deep for me with Ghostty and, while my PRs had best intentions in-mind, I don't think they've offered much in the way of valuable contributions. |
Summary
Add missing protocol handling for tmux control mode: octal escape decoding in
%outputdata, three new notification parsers, and thewindow_nameoutput variable.Changes
src/terminal/tmux/control.zig:unescapeOctal()to decode\NNNoctal escapes in%outputdata in-place. tmux encodes bytes <32 and backslash as octal; without
decoding, all ANSI escape sequences arrive as literal text (e.g.
\033instead of ESC). 9 unit tests + 5 integration tests.%window-closenotification (extracts window ID)%session-window-changednotification (extracts session +window IDs)
%exitnotification explicitly — previously only generatedsynthetically on unexpected input in idle state, but tmux actually
sends
%exitwhen the control mode client is detachingwindow_closeandsession_window_changedvariants to theNotificationunionsrc/terminal/tmux/output.zig:window_namevariable for use inlist-windowsformat stringsContext
Found while building tmux control mode support in Geistty, an iOS SSH terminal using Ghostty as its rendering engine (fork). The octal decoding was the most impactful fix — without it, every
%outputnotification containing ANSI escape sequences (colors, cursor movement, etc.) was delivered as literal backslash-digit text instead of the actual control bytes.This is a prerequisite for #1935 (tmux control mode support).
AI Disclosure
Fix was developed with Claude Code assistance. I understand the changes: tmux control mode's wire format encodes bytes <32 and backslash as
\NNNoctal sequences (documented in tmux(1) under "CONTROL MODE"). TheunescapeOctalfunction decodes in-place since the decoded output is always <= the encoded length, avoiding allocation. The new notification parsers follow the exact same pattern as the existing upstream parsers (regex match, capture group extraction,parseInt). The explicit%exithandler ensures proper cleanup rather than relying on the synthetic exit from unexpected-byte detection.