Recognize terminal mouse input and generate MOUSE_EVENT records.

Tracked by https://github.com/rprichard/winpty/issues/57
This commit is contained in:
Ryan Prichard 2015-12-16 04:00:41 -06:00
parent d4b4cb6bcc
commit 48ddc93ed9
3 changed files with 333 additions and 88 deletions

View File

@ -371,6 +371,7 @@ void Agent::updateMouseInputFlag(bool forceTrace)
newFlag ? "enabled" : "disabled"); newFlag ? "enabled" : "disabled");
} }
m_consoleMouseInputFlag = newFlag; m_consoleMouseInputFlag = newFlag;
m_consoleInput->setMouseInputEnabled(newFlag);
} }
void Agent::onPollTimeout() void Agent::onPollTimeout()
@ -584,6 +585,7 @@ void Agent::syncConsoleContentAndSize(bool forceResize)
syncConsoleTitle(); syncConsoleTitle();
const ConsoleScreenBufferInfo info = m_console->bufferInfo(); const ConsoleScreenBufferInfo info = m_console->bufferInfo();
m_consoleInput->setMouseWindowRect(info.windowRect());
// If an app resizes the buffer height, then we enter "direct mode", where // If an app resizes the buffer height, then we enter "direct mode", where
// we stop trying to track incremental console changes. // we stop trying to track incremental console changes.

View File

@ -23,8 +23,11 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <algorithm>
#include <sstream>
#include <string> #include <string>
#include "DebugShowInput.h"
#include "DefaultInputMap.h" #include "DefaultInputMap.h"
#include "DsrSender.h" #include "DsrSender.h"
#include "Win32Console.h" #include "Win32Console.h"
@ -35,13 +38,181 @@
#define MAPVK_VK_TO_VSC 0 #define MAPVK_VK_TO_VSC 0
#endif #endif
const int kIncompleteEscapeTimeoutMs = 1000; namespace {
struct MouseRecord {
bool release;
int flags;
COORD coord;
std::string toString() const;
};
std::string MouseRecord::toString() const {
std::stringstream ss;
ss << "pos=" << std::dec << coord.X << "," << coord.Y
<< " flags=0x"
<< std::hex << flags;
if (release) {
ss << " release";
}
return ss.str();
}
const int kIncompleteEscapeTimeoutMs = 1000u;
#define CHECK(cond) \
do { \
if (!(cond)) { return 0; } \
} while(0)
#define ADVANCE() \
do { \
pch++; \
if (pch == stop) { return -1; } \
} while(0)
#define SCAN_INT(out, maxLen) \
do { \
(out) = 0; \
CHECK(isdigit(*pch)); \
const char *begin = pch; \
do { \
CHECK(pch - begin + 1 < maxLen); \
(out) = (out) * 10 + *pch - '0'; \
ADVANCE(); \
} while (isdigit(*pch)); \
} while(0)
#define SCAN_SIGNED_INT(out, maxLen) \
do { \
bool negative = false; \
if (*pch == '-') { \
negative = true; \
ADVANCE(); \
} \
SCAN_INT(out, maxLen); \
if (negative) { \
(out) = -(out); \
} \
} while(0)
// Match the Device Status Report console input: ESC [ nn ; mm R
// Returns:
// 0 no match
// >0 match, returns length of match
// -1 incomplete match
static int matchDsr(const char *input, int inputSize)
{
int32_t dummy = 0;
const char *pch = input;
const char *stop = input + inputSize;
CHECK(*pch == '\x1B'); ADVANCE();
CHECK(*pch == '['); ADVANCE();
SCAN_INT(dummy, 8);
CHECK(*pch == ';'); ADVANCE();
SCAN_INT(dummy, 8);
CHECK(*pch == 'R');
return pch - input + 1;
}
static int matchMouseDefault(const char *input, int inputSize,
MouseRecord &out)
{
const char *pch = input;
const char *stop = input + inputSize;
CHECK(*pch == '\x1B'); ADVANCE();
CHECK(*pch == '['); ADVANCE();
CHECK(*pch == 'M'); ADVANCE();
out.flags = (*pch - 32) & 0xFF; ADVANCE();
out.coord.X = (*pch - '!') & 0xFF;
ADVANCE();
out.coord.Y = (*pch - '!') & 0xFF;
out.release = false;
return pch - input + 1;
}
static int matchMouse1006(const char *input, int inputSize, MouseRecord &out)
{
const char *pch = input;
const char *stop = input + inputSize;
int32_t temp;
CHECK(*pch == '\x1B'); ADVANCE();
CHECK(*pch == '['); ADVANCE();
CHECK(*pch == '<'); ADVANCE();
SCAN_INT(out.flags, 8);
CHECK(*pch == ';'); ADVANCE();
SCAN_SIGNED_INT(temp, 8); out.coord.X = temp - 1;
CHECK(*pch == ';'); ADVANCE();
SCAN_SIGNED_INT(temp, 8); out.coord.Y = temp - 1;
CHECK(*pch == 'M' || *pch == 'm');
out.release = (*pch == 'm');
return pch - input + 1;
}
static int matchMouse1015(const char *input, int inputSize, MouseRecord &out)
{
const char *pch = input;
const char *stop = input + inputSize;
int32_t temp;
CHECK(*pch == '\x1B'); ADVANCE();
CHECK(*pch == '['); ADVANCE();
SCAN_INT(out.flags, 8); out.flags -= 32;
CHECK(*pch == ';'); ADVANCE();
SCAN_SIGNED_INT(temp, 8); out.coord.X = temp - 1;
CHECK(*pch == ';'); ADVANCE();
SCAN_SIGNED_INT(temp, 8); out.coord.Y = temp - 1;
CHECK(*pch == 'M');
out.release = false;
return pch - input + 1;
}
static int matchMouseRecord(const char *input, int inputSize, MouseRecord &out)
{
memset(&out, 0, sizeof(out));
int ret;
if ((ret = matchMouse1006(input, inputSize, out)) != 0) { return ret; }
if ((ret = matchMouse1015(input, inputSize, out)) != 0) { return ret; }
if ((ret = matchMouseDefault(input, inputSize, out)) != 0) { return ret; }
return 0;
}
#undef CHECK
#undef ADVANCE
#undef SCAN_INT
// Return the byte size of a UTF-8 character using the value of the first
// byte.
static int utf8CharLength(char firstByte)
{
// This code would probably be faster if it used __builtin_clz.
if ((firstByte & 0x80) == 0) {
return 1;
} else if ((firstByte & 0xE0) == 0xC0) {
return 2;
} else if ((firstByte & 0xF0) == 0xE0) {
return 3;
} else if ((firstByte & 0xF8) == 0xF0) {
return 4;
} else if ((firstByte & 0xFC) == 0xF8) {
return 5;
} else if ((firstByte & 0xFE) == 0xFC) {
return 6;
} else {
// Malformed UTF-8.
return 1;
}
}
} // anonymous namespace
ConsoleInput::ConsoleInput(DsrSender *dsrSender) : ConsoleInput::ConsoleInput(DsrSender *dsrSender) :
m_console(new Win32Console), m_console(new Win32Console),
m_dsrSender(dsrSender), m_dsrSender(dsrSender),
m_dsrSent(false), m_dsrSent(false),
m_lastWriteTick(0) m_lastWriteTick(0),
m_mouseButtonState(0),
m_mouseInputEnabled(false)
{ {
addDefaultEntriesToInputMap(m_inputMap); addDefaultEntriesToInputMap(m_inputMap);
if (hasDebugFlag("dump_input_map")) { if (hasDebugFlag("dump_input_map")) {
@ -102,7 +273,7 @@ void ConsoleInput::writeInput(const std::string &input)
void ConsoleInput::flushIncompleteEscapeCode() void ConsoleInput::flushIncompleteEscapeCode()
{ {
if (!m_byteQueue.empty() && if (!m_byteQueue.empty() &&
(int)(GetTickCount() - m_lastWriteTick) > kIncompleteEscapeTimeoutMs) { (GetTickCount() - m_lastWriteTick) > kIncompleteEscapeTimeoutMs) {
doWrite(true); doWrite(true);
m_byteQueue.clear(); m_byteQueue.clear();
} }
@ -114,7 +285,7 @@ void ConsoleInput::doWrite(bool isEof)
std::vector<INPUT_RECORD> records; std::vector<INPUT_RECORD> records;
size_t idx = 0; size_t idx = 0;
while (idx < m_byteQueue.size()) { while (idx < m_byteQueue.size()) {
int charSize = scanKeyPress(records, &data[idx], m_byteQueue.size() - idx, isEof); int charSize = scanInput(records, &data[idx], m_byteQueue.size() - idx, isEof);
if (charSize == -1) if (charSize == -1)
break; break;
idx += charSize; idx += charSize;
@ -123,12 +294,12 @@ void ConsoleInput::doWrite(bool isEof)
m_console->writeInput(records.data(), records.size()); m_console->writeInput(records.data(), records.size());
} }
int ConsoleInput::scanKeyPress(std::vector<INPUT_RECORD> &records, int ConsoleInput::scanInput(std::vector<INPUT_RECORD> &records,
const char *input, const char *input,
int inputSize, int inputSize,
bool isEof) bool isEof)
{ {
//trace("scanKeyPress: %d bytes", inputSize); ASSERT(inputSize >= 1);
// Ctrl-C. // Ctrl-C.
if (input[0] == '\x03' && m_console->processedInputMode()) { if (input[0] == '\x03' && m_console->processedInputMode()) {
@ -138,16 +309,23 @@ int ConsoleInput::scanKeyPress(std::vector<INPUT_RECORD> &records,
return 1; return 1;
} }
// Attempt to match the Device Status Report (DSR) reply. if (input[0] == '\x1B') {
int dsrLen = matchDsr(input, inputSize); // Attempt to match the Device Status Report (DSR) reply.
if (dsrLen > 0) { int dsrLen = matchDsr(input, inputSize);
trace("Received a DSR reply"); if (dsrLen > 0) {
m_dsrSent = false; trace("Received a DSR reply");
return dsrLen; m_dsrSent = false;
} else if (!isEof && dsrLen == -1) { return dsrLen;
// Incomplete DSR match. } else if (!isEof && dsrLen == -1) {
trace("Incomplete DSR match"); // Incomplete DSR match.
return -1; trace("Incomplete DSR match");
return -1;
}
int mouseLen = scanMouseInput(records, input, inputSize);
if (mouseLen > 0 || (!isEof && mouseLen == -1)) {
return mouseLen;
}
} }
// Search the input map. // Search the input map.
@ -196,6 +374,119 @@ int ConsoleInput::scanKeyPress(std::vector<INPUT_RECORD> &records,
return len; return len;
} }
int ConsoleInput::scanMouseInput(std::vector<INPUT_RECORD> &records,
const char *input,
int inputSize)
{
MouseRecord record;
int len = matchMouseRecord(input, inputSize, record);
if (len == 0) {
return 0;
}
if (isTracingEnabled()) {
static bool debugInput = hasDebugFlag("input");
if (debugInput) {
trace("mouse input: %s", record.toString().c_str());
}
}
const int button = record.flags & 0x03;
INPUT_RECORD newRecord = {0};
newRecord.EventType = MOUSE_EVENT;
MOUSE_EVENT_RECORD &mer = newRecord.Event.MouseEvent;
mer.dwMousePosition.X =
m_mouseWindowRect.Left +
std::max(0, std::min<int>(record.coord.X,
m_mouseWindowRect.width() - 1));
mer.dwMousePosition.Y =
m_mouseWindowRect.Top +
std::max(0, std::min<int>(record.coord.Y,
m_mouseWindowRect.height() - 1));
// The modifier state is neatly independent of everything else.
if (record.flags & 0x04) { mer.dwControlKeyState |= SHIFT_PRESSED; }
if (record.flags & 0x08) { mer.dwControlKeyState |= LEFT_ALT_PRESSED; }
if (record.flags & 0x10) { mer.dwControlKeyState |= LEFT_CTRL_PRESSED; }
if (record.flags & 0x40) {
// Mouse wheel
mer.dwEventFlags |= MOUSE_WHEELED;
if (button == 0) {
// up
mer.dwButtonState |= 0x00780000;
} else if (button == 1) {
// down
mer.dwButtonState |= 0xff880000;
} else {
// Invalid -- do nothing
return len;
}
} else {
// Ordinary mouse event
if (record.flags & 0x20) { mer.dwEventFlags |= MOUSE_MOVED; }
if (button == 3) {
m_mouseButtonState = 0;
// Potentially advance double-click detection.
m_doubleClick.released = true;
} else {
const DWORD relevantFlag =
(button == 0) ? FROM_LEFT_1ST_BUTTON_PRESSED :
(button == 1) ? FROM_LEFT_2ND_BUTTON_PRESSED :
(button == 2) ? RIGHTMOST_BUTTON_PRESSED :
0;
ASSERT(relevantFlag != 0);
if (record.release) {
m_mouseButtonState &= ~relevantFlag;
if (relevantFlag == m_doubleClick.button) {
// Potentially advance double-click detection.
m_doubleClick.released = true;
} else {
// End double-click detection.
m_doubleClick = DoubleClickDetection();
}
} else if ((m_mouseButtonState & relevantFlag) == 0) {
// The button has been newly pressed.
m_mouseButtonState |= relevantFlag;
// Detect a double-click. This code looks for an exact
// coordinate match, which is stricter than what Windows does,
// but Windows has pixel coordinates, and we only have terminal
// coordinates.
if (m_doubleClick.button == relevantFlag &&
m_doubleClick.pos == record.coord &&
(GetTickCount() - m_doubleClick.tick <
GetDoubleClickTime())) {
// Record a double-click and end double-click detection.
mer.dwEventFlags |= DOUBLE_CLICK;
m_doubleClick = DoubleClickDetection();
} else {
// Begin double-click detection.
m_doubleClick.button = relevantFlag;
m_doubleClick.pos = record.coord;
m_doubleClick.tick = GetTickCount();
}
}
}
}
mer.dwButtonState |= m_mouseButtonState;
if (m_mouseInputEnabled) {
if (isTracingEnabled()) {
static bool debugInput = hasDebugFlag("input");
if (debugInput) {
trace("mouse event: %s", mouseEventToString(mer).c_str());
}
}
records.push_back(newRecord);
}
return len;
}
void ConsoleInput::appendUtf8Char(std::vector<INPUT_RECORD> &records, void ConsoleInput::appendUtf8Char(std::vector<INPUT_RECORD> &records,
const char *charBuffer, const char *charBuffer,
const int charLen, const int charLen,
@ -299,66 +590,3 @@ void ConsoleInput::appendInputRecord(std::vector<INPUT_RECORD> &records,
ir.Event.KeyEvent.dwControlKeyState = keyState; ir.Event.KeyEvent.dwControlKeyState = keyState;
records.push_back(ir); records.push_back(ir);
} }
// Return the byte size of a UTF-8 character using the value of the first
// byte.
int ConsoleInput::utf8CharLength(char firstByte)
{
// This code would probably be faster if it used __builtin_clz.
if ((firstByte & 0x80) == 0) {
return 1;
} else if ((firstByte & 0xE0) == 0xC0) {
return 2;
} else if ((firstByte & 0xF0) == 0xE0) {
return 3;
} else if ((firstByte & 0xF8) == 0xF0) {
return 4;
} else if ((firstByte & 0xFC) == 0xF8) {
return 5;
} else if ((firstByte & 0xFE) == 0xFC) {
return 6;
} else {
// Malformed UTF-8.
return 1;
}
}
// Match the Device Status Report console input: ESC [ nn ; mm R
// Returns:
// 0 no match
// >0 match, returns length of match
// -1 incomplete match
int ConsoleInput::matchDsr(const char *input, int inputSize)
{
const char *pch = input;
const char *stop = input + inputSize;
if (pch == stop) { return -1; }
#define CHECK(cond) \
do { \
if (!(cond)) { return 0; } \
} while(0)
#define ADVANCE() \
do { \
pch++; \
if (pch == stop) { return -1; } \
} while(0)
CHECK(*pch == '\x1B'); ADVANCE();
CHECK(*pch == '['); ADVANCE();
CHECK(isdigit(*pch)); ADVANCE();
while (isdigit(*pch)) {
ADVANCE();
}
CHECK(*pch == ';'); ADVANCE();
CHECK(isdigit(*pch)); ADVANCE();
while (isdigit(*pch)) {
ADVANCE();
}
CHECK(*pch == 'R');
return pch - input + 1;
#undef CHECK
#undef ADVANCE
}

View File

@ -27,7 +27,9 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "Coord.h"
#include "InputMap.h" #include "InputMap.h"
#include "SmallRect.h"
class Win32Console; class Win32Console;
class DsrSender; class DsrSender;
@ -39,13 +41,18 @@ public:
~ConsoleInput(); ~ConsoleInput();
void writeInput(const std::string &input); void writeInput(const std::string &input);
void flushIncompleteEscapeCode(); void flushIncompleteEscapeCode();
void setMouseInputEnabled(bool val) { m_mouseInputEnabled = val; }
void setMouseWindowRect(SmallRect val) { m_mouseWindowRect = val; }
private: private:
void doWrite(bool isEof); void doWrite(bool isEof);
int scanKeyPress(std::vector<INPUT_RECORD> &records, int scanInput(std::vector<INPUT_RECORD> &records,
const char *input, const char *input,
int inputSize, int inputSize,
bool isEof); bool isEof);
int scanMouseInput(std::vector<INPUT_RECORD> &records,
const char *input,
int inputSize);
void appendUtf8Char(std::vector<INPUT_RECORD> &records, void appendUtf8Char(std::vector<INPUT_RECORD> &records,
const char *charBuffer, const char *charBuffer,
int charLen, int charLen,
@ -59,8 +66,6 @@ private:
uint16_t virtualKey, uint16_t virtualKey,
uint16_t unicodeChar, uint16_t unicodeChar,
uint16_t keyState); uint16_t keyState);
static int utf8CharLength(char firstByte);
static int matchDsr(const char *input, int inputSize);
private: private:
Win32Console *m_console; Win32Console *m_console;
@ -69,6 +74,16 @@ private:
std::string m_byteQueue; std::string m_byteQueue;
InputMap m_inputMap; InputMap m_inputMap;
DWORD m_lastWriteTick; DWORD m_lastWriteTick;
DWORD m_mouseButtonState;
struct DoubleClickDetection {
DoubleClickDetection() : button(0), tick(0), released(0) {}
DWORD button;
Coord pos;
DWORD tick;
bool released;
} m_doubleClick;
bool m_mouseInputEnabled;
SmallRect m_mouseWindowRect;
}; };
#endif // CONSOLEINPUT_H #endif // CONSOLEINPUT_H