pubnix/lib/neovision/src/crt.cpp

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