Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions include/tvision/compat/windows/windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,18 @@ typedef unsigned int UINT;
#define VK_PA1 0xFD
#define VK_OEM_CLEAR 0xFE

#define VK_OEM_1 0xBA // ';:' for US
#define VK_OEM_PLUS 0xBB // '+' any country
#define VK_OEM_COMMA 0xBC // ',' any country
#define VK_OEM_MINUS 0xBD // '-' any country
#define VK_OEM_PERIOD 0xBE // '.' any country
#define VK_OEM_2 0xBF // '/?' for US
#define VK_OEM_3 0xC0 // '`~' for US
#define VK_OEM_4 0xDB // '[{' for US
#define VK_OEM_5 0xDC // '\|' for US
#define VK_OEM_6 0xDD // ']}' for US
#define VK_OEM_7 0xDE // ''"' for US

//
// Predefined Clipboard Formats
//
Expand Down
184 changes: 112 additions & 72 deletions source/platform/termio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <internal/utf8.h>

#include <chrono>
#include <ctype.h>

namespace tvision
{
Expand Down Expand Up @@ -306,7 +307,11 @@ void TermIO::keyModsOn(ConsoleCtl &con) noexcept
"\x1B[?2004s" // Save bracketed paste.
"\x1B[?2004h" // Enable bracketed paste.
"\x1B[>4;1m" // Enable modifyOtherKeys (XTerm).
"\x1B[>5u" // Disambiguate escape codes (1) + Report alternate keys (4) (Kitty).
// Enable Kitty protocol mode (29u).
// 29 = 1 (disambiguate) + 4 (report alternate keys)
// + 8 (report all keys) + 16 (report associated text).
// This gives us the base-layout-key for shortcuts and proper text handling.
"\x1B[>29u"
"\x1B[?9001h" // Enable win32-input-mode (Conpty).
far2lEnableSeq // Enable far2l terminal extensions.
);
Expand Down Expand Up @@ -373,6 +378,104 @@ void TermIO::normalizeKey(KeyDownEvent &keyDown) noexcept
;
}

static ParseResult parseKittyKey(GetChBuf &buf, TEvent &ev) noexcept
// https://sw.kovidgoyal.net/kitty/keyboard-protocol.html
{
// A robust parser for Kitty sequences.
// It reads the entire potential sequence into a buffer, identifies the
// terminator, and only if it's 'u', proceeds to parse. Otherwise, it
// puts all characters back into the input stream.
char seq_buf[128];
int seq_len = 0;
int terminator = 0;

// Read until a valid CSI terminator is found.
while (seq_len < (int) sizeof(seq_buf) - 1) {
int k = buf.get();
if (k == -1) { // End of input before terminator.
// Put back what we've read and fail.
for(int i = 0; i < seq_len; ++i) buf.unget();
return Rejected;
}
seq_buf[seq_len++] = (char)k;
if (isalpha(k) || k == '~' || k == '_') {
terminator = k;
break;
}
}

if (terminator != 'u') {
// Not a Kitty sequence, put everything back.
for(int i = 0; i < seq_len; ++i) buf.unget();
return Rejected;
}

// It's a Kitty sequence. Let's parse it from our buffer.
seq_buf[seq_len - 1] = '\0'; // Null-terminate before the 'u'.
const char* p = seq_buf;

auto get_num = [&](uint& val) {
if (!isdigit((unsigned char)*p)) return false;
val = 0;
while (isdigit((unsigned char)*p)) {
val = val * 10 + (*p - '0');
p++;
}
return true;
};

// CSI unicode-key-code:shifted:base-layout ; modifiers:event-type ; text-codepoints u
uint params[3] = {0, 0, 0}; // 0: unicode, 1: shifted, 2: base-layout
get_num(params[0]);

if (*p == ':') { p++;
if (*p == ':') { p++; // "::" case
get_num(params[2]);
} else { // ":" case
if (get_num(params[1])) {
if (*p == ':') { p++;
get_num(params[2]);
}
}
}
}

uint mods = 1;
if (*p == ';') { p++;
if (!get_num(mods)) mods = 1; // Handle empty modifier part like ';;'
if (*p == ':') { p++; uint dummy; get_num(dummy); } // consume event type
}

char text_utf8_buf[sizeof(KeyDownEvent::text)] = {};
size_t text_len = 0;
if (*p == ';') { p++;
while (*p != '\0') {
uint codepoint;
if (get_num(codepoint)) {
if (text_len < sizeof(text_utf8_buf)) {
text_len += utf32To8(codepoint, text_utf8_buf + text_len);
}
}
if (*p == ':') p++; else break;
}
}

// Success. Build the event.
uint key_to_use = params[2] ? params[2] : (params[1] ? params[1] : params[0]);

if (keyFromCodepoint(key_to_use, mods, ev.keyDown))
{
if (text_len > 0) {
memcpy(ev.keyDown.text, text_utf8_buf, text_len);
ev.keyDown.textLength = text_len;
}
ev.what = evKeyDown;

return Accepted;
}
return Ignored;
}

ParseResult TermIO::parseEvent(GetChBuf &buf, TEvent &ev, InputState &state) noexcept
{
if (buf.get() == '\x1B')
Expand All @@ -393,6 +496,13 @@ ParseResult TermIO::parseEscapeSeq(GetChBuf &buf, TEvent &ev, InputState &state)
return parseFar2lAnswer(buf, ev, state);
break;
case '[':
{
// Prioritize the Kitty parser. If it fails, it will have restored the
// buffer, so we can proceed with other parsers.
ParseResult res = parseKittyKey(buf, ev);
if (res != Rejected) return res;
}
// If parseKittyKey failed, it's some other CSI sequence.
switch (buf.get())
{
// Note: mouse events are usually detected in 'NcursesInput::parseCursesMouse'.
Expand All @@ -408,8 +518,7 @@ ParseResult TermIO::parseEscapeSeq(GetChBuf &buf, TEvent &ev, InputState &state)
{
switch (csi.terminator)
{
case 'u':
return parseKittyKey(csi, ev);
// 'u' is handled by parseKittyKey now.
case 'R':
return parseCPR(csi, state);
case '_':
Expand Down Expand Up @@ -676,75 +785,6 @@ ParseResult TermIO::parseSS3Key(GetChBuf &buf, TEvent &ev) noexcept
return Accepted;
}

ParseResult TermIO::parseKittyKey(const CSIData &csi, TEvent &ev) noexcept
// https://sw.kovidgoyal.net/kitty/keyboard-protocol.html
// http://www.leonerd.org.uk/hacks/fixterms/
{
if (csi.length < 1 || csi.terminator != 'u')
return Rejected;

// NOTE: We are not requesting all of Kitty's Keyboard Protocol features in
// keyModsOn(), yet this code supports them.
//
// Kitty events are structured like:
//
// unicode-key-code : shifted-key-code : base-layout-key ; modifiers : event-type ; text-as-codepoints u
//
// Only the 'unicode-key-code' is mandatory; the rest are optional.

uint kittyKeyCode = 0;
uint kittyShiftedKeyCode = 0;
uint kittyBaseLayoutKey = 0;
uint kittyModifiers = 1;
uint kittyEventType = 1;
uint kittyText = 0;

uint i = 0;
kittyKeyCode = csi.getValue(i++, 0);
if (i < csi.length && csi.getSeparator(i - 1) == ':')
kittyShiftedKeyCode = csi.getValue(i++, 0);
if (i < csi.length && csi.getSeparator(i - 1) == ':')
kittyBaseLayoutKey = csi.getValue(i++, 0);
if (i < csi.length)
kittyModifiers = csi.getValue(i++, 1);
if (i < csi.length && csi.getSeparator(i - 1) == ':')
kittyEventType = csi.getValue(i++, 1);
if (i < csi.length)
// In theory, there could be more than one character, but we
// do not currently support this, so just take the first one.
kittyText = csi.getValue(i++, 0);

if (kittyEventType != 1)
// We are only interested in Press events.
return Ignored;

uint codepoint = kittyKeyCode;
if (kittyText != 0)
codepoint = kittyText;
else if (kittyShiftedKeyCode != 0)
codepoint = kittyShiftedKeyCode;

if (!keyFromCodepoint(codepoint, kittyModifiers, ev.keyDown))
return Ignored;

// When the unicode-key-code isn't an ASCII letter, but the base-layout-key
// is, and at the same time the Ctrl or Alt modifiers are present,
// initialize the event's keyCode as if it was Ctrl/Alt + A-Z, so that
// standard keyboard shortcuts can still be triggered when using a non-ASCII
// keyboard layout (e.g. Ctrl+Ф in the RU keyboard layout will match Ctrl+A).
if ( !isAsciiLetter(kittyKeyCode) && isAsciiLetter(kittyBaseLayoutKey) &&
(ev.keyDown.controlKeyState & (kbCtrlShift | kbAltShift)) != 0 )
{
// Kitty's 'base-layout-key' is always in lowercase,
char upperBaseLayoutKey = kittyBaseLayoutKey - 'a' + 'A';
ev.keyDown.keyCode = getModdedKeyCode(upperBaseLayoutKey, ev.keyDown.controlKeyState);
// Note that 'ev.keyDown.text' still contains the original key text.
}

ev.what = evKeyDown;
return Accepted;
}

ParseResult TermIO::parseDCS(GetChBuf &buf, InputState &state) noexcept
// Pre: '\x1BP' has just been read.
{
Expand Down
50 changes: 49 additions & 1 deletion source/platform/win32con.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <internal/termio.h>
#include <internal/utf8.h>
#include <locale.h>
#include <wctype.h>

namespace tvision
{
Expand Down Expand Up @@ -548,7 +549,54 @@ bool getWin32Key(const KEY_EVENT_RECORD &KeyEvent, TEvent &ev, InputState &state
// If the character cannot be represented in the current codepage,
// or if it would accidentally trigger a Ctrl+Key combination,
// make the whole keyCode zero to avoid side effects.
ev.keyDown.keyCode = kbNoKey;
{
// This breaks menu hotkeys in non-Latin kb layouts
//ev.keyDown.keyCode = kbNoKey;

// Let's use Latin char code detected from virtual key code
int latinKey = 0;

if (
((KeyEvent.wVirtualKeyCode >= 'A') && (KeyEvent.wVirtualKeyCode <= 'Z')) ||
((KeyEvent.wVirtualKeyCode >= '0') && (KeyEvent.wVirtualKeyCode <= '9'))
) {

latinKey = towlower(KeyEvent.wVirtualKeyCode);
}

switch (KeyEvent.wVirtualKeyCode) {

// top row
case VK_OEM_3: latinKey = '`'; break;
// ...digits...
case VK_OEM_MINUS: latinKey = '-'; break;
case VK_OEM_PLUS: latinKey = '+'; break;

// second row
// ...letters...
case VK_OEM_4: latinKey = '['; break;
case VK_OEM_6: latinKey = ']'; break;

// third row
// ...letters...
case VK_OEM_1: latinKey = ';'; break;
case VK_OEM_7: latinKey = '\''; break;
case VK_OEM_5: latinKey = '\\'; break;

// forth row
case 0xE1: latinKey = '/'; break;
// ...letters...
case VK_OEM_COMMA: latinKey = ','; break;
case VK_OEM_PERIOD: latinKey = '.'; break;
case VK_OEM_2: latinKey = '/'; break;
}

if (latinKey) {
ev.keyDown.keyCode = latinKey;
} else {
ev.keyDown.keyCode = kbNoKey;
}
}
}

if ( ev.keyDown.keyCode == 0x2A00 || ev.keyDown.keyCode == 0x1D00 ||
Expand Down
Loading