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,178 @@
|
||||
#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
|
||||
@@ -0,0 +1,68 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
#include "alvin/frame.hpp"
|
||||
|
||||
namespace alvin {
|
||||
|
||||
Frame::Frame(int width, int height)
|
||||
: width_(std::max(width, 0)),
|
||||
height_(std::max(height, 0)),
|
||||
data_(static_cast<std::size_t>(width_) * height_ * 4, 0),
|
||||
desktopRect_{0, 0, width_, height_} {}
|
||||
|
||||
Color Frame::pixel(int x, int y) const {
|
||||
if (x < 0 || y < 0 || x >= width_ || y >= height_) return {};
|
||||
const std::uint8_t* p = row(y) + x * 4;
|
||||
return {p[2], p[1], p[0], p[3]};
|
||||
}
|
||||
|
||||
Point Frame::toDesktop(Point px) const {
|
||||
if (width_ == 0 || height_ == 0) return px;
|
||||
double sx = static_cast<double>(desktopRect_.width) / width_;
|
||||
double sy = static_cast<double>(desktopRect_.height) / height_;
|
||||
return {desktopRect_.x + static_cast<int>(std::lround(px.x * sx)),
|
||||
desktopRect_.y + static_cast<int>(std::lround(px.y * sy))};
|
||||
}
|
||||
|
||||
Point Frame::fromDesktop(Point p) const {
|
||||
if (desktopRect_.width == 0 || desktopRect_.height == 0) return p;
|
||||
double sx = static_cast<double>(width_) / desktopRect_.width;
|
||||
double sy = static_cast<double>(height_) / desktopRect_.height;
|
||||
return {static_cast<int>(std::lround((p.x - desktopRect_.x) * sx)),
|
||||
static_cast<int>(std::lround((p.y - desktopRect_.y) * sy))};
|
||||
}
|
||||
|
||||
Frame Frame::crop(Rect r) const {
|
||||
int x0 = std::clamp(r.x, 0, width_), y0 = std::clamp(r.y, 0, height_);
|
||||
int x1 = std::clamp(r.right(), 0, width_), y1 = std::clamp(r.bottom(), 0, height_);
|
||||
Frame out(x1 - x0, y1 - y0);
|
||||
for (int y = 0; y < out.height_; ++y) std::memcpy(out.row(y), row(y0 + y) + x0 * 4, out.stride());
|
||||
|
||||
Point tl = toDesktop({x0, y0}), br = toDesktop({x1, y1});
|
||||
out.desktopRect_ = {tl.x, tl.y, br.x - tl.x, br.y - tl.y};
|
||||
out.timestampNs_ = timestampNs_;
|
||||
return out;
|
||||
}
|
||||
|
||||
Status Frame::savePpm(const std::filesystem::path& path) const {
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f) return fail(ErrorCode::Io, "cannot open " + path.string());
|
||||
f << "P6\n" << width_ << ' ' << height_ << "\n255\n";
|
||||
std::vector<char> line(static_cast<std::size_t>(width_) * 3);
|
||||
for (int y = 0; y < height_; ++y) {
|
||||
const std::uint8_t* src = row(y);
|
||||
for (int x = 0; x < width_; ++x) {
|
||||
line[x * 3 + 0] = static_cast<char>(src[x * 4 + 2]);
|
||||
line[x * 3 + 1] = static_cast<char>(src[x * 4 + 1]);
|
||||
line[x * 3 + 2] = static_cast<char>(src[x * 4 + 0]);
|
||||
}
|
||||
f.write(line.data(), static_cast<std::streamsize>(line.size()));
|
||||
}
|
||||
if (!f) return fail(ErrorCode::Io, "write failed for " + path.string());
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,215 @@
|
||||
#include "json.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace alvin::json {
|
||||
namespace {
|
||||
|
||||
const Value kNull{};
|
||||
const Array kEmptyArray{};
|
||||
const Object kEmptyObject{};
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
explicit Parser(std::string_view s) : s_(s) {}
|
||||
|
||||
std::optional<Value> document() {
|
||||
auto v = value(0);
|
||||
skipSpace();
|
||||
if (!v || pos_ != s_.size()) return std::nullopt;
|
||||
return v;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int kMaxDepth = 256;
|
||||
|
||||
void skipSpace() {
|
||||
while (pos_ < s_.size() && std::isspace(static_cast<unsigned char>(s_[pos_]))) ++pos_;
|
||||
}
|
||||
|
||||
bool consume(char c) {
|
||||
skipSpace();
|
||||
if (pos_ < s_.size() && s_[pos_] == c) {
|
||||
++pos_;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool literal(std::string_view word) {
|
||||
if (s_.substr(pos_, word.size()) != word) return false;
|
||||
pos_ += word.size();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<Value> value(int depth) {
|
||||
if (depth > kMaxDepth) return std::nullopt;
|
||||
skipSpace();
|
||||
if (pos_ >= s_.size()) return std::nullopt;
|
||||
char c = s_[pos_];
|
||||
if (c == '{') return object(depth);
|
||||
if (c == '[') return array(depth);
|
||||
if (c == '"') {
|
||||
auto str = string();
|
||||
if (!str) return std::nullopt;
|
||||
return Value{std::move(*str)};
|
||||
}
|
||||
if (literal("true")) return Value{true};
|
||||
if (literal("false")) return Value{false};
|
||||
if (literal("null")) return Value{nullptr};
|
||||
return number();
|
||||
}
|
||||
|
||||
std::optional<Value> number() {
|
||||
std::size_t start = pos_;
|
||||
while (pos_ < s_.size() && (std::isdigit(static_cast<unsigned char>(s_[pos_])) || s_[pos_] == '-' ||
|
||||
s_[pos_] == '+' || s_[pos_] == '.' || s_[pos_] == 'e' || s_[pos_] == 'E'))
|
||||
++pos_;
|
||||
if (start == pos_) return std::nullopt;
|
||||
std::string text(s_.substr(start, pos_ - start));
|
||||
char* end = nullptr;
|
||||
double d = std::strtod(text.c_str(), &end);
|
||||
if (end != text.c_str() + text.size()) return std::nullopt;
|
||||
return Value{d};
|
||||
}
|
||||
|
||||
static void appendUtf8(std::string& out, char32_t cp) {
|
||||
if (cp < 0x80) {
|
||||
out += static_cast<char>(cp);
|
||||
} else if (cp < 0x800) {
|
||||
out += static_cast<char>(0xC0 | (cp >> 6));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
out += static_cast<char>(0xE0 | (cp >> 12));
|
||||
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
out += static_cast<char>(0xF0 | (cp >> 18));
|
||||
out += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
|
||||
out += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
|
||||
out += static_cast<char>(0x80 | (cp & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<unsigned> hex4() {
|
||||
if (pos_ + 4 > s_.size()) return std::nullopt;
|
||||
unsigned v = 0;
|
||||
auto r = std::from_chars(s_.data() + pos_, s_.data() + pos_ + 4, v, 16);
|
||||
if (r.ptr != s_.data() + pos_ + 4) return std::nullopt;
|
||||
pos_ += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
std::optional<std::string> string() {
|
||||
if (!consume('"')) return std::nullopt;
|
||||
std::string out;
|
||||
while (pos_ < s_.size()) {
|
||||
char c = s_[pos_++];
|
||||
if (c == '"') return out;
|
||||
if (c != '\\') {
|
||||
out += c;
|
||||
continue;
|
||||
}
|
||||
if (pos_ >= s_.size()) return std::nullopt;
|
||||
switch (char e = s_[pos_++]) {
|
||||
case '"': case '\\': case '/': out += e; break;
|
||||
case 'b': out += '\b'; break;
|
||||
case 'f': out += '\f'; break;
|
||||
case 'n': out += '\n'; break;
|
||||
case 'r': out += '\r'; break;
|
||||
case 't': out += '\t'; break;
|
||||
case 'u': {
|
||||
auto hi = hex4();
|
||||
if (!hi) return std::nullopt;
|
||||
char32_t cp = *hi;
|
||||
if (cp >= 0xD800 && cp < 0xDC00 && s_.substr(pos_, 2) == "\\u") {
|
||||
pos_ += 2;
|
||||
auto lo = hex4();
|
||||
if (!lo) return std::nullopt;
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (*lo - 0xDC00);
|
||||
}
|
||||
appendUtf8(out, cp);
|
||||
break;
|
||||
}
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Value> array(int depth) {
|
||||
consume('[');
|
||||
auto arr = std::make_shared<Array>();
|
||||
if (consume(']')) return Value{arr};
|
||||
do {
|
||||
auto v = value(depth + 1);
|
||||
if (!v) return std::nullopt;
|
||||
arr->push_back(std::move(*v));
|
||||
} while (consume(','));
|
||||
if (!consume(']')) return std::nullopt;
|
||||
return Value{arr};
|
||||
}
|
||||
|
||||
std::optional<Value> object(int depth) {
|
||||
consume('{');
|
||||
auto obj = std::make_shared<Object>();
|
||||
if (consume('}')) return Value{obj};
|
||||
do {
|
||||
skipSpace();
|
||||
auto key = string();
|
||||
if (!key || !consume(':')) return std::nullopt;
|
||||
auto v = value(depth + 1);
|
||||
if (!v) return std::nullopt;
|
||||
obj->insert_or_assign(std::move(*key), std::move(*v));
|
||||
} while (consume(','));
|
||||
if (!consume('}')) return std::nullopt;
|
||||
return Value{obj};
|
||||
}
|
||||
|
||||
std::string_view s_;
|
||||
std::size_t pos_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Value::boolean(bool fallback) const {
|
||||
auto* b = std::get_if<bool>(&v);
|
||||
return b ? *b : fallback;
|
||||
}
|
||||
|
||||
double Value::number(double fallback) const {
|
||||
auto* d = std::get_if<double>(&v);
|
||||
return d ? *d : fallback;
|
||||
}
|
||||
|
||||
std::string Value::string(std::string fallback) const {
|
||||
auto* s = std::get_if<std::string>(&v);
|
||||
return s ? *s : fallback;
|
||||
}
|
||||
|
||||
const Array& Value::array() const {
|
||||
auto* a = std::get_if<std::shared_ptr<Array>>(&v);
|
||||
return a ? **a : kEmptyArray;
|
||||
}
|
||||
|
||||
const Object& Value::object() const {
|
||||
auto* o = std::get_if<std::shared_ptr<Object>>(&v);
|
||||
return o ? **o : kEmptyObject;
|
||||
}
|
||||
|
||||
const Value& Value::operator[](std::string_view key) const {
|
||||
const auto& o = object();
|
||||
auto it = o.find(key);
|
||||
return it == o.end() ? kNull : it->second;
|
||||
}
|
||||
|
||||
const Value& Value::operator[](std::size_t index) const {
|
||||
const auto& a = array();
|
||||
return index < a.size() ? a[index] : kNull;
|
||||
}
|
||||
|
||||
std::optional<Value> parse(std::string_view text) { return Parser(text).document(); }
|
||||
|
||||
} // namespace alvin::json
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
// A deliberately small JSON reader, enough for compositor IPC replies.
|
||||
// Not a general-purpose library: no serialisation, numbers are doubles.
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace alvin::json {
|
||||
|
||||
struct Value;
|
||||
using Array = std::vector<Value>;
|
||||
using Object = std::map<std::string, Value, std::less<>>;
|
||||
|
||||
struct Value {
|
||||
std::variant<std::nullptr_t, bool, double, std::string, std::shared_ptr<Array>, std::shared_ptr<Object>> v;
|
||||
|
||||
bool isNull() const { return std::holds_alternative<std::nullptr_t>(v); }
|
||||
bool isObject() const { return std::holds_alternative<std::shared_ptr<Object>>(v); }
|
||||
bool isArray() const { return std::holds_alternative<std::shared_ptr<Array>>(v); }
|
||||
|
||||
// Lenient accessors: return the fallback on type mismatch.
|
||||
bool boolean(bool fallback = false) const;
|
||||
double number(double fallback = 0) const;
|
||||
std::string string(std::string fallback = {}) const;
|
||||
const Array& array() const; // empty array if not an array
|
||||
const Object& object() const; // empty object if not an object
|
||||
|
||||
// Member lookup; returns a null value if missing or not an object.
|
||||
const Value& operator[](std::string_view key) const;
|
||||
// Element lookup; returns a null value if out of range or not an array.
|
||||
const Value& operator[](std::size_t index) const;
|
||||
};
|
||||
|
||||
std::optional<Value> parse(std::string_view text);
|
||||
|
||||
} // namespace alvin::json
|
||||
@@ -0,0 +1,125 @@
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "alvin/keys.hpp"
|
||||
|
||||
namespace alvin {
|
||||
namespace {
|
||||
|
||||
struct NamedKey {
|
||||
std::string_view name;
|
||||
Key key;
|
||||
};
|
||||
|
||||
// First entry for each key is its canonical name.
|
||||
constexpr NamedKey kNames[] = {
|
||||
{"a", Key::A}, {"b", Key::B}, {"c", Key::C}, {"d", Key::D}, {"e", Key::E}, {"f", Key::F},
|
||||
{"g", Key::G}, {"h", Key::H}, {"i", Key::I}, {"j", Key::J}, {"k", Key::K}, {"l", Key::L},
|
||||
{"m", Key::M}, {"n", Key::N}, {"o", Key::O}, {"p", Key::P}, {"q", Key::Q}, {"r", Key::R},
|
||||
{"s", Key::S}, {"t", Key::T}, {"u", Key::U}, {"v", Key::V}, {"w", Key::W}, {"x", Key::X},
|
||||
{"y", Key::Y}, {"z", Key::Z},
|
||||
{"0", Key::Num0}, {"1", Key::Num1}, {"2", Key::Num2}, {"3", Key::Num3}, {"4", Key::Num4},
|
||||
{"5", Key::Num5}, {"6", Key::Num6}, {"7", Key::Num7}, {"8", Key::Num8}, {"9", Key::Num9},
|
||||
{"f1", Key::F1}, {"f2", Key::F2}, {"f3", Key::F3}, {"f4", Key::F4}, {"f5", Key::F5},
|
||||
{"f6", Key::F6}, {"f7", Key::F7}, {"f8", Key::F8}, {"f9", Key::F9}, {"f10", Key::F10},
|
||||
{"f11", Key::F11}, {"f12", Key::F12}, {"f13", Key::F13}, {"f14", Key::F14}, {"f15", Key::F15},
|
||||
{"f16", Key::F16}, {"f17", Key::F17}, {"f18", Key::F18}, {"f19", Key::F19}, {"f20", Key::F20},
|
||||
{"f21", Key::F21}, {"f22", Key::F22}, {"f23", Key::F23}, {"f24", Key::F24},
|
||||
{"escape", Key::Escape}, {"esc", Key::Escape},
|
||||
{"enter", Key::Enter}, {"return", Key::Enter},
|
||||
{"tab", Key::Tab},
|
||||
{"backspace", Key::Backspace},
|
||||
{"space", Key::Space},
|
||||
{"minus", Key::Minus}, {"-", Key::Minus},
|
||||
{"equal", Key::Equal}, {"=", Key::Equal},
|
||||
{"leftbracket", Key::LeftBracket}, {"[", Key::LeftBracket},
|
||||
{"rightbracket", Key::RightBracket}, {"]", Key::RightBracket},
|
||||
{"backslash", Key::Backslash}, {"\\", Key::Backslash},
|
||||
{"semicolon", Key::Semicolon}, {";", Key::Semicolon},
|
||||
{"apostrophe", Key::Apostrophe}, {"'", Key::Apostrophe},
|
||||
{"grave", Key::Grave}, {"`", Key::Grave},
|
||||
{"comma", Key::Comma}, {",", Key::Comma},
|
||||
{"period", Key::Period}, {".", Key::Period},
|
||||
{"slash", Key::Slash}, {"/", Key::Slash},
|
||||
{"capslock", Key::CapsLock}, {"numlock", Key::NumLock}, {"scrolllock", Key::ScrollLock},
|
||||
{"shift", Key::LeftShift}, {"lshift", Key::LeftShift}, {"rshift", Key::RightShift},
|
||||
{"ctrl", Key::LeftCtrl}, {"control", Key::LeftCtrl}, {"lctrl", Key::LeftCtrl}, {"rctrl", Key::RightCtrl},
|
||||
{"alt", Key::LeftAlt}, {"lalt", Key::LeftAlt}, {"ralt", Key::RightAlt}, {"altgr", Key::RightAlt},
|
||||
{"super", Key::LeftSuper}, {"win", Key::LeftSuper}, {"meta", Key::LeftSuper}, {"cmd", Key::LeftSuper},
|
||||
{"lsuper", Key::LeftSuper}, {"rsuper", Key::RightSuper},
|
||||
{"menu", Key::Menu},
|
||||
{"insert", Key::Insert}, {"delete", Key::Delete}, {"del", Key::Delete},
|
||||
{"home", Key::Home}, {"end", Key::End},
|
||||
{"pageup", Key::PageUp}, {"pgup", Key::PageUp}, {"pagedown", Key::PageDown}, {"pgdn", Key::PageDown},
|
||||
{"left", Key::Left}, {"right", Key::Right}, {"up", Key::Up}, {"down", Key::Down},
|
||||
{"printscreen", Key::PrintScreen}, {"print", Key::PrintScreen}, {"pause", Key::Pause},
|
||||
{"kp0", Key::Kp0}, {"kp1", Key::Kp1}, {"kp2", Key::Kp2}, {"kp3", Key::Kp3}, {"kp4", Key::Kp4},
|
||||
{"kp5", Key::Kp5}, {"kp6", Key::Kp6}, {"kp7", Key::Kp7}, {"kp8", Key::Kp8}, {"kp9", Key::Kp9},
|
||||
{"kpdecimal", Key::KpDecimal}, {"kpdivide", Key::KpDivide}, {"kpmultiply", Key::KpMultiply},
|
||||
{"kpsubtract", Key::KpSubtract}, {"kpadd", Key::KpAdd}, {"kpenter", Key::KpEnter},
|
||||
{"mute", Key::VolumeMute}, {"volumedown", Key::VolumeDown}, {"volumeup", Key::VolumeUp},
|
||||
{"playpause", Key::MediaPlayPause}, {"stop", Key::MediaStop},
|
||||
{"next", Key::MediaNext}, {"previous", Key::MediaPrevious}, {"prev", Key::MediaPrevious},
|
||||
};
|
||||
|
||||
std::string lower(std::string_view s) {
|
||||
std::string out(s);
|
||||
for (char& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<Key> keyFromName(std::string_view name) {
|
||||
auto n = lower(name);
|
||||
for (const auto& e : kNames)
|
||||
if (e.name == n) return e.key;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string_view keyName(Key key) {
|
||||
for (const auto& e : kNames)
|
||||
if (e.key == key) return e.name;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::optional<std::vector<Key>> parseChord(std::string_view chord) {
|
||||
// "ctrl++" means ctrl plus the '+' key, which is the Equal key position on US layouts.
|
||||
bool plusKey = false;
|
||||
if (chord == "+") return std::vector<Key>{Key::Equal};
|
||||
if (chord.ends_with("++")) {
|
||||
plusKey = true;
|
||||
chord.remove_suffix(2);
|
||||
}
|
||||
|
||||
std::vector<Key> keys;
|
||||
while (true) {
|
||||
auto end = chord.find('+');
|
||||
auto token = chord.substr(0, end);
|
||||
while (!token.empty() && std::isspace(static_cast<unsigned char>(token.front()))) token.remove_prefix(1);
|
||||
while (!token.empty() && std::isspace(static_cast<unsigned char>(token.back()))) token.remove_suffix(1);
|
||||
auto key = keyFromName(token);
|
||||
if (!key) return std::nullopt;
|
||||
keys.push_back(*key);
|
||||
if (end == std::string_view::npos) break;
|
||||
chord.remove_prefix(end + 1);
|
||||
}
|
||||
if (plusKey) keys.push_back(Key::Equal);
|
||||
return keys;
|
||||
}
|
||||
|
||||
bool isModifier(Key key) {
|
||||
switch (key) {
|
||||
case Key::LeftShift: case Key::RightShift:
|
||||
case Key::LeftCtrl: case Key::RightCtrl:
|
||||
case Key::LeftAlt: case Key::RightAlt:
|
||||
case Key::LeftSuper: case Key::RightSuper:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace alvin
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
// Entry points every platform directory (src/linux, src/windows, ...) implements.
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "alvin/capture.hpp"
|
||||
#include "alvin/input.hpp"
|
||||
#include "alvin/types.hpp"
|
||||
#include "alvin/window.hpp"
|
||||
|
||||
namespace alvin::detail {
|
||||
|
||||
Result<std::unique_ptr<Input>> createInput(const InputOptions& options);
|
||||
Result<std::unique_ptr<Capture>> createCapture(const CaptureOptions& options);
|
||||
Result<std::unique_ptr<WindowManager>> createWindowManager(WindowBackend backend);
|
||||
Result<std::vector<Monitor>> listMonitors();
|
||||
|
||||
} // namespace alvin::detail
|
||||
Reference in new issue
Block a user