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>
119 lines
4.1 KiB
C++
119 lines
4.1 KiB
C++
// 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
|