Merge remote-tracking branch 'origin/feature/eterm-worker' into develop

This commit is contained in:
Martín Lucas Golini
2026-09-02 00:48:17 -03:00
20 changed files with 3010 additions and 1116 deletions
@@ -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 ? <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 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:
@@ -11,9 +11,8 @@
#include <eepp/window/keycodes.hpp>
#include <eepp/window/window.hpp>
#include <eterm/system/iprocessfactory.hpp>
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/terminalcolorscheme.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminalsession.hpp>
#include <memory>
#include <unordered_map>
@@ -128,7 +127,7 @@ class TerminalKeyMap {
extern TerminalKeyMap terminalKeyMap;
class TerminalDisplay : public ITerminalDisplay {
class TerminalDisplay {
public:
enum class EventType {
TITLE,
@@ -136,6 +135,10 @@ class TerminalDisplay : public ITerminalDisplay {
SCROLL_HISTORY,
HISTORY_LENGTH_CHANGE,
PROCESS_EXIT,
BELL,
CLIPBOARD,
RESTART_FAILURE,
WORKER_ERROR,
UNKNOWN
};
@@ -145,11 +148,8 @@ class TerminalDisplay : public ITerminalDisplay {
};
typedef std::function<void( const TerminalDisplay::Event& event )> EventFunc;
static std::shared_ptr<TerminalDisplay>
create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
std::shared_ptr<TerminalEmulator>&& terminalEmulator,
const bool& useFrameBuffer = false );
using DataFunc = std::function<void( const char*, size_t )>;
using PromptStateChangedFunc = std::function<void( PromptState, std::string_view )>;
static std::shared_ptr<TerminalDisplay>
create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
@@ -158,25 +158,16 @@ class TerminalDisplay : public ITerminalDisplay {
IProcessFactory* processFactory = nullptr, bool useFrameBuffer = false,
bool keepAlive = true, const std::unordered_map<std::string, std::string>& env = {} );
virtual ~TerminalDisplay();
~TerminalDisplay();
virtual void resetColors();
virtual int resetColor( const Uint32& index, const char* name );
virtual bool getColor( const Uint32& index, unsigned char* r, unsigned char* g,
unsigned char* b );
void resetColors();
int resetColor( const Uint32& index, const char* name );
bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, unsigned char* b );
virtual void setTitle( const char* title );
virtual void setIconTitle( const char* title );
void setClipboard( const char* text );
const char* getClipboard() const;
virtual void setClipboard( const char* text );
virtual const char* getClipboard() const;
virtual bool drawBegin( Uint32 columns, Uint32 rows );
virtual void drawLine( Line line, int x1, int y, int x2 );
virtual void drawCursor( int cx, int cy, TerminalGlyph g, int ox, int oy, TerminalGlyph og );
virtual void drawEnd();
virtual bool update( bool isMouseOverMe = true );
bool update( bool isMouseOverMe = true );
void executeFile( const std::string& cmd );
@@ -188,20 +179,20 @@ class TerminalDisplay : public ITerminalDisplay {
void draw();
virtual void onMouseDoubleClick( const Vector2i& pos, const Uint32& flags );
void onMouseDoubleClick( const Vector2i& pos, const Uint32& flags );
virtual void onMouseMove( const Vector2i& pos, const Uint32& flags );
void onMouseMove( const Vector2i& pos, const Uint32& flags );
virtual void onMouseDown( const Vector2i& pos, const Uint32& flags );
void onMouseDown( const Vector2i& pos, const Uint32& flags );
virtual void onMouseUp( const Vector2i& pos, const Uint32& flags );
void onMouseUp( const Vector2i& pos, const Uint32& flags );
virtual void onTextInput( const Uint32& chr );
void onTextInput( const Uint32& chr );
virtual void onTextEditing( const String& text, const Int32& start, const Int32& length );
void onTextEditing( const String& text, const Int32& start, const Int32& length );
virtual void onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod,
const Scancode& scancode );
void onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod,
const Scancode& scancode );
bool isRegisteredShortcut( const Keycode& keyCode, const Uint32& mod ) const;
@@ -251,14 +242,40 @@ class TerminalDisplay : public ITerminalDisplay {
void setPadding( const Rectf& padding );
const std::shared_ptr<TerminalEmulator>& getTerminal() const;
const std::shared_ptr<TerminalSession>& getSession() const;
virtual void attach( TerminalEmulator* terminal );
std::string getSelection();
bool hasSelection() const;
TerminalSelectionMode getSelectionMode() const;
int getProcessId() const;
int getExitCode() const;
void terminate();
void setAllowMemoryTrimming( bool allow );
void setDataCallback( DataFunc callback );
void setPromptStateChangedCallback( PromptStateChangedFunc callback );
void setCursorMode( TerminalCursorMode mode );
TerminalCursorMode getCursorMode() const;
int scrollSize() const;
int rowCount() const;
int scrollPosition() const;
Uint64 scrollTo( int position );
Uint64 lastAppliedScrollCommand() const;
Uint32 pushEventCallback( const EventFunc& func );
void popEventCallback( const Uint32& id );
@@ -295,12 +312,14 @@ class TerminalDisplay : public ITerminalDisplay {
protected:
EE::Window::Window* mWindow;
std::vector<TerminalGlyph> mBuffer;
std::vector<Color> mColors;
std::shared_ptr<TerminalEmulator> mTerminal;
std::shared_ptr<TerminalSession> mSession;
std::shared_ptr<const TerminalSnapshot> mSnapshot;
mutable std::string mClipboardUtf8;
Uint32 mNumCallBacks;
Uint32 mNumCallBacks{ 0 };
std::map<Uint32, EventFunc> mCallbacks;
DataFunc mDataCallback;
PromptStateChangedFunc mPromptStateChangedCallback;
Font* mFont{ nullptr };
Float mFontSize{ 12 };
@@ -322,11 +341,14 @@ class TerminalDisplay : public ITerminalDisplay {
bool mAlreadyClickedMButton{ false };
bool mKeepAlive{ true };
bool mDraggingSel{ false };
int mMode{ MODE_VISIBLE | MODE_FOCUSED };
TerminalCursorMode mCursorMode{ SteadyUnderline };
Clock mClock;
Clock mLastDoubleClick;
Uint32 mColumns{ 0 };
Uint32 mRows{ 0 };
Uint32 mClickStep{ 5 };
Uint64 mSnapshotGeneration{ 0 };
FontHinting mFontHinting{ FontHinting::Full };
FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale };
FrameBufferUniquePtr mFrameBuffer;
@@ -334,6 +356,7 @@ class TerminalDisplay : public ITerminalDisplay {
VertexBufferUniquePtr mVBForeground;
std::vector<VertexBufferUniquePtr> mVBStyles;
TerminalColorScheme mColorScheme;
TerminalColorScheme mInitialColorScheme;
Uint32 mQuadVertex{ 6 };
Primitives mPrimitives;
Vector2u mCurGridPos;
@@ -356,9 +379,13 @@ class TerminalDisplay : public ITerminalDisplay {
void onSizeChange();
virtual void onProcessExit( int exitCode );
void onProcessExit( int exitCode );
virtual void onScrollPositionChange();
void consumeSnapshot();
void drainSessionEvents();
TerminalColorPalette makeColorPalette() const;
void sendEvent( const TerminalDisplay::Event& event );
@@ -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];
};
@@ -155,6 +158,9 @@ class TerminalEmulator final {
void redraw();
/** Worker-owned terminal state reset (RIS semantics without replacing the PTY/process). */
void reset();
void logError( const char* err );
/** @return If the tty read was completed or there's still buffer to read (true completed) */
@@ -245,6 +251,15 @@ class TerminalEmulator final {
void setAllowMemoryTrimnming( bool allowMemoryTrimnming );
/** 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;
@@ -277,8 +292,9 @@ class TerminalEmulator final {
bool mDirty{ true };
bool mAllDirty{ true };
uint8_t mDeferredPresentationBatches{ 0 };
Clock mPresentationClock;
Time mPresentationInterval{ Microseconds( 1000000.0 / 60.0 ) };
Clock mSynchronizedUpdateClock;
bool mAllowMemoryTrimnming{ false };
int mExitCode;
@@ -299,8 +315,12 @@ class TerminalEmulator final {
int mAllowAltScreen;
int mAllowWindowOps;
TerminalCursorMode mDefaultCursorMode{ SteadyUnderline };
bool mColorSchemeNotifications{ false };
int mColorScheme{ 0 };
std::string mCurrentWorkingDirectory;
Vector2i mLastMousePosition{ -1, -1 };
PromptState mPromptState{ PromptState::Unknown };
PromptStateChangedCb mPromptStateChangedCb;
DataCb mDataCb;
@@ -355,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 );
@@ -366,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 );
@@ -406,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();
@@ -0,0 +1,249 @@
#ifndef ETERM_TERMINALSESSION_HPP
#define ETERM_TERMINALSESSION_HPP
#include <eepp/math/vector2.hpp>
#include <eepp/window/keycodes.hpp>
#include <eterm/system/iprocess.hpp>
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <variant>
#include <vector>
using namespace EE;
using namespace EE::Math;
using namespace EE::Window;
using namespace eterm::System;
namespace eterm { namespace Terminal {
/** Immutable worker-to-UI presentation state. Cell selection is already applied as ATTR_REVERSE. */
struct TerminalSnapshot {
std::vector<TerminalGlyph> cells;
std::vector<Uint8> dirtyRows;
std::string title;
std::string currentWorkingDirectory;
std::string selection;
Uint64 generation{ 0 };
Uint64 lastAppliedScrollCommand{ 0 };
Vector2i cursor;
TerminalGlyph cursorGlyph;
int columns{ 0 };
int rows{ 0 };
int historyLength{ 0 };
int scrollPosition{ 0 };
int windowMode{ MODE_VISIBLE | MODE_FOCUSED };
int processId{ 0 };
int exitCode{ 0 };
Uint32 presentationRate{ 60 };
TerminalCursorMode cursorMode{ SteadyUnderline };
TerminalSelectionMode selectionMode{ SEL_IDLE };
PromptState promptState{ PromptState::Unknown };
bool cursorVisible{ false };
bool cursorSelected{ false };
bool hasSelection{ false };
bool altScreen{ false };
bool processExited{ false };
/** Dirty rows describe only the transition from the immediately preceding generation. */
bool dirtyRowsFollow( Uint64 previousGeneration ) const {
return generation == previousGeneration + 1;
}
};
struct TerminalColorPalette {
std::vector<Uint32> colors;
Uint32 cursor{ 0 };
Uint32 foreground{ 0 };
Uint32 background{ 0 };
};
/**
* Per-terminal worker/session.
*
* After create() returns, the worker thread exclusively owns the emulator, PTY, process, parser,
* history, selection, and cursor. UI code may only enqueue commands, drain events, or retain an
* immutable snapshot returned by snapshot(). No session callback is invoked by the worker.
*/
class TerminalSession final : public std::enable_shared_from_this<TerminalSession> {
public:
using PtyPtr = std::unique_ptr<IPseudoTerminal>;
using ProcPtr = std::unique_ptr<IProcess>;
enum class EventType : Uint8 {
Title,
IconTitle,
HistoryLength,
ScrollPosition,
Bell,
Clipboard,
ProcessExit,
RestartFailure,
SnapshotReady,
Data,
PromptState,
Color,
Error
};
struct Event {
EventType type{ EventType::Error };
std::string data;
Uint64 generation{ 0 };
int value{ 0 };
PromptState promptState{ PromptState::Unknown };
};
static std::shared_ptr<TerminalSession> create( PtyPtr&& pty, ProcPtr&& process,
size_t historySize,
TerminalColorPalette palette = {} );
~TerminalSession();
TerminalSession( const TerminalSession& ) = delete;
TerminalSession( TerminalSession&& ) = delete;
TerminalSession& operator=( const TerminalSession& ) = delete;
TerminalSession& operator=( TerminalSession&& ) = delete;
void write( std::string data, bool mayEcho = true );
void writeRaw( std::string data );
void resize( int columns, int rows );
void scrollUp( int amount );
void scrollDown( int amount );
/** Returns an ordered command id that is copied into snapshots after the scroll is applied. */
Uint64 scrollTo( int position );
void selectionStart( int column, int row, int snap );
void selectionExtend( int column, int row, int type, bool done );
void selectionClear();
void mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
Uint32 modifiers );
void setFocus( bool focus );
void setCursorMode( TerminalCursorMode mode );
void setColorPalette( TerminalColorPalette palette );
void setAllowMemoryTrimming( bool allow );
void setPresentationRate( Uint32 framesPerSecond );
void setDataEventsEnabled( bool enabled );
void setPromptEventsEnabled( bool enabled );
void reset();
void terminate();
void restart( PtyPtr&& pty, ProcPtr&& process );
void shutdown();
std::shared_ptr<const TerminalSnapshot> snapshot() const;
std::vector<Event> drainEvents();
/** Bounded exact-selection request. Returns no value on timeout or during shutdown. */
std::optional<std::string>
requestSelection( std::chrono::milliseconds timeout = std::chrono::milliseconds( 50 ) );
private:
class WorkerDisplay;
struct SelectionResponse;
struct WriteCommand {
std::string data;
bool mayEcho{ true };
};
struct WriteRawCommand {
std::string data;
};
struct ResizeCommand {
int columns{ 0 };
int rows{ 0 };
};
struct ScrollCommand {
int amount{ 0 };
int direction{ 0 };
Uint64 commandId{ 0 };
};
struct SelectionStartCommand {
int column{ 0 };
int row{ 0 };
int snap{ 0 };
};
struct SelectionExtendCommand {
int column{ 0 };
int row{ 0 };
int type{ 0 };
bool done{ false };
};
struct MouseCommand {
TerminalMouseEventType type{ TerminalMouseEventType::MouseMotion };
Vector2i position;
Uint32 flags{ 0 };
Uint32 modifiers{ 0 };
};
struct BoolCommand {
bool value{ false };
};
struct CursorModeCommand {
TerminalCursorMode mode{ SteadyUnderline };
};
struct PaletteCommand {
TerminalColorPalette palette;
};
struct PresentationRateCommand {
Uint32 framesPerSecond{ 60 };
};
struct RestartCommand {
PtyPtr pty;
ProcPtr process;
};
struct SelectionRequestCommand {
std::shared_ptr<SelectionResponse> response;
};
struct SelectionClearCommand {};
struct ResetCommand {};
struct TerminateCommand {};
struct AllowTrimCommand : BoolCommand {};
struct DataEventsCommand : BoolCommand {};
struct PromptEventsCommand : BoolCommand {};
struct FocusCommand : BoolCommand {};
using Command =
std::variant<WriteCommand, WriteRawCommand, ResizeCommand, ScrollCommand,
SelectionStartCommand, SelectionExtendCommand, SelectionClearCommand,
MouseCommand, FocusCommand, CursorModeCommand, PaletteCommand,
PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
PromptEventsCommand, TerminateCommand, RestartCommand, ResetCommand,
SelectionRequestCommand>;
TerminalSession( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
TerminalColorPalette palette );
void start();
bool enqueue( Command&& command );
void workerLoop();
void processCommands();
void processCommand( Command&& command );
void enqueueEvent( Event event, bool coalescable );
void publishSnapshot( std::shared_ptr<const TerminalSnapshot> snapshot );
std::shared_ptr<WorkerDisplay> mWorkerDisplay;
std::unique_ptr<TerminalEmulator> mEmulator;
std::thread mWorker;
std::mutex mShutdownMutex;
mutable std::mutex mCommandMutex;
std::condition_variable mCommandCondition;
std::deque<Command> mCommands;
std::mutex mEventMutex;
std::deque<Event> mEvents;
mutable std::mutex mPublishedSnapshotMutex;
std::shared_ptr<const TerminalSnapshot> mPublishedSnapshot;
std::atomic<bool> mShutdownRequested{ false };
std::atomic<Uint64> mNextScrollCommand{ 0 };
};
}} // namespace eterm::Terminal
#endif
@@ -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,
};
@@ -128,10 +128,13 @@ class UITerminal : public UIWidget {
ScrollViewType mViewType{ ScrollViewType::Overlay };
ScrollBarMode mVScrollMode{ ScrollBarMode::Auto };
UIScrollBar* mVScroll{ nullptr };
int mScrollOffset;
int mScrollOffset{ 0 };
bool mScrollByBar{ false };
bool mPendingContentSizeChange{ false };
Uint64 mPendingScrollCommand{ 0 };
Clock mMouseClock;
std::shared_ptr<TerminalDisplay> mTerm;
Uint32 mTerminalEventCallbackId{ 0 };
UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay );
+96 -61
View File
@@ -23,6 +23,7 @@
#ifndef _WIN32
#include <eepp/system/filesystem.hpp>
#include <eepp/system/log.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/version.hpp>
#include <eterm/system/process.hpp>
#include <poll.h>
@@ -109,54 +110,83 @@ int Process::getExitCode() const {
Process::Process( int pid ) : mPID( pid ) {}
static void execshell( const char* cmd, const char* const* args, std::string workingDirectory,
const std::unordered_map<std::string, std::string>& env ) {
const struct passwd* pw;
const char* sh;
struct ProcessLaunchData {
std::string executable;
std::string workingDirectory;
std::vector<char*> arguments;
std::vector<std::string> environmentStorage;
std::vector<char*> environment;
};
errno = 0;
if ( ( pw = getpwuid( getuid() ) ) == NULL ) {
if ( errno ) {
fprintf( stderr, "getpwuid: %s\n", strerror( errno ) );
_exit( 1 );
} else {
static bool prepareProcessLaunch( const std::string& program, const std::vector<std::string>& args,
const std::string& workingDirectory,
const std::unordered_map<std::string, std::string>& env,
ProcessLaunchData& launch ) {
long passwordBufferSize = sysconf( _SC_GETPW_R_SIZE_MAX );
if ( passwordBufferSize < 0 )
passwordBufferSize = 16384;
std::vector<char> passwordBuffer( static_cast<size_t>( passwordBufferSize ) );
struct passwd passwordData;
struct passwd* passwordResult = nullptr;
const int passwordError = getpwuid_r( getuid(), &passwordData, passwordBuffer.data(),
passwordBuffer.size(), &passwordResult );
if ( passwordError || !passwordResult ) {
if ( passwordError )
fprintf( stderr, "getpwuid_r: %s\n", strerror( passwordError ) );
else
fprintf( stderr, "who are you?\n" );
_exit( 1 );
}
return false;
}
const struct passwd* pw = passwordResult;
if ( ( sh = getenv( "SHELL" ) ) == NULL )
sh = ( pw->pw_shell[0] ) ? pw->pw_shell : cmd;
launch.executable = Sys::which( program );
if ( launch.executable.empty() )
launch.executable = program;
launch.workingDirectory = workingDirectory.empty() ? pw->pw_dir : workingDirectory;
if ( workingDirectory.empty() )
workingDirectory = pw->pw_dir;
launch.arguments.reserve( args.size() + 2 );
launch.arguments.emplace_back( const_cast<char*>( program.c_str() ) );
for ( const auto& argument : args )
launch.arguments.emplace_back( const_cast<char*>( argument.c_str() ) );
launch.arguments.emplace_back( nullptr );
FileSystem::changeWorkingDirectory( workingDirectory );
auto environment = Sys::getEnvironmentVariables();
environment.erase( "COLUMNS" );
environment.erase( "LINES" );
environment.erase( "TERMCAP" );
environment["LOGNAME"] = pw->pw_name;
environment["USER"] = pw->pw_name;
const char* shell = getenv( "SHELL" );
environment["SHELL"] = shell ? shell : ( pw->pw_shell[0] ? pw->pw_shell : program );
environment["HOME"] = pw->pw_dir;
environment["TERM"] = "xterm-256color";
environment["TERM_PROGRAM"] = "eterm";
environment["TERM_PROGRAM_VERSION"] = EE::Version::getVersionName( false );
environment["COLORTERM"] = "24bit";
for ( const auto& entry : env )
environment[entry.first] = entry.second;
unsetenv( "COLUMNS" );
unsetenv( "LINES" );
unsetenv( "TERMCAP" );
setenv( "LOGNAME", pw->pw_name, 1 );
setenv( "USER", pw->pw_name, 1 );
setenv( "SHELL", sh, 1 );
setenv( "HOME", pw->pw_dir, 1 );
setenv( "TERM", "xterm-256color", 1 );
setenv( "TERM_PROGRAM", "eterm", 1 );
setenv( "TERM_PROGRAM_VERSION", EE::Version::getVersionName( false ).c_str(), 1 );
setenv( "COLORTERM", "24bit", 1 );
launch.environmentStorage.reserve( environment.size() );
for ( const auto& entry : environment )
launch.environmentStorage.emplace_back( entry.first + "=" + entry.second );
launch.environment.reserve( launch.environmentStorage.size() + 1 );
for ( auto& entry : launch.environmentStorage )
launch.environment.emplace_back( entry.data() );
launch.environment.emplace_back( nullptr );
return true;
}
for ( const auto& e : env )
setenv( e.first.c_str(), e.second.c_str(), 1 );
signal( SIGCHLD, SIG_DFL );
signal( SIGHUP, SIG_DFL );
signal( SIGINT, SIG_DFL );
signal( SIGQUIT, SIG_DFL );
signal( SIGTERM, SIG_DFL );
signal( SIGALRM, SIG_DFL );
execvp( cmd, (char* const*)args );
_exit( 1 );
static void resetSignalHandlers() {
struct sigaction action;
memset( &action, 0, sizeof( action ) );
action.sa_handler = SIG_DFL;
sigemptyset( &action.sa_mask );
sigaction( SIGCHLD, &action, nullptr );
sigaction( SIGHUP, &action, nullptr );
sigaction( SIGINT, &action, nullptr );
sigaction( SIGQUIT, &action, nullptr );
sigaction( SIGTERM, &action, nullptr );
sigaction( SIGALRM, &action, nullptr );
}
std::unique_ptr<Process> Process::createWithPipe( const std::string& /*program*/,
@@ -173,37 +203,42 @@ Process::createWithPseudoTerminal( const std::string& program, const std::vector
const std::string& workingDirectory,
Terminal::PseudoTerminal& pseudoTerminal,
const std::unordered_map<std::string, std::string>& env ) {
// The calling process may already contain other threads. Prepare every allocation and libc
// lookup before fork: the child may safely call only async-signal-safe operations until exec
// replaces the inherited multithreaded process image.
ProcessLaunchData launch;
if ( !prepareProcessLaunch( program, args, workingDirectory, env, launch ) )
return nullptr;
int pid = fork();
if ( pid == -1 ) {
fprintf( stderr, "Failed to fork process\n" );
return nullptr;
} else if ( pid == 0 ) {
setsid();
dup2( (int)pseudoTerminal.mSlave, 0 );
dup2( (int)pseudoTerminal.mSlave, 1 );
dup2( (int)pseudoTerminal.mSlave, 2 );
if ( ioctl( (int)pseudoTerminal.mSlave, TIOCSCTTY, NULL ) < 0 ) {
fprintf( stderr, "ioctl TIOCSCTTY failed: %s", strerror( errno ) );
exit( 1 );
const int master = (int)pseudoTerminal.mMaster;
const int slave = (int)pseudoTerminal.mSlave;
if ( setsid() < 0 || dup2( slave, STDIN_FILENO ) < 0 || dup2( slave, STDOUT_FILENO ) < 0 ||
dup2( slave, STDERR_FILENO ) < 0 ) {
_exit( 1 );
}
if ( (int)pseudoTerminal.mSlave > 2 )
close( (int)pseudoTerminal.mSlave );
if ( ioctl( slave, TIOCSCTTY, NULL ) < 0 )
_exit( 1 );
if ( slave > STDERR_FILENO )
close( slave );
if ( master > STDERR_FILENO )
close( master );
if ( chdir( launch.workingDirectory.c_str() ) < 0 )
_exit( 1 );
#ifdef __OpenBSD__
if ( pledge( "stdio getpw proc exec", NULL ) == -1 ) {
fprintf( stderr, "pledge\n" );
exit( 1 );
}
if ( pledge( "stdio proc exec", NULL ) == -1 )
_exit( 1 );
#endif
std::vector<const char*> argsV;
argsV.push_back( program.c_str() );
for ( auto& a : args ) {
argsV.push_back( a.c_str() );
}
argsV.push_back( nullptr );
execshell( program.c_str(), argsV.data(), workingDirectory, env );
resetSignalHandlers();
execve( launch.executable.c_str(), launch.arguments.data(), launch.environment.data() );
_exit( 1 );
} else {
pseudoTerminal.mSlave.release();
return std::unique_ptr<Process>( new Process( pid ) );
@@ -116,7 +116,8 @@ bool PseudoTerminal::resize( int columns, int rows ) {
w.ws_ypixel = 0;
bool masterResized = ioctl( (int)mMaster, TIOCSWINSZ, &w ) >= 0;
bool slaveResized = mSlave.handle() != -1 ? ioctl( mSlave.handle(), TIOCSWINSZ, &w ) >= 0 : false;
bool slaveResized =
mSlave.handle() != -1 ? ioctl( mSlave.handle(), TIOCSWINSZ, &w ) >= 0 : false;
if ( !masterResized && !slaveResized ) {
perror( "PseudoTerminal::Resize" );
@@ -226,6 +227,17 @@ std::unique_ptr<PseudoTerminal> PseudoTerminal::create( int columns, int rows )
return nullptr;
}
// The selected slave is duplicated onto standard input/output/error before exec. The original
// PTY descriptors must not otherwise survive exec or leak into unrelated child processes.
const int masterDescriptorFlags = fcntl( master, F_GETFD );
const int slaveDescriptorFlags = fcntl( slave, F_GETFD );
if ( masterDescriptorFlags < 0 || slaveDescriptorFlags < 0 ||
fcntl( master, F_SETFD, masterDescriptorFlags | FD_CLOEXEC ) < 0 ||
fcntl( slave, F_SETFD, slaveDescriptorFlags | FD_CLOEXEC ) < 0 ) {
perror( "PseudoTerminal::create(fcntl FD_CLOEXEC)" );
Log::error( "PseudoTerminal::create(fcntl FD_CLOEXEC)" );
return nullptr;
}
int flags = fcntl( master, F_GETFL, 0 );
fcntl( master, F_SETFL, flags | O_NONBLOCK );
@@ -371,15 +371,6 @@ static const Color colormapped[256] = {
Color( 208, 208, 208 ), Color( 218, 218, 218 ), Color( 228, 228, 228 ),
Color( 238, 238, 238 ) };
std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
std::shared_ptr<TerminalEmulator>&& terminalEmulator, const bool& useFrameBuffer ) {
std::shared_ptr<TerminalDisplay> terminal = std::shared_ptr<TerminalDisplay>(
new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) );
terminal->mTerminal = std::move( terminalEmulator );
return terminal;
}
static Sizei gridSizeFromTermDimensions( Font* font, const Float& fontSize,
const Sizef& pixelsSize ) {
auto fontHeight = (Float)font->getFontHeight( fontSize );
@@ -390,6 +381,19 @@ static Sizei gridSizeFromTermDimensions( Font* font, const Float& fontSize,
return { clipColumns, clipRows };
}
static Uint32 presentationRateForWindow( EE::Window::Window* window ) {
if ( !window )
return 60;
Uint32 presentationRate = window->getFrameRateLimit();
if ( presentationRate == 0 && Engine::existsSingleton() &&
Engine::instance()->getDisplayManager() ) {
if ( auto* display = Engine::instance()->getDisplayManager()->getDisplayIndex(
window->getCurrentDisplayIndex() ) )
presentationRate = display->getRefreshRate();
}
return presentationRate > 0 ? presentationRate : 60;
}
std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
std::string program, std::vector<std::string> args, const std::string& workingDir,
@@ -446,8 +450,14 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
std::shared_ptr<TerminalDisplay> terminal = std::shared_ptr<TerminalDisplay>(
new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) );
terminal->mTerminal = TerminalEmulator::create( std::move( pseudoTerminal ),
std::move( process ), terminal, historySize );
terminal->mSession = TerminalSession::create( std::move( pseudoTerminal ), std::move( process ),
historySize, terminal->makeColorPalette() );
if ( !terminal->mSession ) {
if ( freeProcessFactory )
eeSAFE_DELETE( processFactory );
return nullptr;
}
terminal->mSession->setPresentationRate( presentationRateForWindow( window ) );
terminal->mProgram = program;
terminal->mArgs = args;
terminal->mEnv = env;
@@ -461,23 +471,26 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
return terminal;
}
TerminalDisplay::~TerminalDisplay() = default;
TerminalDisplay::~TerminalDisplay() {
if ( mSession )
mSession->shutdown();
}
TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize,
const Sizef& pixelsSize, const bool& useFrameBuffer ) :
ITerminalDisplay(),
mWindow( window ),
mFont( font ),
mFontSize( fontSize ),
mSize( pixelsSize ),
mUseFrameBuffer( useFrameBuffer ),
mColorScheme( TerminalColorScheme::getDefault() ) {
mColorScheme( TerminalColorScheme::getDefault() ),
mInitialColorScheme( mColorScheme ) {
TerminalGlyph defaultGlyph;
defaultGlyph.mode = ATTR_INVISIBLE;
mCursorGlyph = defaultGlyph;
mColors.resize( eeARRAY_SIZE( colormapped ), Color::Transparent );
mBuffer.resize( mColumns * mRows, defaultGlyph );
( (int&)mMode ) |= MODE_FOCUSED;
mMode |= MODE_FOCUSED;
resetColors();
Sizei gridSize( gridSizeFromTermDimensions( mFont, mFontSize, mSize - mPadding * 2.f ) );
mDirtyLines.resize( gridSize.getHeight(), 1 );
@@ -491,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;
}
@@ -536,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;
@@ -608,26 +625,40 @@ void TerminalDisplay::setPadding( const Rectf& padding ) {
}
}
const std::shared_ptr<TerminalEmulator>& TerminalDisplay::getTerminal() const {
return mTerminal;
}
void TerminalDisplay::attach( TerminalEmulator* terminal ) {
ITerminalDisplay::attach( terminal );
onSizeChange();
const std::shared_ptr<TerminalSession>& TerminalDisplay::getSession() const {
return mSession;
}
int TerminalDisplay::scrollSize() const {
return mEmulator ? mEmulator->scrollSize() : 0;
return mSnapshot ? mSnapshot->historyLength : 0;
}
int TerminalDisplay::rowCount() const {
return mEmulator ? mEmulator->rowCount() : 0;
return mSnapshot ? mSnapshot->rows : 0;
}
int TerminalDisplay::scrollPosition() const {
return mSnapshot ? mSnapshot->scrollPosition : 0;
}
Uint64 TerminalDisplay::scrollTo( int position ) {
return mSession ? mSession->scrollTo( position ) : 0;
}
Uint64 TerminalDisplay::lastAppliedScrollCommand() const {
return mSnapshot ? mSnapshot->lastAppliedScrollCommand : 0;
}
void TerminalDisplay::sendEvent( const Event& event ) {
for ( auto it : mCallbacks )
it.second( event );
std::vector<Uint32> callbacks;
callbacks.reserve( mCallbacks.size() );
for ( const auto& callback : mCallbacks )
callbacks.emplace_back( callback.first );
for ( const Uint32 callbackId : callbacks ) {
auto callback = mCallbacks.find( callbackId );
if ( callback != mCallbacks.end() )
callback->second( event );
}
}
Uint32 TerminalDisplay::pushEventCallback( const EventFunc& func ) {
@@ -650,18 +681,21 @@ const TerminalColorScheme& TerminalDisplay::getColorScheme() const {
}
void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) {
mInitialColorScheme = colorScheme;
mColorScheme = colorScheme;
resetColors();
if ( mSession )
mSession->setColorPalette( makeColorPalette() );
invalidateLines();
}
bool TerminalDisplay::isAppCapturingMouse() const {
return mTerminal &&
return mSession &&
( mMode & ( MODE_MOUSEX10 | MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEMANY ) );
}
bool TerminalDisplay::isAltScr() const {
return mEmulator && mEmulator->tisaltscr();
return mSnapshot && mSnapshot->altScreen;
}
const Uint32& TerminalDisplay::getClickStep() const {
@@ -681,18 +715,13 @@ void TerminalDisplay::setKeepAlive( bool keepAlive ) {
}
bool TerminalDisplay::update( bool isMouseOverMe ) {
bool ret = true;
consumeSnapshot();
drainSessionEvents();
if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) {
mMode ^= MODE_BLINK;
mClock.restart();
invalidateCursor();
}
if ( mTerminal ) {
int histi = mTerminal->getHistorySize();
ret = mTerminal->update();
if ( histi != mTerminal->getHistorySize() )
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
}
if ( mAlreadyClickedLButton ) {
if ( !( mWindow->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) {
mWindow->getInput()->captureMouse( false );
@@ -702,42 +731,222 @@ bool TerminalDisplay::update( bool isMouseOverMe ) {
mWindow->getInput()->getPressTrigger() );
}
}
return ret;
return true;
}
void TerminalDisplay::consumeSnapshot() {
if ( !mSession )
return;
auto snapshot = mSession->snapshot();
if ( !snapshot || snapshot->generation == mSnapshotGeneration )
return;
const Vector2i previousCursor = mCursor;
const bool dimensionsChanged = snapshot->columns != static_cast<int>( mColumns ) ||
snapshot->rows != static_cast<int>( mRows );
if ( dimensionsChanged ) {
mColumns = snapshot->columns;
mRows = snapshot->rows;
mDirtyLines.assign( mRows, true );
if ( !mUseFrameBuffer )
initVBOs();
mFullDirty = true;
} else if ( !snapshot->dirtyRowsFollow( mSnapshotGeneration ) ) {
// Atomic publication intentionally allows the worker to lap the renderer. The latest
// snapshot contains every cell, but its dirty rows only cover the immediately preceding
// generation, so a skipped generation requires rebuilding every visible row.
invalidateLines();
} else {
for ( size_t row = 0; row < snapshot->dirtyRows.size(); ++row ) {
if ( snapshot->dirtyRows[row] )
invalidateLine( row );
}
}
mSnapshot = std::move( snapshot );
mSnapshotGeneration = mSnapshot->generation;
mCursor = mSnapshot->cursor;
mCursorGlyph = mSnapshot->cursorGlyph;
mCursorMode = mSnapshot->cursorMode;
const int presentationBits = mMode & MODE_BLINK;
mMode = mSnapshot->windowMode | presentationBits;
if ( mFocus )
mMode |= MODE_FOCUSED;
else
mMode &= ~MODE_FOCUSED;
if ( previousCursor != mCursor ) {
invalidateLine( previousCursor.y );
invalidateCursor();
}
if ( isBlinkingCursor() ) {
mMode |= MODE_BLINK;
mClock.restart();
}
mDirty = true;
}
void TerminalDisplay::drainSessionEvents() {
if ( !mSession )
return;
for ( auto& event : mSession->drainEvents() ) {
switch ( event.type ) {
case TerminalSession::EventType::Title:
sendEvent( { EventType::TITLE, std::move( event.data ) } );
break;
case TerminalSession::EventType::IconTitle:
sendEvent( { EventType::ICON_TITLE, std::move( event.data ) } );
break;
case TerminalSession::EventType::ScrollPosition:
sendEvent( { EventType::SCROLL_HISTORY } );
break;
case TerminalSession::EventType::Bell:
sendEvent( { EventType::BELL } );
break;
case TerminalSession::EventType::Clipboard:
setClipboard( event.data.c_str() );
sendEvent( { EventType::CLIPBOARD } );
break;
case TerminalSession::EventType::ProcessExit:
onProcessExit( event.value );
break;
case TerminalSession::EventType::RestartFailure:
sendEvent( { EventType::RESTART_FAILURE, std::move( event.data ) } );
break;
case TerminalSession::EventType::Data:
if ( mDataCallback )
mDataCallback( event.data.data(), event.data.size() );
break;
case TerminalSession::EventType::PromptState:
if ( mPromptStateChangedCallback )
mPromptStateChangedCallback( event.promptState, event.data );
break;
case TerminalSession::EventType::Color:
if ( event.value < 0 )
resetColors();
else
resetColor( event.value, event.data.empty() ? nullptr : event.data.c_str() );
invalidateLines();
break;
case TerminalSession::EventType::Error:
Log::error( "Terminal worker error: %s", event.data.c_str() );
sendEvent( { EventType::WORKER_ERROR, std::move( event.data ) } );
break;
case TerminalSession::EventType::HistoryLength:
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
break;
case TerminalSession::EventType::SnapshotReady:
break;
}
}
}
TerminalColorPalette TerminalDisplay::makeColorPalette() const {
TerminalColorPalette palette;
palette.colors.reserve( mColors.size() );
for ( const auto& color : mColors )
palette.colors.emplace_back( color.getValue() );
palette.cursor = mColorScheme.getCursor().getValue();
palette.foreground = mColorScheme.getForeground().getValue();
palette.background = mColorScheme.getBackground().getValue();
return palette;
}
std::string TerminalDisplay::getSelection() {
if ( mSession ) {
if ( auto selection = mSession->requestSelection() )
return std::move( *selection );
}
return mSnapshot ? mSnapshot->selection : std::string{};
}
bool TerminalDisplay::hasSelection() const {
return mSnapshot && mSnapshot->hasSelection;
}
TerminalSelectionMode TerminalDisplay::getSelectionMode() const {
return mSnapshot ? mSnapshot->selectionMode : SEL_IDLE;
}
int TerminalDisplay::getProcessId() const {
return mSnapshot ? mSnapshot->processId : 0;
}
int TerminalDisplay::getExitCode() const {
return mSnapshot ? mSnapshot->exitCode : 0;
}
void TerminalDisplay::terminate() {
if ( mSession )
mSession->terminate();
}
void TerminalDisplay::setAllowMemoryTrimming( bool allow ) {
if ( mSession )
mSession->setAllowMemoryTrimming( allow );
}
void TerminalDisplay::setDataCallback( DataFunc callback ) {
mDataCallback = std::move( callback );
if ( mSession )
mSession->setDataEventsEnabled( static_cast<bool>( mDataCallback ) );
}
void TerminalDisplay::setPromptStateChangedCallback( PromptStateChangedFunc callback ) {
mPromptStateChangedCallback = std::move( callback );
if ( mSession )
mSession->setPromptEventsEnabled( static_cast<bool>( mPromptStateChangedCallback ) );
}
void TerminalDisplay::setCursorMode( TerminalCursorMode mode ) {
if ( mCursorMode == mode )
return;
mCursorMode = mode;
if ( mSession )
mSession->setCursorMode( mode );
invalidateCursor();
}
TerminalCursorMode TerminalDisplay::getCursorMode() const {
return mCursorMode;
}
void TerminalDisplay::executeFile( const std::string& cmd ) {
if ( mTerminal ) {
std::string rcmd( cmd + "\r" );
if ( mSession ) {
std::string rcmd;
#if EE_PLATFORM != EE_PLATFORM_WIN
char clearLine = 0x15;
mTerminal->ttywrite( &clearLine, 1, 1 );
rcmd.push_back( 0x15 );
#endif
mTerminal->ttywrite( rcmd.c_str(), rcmd.size(), 1 );
rcmd.append( cmd ).push_back( '\r' );
mSession->write( std::move( rcmd ) );
}
}
void TerminalDisplay::executeBinary( const std::string& binaryPath, const std::string& args ) {
if ( mTerminal ) {
std::string rcmd( "\"" + binaryPath + "\"" + " " + args + "\r" );
if ( mSession ) {
std::string rcmd;
#if EE_PLATFORM != EE_PLATFORM_WIN
char clearLine = 0x15;
mTerminal->ttywrite( &clearLine, 1, 1 );
rcmd.push_back( 0x15 );
#endif
mTerminal->ttywrite( rcmd.c_str(), rcmd.size(), 1 );
rcmd.append( "\"" ).append( binaryPath ).append( "\" " ).append( args ).push_back( '\r' );
mSession->write( std::move( rcmd ) );
}
}
void TerminalDisplay::action( TerminalShortcutAction action ) {
if ( !mSession && action != TerminalShortcutAction::FONTSIZE_GROW &&
action != TerminalShortcutAction::FONTSIZE_SHRINK )
return;
switch ( action ) {
case TerminalShortcutAction::PASTE: {
getClipboard();
if ( !mClipboardUtf8.empty() ) {
if ( mMode & MODE_BRCKTPASTE ) {
mTerminal->write( "\033[200~", 6 );
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
mTerminal->write( "\033[201~", 6 );
mSession->writeRaw( "\033[200~" );
mSession->writeRaw( std::move( mClipboardUtf8 ) );
mSession->writeRaw( "\033[201~" );
} else {
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
mSession->writeRaw( std::move( mClipboardUtf8 ) );
}
}
break;
@@ -746,61 +955,48 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
std::string selection =
mWindow->getClipboard()->hasPrimarySelection()
? mWindow->getClipboard()->getPrimarySelectionText()
: ( mTerminal->hasSelection()
? mTerminal->getSelection()
: mWindow->getClipboard()->getPrimarySelectionText() );
: ( hasSelection() ? getSelection()
: mWindow->getClipboard()->getPrimarySelectionText() );
sanitizeInput( selection );
if ( !selection.empty() ) {
if ( mMode & MODE_BRCKTPASTE ) {
mTerminal->write( "\033[200~", 6 );
mTerminal->write( selection.c_str(), selection.size() );
mTerminal->write( "\033[201~", 6 );
mSession->writeRaw( "\033[200~" );
mSession->writeRaw( std::move( selection ) );
mSession->writeRaw( "\033[201~" );
} else {
mTerminal->write( selection.c_str(), selection.size() );
mSession->writeRaw( std::move( selection ) );
}
}
break;
}
case TerminalShortcutAction::COPY: {
auto selection = mTerminal->getSelection();
auto selection = getSelection();
if ( !selection.empty() )
setClipboard( selection.c_str() );
break;
}
case TerminalShortcutAction::SCROLLUP_SCREEN: {
TerminalArg arg( (int)-mClickStep );
mTerminal->kscrollup( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mSession->scrollUp( -(int)mClickStep );
break;
}
case TerminalShortcutAction::SCROLLDOWN_SCREEN: {
TerminalArg arg( (int)-mClickStep );
mTerminal->kscrolldown( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mSession->scrollDown( -(int)mClickStep );
break;
}
case TerminalShortcutAction::SCROLLUP_ROW: {
TerminalArg arg( (int)mClickStep );
mTerminal->kscrollup( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mSession->scrollUp( mClickStep );
break;
}
case TerminalShortcutAction::SCROLLDOWN_ROW: {
TerminalArg arg( (int)mClickStep );
mTerminal->kscrolldown( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mSession->scrollDown( mClickStep );
break;
}
case TerminalShortcutAction::SCROLLUP_HISTORY: {
TerminalArg arg( (int)INT_MAX );
mTerminal->kscrollup( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mSession->scrollUp( INT_MAX );
break;
}
case TerminalShortcutAction::SCROLLDOWN_HISTORY: {
TerminalArg arg( (int)INT_MAX );
mTerminal->kscrolldown( &arg );
sendEvent( { EventType::SCROLL_HISTORY } );
mSession->scrollDown( INT_MAX );
break;
}
case TerminalShortcutAction::FONTSIZE_GROW: {
@@ -815,17 +1011,7 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
}
bool TerminalDisplay::hasTerminated() const {
return mTerminal->hasExited();
}
void TerminalDisplay::setTitle( const char* title ) {
if ( title )
sendEvent( { EventType::TITLE, std::string( title ) } );
}
void TerminalDisplay::setIconTitle( const char* title ) {
if ( title )
sendEvent( { EventType::ICON_TITLE, std::string( title ) } );
return mSnapshot && mSnapshot->processExited;
}
void TerminalDisplay::setClipboard( const char* text ) {
@@ -862,48 +1048,6 @@ void TerminalDisplay::sanitizeInput( std::string& input ) {
}
}
bool TerminalDisplay::drawBegin( Uint32 columns, Uint32 rows ) {
if ( columns != mColumns || rows != mRows ) {
TerminalGlyph defaultGlyph{};
mBuffer.resize( columns * rows, defaultGlyph );
mColumns = columns;
mRows = rows;
if ( !mUseFrameBuffer )
initVBOs();
invalidateLines();
invalidateCursor();
}
return ( ( mMode & MODE_VISIBLE ) != 0 );
}
void TerminalDisplay::drawLine( Line line, int x1, int y, int x2 ) {
memcpy( &mBuffer[y * mColumns + x1], line, ( x2 - x1 ) * sizeof( TerminalGlyph ) );
for ( int i = x1; i < x2; i++ ) {
if ( mTerminal->selected( i, y ) ) {
mBuffer[y * mColumns + i].mode |= ATTR_REVERSE;
}
}
invalidateLine( y );
}
void TerminalDisplay::drawCursor( int cx, int cy, TerminalGlyph g, int, int, TerminalGlyph ) {
if ( mCursor != Vector2i( cx, cy ) || mCursorGlyph != g ) {
mCursor.x = cx;
mCursor.y = cy;
if ( isBlinkingCursor() ) {
mMode |= MODE_BLINK;
mClock.restart();
}
mCursorGlyph = g;
invalidateCursor();
}
}
void TerminalDisplay::drawEnd() {}
void TerminalDisplay::draw() {
draw( nullptr != mFrameBuffer ? Vector2f( mPadding.Left, mPadding.Top )
: mPosition.floor() + Vector2f( mPadding.Left, mPadding.Top ) );
@@ -914,11 +1058,9 @@ void TerminalDisplay::onMouseDoubleClick( const Vector2i& pos, const Uint32& fla
mLastDoubleClick.restart();
if ( !isAppCapturingMouse() && ( flags & EE_BUTTON_LMASK ) &&
( mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_EMPTY ||
mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_IDLE ) ) {
( getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_IDLE ) ) {
auto gridPos{ positionToGrid( pos ) };
mTerminal->selstart( gridPos.x, gridPos.y, SNAP_WORD );
invalidateLines();
mSession->selectionStart( gridPos.x, gridPos.y, SNAP_WORD );
}
}
@@ -944,16 +1086,15 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
}
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
( mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_EMPTY ||
mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_READY ) ) {
( mDraggingSel || getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_READY ) ) {
auto gridPos{ positionToGrid( pos ) };
mTerminal->selextend(
mSession->selectionExtend(
gridPos.x, gridPos.y,
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR, 0 );
invalidateLines();
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR,
false );
}
mTerminal->mousereport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
}
void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
@@ -967,10 +1108,10 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
mLastDoubleClick.getElapsedTime() < Milliseconds( 300.f ) ) {
mTerminal->selstart( gridPos.x, gridPos.y, SNAP_LINE );
mSession->selectionStart( gridPos.x, gridPos.y, SNAP_LINE );
} else if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) ) {
if ( !mDraggingSel ) {
mTerminal->selstart( gridPos.x, gridPos.y, 0 );
mSession->selectionStart( gridPos.x, gridPos.y, 0 );
mDraggingSel = true;
invalidateLines();
mWindow->getInput()->captureMouse( true );
@@ -993,8 +1134,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
}
}
mTerminal->mousereport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
}
void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
@@ -1003,7 +1144,7 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
}
if ( ( flags & EE_BUTTON_LMASK ) && mWindow->getClipboard()->hasPrimarySelection() ) {
mWindow->getClipboard()->setPrimarySelectionText( mTerminal->getSelection() );
mWindow->getClipboard()->setPrimarySelectionText( getSelection() );
}
Uint32 smod = sanitizeMod( mWindow->getInput()->getModState() );
@@ -1033,15 +1174,15 @@ void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
continue;
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
action( k.action );
return;
}
}
}
mTerminal->mousereport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
flags, mWindow->getInput()->getModState() );
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ), flags,
mWindow->getInput()->getModState() );
}
static inline Color termColor( unsigned int terminalColor, const std::vector<Color>& colors ) {
@@ -1256,7 +1397,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
for ( Uint32 i = 0; i < mColumns; i++ ) {
mCurGridPos = { i, j };
auto& glyph = mBuffer[j * mColumns + i];
const auto& glyph = mSnapshot->cells[j * mColumns + i];
auto fg = termColor( glyph.fg, mColors );
auto bg = termColor( glyph.bg, mColors );
@@ -1328,7 +1469,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
for ( Uint32 i = 0; i < mColumns; i++ ) {
mCurGridPos = { i, j };
auto& glyph = mBuffer[j * mColumns + i];
const auto& glyph = mSnapshot->cells[j * mColumns + i];
auto fg = termColor( glyph.fg, mColors );
auto bg = termColor( glyph.bg, mColors );
Color temp{ Color::Transparent };
@@ -1439,8 +1580,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
invalidateCursor();
}
bool redrawCursor =
!mEmulator->isScrolling() && !IS_SET( MODE_HIDE ) && ( !mUseFrameBuffer || mDirtyCursor );
bool redrawCursor = mSnapshot && mSnapshot->cursorVisible && !IS_SET( MODE_HIDE ) &&
( !mUseFrameBuffer || mDirtyCursor );
bool mustRenderUnderline = false;
if ( redrawCursor ) {
@@ -1448,14 +1589,14 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
Color drawcol;
if ( IS_SET( MODE_REVERSE ) ) {
if ( mEmulator->isSelected( mCursor.x, mCursor.y ) ) {
if ( mSnapshot->cursorSelected ) {
drawcol = mColorScheme.getCursor();
} else {
drawcol = mColorScheme.getBackground();
}
} else {
drawcol = mEmulator->isSelected( mCursor.x, mCursor.y ) ? mColorScheme.getBackground()
: mColorScheme.getCursor();
drawcol =
mSnapshot->cursorSelected ? mColorScheme.getBackground() : mColorScheme.getCursor();
}
mPrimitives.setColor( drawcol );
@@ -1564,7 +1705,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
}
void TerminalDisplay::drawBg( bool toFBO ) {
auto defaultBg = termColor( mEmulator->getDefaultBackground(), mColors );
auto defaultBg = mColorScheme.getBackground();
Primitives p;
p.setForceDraw( toFBO );
p.setColor( defaultBg );
@@ -1577,7 +1718,7 @@ void TerminalDisplay::drawBg( bool toFBO ) {
}
void TerminalDisplay::draw( const Vector2f& pos ) {
if ( !mEmulator || !mTerminal )
if ( !mSession || !mSnapshot )
return;
mDrawing = true;
@@ -1625,10 +1766,10 @@ Vector2i TerminalDisplay::positionToGrid( const Vector2i& pos ) {
}
// All these checks are because there's a very rare bug I cannot find how it happens
auto termSize = mTerminal->getSize();
auto termSize = mSnapshot ? Vector2i( mSnapshot->columns, mSnapshot->rows ) : Vector2i::Zero;
eeASSERT( mouseX >= 0 && mouseX <= mTerminal->getSize().x );
eeASSERT( mouseY >= 0 && mouseY <= mTerminal->getSize().y );
eeASSERT( mouseX >= 0 && mouseX <= termSize.x );
eeASSERT( mouseY >= 0 && mouseY <= termSize.y );
mouseX = eeclamp( mouseX, 0, termSize.x );
mouseY = eeclamp( mouseY, 0, termSize.y );
@@ -1641,18 +1782,10 @@ void TerminalDisplay::onSizeChange() {
mFont, mFontSize,
mSize - Vector2f( mPadding.Left + mPadding.Right, mPadding.Top + mPadding.Bottom ) ) );
if ( mTerminal ) {
if ( gridSize.getWidth() != mTerminal->getNumColumns() ||
gridSize.getHeight() != mTerminal->getNumRows() ) {
mTerminal->resize( gridSize.getWidth(), gridSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
} else if ( mEmulator ) {
if ( gridSize.getWidth() != mEmulator->getNumColumns() ||
gridSize.getHeight() != mEmulator->getNumRows() ) {
mEmulator->resize( gridSize.getWidth(), gridSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
if ( mSession && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns ||
gridSize.getHeight() != mSnapshot->rows ) ) {
mSession->resize( gridSize.getWidth(), gridSize.getHeight() );
mDirtyLines.resize( gridSize.getHeight(), 1 );
}
if ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() ||
@@ -1670,7 +1803,7 @@ void TerminalDisplay::onSizeChange() {
void TerminalDisplay::onProcessExit( int exitCode ) {
sendEvent( { EventType::PROCESS_EXIT, String::toString( exitCode ) } );
if ( !mTerminal || mProgram.empty() || exitCode != 0 || !mKeepAlive )
if ( !mSession || mProgram.empty() || exitCode != 0 || !mKeepAlive )
return;
auto processFactory = eeNew( ProcessFactory, () );
@@ -1685,35 +1818,34 @@ void TerminalDisplay::onProcessExit( int exitCode ) {
if ( !pseudoTerminal ) {
eeSAFE_DELETE( processFactory );
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to create pseudo terminal\n" );
sendEvent( { EventType::RESTART_FAILURE, "Failed to create pseudo terminal" } );
return;
}
if ( !process ) {
eeSAFE_DELETE( processFactory );
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to spawn process\n" );
sendEvent( { EventType::RESTART_FAILURE, "Failed to spawn process" } );
return;
}
mTerminal->clearHistory();
mTerminal->setPtyAndProcess( std::move( pseudoTerminal ), std::move( process ) );
mSession->restart( std::move( pseudoTerminal ), std::move( process ) );
eeSAFE_DELETE( processFactory );
}
void TerminalDisplay::onScrollPositionChange() {
sendEvent( { EventType::SCROLL_HISTORY } );
}
void TerminalDisplay::onTextInput( const Uint32& chr ) {
if ( !mTerminal )
if ( !mSession )
return;
String input;
input.push_back( chr );
std::string utf8Input( input.toUtf8() );
mTerminal->ttywrite( utf8Input.c_str(), utf8Input.size(), 1 );
mSession->write( std::move( utf8Input ) );
mDirty = true;
}
void TerminalDisplay::onTextEditing( const String&, const Int32&, const Int32& ) {
if ( !mTerminal )
if ( !mSession )
return;
invalidateCursor();
updateIMELocation();
@@ -1734,7 +1866,7 @@ bool TerminalDisplay::isRegisteredShortcut( const Keycode& keyCode, const Uint32
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
continue;
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
return true;
}
}
@@ -1762,7 +1894,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
continue;
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
action( k.action );
return;
}
@@ -1783,7 +1915,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
}
}
mTerminal->ttywrite( &tmp, 1, 1 );
mSession->write( std::string( 1, tmp ) );
return;
}
}
@@ -1802,7 +1934,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
continue;
if ( k.string.size() > 0 ) {
mTerminal->ttywrite( k.string.c_str(), k.string.size(), 1 );
mSession->write( k.string );
return;
}
break;
@@ -1824,7 +1956,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
continue;
if ( k.string.size() > 0 ) {
mTerminal->ttywrite( k.string.c_str(), k.string.size(), 1 );
mSession->write( k.string );
return;
}
break;
@@ -1936,17 +2068,14 @@ void TerminalDisplay::setFocus( bool focus ) {
}
mFocus = focus;
bool modeFocus = mMode & MODE_FOCUSED;
if ( mFocus != modeFocus ) {
if ( mFocus ) {
mMode |= MODE_FOCUSED | MODE_FOCUS;
mWindow->startTextInput();
} else {
mMode ^= MODE_FOCUS | MODE_FOCUSED;
}
if ( mFocus ) {
mMode |= MODE_FOCUSED;
mWindow->startTextInput();
} else {
mMode ^= MODE_FOCUS;
mMode &= ~MODE_FOCUSED;
}
if ( mSession )
mSession->setFocus( focus );
invalidateCursor();
}
@@ -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" ) ) )
@@ -844,6 +891,21 @@ void TerminalEmulator::setAllowMemoryTrimnming( bool allowMemoryTrimnming ) {
mAllowMemoryTrimnming = allowMemoryTrimnming;
}
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 };
}
@@ -958,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 = ' ';
@@ -986,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 ) {
@@ -1415,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++;
}
@@ -1592,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;
@@ -1627,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 ) {
@@ -1702,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 */
@@ -1719,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 ) ) {
@@ -1731,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;
}
@@ -1813,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;
@@ -1852,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 {
@@ -1884,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;
}
}
@@ -1894,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
@@ -1917,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 );
}
@@ -1932,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 */
@@ -2072,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 );
@@ -2095,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 );
}
@@ -2148,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 );
@@ -2157,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] ) {
@@ -2195,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;
@@ -2279,7 +2379,7 @@ void TerminalEmulator::strhandle( void ) {
setClipboard( dec );
xfree( dec );
} else {
fprintf( stderr, "erresc: invalid base64\n" );
terminalDiagnostic( "erresc: invalid base64\n" );
}
}
return;
@@ -2295,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 */
@@ -2328,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();
}
@@ -2338,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;
@@ -2384,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 ) {
@@ -2411,6 +2542,8 @@ void TerminalEmulator::strparse( void ) {
}
void TerminalEmulator::strdump( void ) {
if ( !terminalDiagnosticsEnabled() )
return;
size_t i;
uint c;
@@ -2527,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];
}
@@ -2739,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;
@@ -2992,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;
}
@@ -3201,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*/;
@@ -3232,7 +3366,6 @@ void TerminalEmulator::draw() {
mTerm.ocy = mTerm.c.y;
dpy->drawEnd();
mDeferredPresentationBatches = 0;
mPresentationClock.restart();
}
@@ -3245,6 +3378,11 @@ void TerminalEmulator::redraw() {
draw();
}
void TerminalEmulator::reset() {
treset();
redraw();
}
int TerminalEmulator::xsetcolorname( int x, const char* name ) {
return resetColor( x, name );
}
@@ -3276,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;
@@ -3316,8 +3473,6 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
int len, btn, code;
char buf[40];
static int ox, oy;
for ( btn = 1; btn <= 31 && !( flags & ( 1 << ( btn - 1 ) ) ); btn++ )
;
@@ -3343,7 +3498,7 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
}
if ( type == TerminalMouseEventType::MouseMotion ) {
if ( pos.x == ox && pos.y == oy )
if ( pos == mLastMousePosition )
return;
if ( !xgetmode( MODE_MOUSEMOTION ) && !xgetmode( MODE_MOUSEMANY ) )
return;
@@ -3371,8 +3526,7 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
code = 0;
}
ox = pos.x;
oy = pos.y;
mLastMousePosition = pos;
/* Encode btn into code. If no button is pressed for a motion event in
* MODE_MOUSEMANY, then encode it as a release. */
@@ -3563,7 +3717,6 @@ void TerminalEmulator::resize( int columns, int rows ) {
return;
}
mTerm.is_syncing = true;
tresize( columns, rows );
redraw();
@@ -3583,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();
}
@@ -3598,23 +3756,23 @@ bool TerminalEmulator::update() {
int reads = 0;
Clock readBudgetClock;
bool presentationDeadlineReached = false;
while ( reads < MAX_TTY_READS && ttyread() > 0 ) {
++reads;
if ( mPresentationClock.getElapsedTime() >= mPresentationInterval ) {
presentationDeadlineReached = true;
break;
}
if ( readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) )
break;
}
bool readBudgetSaturated =
reads == MAX_TTY_READS ||
reads == MAX_TTY_READS || presentationDeadlineReached ||
( reads > 0 && readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) );
/* Keep presentation decoupled from every PTY read batch, but bound the
* deferral so sustained output remains visibly live. The time limit handles
* expensive batches; the batch limit guarantees progress when updates are
* individually very fast. */
if ( readBudgetSaturated )
++mDeferredPresentationBatches;
bool presentationDue = !readBudgetSaturated || mDeferredPresentationBatches >= 32 ||
mPresentationClock.getElapsedTime() >= Milliseconds( 75 );
/* Keep presentation decoupled from every PTY read batch. Sustained output publishes on the
* host frame deadline, while a drained/idle burst still publishes immediately. */
bool presentationDue = !readBudgetSaturated || presentationDeadlineReached;
if ( presentationDue && ( reads > 0 || mDirty ) )
draw();
@@ -3626,6 +3784,9 @@ bool TerminalEmulator::update() {
if ( mProcess->hasExited() && !readBudgetSaturated ) {
mExitCode = mProcess->getExitCode();
mStatus = TERMINATED;
// Publish process state together with the final drained frame before the ordered exit
// event.
redraw();
onProcessExit( mExitCode );
}
@@ -0,0 +1,564 @@
#include <eterm/terminal/terminalsession.hpp>
#include <eepp/system/color.hpp>
#include <eepp/system/log.hpp>
#include <algorithm>
#include <cstring>
using namespace EE::System;
namespace eterm { namespace Terminal {
struct TerminalSession::SelectionResponse {
std::mutex mutex;
std::condition_variable condition;
std::string selection;
bool ready{ false };
};
class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
public:
WorkerDisplay( TerminalSession& session, TerminalColorPalette palette ) :
mSession( session ), mInitialPalette( std::move( palette ) ), mPalette( mInitialPalette ) {
mMode |= MODE_FOCUSED;
}
bool drawBegin( Uint32 columns, Uint32 rows ) {
const size_t cellCount = static_cast<size_t>( columns ) * rows;
if ( columns != static_cast<Uint32>( mColumns ) || rows != static_cast<Uint32>( mRows ) ) {
mColumns = columns;
mRows = rows;
mCells.assign( cellCount, TerminalGlyph{} );
mDirtyRows.assign( rows, 1 );
} else {
mDirtyRows.assign( rows, 0 );
}
mCursorVisible = false;
return getMode( MODE_VISIBLE );
}
void drawLine( Line line, int x1, int y, int x2 ) {
if ( y < 0 || y >= mRows || x1 < 0 || x2 > mColumns || x1 >= x2 )
return;
TerminalGlyph* destination = mCells.data() + static_cast<size_t>( y ) * mColumns + x1;
std::memcpy( destination, line + x1, static_cast<size_t>( x2 - x1 ) * sizeof( *line ) );
if ( mEmulator ) {
for ( int column = x1; column < x2; ++column ) {
if ( mEmulator->isSelected( column, y ) )
mCells[static_cast<size_t>( y ) * mColumns + column].mode |= ATTR_REVERSE;
}
}
mDirtyRows[y] = 1;
}
void drawCursor( int cx, int cy, TerminalGlyph glyph, int, int, TerminalGlyph ) {
mCursor = { cx, cy };
mCursorGlyph = glyph;
mCursorVisible = true;
}
void drawEnd() {
auto snapshot = std::make_shared<TerminalSnapshot>();
snapshot->cells = mCells;
snapshot->dirtyRows = mDirtyRows;
snapshot->title = mTitle;
snapshot->generation = ++mGeneration;
snapshot->lastAppliedScrollCommand = mLastAppliedScrollCommand;
snapshot->cursor = mCursor;
snapshot->cursorGlyph = mCursorGlyph;
snapshot->columns = mColumns;
snapshot->rows = mRows;
snapshot->windowMode = mMode;
snapshot->presentationRate = mPresentationRate;
snapshot->cursorMode = mCursorMode;
snapshot->cursorVisible = mCursorVisible;
if ( mEmulator ) {
snapshot->historyLength = mEmulator->scrollSize();
snapshot->scrollPosition = mEmulator->scrollPos();
snapshot->hasSelection = mEmulator->hasSelection();
snapshot->selectionMode = mEmulator->getSelectionMode();
snapshot->cursorSelected =
snapshot->cursorVisible && mEmulator->isSelected( mCursor.x, mCursor.y );
if ( snapshot->hasSelection )
snapshot->selection = mEmulator->getSelection();
snapshot->altScreen = mEmulator->tisaltscr();
snapshot->processExited = mEmulator->hasExited();
snapshot->exitCode = mEmulator->getExitCode();
snapshot->currentWorkingDirectory = mEmulator->getCurrentWorkingDirectory();
snapshot->promptState = mEmulator->getPromptState();
if ( auto* process = mEmulator->getProcess() )
snapshot->processId = process->pid();
}
if ( snapshot->historyLength != mLastHistoryLength ) {
mLastHistoryLength = snapshot->historyLength;
Event event{ EventType::HistoryLength };
event.value = mLastHistoryLength;
mSession.enqueueEvent( std::move( event ), true );
}
mSession.publishSnapshot( std::move( snapshot ) );
}
void bell() { mSession.enqueueEvent( { EventType::Bell }, false ); }
void resetColors() {
mPalette = mInitialPalette;
if ( mEmulator )
mEmulator->notifyColorSchemeChanged();
Event event{ EventType::Color };
event.value = -1;
mSession.enqueueEvent( std::move( event ), false );
}
int resetColor( const Uint32& index, const char* name ) {
Uint32 color = 0;
bool parsed = false;
if ( name && String::startsWith( name, "rgb:" ) ) {
auto components = String::split( std::string( name + 4 ), '/' );
if ( components.size() == 3 ) {
char* ends[3]{};
long rgb[3]{};
for ( size_t i = 0; i < 3; ++i )
rgb[i] = std::strtol( components[i].c_str(), &ends[i], 16 );
if ( ends[0] && ends[1] && ends[2] ) {
color = Color( rgb[0], rgb[1], rgb[2] ).getValue();
parsed = true;
}
}
} else if ( name && Color::isColorString( std::string_view{ name }, true ) ) {
color = Color::fromString( name ).getValue();
parsed = true;
} else if ( !name || String::iequals( "default", name ) ) {
if ( index < mInitialPalette.colors.size() ) {
color = mInitialPalette.colors[index];
parsed = true;
} else if ( index == 256 || index == 257 ) {
color = mInitialPalette.cursor;
parsed = true;
} else if ( index == 258 ) {
color = mInitialPalette.foreground;
parsed = true;
} else if ( index == 259 ) {
color = mInitialPalette.background;
parsed = true;
}
}
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 );
mSession.enqueueEvent( std::move( event ), false );
return 0;
}
bool getColor( const Uint32& index, unsigned char* red, unsigned char* green,
unsigned char* blue ) {
Uint32 color = 0;
if ( index < mPalette.colors.size() )
color = mPalette.colors[index];
else if ( index == 256 || index == 257 )
color = mPalette.cursor;
else if ( index == 258 )
color = mPalette.foreground;
else if ( index == 259 )
color = mPalette.background;
else
return false;
*red = ( color >> 24 ) & 0xFF;
*green = ( color >> 16 ) & 0xFF;
*blue = ( color >> 8 ) & 0xFF;
return true;
}
void setTitle( const char* title ) {
mTitle = title ? title : "";
mSession.enqueueEvent( { EventType::Title, mTitle }, true );
}
void setIconTitle( const char* title ) {
mSession.enqueueEvent( { EventType::IconTitle, title ? title : "" }, true );
}
void setClipboard( const char* text ) {
if ( text )
mSession.enqueueEvent( { EventType::Clipboard, text }, false );
}
void onProcessExit( int exitCode ) {
Event event{ EventType::ProcessExit };
event.value = exitCode;
mSession.enqueueEvent( std::move( event ), false );
}
void onScrollPositionChange() { mSession.enqueueEvent( { EventType::ScrollPosition }, true ); }
void setPalette( TerminalColorPalette palette ) {
mInitialPalette = palette;
mPalette = std::move( palette );
if ( mEmulator )
mEmulator->notifyColorSchemeChanged();
}
void setFocused( bool focused ) {
if ( focused )
mMode |= MODE_FOCUSED;
else
mMode &= ~MODE_FOCUSED;
}
void setLastAppliedScrollCommand( Uint64 commandId ) { mLastAppliedScrollCommand = commandId; }
void setPresentationRate( Uint32 framesPerSecond ) { mPresentationRate = framesPerSecond; }
private:
void setPaletteColor( Uint32 index, Uint32 color ) {
if ( index < mPalette.colors.size() )
mPalette.colors[index] = color;
else if ( index == 256 || index == 257 )
mPalette.cursor = color;
else if ( index == 258 )
mPalette.foreground = color;
else if ( index == 259 )
mPalette.background = color;
}
TerminalSession& mSession;
TerminalColorPalette mInitialPalette;
TerminalColorPalette mPalette;
std::vector<TerminalGlyph> mCells;
std::vector<Uint8> mDirtyRows;
std::string mTitle;
Uint64 mGeneration{ 0 };
Uint64 mLastAppliedScrollCommand{ 0 };
Vector2i mCursor;
TerminalGlyph mCursorGlyph;
int mColumns{ 0 };
int mRows{ 0 };
int mLastHistoryLength{ -1 };
Uint32 mPresentationRate{ 60 };
bool mCursorVisible{ false };
};
std::shared_ptr<TerminalSession> TerminalSession::create( PtyPtr&& pty, ProcPtr&& process,
size_t historySize,
TerminalColorPalette palette ) {
if ( !pty || !process )
return nullptr;
auto session = std::shared_ptr<TerminalSession>( new TerminalSession(
std::move( pty ), std::move( process ), historySize, std::move( palette ) ) );
session->start();
return session;
}
TerminalSession::TerminalSession( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
TerminalColorPalette palette ) {
mWorkerDisplay = std::make_shared<WorkerDisplay>( *this, std::move( palette ) );
mEmulator = TerminalEmulator::create( std::move( pty ), std::move( process ), mWorkerDisplay,
historySize );
// Establish a complete generation before the session becomes concurrently visible.
mEmulator->redraw();
}
TerminalSession::~TerminalSession() {
shutdown();
}
void TerminalSession::start() {
mWorker = std::thread( [this] { workerLoop(); } );
}
void TerminalSession::shutdown() {
std::lock_guard<std::mutex> shutdownLock( mShutdownMutex );
if ( !mShutdownRequested.exchange( true, std::memory_order_acq_rel ) )
mCommandCondition.notify_all();
if ( mWorker.joinable() && mWorker.get_id() != std::this_thread::get_id() )
mWorker.join();
}
bool TerminalSession::enqueue( Command&& command ) {
{
std::lock_guard<std::mutex> lock( mCommandMutex );
if ( mShutdownRequested.load( std::memory_order_relaxed ) )
return false;
mCommands.emplace_back( std::move( command ) );
}
mCommandCondition.notify_one();
return true;
}
void TerminalSession::write( std::string data, bool mayEcho ) {
enqueue( WriteCommand{ std::move( data ), mayEcho } );
}
void TerminalSession::writeRaw( std::string data ) {
enqueue( WriteRawCommand{ std::move( data ) } );
}
void TerminalSession::resize( int columns, int rows ) {
enqueue( ResizeCommand{ columns, rows } );
}
void TerminalSession::scrollUp( int amount ) {
enqueue( ScrollCommand{ amount, -1 } );
}
void TerminalSession::scrollDown( int amount ) {
enqueue( ScrollCommand{ amount, 1 } );
}
Uint64 TerminalSession::scrollTo( int position ) {
const Uint64 commandId = mNextScrollCommand.fetch_add( 1, std::memory_order_relaxed ) + 1;
if ( !enqueue( ScrollCommand{ position, 0, commandId } ) )
return 0;
return commandId;
}
void TerminalSession::selectionStart( int column, int row, int snap ) {
enqueue( SelectionStartCommand{ column, row, snap } );
}
void TerminalSession::selectionExtend( int column, int row, int type, bool done ) {
enqueue( SelectionExtendCommand{ column, row, type, done } );
}
void TerminalSession::selectionClear() {
enqueue( SelectionClearCommand{} );
}
void TerminalSession::mouseReport( TerminalMouseEventType type, Vector2i position, Uint32 flags,
Uint32 modifiers ) {
enqueue( MouseCommand{ type, position, flags, modifiers } );
}
void TerminalSession::setFocus( bool focus ) {
enqueue( FocusCommand{ { focus } } );
}
void TerminalSession::setCursorMode( TerminalCursorMode mode ) {
enqueue( CursorModeCommand{ mode } );
}
void TerminalSession::setColorPalette( TerminalColorPalette palette ) {
enqueue( PaletteCommand{ std::move( palette ) } );
}
void TerminalSession::setAllowMemoryTrimming( bool allow ) {
enqueue( AllowTrimCommand{ { allow } } );
}
void TerminalSession::setPresentationRate( Uint32 framesPerSecond ) {
enqueue( PresentationRateCommand{ framesPerSecond } );
}
void TerminalSession::setDataEventsEnabled( bool enabled ) {
enqueue( DataEventsCommand{ { enabled } } );
}
void TerminalSession::setPromptEventsEnabled( bool enabled ) {
enqueue( PromptEventsCommand{ { enabled } } );
}
void TerminalSession::reset() {
enqueue( ResetCommand{} );
}
void TerminalSession::terminate() {
enqueue( TerminateCommand{} );
}
void TerminalSession::restart( PtyPtr&& pty, ProcPtr&& process ) {
if ( !pty || !process ) {
enqueueEvent( { EventType::RestartFailure, "Invalid PTY or process" }, false );
return;
}
enqueue( RestartCommand{ std::move( pty ), std::move( process ) } );
}
std::shared_ptr<const TerminalSnapshot> TerminalSession::snapshot() const {
std::lock_guard<std::mutex> lock( mPublishedSnapshotMutex );
return mPublishedSnapshot;
}
std::optional<std::string> TerminalSession::requestSelection( std::chrono::milliseconds timeout ) {
if ( mShutdownRequested.load( std::memory_order_acquire ) )
return std::nullopt;
auto response = std::make_shared<SelectionResponse>();
enqueue( SelectionRequestCommand{ response } );
std::unique_lock<std::mutex> lock( response->mutex );
if ( !response->condition.wait_for( lock, timeout, [&response] { return response->ready; } ) )
return std::nullopt;
return std::move( response->selection );
}
std::vector<TerminalSession::Event> TerminalSession::drainEvents() {
std::vector<Event> events;
std::lock_guard<std::mutex> lock( mEventMutex );
events.reserve( mEvents.size() );
while ( !mEvents.empty() ) {
events.emplace_back( std::move( mEvents.front() ) );
mEvents.pop_front();
}
return events;
}
void TerminalSession::enqueueEvent( Event event, bool coalescable ) {
std::lock_guard<std::mutex> lock( mEventMutex );
if ( coalescable ) {
for ( auto it = mEvents.rbegin(); it != mEvents.rend(); ++it ) {
const bool replaceable =
it->type == EventType::Title || it->type == EventType::IconTitle ||
it->type == EventType::HistoryLength || it->type == EventType::ScrollPosition ||
it->type == EventType::SnapshotReady;
if ( !replaceable )
break;
if ( it->type == event.type ) {
*it = std::move( event );
return;
}
}
}
mEvents.emplace_back( std::move( event ) );
}
void TerminalSession::publishSnapshot( std::shared_ptr<const TerminalSnapshot> snapshot ) {
const Uint64 generation = snapshot->generation;
{
std::lock_guard<std::mutex> lock( mPublishedSnapshotMutex );
mPublishedSnapshot = std::move( snapshot );
}
Event event{ EventType::SnapshotReady };
event.generation = generation;
enqueueEvent( std::move( event ), true );
}
void TerminalSession::workerLoop() {
while ( !mShutdownRequested.load( std::memory_order_acquire ) ) {
processCommands();
if ( mShutdownRequested.load( std::memory_order_acquire ) )
break;
const bool inputDrained = mEmulator->update();
if ( !inputDrained )
continue;
std::unique_lock<std::mutex> lock( mCommandMutex );
if ( mCommands.empty() && !mShutdownRequested.load( std::memory_order_relaxed ) )
mCommandCondition.wait_for( lock, std::chrono::milliseconds( 8 ) );
}
mShutdownRequested.store( true, std::memory_order_release );
mEmulator.reset();
mWorkerDisplay.reset();
}
void TerminalSession::processCommands() {
std::deque<Command> commands;
{
std::lock_guard<std::mutex> lock( mCommandMutex );
commands.swap( mCommands );
}
while ( !commands.empty() && !mShutdownRequested.load( std::memory_order_relaxed ) ) {
processCommand( std::move( commands.front() ) );
commands.pop_front();
}
}
void TerminalSession::processCommand( Command&& command ) {
std::visit(
[this]( auto&& value ) {
using T = std::decay_t<decltype( value )>;
if constexpr ( std::is_same_v<T, WriteCommand> ) {
mEmulator->ttywrite( value.data.data(), value.data.size(), value.mayEcho );
} else if constexpr ( std::is_same_v<T, WriteRawCommand> ) {
mEmulator->write( value.data.data(), value.data.size() );
} else if constexpr ( std::is_same_v<T, ResizeCommand> ) {
mEmulator->resize( value.columns, value.rows );
} else if constexpr ( std::is_same_v<T, ScrollCommand> ) {
TerminalArg argument( value.amount );
if ( value.direction < 0 )
mEmulator->kscrollup( &argument );
else if ( value.direction > 0 )
mEmulator->kscrolldown( &argument );
else {
mEmulator->kscrollto( &argument );
mWorkerDisplay->setLastAppliedScrollCommand( value.commandId );
}
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionStartCommand> ) {
mEmulator->selstart( value.column, value.row, value.snap );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionExtendCommand> ) {
mEmulator->selextend( value.column, value.row, value.type, value.done );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionClearCommand> ) {
mEmulator->selclear();
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, MouseCommand> ) {
mEmulator->mousereport( value.type, value.position, value.flags, value.modifiers );
} else if constexpr ( std::is_same_v<T, FocusCommand> ) {
if ( mWorkerDisplay->getMode( MODE_FOCUS ) )
mEmulator->ttywrite( value.value ? "\033[I" : "\033[O", 3, false );
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> ) {
mWorkerDisplay->setPalette( std::move( value.palette ) );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, PresentationRateCommand> ) {
const Uint32 framesPerSecond = eeclamp<Uint32>( value.framesPerSecond, 1, 1000 );
mEmulator->setPresentationInterval(
Microseconds( 1000000.0 / static_cast<double>( framesPerSecond ) ) );
mWorkerDisplay->setPresentationRate( framesPerSecond );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, AllowTrimCommand> ) {
mEmulator->setAllowMemoryTrimnming( value.value );
} else if constexpr ( std::is_same_v<T, DataEventsCommand> ) {
if ( value.value ) {
mEmulator->setDataCb( [this]( const char* data, size_t size ) {
enqueueEvent( { EventType::Data, std::string( data, size ) }, false );
} );
} else {
mEmulator->setDataCb( {} );
}
} else if constexpr ( std::is_same_v<T, PromptEventsCommand> ) {
if ( value.value ) {
mEmulator->setPromptStateChangedCb(
[this]( PromptState state, std::string_view data ) {
Event event{ EventType::PromptState, std::string( data ) };
event.promptState = state;
enqueueEvent( std::move( event ), false );
} );
} else {
mEmulator->setPromptStateChangedCb( {} );
}
} else if constexpr ( std::is_same_v<T, TerminateCommand> ) {
mEmulator->terminate();
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, ResetCommand> ) {
mEmulator->reset();
} else if constexpr ( std::is_same_v<T, RestartCommand> ) {
mEmulator->clearHistory();
mEmulator->setPtyAndProcess( std::move( value.pty ), std::move( value.process ) );
mEmulator->redraw();
} else if constexpr ( std::is_same_v<T, SelectionRequestCommand> ) {
std::lock_guard<std::mutex> lock( value.response->mutex );
value.response->selection = mEmulator->getSelection();
value.response->ready = true;
value.response->condition.notify_one();
}
},
std::move( command ) );
}
}} // namespace eterm::Terminal
+68 -43
View File
@@ -31,7 +31,10 @@ UITerminal* UITerminal::New( const std::shared_ptr<TerminalDisplay>& terminalDis
return eeNew( UITerminal, ( terminalDisplay ) );
}
UITerminal::~UITerminal() {}
UITerminal::~UITerminal() {
if ( mTerm && mTerminalEventCallbackId )
mTerm->popEventCallback( mTerminalEventCallbackId );
}
Uint32 UITerminal::getType() const {
return UI_TYPE_TERMINAL;
@@ -51,28 +54,29 @@ void UITerminal::draw() {
void UITerminal::registerNewTerminal() {
if ( !mTerm )
return;
mTerm->pushEventCallback( [this]( const TerminalDisplay::Event& event ) {
switch ( event.type ) {
case TerminalDisplay::EventType::TITLE: {
if ( !mIsCustomTitle && mTitle != event.eventData ) {
mTitle = event.eventData;
sendTextEvent( Event::OnTitleChange, mTitle );
mTerminalEventCallbackId =
mTerm->pushEventCallback( [this]( const TerminalDisplay::Event& event ) {
switch ( event.type ) {
case TerminalDisplay::EventType::TITLE: {
if ( !mIsCustomTitle && mTitle != event.eventData ) {
mTitle = event.eventData;
sendTextEvent( Event::OnTitleChange, mTitle );
}
break;
}
case TerminalDisplay::EventType::HISTORY_LENGTH_CHANGE: {
if ( !mTerm->isAltScr() )
onContentSizeChange();
break;
}
case TerminalDisplay::EventType::SCROLL_HISTORY: {
updateScrollPosition();
break;
}
default: {
}
break;
}
case TerminalDisplay::EventType::HISTORY_LENGTH_CHANGE: {
if ( !mTerm->getTerminal()->tisaltscr() )
onContentSizeChange();
break;
}
case TerminalDisplay::EventType::SCROLL_HISTORY: {
updateScrollPosition();
break;
}
default: {
}
}
} );
} );
}
UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay ) :
@@ -109,13 +113,13 @@ UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay
[this] { mTerm->action( TerminalShortcutAction::PASTE_SELECTION ); } );
setCommand( "terminal-copy", [this] { mTerm->action( TerminalShortcutAction::COPY ); } );
setCommand( "terminal-open-link",
[this] { Engine::instance()->openURI( mTerm->getTerminal()->getSelection() ); } );
[this] { Engine::instance()->openURI( mTerm->getSelection() ); } );
subscribeScheduledUpdate();
}
int UITerminal::getContentSize() const {
if ( mTerm && mTerm->getTerminal() )
return mTerm->getTerminal()->getHistorySize() + mTerm->getTerminal()->getNumRows();
if ( mTerm )
return mTerm->scrollSize() + mTerm->rowCount();
return 0;
}
@@ -139,6 +143,13 @@ void UITerminal::onContentSizeChange() {
mVScroll->setPixelsSize( mVScroll->getPixelsSize().getWidth(),
getPixelsSize().getHeight() - mPaddingPx.Top - mPaddingPx.Bottom );
// Changing the page step resizes the thumb and therefore changes the mouse-to-value mapping.
// Keep both the thumb and its range stable while an asynchronous drag scroll is in flight.
if ( mVScroll->isDragging() || mScrollByBar ) {
mPendingContentSizeChange = true;
return;
}
mPendingContentSizeChange = false;
updateScrollPosition();
mVScroll->setPageStep( contentSize > 0 ? ( visibleArea / (Float)contentSize ) : 1.f );
updateScroll();
@@ -179,17 +190,22 @@ void UITerminal::onPaddingChange() {
}
int UITerminal::getVisibleArea() const {
return ( mTerm && mTerm->getTerminal() ) ? mTerm->getTerminal()->getNumRows() : 0;
return mTerm ? mTerm->rowCount() : 0;
}
void UITerminal::updateScrollPosition() {
if ( mTerm && mTerm->getTerminal() ) {
int historySize = mTerm->getTerminal()->getHistorySize();
Float val = historySize > 0
? ( 1.f - mTerm->getTerminal()->scrollPos() / (Float)historySize )
: 1.f;
mVScroll->setValue( val, false );
if ( !mTerm || mVScroll->isDragging() )
return;
if ( mScrollByBar ) {
if ( mPendingScrollCommand != 0 &&
mTerm->lastAppliedScrollCommand() < mPendingScrollCommand )
return;
mScrollByBar = false;
mPendingScrollCommand = 0;
}
int historySize = mTerm->scrollSize();
Float val = historySize > 0 ? ( 1.f - mTerm->scrollPosition() / (Float)historySize ) : 1.f;
mVScroll->setValue( val, false );
}
int UITerminal::getScrollableArea() const {
@@ -211,11 +227,11 @@ void UITerminal::updateScroll() {
}
void UITerminal::onScrollChange() {
if ( !mTerm || !mTerm->getTerminal() )
if ( !mTerm )
return;
int scrollTo = ( getScrollableArea() - mScrollOffset );
TerminalArg arg( scrollTo );
mTerm->getTerminal()->kscrollto( &arg );
mPendingScrollCommand = mTerm->scrollTo( scrollTo );
mScrollByBar = mPendingScrollCommand != 0;
}
void UITerminal::setVerticalScrollMode( const ScrollBarMode& Mode ) {
@@ -321,18 +337,23 @@ const std::shared_ptr<TerminalDisplay>& UITerminal::getTerm() const {
void UITerminal::scheduledUpdate( const Time& ) {
if ( !mTerm )
return;
auto terminal = mTerm;
auto mousePos = getInput()->getRelativeMousePos();
bool mouseOutsideBounds =
mousePos.y < 0 || mousePos.y > getUISceneNode()->getWindow()->getSize().getHeight();
mTerm->update( isMouseOverMeOrChildren() && !mouseOutsideBounds );
terminal->update( isMouseOverMeOrChildren() && !mouseOutsideBounds );
if ( !mVScroll->isDragging() && ( mScrollByBar || mPendingContentSizeChange ) ) {
updateScrollPosition();
if ( !mScrollByBar && mPendingContentSizeChange )
onContentSizeChange();
}
if ( mTerm->isDirty() && isVisible() )
if ( terminal->isDirty() && isVisible() )
invalidateDraw();
if ( ScrollBarMode::AlwaysOn == mVScrollMode ) {
mVScroll->setVisible( !mTerm->getTerminal()->tisaltscr() )
->setEnabled( !mTerm->getTerminal()->tisaltscr() );
mVScroll->setVisible( !terminal->isAltScr() )->setEnabled( !terminal->isAltScr() );
} else if ( ScrollBarMode::Auto == mVScrollMode ) {
if ( mViewType == ScrollViewType::Overlay && mMouseClock.getElapsedTime() > Seconds( 1 ) &&
!mVScroll->isDragging() )
@@ -493,8 +514,8 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& ) {
Uint32 UITerminal::onMouseMove( const Vector2i& position, const Uint32& flags ) {
if ( mViewType == ScrollViewType::Overlay && ScrollBarMode::Auto == mVScrollMode ) {
mMouseClock.restart();
bool visible = !mTerm->getTerminal()->tisaltscr() && getContentSize() > getVisibleArea() &&
!mTerm->getTerminal()->hasSelection();
bool visible =
!mTerm->isAltScr() && getContentSize() > getVisibleArea() && !mTerm->hasSelection();
mVScroll->setVisible( visible )->setEnabled( visible );
}
@@ -563,8 +584,8 @@ void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
if ( !mCreateDefaultContextMenuOptions )
return;
if ( mTerm->getTerminal()->hasSelection() ) {
auto sel( mTerm->getTerminal()->getSelection() );
if ( mTerm->hasSelection() ) {
auto sel( mTerm->getSelection() );
if ( LuaPattern::hasMatches( sel, LuaPattern::getURIPattern() ) ) {
menuAdd( menu, i18n( "uiterminal_open_link", "Open Link" ), "earth",
@@ -573,7 +594,7 @@ void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
}
menuAdd( menu, i18n( "uiterminal_copy", "Copy" ), "copy", "terminal-copy" )
->setEnabled( mTerm->getTerminal() && mTerm->getTerminal()->hasSelection() );
->setEnabled( mTerm->hasSelection() );
menuAdd( menu, i18n( "uiterminal_paste", "Paste" ), "paste", "terminal-paste" )
->setEnabled( !getUISceneNode()->getWindow()->getClipboard()->getText().empty() );
}
@@ -628,10 +649,14 @@ bool UITerminal::onCreateContextMenu( const Vector2i& position, const Uint32& fl
void UITerminal::restart() {
auto win = SceneManager::instance()->getUISceneNode()->getWindow();
if ( mTerm && mTerminalEventCallbackId )
mTerm->popEventCallback( mTerminalEventCallbackId );
mTerm = TerminalDisplay::create( win, mTerm->getFont(), mTerm->getFontSize(), mTerm->getSize(),
mTerm->getProgram(), mTerm->getArgs(), mTerm->getWorkingDir(),
mTerm->getHistorySize(), nullptr, mTerm->useFrameBuffer(),
mTerm->getKeepAlive(), mTerm->getEnv() );
mTerminalEventCallbackId = 0;
registerNewTerminal();
syncFontRenderingConfig();
}
+523 -17
View File
@@ -1,9 +1,13 @@
#include "utest.hpp"
#include <atomic>
#include <chrono>
#include <eterm/system/iprocess.hpp>
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminalsession.hpp>
#include <limits>
#include <thread>
using namespace eterm::Terminal;
using namespace eterm::System;
@@ -11,7 +15,11 @@ using namespace eterm::System;
class MockPty : public IPseudoTerminal {
public:
std::string mBuffer;
std::string mWrites;
bool mLoopWrites{ true };
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
size_t mReadOffset{ 0 };
std::atomic<size_t> mBytesRead{ 0 };
int mCols = 80;
int mRows = 24;
int getNumColumns() const override { return mCols; }
@@ -23,45 +31,458 @@ class MockPty : public IPseudoTerminal {
}
bool isTTY() const override { return true; }
int write( const char* s, size_t n ) override {
mBuffer.append( s, n );
mWrites.append( s, n );
if ( mLoopWrites )
mBuffer.append( s, n );
return n;
}
int read( char* buf, size_t n, bool ) override {
if ( mBuffer.empty() )
if ( mReadOffset == mBuffer.size() )
return 0;
size_t toRead = std::min( { n, mBuffer.size(), mMaxRead } );
memcpy( buf, mBuffer.data(), toRead );
mBuffer.erase( 0, toRead );
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, mMaxRead } );
memcpy( buf, mBuffer.data() + mReadOffset, toRead );
mReadOffset += toRead;
mBytesRead.fetch_add( toRead, std::memory_order_relaxed );
return toRead;
}
};
class MockProcess : public IProcess {
public:
bool mExited{ false };
std::atomic<bool> mExited{ false };
void checkExitStatus() override {}
bool hasExited() const override { return mExited; }
bool hasExited() const override { return mExited.load(); }
int getExitCode() const override { return 0; }
void terminate() override {}
void waitForExit() override {}
int pid() override { return 123; }
};
static std::shared_ptr<const TerminalSnapshot>
waitForSnapshot( const std::shared_ptr<TerminalSession>& session,
const std::function<bool( const TerminalSnapshot& )>& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds( 1000 ) ) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
while ( std::chrono::steady_clock::now() < deadline ) {
auto snapshot = session->snapshot();
if ( snapshot && predicate( *snapshot ) )
return snapshot;
std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) );
}
return nullptr;
}
UTEST( eterm_session, command_wakeup_and_snapshot_immutability ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
ASSERT_TRUE( session != nullptr );
session->writeRaw( "ABC" );
auto first = waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return snapshot.cells.size() >= 3 && snapshot.cells[0].u == 'A' &&
snapshot.cells[1].u == 'B' && snapshot.cells[2].u == 'C';
} );
ASSERT_TRUE( first != nullptr );
const Uint64 firstGeneration = first->generation;
session->writeRaw( "\rXYZ" );
auto second = waitForSnapshot( session, [firstGeneration]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > firstGeneration && snapshot.cells[0].u == 'X';
} );
ASSERT_TRUE( second != nullptr );
EXPECT_EQ( static_cast<Rune>( 'A' ), first->cells[0].u );
EXPECT_TRUE( second->generation > first->generation );
}
UTEST( eterm_session, skipped_snapshot_generation_requires_full_redraw ) {
TerminalSnapshot snapshot;
snapshot.generation = 42;
EXPECT_TRUE( snapshot.dirtyRowsFollow( 41 ) );
EXPECT_FALSE( snapshot.dirtyRowsFollow( 40 ) );
}
UTEST( eterm_session, ordered_selection_request ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "ordered selection";
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return !snapshot.cells.empty() && snapshot.cells[0].u == 'o';
} ) != nullptr );
session->selectionStart( 0, 0, 0 );
session->selectionExtend( 6, 0, SEL_REGULAR, false );
auto selection = session->requestSelection();
ASSERT_TRUE( selection.has_value() );
EXPECT_STDSTREQ( "ordered", *selection );
}
UTEST( eterm_session, loaded_command_latency_stays_bounded ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer.assign( 32 * 1024 * 1024, 'L' );
pty->mMaxRead = 64;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
const auto readDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
while ( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) == 0 &&
std::chrono::steady_clock::now() < readDeadline )
std::this_thread::yield();
ASSERT_TRUE( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) > 0 );
const auto start = std::chrono::steady_clock::now();
auto selection = session->requestSelection();
const auto latency = std::chrono::steady_clock::now() - start;
EXPECT_TRUE( selection.has_value() );
EXPECT_TRUE( latency < std::chrono::milliseconds( 50 ) );
}
UTEST( eterm_session, resize_and_output_are_serialized ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
session->resize( 40, 12 );
session->writeRaw( "after resize" );
auto snapshot = waitForSnapshot( session, []( const TerminalSnapshot& value ) {
return value.columns == 40 && value.rows == 12 && !value.cells.empty() &&
value.cells[0].u == 'a';
} );
ASSERT_TRUE( snapshot != nullptr );
EXPECT_EQ( static_cast<size_t>( 40 * 12 ), snapshot->cells.size() );
}
UTEST( eterm_session, scroll_snapshots_acknowledge_the_latest_ordered_command ) {
auto pty = std::make_unique<MockPty>();
for ( int line = 0; line < 80; ++line )
pty->mBuffer += "history " + std::to_string( line ) + "\r\n";
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return snapshot.historyLength >= 25;
} ) != nullptr );
const Uint64 firstCommand = session->scrollTo( 10 );
const Uint64 secondCommand = session->scrollTo( 25 );
EXPECT_EQ( firstCommand + 1, secondCommand );
auto acknowledged =
waitForSnapshot( session, [secondCommand]( const TerminalSnapshot& snapshot ) {
return snapshot.lastAppliedScrollCommand == secondCommand;
} );
ASSERT_TRUE( acknowledged != nullptr );
EXPECT_EQ( 25, acknowledged->scrollPosition );
}
UTEST( eterm_session, presentation_rate_is_applied_on_the_worker ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
session->setPresentationRate( 120 );
ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return snapshot.presentationRate == 120;
} ) != nullptr );
}
UTEST( eterm_session, focus_reporting_is_ordered_on_worker ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "\033[?1004h";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
auto enabled = waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return snapshot.windowMode & MODE_FOCUS;
} );
ASSERT_TRUE( enabled != nullptr );
session->setFocus( false );
auto unfocused = waitForSnapshot( session, [enabled]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > enabled->generation && !( snapshot.windowMode & MODE_FOCUSED );
} );
ASSERT_TRUE( unfocused != nullptr );
ASSERT_TRUE( ptyPtr->mWrites.size() >= 3 );
EXPECT_STDSTREQ( "\033[O", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 3 ) );
session->setFocus( true );
ASSERT_TRUE( waitForSnapshot( session, [unfocused]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > unfocused->generation &&
snapshot.windowMode & MODE_FOCUSED;
} ) != nullptr );
ASSERT_TRUE( ptyPtr->mWrites.size() >= 3 );
EXPECT_STDSTREQ( "\033[I", ptyPtr->mWrites.substr( ptyPtr->mWrites.size() - 3 ) );
}
UTEST( eterm_session, replaceable_events_coalesce_without_losing_ordered_events ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
session->drainEvents();
session->writeRaw( "\033]0;first\a\033]0;second\a" );
ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return snapshot.title == "second";
} ) != nullptr );
int titleEvents = 0;
std::string title;
for ( auto& event : session->drainEvents() ) {
if ( event.type == TerminalSession::EventType::Title ) {
++titleEvents;
title = std::move( event.data );
}
}
EXPECT_EQ( 1, titleEvents );
EXPECT_STDSTREQ( "second", title );
}
UTEST( eterm_session, ordered_events_are_coalescing_barriers ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
session->drainEvents();
session->writeRaw( "\033]0;before\a\a\033]0;after\a" );
ASSERT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return snapshot.title == "after";
} ) != nullptr );
std::vector<TerminalSession::EventType> semanticEvents;
for ( const auto& event : session->drainEvents() ) {
if ( event.type == TerminalSession::EventType::Title ||
event.type == TerminalSession::EventType::Bell )
semanticEvents.emplace_back( event.type );
}
ASSERT_EQ( static_cast<size_t>( 3 ), semanticEvents.size() );
EXPECT_EQ( TerminalSession::EventType::Title, semanticEvents[0] );
EXPECT_EQ( TerminalSession::EventType::Bell, semanticEvents[1] );
EXPECT_EQ( TerminalSession::EventType::Title, semanticEvents[2] );
}
UTEST( eterm_session, reset_is_ordered_and_publishes_immediately ) {
auto pty = std::make_unique<MockPty>();
pty->mBuffer = "content";
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
auto populated = waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return !snapshot.cells.empty() && snapshot.cells[0].u == 'c';
} );
ASSERT_TRUE( populated != nullptr );
session->reset();
auto reset = waitForSnapshot( session, [populated]( const TerminalSnapshot& snapshot ) {
return snapshot.generation > populated->generation && !snapshot.cells.empty() &&
snapshot.cells[0].u == ' ';
} );
ASSERT_TRUE( reset != nullptr );
}
UTEST( eterm_session, process_exit_follows_buffered_output_and_final_snapshot ) {
auto pty = std::make_unique<MockPty>();
pty->mMaxRead = 1;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
MockProcess* processPtr = process.get();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
session->setDataEventsEnabled( true );
session->writeRaw( std::string( 3 * 1024, 'Q' ) );
const auto readDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
while ( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) == 0 &&
std::chrono::steady_clock::now() < readDeadline )
std::this_thread::yield();
ASSERT_TRUE( ptyPtr->mBytesRead.load( std::memory_order_relaxed ) > 0 );
processPtr->mExited.store( true );
auto finalSnapshot = waitForSnapshot(
session, []( const TerminalSnapshot& snapshot ) { return snapshot.processExited; } );
ASSERT_TRUE( finalSnapshot != nullptr );
EXPECT_EQ( 0, finalSnapshot->exitCode );
size_t bytesRead = 0;
bool sawExit = false;
for ( auto& event : session->drainEvents() ) {
if ( event.type == TerminalSession::EventType::Data )
bytesRead += event.data.size();
else if ( event.type == TerminalSession::EventType::ProcessExit )
sawExit = true;
}
EXPECT_EQ( static_cast<size_t>( 3 * 1024 ), bytesRead );
EXPECT_TRUE( sawExit );
}
UTEST( eterm_session, repeated_create_destroy_and_concurrent_workers ) {
for ( int iteration = 0; iteration < 16; ++iteration ) {
std::vector<std::shared_ptr<TerminalSession>> sessions;
for ( int terminal = 0; terminal < 4; ++terminal ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
session->writeRaw( "worker" + std::to_string( terminal ) );
sessions.emplace_back( std::move( session ) );
}
for ( const auto& session : sessions ) {
EXPECT_TRUE( waitForSnapshot( session, []( const TerminalSnapshot& snapshot ) {
return !snapshot.cells.empty() && snapshot.cells[0].u == 'w';
} ) != nullptr );
}
}
}
UTEST( eterm_session, concurrent_shutdown_is_idempotent ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto session = TerminalSession::create( std::move( pty ), std::move( process ), 100 );
std::vector<std::thread> shutdownThreads;
for ( int thread = 0; thread < 4; ++thread )
shutdownThreads.emplace_back( [session] { session->shutdown(); } );
for ( auto& thread : shutdownThreads )
thread.join();
session->shutdown();
}
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>();
@@ -77,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>();
@@ -120,14 +628,12 @@ UTEST( eterm, sustained_saturated_reads_present_periodically ) {
term->update();
display->mDrawLines = 0;
std::string output( 33 * 1024, 'A' );
std::string output( 1024 * 1024, 'A' );
term->write( output.data(), output.size() );
for ( int batch = 0; batch < 31; ++batch ) {
EXPECT_FALSE( term->update() );
EXPECT_EQ( 0, display->mDrawLines );
}
EXPECT_FALSE( term->update() );
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
while ( display->mDrawLines == 0 && std::chrono::steady_clock::now() < deadline )
term->update();
EXPECT_TRUE( display->mDrawLines > 0 );
}
+2 -2
View File
@@ -77,7 +77,7 @@ App* App::instance() {
bool App::isAnyTerminalDirty() const {
bool dirty = false;
mSplitter->forEachWidgetTypeStoppable( UI_TYPE_TERMINAL, [&dirty]( UIWidget* widget ) -> bool {
ProcessID pid = widget->asType<UITerminal>()->getTerm()->getTerminal()->getProcess()->pid();
ProcessID pid = widget->asType<UITerminal>()->getTerm()->getProcessId();
if ( Sys::processHasChildren( pid ) ) {
dirty = true;
return true;
@@ -5063,7 +5063,7 @@ void App::init( InitParameters& params ) {
focusTabBehavior, onMsgBoxCloseCb );
} else if ( mConfig.term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
UITerminal* term = widget->asType<UITerminal>();
ProcessID pid = term->getTerm()->getTerminal()->getProcess()->pid();
ProcessID pid = term->getTerm()->getProcessId();
std::string msgBoxId = String::format( "msgbox_%p", this );
if ( Sys::processHasChildren( pid ) ) {
if ( nullptr != getUISceneNode()->find( msgBoxId ) )
@@ -126,8 +126,11 @@ void AgentSession::listSessions(
void AgentSession::stop() {
for ( auto& term : mTerminals ) {
if ( term.second.display && term.second.eventCbId )
term.second.display->popEventCallback( term.second.eventCbId );
if ( term.second.display ) {
if ( term.second.eventCbId )
term.second.display->popEventCallback( term.second.eventCbId );
term.second.display->setDataCallback( {} );
}
}
mTerminals.clear();
@@ -171,10 +174,9 @@ void AgentSession::cancel() {
void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* uiTerm ) {
auto& termData = mTerminals[terminalId];
termData.display = uiTerm->getTerm();
termData.emulator = uiTerm->getTerm()->getTerminal();
termData.uiTerm = uiTerm;
if ( termData.emulator ) {
termData.emulator->setDataCb( [this, terminalId]( const char* data, size_t size ) {
if ( termData.display ) {
termData.display->setDataCallback( [this, terminalId]( const char* data, size_t size ) {
auto it = mTerminals.find( terminalId );
if ( it != mTerminals.end() ) {
it->second.outputBuffer.append( data, size );
@@ -188,7 +190,7 @@ void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* u
auto it = mTerminals.find( terminalId );
if ( it != mTerminals.end() ) {
WaitForTerminalExitResponse res;
res.exitCode = it->second.emulator ? it->second.emulator->getExitCode() : 0;
res.exitCode = it->second.display ? it->second.display->getExitCode() : 0;
auto callbacks = std::move( it->second.exitCallbacks );
it->second.exitCallbacks.clear();
for ( const auto& cb : callbacks ) {
@@ -268,9 +270,9 @@ void AgentSession::setupClient() {
res.truncated = false;
}
if ( it->second.emulator && it->second.emulator->hasExited() ) {
if ( it->second.display && it->second.display->hasTerminated() ) {
TerminalExitStatus status;
status.exitCode = it->second.emulator->getExitCode();
status.exitCode = it->second.display->getExitCode();
res.exitStatus = status;
}
}
@@ -279,8 +281,8 @@ void AgentSession::setupClient() {
mClient->onKillTerminal = [this]( const KillTerminalRequest& req, auto cb ) {
auto it = mTerminals.find( req.terminalId );
if ( it != mTerminals.end() && it->second.emulator ) {
it->second.emulator->terminate();
if ( it != mTerminals.end() && it->second.display ) {
it->second.display->terminate();
}
cb( KillTerminalResponse() );
};
@@ -290,8 +292,10 @@ void AgentSession::setupClient() {
if ( it != mTerminals.end() ) {
if ( it->second.display && it->second.eventCbId )
it->second.display->popEventCallback( it->second.eventCbId );
if ( it->second.emulator )
it->second.emulator->terminate();
if ( it->second.display ) {
it->second.display->setDataCallback( {} );
it->second.display->terminate();
}
if ( it->second.uiTerm )
it->second.uiTerm->close();
mTerminals.erase( it );
@@ -301,10 +305,10 @@ void AgentSession::setupClient() {
mClient->onWaitForTerminalExit = [this]( const WaitForTerminalExitRequest& req, auto cb ) {
auto it = mTerminals.find( req.terminalId );
if ( it != mTerminals.end() && it->second.emulator ) {
if ( it->second.emulator->hasExited() ) {
if ( it != mTerminals.end() && it->second.display ) {
if ( it->second.display->hasTerminated() ) {
WaitForTerminalExitResponse res;
res.exitCode = it->second.emulator->getExitCode();
res.exitCode = it->second.display->getExitCode();
cb( res );
} else {
it->second.exitCallbacks.push_back( cb );
@@ -2,7 +2,6 @@
#include "acpclient.hpp"
#include <eepp/system/threadpool.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <functional>
#include <memory>
@@ -60,7 +59,6 @@ class AgentSession {
struct TermData {
std::shared_ptr<TerminalDisplay> display;
std::shared_ptr<TerminalEmulator> emulator;
UITerminal* uiTerm{ nullptr };
std::string outputBuffer;
Uint32 eventCbId{ 0 };
@@ -2360,10 +2360,7 @@ void DebuggerPlugin::run( const std::string& debugger, ProtocolSettings&& protoc
plugin->getPluginContext()->getTerminalManager()->createTerminalInSplitter(
cwd, cmd, args, env, false, false );
doneFn( term && term->getTerm() && term->getTerm()->getTerminal() &&
term->getTerm()->getTerminal()->getProcess()
? term->getTerm()->getTerminal()->getProcess()->pid()
: 0 );
doneFn( term && term->getTerm() ? term->getTerm()->getProcessId() : 0 );
} else {
std::string fcmd = cmd + ( !args.empty() ? " " : "" ) + String::join( args, ' ' );
doneFn( plugin->getPluginContext()->getTerminalManager()->openInExternalTerminal(
+5 -5
View File
@@ -27,7 +27,7 @@ bool StatusTerminalController::tryTabClose( UITab* tab ) {
if ( mContext->getConfig().term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
UITerminal* term = widget->asType<UITerminal>();
ProcessID pid = term->getTerm()->getTerminal()->getProcess()->pid();
ProcessID pid = term->getTerm()->getProcessId();
if ( Sys::processHasChildren( pid ) ) {
UIMessageBox* msgBox =
UIMessageBox::New( UIMessageBox::OK_CANCEL,
@@ -141,8 +141,7 @@ UITerminal* StatusTerminalController::createTerminal(
mContext->getTerminalFont() ? mContext->getTerminalFont() : mContext->getFontMono(),
mContext->termConfig().fontSize.asPixels( 0, Sizef(), mContext->getDisplayDPI() ),
initialSize, program, args, env,
!workingDir.empty() ? workingDir
: mContext->getTerminalManager()->getSelectedWorkingDir(),
!workingDir.empty() ? workingDir : mContext->getTerminalManager()->getSelectedWorkingDir(),
10000, nullptr, false );
if ( term == nullptr || term->getTerm() == nullptr ) {
@@ -154,7 +153,7 @@ UITerminal* StatusTerminalController::createTerminal(
const auto& currentTerminalColorScheme =
mContext->getTerminalManager()->getTerminalCurrentColorScheme();
auto csIt = terminalColorSchemes.find( currentTerminalColorScheme );
term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true );
term->getTerm()->setAllowMemoryTrimming( true );
term->getTerm()->setCursorMode( mContext->termConfig().cursorStyle );
term->setExclusiveMode( mContext->termConfig().exclusiveMode );
term->setScrollViewType( mContext->termConfig().scrollBarType );
@@ -242,7 +241,8 @@ UITerminal* StatusTerminalController::createTerminal(
UIIcon* icon = mUISceneNode->findIcon( "terminal" );
auto tab = mTabWidget->add(
program, term, icon != nullptr ? icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) : nullptr );
program, term,
icon != nullptr ? icon->createDrawable( PixelDensity::dpToPxI( 12 ) ) : nullptr );
term->setData( (UintPtr)tab );
term->on( Event::OnTitleChange, [tab, term]( auto ) { tab->setText( term->getTitle() ); } );
+1 -1
View File
@@ -658,7 +658,7 @@ UITerminal* TerminalManager::createNewTerminal(
} );
term->setTitle( title );
auto csIt = mTerminalColorSchemes.find( mTerminalCurrentColorScheme );
term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true );
term->getTerm()->setAllowMemoryTrimming( true );
term->getTerm()->setKeepAlive( !mApp->getConfig().term.closeTerminalTabOnExit );
term->getTerm()->pushEventCallback( [this, term]( const TerminalDisplay::Event& event ) {
if ( event.type == TerminalDisplay::EventType::PROCESS_EXIT &&
+496 -377
View File
@@ -1,163 +1,343 @@
#include <args/args.hxx>
#include <eepp/core/small_vector.hpp>
#include <eepp/ee.hpp>
#include <eterm/terminal/terminaldisplay.hpp>
#include <eepp/ui/iconmanager.hpp>
#include <eepp/ui/tools/uitabwidgetsplitter.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uimessagebox.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <map>
#include <unordered_map>
EE::Window::Window* win = NULL;
std::shared_ptr<TerminalDisplay> terminal = nullptr;
Clock lastRender;
Clock secondsCounter;
Time frameTime{ Time::Zero };
bool benchmarkMode{ false };
bool warnBeforeClose{ false };
std::string windowStringData;
using namespace EE;
using namespace EE::Graphics;
using namespace EE::Scene;
using namespace EE::System;
using namespace EE::UI;
using namespace EE::UI::Tools;
using namespace EE::Window;
using namespace eterm::Terminal;
using namespace eterm::UI;
namespace {
struct TerminalLaunchConfig {
std::string program;
std::vector<std::string> arguments;
std::string workingDirectory;
std::string executeInShell;
size_t historySize{ 10000 };
TerminalCursorMode cursorStyle{ TerminalCursorMode::SteadyUnderline };
FontHinting fontHinting{ FontHinting::Full };
FontAntialiasing fontAntialiasing{ FontAntialiasing::Grayscale };
bool useFrameBuffer{ false };
bool keepAlive{ true };
bool closeOnExit{ false };
};
EE::Window::Window* appWindow{ nullptr };
UISceneNode* scene{ nullptr };
UILinearLayout* mainLayout{ nullptr };
UITabWidgetSplitter* tabSplitter{ nullptr };
FontTrueType* terminalFont{ nullptr };
UIIcon* terminalIcon{ nullptr };
UIMessageBox* closeDialog{ nullptr };
UIWidget* closeDialogWidget{ nullptr };
TerminalLaunchConfig terminalConfig;
std::map<std::string, TerminalColorScheme> terminalColorSchemes;
bool displayingWarnBeforeClose{ false };
bool yesPicked{ true };
bool needsRedraw{ false };
Rectf yesBtn;
Rectf noBtn;
const TerminalColorScheme* selectedColorScheme{ nullptr };
Float terminalFontSize{ 12 };
bool warnBeforeClose{ false };
bool closeApproved{ false };
bool benchmarkMode{ false };
Clock secondsCounter;
SmallVector<UITab*, 8> pendingExitCloseTabs;
void updateWindowTitle();
class TerminalSplitterClient : public UITabWidgetSplitter::Client {
public:
void onTabCreated( UITab* tab, UIWidget* ) override {
if ( terminalIcon )
tab->setIcon( terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) );
}
void onWidgetFocusChange( UIWidget* ) override { updateWindowTitle(); }
};
TerminalSplitterClient splitterClient;
std::string getResourcePath() {
std::string resPath = Sys::getProcessPath();
#if EE_PLATFORM == EE_PLATFORM_MACOS
if ( String::contains( resPath, "ecode.app" ) ) {
resPath = FileSystem::getCurrentWorkingDirectory();
FileSystem::dirAddSlashAtEnd( resPath );
}
#elif EE_PLATFORM == EE_PLATFORM_LINUX
if ( String::contains( resPath, ".mount_" ) ) {
resPath = FileSystem::getCurrentWorkingDirectory();
FileSystem::dirAddSlashAtEnd( resPath );
}
#elif EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
resPath += "eterm/";
#endif
resPath += "assets";
FileSystem::dirAddSlashAtEnd( resPath );
return resPath;
}
void loadColorSchemes( const std::string& resPath ) {
auto configPath = Sys::getConfigPath( "eterm" );
auto colorSchemes =
TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" );
auto colorSchemesPath = configPath + FileSystem::getOSSlash() + "colorschemes";
if ( FileSystem::isDirectory( colorSchemesPath ) ) {
auto colorSchemesFiles = FileSystem::filesGetInPath( colorSchemesPath );
for ( auto& file : colorSchemesFiles ) {
auto colorSchemesInFile = TerminalColorScheme::loadFromFile( file );
std::copy( colorSchemesInFile.begin(), colorSchemesInFile.end(),
std::back_inserter( colorSchemes ) );
const std::string configColorSchemesPath =
Sys::getConfigPath( "eterm" ) + FileSystem::getOSSlash() + "colorschemes";
if ( FileSystem::isDirectory( configColorSchemesPath ) ) {
for ( const auto& file : FileSystem::filesGetInPath( configColorSchemesPath ) ) {
auto fileColorSchemes = TerminalColorScheme::loadFromFile( file );
colorSchemes.insert( colorSchemes.end(),
std::make_move_iterator( fileColorSchemes.begin() ),
std::make_move_iterator( fileColorSchemes.end() ) );
}
}
for ( auto colorScheme : colorSchemes )
terminalColorSchemes.insert( { colorScheme.getName(), colorScheme } );
for ( auto& colorScheme : colorSchemes ) {
std::string name = colorScheme.getName();
terminalColorSchemes.emplace( std::move( name ), std::move( colorScheme ) );
}
}
void inputCallback( InputEvent* event ) {
if ( !terminal || event->Type == InputEvent::EventsSent )
UITerminal* terminalFromTab( UITab* tab ) {
return tab && tab->getOwnedWidget() && tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL )
? tab->getOwnedWidget()->asType<UITerminal>()
: nullptr;
}
void updateWindowTitle() {
if ( !appWindow )
return;
std::string title{ "eterm" };
if ( tabSplitter ) {
if ( auto* terminal = tabSplitter->getCurWidget() &&
tabSplitter->getCurWidget()->isType( UI_TYPE_TERMINAL )
? tabSplitter->getCurWidget()->asType<UITerminal>()
: nullptr;
terminal && !terminal->getTitle().empty() ) {
title += " - ";
title += terminal->getTitle();
}
}
if ( benchmarkMode ) {
title += " - ";
title += String::toString( appWindow->getFPS() );
title += " FPS";
}
appWindow->setTitle( title );
}
switch ( event->Type ) {
case InputEvent::MouseMotion: {
terminal->onMouseMove( win->getInput()->getMousePos(),
win->getInput()->getPressTrigger() );
break;
}
case InputEvent::MouseButtonDown: {
if ( displayingWarnBeforeClose ) {
if ( ( win->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) {
if ( yesBtn.contains( win->getInput()->getMousePos().asFloat() ) ) {
win->close();
} else if ( noBtn.contains( win->getInput()->getMousePos().asFloat() ) ) {
displayingWarnBeforeClose = false;
needsRedraw = true;
}
}
} else {
terminal->onMouseDown( win->getInput()->getMousePos(),
win->getInput()->getPressTrigger() );
#if EE_PLATFORM == EE_PLATFORM_ANDROID
win->startTextInput();
#endif
}
break;
}
case InputEvent::MouseButtonUp: {
terminal->onMouseUp( win->getInput()->getMousePos(),
win->getInput()->getReleaseTrigger() );
bool hasTerminals() {
bool found = false;
if ( tabSplitter )
tabSplitter->forEachWidgetStoppable( [&found]( UIWidget* ) {
found = true;
return true;
} );
return found;
}
if ( win->getInput()->getDoubleClickTrigger() ) {
terminal->onMouseDoubleClick( win->getInput()->getMousePos(),
win->getInput()->getDoubleClickTrigger() );
}
bool hasRunningChildren( UITab* tab ) {
auto* terminal = terminalFromTab( tab );
return terminal && terminal->getTerm() &&
Sys::processHasChildren( terminal->getTerm()->getProcessId() );
}
break;
}
case InputEvent::Window: {
switch ( event->window.type ) {
case InputEvent::WindowKeyboardFocusLost:
case InputEvent::WindowKeyboardFocusGain: {
terminal->setFocus( win->hasFocus() );
break;
}
}
break;
}
case InputEvent::KeyUp: {
break;
}
case InputEvent::KeyDown: {
if ( displayingWarnBeforeClose ) {
if ( event->key.keysym.sym == EE::Window::KEY_TAB ||
event->key.keysym.sym == EE::Window::KEY_LEFT ||
event->key.keysym.sym == EE::Window::KEY_RIGHT ) {
yesPicked = !yesPicked;
needsRedraw = true;
} else if ( event->key.keysym.sym == EE::Window::KEY_Y ) {
win->close();
} else if ( event->key.keysym.sym == EE::Window::KEY_N ) {
displayingWarnBeforeClose = false;
needsRedraw = true;
} else if ( event->key.keysym.sym == EE::Window::KEY_RETURN ||
event->key.keysym.sym == EE::Window::KEY_KP_ENTER ) {
if ( yesPicked )
win->close();
else {
displayingWarnBeforeClose = false;
needsRedraw = true;
}
} else if ( event->key.keysym.sym == EE::Window::KEY_ESCAPE ) {
displayingWarnBeforeClose = false;
needsRedraw = true;
}
} else {
terminal->onKeyDown( event->key.keysym.sym, event->key.keysym.unicode,
event->key.keysym.mod, event->key.keysym.scancode );
}
void closeTab( UITab* tab ) {
if ( !tabSplitter || !tab || !tab->getOwnedWidget() )
return;
tabSplitter->closeTab( tab->getOwnedWidget()->asType<UIWidget>(),
UITabWidget::FocusTabBehavior::Default );
}
#if EE_PLATFORM == EE_PLATFORM_ANDROID
if ( event->key.keysym.sym == KEY_RETURN ||
event->key.keysym.scancode == SCANCODE_RETURN ) {
win->startTextInput();
}
#endif
break;
}
case InputEvent::TextInput: {
terminal->onTextInput( event->text.text );
break;
}
case InputEvent::TextEditing: {
terminal->onTextEditing( event->textediting.text, event->textediting.start,
event->textediting.length );
break;
}
case InputEvent::VideoExpose:
terminal->setFocus( win->hasFocus() );
terminal->invalidate();
break;
case InputEvent::VideoResize: {
terminal->setPosition( { 0, 0 } );
terminal->setSize( win->getSize().asFloat() );
break;
}
void queueExitCloseTab( UITab* tab ) {
if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) ==
pendingExitCloseTabs.end() ) {
pendingExitCloseTabs.emplace_back( tab );
}
}
bool onCloseRequestCallback( EE::Window::Window* ) {
if ( warnBeforeClose &&
Sys::processHasChildren( terminal->getTerminal()->getProcess()->pid() ) ) {
displayingWarnBeforeClose = true;
needsRedraw = true;
void queueExitedTabs() {
if ( !terminalConfig.closeOnExit )
return;
tabSplitter->forEachTab( []( UITab* tab ) {
auto* terminal = terminalFromTab( tab );
if ( !terminal || !terminal->getTerm() )
return;
const auto& session = terminal->getTerm()->getSession();
auto snapshot = session ? session->snapshot() : nullptr;
if ( snapshot && snapshot->processExited )
queueExitCloseTab( tab );
} );
}
void requestCloseTab( UITab* tab ) {
if ( !warnBeforeClose || !hasRunningChildren( tab ) ) {
closeTab( tab );
return;
}
if ( closeDialog )
return;
closeDialog = UIMessageBox::New(
UIMessageBox::OK_CANCEL,
"Are you sure you want to close this terminal?\nIt is still running a process." );
closeDialogWidget = tab->getOwnedWidget()->asType<UIWidget>();
closeDialog->setTitle( "eterm" );
closeDialog->on( Event::OnConfirm, []( const Event* ) {
if ( closeDialogWidget && tabSplitter->ownedWidgetExists( closeDialogWidget ) )
tabSplitter->closeTab( closeDialogWidget, UITabWidget::FocusTabBehavior::Default );
} );
closeDialog->on( Event::OnClose, []( const Event* ) {
closeDialog = nullptr;
closeDialogWidget = nullptr;
} );
closeDialog->center();
closeDialog->showWhenReady();
}
void addTabKeyBindings( UITerminal* terminal, UITab* tab );
UITerminal* createTerminal( UITabWidget* target = nullptr ) {
if ( !target && tabSplitter ) {
auto* current = tabSplitter->getCurWidget();
target = current ? tabSplitter->tabWidgetFromWidget( current )
: tabSplitter->getFirstTabWidget();
}
if ( !target )
return nullptr;
Sizef initialSize{ 16, 16 };
if ( target->getContainerNode() &&
target->getContainerNode()->getPixelsSize() != Sizef::Zero ) {
initialSize = target->getContainerNode()->getPixelsSize();
}
auto* terminal = UITerminal::New(
terminalFont, terminalFontSize, initialSize, terminalConfig.program,
terminalConfig.arguments, {}, terminalConfig.workingDirectory, terminalConfig.historySize,
nullptr, terminalConfig.useFrameBuffer, terminalConfig.keepAlive );
if ( !terminal || !terminal->getTerm() ) {
eeSAFE_DELETE( terminal );
return nullptr;
}
terminal->getTerm()->setAllowMemoryTrimming( true );
terminal->getTerm()->setCursorMode( terminalConfig.cursorStyle );
terminal->getTerm()->setFontHinting( terminalConfig.fontHinting );
terminal->getTerm()->setFontAntialiasing( terminalConfig.fontAntialiasing );
if ( selectedColorScheme )
terminal->setColorScheme( *selectedColorScheme );
auto* tab = tabSplitter->createWidgetInTabWidget( target, terminal, "Terminal" ).first;
addTabKeyBindings( terminal, tab );
terminal->on( Event::OnTitleChange, [tab, terminal]( const Event* ) {
tab->setText( terminal->getTitle().empty() ? "Terminal" : terminal->getTitle() );
if ( tabSplitter->getCurWidget() == terminal )
updateWindowTitle();
} );
terminal->getTerm()->pushEventCallback( [tab]( const TerminalDisplay::Event& event ) {
if ( terminalConfig.closeOnExit && event.type == TerminalDisplay::EventType::PROCESS_EXIT )
queueExitCloseTab( tab );
} );
tabSplitter->setCurrentWidget( terminal );
if ( !terminalConfig.executeInShell.empty() )
terminal->executeFile( terminalConfig.executeInShell );
terminal->setFocus();
updateWindowTitle();
return terminal;
}
UITerminal* createTerminalSplit( SplitDirection direction, UITerminal* terminal ) {
auto* source = terminal ? tabSplitter->tabWidgetFromWidget( terminal ) : nullptr;
auto* target = source ? tabSplitter->splitTabWidget( direction, source ) : nullptr;
return target ? createTerminal( target ) : nullptr;
}
void addTabKeyBindings( UITerminal* terminal, UITab* tab ) {
terminal->setCommand( "create-new-terminal", [] { createTerminal(); } );
terminal->setCommand( "close-tab", [tab] { requestCloseTab( tab ); } );
terminal->setCommand( "next-tab", [terminal] {
if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) )
tabs->focusNextTab();
} );
terminal->setCommand( "previous-tab", [terminal] {
if ( auto* tabs = tabSplitter->tabWidgetFromWidget( terminal ) )
tabs->focusPreviousTab();
} );
terminal->setCommand( "split-right",
[terminal] { createTerminalSplit( SplitDirection::Right, terminal ); } );
terminal->setCommand( "split-bottom",
[terminal] { createTerminalSplit( SplitDirection::Bottom, terminal ); } );
terminal->setCommand( "split-left",
[terminal] { createTerminalSplit( SplitDirection::Left, terminal ); } );
terminal->setCommand( "split-top",
[terminal] { createTerminalSplit( SplitDirection::Top, terminal ); } );
terminal->setCommand( "switch-to-previous-split",
[terminal] { tabSplitter->switchPreviousSplit( terminal ); } );
terminal->setCommand( "switch-to-next-split",
[terminal] { tabSplitter->switchNextSplit( terminal ); } );
terminal->addKeyBinding( { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT },
"create-new-terminal" );
terminal->addKeyBinding( { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-tab" );
terminal->addKeyBinding( { KEY_PAGEDOWN, KEYMOD_CTRL }, "next-tab" );
terminal->addKeyBinding( { KEY_PAGEUP, KEYMOD_CTRL }, "previous-tab" );
terminal->addKeyBinding( { KEY_TAB, KEYMOD_CTRL }, "next-tab" );
terminal->addKeyBinding( { KEY_TAB, KEYMOD_CTRL | KEYMOD_SHIFT }, "previous-tab" );
terminal->addKeyBinding( { KEY_L, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT },
"split-right" );
terminal->addKeyBinding( { KEY_K, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT },
"split-bottom" );
terminal->addKeyBinding( { KEY_J, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT },
"split-left" );
terminal->addKeyBinding( { KEY_I, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT },
"split-top" );
terminal->addKeyBinding(
{ KEY_J, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() },
"switch-to-previous-split" );
terminal->addKeyBinding(
{ KEY_L, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() },
"switch-to-next-split" );
}
bool closeWindow( EE::Window::Window* ) {
if ( closeApproved || !warnBeforeClose )
return true;
bool running = false;
tabSplitter->forEachTab( [&running]( UITab* tab ) { running |= hasRunningChildren( tab ); } );
if ( !running )
return true;
if ( closeDialog )
return false;
}
return true;
closeDialog = UIMessageBox::New(
UIMessageBox::OK_CANCEL,
"Are you sure you want to close this window? It is still running a process." );
closeDialog->setTitle( "eterm" );
closeDialog->on( Event::OnConfirm, []( const Event* ) {
closeApproved = true;
appWindow->close();
} );
closeDialog->on( Event::OnClose, []( const Event* ) {
closeDialog = nullptr;
closeDialogWidget = nullptr;
} );
closeDialog->center();
closeDialog->showWhenReady();
return false;
}
} // namespace
EE_MAIN_FUNC int main( int argc, char* argv[] ) {
#ifdef EE_DEBUG
Log::instance()->setLogToStdOut( true );
@@ -174,8 +354,8 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
args::Flag fb( parser, "framebuffer", "Use frame buffer (more memory usage, less CPU usage)",
{ "fb", "framebuffer" } );
args::ValueFlag<std::string> fontPath( parser, "fontpath", "Font path", { 'f', "font" } );
args::ValueFlag<std::string> fallbackFontPathF( parser, "fallback-fontpath",
"Fallback Font path", { "fallback-font" } );
args::ValueFlag<std::string> fallbackFontPath( parser, "fallback-fontpath",
"Fallback Font path", { "fallback-font" } );
args::ValueFlag<Float> fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 );
const std::unordered_map<std::string, FontHinting> fontHintingMap{
{ "none", FontHinting::None },
@@ -197,9 +377,9 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
args::ValueFlag<Float> width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 );
args::ValueFlag<Float> height( parser, "winheight", "Window height (in dp)", { "height" },
720 );
args::ValueFlag<Float> pixelDensityConf( parser, "pixel-density",
"Set default application pixel density",
{ 'd', "pixel-density" } );
args::ValueFlag<Float> pixelDensity( parser, "pixel-density",
"Set default application pixel density",
{ 'd', "pixel-density" } );
args::Positional<std::string> wd( parser, "wording-dir", "Working Directory / executable" );
args::Flag closeOnExit( parser, "close-on-exit",
"close the application when the executable exits", { 'c', "close" } );
@@ -227,267 +407,206 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) {
parser, "warn-before-closing",
"Prompts for confirmation if a program is still running when closing the terminal.",
{ "warn-before-closing" } );
args::ValueFlag<size_t> initialTabs( parser, "tabs", "Number of initial terminal tabs",
{ "tabs" }, 1 );
try {
parser.ParseCLI( argc, argv );
} catch ( const args::Help& ) {
std::cout << parser;
return EXIT_SUCCESS;
} catch ( const args::ParseError& e ) {
std::cerr << e.what() << std::endl;
} catch ( const args::ParseError& error ) {
std::cerr << error.what() << std::endl;
std::cerr << parser;
return EXIT_FAILURE;
} catch ( args::ValidationError& e ) {
std::cerr << e.what() << std::endl;
} catch ( args::ValidationError& error ) {
std::cerr << error.what() << std::endl;
std::cerr << parser;
return EXIT_FAILURE;
}
SystemFontResolver::setEnabled( true );
const std::string initialWorkingDirectory = FileSystem::getCurrentWorkingDirectory();
const std::string resPath = getResourcePath();
if ( listColorSchemes.Get() || colorScheme )
loadColorSchemes( resPath );
if ( listColorSchemes.Get() ) {
std::cout << "Color schemes:\n";
for ( const auto& colorSchemeEntry : terminalColorSchemes )
std::cout << "\t" << colorSchemeEntry.first << "\n";
return EXIT_SUCCESS;
}
if ( colorScheme ) {
auto colorSchemeIt = terminalColorSchemes.find( colorScheme.Get() );
if ( colorSchemeIt != terminalColorSchemes.end() )
selectedColorScheme = &colorSchemeIt->second;
}
DisplayManager* displayManager = Engine::instance()->getDisplayManager();
Display* currentDisplay = displayManager->getDisplayIndex( 0 );
std::string resPath = Sys::getProcessPath();
#if EE_PLATFORM == EE_PLATFORM_MACOS
if ( String::contains( resPath, "ecode.app" ) ) {
resPath = FileSystem::getCurrentWorkingDirectory();
FileSystem::dirAddSlashAtEnd( resPath );
}
#elif EE_PLATFORM == EE_PLATFORM_LINUX
if ( String::contains( resPath, ".mount_" ) ) {
resPath = FileSystem::getCurrentWorkingDirectory();
FileSystem::dirAddSlashAtEnd( resPath );
}
#elif EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
resPath += "eterm/";
#endif
resPath += "assets";
FileSystem::dirAddSlashAtEnd( resPath );
if ( listColorSchemes.Get() || colorScheme )
loadColorSchemes( resPath );
if ( listColorSchemes.Get() ) {
std::cout << "Color schemes:\n";
for ( const auto& tcs : terminalColorSchemes )
std::cout << "\t" << tcs.first << "\n";
return EXIT_SUCCESS;
if ( !currentDisplay ) {
std::cerr << "Display not found, exiting" << std::endl;
return EXIT_FAILURE;
}
std::unique_ptr<Thread> systemFontWarmUp;
if ( SystemFontResolver::isEnabled() ) {
systemFontWarmUp =
std::make_unique<Thread>( [] { SystemFontResolver::instance()->warmUp(); } );
systemFontWarmUp->launch();
Sizei windowSize( width.Get(), height.Get() );
const auto displaySize = currentDisplay->getUsableBounds().getSize();
if ( displaySize.getWidth() > 0 && windowSize.getWidth() >= displaySize.getWidth() )
windowSize.setWidth( static_cast<int>( displaySize.getWidth() * 0.8f ) );
if ( displaySize.getHeight() > 0 && windowSize.getHeight() >= displaySize.getHeight() )
windowSize.setHeight( static_cast<int>( displaySize.getHeight() * 0.75f ) );
UIApplication::Settings appSettings;
appSettings.basePath = FileSystem::removeLastFolderFromPath( resPath );
appSettings.pixelDensity =
pixelDensity ? pixelDensity.Get() : currentDisplay->getPixelDensity();
appSettings.fontHinting = fontHinting.Get();
appSettings.fontAntialiasing = fontAntialiasing.Get();
const Int32 frameRateLimit =
benchmarkModeFlag.Get()
? 0
: static_cast<Int32>( maxFPS.Get() ? maxFPS.Get() : currentDisplay->getRefreshRate() );
UIApplication app( WindowSettings( windowSize.getWidth(), windowSize.getHeight(), "eterm",
WindowStyle::Default, WindowBackend::Default, 32,
resPath + "icon/eterm.png",
appSettings.pixelDensity.value() ),
appSettings, ContextSettings( vsync.Get(), frameRateLimit ) );
appWindow = app.getWindow();
scene = app.getUI();
if ( !appWindow || !appWindow->isOpen() || !scene )
return EXIT_FAILURE;
FileSystem::changeWorkingDirectory( initialWorkingDirectory );
appWindow->setClearColor( RGB( 0, 0, 0 ) );
auto& resourceScope = *scene->getResourceScope();
auto remixIconFont = FontTrueType::New( "eterm-remixicon", resourceScope );
auto noniconsFont = FontTrueType::New( "eterm-nonicons", resourceScope );
auto codIconFont = FontTrueType::New( "eterm-codicon", resourceScope );
if ( remixIconFont->loadFromFile( resPath + "fonts/remixicon.ttf" ) &&
noniconsFont->loadFromFile( resPath + "fonts/nonicons.ttf" ) &&
codIconFont->loadFromFile( resPath + "fonts/codicon.ttf" ) ) {
scene->getUIIconThemeManager()->setCurrentTheme( IconManager::init(
"eterm", remixIconFont.get(), noniconsFont.get(), codIconFont.get() ) );
terminalIcon = scene->findIcon( "terminal" );
}
displayManager->enableScreenSaver();
displayManager->enableMouseFocusClickThrough();
displayManager->disableBypassCompositor();
defaultResourceScope().getFontService().setHinting( fontHinting.Get() );
defaultResourceScope().getFontService().setAntialiasing( fontAntialiasing.Get() );
Sizei winSize( width.Get(), height.Get() );
auto displaySize( currentDisplay->getUsableBounds().getSize() );
if ( displaySize.getWidth() > 0 && winSize.getWidth() >= displaySize.getWidth() )
winSize.setWidth( static_cast<int>( displaySize.getWidth() * 0.8 ) );
if ( displaySize.getHeight() > 0 && winSize.getHeight() >= displaySize.getHeight() )
winSize.setHeight( static_cast<int>( displaySize.getHeight() * 0.75 ) );
win = Engine::instance()->createWindow(
WindowSettings( winSize.getWidth(), winSize.getHeight(), "eterm", WindowStyle::Default,
WindowBackend::Default, 32, resPath + "icon/eterm.png",
pixelDensityConf ? pixelDensityConf.Get()
: currentDisplay->getPixelDensity() ),
ContextSettings( vsync.Get(), benchmarkModeFlag.Get() ? 0 : maxFPS.Get() ) );
if ( win->isOpen() ) {
win->setClearColor( RGB( 0, 0, 0 ) );
benchmarkMode = benchmarkModeFlag.Get();
warnBeforeClose = warnBeforeCloseFlag.Get();
FontTrueType* fontMono = nullptr;
if ( fontPath && FileSystem::fileExists( fontPath.Get() ) ) {
FileInfo file( fontPath.Get() );
fontMono = FontTrueType::New( "monospace" ).get();
if ( fontMono->loadFromFile( file.getFilepath() ) ) {
FontFamily::loadFromRegular( fontMono );
} else {
fontMono = nullptr;
}
}
if ( fontMono == nullptr ) {
fontMono = FontTrueType::New( "monospace" ).get();
fontMono->loadFromFile( resPath + "fonts/DejaVuSansMonoNerdFontComplete.ttf" );
FontFamily::loadFromRegular( fontMono, "DejaVuSansMono" );
}
if ( FileSystem::fileExists( resPath + "fonts/NotoColorEmoji.ttf" ) ) {
FontTrueType::New( "emoji-color" )
->loadFromFile( resPath + "fonts/NotoColorEmoji.ttf" );
} else if ( FileSystem::fileExists( resPath + "fonts/NotoEmoji-Regular.ttf" ) ) {
FontTrueType::New( "emoji-font" )
->loadFromFile( resPath + "fonts/NotoEmoji-Regular.ttf" );
}
std::string fallbackFontPath( fallbackFontPathF
? fallbackFontPathF.Get()
: resPath + "fonts/DroidSansFallbackFull.ttf" );
if ( FileSystem::fileExists( fallbackFontPath ) ) {
FontTrueType* fallbackFont = FontTrueType::New( "fallback-font" ).get();
if ( fallbackFont->loadFromFile( fallbackFontPath ) )
defaultResourceScope().getFontService().addFallbackFont( fallbackFont );
}
Float realMaxFPS = maxFPS.Get() ? maxFPS.Get() : currentDisplay->getRefreshRate();
frameTime = benchmarkMode ? Time::Zero : Milliseconds( 1000.f / realMaxFPS );
FileInfo file( wd ? wd.Get() : FileSystem::getCurrentWorkingDirectory() );
terminal = TerminalDisplay::create(
win, fontMono, PixelDensity::dpToPx( fontSize.Get() ), win->getSize().asFloat(),
file.isRegularFile() && file.isExecutable() ? file.getFilepath() : shell.Get(),
shellArgs ? String::split( shellArgs.Get() ) : std::vector<std::string>(),
file.getDirectoryPath(), historySize.Get(), nullptr, fb.Get(),
!( file.isRegularFile() && file.isExecutable() ) );
if ( terminal == nullptr ) {
win->close();
win->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm",
"Operating System not supported." );
terminal.reset();
systemFontWarmUp.reset();
Engine::destroySingleton();
MemoryManager::showResults();
if ( fontPath && FileSystem::fileExists( fontPath.Get() ) ) {
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
if ( terminalFont->loadFromFile( fontPath.Get() ) )
FontFamily::loadFromRegular( terminalFont );
else
terminalFont = nullptr;
}
if ( !terminalFont ) {
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
if ( !terminalFont->loadFromFile( resPath + "fonts/DejaVuSansMonoNerdFontComplete.ttf" ) ) {
std::cerr << "Could not load terminal font" << std::endl;
return EXIT_FAILURE;
}
terminal->setFontHinting( fontHinting.Get() );
terminal->setFontAntialiasing( fontAntialiasing.Get() );
terminal->getTerminal()->setAllowMemoryTrimnming( true );
terminal->setCursorMode( cursorStyle.Get() );
terminal->pushEventCallback( [&closeOnExit]( const TerminalDisplay::Event& event ) {
if ( event.type == TerminalDisplay::EventType::TITLE ) {
windowStringData = event.eventData;
win->setTitle( "eterm - " + windowStringData );
} else if ( event.type == TerminalDisplay::EventType::PROCESS_EXIT &&
closeOnExit.Get() ) {
win->close();
}
} );
if ( shell )
terminal->setKeepAlive( false );
if ( colorScheme ) {
auto selColorScheme = terminalColorSchemes.find( colorScheme.Get() );
if ( selColorScheme != terminalColorSchemes.end() )
terminal->setColorScheme( selColorScheme->second );
}
if ( !executeInShell.Get().empty() )
terminal->executeFile( executeInShell.Get() );
win->startTextInput();
win->getInput()->pushCallback( &inputCallback );
win->setCloseRequestCallback(
[]( EE::Window::Window* win ) -> bool { return onCloseRequestCallback( win ); } );
win->runMainLoop( [fontMono] {
bool termNeedsUpdate = false;
win->getInput()->update();
auto mousePos = win->getInput()->getRelativeMousePos();
bool mouseOutsideBounds = mousePos.y < 0 || mousePos.y > win->getSize().getHeight();
if ( terminal )
termNeedsUpdate = !terminal->update( !mouseOutsideBounds );
if ( ( terminal && ( benchmarkMode || terminal->isDirty() ) &&
( !termNeedsUpdate || lastRender.getElapsedTime() >= frameTime ) ) ||
needsRedraw ) {
lastRender.restart();
win->clear();
terminal->draw();
if ( displayingWarnBeforeClose ) {
Sizef winSize{ win->getSize().asFloat() };
Sizef buttonSize{ PixelDensity::dpToPx( 100 ), PixelDensity::dpToPx( 32 ) };
Primitives p;
p.setColor( Color( terminal->getColorScheme().getBackground(), 200 ) );
p.drawRectangle( { { 0, 0 }, winSize } );
Text text( "Are you sure you want to close this window? It is still running a "
"process.",
fontMono );
text.draw( ( winSize.getWidth() - text.getLocalBounds().getWidth() ) * 0.5f,
winSize.getHeight() * 0.5f - text.getTextHeight() -
PixelDensity::dpToPx( 32 ) );
yesBtn = Rectf{ { ( winSize.getWidth() * 0.5f - buttonSize.getWidth() * 0.5f -
PixelDensity::dpToPx( 75 ) ),
win->getHeight() * 0.5f },
buttonSize }
.floor();
p.setColor( terminal->getColorScheme().getBackground() );
p.drawRoundedRectangle( yesBtn );
noBtn = { Vector2f( yesBtn.getPosition().x + yesBtn.getSize().getWidth(),
yesBtn.getPosition().y ) +
Vector2f( PixelDensity::dpToPx( 50 ), 0 ),
yesBtn.getSize() };
p.drawRoundedRectangle( noBtn );
Text yes( "Yes", fontMono );
yes.draw(
eefloor( yesBtn.getPosition().x +
( yesBtn.getSize().getWidth() - yes.getLocalBounds().getWidth() ) *
0.5f ),
eefloor( yesBtn.getPosition().y +
( yesBtn.getSize().getHeight() - yes.getTextHeight() ) * 0.5f ) );
Text no( "No", fontMono );
no.draw(
eeceil( noBtn.getPosition().x +
( noBtn.getSize().getWidth() - no.getLocalBounds().getWidth() ) *
0.5f ),
eefloor( noBtn.getPosition().y +
( noBtn.getSize().getHeight() - no.getTextHeight() ) * 0.5f ) );
p.setFillMode( PrimitiveFillMode::DRAW_LINE );
p.setColor( terminal->getColorScheme().getForeground() );
p.drawRoundedRectangle( yesBtn );
p.drawRoundedRectangle( noBtn );
p.setColor( terminal->getColorScheme().getPaletteIndex( 5 ) );
p.drawRoundedRectangle( yesPicked ? yesBtn : noBtn );
}
win->display();
needsRedraw = false;
} else if ( !benchmarkMode && !termNeedsUpdate ) {
win->getInput()->waitEvent( Milliseconds( win->hasFocus() ? 16 : 100 ) );
}
if ( benchmarkMode && secondsCounter.getElapsedTime() >= Seconds( 1 ) ) {
win->setTitle( "eterm - " + windowStringData + " - " +
String::toString( win->getFPS() ) + " FPS" );
secondsCounter.restart();
}
} );
FontFamily::loadFromRegular( terminalFont, "DejaVuSansMono" );
}
terminal.reset();
systemFontWarmUp.reset();
if ( fallbackFontPath ) {
if ( FileSystem::fileExists( fallbackFontPath.Get() ) ) {
auto fallback = FontTrueType::New( "eterm-fallback-font", resourceScope );
if ( fallback->loadFromFile( fallbackFontPath.Get() ) )
resourceScope.getFontService().addFallbackFont( std::move( fallback ) );
}
} else if ( auto fallback = resourceScope.findFont( "DroidSansFallbackFull" ) ) {
resourceScope.getFontService().addFallbackFont( std::move( fallback ) );
}
Engine::destroySingleton();
const std::string launchPath = wd ? wd.Get() : initialWorkingDirectory;
FileInfo launchFile( launchPath );
const bool launchExecutable = launchFile.isRegularFile() && launchFile.isExecutable();
terminalConfig.program = launchExecutable ? launchFile.getFilepath() : shell.Get();
terminalConfig.arguments =
shellArgs ? String::split( shellArgs.Get() ) : std::vector<std::string>{};
terminalConfig.workingDirectory = launchFile.getDirectoryPath();
terminalConfig.executeInShell = executeInShell.Get();
terminalConfig.historySize = historySize.Get();
terminalConfig.cursorStyle = cursorStyle.Get();
terminalConfig.fontHinting = fontHinting.Get();
terminalConfig.fontAntialiasing = fontAntialiasing.Get();
terminalConfig.useFrameBuffer = fb.Get();
terminalConfig.keepAlive = !launchExecutable && !shell;
terminalConfig.closeOnExit = closeOnExit.Get();
warnBeforeClose = warnBeforeCloseFlag.Get();
benchmarkMode = benchmarkModeFlag.Get();
terminalFontSize = PixelDensity::dpToPx( fontSize.Get() );
MemoryManager::showResults();
mainLayout = UILinearLayout::NewVertical();
mainLayout->setParent( scene->getRoot() );
mainLayout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
mainLayout->setPixelsSize( appWindow->getSize().asFloat() );
tabSplitter = UITabWidgetSplitter::New( &splitterClient, scene );
tabSplitter->setHideTabBarOnSingleTab( true );
tabSplitter->setTabTryCloseCallback(
[]( UIWidget* widget, UITabWidget::FocusTabBehavior, std::function<void()> ) {
if ( warnBeforeClose ) {
auto* tab = tabSplitter->getTabFromWidget( widget );
if ( hasRunningChildren( tab ) ) {
requestCloseTab( tab );
return false;
}
}
return true;
} );
tabSplitter->setOnTabWidgetCreateCb( []( UITabWidget* tabs ) {
tabs->on( Event::OnTabSelected, []( const Event* ) { updateWindowTitle(); } );
tabs->on( Event::OnTabClosed, []( const Event* event ) {
auto* closedTab = static_cast<const TabEvent*>( event )->getTab();
pendingExitCloseTabs.erase(
std::remove( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), closedTab ),
pendingExitCloseTabs.end() );
if ( closeDialogWidget == closedTab->getOwnedWidget() )
closeDialogWidget = nullptr;
if ( !hasTerminals() )
appWindow->close();
else
updateWindowTitle();
} );
} );
auto* tabs = tabSplitter->createTabWidget( mainLayout );
if ( !tabs ) {
std::cerr << "Could not create terminal tab widget" << std::endl;
return EXIT_FAILURE;
}
mainLayout->updateLayout();
for ( size_t tab = 0; tab < eemax( static_cast<size_t>( 1 ), initialTabs.Get() ); ++tab ) {
if ( !createTerminal( tabs ) ) {
appWindow->showMessageBox( EE::Window::Window::MessageBoxType::Error, "eterm",
"Operating System not supported." );
return EXIT_FAILURE;
}
}
appWindow->setCloseRequestCallback( &closeWindow );
app.setShowMemoryManagerResult( true );
appWindow->runMainLoop( [] {
appWindow->getInput()->update();
SceneManager::instance()->update();
queueExitedTabs();
// Process-exit events are drained from UITerminal scheduled updates. Removing a tab from
// that callback would mutate the scheduled-widget set while it is being traversed.
while ( !pendingExitCloseTabs.empty() ) {
auto* tab = pendingExitCloseTabs.back();
pendingExitCloseTabs.pop_back();
closeTab( tab );
}
if ( benchmarkMode || scene->invalidated() ) {
appWindow->clear();
SceneManager::instance()->draw();
appWindow->display();
} else {
#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
appWindow->getInput()->waitEvent( Milliseconds( appWindow->hasFocus() ? 16 : 100 ) );
#endif
}
if ( benchmarkMode && secondsCounter.getElapsedTime() >= Seconds( 1 ) ) {
updateWindowTitle();
secondsCounter.restart();
}
} );
return EXIT_SUCCESS;
}