542 lines
15 KiB
C++
542 lines
15 KiB
C++
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <functional>
|
|
#include <future>
|
|
#include <iostream>
|
|
#include <locale>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <regex>
|
|
#include <streambuf>
|
|
|
|
#include "neovision/ansi.h"
|
|
|
|
#include "neovision/crt.h"
|
|
|
|
namespace neovision {
|
|
|
|
// Position struct -------------------------------------------------------------
|
|
|
|
Vector2D::Vector2D(): m_x{0}, m_y{0}
|
|
{
|
|
}
|
|
|
|
Vector2D::Vector2D(std::uint32_t x, std::uint32_t y): m_x{x}, m_y{y}
|
|
{
|
|
}
|
|
|
|
Vector2D::Vector2D(const Vector2D& other)
|
|
{
|
|
m_x = other.X();
|
|
m_y = other.Y();
|
|
}
|
|
|
|
std::string Vector2D::Str() const
|
|
{
|
|
return std::to_string(m_x) + "," + std::to_string(m_y);
|
|
}
|
|
|
|
std::uint32_t Vector2D::X() const
|
|
{
|
|
return m_x;
|
|
}
|
|
|
|
void Vector2D::X(std::uint32_t n)
|
|
{
|
|
m_x = n;
|
|
}
|
|
|
|
std::uint32_t Vector2D::Y() const
|
|
{
|
|
return m_y;
|
|
}
|
|
|
|
void Vector2D::Y(std::uint32_t n)
|
|
{
|
|
m_y = n;
|
|
}
|
|
|
|
Vector2D Vector2D::operator+(const Vector2D& other) const
|
|
{
|
|
return Vector2D(m_x + other.m_x, m_y + other.m_y);
|
|
}
|
|
|
|
Vector2D Vector2D::operator+=(const Vector2D& other)
|
|
{
|
|
m_x += other.m_x;
|
|
m_y += other.m_y;
|
|
return *this;
|
|
}
|
|
|
|
Vector2D Vector2D::operator-(const Vector2D& other) const
|
|
{
|
|
return Vector2D(m_x - other.m_x, m_y - other.m_y);
|
|
}
|
|
|
|
Vector2D Vector2D::operator-=(const Vector2D& other)
|
|
{
|
|
m_x -= other.m_x;
|
|
m_y -= other.m_y;
|
|
return *this;
|
|
}
|
|
|
|
void Vector2D::operator=(const Vector2D& other)
|
|
{
|
|
m_x.store(other.m_x.load());
|
|
m_y.store(other.m_y.load());
|
|
}
|
|
|
|
bool Vector2D::operator==(const Vector2D& other) const
|
|
{
|
|
return (m_x == other.m_x) && (m_y == other.m_y);
|
|
}
|
|
|
|
bool Vector2D::operator!=(const Vector2D& other) const
|
|
{
|
|
return !((m_x == other.m_x) && (m_y == other.m_y));
|
|
}
|
|
|
|
// IO class --------------------------------------------------------------------
|
|
|
|
IO::IO()
|
|
{
|
|
std::wcin.setf(std::ios::unitbuf);
|
|
std::wcout.setf(std::ios::unitbuf);
|
|
/// @todo - this should probably be aware of the system locale.
|
|
std::locale::global(std::locale("en_US.utf8"));
|
|
std::wcout.imbue(std::locale());
|
|
}
|
|
|
|
IO::~IO()
|
|
{
|
|
std::locale::global(std::locale("C"));
|
|
}
|
|
|
|
IO& IO::Get()
|
|
{
|
|
static IO instance;
|
|
return instance;
|
|
}
|
|
|
|
void IO::LockInputStream()
|
|
{
|
|
m_inputMutex.lock();
|
|
}
|
|
|
|
void IO::LockOutputStream()
|
|
{
|
|
m_inputMutex.unlock();
|
|
}
|
|
|
|
std::wstring IO::Read(std::size_t bytes)
|
|
{
|
|
const std::lock_guard<std::mutex> lock(m_inputMutex);
|
|
if (m_readFunction) return m_readFunction(bytes);
|
|
std::vector<wchar_t> buffer(bytes);
|
|
wchar_t charBuffer[bytes];
|
|
std::wcin.rdbuf()->pubsetbuf(charBuffer, bytes);
|
|
std::wcin.read(&buffer[0], bytes);
|
|
return std::wstring(buffer.begin(), buffer.end());
|
|
}
|
|
|
|
std::wstring IO::ReadUntil(wchar_t c)
|
|
{
|
|
const std::lock_guard<std::mutex> lock(m_inputMutex);
|
|
std::wstring buffer;
|
|
while (true)
|
|
{
|
|
const std::wstring wc = Read(1);
|
|
if (wc.size() != 1) break;
|
|
buffer += wc[0];
|
|
if (wc[0] == c) break;
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
void IO::SetInputFunction(IoReadFunction f)
|
|
{
|
|
const std::lock_guard<std::mutex> lock(m_inputMutex);
|
|
m_readFunction = f;
|
|
}
|
|
|
|
void IO::SetOutputFunction(IoWriteFunction f)
|
|
{
|
|
const std::lock_guard<std::mutex> lock(m_outputMutex);
|
|
m_writeFunction = f;
|
|
}
|
|
|
|
void IO::UnlockInputStream()
|
|
{
|
|
m_inputMutex.unlock();
|
|
}
|
|
|
|
void IO::UnlockOutputStream()
|
|
{
|
|
m_outputMutex.unlock();
|
|
}
|
|
|
|
void IO::Write(const std::wstring& str)
|
|
{
|
|
const std::lock_guard<std::mutex> lock(m_outputMutex);
|
|
if (m_writeFunction)
|
|
{
|
|
m_writeFunction(str);
|
|
return;
|
|
}
|
|
std::wcout << str;
|
|
}
|
|
|
|
// Input parser class ----------------------------------------------------------
|
|
|
|
void InputParser::Read()
|
|
{
|
|
ansi::AnsiParser ansiParser;
|
|
std::wstring result;
|
|
std::wstring curChar = IO::Get().Read(1);
|
|
if (curChar.length() != 1) return;
|
|
while (ansiParser.Parse(curChar[0]))
|
|
{
|
|
curChar = IO::Get().Read(1);
|
|
if (curChar.length() != 1) break;
|
|
}
|
|
// Check if we got mouse data.
|
|
const std::wstring csiData = ansiParser.CsiPortion();
|
|
const std::vector<std::wstring> csiParams = ansiParser.CsiParameters();
|
|
if ((ansiParser.State() == ansi::AnsiParser::ParseState::None)
|
|
&& (!csiData.empty()) && (csiParams.size() == 3))
|
|
{
|
|
const wchar_t csiChar = csiData.back();
|
|
if ((csiChar == L'M') || (csiChar == L'm'))
|
|
{
|
|
// Looks like mouse data... try to parse it.
|
|
MouseEventData mouseEvent;
|
|
if (csiChar == L'M')
|
|
{
|
|
mouseEvent.eventType = ButtonEventType::Press;
|
|
}
|
|
else if (csiChar == L'm')
|
|
{
|
|
mouseEvent.eventType = ButtonEventType::Release;
|
|
}
|
|
bool validMouseData{false};
|
|
std::uint32_t x{0};
|
|
std::uint32_t y{0};
|
|
std::uint32_t b{0};
|
|
if (
|
|
(csiParams[0].size() > 1)
|
|
&& (!csiParams[1].empty())
|
|
&& (!csiParams[2].empty()) )
|
|
{
|
|
try
|
|
{
|
|
x = std::stoi(csiParams[1]);
|
|
y = std::stoi(csiParams[2]);
|
|
if (csiParams[0][0] == L'<')
|
|
{
|
|
std::wstring bstr = csiParams[0];
|
|
bstr.erase(0, 1);
|
|
b = std::stoi(bstr);
|
|
const bool ctrl = (b & 16) == 16;
|
|
const bool meta = (b & 8) == 8;
|
|
const bool shift = (b & 4) == 4;
|
|
/* Only the ctrl modifier seems to be sent in most
|
|
terminals. The shift modifier is actually re-purposed
|
|
to enable extra mouse buttons (eg, wheel etc,...) */
|
|
const bool extraButtons = (b & 64) == 64;
|
|
const bool extraExtraButtons = (b & 128) == 128;
|
|
std::uint8_t buttonMultiplier{0};
|
|
if (shift == true) buttonMultiplier = 3;
|
|
if (extraButtons == true) buttonMultiplier += 4;
|
|
if (extraExtraButtons == true) buttonMultiplier += 8;
|
|
const std::uint8_t buttons = b & 3;
|
|
mouseEvent.m_button = buttons + buttonMultiplier;
|
|
if (meta == true)
|
|
mouseEvent.m_modifiers.emplace_back(ModKey::Meta);
|
|
if (ctrl == true)
|
|
mouseEvent.m_modifiers.emplace_back(ModKey::Ctrl);
|
|
validMouseData = true;
|
|
}
|
|
}
|
|
catch (const std::invalid_argument& e)
|
|
{
|
|
validMouseData = false;
|
|
}
|
|
catch (const std::out_of_range& e)
|
|
{
|
|
validMouseData = false;
|
|
}
|
|
}
|
|
if (validMouseData == true)
|
|
{
|
|
/* If we got here, we successfully parsed mouse data, we'll fire
|
|
a mouse event. If not, we'll fall through to firing a normal
|
|
key event. */
|
|
mouseEvent.position.X(x);
|
|
mouseEvent.position.Y(y);
|
|
if (m_onMouseEvent) m_onMouseEvent(mouseEvent);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
/* If we didn't get a mouse event, just fire a key event with the data we
|
|
have. */
|
|
if (m_onKeyEvent) m_onKeyEvent(ansiParser.ProcessedCharacters());
|
|
}
|
|
|
|
void InputParser::SetOnKeyEvent(const KeyboardEventCallback& ev)
|
|
{
|
|
m_onKeyEvent = ev;
|
|
}
|
|
|
|
void InputParser::SetOnMouseEvent(const MouseEventCallback& ev)
|
|
{
|
|
m_onMouseEvent = ev;
|
|
}
|
|
|
|
// Functions -------------------------------------------------------------------
|
|
|
|
void ClearScreen() {
|
|
IO::Get().Write(ansi::MakeCsiSequence({
|
|
std::wstring{L"2"}, std::wstring{ansi::CsiSequence::ED}
|
|
}));
|
|
}
|
|
|
|
Vector2D GetCursorPos()
|
|
{
|
|
static const std::wregex getCursorPosRegex{L"^\x1b\\[(\\d+);(\\d+)R$"};
|
|
IO::Get().Write(ansi::MakeCsiSequence({
|
|
std::wstring{L"6"}, std::wstring{ansi::CsiSequence::DSR}
|
|
}));
|
|
std::wstring response = IO::Get().ReadUntil(L'R');
|
|
std::wsmatch responseMatches;
|
|
if (!std::regex_match(
|
|
response, responseMatches, getCursorPosRegex
|
|
))
|
|
{
|
|
throw std::runtime_error(
|
|
"Could not parse GetCursorPos response from terminal. (1)"
|
|
);
|
|
}
|
|
if (responseMatches.size() != 3)
|
|
{
|
|
throw std::runtime_error(
|
|
"Could not parse GetCursorPos response from terminal. (2)"
|
|
);
|
|
}
|
|
const std::uint32_t x = std::stoi(responseMatches[1].str());
|
|
const std::uint32_t y = std::stoi(responseMatches[2].str());
|
|
return {x, y};
|
|
}
|
|
|
|
void SetCursorPos(const Vector2D& pos, bool relative)
|
|
{
|
|
// ansi cursor positions are 1-based, ours are 0-based, so we add one.
|
|
Vector2D newPos(pos + Vector2D(1, 1));
|
|
if (relative)
|
|
{
|
|
newPos += GetCursorPos();
|
|
if (newPos == pos) return;
|
|
}
|
|
IO::Get().Write(
|
|
ansi::MakeCsiSequence({
|
|
std::to_wstring(newPos.Y()),
|
|
std::to_wstring(newPos.X()),
|
|
ansi::CsiSequence::CUP
|
|
})
|
|
);
|
|
}
|
|
|
|
void SetBackgroundColor(StandardColor color)
|
|
{
|
|
IO::Get().Write(
|
|
ansi::MakeSgrSequence({ToAnsiBgColor(color)})
|
|
);
|
|
}
|
|
|
|
void SetBackgroundColor(std::uint8_t r, std::uint8_t g, std::uint8_t b)
|
|
{
|
|
IO::Get().Write(
|
|
ansi::MakeSgrSequence({
|
|
ansi::SgrSequence::SET_BG_COLOR,
|
|
L"2",
|
|
std::to_wstring(r),
|
|
std::to_wstring(g),
|
|
std::to_wstring(b)
|
|
})
|
|
);
|
|
}
|
|
|
|
void SetForegroundColor(StandardColor color)
|
|
{
|
|
IO::Get().Write(
|
|
ansi::MakeSgrSequence({ToAnsiFgColor(color)})
|
|
);
|
|
}
|
|
|
|
void SetForegroundColor(std::uint8_t r, std::uint8_t g, std::uint8_t b)
|
|
{
|
|
IO::Get().Write(
|
|
ansi::MakeSgrSequence({
|
|
ansi::SgrSequence::SET_FG_COLOR,
|
|
L"2",
|
|
std::to_wstring(r),
|
|
std::to_wstring(g),
|
|
std::to_wstring(b)
|
|
})
|
|
);
|
|
}
|
|
|
|
void SetMouseReporting(
|
|
MouseEventMode eventMode,
|
|
MouseCoordinateMode coordinateMode
|
|
)
|
|
{
|
|
std::wstring modeChar;
|
|
std::wstring encChar;
|
|
std::wstring outStr;
|
|
switch (eventMode)
|
|
{
|
|
case MouseEventMode::All:
|
|
modeChar = ansi::MouseTrackMode::ANY_EVENT;
|
|
break;
|
|
case MouseEventMode::ButtonOnly:
|
|
modeChar = ansi::MouseTrackMode::BTN_EVENT;
|
|
break;
|
|
case MouseEventMode::Highlight:
|
|
modeChar = ansi::MouseTrackMode::VT200_HIGHLIGHT;
|
|
break;
|
|
default:
|
|
modeChar = ansi::MouseTrackMode::ANY_EVENT;
|
|
break;
|
|
}
|
|
if (coordinateMode == MouseCoordinateMode::Pixel)
|
|
{
|
|
encChar = ansi::MouseExtCoord::SGR_PIXELS;
|
|
}
|
|
else
|
|
{
|
|
encChar = ansi::MouseExtCoord::SGR;
|
|
}
|
|
if (eventMode == MouseEventMode::None)
|
|
{
|
|
outStr += ansi::MakeCsiSequence({
|
|
ansi::CsiSequence::DECPM +
|
|
std::wstring{ansi::MouseTrackMode::ANY_EVENT} +
|
|
ansi::DecPmSuffix::DISABLE
|
|
});
|
|
outStr += ansi::MakeCsiSequence({
|
|
ansi::CsiSequence::DECPM +
|
|
std::wstring{ansi::MouseTrackMode::VT200} +
|
|
ansi::DecPmSuffix::DISABLE
|
|
});
|
|
outStr += ansi::MakeCsiSequence({
|
|
ansi::CsiSequence::DECPM +
|
|
std::wstring{ansi::MouseTrackMode::BTN_EVENT} +
|
|
ansi::DecPmSuffix::DISABLE
|
|
});
|
|
outStr += ansi::MakeCsiSequence({
|
|
ansi::CsiSequence::DECPM +
|
|
std::wstring{ansi::MouseTrackMode::VT200_HIGHLIGHT} +
|
|
ansi::DecPmSuffix::DISABLE
|
|
});
|
|
}
|
|
else
|
|
{
|
|
outStr += ansi::MakeCsiSequence({
|
|
ansi::CsiSequence::DECPM +
|
|
modeChar +
|
|
ansi::DecPmSuffix::ENABLE
|
|
});
|
|
outStr += ansi::MakeCsiSequence({
|
|
ansi::CsiSequence::DECPM +
|
|
encChar +
|
|
ansi::DecPmSuffix::ENABLE
|
|
});
|
|
}
|
|
IO::Get().Write(outStr);
|
|
}
|
|
|
|
std::wstring ToAnsiBgColor(StandardColor c)
|
|
{
|
|
switch(c)
|
|
{
|
|
case StandardColor::BLACK:
|
|
return ansi::SgrSequence::BG_BLACK;
|
|
case StandardColor::RED:
|
|
return ansi::SgrSequence::BG_RED;
|
|
case StandardColor::GREEN:
|
|
return ansi::SgrSequence::BG_GREEN;
|
|
case StandardColor::YELLOW:
|
|
return ansi::SgrSequence::BG_YELLOW;
|
|
case StandardColor::BLUE:
|
|
return ansi::SgrSequence::BG_BLUE;
|
|
case StandardColor::MAGENTA:
|
|
return ansi::SgrSequence::BG_MAGENTA;
|
|
case StandardColor::CYAN:
|
|
return ansi::SgrSequence::BG_MAGENTA;
|
|
case StandardColor::WHITE:
|
|
return ansi::SgrSequence::BG_WHITE;
|
|
case StandardColor::BRIGHT_BLACK:
|
|
return ansi::SgrSequence::BG_BRIGHT_BLACK;
|
|
case StandardColor::BRIGHT_RED:
|
|
return ansi::SgrSequence::BG_BRIGHT_RED;
|
|
case StandardColor::BRIGHT_GREEN:
|
|
return ansi::SgrSequence::BG_BRIGHT_GREEN;
|
|
case StandardColor::BRIGHT_YELLOW:
|
|
return ansi::SgrSequence::BG_BRIGHT_YELLOW;
|
|
case StandardColor::BRIGHT_BLUE:
|
|
return ansi::SgrSequence::BG_BRIGHT_BLUE;
|
|
case StandardColor::BRIGHT_MAGENTA:
|
|
return ansi::SgrSequence::BG_BRIGHT_MAGENTA;
|
|
case StandardColor::BRIGHT_CYAN:
|
|
return ansi::SgrSequence::BG_BRIGHT_MAGENTA;
|
|
case StandardColor::BRIGHT_WHITE:
|
|
return ansi::SgrSequence::BG_BRIGHT_WHITE;
|
|
default:
|
|
return ansi::SgrSequence::BG_BLACK;
|
|
}
|
|
}
|
|
|
|
std::wstring ToAnsiFgColor(StandardColor c)
|
|
{
|
|
switch(c)
|
|
{
|
|
case StandardColor::BLACK:
|
|
return ansi::SgrSequence::FG_BLACK;
|
|
case StandardColor::RED:
|
|
return ansi::SgrSequence::FG_RED;
|
|
case StandardColor::GREEN:
|
|
return ansi::SgrSequence::FG_GREEN;
|
|
case StandardColor::YELLOW:
|
|
return ansi::SgrSequence::FG_YELLOW;
|
|
case StandardColor::BLUE:
|
|
return ansi::SgrSequence::FG_BLUE;
|
|
case StandardColor::MAGENTA:
|
|
return ansi::SgrSequence::FG_MAGENTA;
|
|
case StandardColor::CYAN:
|
|
return ansi::SgrSequence::FG_MAGENTA;
|
|
case StandardColor::WHITE:
|
|
return ansi::SgrSequence::FG_WHITE;
|
|
case StandardColor::BRIGHT_BLACK:
|
|
return ansi::SgrSequence::FG_BRIGHT_BLACK;
|
|
case StandardColor::BRIGHT_RED:
|
|
return ansi::SgrSequence::FG_BRIGHT_RED;
|
|
case StandardColor::BRIGHT_GREEN:
|
|
return ansi::SgrSequence::FG_BRIGHT_GREEN;
|
|
case StandardColor::BRIGHT_YELLOW:
|
|
return ansi::SgrSequence::FG_BRIGHT_YELLOW;
|
|
case StandardColor::BRIGHT_BLUE:
|
|
return ansi::SgrSequence::FG_BRIGHT_BLUE;
|
|
case StandardColor::BRIGHT_MAGENTA:
|
|
return ansi::SgrSequence::FG_BRIGHT_MAGENTA;
|
|
case StandardColor::BRIGHT_CYAN:
|
|
return ansi::SgrSequence::FG_BRIGHT_MAGENTA;
|
|
case StandardColor::BRIGHT_WHITE:
|
|
return ansi::SgrSequence::FG_BRIGHT_WHITE;
|
|
default:
|
|
return ansi::SgrSequence::FG_BLACK;
|
|
}
|
|
}
|
|
|
|
} // namespace neovision
|