From 6a24bc0d6e6808231f1191a436f20c5f13de5652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 4 Sep 2026 21:13:27 -0300 Subject: [PATCH] eterm: implement Kitty keyboard protocol Add Kitty keyboard protocol negotiation, state stacks, and semantic key encoding with support for alternate keys, event types, associated text, and reporting all keys as escape sequences. Forward key press, repeat, and release events through the terminal worker while preserving legacy input behavior. Keep AltGr composition distinct from Alt shortcuts and correlate SDL text events to prevent missing or duplicated international-layout input. Add protocol documentation and unit coverage for negotiation, state transitions, modifiers, function and keypad keys, shifted input, AltGr composition, repeats, releases, and duplicate suppression. --- docs/articles/eterm-kitty-keyboard.md | 17 + include/eepp/scene/eventdispatcher.hpp | 2 +- include/eepp/scene/keyevent.hpp | 5 +- include/eepp/window/inputevent.hpp | 1 + src/eepp/scene/eventdispatcher.cpp | 6 +- src/eepp/scene/keyevent.cpp | 12 +- src/eepp/window/backend/SDL2/inputsdl2.cpp | 2 + src/eepp/window/backend/SDL3/inputsdl3.cpp | 8 +- .../eterm/terminal/kittykeyboardprotocol.hpp | 67 ++++ .../eterm/terminal/terminaldisplay.hpp | 11 +- .../eterm/terminal/terminalemulator.hpp | 15 + .../eterm/terminal/terminalsession.hpp | 16 +- .../eterm/terminal/kittykeyboardprotocol.cpp | 321 ++++++++++++++++++ .../src/eterm/terminal/terminaldisplay.cpp | 92 ++--- .../src/eterm/terminal/terminalemulator.cpp | 168 ++++++++- .../src/eterm/terminal/terminalsession.cpp | 14 + src/modules/eterm/src/eterm/ui/uiterminal.cpp | 9 +- src/tests/unit_tests/eterm_tests.cpp | 121 ++++++- 18 files changed, 791 insertions(+), 96 deletions(-) create mode 100644 docs/articles/eterm-kitty-keyboard.md create mode 100644 src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp create mode 100644 src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp diff --git a/docs/articles/eterm-kitty-keyboard.md b/docs/articles/eterm-kitty-keyboard.md new file mode 100644 index 000000000..befca06bf --- /dev/null +++ b/docs/articles/eterm-kitty-keyboard.md @@ -0,0 +1,17 @@ +# eterm Kitty keyboard protocol + +eterm implements Kitty keyboard protocol progressive enhancement. Applications negotiate it at +runtime with `CSI ? u`, `CSI > flags u`, `CSI < count u`, and `CSI = flags ; mode u`; changing +`TERM` to `xterm-kitty` is neither necessary nor recommended. + +The implementation accepts all standardized flags. Main and alternate screens have independent +bounded mode stacks. Alternate-key reporting derives the standard PC-layout key from EEPP's +layout-independent scancode and the shifted key from committed text. Associated committed text is +reported when available from text input. + +SDL2 and SDL3 repeat markers are preserved, so press, repeat, and release events are distinct when +event reporting is enabled. IME pre-edit input remains local; only committed text is forwarded. + +For a raw diagnostic, an application can push report-all mode with `ESC [ > 8 u`. In that mode, +Ctrl+Enter is sent as `ESC [ 13 ; 5 u`. It must pop the mode with `ESC [ < u` before exiting. +Intermediaries such as tmux or screen must independently support and forward the protocol. diff --git a/include/eepp/scene/eventdispatcher.hpp b/include/eepp/scene/eventdispatcher.hpp index 716b8a5c7..2f40aa127 100644 --- a/include/eepp/scene/eventdispatcher.hpp +++ b/include/eepp/scene/eventdispatcher.hpp @@ -60,7 +60,7 @@ class EE_API EventDispatcher { const Uint32& mod ); void sendKeyDown( const Keycode& keyCode, const Scancode& scancode, const Uint32& chr, - const Uint32& mod ); + const Uint32& mod, bool repeat = false ); void sendMouseClick( Node* toNode, const Vector2i& pos, const Uint32 flags ); diff --git a/include/eepp/scene/keyevent.hpp b/include/eepp/scene/keyevent.hpp index cbdff3892..50ae89831 100644 --- a/include/eepp/scene/keyevent.hpp +++ b/include/eepp/scene/keyevent.hpp @@ -16,7 +16,7 @@ namespace EE { namespace Scene { class EE_API KeyEvent : public Event { public: KeyEvent( Node* node, const Uint32& eventNum, const Keycode& keyCode, const Scancode& scancode, - const Uint32& chr, const Uint32& mod ); + const Uint32& chr, const Uint32& mod, bool repeat = false ); KeyEvent( const KeyEvent& event ); @@ -30,6 +30,8 @@ class EE_API KeyEvent : public Event { const Uint32& getMod() const; + bool isRepeat() const; + /** The modifier key mask only for CTRL ALT SHIFT and META (no caps, num, etc) */ Uint32 getSanitizedMod() const; @@ -38,6 +40,7 @@ class EE_API KeyEvent : public Event { Scancode mScancode{ Scancode::SCANCODE_UNKNOWN }; String::StringBaseType mChar{ 0 }; Uint32 mMod{ 0 }; + bool mRepeat{ false }; }; class EE_API TextInputEvent : public Event { diff --git a/include/eepp/window/inputevent.hpp b/include/eepp/window/inputevent.hpp index 5aa9e776b..6c80cb194 100644 --- a/include/eepp/window/inputevent.hpp +++ b/include/eepp/window/inputevent.hpp @@ -56,6 +56,7 @@ class InputEvent { struct KeyboardEvent { Uint8 which; /** The keyboard device index */ Uint8 state; + Uint8 repeat; /** Non-zero for an automatically repeated key-down. */ KeySym keysym; }; diff --git a/src/eepp/scene/eventdispatcher.cpp b/src/eepp/scene/eventdispatcher.cpp index 51c1106d1..f5cc6d032 100644 --- a/src/eepp/scene/eventdispatcher.cpp +++ b/src/eepp/scene/eventdispatcher.cpp @@ -56,7 +56,7 @@ void EventDispatcher::inputCallback( InputEvent* event ) { break; case InputEvent::KeyDown: sendKeyDown( event->key.keysym.sym, event->key.keysym.scancode, - event->key.keysym.unicode, event->key.keysym.mod ); + event->key.keysym.unicode, event->key.keysym.mod, event->key.repeat != 0 ); break; case InputEvent::TextInput: sendTextInput( event->text.text, event->text.timestamp ); @@ -264,8 +264,8 @@ void EventDispatcher::sendKeyUp( const Keycode& keyCode, const Scancode& scancod } void EventDispatcher::sendKeyDown( const Keycode& keyCode, const Scancode& scancode, - const Uint32& chr, const Uint32& mod ) { - KeyEvent keyEvent = KeyEvent( mFocusNode, Event::KeyDown, keyCode, scancode, chr, mod ); + const Uint32& chr, const Uint32& mod, bool repeat ) { + KeyEvent keyEvent = KeyEvent( mFocusNode, Event::KeyDown, keyCode, scancode, chr, mod, repeat ); Node* node = mFocusNode; while ( NULL != node ) { if ( node->isEnabled() && node->onKeyDown( keyEvent ) ) diff --git a/src/eepp/scene/keyevent.cpp b/src/eepp/scene/keyevent.cpp index c7e2f43b3..f6655492e 100644 --- a/src/eepp/scene/keyevent.cpp +++ b/src/eepp/scene/keyevent.cpp @@ -5,19 +5,21 @@ namespace EE { namespace Scene { KeyEvent::KeyEvent( Node* node, const Uint32& eventNum, const Keycode& keyCode, - const Scancode& scancode, const Uint32& chr, const Uint32& mod ) : + const Scancode& scancode, const Uint32& chr, const Uint32& mod, bool repeat ) : Event( node, eventNum ), mKeyCode( keyCode ), mScancode( scancode ), mChar( chr ), - mMod( mod ) {} + mMod( mod ), + mRepeat( repeat ) {} KeyEvent::KeyEvent( const KeyEvent& event ) : Event( event.getNode(), event.getType() ), mKeyCode( event.getKeyCode() ), mScancode( event.getScancode() ), mChar( event.getChar() ), - mMod( event.getMod() ) {} + mMod( event.getMod() ), + mRepeat( event.isRepeat() ) {} KeyEvent::~KeyEvent() {} @@ -37,6 +39,10 @@ const Uint32& KeyEvent::getMod() const { return mMod; } +bool KeyEvent::isRepeat() const { + return mRepeat; +} + Uint32 KeyEvent::getSanitizedMod() const { return mMod & KEYMOD_CTRL_SHIFT_ALT_META; } diff --git a/src/eepp/window/backend/SDL2/inputsdl2.cpp b/src/eepp/window/backend/SDL2/inputsdl2.cpp index 2b2c6bb9b..45c1b18de 100644 --- a/src/eepp/window/backend/SDL2/inputsdl2.cpp +++ b/src/eepp/window/backend/SDL2/inputsdl2.cpp @@ -273,6 +273,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { case SDL_KEYDOWN: { event.Type = InputEvent::KeyDown; event.key.state = SDLEvent.key.state; + event.key.repeat = SDLEvent.key.repeat; event.key.which = SDLEvent.key.windowID; event.key.keysym.sym = (Keycode)SDLEvent.key.keysym.sym; event.key.keysym.scancode = (Scancode)SDLEvent.key.keysym.scancode; @@ -284,6 +285,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { case SDL_KEYUP: { event.Type = InputEvent::KeyUp; event.key.state = SDLEvent.key.state; + event.key.repeat = 0; event.key.which = SDLEvent.key.windowID; event.key.keysym.sym = (Keycode)SDLEvent.key.keysym.sym; event.key.keysym.scancode = (Scancode)SDLEvent.key.keysym.scancode; diff --git a/src/eepp/window/backend/SDL3/inputsdl3.cpp b/src/eepp/window/backend/SDL3/inputsdl3.cpp index 6662c6027..6bfd95df3 100644 --- a/src/eepp/window/backend/SDL3/inputsdl3.cpp +++ b/src/eepp/window/backend/SDL3/inputsdl3.cpp @@ -251,6 +251,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { case SDL_EVENT_KEY_DOWN: { event.Type = InputEvent::KeyDown; event.key.state = SDLEvent.key.down ? 1 : 0; + event.key.repeat = SDLEvent.key.repeat ? 1 : 0; event.key.which = SDLEvent.key.windowID; event.key.keysym.sym = static_cast( SDLEvent.key.key ); event.key.keysym.scancode = static_cast( SDLEvent.key.scancode ); @@ -262,6 +263,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { case SDL_EVENT_KEY_UP: { event.Type = InputEvent::KeyUp; event.key.state = SDLEvent.key.down ? 1 : 0; + event.key.repeat = 0; event.key.which = SDLEvent.key.windowID; event.key.keysym.sym = static_cast( SDLEvent.key.key ); event.key.keysym.scancode = static_cast( SDLEvent.key.scancode ); @@ -383,11 +385,13 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { break; } case SDL_EVENT_JOYSTICK_ADDED: { - static_cast( mJoystickManager )->addJoystick( SDLEvent.jdevice.which ); + static_cast( mJoystickManager ) + ->addJoystick( SDLEvent.jdevice.which ); break; } case SDL_EVENT_JOYSTICK_REMOVED: { - static_cast( mJoystickManager )->removeJoystick( SDLEvent.jdevice.which ); + static_cast( mJoystickManager ) + ->removeJoystick( SDLEvent.jdevice.which ); break; } case SDL_EVENT_QUIT: { diff --git a/src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp b/src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp new file mode 100644 index 000000000..50ea9091f --- /dev/null +++ b/src/modules/eterm/include/eterm/terminal/kittykeyboardprotocol.hpp @@ -0,0 +1,67 @@ +#ifndef ETERM_KITTYKEYBOARDPROTOCOL_HPP +#define ETERM_KITTYKEYBOARDPROTOCOL_HPP + +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Window; + +namespace eterm { namespace Terminal { + +enum class KittyKeyboardFlag : Uint32 { + DisambiguateEscapeCodes = 1u, + ReportEventTypes = 2u, + ReportAlternateKeys = 4u, + ReportAllKeysAsEscapeCodes = 8u, + ReportAssociatedText = 16u, +}; + +constexpr Uint32 kittyKeyboardFlag( KittyKeyboardFlag flag ) { + return static_cast( flag ); +} + +constexpr Uint32 KITTY_KEYBOARD_SUPPORTED_FLAGS = 31u; + +enum class KittyKeyEventType : Uint8 { Press = 1, Repeat = 2, Release = 3 }; + +struct KittyKeyboardState { + static constexpr size_t MaxStackDepth = 64; + + Uint32 flags{ 0 }; + SmallVector stack; + + void reset(); + void push( Uint32 requestedFlags ); + void pop( size_t count = 1 ); + void set( Uint32 requestedFlags, Uint32 mode = 1 ); +}; + +struct KittyKeyEvent { + Keycode keycode{ KEY_UNKNOWN }; + Scancode scancode{ SCANCODE_UNKNOWN }; + Uint32 character{ 0 }; + Uint32 modifiers{ 0 }; + KittyKeyEventType type{ KittyKeyEventType::Press }; +}; + +struct KittyEncodedKey { + std::string bytes; + Uint32 expectedText{ 0 }; + bool handled{ false }; +}; + +class KittyKeyboardEncoder { + public: + static KittyEncodedKey encode( const KittyKeyEvent& event, Uint32 activeFlags ); + + static std::string encodeText( Uint32 codepoint, Uint32 activeFlags ); + + static Uint32 encodeModifiers( Uint32 eeppModifiers ); +}; + +}} // namespace eterm::Terminal + +#endif diff --git a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp index 12116a1b7..88526de0b 100644 --- a/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminaldisplay.hpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -198,7 +199,14 @@ class TerminalDisplay { void onTextEditing( const String& text, const Int32& start, const Int32& length ); void onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod, - const Scancode& scancode ); + const Scancode& scancode, bool repeat = false ); + + void onKeyUp( const Keycode& keyCode, const Uint32& chr, const Uint32& mod, + const Scancode& scancode ); + + void suppressKeyUp( const Scancode& scancode ); + + void clearSuppressedKeys(); bool isRegisteredShortcut( const Keycode& keyCode, const Uint32& mod ) const; @@ -334,6 +342,7 @@ class TerminalDisplay { Vector2f mPosition; Sizef mSize; std::vector mDirtyLines; + std::bitset mSuppressedKeyUps; bool mDirty{ true }; bool mDirtyCursor{ true }; bool mDrawing{ false }; diff --git a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp index 24de7e7c6..676db2590 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalemulator.hpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -245,6 +246,12 @@ class TerminalEmulator final { void ttywrite( const char* s, size_t n, int may_echo ); + void keyEvent( const KittyKeyEvent& event ); + + void textInput( Uint32 codepoint ); + + void clearPendingKeyboardInput(); + int tisaltscr(); int scrollSize() const; @@ -322,6 +329,11 @@ class TerminalEmulator final { CSIEscape mCsiescseq; STREscape mStrescseq; KittyGraphicsProtocol mKittyGraphics; + KittyKeyboardState mPrimaryKeyboardState; + KittyKeyboardState mAlternateKeyboardState; + KittyKeyEvent mPendingTextKey; + Uint32 mExpectedTextInput{ 0 }; + bool mHasPendingTextKey{ false }; uint32_t mDefaultFg; uint32_t mDefaultBg; @@ -361,6 +373,9 @@ class TerminalEmulator final { void csidump(); void csihandle(); + bool handleKittyKeyboardProtocol(); + KittyKeyboardState& activeKeyboardState(); + void resetKittyKeyboardProtocol(); void csiparse(); void csireset(); diff --git a/src/modules/eterm/include/eterm/terminal/terminalsession.hpp b/src/modules/eterm/include/eterm/terminal/terminalsession.hpp index 37b27d7cd..a1efd0786 100644 --- a/src/modules/eterm/include/eterm/terminal/terminalsession.hpp +++ b/src/modules/eterm/include/eterm/terminal/terminalsession.hpp @@ -119,6 +119,8 @@ class TerminalSession final : public std::enable_shared_from_this; diff --git a/src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp b/src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp new file mode 100644 index 000000000..e45666401 --- /dev/null +++ b/src/modules/eterm/src/eterm/terminal/kittykeyboardprotocol.cpp @@ -0,0 +1,321 @@ +#include +#include +#include + +namespace eterm { namespace Terminal { + +static bool validCodepoint( Uint32 codepoint ) { + return codepoint <= 0x10FFFF && !( codepoint >= 0xD800 && codepoint <= 0xDFFF ); +} + +void KittyKeyboardState::reset() { + flags = 0; + stack.clear(); +} + +void KittyKeyboardState::push( Uint32 requestedFlags ) { + if ( stack.size() == MaxStackDepth ) + stack.erase( stack.begin() ); + stack.push_back( flags ); + flags = requestedFlags & KITTY_KEYBOARD_SUPPORTED_FLAGS; +} + +void KittyKeyboardState::pop( size_t count ) { + if ( count == 0 ) + count = 1; + count = std::min( count, MaxStackDepth ); + while ( count-- ) { + if ( stack.empty() ) { + flags = 0; + return; + } + flags = stack.back(); + stack.pop_back(); + } +} + +void KittyKeyboardState::set( Uint32 requestedFlags, Uint32 mode ) { + const Uint32 supported = requestedFlags & KITTY_KEYBOARD_SUPPORTED_FLAGS; + switch ( mode ) { + case 1: + flags = supported; + break; + case 2: + flags |= supported; + break; + case 3: + flags &= ~supported; + break; + } +} + +Uint32 KittyKeyboardEncoder::encodeModifiers( Uint32 modifiers ) { + Uint32 result = 0; + if ( modifiers & KEYMOD_SHIFT ) + result |= 1; + if ( modifiers & KEYMOD_ALT ) + result |= 2; + if ( modifiers & KEYMOD_CTRL ) + result |= 4; + // EEPP's META is the platform GUI/Command key and maps to Kitty Super. + if ( modifiers & KEYMOD_META ) + result |= 8; + if ( modifiers & KEYMOD_CAPS ) + result |= 64; + if ( modifiers & KEYMOD_NUM ) + result |= 128; + return result + 1; +} + +enum class LegacyShape : Uint8 { CsiU, Tilde, Letter }; + +struct MappedKey { + Uint32 code{ 0 }; + LegacyShape shape{ LegacyShape::CsiU }; + char suffix{ 0 }; + bool resetKey{ false }; + bool printable{ false }; +}; + +static MappedKey mapKey( const KittyKeyEvent& event ) { + switch ( event.scancode ) { + case SCANCODE_ESCAPE: + return { 27, LegacyShape::CsiU, 0, false }; + case SCANCODE_RETURN: + case SCANCODE_RETURN2: + return { 13, LegacyShape::CsiU, 0, true }; + case SCANCODE_TAB: + case SCANCODE_KP_TAB: + return { 9, LegacyShape::CsiU, 0, true }; + case SCANCODE_BACKSPACE: + case SCANCODE_KP_BACKSPACE: + return { 127, LegacyShape::CsiU, 0, true }; + case SCANCODE_INSERT: + return { 2, LegacyShape::Tilde }; + case SCANCODE_DELETE: + return { 3, LegacyShape::Tilde }; + case SCANCODE_PAGEUP: + return { 5, LegacyShape::Tilde }; + case SCANCODE_PAGEDOWN: + return { 6, LegacyShape::Tilde }; + case SCANCODE_UP: + return { 1, LegacyShape::Letter, 'A' }; + case SCANCODE_DOWN: + return { 1, LegacyShape::Letter, 'B' }; + case SCANCODE_RIGHT: + return { 1, LegacyShape::Letter, 'C' }; + case SCANCODE_LEFT: + return { 1, LegacyShape::Letter, 'D' }; + case SCANCODE_END: + return { 1, LegacyShape::Letter, 'F' }; + case SCANCODE_HOME: + return { 1, LegacyShape::Letter, 'H' }; + case SCANCODE_F1: + return { 1, LegacyShape::Letter, 'P' }; + case SCANCODE_F2: + return { 1, LegacyShape::Letter, 'Q' }; + case SCANCODE_F3: + // CSI R is a cursor-position report and was removed as an allowed F3 encoding. + return { 13, LegacyShape::Tilde }; + case SCANCODE_F4: + return { 1, LegacyShape::Letter, 'S' }; + case SCANCODE_F5: + return { 15, LegacyShape::Tilde }; + case SCANCODE_F6: + return { 17, LegacyShape::Tilde }; + case SCANCODE_F7: + return { 18, LegacyShape::Tilde }; + case SCANCODE_F8: + return { 19, LegacyShape::Tilde }; + case SCANCODE_F9: + return { 20, LegacyShape::Tilde }; + case SCANCODE_F10: + return { 21, LegacyShape::Tilde }; + case SCANCODE_F11: + return { 23, LegacyShape::Tilde }; + case SCANCODE_F12: + return { 24, LegacyShape::Tilde }; + case SCANCODE_CAPSLOCK: + return { 57358 }; + case SCANCODE_SCROLLLOCK: + return { 57359 }; + case SCANCODE_NUMLOCKCLEAR: + return { 57360 }; + case SCANCODE_PRINTSCREEN: + return { 57361 }; + case SCANCODE_PAUSE: + return { 57362 }; + case SCANCODE_APPLICATION: + case SCANCODE_MENU: + return { 57363 }; + case SCANCODE_KP_0: + return { 57399 }; + case SCANCODE_KP_1: + case SCANCODE_KP_2: + case SCANCODE_KP_3: + case SCANCODE_KP_4: + case SCANCODE_KP_5: + case SCANCODE_KP_6: + case SCANCODE_KP_7: + case SCANCODE_KP_8: + case SCANCODE_KP_9: + return { 57400u + static_cast( event.scancode - SCANCODE_KP_1 ) }; + case SCANCODE_KP_PERIOD: + case SCANCODE_KP_DECIMAL: + return { 57409 }; + case SCANCODE_KP_DIVIDE: + return { 57410 }; + case SCANCODE_KP_MULTIPLY: + return { 57411 }; + case SCANCODE_KP_MINUS: + return { 57412 }; + case SCANCODE_KP_PLUS: + return { 57413 }; + case SCANCODE_KP_ENTER: + return { 57414 }; + case SCANCODE_KP_EQUALS: + case SCANCODE_KP_EQUALSAS400: + return { 57415 }; + case SCANCODE_KP_COMMA: + case SCANCODE_SEPARATOR: + return { 57416 }; + case SCANCODE_LSHIFT: + return { 57441 }; + case SCANCODE_LCTRL: + return { 57442 }; + case SCANCODE_LALT: + return { 57443 }; + case SCANCODE_LGUI: + return { 57444 }; + case SCANCODE_RSHIFT: + return { 57447 }; + case SCANCODE_RCTRL: + return { 57448 }; + case SCANCODE_RALT: + return { 57449 }; + case SCANCODE_RGUI: + return { 57450 }; + default: + if ( event.scancode >= SCANCODE_F13 && event.scancode <= SCANCODE_F24 ) + return { 57376u + static_cast( event.scancode - SCANCODE_F13 ) }; + break; + } + const auto keycode = static_cast( event.keycode ); + if ( keycode >= 32 && keycode <= 126 ) { + const Uint32 unshifted = keycode >= 'A' && keycode <= 'Z' ? keycode + 32 : keycode; + return { unshifted, LegacyShape::CsiU, 0, false, true }; + } + if ( validCodepoint( event.character ) && event.character != 0 ) + return { event.character, LegacyShape::CsiU, 0, false, event.character >= 32 }; + return {}; +} + +static Uint32 baseLayoutCodepoint( Scancode scancode ) { + if ( scancode >= SCANCODE_A && scancode <= SCANCODE_Z ) + return 'a' + static_cast( scancode - SCANCODE_A ); + if ( scancode >= SCANCODE_1 && scancode <= SCANCODE_9 ) + return '1' + static_cast( scancode - SCANCODE_1 ); + if ( scancode == SCANCODE_0 ) + return '0'; + static constexpr struct { + Scancode scancode; + Uint32 codepoint; + } punctuation[] = { + { SCANCODE_SPACE, ' ' }, { SCANCODE_MINUS, '-' }, { SCANCODE_EQUALS, '=' }, + { SCANCODE_LEFTBRACKET, '[' }, { SCANCODE_RIGHTBRACKET, ']' }, { SCANCODE_BACKSLASH, '\\' }, + { SCANCODE_SEMICOLON, ';' }, { SCANCODE_APOSTROPHE, '\'' }, { SCANCODE_GRAVE, '`' }, + { SCANCODE_COMMA, ',' }, { SCANCODE_PERIOD, '.' }, { SCANCODE_SLASH, '/' } }; + for ( const auto& entry : punctuation ) { + if ( entry.scancode == scancode ) + return entry.codepoint; + } + return 0; +} + +static std::string serialize( const MappedKey& key, Uint32 modifiers, KittyKeyEventType type, + bool reportEvents, bool reportAlternate, bool associatedText, + Uint32 text, Scancode scancode ) { + char buf[96]; + char keyField[48]; + const Uint32 rawBaseLayout = reportAlternate ? baseLayoutCodepoint( scancode ) : 0; + const Uint32 baseLayout = rawBaseLayout != key.code ? rawBaseLayout : 0; + const bool shiftPressed = ( ( modifiers - 1 ) & 1u ) != 0; + const Uint32 shifted = + reportAlternate && shiftPressed && validCodepoint( text ) && text != key.code ? text : 0; + if ( shifted && baseLayout ) + std::snprintf( keyField, sizeof( keyField ), "%u:%u:%u", key.code, shifted, baseLayout ); + else if ( shifted ) + std::snprintf( keyField, sizeof( keyField ), "%u:%u", key.code, shifted ); + else if ( baseLayout ) + std::snprintf( keyField, sizeof( keyField ), "%u::%u", key.code, baseLayout ); + else + std::snprintf( keyField, sizeof( keyField ), "%u", key.code ); + const char suffix = key.shape == LegacyShape::Tilde ? '~' + : key.shape == LegacyShape::Letter ? key.suffix + : 'u'; + int len; + if ( associatedText && text >= 32 && !( text >= 127 && text <= 159 ) ) { + len = reportEvents ? std::snprintf( buf, sizeof( buf ), "\033[%s;%u:%u;%uu", keyField, + modifiers, static_cast( type ), text ) + : std::snprintf( buf, sizeof( buf ), "\033[%s;%u;%uu", keyField, + modifiers, text ); + } else if ( reportEvents ) { + len = std::snprintf( buf, sizeof( buf ), "\033[%s;%u:%u%c", keyField, modifiers, + static_cast( type ), suffix ); + } else { + len = std::snprintf( buf, sizeof( buf ), "\033[%s;%u%c", keyField, modifiers, suffix ); + } + return len > 0 ? std::string( buf, static_cast( len ) ) : std::string{}; +} + +KittyEncodedKey KittyKeyboardEncoder::encode( const KittyKeyEvent& event, Uint32 flags ) { + KittyEncodedKey result; + if ( flags == 0 ) + return result; + const bool reportAll = + flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportAllKeysAsEscapeCodes ); + const bool disambiguate = + flags & kittyKeyboardFlag( KittyKeyboardFlag::DisambiguateEscapeCodes ); + const bool reportEvents = flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportEventTypes ); + if ( event.type != KittyKeyEventType::Press && !reportEvents ) + return result; + const MappedKey key = mapKey( event ); + if ( key.code == 0 || ( key.resetKey && !reportAll ) ) + return result; + const Uint32 modifiers = encodeModifiers( event.modifiers ); + // Shift-only printable keys still use committed text under disambiguation. Encoding them here + // would duplicate the subsequent text-input event. Right Alt is AltGr in EEPP and is likewise a + // text-producing layout selector, not a shortcut modifier. Some platforms also expose a + // synthetic Ctrl while AltGr is held, so Ctrl only disambiguates when AltGr is absent. + const bool altGr = event.modifiers & KEYMOD_RALT; + const bool disambiguatingModifier = ( event.modifiers & ( KEYMOD_LALT | KEYMOD_META ) ) != 0 || + ( !altGr && ( event.modifiers & KEYMOD_CTRL ) != 0 ); + if ( key.printable && !reportAll && !( disambiguate && disambiguatingModifier ) ) + return result; + const bool mustEncode = reportAll || event.type != KittyKeyEventType::Press || + ( disambiguate && ( disambiguatingModifier || key.code == 27 ) ); + if ( !mustEncode ) + return result; + const bool associated = + reportAll && ( flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportAssociatedText ) ) && + validCodepoint( event.character ); + result.bytes = serialize( key, modifiers, event.type, reportEvents, + flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportAlternateKeys ), + associated, event.character, event.scancode ); + result.expectedText = reportAll && event.character >= 32 ? event.character : 0; + result.handled = !result.bytes.empty(); + return result; +} + +std::string KittyKeyboardEncoder::encodeText( Uint32 codepoint, Uint32 flags ) { + if ( !( flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportAllKeysAsEscapeCodes ) ) || + !validCodepoint( codepoint ) ) + return {}; + const bool associated = flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportAssociatedText ); + MappedKey key{ associated ? 0 : codepoint }; + return serialize( key, 1, KittyKeyEventType::Press, + flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportEventTypes ), false, + associated, codepoint, SCANCODE_UNKNOWN ); +} + +}} // namespace eterm::Terminal diff --git a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp index 0fffc3ae1..26f93222f 100644 --- a/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminaldisplay.cpp @@ -20,13 +20,6 @@ namespace eterm { namespace Terminal { #define DIV( n, d ) ( ( ( n ) + ( d ) / 2.0f ) / ( d ) ) #define DIVI( n, d ) ( ( ( n ) + ( d ) / 2 ) / ( d ) ) -static const Scancode asciiScancodeTable[] = { - SCANCODE_A, SCANCODE_B, SCANCODE_C, SCANCODE_D, SCANCODE_E, SCANCODE_F, - SCANCODE_G, SCANCODE_H, SCANCODE_I, SCANCODE_J, SCANCODE_K, SCANCODE_L, - SCANCODE_M, SCANCODE_N, SCANCODE_O, SCANCODE_P, SCANCODE_Q, SCANCODE_R, - SCANCODE_S, SCANCODE_T, SCANCODE_U, SCANCODE_V, SCANCODE_W, SCANCODE_X, - SCANCODE_Y, SCANCODE_Z, SCANCODE_LEFTBRACKET, SCANCODE_SLASH, SCANCODE_RIGHTBRACKET }; - static Uint32 sanitizeMod( const Uint32& mod ) { Uint32 smod = 0; if ( mod & KEYMOD_CTRL ) @@ -1935,10 +1928,7 @@ void TerminalDisplay::onProcessExit( int exitCode ) { void TerminalDisplay::onTextInput( const Uint32& chr ) { if ( !mSession ) return; - String input; - input.push_back( chr ); - std::string utf8Input( input.toUtf8() ); - mSession->write( std::move( utf8Input ) ); + mSession->textInput( chr ); mDirty = true; } @@ -1973,8 +1963,8 @@ bool TerminalDisplay::isRegisteredShortcut( const Keycode& keyCode, const Uint32 return false; } -void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/, const Uint32& mod, - const Scancode& scancode ) { +void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& chr, const Uint32& mod, + const Scancode& scancode, bool repeat ) { if ( mWindow->getIME().isEditing() ) return; Uint32 smod = sanitizeMod( mod ); @@ -1993,6 +1983,7 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/, continue; if ( !k.altscrn || ( k.altscrn == ( isAltScr() ? 1 : -1 ) ) ) { + suppressKeyUp( scancode ); action( k.action ); return; } @@ -2000,67 +1991,28 @@ void TerminalDisplay::onKeyDown( const Keycode& keyCode, const Uint32& /*chr*/, } } - if ( mod & KEYMOD_CTRL ) { - // I really dont like this, as it depends on the underlying backend implementation (SDL in - // this case) - if ( ( scancode >= SCANCODE_A && scancode <= SCANCODE_0 ) || - SCANCODE_LEFTBRACKET == scancode || SCANCODE_RIGHTBRACKET == scancode ) { - char tmp = 0; - for ( size_t i = 0; i < eeARRAY_SIZE( asciiScancodeTable ); ++i ) { - if ( asciiScancodeTable[i] == scancode ) { - tmp = i + 1; - break; - } - } + mSession->keyEvent( { keyCode, scancode, chr, mod, + repeat ? KittyKeyEventType::Repeat : KittyKeyEventType::Press } ); +} - mSession->write( std::string( 1, tmp ) ); - return; - } +void TerminalDisplay::onKeyUp( const Keycode& keyCode, const Uint32& chr, const Uint32& mod, + const Scancode& scancode ) { + if ( scancode >= 0 && static_cast( scancode ) < mSuppressedKeyUps.size() && + mSuppressedKeyUps.test( static_cast( scancode ) ) ) { + mSuppressedKeyUps.reset( static_cast( scancode ) ); + return; } + if ( mSession ) + mSession->keyEvent( { keyCode, scancode, chr, mod, KittyKeyEventType::Release } ); +} - auto kvIt = terminalKeyMap.KeyMap().find( keyCode ); - if ( kvIt != terminalKeyMap.KeyMap().end() ) { - for ( auto& k : kvIt->second ) { - if ( k.mask == KEYMOD_CTRL_SHIFT_ALT_META || k.mask == smod ) { - if ( IS_SET( MODE_APPKEYPAD ) ? k.appkey < 0 : k.appkey > 0 ) - continue; +void TerminalDisplay::suppressKeyUp( const Scancode& scancode ) { + if ( scancode >= 0 && static_cast( scancode ) < mSuppressedKeyUps.size() ) + mSuppressedKeyUps.set( static_cast( scancode ) ); +} - if ( IS_SET( MODE_NUMLOCK ) && k.appkey == 2 ) - continue; - - if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 ) - continue; - - if ( k.string.size() > 0 ) { - mSession->write( k.string ); - return; - } - break; - } - } - } - - auto pkmIt = terminalKeyMap.PlatformKeyMap().find( scancode ); - if ( pkmIt != terminalKeyMap.PlatformKeyMap().end() ) { - for ( auto& k : pkmIt->second ) { - if ( k.mask == KEYMOD_CTRL_SHIFT_ALT_META || k.mask == smod ) { - if ( IS_SET( MODE_APPKEYPAD ) ? k.appkey < 0 : k.appkey > 0 ) - continue; - - if ( IS_SET( MODE_NUMLOCK ) && k.appkey == 2 ) - continue; - - if ( IS_SET( MODE_APPCURSOR ) ? k.appcursor < 0 : k.appcursor > 0 ) - continue; - - if ( k.string.size() > 0 ) { - mSession->write( k.string ); - return; - } - break; - } - } - } +void TerminalDisplay::clearSuppressedKeys() { + mSuppressedKeyUps.reset(); } Font* TerminalDisplay::getFont() const { diff --git a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp index c9c249b3a..7025a81f5 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalemulator.cpp @@ -34,6 +34,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #include +#include #include #include @@ -1051,6 +1052,126 @@ void TerminalEmulator::ttywrite( const char* s, size_t n, int may_echo ) { } } +static Uint32 keyboardSanitizeMod( Uint32 mod ) { + return mod & KEYMOD_CTRL_SHIFT_ALT_META; +} + +static char legacyControlCharacter( Scancode scancode ) { + if ( scancode >= SCANCODE_A && scancode <= SCANCODE_Z ) + return static_cast( scancode - SCANCODE_A + 1 ); + if ( scancode == SCANCODE_LEFTBRACKET ) + return 27; + if ( scancode == SCANCODE_SLASH ) + return 28; + if ( scancode == SCANCODE_RIGHTBRACKET ) + return 29; + return 0; +} + +void TerminalEmulator::keyEvent( const KittyKeyEvent& event ) { + const Uint32 flags = activeKeyboardState().flags; + const Uint32 keycode = static_cast( event.keycode ); + const bool reportAll = + flags & kittyKeyboardFlag( KittyKeyboardFlag::ReportAllKeysAsEscapeCodes ); + const bool altGr = event.modifiers & KEYMOD_RALT; + const bool textProducingModifiers = ( event.modifiers & ( KEYMOD_LALT | KEYMOD_META ) ) == 0 && + ( altGr || ( event.modifiers & KEYMOD_CTRL ) == 0 ); + // AltGr keydown can already carry the layout-produced character, but SDL still follows it with + // the authoritative text-input event. Always defer AltGr here so it is normalized as composed + // text instead of being emitted immediately as an Alt shortcut. + if ( reportAll && event.type != KittyKeyEventType::Release && + ( event.character == 0 || altGr ) && keycode >= 32 && keycode <= 126 && + textProducingModifiers ) { + mPendingTextKey = event; + mHasPendingTextKey = true; + return; + } + const auto enhanced = KittyKeyboardEncoder::encode( event, flags ); + if ( enhanced.handled ) { + ttywrite( enhanced.bytes.data(), enhanced.bytes.size(), 1 ); + mExpectedTextInput = enhanced.expectedText; + return; + } + if ( event.type == KittyKeyEventType::Release ) + return; + + if ( event.modifiers & KEYMOD_CTRL ) { + const char control = legacyControlCharacter( event.scancode ); + if ( control ) { + ttywrite( &control, 1, 1 ); + return; + } + } + + const Uint32 modifiers = keyboardSanitizeMod( event.modifiers ); + auto writeMapped = [this, modifiers]( const auto& entries ) { + for ( const auto& entry : entries ) { + if ( entry.mask != KEYMOD_CTRL_SHIFT_ALT_META && entry.mask != modifiers ) + continue; + if ( IS_SET( MODE_APPKEYPAD ) ? entry.appkey < 0 : entry.appkey > 0 ) + continue; + if ( IS_SET( MODE_NUMLOCK ) && entry.appkey == 2 ) + continue; + if ( IS_SET( MODE_APPCURSOR ) ? entry.appcursor < 0 : entry.appcursor > 0 ) + continue; + if ( !entry.string.empty() ) { + ttywrite( entry.string.data(), entry.string.size(), 1 ); + return true; + } + break; + } + return false; + }; + + const auto key = terminalKeyMap.KeyMap().find( event.keycode ); + if ( key != terminalKeyMap.KeyMap().end() && writeMapped( key->second ) ) + return; + const auto platform = terminalKeyMap.PlatformKeyMap().find( event.scancode ); + if ( platform != terminalKeyMap.PlatformKeyMap().end() ) + writeMapped( platform->second ); +} + +void TerminalEmulator::textInput( Uint32 codepoint ) { + if ( mHasPendingTextKey ) { + if ( mPendingTextKey.modifiers & KEYMOD_RALT ) { + // SDL's text event is the authoritative result of the AltGr layout level. Report that + // result as text, without turning the consumed AltGr (or its platform-synthetic Ctrl) + // into an application shortcut. Keep the physical scancode for base-layout reporting. + mPendingTextKey.keycode = static_cast( codepoint ); + mPendingTextKey.modifiers &= ~( KEYMOD_RALT | KEYMOD_CTRL | KEYMOD_SHIFT ); + } + mPendingTextKey.character = codepoint; + const auto encoded = + KittyKeyboardEncoder::encode( mPendingTextKey, activeKeyboardState().flags ); + mHasPendingTextKey = false; + if ( encoded.handled ) { + ttywrite( encoded.bytes.data(), encoded.bytes.size(), 1 ); + return; + } + } + if ( mExpectedTextInput ) { + const bool matches = mExpectedTextInput == codepoint; + mExpectedTextInput = 0; + if ( matches ) + return; + } + const std::string enhanced = + KittyKeyboardEncoder::encodeText( codepoint, activeKeyboardState().flags ); + if ( !enhanced.empty() ) { + ttywrite( enhanced.data(), enhanced.size(), 1 ); + return; + } + String input; + input.push_back( codepoint ); + const std::string utf8 = input.toUtf8(); + ttywrite( utf8.data(), utf8.size(), 1 ); +} + +void TerminalEmulator::clearPendingKeyboardInput() { + mExpectedTextInput = 0; + mHasPendingTextKey = false; +} + void TerminalEmulator::ttywriteraw( const char* s, size_t n ) { if ( mPty->write( s, n ) < (int)n ) { _die( "Failed to write to TTY" ); @@ -1124,6 +1245,7 @@ void TerminalEmulator::tcursor( int mode ) { void TerminalEmulator::treset( void ) { uint i; + resetKittyKeyboardProtocol(); mColorSchemeNotifications = false; mTerm.is_syncing = false; mTerm.c = TerminalCursor{}; @@ -1186,6 +1308,42 @@ void TerminalEmulator::tswapscreen( void ) { tfulldirt(); } +KittyKeyboardState& TerminalEmulator::activeKeyboardState() { + return tisaltscr() ? mAlternateKeyboardState : mPrimaryKeyboardState; +} + +void TerminalEmulator::resetKittyKeyboardProtocol() { + mPrimaryKeyboardState.reset(); + mAlternateKeyboardState.reset(); + clearPendingKeyboardInput(); +} + +bool TerminalEmulator::handleKittyKeyboardProtocol() { + if ( mCsiescseq.mode[0] != 'u' || ( mCsiescseq.priv != '?' && mCsiescseq.priv != '>' && + mCsiescseq.priv != '<' && mCsiescseq.priv != '=' ) ) + return false; + + auto& state = activeKeyboardState(); + const bool omitted = mCsiescseq.buf[1] == 'u'; + if ( mCsiescseq.priv == '?' ) { + if ( omitted ) { + char response[24]; + const int len = snprintf( response, sizeof( response ), "\033[?%uu", state.flags ); + ttywrite( response, static_cast( len ), 0 ); + } + } else if ( mCsiescseq.priv == '>' ) { + if ( mCsiescseq.narg == 1 && mCsiescseq.arg[0] >= 0 ) + state.push( static_cast( mCsiescseq.arg[0] ) ); + } else if ( mCsiescseq.priv == '<' ) { + if ( mCsiescseq.narg == 1 && mCsiescseq.arg[0] >= 0 ) + state.pop( omitted ? 1 : static_cast( mCsiescseq.arg[0] ) ); + } else if ( mCsiescseq.priv == '=' && mCsiescseq.narg <= 2 && mCsiescseq.arg[0] >= 0 ) { + const Uint32 mode = mCsiescseq.narg == 1 ? 1 : static_cast( mCsiescseq.arg[1] ); + state.set( static_cast( mCsiescseq.arg[0] ), mode ); + } + return true; +} + void TerminalEmulator::tscrolldown( int top, int n ) { int i; Line temp; @@ -2168,6 +2326,8 @@ void TerminalEmulator::csihandle( void ) { int len; std::shared_ptr dpy{}; + if ( handleKittyKeyboardProtocol() ) + return; switch ( mCsiescseq.mode[0] ) { default: @@ -2367,12 +2527,7 @@ void TerminalEmulator::csihandle( void ) { tcursor( CURSOR_SAVE ); break; case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ - 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 ) { + if ( mCsiescseq.priv ) { goto unknown; } else { tcursor( CURSOR_LOAD ); @@ -3912,6 +4067,7 @@ void TerminalEmulator::mousereport( const TerminalMouseEventType& type, void TerminalEmulator::setPtyAndProcess( PtyPtr&& pty, ProcPtr&& process ) { mKittyGraphics.reset(); + resetKittyKeyboardProtocol(); mKittyPlaceholderMetadata.clear(); mKittyPlaceholderCell = Vector2i( -1, -1 ); mBuflen = 0; diff --git a/src/modules/eterm/src/eterm/terminal/terminalsession.cpp b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp index ae6132e05..f68079a53 100644 --- a/src/modules/eterm/src/eterm/terminal/terminalsession.cpp +++ b/src/modules/eterm/src/eterm/terminal/terminalsession.cpp @@ -317,6 +317,14 @@ void TerminalSession::writeRaw( std::string data ) { enqueue( WriteRawCommand{ std::move( data ) } ); } +void TerminalSession::keyEvent( KittyKeyEvent event ) { + enqueue( KeyCommand{ std::move( event ) } ); +} + +void TerminalSession::textInput( Uint32 codepoint ) { + enqueue( TextInputCommand{ codepoint } ); +} + void TerminalSession::resize( int columns, int rows ) { resize( columns, rows, 0, 0 ); } @@ -511,6 +519,10 @@ void TerminalSession::processCommand( Command&& command ) { mEmulator->ttywrite( value.data.data(), value.data.size(), value.mayEcho ); } else if constexpr ( std::is_same_v ) { mEmulator->write( value.data.data(), value.data.size() ); + } else if constexpr ( std::is_same_v ) { + mEmulator->keyEvent( value.event ); + } else if constexpr ( std::is_same_v ) { + mEmulator->textInput( value.codepoint ); } else if constexpr ( std::is_same_v ) { mEmulator->resize( value.columns, value.rows, value.pixelWidth, value.pixelHeight ); } else if constexpr ( std::is_same_v ) { @@ -537,6 +549,8 @@ void TerminalSession::processCommand( Command&& command ) { mEmulator->mousereport( value.type, value.cellPosition, value.pixelPosition, value.flags, value.modifiers ); } else if constexpr ( std::is_same_v ) { + if ( !value.value ) + mEmulator->clearPendingKeyboardInput(); if ( mWorkerDisplay->getMode( MODE_FOCUS ) ) mEmulator->ttywrite( value.value ? "\033[I" : "\033[O", 3, false ); mWorkerDisplay->setFocused( value.value ); diff --git a/src/modules/eterm/src/eterm/ui/uiterminal.cpp b/src/modules/eterm/src/eterm/ui/uiterminal.cpp index 4d753b370..75858aefd 100644 --- a/src/modules/eterm/src/eterm/ui/uiterminal.cpp +++ b/src/modules/eterm/src/eterm/ui/uiterminal.cpp @@ -498,16 +498,20 @@ Uint32 UITerminal::onKeyDown( const KeyEvent& event ) { std::string cmd = mKeyBindings.getCommandFromKeyBind( { event.getKeyCode(), event.getMod() } ); if ( !cmd.empty() && ( !mExclusiveMode || cmd == getExclusiveModeToggleCommandName() ) ) { + mTerm->suppressKeyUp( event.getScancode() ); execute( cmd ); return 1; } } - mTerm->onKeyDown( event.getKeyCode(), event.getChar(), event.getMod(), event.getScancode() ); + mTerm->onKeyDown( event.getKeyCode(), event.getChar(), event.getMod(), event.getScancode(), + event.isRepeat() ); return 1; } -Uint32 UITerminal::onKeyUp( const KeyEvent& ) { +Uint32 UITerminal::onKeyUp( const KeyEvent& event ) { + if ( mTerm ) + mTerm->onKeyUp( event.getKeyCode(), event.getChar(), event.getMod(), event.getScancode() ); return 1; } @@ -575,6 +579,7 @@ Uint32 UITerminal::onFocus( NodeFocusReason reason ) { Uint32 UITerminal::onFocusLoss() { getUISceneNode()->getWindow()->stopTextInput(); + mTerm->clearSuppressedKeys(); mTerm->setFocus( false ); invalidateDraw(); return UIWidget::onFocusLoss(); diff --git a/src/tests/unit_tests/eterm_tests.cpp b/src/tests/unit_tests/eterm_tests.cpp index f9924522b..b8d975730 100644 --- a/src/tests/unit_tests/eterm_tests.cpp +++ b/src/tests/unit_tests/eterm_tests.cpp @@ -908,9 +908,88 @@ UTEST( eterm_session, kitty_graphics_update_and_metadata_cross_worker_boundary ) EXPECT_STDSTREQ( "\033_Gi=13;OK\033\\", ptyPtr->mWrites ); } -UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) { +UTEST( eterm, kitty_keyboard_state_stack_and_modes ) { + KittyKeyboardState state; + state.push( 1 ); + EXPECT_EQ( static_cast( 1 ), state.flags ); + state.push( 7 ); + EXPECT_EQ( static_cast( 7 ), state.flags ); + state.pop(); + EXPECT_EQ( static_cast( 1 ), state.flags ); + state.set( 8, 2 ); + EXPECT_EQ( static_cast( 9 ), state.flags ); + state.set( 1, 3 ); + EXPECT_EQ( static_cast( 8 ), state.flags ); + state.set( 8, 1 ); + EXPECT_EQ( static_cast( 8 ), state.flags ); + state.set( 1, 99 ); + EXPECT_EQ( static_cast( 8 ), state.flags ); + state.pop( 1000000 ); + EXPECT_EQ( static_cast( 0 ), state.flags ); + EXPECT_TRUE( state.stack.empty() ); +} + +UTEST( eterm, kitty_keyboard_encoder_modifiers_and_enter ) { + EXPECT_EQ( static_cast( 1 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_NONE ) ); + EXPECT_EQ( static_cast( 2 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_SHIFT ) ); + EXPECT_EQ( static_cast( 3 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_ALT ) ); + EXPECT_EQ( static_cast( 5 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_CTRL ) ); + EXPECT_EQ( static_cast( 6 ), + KittyKeyboardEncoder::encodeModifiers( KEYMOD_CTRL | KEYMOD_SHIFT ) ); + + KittyKeyEvent enter{ KEY_RETURN, SCANCODE_RETURN, '\r', KEYMOD_CTRL, KittyKeyEventType::Press }; + EXPECT_FALSE( KittyKeyboardEncoder::encode( enter, 1 ).handled ); + EXPECT_STDSTREQ( "\033[13;5u", KittyKeyboardEncoder::encode( enter, 8 ).bytes ); + EXPECT_STDSTREQ( "\033[13;5:1u", KittyKeyboardEncoder::encode( enter, 10 ).bytes ); + enter.type = KittyKeyEventType::Repeat; + EXPECT_STDSTREQ( "\033[13;5:2u", KittyKeyboardEncoder::encode( enter, 10 ).bytes ); + enter.type = KittyKeyEventType::Release; + EXPECT_STDSTREQ( "\033[13;5:3u", KittyKeyboardEncoder::encode( enter, 10 ).bytes ); + EXPECT_STDSTREQ( "\033[97;5u", + KittyKeyboardEncoder::encode( + { KEY_A, SCANCODE_A, 0, KEYMOD_CTRL, KittyKeyEventType::Press }, 1 ) + .bytes ); + EXPECT_FALSE( KittyKeyboardEncoder::encode( + { KEY_A, SCANCODE_A, 0, KEYMOD_SHIFT, KittyKeyEventType::Press }, 1 ) + .handled ); + EXPECT_FALSE( KittyKeyboardEncoder::encode( + { KEY_1, SCANCODE_1, 0, KEYMOD_SHIFT, KittyKeyEventType::Press }, 7 ) + .handled ); + EXPECT_STDSTREQ( "\033[97:65;2u", + KittyKeyboardEncoder::encode( + { KEY_A, SCANCODE_A, 'A', KEYMOD_SHIFT, KittyKeyEventType::Press }, 12 ) + .bytes ); + EXPECT_STDSTREQ( "\033[13;1:1~", + KittyKeyboardEncoder::encode( + { KEY_F3, SCANCODE_F3, 0, KEYMOD_NONE, KittyKeyEventType::Press }, 31 ) + .bytes ); + EXPECT_STDSTREQ( + "\033[57414;1:1u", + KittyKeyboardEncoder::encode( + { KEY_KP_ENTER, SCANCODE_KP_ENTER, 0, KEYMOD_NONE, KittyKeyEventType::Press }, 31 ) + .bytes ); + EXPECT_STDSTREQ( + "\033[57442;5:1u", + KittyKeyboardEncoder::encode( + { KEY_LCTRL, SCANCODE_LCTRL, 0, KEYMOD_CTRL, KittyKeyEventType::Press }, 31 ) + .bytes ); +} + +UTEST( eterm, kitty_keyboard_protocol_keeps_screen_state_independent ) { auto pty = std::make_unique(); - pty->mBuffer = "\033[?u\033[>7u\033[<1u\033[mBuffer = "\033[=1u\033[?1049h\033[?u\033[=3u\033[?1049l\033[?u"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + term->update(); + EXPECT_STDSTREQ( "\033[?0u\033[?1u", ptyPtr->mWrites ); +} + +UTEST( eterm, kitty_keyboard_protocol_negotiates_and_reports_active_state ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[?u\033[>7u\033[?u\033[mLoopWrites = false; MockPty* ptyPtr = pty.get(); auto process = std::make_unique(); @@ -919,7 +998,43 @@ UTEST( eterm, modern_csi_prefixes_do_not_claim_unsupported_keyboard_protocol ) { term->update(); - EXPECT_TRUE( ptyPtr->mWrites.empty() ); + EXPECT_STDSTREQ( "\033[?0u\033[?7u\033[?0u", ptyPtr->mWrites ); +} + +UTEST( eterm, kitty_keyboard_protocol_encodes_worker_key_without_duplicate_text ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[>8u"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + term->update(); + term->keyEvent( { KEY_A, SCANCODE_A, 0, KEYMOD_NONE, KittyKeyEventType::Press } ); + term->textInput( 'a' ); + term->keyEvent( { KEY_RETURN, SCANCODE_RETURN, '\r', KEYMOD_CTRL, KittyKeyEventType::Press } ); + EXPECT_STDSTREQ( "\033[97;1u\033[13;5u", ptyPtr->mWrites ); +} + +UTEST( eterm, kitty_keyboard_protocol_preserves_altgr_text ) { + auto pty = std::make_unique(); + pty->mBuffer = "\033[>15u"; + pty->mLoopWrites = false; + MockPty* ptyPtr = pty.get(); + auto process = std::make_unique(); + auto display = std::make_shared(); + auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 ); + term->update(); + + // Spanish AltGr+2 produces '@'. Windows may include a synthetic Ctrl modifier for AltGr. + term->keyEvent( + { KEY_2, SCANCODE_2, '@', KEYMOD_RALT | KEYMOD_LCTRL, KittyKeyEventType::Press } ); + term->textInput( '@' ); + + EXPECT_STDSTREQ( "\033[64::50;1:1u", ptyPtr->mWrites ); + EXPECT_FALSE( KittyKeyboardEncoder::encode( + { KEY_2, SCANCODE_2, 0, KEYMOD_RALT, KittyKeyEventType::Press }, 7 ) + .handled ); } UTEST( eterm, kitty_graphics_unicode_placeholder_uses_color_and_diacritics ) {