mirror of
https://github.com/microsoft/terminal.git
synced 2026-04-05 21:44:31 +00:00
wip
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "InputBuffer.h"
|
||||
|
||||
#pragma warning(disable : 4100)
|
||||
|
||||
void InputBuffer::write(std::string_view text)
|
||||
{
|
||||
m_buf.append(text);
|
||||
@@ -10,18 +8,13 @@ void InputBuffer::write(std::string_view text)
|
||||
|
||||
bool InputBuffer::hasData() const noexcept
|
||||
{
|
||||
return m_readPos < m_buf.size();
|
||||
return m_readPos < m_buf.size() || m_recordReadPos < m_records.size();
|
||||
}
|
||||
|
||||
size_t InputBuffer::pendingEventCount() const noexcept
|
||||
{
|
||||
// Rough lower-bound: each byte in the buffer can generate at least 1 record,
|
||||
// and we may already have pre-parsed records queued.
|
||||
const auto queued = (m_records.size() > m_recordReadPos) ? (m_records.size() - m_recordReadPos) : 0;
|
||||
const auto rawBytes = (m_buf.size() > m_readPos) ? (m_buf.size() - m_readPos) : 0;
|
||||
// Each raw byte produces at most 2 records (key down + up), so this is
|
||||
// an over-estimate. But for GetNumberOfConsoleInputEvents we need a
|
||||
// lower bound that's non-zero when data is available.
|
||||
return queued + rawBytes;
|
||||
}
|
||||
|
||||
@@ -42,9 +35,7 @@ size_t InputBuffer::readInputRecords(INPUT_RECORD* dst, size_t maxRecords, bool
|
||||
{
|
||||
// Parse any new data from the raw buffer into m_records.
|
||||
if (m_readPos < m_buf.size())
|
||||
{
|
||||
m_readPos = parse(m_readPos);
|
||||
}
|
||||
parseTokensToRecords();
|
||||
|
||||
const auto available = m_records.size() - m_recordReadPos;
|
||||
const auto toCopy = std::min(available, maxRecords);
|
||||
@@ -54,8 +45,6 @@ size_t InputBuffer::readInputRecords(INPUT_RECORD* dst, size_t maxRecords, bool
|
||||
if (!peek)
|
||||
{
|
||||
m_recordReadPos += toCopy;
|
||||
|
||||
// If we've consumed all records, reset the vector.
|
||||
if (m_recordReadPos == m_records.size())
|
||||
{
|
||||
m_records.clear();
|
||||
@@ -65,9 +54,7 @@ size_t InputBuffer::readInputRecords(INPUT_RECORD* dst, size_t maxRecords, bool
|
||||
}
|
||||
|
||||
if (!peek)
|
||||
{
|
||||
compact();
|
||||
}
|
||||
|
||||
return toCopy;
|
||||
}
|
||||
@@ -82,7 +69,6 @@ void InputBuffer::flush()
|
||||
|
||||
void InputBuffer::compact()
|
||||
{
|
||||
// Only compact when we've consumed a meaningful prefix.
|
||||
if (m_readPos > 4096 || (m_readPos > 0 && m_readPos == m_buf.size()))
|
||||
{
|
||||
m_buf.erase(0, m_readPos);
|
||||
@@ -91,428 +77,294 @@ void InputBuffer::compact()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VT Parser
|
||||
// Token → INPUT_RECORD conversion
|
||||
// ============================================================================
|
||||
//
|
||||
// Grammar (simplified):
|
||||
// Ground:
|
||||
// 0x1B → Escape
|
||||
// 0x00..0x1A, 0x1C..0x7F → plain character (emit key event)
|
||||
// 0x80..0xFF → UTF-8 lead byte (consume full codepoint, emit key event)
|
||||
//
|
||||
// Escape:
|
||||
// '[' → CsiEntry
|
||||
// 'O' → Ss3
|
||||
// 0x20..0x7E → Alt+<char> (emit key event with LEFT_ALT_PRESSED)
|
||||
// timeout/buf-end → bare ESC key
|
||||
//
|
||||
// CsiEntry:
|
||||
// '0'..'9',';' → accumulate parameters
|
||||
// 'A'..'Z','a'..'z','~','_' → dispatch CSI sequence
|
||||
//
|
||||
// Ss3:
|
||||
// 'A'..'Z' → dispatch SS3 key
|
||||
|
||||
size_t InputBuffer::parse(size_t pos)
|
||||
void InputBuffer::parseTokensToRecords()
|
||||
{
|
||||
while (pos < m_buf.size())
|
||||
const auto input = std::string_view{ m_buf.data() + m_readPos, m_buf.size() - m_readPos };
|
||||
auto stream = m_parser.parse(input);
|
||||
|
||||
VtToken token;
|
||||
while (stream.next(token))
|
||||
{
|
||||
const auto consumed = parseGround(pos);
|
||||
if (consumed == 0)
|
||||
break; // Need more data.
|
||||
pos += consumed;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
size_t InputBuffer::parseGround(size_t pos)
|
||||
{
|
||||
const auto ch = static_cast<uint8_t>(m_buf[pos]);
|
||||
|
||||
if (ch == 0x1B) // ESC
|
||||
{
|
||||
// Look ahead to decide if this is ESC-prefix or bare ESC.
|
||||
if (pos + 1 >= m_buf.size())
|
||||
switch (token.type)
|
||||
{
|
||||
// TODO: ESC timeout for out-of-proc hosts (e.g. sshd).
|
||||
// For now, treat a lone ESC at the end of the buffer as a bare ESC key.
|
||||
// In-proc hosts always deliver complete sequences, so this is fine.
|
||||
ParsedKey key;
|
||||
key.vk = VK_ESCAPE;
|
||||
key.scanCode = vkToScanCode(VK_ESCAPE);
|
||||
key.ch = 0x1B;
|
||||
emitKey(key);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto next = static_cast<uint8_t>(m_buf[pos + 1]);
|
||||
|
||||
if (next == '[')
|
||||
{
|
||||
// ESC [ → CSI
|
||||
const auto consumed = parseCsi(pos + 2);
|
||||
if (consumed == 0)
|
||||
return 0; // Incomplete CSI, need more data.
|
||||
return 2 + consumed;
|
||||
}
|
||||
|
||||
if (next == 'O')
|
||||
{
|
||||
// ESC O → SS3
|
||||
const auto consumed = parseSs3(pos + 2);
|
||||
if (consumed == 0)
|
||||
return 0; // Incomplete SS3, need more data.
|
||||
return 2 + consumed;
|
||||
}
|
||||
|
||||
if (next >= 0x20 && next <= 0x7E)
|
||||
{
|
||||
// Alt+character.
|
||||
ParsedKey key;
|
||||
key.ch = static_cast<wchar_t>(next);
|
||||
key.vk = LOBYTE(VkKeyScanW(key.ch));
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
key.modifiers = LEFT_ALT_PRESSED;
|
||||
emitKey(key);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ESC followed by something unexpected — just emit bare ESC.
|
||||
ParsedKey key;
|
||||
key.vk = VK_ESCAPE;
|
||||
key.scanCode = vkToScanCode(VK_ESCAPE);
|
||||
key.ch = 0x1B;
|
||||
emitKey(key);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (ch < 0x20) // C0 control characters.
|
||||
{
|
||||
ParsedKey key;
|
||||
key.ch = static_cast<wchar_t>(ch);
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\r': // Enter
|
||||
key.vk = VK_RETURN;
|
||||
key.scanCode = vkToScanCode(VK_RETURN);
|
||||
key.ch = L'\r';
|
||||
case VtToken::Text:
|
||||
handleText(token.payload);
|
||||
break;
|
||||
case '\n': // Ctrl+Enter or LF
|
||||
key.vk = VK_RETURN;
|
||||
key.scanCode = vkToScanCode(VK_RETURN);
|
||||
key.ch = L'\n';
|
||||
key.modifiers = LEFT_CTRL_PRESSED;
|
||||
case VtToken::Ctrl:
|
||||
handleCtrl(token.ch);
|
||||
break;
|
||||
case '\t': // Tab
|
||||
key.vk = VK_TAB;
|
||||
key.scanCode = vkToScanCode(VK_TAB);
|
||||
key.ch = L'\t';
|
||||
case VtToken::Esc:
|
||||
handleEsc(token.ch);
|
||||
break;
|
||||
case '\b': // Backspace
|
||||
key.vk = VK_BACK;
|
||||
key.scanCode = vkToScanCode(VK_BACK);
|
||||
key.ch = L'\b';
|
||||
case VtToken::SS3:
|
||||
handleSs3(token.ch);
|
||||
break;
|
||||
case 0x00: // Ctrl+Space or Ctrl+@
|
||||
key.vk = VK_SPACE;
|
||||
key.scanCode = vkToScanCode(VK_SPACE);
|
||||
key.ch = L'\0';
|
||||
key.modifiers = LEFT_CTRL_PRESSED;
|
||||
case VtToken::Csi:
|
||||
handleCsi(*token.csi);
|
||||
break;
|
||||
default:
|
||||
// Ctrl+A..Ctrl+Z: ch = 0x01..0x1A
|
||||
if (ch >= 0x01 && ch <= 0x1A)
|
||||
{
|
||||
key.ch = static_cast<wchar_t>(ch);
|
||||
key.vk = static_cast<WORD>('A' + ch - 1);
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
key.modifiers = LEFT_CTRL_PRESSED;
|
||||
}
|
||||
// Osc/Dcs — irrelevant for input records, skip.
|
||||
break;
|
||||
}
|
||||
|
||||
emitKey(key);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (ch == 0x7F) // DEL — treat as backspace.
|
||||
// Advance m_readPos by what the parser consumed.
|
||||
m_readPos += stream.offset();
|
||||
}
|
||||
|
||||
void InputBuffer::handleText(std::string_view text)
|
||||
{
|
||||
// Decode UTF-8 codepoints and emit key events.
|
||||
const auto* bytes = reinterpret_cast<const uint8_t*>(text.data());
|
||||
size_t i = 0;
|
||||
|
||||
while (i < text.size())
|
||||
{
|
||||
ParsedKey key;
|
||||
uint32_t cp;
|
||||
size_t seqLen;
|
||||
const auto b = bytes[i];
|
||||
|
||||
if (b < 0x80) { cp = b; seqLen = 1; }
|
||||
else if ((b & 0xE0) == 0xC0) { cp = b & 0x1F; seqLen = 2; }
|
||||
else if ((b & 0xF0) == 0xE0) { cp = b & 0x0F; seqLen = 3; }
|
||||
else if ((b & 0xF8) == 0xF0) { cp = b & 0x07; seqLen = 4; }
|
||||
else { cp = 0xFFFD; seqLen = 1; i++; continue; }
|
||||
|
||||
if (i + seqLen > text.size()) break;
|
||||
|
||||
for (size_t j = 1; j < seqLen; j++)
|
||||
{
|
||||
const auto cont = bytes[i + j];
|
||||
if ((cont & 0xC0) != 0x80) { cp = 0xFFFD; break; }
|
||||
cp = (cp << 6) | (cont & 0x3F);
|
||||
}
|
||||
i += seqLen;
|
||||
|
||||
// Emit one or two INPUT_RECORDs (surrogate pair for supplementary plane).
|
||||
if (cp <= 0xFFFF)
|
||||
{
|
||||
ParsedKey key;
|
||||
key.ch = static_cast<wchar_t>(cp);
|
||||
key.vk = LOBYTE(VkKeyScanW(key.ch));
|
||||
if (key.vk == 0xFF) key.vk = 0;
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
emitKey(key);
|
||||
}
|
||||
else if (cp <= 0x10FFFF)
|
||||
{
|
||||
ParsedKey key;
|
||||
key.ch = static_cast<wchar_t>(0xD800 + ((cp - 0x10000) >> 10));
|
||||
emitKey(key);
|
||||
key.ch = static_cast<wchar_t>(0xDC00 + ((cp - 0x10000) & 0x3FF));
|
||||
emitKey(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InputBuffer::handleCtrl(char ch)
|
||||
{
|
||||
ParsedKey key;
|
||||
|
||||
switch (static_cast<uint8_t>(ch))
|
||||
{
|
||||
case '\r':
|
||||
key.vk = VK_RETURN;
|
||||
key.scanCode = vkToScanCode(VK_RETURN);
|
||||
key.ch = L'\r';
|
||||
break;
|
||||
case '\n':
|
||||
key.vk = VK_RETURN;
|
||||
key.scanCode = vkToScanCode(VK_RETURN);
|
||||
key.ch = L'\n';
|
||||
key.modifiers = LEFT_CTRL_PRESSED;
|
||||
break;
|
||||
case '\t':
|
||||
key.vk = VK_TAB;
|
||||
key.scanCode = vkToScanCode(VK_TAB);
|
||||
key.ch = L'\t';
|
||||
break;
|
||||
case '\b':
|
||||
key.vk = VK_BACK;
|
||||
key.scanCode = vkToScanCode(VK_BACK);
|
||||
key.ch = L'\b';
|
||||
emitKey(key);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ASCII printable or UTF-8 multi-byte sequence.
|
||||
// Decode one UTF-8 codepoint.
|
||||
size_t seqLen = 1;
|
||||
uint32_t codepoint = ch;
|
||||
|
||||
if (ch >= 0x80)
|
||||
{
|
||||
if ((ch & 0xE0) == 0xC0) { seqLen = 2; codepoint = ch & 0x1F; }
|
||||
else if ((ch & 0xF0) == 0xE0) { seqLen = 3; codepoint = ch & 0x0F; }
|
||||
else if ((ch & 0xF8) == 0xF0) { seqLen = 4; codepoint = ch & 0x07; }
|
||||
else { seqLen = 1; codepoint = 0xFFFD; } // Invalid lead byte.
|
||||
|
||||
if (pos + seqLen > m_buf.size())
|
||||
return 0; // Incomplete codepoint, need more data.
|
||||
|
||||
for (size_t i = 1; i < seqLen; i++)
|
||||
break;
|
||||
case 0x7F:
|
||||
key.vk = VK_BACK;
|
||||
key.scanCode = vkToScanCode(VK_BACK);
|
||||
key.ch = L'\b';
|
||||
break;
|
||||
case 0x00:
|
||||
key.vk = VK_SPACE;
|
||||
key.scanCode = vkToScanCode(VK_SPACE);
|
||||
key.ch = L'\0';
|
||||
key.modifiers = LEFT_CTRL_PRESSED;
|
||||
break;
|
||||
default:
|
||||
if (ch >= 0x01 && ch <= 0x1A)
|
||||
{
|
||||
const auto cont = static_cast<uint8_t>(m_buf[pos + i]);
|
||||
if ((cont & 0xC0) != 0x80)
|
||||
{
|
||||
codepoint = 0xFFFD;
|
||||
seqLen = i; // Consume up to the bad byte.
|
||||
break;
|
||||
}
|
||||
codepoint = (codepoint << 6) | (cont & 0x3F);
|
||||
key.ch = static_cast<wchar_t>(ch);
|
||||
key.vk = static_cast<WORD>('A' + ch - 1);
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
key.modifiers = LEFT_CTRL_PRESSED;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Emit one or two INPUT_RECORDs (surrogate pair for codepoints > 0xFFFF).
|
||||
if (codepoint <= 0xFFFF)
|
||||
emitKey(key);
|
||||
}
|
||||
|
||||
void InputBuffer::handleEsc(char ch)
|
||||
{
|
||||
ParsedKey key;
|
||||
|
||||
if (ch == '\0')
|
||||
{
|
||||
ParsedKey key;
|
||||
key.ch = static_cast<wchar_t>(codepoint);
|
||||
// Bare ESC (timeout or end of buffer).
|
||||
key.vk = VK_ESCAPE;
|
||||
key.scanCode = vkToScanCode(VK_ESCAPE);
|
||||
key.ch = 0x1B;
|
||||
}
|
||||
else if (ch >= 0x20 && ch <= 0x7E)
|
||||
{
|
||||
// Alt+character.
|
||||
key.ch = static_cast<wchar_t>(ch);
|
||||
key.vk = LOBYTE(VkKeyScanW(key.ch));
|
||||
if (key.vk == 0xFF) key.vk = 0; // No VK mapping.
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
emitKey(key);
|
||||
}
|
||||
else if (codepoint <= 0x10FFFF)
|
||||
{
|
||||
// Surrogate pair.
|
||||
const auto hi = static_cast<wchar_t>(0xD800 + ((codepoint - 0x10000) >> 10));
|
||||
const auto lo = static_cast<wchar_t>(0xDC00 + ((codepoint - 0x10000) & 0x3FF));
|
||||
|
||||
ParsedKey key;
|
||||
key.ch = hi;
|
||||
emitKey(key);
|
||||
key.ch = lo;
|
||||
emitKey(key);
|
||||
key.modifiers = LEFT_ALT_PRESSED;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid codepoint → U+FFFD.
|
||||
ParsedKey key;
|
||||
key.ch = 0xFFFD;
|
||||
emitKey(key);
|
||||
// Unexpected — emit bare ESC.
|
||||
key.vk = VK_ESCAPE;
|
||||
key.scanCode = vkToScanCode(VK_ESCAPE);
|
||||
key.ch = 0x1B;
|
||||
}
|
||||
|
||||
return seqLen;
|
||||
emitKey(key);
|
||||
}
|
||||
|
||||
size_t InputBuffer::parseCsi(size_t pos)
|
||||
void InputBuffer::handleSs3(char ch)
|
||||
{
|
||||
// Parse parameters and find the final byte.
|
||||
const auto paramEnd = parseCsiParams(pos);
|
||||
if (paramEnd >= m_buf.size())
|
||||
return 0; // Incomplete, need more data.
|
||||
static constexpr WORD keypadLut[] = {
|
||||
VK_UP, // A
|
||||
VK_DOWN, // B
|
||||
VK_RIGHT,// C
|
||||
VK_LEFT, // D
|
||||
0, // E
|
||||
VK_END, // F
|
||||
0, // G
|
||||
VK_HOME, // H
|
||||
};
|
||||
|
||||
const auto finalByte = static_cast<uint8_t>(m_buf[paramEnd]);
|
||||
const auto totalConsumed = paramEnd - pos + 1;
|
||||
ParsedKey key;
|
||||
key.modifiers = ENHANCED_KEY;
|
||||
|
||||
if (ch >= 'A' && ch <= 'H')
|
||||
{
|
||||
key.vk = keypadLut[ch - 'A'];
|
||||
if (key.vk == 0) return;
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
}
|
||||
else if (ch >= 'P' && ch <= 'S')
|
||||
{
|
||||
key.vk = static_cast<WORD>(VK_F1 + (ch - 'P'));
|
||||
key.scanCode = vkToScanCode(key.vk);
|
||||
}
|
||||
else
|
||||
{
|
||||
return; // Unknown SS3.
|
||||
}
|
||||
|
||||
emitKey(key);
|
||||
}
|
||||
|
||||
void InputBuffer::handleCsi(const VtCsi& csi)
|
||||
{
|
||||
// Win32 Input Mode: CSI Vk ; Sc ; Uc ; Kd ; Cs ; Rc _
|
||||
if (finalByte == '_')
|
||||
if (csi.finalByte == '_' && csi.paramCount >= 4)
|
||||
{
|
||||
if (m_paramCount >= 4) // At minimum: Vk, Sc, Uc, Kd
|
||||
{
|
||||
ParsedKey key;
|
||||
key.vk = static_cast<WORD>(m_params[0]);
|
||||
key.scanCode = static_cast<WORD>(m_params[1]);
|
||||
key.ch = static_cast<wchar_t>(m_params[2]);
|
||||
key.keyDown = m_params[3] != 0;
|
||||
key.modifiers = (m_paramCount >= 5) ? static_cast<DWORD>(m_params[4]) : 0;
|
||||
key.repeatCount = (m_paramCount >= 6) ? static_cast<WORD>(m_params[5]) : 1;
|
||||
if (key.repeatCount == 0)
|
||||
key.repeatCount = 1;
|
||||
key.isW32IM = true;
|
||||
emitKey(key);
|
||||
}
|
||||
return totalConsumed;
|
||||
ParsedKey key;
|
||||
key.vk = static_cast<WORD>(csi.params[0]);
|
||||
key.scanCode = static_cast<WORD>(csi.params[1]);
|
||||
key.ch = static_cast<wchar_t>(csi.params[2]);
|
||||
key.keyDown = csi.params[3] != 0;
|
||||
key.modifiers = (csi.paramCount >= 5) ? static_cast<DWORD>(csi.params[4]) : 0;
|
||||
key.repeatCount = (csi.paramCount >= 6) ? static_cast<WORD>(csi.params[5]) : 1;
|
||||
if (key.repeatCount == 0) key.repeatCount = 1;
|
||||
key.isW32IM = true;
|
||||
emitKey(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cursor keys and simple CSI sequences: CSI [params] <letter>
|
||||
if (finalByte >= 'A' && finalByte <= 'Z')
|
||||
// Cursor keys: CSI [1;mod] A-H
|
||||
static constexpr WORD keypadLut[] = {
|
||||
VK_UP, VK_DOWN, VK_RIGHT, VK_LEFT, 0, VK_END, 0, VK_HOME,
|
||||
};
|
||||
|
||||
if (csi.finalByte >= 'A' && csi.finalByte <= 'H')
|
||||
{
|
||||
WORD vk = 0;
|
||||
switch (finalByte)
|
||||
{
|
||||
case 'A': vk = VK_UP; break;
|
||||
case 'B': vk = VK_DOWN; break;
|
||||
case 'C': vk = VK_RIGHT; break;
|
||||
case 'D': vk = VK_LEFT; break;
|
||||
case 'H': vk = VK_HOME; break;
|
||||
case 'F': vk = VK_END; break;
|
||||
case 'P': vk = VK_F1; break;
|
||||
case 'Q': vk = VK_F2; break;
|
||||
case 'R': vk = VK_F3; break;
|
||||
case 'S': vk = VK_F4; break;
|
||||
case 'Z': // Shift+Tab (CSI Z = "backtab")
|
||||
{
|
||||
ParsedKey key;
|
||||
key.vk = VK_TAB;
|
||||
key.scanCode = vkToScanCode(VK_TAB);
|
||||
key.ch = L'\t';
|
||||
key.modifiers = SHIFT_PRESSED;
|
||||
emitKey(key);
|
||||
return totalConsumed;
|
||||
}
|
||||
default:
|
||||
// Unknown CSI sequence — discard.
|
||||
return totalConsumed;
|
||||
}
|
||||
|
||||
if (vk != 0)
|
||||
{
|
||||
ParsedKey key;
|
||||
key.vk = vk;
|
||||
key.scanCode = vkToScanCode(vk);
|
||||
key.modifiers = ENHANCED_KEY;
|
||||
|
||||
// Modifier in second parameter: CSI 1;{mod} A
|
||||
if (m_paramCount >= 2)
|
||||
key.modifiers |= vtModifierToControlKeyState(m_params[1]);
|
||||
|
||||
emitKey(key);
|
||||
}
|
||||
return totalConsumed;
|
||||
}
|
||||
|
||||
// Generic keys: CSI {num} ~ (Insert, Delete, PgUp, PgDn, F5–F12)
|
||||
if (finalByte == '~' && m_paramCount >= 1)
|
||||
{
|
||||
WORD vk = 0;
|
||||
switch (m_params[0])
|
||||
{
|
||||
case 1: vk = VK_HOME; break;
|
||||
case 2: vk = VK_INSERT; break;
|
||||
case 3: vk = VK_DELETE; break;
|
||||
case 4: vk = VK_END; break;
|
||||
case 5: vk = VK_PRIOR; break; // PageUp
|
||||
case 6: vk = VK_NEXT; break; // PageDown
|
||||
case 15: vk = VK_F5; break;
|
||||
case 17: vk = VK_F6; break;
|
||||
case 18: vk = VK_F7; break;
|
||||
case 19: vk = VK_F8; break;
|
||||
case 20: vk = VK_F9; break;
|
||||
case 21: vk = VK_F10; break;
|
||||
case 23: vk = VK_F11; break;
|
||||
case 24: vk = VK_F12; break;
|
||||
default:
|
||||
return totalConsumed; // Unknown — discard.
|
||||
}
|
||||
const auto vk = keypadLut[csi.finalByte - 'A'];
|
||||
if (vk == 0) return;
|
||||
|
||||
ParsedKey key;
|
||||
key.vk = vk;
|
||||
key.scanCode = vkToScanCode(vk);
|
||||
key.modifiers = ENHANCED_KEY;
|
||||
|
||||
// Modifier in second parameter: CSI {num};{mod} ~
|
||||
if (m_paramCount >= 2)
|
||||
key.modifiers |= vtModifierToControlKeyState(m_params[1]);
|
||||
|
||||
if (csi.paramCount >= 2)
|
||||
key.modifiers |= vtModifierToControlKeyState(csi.params[1]);
|
||||
emitKey(key);
|
||||
return totalConsumed;
|
||||
return;
|
||||
}
|
||||
|
||||
// Shift+Tab: CSI Z
|
||||
if (csi.finalByte == 'Z')
|
||||
{
|
||||
ParsedKey key;
|
||||
key.vk = VK_TAB;
|
||||
key.scanCode = vkToScanCode(VK_TAB);
|
||||
key.ch = L'\t';
|
||||
key.modifiers = SHIFT_PRESSED;
|
||||
emitKey(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic keys: CSI {num} [;mod] ~
|
||||
if (csi.finalByte == '~' && csi.paramCount >= 1)
|
||||
{
|
||||
static constexpr struct { uint16_t param; WORD vk; } genericLut[] = {
|
||||
{ 1, VK_HOME }, { 2, VK_INSERT }, { 3, VK_DELETE },
|
||||
{ 4, VK_END }, { 5, VK_PRIOR }, { 6, VK_NEXT },
|
||||
{ 15, VK_F5 }, { 17, VK_F6 }, { 18, VK_F7 },
|
||||
{ 19, VK_F8 }, { 20, VK_F9 }, { 21, VK_F10 },
|
||||
{ 23, VK_F11 }, { 24, VK_F12 },
|
||||
};
|
||||
|
||||
for (const auto& entry : genericLut)
|
||||
{
|
||||
if (csi.params[0] == entry.param)
|
||||
{
|
||||
ParsedKey key;
|
||||
key.vk = entry.vk;
|
||||
key.scanCode = vkToScanCode(entry.vk);
|
||||
key.modifiers = ENHANCED_KEY;
|
||||
if (csi.paramCount >= 2)
|
||||
key.modifiers |= vtModifierToControlKeyState(csi.params[1]);
|
||||
emitKey(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unrecognised CSI — discard.
|
||||
return totalConsumed;
|
||||
}
|
||||
|
||||
size_t InputBuffer::parseSs3(size_t pos)
|
||||
{
|
||||
if (pos >= m_buf.size())
|
||||
return 0; // Need more data.
|
||||
|
||||
const auto finalByte = static_cast<uint8_t>(m_buf[pos]);
|
||||
|
||||
if (finalByte >= 'A' && finalByte <= 'Z')
|
||||
{
|
||||
WORD vk = 0;
|
||||
switch (finalByte)
|
||||
{
|
||||
case 'A': vk = VK_UP; break;
|
||||
case 'B': vk = VK_DOWN; break;
|
||||
case 'C': vk = VK_RIGHT; break;
|
||||
case 'D': vk = VK_LEFT; break;
|
||||
case 'H': vk = VK_HOME; break;
|
||||
case 'F': vk = VK_END; break;
|
||||
case 'P': vk = VK_F1; break;
|
||||
case 'Q': vk = VK_F2; break;
|
||||
case 'R': vk = VK_F3; break;
|
||||
case 'S': vk = VK_F4; break;
|
||||
default:
|
||||
return 1; // Unknown — discard.
|
||||
}
|
||||
|
||||
ParsedKey key;
|
||||
key.vk = vk;
|
||||
key.scanCode = vkToScanCode(vk);
|
||||
key.modifiers = ENHANCED_KEY;
|
||||
emitKey(key);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Unknown SS3 sequence — discard.
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t InputBuffer::parseCsiParams(size_t pos)
|
||||
{
|
||||
m_paramCount = 0;
|
||||
memset(m_params, 0, sizeof(m_params));
|
||||
|
||||
int current = 0;
|
||||
bool hasDigit = false;
|
||||
|
||||
while (pos < m_buf.size())
|
||||
{
|
||||
const auto ch = static_cast<uint8_t>(m_buf[pos]);
|
||||
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
current = current * 10 + (ch - '0');
|
||||
hasDigit = true;
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == ';')
|
||||
{
|
||||
if (m_paramCount < MaxParams)
|
||||
m_params[m_paramCount++] = current;
|
||||
current = 0;
|
||||
hasDigit = false;
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Final byte or intermediate byte — stop parameter parsing.
|
||||
// Store the last accumulated parameter.
|
||||
if (hasDigit || m_paramCount > 0)
|
||||
{
|
||||
if (m_paramCount < MaxParams)
|
||||
m_params[m_paramCount++] = current;
|
||||
}
|
||||
|
||||
return pos; // pos now points at the final byte.
|
||||
}
|
||||
|
||||
// Ran off the end of the buffer — incomplete.
|
||||
return pos;
|
||||
}
|
||||
|
||||
void InputBuffer::emitKey(const ParsedKey& key)
|
||||
{
|
||||
if (key.isW32IM)
|
||||
{
|
||||
// W32IM: emit exactly one record as specified.
|
||||
INPUT_RECORD rec{};
|
||||
rec.EventType = KEY_EVENT;
|
||||
rec.Event.KeyEvent.bKeyDown = key.keyDown ? TRUE : FALSE;
|
||||
@@ -525,7 +377,6 @@ void InputBuffer::emitKey(const ParsedKey& key)
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard path: emit a key-down and key-up pair.
|
||||
INPUT_RECORD down{};
|
||||
down.EventType = KEY_EVENT;
|
||||
down.Event.KeyEvent.bKeyDown = TRUE;
|
||||
@@ -546,13 +397,9 @@ WORD InputBuffer::vkToScanCode(WORD vk)
|
||||
return static_cast<WORD>(MapVirtualKeyW(vk, MAPVK_VK_TO_VSC));
|
||||
}
|
||||
|
||||
DWORD InputBuffer::vtModifierToControlKeyState(int vtMod)
|
||||
DWORD InputBuffer::vtModifierToControlKeyState(uint16_t vtMod)
|
||||
{
|
||||
// VT modifier parameter is 1-based: value = 1 + (flags).
|
||||
// Bit 0 = Shift, Bit 1 = Alt, Bit 2 = Ctrl.
|
||||
if (vtMod <= 1)
|
||||
return 0;
|
||||
|
||||
if (vtMod <= 1) return 0;
|
||||
const auto flags = vtMod - 1;
|
||||
DWORD state = 0;
|
||||
if (flags & 1) state |= SHIFT_PRESSED;
|
||||
|
||||
@@ -1,120 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// InputBuffer: Text-based input buffer with an integrated VT parser.
|
||||
//
|
||||
// The hosting terminal writes UTF-8 VT sequences via write(). Consumers
|
||||
// (console API handlers) dequeue data in one of two forms:
|
||||
//
|
||||
// 1. As raw VT text (ENABLE_VIRTUAL_TERMINAL_INPUT is set):
|
||||
// readRawText() returns the raw bytes 1:1.
|
||||
//
|
||||
// 2. As INPUT_RECORDs (ENABLE_VIRTUAL_TERMINAL_INPUT is NOT set):
|
||||
// readInputRecords() parses VT sequences on-demand and converts them
|
||||
// to key events. The parser recognises:
|
||||
// - Win32InputMode (W32IM, CSI ... _) for full INPUT_RECORD fidelity
|
||||
// - Standard VT520 cursor/function keys (CSI A–D, CSI ~, SS3)
|
||||
// - Plain text (each grapheme cluster → key down + key up pair)
|
||||
//
|
||||
// The parser is intentionally minimal — just enough to cover what terminals
|
||||
// actually send. It does NOT handle mouse, focus, or DCS sequences since
|
||||
// those are irrelevant for the input record path.
|
||||
//
|
||||
// Thread safety: NOT thread-safe. All calls must be serialized by the caller
|
||||
// (Server's message loop + WriteInput are on the same thread or the
|
||||
// caller holds the console lock).
|
||||
|
||||
class InputBuffer
|
||||
{
|
||||
public:
|
||||
InputBuffer() = default;
|
||||
|
||||
// Append UTF-8 text to the input buffer.
|
||||
void write(std::string_view text);
|
||||
|
||||
// True if there is any data available for reading.
|
||||
bool hasData() const noexcept;
|
||||
|
||||
// Returns the number of pending INPUT_RECORDs.
|
||||
// This is an estimate — it counts the *minimum* number of records
|
||||
// currently parseable from the buffer.
|
||||
size_t pendingEventCount() const noexcept;
|
||||
|
||||
// Read raw VT text (for ENABLE_VIRTUAL_TERMINAL_INPUT mode).
|
||||
// Returns the number of bytes copied. `dst` is the output buffer
|
||||
// of `dstCapacity` bytes.
|
||||
size_t readRawText(char* dst, size_t dstCapacity);
|
||||
|
||||
// Generate INPUT_RECORDs from the buffer (for legacy mode).
|
||||
// Fills `dst` with up to `maxRecords` records.
|
||||
// Returns the number of records written.
|
||||
// If `peek` is true, the buffer position is not advanced.
|
||||
size_t readInputRecords(INPUT_RECORD* dst, size_t maxRecords, bool peek = false);
|
||||
|
||||
// Discard all buffered data.
|
||||
void flush();
|
||||
|
||||
private:
|
||||
// The VT parser state machine.
|
||||
enum class ParserState
|
||||
{
|
||||
Ground, // Normal text.
|
||||
Escape, // After ESC.
|
||||
CsiEntry, // After ESC [, collecting parameters.
|
||||
Ss3, // After ESC O.
|
||||
};
|
||||
|
||||
// A parsed input event (intermediate form between VT and INPUT_RECORD).
|
||||
struct ParsedKey
|
||||
{
|
||||
WORD vk = 0;
|
||||
WORD scanCode = 0;
|
||||
wchar_t ch = 0;
|
||||
DWORD modifiers = 0;
|
||||
WORD repeatCount = 1;
|
||||
bool keyDown = true;
|
||||
bool isW32IM = false; // If true, emit exactly as specified (no synthetic up/down).
|
||||
};
|
||||
|
||||
// Parse as many events as possible from m_buf starting at m_readPos.
|
||||
// Appends to m_records. Returns the new read position.
|
||||
size_t parse(size_t pos);
|
||||
|
||||
// Parse helpers — each returns the number of bytes consumed (0 = need more data).
|
||||
size_t parseGround(size_t pos);
|
||||
size_t parseCsi(size_t pos);
|
||||
size_t parseSs3(size_t pos);
|
||||
|
||||
// CSI parameter parsing. Parses semicolon-separated decimal integers
|
||||
// starting at `pos`. Returns the position after the last parameter digit.
|
||||
// Populates m_params and m_paramCount.
|
||||
size_t parseCsiParams(size_t pos);
|
||||
|
||||
// Emit an INPUT_RECORD pair (key down + key up) into m_records.
|
||||
void emitKey(const ParsedKey& key);
|
||||
|
||||
// Map a VK + modifiers to a scan code via MapVirtualKey.
|
||||
static WORD vkToScanCode(WORD vk);
|
||||
|
||||
// Map VT modifier parameter (1-based) to dwControlKeyState flags.
|
||||
static DWORD vtModifierToControlKeyState(int vtMod);
|
||||
|
||||
// Compact the buffer: discard consumed bytes.
|
||||
void compact();
|
||||
|
||||
// Raw byte buffer. Text appended by write(), consumed by read*/parse.
|
||||
std::string m_buf;
|
||||
size_t m_readPos = 0;
|
||||
|
||||
// Pre-parsed INPUT_RECORD queue. Filled by parse(), drained by readInputRecords().
|
||||
std::vector<INPUT_RECORD> m_records;
|
||||
size_t m_recordReadPos = 0;
|
||||
|
||||
// CSI parameter scratch space.
|
||||
static constexpr size_t MaxParams = 8;
|
||||
int m_params[MaxParams]{};
|
||||
size_t m_paramCount = 0;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "VtParser.h"
|
||||
|
||||
// InputBuffer: Text-based input buffer with an integrated VT parser.
|
||||
//
|
||||
// The hosting terminal writes UTF-8 VT sequences via write(). Consumers
|
||||
// (console API handlers) dequeue data in one of two forms:
|
||||
//
|
||||
// 1. As raw VT text (ENABLE_VIRTUAL_TERMINAL_INPUT is set):
|
||||
// readRawText() returns the raw bytes 1:1.
|
||||
//
|
||||
// 2. As INPUT_RECORDs (ENABLE_VIRTUAL_TERMINAL_INPUT is NOT set):
|
||||
// readInputRecords() uses VtParser to tokenize sequences on-demand
|
||||
// and converts them to key events:
|
||||
// - Win32InputMode (W32IM, CSI ... _) for full INPUT_RECORD fidelity
|
||||
// - Standard VT520 cursor/function keys (CSI A-D, CSI ~, SS3)
|
||||
// - Plain text (each codepoint -> key down + key up pair)
|
||||
//
|
||||
// Thread safety: NOT thread-safe. All calls must be serialized by the caller.
|
||||
|
||||
class InputBuffer
|
||||
{
|
||||
public:
|
||||
InputBuffer() = default;
|
||||
|
||||
// Append UTF-8 text to the input buffer.
|
||||
void write(std::string_view text);
|
||||
|
||||
// True if there is any data available for reading.
|
||||
bool hasData() const noexcept;
|
||||
|
||||
// Returns the number of pending INPUT_RECORDs (rough estimate).
|
||||
size_t pendingEventCount() const noexcept;
|
||||
|
||||
// Read raw VT text (for ENABLE_VIRTUAL_TERMINAL_INPUT mode).
|
||||
size_t readRawText(char* dst, size_t dstCapacity);
|
||||
|
||||
// Generate INPUT_RECORDs from the buffer (for legacy mode).
|
||||
size_t readInputRecords(INPUT_RECORD* dst, size_t maxRecords, bool peek = false);
|
||||
|
||||
// Discard all buffered data.
|
||||
void flush();
|
||||
|
||||
private:
|
||||
struct ParsedKey
|
||||
{
|
||||
WORD vk = 0;
|
||||
WORD scanCode = 0;
|
||||
wchar_t ch = 0;
|
||||
DWORD modifiers = 0;
|
||||
WORD repeatCount = 1;
|
||||
bool keyDown = true;
|
||||
bool isW32IM = false;
|
||||
};
|
||||
|
||||
// Convert VtTokens into INPUT_RECORDs.
|
||||
void parseTokensToRecords();
|
||||
|
||||
// Token interpretation helpers.
|
||||
void handleText(std::string_view text);
|
||||
void handleCtrl(char ch);
|
||||
void handleEsc(char ch);
|
||||
void handleSs3(char ch);
|
||||
void handleCsi(const VtCsi& csi);
|
||||
|
||||
void emitKey(const ParsedKey& key);
|
||||
|
||||
static WORD vkToScanCode(WORD vk);
|
||||
static DWORD vtModifierToControlKeyState(uint16_t vtMod);
|
||||
|
||||
void compact();
|
||||
|
||||
std::string m_buf;
|
||||
size_t m_readPos = 0;
|
||||
|
||||
VtParser m_parser;
|
||||
|
||||
std::vector<INPUT_RECORD> m_records;
|
||||
size_t m_recordReadPos = 0;
|
||||
};
|
||||
|
||||
300
src/conpty/VtParser.cpp
Normal file
300
src/conpty/VtParser.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#include "pch.h"
|
||||
#include "VtParser.h"
|
||||
|
||||
bool VtParser::hasEscTimeout() const noexcept
|
||||
{
|
||||
return m_state == State::Esc;
|
||||
}
|
||||
|
||||
VtParser::Stream VtParser::parse(std::string_view input) noexcept
|
||||
{
|
||||
return Stream{ this, input };
|
||||
}
|
||||
|
||||
// Decode one UTF-8 codepoint from the input at the current offset.
|
||||
// Advances m_off past the consumed bytes. Returns U+0000 at end-of-input.
|
||||
char32_t VtParser::Stream::nextChar()
|
||||
{
|
||||
const auto* bytes = reinterpret_cast<const uint8_t*>(m_input.data());
|
||||
const auto len = m_input.size();
|
||||
|
||||
if (m_off >= len)
|
||||
return U'\0';
|
||||
|
||||
const auto b0 = bytes[m_off];
|
||||
|
||||
// ASCII fast path.
|
||||
if (b0 < 0x80)
|
||||
{
|
||||
m_off++;
|
||||
return static_cast<char32_t>(b0);
|
||||
}
|
||||
|
||||
// Determine sequence length and initial bits.
|
||||
size_t seqLen;
|
||||
char32_t cp;
|
||||
if ((b0 & 0xE0) == 0xC0) { seqLen = 2; cp = b0 & 0x1F; }
|
||||
else if ((b0 & 0xF0) == 0xE0) { seqLen = 3; cp = b0 & 0x0F; }
|
||||
else if ((b0 & 0xF8) == 0xF0) { seqLen = 4; cp = b0 & 0x07; }
|
||||
else { m_off++; return U'\xFFFD'; } // Invalid lead byte.
|
||||
|
||||
if (m_off + seqLen > len)
|
||||
{
|
||||
// Incomplete codepoint at end of input — consume what we have.
|
||||
m_off = len;
|
||||
return U'\xFFFD';
|
||||
}
|
||||
|
||||
for (size_t i = 1; i < seqLen; i++)
|
||||
{
|
||||
const auto cont = bytes[m_off + i];
|
||||
if ((cont & 0xC0) != 0x80)
|
||||
{
|
||||
m_off += i;
|
||||
return U'\xFFFD';
|
||||
}
|
||||
cp = (cp << 6) | (cont & 0x3F);
|
||||
}
|
||||
|
||||
m_off += seqLen;
|
||||
return cp;
|
||||
}
|
||||
|
||||
bool VtParser::Stream::next(VtToken& out)
|
||||
{
|
||||
const auto* bytes = reinterpret_cast<const uint8_t*>(m_input.data());
|
||||
const auto len = m_input.size();
|
||||
|
||||
// If the previous input ended with an escape character, and we're called
|
||||
// with empty input (timeout fired), return the bare ESC.
|
||||
if (len == 0 && m_parser->m_state == State::Esc)
|
||||
{
|
||||
m_parser->m_state = State::Ground;
|
||||
out.type = VtToken::Esc;
|
||||
out.ch = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
while (m_off < len)
|
||||
{
|
||||
switch (m_parser->m_state)
|
||||
{
|
||||
case State::Ground:
|
||||
{
|
||||
const auto b = bytes[m_off];
|
||||
|
||||
if (b == 0x1B)
|
||||
{
|
||||
m_parser->m_state = State::Esc;
|
||||
m_off++;
|
||||
break; // Continue the outer loop to process the Esc state.
|
||||
}
|
||||
|
||||
if (b < 0x20 || b == 0x7F)
|
||||
{
|
||||
m_off++;
|
||||
out.type = VtToken::Ctrl;
|
||||
out.ch = static_cast<char>(b);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bulk scan printable text (>= 0x20, != 0x7F, != 0x1B).
|
||||
const auto beg = m_off;
|
||||
do {
|
||||
m_off++;
|
||||
} while (m_off < len && bytes[m_off] >= 0x20 && bytes[m_off] != 0x7F && bytes[m_off] != 0x1B);
|
||||
|
||||
out.type = VtToken::Text;
|
||||
out.payload = m_input.substr(beg, m_off - beg);
|
||||
return true;
|
||||
}
|
||||
|
||||
case State::Esc:
|
||||
{
|
||||
const auto ch = nextChar();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '[':
|
||||
m_parser->m_state = State::Csi;
|
||||
m_parser->m_csi.privateByte = '\0';
|
||||
m_parser->m_csi.finalByte = '\0';
|
||||
// Clear only params that were used last time.
|
||||
while (m_parser->m_csi.paramCount > 0)
|
||||
{
|
||||
m_parser->m_csi.paramCount--;
|
||||
m_parser->m_csi.params[m_parser->m_csi.paramCount] = 0;
|
||||
}
|
||||
break;
|
||||
case ']':
|
||||
m_parser->m_state = State::Osc;
|
||||
break;
|
||||
case 'O':
|
||||
m_parser->m_state = State::Ss3;
|
||||
break;
|
||||
case 'P':
|
||||
m_parser->m_state = State::Dcs;
|
||||
break;
|
||||
default:
|
||||
m_parser->m_state = State::Ground;
|
||||
out.type = VtToken::Esc;
|
||||
// Truncate to char. For the sequences we care about this is always ASCII.
|
||||
out.ch = static_cast<char>(ch);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case State::Ss3:
|
||||
{
|
||||
m_parser->m_state = State::Ground;
|
||||
const auto ch = nextChar();
|
||||
out.type = VtToken::SS3;
|
||||
out.ch = static_cast<char>(ch);
|
||||
return true;
|
||||
}
|
||||
|
||||
case State::Csi:
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
// Parse parameter digits.
|
||||
if (m_parser->m_csi.paramCount < std::size(m_parser->m_csi.params))
|
||||
{
|
||||
auto& dst = m_parser->m_csi.params[m_parser->m_csi.paramCount];
|
||||
while (m_off < len && bytes[m_off] >= '0' && bytes[m_off] <= '9')
|
||||
{
|
||||
const uint32_t add = bytes[m_off] - '0';
|
||||
const uint32_t value = static_cast<uint32_t>(dst) * 10 + add;
|
||||
dst = static_cast<uint16_t>(std::min(value, static_cast<uint32_t>(UINT16_MAX)));
|
||||
m_off++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Overflow: skip digits.
|
||||
while (m_off < len && bytes[m_off] >= '0' && bytes[m_off] <= '9')
|
||||
m_off++;
|
||||
}
|
||||
|
||||
// Need more data?
|
||||
if (m_off >= len)
|
||||
return false;
|
||||
|
||||
const auto c = bytes[m_off];
|
||||
m_off++;
|
||||
|
||||
if (c >= 0x40 && c <= 0x7E)
|
||||
{
|
||||
// Final byte.
|
||||
m_parser->m_state = State::Ground;
|
||||
m_parser->m_csi.finalByte = static_cast<char>(c);
|
||||
if (m_parser->m_csi.paramCount != 0 || m_parser->m_csi.params[0] != 0)
|
||||
m_parser->m_csi.paramCount++;
|
||||
out.type = VtToken::Csi;
|
||||
out.csi = &m_parser->m_csi;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == ';')
|
||||
{
|
||||
m_parser->m_csi.paramCount++;
|
||||
}
|
||||
else if (c >= '<' && c <= '?')
|
||||
{
|
||||
m_parser->m_csi.privateByte = static_cast<char>(c);
|
||||
}
|
||||
// else: intermediate bytes (0x20-0x2F) or unknown — silently skip.
|
||||
}
|
||||
}
|
||||
|
||||
case State::Osc:
|
||||
case State::Dcs:
|
||||
{
|
||||
const auto beg = m_off;
|
||||
std::string_view data;
|
||||
bool partial;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
// Scan for BEL (0x07) or ESC (0x1B) — potential terminators.
|
||||
while (m_off < len && bytes[m_off] != 0x07 && bytes[m_off] != 0x1B)
|
||||
m_off++;
|
||||
|
||||
data = m_input.substr(beg, m_off - beg);
|
||||
partial = m_off >= len;
|
||||
|
||||
if (partial)
|
||||
break;
|
||||
|
||||
const auto c = bytes[m_off];
|
||||
m_off++;
|
||||
|
||||
if (c == 0x1B)
|
||||
{
|
||||
// ESC might start ST (ESC \). Check next byte.
|
||||
if (m_off >= len)
|
||||
{
|
||||
// At end of input — save state for next chunk.
|
||||
m_parser->m_state = (m_parser->m_state == State::Osc) ? State::OscEsc : State::DcsEsc;
|
||||
partial = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytes[m_off] != '\\')
|
||||
continue; // False alarm, not ST.
|
||||
|
||||
m_off++; // Consume the backslash.
|
||||
}
|
||||
|
||||
// BEL or ESC \ — sequence is complete.
|
||||
break;
|
||||
}
|
||||
|
||||
const auto wasOsc = (m_parser->m_state == State::Osc);
|
||||
if (!partial)
|
||||
m_parser->m_state = State::Ground;
|
||||
|
||||
out.type = wasOsc ? VtToken::Osc : VtToken::Dcs;
|
||||
out.payload = data;
|
||||
out.partial = partial;
|
||||
return true;
|
||||
}
|
||||
|
||||
case State::OscEsc:
|
||||
case State::DcsEsc:
|
||||
{
|
||||
// Previous chunk ended with ESC inside an OSC/DCS.
|
||||
// Check if this chunk starts with '\' to complete the ST.
|
||||
if (bytes[m_off] == '\\')
|
||||
{
|
||||
const auto wasOsc = (m_parser->m_state == State::OscEsc);
|
||||
m_parser->m_state = State::Ground;
|
||||
m_off++;
|
||||
|
||||
out.type = wasOsc ? VtToken::Osc : VtToken::Dcs;
|
||||
out.payload = {};
|
||||
out.partial = false;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// False alarm — the ESC was not a string terminator.
|
||||
// Return it as partial payload and resume the string state.
|
||||
const auto wasOsc = (m_parser->m_state == State::OscEsc);
|
||||
m_parser->m_state = wasOsc ? State::Osc : State::Dcs;
|
||||
|
||||
out.type = wasOsc ? VtToken::Osc : VtToken::Dcs;
|
||||
out.payload = "\x1b";
|
||||
out.partial = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} // switch
|
||||
} // while
|
||||
|
||||
return false;
|
||||
}
|
||||
134
src/conpty/VtParser.h
Normal file
134
src/conpty/VtParser.h
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
// A reusable VT tokenizer, ported from the Rust implementation in Microsoft Edit.
|
||||
//
|
||||
// The parser produces tokens from a UTF-8 byte stream. It handles chunked input
|
||||
// correctly — if a sequence is split across two feed() calls, the parser buffers
|
||||
// the incomplete prefix and completes it on the next call.
|
||||
//
|
||||
// Usage:
|
||||
// VtParser parser;
|
||||
// VtParser::Stream stream = parser.parse(input);
|
||||
// VtToken token;
|
||||
// while (stream.next(token)) {
|
||||
// switch (token.type) { ... }
|
||||
// }
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
// A single CSI sequence, parsed for your convenience.
|
||||
struct VtCsi
|
||||
{
|
||||
// The parameters of the CSI sequence.
|
||||
uint16_t params[32]{};
|
||||
// The number of parameters stored in params[].
|
||||
size_t paramCount = 0;
|
||||
// The private byte, if any. '\0' if none.
|
||||
// The private byte is the first character right after the ESC [ sequence.
|
||||
// It is usually a '?' or '<'.
|
||||
char privateByte = '\0';
|
||||
// The final byte of the CSI sequence.
|
||||
// This is the last character of the sequence, e.g. 'm' or 'H'.
|
||||
char finalByte = '\0';
|
||||
};
|
||||
|
||||
struct VtToken
|
||||
{
|
||||
enum Type : uint8_t
|
||||
{
|
||||
// A bunch of text. Doesn't contain any control characters.
|
||||
Text,
|
||||
// A single control character, like backspace or return.
|
||||
Ctrl,
|
||||
// We encountered ESC x and this contains x (in `ch`).
|
||||
Esc,
|
||||
// We encountered ESC O x and this contains x (in `ch`).
|
||||
SS3,
|
||||
// A CSI sequence started with ESC [. See `csi`.
|
||||
Csi,
|
||||
// An OSC sequence started with ESC ]. May be partial (chunked).
|
||||
Osc,
|
||||
// A DCS sequence started with ESC P. May be partial (chunked).
|
||||
Dcs,
|
||||
};
|
||||
|
||||
Type type = Text;
|
||||
// For Ctrl: the control byte itself.
|
||||
// For Esc/SS3: the character after ESC / ESC O.
|
||||
char ch = '\0';
|
||||
// For Csi: pointer to the parser's Csi struct. Valid until the next next() call.
|
||||
const VtCsi* csi = nullptr;
|
||||
// For Text/Osc/Dcs: the string payload (points into the input buffer, zero-copy).
|
||||
std::string_view payload;
|
||||
// For Osc/Dcs: true if the sequence is incomplete (split across chunks).
|
||||
bool partial = false;
|
||||
};
|
||||
|
||||
class VtParser
|
||||
{
|
||||
public:
|
||||
class Stream;
|
||||
|
||||
VtParser() = default;
|
||||
|
||||
// Returns true if the parser is in the middle of an ESC sequence,
|
||||
// meaning the caller should apply a timeout before the next parse() call.
|
||||
// If the timeout fires, call parse("") to flush the bare ESC.
|
||||
bool hasEscTimeout() const noexcept;
|
||||
|
||||
// Begin parsing the given input. Returns a Stream that yields tokens.
|
||||
// The returned Stream borrows from both `this` and `input` — do not
|
||||
// modify either while the Stream is alive.
|
||||
Stream parse(std::string_view input) noexcept;
|
||||
|
||||
private:
|
||||
enum class State : uint8_t
|
||||
{
|
||||
Ground,
|
||||
Esc,
|
||||
Ss3,
|
||||
Csi,
|
||||
Osc,
|
||||
Dcs,
|
||||
OscEsc,
|
||||
DcsEsc,
|
||||
};
|
||||
|
||||
State m_state = State::Ground;
|
||||
VtCsi m_csi;
|
||||
};
|
||||
|
||||
// An iterator that yields VtTokens from a single parse() call.
|
||||
// This is a "lending iterator" — the token references data owned by
|
||||
// the parser and the input string_view.
|
||||
class VtParser::Stream
|
||||
{
|
||||
public:
|
||||
Stream(VtParser* parser, std::string_view input) noexcept
|
||||
: m_parser(parser), m_input(input) {}
|
||||
|
||||
// The input being parsed.
|
||||
std::string_view input() const noexcept { return m_input; }
|
||||
|
||||
// Current byte offset into the input.
|
||||
size_t offset() const noexcept { return m_off; }
|
||||
|
||||
// True if all input has been consumed.
|
||||
bool done() const noexcept { return m_off >= m_input.size(); }
|
||||
|
||||
// Get the next token. Returns false when no more complete tokens
|
||||
// can be extracted (remaining bytes are an incomplete sequence).
|
||||
bool next(VtToken& out);
|
||||
|
||||
// Decode and consume one UTF-8 codepoint. Returns '\0' at end.
|
||||
char32_t nextChar();
|
||||
|
||||
private:
|
||||
VtParser* m_parser;
|
||||
std::string_view m_input;
|
||||
size_t m_off = 0;
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,124 +1,589 @@
|
||||
// A minimal Win32 terminal — Windows 95 conhost style.
|
||||
// Fixed-size char32_t grid, GDI rendering, no Unicode shaping, no scrollback.
|
||||
// Implements IPtyHost and uses VtParser to interpret output from the server.
|
||||
|
||||
#define NOMINMAX
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <wil/com.h>
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include <conpty.h>
|
||||
#include "../VtParser.h"
|
||||
|
||||
static std::string visualize_control_codes(std::string_view str) noexcept
|
||||
// ============================================================================
|
||||
// Terminal grid
|
||||
// ============================================================================
|
||||
|
||||
static constexpr SHORT COLS = 120;
|
||||
static constexpr SHORT ROWS = 30;
|
||||
static constexpr int CELL_W = 8;
|
||||
static constexpr int CELL_H = 16;
|
||||
|
||||
struct Cell
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(str.size() * 3);
|
||||
for (size_t i = 0; i < str.size();)
|
||||
char32_t ch = ' ';
|
||||
WORD attr = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
|
||||
};
|
||||
|
||||
struct TermState
|
||||
{
|
||||
Cell grid[ROWS][COLS]{};
|
||||
SHORT cursorX = 0;
|
||||
SHORT cursorY = 0;
|
||||
WORD currentAttr = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
|
||||
bool cursorVisible = true;
|
||||
std::wstring title = L"conpty-test";
|
||||
|
||||
COLORREF colorTable[16] = {
|
||||
0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xC0C0C0,
|
||||
0x808080, 0xFF0000, 0x00FF00, 0xFFFF00, 0x0000FF, 0xFF00FF, 0x00FFFF, 0xFFFFFF,
|
||||
};
|
||||
|
||||
void scrollUp()
|
||||
{
|
||||
const auto ch = static_cast<unsigned char>(str[i]);
|
||||
if (ch < 0x20 || ch == 0x7f)
|
||||
{
|
||||
// Map C0 controls to U+2400..U+241F and DEL to U+2421.
|
||||
const char32_t cp = ch == 0x7f ? 0x2421 : 0x2400 + ch;
|
||||
out += static_cast<char>(0xE0 | (cp >> 12));
|
||||
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
else if (ch == 0x20)
|
||||
{
|
||||
// Replace space with ␣ (U+2423).
|
||||
out += "\xE2\x90\xA3";
|
||||
}
|
||||
else
|
||||
{
|
||||
out += static_cast<char>(ch);
|
||||
}
|
||||
++i;
|
||||
memmove(&grid[0], &grid[1], sizeof(Cell) * COLS * (ROWS - 1));
|
||||
for (SHORT x = 0; x < COLS; x++)
|
||||
grid[ROWS - 1][x] = Cell{};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string u16u8(std::wstring_view str) noexcept
|
||||
{
|
||||
std::string out;
|
||||
out.resize(str.size() * 3); // worst case
|
||||
const auto len = WideCharToMultiByte(CP_UTF8, 0, str.data(), static_cast<int>(str.size()), out.data(), static_cast<int>(out.size()), nullptr, nullptr);
|
||||
out.resize(std::max(0, len));
|
||||
return out;
|
||||
}
|
||||
|
||||
static void write_stdout(std::string_view str) noexcept
|
||||
{
|
||||
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), str.data(), static_cast<DWORD>(str.size()), nullptr, nullptr);
|
||||
}
|
||||
|
||||
struct Host : IPtyHost
|
||||
{
|
||||
HRESULT QueryInterface(const IID& riid, void** ppvObject)
|
||||
void advanceCursor()
|
||||
{
|
||||
if (ppvObject == nullptr)
|
||||
cursorX++;
|
||||
if (cursorX >= COLS)
|
||||
{
|
||||
return E_POINTER;
|
||||
cursorX = 0;
|
||||
cursorY++;
|
||||
if (cursorY >= ROWS)
|
||||
{
|
||||
cursorY = ROWS - 1;
|
||||
scrollUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void linefeed()
|
||||
{
|
||||
cursorY++;
|
||||
if (cursorY >= ROWS)
|
||||
{
|
||||
cursorY = ROWS - 1;
|
||||
scrollUp();
|
||||
}
|
||||
}
|
||||
|
||||
void putChar(char32_t ch)
|
||||
{
|
||||
if (cursorX < COLS && cursorY < ROWS)
|
||||
{
|
||||
grid[cursorY][cursorX] = { ch, currentAttr };
|
||||
advanceCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void eraseDisplay(int mode)
|
||||
{
|
||||
const Cell blank = { ' ', currentAttr };
|
||||
if (mode == 0)
|
||||
{
|
||||
for (SHORT x = cursorX; x < COLS; x++) grid[cursorY][x] = blank;
|
||||
for (SHORT y = cursorY + 1; y < ROWS; y++)
|
||||
for (SHORT x = 0; x < COLS; x++) grid[y][x] = blank;
|
||||
}
|
||||
else if (mode == 1)
|
||||
{
|
||||
for (SHORT y = 0; y < cursorY; y++)
|
||||
for (SHORT x = 0; x < COLS; x++) grid[y][x] = blank;
|
||||
for (SHORT x = 0; x <= cursorX && x < COLS; x++) grid[cursorY][x] = blank;
|
||||
}
|
||||
else if (mode == 2 || mode == 3)
|
||||
{
|
||||
for (SHORT y = 0; y < ROWS; y++)
|
||||
for (SHORT x = 0; x < COLS; x++)
|
||||
grid[y][x] = blank;
|
||||
}
|
||||
}
|
||||
|
||||
void eraseLine(int mode)
|
||||
{
|
||||
const Cell blank = { ' ', currentAttr };
|
||||
SHORT start = 0, end = COLS;
|
||||
if (mode == 0) start = cursorX;
|
||||
else if (mode == 1) end = cursorX + 1;
|
||||
for (SHORT x = start; x < end; x++)
|
||||
grid[cursorY][x] = blank;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Globals
|
||||
// ============================================================================
|
||||
|
||||
static TermState g_term;
|
||||
static VtParser g_vtParser;
|
||||
static HWND g_hwnd = nullptr;
|
||||
static wil::com_ptr<IPtyServer> g_server;
|
||||
static CRITICAL_SECTION g_lock;
|
||||
|
||||
static void invalidate()
|
||||
{
|
||||
if (g_hwnd)
|
||||
InvalidateRect(g_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VT output interpreter
|
||||
// ============================================================================
|
||||
|
||||
static COLORREF attrToFg(WORD attr)
|
||||
{
|
||||
return g_term.colorTable[attr & 0x0F];
|
||||
}
|
||||
|
||||
static COLORREF attrToBg(WORD attr)
|
||||
{
|
||||
return g_term.colorTable[(attr >> 4) & 0x0F];
|
||||
}
|
||||
|
||||
static void parseSGR(const VtCsi& csi)
|
||||
{
|
||||
if (csi.paramCount == 0)
|
||||
{
|
||||
g_term.currentAttr = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
|
||||
return;
|
||||
}
|
||||
|
||||
// VT→Console color index: VT uses RGB bit order, Console uses BGR.
|
||||
static constexpr WORD vtToConsole[] = { 0, 4, 2, 6, 1, 5, 3, 7 };
|
||||
|
||||
for (size_t i = 0; i < csi.paramCount; i++)
|
||||
{
|
||||
const auto p = csi.params[i];
|
||||
switch (p)
|
||||
{
|
||||
case 0: g_term.currentAttr = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; break;
|
||||
case 1: g_term.currentAttr |= FOREGROUND_INTENSITY; break;
|
||||
case 7: g_term.currentAttr |= COMMON_LVB_REVERSE_VIDEO; break;
|
||||
case 22: g_term.currentAttr &= ~FOREGROUND_INTENSITY; break;
|
||||
case 27: g_term.currentAttr &= ~COMMON_LVB_REVERSE_VIDEO; break;
|
||||
case 39: g_term.currentAttr = (g_term.currentAttr & ~0x0F) | 0x07; break;
|
||||
case 49: g_term.currentAttr &= ~0xF0; break;
|
||||
default:
|
||||
if (p >= 30 && p <= 37)
|
||||
g_term.currentAttr = (g_term.currentAttr & ~0x07) | vtToConsole[p - 30];
|
||||
else if (p >= 40 && p <= 47)
|
||||
g_term.currentAttr = (g_term.currentAttr & ~0x70) | (vtToConsole[p - 40] << 4);
|
||||
else if (p >= 90 && p <= 97)
|
||||
g_term.currentAttr = (g_term.currentAttr & ~0x0F) | vtToConsole[p - 90] | FOREGROUND_INTENSITY;
|
||||
else if (p >= 100 && p <= 107)
|
||||
g_term.currentAttr = (g_term.currentAttr & ~0xF0) | (vtToConsole[p - 100] << 4) | BACKGROUND_INTENSITY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void handleCsiOutput(const VtCsi& csi)
|
||||
{
|
||||
const auto p0 = (csi.paramCount >= 1) ? csi.params[0] : 0;
|
||||
const auto p1 = (csi.paramCount >= 2) ? csi.params[1] : 0;
|
||||
|
||||
switch (csi.finalByte)
|
||||
{
|
||||
case 'A': g_term.cursorY = std::max<SHORT>(0, g_term.cursorY - std::max<SHORT>(1, (SHORT)p0)); break;
|
||||
case 'B': g_term.cursorY = std::min<SHORT>(ROWS - 1, g_term.cursorY + std::max<SHORT>(1, (SHORT)p0)); break;
|
||||
case 'C': g_term.cursorX = std::min<SHORT>(COLS - 1, g_term.cursorX + std::max<SHORT>(1, (SHORT)p0)); break;
|
||||
case 'D': g_term.cursorX = std::max<SHORT>(0, g_term.cursorX - std::max<SHORT>(1, (SHORT)p0)); break;
|
||||
case 'H':
|
||||
case 'f':
|
||||
g_term.cursorY = std::clamp<SHORT>(p0 > 0 ? (SHORT)(p0 - 1) : 0, 0, ROWS - 1);
|
||||
g_term.cursorX = std::clamp<SHORT>(p1 > 0 ? (SHORT)(p1 - 1) : 0, 0, COLS - 1);
|
||||
break;
|
||||
case 'J': g_term.eraseDisplay(p0); break;
|
||||
case 'K': g_term.eraseLine(p0); break;
|
||||
case 'm': parseSGR(csi); break;
|
||||
case 'h':
|
||||
if (csi.privateByte == '?' && p0 == 25) g_term.cursorVisible = true;
|
||||
break;
|
||||
case 'l':
|
||||
if (csi.privateByte == '?' && p0 == 25) g_term.cursorVisible = false;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static void processOutput(std::string_view text)
|
||||
{
|
||||
auto stream = g_vtParser.parse(text);
|
||||
VtToken token;
|
||||
|
||||
while (stream.next(token))
|
||||
{
|
||||
switch (token.type)
|
||||
{
|
||||
case VtToken::Text:
|
||||
{
|
||||
const auto* bytes = reinterpret_cast<const uint8_t*>(token.payload.data());
|
||||
size_t i = 0;
|
||||
while (i < token.payload.size())
|
||||
{
|
||||
uint32_t cp;
|
||||
size_t seqLen;
|
||||
const auto b = bytes[i];
|
||||
if (b < 0x80) { cp = b; seqLen = 1; }
|
||||
else if ((b & 0xE0) == 0xC0) { cp = b & 0x1F; seqLen = 2; }
|
||||
else if ((b & 0xF0) == 0xE0) { cp = b & 0x0F; seqLen = 3; }
|
||||
else if ((b & 0xF8) == 0xF0) { cp = b & 0x07; seqLen = 4; }
|
||||
else { i++; continue; }
|
||||
if (i + seqLen > token.payload.size()) break;
|
||||
for (size_t j = 1; j < seqLen; j++)
|
||||
cp = (cp << 6) | (bytes[i + j] & 0x3F);
|
||||
i += seqLen;
|
||||
g_term.putChar(cp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VtToken::Ctrl:
|
||||
switch (token.ch)
|
||||
{
|
||||
case '\r': g_term.cursorX = 0; break;
|
||||
case '\n': g_term.linefeed(); break;
|
||||
case '\b': if (g_term.cursorX > 0) g_term.cursorX--; break;
|
||||
case '\t': g_term.cursorX = std::min<SHORT>(COLS - 1, (g_term.cursorX + 8) & ~7); break;
|
||||
case '\a': MessageBeep(MB_OK); break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
case VtToken::Csi:
|
||||
handleCsiOutput(*token.csi);
|
||||
break;
|
||||
case VtToken::Osc:
|
||||
if (!token.partial && token.payload.size() >= 2 &&
|
||||
(token.payload[0] == '0' || token.payload[0] == '2') && token.payload[1] == ';')
|
||||
{
|
||||
auto data = token.payload.substr(2);
|
||||
const auto wLen = MultiByteToWideChar(CP_UTF8, 0, data.data(), (int)data.size(), nullptr, 0);
|
||||
if (wLen > 0)
|
||||
{
|
||||
g_term.title.resize(wLen);
|
||||
MultiByteToWideChar(CP_UTF8, 0, data.data(), (int)data.size(), g_term.title.data(), wLen);
|
||||
if (g_hwnd) SetWindowTextW(g_hwnd, g_term.title.c_str());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// IPtyHost implementation
|
||||
// ============================================================================
|
||||
|
||||
struct TestHost : IPtyHost
|
||||
{
|
||||
HRESULT QueryInterface(const IID& riid, void** ppvObject) override
|
||||
{
|
||||
if (!ppvObject) return E_POINTER;
|
||||
if (riid == __uuidof(IPtyHost) || riid == __uuidof(IUnknown))
|
||||
{
|
||||
*ppvObject = static_cast<IPtyHost*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
*ppvObject = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG AddRef()
|
||||
ULONG AddRef() override { return m_refCount.fetch_add(1) + 1; }
|
||||
ULONG Release() override
|
||||
{
|
||||
return m_refCount.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
const auto c = m_refCount.fetch_sub(1) - 1;
|
||||
if (c == 0) delete this;
|
||||
return c;
|
||||
}
|
||||
|
||||
ULONG Release()
|
||||
void WriteUTF8(PTY_UTF8_STRING text) override
|
||||
{
|
||||
const auto count = m_refCount.fetch_sub(1, std::memory_order_relaxed) - 1;
|
||||
if (count == 0)
|
||||
EnterCriticalSection(&g_lock);
|
||||
processOutput({ text.data, text.length });
|
||||
LeaveCriticalSection(&g_lock);
|
||||
}
|
||||
|
||||
void WriteUTF16(PTY_UTF16_STRING text) override
|
||||
{
|
||||
const auto len = WideCharToMultiByte(CP_UTF8, 0, text.data, (int)text.length, nullptr, 0, nullptr, nullptr);
|
||||
if (len > 0)
|
||||
{
|
||||
delete this;
|
||||
std::string utf8(len, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, text.data, (int)text.length, utf8.data(), len, nullptr, nullptr);
|
||||
EnterCriticalSection(&g_lock);
|
||||
processOutput(utf8);
|
||||
LeaveCriticalSection(&g_lock);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void HandleUTF8Output(LPCSTR data, SIZE_T length) override
|
||||
HRESULT CreateBuffer(void** buffer) override { *buffer = nullptr; return E_NOTIMPL; }
|
||||
HRESULT ReleaseBuffer(void*) override { return S_OK; }
|
||||
HRESULT ActivateBuffer(void*) override { return S_OK; }
|
||||
|
||||
HRESULT GetScreenBufferInfo(PTY_SCREEN_BUFFER_INFO* info) override
|
||||
{
|
||||
write_stdout(visualize_control_codes({ data, length }));
|
||||
EnterCriticalSection(&g_lock);
|
||||
*info = {};
|
||||
info->Size = { COLS, ROWS };
|
||||
info->CursorPosition = { g_term.cursorX, g_term.cursorY };
|
||||
info->Attributes = g_term.currentAttr;
|
||||
info->Window = { 0, 0, COLS - 1, ROWS - 1 };
|
||||
info->MaximumWindowSize = { COLS, ROWS };
|
||||
info->CursorSize = 25;
|
||||
info->CursorVisible = g_term.cursorVisible;
|
||||
info->FontSize = { CELL_W, CELL_H };
|
||||
info->FontFamily = FF_MODERN | FIXED_PITCH;
|
||||
info->FontWeight = FW_NORMAL;
|
||||
wcscpy_s(info->FaceName, L"Terminal");
|
||||
memcpy(info->ColorTable, g_term.colorTable, sizeof(g_term.colorTable));
|
||||
LeaveCriticalSection(&g_lock);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
void HandleUTF16Output(LPCWSTR data, SIZE_T length) override
|
||||
HRESULT SetScreenBufferInfo(const PTY_SCREEN_BUFFER_INFO_CHANGE* change) override
|
||||
{
|
||||
write_stdout(visualize_control_codes(u16u8({ data, length })));
|
||||
EnterCriticalSection(&g_lock);
|
||||
if (change->CursorPosition)
|
||||
{
|
||||
g_term.cursorX = std::clamp(change->CursorPosition->X, SHORT(0), SHORT(COLS - 1));
|
||||
g_term.cursorY = std::clamp(change->CursorPosition->Y, SHORT(0), SHORT(ROWS - 1));
|
||||
}
|
||||
if (change->Attributes) g_term.currentAttr = *change->Attributes;
|
||||
if (change->CursorVisible) g_term.cursorVisible = *change->CursorVisible != 0;
|
||||
if (change->ColorTable) memcpy(g_term.colorTable, change->ColorTable, sizeof(g_term.colorTable));
|
||||
invalidate();
|
||||
LeaveCriticalSection(&g_lock);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT ReadBuffer(COORD pos, LONG count, PTY_CHAR_INFO* infos) override
|
||||
{
|
||||
EnterCriticalSection(&g_lock);
|
||||
for (LONG i = 0; i < count; i++)
|
||||
{
|
||||
SHORT x = pos.X + static_cast<SHORT>(i);
|
||||
SHORT y = pos.Y;
|
||||
while (x >= COLS && y < ROWS) { x -= COLS; y++; }
|
||||
if (y >= 0 && y < ROWS && x >= 0 && x < COLS)
|
||||
{
|
||||
infos[i].Char = static_cast<WCHAR>(g_term.grid[y][x].ch <= 0xFFFF ? g_term.grid[y][x].ch : L'?');
|
||||
infos[i].Attributes = g_term.grid[y][x].attr;
|
||||
}
|
||||
else
|
||||
{
|
||||
infos[i].Char = L' ';
|
||||
infos[i].Attributes = 0x07;
|
||||
}
|
||||
}
|
||||
LeaveCriticalSection(&g_lock);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT GetConsoleWindow(HWND* hwnd) override { *hwnd = g_hwnd; return S_OK; }
|
||||
|
||||
private:
|
||||
std::atomic<ULONG> m_refCount{ 1 };
|
||||
};
|
||||
|
||||
int main()
|
||||
{
|
||||
SetConsoleCP(CP_UTF8);
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
// ============================================================================
|
||||
// GDI rendering
|
||||
// ============================================================================
|
||||
|
||||
wil::com_ptr<IPtyServer> server;
|
||||
THROW_IF_FAILED(PtyCreateServer(IID_PPV_ARGS(server.addressof())));
|
||||
THROW_IF_FAILED(server->SetHost(new Host()));
|
||||
static void paint(HWND hwnd, HDC hdc)
|
||||
{
|
||||
EnterCriticalSection(&g_lock);
|
||||
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
const auto bgBrush = CreateSolidBrush(g_term.colorTable[0]);
|
||||
FillRect(hdc, &rc, bgBrush);
|
||||
DeleteObject(bgBrush);
|
||||
|
||||
const auto font = CreateFontW(
|
||||
CELL_H, CELL_W, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
||||
NONANTIALIASED_QUALITY, FIXED_PITCH | FF_MODERN, L"Terminal");
|
||||
const auto oldFont = SelectObject(hdc, font);
|
||||
SetBkMode(hdc, OPAQUE);
|
||||
|
||||
for (SHORT y = 0; y < ROWS; y++)
|
||||
{
|
||||
for (SHORT x = 0; x < COLS; x++)
|
||||
{
|
||||
const auto& cell = g_term.grid[y][x];
|
||||
COLORREF fg, bg;
|
||||
if (cell.attr & COMMON_LVB_REVERSE_VIDEO)
|
||||
{
|
||||
fg = attrToBg(cell.attr);
|
||||
bg = attrToFg(cell.attr);
|
||||
}
|
||||
else
|
||||
{
|
||||
fg = attrToFg(cell.attr);
|
||||
bg = attrToBg(cell.attr);
|
||||
}
|
||||
|
||||
SetTextColor(hdc, fg);
|
||||
SetBkColor(hdc, bg);
|
||||
|
||||
wchar_t wch = static_cast<wchar_t>(cell.ch <= 0xFFFF ? cell.ch : L'?');
|
||||
if (wch < 0x20) wch = L' ';
|
||||
TextOutW(hdc, x * CELL_W, y * CELL_H, &wch, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_term.cursorVisible)
|
||||
{
|
||||
RECT cur = { g_term.cursorX * CELL_W, g_term.cursorY * CELL_H + CELL_H - 2,
|
||||
g_term.cursorX * CELL_W + CELL_W, g_term.cursorY * CELL_H + CELL_H };
|
||||
const auto curBrush = CreateSolidBrush(g_term.colorTable[7]);
|
||||
FillRect(hdc, &cur, curBrush);
|
||||
DeleteObject(curBrush);
|
||||
}
|
||||
|
||||
SelectObject(hdc, oldFont);
|
||||
DeleteObject(font);
|
||||
LeaveCriticalSection(&g_lock);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Window procedure
|
||||
// ============================================================================
|
||||
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
const auto hdc = BeginPaint(hwnd, &ps);
|
||||
paint(hwnd, hdc);
|
||||
EndPaint(hwnd, &ps);
|
||||
return 0;
|
||||
}
|
||||
case WM_CHAR:
|
||||
{
|
||||
wchar_t wch = static_cast<wchar_t>(wParam);
|
||||
char utf8[4];
|
||||
const auto len = WideCharToMultiByte(CP_UTF8, 0, &wch, 1, utf8, sizeof(utf8), nullptr, nullptr);
|
||||
if (len > 0 && g_server)
|
||||
{
|
||||
PTY_UTF8_STRING input{ utf8, static_cast<SIZE_T>(len) };
|
||||
g_server->WriteUTF8(input);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_KEYDOWN:
|
||||
{
|
||||
const char* seq = nullptr;
|
||||
switch (wParam)
|
||||
{
|
||||
case VK_UP: seq = "\x1b[A"; break;
|
||||
case VK_DOWN: seq = "\x1b[B"; break;
|
||||
case VK_RIGHT: seq = "\x1b[C"; break;
|
||||
case VK_LEFT: seq = "\x1b[D"; break;
|
||||
case VK_HOME: seq = "\x1b[H"; break;
|
||||
case VK_END: seq = "\x1b[F"; break;
|
||||
case VK_INSERT: seq = "\x1b[2~"; break;
|
||||
case VK_DELETE: seq = "\x1b[3~"; break;
|
||||
case VK_PRIOR: seq = "\x1b[5~"; break;
|
||||
case VK_NEXT: seq = "\x1b[6~"; break;
|
||||
case VK_F1: seq = "\x1bOP"; break;
|
||||
case VK_F2: seq = "\x1bOQ"; break;
|
||||
case VK_F3: seq = "\x1bOR"; break;
|
||||
case VK_F4: seq = "\x1bOS"; break;
|
||||
case VK_F5: seq = "\x1b[15~"; break;
|
||||
case VK_F6: seq = "\x1b[17~"; break;
|
||||
case VK_F7: seq = "\x1b[18~"; break;
|
||||
case VK_F8: seq = "\x1b[19~"; break;
|
||||
case VK_F9: seq = "\x1b[20~"; break;
|
||||
case VK_F10: seq = "\x1b[21~"; break;
|
||||
case VK_F11: seq = "\x1b[23~"; break;
|
||||
case VK_F12: seq = "\x1b[24~"; break;
|
||||
default: return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
if (seq && g_server)
|
||||
{
|
||||
PTY_UTF8_STRING input{ seq, strlen(seq) };
|
||||
g_server->WriteUTF8(input);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
default:
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow)
|
||||
{
|
||||
InitializeCriticalSection(&g_lock);
|
||||
|
||||
WNDCLASSEXW wc{};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wc.lpfnWndProc = WndProc;
|
||||
wc.hInstance = hInstance;
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
wc.lpszClassName = L"ConPtyTestWindow";
|
||||
RegisterClassExW(&wc);
|
||||
|
||||
RECT wr = { 0, 0, COLS * CELL_W, ROWS * CELL_H };
|
||||
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
|
||||
g_hwnd = CreateWindowExW(
|
||||
0, L"ConPtyTestWindow", L"conpty-test",
|
||||
WS_OVERLAPPEDWINDOW & ~(WS_THICKFRAME | WS_MAXIMIZEBOX),
|
||||
CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
wr.right - wr.left, wr.bottom - wr.top,
|
||||
nullptr, nullptr, hInstance, nullptr);
|
||||
|
||||
ShowWindow(g_hwnd, nCmdShow);
|
||||
UpdateWindow(g_hwnd);
|
||||
|
||||
THROW_IF_FAILED(PtyCreateServer(IID_PPV_ARGS(g_server.addressof())));
|
||||
THROW_IF_FAILED(g_server->SetHost(new TestHost()));
|
||||
|
||||
wil::unique_process_information pi;
|
||||
THROW_IF_FAILED(server->CreateProcessW(
|
||||
nullptr,
|
||||
_wcsdup(L"C:\\Windows\\System32\\edit.exe"),
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
0,
|
||||
nullptr,
|
||||
nullptr,
|
||||
THROW_IF_FAILED(g_server->CreateProcessW(
|
||||
nullptr, _wcsdup(L"cmd.exe"),
|
||||
nullptr, nullptr, FALSE, 0, nullptr, nullptr,
|
||||
pi.addressof()));
|
||||
|
||||
THROW_IF_FAILED(server->Run());
|
||||
// Run the console server on a background thread.
|
||||
// It blocks in its message loop until all clients disconnect.
|
||||
std::thread serverThread([&] {
|
||||
g_server->Run();
|
||||
PostMessage(g_hwnd, WM_CLOSE, 0, 0);
|
||||
});
|
||||
serverThread.detach();
|
||||
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
DeleteCriticalSection(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<ClCompile Include="Server.msg.l2.cpp" />
|
||||
<ClCompile Include="Server.msg.l3.cpp" />
|
||||
<ClCompile Include="Server.raw.cpp" />
|
||||
<ClCompile Include="VtParser.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="conpty.idl">
|
||||
@@ -53,6 +54,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="InputBuffer.h" />
|
||||
<ClInclude Include="Server.h" />
|
||||
<ClInclude Include="VtParser.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(SolutionDir)src\common.build.post.props" />
|
||||
<Import Project="$(SolutionDir)src\common.nugetversions.targets" />
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
<ClCompile Include="InputBuffer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="VtParser.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="conpty.idl">
|
||||
@@ -63,5 +66,8 @@
|
||||
<ClInclude Include="InputBuffer.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="VtParser.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user