Files
Alvin/src/common/core.cpp
T
Aeth3randClaude Opus 5.5 ca7e62ac8d Add Alvin library: input, capture, windows and vision
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>
2026-09-27 13:58:20 -04:00

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