diff --git a/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md b/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md index 83f9b9012..e7fb2945a 100644 --- a/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md +++ b/.agent/plans/eterm_kitty_keyboard_protocol_implementation_plan.md @@ -7,7 +7,13 @@ - **Primary module:** `src/modules/eterm` - **Primary goal:** Implement Kitty keyboard protocol progressive enhancement in eTerm so modern terminal applications can distinguish key combinations that legacy terminal encoding collapses, especially `Ctrl+Enter`, `Shift+Enter`, modified Escape, modified Tab, modified Backspace, and modified printable keys. - **Initial motivating application:** OpenAI Codex CLI, which uses Crossterm keyboard enhancement flags. -- **Expected result for the motivating case:** When Codex enables the Kitty keyboard protocol and the user presses `Ctrl+Enter`, eTerm must send `CSI 13;5u` (`ESC [ 1 3 ; 5 u`) instead of the same carriage-return byte sent for plain Enter. +- **Expected result for the motivating case:** When an application enables + `REPORT_ALL_KEYS_AS_ESCAPE_CODES` and the user presses `Ctrl+Enter`, eTerm must send + `CSI 13;5u` (`ESC [ 1 3 ; 5 u`) instead of the same carriage-return byte sent for plain Enter. + `DISAMBIGUATE_ESCAPE_CODES` alone deliberately preserves legacy Enter, Tab, and Backspace + encodings. If Codex continues requesting only flags `7`, distinguishable `Ctrl+Enter` cannot be + promised without a documented protocol deviation or an application-side change to request bit + `8`. - **Compatibility requirement:** When no application has enabled the protocol, eTerm must preserve its existing legacy key behavior byte-for-byte. --- @@ -26,7 +32,7 @@ The proper fix is not an unconditional special case for Codex. eTerm must implem 1. Parse requests from the child application to query, push, set, or pop keyboard enhancement flags. 2. Store enhancement state per terminal screen, with stack behavior. -3. Reply to support queries. +3. Reply to current-state queries only once enhanced key encoding is operational. 4. Encode keyboard events according to the active flags. 5. Continue using existing legacy mappings when the protocol is inactive. 6. Support press, repeat, and release events when requested. @@ -38,8 +44,9 @@ The implementation should be split into four logical pieces: - **Protocol/state model** in `TerminalEmulator` - **CSI command parsing and responses** in `TerminalEmulator::csihandle()` -- **Key event encoding** in a focused encoder class/helper used by `TerminalDisplay` -- **Input event plumbing** in `UITerminal` / `TerminalDisplay`, including key-up and repeat support +- **Key event encoding** in a focused encoder class/helper invoked on the terminal worker +- **Input event plumbing** from `UITerminal` / `TerminalDisplay` into typed `TerminalSession` + commands, including key-up and repeat support Do not mix the new protocol rules into the existing large `keys[]` table. Keep the legacy table intact as a fallback and add a separate enhanced encoder that runs before it when active. @@ -178,30 +185,42 @@ Codex has used a combination including disambiguation, event types, and alternat ## 4.1 Event flow -The current flow is approximately: +eTerm now has a dedicated `TerminalSession` worker. The worker exclusively owns the emulator, +PTY, process, parser, terminal modes, history, and cursor. UI code may enqueue commands, drain +events, and retain immutable snapshots, but it must never read mutable emulator state. + +The current keyboard path still pre-encodes legacy bytes on the UI thread: ```text EEPP Window/Input backend -> UI event dispatcher - -> UITerminal::onKeyDown(KeyEvent) - -> TerminalDisplay::onKeyDown(keyCode, char, mod, scancode) - -> terminal shortcuts - -> Ctrl-letter handling - -> legacy TerminalKeyMap - -> PTY ttywrite() + -> UITerminal / TerminalDisplay + -> terminal-local shortcuts + -> Ctrl-letter and legacy TerminalKeyMap encoding + -> TerminalSession::write(encoded bytes) + -> ordered worker command + -> PTY ttywrite() ``` -Text input follows a separate path: +The Kitty implementation must change the child-input portion to: ```text -EEPP text input event - -> UITerminal::onTextInput(TextInputEvent) - -> TerminalDisplay::onTextInput(codepoint) - -> UTF-8 conversion - -> PTY ttywrite() +EEPP Window/Input backend + -> UI event dispatcher + -> UITerminal / TerminalDisplay + -> consume terminal-local shortcuts + -> enqueue semantic KeyDown / KeyUp / TextInput command + -> TerminalSession worker + -> read authoritative Kitty state and terminal modes + -> correlate key and committed-text events + -> enhanced or legacy encoding + -> PTY ttywrite() ``` -This split is important. Printable characters are normally emitted by `onTextInput()`, while special/control keys are emitted by `onKeyDown()`. Kitty’s `REPORT_ALL_KEYS_AS_ESCAPE_CODES` changes this assumption: printable keys may need to be emitted from the key-event path as CSI-u events, and the later text-input event must then be suppressed to prevent duplicate characters. +Printable characters currently arrive through a separate text-input event. Kitty's +`REPORT_ALL_KEYS_AS_ESCAPE_CODES` requires key/text correlation to avoid duplicate input. That +correlation belongs on the worker because the ordered command queue preserves UI event order and +the worker owns the negotiated state. ## 4.2 Existing legacy keyboard map @@ -236,16 +255,18 @@ This is why modified Enter collapses to carriage return. ## 4.3 Existing parser support placeholder -`TerminalEmulator::csihandle()` already has a branch similar to: +The CSI parser has now been characterized and updated. It stores `<`, `=`, `>`, or `?` in +`CSIEscape::priv`, preserves `;`/`:` separators in `CSIEscape::sep`, and places the final byte in +`mode[0]`. Therefore Kitty controls are represented as a prefixed final `u`, for example: -```cpp -case '=': /* Progressive enhancement sequences */ - /* Keyboard protocol ESC[=Nu */ - /* Do nothing for the moment */ - break; +```text +ESC [ > 7 u -> priv='>', arg[0]=7, mode[0]='u' +ESC [ ? u -> priv='?', arg[0]=0, mode[0]='u' ``` -This is a strong signal that progressive enhancement belongs in `TerminalEmulator`, not in the UI widget. +`TerminalEmulator::csihandle()` currently recognizes all four prefixed `u` variants and silently +ignores them. It intentionally does not answer `CSI ? u`, because a response would claim support +before enhanced input encoding exists. Bare `CSI u` continues to mean DECRC cursor restore. The CSI parser stores: @@ -260,7 +281,7 @@ struct CSIEscape { }; ``` -Before implementation, verify exactly how these sequences are represented after `csiparse()`: +The existing parser regression test covers these exact byte strings: ```text ESC [ ? u @@ -273,7 +294,10 @@ ESC [ = 7 ; 2 u ESC [ = 7 ; 3 u ``` -Do not assume that `?`, `>`, `<`, and `=` always land in the same field. The current `csihandle()` switch structure suggests some private/intermediate bytes may be placed into `mode[0]` or `priv`. Add parser tests first and adjust the handler according to observed representation. +Extend that test into state/response assertions when negotiation is implemented. One remaining +detail is that the parser currently represents an omitted numeric parameter as zero. If exact +semantics require distinguishing omitted `CSI < u` from explicit `CSI < 0 u`, add argument-presence +metadata rather than inferring presence from `narg`. ## 4.4 Screen swapping @@ -304,10 +328,10 @@ src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp ``` -Possible test file: +Use the repository's existing unit-test file: ```text -src/modules/eterm/tests/kittykeyboardprotocol_test.cpp +src/tests/unit_tests/eterm_tests.cpp ``` Adapt to the repository’s actual test layout and build system. @@ -420,7 +444,7 @@ Names may be adjusted to project style, but maintain clear separation between: - output bytes, - duplicate-text suppression. -## 5.2 Keep protocol state owned by `TerminalEmulator` +## 5.2 Keep protocol state and encoding on the worker `TerminalEmulator` receives application output and therefore owns negotiation state. @@ -431,14 +455,7 @@ KittyKeyboardState mPrimaryKeyboardState; KittyKeyboardState mAlternateKeyboardState; ``` -Add public read-only accessors and event-state operations: - -```cpp -std::uint32_t getKeyboardEnhancementFlags() const; -bool hasKeyboardEnhancementFlag( KittyKeyboardFlag flag ) const; -``` - -Potentially add: +Potentially add private worker-only helpers: ```cpp const KittyKeyboardState& getKeyboardState() const; @@ -447,7 +464,11 @@ KittyKeyboardState& getKeyboardState(); Keep mutating access private if possible. -`TerminalDisplay` should only query active flags. It must not parse application control sequences or mutate protocol stacks directly. +Do not add a UI-facing `getKeyboardEnhancementFlags()` accessor. `TerminalDisplay` must not read +the emulator or use snapshot flags to decide how to encode input: snapshots can lag negotiation. +Instead add typed key and text commands to `TerminalSession`; their worker-side handlers query the +active state and invoke the encoder. Active flags may be copied into `TerminalSnapshot` for +diagnostics only. ## 5.3 Consider moving `sanitizeMod()` @@ -485,7 +506,10 @@ DisambiguateEscapeCodes Add `ReportAlternateKeys` only if EEPP can reliably supply shifted/base-layout key values. Add `ReportAssociatedText` only when the implementation can correlate committed text with the originating key event without duplication or loss. -However, for Codex compatibility, determine whether Crossterm requires the terminal to echo all requested bits or merely accepts the subset returned by the query. The preferred behavior is standards-compliant subset support, not falsely claiming unsupported features. +When an application sets or pushes flags, mask the request to features eTerm can honor. A later +current-state query reports the effective active subset, allowing applications to observe which +requested bits took effect. Do not treat the initial query as capability advertisement and do not +retain unsupported bits as active. A safe first complete target is to implement all five flags to the degree required by the spec, with documented backend limitations. @@ -497,7 +521,8 @@ When the child sends: CSI ? u ``` -eTerm must respond through the PTY input channel with: +eTerm must respond through the PTY input channel with the current effective flags for the active +screen: ```text CSI ? u @@ -509,24 +534,29 @@ For example: ESC [ ? 31 u ``` -if all five flags are supported. +if all five flags are currently active. A newly initialized terminal responds with `CSI ? 0 u` +even if it implements every enhancement. This is a state query, not a supported-capability mask. Use `ttywrite()` or the appropriate low-level response method with echo disabled. Follow existing device-status response patterns. -Suggested helper: +Suggested worker-only helper: ```cpp -void TerminalEmulator::replyKeyboardEnhancementFlags() { +void TerminalEmulator::replyKeyboardEnhancementState() { char buf[32]; const int len = std::snprintf( buf, sizeof( buf ), "\033[?%uu", - KITTY_KEYBOARD_SUPPORTED_FLAGS + activeKeyboardState().flags ); ttywrite( buf, static_cast( len ), 0 ); } ``` -Important: verify from the official spec whether the query response reports **supported flags** or **currently active flags**. Implement exactly the current spec. Do not infer from naming. Add a test matching Kitty and Crossterm behavior. +Applications detect protocol support by sending the state query followed by primary device +attributes. No Kitty response before DA means unsupported. To discover whether requested bits took +effect, an application sets/pushes them and then queries the resulting current state. Keep a +supported mask internally for safe flag application, but never return that mask in place of the +active state. ## 6.3 Push semantics @@ -602,15 +632,11 @@ The protocol includes: CSI = flags ; mode u ``` -The `mode` controls how `flags` affects current state. Implement all standardized modes exactly. +The optional `mode` defaults to `1` and has these standardized meanings: -Expected conceptual operations are generally: - -- replace/set absolute flags, -- set/add bits, -- clear/remove bits. - -Do not hardcode these assumptions without checking the current official table. +- `1`: replace the current state; set specified bits and reset all unspecified bits, +- `2`: set the specified bits and leave unspecified bits unchanged, +- `3`: reset the specified bits and leave unspecified bits unchanged. Implement through one function: @@ -623,13 +649,13 @@ void KittyKeyboardState::set( requestedFlags & KITTY_KEYBOARD_SUPPORTED_FLAGS; switch ( mode ) { - case /* set */: + case 1: flags = supported; break; - case /* OR */: + case 2: flags |= supported; break; - case /* clear */: + case 3: flags &= ~supported; break; default: @@ -685,9 +711,12 @@ Do not leave enhancement flags active when a new shell is spawned in the same te # 7. CSI parser implementation -## 7.1 Add parser characterization tests first +## 7.1 Extend the existing parser characterization test -Before modifying `csihandle()`, create tests that feed these exact byte strings into `TerminalEmulator`: +The existing `modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol` test already feeds +the representative query/push/pop/set strings into `TerminalEmulator` and verifies that the +unsupported implementation stays silent. Preserve it until negotiation and encoding land +together, then extend it to assert state changes and responses: ```cpp "\033[?u" @@ -701,7 +730,7 @@ Before modifying `csihandle()`, create tests that feed these exact byte strings "\033[=1;3u" ``` -Expose or instrument parsed `CSIEscape` only in tests if needed. Capture: +The established parser representation is: - `priv` - `arg[]` @@ -710,7 +739,7 @@ Expose or instrument parsed `CSIEscape` only in tests if needed. Capture: - `mode[1]` - raw buffer -This prevents implementing against an incorrect mental model of the inherited `st` parser. +Do not add test-only parser exposure unless a future parser change requires it. ## 7.2 Add a dedicated handler @@ -798,7 +827,8 @@ constexpr unsigned int MAX_POP_COUNT = 64; The control sequence is application output; the response must be terminal input. -Use the same response path as device status reports. Ensure: +Handle and answer the query on the `TerminalSession` worker, using the same response path as device +status reports. Ensure: - no local echo, - no screen rendering, @@ -807,7 +837,9 @@ Use the same response path as device status reports. Ensure: ## 7.6 Debug tracing -Add optional protocol tracing controlled by a compile-time flag or existing logger level: +Use the existing `ETERM_LOG_ERRORS` diagnostics switch for malformed protocol diagnostics. If +verbose per-key tracing is later needed, give it a distinct trace-level switch so +`ETERM_LOG_ERRORS=1` does not log every key: ```text eterm kitty-kbd: query -> response flags=31 @@ -822,6 +854,12 @@ Do not log every key at normal log levels. # 8. Key event model and EEPP plumbing +The UI thread may identify terminal-local shortcuts and normalize EEPP event data, but it must +enqueue semantic events rather than encoded bytes. Add `KeyDownCommand`, `KeyUpCommand`, and +`TextInputCommand` to `TerminalSession`. The worker performs event correlation, reads the active +screen's Kitty state and terminal modes, chooses enhanced versus legacy encoding, and writes to +the PTY. + ## 8.1 Add key-up forwarding Current `UITerminal::onKeyUp()` returns `1` without forwarding: @@ -832,7 +870,7 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& ) { } ``` -To implement release reporting: +To implement release reporting, forward a semantic release event into `TerminalSession`: ```cpp Uint32 UITerminal::onKeyUp( const KeyEvent& event ) { @@ -850,7 +888,7 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& event ) { Add `TerminalDisplay::onKeyUp()`. -Only emit a release sequence when: +The worker only emits a release sequence when: ```cpp activeFlags & REPORT_EVENT_TYPES @@ -1078,11 +1116,11 @@ Do not invent code values. Copy them from the official key-code table and add a ## 10.3 Enter special case -With `DISAMBIGUATE_ESCAPE_CODES` enabled: +With only `DISAMBIGUATE_ESCAPE_CODES` enabled, Enter remains a legacy exception regardless of +modifiers. The same exception applies to Tab and Backspace, allowing a user to type `reset` if an +application crashes without restoring modes. -- Plain Enter may retain legacy `\r` unless another active flag requires CSI-u. -- Modified Enter must be encoded as CSI-u. -- Ctrl+Enter must become: +With `REPORT_ALL_KEYS_AS_ESCAPE_CODES` enabled, Enter is encoded as CSI-u. Ctrl+Enter becomes: ```text CSI 13 ; 5 u @@ -1108,7 +1146,9 @@ Ctrl+Shift+Enter: CSI 13 ; 6 u ``` -When `REPORT_ALL_KEYS_AS_ESCAPE_CODES` is active, plain Enter should also use the protocol form required by the spec. +Plain Enter also uses the protocol form when report-all is active. Event-type fields are included +only when requested. Tests and acceptance criteria must not expect `CSI 13;5u` from disambiguation +alone. ## 10.4 Legacy cursor/function-key forms under enhancement @@ -1205,7 +1245,8 @@ This feature should be implemented only after input-event correlation is sound. # 12. Progressive enhancement decision algorithm -Implement one deterministic function that decides enhanced versus legacy output. +Implement one deterministic function that decides enhanced versus legacy output. It is invoked by +the `TerminalSession` worker, never by rendering/UI code. Pseudo-code: @@ -1254,7 +1295,9 @@ KittyEncodedKey KittyKeyboardEncoder::encode( } ``` -The exact `mustEncode` rules must match the spec. The above is architecture, not a substitute for the official rules. +The exact `mustEncode` rules must match the spec. In particular, Enter, Tab, and Backspace remain +legacy under disambiguation alone. The above is architecture, not a substitute for the official +rules. --- @@ -1273,9 +1316,10 @@ TextInput("a") If Kitty `REPORT_ALL_KEYS_AS_ESCAPE_CODES` causes `KeyDown(A)` to emit a CSI-u sequence and `TextInput("a")` still emits `a`, the child receives the key twice. -## 13.2 Preferred solution: correlated suppression queue +## 13.2 Preferred solution: worker-side correlated suppression queue -Add a small queue of expected text-input codepoints after an enhanced keydown consumed the event: +Add a small queue of expected text-input codepoints after an enhanced keydown consumed the event. +Keep this queue beside the worker-side encoder, not in `TerminalDisplay`: ```cpp struct SuppressedTextInput { @@ -1308,7 +1352,7 @@ For printable keys under `REPORT_ALL_KEYS_AS_ESCAPE_CODES`: This is more correct for keyboard layouts and IME but requires careful event ordering and latency handling. -Recommended architecture: +Recommended worker-side architecture: ```cpp std::optional mPendingTextKey; @@ -1357,16 +1401,11 @@ Do not ship an implementation that duplicates printable characters. # 14. Changes to `TerminalDisplay` -## 14.1 Add active flag accessor +## 14.1 Do not expose active flags to the display -Use: - -```cpp -const auto flags = - mTerminal->getKeyboardEnhancementFlags(); -``` - -Do not cache flags in `TerminalDisplay`; application output may change them at any moment. +`TerminalDisplay` must not query `TerminalEmulator`, and a flag value copied through +`TerminalSnapshot` is not authoritative enough for input encoding. The display handles local UI +shortcuts and forwards the remaining semantic event to `TerminalSession`. ## 14.2 Refactor `onKeyDown()` @@ -1388,7 +1427,7 @@ void TerminalDisplay::onKeyDown( if ( handleTerminalShortcut( keyCode, smod ) ) return; - const KittyKeyEvent event{ + const TerminalKeyEvent event{ keyCode, scancode, chr, @@ -1398,33 +1437,17 @@ void TerminalDisplay::onKeyDown( : KittyKeyEventType::Press }; - const auto enhanced = KittyKeyboardEncoder::encode( - event, - mTerminal->getKeyboardEnhancementFlags() - ); - - if ( enhanced.handled ) { - mTerminal->ttywrite( - enhanced.bytes.data(), - enhanced.bytes.size(), - 1 - ); - - if ( enhanced.suppressTextInput ) - registerTextInputSuppression( event ); - - return; - } - - handleLegacyKeyDown( keyCode, chr, mod, scancode ); + mSession->keyDown( std::move( event ) ); } ``` -Extract current shortcut and legacy map logic into helpers to make tests possible: +Extract current shortcut logic into a helper. Move the child-facing Ctrl conversion and legacy +key map into a worker-side encoder/fallback so both legacy and enhanced output use authoritative +terminal modes: ```cpp bool handleTerminalShortcut(...); -void handleLegacyKeyDown(...); +KittyEncodedKey encodeChildKeyEvent(...); // worker only ``` ## 14.3 Add `onKeyUp()` @@ -1436,33 +1459,20 @@ void TerminalDisplay::onKeyUp( const Uint32& mod, const Scancode& scancode ) { - const auto flags = - mTerminal->getKeyboardEnhancementFlags(); - - if ( !( flags & REPORT_EVENT_TYPES ) ) - return; - - KittyKeyEvent event{ + TerminalKeyEvent event{ keyCode, scancode, chr, mod, KittyKeyEventType::Release }; - - const auto encoded = - KittyKeyboardEncoder::encode( event, flags ); - - if ( encoded.handled ) - mTerminal->ttywrite( - encoded.bytes.data(), - encoded.bytes.size(), - 1 - ); + mSession->keyUp( std::move( event ) ); } ``` -Terminal shortcuts consumed on keydown should generally not emit release events to the child. Track consumed physical keys until key-up if necessary: +The worker emits a release only if the negotiated flags require it. Terminal shortcuts consumed +on keydown should generally not emit release events to the child. Track consumed physical keys on +the UI side until key-up if necessary: ```cpp std::unordered_set mLocallyConsumedKeys; @@ -1474,17 +1484,15 @@ Refactor: ```cpp void TerminalDisplay::onTextInput( const Uint32& chr ) { - if ( !mTerminal ) + if ( !mSession ) return; - - if ( handlePendingKittyTextInput( chr ) ) - return; - - // Existing UTF-8 legacy behavior. + mSession->textInput( chr ); } ``` -Ensure a multi-codepoint text-input event is handled correctly. Current API appears to pass one `Uint32`; inspect whether EEPP emits one event per codepoint. +The worker correlates this command with pending key events and chooses enhanced versus legacy +output. Ensure a multi-codepoint text-input event is handled correctly. Current API appears to +pass one `Uint32`; inspect whether EEPP emits one event per codepoint. --- @@ -1494,6 +1502,7 @@ Required changes: - Forward key-up. - Forward repeat status if `KeyEvent` exposes it. +- Forward semantic key/text data through `TerminalSession`; never read emulator state. - Preserve focus checks. - Preserve custom keybinding command interception. - Clear pending Kitty input state on focus loss if there is an existing focus callback. @@ -1525,8 +1534,8 @@ Update interfaces and all callers. When active flags are zero: -- `TerminalDisplay::onKeyDown()` must execute the old code path. -- `onTextInput()` must behave exactly as before. +- semantic key/text commands must produce the same bytes as the old `TerminalDisplay` path, +- worker-side legacy text input must behave exactly as before, - `onKeyUp()` must emit nothing. - Existing `Ctrl+A` through control-code conversion must remain unchanged. - Alt+Enter must retain `ESC CR`. @@ -1540,7 +1549,9 @@ Add regression tests comparing old expected output bytes. # 17. Handling Ctrl-letter logic -Current code manually maps Ctrl plus certain scancodes to bytes `0x01` etc. This block must occur **after** enhanced protocol encoding. +Current code manually maps Ctrl plus certain scancodes to bytes `0x01` etc. Move this child-facing +logic from `TerminalDisplay` into the worker-side encoder/fallback, after enhanced protocol +encoding. Reason: @@ -1564,10 +1575,12 @@ Do not leave the Ctrl block before the enhanced encoder. Distinguish: - Child output is parsed by `TerminalEmulator`. -- Terminal responses and user input are written to the PTY. +- Terminal responses and semantic input commands are encoded on the worker and written to the PTY. - Renderer output must never contain protocol responses. - `ttywrite(..., may_echo=0)` is likely correct for terminal-generated responses. - User key input should maintain current `may_echo` semantics, likely `1`. +- UI code must never call `TerminalEmulator::ttywrite()` directly or base encoding on a potentially + stale snapshot. Audit current uses: @@ -1703,20 +1716,23 @@ Enter -> 0d Ctrl+Enter -> 0d // current legacy behavior retained ``` -With disambiguation: +With disambiguation but without report-all: ```text -Enter -> legacy CR, unless spec requires encoded form -Ctrl+Enter -> 1b 5b 31 33 3b 35 75 -Shift+Enter -> 1b 5b 31 33 3b 32 75 -Alt+Enter -> protocol form required by spec -Ctrl+Shift+Enter -> protocol form with modifier 6 +Enter -> legacy CR +Ctrl+Enter -> legacy CR +Shift+Enter -> legacy CR +Alt+Enter -> legacy ESC CR +Ctrl+Shift+Enter -> legacy CR ``` With report-all: ```text -Enter -> enhanced form +Enter -> enhanced form +Ctrl+Enter -> 1b 5b 31 33 3b 35 75 +Shift+Enter -> 1b 5b 31 33 3b 32 75 +Ctrl+Shift+Enter -> protocol form with modifier 6 ``` With event types: @@ -1817,10 +1833,10 @@ Enter -> 0d Ctrl+Enter -> 0d ``` -Use a helper to activate flags: +Use a helper to activate report-all (which implies disambiguation): ```bash -printf '\033[>1u' +printf '\033[>8u' ``` Then press Ctrl+Enter and verify: @@ -1850,23 +1866,26 @@ ESC [ ? u and read: ```text -ESC [ ? u +ESC [ ? u ``` ## 21.3 Codex CLI 1. Build and run eTerm from the modified `develop` branch. -2. Start current Codex CLI. -3. Configure Codex: +2. Start current Codex CLI and capture its push/set sequence. +3. Verify Codex requests `REPORT_ALL_KEYS_AS_ESCAPE_CODES` (bit `8`). If it requests only flags + `7`, record that standards-compliant `Ctrl+Enter` disambiguation is unavailable and do not make + the terminal silently deviate from Kitty. +4. Configure Codex: - Enter inserts newline. - Ctrl+Enter submits. -4. Type a multiline prompt. -5. Press Enter: newline appears. -6. Press Ctrl+Enter: message submits. -7. Hold/repeat keys to ensure no duplicate or stuck events. -8. Exit Codex normally. -9. Verify shell input returns to legacy behavior. -10. Kill Codex abruptly and verify a restarted shell/process does not inherit stale flags. +5. Type a multiline prompt. +6. Press Enter: newline appears. +7. Press Ctrl+Enter: message submits when report-all is active. +8. Hold/repeat keys to ensure no duplicate or stuck events. +9. Exit Codex normally. +10. Verify shell input returns to legacy behavior. +11. Kill Codex abruptly and verify a restarted shell/process does not inherit stale flags. ## 21.4 Other TUIs @@ -1921,25 +1940,23 @@ Use constants and comments. # 23. Threading and lifetime -Determine which thread: +The thread model is established: -- parses PTY output, -- handles UI input, -- reads active flags. +- the `TerminalSession` worker exclusively owns `TerminalEmulator`, PTY reads/writes, parser state, + terminal modes, Kitty flags/stacks, pending key/text correlation, and child-input encoding; +- the UI thread handles terminal-local shortcuts and enqueues semantic commands; +- immutable snapshots are presentation-only and must not drive input encoding; +- no emulator accessor, atomic flag mirror, or mutex-protected stack is needed. -If parser updates and UI reads can occur on different threads, `flags` access is a data race. +Typed key and text commands participate in the existing ordered command queue. Add deterministic +tests for negotiation and key commands arriving close together. The worker must give already +available PTY negotiation a fair bounded parsing opportunity without allowing heavy output to +starve input commands. Query/response capability detection is also an ordering barrier: an +application cannot observe the Kitty query response before the worker has applied the state that +produced it. -Options: - -1. Confirm all terminal update/input operations run on the main thread. -2. If not, use: - - atomic active flags, - - mutex around stack mutation, - - or message passing. - -Stacks are modified rarely; a mutex is acceptable. Do not add atomics without protecting the vectors. - -Document the threading guarantee in code. +Reset both screen states in full reset and explicitly in `RestartCommand` so a replacement process +cannot inherit flags or pending input correlation from the old child. --- @@ -2019,12 +2036,13 @@ allowing modern TUIs to distinguish modified Enter and other keys. Deliverables: -- Parser tests for private CSI prefixes +- Parser tests for private CSI prefixes (**completed**; prefixed Kitty sequences are currently + recognized and deliberately ignored without a response) - Legacy byte snapshots - Confirm keydown/textinput ordering - Confirm repeat exposure - Confirm modifier constants -- Confirm thread model +- Confirm thread model (**completed**; emulator and protocol state are worker-owned) No functional change. @@ -2038,6 +2056,7 @@ Deliverables: - Push/pop/set - Reset behavior - Unit tests +- Typed semantic key/text `TerminalSession` commands, initially using legacy worker-side encoding Still no enhanced key emission. @@ -2048,7 +2067,8 @@ Deliverables: - Encoder framework - Enter, Tab, Escape, Backspace - Modified key CSI-u emission -- Ctrl+Enter works in Crossterm/Codex +- Modified ASCII/Escape disambiguation works in Crossterm +- Ctrl+Enter remains legacy unless report-all is active - Legacy fallback intact This phase can be merged independently if desired. @@ -2121,9 +2141,10 @@ The implementation is complete only when all of the following are true. ## Encoding - [ ] Plain legacy behavior is unchanged with flags zero. -- [ ] Ctrl+Enter is `CSI 13;5u` when disambiguation is active. -- [ ] Shift+Enter is distinguishable. -- [ ] Modified Escape, Tab, and Backspace are distinguishable. +- [ ] Ctrl+Enter is `CSI 13;5u` when report-all is active. +- [ ] Shift+Enter is distinguishable when report-all is active. +- [ ] Modified Escape is distinguishable under disambiguation; modified Tab and Backspace are + distinguishable when report-all is active. - [ ] Special/function keys follow official Kitty encoding. - [ ] Printable Unicode keys work in report-all mode. - [ ] No duplicate text-input events occur. @@ -2133,7 +2154,8 @@ The implementation is complete only when all of the following are true. ## Integration - [ ] Crossterm decodes the events with correct code/modifier/kind. -- [ ] Codex can bind Ctrl+Enter separately from Enter. +- [ ] Codex can bind Ctrl+Enter separately from Enter when it requests report-all, or any deliberate + compatibility deviation is separately designed and documented. - [ ] Shell, Vim/Neovim, and existing terminal use remain unaffected when protocol is inactive. - [ ] Abrupt child termination does not leave stale protocol state for a replacement process. @@ -2191,16 +2213,33 @@ KittyKeyboardState& activeKeyboardState(); const KittyKeyboardState& activeKeyboardState() const; bool handleKittyKeyboardProtocol(); -void replyKittyKeyboardProtocolSupport(); +void replyKittyKeyboardProtocolState(); void resetKittyKeyboardProtocol(); ``` -Public: +No public emulator accessor is added. State and stack access remain worker-only. + +## `src/modules/eterm/include/eterm/terminal/terminalsession.hpp` + +Add semantic event data and typed commands: ```cpp -std::uint32_t getKeyboardEnhancementFlags() const; +void keyDown( TerminalKeyEvent event ); +void keyUp( TerminalKeyEvent event ); +void textInput( TerminalTextInput event ); + +struct KeyDownCommand { TerminalKeyEvent event; }; +struct KeyUpCommand { TerminalKeyEvent event; }; +struct TextInputCommand { TerminalTextInput event; }; ``` +Add them to the ordered command variant. + +## `src/modules/eterm/src/eterm/terminal/terminalsession.cpp` + +Handle semantic input on the worker. Read the active Kitty state and current emulator modes there, +perform key/text correlation, encode either Kitty or legacy bytes, and call `ttywrite()`. + ## `src/modules/eterm/src/eterm/terminal/terminalemulator.cpp` Modify: @@ -2213,7 +2252,8 @@ Modify: - terminal-generated query response - optional debug tracing -Remove the current no-op placeholder only after replacing it with real handling. +Replace the current prefixed-`u` no-op only after negotiation state and enhanced input encoding are +implemented together. ## `src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp` @@ -2221,20 +2261,18 @@ Add: - `onKeyUp` - repeat parameter or event kind -- pending/suppressed text state - locally consumed key tracking -- helper declarations for enhanced and legacy paths +- helper declarations for terminal-local shortcuts ## `src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp` Modify: -- place enhanced encoding before Ctrl-letter legacy handling - retain terminal shortcut priority - add key-up behavior - add repeat behavior -- add text-input correlation -- preserve legacy map unchanged as fallback +- enqueue semantic key/text commands instead of encoded bytes +- move child-facing Ctrl conversion and the legacy map into the worker-side fallback ## `src/modules/eterm/src/eterm/ui/uiterminal.cpp` @@ -2264,12 +2302,12 @@ Before submitting: 8. Verify release events for locally consumed shortcuts are suppressed. 9. Verify stack depth and pop count are bounded. 10. Verify unsupported bits are masked. -11. Verify query response mask matches actual implementation. +11. Verify the query returns current effective flags, not the supported mask. 12. Verify Unicode scalar validation. 13. Verify AltGr/IME behavior. 14. Verify legacy snapshots with flags zero. 15. Verify Crossterm key dump. -16. Verify Codex Ctrl+Enter. +16. Verify Codex's requested flags before expecting Ctrl+Enter to be distinct. 17. Verify process restart clears stale state. 18. Run formatter and full test suite. @@ -2280,7 +2318,7 @@ Before submitting: A very small patch can solve only the immediate issue: ```cpp -if ( flags & DISAMBIGUATE_ESCAPE_CODES && +if ( flags & REPORT_ALL_KEYS_AS_ESCAPE_CODES && keyCode == KEY_RETURN && mod & KEYMOD_CTRL ) { ttywrite( "\033[13;5u", 7, 1 ); @@ -2288,7 +2326,9 @@ if ( flags & DISAMBIGUATE_ESCAPE_CODES && } ``` -This is **not** the requested final implementation. It may be useful as a temporary proof of concept, but it is incomplete because it omits: +This is **not** the requested final implementation. It is shown only as an illustration of the +report-all requirement and must not be added on the UI thread. A production implementation also +cannot omit: - capability query, - push/pop stack, @@ -2316,13 +2356,13 @@ ESC [ ? u eTerm replies: ```text -ESC [ ? 31 u +ESC [ ? 0 u ``` Application sends: ```text -ESC [ > 7 u +ESC [ > 15 u ``` eTerm stores prior flags on the current screen’s stack and activates: @@ -2331,6 +2371,7 @@ eTerm stores prior flags on the current screen’s stack and activates: DISAMBIGUATE_ESCAPE_CODES REPORT_EVENT_TYPES REPORT_ALTERNATE_KEYS +REPORT_ALL_KEYS_AS_ESCAPE_CODES ``` User presses Ctrl+Enter. @@ -2364,7 +2405,8 @@ KeyModifiers::CONTROL KeyEventKind::Press ``` -Codex matches its Ctrl+Enter submit binding. +An application such as Codex can match its Ctrl+Enter submit binding when it requests report-all. +If it pushes only flags `7`, Enter remains in its legacy form by design. Application exits cleanly and sends: diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index 76904182d..bf8505f11 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -356,6 +356,7 @@ class TerminalDisplay { VertexBufferUniquePtr mVBForeground; std::vector mVBStyles; TerminalColorScheme mColorScheme; + TerminalColorScheme mInitialColorScheme; Uint32 mQuadVertex{ 6 }; Primitives mPrimitives; Vector2u mCurGridPos; diff --git a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp index e99d4197e..1ac34934d 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp @@ -55,7 +55,7 @@ using namespace eterm::System; namespace eterm { namespace Terminal { constexpr int ESC_BUF_SIZ = 512; -constexpr int ESC_ARG_SIZ = 16; +constexpr int ESC_ARG_SIZ = 32; constexpr int STR_BUF_SIZ = ESC_BUF_SIZ; constexpr int STR_ARG_SIZ = ESC_ARG_SIZ; @@ -104,6 +104,9 @@ struct CSIEscape { size_t len; /* raw string length */ char priv; int arg[ESC_ARG_SIZ]; + /* Separator following each argument. ECMA-48 uses ';' between parameters and ':' + * between subparameters, so preserving it is required for modern SGR colors. */ + char sep[ESC_ARG_SIZ]; int narg; /* nb of args */ char mode[2]; }; @@ -251,6 +254,12 @@ class TerminalEmulator final { /** Worker-only maximum interval between snapshots while PTY reads remain saturated. */ void setPresentationInterval( Time interval ); + /** Set the user-configured cursor style restored by DECSCUSR parameter 7. */ + void setDefaultCursorMode( TerminalCursorMode mode ); + + /** Worker-only notification that the display palette changed. */ + void notifyColorSchemeChanged(); + Vector2i getSize() const; System::IProcess* getProcess() const; @@ -285,6 +294,7 @@ class TerminalEmulator final { bool mAllDirty{ true }; Clock mPresentationClock; Time mPresentationInterval{ Microseconds( 1000000.0 / 60.0 ) }; + Clock mSynchronizedUpdateClock; bool mAllowMemoryTrimnming{ false }; int mExitCode; @@ -305,6 +315,9 @@ class TerminalEmulator final { int mAllowAltScreen; int mAllowWindowOps; + TerminalCursorMode mDefaultCursorMode{ SteadyUnderline }; + bool mColorSchemeNotifications{ false }; + int mColorScheme{ 0 }; std::string mCurrentWorkingDirectory; Vector2i mLastMousePosition{ -1, -1 }; @@ -362,7 +375,7 @@ class TerminalEmulator final { void historyStealPush( Line* lineSlot, int col ); void historyReflow( int old_col, int new_col ); void historyPopToScreen( int loaded, int col ); - void tsetattr( int*, int ); + void tsetattr( int*, int, const char* ); void tsetchar( Rune, TerminalGlyph*, int, int ); void tsetdirt( int, int ); void tsetscroll( int, int ); @@ -373,7 +386,7 @@ class TerminalEmulator final { void tcontrolcode( uchar ); void tdectest( char ); void tdefutf8( char ); - int32_t tdefcolor( int*, int*, int ); + int32_t tdefcolor( int*, const char*, int*, int ); void tdeftran( char ); void tstrsequence( uchar ); @@ -413,6 +426,8 @@ class TerminalEmulator final { int xgetcolor( int x, unsigned char* r, unsigned char* g, unsigned char* b ); void osc_color_response( int num, int index, int is_osc4 ); void handleDeviceAttributes(); + int colorScheme(); + void reportColorScheme(); void trimMemory(); diff --git a/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp b/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp index d9fcaae6d..ed8df0c90 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaltypes.hpp @@ -115,6 +115,7 @@ enum TerminalWinMode { MODE_MOUSEMANY = 1 << 15, MODE_BRCKTPASTE = 1 << 16, MODE_NUMLOCK = 1 << 17, + MODE_ALTSCRROLL = 1 << 18, MODE_MOUSE = MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEX10 | MODE_MOUSEMANY, }; diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index ac50d7439..1256fcf1b 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -450,9 +450,8 @@ std::shared_ptr TerminalDisplay::create( std::shared_ptr terminal = std::shared_ptr( new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) ); - terminal->mSession = - TerminalSession::create( std::move( pseudoTerminal ), std::move( process ), historySize, - terminal->makeColorPalette() ); + terminal->mSession = TerminalSession::create( std::move( pseudoTerminal ), std::move( process ), + historySize, terminal->makeColorPalette() ); if ( !terminal->mSession ) { if ( freeProcessFactory ) eeSAFE_DELETE( processFactory ); @@ -484,7 +483,8 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const mFontSize( fontSize ), mSize( pixelsSize ), mUseFrameBuffer( useFrameBuffer ), - mColorScheme( TerminalColorScheme::getDefault() ) { + mColorScheme( TerminalColorScheme::getDefault() ), + mInitialColorScheme( mColorScheme ) { TerminalGlyph defaultGlyph; defaultGlyph.mode = ATTR_INVISIBLE; mCursorGlyph = defaultGlyph; @@ -504,26 +504,33 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const } void TerminalDisplay::resetColors() { + mColorScheme = mInitialColorScheme; for ( Uint32 i = 0; i < eeARRAY_SIZE( colormapped ); i++ ) - resetColor( i, i < mColorScheme.getPaletteSize() - ? mColorScheme.getPaletteIndex( i ).toHexString().c_str() + resetColor( i, i < mInitialColorScheme.getPaletteSize() + ? mInitialColorScheme.getPaletteIndex( i ).toHexString().c_str() : nullptr ); } int TerminalDisplay::resetColor( const Uint32& index, const char* name ) { if ( !name ) { if ( index < mColors.size() ) { - Color col = 0x000000FF; - - if ( index < 256 ) - col = colormapped[index]; + const Color col = index < mInitialColorScheme.getPaletteSize() + ? mInitialColorScheme.getPaletteIndex( index ) + : colormapped[index]; mColors[index] = col; mColorScheme.setPaletteIndex( index, col ); return 0; + } else if ( index == 256 || index == 257 ) { + mColorScheme.setCursor( mInitialColorScheme.getCursor() ); + return 0; + } else if ( index == 258 ) { + mColorScheme.setForeground( mInitialColorScheme.getForeground() ); + return 0; + } else if ( index == 259 ) { + mColorScheme.setBackground( mInitialColorScheme.getBackground() ); + return 0; } - // Reset to default for 256, 257, 258, 259 is not well defined here without original - // defaults return 1; } @@ -549,10 +556,7 @@ int TerminalDisplay::resetColor( const Uint32& index, const char* name ) { } } } else if ( String::iequals( "default", name ) ) { - unsigned char r, g, b; - getColor( index, &r, &g, &b ); - col = Color( r, g, b, 255 ); - colorParsed = true; + return resetColor( index, nullptr ); } else if ( Color::isColorString( std::string_view{ name }, true ) ) { col = Color::fromString( name ); colorParsed = true; @@ -677,6 +681,7 @@ const TerminalColorScheme& TerminalDisplay::getColorScheme() const { } void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) { + mInitialColorScheme = colorScheme; mColorScheme = colorScheme; resetColors(); if ( mSession ) diff --git a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp index f2c41b860..aaed40aa1 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp @@ -41,6 +41,7 @@ #include #include #include +#include using namespace EE::Network; using namespace EE::System; @@ -76,6 +77,52 @@ extern "C" { namespace eterm { namespace Terminal { +namespace { + +bool terminalDiagnosticsEnabled() { +#ifdef EE_DEBUG + return true; +#else + // Release builds stay quiet unless diagnostics are explicitly requested. This is cached because + // malformed or unsupported sequences can otherwise hit this path for every rendered cell. + static const char* setting = getenv( "ETERM_LOG_ERRORS" ); + static const bool enabled = + setting != nullptr && setting[0] != '\0' && strcmp( setting, "0" ) != 0; + return enabled; +#endif +} + +void terminalDiagnostic( const char* format, ... ) { + if ( !terminalDiagnosticsEnabled() ) + return; + va_list args; + va_start( args, format ); + vfprintf( stderr, format, args ); + va_end( args ); +} + +TerminalCursorMode blinkingCursorVariant( TerminalCursorMode mode ) { + switch ( mode ) { + case BlinkUnderline: + case SteadyUnderline: + return BlinkUnderline; + case BlinkBar: + case SteadyBar: + return BlinkBar; + case BlinkingBlock: + case BlinkingBlockDefault: + case SteadyBlock: + return BlinkingBlock; + case StExtension: + return StExtension; + case TerminalCursorMode::MAX_CURSOR: + return BlinkingBlock; + } + return BlinkingBlock; +} + +} // namespace + #if defined( EE_ARCH_X86_64 ) #if defined( __GNUC__ ) || defined( __clang__ ) __attribute__( ( target( "avx2" ) ) ) @@ -848,6 +895,17 @@ void TerminalEmulator::setPresentationInterval( Time interval ) { mPresentationInterval = interval > Time::Zero ? interval : Microseconds( 1000000.0 / 60.0 ); } +void TerminalEmulator::setDefaultCursorMode( TerminalCursorMode mode ) { + mDefaultCursorMode = mode; +} + +void TerminalEmulator::notifyColorSchemeChanged() { + const int scheme = colorScheme(); + if ( mColorSchemeNotifications && mColorScheme != 0 && scheme != mColorScheme ) + reportColorScheme(); + mColorScheme = scheme; +} + Vector2i TerminalEmulator::getSize() const { return { mTerm.col, mTerm.row }; } @@ -962,6 +1020,8 @@ void TerminalEmulator::tcursor( int mode ) { void TerminalEmulator::treset( void ) { uint i; + mColorSchemeNotifications = false; + mTerm.is_syncing = false; mTerm.c = TerminalCursor{}; mTerm.c.attr = TerminalGlyph{}; mTerm.c.attr.u = ' '; @@ -990,9 +1050,13 @@ void TerminalEmulator::treset( void ) { xsetmode( 0, MODE_MOUSE | MODE_MOUSESGR | MODE_APPKEYPAD | MODE_APPCURSOR | MODE_FOCUS | MODE_BRCKTPASTE | MODE_MOUSEX10 | MODE_MOUSEMANY ); + // Preserve eterm's established behavior and xterm's default alternateScroll resource. + xsetmode( 1, MODE_ALTSCRROLL ); auto dpy = mDpy.lock(); - if ( dpy ) + if ( dpy ) { dpy->setMode( MODE_VISIBLE, 1 ); + dpy->setCursorMode( mDefaultCursorMode ); + } } void TerminalEmulator::tnew( int col, int row, size_t historySize ) { @@ -1419,27 +1483,25 @@ void TerminalEmulator::tnewline( int first_col ) { void TerminalEmulator::csiparse( void ) { char *p = mCsiescseq.buf, *np; long int v; - int sep = ';'; /* colon or semi-colon, but not both */ mCsiescseq.narg = 0; - if ( *p == '?' ) { - mCsiescseq.priv = 1; + if ( *p == '<' || *p == '=' || *p == '>' || *p == '?' ) { + mCsiescseq.priv = *p; p++; } mCsiescseq.buf[mCsiescseq.len] = '\0'; - while ( p < mCsiescseq.buf + mCsiescseq.len ) { + while ( p < mCsiescseq.buf + mCsiescseq.len && mCsiescseq.narg < ESC_ARG_SIZ ) { np = NULL; v = strtol( p, &np, 10 ); if ( np == p ) v = 0; if ( v == LONG_MAX || v == LONG_MIN ) v = -1; - mCsiescseq.arg[mCsiescseq.narg++] = v; + mCsiescseq.arg[mCsiescseq.narg] = v; p = np; - if ( sep == ';' && *p == ':' ) - sep = ':'; /* allow override to colon once */ - if ( *p != sep || mCsiescseq.narg == ESC_ARG_SIZ ) + mCsiescseq.sep[mCsiescseq.narg++] = *p == ';' || *p == ':' ? *p : '\0'; + if ( *p != ';' && *p != ':' ) break; p++; } @@ -1596,33 +1658,63 @@ void TerminalEmulator::tdeleteline( int n ) { tscrollup( mTerm.c.y, n, 0 ); } -int32_t TerminalEmulator::tdefcolor( int* attr, int* npar, int l ) { +int32_t TerminalEmulator::tdefcolor( int* attr, const char* separators, int* npar, int l ) { int32_t idx = -1; uint r, g, b; + if ( !attr || !npar || l < 0 || l > ESC_ARG_SIZ || *npar < 0 || *npar >= l ) { + terminalDiagnostic( "erresc(color): invalid parameter index\n" ); + return idx; + } + const bool subparameters = separators && separators[*npar] == ':'; + const int selector = *npar + 1; - switch ( attr[*npar + 1] ) { + if ( selector >= l ) { + terminalDiagnostic( "erresc(color): missing color type\n" ); + return idx; + } + + switch ( attr[selector] ) { case 2: /* direct color in RGB space */ - if ( *npar + 4 >= l ) { - fprintf( stderr, "erresc(38): Incorrect number of parameters (%d)\n", *npar ); + if ( subparameters ) { + int end = selector; + while ( end < l - 1 && separators[end] == ':' ) + ++end; + const int componentCount = end - selector; + if ( componentCount != 3 && componentCount != 4 ) { + terminalDiagnostic( "erresc(color): invalid RGB subparameter count %d\n", + componentCount ); + *npar = end; + break; + } + const int rgb = selector + ( componentCount == 4 ? 2 : 1 ); + r = attr[rgb]; + g = attr[rgb + 1]; + b = attr[rgb + 2]; + *npar = end; + } else if ( *npar + 4 < l ) { + r = attr[*npar + 2]; + g = attr[*npar + 3]; + b = attr[*npar + 4]; + *npar += 4; + } else { + terminalDiagnostic( "erresc(color): incorrect number of RGB parameters (%d)\n", + *npar ); break; } - r = attr[*npar + 2]; - g = attr[*npar + 3]; - b = attr[*npar + 4]; - *npar += 4; if ( !BETWEEN( r, 0, 255 ) || !BETWEEN( g, 0, 255 ) || !BETWEEN( b, 0, 255 ) ) - fprintf( stderr, "erresc: bad rgb color (%u,%u,%u)\n", r, g, b ); + terminalDiagnostic( "erresc: bad rgb color (%u,%u,%u)\n", r, g, b ); else idx = TRUECOLOR( r, g, b ); break; case 5: /* indexed color */ if ( *npar + 2 >= l ) { - fprintf( stderr, "erresc(38): Incorrect number of parameters (%d)\n", *npar ); + terminalDiagnostic( "erresc(color): incorrect number of indexed parameters (%d)\n", + *npar ); break; } *npar += 2; if ( !BETWEEN( attr[*npar], 0, 255 ) ) - fprintf( stderr, "erresc: bad fgcolor %d\n", attr[*npar] ); + terminalDiagnostic( "erresc: bad indexed color %d\n", attr[*npar] ); else idx = attr[*npar]; break; @@ -1631,14 +1723,18 @@ int32_t TerminalEmulator::tdefcolor( int* attr, int* npar, int l ) { case 3: /* direct color in CMY space */ case 4: /* direct color in CMYK space */ default: - fprintf( stderr, "erresc(38): gfx attr %d unknown\n", attr[*npar] ); + terminalDiagnostic( "erresc(color): color type %d unknown\n", attr[selector] ); + if ( subparameters ) { + while ( *npar < l - 1 && separators[*npar] == ':' ) + ++*npar; + } break; } return idx; } -void TerminalEmulator::tsetattr( int* attr, int l ) { +void TerminalEmulator::tsetattr( int* attr, int l, const char* separators ) { // Check if this is a private sequence (should be ignored for SGR) // Private sequences start with '?' and should not affect text attributes if ( mCsiescseq.priv ) { @@ -1706,14 +1802,14 @@ void TerminalEmulator::tsetattr( int* attr, int l ) { mTerm.c.attr.mode &= ~ATTR_STRUCK; break; case 38: - if ( ( idx = tdefcolor( attr, &i, l ) ) >= 0 ) + if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 ) mTerm.c.attr.fg = idx; break; case 39: /* set foreground color to default */ mTerm.c.attr.fg = mDefaultFg; break; case 48: - if ( ( idx = tdefcolor( attr, &i, l ) ) >= 0 ) + if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 ) mTerm.c.attr.bg = idx; break; case 49: /* set background color to default */ @@ -1723,7 +1819,9 @@ void TerminalEmulator::tsetattr( int* attr, int l ) { /* This starts a sequence to change the color of * "underline" pixels. We don't support that and * instead eat up a following "5;n" or "2;r;g;b". */ - tdefcolor( attr, &i, l ); + tdefcolor( attr, separators, &i, l ); + break; + case 59: /* reset underline color (unsupported, therefore a no-op) */ break; default: if ( BETWEEN( attr[i], 30, 37 ) ) { @@ -1735,8 +1833,9 @@ void TerminalEmulator::tsetattr( int* attr, int l ) { } else if ( BETWEEN( attr[i], 100, 107 ) ) { mTerm.c.attr.bg = attr[i] - 100 + 8; } else { - fprintf( stderr, "erresc(default): gfx attr %d unknown\n", attr[i] ); - csidump(); + terminalDiagnostic( "erresc(default): gfx attr %d unknown\n", attr[i] ); + if ( terminalDiagnosticsEnabled() ) + csidump(); } break; } @@ -1817,6 +1916,9 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { case 1006: /* 1006: extended reporting mode */ xsetmode( set, MODE_MOUSESGR ); break; + case 1007: /* wheel sends cursor keys on the alternate screen */ + xsetmode( set, MODE_ALTSCRROLL ); + break; case 1034: xsetmode( set, MODE_8BIT ); break; @@ -1856,19 +1958,23 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { case 1039: /* ESC to Meta (not implemented) */ break; case 2026: { - // IGNORE DECSET/DECRST 2026 for sync updates? - // (https://codeberg.org/dnkl/foot/pulls/461/files) - // mTerm.is_syncing = ( set == 1 ); - /* if ( !mTerm.is_syncing ) { - // When syncing ends, we must perform the deferred draw + if ( set ) { + mTerm.is_syncing = true; + mSynchronizedUpdateClock.restart(); + } else if ( mTerm.is_syncing ) { + mTerm.is_syncing = false; + // Publish the complete frame immediately instead of waiting for the next + // presentation deadline. draw(); - } */ + } break; } + case 2031: /* Light/dark color-scheme change notifications. */ + mColorSchemeNotifications = set; + mColorScheme = colorScheme(); + break; default: -#ifdef EE_DEBUG - fprintf( stderr, "erresc: unknown private set/reset mode %d\n", *args ); -#endif + terminalDiagnostic( "erresc: unknown private set/reset mode %d\n", *args ); break; } } else { @@ -1888,9 +1994,7 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { MODBIT( mTerm.mode, set, MODE_CRLF ); break; default: -#ifdef EE_DEBUG - fprintf( stderr, "erresc: unknown set/reset mode %d\n", *args ); -#endif + terminalDiagnostic( "erresc: unknown set/reset mode %d\n", *args ); break; } } @@ -1898,7 +2002,13 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) { } void TerminalEmulator::handleDeviceAttributes() { - if ( mCsiescseq.priv ) { + if ( mCsiescseq.priv == '>' ) { + char buf[64]; + const auto version = EE::Version::getVersion(); + const int revision = version.major * 10000 + version.minor * 100 + version.patch; + const int len = snprintf( buf, sizeof( buf ), "\033[>0;%d;0c", revision ); + ttywrite( buf, len, 0 ); + } else if ( mCsiescseq.priv == '?' ) { char buf[64]; int len; // Private Device Attributes - respond with terminal capabilities @@ -1921,7 +2031,7 @@ void TerminalEmulator::handleDeviceAttributes() { // Unknown private DA query break; } - } else { + } else if ( !mCsiescseq.priv ) { // Standard DA - respond with VT100 identification ttywrite( vtiden, strlen( vtiden ), 0 ); } @@ -1936,10 +2046,8 @@ void TerminalEmulator::csihandle( void ) { switch ( mCsiescseq.mode[0] ) { default: unknown: -#ifdef EE_DEBUG - fprintf( stderr, "erresc: unknown csi " ); + terminalDiagnostic( "erresc: unknown csi " ); csidump(); -#endif /* die(""); */ break; case '@': /* ICH -- Insert blank char */ @@ -2076,7 +2184,9 @@ void TerminalEmulator::csihandle( void ) { tinsertblankline( mCsiescseq.arg[0] ); break; case 'l': /* RM -- Reset Mode */ - tsetmode( mCsiescseq.priv, 0, mCsiescseq.arg, mCsiescseq.narg ); + if ( mCsiescseq.priv && mCsiescseq.priv != '?' ) + goto unknown; + tsetmode( mCsiescseq.priv == '?', 0, mCsiescseq.arg, mCsiescseq.narg ); break; case 'M': /* DL -- Delete lines */ DEFAULT( mCsiescseq.arg[0], 1 ); @@ -2099,41 +2209,19 @@ void TerminalEmulator::csihandle( void ) { tmoveato( mTerm.c.x, mCsiescseq.arg[0] - 1 ); break; case 'h': /* SM -- Set terminal mode */ - tsetmode( mCsiescseq.priv, 1, mCsiescseq.arg, mCsiescseq.narg ); + if ( mCsiescseq.priv && mCsiescseq.priv != '?' ) + goto unknown; + tsetmode( mCsiescseq.priv == '?', 1, mCsiescseq.arg, mCsiescseq.narg ); break; case 'm': /* SGR -- Terminal attribute (color) */ - tsetattr( mCsiescseq.arg, mCsiescseq.narg ); - break; - case '>': /* Private sequences */ - switch ( mCsiescseq.mode[1] ) { - case '4': /* Extended underline styles ESC[>4;Nm */ - // Extended underline styles - fallback to standard underline - /* DEFAULT( mCsiescseq.arg[0], 1 ); - switch ( mCsiescseq.arg[0] ) { - case 0: // No underline - fallback to ESC[24m - { - int fallback_args[] = { 24 }; // Reset underline - tsetattr( fallback_args, 1 ); - } break; - case 1: // Straight underline - fallback to ESC[4m - case 2: // Double underline - case 3: // Curly underline - case 4: // Dotted underline - case 5: // Dashed underline - { - int fallback_args[] = { 4 }; // Standard underline - tsetattr( fallback_args, 1 ); - } break; - default: - goto unknown; - } */ - break; - default: - goto unknown; - } + if ( mCsiescseq.priv == '>' ) + break; // Extended underline styles are not rendered yet. + tsetattr( mCsiescseq.arg, mCsiescseq.narg, mCsiescseq.sep ); break; case 'n': /* DSR – Device Status Report (cursor position) */ - if ( mCsiescseq.arg[0] == 6 ) { + if ( mCsiescseq.priv == '?' && mCsiescseq.arg[0] == 996 ) { + reportColorScheme(); + } else if ( !mCsiescseq.priv && mCsiescseq.arg[0] == 6 ) { len = snprintf( buf, sizeof( buf ), "\033[%i;%iR", mTerm.c.y + 1, mTerm.c.x + 1 ); ttywrite( buf, len, 0 ); } @@ -2152,7 +2240,12 @@ void TerminalEmulator::csihandle( void ) { tcursor( CURSOR_SAVE ); break; case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ - if ( mCsiescseq.priv ) { + if ( mCsiescseq.priv == '?' || mCsiescseq.priv == '>' || mCsiescseq.priv == '<' || + mCsiescseq.priv == '=' ) { + // Kitty keyboard protocol. Remain in legacy mode and do not answer its query: a + // response would claim support and require encoding all subsequent key events. + break; + } else if ( mCsiescseq.priv ) { goto unknown; } else { tcursor( CURSOR_LOAD ); @@ -2161,20 +2254,27 @@ void TerminalEmulator::csihandle( void ) { case ' ': switch ( mCsiescseq.mode[1] ) { case 'q': /* DECSCUSR -- Set Cursor Style */ - if ( mCsiescseq.arg[0] < 0 || - mCsiescseq.arg[0] < TerminalCursorMode::MAX_CURSOR ) + if ( mCsiescseq.priv || mCsiescseq.arg[0] < 0 || + mCsiescseq.arg[0] >= TerminalCursorMode::StExtension ) goto unknown; dpy = mDpy.lock(); - if ( dpy ) - dpy->setCursorMode( (TerminalCursorMode)mCsiescseq.arg[0] ); + if ( dpy ) { + const auto mode = static_cast( mCsiescseq.arg[0] ); + dpy->setCursorMode( mode == BlinkingBlock + ? blinkingCursorVariant( mDefaultCursorMode ) + : mode ); + } break; default: goto unknown; } break; - case '=': /* Progressive enhancement sequences */ - /* Keyboard protocol ESC[=Nu */ - /* Do nothing for the moment */ + case 'q': /* XTVERSION -- Report terminal name and version */ + if ( mCsiescseq.priv != '>' || mCsiescseq.arg[0] != 0 ) + goto unknown; + len = snprintf( buf, sizeof( buf ), "\033P>|eterm %s\033\\", + EE::Version::getVersionName( false ).c_str() ); + ttywrite( buf, len, 0 ); break; case 't': /* Window manipulation */ switch ( mCsiescseq.arg[0] ) { @@ -2199,16 +2299,12 @@ void TerminalEmulator::csihandle( void ) { goto unknown; } break; - case '?': - /* Private mode queries - ignore or handle appropriately */ - /* For XTQMODKEYS and similar queries, we should either: - 1. Ignore completely (do nothing) - 2. Send a proper response if required */ - break; } } void TerminalEmulator::csidump( void ) { + if ( !terminalDiagnosticsEnabled() ) + return; size_t i; uint c; @@ -2283,7 +2379,7 @@ void TerminalEmulator::strhandle( void ) { setClipboard( dec ); xfree( dec ); } else { - fprintf( stderr, "erresc: invalid base64\n" ); + terminalDiagnostic( "erresc: invalid base64\n" ); } } return; @@ -2299,31 +2395,57 @@ void TerminalEmulator::strhandle( void ) { if ( !strcmp( p, "?" ) ) { osc_color_response( par, osc_table[j].idx, 0 ); } else if ( xsetcolorname( osc_table[j].idx, p ) ) { - fprintf( stderr, "erresc: invalid %s color: %s\n", osc_table[j].str, p ); + terminalDiagnostic( "erresc: invalid %s color: %s\n", osc_table[j].str, p ); } else { tfulldirt(); } return; - case 4: /* color set */ - if ( narg < 3 ) - break; - p = mStrescseq.args[2]; - /* FALLTHROUGH */ - case 104: /* color reset, here p = NULL */ - j = ( narg > 1 ) ? atoi( mStrescseq.args[1] ) : -1; - if ( resetColor( j, p ) ) { - if ( par == 104 && narg <= 1 ) - return; /* color reset without parameter */ - fprintf( stderr, "erresc: invalid color j=%d, p=%s\n", j, - p ? p : "(null)" ); - } else { - /* - * TODO if defaultbg color is changed, borders - * are dirty - */ - redraw(); + case 4: { /* set or query palette colors */ + bool changed = false; + for ( int arg = 1; arg + 1 < narg; arg += 2 ) { + p = mStrescseq.args[arg + 1]; + if ( !String::fromString( j, std::string_view{ mStrescseq.args[arg] } ) || + j < 0 ) { + terminalDiagnostic( "erresc: invalid OSC 4 color index: %s\n", + mStrescseq.args[arg] ); + continue; + } + if ( !strcmp( p, "?" ) ) { + osc_color_response( j, j, 1 ); + } else if ( resetColor( j, p ) ) { + terminalDiagnostic( "erresc: invalid color j=%d, p=%s\n", j, p ); + } else { + changed = true; + } } + if ( changed ) + redraw(); return; + } + case 104: { /* reset palette colors */ + if ( narg <= 1 ) { + loadColors(); + tfulldirt(); + return; + } + bool changed = false; + for ( int arg = 1; arg < narg; ++arg ) { + if ( !String::fromString( j, std::string_view{ mStrescseq.args[arg] } ) || + j < 0 ) { + terminalDiagnostic( "erresc: invalid OSC 104 color index: %s\n", + mStrescseq.args[arg] ); + continue; + } + if ( resetColor( j, nullptr ) ) { + terminalDiagnostic( "erresc: palette color %d not found\n", j ); + } else { + changed = true; + } + } + if ( changed ) + tfulldirt(); + return; + } case 110: /* reset dynamic VT100 text foreground color */ case 111: /* reset dynamic VT100 text background color */ case 112: /* reset dynamic text cursor color */ @@ -2332,7 +2454,7 @@ void TerminalEmulator::strhandle( void ) { if ( ( j = par - 110 ) < 0 || j >= (int)LEN( osc_table ) ) break; /* shouldn't be possible */ if ( resetColor( osc_table[j].idx, NULL ) ) { - fprintf( stderr, "erresc: %s color not found\n", osc_table[j].str ); + terminalDiagnostic( "erresc: %s color not found\n", osc_table[j].str ); } else { tfulldirt(); } @@ -2342,6 +2464,13 @@ void TerminalEmulator::strhandle( void ) { mCurrentWorkingDirectory = URI( mStrescseq.args[1] ).getPath(); return; } + case 8: /* Hyperlink: OSC 8 ; params ; URI ST */ + // Hyperlink metadata is not stored in terminal cells yet. Recognize valid open + // and close markers so applications can emit OSC 8 without producing + // diagnostics. + if ( narg >= 3 ) + return; + break; case 133: { if ( narg > 1 ) { j = ( narg > 1 ) ? mStrescseq.args[1][0] : -1; @@ -2388,10 +2517,8 @@ void TerminalEmulator::strhandle( void ) { return; } -#ifdef EE_DEBUG - logError( "erresc: unknown str " ); + terminalDiagnostic( "erresc: unknown str " ); strdump(); -#endif } void TerminalEmulator::strparse( void ) { @@ -2415,6 +2542,8 @@ void TerminalEmulator::strparse( void ) { } void TerminalEmulator::strdump( void ) { + if ( !terminalDiagnosticsEnabled() ) + return; size_t i; uint c; @@ -2531,7 +2660,7 @@ void TerminalEmulator::tdeftran( char ascii ) { char* p; if ( ( p = strchr( cs, ascii ) ) == NULL ) { - fprintf( stderr, "esc unhandled charset: ESC ( %c\n", ascii ); + terminalDiagnostic( "esc unhandled charset: ESC ( %c\n", ascii ); } else { mTerm.trantbl[mTerm.icharset] = vcs[p - cs]; } @@ -2743,8 +2872,8 @@ int TerminalEmulator::eschandle( uchar ascii ) { strhandle(); break; default: - fprintf( stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", (uchar)ascii, - isprint( ascii ) ? ascii : '.' ); + terminalDiagnostic( "erresc: unknown sequence ESC 0x%02X '%c'\n", (uchar)ascii, + isprint( ascii ) ? ascii : '.' ); break; } return 1; @@ -2996,7 +3125,7 @@ void TerminalEmulator::tresize( int col, int row ) { bool is_alt = IS_SET( MODE_ALTSCREEN ); if ( col < 1 || row < 1 ) { - fprintf( stderr, "tresize: error resizing to %dx%d\n", col, row ); + terminalDiagnostic( "tresize: error resizing to %dx%d\n", col, row ); return; } @@ -3205,9 +3334,10 @@ void TerminalEmulator::drawregion( ITerminalDisplay& dpy, int x1, int y1, int x2 } void TerminalEmulator::draw() { - // If a synchronized update is in progress, skip the physical render - // if ( mTerm.is_syncing ) - // return; + // DEC private mode 2026 makes the bytes between DECSET and DECRST one presentation unit. + // Parsing continues normally, but no partially cleared/rebuilt frame may reach the UI. + if ( mTerm.is_syncing ) + return; int cx = mTerm.c.x /*, ocx = term.ocx, ocy = term.ocy*/; @@ -3284,28 +3414,47 @@ void TerminalEmulator::osc_color_response( int num, int index, int is_osc4 ) { unsigned char r, g, b; if ( xgetcolor( is_osc4 ? num : index, &r, &g, &b ) ) { - fprintf( stderr, "erresc: failed to fetch %s color %d\n", is_osc4 ? "osc4" : "osc", - is_osc4 ? num : index ); + terminalDiagnostic( "erresc: failed to fetch %s color %d\n", is_osc4 ? "osc4" : "osc", + is_osc4 ? num : index ); return; } n = snprintf( buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", is_osc4 ? "4;" : "", num, r, r, g, g, b, b ); if ( n < 0 || n >= (int)sizeof( buf ) ) { - fprintf( stderr, "error: %s while printing %s response\n", - n < 0 ? "snprintf failed" : "truncation occurred", is_osc4 ? "osc4" : "osc" ); + terminalDiagnostic( "error: %s while printing %s response\n", + n < 0 ? "snprintf failed" : "truncation occurred", + is_osc4 ? "osc4" : "osc" ); } else { ttywrite( buf, n, 1 ); } } +int TerminalEmulator::colorScheme() { + unsigned char red, green, blue; + if ( xgetcolor( 259, &red, &green, &blue ) ) + return 0; + // The protocol describes the OS preference, which eterm does not store separately. The active + // background is the useful equivalent for applications choosing contrasting colors. + return red * 299 + green * 587 + blue * 114 < 128000 ? 1 : 2; +} + +void TerminalEmulator::reportColorScheme() { + const int scheme = colorScheme(); + if ( !scheme ) + return; + char buf[16]; + const int len = snprintf( buf, sizeof( buf ), "\033[?997;%dn", scheme ); + ttywrite( buf, len, 0 ); +} + void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Vector2i& pos, const Uint32& flags, const Uint32& mod ) { if ( !xgetmode( (TerminalWinMode)MODE_MOUSE ) && !xgetmode( MODE_MOUSESGR ) && ( TerminalMouseEventType::MouseButtonDown == type || TerminalMouseEventType::MouseButtonRelease == type ) ) { /* If mouse mode is not enabled, we send arrow keys for scroll events */ - if ( type == TerminalMouseEventType::MouseButtonDown && + if ( type == TerminalMouseEventType::MouseButtonDown && xgetmode( MODE_ALTSCRROLL ) && ( flags & ( EE_BUTTON_WUMASK | EE_BUTTON_WDMASK ) ) && tisaltscr() ) { char buf[64]; int len = 0; @@ -3568,7 +3717,6 @@ void TerminalEmulator::resize( int columns, int rows ) { return; } - mTerm.is_syncing = true; tresize( columns, rows ); redraw(); @@ -3588,6 +3736,11 @@ bool TerminalEmulator::update() { _die( "Failed to resize pty!" ); } + redraw(); + } + + // A client that fails to close a synchronized update must not freeze presentation forever. + if ( mTerm.is_syncing && mSynchronizedUpdateClock.getElapsedTime() >= Seconds( 1 ) ) { mTerm.is_syncing = false; redraw(); } diff --git a/src/modules/eterm/src/eterm/terminal/terminalsession.cpp b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp index d09c8e97c..eb270fc62 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalsession.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp @@ -103,6 +103,8 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay { void resetColors() { mPalette = mInitialPalette; + if ( mEmulator ) + mEmulator->notifyColorSchemeChanged(); Event event{ EventType::Color }; event.value = -1; mSession.enqueueEvent( std::move( event ), false ); @@ -144,6 +146,8 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay { if ( !parsed ) return 1; setPaletteColor( index, color ); + if ( mEmulator ) + mEmulator->notifyColorSchemeChanged(); Event event{ EventType::Color }; event.data = name ? name : ""; event.value = static_cast( index ); @@ -195,6 +199,8 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay { void setPalette( TerminalColorPalette palette ) { mInitialPalette = palette; mPalette = std::move( palette ); + if ( mEmulator ) + mEmulator->notifyColorSchemeChanged(); } void setFocused( bool focused ) { @@ -503,6 +509,7 @@ void TerminalSession::processCommand( Command&& command ) { mWorkerDisplay->setFocused( value.value ); mEmulator->redraw(); } else if constexpr ( std::is_same_v ) { + mEmulator->setDefaultCursorMode( value.mode ); mWorkerDisplay->setCursorMode( value.mode ); mEmulator->redraw(); } else if constexpr ( std::is_same_v ) { @@ -554,4 +561,4 @@ void TerminalSession::processCommand( Command&& command ) { std::move( command ) ); } -}} // namespace eterm::Terminal \ No newline at end of file +}} // namespace eterm::Terminal diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index 270134ef4..0570bec89 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -339,18 +339,150 @@ UTEST( eterm_session, concurrent_shutdown_is_idempotent ) { class MockDisplay : public ITerminalDisplay { public: int mDrawLines{ 0 }; + int mDrawEnds{ 0 }; uint32_t mFirstMode{ 0 }; uint32_t mSecondMode{ 0 }; + TerminalGlyph mFirstGlyph; + TerminalGlyph mSecondGlyph; + std::vector mResetColorIndices; + int mResetColorsCount{ 0 }; + Uint32 mBackground{ 0x101010FF }; bool drawBegin( Uint32, Uint32 ) override { return true; } - void drawLine( Line line, int, int, int ) override { + void drawLine( Line line, int, int y, int ) override { ++mDrawLines; - mFirstMode = line[0].mode; - mSecondMode = line[1].mode; + if ( y == 0 ) { + mFirstMode = line[0].mode; + mSecondMode = line[1].mode; + mFirstGlyph = line[0]; + mSecondGlyph = line[1]; + } } void drawCursor( int, int, TerminalGlyph, int, int, TerminalGlyph ) override {} - void drawEnd() override {} + void drawEnd() override { ++mDrawEnds; } + void resetColors() override { ++mResetColorsCount; } + int resetColor( const Uint32& index, const char* ) override { + mResetColorIndices.emplace_back( index ); + return 0; + } + bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, + unsigned char* b ) override { + if ( index == 259 ) { + *r = static_cast( mBackground >> 24 ); + *g = static_cast( mBackground >> 16 ); + *b = static_cast( mBackground >> 8 ); + return true; + } + if ( index > 1 ) + return false; + *r = static_cast( 1 + index * 3 ); + *g = static_cast( 2 + index * 3 ); + *b = static_cast( 3 + index * 3 ); + return true; + } }; +UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?u\033[>7u\033[<1u\033[mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_TRUE( ptyPtr->mWrites.empty() ); +} + +UTEST( eterm, cursor_style_and_xterm_version_queries ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[0 q\033[6 q\033[>0q"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_EQ( TerminalCursorMode::SteadyBar, display->getCursorMode() ); + EXPECT_EQ( static_cast( 0 ), ptyPtr->mWrites.find( "\033P>|eterm " ) ); + ASSERT_TRUE( ptyPtr->mWrites.size() >= 2 ); + EXPECT_STDSTREQ( "\033\\", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 2 ) ); +} + +UTEST( eterm, cursor_style_zero_uses_blinking_configured_shape ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[2 q\033[0 q"; + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + term->setDefaultCursorMode( TerminalCursorMode::SteadyUnderline ); + + term->update(); + + EXPECT_EQ( TerminalCursorMode::BlinkUnderline, display->getCursorMode() ); +} + +UTEST( eterm, osc_hyperlink_markers_are_recognized ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033]8;id=codex;https://example.com/\033\\OK\033]8;;\033\\"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_EQ( static_cast( 'O' ), display->mFirstGlyph.u ); + EXPECT_EQ( static_cast( 'K' ), display->mSecondGlyph.u ); + EXPECT_TRUE( ptyPtr->mWrites.empty() ); +} + +UTEST( eterm, alternate_scroll_mode_controls_wheel_key_translation ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?1049h\033[?1007l"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, EE_BUTTON_WUMASK, 0 ); + EXPECT_TRUE( ptyPtr->mWrites.empty() ); + + ptyPtr->mBuffer += "\033[?1007h"; + term->update(); + term->mousereport( TerminalMouseEventType::MouseButtonDown, { 0, 0 }, EE_BUTTON_WUMASK, 0 ); + EXPECT_STDSTREQ( "\033[A", ptyPtr->mWrites ); +} + +UTEST( eterm, color_scheme_query_and_change_notification ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?996n\033[?2031h"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + EXPECT_STDSTREQ( "\033[?997;1n", ptyPtr->mWrites ); + + display->mBackground = 0xF0F0F0FF; + term->notifyColorSchemeChanged(); + EXPECT_STDSTREQ( "\033[?997;1n\033[?997;2n", ptyPtr->mWrites ); + + ptyPtr->mBuffer += "\033[?2031l"; + term->update(); + display->mBackground = 0x101010FF; + term->notifyColorSchemeChanged(); + EXPECT_STDSTREQ( "\033[?997;1n\033[?997;2n", ptyPtr->mWrites ); +} + UTEST( eterm, basic_write ) { auto pty = std::make_unique(); auto process = std::make_unique(); @@ -366,6 +498,93 @@ UTEST( eterm, basic_write ) { EXPECT_STDSTREQ( "ABC", term->getSelection() ); } +UTEST( eterm, synchronized_updates_publish_only_complete_frames ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + display->mDrawEnds = 0; + const char partialFrame[] = "\033[?2026h\033[2Jpartial"; + term->write( partialFrame, sizeof( partialFrame ) - 1 ); + term->update(); + + EXPECT_EQ( 0, display->mDrawEnds ); + + const char completeFrame[] = "\033[Hcomplete\033[?2026l"; + term->write( completeFrame, sizeof( completeFrame ) - 1 ); + term->update(); + + EXPECT_TRUE( display->mDrawEnds > 0 ); + term->selstart( 0, 0, 0 ); + term->selextend( 7, 0, SEL_REGULAR, false ); + EXPECT_STDSTREQ( "complete", term->getSelection() ); +} + +UTEST( eterm, sgr_colon_subparameters_preserve_groups_and_optional_color_space ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + const char sequence[] = "\033[58:2::255:192:185;59;1;38:2::12:34:56;48:5:42mX"; + term->write( sequence, sizeof( sequence ) - 1 ); + term->update(); + + EXPECT_TRUE( display->mFirstGlyph.mode & ATTR_BOLD ); + EXPECT_EQ( 0x010C2238u, display->mFirstGlyph.fg ); + EXPECT_EQ( 42u, display->mFirstGlyph.bg ); +} + +UTEST( eterm, invalid_indexed_color_keeps_the_previous_color ) { + auto pty = std::make_unique(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + const char sequence[] = "\033[31mA\033[38;5;283mB"; + term->write( sequence, sizeof( sequence ) - 1 ); + term->update(); + + EXPECT_EQ( 1u, display->mFirstGlyph.fg ); + EXPECT_EQ( 1u, display->mSecondGlyph.fg ); +} + +UTEST( eterm, osc_palette_queries_return_each_requested_color ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033]4;0;?;1;?\a"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + EXPECT_STDSTREQ( "\033]4;0;rgb:0101/0202/0303\a\033]4;1;rgb:0404/0505/0606\a", + ptyPtr->mWrites ); +} + +UTEST( eterm, osc_color_resets_reach_palette_and_dynamic_defaults ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033]104;1;2\a\033]110\a\033]111\a\033]112\a\033]104\a"; + pty->mLoopWrites = false; + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + + term->update(); + + ASSERT_EQ( static_cast( 5 ), display->mResetColorIndices.size() ); + EXPECT_EQ( 1u, display->mResetColorIndices[0] ); + EXPECT_EQ( 2u, display->mResetColorIndices[1] ); + EXPECT_EQ( 258u, display->mResetColorIndices[2] ); + EXPECT_EQ( 259u, display->mResetColorIndices[3] ); + EXPECT_EQ( 256u, display->mResetColorIndices[4] ); + EXPECT_EQ( 2, display->mResetColorsCount ); +} + UTEST( eterm, selection_redraw_while_idle ) { auto pty = std::make_unique(); auto process = std::make_unique();