eterm: harden threaded presentation and terminal protocol handling

- preserve CSI subparameter separators for modern SGR color sequences
  - fix synchronized updates and periodic presentation under sustained output
  - support OSC palette queries, resets, and color scheme notifications
  - implement alternate scroll, cursor style, and terminal version queries
  - preserve the configured cursor shape for default blinking cursor requests
  - recognize Kitty keyboard negotiation without falsely advertising support
  - recognize OSC 8 hyperlink markers without emitting parser errors
  - make release parser diagnostics opt-in through ETERM_LOG_ERRORS
  - update the Kitty keyboard protocol implementation plan for worker ownership
  - add regression coverage for rendering, colors, CSI modes, cursors, and OSC 8
This commit is contained in:
Martín Lucas Golini
2026-08-31 03:01:40 -03:00
parent bfa9b6ab60
commit c5c6350920
8 changed files with 787 additions and 344 deletions

View File

@@ -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()`. Kittys `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 repositorys 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 ? <flags> 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<std::size_t>( 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<PendingKittyTextKey> 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<Scancode> 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 [ ? <supported-mask> u
ESC [ ? <current-flags> 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 screens stack and activates:
@@ -2331,6 +2371,7 @@ eTerm stores prior flags on the current screens 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:

View File

@@ -356,6 +356,7 @@ class TerminalDisplay {
VertexBufferUniquePtr mVBForeground;
std::vector<VertexBufferUniquePtr> mVBStyles;
TerminalColorScheme mColorScheme;
TerminalColorScheme mInitialColorScheme;
Uint32 mQuadVertex{ 6 };
Primitives mPrimitives;
Vector2u mCurGridPos;

View File

@@ -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();

View File

@@ -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,
};

View File

@@ -450,9 +450,8 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
std::shared_ptr<TerminalDisplay> terminal = std::shared_ptr<TerminalDisplay>(
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 )

View File

@@ -41,6 +41,7 @@
#include <eepp/system/cpu.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/version.hpp>
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 <n> 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 <n> 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<TerminalCursorMode>( 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();
}

View File

@@ -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<int>( 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<T, CursorModeCommand> ) {
mEmulator->setDefaultCursorMode( value.mode );
mWorkerDisplay->setCursorMode( value.mode );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, PaletteCommand> ) {
@@ -554,4 +561,4 @@ void TerminalSession::processCommand( Command&& command ) {
std::move( command ) );
}
}} // namespace eterm::Terminal
}} // namespace eterm::Terminal

View File

@@ -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<Uint32> 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<unsigned char>( mBackground >> 24 );
*g = static_cast<unsigned char>( mBackground >> 16 );
*b = static_cast<unsigned char>( mBackground >> 8 );
return true;
}
if ( index > 1 )
return false;
*r = static_cast<unsigned char>( 1 + index * 3 );
*g = static_cast<unsigned char>( 2 + index * 3 );
*b = static_cast<unsigned char>( 3 + index * 3 );
return true;
}
};
UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "\033[?u\033[>7u\033[<1u\033[<u\033[=3u";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
pty->mBuffer = "\033[0 q\033[6 q\033[>0q";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
term->update();
EXPECT_EQ( TerminalCursorMode::SteadyBar, display->getCursorMode() );
EXPECT_EQ( static_cast<size_t>( 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<MockPty>();
pty->mBuffer = "\033[2 q\033[0 q";
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
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<MockProcess>();
auto display = std::make_shared<MockDisplay>();
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
term->update();
EXPECT_EQ( static_cast<Rune>( 'O' ), display->mFirstGlyph.u );
EXPECT_EQ( static_cast<Rune>( 'K' ), display->mSecondGlyph.u );
EXPECT_TRUE( ptyPtr->mWrites.empty() );
}
UTEST( eterm, alternate_scroll_mode_controls_wheel_key_translation ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "\033[?1049h\033[?1007l";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
pty->mBuffer = "\033[?996n\033[?2031h";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
auto process = std::make_unique<MockProcess>();
@@ -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<MockPty>();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
pty->mBuffer = "\033]4;0;?;1;?\a";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
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<MockProcess>();
auto display = std::make_shared<MockDisplay>();
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
term->update();
ASSERT_EQ( static_cast<size_t>( 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<MockPty>();
auto process = std::make_unique<MockProcess>();