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,118 @@
|
||||
// Top-level window enumeration and focus via Win32 + DWM.
|
||||
|
||||
#include <dwmapi.h>
|
||||
|
||||
#include "backends.hpp"
|
||||
#include "win_util.hpp"
|
||||
|
||||
namespace alvin::win_impl {
|
||||
namespace {
|
||||
|
||||
std::string processName(DWORD pid) {
|
||||
HANDLE proc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!proc) return {};
|
||||
wchar_t path[MAX_PATH];
|
||||
DWORD size = MAX_PATH;
|
||||
std::string name;
|
||||
if (QueryFullProcessImageNameW(proc, 0, path, &size)) {
|
||||
std::wstring_view p(path, size);
|
||||
if (auto slash = p.find_last_of(L"\\/"); slash != std::wstring_view::npos) p.remove_prefix(slash + 1);
|
||||
name = narrow(p);
|
||||
}
|
||||
CloseHandle(proc);
|
||||
return name;
|
||||
}
|
||||
|
||||
// Windows a user would consider "open windows" (roughly what Alt+Tab shows).
|
||||
bool isUserWindow(HWND hwnd) {
|
||||
if (!IsWindowVisible(hwnd) || GetWindow(hwnd, GW_OWNER) != nullptr) return false;
|
||||
if (GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW) return false;
|
||||
DWORD cloaked = 0; // hidden UWP frames and windows on other virtual desktops
|
||||
if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof cloaked)) && cloaked) return false;
|
||||
return GetWindowTextLengthW(hwnd) > 0;
|
||||
}
|
||||
|
||||
WindowInfo describe(HWND hwnd, HWND foreground) {
|
||||
WindowInfo w;
|
||||
w.id.value = reinterpret_cast<std::uint64_t>(hwnd);
|
||||
int len = GetWindowTextLengthW(hwnd);
|
||||
std::wstring title(static_cast<std::size_t>(len) + 1, L'\0');
|
||||
GetWindowTextW(hwnd, title.data(), len + 1);
|
||||
title.resize(static_cast<std::size_t>(len));
|
||||
w.title = narrow(title);
|
||||
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(hwnd, &pid);
|
||||
w.pid = pid;
|
||||
w.appId = processName(pid);
|
||||
|
||||
// Extended frame bounds exclude the invisible resize borders of Windows 10+.
|
||||
RECT r{};
|
||||
if (FAILED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &r, sizeof r))) GetWindowRect(hwnd, &r);
|
||||
w.bounds = {r.left, r.top, r.right - r.left, r.bottom - r.top};
|
||||
|
||||
w.focused = hwnd == foreground;
|
||||
w.minimized = IsIconic(hwnd) != 0;
|
||||
w.visible = !w.minimized;
|
||||
|
||||
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL);
|
||||
MONITORINFO mi{};
|
||||
mi.cbSize = sizeof mi;
|
||||
if (mon && GetMonitorInfoW(mon, &mi)) {
|
||||
const RECT& m = mi.rcMonitor;
|
||||
w.fullscreen = r.left <= m.left && r.top <= m.top && r.right >= m.right && r.bottom >= m.bottom;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
class Win32Windows final : public WindowManager {
|
||||
public:
|
||||
WindowBackend backend() const override { return WindowBackend::Win32; }
|
||||
|
||||
Result<std::vector<WindowInfo>> list() override {
|
||||
struct Ctx {
|
||||
HWND foreground;
|
||||
std::vector<WindowInfo> out;
|
||||
} ctx{GetForegroundWindow(), {}};
|
||||
EnumWindows(
|
||||
[](HWND hwnd, LPARAM data) -> BOOL {
|
||||
auto* c = reinterpret_cast<Ctx*>(data);
|
||||
if (isUserWindow(hwnd)) c->out.push_back(describe(hwnd, c->foreground));
|
||||
return TRUE;
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&ctx));
|
||||
return std::move(ctx.out);
|
||||
}
|
||||
|
||||
Result<std::optional<WindowInfo>> active() override {
|
||||
HWND fg = GetForegroundWindow();
|
||||
if (!fg) return std::optional<WindowInfo>{};
|
||||
return std::optional{describe(fg, fg)};
|
||||
}
|
||||
|
||||
Status focus(WindowId id) override {
|
||||
HWND hwnd = reinterpret_cast<HWND>(id.value);
|
||||
if (!IsWindow(hwnd)) return fail(ErrorCode::NotFound, "no such window");
|
||||
if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE);
|
||||
if (SetForegroundWindow(hwnd)) return {};
|
||||
|
||||
// The foreground lock only lets the process that received the last input
|
||||
// change focus. A synthetic Alt tap makes us that process.
|
||||
INPUT alt[2]{};
|
||||
alt[0].type = alt[1].type = INPUT_KEYBOARD;
|
||||
alt[0].ki.wVk = alt[1].ki.wVk = VK_MENU;
|
||||
alt[1].ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
SendInput(2, alt, sizeof(INPUT));
|
||||
if (SetForegroundWindow(hwnd)) return {};
|
||||
return fail(ErrorCode::PermissionDenied, "Windows refused to change the foreground window");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<std::unique_ptr<WindowManager>> createWin32Windows() {
|
||||
ensureDpiAware();
|
||||
return std::make_unique<Win32Windows>();
|
||||
}
|
||||
|
||||
} // namespace alvin::win_impl
|
||||
Reference in new issue
Block a user