A cross-platform C++23 library with a platform-neutral public API and runtime-selected backends: - Input: Wayland virtual pointer/keyboard (Hyprland, wlroots), libei via the RemoteDesktop portal (GNOME, KDE), uinput, and Win32 SendInput - Capture: ScreenCast portal + PipeWire, and Win32 GDI - Windows: Hyprland IPC and Win32 enumeration - Monitors via wl_output/xdg-output and EnumDisplayMonitors - Optional OpenCV template matching (alvin::vision) Also adds the `alvin` CLI, Catch2 unit tests, install rules with find_package(Alvin) support, API docs (docs/API.md and the Obsidian vault), and README instructions for both ways of consuming the library. Windows backends are written but not yet compiled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
179 lines
5.8 KiB
C++
179 lines
5.8 KiB
C++
#include <algorithm>
|
|
#include <cctype>
|
|
#include <span>
|
|
#include <thread>
|
|
|
|
#include "alvin/capture.hpp"
|
|
#include "alvin/error.hpp"
|
|
#include "alvin/input.hpp"
|
|
#include "alvin/types.hpp"
|
|
#include "alvin/window.hpp"
|
|
#include "platform.hpp"
|
|
|
|
namespace alvin {
|
|
|
|
std::string_view toString(ErrorCode code) noexcept {
|
|
switch (code) {
|
|
case ErrorCode::Unknown: return "unknown";
|
|
case ErrorCode::Unsupported: return "unsupported";
|
|
case ErrorCode::NotAvailable: return "not available";
|
|
case ErrorCode::PermissionDenied: return "permission denied";
|
|
case ErrorCode::Cancelled: return "cancelled";
|
|
case ErrorCode::Timeout: return "timeout";
|
|
case ErrorCode::InvalidArgument: return "invalid argument";
|
|
case ErrorCode::NotFound: return "not found";
|
|
case ErrorCode::Io: return "i/o error";
|
|
case ErrorCode::Protocol: return "protocol error";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
std::string_view toString(InputBackend backend) noexcept {
|
|
switch (backend) {
|
|
case InputBackend::Auto: return "auto";
|
|
case InputBackend::WlrVirtual: return "wlr";
|
|
case InputBackend::LibEi: return "libei";
|
|
case InputBackend::UInput: return "uinput";
|
|
case InputBackend::SendInput: return "sendinput";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
std::string_view toString(CaptureBackend backend) noexcept {
|
|
switch (backend) {
|
|
case CaptureBackend::Auto: return "auto";
|
|
case CaptureBackend::Portal: return "portal";
|
|
case CaptureBackend::Gdi: return "gdi";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
std::string_view toString(WindowBackend backend) noexcept {
|
|
switch (backend) {
|
|
case WindowBackend::Auto: return "auto";
|
|
case WindowBackend::Hyprland: return "hyprland";
|
|
case WindowBackend::Win32: return "win32";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
Rect Rect::united(const Rect& o) const {
|
|
if (empty()) return o;
|
|
if (o.empty()) return *this;
|
|
int l = std::min(x, o.x), t = std::min(y, o.y);
|
|
int r = std::max(right(), o.right()), b = std::max(bottom(), o.bottom());
|
|
return {l, t, r - l, b - t};
|
|
}
|
|
|
|
Result<std::vector<Monitor>> listMonitors() { return detail::listMonitors(); }
|
|
|
|
Result<Rect> desktopBounds() {
|
|
auto monitors = listMonitors();
|
|
if (!monitors) return std::unexpected(monitors.error());
|
|
Rect box;
|
|
for (const auto& m : *monitors) box = box.united(m.bounds);
|
|
if (box.empty()) return fail(ErrorCode::NotFound, "no monitors reported");
|
|
return box;
|
|
}
|
|
|
|
// ---- Input ----
|
|
|
|
Result<std::unique_ptr<Input>> Input::create(const InputOptions& options) { return detail::createInput(options); }
|
|
|
|
void Input::pause() const {
|
|
if (eventDelay_.count() > 0) std::this_thread::sleep_for(eventDelay_);
|
|
}
|
|
|
|
Status Input::click(MouseButton button, int count) {
|
|
for (int i = 0; i < count; ++i) {
|
|
if (auto s = buttonDown(button); !s) return s;
|
|
pause();
|
|
if (auto s = buttonUp(button); !s) return s;
|
|
if (i + 1 < count) pause();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Status Input::clickAt(Point p, MouseButton button, int count) {
|
|
if (auto s = moveTo(p); !s) return s;
|
|
pause();
|
|
return click(button, count);
|
|
}
|
|
|
|
Status Input::drag(Point from, Point to, MouseButton button) {
|
|
if (auto s = moveTo(from); !s) return s;
|
|
pause();
|
|
if (auto s = buttonDown(button); !s) return s;
|
|
// Intermediate steps so drag thresholds and hover handlers fire.
|
|
constexpr int steps = 10;
|
|
for (int i = 1; i <= steps; ++i) {
|
|
pause();
|
|
Point p{from.x + (to.x - from.x) * i / steps, from.y + (to.y - from.y) * i / steps};
|
|
if (auto s = moveTo(p); !s) {
|
|
(void)buttonUp(button);
|
|
return s;
|
|
}
|
|
}
|
|
pause();
|
|
return buttonUp(button);
|
|
}
|
|
|
|
Status Input::keyPress(Key key) {
|
|
if (auto s = keyDown(key); !s) return s;
|
|
pause();
|
|
return keyUp(key);
|
|
}
|
|
|
|
// Presses keys in order and releases them in reverse. Always releases what it
|
|
// pressed, even after a failure, so no key is left stuck down.
|
|
static Status pressChord(Input& input, std::span<const Key> keys, void (*pause)(const Input&)) {
|
|
std::vector<Key> pressed;
|
|
Status result;
|
|
for (Key k : keys) {
|
|
result = input.keyDown(k);
|
|
if (!result) break;
|
|
pressed.push_back(k);
|
|
pause(input);
|
|
}
|
|
for (auto it = pressed.rbegin(); it != pressed.rend(); ++it) {
|
|
auto s = input.keyUp(*it);
|
|
if (result && !s) result = s;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
Status Input::chord(std::initializer_list<Key> keys) {
|
|
return pressChord(*this, std::span(keys.begin(), keys.size()), [](const Input& in) { in.pause(); });
|
|
}
|
|
|
|
Status Input::chord(std::string_view text) {
|
|
auto keys = parseChord(text);
|
|
if (!keys) return fail(ErrorCode::InvalidArgument, "cannot parse key chord '" + std::string(text) + "'");
|
|
return pressChord(*this, *keys, [](const Input& in) { in.pause(); });
|
|
}
|
|
|
|
// ---- Capture / windows ----
|
|
|
|
Result<std::unique_ptr<Capture>> Capture::create(const CaptureOptions& options) { return detail::createCapture(options); }
|
|
|
|
Result<std::unique_ptr<WindowManager>> WindowManager::create(WindowBackend backend) {
|
|
return detail::createWindowManager(backend);
|
|
}
|
|
|
|
static bool containsNoCase(std::string_view haystack, std::string_view needle) {
|
|
auto it = std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end(), [](char a, char b) {
|
|
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
|
|
});
|
|
return it != haystack.end();
|
|
}
|
|
|
|
Result<std::optional<WindowInfo>> WindowManager::find(std::string_view needle) {
|
|
auto windows = list();
|
|
if (!windows) return std::unexpected(windows.error());
|
|
for (auto& w : *windows)
|
|
if (containsNoCase(w.title, needle) || containsNoCase(w.appId, needle)) return std::optional{std::move(w)};
|
|
return std::optional<WindowInfo>{};
|
|
}
|
|
|
|
} // namespace alvin
|