Files
Alvin/CMakeLists.txt
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

260 lines
11 KiB
CMake

cmake_minimum_required(VERSION 3.24)
project(Alvin
VERSION 0.1.0
DESCRIPTION "Agent Leveraging Vision and Input: cross-platform input synthesis and screen capture"
LANGUAGES C CXX) # C only for wayland-scanner output
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
include(GNUInstallDirs)
# ---------------------------------------------------------------------------
# Options. "AUTO" enables a backend when its dependencies are found.
# ---------------------------------------------------------------------------
set(ALVIN_WITH_WAYLAND AUTO CACHE STRING "Wayland virtual pointer/keyboard input + monitor listing (ON/OFF/AUTO)")
set(ALVIN_WITH_LIBEI AUTO CACHE STRING "libei input via the RemoteDesktop portal (ON/OFF/AUTO)")
set(ALVIN_WITH_PORTAL AUTO CACHE STRING "ScreenCast portal + PipeWire capture (ON/OFF/AUTO)")
set(ALVIN_WITH_UINPUT ON CACHE BOOL "/dev/uinput input backend")
set(ALVIN_WITH_VISION AUTO CACHE STRING "OpenCV-based alvin::vision library (ON/OFF/AUTO)")
option(ALVIN_BUILD_EXAMPLES "Build the alvin command-line tool" ON)
option(ALVIN_BUILD_TESTS "Build unit tests" ${PROJECT_IS_TOP_LEVEL})
# Resolves an AUTO/ON/OFF option against whether its dependencies were found.
function(alvin_resolve option found)
if(${option} STREQUAL "AUTO")
set(${option}_ENABLED ${found} PARENT_SCOPE)
elseif(${option})
if(NOT ${found})
message(FATAL_ERROR "${option}=ON but its dependencies were not found")
endif()
set(${option}_ENABLED TRUE PARENT_SCOPE)
else()
set(${option}_ENABLED FALSE PARENT_SCOPE)
endif()
endfunction()
add_library(alvin
src/common/core.cpp
src/common/frame.cpp
src/common/json.cpp
src/common/keys.cpp
)
add_library(alvin::alvin ALIAS alvin)
target_include_directories(alvin
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_features(alvin PUBLIC cxx_std_23)
if(MSVC)
target_compile_options(alvin PRIVATE /W4 /utf-8)
else()
target_compile_options(alvin PRIVATE -Wall -Wextra -Wpedantic)
endif()
if(WIN32)
# No export macros in the headers; let CMake export every symbol from the DLL.
set_target_properties(alvin PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
set(ALVIN_ENABLED_BACKENDS "")
# Links a pkg-config dependency privately and records how AlvinConfig.cmake can
# find it again: a static libalvin carries no record of its own dependencies.
set(ALVIN_CONFIG_DEPENDENCIES "")
macro(alvin_private_pkg var)
target_link_libraries(alvin PRIVATE PkgConfig::${var})
string(APPEND ALVIN_CONFIG_DEPENDENCIES " pkg_check_modules(${var} REQUIRED QUIET IMPORTED_TARGET ${ARGN})\n")
endmacro()
# ---------------------------------------------------------------------------
# Linux
# ---------------------------------------------------------------------------
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(PkgConfig REQUIRED)
pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
pkg_check_modules(WAYLAND IMPORTED_TARGET wayland-client)
pkg_check_modules(WAYLAND_PROTOCOLS wayland-protocols)
find_program(WAYLAND_SCANNER wayland-scanner)
pkg_check_modules(GIO IMPORTED_TARGET gio-2.0 gio-unix-2.0)
pkg_check_modules(PIPEWIRE IMPORTED_TARGET libpipewire-0.3)
pkg_check_modules(LIBEI IMPORTED_TARGET libei-1.0)
set(_wayland_found FALSE)
if(WAYLAND_FOUND AND WAYLAND_PROTOCOLS_FOUND AND WAYLAND_SCANNER)
set(_wayland_found TRUE)
endif()
set(_portal_found FALSE)
if(GIO_FOUND AND PIPEWIRE_FOUND)
set(_portal_found TRUE)
endif()
set(_libei_found FALSE)
if(GIO_FOUND AND LIBEI_FOUND)
set(_libei_found TRUE)
endif()
alvin_resolve(ALVIN_WITH_WAYLAND _wayland_found)
alvin_resolve(ALVIN_WITH_PORTAL _portal_found)
alvin_resolve(ALVIN_WITH_LIBEI _libei_found)
target_sources(alvin PRIVATE
src/linux/platform_linux.cpp
src/linux/hyprland_windows.cpp
src/linux/keycodes.cpp
src/linux/xkb.cpp)
alvin_private_pkg(XKBCOMMON xkbcommon)
if(ALVIN_WITH_WAYLAND_ENABLED)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
set(_proto_out ${CMAKE_CURRENT_BINARY_DIR}/protocols)
file(MAKE_DIRECTORY ${_proto_out})
set(_protocols
${CMAKE_CURRENT_SOURCE_DIR}/protocols/wlr-virtual-pointer-unstable-v1.xml
${CMAKE_CURRENT_SOURCE_DIR}/protocols/virtual-keyboard-unstable-v1.xml
${WAYLAND_PROTOCOLS_DIR}/unstable/xdg-output/xdg-output-unstable-v1.xml)
foreach(xml ${_protocols})
get_filename_component(name ${xml} NAME_WE)
add_custom_command(
OUTPUT ${_proto_out}/${name}-client-protocol.h ${_proto_out}/${name}-protocol.c
COMMAND ${WAYLAND_SCANNER} client-header ${xml} ${_proto_out}/${name}-client-protocol.h
COMMAND ${WAYLAND_SCANNER} private-code ${xml} ${_proto_out}/${name}-protocol.c
DEPENDS ${xml}
VERBATIM)
list(APPEND _proto_sources ${_proto_out}/${name}-protocol.c)
list(APPEND _proto_headers ${_proto_out}/${name}-client-protocol.h)
endforeach()
# wayland-scanner emits C; build it as a small C object library.
add_library(alvin_wayland_protocols OBJECT ${_proto_sources} ${_proto_headers})
target_include_directories(alvin_wayland_protocols PUBLIC ${_proto_out})
target_link_libraries(alvin_wayland_protocols PUBLIC PkgConfig::WAYLAND)
set_target_properties(alvin_wayland_protocols PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_sources(alvin PRIVATE
src/linux/wayland.cpp
src/linux/wlr_input.cpp
$<TARGET_OBJECTS:alvin_wayland_protocols>)
target_include_directories(alvin PRIVATE ${_proto_out})
alvin_private_pkg(WAYLAND wayland-client)
add_dependencies(alvin alvin_wayland_protocols)
target_compile_definitions(alvin PRIVATE ALVIN_HAVE_WAYLAND)
list(APPEND ALVIN_ENABLED_BACKENDS "wlr-input" "wayland-monitors")
endif()
if(ALVIN_WITH_PORTAL_ENABLED OR ALVIN_WITH_LIBEI_ENABLED)
target_sources(alvin PRIVATE src/linux/portal.cpp)
alvin_private_pkg(GIO gio-2.0 gio-unix-2.0)
endif()
if(ALVIN_WITH_PORTAL_ENABLED)
target_sources(alvin PRIVATE src/linux/pipewire_capture.cpp)
alvin_private_pkg(PIPEWIRE libpipewire-0.3)
target_compile_definitions(alvin PRIVATE ALVIN_HAVE_PORTAL)
list(APPEND ALVIN_ENABLED_BACKENDS "portal-capture")
endif()
if(ALVIN_WITH_LIBEI_ENABLED)
target_sources(alvin PRIVATE src/linux/libei_input.cpp)
alvin_private_pkg(LIBEI libei-1.0)
target_compile_definitions(alvin PRIVATE ALVIN_HAVE_LIBEI)
if(LIBEI_VERSION VERSION_GREATER_EQUAL 1.6)
target_compile_definitions(alvin PRIVATE ALVIN_LIBEI_HAS_TEXT)
endif()
list(APPEND ALVIN_ENABLED_BACKENDS "libei-input")
endif()
if(ALVIN_WITH_UINPUT)
target_sources(alvin PRIVATE src/linux/uinput_input.cpp)
target_compile_definitions(alvin PRIVATE ALVIN_HAVE_UINPUT)
list(APPEND ALVIN_ENABLED_BACKENDS "uinput-input")
endif()
list(APPEND ALVIN_ENABLED_BACKENDS "hyprland-windows")
# ---------------------------------------------------------------------------
# Windows
# ---------------------------------------------------------------------------
elseif(WIN32)
target_sources(alvin PRIVATE
src/windows/platform_windows.cpp
src/windows/sendinput_input.cpp
src/windows/gdi_capture.cpp
src/windows/win32_windows.cpp)
target_compile_definitions(alvin PRIVATE UNICODE _UNICODE)
target_link_libraries(alvin PRIVATE user32 gdi32 dwmapi)
list(APPEND ALVIN_ENABLED_BACKENDS "sendinput-input" "gdi-capture" "win32-windows")
else()
message(FATAL_ERROR "Alvin does not support ${CMAKE_SYSTEM_NAME} yet")
endif()
# ---------------------------------------------------------------------------
# Optional OpenCV integration
# ---------------------------------------------------------------------------
find_package(OpenCV QUIET COMPONENTS core imgproc imgcodecs)
alvin_resolve(ALVIN_WITH_VISION OpenCV_FOUND)
if(ALVIN_WITH_VISION_ENABLED)
add_library(alvin_vision src/vision/vision.cpp)
add_library(alvin::vision ALIAS alvin_vision)
target_link_libraries(alvin_vision PUBLIC alvin ${OpenCV_LIBS})
set_target_properties(alvin_vision PROPERTIES EXPORT_NAME vision)
if(WIN32)
set_target_properties(alvin_vision PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
list(APPEND ALVIN_ENABLED_BACKENDS "vision(OpenCV ${OpenCV_VERSION})")
endif()
message(STATUS "Alvin backends: ${ALVIN_ENABLED_BACKENDS}")
if(ALVIN_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
if(ALVIN_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# ---------------------------------------------------------------------------
# Install + find_package(Alvin) support
# ---------------------------------------------------------------------------
include(CMakePackageConfigHelpers)
set(ALVIN_CMAKE_DIR ${CMAKE_INSTALL_LIBDIR}/cmake/Alvin)
set(_alvin_install_targets alvin)
if(ALVIN_WITH_VISION_ENABLED)
list(APPEND _alvin_install_targets alvin_vision)
endif()
install(TARGETS ${_alvin_install_targets} EXPORT AlvinTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT AlvinTargets NAMESPACE alvin:: DESTINATION ${ALVIN_CMAKE_DIR})
# Values baked into AlvinConfig.cmake.
get_target_property(_alvin_type alvin TYPE)
if(_alvin_type STREQUAL "STATIC_LIBRARY" AND NOT ALVIN_CONFIG_DEPENDENCIES STREQUAL "")
set(ALVIN_NEEDS_PKGCONFIG TRUE)
else()
set(ALVIN_NEEDS_PKGCONFIG FALSE)
endif()
if(ALVIN_WITH_VISION_ENABLED)
set(ALVIN_HAS_VISION TRUE)
else()
set(ALVIN_HAS_VISION FALSE)
endif()
configure_package_config_file(cmake/AlvinConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/AlvinConfig.cmake
INSTALL_DESTINATION ${ALVIN_CMAKE_DIR})
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/AlvinConfigVersion.cmake
COMPATIBILITY SameMinorVersion) # pre-1.0: minor versions may break the API
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/AlvinConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/AlvinConfigVersion.cmake
DESTINATION ${ALVIN_CMAKE_DIR})