Repository navigation
ENQ answerback, OSC colour report format, and opt-in KAM - #181
Conversation
ENQ (0x05) was dropped: the execute switch began at BEL. It now sends EnquiryResponse, xterm's answerbackString, when that is non-empty; empty stays the default, so nothing changes for an existing host. The case sits in the same dense switch, which compiles to a jump table, so LF, CR and BS pay no extra compare for it. A host handler registered for 0x05 runs in the parser before this and still wins when it consumes the control. The reply goes out as input-direction data with no S8C1T rewrite, since it is the user's literal string, not a report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rmat OSC 4, 5 and 10/11/12 queries always answered rgb:rrrr/gggg/bbbb. The new option keeps that as SixteenBit, the default, and adds EightBit (rgb:rr/gg/bb) and None (no reply; setting colours is unaffected). All three reply sites -- the palette, the special colours, and the dynamic colours -- now go through one ReplyColor helper so none can miss the option. There are no OSC 17/19 or other colour replies to cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SM 2 was stored and reported by DECRQM, and nothing read it. Now: - Off (the default), SM 2 is ignored and DECRQM reports mode 2 reset, as Ghostty does. A program that locks the keyboard and dies leaves the user unable to type. - On, SM 2 sets the new read-only Terminal.KeyboardLocked, and the library's own input paths drop input while it is set: GenerateKeyInput and GenerateCharInput return empty, GenerateKittyKeyInput returns null, and Paste sends nothing. - RM 2, RIS and DECSTR clear it. RM 2 is honoured even with the option off, so turning the option off cannot strand a locked keyboard. The stored-modes theory in RequestModeTests no longer lists KAM, since toggling it by default is the behaviour this changes; the toggle is now asserted with the option on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tomlm
left a comment
There was a problem hiding this comment.
Adds three opt-in options for Ghostty parity: EnquiryResponse (ENQ answerback), ColorReportFormat (OSC 4/5/10/11/12 reply width, or no reply), and KeyboardActionModeAllowed with a new Terminal.KeyboardLocked that the key, char, Kitty and paste paths check.
The ENQ and colour-format halves are sound. The KAM half changes default behaviour in a way that contradicts a test the repo deliberately added.
Medium
- KAM refused by default breaks esctest
test_DECRQM_ANSI_KAM, and the test that pinned it was deleted rather than reconciled (InputHandler.StoredModes.cs:87). xterm, which the repo grades against, toggles and reports KAM. This PR changes the default path to Ghostty's behaviour without making it opt-in.
Doc comments
Terminal.KeyboardLockedremarks say it is "only ever true when KeyboardActionModeAllowed is".TerminalOptions.KeyboardActionModeAllowedand the PR's own testDisallowing_after_a_lock_still_lets_RM_2_unlockboth say the opposite (Terminal.cs:53).- The
RequestModeTestssummary still justifies KAM being stored with "esctest toggles them and expects the toggle reported". That reason no longer holds for KAM, and the new line added under it contradicts it (RequestModeTests.cs:304).
Notes (checked, fine)
- ENQ dispatch: the parser routes 0x05 through
OnExecute, so a registered execute handler runs first and a consuming one suppresses the reply. The TerminalOptions remark is accurate. Inside OSC and DCS payloads ENQ is not executed, and the tests cover both. Addingcase 0x05only moves the jump table's base from 7 to 5, so LF, CR and BS pay no extra compare, as the PR says. - ENQ reply path: sending through
RaiseInputDatawith no C1 conversion matches xterm'sCASE_ENQ, whichunparseputcsanswer_backbyte for byte. A nullEnquiryResponseis handled by theis { Length: > 0 }pattern. - Colour replies: every colour reply in
InputHandler.Osc.csnow goes throughReplyColor. I grepped forrgb:andRaiseDataReceivedand found no other colour-reply site. The OSC terminator is still mirrored. EightBitrgb:rr/gg/bbmatches Ghostty's8-bitformat.CopyFromcopies all three new options. - Unlocking: DECSTR unlocking KAM matches the VT510 DECSTR table. Both RIS and DECSTR reach
ResetStoredModes, which now clearsKeyboardLocked. - Mouse input:
GenerateMouseEventandGenerateFocusEventare not gated by KAM. That is correct, because KAM locks the keyboard only. - Low:
ColorSpec.Format(int, ColorReportFormat)returns null, meaning no reply, for any out-of-range enum value as well asNone. A host that casts a bad int gets silence rather than an error. - No new hot-path cost.
TrySetStoredModeand the input-generation guards sit off the parse and print paths.
| // KAM locks the keyboard, which a program can do to a user but not undo for them if it | ||
| // dies -- so it is refused unless the host opts in, and stays reported reset, as Ghostty | ||
| // does. RM 2 is always honoured: unlocking is never the risk. | ||
| if (value && !_terminal.Options.KeyboardActionModeAllowed) |
There was a problem hiding this comment.
Medium: refusing SM 2 by default regresses DECRQM conformance against xterm and esctest, and the test that encoded it was removed rather than reconciled.
With the default options, CSI 2 h followed by CSI 2 $ p now answers CSI 2;2 $ y. Before this PR it answered CSI 2;1 $ y. esctest2 decrqm.py:
def test_DECRQM_ANSI_KAM(self):
self.doModifiableAnsiTest(esccmd.KAM)
...
esccmd.SM(mode)
AssertEQ(self.requestAnsiMode(mode), [mode, 1])That now fails on a default terminal. xterm, the grading key elsewhere in this repo (see the DECARM and DECKPM comments in this file), sets MODE_KAM on SM 2 and reports it through DECRQM. RequestModeTests.Stored_ansi_modes_toggle_and_report had [InlineData(2)] precisely because aca3941 made KAM reportable for esctest. This PR deletes that case instead of keeping it green. CLAUDE.md lists this pattern under "things that have bitten twice": check whether an existing test already encodes the behaviour.
The safety goal does not require lying to DECRQM. The pre-PR behaviour was "stored and reported, not acted on", and that is what a disallowed KAM can keep doing:
if (!isPrivate && mode == 2)
_terminal.KeyboardLocked = value && _terminal.Options.KeyboardActionModeAllowed;
table[mode] = value;Done that way, a default terminal still cannot be locked, esctest stays green, and [InlineData(2)] can go back into the theory. If Ghostty's "report reset" is the deliberate choice, say so in the PR body. It is a user-visible change to the default path, not an opt-in.
There was a problem hiding this comment.
Agreed, fixed in c4184b5. SM 2 always stores and reports, as xterm does. KeyboardLocked is your expression, but read live: mode 2 is set AND KeyboardActionModeAllowed is on.
- A default terminal still can't be locked.
- Because it's live, turning the option off unlocks at once, which is the host's way out for a user that a dead program left locked.
- Turning the option on while mode 2 is set locks at once.
Both are documented and tested. [InlineData(2)] is restored in Stored_ansi_modes_toggle_and_report.
| /// sends nothing; a host that encodes its own input should check this too. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Only ever true when <see cref="TerminalOptions.KeyboardActionModeAllowed"/> is: otherwise |
There was a problem hiding this comment.
Doc: this remark contradicts the option's own documentation and the PR's test.
KeyboardLocked can be true while KeyboardActionModeAllowed is false. TerminalOptions.KeyboardActionModeAllowed says "Turning this off does not unlock a keyboard already locked". KeyboardActionModeTests.Disallowing_after_a_lock_still_lets_RM_2_unlock asserts exactly that state: Options.KeyboardActionModeAllowed = false; Assert.True(terminal.KeyboardLocked);.
Suggest: "Only SM 2 sets it, and SM 2 is refused unless KeyboardActionModeAllowed is on at the time. Turning the option off later does not clear an existing lock. RM 2, RIS and DECSTR clear it."
There was a problem hiding this comment.
Rewritten in c4184b5:
KeyboardLockedis true exactly when mode 2 is set and the option is on.- DECRQM reports mode 2 whatever the option says.
- Turning the option off unlocks; turning it on with mode 2 set locks.
- RM 2, RIS and DECSTR reset it.
The contradicting test is replaced by Turning_the_option_off_unlocks_at_once_and_mode_2_stays_reported.
| /// <summary> | ||
| /// KAM, SRM and LNM are STORED now -- set, reset and read back like any tracked mode, because | ||
| /// esctest (and the programs it stands in for) toggle them and expect the toggle reported. | ||
| /// KAM toggles only when the host allows it -- see KeyboardActionModeTests. |
There was a problem hiding this comment.
Doc: the summary's first two lines still give esctest as the reason KAM is stored and toggled. This PR makes that untrue by default, and the added third line contradicts them.
Either drop KAM from the first sentence and state why it now diverges from esctest's test_DECRQM_ANSI_KAM, or keep KAM reportable when disallowed (see the comment on InputHandler.StoredModes.cs:87) and put [InlineData(2)] back.
There was a problem hiding this comment.
Fixed in c4184b5. KAM is reportable again, so the esctest reason holds for all three modes. [InlineData(2)] is back, and the added line now says KAM is reported whether or not the host lets it lock, pointing to KeyboardActionModeTests.
Review of tomlm#181: refusing SM 2 by default made DECRQM report mode 2 reset after a set, which fails esctest's test_DECRQM_ANSI_KAM, and the RequestModeTests case that pinned it had been deleted instead of kept. KAM is now always stored and reported. KeyboardLocked is derived live as "mode 2 set AND KeyboardActionModeAllowed", so a default terminal still cannot be locked, turning the option off unlocks at once (the host's way out for a user a dead program left locked), and turning it on while mode 2 is set locks. RM 2, RIS and DECSTR reset mode 2 and the lock with it. [InlineData(2)] is back in Stored_ansi_modes_toggle_and_report, and the docs on KeyboardLocked, KeyboardActionModeAllowed and the test summary now describe that behaviour. Also from the review: ColorSpec.Format(int, ColorReportFormat) is silent only for an explicit None; an out-of-range value gets the sixteen-bit default instead of no reply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of tomlm#181: refusing SM 2 by default made DECRQM report mode 2 reset after a set, which fails esctest's test_DECRQM_ANSI_KAM, and the RequestModeTests case that pinned it had been deleted instead of kept. KAM is now always stored and reported. KeyboardLocked is derived live as "mode 2 set AND KeyboardActionModeAllowed", so a default terminal still cannot be locked, turning the option off unlocks at once (the host's way out for a user a dead program left locked), and turning it on while mode 2 is set locks. RM 2, RIS and DECSTR reset mode 2 and the lock with it. [InlineData(2)] is back in Stored_ansi_modes_toggle_and_report, and the docs on KeyboardLocked, KeyboardActionModeAllowed and the test summary now describe that behaviour. Also from the review: ColorSpec.Format(int, ColorReportFormat) is silent only for an explicit None; an out-of-range value gets the sixteen-bit default instead of no reply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks. All taken in c4184b5:
I've updated the PR description to match. |
A host showing a "keyboard locked" indicator had to poll KeyboardLocked. Now Terminal raises KeyboardLockedChanged (EventHandler of TerminalEvents.KeyboardLockedEventArgs, carrying Locked) when the effective value changes, and only then. - SM 2, RM 2, RIS and DECSTR go through KeyboardActionMode, whose setter now ignores a write of the current value and raises only while the option allows the lock. Raised on the thread feeding Write. - KeyboardActionModeAllowed gets a backing field and an internal KeyboardActionModeAllowedChanged hook, installed by the Terminal on its own snapshot like ScrollbackChanged and TabStopWidthChanged. A flip raises only while mode 2 is set, on the thread that set the option. - Dispose clears the event and the options hook with their siblings, since the options snapshot can outlive the terminal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4dcae26 to
c4184b5
Compare
|
One addition since your review, in c0a37f6: The earlier commits are unchanged in content. Their messages were reworded, so the hashes in the replies above now read c4184b5 and friends. The description has a new section on it. |
…(KAM) (#197) * Respect Terminal.KeyboardLocked (KAM) in the control's own input encoding XTerm.NET (tomlm/XTerm.NET#181) gates its own key, char and Kitty generators and Terminal.Paste while an application holds Keyboard Action Mode (CSI 2 h). The view encodes much of the keyboard's output itself -- Win32 input records, AltGr and composed characters, IME commits, the classic and bracketed paste, the selection deletion, the ESC-b/ESC-f and Cmd+arrow aliases -- and all of that went straight past the lock. One helper, IsKeyboardLocked, read at the moment of encoding (there is no change event), gates the keyboard entry points: OnKeyDown after the view's local gestures and copy, OnKeyUp, OnTextInput (which also carries IME commits), PasteAsync at the point of sending, CutAsync and SelectInputAsync. A locked keystroke is consumed so Tab cannot move focus away; legacy Meta chords and bare modifiers still bubble as before. SendToPtyAsync is deliberately not gated: mouse reports, focus events and the emulator's replies share it and keep flowing, as in xterm. SendInputAsync is the host's channel, not the user's keyboard, and bypasses the lock; its XML doc says so. Depends on tomlm/XTerm.NET#181 (Terminal.KeyboardLocked, TerminalOptions.KeyboardActionModeAllowed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keyboard lock: finish pre-lock keystrokes, drop stale deletions, keep selection Review fixes for the KAM gate. - A release whose press went out before the lock is sent: it finishes a keystroke the application already saw. Kitty presses are already tracked in _keysHeld; Win32 key-downs are now tracked in _win32KeysDown. The emulator's Kitty generator answers null for everything while locked, so a locked release is encoded with KittyKeyboard.Evaluate, which is all that generator does for a release. Releases of presses the lock dropped are still dropped. - The locked TextInput return clears _pendingReplaceKeys, so a deletion taken by a key-down just before the lock landed cannot ride out with the next composed text after the unlock. - CanCut asks IsKeyboardLocked, as CutAsync does, so a locked Cmd+X is left to the host instead of being claimed and cutting nothing. - Keyboard selection (TryExtendKeyboardSelection) runs above the lock: it only moves the selection on the view. The guard's comment now says what is above it and what it stops below it, the standard paste chords included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Tom Laird-McConnell <thermous@iciclecreek.com>
Three small opt-in options, for parity with Ghostty's
enquiry-response,osc-color-report-formatandvt-kam-allowed. Each feature is its own commit, and review fixes follow as separate commits. Every default keeps today's behaviour, including DECRQM reporting KAM as xterm and esctest expect.TerminalOptions.EnquiryResponse(ENQ answerback)HandleExecutedropped ENQ (0x05). When this string is non-empty, ENQ replies with it. The reply is sent throughRaiseInputData, notRaiseDataReceived, because the answerback is the user's literal string: the S8C1T converter would otherwise rewrite a leading ESC in it as one of the terminal's own reports. The case widens the execute switch's jump table, so LF, CR and BS pay no extra compare.TerminalOptions.ColorReportFormat(None, EightBit, SixteenBit; default SixteenBit)Every OSC colour reply (OSC 4
?, the OSC 4/5 special colours, OSC 10/11/12) now goes through oneReplyColorhelper:rgb:rrrr/gggg/bbbb, as before.rgb:rr/gg/bb.Only an explicit
Noneis silent. A value outside the enum falls back to the sixteen-bit default.TerminalOptions.KeyboardActionModeAllowed(default false) andTerminal.KeyboardLockedKAM (ANSI mode 2) used to be recorded but never acted on.
test_DECRQM_ANSI_KAMexpects. This is unchanged frommain.KeyboardLockedis true exactly when mode 2 is set andKeyboardActionModeAllowedis on. While it is locked,GenerateKeyInput,GenerateCharInputandGenerateKittyKeyInputproduce nothing, andPastesends nothing. Hosts that encode their own input should checkKeyboardLockedtoo.A default terminal therefore can't be locked, but still reports KAM faithfully.
Terminal.KeyboardLockedChangedA host showing a "keyboard locked" indicator no longer has to poll.
KeyboardActionModeAllowed. Nothing fires for SM 2 while the option is off, SM 2 when already locked, or a flip while mode 2 is reset.Write; for an option flip, the thread that set it.SynchronizedOutputChanged/SynchronizedOutputEventArgs. Option flips reach it through an internal hook installed likeScrollbackChanged, andDisposeclears both.Tests
New
EnquiryResponseTests,ColorReportFormatTests,KeyboardActionModeTestsandKeyboardLockedChangedTests. KAM's case is kept inRequestModeTests.Stored_ansi_modes_toggle_and_report. The wholeXTerm.NET.Testsproject passes: 2386 passed, 1 skipped (the existing DA3 skip), 0 failed. Mutants that raise on every write of the mode or the option are each caught.🤖 Generated with Claude Code