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.
This commit is contained in:
Martín Lucas Golini
2026-09-04 21:13:27 -03:00
parent 2a3ba5271e
commit 6a24bc0d6e
18 changed files with 791 additions and 96 deletions

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Keycode>( SDLEvent.key.key );
event.key.keysym.scancode = static_cast<Scancode>( 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<Keycode>( SDLEvent.key.key );
event.key.keysym.scancode = static_cast<Scancode>( SDLEvent.key.scancode );
@@ -383,11 +385,13 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
break;
}
case SDL_EVENT_JOYSTICK_ADDED: {
static_cast<JoystickManagerSDL*>( mJoystickManager )->addJoystick( SDLEvent.jdevice.which );
static_cast<JoystickManagerSDL*>( mJoystickManager )
->addJoystick( SDLEvent.jdevice.which );
break;
}
case SDL_EVENT_JOYSTICK_REMOVED: {
static_cast<JoystickManagerSDL*>( mJoystickManager )->removeJoystick( SDLEvent.jdevice.which );
static_cast<JoystickManagerSDL*>( mJoystickManager )
->removeJoystick( SDLEvent.jdevice.which );
break;
}
case SDL_EVENT_QUIT: {

View File

@@ -0,0 +1,67 @@
#ifndef ETERM_KITTYKEYBOARDPROTOCOL_HPP
#define ETERM_KITTYKEYBOARDPROTOCOL_HPP
#include <cstdint>
#include <eepp/core/small_vector.hpp>
#include <eepp/window/keycodes.hpp>
#include <string>
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<Uint32>( 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<Uint32, 8> 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

View File

@@ -14,6 +14,7 @@
#include <eterm/terminal/terminalcolorscheme.hpp>
#include <eterm/terminal/terminalsession.hpp>
#include <bitset>
#include <memory>
#include <unordered_map>
#include <vector>
@@ -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<bool> mDirtyLines;
std::bitset<SCANCODES_NUM> mSuppressedKeyUps;
bool mDirty{ true };
bool mDirtyCursor{ true };
bool mDrawing{ false };

View File

@@ -42,6 +42,7 @@
#include <eterm/terminal/ipseudoterminal.hpp>
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/kittygraphicsprotocol.hpp>
#include <eterm/terminal/kittykeyboardprotocol.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <memory>
#include <stdint.h>
@@ -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();

View File

@@ -119,6 +119,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
void write( std::string data, bool mayEcho = true );
void writeRaw( std::string data );
void keyEvent( KittyKeyEvent event );
void textInput( Uint32 codepoint );
void resize( int columns, int rows );
void resize( int columns, int rows, int pixelWidth, int pixelHeight );
void scrollUp( int amount );
@@ -162,6 +164,12 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
struct WriteRawCommand {
std::string data;
};
struct KeyCommand {
KittyKeyEvent event;
};
struct TextInputCommand {
Uint32 codepoint{ 0 };
};
struct ResizeCommand {
int columns{ 0 };
int rows{ 0 };
@@ -220,10 +228,10 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
struct FocusCommand : BoolCommand {};
using Command =
std::variant<WriteCommand, WriteRawCommand, ResizeCommand, ScrollCommand,
SelectionStartCommand, SelectionExtendCommand, SelectionClearCommand,
MouseCommand, FocusCommand, CursorModeCommand, PaletteCommand,
PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
std::variant<WriteCommand, WriteRawCommand, KeyCommand, TextInputCommand, ResizeCommand,
ScrollCommand, SelectionStartCommand, SelectionExtendCommand,
SelectionClearCommand, MouseCommand, FocusCommand, CursorModeCommand,
PaletteCommand, PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
PromptEventsCommand, TerminateCommand, RestartCommand, ResetCommand,
SelectionRequestCommand, GraphicsResyncCommand>;

View File

@@ -0,0 +1,321 @@
#include <algorithm>
#include <cstdio>
#include <eterm/terminal/kittykeyboardprotocol.hpp>
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<Uint32>( 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<Uint32>( event.scancode - SCANCODE_F13 ) };
break;
}
const auto keycode = static_cast<Uint32>( 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<Uint32>( scancode - SCANCODE_A );
if ( scancode >= SCANCODE_1 && scancode <= SCANCODE_9 )
return '1' + static_cast<Uint32>( 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<Uint32>( 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<Uint32>( type ), suffix );
} else {
len = std::snprintf( buf, sizeof( buf ), "\033[%s;%u%c", keyField, modifiers, suffix );
}
return len > 0 ? std::string( buf, static_cast<size_t>( 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

View File

@@ -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<size_t>( scancode ) < mSuppressedKeyUps.size() &&
mSuppressedKeyUps.test( static_cast<size_t>( scancode ) ) ) {
mSuppressedKeyUps.reset( static_cast<size_t>( 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<size_t>( scancode ) < mSuppressedKeyUps.size() )
mSuppressedKeyUps.set( static_cast<size_t>( 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 {

View File

@@ -34,6 +34,7 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#include <eterm/terminal/boxdrawdata.hpp>
#include <eterm/terminal/terminaldisplay.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eepp/core/memorymanager.hpp>
@@ -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<char>( 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<Uint32>( 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<Keycode>( 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<size_t>( len ), 0 );
}
} else if ( mCsiescseq.priv == '>' ) {
if ( mCsiescseq.narg == 1 && mCsiescseq.arg[0] >= 0 )
state.push( static_cast<Uint32>( mCsiescseq.arg[0] ) );
} else if ( mCsiescseq.priv == '<' ) {
if ( mCsiescseq.narg == 1 && mCsiescseq.arg[0] >= 0 )
state.pop( omitted ? 1 : static_cast<size_t>( mCsiescseq.arg[0] ) );
} else if ( mCsiescseq.priv == '=' && mCsiescseq.narg <= 2 && mCsiescseq.arg[0] >= 0 ) {
const Uint32 mode = mCsiescseq.narg == 1 ? 1 : static_cast<Uint32>( mCsiescseq.arg[1] );
state.set( static_cast<Uint32>( 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<ITerminalDisplay> 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;

View File

@@ -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<T, WriteRawCommand> ) {
mEmulator->write( value.data.data(), value.data.size() );
} else if constexpr ( std::is_same_v<T, KeyCommand> ) {
mEmulator->keyEvent( value.event );
} else if constexpr ( std::is_same_v<T, TextInputCommand> ) {
mEmulator->textInput( value.codepoint );
} else if constexpr ( std::is_same_v<T, ResizeCommand> ) {
mEmulator->resize( value.columns, value.rows, value.pixelWidth, value.pixelHeight );
} else if constexpr ( std::is_same_v<T, ScrollCommand> ) {
@@ -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<T, FocusCommand> ) {
if ( !value.value )
mEmulator->clearPendingKeyboardInput();
if ( mWorkerDisplay->getMode( MODE_FOCUS ) )
mEmulator->ttywrite( value.value ? "\033[I" : "\033[O", 3, false );
mWorkerDisplay->setFocused( value.value );

View File

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

View File

@@ -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<Uint32>( 1 ), state.flags );
state.push( 7 );
EXPECT_EQ( static_cast<Uint32>( 7 ), state.flags );
state.pop();
EXPECT_EQ( static_cast<Uint32>( 1 ), state.flags );
state.set( 8, 2 );
EXPECT_EQ( static_cast<Uint32>( 9 ), state.flags );
state.set( 1, 3 );
EXPECT_EQ( static_cast<Uint32>( 8 ), state.flags );
state.set( 8, 1 );
EXPECT_EQ( static_cast<Uint32>( 8 ), state.flags );
state.set( 1, 99 );
EXPECT_EQ( static_cast<Uint32>( 8 ), state.flags );
state.pop( 1000000 );
EXPECT_EQ( static_cast<Uint32>( 0 ), state.flags );
EXPECT_TRUE( state.stack.empty() );
}
UTEST( eterm, kitty_keyboard_encoder_modifiers_and_enter ) {
EXPECT_EQ( static_cast<Uint32>( 1 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_NONE ) );
EXPECT_EQ( static_cast<Uint32>( 2 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_SHIFT ) );
EXPECT_EQ( static_cast<Uint32>( 3 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_ALT ) );
EXPECT_EQ( static_cast<Uint32>( 5 ), KittyKeyboardEncoder::encodeModifiers( KEYMOD_CTRL ) );
EXPECT_EQ( static_cast<Uint32>( 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<MockPty>();
pty->mBuffer = "\033[?u\033[>7u\033[<1u\033[<u\033[=3u";
pty->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<MockProcess>();
auto display = std::make_shared<MockDisplay>();
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<MockPty>();
pty->mBuffer = "\033[?u\033[>7u\033[?u\033[<u\033[?u";
pty->mLoopWrites = false;
MockPty* ptyPtr = pty.get();
auto process = std::make_unique<MockProcess>();
@@ -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<MockPty>();
pty->mBuffer = "\033[>8u";
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->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<MockPty>();
pty->mBuffer = "\033[>15u";
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();
// 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 ) {