Skip to content

Commit 1fd4ad7

Browse files
committed
Terminal/Mouse
1 parent b9b973e commit 1fd4ad7

5 files changed

Lines changed: 212 additions & 0 deletions

File tree

modules/Terminal/Mouse.mpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
module;
2+
3+
#include <CppUtils/System/Windows.hpp>
4+
#include <cstdio>
5+
6+
export module CppUtils.Terminal.Mouse;
7+
8+
import std;
9+
import CppUtils.Container.Size;
10+
import CppUtils.Terminal.RawTerminal;
11+
12+
export namespace CppUtils::Terminal::Mouse
13+
{
14+
enum class Button
15+
{
16+
None,
17+
Left,
18+
Middle,
19+
Right,
20+
ScrollUp,
21+
ScrollDown
22+
};
23+
24+
struct Event
25+
{
26+
Button button = Button::None;
27+
bool pressed = false;
28+
bool dragging = false;
29+
bool shift = false;
30+
bool alt = false;
31+
bool ctrl = false;
32+
Container::Size2 position;
33+
};
34+
35+
namespace Ansi
36+
{
37+
inline constexpr auto EnableMouseTracking = "\x1b[?1003h"; // All motion
38+
inline constexpr auto DisableMouseTracking = "\x1b[?1003l";
39+
inline constexpr auto EnableExtendedCoordinates = "\x1b[?1006h"; // SGR extended mode
40+
inline constexpr auto DisableExtendedCoordinates = "\x1b[?1006l";
41+
}
42+
43+
class Tracker final
44+
{
45+
public:
46+
inline Tracker():
47+
m_active{true}
48+
{
49+
#if defined(OS_LINUX) or defined(OS_MACOS)
50+
std::print("{}{}", Ansi::EnableMouseTracking, Ansi::EnableExtendedCoordinates);
51+
std::fflush(stdout);
52+
#endif
53+
}
54+
55+
inline ~Tracker()
56+
{
57+
if (m_active)
58+
{
59+
#if defined(OS_LINUX) or defined(OS_MACOS)
60+
std::print("{}{}", Ansi::DisableMouseTracking, Ansi::DisableExtendedCoordinates);
61+
std::fflush(stdout);
62+
#endif
63+
}
64+
}
65+
66+
Tracker(const Tracker&) = delete;
67+
Tracker& operator=(const Tracker&) = delete;
68+
69+
inline Tracker(Tracker&& other) noexcept:
70+
m_active{std::exchange(other.m_active, false)}
71+
{}
72+
73+
inline Tracker& operator=(Tracker&& other) noexcept
74+
{
75+
if (this != std::addressof(other))
76+
m_active = std::exchange(other.m_active, false);
77+
return *this;
78+
}
79+
80+
private:
81+
bool m_active = false;
82+
};
83+
84+
[[nodiscard]] inline auto getEvent() -> std::optional<Event>
85+
{
86+
#if defined(OS_LINUX) or defined(OS_MACOS)
87+
if (not RawTerminal::isInputAvailable())
88+
return std::nullopt;
89+
90+
auto raw = RawTerminal{};
91+
92+
if (raw.readChar() != '\x1b')
93+
return std::nullopt;
94+
if (raw.readChar() != '[')
95+
return std::nullopt;
96+
if (raw.readChar() != '<')
97+
return std::nullopt;
98+
99+
auto buffer = std::string{};
100+
auto c = char{};
101+
while ((c = raw.readChar()) != 'M' and c != 'm')
102+
buffer += c;
103+
104+
auto buttonData = 0, x = 0, y = 0;
105+
if (std::sscanf(buffer.c_str(), "%d;%d;%d", std::addressof(buttonData), std::addressof(x), std::addressof(y)) != 3)
106+
return std::nullopt;
107+
108+
auto event = Event{};
109+
event.position = Container::Size2{static_cast<std::size_t>(x - 1), static_cast<std::size_t>(y - 1)};
110+
event.shift = (buttonData & 4) != 0;
111+
event.alt = (buttonData & 8) != 0;
112+
event.ctrl = (buttonData & 16) != 0;
113+
event.dragging = (buttonData & 32) != 0;
114+
115+
const auto isRelease = (c == 'm');
116+
event.pressed = not isRelease;
117+
118+
if (auto scroll = buttonData & 64; scroll != 0)
119+
{
120+
event.button = (buttonData & 1) ? Button::ScrollDown : Button::ScrollUp;
121+
event.pressed = true;
122+
}
123+
else
124+
switch (buttonData & 3)
125+
{
126+
case 0: event.button = Button::Left; break;
127+
case 1: event.button = Button::Middle; break;
128+
case 2: event.button = Button::Right; break;
129+
case 3: event.button = Button::None; break;
130+
}
131+
132+
return event;
133+
#elif defined(OS_WINDOWS)
134+
return std::nullopt;
135+
#endif
136+
}
137+
138+
[[nodiscard]] inline auto getPosition() -> std::optional<Container::Size2>
139+
{
140+
if (const auto event = getEvent())
141+
return event->position;
142+
return std::nullopt;
143+
}
144+
}

modules/Terminal/RawTerminal.mpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module;
55
#if defined(OS_LINUX) or defined(OS_MACOS)
66
# include <unistd.h>
77
# include <termios.h>
8+
# include <poll.h>
89
#endif
910

1011
export module CppUtils.Terminal.RawTerminal;
@@ -51,6 +52,12 @@ export namespace CppUtils::Terminal
5152
tcsetattr(STDIN_FILENO, TCSANOW, std::addressof(m_oldTerminalState));
5253
}
5354

55+
[[nodiscard]] static inline auto isInputAvailable() -> bool
56+
{
57+
auto pfd = pollfd{STDIN_FILENO, POLLIN, 0};
58+
return ::poll(std::addressof(pfd), 1, 0) > 0;
59+
}
60+
5461
[[nodiscard]] inline auto readChar() const -> char
5562
{
5663
auto c = char{};

modules/Terminal/Terminal.mpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export import CppUtils.Terminal.CharAttributes;
77
export import CppUtils.Terminal.Cursor;
88
export import CppUtils.Terminal.Handle;
99
export import CppUtils.Terminal.Layout;
10+
export import CppUtils.Terminal.Mouse;
1011
export import CppUtils.Terminal.ProgressBar;
1112
export import CppUtils.Terminal.Primitive;
1213
export import CppUtils.Terminal.Scrollable;

tests/Terminal/Mouse.mpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export module CppUtils.UnitTests.Terminal.Mouse;
2+
3+
import std;
4+
import CppUtils;
5+
6+
namespace CppUtils::UnitTest::Terminal::Mouse
7+
{
8+
auto _ = TestSuite{"Terminal/Mouse", {"UnitTest", "Terminal/Canvas"}, [](TestSuite& suite) {
9+
using namespace std::chrono_literals;
10+
11+
suite.addTest("Tracking", [] {
12+
const auto size = CppUtils::Container::Size2{40, 20};
13+
auto canvas = CppUtils::Terminal::Canvas{size};
14+
15+
auto mouseTracker = CppUtils::Terminal::Mouse::Tracker{};
16+
auto rawTerminal = CppUtils::Terminal::RawTerminal{};
17+
18+
for (const auto startTime = std::chrono::steady_clock::now();
19+
std::chrono::steady_clock::now() - startTime < 5s;
20+
std::this_thread::sleep_for(16ms))
21+
{
22+
if (const auto event = CppUtils::Terminal::Mouse::getEvent())
23+
{
24+
canvas.fill(' ');
25+
26+
auto view = canvas.getWritableView();
27+
CppUtils::Terminal::drawRectangle(
28+
view,
29+
{0, 0},
30+
size,
31+
CppUtils::Terminal::PrimitiveStyle::Outline,
32+
CppUtils::Terminal::CharAttributes{});
33+
34+
if (event->position.x() < size.width() and event->position.y() < size.height())
35+
{
36+
auto c = ' ';
37+
switch (event->button)
38+
{
39+
case CppUtils::Terminal::Mouse::Button::Left: c = 'L'; break;
40+
case CppUtils::Terminal::Mouse::Button::Middle: c = 'M'; break;
41+
case CppUtils::Terminal::Mouse::Button::Right: c = 'R'; break;
42+
case CppUtils::Terminal::Mouse::Button::ScrollUp: c = '^'; break;
43+
case CppUtils::Terminal::Mouse::Button::ScrollDown: c = 'v'; break;
44+
default: c = '.'; break;
45+
}
46+
47+
c = event->pressed ? static_cast<char>(std::toupper(c)) : static_cast<char>(std::tolower(c));
48+
canvas.setChar(event->position, CppUtils::Terminal::CharAttributes{c});
49+
50+
canvas.printText({1, 1}, std::format("Position: {}, {}", event->position.x(), event->position.y()));
51+
canvas.printText({1, 2}, std::format("Button: {} Pressed: {}", (int)event->button, event->pressed));
52+
canvas.printText({1, 3}, std::format("Modifiers: {}{}{}", event->ctrl ? "C" : "", event->alt ? "A" : "", event->shift ? "S" : ""));
53+
}
54+
canvas.print();
55+
}
56+
}
57+
});
58+
}};
59+
}

tests/UnitTests.mpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export import CppUtils.UnitTests.String.Utility;
5252
export import CppUtils.UnitTests.System.Error;
5353
export import CppUtils.UnitTests.Terminal;
5454
export import CppUtils.UnitTests.Terminal.Canvas;
55+
export import CppUtils.UnitTests.Terminal.Mouse;
5556
export import CppUtils.UnitTests.Terminal.Primitive;
5657
export import CppUtils.UnitTests.Terminal.ProgressBar;
5758
export import CppUtils.UnitTests.Terminal.Scrollable;

0 commit comments

Comments
 (0)