mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Merge remote-tracking branch 'origin/feature/eterm-worker' into develop
This commit is contained in:
@@ -7,7 +7,13 @@
|
|||||||
- **Primary module:** `src/modules/eterm`
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
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.
|
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.
|
4. Encode keyboard events according to the active flags.
|
||||||
5. Continue using existing legacy mappings when the protocol is inactive.
|
5. Continue using existing legacy mappings when the protocol is inactive.
|
||||||
6. Support press, repeat, and release events when requested.
|
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`
|
- **Protocol/state model** in `TerminalEmulator`
|
||||||
- **CSI command parsing and responses** in `TerminalEmulator::csihandle()`
|
- **CSI command parsing and responses** in `TerminalEmulator::csihandle()`
|
||||||
- **Key event encoding** in a focused encoder class/helper used by `TerminalDisplay`
|
- **Key event encoding** in a focused encoder class/helper invoked on the terminal worker
|
||||||
- **Input event plumbing** in `UITerminal` / `TerminalDisplay`, including key-up and repeat support
|
- **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.
|
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
|
## 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
|
```text
|
||||||
EEPP Window/Input backend
|
EEPP Window/Input backend
|
||||||
-> UI event dispatcher
|
-> UI event dispatcher
|
||||||
-> UITerminal::onKeyDown(KeyEvent)
|
-> UITerminal / TerminalDisplay
|
||||||
-> TerminalDisplay::onKeyDown(keyCode, char, mod, scancode)
|
-> terminal-local shortcuts
|
||||||
-> terminal shortcuts
|
-> Ctrl-letter and legacy TerminalKeyMap encoding
|
||||||
-> Ctrl-letter handling
|
-> TerminalSession::write(encoded bytes)
|
||||||
-> legacy TerminalKeyMap
|
-> ordered worker command
|
||||||
-> PTY ttywrite()
|
-> PTY ttywrite()
|
||||||
```
|
```
|
||||||
|
|
||||||
Text input follows a separate path:
|
The Kitty implementation must change the child-input portion to:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
EEPP text input event
|
EEPP Window/Input backend
|
||||||
-> UITerminal::onTextInput(TextInputEvent)
|
-> UI event dispatcher
|
||||||
-> TerminalDisplay::onTextInput(codepoint)
|
-> UITerminal / TerminalDisplay
|
||||||
-> UTF-8 conversion
|
-> consume terminal-local shortcuts
|
||||||
-> PTY ttywrite()
|
-> 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
|
## 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
|
## 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
|
```text
|
||||||
case '=': /* Progressive enhancement sequences */
|
ESC [ > 7 u -> priv='>', arg[0]=7, mode[0]='u'
|
||||||
/* Keyboard protocol ESC[=Nu */
|
ESC [ ? u -> priv='?', arg[0]=0, mode[0]='u'
|
||||||
/* Do nothing for the moment */
|
|
||||||
break;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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:
|
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
|
```text
|
||||||
ESC [ ? u
|
ESC [ ? u
|
||||||
@@ -273,7 +294,10 @@ ESC [ = 7 ; 2 u
|
|||||||
ESC [ = 7 ; 3 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
|
## 4.4 Screen swapping
|
||||||
|
|
||||||
@@ -304,10 +328,10 @@ src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp
|
|||||||
src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp
|
src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp
|
||||||
```
|
```
|
||||||
|
|
||||||
Possible test file:
|
Use the repository's existing unit-test file:
|
||||||
|
|
||||||
```text
|
```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.
|
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,
|
- output bytes,
|
||||||
- duplicate-text suppression.
|
- 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.
|
`TerminalEmulator` receives application output and therefore owns negotiation state.
|
||||||
|
|
||||||
@@ -431,14 +455,7 @@ KittyKeyboardState mPrimaryKeyboardState;
|
|||||||
KittyKeyboardState mAlternateKeyboardState;
|
KittyKeyboardState mAlternateKeyboardState;
|
||||||
```
|
```
|
||||||
|
|
||||||
Add public read-only accessors and event-state operations:
|
Potentially add private worker-only helpers:
|
||||||
|
|
||||||
```cpp
|
|
||||||
std::uint32_t getKeyboardEnhancementFlags() const;
|
|
||||||
bool hasKeyboardEnhancementFlag( KittyKeyboardFlag flag ) const;
|
|
||||||
```
|
|
||||||
|
|
||||||
Potentially add:
|
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
const KittyKeyboardState& getKeyboardState() const;
|
const KittyKeyboardState& getKeyboardState() const;
|
||||||
@@ -447,7 +464,11 @@ KittyKeyboardState& getKeyboardState();
|
|||||||
|
|
||||||
Keep mutating access private if possible.
|
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()`
|
## 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.
|
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.
|
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
|
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
|
```text
|
||||||
CSI ? <flags> u
|
CSI ? <flags> u
|
||||||
@@ -509,24 +534,29 @@ For example:
|
|||||||
ESC [ ? 31 u
|
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.
|
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
|
```cpp
|
||||||
void TerminalEmulator::replyKeyboardEnhancementFlags() {
|
void TerminalEmulator::replyKeyboardEnhancementState() {
|
||||||
char buf[32];
|
char buf[32];
|
||||||
const int len = std::snprintf(
|
const int len = std::snprintf(
|
||||||
buf, sizeof( buf ), "\033[?%uu",
|
buf, sizeof( buf ), "\033[?%uu",
|
||||||
KITTY_KEYBOARD_SUPPORTED_FLAGS
|
activeKeyboardState().flags
|
||||||
);
|
);
|
||||||
ttywrite( buf, static_cast<std::size_t>( len ), 0 );
|
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
|
## 6.3 Push semantics
|
||||||
|
|
||||||
@@ -602,15 +632,11 @@ The protocol includes:
|
|||||||
CSI = flags ; mode u
|
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:
|
- `1`: replace the current state; set specified bits and reset all unspecified bits,
|
||||||
|
- `2`: set the specified bits and leave unspecified bits unchanged,
|
||||||
- replace/set absolute flags,
|
- `3`: reset the specified bits and leave unspecified bits unchanged.
|
||||||
- set/add bits,
|
|
||||||
- clear/remove bits.
|
|
||||||
|
|
||||||
Do not hardcode these assumptions without checking the current official table.
|
|
||||||
|
|
||||||
Implement through one function:
|
Implement through one function:
|
||||||
|
|
||||||
@@ -623,13 +649,13 @@ void KittyKeyboardState::set(
|
|||||||
requestedFlags & KITTY_KEYBOARD_SUPPORTED_FLAGS;
|
requestedFlags & KITTY_KEYBOARD_SUPPORTED_FLAGS;
|
||||||
|
|
||||||
switch ( mode ) {
|
switch ( mode ) {
|
||||||
case /* set */:
|
case 1:
|
||||||
flags = supported;
|
flags = supported;
|
||||||
break;
|
break;
|
||||||
case /* OR */:
|
case 2:
|
||||||
flags |= supported;
|
flags |= supported;
|
||||||
break;
|
break;
|
||||||
case /* clear */:
|
case 3:
|
||||||
flags &= ~supported;
|
flags &= ~supported;
|
||||||
break;
|
break;
|
||||||
default:
|
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. 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
|
```cpp
|
||||||
"\033[?u"
|
"\033[?u"
|
||||||
@@ -701,7 +730,7 @@ Before modifying `csihandle()`, create tests that feed these exact byte strings
|
|||||||
"\033[=1;3u"
|
"\033[=1;3u"
|
||||||
```
|
```
|
||||||
|
|
||||||
Expose or instrument parsed `CSIEscape` only in tests if needed. Capture:
|
The established parser representation is:
|
||||||
|
|
||||||
- `priv`
|
- `priv`
|
||||||
- `arg[]`
|
- `arg[]`
|
||||||
@@ -710,7 +739,7 @@ Expose or instrument parsed `CSIEscape` only in tests if needed. Capture:
|
|||||||
- `mode[1]`
|
- `mode[1]`
|
||||||
- raw buffer
|
- 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
|
## 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.
|
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 local echo,
|
||||||
- no screen rendering,
|
- no screen rendering,
|
||||||
@@ -807,7 +837,9 @@ Use the same response path as device status reports. Ensure:
|
|||||||
|
|
||||||
## 7.6 Debug tracing
|
## 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
|
```text
|
||||||
eterm kitty-kbd: query -> response flags=31
|
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
|
# 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
|
## 8.1 Add key-up forwarding
|
||||||
|
|
||||||
Current `UITerminal::onKeyUp()` returns `1` without 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
|
```cpp
|
||||||
Uint32 UITerminal::onKeyUp( const KeyEvent& event ) {
|
Uint32 UITerminal::onKeyUp( const KeyEvent& event ) {
|
||||||
@@ -850,7 +888,7 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& event ) {
|
|||||||
|
|
||||||
Add `TerminalDisplay::onKeyUp()`.
|
Add `TerminalDisplay::onKeyUp()`.
|
||||||
|
|
||||||
Only emit a release sequence when:
|
The worker only emits a release sequence when:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
activeFlags & REPORT_EVENT_TYPES
|
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
|
## 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.
|
With `REPORT_ALL_KEYS_AS_ESCAPE_CODES` enabled, Enter is encoded as CSI-u. Ctrl+Enter becomes:
|
||||||
- Modified Enter must be encoded as CSI-u.
|
|
||||||
- Ctrl+Enter must become:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CSI 13 ; 5 u
|
CSI 13 ; 5 u
|
||||||
@@ -1108,7 +1146,9 @@ Ctrl+Shift+Enter:
|
|||||||
CSI 13 ; 6 u
|
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
|
## 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
|
# 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:
|
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.
|
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
|
```cpp
|
||||||
struct SuppressedTextInput {
|
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.
|
This is more correct for keyboard layouts and IME but requires careful event ordering and latency handling.
|
||||||
|
|
||||||
Recommended architecture:
|
Recommended worker-side architecture:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
std::optional<PendingKittyTextKey> mPendingTextKey;
|
std::optional<PendingKittyTextKey> mPendingTextKey;
|
||||||
@@ -1357,16 +1401,11 @@ Do not ship an implementation that duplicates printable characters.
|
|||||||
|
|
||||||
# 14. Changes to `TerminalDisplay`
|
# 14. Changes to `TerminalDisplay`
|
||||||
|
|
||||||
## 14.1 Add active flag accessor
|
## 14.1 Do not expose active flags to the display
|
||||||
|
|
||||||
Use:
|
`TerminalDisplay` must not query `TerminalEmulator`, and a flag value copied through
|
||||||
|
`TerminalSnapshot` is not authoritative enough for input encoding. The display handles local UI
|
||||||
```cpp
|
shortcuts and forwards the remaining semantic event to `TerminalSession`.
|
||||||
const auto flags =
|
|
||||||
mTerminal->getKeyboardEnhancementFlags();
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not cache flags in `TerminalDisplay`; application output may change them at any moment.
|
|
||||||
|
|
||||||
## 14.2 Refactor `onKeyDown()`
|
## 14.2 Refactor `onKeyDown()`
|
||||||
|
|
||||||
@@ -1388,7 +1427,7 @@ void TerminalDisplay::onKeyDown(
|
|||||||
if ( handleTerminalShortcut( keyCode, smod ) )
|
if ( handleTerminalShortcut( keyCode, smod ) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const KittyKeyEvent event{
|
const TerminalKeyEvent event{
|
||||||
keyCode,
|
keyCode,
|
||||||
scancode,
|
scancode,
|
||||||
chr,
|
chr,
|
||||||
@@ -1398,33 +1437,17 @@ void TerminalDisplay::onKeyDown(
|
|||||||
: KittyKeyEventType::Press
|
: KittyKeyEventType::Press
|
||||||
};
|
};
|
||||||
|
|
||||||
const auto enhanced = KittyKeyboardEncoder::encode(
|
mSession->keyDown( std::move( event ) );
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```cpp
|
||||||
bool handleTerminalShortcut(...);
|
bool handleTerminalShortcut(...);
|
||||||
void handleLegacyKeyDown(...);
|
KittyEncodedKey encodeChildKeyEvent(...); // worker only
|
||||||
```
|
```
|
||||||
|
|
||||||
## 14.3 Add `onKeyUp()`
|
## 14.3 Add `onKeyUp()`
|
||||||
@@ -1436,33 +1459,20 @@ void TerminalDisplay::onKeyUp(
|
|||||||
const Uint32& mod,
|
const Uint32& mod,
|
||||||
const Scancode& scancode
|
const Scancode& scancode
|
||||||
) {
|
) {
|
||||||
const auto flags =
|
TerminalKeyEvent event{
|
||||||
mTerminal->getKeyboardEnhancementFlags();
|
|
||||||
|
|
||||||
if ( !( flags & REPORT_EVENT_TYPES ) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
KittyKeyEvent event{
|
|
||||||
keyCode,
|
keyCode,
|
||||||
scancode,
|
scancode,
|
||||||
chr,
|
chr,
|
||||||
mod,
|
mod,
|
||||||
KittyKeyEventType::Release
|
KittyKeyEventType::Release
|
||||||
};
|
};
|
||||||
|
mSession->keyUp( std::move( event ) );
|
||||||
const auto encoded =
|
|
||||||
KittyKeyboardEncoder::encode( event, flags );
|
|
||||||
|
|
||||||
if ( encoded.handled )
|
|
||||||
mTerminal->ttywrite(
|
|
||||||
encoded.bytes.data(),
|
|
||||||
encoded.bytes.size(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
```cpp
|
||||||
std::unordered_set<Scancode> mLocallyConsumedKeys;
|
std::unordered_set<Scancode> mLocallyConsumedKeys;
|
||||||
@@ -1474,17 +1484,15 @@ Refactor:
|
|||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
void TerminalDisplay::onTextInput( const Uint32& chr ) {
|
void TerminalDisplay::onTextInput( const Uint32& chr ) {
|
||||||
if ( !mTerminal )
|
if ( !mSession )
|
||||||
return;
|
return;
|
||||||
|
mSession->textInput( chr );
|
||||||
if ( handlePendingKittyTextInput( chr ) )
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Existing UTF-8 legacy behavior.
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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 key-up.
|
||||||
- Forward repeat status if `KeyEvent` exposes it.
|
- Forward repeat status if `KeyEvent` exposes it.
|
||||||
|
- Forward semantic key/text data through `TerminalSession`; never read emulator state.
|
||||||
- Preserve focus checks.
|
- Preserve focus checks.
|
||||||
- Preserve custom keybinding command interception.
|
- Preserve custom keybinding command interception.
|
||||||
- Clear pending Kitty input state on focus loss if there is an existing focus callback.
|
- 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:
|
When active flags are zero:
|
||||||
|
|
||||||
- `TerminalDisplay::onKeyDown()` must execute the old code path.
|
- semantic key/text commands must produce the same bytes as the old `TerminalDisplay` path,
|
||||||
- `onTextInput()` must behave exactly as before.
|
- worker-side legacy text input must behave exactly as before,
|
||||||
- `onKeyUp()` must emit nothing.
|
- `onKeyUp()` must emit nothing.
|
||||||
- Existing `Ctrl+A` through control-code conversion must remain unchanged.
|
- Existing `Ctrl+A` through control-code conversion must remain unchanged.
|
||||||
- Alt+Enter must retain `ESC CR`.
|
- Alt+Enter must retain `ESC CR`.
|
||||||
@@ -1540,7 +1549,9 @@ Add regression tests comparing old expected output bytes.
|
|||||||
|
|
||||||
# 17. Handling Ctrl-letter logic
|
# 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:
|
Reason:
|
||||||
|
|
||||||
@@ -1564,10 +1575,12 @@ Do not leave the Ctrl block before the enhanced encoder.
|
|||||||
Distinguish:
|
Distinguish:
|
||||||
|
|
||||||
- Child output is parsed by `TerminalEmulator`.
|
- 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.
|
- Renderer output must never contain protocol responses.
|
||||||
- `ttywrite(..., may_echo=0)` is likely correct for terminal-generated responses.
|
- `ttywrite(..., may_echo=0)` is likely correct for terminal-generated responses.
|
||||||
- User key input should maintain current `may_echo` semantics, likely `1`.
|
- 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:
|
Audit current uses:
|
||||||
|
|
||||||
@@ -1703,20 +1716,23 @@ Enter -> 0d
|
|||||||
Ctrl+Enter -> 0d // current legacy behavior retained
|
Ctrl+Enter -> 0d // current legacy behavior retained
|
||||||
```
|
```
|
||||||
|
|
||||||
With disambiguation:
|
With disambiguation but without report-all:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Enter -> legacy CR, unless spec requires encoded form
|
Enter -> legacy CR
|
||||||
Ctrl+Enter -> 1b 5b 31 33 3b 35 75
|
Ctrl+Enter -> legacy CR
|
||||||
Shift+Enter -> 1b 5b 31 33 3b 32 75
|
Shift+Enter -> legacy CR
|
||||||
Alt+Enter -> protocol form required by spec
|
Alt+Enter -> legacy ESC CR
|
||||||
Ctrl+Shift+Enter -> protocol form with modifier 6
|
Ctrl+Shift+Enter -> legacy CR
|
||||||
```
|
```
|
||||||
|
|
||||||
With report-all:
|
With report-all:
|
||||||
|
|
||||||
```text
|
```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:
|
With event types:
|
||||||
@@ -1817,10 +1833,10 @@ Enter -> 0d
|
|||||||
Ctrl+Enter -> 0d
|
Ctrl+Enter -> 0d
|
||||||
```
|
```
|
||||||
|
|
||||||
Use a helper to activate flags:
|
Use a helper to activate report-all (which implies disambiguation):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
printf '\033[>1u'
|
printf '\033[>8u'
|
||||||
```
|
```
|
||||||
|
|
||||||
Then press Ctrl+Enter and verify:
|
Then press Ctrl+Enter and verify:
|
||||||
@@ -1850,23 +1866,26 @@ ESC [ ? u
|
|||||||
and read:
|
and read:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ESC [ ? <supported-mask> u
|
ESC [ ? <current-flags> u
|
||||||
```
|
```
|
||||||
|
|
||||||
## 21.3 Codex CLI
|
## 21.3 Codex CLI
|
||||||
|
|
||||||
1. Build and run eTerm from the modified `develop` branch.
|
1. Build and run eTerm from the modified `develop` branch.
|
||||||
2. Start current Codex CLI.
|
2. Start current Codex CLI and capture its push/set sequence.
|
||||||
3. Configure Codex:
|
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.
|
- Enter inserts newline.
|
||||||
- Ctrl+Enter submits.
|
- Ctrl+Enter submits.
|
||||||
4. Type a multiline prompt.
|
5. Type a multiline prompt.
|
||||||
5. Press Enter: newline appears.
|
6. Press Enter: newline appears.
|
||||||
6. Press Ctrl+Enter: message submits.
|
7. Press Ctrl+Enter: message submits when report-all is active.
|
||||||
7. Hold/repeat keys to ensure no duplicate or stuck events.
|
8. Hold/repeat keys to ensure no duplicate or stuck events.
|
||||||
8. Exit Codex normally.
|
9. Exit Codex normally.
|
||||||
9. Verify shell input returns to legacy behavior.
|
10. Verify shell input returns to legacy behavior.
|
||||||
10. Kill Codex abruptly and verify a restarted shell/process does not inherit stale flags.
|
11. Kill Codex abruptly and verify a restarted shell/process does not inherit stale flags.
|
||||||
|
|
||||||
## 21.4 Other TUIs
|
## 21.4 Other TUIs
|
||||||
|
|
||||||
@@ -1921,25 +1940,23 @@ Use constants and comments.
|
|||||||
|
|
||||||
# 23. Threading and lifetime
|
# 23. Threading and lifetime
|
||||||
|
|
||||||
Determine which thread:
|
The thread model is established:
|
||||||
|
|
||||||
- parses PTY output,
|
- the `TerminalSession` worker exclusively owns `TerminalEmulator`, PTY reads/writes, parser state,
|
||||||
- handles UI input,
|
terminal modes, Kitty flags/stacks, pending key/text correlation, and child-input encoding;
|
||||||
- reads active flags.
|
- 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:
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -2019,12 +2036,13 @@ allowing modern TUIs to distinguish modified Enter and other keys.
|
|||||||
|
|
||||||
Deliverables:
|
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
|
- Legacy byte snapshots
|
||||||
- Confirm keydown/textinput ordering
|
- Confirm keydown/textinput ordering
|
||||||
- Confirm repeat exposure
|
- Confirm repeat exposure
|
||||||
- Confirm modifier constants
|
- Confirm modifier constants
|
||||||
- Confirm thread model
|
- Confirm thread model (**completed**; emulator and protocol state are worker-owned)
|
||||||
|
|
||||||
No functional change.
|
No functional change.
|
||||||
|
|
||||||
@@ -2038,6 +2056,7 @@ Deliverables:
|
|||||||
- Push/pop/set
|
- Push/pop/set
|
||||||
- Reset behavior
|
- Reset behavior
|
||||||
- Unit tests
|
- Unit tests
|
||||||
|
- Typed semantic key/text `TerminalSession` commands, initially using legacy worker-side encoding
|
||||||
|
|
||||||
Still no enhanced key emission.
|
Still no enhanced key emission.
|
||||||
|
|
||||||
@@ -2048,7 +2067,8 @@ Deliverables:
|
|||||||
- Encoder framework
|
- Encoder framework
|
||||||
- Enter, Tab, Escape, Backspace
|
- Enter, Tab, Escape, Backspace
|
||||||
- Modified key CSI-u emission
|
- 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
|
- Legacy fallback intact
|
||||||
|
|
||||||
This phase can be merged independently if desired.
|
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
|
## Encoding
|
||||||
|
|
||||||
- [ ] Plain legacy behavior is unchanged with flags zero.
|
- [ ] Plain legacy behavior is unchanged with flags zero.
|
||||||
- [ ] Ctrl+Enter is `CSI 13;5u` when disambiguation is active.
|
- [ ] Ctrl+Enter is `CSI 13;5u` when report-all is active.
|
||||||
- [ ] Shift+Enter is distinguishable.
|
- [ ] Shift+Enter is distinguishable when report-all is active.
|
||||||
- [ ] Modified Escape, Tab, and Backspace are distinguishable.
|
- [ ] Modified Escape is distinguishable under disambiguation; modified Tab and Backspace are
|
||||||
|
distinguishable when report-all is active.
|
||||||
- [ ] Special/function keys follow official Kitty encoding.
|
- [ ] Special/function keys follow official Kitty encoding.
|
||||||
- [ ] Printable Unicode keys work in report-all mode.
|
- [ ] Printable Unicode keys work in report-all mode.
|
||||||
- [ ] No duplicate text-input events occur.
|
- [ ] No duplicate text-input events occur.
|
||||||
@@ -2133,7 +2154,8 @@ The implementation is complete only when all of the following are true.
|
|||||||
## Integration
|
## Integration
|
||||||
|
|
||||||
- [ ] Crossterm decodes the events with correct code/modifier/kind.
|
- [ ] 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.
|
- [ ] 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.
|
- [ ] Abrupt child termination does not leave stale protocol state for a replacement process.
|
||||||
|
|
||||||
@@ -2191,16 +2213,33 @@ KittyKeyboardState& activeKeyboardState();
|
|||||||
const KittyKeyboardState& activeKeyboardState() const;
|
const KittyKeyboardState& activeKeyboardState() const;
|
||||||
|
|
||||||
bool handleKittyKeyboardProtocol();
|
bool handleKittyKeyboardProtocol();
|
||||||
void replyKittyKeyboardProtocolSupport();
|
void replyKittyKeyboardProtocolState();
|
||||||
void resetKittyKeyboardProtocol();
|
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
|
```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`
|
## `src/modules/eterm/src/eterm/terminal/terminalemulator.cpp`
|
||||||
|
|
||||||
Modify:
|
Modify:
|
||||||
@@ -2213,7 +2252,8 @@ Modify:
|
|||||||
- terminal-generated query response
|
- terminal-generated query response
|
||||||
- optional debug tracing
|
- 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`
|
## `src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp`
|
||||||
|
|
||||||
@@ -2221,20 +2261,18 @@ Add:
|
|||||||
|
|
||||||
- `onKeyUp`
|
- `onKeyUp`
|
||||||
- repeat parameter or event kind
|
- repeat parameter or event kind
|
||||||
- pending/suppressed text state
|
|
||||||
- locally consumed key tracking
|
- 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`
|
## `src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp`
|
||||||
|
|
||||||
Modify:
|
Modify:
|
||||||
|
|
||||||
- place enhanced encoding before Ctrl-letter legacy handling
|
|
||||||
- retain terminal shortcut priority
|
- retain terminal shortcut priority
|
||||||
- add key-up behavior
|
- add key-up behavior
|
||||||
- add repeat behavior
|
- add repeat behavior
|
||||||
- add text-input correlation
|
- enqueue semantic key/text commands instead of encoded bytes
|
||||||
- preserve legacy map unchanged as fallback
|
- move child-facing Ctrl conversion and the legacy map into the worker-side fallback
|
||||||
|
|
||||||
## `src/modules/eterm/src/eterm/ui/uiterminal.cpp`
|
## `src/modules/eterm/src/eterm/ui/uiterminal.cpp`
|
||||||
|
|
||||||
@@ -2264,12 +2302,12 @@ Before submitting:
|
|||||||
8. Verify release events for locally consumed shortcuts are suppressed.
|
8. Verify release events for locally consumed shortcuts are suppressed.
|
||||||
9. Verify stack depth and pop count are bounded.
|
9. Verify stack depth and pop count are bounded.
|
||||||
10. Verify unsupported bits are masked.
|
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.
|
12. Verify Unicode scalar validation.
|
||||||
13. Verify AltGr/IME behavior.
|
13. Verify AltGr/IME behavior.
|
||||||
14. Verify legacy snapshots with flags zero.
|
14. Verify legacy snapshots with flags zero.
|
||||||
15. Verify Crossterm key dump.
|
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.
|
17. Verify process restart clears stale state.
|
||||||
18. Run formatter and full test suite.
|
18. Run formatter and full test suite.
|
||||||
|
|
||||||
@@ -2280,7 +2318,7 @@ Before submitting:
|
|||||||
A very small patch can solve only the immediate issue:
|
A very small patch can solve only the immediate issue:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if ( flags & DISAMBIGUATE_ESCAPE_CODES &&
|
if ( flags & REPORT_ALL_KEYS_AS_ESCAPE_CODES &&
|
||||||
keyCode == KEY_RETURN &&
|
keyCode == KEY_RETURN &&
|
||||||
mod & KEYMOD_CTRL ) {
|
mod & KEYMOD_CTRL ) {
|
||||||
ttywrite( "\033[13;5u", 7, 1 );
|
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,
|
- capability query,
|
||||||
- push/pop stack,
|
- push/pop stack,
|
||||||
@@ -2316,13 +2356,13 @@ ESC [ ? u
|
|||||||
eTerm replies:
|
eTerm replies:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ESC [ ? 31 u
|
ESC [ ? 0 u
|
||||||
```
|
```
|
||||||
|
|
||||||
Application sends:
|
Application sends:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ESC [ > 7 u
|
ESC [ > 15 u
|
||||||
```
|
```
|
||||||
|
|
||||||
eTerm stores prior flags on the current screen’s stack and activates:
|
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
|
DISAMBIGUATE_ESCAPE_CODES
|
||||||
REPORT_EVENT_TYPES
|
REPORT_EVENT_TYPES
|
||||||
REPORT_ALTERNATE_KEYS
|
REPORT_ALTERNATE_KEYS
|
||||||
|
REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||||
```
|
```
|
||||||
|
|
||||||
User presses Ctrl+Enter.
|
User presses Ctrl+Enter.
|
||||||
@@ -2364,7 +2405,8 @@ KeyModifiers::CONTROL
|
|||||||
KeyEventKind::Press
|
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:
|
Application exits cleanly and sends:
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
#include <eepp/window/keycodes.hpp>
|
#include <eepp/window/keycodes.hpp>
|
||||||
#include <eepp/window/window.hpp>
|
#include <eepp/window/window.hpp>
|
||||||
#include <eterm/system/iprocessfactory.hpp>
|
#include <eterm/system/iprocessfactory.hpp>
|
||||||
#include <eterm/terminal/iterminaldisplay.hpp>
|
|
||||||
#include <eterm/terminal/terminalcolorscheme.hpp>
|
#include <eterm/terminal/terminalcolorscheme.hpp>
|
||||||
#include <eterm/terminal/terminalemulator.hpp>
|
#include <eterm/terminal/terminalsession.hpp>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -128,7 +127,7 @@ class TerminalKeyMap {
|
|||||||
|
|
||||||
extern TerminalKeyMap terminalKeyMap;
|
extern TerminalKeyMap terminalKeyMap;
|
||||||
|
|
||||||
class TerminalDisplay : public ITerminalDisplay {
|
class TerminalDisplay {
|
||||||
public:
|
public:
|
||||||
enum class EventType {
|
enum class EventType {
|
||||||
TITLE,
|
TITLE,
|
||||||
@@ -136,6 +135,10 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
SCROLL_HISTORY,
|
SCROLL_HISTORY,
|
||||||
HISTORY_LENGTH_CHANGE,
|
HISTORY_LENGTH_CHANGE,
|
||||||
PROCESS_EXIT,
|
PROCESS_EXIT,
|
||||||
|
BELL,
|
||||||
|
CLIPBOARD,
|
||||||
|
RESTART_FAILURE,
|
||||||
|
WORKER_ERROR,
|
||||||
UNKNOWN
|
UNKNOWN
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,11 +148,8 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
};
|
};
|
||||||
|
|
||||||
typedef std::function<void( const TerminalDisplay::Event& event )> EventFunc;
|
typedef std::function<void( const TerminalDisplay::Event& event )> EventFunc;
|
||||||
|
using DataFunc = std::function<void( const char*, size_t )>;
|
||||||
static std::shared_ptr<TerminalDisplay>
|
using PromptStateChangedFunc = std::function<void( PromptState, std::string_view )>;
|
||||||
create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
|
|
||||||
std::shared_ptr<TerminalEmulator>&& terminalEmulator,
|
|
||||||
const bool& useFrameBuffer = false );
|
|
||||||
|
|
||||||
static std::shared_ptr<TerminalDisplay>
|
static std::shared_ptr<TerminalDisplay>
|
||||||
create( EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
|
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,
|
IProcessFactory* processFactory = nullptr, bool useFrameBuffer = false,
|
||||||
bool keepAlive = true, const std::unordered_map<std::string, std::string>& env = {} );
|
bool keepAlive = true, const std::unordered_map<std::string, std::string>& env = {} );
|
||||||
|
|
||||||
virtual ~TerminalDisplay();
|
~TerminalDisplay();
|
||||||
|
|
||||||
virtual void resetColors();
|
void resetColors();
|
||||||
virtual int resetColor( const Uint32& index, const char* name );
|
int resetColor( const Uint32& index, const char* name );
|
||||||
virtual bool getColor( const Uint32& index, unsigned char* r, unsigned char* g,
|
bool getColor( const Uint32& index, unsigned char* r, unsigned char* g, unsigned char* b );
|
||||||
unsigned char* b );
|
|
||||||
|
|
||||||
virtual void setTitle( const char* title );
|
void setClipboard( const char* text );
|
||||||
virtual void setIconTitle( const char* title );
|
const char* getClipboard() const;
|
||||||
|
|
||||||
virtual void setClipboard( const char* text );
|
bool update( bool isMouseOverMe = true );
|
||||||
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 );
|
|
||||||
|
|
||||||
void executeFile( const std::string& cmd );
|
void executeFile( const std::string& cmd );
|
||||||
|
|
||||||
@@ -188,20 +179,20 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
|
|
||||||
void draw();
|
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,
|
void onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod,
|
||||||
const Scancode& scancode );
|
const Scancode& scancode );
|
||||||
|
|
||||||
bool isRegisteredShortcut( const Keycode& keyCode, const Uint32& mod ) const;
|
bool isRegisteredShortcut( const Keycode& keyCode, const Uint32& mod ) const;
|
||||||
|
|
||||||
@@ -251,14 +242,40 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
|
|
||||||
void setPadding( const Rectf& padding );
|
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 scrollSize() const;
|
||||||
|
|
||||||
int rowCount() const;
|
int rowCount() const;
|
||||||
|
|
||||||
|
int scrollPosition() const;
|
||||||
|
|
||||||
|
Uint64 scrollTo( int position );
|
||||||
|
|
||||||
|
Uint64 lastAppliedScrollCommand() const;
|
||||||
|
|
||||||
Uint32 pushEventCallback( const EventFunc& func );
|
Uint32 pushEventCallback( const EventFunc& func );
|
||||||
|
|
||||||
void popEventCallback( const Uint32& id );
|
void popEventCallback( const Uint32& id );
|
||||||
@@ -295,12 +312,14 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
EE::Window::Window* mWindow;
|
EE::Window::Window* mWindow;
|
||||||
std::vector<TerminalGlyph> mBuffer;
|
|
||||||
std::vector<Color> mColors;
|
std::vector<Color> mColors;
|
||||||
std::shared_ptr<TerminalEmulator> mTerminal;
|
std::shared_ptr<TerminalSession> mSession;
|
||||||
|
std::shared_ptr<const TerminalSnapshot> mSnapshot;
|
||||||
mutable std::string mClipboardUtf8;
|
mutable std::string mClipboardUtf8;
|
||||||
Uint32 mNumCallBacks;
|
Uint32 mNumCallBacks{ 0 };
|
||||||
std::map<Uint32, EventFunc> mCallbacks;
|
std::map<Uint32, EventFunc> mCallbacks;
|
||||||
|
DataFunc mDataCallback;
|
||||||
|
PromptStateChangedFunc mPromptStateChangedCallback;
|
||||||
|
|
||||||
Font* mFont{ nullptr };
|
Font* mFont{ nullptr };
|
||||||
Float mFontSize{ 12 };
|
Float mFontSize{ 12 };
|
||||||
@@ -322,11 +341,14 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
bool mAlreadyClickedMButton{ false };
|
bool mAlreadyClickedMButton{ false };
|
||||||
bool mKeepAlive{ true };
|
bool mKeepAlive{ true };
|
||||||
bool mDraggingSel{ false };
|
bool mDraggingSel{ false };
|
||||||
|
int mMode{ MODE_VISIBLE | MODE_FOCUSED };
|
||||||
|
TerminalCursorMode mCursorMode{ SteadyUnderline };
|
||||||
Clock mClock;
|
Clock mClock;
|
||||||
Clock mLastDoubleClick;
|
Clock mLastDoubleClick;
|
||||||
Uint32 mColumns{ 0 };
|
Uint32 mColumns{ 0 };
|
||||||
Uint32 mRows{ 0 };
|
Uint32 mRows{ 0 };
|
||||||
Uint32 mClickStep{ 5 };
|
Uint32 mClickStep{ 5 };
|
||||||
|
Uint64 mSnapshotGeneration{ 0 };
|
||||||
FontHinting mFontHinting{ FontHinting::Full };
|
FontHinting mFontHinting{ FontHinting::Full };
|
||||||
FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale };
|
FontAntialiasing mFontAntialiasing{ FontAntialiasing::Grayscale };
|
||||||
FrameBufferUniquePtr mFrameBuffer;
|
FrameBufferUniquePtr mFrameBuffer;
|
||||||
@@ -334,6 +356,7 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
VertexBufferUniquePtr mVBForeground;
|
VertexBufferUniquePtr mVBForeground;
|
||||||
std::vector<VertexBufferUniquePtr> mVBStyles;
|
std::vector<VertexBufferUniquePtr> mVBStyles;
|
||||||
TerminalColorScheme mColorScheme;
|
TerminalColorScheme mColorScheme;
|
||||||
|
TerminalColorScheme mInitialColorScheme;
|
||||||
Uint32 mQuadVertex{ 6 };
|
Uint32 mQuadVertex{ 6 };
|
||||||
Primitives mPrimitives;
|
Primitives mPrimitives;
|
||||||
Vector2u mCurGridPos;
|
Vector2u mCurGridPos;
|
||||||
@@ -356,9 +379,13 @@ class TerminalDisplay : public ITerminalDisplay {
|
|||||||
|
|
||||||
void onSizeChange();
|
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 );
|
void sendEvent( const TerminalDisplay::Event& event );
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ using namespace eterm::System;
|
|||||||
namespace eterm { namespace Terminal {
|
namespace eterm { namespace Terminal {
|
||||||
|
|
||||||
constexpr int ESC_BUF_SIZ = 512;
|
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_BUF_SIZ = ESC_BUF_SIZ;
|
||||||
constexpr int STR_ARG_SIZ = ESC_ARG_SIZ;
|
constexpr int STR_ARG_SIZ = ESC_ARG_SIZ;
|
||||||
|
|
||||||
@@ -104,6 +104,9 @@ struct CSIEscape {
|
|||||||
size_t len; /* raw string length */
|
size_t len; /* raw string length */
|
||||||
char priv;
|
char priv;
|
||||||
int arg[ESC_ARG_SIZ];
|
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 */
|
int narg; /* nb of args */
|
||||||
char mode[2];
|
char mode[2];
|
||||||
};
|
};
|
||||||
@@ -155,6 +158,9 @@ class TerminalEmulator final {
|
|||||||
|
|
||||||
void redraw();
|
void redraw();
|
||||||
|
|
||||||
|
/** Worker-owned terminal state reset (RIS semantics without replacing the PTY/process). */
|
||||||
|
void reset();
|
||||||
|
|
||||||
void logError( const char* err );
|
void logError( const char* err );
|
||||||
|
|
||||||
/** @return If the tty read was completed or there's still buffer to read (true completed) */
|
/** @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 );
|
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;
|
Vector2i getSize() const;
|
||||||
|
|
||||||
System::IProcess* getProcess() const;
|
System::IProcess* getProcess() const;
|
||||||
@@ -277,8 +292,9 @@ class TerminalEmulator final {
|
|||||||
|
|
||||||
bool mDirty{ true };
|
bool mDirty{ true };
|
||||||
bool mAllDirty{ true };
|
bool mAllDirty{ true };
|
||||||
uint8_t mDeferredPresentationBatches{ 0 };
|
|
||||||
Clock mPresentationClock;
|
Clock mPresentationClock;
|
||||||
|
Time mPresentationInterval{ Microseconds( 1000000.0 / 60.0 ) };
|
||||||
|
Clock mSynchronizedUpdateClock;
|
||||||
bool mAllowMemoryTrimnming{ false };
|
bool mAllowMemoryTrimnming{ false };
|
||||||
int mExitCode;
|
int mExitCode;
|
||||||
|
|
||||||
@@ -299,8 +315,12 @@ class TerminalEmulator final {
|
|||||||
|
|
||||||
int mAllowAltScreen;
|
int mAllowAltScreen;
|
||||||
int mAllowWindowOps;
|
int mAllowWindowOps;
|
||||||
|
TerminalCursorMode mDefaultCursorMode{ SteadyUnderline };
|
||||||
|
bool mColorSchemeNotifications{ false };
|
||||||
|
int mColorScheme{ 0 };
|
||||||
|
|
||||||
std::string mCurrentWorkingDirectory;
|
std::string mCurrentWorkingDirectory;
|
||||||
|
Vector2i mLastMousePosition{ -1, -1 };
|
||||||
PromptState mPromptState{ PromptState::Unknown };
|
PromptState mPromptState{ PromptState::Unknown };
|
||||||
PromptStateChangedCb mPromptStateChangedCb;
|
PromptStateChangedCb mPromptStateChangedCb;
|
||||||
DataCb mDataCb;
|
DataCb mDataCb;
|
||||||
@@ -355,7 +375,7 @@ class TerminalEmulator final {
|
|||||||
void historyStealPush( Line* lineSlot, int col );
|
void historyStealPush( Line* lineSlot, int col );
|
||||||
void historyReflow( int old_col, int new_col );
|
void historyReflow( int old_col, int new_col );
|
||||||
void historyPopToScreen( int loaded, int col );
|
void historyPopToScreen( int loaded, int col );
|
||||||
void tsetattr( int*, int );
|
void tsetattr( int*, int, const char* );
|
||||||
void tsetchar( Rune, TerminalGlyph*, int, int );
|
void tsetchar( Rune, TerminalGlyph*, int, int );
|
||||||
void tsetdirt( int, int );
|
void tsetdirt( int, int );
|
||||||
void tsetscroll( int, int );
|
void tsetscroll( int, int );
|
||||||
@@ -366,7 +386,7 @@ class TerminalEmulator final {
|
|||||||
void tcontrolcode( uchar );
|
void tcontrolcode( uchar );
|
||||||
void tdectest( char );
|
void tdectest( char );
|
||||||
void tdefutf8( char );
|
void tdefutf8( char );
|
||||||
int32_t tdefcolor( int*, int*, int );
|
int32_t tdefcolor( int*, const char*, int*, int );
|
||||||
void tdeftran( char );
|
void tdeftran( char );
|
||||||
void tstrsequence( uchar );
|
void tstrsequence( uchar );
|
||||||
|
|
||||||
@@ -406,6 +426,8 @@ class TerminalEmulator final {
|
|||||||
int xgetcolor( int x, unsigned char* r, unsigned char* g, unsigned char* b );
|
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 osc_color_response( int num, int index, int is_osc4 );
|
||||||
void handleDeviceAttributes();
|
void handleDeviceAttributes();
|
||||||
|
int colorScheme();
|
||||||
|
void reportColorScheme();
|
||||||
|
|
||||||
void trimMemory();
|
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_MOUSEMANY = 1 << 15,
|
||||||
MODE_BRCKTPASTE = 1 << 16,
|
MODE_BRCKTPASTE = 1 << 16,
|
||||||
MODE_NUMLOCK = 1 << 17,
|
MODE_NUMLOCK = 1 << 17,
|
||||||
|
MODE_ALTSCRROLL = 1 << 18,
|
||||||
MODE_MOUSE = MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEX10 | MODE_MOUSEMANY,
|
MODE_MOUSE = MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEX10 | MODE_MOUSEMANY,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -128,10 +128,13 @@ class UITerminal : public UIWidget {
|
|||||||
ScrollViewType mViewType{ ScrollViewType::Overlay };
|
ScrollViewType mViewType{ ScrollViewType::Overlay };
|
||||||
ScrollBarMode mVScrollMode{ ScrollBarMode::Auto };
|
ScrollBarMode mVScrollMode{ ScrollBarMode::Auto };
|
||||||
UIScrollBar* mVScroll{ nullptr };
|
UIScrollBar* mVScroll{ nullptr };
|
||||||
int mScrollOffset;
|
int mScrollOffset{ 0 };
|
||||||
bool mScrollByBar{ false };
|
bool mScrollByBar{ false };
|
||||||
|
bool mPendingContentSizeChange{ false };
|
||||||
|
Uint64 mPendingScrollCommand{ 0 };
|
||||||
Clock mMouseClock;
|
Clock mMouseClock;
|
||||||
std::shared_ptr<TerminalDisplay> mTerm;
|
std::shared_ptr<TerminalDisplay> mTerm;
|
||||||
|
Uint32 mTerminalEventCallbackId{ 0 };
|
||||||
|
|
||||||
UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay );
|
UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay );
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <eepp/system/filesystem.hpp>
|
#include <eepp/system/filesystem.hpp>
|
||||||
#include <eepp/system/log.hpp>
|
#include <eepp/system/log.hpp>
|
||||||
|
#include <eepp/system/sys.hpp>
|
||||||
#include <eepp/version.hpp>
|
#include <eepp/version.hpp>
|
||||||
#include <eterm/system/process.hpp>
|
#include <eterm/system/process.hpp>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
@@ -109,54 +110,83 @@ int Process::getExitCode() const {
|
|||||||
|
|
||||||
Process::Process( int pid ) : mPID( pid ) {}
|
Process::Process( int pid ) : mPID( pid ) {}
|
||||||
|
|
||||||
static void execshell( const char* cmd, const char* const* args, std::string workingDirectory,
|
struct ProcessLaunchData {
|
||||||
const std::unordered_map<std::string, std::string>& env ) {
|
std::string executable;
|
||||||
const struct passwd* pw;
|
std::string workingDirectory;
|
||||||
const char* sh;
|
std::vector<char*> arguments;
|
||||||
|
std::vector<std::string> environmentStorage;
|
||||||
|
std::vector<char*> environment;
|
||||||
|
};
|
||||||
|
|
||||||
errno = 0;
|
static bool prepareProcessLaunch( const std::string& program, const std::vector<std::string>& args,
|
||||||
if ( ( pw = getpwuid( getuid() ) ) == NULL ) {
|
const std::string& workingDirectory,
|
||||||
if ( errno ) {
|
const std::unordered_map<std::string, std::string>& env,
|
||||||
fprintf( stderr, "getpwuid: %s\n", strerror( errno ) );
|
ProcessLaunchData& launch ) {
|
||||||
_exit( 1 );
|
long passwordBufferSize = sysconf( _SC_GETPW_R_SIZE_MAX );
|
||||||
} else {
|
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" );
|
fprintf( stderr, "who are you?\n" );
|
||||||
_exit( 1 );
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const struct passwd* pw = passwordResult;
|
||||||
|
|
||||||
if ( ( sh = getenv( "SHELL" ) ) == NULL )
|
launch.executable = Sys::which( program );
|
||||||
sh = ( pw->pw_shell[0] ) ? pw->pw_shell : cmd;
|
if ( launch.executable.empty() )
|
||||||
|
launch.executable = program;
|
||||||
|
launch.workingDirectory = workingDirectory.empty() ? pw->pw_dir : workingDirectory;
|
||||||
|
|
||||||
if ( workingDirectory.empty() )
|
launch.arguments.reserve( args.size() + 2 );
|
||||||
workingDirectory = pw->pw_dir;
|
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" );
|
launch.environmentStorage.reserve( environment.size() );
|
||||||
unsetenv( "LINES" );
|
for ( const auto& entry : environment )
|
||||||
unsetenv( "TERMCAP" );
|
launch.environmentStorage.emplace_back( entry.first + "=" + entry.second );
|
||||||
setenv( "LOGNAME", pw->pw_name, 1 );
|
launch.environment.reserve( launch.environmentStorage.size() + 1 );
|
||||||
setenv( "USER", pw->pw_name, 1 );
|
for ( auto& entry : launch.environmentStorage )
|
||||||
setenv( "SHELL", sh, 1 );
|
launch.environment.emplace_back( entry.data() );
|
||||||
setenv( "HOME", pw->pw_dir, 1 );
|
launch.environment.emplace_back( nullptr );
|
||||||
setenv( "TERM", "xterm-256color", 1 );
|
return true;
|
||||||
setenv( "TERM_PROGRAM", "eterm", 1 );
|
}
|
||||||
setenv( "TERM_PROGRAM_VERSION", EE::Version::getVersionName( false ).c_str(), 1 );
|
|
||||||
setenv( "COLORTERM", "24bit", 1 );
|
|
||||||
|
|
||||||
for ( const auto& e : env )
|
static void resetSignalHandlers() {
|
||||||
setenv( e.first.c_str(), e.second.c_str(), 1 );
|
struct sigaction action;
|
||||||
|
memset( &action, 0, sizeof( action ) );
|
||||||
signal( SIGCHLD, SIG_DFL );
|
action.sa_handler = SIG_DFL;
|
||||||
signal( SIGHUP, SIG_DFL );
|
sigemptyset( &action.sa_mask );
|
||||||
signal( SIGINT, SIG_DFL );
|
sigaction( SIGCHLD, &action, nullptr );
|
||||||
signal( SIGQUIT, SIG_DFL );
|
sigaction( SIGHUP, &action, nullptr );
|
||||||
signal( SIGTERM, SIG_DFL );
|
sigaction( SIGINT, &action, nullptr );
|
||||||
signal( SIGALRM, SIG_DFL );
|
sigaction( SIGQUIT, &action, nullptr );
|
||||||
|
sigaction( SIGTERM, &action, nullptr );
|
||||||
execvp( cmd, (char* const*)args );
|
sigaction( SIGALRM, &action, nullptr );
|
||||||
_exit( 1 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Process> Process::createWithPipe( const std::string& /*program*/,
|
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,
|
const std::string& workingDirectory,
|
||||||
Terminal::PseudoTerminal& pseudoTerminal,
|
Terminal::PseudoTerminal& pseudoTerminal,
|
||||||
const std::unordered_map<std::string, std::string>& env ) {
|
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();
|
int pid = fork();
|
||||||
if ( pid == -1 ) {
|
if ( pid == -1 ) {
|
||||||
fprintf( stderr, "Failed to fork process\n" );
|
fprintf( stderr, "Failed to fork process\n" );
|
||||||
return nullptr;
|
return nullptr;
|
||||||
} else if ( pid == 0 ) {
|
} else if ( pid == 0 ) {
|
||||||
setsid();
|
const int master = (int)pseudoTerminal.mMaster;
|
||||||
dup2( (int)pseudoTerminal.mSlave, 0 );
|
const int slave = (int)pseudoTerminal.mSlave;
|
||||||
dup2( (int)pseudoTerminal.mSlave, 1 );
|
if ( setsid() < 0 || dup2( slave, STDIN_FILENO ) < 0 || dup2( slave, STDOUT_FILENO ) < 0 ||
|
||||||
dup2( (int)pseudoTerminal.mSlave, 2 );
|
dup2( slave, STDERR_FILENO ) < 0 ) {
|
||||||
|
_exit( 1 );
|
||||||
if ( ioctl( (int)pseudoTerminal.mSlave, TIOCSCTTY, NULL ) < 0 ) {
|
|
||||||
fprintf( stderr, "ioctl TIOCSCTTY failed: %s", strerror( errno ) );
|
|
||||||
exit( 1 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( (int)pseudoTerminal.mSlave > 2 )
|
if ( ioctl( slave, TIOCSCTTY, NULL ) < 0 )
|
||||||
close( (int)pseudoTerminal.mSlave );
|
_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__
|
#ifdef __OpenBSD__
|
||||||
if ( pledge( "stdio getpw proc exec", NULL ) == -1 ) {
|
if ( pledge( "stdio proc exec", NULL ) == -1 )
|
||||||
fprintf( stderr, "pledge\n" );
|
_exit( 1 );
|
||||||
exit( 1 );
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
std::vector<const char*> argsV;
|
resetSignalHandlers();
|
||||||
argsV.push_back( program.c_str() );
|
execve( launch.executable.c_str(), launch.arguments.data(), launch.environment.data() );
|
||||||
for ( auto& a : args ) {
|
_exit( 1 );
|
||||||
argsV.push_back( a.c_str() );
|
|
||||||
}
|
|
||||||
argsV.push_back( nullptr );
|
|
||||||
execshell( program.c_str(), argsV.data(), workingDirectory, env );
|
|
||||||
} else {
|
} else {
|
||||||
pseudoTerminal.mSlave.release();
|
pseudoTerminal.mSlave.release();
|
||||||
return std::unique_ptr<Process>( new Process( pid ) );
|
return std::unique_ptr<Process>( new Process( pid ) );
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ bool PseudoTerminal::resize( int columns, int rows ) {
|
|||||||
w.ws_ypixel = 0;
|
w.ws_ypixel = 0;
|
||||||
|
|
||||||
bool masterResized = ioctl( (int)mMaster, TIOCSWINSZ, &w ) >= 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 ) {
|
if ( !masterResized && !slaveResized ) {
|
||||||
perror( "PseudoTerminal::Resize" );
|
perror( "PseudoTerminal::Resize" );
|
||||||
@@ -226,6 +227,17 @@ std::unique_ptr<PseudoTerminal> PseudoTerminal::create( int columns, int rows )
|
|||||||
return nullptr;
|
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 );
|
int flags = fcntl( master, F_GETFL, 0 );
|
||||||
fcntl( master, F_SETFL, flags | O_NONBLOCK );
|
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( 208, 208, 208 ), Color( 218, 218, 218 ), Color( 228, 228, 228 ),
|
||||||
Color( 238, 238, 238 ) };
|
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,
|
static Sizei gridSizeFromTermDimensions( Font* font, const Float& fontSize,
|
||||||
const Sizef& pixelsSize ) {
|
const Sizef& pixelsSize ) {
|
||||||
auto fontHeight = (Float)font->getFontHeight( fontSize );
|
auto fontHeight = (Float)font->getFontHeight( fontSize );
|
||||||
@@ -390,6 +381,19 @@ static Sizei gridSizeFromTermDimensions( Font* font, const Float& fontSize,
|
|||||||
return { clipColumns, clipRows };
|
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(
|
std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
|
||||||
EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
|
EE::Window::Window* window, Font* font, const Float& fontSize, const Sizef& pixelsSize,
|
||||||
std::string program, std::vector<std::string> args, const std::string& workingDir,
|
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>(
|
std::shared_ptr<TerminalDisplay> terminal = std::shared_ptr<TerminalDisplay>(
|
||||||
new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) );
|
new TerminalDisplay( window, font, fontSize, pixelsSize, useFrameBuffer ) );
|
||||||
|
|
||||||
terminal->mTerminal = TerminalEmulator::create( std::move( pseudoTerminal ),
|
terminal->mSession = TerminalSession::create( std::move( pseudoTerminal ), std::move( process ),
|
||||||
std::move( process ), terminal, historySize );
|
historySize, terminal->makeColorPalette() );
|
||||||
|
if ( !terminal->mSession ) {
|
||||||
|
if ( freeProcessFactory )
|
||||||
|
eeSAFE_DELETE( processFactory );
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
terminal->mSession->setPresentationRate( presentationRateForWindow( window ) );
|
||||||
terminal->mProgram = program;
|
terminal->mProgram = program;
|
||||||
terminal->mArgs = args;
|
terminal->mArgs = args;
|
||||||
terminal->mEnv = env;
|
terminal->mEnv = env;
|
||||||
@@ -461,23 +471,26 @@ std::shared_ptr<TerminalDisplay> TerminalDisplay::create(
|
|||||||
return terminal;
|
return terminal;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerminalDisplay::~TerminalDisplay() = default;
|
TerminalDisplay::~TerminalDisplay() {
|
||||||
|
if ( mSession )
|
||||||
|
mSession->shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize,
|
TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const Float& fontSize,
|
||||||
const Sizef& pixelsSize, const bool& useFrameBuffer ) :
|
const Sizef& pixelsSize, const bool& useFrameBuffer ) :
|
||||||
ITerminalDisplay(),
|
|
||||||
mWindow( window ),
|
mWindow( window ),
|
||||||
mFont( font ),
|
mFont( font ),
|
||||||
mFontSize( fontSize ),
|
mFontSize( fontSize ),
|
||||||
mSize( pixelsSize ),
|
mSize( pixelsSize ),
|
||||||
mUseFrameBuffer( useFrameBuffer ),
|
mUseFrameBuffer( useFrameBuffer ),
|
||||||
mColorScheme( TerminalColorScheme::getDefault() ) {
|
mColorScheme( TerminalColorScheme::getDefault() ),
|
||||||
|
mInitialColorScheme( mColorScheme ) {
|
||||||
TerminalGlyph defaultGlyph;
|
TerminalGlyph defaultGlyph;
|
||||||
defaultGlyph.mode = ATTR_INVISIBLE;
|
defaultGlyph.mode = ATTR_INVISIBLE;
|
||||||
mCursorGlyph = defaultGlyph;
|
mCursorGlyph = defaultGlyph;
|
||||||
mColors.resize( eeARRAY_SIZE( colormapped ), Color::Transparent );
|
mColors.resize( eeARRAY_SIZE( colormapped ), Color::Transparent );
|
||||||
mBuffer.resize( mColumns * mRows, defaultGlyph );
|
mMode |= MODE_FOCUSED;
|
||||||
( (int&)mMode ) |= MODE_FOCUSED;
|
resetColors();
|
||||||
|
|
||||||
Sizei gridSize( gridSizeFromTermDimensions( mFont, mFontSize, mSize - mPadding * 2.f ) );
|
Sizei gridSize( gridSizeFromTermDimensions( mFont, mFontSize, mSize - mPadding * 2.f ) );
|
||||||
mDirtyLines.resize( gridSize.getHeight(), 1 );
|
mDirtyLines.resize( gridSize.getHeight(), 1 );
|
||||||
@@ -491,26 +504,33 @@ TerminalDisplay::TerminalDisplay( EE::Window::Window* window, Font* font, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::resetColors() {
|
void TerminalDisplay::resetColors() {
|
||||||
|
mColorScheme = mInitialColorScheme;
|
||||||
for ( Uint32 i = 0; i < eeARRAY_SIZE( colormapped ); i++ )
|
for ( Uint32 i = 0; i < eeARRAY_SIZE( colormapped ); i++ )
|
||||||
resetColor( i, i < mColorScheme.getPaletteSize()
|
resetColor( i, i < mInitialColorScheme.getPaletteSize()
|
||||||
? mColorScheme.getPaletteIndex( i ).toHexString().c_str()
|
? mInitialColorScheme.getPaletteIndex( i ).toHexString().c_str()
|
||||||
: nullptr );
|
: nullptr );
|
||||||
}
|
}
|
||||||
|
|
||||||
int TerminalDisplay::resetColor( const Uint32& index, const char* name ) {
|
int TerminalDisplay::resetColor( const Uint32& index, const char* name ) {
|
||||||
if ( !name ) {
|
if ( !name ) {
|
||||||
if ( index < mColors.size() ) {
|
if ( index < mColors.size() ) {
|
||||||
Color col = 0x000000FF;
|
const Color col = index < mInitialColorScheme.getPaletteSize()
|
||||||
|
? mInitialColorScheme.getPaletteIndex( index )
|
||||||
if ( index < 256 )
|
: colormapped[index];
|
||||||
col = colormapped[index];
|
|
||||||
|
|
||||||
mColors[index] = col;
|
mColors[index] = col;
|
||||||
mColorScheme.setPaletteIndex( index, col );
|
mColorScheme.setPaletteIndex( index, col );
|
||||||
return 0;
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,10 +556,7 @@ int TerminalDisplay::resetColor( const Uint32& index, const char* name ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ( String::iequals( "default", name ) ) {
|
} else if ( String::iequals( "default", name ) ) {
|
||||||
unsigned char r, g, b;
|
return resetColor( index, nullptr );
|
||||||
getColor( index, &r, &g, &b );
|
|
||||||
col = Color( r, g, b, 255 );
|
|
||||||
colorParsed = true;
|
|
||||||
} else if ( Color::isColorString( std::string_view{ name }, true ) ) {
|
} else if ( Color::isColorString( std::string_view{ name }, true ) ) {
|
||||||
col = Color::fromString( name );
|
col = Color::fromString( name );
|
||||||
colorParsed = true;
|
colorParsed = true;
|
||||||
@@ -608,26 +625,40 @@ void TerminalDisplay::setPadding( const Rectf& padding ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::shared_ptr<TerminalEmulator>& TerminalDisplay::getTerminal() const {
|
const std::shared_ptr<TerminalSession>& TerminalDisplay::getSession() const {
|
||||||
return mTerminal;
|
return mSession;
|
||||||
}
|
|
||||||
|
|
||||||
void TerminalDisplay::attach( TerminalEmulator* terminal ) {
|
|
||||||
ITerminalDisplay::attach( terminal );
|
|
||||||
onSizeChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int TerminalDisplay::scrollSize() const {
|
int TerminalDisplay::scrollSize() const {
|
||||||
return mEmulator ? mEmulator->scrollSize() : 0;
|
return mSnapshot ? mSnapshot->historyLength : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int TerminalDisplay::rowCount() const {
|
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 ) {
|
void TerminalDisplay::sendEvent( const Event& event ) {
|
||||||
for ( auto it : mCallbacks )
|
std::vector<Uint32> callbacks;
|
||||||
it.second( event );
|
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 ) {
|
Uint32 TerminalDisplay::pushEventCallback( const EventFunc& func ) {
|
||||||
@@ -650,18 +681,21 @@ const TerminalColorScheme& TerminalDisplay::getColorScheme() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) {
|
void TerminalDisplay::setColorScheme( const TerminalColorScheme& colorScheme ) {
|
||||||
|
mInitialColorScheme = colorScheme;
|
||||||
mColorScheme = colorScheme;
|
mColorScheme = colorScheme;
|
||||||
resetColors();
|
resetColors();
|
||||||
|
if ( mSession )
|
||||||
|
mSession->setColorPalette( makeColorPalette() );
|
||||||
invalidateLines();
|
invalidateLines();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TerminalDisplay::isAppCapturingMouse() const {
|
bool TerminalDisplay::isAppCapturingMouse() const {
|
||||||
return mTerminal &&
|
return mSession &&
|
||||||
( mMode & ( MODE_MOUSEX10 | MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEMANY ) );
|
( mMode & ( MODE_MOUSEX10 | MODE_MOUSEBTN | MODE_MOUSEMOTION | MODE_MOUSEMANY ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TerminalDisplay::isAltScr() const {
|
bool TerminalDisplay::isAltScr() const {
|
||||||
return mEmulator && mEmulator->tisaltscr();
|
return mSnapshot && mSnapshot->altScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Uint32& TerminalDisplay::getClickStep() const {
|
const Uint32& TerminalDisplay::getClickStep() const {
|
||||||
@@ -681,18 +715,13 @@ void TerminalDisplay::setKeepAlive( bool keepAlive ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TerminalDisplay::update( bool isMouseOverMe ) {
|
bool TerminalDisplay::update( bool isMouseOverMe ) {
|
||||||
bool ret = true;
|
consumeSnapshot();
|
||||||
|
drainSessionEvents();
|
||||||
if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) {
|
if ( mFocus && isBlinkingCursor() && mClock.getElapsedTime().asSeconds() > 0.7 ) {
|
||||||
mMode ^= MODE_BLINK;
|
mMode ^= MODE_BLINK;
|
||||||
mClock.restart();
|
mClock.restart();
|
||||||
invalidateCursor();
|
invalidateCursor();
|
||||||
}
|
}
|
||||||
if ( mTerminal ) {
|
|
||||||
int histi = mTerminal->getHistorySize();
|
|
||||||
ret = mTerminal->update();
|
|
||||||
if ( histi != mTerminal->getHistorySize() )
|
|
||||||
sendEvent( { EventType::HISTORY_LENGTH_CHANGE } );
|
|
||||||
}
|
|
||||||
if ( mAlreadyClickedLButton ) {
|
if ( mAlreadyClickedLButton ) {
|
||||||
if ( !( mWindow->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) {
|
if ( !( mWindow->getInput()->getPressTrigger() & EE_BUTTON_LMASK ) ) {
|
||||||
mWindow->getInput()->captureMouse( false );
|
mWindow->getInput()->captureMouse( false );
|
||||||
@@ -702,42 +731,222 @@ bool TerminalDisplay::update( bool isMouseOverMe ) {
|
|||||||
mWindow->getInput()->getPressTrigger() );
|
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 ) {
|
void TerminalDisplay::executeFile( const std::string& cmd ) {
|
||||||
if ( mTerminal ) {
|
if ( mSession ) {
|
||||||
std::string rcmd( cmd + "\r" );
|
std::string rcmd;
|
||||||
#if EE_PLATFORM != EE_PLATFORM_WIN
|
#if EE_PLATFORM != EE_PLATFORM_WIN
|
||||||
char clearLine = 0x15;
|
rcmd.push_back( 0x15 );
|
||||||
mTerminal->ttywrite( &clearLine, 1, 1 );
|
|
||||||
#endif
|
#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 ) {
|
void TerminalDisplay::executeBinary( const std::string& binaryPath, const std::string& args ) {
|
||||||
if ( mTerminal ) {
|
if ( mSession ) {
|
||||||
std::string rcmd( "\"" + binaryPath + "\"" + " " + args + "\r" );
|
std::string rcmd;
|
||||||
#if EE_PLATFORM != EE_PLATFORM_WIN
|
#if EE_PLATFORM != EE_PLATFORM_WIN
|
||||||
char clearLine = 0x15;
|
rcmd.push_back( 0x15 );
|
||||||
mTerminal->ttywrite( &clearLine, 1, 1 );
|
|
||||||
#endif
|
#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 ) {
|
void TerminalDisplay::action( TerminalShortcutAction action ) {
|
||||||
|
if ( !mSession && action != TerminalShortcutAction::FONTSIZE_GROW &&
|
||||||
|
action != TerminalShortcutAction::FONTSIZE_SHRINK )
|
||||||
|
return;
|
||||||
switch ( action ) {
|
switch ( action ) {
|
||||||
case TerminalShortcutAction::PASTE: {
|
case TerminalShortcutAction::PASTE: {
|
||||||
getClipboard();
|
getClipboard();
|
||||||
if ( !mClipboardUtf8.empty() ) {
|
if ( !mClipboardUtf8.empty() ) {
|
||||||
if ( mMode & MODE_BRCKTPASTE ) {
|
if ( mMode & MODE_BRCKTPASTE ) {
|
||||||
mTerminal->write( "\033[200~", 6 );
|
mSession->writeRaw( "\033[200~" );
|
||||||
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
|
mSession->writeRaw( std::move( mClipboardUtf8 ) );
|
||||||
mTerminal->write( "\033[201~", 6 );
|
mSession->writeRaw( "\033[201~" );
|
||||||
} else {
|
} else {
|
||||||
mTerminal->write( mClipboardUtf8.c_str(), mClipboardUtf8.size() );
|
mSession->writeRaw( std::move( mClipboardUtf8 ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -746,61 +955,48 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
|
|||||||
std::string selection =
|
std::string selection =
|
||||||
mWindow->getClipboard()->hasPrimarySelection()
|
mWindow->getClipboard()->hasPrimarySelection()
|
||||||
? mWindow->getClipboard()->getPrimarySelectionText()
|
? mWindow->getClipboard()->getPrimarySelectionText()
|
||||||
: ( mTerminal->hasSelection()
|
: ( hasSelection() ? getSelection()
|
||||||
? mTerminal->getSelection()
|
: mWindow->getClipboard()->getPrimarySelectionText() );
|
||||||
: mWindow->getClipboard()->getPrimarySelectionText() );
|
|
||||||
sanitizeInput( selection );
|
sanitizeInput( selection );
|
||||||
if ( !selection.empty() ) {
|
if ( !selection.empty() ) {
|
||||||
if ( mMode & MODE_BRCKTPASTE ) {
|
if ( mMode & MODE_BRCKTPASTE ) {
|
||||||
mTerminal->write( "\033[200~", 6 );
|
mSession->writeRaw( "\033[200~" );
|
||||||
mTerminal->write( selection.c_str(), selection.size() );
|
mSession->writeRaw( std::move( selection ) );
|
||||||
mTerminal->write( "\033[201~", 6 );
|
mSession->writeRaw( "\033[201~" );
|
||||||
} else {
|
} else {
|
||||||
mTerminal->write( selection.c_str(), selection.size() );
|
mSession->writeRaw( std::move( selection ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::COPY: {
|
case TerminalShortcutAction::COPY: {
|
||||||
auto selection = mTerminal->getSelection();
|
auto selection = getSelection();
|
||||||
if ( !selection.empty() )
|
if ( !selection.empty() )
|
||||||
setClipboard( selection.c_str() );
|
setClipboard( selection.c_str() );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::SCROLLUP_SCREEN: {
|
case TerminalShortcutAction::SCROLLUP_SCREEN: {
|
||||||
TerminalArg arg( (int)-mClickStep );
|
mSession->scrollUp( -(int)mClickStep );
|
||||||
mTerminal->kscrollup( &arg );
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::SCROLLDOWN_SCREEN: {
|
case TerminalShortcutAction::SCROLLDOWN_SCREEN: {
|
||||||
TerminalArg arg( (int)-mClickStep );
|
mSession->scrollDown( -(int)mClickStep );
|
||||||
mTerminal->kscrolldown( &arg );
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::SCROLLUP_ROW: {
|
case TerminalShortcutAction::SCROLLUP_ROW: {
|
||||||
TerminalArg arg( (int)mClickStep );
|
mSession->scrollUp( mClickStep );
|
||||||
mTerminal->kscrollup( &arg );
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::SCROLLDOWN_ROW: {
|
case TerminalShortcutAction::SCROLLDOWN_ROW: {
|
||||||
TerminalArg arg( (int)mClickStep );
|
mSession->scrollDown( mClickStep );
|
||||||
mTerminal->kscrolldown( &arg );
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::SCROLLUP_HISTORY: {
|
case TerminalShortcutAction::SCROLLUP_HISTORY: {
|
||||||
TerminalArg arg( (int)INT_MAX );
|
mSession->scrollUp( INT_MAX );
|
||||||
mTerminal->kscrollup( &arg );
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::SCROLLDOWN_HISTORY: {
|
case TerminalShortcutAction::SCROLLDOWN_HISTORY: {
|
||||||
TerminalArg arg( (int)INT_MAX );
|
mSession->scrollDown( INT_MAX );
|
||||||
mTerminal->kscrolldown( &arg );
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TerminalShortcutAction::FONTSIZE_GROW: {
|
case TerminalShortcutAction::FONTSIZE_GROW: {
|
||||||
@@ -815,17 +1011,7 @@ void TerminalDisplay::action( TerminalShortcutAction action ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TerminalDisplay::hasTerminated() const {
|
bool TerminalDisplay::hasTerminated() const {
|
||||||
return mTerminal->hasExited();
|
return mSnapshot && mSnapshot->processExited;
|
||||||
}
|
|
||||||
|
|
||||||
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 ) } );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::setClipboard( const char* text ) {
|
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() {
|
void TerminalDisplay::draw() {
|
||||||
draw( nullptr != mFrameBuffer ? Vector2f( mPadding.Left, mPadding.Top )
|
draw( nullptr != mFrameBuffer ? Vector2f( mPadding.Left, mPadding.Top )
|
||||||
: mPosition.floor() + 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();
|
mLastDoubleClick.restart();
|
||||||
|
|
||||||
if ( !isAppCapturingMouse() && ( flags & EE_BUTTON_LMASK ) &&
|
if ( !isAppCapturingMouse() && ( flags & EE_BUTTON_LMASK ) &&
|
||||||
( mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_EMPTY ||
|
( getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_IDLE ) ) {
|
||||||
mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_IDLE ) ) {
|
|
||||||
auto gridPos{ positionToGrid( pos ) };
|
auto gridPos{ positionToGrid( pos ) };
|
||||||
mTerminal->selstart( gridPos.x, gridPos.y, SNAP_WORD );
|
mSession->selectionStart( gridPos.x, gridPos.y, SNAP_WORD );
|
||||||
invalidateLines();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,16 +1086,15 @@ void TerminalDisplay::onMouseMove( const Vector2i& pos, const Uint32& flags ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
|
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
|
||||||
( mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_EMPTY ||
|
( mDraggingSel || getSelectionMode() == SEL_EMPTY || getSelectionMode() == SEL_READY ) ) {
|
||||||
mTerminal->getSelectionMode() == TerminalSelectionMode::SEL_READY ) ) {
|
|
||||||
auto gridPos{ positionToGrid( pos ) };
|
auto gridPos{ positionToGrid( pos ) };
|
||||||
mTerminal->selextend(
|
mSession->selectionExtend(
|
||||||
gridPos.x, gridPos.y,
|
gridPos.x, gridPos.y,
|
||||||
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR, 0 );
|
mWindow->getInput()->getModState() & KEYMOD_SHIFT ? SEL_RECTANGULAR : SEL_REGULAR,
|
||||||
invalidateLines();
|
false );
|
||||||
}
|
}
|
||||||
mTerminal->mousereport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
|
mSession->mouseReport( TerminalMouseEventType::MouseMotion, positionToGrid( pos ), flags,
|
||||||
mWindow->getInput()->getModState() );
|
mWindow->getInput()->getModState() );
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
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 ) &&
|
if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) &&
|
||||||
mLastDoubleClick.getElapsedTime() < Milliseconds( 300.f ) ) {
|
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 ) ) {
|
} else if ( !isCapturingMouse && ( flags & EE_BUTTON_LMASK ) ) {
|
||||||
if ( !mDraggingSel ) {
|
if ( !mDraggingSel ) {
|
||||||
mTerminal->selstart( gridPos.x, gridPos.y, 0 );
|
mSession->selectionStart( gridPos.x, gridPos.y, 0 );
|
||||||
mDraggingSel = true;
|
mDraggingSel = true;
|
||||||
invalidateLines();
|
invalidateLines();
|
||||||
mWindow->getInput()->captureMouse( true );
|
mWindow->getInput()->captureMouse( true );
|
||||||
@@ -993,8 +1134,8 @@ void TerminalDisplay::onMouseDown( const Vector2i& pos, const Uint32& flags ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mTerminal->mousereport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
|
mSession->mouseReport( TerminalMouseEventType::MouseButtonDown, positionToGrid( pos ), flags,
|
||||||
mWindow->getInput()->getModState() );
|
mWindow->getInput()->getModState() );
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::onMouseUp( const Vector2i& pos, const Uint32& flags ) {
|
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() ) {
|
if ( ( flags & EE_BUTTON_LMASK ) && mWindow->getClipboard()->hasPrimarySelection() ) {
|
||||||
mWindow->getClipboard()->setPrimarySelectionText( mTerminal->getSelection() );
|
mWindow->getClipboard()->setPrimarySelectionText( getSelection() );
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint32 smod = sanitizeMod( mWindow->getInput()->getModState() );
|
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 )
|
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
|
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
|
||||||
action( k.action );
|
action( k.action );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mTerminal->mousereport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ),
|
mSession->mouseReport( TerminalMouseEventType::MouseButtonRelease, positionToGrid( pos ), flags,
|
||||||
flags, mWindow->getInput()->getModState() );
|
mWindow->getInput()->getModState() );
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline Color termColor( unsigned int terminalColor, const std::vector<Color>& colors ) {
|
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++ ) {
|
for ( Uint32 i = 0; i < mColumns; i++ ) {
|
||||||
mCurGridPos = { i, j };
|
mCurGridPos = { i, j };
|
||||||
auto& glyph = mBuffer[j * mColumns + i];
|
const auto& glyph = mSnapshot->cells[j * mColumns + i];
|
||||||
auto fg = termColor( glyph.fg, mColors );
|
auto fg = termColor( glyph.fg, mColors );
|
||||||
auto bg = termColor( glyph.bg, mColors );
|
auto bg = termColor( glyph.bg, mColors );
|
||||||
|
|
||||||
@@ -1328,7 +1469,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
|||||||
|
|
||||||
for ( Uint32 i = 0; i < mColumns; i++ ) {
|
for ( Uint32 i = 0; i < mColumns; i++ ) {
|
||||||
mCurGridPos = { i, j };
|
mCurGridPos = { i, j };
|
||||||
auto& glyph = mBuffer[j * mColumns + i];
|
const auto& glyph = mSnapshot->cells[j * mColumns + i];
|
||||||
auto fg = termColor( glyph.fg, mColors );
|
auto fg = termColor( glyph.fg, mColors );
|
||||||
auto bg = termColor( glyph.bg, mColors );
|
auto bg = termColor( glyph.bg, mColors );
|
||||||
Color temp{ Color::Transparent };
|
Color temp{ Color::Transparent };
|
||||||
@@ -1439,8 +1580,8 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
|||||||
invalidateCursor();
|
invalidateCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool redrawCursor =
|
bool redrawCursor = mSnapshot && mSnapshot->cursorVisible && !IS_SET( MODE_HIDE ) &&
|
||||||
!mEmulator->isScrolling() && !IS_SET( MODE_HIDE ) && ( !mUseFrameBuffer || mDirtyCursor );
|
( !mUseFrameBuffer || mDirtyCursor );
|
||||||
bool mustRenderUnderline = false;
|
bool mustRenderUnderline = false;
|
||||||
|
|
||||||
if ( redrawCursor ) {
|
if ( redrawCursor ) {
|
||||||
@@ -1448,14 +1589,14 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
|||||||
Color drawcol;
|
Color drawcol;
|
||||||
|
|
||||||
if ( IS_SET( MODE_REVERSE ) ) {
|
if ( IS_SET( MODE_REVERSE ) ) {
|
||||||
if ( mEmulator->isSelected( mCursor.x, mCursor.y ) ) {
|
if ( mSnapshot->cursorSelected ) {
|
||||||
drawcol = mColorScheme.getCursor();
|
drawcol = mColorScheme.getCursor();
|
||||||
} else {
|
} else {
|
||||||
drawcol = mColorScheme.getBackground();
|
drawcol = mColorScheme.getBackground();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
drawcol = mEmulator->isSelected( mCursor.x, mCursor.y ) ? mColorScheme.getBackground()
|
drawcol =
|
||||||
: mColorScheme.getCursor();
|
mSnapshot->cursorSelected ? mColorScheme.getBackground() : mColorScheme.getCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
mPrimitives.setColor( drawcol );
|
mPrimitives.setColor( drawcol );
|
||||||
@@ -1564,7 +1705,7 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::drawBg( bool toFBO ) {
|
void TerminalDisplay::drawBg( bool toFBO ) {
|
||||||
auto defaultBg = termColor( mEmulator->getDefaultBackground(), mColors );
|
auto defaultBg = mColorScheme.getBackground();
|
||||||
Primitives p;
|
Primitives p;
|
||||||
p.setForceDraw( toFBO );
|
p.setForceDraw( toFBO );
|
||||||
p.setColor( defaultBg );
|
p.setColor( defaultBg );
|
||||||
@@ -1577,7 +1718,7 @@ void TerminalDisplay::drawBg( bool toFBO ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::draw( const Vector2f& pos ) {
|
void TerminalDisplay::draw( const Vector2f& pos ) {
|
||||||
if ( !mEmulator || !mTerminal )
|
if ( !mSession || !mSnapshot )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mDrawing = true;
|
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
|
// 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( mouseX >= 0 && mouseX <= termSize.x );
|
||||||
eeASSERT( mouseY >= 0 && mouseY <= mTerminal->getSize().y );
|
eeASSERT( mouseY >= 0 && mouseY <= termSize.y );
|
||||||
|
|
||||||
mouseX = eeclamp( mouseX, 0, termSize.x );
|
mouseX = eeclamp( mouseX, 0, termSize.x );
|
||||||
mouseY = eeclamp( mouseY, 0, termSize.y );
|
mouseY = eeclamp( mouseY, 0, termSize.y );
|
||||||
@@ -1641,18 +1782,10 @@ void TerminalDisplay::onSizeChange() {
|
|||||||
mFont, mFontSize,
|
mFont, mFontSize,
|
||||||
mSize - Vector2f( mPadding.Left + mPadding.Right, mPadding.Top + mPadding.Bottom ) ) );
|
mSize - Vector2f( mPadding.Left + mPadding.Right, mPadding.Top + mPadding.Bottom ) ) );
|
||||||
|
|
||||||
if ( mTerminal ) {
|
if ( mSession && ( !mSnapshot || gridSize.getWidth() != mSnapshot->columns ||
|
||||||
if ( gridSize.getWidth() != mTerminal->getNumColumns() ||
|
gridSize.getHeight() != mSnapshot->rows ) ) {
|
||||||
gridSize.getHeight() != mTerminal->getNumRows() ) {
|
mSession->resize( gridSize.getWidth(), gridSize.getHeight() );
|
||||||
mTerminal->resize( gridSize.getWidth(), gridSize.getHeight() );
|
mDirtyLines.resize( gridSize.getHeight(), 1 );
|
||||||
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 ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() ||
|
if ( mFrameBuffer && ( mFrameBuffer->getWidth() < mSize.getWidth() ||
|
||||||
@@ -1670,7 +1803,7 @@ void TerminalDisplay::onSizeChange() {
|
|||||||
void TerminalDisplay::onProcessExit( int exitCode ) {
|
void TerminalDisplay::onProcessExit( int exitCode ) {
|
||||||
sendEvent( { EventType::PROCESS_EXIT, String::toString( exitCode ) } );
|
sendEvent( { EventType::PROCESS_EXIT, String::toString( exitCode ) } );
|
||||||
|
|
||||||
if ( !mTerminal || mProgram.empty() || exitCode != 0 || !mKeepAlive )
|
if ( !mSession || mProgram.empty() || exitCode != 0 || !mKeepAlive )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto processFactory = eeNew( ProcessFactory, () );
|
auto processFactory = eeNew( ProcessFactory, () );
|
||||||
@@ -1685,35 +1818,34 @@ void TerminalDisplay::onProcessExit( int exitCode ) {
|
|||||||
if ( !pseudoTerminal ) {
|
if ( !pseudoTerminal ) {
|
||||||
eeSAFE_DELETE( processFactory );
|
eeSAFE_DELETE( processFactory );
|
||||||
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to create pseudo terminal\n" );
|
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to create pseudo terminal\n" );
|
||||||
|
sendEvent( { EventType::RESTART_FAILURE, "Failed to create pseudo terminal" } );
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !process ) {
|
if ( !process ) {
|
||||||
eeSAFE_DELETE( processFactory );
|
eeSAFE_DELETE( processFactory );
|
||||||
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to spawn process\n" );
|
fprintf( stderr, "TerminalDisplay::onProcessExit: Failed to spawn process\n" );
|
||||||
|
sendEvent( { EventType::RESTART_FAILURE, "Failed to spawn process" } );
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mTerminal->clearHistory();
|
mSession->restart( std::move( pseudoTerminal ), std::move( process ) );
|
||||||
mTerminal->setPtyAndProcess( std::move( pseudoTerminal ), std::move( process ) );
|
|
||||||
|
|
||||||
eeSAFE_DELETE( processFactory );
|
eeSAFE_DELETE( processFactory );
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::onScrollPositionChange() {
|
|
||||||
sendEvent( { EventType::SCROLL_HISTORY } );
|
|
||||||
}
|
|
||||||
|
|
||||||
void TerminalDisplay::onTextInput( const Uint32& chr ) {
|
void TerminalDisplay::onTextInput( const Uint32& chr ) {
|
||||||
if ( !mTerminal )
|
if ( !mSession )
|
||||||
return;
|
return;
|
||||||
String input;
|
String input;
|
||||||
input.push_back( chr );
|
input.push_back( chr );
|
||||||
std::string utf8Input( input.toUtf8() );
|
std::string utf8Input( input.toUtf8() );
|
||||||
mTerminal->ttywrite( utf8Input.c_str(), utf8Input.size(), 1 );
|
mSession->write( std::move( utf8Input ) );
|
||||||
mDirty = true;
|
mDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalDisplay::onTextEditing( const String&, const Int32&, const Int32& ) {
|
void TerminalDisplay::onTextEditing( const String&, const Int32&, const Int32& ) {
|
||||||
if ( !mTerminal )
|
if ( !mSession )
|
||||||
return;
|
return;
|
||||||
invalidateCursor();
|
invalidateCursor();
|
||||||
updateIMELocation();
|
updateIMELocation();
|
||||||
@@ -1734,7 +1866,7 @@ bool TerminalDisplay::isRegisteredShortcut( const Keycode& keyCode, const Uint32
|
|||||||
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
|
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
|
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
|
||||||
return true;
|
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 )
|
if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 )
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if ( !k.altscrn || ( k.altscrn == ( mEmulator->tisaltscr() ? 1 : -1 ) ) ) {
|
if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) {
|
||||||
action( k.action );
|
action( k.action );
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1802,7 +1934,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if ( k.string.size() > 0 ) {
|
if ( k.string.size() > 0 ) {
|
||||||
mTerminal->ttywrite( k.string.c_str(), k.string.size(), 1 );
|
mSession->write( k.string );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1824,7 +1956,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/,
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if ( k.string.size() > 0 ) {
|
if ( k.string.size() > 0 ) {
|
||||||
mTerminal->ttywrite( k.string.c_str(), k.string.size(), 1 );
|
mSession->write( k.string );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1936,17 +2068,14 @@ void TerminalDisplay::setFocus( bool focus ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mFocus = focus;
|
mFocus = focus;
|
||||||
bool modeFocus = mMode & MODE_FOCUSED;
|
if ( mFocus ) {
|
||||||
if ( mFocus != modeFocus ) {
|
mMode |= MODE_FOCUSED;
|
||||||
if ( mFocus ) {
|
mWindow->startTextInput();
|
||||||
mMode |= MODE_FOCUSED | MODE_FOCUS;
|
|
||||||
mWindow->startTextInput();
|
|
||||||
} else {
|
|
||||||
mMode ^= MODE_FOCUS | MODE_FOCUSED;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mMode ^= MODE_FOCUS;
|
mMode &= ~MODE_FOCUSED;
|
||||||
}
|
}
|
||||||
|
if ( mSession )
|
||||||
|
mSession->setFocus( focus );
|
||||||
invalidateCursor();
|
invalidateCursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
#include <eepp/system/cpu.hpp>
|
#include <eepp/system/cpu.hpp>
|
||||||
#include <eepp/system/filesystem.hpp>
|
#include <eepp/system/filesystem.hpp>
|
||||||
#include <eepp/system/sys.hpp>
|
#include <eepp/system/sys.hpp>
|
||||||
|
#include <eepp/version.hpp>
|
||||||
|
|
||||||
using namespace EE::Network;
|
using namespace EE::Network;
|
||||||
using namespace EE::System;
|
using namespace EE::System;
|
||||||
@@ -76,6 +77,52 @@ extern "C" {
|
|||||||
|
|
||||||
namespace eterm { namespace Terminal {
|
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( EE_ARCH_X86_64 )
|
||||||
#if defined( __GNUC__ ) || defined( __clang__ )
|
#if defined( __GNUC__ ) || defined( __clang__ )
|
||||||
__attribute__( ( target( "avx2" ) ) )
|
__attribute__( ( target( "avx2" ) ) )
|
||||||
@@ -844,6 +891,21 @@ void TerminalEmulator::setAllowMemoryTrimnming( bool allowMemoryTrimnming ) {
|
|||||||
mAllowMemoryTrimnming = 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 {
|
Vector2i TerminalEmulator::getSize() const {
|
||||||
return { mTerm.col, mTerm.row };
|
return { mTerm.col, mTerm.row };
|
||||||
}
|
}
|
||||||
@@ -958,6 +1020,8 @@ void TerminalEmulator::tcursor( int mode ) {
|
|||||||
void TerminalEmulator::treset( void ) {
|
void TerminalEmulator::treset( void ) {
|
||||||
uint i;
|
uint i;
|
||||||
|
|
||||||
|
mColorSchemeNotifications = false;
|
||||||
|
mTerm.is_syncing = false;
|
||||||
mTerm.c = TerminalCursor{};
|
mTerm.c = TerminalCursor{};
|
||||||
mTerm.c.attr = TerminalGlyph{};
|
mTerm.c.attr = TerminalGlyph{};
|
||||||
mTerm.c.attr.u = ' ';
|
mTerm.c.attr.u = ' ';
|
||||||
@@ -986,9 +1050,13 @@ void TerminalEmulator::treset( void ) {
|
|||||||
|
|
||||||
xsetmode( 0, MODE_MOUSE | MODE_MOUSESGR | MODE_APPKEYPAD | MODE_APPCURSOR | MODE_FOCUS |
|
xsetmode( 0, MODE_MOUSE | MODE_MOUSESGR | MODE_APPKEYPAD | MODE_APPCURSOR | MODE_FOCUS |
|
||||||
MODE_BRCKTPASTE | MODE_MOUSEX10 | MODE_MOUSEMANY );
|
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();
|
auto dpy = mDpy.lock();
|
||||||
if ( dpy )
|
if ( dpy ) {
|
||||||
dpy->setMode( MODE_VISIBLE, 1 );
|
dpy->setMode( MODE_VISIBLE, 1 );
|
||||||
|
dpy->setCursorMode( mDefaultCursorMode );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalEmulator::tnew( int col, int row, size_t historySize ) {
|
void TerminalEmulator::tnew( int col, int row, size_t historySize ) {
|
||||||
@@ -1415,27 +1483,25 @@ void TerminalEmulator::tnewline( int first_col ) {
|
|||||||
void TerminalEmulator::csiparse( void ) {
|
void TerminalEmulator::csiparse( void ) {
|
||||||
char *p = mCsiescseq.buf, *np;
|
char *p = mCsiescseq.buf, *np;
|
||||||
long int v;
|
long int v;
|
||||||
int sep = ';'; /* colon or semi-colon, but not both */
|
|
||||||
|
|
||||||
mCsiescseq.narg = 0;
|
mCsiescseq.narg = 0;
|
||||||
if ( *p == '?' ) {
|
if ( *p == '<' || *p == '=' || *p == '>' || *p == '?' ) {
|
||||||
mCsiescseq.priv = 1;
|
mCsiescseq.priv = *p;
|
||||||
p++;
|
p++;
|
||||||
}
|
}
|
||||||
|
|
||||||
mCsiescseq.buf[mCsiescseq.len] = '\0';
|
mCsiescseq.buf[mCsiescseq.len] = '\0';
|
||||||
while ( p < mCsiescseq.buf + mCsiescseq.len ) {
|
while ( p < mCsiescseq.buf + mCsiescseq.len && mCsiescseq.narg < ESC_ARG_SIZ ) {
|
||||||
np = NULL;
|
np = NULL;
|
||||||
v = strtol( p, &np, 10 );
|
v = strtol( p, &np, 10 );
|
||||||
if ( np == p )
|
if ( np == p )
|
||||||
v = 0;
|
v = 0;
|
||||||
if ( v == LONG_MAX || v == LONG_MIN )
|
if ( v == LONG_MAX || v == LONG_MIN )
|
||||||
v = -1;
|
v = -1;
|
||||||
mCsiescseq.arg[mCsiescseq.narg++] = v;
|
mCsiescseq.arg[mCsiescseq.narg] = v;
|
||||||
p = np;
|
p = np;
|
||||||
if ( sep == ';' && *p == ':' )
|
mCsiescseq.sep[mCsiescseq.narg++] = *p == ';' || *p == ':' ? *p : '\0';
|
||||||
sep = ':'; /* allow override to colon once */
|
if ( *p != ';' && *p != ':' )
|
||||||
if ( *p != sep || mCsiescseq.narg == ESC_ARG_SIZ )
|
|
||||||
break;
|
break;
|
||||||
p++;
|
p++;
|
||||||
}
|
}
|
||||||
@@ -1592,33 +1658,63 @@ void TerminalEmulator::tdeleteline( int n ) {
|
|||||||
tscrollup( mTerm.c.y, n, 0 );
|
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;
|
int32_t idx = -1;
|
||||||
uint r, g, b;
|
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 */
|
case 2: /* direct color in RGB space */
|
||||||
if ( *npar + 4 >= l ) {
|
if ( subparameters ) {
|
||||||
fprintf( stderr, "erresc(38): Incorrect number of parameters (%d)\n", *npar );
|
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;
|
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 ) )
|
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
|
else
|
||||||
idx = TRUECOLOR( r, g, b );
|
idx = TRUECOLOR( r, g, b );
|
||||||
break;
|
break;
|
||||||
case 5: /* indexed color */
|
case 5: /* indexed color */
|
||||||
if ( *npar + 2 >= l ) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
*npar += 2;
|
*npar += 2;
|
||||||
if ( !BETWEEN( attr[*npar], 0, 255 ) )
|
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
|
else
|
||||||
idx = attr[*npar];
|
idx = attr[*npar];
|
||||||
break;
|
break;
|
||||||
@@ -1627,14 +1723,18 @@ int32_t TerminalEmulator::tdefcolor( int* attr, int* npar, int l ) {
|
|||||||
case 3: /* direct color in CMY space */
|
case 3: /* direct color in CMY space */
|
||||||
case 4: /* direct color in CMYK space */
|
case 4: /* direct color in CMYK space */
|
||||||
default:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return idx;
|
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)
|
// Check if this is a private sequence (should be ignored for SGR)
|
||||||
// Private sequences start with '?' and should not affect text attributes
|
// Private sequences start with '?' and should not affect text attributes
|
||||||
if ( mCsiescseq.priv ) {
|
if ( mCsiescseq.priv ) {
|
||||||
@@ -1702,14 +1802,14 @@ void TerminalEmulator::tsetattr( int* attr, int l ) {
|
|||||||
mTerm.c.attr.mode &= ~ATTR_STRUCK;
|
mTerm.c.attr.mode &= ~ATTR_STRUCK;
|
||||||
break;
|
break;
|
||||||
case 38:
|
case 38:
|
||||||
if ( ( idx = tdefcolor( attr, &i, l ) ) >= 0 )
|
if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 )
|
||||||
mTerm.c.attr.fg = idx;
|
mTerm.c.attr.fg = idx;
|
||||||
break;
|
break;
|
||||||
case 39: /* set foreground color to default */
|
case 39: /* set foreground color to default */
|
||||||
mTerm.c.attr.fg = mDefaultFg;
|
mTerm.c.attr.fg = mDefaultFg;
|
||||||
break;
|
break;
|
||||||
case 48:
|
case 48:
|
||||||
if ( ( idx = tdefcolor( attr, &i, l ) ) >= 0 )
|
if ( ( idx = tdefcolor( attr, separators, &i, l ) ) >= 0 )
|
||||||
mTerm.c.attr.bg = idx;
|
mTerm.c.attr.bg = idx;
|
||||||
break;
|
break;
|
||||||
case 49: /* set background color to default */
|
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
|
/* This starts a sequence to change the color of
|
||||||
* "underline" pixels. We don't support that and
|
* "underline" pixels. We don't support that and
|
||||||
* instead eat up a following "5;n" or "2;r;g;b". */
|
* 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;
|
break;
|
||||||
default:
|
default:
|
||||||
if ( BETWEEN( attr[i], 30, 37 ) ) {
|
if ( BETWEEN( attr[i], 30, 37 ) ) {
|
||||||
@@ -1731,8 +1833,9 @@ void TerminalEmulator::tsetattr( int* attr, int l ) {
|
|||||||
} else if ( BETWEEN( attr[i], 100, 107 ) ) {
|
} else if ( BETWEEN( attr[i], 100, 107 ) ) {
|
||||||
mTerm.c.attr.bg = attr[i] - 100 + 8;
|
mTerm.c.attr.bg = attr[i] - 100 + 8;
|
||||||
} else {
|
} else {
|
||||||
fprintf( stderr, "erresc(default): gfx attr %d unknown\n", attr[i] );
|
terminalDiagnostic( "erresc(default): gfx attr %d unknown\n", attr[i] );
|
||||||
csidump();
|
if ( terminalDiagnosticsEnabled() )
|
||||||
|
csidump();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1813,6 +1916,9 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) {
|
|||||||
case 1006: /* 1006: extended reporting mode */
|
case 1006: /* 1006: extended reporting mode */
|
||||||
xsetmode( set, MODE_MOUSESGR );
|
xsetmode( set, MODE_MOUSESGR );
|
||||||
break;
|
break;
|
||||||
|
case 1007: /* wheel sends cursor keys on the alternate screen */
|
||||||
|
xsetmode( set, MODE_ALTSCRROLL );
|
||||||
|
break;
|
||||||
case 1034:
|
case 1034:
|
||||||
xsetmode( set, MODE_8BIT );
|
xsetmode( set, MODE_8BIT );
|
||||||
break;
|
break;
|
||||||
@@ -1852,19 +1958,23 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) {
|
|||||||
case 1039: /* ESC to Meta (not implemented) */
|
case 1039: /* ESC to Meta (not implemented) */
|
||||||
break;
|
break;
|
||||||
case 2026: {
|
case 2026: {
|
||||||
// IGNORE DECSET/DECRST 2026 for sync updates?
|
if ( set ) {
|
||||||
// (https://codeberg.org/dnkl/foot/pulls/461/files)
|
mTerm.is_syncing = true;
|
||||||
// mTerm.is_syncing = ( set == 1 );
|
mSynchronizedUpdateClock.restart();
|
||||||
/* if ( !mTerm.is_syncing ) {
|
} else if ( mTerm.is_syncing ) {
|
||||||
// When syncing ends, we must perform the deferred draw
|
mTerm.is_syncing = false;
|
||||||
|
// Publish the complete frame immediately instead of waiting for the next
|
||||||
|
// presentation deadline.
|
||||||
draw();
|
draw();
|
||||||
} */
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 2031: /* Light/dark color-scheme change notifications. */
|
||||||
|
mColorSchemeNotifications = set;
|
||||||
|
mColorScheme = colorScheme();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
#ifdef EE_DEBUG
|
terminalDiagnostic( "erresc: unknown private set/reset mode %d\n", *args );
|
||||||
fprintf( stderr, "erresc: unknown private set/reset mode %d\n", *args );
|
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1884,9 +1994,7 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) {
|
|||||||
MODBIT( mTerm.mode, set, MODE_CRLF );
|
MODBIT( mTerm.mode, set, MODE_CRLF );
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
#ifdef EE_DEBUG
|
terminalDiagnostic( "erresc: unknown set/reset mode %d\n", *args );
|
||||||
fprintf( stderr, "erresc: unknown set/reset mode %d\n", *args );
|
|
||||||
#endif
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1894,7 +2002,13 @@ void TerminalEmulator::tsetmode( int priv, int set, int* args, int narg ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalEmulator::handleDeviceAttributes() {
|
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];
|
char buf[64];
|
||||||
int len;
|
int len;
|
||||||
// Private Device Attributes - respond with terminal capabilities
|
// Private Device Attributes - respond with terminal capabilities
|
||||||
@@ -1917,7 +2031,7 @@ void TerminalEmulator::handleDeviceAttributes() {
|
|||||||
// Unknown private DA query
|
// Unknown private DA query
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else if ( !mCsiescseq.priv ) {
|
||||||
// Standard DA - respond with VT100 identification
|
// Standard DA - respond with VT100 identification
|
||||||
ttywrite( vtiden, strlen( vtiden ), 0 );
|
ttywrite( vtiden, strlen( vtiden ), 0 );
|
||||||
}
|
}
|
||||||
@@ -1932,10 +2046,8 @@ void TerminalEmulator::csihandle( void ) {
|
|||||||
switch ( mCsiescseq.mode[0] ) {
|
switch ( mCsiescseq.mode[0] ) {
|
||||||
default:
|
default:
|
||||||
unknown:
|
unknown:
|
||||||
#ifdef EE_DEBUG
|
terminalDiagnostic( "erresc: unknown csi " );
|
||||||
fprintf( stderr, "erresc: unknown csi " );
|
|
||||||
csidump();
|
csidump();
|
||||||
#endif
|
|
||||||
/* die(""); */
|
/* die(""); */
|
||||||
break;
|
break;
|
||||||
case '@': /* ICH -- Insert <n> blank char */
|
case '@': /* ICH -- Insert <n> blank char */
|
||||||
@@ -2072,7 +2184,9 @@ void TerminalEmulator::csihandle( void ) {
|
|||||||
tinsertblankline( mCsiescseq.arg[0] );
|
tinsertblankline( mCsiescseq.arg[0] );
|
||||||
break;
|
break;
|
||||||
case 'l': /* RM -- Reset Mode */
|
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;
|
break;
|
||||||
case 'M': /* DL -- Delete <n> lines */
|
case 'M': /* DL -- Delete <n> lines */
|
||||||
DEFAULT( mCsiescseq.arg[0], 1 );
|
DEFAULT( mCsiescseq.arg[0], 1 );
|
||||||
@@ -2095,41 +2209,19 @@ void TerminalEmulator::csihandle( void ) {
|
|||||||
tmoveato( mTerm.c.x, mCsiescseq.arg[0] - 1 );
|
tmoveato( mTerm.c.x, mCsiescseq.arg[0] - 1 );
|
||||||
break;
|
break;
|
||||||
case 'h': /* SM -- Set terminal mode */
|
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;
|
break;
|
||||||
case 'm': /* SGR -- Terminal attribute (color) */
|
case 'm': /* SGR -- Terminal attribute (color) */
|
||||||
tsetattr( mCsiescseq.arg, mCsiescseq.narg );
|
if ( mCsiescseq.priv == '>' )
|
||||||
break;
|
break; // Extended underline styles are not rendered yet.
|
||||||
case '>': /* Private sequences */
|
tsetattr( mCsiescseq.arg, mCsiescseq.narg, mCsiescseq.sep );
|
||||||
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;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'n': /* DSR – Device Status Report (cursor position) */
|
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 );
|
len = snprintf( buf, sizeof( buf ), "\033[%i;%iR", mTerm.c.y + 1, mTerm.c.x + 1 );
|
||||||
ttywrite( buf, len, 0 );
|
ttywrite( buf, len, 0 );
|
||||||
}
|
}
|
||||||
@@ -2148,7 +2240,12 @@ void TerminalEmulator::csihandle( void ) {
|
|||||||
tcursor( CURSOR_SAVE );
|
tcursor( CURSOR_SAVE );
|
||||||
break;
|
break;
|
||||||
case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */
|
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;
|
goto unknown;
|
||||||
} else {
|
} else {
|
||||||
tcursor( CURSOR_LOAD );
|
tcursor( CURSOR_LOAD );
|
||||||
@@ -2157,20 +2254,27 @@ void TerminalEmulator::csihandle( void ) {
|
|||||||
case ' ':
|
case ' ':
|
||||||
switch ( mCsiescseq.mode[1] ) {
|
switch ( mCsiescseq.mode[1] ) {
|
||||||
case 'q': /* DECSCUSR -- Set Cursor Style */
|
case 'q': /* DECSCUSR -- Set Cursor Style */
|
||||||
if ( mCsiescseq.arg[0] < 0 ||
|
if ( mCsiescseq.priv || mCsiescseq.arg[0] < 0 ||
|
||||||
mCsiescseq.arg[0] < TerminalCursorMode::MAX_CURSOR )
|
mCsiescseq.arg[0] >= TerminalCursorMode::StExtension )
|
||||||
goto unknown;
|
goto unknown;
|
||||||
dpy = mDpy.lock();
|
dpy = mDpy.lock();
|
||||||
if ( dpy )
|
if ( dpy ) {
|
||||||
dpy->setCursorMode( (TerminalCursorMode)mCsiescseq.arg[0] );
|
const auto mode = static_cast<TerminalCursorMode>( mCsiescseq.arg[0] );
|
||||||
|
dpy->setCursorMode( mode == BlinkingBlock
|
||||||
|
? blinkingCursorVariant( mDefaultCursorMode )
|
||||||
|
: mode );
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
goto unknown;
|
goto unknown;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case '=': /* Progressive enhancement sequences */
|
case 'q': /* XTVERSION -- Report terminal name and version */
|
||||||
/* Keyboard protocol ESC[=Nu */
|
if ( mCsiescseq.priv != '>' || mCsiescseq.arg[0] != 0 )
|
||||||
/* Do nothing for the moment */
|
goto unknown;
|
||||||
|
len = snprintf( buf, sizeof( buf ), "\033P>|eterm %s\033\\",
|
||||||
|
EE::Version::getVersionName( false ).c_str() );
|
||||||
|
ttywrite( buf, len, 0 );
|
||||||
break;
|
break;
|
||||||
case 't': /* Window manipulation */
|
case 't': /* Window manipulation */
|
||||||
switch ( mCsiescseq.arg[0] ) {
|
switch ( mCsiescseq.arg[0] ) {
|
||||||
@@ -2195,16 +2299,12 @@ void TerminalEmulator::csihandle( void ) {
|
|||||||
goto unknown;
|
goto unknown;
|
||||||
}
|
}
|
||||||
break;
|
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 ) {
|
void TerminalEmulator::csidump( void ) {
|
||||||
|
if ( !terminalDiagnosticsEnabled() )
|
||||||
|
return;
|
||||||
size_t i;
|
size_t i;
|
||||||
uint c;
|
uint c;
|
||||||
|
|
||||||
@@ -2279,7 +2379,7 @@ void TerminalEmulator::strhandle( void ) {
|
|||||||
setClipboard( dec );
|
setClipboard( dec );
|
||||||
xfree( dec );
|
xfree( dec );
|
||||||
} else {
|
} else {
|
||||||
fprintf( stderr, "erresc: invalid base64\n" );
|
terminalDiagnostic( "erresc: invalid base64\n" );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -2295,31 +2395,57 @@ void TerminalEmulator::strhandle( void ) {
|
|||||||
if ( !strcmp( p, "?" ) ) {
|
if ( !strcmp( p, "?" ) ) {
|
||||||
osc_color_response( par, osc_table[j].idx, 0 );
|
osc_color_response( par, osc_table[j].idx, 0 );
|
||||||
} else if ( xsetcolorname( osc_table[j].idx, p ) ) {
|
} 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 {
|
} else {
|
||||||
tfulldirt();
|
tfulldirt();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case 4: /* color set */
|
case 4: { /* set or query palette colors */
|
||||||
if ( narg < 3 )
|
bool changed = false;
|
||||||
break;
|
for ( int arg = 1; arg + 1 < narg; arg += 2 ) {
|
||||||
p = mStrescseq.args[2];
|
p = mStrescseq.args[arg + 1];
|
||||||
/* FALLTHROUGH */
|
if ( !String::fromString( j, std::string_view{ mStrescseq.args[arg] } ) ||
|
||||||
case 104: /* color reset, here p = NULL */
|
j < 0 ) {
|
||||||
j = ( narg > 1 ) ? atoi( mStrescseq.args[1] ) : -1;
|
terminalDiagnostic( "erresc: invalid OSC 4 color index: %s\n",
|
||||||
if ( resetColor( j, p ) ) {
|
mStrescseq.args[arg] );
|
||||||
if ( par == 104 && narg <= 1 )
|
continue;
|
||||||
return; /* color reset without parameter */
|
}
|
||||||
fprintf( stderr, "erresc: invalid color j=%d, p=%s\n", j,
|
if ( !strcmp( p, "?" ) ) {
|
||||||
p ? p : "(null)" );
|
osc_color_response( j, j, 1 );
|
||||||
} else {
|
} else if ( resetColor( j, p ) ) {
|
||||||
/*
|
terminalDiagnostic( "erresc: invalid color j=%d, p=%s\n", j, p );
|
||||||
* TODO if defaultbg color is changed, borders
|
} else {
|
||||||
* are dirty
|
changed = true;
|
||||||
*/
|
}
|
||||||
redraw();
|
|
||||||
}
|
}
|
||||||
|
if ( changed )
|
||||||
|
redraw();
|
||||||
return;
|
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 110: /* reset dynamic VT100 text foreground color */
|
||||||
case 111: /* reset dynamic VT100 text background color */
|
case 111: /* reset dynamic VT100 text background color */
|
||||||
case 112: /* reset dynamic text cursor 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 ) )
|
if ( ( j = par - 110 ) < 0 || j >= (int)LEN( osc_table ) )
|
||||||
break; /* shouldn't be possible */
|
break; /* shouldn't be possible */
|
||||||
if ( resetColor( osc_table[j].idx, NULL ) ) {
|
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 {
|
} else {
|
||||||
tfulldirt();
|
tfulldirt();
|
||||||
}
|
}
|
||||||
@@ -2338,6 +2464,13 @@ void TerminalEmulator::strhandle( void ) {
|
|||||||
mCurrentWorkingDirectory = URI( mStrescseq.args[1] ).getPath();
|
mCurrentWorkingDirectory = URI( mStrescseq.args[1] ).getPath();
|
||||||
return;
|
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: {
|
case 133: {
|
||||||
if ( narg > 1 ) {
|
if ( narg > 1 ) {
|
||||||
j = ( narg > 1 ) ? mStrescseq.args[1][0] : -1;
|
j = ( narg > 1 ) ? mStrescseq.args[1][0] : -1;
|
||||||
@@ -2384,10 +2517,8 @@ void TerminalEmulator::strhandle( void ) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef EE_DEBUG
|
terminalDiagnostic( "erresc: unknown str " );
|
||||||
logError( "erresc: unknown str " );
|
|
||||||
strdump();
|
strdump();
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TerminalEmulator::strparse( void ) {
|
void TerminalEmulator::strparse( void ) {
|
||||||
@@ -2411,6 +2542,8 @@ void TerminalEmulator::strparse( void ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalEmulator::strdump( void ) {
|
void TerminalEmulator::strdump( void ) {
|
||||||
|
if ( !terminalDiagnosticsEnabled() )
|
||||||
|
return;
|
||||||
size_t i;
|
size_t i;
|
||||||
uint c;
|
uint c;
|
||||||
|
|
||||||
@@ -2527,7 +2660,7 @@ void TerminalEmulator::tdeftran( char ascii ) {
|
|||||||
char* p;
|
char* p;
|
||||||
|
|
||||||
if ( ( p = strchr( cs, ascii ) ) == NULL ) {
|
if ( ( p = strchr( cs, ascii ) ) == NULL ) {
|
||||||
fprintf( stderr, "esc unhandled charset: ESC ( %c\n", ascii );
|
terminalDiagnostic( "esc unhandled charset: ESC ( %c\n", ascii );
|
||||||
} else {
|
} else {
|
||||||
mTerm.trantbl[mTerm.icharset] = vcs[p - cs];
|
mTerm.trantbl[mTerm.icharset] = vcs[p - cs];
|
||||||
}
|
}
|
||||||
@@ -2739,8 +2872,8 @@ int TerminalEmulator::eschandle( uchar ascii ) {
|
|||||||
strhandle();
|
strhandle();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
fprintf( stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", (uchar)ascii,
|
terminalDiagnostic( "erresc: unknown sequence ESC 0x%02X '%c'\n", (uchar)ascii,
|
||||||
isprint( ascii ) ? ascii : '.' );
|
isprint( ascii ) ? ascii : '.' );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
@@ -2992,7 +3125,7 @@ void TerminalEmulator::tresize( int col, int row ) {
|
|||||||
bool is_alt = IS_SET( MODE_ALTSCREEN );
|
bool is_alt = IS_SET( MODE_ALTSCREEN );
|
||||||
|
|
||||||
if ( col < 1 || row < 1 ) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3201,9 +3334,10 @@ void TerminalEmulator::drawregion( ITerminalDisplay& dpy, int x1, int y1, int x2
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TerminalEmulator::draw() {
|
void TerminalEmulator::draw() {
|
||||||
// If a synchronized update is in progress, skip the physical render
|
// DEC private mode 2026 makes the bytes between DECSET and DECRST one presentation unit.
|
||||||
// if ( mTerm.is_syncing )
|
// Parsing continues normally, but no partially cleared/rebuilt frame may reach the UI.
|
||||||
// return;
|
if ( mTerm.is_syncing )
|
||||||
|
return;
|
||||||
|
|
||||||
int cx = mTerm.c.x /*, ocx = term.ocx, ocy = term.ocy*/;
|
int cx = mTerm.c.x /*, ocx = term.ocx, ocy = term.ocy*/;
|
||||||
|
|
||||||
@@ -3232,7 +3366,6 @@ void TerminalEmulator::draw() {
|
|||||||
mTerm.ocy = mTerm.c.y;
|
mTerm.ocy = mTerm.c.y;
|
||||||
|
|
||||||
dpy->drawEnd();
|
dpy->drawEnd();
|
||||||
mDeferredPresentationBatches = 0;
|
|
||||||
mPresentationClock.restart();
|
mPresentationClock.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3245,6 +3378,11 @@ void TerminalEmulator::redraw() {
|
|||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TerminalEmulator::reset() {
|
||||||
|
treset();
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
int TerminalEmulator::xsetcolorname( int x, const char* name ) {
|
int TerminalEmulator::xsetcolorname( int x, const char* name ) {
|
||||||
return resetColor( x, 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;
|
unsigned char r, g, b;
|
||||||
|
|
||||||
if ( xgetcolor( is_osc4 ? num : index, &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",
|
terminalDiagnostic( "erresc: failed to fetch %s color %d\n", is_osc4 ? "osc4" : "osc",
|
||||||
is_osc4 ? num : index );
|
is_osc4 ? num : index );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
n = snprintf( buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007",
|
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 );
|
is_osc4 ? "4;" : "", num, r, r, g, g, b, b );
|
||||||
if ( n < 0 || n >= (int)sizeof( buf ) ) {
|
if ( n < 0 || n >= (int)sizeof( buf ) ) {
|
||||||
fprintf( stderr, "error: %s while printing %s response\n",
|
terminalDiagnostic( "error: %s while printing %s response\n",
|
||||||
n < 0 ? "snprintf failed" : "truncation occurred", is_osc4 ? "osc4" : "osc" );
|
n < 0 ? "snprintf failed" : "truncation occurred",
|
||||||
|
is_osc4 ? "osc4" : "osc" );
|
||||||
} else {
|
} else {
|
||||||
ttywrite( buf, n, 1 );
|
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,
|
void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Vector2i& pos,
|
||||||
const Uint32& flags, const Uint32& mod ) {
|
const Uint32& flags, const Uint32& mod ) {
|
||||||
if ( !xgetmode( (TerminalWinMode)MODE_MOUSE ) && !xgetmode( MODE_MOUSESGR ) &&
|
if ( !xgetmode( (TerminalWinMode)MODE_MOUSE ) && !xgetmode( MODE_MOUSESGR ) &&
|
||||||
( TerminalMouseEventType::MouseButtonDown == type ||
|
( TerminalMouseEventType::MouseButtonDown == type ||
|
||||||
TerminalMouseEventType::MouseButtonRelease == type ) ) {
|
TerminalMouseEventType::MouseButtonRelease == type ) ) {
|
||||||
/* If mouse mode is not enabled, we send arrow keys for scroll events */
|
/* 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() ) {
|
( flags & ( EE_BUTTON_WUMASK | EE_BUTTON_WDMASK ) ) && tisaltscr() ) {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
int len = 0;
|
int len = 0;
|
||||||
@@ -3316,8 +3473,6 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
|
|||||||
|
|
||||||
int len, btn, code;
|
int len, btn, code;
|
||||||
char buf[40];
|
char buf[40];
|
||||||
static int ox, oy;
|
|
||||||
|
|
||||||
for ( btn = 1; btn <= 31 && !( flags & ( 1 << ( btn - 1 ) ) ); btn++ )
|
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 ( type == TerminalMouseEventType::MouseMotion ) {
|
||||||
if ( pos.x == ox && pos.y == oy )
|
if ( pos == mLastMousePosition )
|
||||||
return;
|
return;
|
||||||
if ( !xgetmode( MODE_MOUSEMOTION ) && !xgetmode( MODE_MOUSEMANY ) )
|
if ( !xgetmode( MODE_MOUSEMOTION ) && !xgetmode( MODE_MOUSEMANY ) )
|
||||||
return;
|
return;
|
||||||
@@ -3371,8 +3526,7 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, const Ve
|
|||||||
code = 0;
|
code = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ox = pos.x;
|
mLastMousePosition = pos;
|
||||||
oy = pos.y;
|
|
||||||
|
|
||||||
/* Encode btn into code. If no button is pressed for a motion event in
|
/* Encode btn into code. If no button is pressed for a motion event in
|
||||||
* MODE_MOUSEMANY, then encode it as a release. */
|
* MODE_MOUSEMANY, then encode it as a release. */
|
||||||
@@ -3563,7 +3717,6 @@ void TerminalEmulator::resize( int columns, int rows ) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
mTerm.is_syncing = true;
|
|
||||||
tresize( columns, rows );
|
tresize( columns, rows );
|
||||||
|
|
||||||
redraw();
|
redraw();
|
||||||
@@ -3583,6 +3736,11 @@ bool TerminalEmulator::update() {
|
|||||||
_die( "Failed to resize pty!" );
|
_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;
|
mTerm.is_syncing = false;
|
||||||
redraw();
|
redraw();
|
||||||
}
|
}
|
||||||
@@ -3598,23 +3756,23 @@ bool TerminalEmulator::update() {
|
|||||||
|
|
||||||
int reads = 0;
|
int reads = 0;
|
||||||
Clock readBudgetClock;
|
Clock readBudgetClock;
|
||||||
|
bool presentationDeadlineReached = false;
|
||||||
while ( reads < MAX_TTY_READS && ttyread() > 0 ) {
|
while ( reads < MAX_TTY_READS && ttyread() > 0 ) {
|
||||||
++reads;
|
++reads;
|
||||||
|
if ( mPresentationClock.getElapsedTime() >= mPresentationInterval ) {
|
||||||
|
presentationDeadlineReached = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if ( readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) )
|
if ( readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) )
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
bool readBudgetSaturated =
|
bool readBudgetSaturated =
|
||||||
reads == MAX_TTY_READS ||
|
reads == MAX_TTY_READS || presentationDeadlineReached ||
|
||||||
( reads > 0 && readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) );
|
( reads > 0 && readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) );
|
||||||
|
|
||||||
/* Keep presentation decoupled from every PTY read batch, but bound the
|
/* Keep presentation decoupled from every PTY read batch. Sustained output publishes on the
|
||||||
* deferral so sustained output remains visibly live. The time limit handles
|
* host frame deadline, while a drained/idle burst still publishes immediately. */
|
||||||
* expensive batches; the batch limit guarantees progress when updates are
|
bool presentationDue = !readBudgetSaturated || presentationDeadlineReached;
|
||||||
* individually very fast. */
|
|
||||||
if ( readBudgetSaturated )
|
|
||||||
++mDeferredPresentationBatches;
|
|
||||||
bool presentationDue = !readBudgetSaturated || mDeferredPresentationBatches >= 32 ||
|
|
||||||
mPresentationClock.getElapsedTime() >= Milliseconds( 75 );
|
|
||||||
if ( presentationDue && ( reads > 0 || mDirty ) )
|
if ( presentationDue && ( reads > 0 || mDirty ) )
|
||||||
draw();
|
draw();
|
||||||
|
|
||||||
@@ -3626,6 +3784,9 @@ bool TerminalEmulator::update() {
|
|||||||
if ( mProcess->hasExited() && !readBudgetSaturated ) {
|
if ( mProcess->hasExited() && !readBudgetSaturated ) {
|
||||||
mExitCode = mProcess->getExitCode();
|
mExitCode = mProcess->getExitCode();
|
||||||
mStatus = TERMINATED;
|
mStatus = TERMINATED;
|
||||||
|
// Publish process state together with the final drained frame before the ordered exit
|
||||||
|
// event.
|
||||||
|
redraw();
|
||||||
onProcessExit( mExitCode );
|
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
|
||||||
@@ -31,7 +31,10 @@ UITerminal* UITerminal::New( const std::shared_ptr<TerminalDisplay>& terminalDis
|
|||||||
return eeNew( UITerminal, ( terminalDisplay ) );
|
return eeNew( UITerminal, ( terminalDisplay ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
UITerminal::~UITerminal() {}
|
UITerminal::~UITerminal() {
|
||||||
|
if ( mTerm && mTerminalEventCallbackId )
|
||||||
|
mTerm->popEventCallback( mTerminalEventCallbackId );
|
||||||
|
}
|
||||||
|
|
||||||
Uint32 UITerminal::getType() const {
|
Uint32 UITerminal::getType() const {
|
||||||
return UI_TYPE_TERMINAL;
|
return UI_TYPE_TERMINAL;
|
||||||
@@ -51,28 +54,29 @@ void UITerminal::draw() {
|
|||||||
void UITerminal::registerNewTerminal() {
|
void UITerminal::registerNewTerminal() {
|
||||||
if ( !mTerm )
|
if ( !mTerm )
|
||||||
return;
|
return;
|
||||||
mTerm->pushEventCallback( [this]( const TerminalDisplay::Event& event ) {
|
mTerminalEventCallbackId =
|
||||||
switch ( event.type ) {
|
mTerm->pushEventCallback( [this]( const TerminalDisplay::Event& event ) {
|
||||||
case TerminalDisplay::EventType::TITLE: {
|
switch ( event.type ) {
|
||||||
if ( !mIsCustomTitle && mTitle != event.eventData ) {
|
case TerminalDisplay::EventType::TITLE: {
|
||||||
mTitle = event.eventData;
|
if ( !mIsCustomTitle && mTitle != event.eventData ) {
|
||||||
sendTextEvent( Event::OnTitleChange, mTitle );
|
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 ) :
|
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 ); } );
|
[this] { mTerm->action( TerminalShortcutAction::PASTE_SELECTION ); } );
|
||||||
setCommand( "terminal-copy", [this] { mTerm->action( TerminalShortcutAction::COPY ); } );
|
setCommand( "terminal-copy", [this] { mTerm->action( TerminalShortcutAction::COPY ); } );
|
||||||
setCommand( "terminal-open-link",
|
setCommand( "terminal-open-link",
|
||||||
[this] { Engine::instance()->openURI( mTerm->getTerminal()->getSelection() ); } );
|
[this] { Engine::instance()->openURI( mTerm->getSelection() ); } );
|
||||||
subscribeScheduledUpdate();
|
subscribeScheduledUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
int UITerminal::getContentSize() const {
|
int UITerminal::getContentSize() const {
|
||||||
if ( mTerm && mTerm->getTerminal() )
|
if ( mTerm )
|
||||||
return mTerm->getTerminal()->getHistorySize() + mTerm->getTerminal()->getNumRows();
|
return mTerm->scrollSize() + mTerm->rowCount();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +143,13 @@ void UITerminal::onContentSizeChange() {
|
|||||||
mVScroll->setPixelsSize( mVScroll->getPixelsSize().getWidth(),
|
mVScroll->setPixelsSize( mVScroll->getPixelsSize().getWidth(),
|
||||||
getPixelsSize().getHeight() - mPaddingPx.Top - mPaddingPx.Bottom );
|
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();
|
updateScrollPosition();
|
||||||
mVScroll->setPageStep( contentSize > 0 ? ( visibleArea / (Float)contentSize ) : 1.f );
|
mVScroll->setPageStep( contentSize > 0 ? ( visibleArea / (Float)contentSize ) : 1.f );
|
||||||
updateScroll();
|
updateScroll();
|
||||||
@@ -179,17 +190,22 @@ void UITerminal::onPaddingChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int UITerminal::getVisibleArea() const {
|
int UITerminal::getVisibleArea() const {
|
||||||
return ( mTerm && mTerm->getTerminal() ) ? mTerm->getTerminal()->getNumRows() : 0;
|
return mTerm ? mTerm->rowCount() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITerminal::updateScrollPosition() {
|
void UITerminal::updateScrollPosition() {
|
||||||
if ( mTerm && mTerm->getTerminal() ) {
|
if ( !mTerm || mVScroll->isDragging() )
|
||||||
int historySize = mTerm->getTerminal()->getHistorySize();
|
return;
|
||||||
Float val = historySize > 0
|
if ( mScrollByBar ) {
|
||||||
? ( 1.f - mTerm->getTerminal()->scrollPos() / (Float)historySize )
|
if ( mPendingScrollCommand != 0 &&
|
||||||
: 1.f;
|
mTerm->lastAppliedScrollCommand() < mPendingScrollCommand )
|
||||||
mVScroll->setValue( val, false );
|
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 {
|
int UITerminal::getScrollableArea() const {
|
||||||
@@ -211,11 +227,11 @@ void UITerminal::updateScroll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UITerminal::onScrollChange() {
|
void UITerminal::onScrollChange() {
|
||||||
if ( !mTerm || !mTerm->getTerminal() )
|
if ( !mTerm )
|
||||||
return;
|
return;
|
||||||
int scrollTo = ( getScrollableArea() - mScrollOffset );
|
int scrollTo = ( getScrollableArea() - mScrollOffset );
|
||||||
TerminalArg arg( scrollTo );
|
mPendingScrollCommand = mTerm->scrollTo( scrollTo );
|
||||||
mTerm->getTerminal()->kscrollto( &arg );
|
mScrollByBar = mPendingScrollCommand != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITerminal::setVerticalScrollMode( const ScrollBarMode& Mode ) {
|
void UITerminal::setVerticalScrollMode( const ScrollBarMode& Mode ) {
|
||||||
@@ -321,18 +337,23 @@ const std::shared_ptr<TerminalDisplay>& UITerminal::getTerm() const {
|
|||||||
void UITerminal::scheduledUpdate( const Time& ) {
|
void UITerminal::scheduledUpdate( const Time& ) {
|
||||||
if ( !mTerm )
|
if ( !mTerm )
|
||||||
return;
|
return;
|
||||||
|
auto terminal = mTerm;
|
||||||
|
|
||||||
auto mousePos = getInput()->getRelativeMousePos();
|
auto mousePos = getInput()->getRelativeMousePos();
|
||||||
bool mouseOutsideBounds =
|
bool mouseOutsideBounds =
|
||||||
mousePos.y < 0 || mousePos.y > getUISceneNode()->getWindow()->getSize().getHeight();
|
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();
|
invalidateDraw();
|
||||||
|
|
||||||
if ( ScrollBarMode::AlwaysOn == mVScrollMode ) {
|
if ( ScrollBarMode::AlwaysOn == mVScrollMode ) {
|
||||||
mVScroll->setVisible( !mTerm->getTerminal()->tisaltscr() )
|
mVScroll->setVisible( !terminal->isAltScr() )->setEnabled( !terminal->isAltScr() );
|
||||||
->setEnabled( !mTerm->getTerminal()->tisaltscr() );
|
|
||||||
} else if ( ScrollBarMode::Auto == mVScrollMode ) {
|
} else if ( ScrollBarMode::Auto == mVScrollMode ) {
|
||||||
if ( mViewType == ScrollViewType::Overlay && mMouseClock.getElapsedTime() > Seconds( 1 ) &&
|
if ( mViewType == ScrollViewType::Overlay && mMouseClock.getElapsedTime() > Seconds( 1 ) &&
|
||||||
!mVScroll->isDragging() )
|
!mVScroll->isDragging() )
|
||||||
@@ -493,8 +514,8 @@ Uint32 UITerminal::onKeyUp( const KeyEvent& ) {
|
|||||||
Uint32 UITerminal::onMouseMove( const Vector2i& position, const Uint32& flags ) {
|
Uint32 UITerminal::onMouseMove( const Vector2i& position, const Uint32& flags ) {
|
||||||
if ( mViewType == ScrollViewType::Overlay && ScrollBarMode::Auto == mVScrollMode ) {
|
if ( mViewType == ScrollViewType::Overlay && ScrollBarMode::Auto == mVScrollMode ) {
|
||||||
mMouseClock.restart();
|
mMouseClock.restart();
|
||||||
bool visible = !mTerm->getTerminal()->tisaltscr() && getContentSize() > getVisibleArea() &&
|
bool visible =
|
||||||
!mTerm->getTerminal()->hasSelection();
|
!mTerm->isAltScr() && getContentSize() > getVisibleArea() && !mTerm->hasSelection();
|
||||||
mVScroll->setVisible( visible )->setEnabled( visible );
|
mVScroll->setVisible( visible )->setEnabled( visible );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,8 +584,8 @@ void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
|
|||||||
if ( !mCreateDefaultContextMenuOptions )
|
if ( !mCreateDefaultContextMenuOptions )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( mTerm->getTerminal()->hasSelection() ) {
|
if ( mTerm->hasSelection() ) {
|
||||||
auto sel( mTerm->getTerminal()->getSelection() );
|
auto sel( mTerm->getSelection() );
|
||||||
|
|
||||||
if ( LuaPattern::hasMatches( sel, LuaPattern::getURIPattern() ) ) {
|
if ( LuaPattern::hasMatches( sel, LuaPattern::getURIPattern() ) ) {
|
||||||
menuAdd( menu, i18n( "uiterminal_open_link", "Open Link" ), "earth",
|
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" )
|
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" )
|
menuAdd( menu, i18n( "uiterminal_paste", "Paste" ), "paste", "terminal-paste" )
|
||||||
->setEnabled( !getUISceneNode()->getWindow()->getClipboard()->getText().empty() );
|
->setEnabled( !getUISceneNode()->getWindow()->getClipboard()->getText().empty() );
|
||||||
}
|
}
|
||||||
@@ -628,10 +649,14 @@ bool UITerminal::onCreateContextMenu( const Vector2i& position, const Uint32& fl
|
|||||||
|
|
||||||
void UITerminal::restart() {
|
void UITerminal::restart() {
|
||||||
auto win = SceneManager::instance()->getUISceneNode()->getWindow();
|
auto win = SceneManager::instance()->getUISceneNode()->getWindow();
|
||||||
|
if ( mTerm && mTerminalEventCallbackId )
|
||||||
|
mTerm->popEventCallback( mTerminalEventCallbackId );
|
||||||
mTerm = TerminalDisplay::create( win, mTerm->getFont(), mTerm->getFontSize(), mTerm->getSize(),
|
mTerm = TerminalDisplay::create( win, mTerm->getFont(), mTerm->getFontSize(), mTerm->getSize(),
|
||||||
mTerm->getProgram(), mTerm->getArgs(), mTerm->getWorkingDir(),
|
mTerm->getProgram(), mTerm->getArgs(), mTerm->getWorkingDir(),
|
||||||
mTerm->getHistorySize(), nullptr, mTerm->useFrameBuffer(),
|
mTerm->getHistorySize(), nullptr, mTerm->useFrameBuffer(),
|
||||||
mTerm->getKeepAlive(), mTerm->getEnv() );
|
mTerm->getKeepAlive(), mTerm->getEnv() );
|
||||||
|
mTerminalEventCallbackId = 0;
|
||||||
|
registerNewTerminal();
|
||||||
syncFontRenderingConfig();
|
syncFontRenderingConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
#include "utest.hpp"
|
#include "utest.hpp"
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <eterm/system/iprocess.hpp>
|
#include <eterm/system/iprocess.hpp>
|
||||||
#include <eterm/terminal/ipseudoterminal.hpp>
|
#include <eterm/terminal/ipseudoterminal.hpp>
|
||||||
#include <eterm/terminal/iterminaldisplay.hpp>
|
#include <eterm/terminal/iterminaldisplay.hpp>
|
||||||
#include <eterm/terminal/terminalemulator.hpp>
|
#include <eterm/terminal/terminalemulator.hpp>
|
||||||
|
#include <eterm/terminal/terminalsession.hpp>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
using namespace eterm::Terminal;
|
using namespace eterm::Terminal;
|
||||||
using namespace eterm::System;
|
using namespace eterm::System;
|
||||||
@@ -11,7 +15,11 @@ using namespace eterm::System;
|
|||||||
class MockPty : public IPseudoTerminal {
|
class MockPty : public IPseudoTerminal {
|
||||||
public:
|
public:
|
||||||
std::string mBuffer;
|
std::string mBuffer;
|
||||||
|
std::string mWrites;
|
||||||
|
bool mLoopWrites{ true };
|
||||||
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
|
size_t mMaxRead{ std::numeric_limits<size_t>::max() };
|
||||||
|
size_t mReadOffset{ 0 };
|
||||||
|
std::atomic<size_t> mBytesRead{ 0 };
|
||||||
int mCols = 80;
|
int mCols = 80;
|
||||||
int mRows = 24;
|
int mRows = 24;
|
||||||
int getNumColumns() const override { return mCols; }
|
int getNumColumns() const override { return mCols; }
|
||||||
@@ -23,45 +31,458 @@ class MockPty : public IPseudoTerminal {
|
|||||||
}
|
}
|
||||||
bool isTTY() const override { return true; }
|
bool isTTY() const override { return true; }
|
||||||
int write( const char* s, size_t n ) override {
|
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;
|
return n;
|
||||||
}
|
}
|
||||||
int read( char* buf, size_t n, bool ) override {
|
int read( char* buf, size_t n, bool ) override {
|
||||||
if ( mBuffer.empty() )
|
if ( mReadOffset == mBuffer.size() )
|
||||||
return 0;
|
return 0;
|
||||||
size_t toRead = std::min( { n, mBuffer.size(), mMaxRead } );
|
size_t toRead = std::min( { n, mBuffer.size() - mReadOffset, mMaxRead } );
|
||||||
memcpy( buf, mBuffer.data(), toRead );
|
memcpy( buf, mBuffer.data() + mReadOffset, toRead );
|
||||||
mBuffer.erase( 0, toRead );
|
mReadOffset += toRead;
|
||||||
|
mBytesRead.fetch_add( toRead, std::memory_order_relaxed );
|
||||||
return toRead;
|
return toRead;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class MockProcess : public IProcess {
|
class MockProcess : public IProcess {
|
||||||
public:
|
public:
|
||||||
bool mExited{ false };
|
std::atomic<bool> mExited{ false };
|
||||||
void checkExitStatus() override {}
|
void checkExitStatus() override {}
|
||||||
bool hasExited() const override { return mExited; }
|
bool hasExited() const override { return mExited.load(); }
|
||||||
int getExitCode() const override { return 0; }
|
int getExitCode() const override { return 0; }
|
||||||
void terminate() override {}
|
void terminate() override {}
|
||||||
void waitForExit() override {}
|
void waitForExit() override {}
|
||||||
int pid() override { return 123; }
|
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 {
|
class MockDisplay : public ITerminalDisplay {
|
||||||
public:
|
public:
|
||||||
int mDrawLines{ 0 };
|
int mDrawLines{ 0 };
|
||||||
|
int mDrawEnds{ 0 };
|
||||||
uint32_t mFirstMode{ 0 };
|
uint32_t mFirstMode{ 0 };
|
||||||
uint32_t mSecondMode{ 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; }
|
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;
|
++mDrawLines;
|
||||||
mFirstMode = line[0].mode;
|
if ( y == 0 ) {
|
||||||
mSecondMode = line[1].mode;
|
mFirstMode = line[0].mode;
|
||||||
|
mSecondMode = line[1].mode;
|
||||||
|
mFirstGlyph = line[0];
|
||||||
|
mSecondGlyph = line[1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void drawCursor( int, int, TerminalGlyph, int, int, TerminalGlyph ) override {}
|
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 ) {
|
UTEST( eterm, basic_write ) {
|
||||||
auto pty = std::make_unique<MockPty>();
|
auto pty = std::make_unique<MockPty>();
|
||||||
auto process = std::make_unique<MockProcess>();
|
auto process = std::make_unique<MockProcess>();
|
||||||
@@ -77,6 +498,93 @@ UTEST( eterm, basic_write ) {
|
|||||||
EXPECT_STDSTREQ( "ABC", term->getSelection() );
|
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 ) {
|
UTEST( eterm, selection_redraw_while_idle ) {
|
||||||
auto pty = std::make_unique<MockPty>();
|
auto pty = std::make_unique<MockPty>();
|
||||||
auto process = std::make_unique<MockProcess>();
|
auto process = std::make_unique<MockProcess>();
|
||||||
@@ -120,14 +628,12 @@ UTEST( eterm, sustained_saturated_reads_present_periodically ) {
|
|||||||
|
|
||||||
term->update();
|
term->update();
|
||||||
display->mDrawLines = 0;
|
display->mDrawLines = 0;
|
||||||
std::string output( 33 * 1024, 'A' );
|
std::string output( 1024 * 1024, 'A' );
|
||||||
term->write( output.data(), output.size() );
|
term->write( output.data(), output.size() );
|
||||||
|
|
||||||
for ( int batch = 0; batch < 31; ++batch ) {
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( 100 );
|
||||||
EXPECT_FALSE( term->update() );
|
while ( display->mDrawLines == 0 && std::chrono::steady_clock::now() < deadline )
|
||||||
EXPECT_EQ( 0, display->mDrawLines );
|
term->update();
|
||||||
}
|
|
||||||
EXPECT_FALSE( term->update() );
|
|
||||||
EXPECT_TRUE( display->mDrawLines > 0 );
|
EXPECT_TRUE( display->mDrawLines > 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ App* App::instance() {
|
|||||||
bool App::isAnyTerminalDirty() const {
|
bool App::isAnyTerminalDirty() const {
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
mSplitter->forEachWidgetTypeStoppable( UI_TYPE_TERMINAL, [&dirty]( UIWidget* widget ) -> bool {
|
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 ) ) {
|
if ( Sys::processHasChildren( pid ) ) {
|
||||||
dirty = true;
|
dirty = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -5063,7 +5063,7 @@ void App::init( InitParameters& params ) {
|
|||||||
focusTabBehavior, onMsgBoxCloseCb );
|
focusTabBehavior, onMsgBoxCloseCb );
|
||||||
} else if ( mConfig.term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
|
} else if ( mConfig.term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
|
||||||
UITerminal* term = widget->asType<UITerminal>();
|
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 );
|
std::string msgBoxId = String::format( "msgbox_%p", this );
|
||||||
if ( Sys::processHasChildren( pid ) ) {
|
if ( Sys::processHasChildren( pid ) ) {
|
||||||
if ( nullptr != getUISceneNode()->find( msgBoxId ) )
|
if ( nullptr != getUISceneNode()->find( msgBoxId ) )
|
||||||
|
|||||||
@@ -126,8 +126,11 @@ void AgentSession::listSessions(
|
|||||||
|
|
||||||
void AgentSession::stop() {
|
void AgentSession::stop() {
|
||||||
for ( auto& term : mTerminals ) {
|
for ( auto& term : mTerminals ) {
|
||||||
if ( term.second.display && term.second.eventCbId )
|
if ( term.second.display ) {
|
||||||
term.second.display->popEventCallback( term.second.eventCbId );
|
if ( term.second.eventCbId )
|
||||||
|
term.second.display->popEventCallback( term.second.eventCbId );
|
||||||
|
term.second.display->setDataCallback( {} );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mTerminals.clear();
|
mTerminals.clear();
|
||||||
|
|
||||||
@@ -171,10 +174,9 @@ void AgentSession::cancel() {
|
|||||||
void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* uiTerm ) {
|
void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* uiTerm ) {
|
||||||
auto& termData = mTerminals[terminalId];
|
auto& termData = mTerminals[terminalId];
|
||||||
termData.display = uiTerm->getTerm();
|
termData.display = uiTerm->getTerm();
|
||||||
termData.emulator = uiTerm->getTerm()->getTerminal();
|
|
||||||
termData.uiTerm = uiTerm;
|
termData.uiTerm = uiTerm;
|
||||||
if ( termData.emulator ) {
|
if ( termData.display ) {
|
||||||
termData.emulator->setDataCb( [this, terminalId]( const char* data, size_t size ) {
|
termData.display->setDataCallback( [this, terminalId]( const char* data, size_t size ) {
|
||||||
auto it = mTerminals.find( terminalId );
|
auto it = mTerminals.find( terminalId );
|
||||||
if ( it != mTerminals.end() ) {
|
if ( it != mTerminals.end() ) {
|
||||||
it->second.outputBuffer.append( data, size );
|
it->second.outputBuffer.append( data, size );
|
||||||
@@ -188,7 +190,7 @@ void AgentSession::setTerminalData( const std::string& terminalId, UITerminal* u
|
|||||||
auto it = mTerminals.find( terminalId );
|
auto it = mTerminals.find( terminalId );
|
||||||
if ( it != mTerminals.end() ) {
|
if ( it != mTerminals.end() ) {
|
||||||
WaitForTerminalExitResponse res;
|
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 );
|
auto callbacks = std::move( it->second.exitCallbacks );
|
||||||
it->second.exitCallbacks.clear();
|
it->second.exitCallbacks.clear();
|
||||||
for ( const auto& cb : callbacks ) {
|
for ( const auto& cb : callbacks ) {
|
||||||
@@ -268,9 +270,9 @@ void AgentSession::setupClient() {
|
|||||||
res.truncated = false;
|
res.truncated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( it->second.emulator && it->second.emulator->hasExited() ) {
|
if ( it->second.display && it->second.display->hasTerminated() ) {
|
||||||
TerminalExitStatus status;
|
TerminalExitStatus status;
|
||||||
status.exitCode = it->second.emulator->getExitCode();
|
status.exitCode = it->second.display->getExitCode();
|
||||||
res.exitStatus = status;
|
res.exitStatus = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,8 +281,8 @@ void AgentSession::setupClient() {
|
|||||||
|
|
||||||
mClient->onKillTerminal = [this]( const KillTerminalRequest& req, auto cb ) {
|
mClient->onKillTerminal = [this]( const KillTerminalRequest& req, auto cb ) {
|
||||||
auto it = mTerminals.find( req.terminalId );
|
auto it = mTerminals.find( req.terminalId );
|
||||||
if ( it != mTerminals.end() && it->second.emulator ) {
|
if ( it != mTerminals.end() && it->second.display ) {
|
||||||
it->second.emulator->terminate();
|
it->second.display->terminate();
|
||||||
}
|
}
|
||||||
cb( KillTerminalResponse() );
|
cb( KillTerminalResponse() );
|
||||||
};
|
};
|
||||||
@@ -290,8 +292,10 @@ void AgentSession::setupClient() {
|
|||||||
if ( it != mTerminals.end() ) {
|
if ( it != mTerminals.end() ) {
|
||||||
if ( it->second.display && it->second.eventCbId )
|
if ( it->second.display && it->second.eventCbId )
|
||||||
it->second.display->popEventCallback( it->second.eventCbId );
|
it->second.display->popEventCallback( it->second.eventCbId );
|
||||||
if ( it->second.emulator )
|
if ( it->second.display ) {
|
||||||
it->second.emulator->terminate();
|
it->second.display->setDataCallback( {} );
|
||||||
|
it->second.display->terminate();
|
||||||
|
}
|
||||||
if ( it->second.uiTerm )
|
if ( it->second.uiTerm )
|
||||||
it->second.uiTerm->close();
|
it->second.uiTerm->close();
|
||||||
mTerminals.erase( it );
|
mTerminals.erase( it );
|
||||||
@@ -301,10 +305,10 @@ void AgentSession::setupClient() {
|
|||||||
|
|
||||||
mClient->onWaitForTerminalExit = [this]( const WaitForTerminalExitRequest& req, auto cb ) {
|
mClient->onWaitForTerminalExit = [this]( const WaitForTerminalExitRequest& req, auto cb ) {
|
||||||
auto it = mTerminals.find( req.terminalId );
|
auto it = mTerminals.find( req.terminalId );
|
||||||
if ( it != mTerminals.end() && it->second.emulator ) {
|
if ( it != mTerminals.end() && it->second.display ) {
|
||||||
if ( it->second.emulator->hasExited() ) {
|
if ( it->second.display->hasTerminated() ) {
|
||||||
WaitForTerminalExitResponse res;
|
WaitForTerminalExitResponse res;
|
||||||
res.exitCode = it->second.emulator->getExitCode();
|
res.exitCode = it->second.display->getExitCode();
|
||||||
cb( res );
|
cb( res );
|
||||||
} else {
|
} else {
|
||||||
it->second.exitCallbacks.push_back( cb );
|
it->second.exitCallbacks.push_back( cb );
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "acpclient.hpp"
|
#include "acpclient.hpp"
|
||||||
#include <eepp/system/threadpool.hpp>
|
#include <eepp/system/threadpool.hpp>
|
||||||
#include <eterm/terminal/terminalemulator.hpp>
|
|
||||||
#include <eterm/ui/uiterminal.hpp>
|
#include <eterm/ui/uiterminal.hpp>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -60,7 +59,6 @@ class AgentSession {
|
|||||||
|
|
||||||
struct TermData {
|
struct TermData {
|
||||||
std::shared_ptr<TerminalDisplay> display;
|
std::shared_ptr<TerminalDisplay> display;
|
||||||
std::shared_ptr<TerminalEmulator> emulator;
|
|
||||||
UITerminal* uiTerm{ nullptr };
|
UITerminal* uiTerm{ nullptr };
|
||||||
std::string outputBuffer;
|
std::string outputBuffer;
|
||||||
Uint32 eventCbId{ 0 };
|
Uint32 eventCbId{ 0 };
|
||||||
|
|||||||
@@ -2360,10 +2360,7 @@ void DebuggerPlugin::run( const std::string& debugger, ProtocolSettings&& protoc
|
|||||||
plugin->getPluginContext()->getTerminalManager()->createTerminalInSplitter(
|
plugin->getPluginContext()->getTerminalManager()->createTerminalInSplitter(
|
||||||
cwd, cmd, args, env, false, false );
|
cwd, cmd, args, env, false, false );
|
||||||
|
|
||||||
doneFn( term && term->getTerm() && term->getTerm()->getTerminal() &&
|
doneFn( term && term->getTerm() ? term->getTerm()->getProcessId() : 0 );
|
||||||
term->getTerm()->getTerminal()->getProcess()
|
|
||||||
? term->getTerm()->getTerminal()->getProcess()->pid()
|
|
||||||
: 0 );
|
|
||||||
} else {
|
} else {
|
||||||
std::string fcmd = cmd + ( !args.empty() ? " " : "" ) + String::join( args, ' ' );
|
std::string fcmd = cmd + ( !args.empty() ? " " : "" ) + String::join( args, ' ' );
|
||||||
doneFn( plugin->getPluginContext()->getTerminalManager()->openInExternalTerminal(
|
doneFn( plugin->getPluginContext()->getTerminalManager()->openInExternalTerminal(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ bool StatusTerminalController::tryTabClose( UITab* tab ) {
|
|||||||
|
|
||||||
if ( mContext->getConfig().term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
|
if ( mContext->getConfig().term.warnBeforeClosingTab && widget->isType( UI_TYPE_TERMINAL ) ) {
|
||||||
UITerminal* term = widget->asType<UITerminal>();
|
UITerminal* term = widget->asType<UITerminal>();
|
||||||
ProcessID pid = term->getTerm()->getTerminal()->getProcess()->pid();
|
ProcessID pid = term->getTerm()->getProcessId();
|
||||||
if ( Sys::processHasChildren( pid ) ) {
|
if ( Sys::processHasChildren( pid ) ) {
|
||||||
UIMessageBox* msgBox =
|
UIMessageBox* msgBox =
|
||||||
UIMessageBox::New( UIMessageBox::OK_CANCEL,
|
UIMessageBox::New( UIMessageBox::OK_CANCEL,
|
||||||
@@ -141,8 +141,7 @@ UITerminal* StatusTerminalController::createTerminal(
|
|||||||
mContext->getTerminalFont() ? mContext->getTerminalFont() : mContext->getFontMono(),
|
mContext->getTerminalFont() ? mContext->getTerminalFont() : mContext->getFontMono(),
|
||||||
mContext->termConfig().fontSize.asPixels( 0, Sizef(), mContext->getDisplayDPI() ),
|
mContext->termConfig().fontSize.asPixels( 0, Sizef(), mContext->getDisplayDPI() ),
|
||||||
initialSize, program, args, env,
|
initialSize, program, args, env,
|
||||||
!workingDir.empty() ? workingDir
|
!workingDir.empty() ? workingDir : mContext->getTerminalManager()->getSelectedWorkingDir(),
|
||||||
: mContext->getTerminalManager()->getSelectedWorkingDir(),
|
|
||||||
10000, nullptr, false );
|
10000, nullptr, false );
|
||||||
|
|
||||||
if ( term == nullptr || term->getTerm() == nullptr ) {
|
if ( term == nullptr || term->getTerm() == nullptr ) {
|
||||||
@@ -154,7 +153,7 @@ UITerminal* StatusTerminalController::createTerminal(
|
|||||||
const auto& currentTerminalColorScheme =
|
const auto& currentTerminalColorScheme =
|
||||||
mContext->getTerminalManager()->getTerminalCurrentColorScheme();
|
mContext->getTerminalManager()->getTerminalCurrentColorScheme();
|
||||||
auto csIt = terminalColorSchemes.find( currentTerminalColorScheme );
|
auto csIt = terminalColorSchemes.find( currentTerminalColorScheme );
|
||||||
term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true );
|
term->getTerm()->setAllowMemoryTrimming( true );
|
||||||
term->getTerm()->setCursorMode( mContext->termConfig().cursorStyle );
|
term->getTerm()->setCursorMode( mContext->termConfig().cursorStyle );
|
||||||
term->setExclusiveMode( mContext->termConfig().exclusiveMode );
|
term->setExclusiveMode( mContext->termConfig().exclusiveMode );
|
||||||
term->setScrollViewType( mContext->termConfig().scrollBarType );
|
term->setScrollViewType( mContext->termConfig().scrollBarType );
|
||||||
@@ -242,7 +241,8 @@ UITerminal* StatusTerminalController::createTerminal(
|
|||||||
|
|
||||||
UIIcon* icon = mUISceneNode->findIcon( "terminal" );
|
UIIcon* icon = mUISceneNode->findIcon( "terminal" );
|
||||||
auto tab = mTabWidget->add(
|
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->setData( (UintPtr)tab );
|
||||||
term->on( Event::OnTitleChange, [tab, term]( auto ) { tab->setText( term->getTitle() ); } );
|
term->on( Event::OnTitleChange, [tab, term]( auto ) { tab->setText( term->getTitle() ); } );
|
||||||
|
|||||||
@@ -658,7 +658,7 @@ UITerminal* TerminalManager::createNewTerminal(
|
|||||||
} );
|
} );
|
||||||
term->setTitle( title );
|
term->setTitle( title );
|
||||||
auto csIt = mTerminalColorSchemes.find( mTerminalCurrentColorScheme );
|
auto csIt = mTerminalColorSchemes.find( mTerminalCurrentColorScheme );
|
||||||
term->getTerm()->getTerminal()->setAllowMemoryTrimnming( true );
|
term->getTerm()->setAllowMemoryTrimming( true );
|
||||||
term->getTerm()->setKeepAlive( !mApp->getConfig().term.closeTerminalTabOnExit );
|
term->getTerm()->setKeepAlive( !mApp->getConfig().term.closeTerminalTabOnExit );
|
||||||
term->getTerm()->pushEventCallback( [this, term]( const TerminalDisplay::Event& event ) {
|
term->getTerm()->pushEventCallback( [this, term]( const TerminalDisplay::Event& event ) {
|
||||||
if ( event.type == TerminalDisplay::EventType::PROCESS_EXIT &&
|
if ( event.type == TerminalDisplay::EventType::PROCESS_EXIT &&
|
||||||
|
|||||||
+496
-377
@@ -1,163 +1,343 @@
|
|||||||
#include <args/args.hxx>
|
#include <args/args.hxx>
|
||||||
|
#include <eepp/core/small_vector.hpp>
|
||||||
#include <eepp/ee.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 <iostream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <map>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
EE::Window::Window* win = NULL;
|
using namespace EE;
|
||||||
std::shared_ptr<TerminalDisplay> terminal = nullptr;
|
using namespace EE::Graphics;
|
||||||
Clock lastRender;
|
using namespace EE::Scene;
|
||||||
Clock secondsCounter;
|
using namespace EE::System;
|
||||||
Time frameTime{ Time::Zero };
|
using namespace EE::UI;
|
||||||
bool benchmarkMode{ false };
|
using namespace EE::UI::Tools;
|
||||||
bool warnBeforeClose{ false };
|
using namespace EE::Window;
|
||||||
std::string windowStringData;
|
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;
|
std::map<std::string, TerminalColorScheme> terminalColorSchemes;
|
||||||
bool displayingWarnBeforeClose{ false };
|
const TerminalColorScheme* selectedColorScheme{ nullptr };
|
||||||
bool yesPicked{ true };
|
Float terminalFontSize{ 12 };
|
||||||
bool needsRedraw{ false };
|
bool warnBeforeClose{ false };
|
||||||
Rectf yesBtn;
|
bool closeApproved{ false };
|
||||||
Rectf noBtn;
|
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 ) {
|
void loadColorSchemes( const std::string& resPath ) {
|
||||||
auto configPath = Sys::getConfigPath( "eterm" );
|
|
||||||
auto colorSchemes =
|
auto colorSchemes =
|
||||||
TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" );
|
TerminalColorScheme::loadFromFile( resPath + "colorschemes/terminalcolorschemes.conf" );
|
||||||
auto colorSchemesPath = configPath + FileSystem::getOSSlash() + "colorschemes";
|
const std::string configColorSchemesPath =
|
||||||
if ( FileSystem::isDirectory( colorSchemesPath ) ) {
|
Sys::getConfigPath( "eterm" ) + FileSystem::getOSSlash() + "colorschemes";
|
||||||
auto colorSchemesFiles = FileSystem::filesGetInPath( colorSchemesPath );
|
if ( FileSystem::isDirectory( configColorSchemesPath ) ) {
|
||||||
for ( auto& file : colorSchemesFiles ) {
|
for ( const auto& file : FileSystem::filesGetInPath( configColorSchemesPath ) ) {
|
||||||
auto colorSchemesInFile = TerminalColorScheme::loadFromFile( file );
|
auto fileColorSchemes = TerminalColorScheme::loadFromFile( file );
|
||||||
std::copy( colorSchemesInFile.begin(), colorSchemesInFile.end(),
|
colorSchemes.insert( colorSchemes.end(),
|
||||||
std::back_inserter( colorSchemes ) );
|
std::make_move_iterator( fileColorSchemes.begin() ),
|
||||||
|
std::make_move_iterator( fileColorSchemes.end() ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for ( auto colorScheme : colorSchemes )
|
for ( auto& colorScheme : colorSchemes ) {
|
||||||
terminalColorSchemes.insert( { colorScheme.getName(), colorScheme } );
|
std::string name = colorScheme.getName();
|
||||||
|
terminalColorSchemes.emplace( std::move( name ), std::move( colorScheme ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputCallback( InputEvent* event ) {
|
UITerminal* terminalFromTab( UITab* tab ) {
|
||||||
if ( !terminal || event->Type == InputEvent::EventsSent )
|
return tab && tab->getOwnedWidget() && tab->getOwnedWidget()->isType( UI_TYPE_TERMINAL )
|
||||||
|
? tab->getOwnedWidget()->asType<UITerminal>()
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateWindowTitle() {
|
||||||
|
if ( !appWindow )
|
||||||
return;
|
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 ) {
|
bool hasTerminals() {
|
||||||
case InputEvent::MouseMotion: {
|
bool found = false;
|
||||||
terminal->onMouseMove( win->getInput()->getMousePos(),
|
if ( tabSplitter )
|
||||||
win->getInput()->getPressTrigger() );
|
tabSplitter->forEachWidgetStoppable( [&found]( UIWidget* ) {
|
||||||
break;
|
found = true;
|
||||||
}
|
return true;
|
||||||
case InputEvent::MouseButtonDown: {
|
} );
|
||||||
if ( displayingWarnBeforeClose ) {
|
return found;
|
||||||
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() );
|
|
||||||
|
|
||||||
if ( win->getInput()->getDoubleClickTrigger() ) {
|
bool hasRunningChildren( UITab* tab ) {
|
||||||
terminal->onMouseDoubleClick( win->getInput()->getMousePos(),
|
auto* terminal = terminalFromTab( tab );
|
||||||
win->getInput()->getDoubleClickTrigger() );
|
return terminal && terminal->getTerm() &&
|
||||||
}
|
Sys::processHasChildren( terminal->getTerm()->getProcessId() );
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
void closeTab( UITab* tab ) {
|
||||||
}
|
if ( !tabSplitter || !tab || !tab->getOwnedWidget() )
|
||||||
case InputEvent::Window: {
|
return;
|
||||||
switch ( event->window.type ) {
|
tabSplitter->closeTab( tab->getOwnedWidget()->asType<UIWidget>(),
|
||||||
case InputEvent::WindowKeyboardFocusLost:
|
UITabWidget::FocusTabBehavior::Default );
|
||||||
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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
#if EE_PLATFORM == EE_PLATFORM_ANDROID
|
void queueExitCloseTab( UITab* tab ) {
|
||||||
if ( event->key.keysym.sym == KEY_RETURN ||
|
if ( tab && std::find( pendingExitCloseTabs.begin(), pendingExitCloseTabs.end(), tab ) ==
|
||||||
event->key.keysym.scancode == SCANCODE_RETURN ) {
|
pendingExitCloseTabs.end() ) {
|
||||||
win->startTextInput();
|
pendingExitCloseTabs.emplace_back( tab );
|
||||||
}
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool onCloseRequestCallback( EE::Window::Window* ) {
|
void queueExitedTabs() {
|
||||||
if ( warnBeforeClose &&
|
if ( !terminalConfig.closeOnExit )
|
||||||
Sys::processHasChildren( terminal->getTerminal()->getProcess()->pid() ) ) {
|
return;
|
||||||
displayingWarnBeforeClose = true;
|
tabSplitter->forEachTab( []( UITab* tab ) {
|
||||||
needsRedraw = true;
|
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 false;
|
||||||
}
|
closeDialog = UIMessageBox::New(
|
||||||
return true;
|
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[] ) {
|
EE_MAIN_FUNC int main( int argc, char* argv[] ) {
|
||||||
#ifdef EE_DEBUG
|
#ifdef EE_DEBUG
|
||||||
Log::instance()->setLogToStdOut( true );
|
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)",
|
args::Flag fb( parser, "framebuffer", "Use frame buffer (more memory usage, less CPU usage)",
|
||||||
{ "fb", "framebuffer" } );
|
{ "fb", "framebuffer" } );
|
||||||
args::ValueFlag<std::string> fontPath( parser, "fontpath", "Font path", { 'f', "font" } );
|
args::ValueFlag<std::string> fontPath( parser, "fontpath", "Font path", { 'f', "font" } );
|
||||||
args::ValueFlag<std::string> fallbackFontPathF( parser, "fallback-fontpath",
|
args::ValueFlag<std::string> fallbackFontPath( parser, "fallback-fontpath",
|
||||||
"Fallback Font path", { "fallback-font" } );
|
"Fallback Font path", { "fallback-font" } );
|
||||||
args::ValueFlag<Float> fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 );
|
args::ValueFlag<Float> fontSize( parser, "fontsize", "Font size (in dp)", { "fontsize" }, 11 );
|
||||||
const std::unordered_map<std::string, FontHinting> fontHintingMap{
|
const std::unordered_map<std::string, FontHinting> fontHintingMap{
|
||||||
{ "none", FontHinting::None },
|
{ "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> width( parser, "winwidth", "Window width (in dp)", { "width" }, 1280 );
|
||||||
args::ValueFlag<Float> height( parser, "winheight", "Window height (in dp)", { "height" },
|
args::ValueFlag<Float> height( parser, "winheight", "Window height (in dp)", { "height" },
|
||||||
720 );
|
720 );
|
||||||
args::ValueFlag<Float> pixelDensityConf( parser, "pixel-density",
|
args::ValueFlag<Float> pixelDensity( parser, "pixel-density",
|
||||||
"Set default application pixel density",
|
"Set default application pixel density",
|
||||||
{ 'd', "pixel-density" } );
|
{ 'd', "pixel-density" } );
|
||||||
args::Positional<std::string> wd( parser, "wording-dir", "Working Directory / executable" );
|
args::Positional<std::string> wd( parser, "wording-dir", "Working Directory / executable" );
|
||||||
args::Flag closeOnExit( parser, "close-on-exit",
|
args::Flag closeOnExit( parser, "close-on-exit",
|
||||||
"close the application when the executable exits", { 'c', "close" } );
|
"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",
|
parser, "warn-before-closing",
|
||||||
"Prompts for confirmation if a program is still running when closing the terminal.",
|
"Prompts for confirmation if a program is still running when closing the terminal.",
|
||||||
{ "warn-before-closing" } );
|
{ "warn-before-closing" } );
|
||||||
|
args::ValueFlag<size_t> initialTabs( parser, "tabs", "Number of initial terminal tabs",
|
||||||
|
{ "tabs" }, 1 );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parser.ParseCLI( argc, argv );
|
parser.ParseCLI( argc, argv );
|
||||||
} catch ( const args::Help& ) {
|
} catch ( const args::Help& ) {
|
||||||
std::cout << parser;
|
std::cout << parser;
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
} catch ( const args::ParseError& e ) {
|
} catch ( const args::ParseError& error ) {
|
||||||
std::cerr << e.what() << std::endl;
|
std::cerr << error.what() << std::endl;
|
||||||
std::cerr << parser;
|
std::cerr << parser;
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
} catch ( args::ValidationError& e ) {
|
} catch ( args::ValidationError& error ) {
|
||||||
std::cerr << e.what() << std::endl;
|
std::cerr << error.what() << std::endl;
|
||||||
std::cerr << parser;
|
std::cerr << parser;
|
||||||
return EXIT_FAILURE;
|
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();
|
DisplayManager* displayManager = Engine::instance()->getDisplayManager();
|
||||||
Display* currentDisplay = displayManager->getDisplayIndex( 0 );
|
Display* currentDisplay = displayManager->getDisplayIndex( 0 );
|
||||||
|
if ( !currentDisplay ) {
|
||||||
std::string resPath = Sys::getProcessPath();
|
std::cerr << "Display not found, exiting" << std::endl;
|
||||||
#if EE_PLATFORM == EE_PLATFORM_MACOS
|
return EXIT_FAILURE;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Thread> systemFontWarmUp;
|
Sizei windowSize( width.Get(), height.Get() );
|
||||||
if ( SystemFontResolver::isEnabled() ) {
|
const auto displaySize = currentDisplay->getUsableBounds().getSize();
|
||||||
systemFontWarmUp =
|
if ( displaySize.getWidth() > 0 && windowSize.getWidth() >= displaySize.getWidth() )
|
||||||
std::make_unique<Thread>( [] { SystemFontResolver::instance()->warmUp(); } );
|
windowSize.setWidth( static_cast<int>( displaySize.getWidth() * 0.8f ) );
|
||||||
systemFontWarmUp->launch();
|
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" );
|
||||||
}
|
}
|
||||||
|
if ( fontPath && FileSystem::fileExists( fontPath.Get() ) ) {
|
||||||
displayManager->enableScreenSaver();
|
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
|
||||||
displayManager->enableMouseFocusClickThrough();
|
if ( terminalFont->loadFromFile( fontPath.Get() ) )
|
||||||
displayManager->disableBypassCompositor();
|
FontFamily::loadFromRegular( terminalFont );
|
||||||
defaultResourceScope().getFontService().setHinting( fontHinting.Get() );
|
else
|
||||||
defaultResourceScope().getFontService().setAntialiasing( fontAntialiasing.Get() );
|
terminalFont = nullptr;
|
||||||
|
}
|
||||||
Sizei winSize( width.Get(), height.Get() );
|
if ( !terminalFont ) {
|
||||||
auto displaySize( currentDisplay->getUsableBounds().getSize() );
|
terminalFont = FontTrueType::New( "eterm-monospace", resourceScope ).get();
|
||||||
if ( displaySize.getWidth() > 0 && winSize.getWidth() >= displaySize.getWidth() )
|
if ( !terminalFont->loadFromFile( resPath + "fonts/DejaVuSansMonoNerdFontComplete.ttf" ) ) {
|
||||||
winSize.setWidth( static_cast<int>( displaySize.getWidth() * 0.8 ) );
|
std::cerr << "Could not load terminal font" << std::endl;
|
||||||
|
|
||||||
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();
|
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
terminal->setFontHinting( fontHinting.Get() );
|
FontFamily::loadFromRegular( terminalFont, "DejaVuSansMono" );
|
||||||
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();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.reset();
|
if ( fallbackFontPath ) {
|
||||||
systemFontWarmUp.reset();
|
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;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user