Skip to content

fix(tmux): complete control mode protocol parsing#11217

Open
daiimus wants to merge 2 commits intoghostty-org:mainfrom
daiimus:fix/tmux-control-mode-parsing
Open

fix(tmux): complete control mode protocol parsing#11217
daiimus wants to merge 2 commits intoghostty-org:mainfrom
daiimus:fix/tmux-control-mode-parsing

Conversation

@daiimus
Copy link
Copy Markdown

@daiimus daiimus commented Mar 7, 2026

Summary

Add missing protocol handling for tmux control mode: octal escape decoding in %output data, three new notification parsers, and the window_name output variable.

Changes

src/terminal/tmux/control.zig:

  • Add unescapeOctal() to decode \NNN octal escapes in %output
    data in-place. tmux encodes bytes <32 and backslash as octal; without
    decoding, all ANSI escape sequences arrive as literal text (e.g.
    \033 instead of ESC). 9 unit tests + 5 integration tests.
  • Parse %window-close notification (extracts window ID)
  • Parse %session-window-changed notification (extracts session +
    window IDs)
  • Parse %exit notification explicitly — previously only generated
    synthetically on unexpected input in idle state, but tmux actually
    sends %exit when the control mode client is detaching
  • Add window_close and session_window_changed variants to the
    Notification union

src/terminal/tmux/output.zig:

  • Add window_name variable for use in list-windows format strings
  • 1 test

Context

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 %output notification 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 \NNN octal sequences (documented in tmux(1) under "CONTROL MODE"). The unescapeOctal function 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 %exit handler ensures proper cleanup rather than relying on the synthetic exit from unexpected-byte detection.

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.
Copilot AI review requested due to automatic review settings March 7, 2026 20:39
@daiimus daiimus requested a review from a team as a code owner March 7, 2026 20:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() to Parser in control.zig for in-place \NNN decoding of %output payloads, plus 9 unit tests and 5 integration tests
  • Adds window_close and session_window_changed notification variants and parsers (including the %exit explicit handler) with corresponding tests
  • Adds window_name as a string-passthrough Variable in output.zig with 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.

Comment thread src/terminal/tmux/control.zig
Comment thread src/terminal/tmux/control.zig
Comment thread src/terminal/tmux/control.zig
Comment thread src/terminal/tmux/output.zig Outdated
- 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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@mitchellh mitchellh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +84 to +87
} else {
data[write] = data[read];
read += 1;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +71 to +75
// 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) |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think we need the u16 cast. tmux only encodes bytes < 32 and \ (92), so the max decoded value is \134 = 92.

source: https://github.com/tmux/tmux/wiki/Control-Mode

@h3nock
Copy link
Copy Markdown

h3nock commented Mar 30, 2026

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.

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

@daiimus
Copy link
Copy Markdown
Author

daiimus commented Mar 31, 2026

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants