Work on mouse input handling

This commit is contained in:
John Sennesael 2021-08-25 20:28:18 -05:00
parent 1ebc805ca3
commit 50477ab304
8 changed files with 1400 additions and 340 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,9 @@
/**
* @brief Provides screen I/O functionality.
*
* High-level screen I/O functions and types.
*/
#pragma once
#include <atomic>
@ -88,7 +94,7 @@ public:
/**
* @brief Set Y.
* @param New y value.
* @param n New y value.
*/
void Y(std::uint32_t n);
@ -102,6 +108,23 @@ public:
};
/**
* @brief Callback signature for IO read operations.
*
* The function should return the read bytes, and takes as parameter the number
* of bytes that should be read.
*
*/
typedef std::function<std::wstring(size_t)> IoReadFunction;
/**
* @brief Callback signature for IO write operations.
*
* The function takes as parameter the data that should be written to the
* terminal.
*/
typedef std::function<void(const std::wstring&)> IoWriteFunction;
/**
* @brief Input/Output.
*
@ -109,11 +132,11 @@ public:
* text input and output.
*
* You typically shouldn't have to interact with this unless you want to change
* the output (or input) stream. For instance, if you were to want to output to
* stderr, you could do the following:
* how input/output to the terminal works. For instance, if you were to want to
* output to stderr, you could do the following:
*
* @code{.cpp}
* IO::get().SetOutputStream(std::cerr);
* IO::get().SetOutputFunction([](std::wstring& s){ std::cerr << s; });
* @endcode
*
*/
@ -121,8 +144,8 @@ class IO {
std::mutex m_inputMutex;
std::mutex m_outputMutex;
std::reference_wrapper<std::wistream> m_inputStream;
std::reference_wrapper<std::wostream> m_outputStream;
IoReadFunction m_readFunction;
IoWriteFunction m_writeFunction;
public:
@ -153,20 +176,6 @@ public:
*/
static IO& Get();
/**
* @brief Get input stream.
*
* This function provides low-level access to the current input stream.
*
* It's best to avoid this and only use the Read() and Write() functions if
* you can.
*
* @warning You must use LockInputStream/UnlockInputStream around this call.
*
* @return Returns a reference to the current input stream.
*/
std::reference_wrapper<std::wistream> InputStream();
/**
* Lock input stream.
*
@ -183,18 +192,6 @@ public:
*/
void LockOutputStream();
/**
* @brief Get output stream.
*
* This function provides low-level access to the current output stream.
*
* @warning You must use LockOutputStream/UnlockOutputStream around this
* call.
*
* @return Returns a reference to the current output stream.
*/
std::reference_wrapper<std::wostream> OutputStream();
/**
* @brief Reads from the current input stream.
*
@ -217,24 +214,24 @@ public:
std::wstring ReadUntil(wchar_t c);
/**
* @brief Sets the current input stream.
* @brief Sets the current input function.
*
* StdIn (std::cin) is the default input stream. This allows you to to
* override this to any input stream.
* By default this class reads from stdin, you can override this by setting
* this function to a callback that reads data from the terminal.
*
* @param stream The new input stream to be used for text output.
* @param f The new input function to be used for text input.
*/
void SetInputStream(std::wistream& stream);
void SetInputFunction(IoReadFunction f);
/**
* @brief Sets the current output stream.
* @brief Sets the current output function.
*
* StdOut (std::cout) is the default output stream. This allows you to to
* override this to any output stream.
* By default this class writes to stdout, you can override this by setting
* this function to a callback that writes data to the terminal.
*
* @param stream The new output stream to be used for text output.
* @param f The new output function to be used for text output.
*/
void SetOutputStream(std::wostream& stream);
void SetOutputFunction(IoWriteFunction f);
/**
* Unlock input stream.
@ -263,6 +260,122 @@ public:
void Write(const std::wstring& str);
};
/**
* @brief Mouse coordinate reporting mode.
*/
enum class MouseCoordinateMode {
Character, ///< Coordinates are character col/row.
Pixel ///< Coordinates are pixel x y.
};
/**
* @brief Mouse coordinate event selection.
*/
enum class MouseEventMode {
None, ///< Disables mouse reporting.
ButtonOnly, ///< Only report on mouse down/up.
All, ///< Report all mouse events.
Highlight ///< Enable highlight mode.
};
/**
* @brief Button event type.
*
* Indicates whether a button was depressed or released.
*/
enum class ButtonEventType{ None, Press, Release };
/**
* @brief Modifier key.
*/
enum class ModKey{ Shift, Ctrl, Meta };
/**
* @brief Holds mouse event data.
*/
struct MouseEventData {
/**
* @brief Mouse position.
*/
Vector2D position{};
/**
* @brief Mouse button pressed/released.
*/
std::uint8_t m_button;
/**
* @brief Modifier keys pressed.
*/
std::vector<ModKey> m_modifiers;
/**
* @brief Mouse even type.
*/
ButtonEventType eventType{ButtonEventType::None};
};
/**
* @brief Input event type.
*/
enum class InputEventType { Mouse, Keyboard };
typedef std::function<void(const std::wstring& keyData)> KeyboardEventCallback;
typedef std::function<void(const MouseEventData& mouseData)> MouseEventCallback;
/**
* @brief Input parser.
*
* Reads from input and triggers events for mouse and keyboard data with parsed
* ansi sequences detailing things such as mouse position and special keycodes.
*/
class InputParser {
KeyboardEventCallback m_onKeyEvent;
MouseEventCallback m_onMouseEvent;
public:
/**
* @brief Default constructor.
*/
InputParser() = default;
/**
* @brief Reads input and triggers events.
*
* Blocking, reads input via IO::Read and returns the result.
*
* Normally one character at a time is read, but may return multiple:
*
* If the read character looks like the start of an escape sequence, more
* characters will be read in attempt to parse it. If parsing is successful
* either an event will be triggered, or the entire sequence is returned.
* If parsing is not successful, the read buffer so far is returned.
*/
void Read();
/**
* @brief Set 'onkey' event.
*
* This triggers when Read() finishes parsing characters from input.
*
* @param ev Function to run on key event.
*/
void SetOnKeyEvent(const KeyboardEventCallback& ev);
/**
* @brief Set 'onmouse' event.
*
* This triggers when Read() successfully finishes parsing mouse data.
*
* @param ev Function to run on mouse event.
*/
void SetOnMouseEvent(const MouseEventCallback& ev);
};
// Functions -------------------------------------------------------------------
/**
@ -351,6 +464,20 @@ void SetForegroundColor(StandardColor color);
*/
void SetForegroundColor(std::uint8_t r, std::uint8_t g, std::uint8_t b);
/**
* @brief Set mouse reporting mode.
*
* Causes the terminal to report on mouse coordinates. We only use the SGR and
* SGR_PIXEL formats, which should be compatible with most terminals.
*
* @param eventMode Sets what type of mouse events should be reported.
* @param coordinateMode Sets how coordinates should be reported.
*/
void SetMouseReporting(
MouseEventMode eventMode,
MouseCoordinateMode coordinateMode = MouseCoordinateMode::Character
);
/**
* Convert Color enum to ANSI background color code.
*

View File

@ -2,6 +2,9 @@
#include <signal.h>
#include <termios.h>
#include <chrono>
#include <streambuf>
#include <unordered_map>
namespace neovision {
@ -16,7 +19,7 @@ class Vector2D;
* The application is passed as a convenience, as anything that needs to be
* accessible within the function can be wired into the application class.
*/
typedef std::function<void(Application& app)> TerminalEventCallback;
typedef std::function<void()> TerminalEventCallback;
/**
* Class for terminal i/o operations.
@ -33,9 +36,15 @@ typedef std::function<void(Application& app)> TerminalEventCallback;
*/
class Terminal {
std::unordered_map<int, termios> m_savedTerminalSettings{};
bool m_initialized{false};
termios m_savedTerminalSettings{};
TerminalEventCallback m_onResize;
TerminalEventCallback m_onTerminate;
struct sigaction m_resizeSignal{};
std::chrono::seconds m_ioTimeOut{1};
int m_fd{0};
static void sigIntHandler(int);
static void sigWinchHandler(int);
public:
@ -50,22 +59,42 @@ public:
*/
~Terminal();
/**
* @brief Cleanup
*
* Call this on exit. The destructor will attempt to, but it's not
* guaranteed to actually run correctly that way since we can't predict the
* order in which things are destroyed.
*/
void Cleanup();
/**
* @brief Initialize.
*
* Initializes the terminal for use. You should call this after
* constructing.
*/
void Initialize();
/**
* @brief Processes terminal events.
*
* Checks for ioctl signals, triggers callbacks as-needed.
*
* @param fd File descriptor on which to check for signals.
*/
void ProcessEvents(int fd);
void ProcessEvents();
/**
* @brief Reads n bytes from terminal input.
*
* @param[in] n How many bytes to read.
* @return Returns the data read from terminal input.
*/
std::wstring Read(size_t n);
/**
* @brief Restores terminal settings back to their original state.
*
* @param fd File descriptor of intput or output stream to restore settings
* for.
*/
void Restore(int fd);
void Restore();
/**
* @brief Sets the on-resize callback function.
@ -76,22 +105,49 @@ public:
*/
void SetOnResize(TerminalEventCallback callback);
/**
* @brief Set the on-terminate callback function.
*
* This function gets executed when the terminal gets killed, for instance
* with ctrl+c or a kill command.
*
* If you've got cleanup to run for graceful termination, you should hook
* into this to execute it.
*/
void SetOnTerminate(TerminalEventCallback callback);
/**
* @brief Get the terminal size.
*
* @param fd File descriptor of stream to get terminal size for.
* @return Returns the terminal size as a Position with X representing the
* number of columns, and Y the number of rows.
*/
Vector2D Size(int fd);
Vector2D Size();
/**
* @brief Get IO timeout.
*
* @return Returns the current I/O timeout.
*/
std::chrono::seconds Timeout() const;
/**
* @brief Set IO timeout.
*
* @param[in] t New IO timeout in seconds.
*/
void Timeout(const std::chrono::seconds& t);
/**
* @brief Disables buffered i/o in the terminal.
*
* @param fd File descriptor of input or output stream to disable terminal
* buffering for.
*/
void Unbuffer(int fd);
void Unbuffer();
/**
* @brief Writes data into the terminal.
* @param data Data to write.
*/
void Write(const std::wstring& data);
};

View File

@ -16,7 +16,7 @@ class Application;
typedef std::vector<std::vector<wchar_t>> ViewBuffer;
/**
* @class Represents a renderable view area.
* @brief Represents a renderable view area.
*
* This is the base class for anything within neovision that can be drawn onto
* the screen.

View File

@ -1,3 +1,4 @@
#include <cwctype>
#include <string>
#include <vector>
@ -6,6 +7,165 @@
namespace neovision {
namespace ansi {
// AnsiParser ------------------------------------------------------------------
std::wstring AnsiParser::ControlPortion() const
{
return m_controlPortion;
}
std::vector<std::wstring> AnsiParser::CsiParameters() const
{
return m_csiParameters;
}
std::wstring AnsiParser::CsiPortion() const
{
return m_csiPortion;
}
std::wstring AnsiParser::EscapePortion() const
{
return m_escapePortion;
}
bool AnsiParser::Parse(wchar_t c)
{
m_processedChars += c;
if (m_state == ParseState::None) ParseNoneState(c);
if (m_state == ParseState::Control)
{
ParseControlState(c);
}
else if (m_state == ParseState::Escape)
{
ParseEscapeState(c);
}
else if (m_state == ParseState::Csi)
{
ParseCsiState(c);
}
if (m_state == ParseState::Error) return false;
if (m_state == ParseState::None) return false;
return true;
}
void AnsiParser::ParseNoneState(wchar_t c)
{
const std::wstring cwstr{c};
if (ControlCharacters.count(cwstr))
{
m_state = ParseState::Control;
}
else
{
m_state = ParseState::Error;
}
}
void AnsiParser::ParseControlState(wchar_t c)
{
const std::wstring cwstr{c};
if (ControlCharacters.count(cwstr))
{
m_controlPortion += cwstr;
if (cwstr == ControlCharacter::CSI)
m_state = ParseState::Csi;
else if (cwstr == ControlCharacter::ESC)
m_state = ParseState::Escape;
else
m_state = ParseState::None;
return;
}
else
{
m_state = ParseState::Error;
}
}
void AnsiParser::ParseEscapeState(wchar_t c)
{
m_escapePortion += c;
if (EscapeSequences.count(m_escapePortion))
{
if (m_escapePortion == EscapeSequence::CSI)
m_state = ParseState::Csi;
else if (m_escapePortion == EscapeSequence::CSSI)
m_state = ParseState::Cssi;
}
else
{
// Longest possible escape sequence start is 2 characters.
if (m_escapePortion.length() > 2) m_state = ParseState::Error;
}
}
void AnsiParser::ParseCsiState(wchar_t c)
{
const std::wstring cwstr{c};
m_csiPortion += cwstr;
if (CsiSequences.count(cwstr))
{
// If c is a csi sequence char, that would mark the end of the sequence.
if (!m_parameterBuffer.empty())
{
m_csiParameters.emplace_back(m_parameterBuffer);
m_parameterBuffer.clear();
}
m_state = ParseState::None;
}
else
{
/* Characters in the range of 0x30-0x3f are part of a parameter.
If the character is a semi-colon, then we're starting the next
parameter. */
if (c == L';')
{
m_csiParameters.emplace_back(m_parameterBuffer);
m_parameterBuffer.clear();
}
else if ((c >= 0x30) && (c <= 0x3f))
{
m_parameterBuffer += c;
}
else
{
m_state = ParseState::Error;
}
}
}
std::wstring AnsiParser::PmPortion() const
{
return m_pmPortion;
}
std::wstring AnsiParser::ProcessedCharacters() const
{
return m_processedChars;
}
void AnsiParser::Reset()
{
m_controlPortion.clear();
m_escapePortion.clear();
m_csiPortion.clear();
m_csiParameters.clear();
m_csiParameterBuffer.clear();
m_cssiPortion.clear();
m_parameterBuffer.clear();
m_pmPortion.clear();
m_processedChars.clear();
m_state = ParseState::None;
}
AnsiParser::ParseState AnsiParser::State() const
{
return m_state;
}
// Functions -------------------------------------------------------------------
std::wstring MakeEscapeSequence(const std::wstring& sequence)
{
return ControlCharacter::ESC + sequence;

View File

@ -22,7 +22,7 @@ Application::Application(): View(), m_selfPtr(this, [](Application*){})
Application::~Application()
{
if (m_running) Stop();
if (m_running == true) Stop();
}
void Application::Add(std::unique_ptr<View> view)
@ -30,7 +30,7 @@ void Application::Add(std::unique_ptr<View> view)
View& viewRef = *view;
m_views.emplace_back(std::move(view));
CalculateZOrders();
if (m_running) viewRef.Initialize();
if (m_running == true) viewRef.Initialize();
}
void Application::CalculateZOrders()
@ -44,6 +44,13 @@ void Application::CalculateZOrders()
void Application::Initialize()
{
m_terminal.Initialize();
IO::Get().SetInputFunction([&](size_t b){
return m_terminal.Read(b);
});
IO::Get().SetOutputFunction([&](const std::wstring& data){
m_terminal.Write(data);
});
for (auto& view: m_views)
{
view->Initialize();
@ -52,44 +59,38 @@ void Application::Initialize()
void Application::Run()
{
/*
IO::Get().Write(ansi::MakeDecPmSequence(ansi::DecPmSequence::DECAWM,
ansi::DecPmSuffix::DISABLE
) + ansi::MakeDecPmSequence(ansi::DecPmSequence::DECTECM,
ansi::DecPmSuffix::DISABLE
));
*/
ClearScreen();
m_running = true;
m_terminal.Unbuffer(STDIN_FILENO);
Size(m_terminal.Size(STDIN_FILENO)- Vector2D(1, 1), m_bgFillCharacter);
m_terminal.SetOnResize([](Application& app){
const Vector2D newSize = app.m_terminal.Size(STDIN_FILENO) - Vector2D(1, 1);
app.Size(newSize, app.m_bgFillCharacter);
for (const auto& v: app.m_views)
m_terminal.Unbuffer();
Size(m_terminal.Size()- Vector2D(1, 1), m_bgFillCharacter);
m_terminal.SetOnResize([&](){
const Vector2D newSize = m_terminal.Size() - Vector2D(1, 1);
Size(newSize, m_bgFillCharacter);
for (const auto& v: m_views)
{
v->SetDirty();
}
app.OnResize(newSize);
OnResize(newSize);
});
m_terminal.SetOnTerminate([&](){
Stop();
});
SetMouseReporting(MouseEventMode::ButtonOnly);
Initialize();
InputParser inputParser;
while (m_running)
{
m_terminal.ProcessEvents(STDIN_FILENO);
m_terminal.ProcessEvents();
OnDraw();
inputParser.Read();
}
}
void Application::Stop()
{
/*
IO::Get().Write(ansi::MakeDecPmSequence(ansi::DecPmSequence::DECAWM,
ansi::DecPmSuffix::ENABLE
) + ansi::MakeDecPmSequence(ansi::DecPmSequence::DECTECM,
ansi::DecPmSuffix::ENABLE
));
*/
SetMouseReporting(MouseEventMode::None);
IO::Get().Write(ansi::MakeEscapeSequence(ansi::EscapeSequence::RIS));
m_terminal.Cleanup();
m_running = false;
}

View File

@ -1,6 +1,7 @@
#include <cstdint>
#include <cstdio>
#include <functional>
#include <future>
#include <iostream>
#include <locale>
#include <memory>
@ -97,11 +98,10 @@ bool Vector2D::operator!=(const Vector2D& other) const
// IO class --------------------------------------------------------------------
IO::IO():
m_inputStream{std::ref(std::wcin)}, m_outputStream{std::ref(std::wcout)}
IO::IO()
{
m_inputStream.get().setf(std::ios::unitbuf);
m_outputStream.get().setf(std::ios::unitbuf);
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());
@ -118,11 +118,6 @@ IO& IO::Get()
return instance;
}
std::reference_wrapper<std::wistream> IO::InputStream()
{
return m_inputStream;
}
void IO::LockInputStream()
{
m_inputMutex.lock();
@ -133,46 +128,41 @@ void IO::LockOutputStream()
m_inputMutex.unlock();
}
std::reference_wrapper<std::wostream> IO::OutputStream()
{
return m_outputStream;
}
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];
m_inputStream.get().rdbuf()->pubsetbuf(charBuffer, bytes);
m_inputStream.get().read(&buffer[0], 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);
wchar_t character;
std::wstring buffer;
while (m_inputStream.get().get(character))
while (true)
{
buffer += character;
if (character == c) break;
const std::wstring wc = Read(1);
if (wc.size() != 1) break;
buffer += wc[0];
if (wc[0] == c) break;
}
return buffer;
}
void IO::SetInputStream(std::wistream& stream)
void IO::SetInputFunction(IoReadFunction f)
{
const std::lock_guard<std::mutex> lock(m_inputMutex);
m_inputStream = std::ref(stream);
m_inputStream.get().setf(std::ios::unitbuf);
m_readFunction = f;
}
void IO::SetOutputStream(std::wostream& stream)
void IO::SetOutputFunction(IoWriteFunction f)
{
const std::lock_guard<std::mutex> lock(m_outputMutex);
m_outputStream = std::ref(stream);
m_outputStream.get().setf(std::ios::unitbuf);
m_writeFunction = f;
}
void IO::UnlockInputStream()
@ -188,7 +178,120 @@ void IO::UnlockOutputStream()
void IO::Write(const std::wstring& str)
{
const std::lock_guard<std::mutex> lock(m_outputMutex);
m_outputStream.get() << str;
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 -------------------------------------------------------------------
@ -284,6 +387,71 @@ void SetForegroundColor(std::uint8_t r, std::uint8_t g, std::uint8_t 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::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)

View File

@ -1,8 +1,15 @@
#include <cstring>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include <cerrno>
#include <codecvt>
#include <cstring>
#include <locale>
#include <string>
#include "neovision/application.h"
#include "neovision/crt.h"
@ -12,6 +19,24 @@ namespace neovision {
Terminal::Terminal()
{
}
Terminal::~Terminal()
{
Cleanup();
}
void Terminal::Cleanup()
{
if (m_initialized != true) return;
Restore();
if (m_fd) close(m_fd);
}
void Terminal::Initialize()
{
// Open terminal file handle with non-blocking I/O.
m_fd = open("/dev/tty", O_NONBLOCK|O_RDWR);
// Register signal handlers.
sigemptyset(&m_resizeSignal.sa_mask);
m_resizeSignal.sa_flags = SA_RESTART;
@ -22,16 +47,20 @@ Terminal::Terminal()
"Could not register terminal resize signal handler."
);
}
signal(SIGINT, sigIntHandler);
m_initialized = true;
}
Terminal::~Terminal()
void Terminal::sigIntHandler(int)
{
// Restore terminal settings for each file descriptor on exit.
for (auto& setting: m_savedTerminalSettings)
// We call the actual c++ callback from here if one is registred.
const std::shared_ptr<Application> app = TheApp.lock();
if (app == nullptr) return; // No application instantiated yet.
const Terminal& term = app->Term();
if (term.m_onTerminate)
{
Restore(setting.first);
term.m_onTerminate();
}
m_savedTerminalSettings.clear();
}
void Terminal::sigWinchHandler(int)
@ -43,14 +72,15 @@ void Terminal::sigWinchHandler(int)
const Terminal& term = app->Term();
if (term.m_onResize)
{
term.m_onResize(*app);
term.m_onResize();
}
}
void Terminal::ProcessEvents(int fd)
void Terminal::ProcessEvents()
{
if (!m_initialized) return;
winsize ws;
if (ioctl(fd, TIOCGWINSZ, &ws) == -1)
if (ioctl(m_fd, TIOCGWINSZ, &ws) == -1)
{
throw std::runtime_error(
std::string{"Error while checking for terminal resize event: "} +
@ -59,20 +89,56 @@ void Terminal::ProcessEvents(int fd)
}
}
void Terminal::Restore(int fd)
std::wstring Terminal::Read(size_t n)
{
if (m_savedTerminalSettings.find(fd) == m_savedTerminalSettings.end())
std::wstring result;
if (!m_initialized) return result;
if (!m_fd) return result;
size_t bytesRead{0};
std::string shortResult;
const auto startTime = std::chrono::system_clock::now();
while (bytesRead < n)
{
// Nothing to restore.
return;
const auto now = std::chrono::system_clock::now();
const auto deltaT = std::chrono::duration_cast<std::chrono::seconds>(
now - startTime
);
if (deltaT > m_ioTimeOut) break;
char buffer[n + 1]{};
const size_t br = read(m_fd, buffer, n);
if (br == -1)
{
if (errno == EAGAIN)
{
std::this_thread::sleep_for(std::chrono::microseconds{10});
}
else
{
// Should this throw instead?
break;
}
}
else if (br != 0)
{
bytesRead += br;
shortResult += buffer;
}
}
tcsetattr(fd, TCSANOW, &m_savedTerminalSettings[fd]);
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> conv;
result = conv.from_bytes(shortResult);
return result;
}
Vector2D Terminal::Size(int fd)
void Terminal::Restore()
{
if (!m_initialized) return;
tcsetattr(m_fd, TCSANOW, &m_savedTerminalSettings);
}
Vector2D Terminal::Size()
{
winsize w{};
ioctl(fd, TIOCGWINSZ, &w);
ioctl(m_fd, TIOCGWINSZ, &w);
return {w.ws_col, w.ws_row};
}
@ -81,15 +147,64 @@ void Terminal::SetOnResize(TerminalEventCallback callback)
m_onResize = callback;
}
void Terminal::Unbuffer(int fd)
void Terminal::SetOnTerminate(TerminalEventCallback callback)
{
m_onTerminate = callback;
}
std::chrono::seconds Terminal::Timeout() const
{
return m_ioTimeOut;
}
void Terminal::Timeout(const std::chrono::seconds& t)
{
m_ioTimeOut = t;
}
void Terminal::Unbuffer()
{
if (!m_initialized) return;
if (!m_fd) return;
termios old_tio{};
termios new_tio{};
tcgetattr(fd, &old_tio);
tcgetattr(m_fd, &old_tio);
new_tio = old_tio;
new_tio.c_lflag &= (~ICANON & ~ECHO);
tcsetattr(fd, TCSANOW, &new_tio);
m_savedTerminalSettings[fd] = old_tio;
tcsetattr(m_fd, TCSANOW, &new_tio);
m_savedTerminalSettings = old_tio;
}
void Terminal::Write(const std::wstring& data)
{
if (!m_initialized) return;
if (!m_fd) return;
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> conv;
const std::string s = conv.to_bytes(data);
size_t bytesWritten{0};
const auto startTime = std::chrono::system_clock::now();
while(bytesWritten != s.size())
{
const auto now = std::chrono::system_clock::now();
const auto deltaT = std::chrono::duration_cast<std::chrono::seconds>(
now - startTime
);
if (deltaT > m_ioTimeOut) break;
const size_t bw = write(m_fd, &s[0], s.size());
if (bw == -1)
{
if (errno == EAGAIN)
{
std::this_thread::sleep_for(std::chrono::microseconds{10});
}
else
{
break; // Should this throw?
}
}
bytesWritten += bw;
}
}
} // namespace neovision