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 ) {