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>
This commit is contained in:
1 parent
76ff506fd0
commit
ca7e62ac8d
59 files changed
+6515
-1
No files matched your search
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
// Alvin - Agent Leveraging Vision and Input. Nice.
|
||||
//
|
||||
// Umbrella header for the core library. Computer-vision helpers live in the
|
||||
// optional alvin::vision target (#include "alvin/vision.hpp").
|
||||
|
||||
#include "alvin/capture.hpp"
|
||||
#include "alvin/error.hpp"
|
||||
#include "alvin/frame.hpp"
|
||||
#include "alvin/input.hpp"
|
||||
#include "alvin/keys.hpp"
|
||||
#include "alvin/types.hpp"
|
||||
#include "alvin/window.hpp"
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "alvin/error.hpp"
|
||||
#include "alvin/frame.hpp"
|
||||
#include "alvin/window.hpp"
|
||||
|
||||
namespace alvin {
|
||||
|
||||
enum class CaptureBackend {
|
||||
Auto,
|
||||
Portal, // xdg-desktop-portal ScreenCast + PipeWire (any modern Wayland desktop)
|
||||
Gdi, // Win32 BitBlt / PrintWindow
|
||||
};
|
||||
|
||||
std::string_view toString(CaptureBackend backend) noexcept;
|
||||
|
||||
enum class CaptureSource : std::uint8_t {
|
||||
Monitor = 1,
|
||||
Window = 2,
|
||||
Any = Monitor | Window,
|
||||
};
|
||||
|
||||
struct CaptureOptions {
|
||||
CaptureBackend backend = CaptureBackend::Auto;
|
||||
|
||||
// What the portal picker offers. On Windows, Monitor + monitorIndex or
|
||||
// Window + window select the target directly with no dialog.
|
||||
CaptureSource source = CaptureSource::Monitor;
|
||||
int monitorIndex = 0;
|
||||
std::optional<WindowId> window;
|
||||
|
||||
bool showCursor = false;
|
||||
|
||||
// Portal only: token from Capture::restoreToken() of an earlier session.
|
||||
// Lets the compositor skip the picker and reuse the previous selection.
|
||||
std::string restoreToken;
|
||||
std::chrono::milliseconds permissionTimeout = std::chrono::minutes(2);
|
||||
};
|
||||
|
||||
// A live capture of one monitor or window.
|
||||
class Capture {
|
||||
public:
|
||||
static Result<std::unique_ptr<Capture>> create(const CaptureOptions& options = {});
|
||||
|
||||
virtual ~Capture() = default;
|
||||
Capture(const Capture&) = delete;
|
||||
Capture& operator=(const Capture&) = delete;
|
||||
|
||||
virtual CaptureBackend backend() const = 0;
|
||||
|
||||
// Most recent frame. Blocks until the first frame arrives (up to timeout).
|
||||
// Streaming backends only deliver frames when the screen changes, so this
|
||||
// does not wait for a *new* frame; use nextFrame() for that.
|
||||
virtual Result<Frame> grab(std::chrono::milliseconds timeout = std::chrono::seconds(5)) = 0;
|
||||
|
||||
// Waits for a frame newer than any previously returned one.
|
||||
virtual Result<Frame> nextFrame(std::chrono::milliseconds timeout = std::chrono::seconds(5)) = 0;
|
||||
|
||||
virtual std::string restoreToken() const { return {}; }
|
||||
|
||||
protected:
|
||||
Capture() = default;
|
||||
};
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace alvin {
|
||||
|
||||
enum class ErrorCode {
|
||||
Unknown,
|
||||
Unsupported, // backend or feature not available on this platform/session
|
||||
NotAvailable, // backend compiled in but its service/device isn't present
|
||||
PermissionDenied, // e.g. /dev/uinput not writable, portal request denied
|
||||
Cancelled, // user dismissed a portal dialog
|
||||
Timeout,
|
||||
InvalidArgument,
|
||||
NotFound,
|
||||
Io,
|
||||
Protocol, // unexpected reply from a compositor / portal / IPC socket
|
||||
};
|
||||
|
||||
struct Error {
|
||||
ErrorCode code = ErrorCode::Unknown;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
using Result = std::expected<T, Error>;
|
||||
|
||||
using Status = Result<void>;
|
||||
|
||||
inline std::unexpected<Error> fail(ErrorCode code, std::string message) {
|
||||
return std::unexpected(Error{code, std::move(message)});
|
||||
}
|
||||
|
||||
std::string_view toString(ErrorCode code) noexcept;
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "alvin/error.hpp"
|
||||
#include "alvin/types.hpp"
|
||||
|
||||
namespace alvin {
|
||||
|
||||
// Byte order in memory. Every capture backend normalises to BGRA8, which is
|
||||
// what OpenCV (CV_8UC4) and Win32 DIBs use natively.
|
||||
enum class PixelFormat : std::uint8_t {
|
||||
BGRA8,
|
||||
};
|
||||
|
||||
struct Color {
|
||||
std::uint8_t r = 0, g = 0, b = 0, a = 255;
|
||||
friend bool operator==(const Color&, const Color&) = default;
|
||||
};
|
||||
|
||||
class Frame {
|
||||
public:
|
||||
Frame() = default;
|
||||
Frame(int width, int height); // zero-filled BGRA8
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
Size size() const { return {width_, height_}; }
|
||||
std::size_t stride() const { return static_cast<std::size_t>(width_) * 4; }
|
||||
PixelFormat format() const { return PixelFormat::BGRA8; }
|
||||
bool empty() const { return data_.empty(); }
|
||||
|
||||
std::uint8_t* data() { return data_.data(); }
|
||||
const std::uint8_t* data() const { return data_.data(); }
|
||||
std::span<const std::uint8_t> bytes() const { return data_; }
|
||||
std::uint8_t* row(int y) { return data_.data() + y * stride(); }
|
||||
const std::uint8_t* row(int y) const { return data_.data() + y * stride(); }
|
||||
|
||||
Color pixel(int x, int y) const;
|
||||
|
||||
// Where this frame sits on the desktop, in logical pixels. Frames from a
|
||||
// HiDPI monitor have more pixels than their logical size; use
|
||||
// toDesktop()/fromDesktop() to convert instead of assuming 1:1.
|
||||
Rect desktopRect() const { return desktopRect_; }
|
||||
void setDesktopRect(Rect r) { desktopRect_ = r; }
|
||||
Point toDesktop(Point framePixel) const;
|
||||
Point fromDesktop(Point desktopPoint) const;
|
||||
|
||||
std::int64_t timestampNs() const { return timestampNs_; }
|
||||
void setTimestampNs(std::int64_t t) { timestampNs_ = t; }
|
||||
|
||||
Frame crop(Rect r) const; // r in frame pixels; clipped to bounds
|
||||
|
||||
// Binary PPM (P6), dependency-free. Alpha is dropped.
|
||||
Status savePpm(const std::filesystem::path& path) const;
|
||||
|
||||
private:
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
std::vector<std::uint8_t> data_;
|
||||
Rect desktopRect_;
|
||||
std::int64_t timestampNs_ = 0;
|
||||
};
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "alvin/error.hpp"
|
||||
#include "alvin/keys.hpp"
|
||||
#include "alvin/types.hpp"
|
||||
|
||||
namespace alvin {
|
||||
|
||||
enum class InputBackend {
|
||||
Auto,
|
||||
WlrVirtual, // Wayland zwlr_virtual_pointer_v1 + zwp_virtual_keyboard_v1 (Hyprland, sway, other wlroots)
|
||||
LibEi, // libei via xdg-desktop-portal RemoteDesktop (GNOME, KDE)
|
||||
UInput, // /dev/uinput kernel virtual devices (any Linux session; needs write access)
|
||||
SendInput, // Win32
|
||||
};
|
||||
|
||||
std::string_view toString(InputBackend backend) noexcept;
|
||||
|
||||
struct InputOptions {
|
||||
InputBackend backend = InputBackend::Auto;
|
||||
|
||||
// LibEi only: token from a previous session (Input::restoreToken()) that
|
||||
// lets the portal skip its permission dialog.
|
||||
std::string restoreToken;
|
||||
|
||||
// How long to wait for the user to answer a portal permission dialog.
|
||||
std::chrono::milliseconds permissionTimeout = std::chrono::minutes(2);
|
||||
};
|
||||
|
||||
// Synthesises mouse and keyboard events.
|
||||
//
|
||||
// Coordinates passed to moveTo() are global desktop coordinates in logical
|
||||
// pixels, matching Monitor::bounds and WindowInfo::bounds.
|
||||
class Input {
|
||||
public:
|
||||
// Picks a backend (or uses options.backend). With Auto, the environment
|
||||
// variable ALVIN_INPUT_BACKEND=wlr|libei|uinput|sendinput overrides the choice.
|
||||
static Result<std::unique_ptr<Input>> create(const InputOptions& options = {});
|
||||
|
||||
virtual ~Input() = default;
|
||||
Input(const Input&) = delete;
|
||||
Input& operator=(const Input&) = delete;
|
||||
|
||||
virtual InputBackend backend() const = 0;
|
||||
|
||||
virtual Status moveTo(Point p) = 0;
|
||||
virtual Status moveBy(int dx, int dy) = 0;
|
||||
virtual Status buttonDown(MouseButton button) = 0;
|
||||
virtual Status buttonUp(MouseButton button) = 0;
|
||||
// Wheel notches; positive dy scrolls down, positive dx scrolls right.
|
||||
virtual Status scroll(int dx, int dy) = 0;
|
||||
|
||||
virtual Status keyDown(Key key) = 0;
|
||||
virtual Status keyUp(Key key) = 0;
|
||||
// Types UTF-8 text using the active keyboard layout (or a synthesized one).
|
||||
virtual Status typeText(std::string_view utf8) = 0;
|
||||
|
||||
// Portal-backed sessions can hand out a token to skip the dialog next time.
|
||||
virtual std::string restoreToken() const { return {}; }
|
||||
|
||||
// ---- Convenience helpers built on the primitives above ----
|
||||
|
||||
Status click(MouseButton button = MouseButton::Left, int count = 1);
|
||||
Status clickAt(Point p, MouseButton button = MouseButton::Left, int count = 1);
|
||||
Status drag(Point from, Point to, MouseButton button = MouseButton::Left);
|
||||
Status keyPress(Key key);
|
||||
// Presses keys in order, releases in reverse: chord({Key::LeftCtrl, Key::C}).
|
||||
Status chord(std::initializer_list<Key> keys);
|
||||
// Same, from a string such as "ctrl+shift+esc".
|
||||
Status chord(std::string_view chord);
|
||||
|
||||
// Delay inserted between the down/up halves of clicks and key presses.
|
||||
// Some applications drop events that arrive with zero spacing.
|
||||
void setEventDelay(std::chrono::milliseconds delay) { eventDelay_ = delay; }
|
||||
std::chrono::milliseconds eventDelay() const { return eventDelay_; }
|
||||
|
||||
protected:
|
||||
Input() = default;
|
||||
void pause() const;
|
||||
|
||||
private:
|
||||
std::chrono::milliseconds eventDelay_{8};
|
||||
};
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace alvin {
|
||||
|
||||
// Platform-neutral physical key identifiers. Names follow a US QWERTY layout
|
||||
// but they identify key *positions*, the same way evdev codes and Win32
|
||||
// scan codes do. Use Input::typeText() to produce characters instead.
|
||||
enum class Key : std::uint16_t {
|
||||
Unknown = 0,
|
||||
|
||||
A, B, C, D, E, F, G, H, I, J, K, L, M,
|
||||
N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
|
||||
|
||||
Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9,
|
||||
|
||||
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
|
||||
F13, F14, F15, F16, F17, F18, F19, F20, F21, F22, F23, F24,
|
||||
|
||||
Escape, Enter, Tab, Backspace, Space,
|
||||
Minus, Equal, LeftBracket, RightBracket, Backslash,
|
||||
Semicolon, Apostrophe, Grave, Comma, Period, Slash,
|
||||
|
||||
CapsLock, NumLock, ScrollLock,
|
||||
LeftShift, RightShift, LeftCtrl, RightCtrl, LeftAlt, RightAlt, LeftSuper, RightSuper,
|
||||
Menu,
|
||||
|
||||
Insert, Delete, Home, End, PageUp, PageDown,
|
||||
Left, Right, Up, Down,
|
||||
PrintScreen, Pause,
|
||||
|
||||
Kp0, Kp1, Kp2, Kp3, Kp4, Kp5, Kp6, Kp7, Kp8, Kp9,
|
||||
KpDecimal, KpDivide, KpMultiply, KpSubtract, KpAdd, KpEnter,
|
||||
|
||||
VolumeMute, VolumeDown, VolumeUp,
|
||||
MediaPlayPause, MediaStop, MediaNext, MediaPrevious,
|
||||
|
||||
Count_,
|
||||
};
|
||||
|
||||
enum class MouseButton : std::uint8_t {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
Back,
|
||||
Forward,
|
||||
};
|
||||
|
||||
// "ctrl", "a", "f5", "enter", "super", ... (case-insensitive).
|
||||
std::optional<Key> keyFromName(std::string_view name);
|
||||
std::string_view keyName(Key key);
|
||||
|
||||
// Parses a chord such as "ctrl+shift+t" into its keys, modifiers first as written.
|
||||
std::optional<std::vector<Key>> parseChord(std::string_view chord);
|
||||
|
||||
bool isModifier(Key key);
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "alvin/error.hpp"
|
||||
|
||||
namespace alvin {
|
||||
|
||||
struct Point {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
friend bool operator==(const Point&, const Point&) = default;
|
||||
};
|
||||
|
||||
struct Size {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
friend bool operator==(const Size&, const Size&) = default;
|
||||
};
|
||||
|
||||
// All rectangles are in global desktop coordinates (logical pixels, i.e. after
|
||||
// monitor scaling) unless documented otherwise.
|
||||
struct Rect {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
int right() const { return x + width; }
|
||||
int bottom() const { return y + height; }
|
||||
Point center() const { return {x + width / 2, y + height / 2}; }
|
||||
bool empty() const { return width <= 0 || height <= 0; }
|
||||
bool contains(Point p) const { return p.x >= x && p.y >= y && p.x < right() && p.y < bottom(); }
|
||||
|
||||
// Smallest rect containing both. An empty rect acts as identity.
|
||||
Rect united(const Rect& o) const;
|
||||
|
||||
friend bool operator==(const Rect&, const Rect&) = default;
|
||||
};
|
||||
|
||||
struct Monitor {
|
||||
std::string name; // connector name, e.g. "DP-1" or "\\\\.\\DISPLAY1"
|
||||
std::string description;
|
||||
Rect bounds; // logical position/size in the global desktop
|
||||
Size pixelSize; // physical mode size in pixels
|
||||
double scale = 1.0;
|
||||
bool primary = false;
|
||||
};
|
||||
|
||||
// Enumerate monitors on the current session.
|
||||
Result<std::vector<Monitor>> listMonitors();
|
||||
|
||||
// Bounding box of all monitors, i.e. the space absolute pointer motion maps onto.
|
||||
Result<Rect> desktopBounds();
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// Optional OpenCV integration (link alvin::vision).
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
#include "alvin/error.hpp"
|
||||
#include "alvin/frame.hpp"
|
||||
|
||||
namespace alvin::vision {
|
||||
|
||||
// Zero-copy CV_8UC4 (BGRA) view. The Frame must outlive the Mat.
|
||||
cv::Mat view(const Frame& frame);
|
||||
// Deep copy converted to CV_8UC3 (BGR), the layout most OpenCV code expects.
|
||||
cv::Mat toBgr(const Frame& frame);
|
||||
|
||||
Result<Frame> fromMat(const cv::Mat& mat);
|
||||
Result<cv::Mat> loadImage(const std::filesystem::path& path); // BGR
|
||||
Status saveImage(const Frame& frame, const std::filesystem::path& path); // png/jpg/...
|
||||
|
||||
struct Match {
|
||||
Rect rect; // in frame pixels
|
||||
double score = 0; // 0..1, higher is better
|
||||
};
|
||||
|
||||
struct MatchOptions {
|
||||
double threshold = 0.9;
|
||||
bool grayscale = false; // faster, ignores colour differences
|
||||
int maxResults = 16; // findAll only
|
||||
};
|
||||
|
||||
// Normalised cross-correlation template matching.
|
||||
Result<std::optional<Match>> find(const Frame& haystack, const cv::Mat& needle, const MatchOptions& opts = {});
|
||||
Result<std::vector<Match>> findAll(const Frame& haystack, const cv::Mat& needle, const MatchOptions& opts = {});
|
||||
|
||||
} // namespace alvin::vision
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "alvin/error.hpp"
|
||||
#include "alvin/types.hpp"
|
||||
|
||||
namespace alvin {
|
||||
|
||||
// Opaque, backend-defined handle: a Hyprland client address, an HWND, ...
|
||||
struct WindowId {
|
||||
std::uint64_t value = 0;
|
||||
friend bool operator==(const WindowId&, const WindowId&) = default;
|
||||
};
|
||||
|
||||
struct WindowInfo {
|
||||
WindowId id;
|
||||
std::string title;
|
||||
std::string appId; // Wayland app_id / X11 class / Win32 executable name
|
||||
Rect bounds; // global desktop coordinates
|
||||
std::int64_t pid = 0;
|
||||
bool focused = false;
|
||||
bool visible = true;
|
||||
bool minimized = false;
|
||||
bool fullscreen = false;
|
||||
std::string workspace; // compositor-specific; empty when not applicable
|
||||
};
|
||||
|
||||
enum class WindowBackend {
|
||||
Auto,
|
||||
Hyprland, // Hyprland IPC socket
|
||||
Win32,
|
||||
};
|
||||
|
||||
std::string_view toString(WindowBackend backend) noexcept;
|
||||
|
||||
// Querying and focusing top-level windows. Wayland deliberately hides this
|
||||
// from normal clients, so each compositor needs its own implementation.
|
||||
class WindowManager {
|
||||
public:
|
||||
static Result<std::unique_ptr<WindowManager>> create(WindowBackend backend = WindowBackend::Auto);
|
||||
|
||||
virtual ~WindowManager() = default;
|
||||
WindowManager(const WindowManager&) = delete;
|
||||
WindowManager& operator=(const WindowManager&) = delete;
|
||||
|
||||
virtual WindowBackend backend() const = 0;
|
||||
virtual Result<std::vector<WindowInfo>> list() = 0;
|
||||
virtual Result<std::optional<WindowInfo>> active() = 0;
|
||||
virtual Status focus(WindowId id) = 0;
|
||||
|
||||
// First window whose title or appId contains `needle` (case-insensitive).
|
||||
Result<std::optional<WindowInfo>> find(std::string_view needle);
|
||||
|
||||
protected:
|
||||
WindowManager() = default;
|
||||
};
|
||||
|
||||
} // namespace alvin
|
||||
Reference in new issue
Block a user