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>
260 lines
11 KiB
CMake
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})
|