Work on mouse input handling
This commit is contained in:
parent
1ebc805ca3
commit
50477ab304
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue