Skip to content

Spend a single shift on the next graphic character, and nothing else - #150

Merged
JohnCampionJr merged 2 commits into
tomlm:mainfrom
JohnCampionJr:fix/single-shift-lifetime
Sep 1, 2026
Merged

Spend a single shift on the next graphic character, and nothing else#150
JohnCampionJr merged 2 commits into
tomlm:mainfrom
JohnCampionJr:fix/single-shift-lifetime

Conversation

@JohnCampionJr

Copy link
Copy Markdown
Collaborator

From the review on #141, and still reproducing on 6d32504.

Four ways in and three answers: SI cancelled a pending single shift, SO and the locking shifts left it standing, and RIS could not reach it at all.

ESC * 0, SS2, RIS, then 'q'   →  ─   on a terminal that had just been reset to ASCII

Why RIS was the sharp one

The pending state held the table G2 had resolved to. ResetCharsets put the tables back and the shift went on pointing at the one it had captured — so the reset was undone by a character arriving after it. RIS is exactly how someone recovers from a program that died mid-sequence, and a shift left dangling by one is the state most likely to be sitting there.

What changed

The pending state is the G-set, not a table, resolved when the character arrives. That fixes a third case for the same reason:

ESC N, ESC * 0, then 'q'      →  q   the shift captured G2 before it was designated

SS2 invokes G2, and what G2 holds is a question with an answer at print time. A designation between the shift and the character it shifts belongs to that character.

SI no longer cancels

The VT510 manual scopes a single shift to "the next graphic character". A locking shift is not one — so of the four entry points, the three that left a pending shift standing were right and the one that cancelled it was wrong. InvokeSingleShift's own remarks already said consumption is what spends it; ShiftIn was the line that disagreed.

Nothing in the suite depended on the old behaviour.

Verification

The test carries the three cases that already worked — SO, LS2, and a shift spending itself on one character rather than two — alongside the three that did not, because a test of only the broken cases cannot tell "missing" from "misrouted".

Checked against the defect rather than assumed: with the production files reverted it fails on the RIS case (Expected "q" / Actual "─"), and a probe on main fails the SI and designate-after-shift cases too.

2150 passed, 1 skipped, 0 failed. The skip is DA3 (#123).

Relationship to #149

Both are based on mainneither is stacked, after what happened to #146. They do touch ResetCharsets and both append a test to VtTestBehaviourTests.cs, so whichever lands second needs a small rebase. Merge in either order.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Supplementary Unicode graphic characters still fail to consume a pending single shift.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates single-shift handling so pending shifts resolve current G-set designations and reset correctly.

Changes:

  • Stores the pending G-set rather than its resolved table.
  • Preserves shifts across locking shifts but clears them on reset.
  • Adds regression coverage for reset, designation, and consumption behavior.
File summaries
File Description
InputHandler.Print.cs Revises shift resolution and reset behavior.
InputHandler.cs Replaces shift table state with a G-set identifier.
VtTestBehaviourTests.cs Adds single-shift behavioral tests.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/XTerm.NET/InputHandler.Print.cs
@JohnCampionJr
JohnCampionJr force-pushed the fix/single-shift-lifetime branch 2 times, most recently from 5c0d158 to 9f71e80 Compare September 1, 2026 01:10
JohnCampionJr and others added 2 commits August 31, 2026 21:11
From the review on tomlm#141. Four ways in and three answers: SI cancelled a
pending single shift, SO and the locking shifts left it standing, and
RIS could not reach it at all.

    ESC * 0, SS2, RIS, then 'q'   a line-drawing dash, on a terminal
                                  that had just been reset to ASCII

RIS is the sharpest of the three because the pending state held the
TABLE G2 had resolved to. ResetCharsets put the tables back and the
shift went on pointing at the one it had captured, so the reset was
undone by a character arriving after it.

The pending state is now the G-SET rather than a table, resolved when
the character arrives. That fixes a third case the same way: a
designation between the shift and the character it shifts belongs to
that character, because SS2 invokes G2 and what G2 holds is a question
with an answer at print time.

    ESC N, ESC * 0, then 'q'      printed a letter; the shift had
                                  captured G2 before it was designated

SI no longer cancels. The VT510 manual scopes a single shift to "the
next graphic character", and a locking shift is not one -- so the three
that left it standing were right and the one that did not was wrong.
Nothing in the suite depended on the old behaviour.

The test carries the three that already worked -- SO, LS2, and a shift
spending itself on one character rather than two -- alongside the three
that did not, because a test of only the broken cases cannot tell
"missing" from "misrouted".

Verified against the defect: with the production files reverted the test
fails on the RIS case, and the earlier probe fails on the SI and
designate-after-shift cases too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From Copilot's review. The invariant this branch sets out -- a single shift is
spent by the next graphic character -- held only for characters that fit in
one UTF-16 code unit. A supplementary character reaches Print as two, and the
clear that spends the shift sat inside the single-code-unit branch.

That does not skip the shift, it MOVES it. ESC * 0, SS2, an emoji, then 'q'
drew the q as a box-drawing glyph on a terminal whose G2 the program had
finished with -- the same shape as the RIS case this branch already fixes,
reached by a different route.

The emoji itself stays untranslated: a 94-character set has no entry outside
the BMP, and TranslateChar takes a single code unit. Spending the shift and
translating through it are separate things, and only the first applies here.

The clear goes AFTER the length test rather than before it. The review
suggested consuming the shift ahead of the graphic-character branches, which
would put a null check in front of the test every ordinary character takes;
CLAUDE.md opens with that. This branch runs for a surrogate pair only, and
only to clear a field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JohnCampionJr
JohnCampionJr force-pushed the fix/single-shift-lifetime branch from 9f71e80 to cb055d0 Compare September 1, 2026 01:11
@JohnCampionJr
JohnCampionJr merged commit 6dcb768 into tomlm:main Sep 1, 2026
3 of 4 checks passed
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.

2 participants